4、固件解包与文件系统分析:squashfs、cramfs、jffs2 文件系统处理
拿到一个固件 bin 文件,第一件事是什么?
不是直接上逆向工具,也不是扔进模拟器跑。你得先把它拆开,看看里面到底装了些什么。说白了,固件解包是后续所有分析工作的基础。如果这步卡住了,后面全是白搭。
我刚开始做固件分析时,就吃过这个亏。拿到一个路由器固件,直接扔进 binwalk 就跑,结果解出来一堆乱码。后来才发现,是文件系统类型判断错了。嗯,今天我们就来把这几个常见的嵌入式文件系统彻底讲清楚。
4.1 固件解包三板斧:binwalk、dd、jefferson
解包工具不少,但真正好用的就那么几个。我个人习惯按这个顺序来:
- binwalk 扫描 —— 先看看固件里有什么
- binwalk -e 自动提取 —— 能解就解
- 手动 dd 切割 —— 自动不行就手动
- 专用工具处理 —— 针对特定文件系统
我的经验:binwalk 不是万能的。遇到加密固件或非标准偏移,它经常翻车。这时候别慌,用 dd 手动切,配合 strings 和 hexdump 定位文件系统头,反而更靠谱。
先看一个典型的 binwalk 扫描结果:
$ binwalk firmware.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 TRX firmware header, little endian, image size: 8388608
196 0xC4 LZMA compressed data, properties: 0x5D
1310720 0x140000 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 7077888 bytes
看到 Squashfs filesystem 了吗?这就是我们要找的文件系统起始位置。偏移量是 0x140000,大小约 7MB。
自动提取很简单:
$ binwalk -e firmware.bin
它会生成一个 _firmware.bin.extracted 目录,里面就是解出来的东西。但有时候 binwalk 会报错,或者解出来的文件不完整。这时候就得手动来了。
4.2 Squashfs:最常用的只读文件系统
Squashfs 是嵌入式设备里最常见的文件系统,没有之一。它只读、压缩率高、支持多种压缩算法。我见过的路由器、摄像头、IoT 设备,十有八九都用它。
Squashfs 的特点:
- 只读文件系统,启动时直接解压到内存
- 支持 gzip、lzma、lzo、xz、zstd 等多种压缩
- 版本 3.0 和 4.0 差异很大,工具不通用
- 文件系统头有魔数
hsqs(小端)或shsq(大端)
注意:Squashfs 4.0 和 3.0 的 unsquashfs 工具不兼容。我曾经用 4.0 的工具去解 3.0 的固件,结果直接 segfault。一定要先确认版本。
查看 Squashfs 版本和信息的命令:
$ unsquashfs -s squashfs.bin
Found a valid SQUASHFS 4:0 superblock on squashfs.bin.
Creation or last modification time: Tue Jan 10 10:30:45 2023
Compression: lzma
Block size: 131072
Filesystem size: 7077888 bytes
Inodes count: 1234
解包命令:
$ unsquashfs -d ./output_dir squashfs.bin
如果遇到 unsquashfs: 无法识别的文件系统,大概率是版本不对。试试用 sasquatch(一个支持更多变种的 unsquashfs 分支):
$ sasquatch -d ./output_dir squashfs.bin
4.3 Cramfs:老牌嵌入式文件系统
Cramfs 比 Squashfs 更老,现在用得少了,但一些老设备(尤其是 2010 年以前的)还在用。它的压缩率不如 Squashfs,但解压速度快,适合资源极度受限的设备。
Cramfs 的特点:
- 只读,最大支持 256MB 文件系统
- 单个文件最大 16MB
- 魔数
CramFS或0x28CD3D45 - 不支持压缩(或者说压缩率很低)
解包 Cramfs 用 cramfsck 或 uncramfs:
$ cramfsck -x ./output_dir cramfs.bin
或者:
$ uncramfs cramfs.bin ./output_dir
避坑指南:我曾经遇到一个 Cramfs 固件,用 cramfsck 解出来全是空文件。后来发现是文件系统被修改过,魔数被改了。用 hexdump 看了下,发现前 4 个字节被改成了 0xDEADBEEF。改回来就正常了。所以,遇到解不开的,先看看魔数对不对。
4.4 JFFS2:NAND Flash 上的日志型文件系统
JFFS2 和上面两个不一样。它是可写的,专门为 NAND Flash 设计。你会在一些高端路由器、工业设备上看到它。
JFFS2 的特点:
- 可读写,支持磨损均衡
- 压缩(默认 zlib,可选 lzo)
- 魔数
0x1985(小端) - 解包需要指定擦除块大小(erase block size)
解包 JFFS2 用 jefferson 工具:
$ jefferson -d ./output_dir jffs2.bin
如果 jefferson 报错,通常是因为擦除块大小不对。试试手动指定:
$ jefferson -d ./output_dir -e 0x20000 jffs2.bin
常见的擦除块大小:64KB(0x10000)、128KB(0x20000)、256KB(0x40000)。
注意:JFFS2 解包时,如果擦除块大小不对,解出来的文件可能乱码或缺失。我建议先用 strings 看看固件里有没有内核日志,里面经常会有擦除块大小的信息。
4.5 三种文件系统对比
为了方便你快速选择工具和方法,我整理了一个对比表:
| 特性 | Squashfs | Cramfs | JFFS2 |
|---|---|---|---|
| 读写属性 | 只读 | 只读 | 可读写 |
| 压缩支持 | gzip/lzma/lzo/xz/zstd | 无(或极低) | zlib/lzo |
| 最大文件系统 | 无限制(4.0) | 256MB | 取决于 Flash 大小 |
| 魔数 | hsqs / shsq |
CramFS / 0x28CD3D45 |
0x1985 |
| 解包工具 | unsquashfs / sasquatch | cramfsck / uncramfs | jefferson |
| 常见设备 | 路由器、摄像头、IoT | 老设备、工控设备 | 高端路由器、NAS |
4.6 文件系统分析流程
下面这张图是我自己总结的分析流程,你可以照着走:
4.7 实战:一个完整的解包案例
假设你拿到一个路由器固件 router_fw.bin,我们来走一遍完整流程。
第一步:扫描
$ binwalk router_fw.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 TRX firmware header, little endian, image size: 16777216
256 0x100 uImage header, header size: 64 bytes, load address: 0x80000000
320 0x140 LZMA compressed data, properties: 0x5D
4194304 0x400000 Squashfs filesystem, little endian, version 4.0, compression:lzma
第二步:自动提取
$ binwalk -e router_fw.bin
$ ls _router_fw.bin.extracted/
0.trx 100.uimage 140.lzma 400000.squashfs
第三步:解包 Squashfs
$ unsquashfs -d ./rootfs 400000.squashfs
$ ls rootfs/
bin dev etc lib mnt proc sbin tmp usr var
看到熟悉的 Linux 目录结构了。接下来就可以分析文件了——检查启动脚本、找硬编码密码、看有没有后门,这些都是后续章节的内容。
核心要点:解包只是第一步,但也是最关键的一步。工具用对、版本匹配、偏移准确,这三个条件缺一不可。如果解出来的文件系统不完整,或者文件内容乱码,先检查这三个方面。
4.8 常见问题与解决方案
最后,我把这些年遇到过的坑整理一下:
- binwalk 报错 "unknown filesystem" —— 试试用
-M参数递归扫描,或者手动用 hexdump 找魔数 - unsquashfs segfault —— 版本不匹配,换 sasquatch 试试
- cramfsck 解出空文件 —— 检查魔数是否被修改,用 hexdump 对比标准魔数
- jefferson 报错 "invalid erase block size" —— 用
-e参数手动指定,从 0x10000 开始试 - 解出来的文件乱码 —— 可能是压缩算法不对,或者文件系统被加密了