4、固件文件系统与格式分析

固件逆向工程的第一步往往不是直接反汇编代码,而是先解开固件的外壳——识别其文件系统格式、解析头部结构、判断压缩与加密状态,并最终重组出可分析的目录树。绝大多数嵌入式设备(路由器、摄像头、IoT网关、工业控制器)的固件都遵循“引导头 + 内核 + 根文件系统”的经典布局,而根文件系统几乎总是采用CramFS、SquashFS、JFFS2或UBIFS中的某一种。本章将系统讲解这四种文件系统的结构特征、固件头解析方法、压缩与加密识别技巧,以及固件结构重组的完整流程。

固件文件系统与格式分析 — 知识框架 原始固件镜像 .bin / .trx / .img 固件头解析 魔数/偏移/长度/校验 压缩/加密识别 熵分析/特征串/签名 文件系统识别 魔数/超级块/节点结构 CramFS 只读/压缩/无写支持 魔数: 0x28CD3D45 zlib压缩/页缓存 适合小容量NOR Flash SquashFS 只读/高压缩比 魔数: hsqs/sqsh gzip/lzma/lzo/xz/zstd Linux内核标准支持 JFFS2 日志型/可读写 节点: JFFS2_NODE_ACC zlib/rtime压缩 NOR/NAND Flash UBIFS 日志型/可读写 UBI卷/磨损均衡 LZO/zlib压缩 NAND Flash专用 静态分析工具链 binwalk | strings | file | hexdump | unsquashfs | jefferson | ubi_reader 动态验证手段 QEMU仿真 | Firmadyne | 串口调试 | JTAG/SWD 输出: 可挂载/可分析的固件目录树 → 进入代码逆向阶段

上图展示了固件格式分析的整体知识框架:从原始镜像出发,依次经过头部解析、压缩/加密识别、文件系统判定,最终借助静态工具链和动态仿真手段完成固件结构重组。下面我们将逐一深入每个环节。

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结尾连续排列。

属性CramFSSquashFSJFFS2UBIFS
读写性只读只读可读写可读写
压缩算法zlibgzip/lzma/lzo/xz/zstdzlib/rtimeLZO/zlib
魔数/标识0x28CD3D45hsqs/sqsh0x1985 (节点魔数)UBI# (卷标识)
适用FlashNORNOR/NANDNOR/NANDNAND (UBI层)
最大容量256MB16TB (v4)128MiB~4GiB取决于UBI卷
典型工具cramfsck, cramfsprogsunsquashfs, mksquashfsjefferson, jffs2dumpubi_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 头部解析流程

  1. 初步扫描:使用binwalk firmware.bin查看整体结构,识别头部和各个分区的偏移。
  2. 魔数定位:使用hexdump -C firmware.bin | head -64查看前64字节,寻找可读ASCII字符串或已知魔数。
  3. 字段解析:根据已知格式或逆向分析,逐个解析头部字段。可使用010 Editor的模板功能或Python脚本(struct模块)批量解析。
  4. 校验验证:计算头部声明的CRC32或MD5,与固件实际数据比对,确认头部字段的准确性。
  5. 分区提取:根据头部中的偏移和长度字段,使用dd提取内核、文件系统等独立分区。
风险提示:某些固件头包含加密签名(如RSA签名),用于验证固件完整性。在解析时不要修改头部任何字节,否则会导致签名验证失败。分析时应保留原始固件副本,所有提取操作在副本上进行。

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)或块密码。
经验之谈:对于加密固件,优先逆向Bootloader或U-Boot源码,查找密钥存储位置。许多厂商使用固定的AES密钥,且密钥以明文或简单混淆形式存储在Bootloader的只读数据段中。使用strings bootloader.bin | grep -i key有时能直接找到线索。

4.4 固件结构重组

4.4.1 重组目标与原则

固件结构重组的核心目标是将原始固件镜像转换为可挂载、可浏览、可分析的目录树,为后续的代码逆向、漏洞挖掘和配置提取奠定基础。重组应遵循以下原则:

  1. 无损提取:确保提取出的文件与原始固件中的数据完全一致,不丢失任何字节。
  2. 结构还原:恢复文件系统的目录层级、文件权限、符号链接和硬链接。
  3. 元数据保留:保留文件时间戳、UID/GID、设备节点等元数据信息。
  4. 可重复性:记录所有提取步骤和参数,确保其他人可以复现分析过程。

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=Ykernel.bin, rootfs.bin
4. 解压缩解压内核和文件系统gzip -d, xz -d, unlzma, zstd -d解压后的镜像文件
5. 文件系统挂载/提取挂载或提取文件系统unsquashfs, jefferson, ubireader_extract_files, mount -o looprootfs/ 目录树
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 总结复盘

固件文件系统与格式分析是固件逆向工程的地基。本章的核心要点可归纳为:

  1. 四种文件系统各有适用场景:CramFS适合极小NOR Flash,SquashFS是当前主流只读方案,JFFS2和UBIFS用于需要可写数据分区的NAND设备。
  2. 固件头解析是分区的钥匙:掌握TRX、U-Boot、厂商自定义头部的解析方法,能够快速定位内核和文件系统的精确偏移。
  3. 压缩与加密识别决定后续路径:压缩可通过特征串和熵分析解决,加密则需要逆向Bootloader提取密钥。
  4. 结构重组要注重验证:每次提取后使用file、ls、readlink验证文件类型和链接完整性,确保分析基础可靠。
实战建议:建立自己的固件分析工具箱,包括binwalk、sasquatch(SquashFS变种)、jefferson、ubi_reader、firmware-mod-kit、以及Python脚本库(struct、binascii、zlib)。同时,维护一个已知固件头格式的数据库,便于快速匹配。