4、固件文件系统与格式分析
固件逆向工程的第一步往往不是直接反汇编代码,而是先解开固件的外壳——识别其文件系统格式、解析头部结构、判断压缩与加密状态,并最终重组出可分析的目录树。绝大多数嵌入式设备(路由器、摄像头、IoT网关、工业控制器)的固件都遵循“引导头 + 内核 + 根文件系统”的经典布局,而根文件系统几乎总是采用CramFS、SquashFS、JFFS2或UBIFS中的某一种。本章将系统讲解这四种文件系统的结构特征、固件头解析方法、压缩与加密识别技巧,以及固件结构重组的完整流程。
上图展示了固件格式分析的整体知识框架:从原始镜像出发,依次经过头部解析、压缩/加密识别、文件系统判定,最终借助静态工具链和动态仿真手段完成固件结构重组。下面我们将逐一深入每个环节。
4.1 常见固件文件系统核心原理
4.1.1 CramFS(Compressed ROM File System)
核心原理:CramFS是Linux内核最早支持的只读压缩文件系统之一,由Linus Torvalds于1999年设计。它直接将zlib压缩后的文件数据与元数据打包在一个线性结构中,没有中间层,因此解压时无需额外内存缓冲。CramFS的超级块(Superblock)位于文件系统起始偏移0处,魔数为0x28CD3D45(小端序),共256字节。每个文件节点(inode)占用12字节,包含文件类型、权限、UID/GID、大小、名称偏移和内容偏移。CramFS的最大文件大小为16MB,最大文件系统大小为256MB,且不支持目录的硬链接。
判断方法:使用binwalk扫描时,CramFS通常会在偏移0或固定偏移处被识别。手动验证时,读取前4字节检查魔数,随后解析超级块中的root.inode字段获取根目录位置。CramFS的特征是文件数据与元数据交错存储,且所有文件名以NUL结尾连续排列。
| 属性 | CramFS | SquashFS | JFFS2 | UBIFS |
|---|---|---|---|---|
| 读写性 | 只读 | 只读 | 可读写 | 可读写 |
| 压缩算法 | zlib | gzip/lzma/lzo/xz/zstd | zlib/rtime | LZO/zlib |
| 魔数/标识 | 0x28CD3D45 | hsqs/sqsh | 0x1985 (节点魔数) | UBI# (卷标识) |
| 适用Flash | NOR | NOR/NAND | NOR/NAND | NAND (UBI层) |
| 最大容量 | 256MB | 16TB (v4) | 128MiB~4GiB | 取决于UBI卷 |
| 典型工具 | cramfsck, cramfsprogs | unsquashfs, mksquashfs | jefferson, jffs2dump | ubi_reader, ubireader_extract_files |
4.1.2 SquashFS
核心原理:SquashFS是当前Linux嵌入式设备中使用最广泛的只读压缩文件系统。它将所有文件、目录、符号链接等元数据组织为inode表,数据块按128KB(默认)分块压缩。SquashFS的超级块起始于偏移0,魔数为hsqs(小端序)或sqsh(大端序),版本号位于偏移4。SquashFS 4.0以上版本支持gzip、lzma、lzo、xz和zstd多种压缩算法,且支持碎片整理(fragment)和重复数据去重,压缩率通常比CramFS高20%-30%。
判断方法:使用file命令可直接识别SquashFS镜像。手动解析时,读取超级块中的bytes_used字段(偏移40)可获取文件系统总大小,inode_table_start(偏移48)和directory_table_start(偏移56)分别指向inode表和目录表。SquashFS的inode类型包括基本目录(1)、基本文件(2)、扩展目录(8)、扩展文件(9)等,每种类型有不同的结构体长度。
unsquashfs工具时,如果固件中的SquashFS带有自定义魔数或偏移,可先用binwalk -A进行熵分析定位,再用dd提取出精确的SquashFS起始偏移。例如:dd if=firmware.bin of=rootfs.sqsh bs=1 skip=262144 count=5242880。
4.1.3 JFFS2(Journalling Flash File System v2)
核心原理:JFFS2是专为NOR/NAND Flash设计的日志型可读写文件系统。它没有传统的超级块,而是以节点(node)为单位在Flash上顺序写入。每个节点以魔数0x1985(JFFS2_MAGIC_BITMASK)开头,节点类型包括:直接节点(JFFS2_NODETYPE_DIRENT,用于目录项)、间接节点(JFFS2_NODETYPE_INODE,用于文件数据)和干净标记节点(JFFS2_NODETYPE_CLEANMARKER)。JFFS2通过垃圾回收机制管理Flash块的擦除与复用,但挂载时需要扫描整个Flash介质来重建文件系统结构,因此挂载时间随容量线性增长。
判断方法:JFFS2没有全局魔数,但可以通过扫描Flash数据中的节点魔数0x1985来定位。使用binwalk时,JFFS2通常显示为“JFFS2 filesystem”或“JFFS2 node”。更可靠的方法是使用jefferson工具,它能够自动识别并提取JFFS2镜像中的文件。JFFS2的节点头为12字节,包含魔数、节点类型、总长度、节点头CRC和数据CRC。
4.1.4 UBIFS(UBI File System)
核心原理:UBIFS是专为NAND Flash设计的日志型文件系统,运行在UBI(Unsorted Block Images)卷之上。UBI层负责磨损均衡、坏块管理和逻辑卷映射,UBIFS则在其上提供文件系统语义。UBIFS的超级块节点(Superblock Node)位于UBI卷的起始位置,标识为UBIFS_NODE_MAGIC(0x06101831)。UBIFS支持LZO和zlib压缩,索引节点(index node)采用B+树结构,挂载速度快于JFFS2,且支持写缓存和异步I/O。
判断方法:UBIFS镜像通常包含UBI卷头(Volume ID header)和UBIFS超级块。使用ubi_reader工具集(ubireader_extract_files、ubireader_display_info)可以解析UBI卷并提取文件。识别时,先查找UBI卷标识符UBI#(0x55424923),再定位UBIFS超级块魔数0x06101831。注意UBIFS通常需要配合NAND的OOB(Out-of-Band)数据,提取时需保留完整的NAND镜像。
4.2 固件头解析
4.2.1 固件头的作用与常见结构
固件头(Firmware Header)是固件镜像最前端的元数据区域,用于引导加载程序(Bootloader)识别固件合法性、版本、目标平台和加载地址。不同厂商的固件头格式差异很大,但通常包含以下字段:魔数(Magic Number)、固件版本、头部长度、总长度、校验和(CRC32/MD5/SHA256)、加载地址(Load Address)、入口地址(Entry Point)、硬件平台ID、镜像类型(内核/文件系统/完整固件)等。
常见固件头示例:
- TRX格式(Broadcom/TP-Link):头部24字节,魔数
0x30524448("HDR0"),包含3个分区偏移。 - D-Link:头部64字节,魔数
0x20000000,包含内核和文件系统偏移。 - OpenWrt:使用TRX或自定义头部,通常包含设备ID和版本信息。
- U-Boot:固件头可能包含IH_MAGIC(
0x27051956),用于U-Boot镜像格式。
4.2.2 头部解析流程
- 初步扫描:使用
binwalk firmware.bin查看整体结构,识别头部和各个分区的偏移。 - 魔数定位:使用
hexdump -C firmware.bin | head -64查看前64字节,寻找可读ASCII字符串或已知魔数。 - 字段解析:根据已知格式或逆向分析,逐个解析头部字段。可使用
010 Editor的模板功能或Python脚本(struct模块)批量解析。 - 校验验证:计算头部声明的CRC32或MD5,与固件实际数据比对,确认头部字段的准确性。
- 分区提取:根据头部中的偏移和长度字段,使用
dd提取内核、文件系统等独立分区。
4.3 压缩与加密识别
4.3.1 压缩识别方法
固件中的内核和文件系统通常经过压缩以节省Flash空间。识别压缩类型是提取文件系统的前置条件。常用方法包括:
- 特征串匹配:gzip文件以
1F 8B开头,xz以FD 37 7A 58 5A 00开头,lzma以5D 00 00开头,zstd以28 B5 2F FD开头。 - 熵分析:使用
binwalk -E或binwalk -A查看固件各区域的熵值。压缩数据的熵接近8.0,未压缩数据熵较低(通常3.0-6.0)。高熵区域通常对应压缩或加密数据。 - 文件类型识别:使用
file命令对提取的分区进行类型判断,例如file kernel.bin可能输出“gzip compressed data”。
4.3.2 加密识别方法
加密固件比压缩固件更难处理,因为加密数据没有明显的特征串。识别加密的主要手段:
- 熵值极高且均匀:加密数据的熵值接近8.0,且没有明显的结构特征。如果整个固件或某个分区的熵值均匀且极高,且没有已知压缩魔数,则可能被加密。
- 头部信息缺失:加密固件通常没有可读的头部字段,或头部本身也被加密。
- 已知加密算法特征:某些固件使用AES-CBC或AES-ECB,密钥硬编码在Bootloader中。通过逆向Bootloader可提取密钥。
- 对比分析:如果同一设备的多个固件版本,可通过对比不同版本中相同偏移的数据块,判断是否使用流密码(如RC4)或块密码。
strings bootloader.bin | grep -i key有时能直接找到线索。
4.4 固件结构重组
4.4.1 重组目标与原则
固件结构重组的核心目标是将原始固件镜像转换为可挂载、可浏览、可分析的目录树,为后续的代码逆向、漏洞挖掘和配置提取奠定基础。重组应遵循以下原则:
- 无损提取:确保提取出的文件与原始固件中的数据完全一致,不丢失任何字节。
- 结构还原:恢复文件系统的目录层级、文件权限、符号链接和硬链接。
- 元数据保留:保留文件时间戳、UID/GID、设备节点等元数据信息。
- 可重复性:记录所有提取步骤和参数,确保其他人可以复现分析过程。
4.4.2 重组操作流程
| 步骤 | 操作 | 工具/命令 | 输出 |
|---|---|---|---|
| 1. 固件识别 | 扫描固件整体结构 | binwalk firmware.bin | 分区偏移和类型列表 |
| 2. 头部解析 | 提取并解析固件头 | hexdump, 010 Editor, Python | 头部字段值 |
| 3. 分区提取 | 按偏移提取各分区 | dd if=firmware.bin of=part.bin bs=1 skip=X count=Y | kernel.bin, rootfs.bin |
| 4. 解压缩 | 解压内核和文件系统 | gzip -d, xz -d, unlzma, zstd -d | 解压后的镜像文件 |
| 5. 文件系统挂载/提取 | 挂载或提取文件系统 | unsquashfs, jefferson, ubireader_extract_files, mount -o loop | rootfs/ 目录树 |
| 6. 验证与整理 | 检查文件完整性、修复符号链接 | file, ls -la, readlink | 可分析的固件目录 |
4.4.3 案例拆解:某品牌路由器固件重组
背景与约束:某品牌家用路由器固件router_fw.bin,大小16MB,目标是从中提取Web管理界面和配置文件,用于分析潜在的命令注入漏洞。已知该设备使用Broadcom芯片组,Bootloader为U-Boot。
初始现象:使用binwalk扫描显示:偏移0x0处为TRX头部,偏移0x1C处为内核(LZMA压缩),偏移0x100000处为SquashFS文件系统。但unsquashfs直接提取失败,提示“Filesystem is corrupt”。
分析判断过程:怀疑SquashFS的起始偏移不准确。使用binwalk -A进行熵分析,发现0x100000到0x100100区域熵值较低,而0x100100之后熵值接近8.0,说明真正的SquashFS起始偏移在0x100100处。进一步检查发现,TRX头部中的分区长度字段比实际SquashFS长度多256字节,导致偏移偏差。
实际操作步骤:
# 1. 提取SquashFS分区(修正偏移)
dd if=router_fw.bin of=rootfs.sqsh bs=1 skip=$((0x100100)) count=$((0xEF0000))
# 2. 验证文件系统类型
file rootfs.sqsh
# 输出: SquashFS filesystem, little endian, version 4.0, compression: xz
# 3. 使用unsquashfs提取
unsquashfs -d rootfs rootfs.sqsh
# 4. 检查提取结果
ls -la rootfs/etc/
cat rootfs/etc/passwd
结果对比:修正偏移后,SquashFS成功提取,共恢复1,247个文件,包括Web目录www/、配置文件etc/config/和启动脚本etc/init.d/。在www/cgi-bin/目录下发现一个名为ping_test.cgi的脚本,其中存在未过滤的system()调用,确认了命令注入漏洞。
风险与代价:初次提取失败浪费了约30分钟,原因是过度信任binwalk的默认扫描结果。教训是:binwalk的偏移识别并非100%准确,尤其是当固件头包含填充字节或厂商自定义字段时,必须结合熵分析和手动验证。
复盘经验:对于SquashFS提取失败的情况,优先尝试binwalk -A查看熵分布,然后使用dd从熵值突变的边界重新提取。同时,检查TRX头部中的分区长度字段是否与实际数据一致。
4.5 常见误区与总结复盘
4.5.1 常见误区
- 误区一:盲目信任binwalk结果。binwalk基于特征串匹配,对于偏移有偏差或魔数被修改的固件可能失效。应结合熵分析和手动验证。
- 误区二:忽略固件头中的校验字段。某些固件在头部包含CRC32,如果提取时修改了头部,可能导致后续分析失败。始终在副本上操作。
- 误区三:混淆压缩与加密。压缩数据有特征串且熵值高但非均匀,加密数据熵值均匀且无特征串。使用
binwalk -E查看熵分布图可区分。 - 误区四:JFFS2和UBIFS的提取方式混淆。JFFS2使用
jefferson,UBIFS使用ubi_reader,两者不能互换。且UBIFS需要完整的NAND镜像(含OOB数据)。 - 误区五:忽略文件系统挂载时的写保护。对于只读文件系统(CramFS/SquashFS),挂载时应使用
-o ro,避免意外写入导致镜像损坏。
4.5.2 总结复盘
固件文件系统与格式分析是固件逆向工程的地基。本章的核心要点可归纳为:
- 四种文件系统各有适用场景:CramFS适合极小NOR Flash,SquashFS是当前主流只读方案,JFFS2和UBIFS用于需要可写数据分区的NAND设备。
- 固件头解析是分区的钥匙:掌握TRX、U-Boot、厂商自定义头部的解析方法,能够快速定位内核和文件系统的精确偏移。
- 压缩与加密识别决定后续路径:压缩可通过特征串和熵分析解决,加密则需要逆向Bootloader提取密钥。
- 结构重组要注重验证:每次提取后使用
file、ls、readlink验证文件类型和链接完整性,确保分析基础可靠。
struct、binascii、zlib)。同时,维护一个已知固件头格式的数据库,便于快速匹配。