修复Binwalk提取ROMFS文件系统固件BUG

IoT 4个月前 admin
8 0 0

目前 Linux 内核编译默认不编写 ROMFS 文件系统支持模块,Binwalk 使用的以 mount 命令提取 ROMFS 固件的方式失效,为了解决这个问题,我们需要了解 ROMFS 文件系统格式,直接写代码解析 ROMFS 文件系统固件并完成提取

问题说明

最近在怼 IoT 安全,发现 Binwalk 无法提取 ROMFS 文件系统的固件,检查原因是 Binwalk 的提取策略是直接通过调用 mount 命令尝试把 ROMFS 文件系统挂载到目录上。但是在当今的 Linux 内核中,已经默认移除了对 ROMFS 文件系统的支持,主流 Linux 发行版也都不支持 ROMFS 文件系统。在 Ubuntu 22 上尝试使用 mount 挂载一个 ROMFS 文件系统,会提示 ROMFS 是未知的文件系统类型。为了能正常提取 ROMFS 文件系统固件,我们尝试写代码直接解析 ROMFS 文件系统。

$ sudo mount -t romfs vela_audio.bin ./testmount: /mnt/c/Users/chengrui/Downloads/6dfbabfa053042cc2b5d8c9e0c4ff29c_upd_mijia.watch.n62/test: unknown filesystem type 'romfs'.

在 Binwalk 的源文件里,还有一个 RomFS 类,在 src/binwalk/plugins/dlromfsextract.py 文件里,但是不要被它迷惑了,它不是提取 ROMFS 固件的,它提取的是 D-LINK 固件,应该是 D-LINK 定制化的 ROMFS 格式。Binwalk 对 ROMFS 的处理逻辑在 src/binwalk/config/extract.conf 文件里,节选如下所示,它的含义就是调用 mount 命令提取 ROMFS 固件。

^romfs filesystem:romfs:mkdir '%%romfs-root%%' && mount -t romfs '%e' '%%romfs-root%%':0:False

ROMFS 文件系统格式说明

Linux 提供了一个简单的文档,见 1 ROMFS 格式说明[1]。这个文档太简略了,有很多东西没有覆盖,我会总结一下我踩完坑的解析。

文件系统头部

文档提供的文件头如下:

offset     content
+---+---+---+---+ 0 | - | r | o | m | +---+---+---+---+ The ASCII representation of those bytes 4 | 1 | f | s | - | / (i.e. "-rom1fs-") +---+---+---+---+ 8 | full size | The number of accessible bytes in this fs. +---+---+---+---+12 | checksum | The checksum of the FIRST 512 BYTES. +---+---+---+---+16 | volume name | The zero terminated name of the volume, : : padded to 16 byte boundary. +---+---+---+---+xx | file | : headers :

前 8 字节是 magic,固定”-rom1fs-“,full size 是整个文件系统的大小,checksum 是校验,我只提取不关心校验,所以后面的内容完全忽略 checksum,volume name 是文件系统名,是一个以x00 结尾的字符串,并且会进行填充。file headers 紧随 volume name 字段,因此对 volume name 字段进行填充,直到 file headers 字段开始地址对齐 16 字节。

file headers 字段是一个数组,数组里的每一个元素都是一个 file header 结构体,每一个 file header 结构体都描述了文件系统里的一个实体,可以是目录,硬链接,常规文件等,接下来我们说明 file header 结构体。

file header 结构

file header 的结构如下:

offset     content
+---+---+---+---+ 0 | next filehdr|X| The offset of the next file header +---+---+---+---+ (zero if no more files) 4 | spec.info | Info for directories/hard links/devices +---+---+---+---+ 8 | size | The size of this file in bytes +---+---+---+---+12 | checksum | Covering the meta data, including the file +---+---+---+---+ name, and padding16 | file name | The zero terminated name of the file, : : padded to 16 byte boundary +---+---+---+---+xx | file data | : :

next filehdr 指示下一个 file header 的开始地址,要注意,由于每一个 file header 的开始地址都是对齐 16 字节的,因此,next fildhdr 字段的低 4 位,也就是 bit0-3,是没有意义的,这 4 位被用来指示当前 file header 描述的实体的类型,bit3 用来指示实体是否有可执行权限,我们的目的是提取固件,这个二进制位不需要考虑,bit0-2 可以转换为一个 0-7 的整数,用来表示实体类型,spec.info 字段随着实体类型不同也有不同的含义,如下表所示

