记一次提取ROMFS文件系统固件

admin 2023年12月27日23:13:20评论32 views字数 4248阅读14分9秒阅读模式

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

问题说明

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

$ sudo mount -t romfs romfs_img.bin ./testmount: ./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格式说明。这个文档太简略了,有很多东西没有覆盖,我会总结一下我踩完坑的解析。

文件系统头部

文档提供的文件头如下:

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


原文始发于微信公众号(中机博也车联网安全):记一次提取ROMFS文件系统固件

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月27日23:13:20
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   记一次提取ROMFS文件系统固件https://cn-sec.com/archives/2338484.html

发表评论

匿名网友 填写信息