第3章 固件获取与预处理:从物理介质到可分析镜像
拿到一块嵌入式设备,第一件事是什么?不是急着上调试器,也不是翻数据手册。你得先把固件弄出来。这活儿看着简单,其实坑不少。我这些年拆过的设备少说也有上百种,每次提取固件都能遇到点新花样。
这一章咱们就聊聊固件获取的几种主流方法,以及拿到原始二进制后怎么处理成能分析的样子。
3.1 固件提取的三种硬核方式
提取固件,说白了就是把芯片里的二进制数据读出来。根据设备提供的接口不同,我一般分三种情况处理。
3.1.1 SPI Flash编程器——最直接的方法
大多数嵌入式设备用SPI Flash存固件。这种芯片通常8个脚,用夹子夹住就能读。我个人习惯用CH341A编程器或者Segger J-Flash,便宜又好用。
- 找到板子上的SPI Flash芯片(常见型号:W25Q64、MX25L128等)
- 用烧录夹夹住芯片,注意方向——芯片上有个小圆点标记1脚
- 连接编程器,选择对应型号
- 读取全片数据,保存为.bin文件
这里有个坑——有些板子会掉电保护。我记得有一次读一个工业控制器,夹上夹子后芯片死活不响应。后来发现是板子上其他电路把SPI总线拉低了。解决办法?把芯片焊下来读。嗯,虽然麻烦,但有时候就得这么干。
| 编程器型号 | 支持协议 | 速度 | 价格区间 |
|---|---|---|---|
| CH341A | SPI/I2C | 慢(约500KB/s) | ¥20-50 |
| TL866II Plus | SPI/并行/串行 | 中(约2MB/s) | ¥300-500 |
| Segger J-Flash | SPI/SWD/JTAG | 快(约10MB/s) | ¥2000+ |
3.1.2 JTAG/SWD——不拆芯片也能读
如果设备有调试接口,那JTAG或SWD是更好的选择。不需要动烙铁,直接连上调试器就能读内存、读Flash。
我常用的工具链:OpenOCD + GDB,或者直接用J-Link Commander。命令很简单:
# 用OpenOCD读取整个Flash
openocd -f interface/jlink.cfg -f target/stm32f4x.cfg \
-c "init" \
-c "flash read_bank 0 firmware.bin" \
-c "shutdown"
# 或者用J-Link直接dump内存
JLinkExe -device STM32F407VG -if SWD -speed 4000
> savebin firmware.bin 0x08000000 0x100000
> exit
3.1.3 OTA抓包——没有硬件也能搞
有些设备你根本拆不开,或者不想拆。比如智能灯泡、插座这种全密封的。那怎么办?抓OTA升级包。
方法很简单:让设备升级,你在中间抓网络流量。我常用Wireshark配合mitmproxy。
抓到的包通常长这样:
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 1048576
[二进制数据...]
把二进制部分提取出来,保存成文件。但这里有个问题——OTA包往往是加密的或者分块的。我遇到过一家厂商,把固件切成256字节一块,每块用AES-128加密,然后前面加个序号。你想想看,如果不知道这个结构,拿到数据也是一堆乱码。
3.2 固件加密与压缩识别
拿到原始二进制后,先别急着解包。你得看看它是不是加密或压缩过。怎么判断?看熵值和文件头特征。
3.2.1 熵值分析
加密数据和压缩数据有个共同特点:字节分布非常均匀。普通固件里有很多0x00、0xFF,还有ASCII字符串。加密后的数据看起来就像白噪声。
我用binwalk的熵分析功能:
binwalk -E firmware.bin
输出会画一条熵值曲线。如果整条线都在0.9以上,那基本可以确定是加密或压缩了。
| 熵值范围 | 含义 | 处理方式 |
|---|---|---|
| 0.0 - 0.3 | 大量重复数据(如全0xFF填充) | 直接跳过填充区 |
| 0.3 - 0.7 | 正常固件代码/数据 | 可直接分析 |
| 0.7 - 0.9 | 压缩数据(如LZMA、gzip) | 尝试解压缩 |
| 0.9 - 1.0 | 加密数据或高度压缩 | 需要解密或解压 |
3.2.2 常见加密与压缩算法识别
压缩算法通常有固定文件头。比如:
- gzip:以1F 8B开头
- LZMA:以5D 00开头
- zlib:以78 9C或78 DA开头
加密就没这么友好了。AES、DES、RSA这些算法没有固定文件头。怎么识别?看上下文。比如OTA包前面有个结构体,里面写着加密算法类型和密钥索引。
3.3 binwalk与固件解包
binwalk是固件分析的瑞士军刀。它能自动识别文件类型、提取文件系统、分析熵值。我几乎每个项目都会用到它。
3.3.1 基本用法
# 扫描固件中的文件
binwalk firmware.bin
# 提取所有识别到的文件
binwalk -e firmware.bin
# 递归提取(解包后再解包)
binwalk -Me firmware.bin
输出示例:
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 U-Boot version string, "U-Boot 2016.01"
262144 0x40000 LZMA compressed data
524288 0x80000 Squashfs filesystem, little endian, version 4.0
看到没?binwalk直接找到了U-Boot、LZMA压缩数据和Squashfs文件系统。这时候用-e参数就能全部提取出来。
3.3.2 手动解包文件系统
binwalk提取出来的文件系统,有时候需要手动挂载。比如Squashfs:
# 查看文件系统信息
unsquashfs -s squashfs.img
# 解压到目录
unsquashfs -d ./rootfs squashfs.img
如果是Cramfs或JFFS2,也有对应的工具。我建议你装个固件分析工具包(firmware-analysis-toolkit),它把这些工具都整合了。
binwalk -y squashfs强制扫描。
3.4 固件头分析
固件头是理解整个固件结构的关键。它通常包含:加载地址、大小、校验和、版本号等信息。
3.4.1 常见固件头格式
| 固件类型 | 头部大小 | 关键字段 |
|---|---|---|
| U-Boot镜像 | 64字节 | 魔数(0x27051956)、加载地址、入口点、大小 |
| STM32 | 可变 | 栈指针、复位向量、中断向量表 |
| ESP32 | 24字节 | 魔数(0xE9)、段数量、Flash配置 |
| TP-Link | 128字节 | 硬件ID、固件版本、校验和 |
3.4.2 手动解析固件头
用hexdump或010 Editor打开固件,看前几个字节:
hexdump -C firmware.bin | head -20
输出:
00000000 27 05 19 56 00 00 00 00 00 10 00 00 00 00 00 00 |'..V............|
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000020 55 2D 42 6F 6F 74 20 32 30 31 36 2E 30 31 00 00 |U-Boot 2016.01..|
看到27 05 19 56了吗?这是U-Boot的魔数。后面跟着的00 10 00 00(小端序0x1000)是加载地址。再后面是U-Boot版本字符串。
我一般会写个Python脚本解析固件头:
import struct
def parse_uboot_header(data):
magic = struct.unpack('>I', data[0:4])[0]
if magic != 0x27051956:
print("不是U-Boot镜像")
return
load_addr = struct.unpack('>I', data[12:16])[0]
entry_point = struct.unpack('>I', data[16:20])[0]
size = struct.unpack('>I', data[20:24])[0]
print(f"加载地址: 0x{load_addr:08X}")
print(f"入口点: 0x{entry_point:08X}")
print(f"大小: {size} 字节")
3.5 本章知识体系
下面这张图总结了固件获取与预处理的完整流程:
这张图把整个流程串起来了。从物理获取到最终拿到可分析的文件系统,每一步都有对应的工具和方法。记住,没有万能的方案,不同设备得用不同招数。
- SPI Flash编程器适合拆机场景,注意掉电保护问题
- JTAG/SWD适合有调试接口的设备,注意读保护
- OTA抓包适合无法拆机的设备,注意加密和分块
- 熵值分析是判断加密/压缩的第一道关卡
- binwalk是解包利器,但遇到定制魔数需要手动处理
- 固件头解析需要结合引导程序逆向