第3章 固件获取与预处理:从物理介质到可分析镜像

拿到一块嵌入式设备,第一件事是什么?不是急着上调试器,也不是翻数据手册。你得先把固件弄出来。这活儿看着简单,其实坑不少。我这些年拆过的设备少说也有上百种,每次提取固件都能遇到点新花样。

这一章咱们就聊聊固件获取的几种主流方法,以及拿到原始二进制后怎么处理成能分析的样子。

3.1 固件提取的三种硬核方式

提取固件,说白了就是把芯片里的二进制数据读出来。根据设备提供的接口不同,我一般分三种情况处理。

3.1.1 SPI Flash编程器——最直接的方法

大多数嵌入式设备用SPI Flash存固件。这种芯片通常8个脚,用夹子夹住就能读。我个人习惯用CH341A编程器或者Segger J-Flash,便宜又好用。

操作步骤:
  1. 找到板子上的SPI Flash芯片(常见型号:W25Q64、MX25L128等)
  2. 用烧录夹夹住芯片,注意方向——芯片上有个小圆点标记1脚
  3. 连接编程器,选择对应型号
  4. 读取全片数据,保存为.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
注意:有些芯片有读保护(RDP)。如果读出来全是0xFF或0x00,八成是被锁了。这时候需要先解除保护——但解除保护会擦除整个Flash。所以,拿到设备后先别急着操作,看看有没有保护。

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包前面有个结构体,里面写着加密算法类型和密钥索引。

我的经验:有一次分析一个路由器固件,binwalk显示熵值0.95,但文件头是正常的U-Boot镜像。后来发现厂商只加密了内核部分,U-Boot是明文的。所以别只看开头,要分段分析。

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识别出Squashfs,但提取时报CRC错误。后来发现厂商修改了文件系统的魔数(magic number)。解决办法是用十六进制编辑器手动修正魔数,或者用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网络抓包 原始二进制固件 (.bin) 预处理:熵值分析 → 加密/压缩识别 → 文件头解析 binwalk解包 → 文件系统提取 → 手动挂载 可分析固件:文件系统 + 内核 + 配置 关键:每一步都可能遇到厂商定制,需要灵活应对 常见坑:读保护、加密魔数修改、OTA包分块加密

这张图把整个流程串起来了。从物理获取到最终拿到可分析的文件系统,每一步都有对应的工具和方法。记住,没有万能的方案,不同设备得用不同招数。

核心要点回顾:
  • SPI Flash编程器适合拆机场景,注意掉电保护问题
  • JTAG/SWD适合有调试接口的设备,注意读保护
  • OTA抓包适合无法拆机的设备,注意加密和分块
  • 熵值分析是判断加密/压缩的第一道关卡
  • binwalk是解包利器,但遇到定制魔数需要手动处理
  • 固件头解析需要结合引导程序逆向