数值 实体类型 spec.info 字段含义
0 硬链接 硬链接目标的实体的 file header 的开始地址
1 目录 目录包含的第一个文件的 file header 的开始地址
2 文件 没用到,必是 0
3 符号链接 没用到
4 块设备 版本号
5 字符设备 没看懂,我也不知道
6 socket 没用到,必是 0
7 fifo 没用到,必是 0

细心的读者可能已经发现了,file header 有一个字段 next filehdr 指示下一个 file header 的开始地址,当实体类型是目录时,spec.info 字段也指示下一个 file header 的地址,这两个字段有什么异同呢?

在文件系统中,文件实际上以一个树形结构组织起来的,目录可以包含文件,子目录,子目录又可以包含文件,子子目录等。next filehdr 指示的就是 「与当前文件属于同一个目录的」 的下一个文件的 file header 开始地址, spec.info 指示的则是 「目录包含的第一个文件」 的 file header 的开始地址。我们以一个目录树说明问题,在 next fildhdr 字段上,有目录 1 -> 文件 3 -> 文件 4,在 spec_info 字段上,有目录 1 -> 文件 1,在 next fildhdr 字段上,又有文件 1 -> 文件 2 -> . -> ..。是的,.与..也有 fild header,.是目录类型,它的 spec.info 字段与目录 1 的 spec.info 字段相同,..是硬编码类型,它的 spec.info 字段指向目录 1 的上层目录的 file header。

目录1    文件1    文件2    .    ..文件3文件4

file name 字段是实体的名字,为目录时则为目录名,为文件时则为文件名。这是一个以x00 结尾的字符串,并且填充对齐,file data 字段紧随 file name 字段,填充 file name 字段直到 file data 字段开始地址对齐 16 字节。size 就是实体的尺寸,为目录时,size 为 0,为文件时,size 字段就是文件大小,file data 字段就是文件内容。

写代码提取

根据前面提到的内容,文件系统实际上是一个树结构,next filehdr 字段形成一个单向链表,通过对它遍历我们就完成了对这个节点的所有兄弟节点的遍历,目录实体的 spec.info 字段指向属于它的第一个文件的 file headhdr,因此对目录的 spec.info 字段指向的实体再做一次兄弟节点遍历,我们就获得了属于这个目录的所有文件。

因此,提取文件的代码很向树的层次遍历,如下所示

    @staticmethod    def from_bytes(data: bytes):        if data[:8] != b"-rom1fs-":            raise TypeError("not a romfs bin")
system_size = int.from_bytes(data[8 : 12], byteorder="big")
entry_start, volume_name = RomfsParse.read_volume_name(data)
root_node = RomfsNode("dir") root_node.name = volume_name root_node.entry_start = entry_start
# 获取根节点作为目录节点集中的第一个元素 path_nodes = [root_node] all_nodes = [root_node] while len(path_nodes) > 0: # 从目录节点集中弹出一个元素 node = path_nodes.pop() node_entry = node.entry_start next_entry = int.from_bytes(data[node_entry + 4 : node_entry + 8], byteorder="big") # 遍历这个目录节点的所属文件 once_nodes = RomfsParse.view_one_level(data, next_entry) all_nodes += once_nodes node.children = once_nodes for _ in once_nodes: if _.type == "dir" and _.name != ".": # 如果目录下还有子目录,添加到目录节点集 path_nodes.append(_)
# 返回根节点与所有节点集 return root_node, all_nodes

提取结果

对某手表的固件尝试提取,并打印目录结构,代码与输出分别如下:

root_node, all_nodes = RomfsParse.from_file(r"vela_misc.bin")# travel_output(root_node)travel_print(root_node)
misc        zoneinfo                zone1970.tab                zone.tab                tzdata.zi                tzbin                        etc                                localtime                leapseconds                iso3166.tab                Zulu                WET                W-SU                Universal                UTC                US                        Samoa**************省略

开源代码

GitHub 项目地址:2 ROMFS_PARSER[2]

Reference

[1]

1 ROMFS格式说明: https://docs.kernel.org/filesystems/romfs.html

[2]

2 ROMFS_PARSER: https://github.com/ddddhm1234/ROMFS_PARSER


原文始发于微信公众号(网络空间威胁观察):修复Binwalk提取ROMFS文件系统固件BUG

版权声明:admin 发表于 2023年12月24日 下午8:54。
转载请注明:修复Binwalk提取ROMFS文件系统固件BUG | CTF导航

相关文章