目前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 ./test
mount: ./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 padding
16 | 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文件系统固件
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论