3、固件提取技术:JTAG/SWD调试接口、Bootloader提取、Flash读取与转储
固件提取,说白了就是怎么把芯片里的程序“拿”出来。这一步做不好,后面所有的逆向分析都是空谈。我见过不少新手,上来就拿着J-Link往板子上怼,结果要么读不出数据,要么直接把芯片锁死了。
这一章,咱们就聊聊三种最主流的提取方式:调试接口、Bootloader和Flash直接读取。每种方法都有它的适用场景,也有它的坑。
3.1 JTAG/SWD调试接口提取
JTAG和SWD,是ARM芯片最常用的调试接口。JTAG需要5根线(TMS、TCK、TDI、TDO、nTRST),SWD只需要2根(SWDIO、SWCLK)。我个人习惯,只要芯片支持,优先用SWD——线少,不容易接错。
3.1.1 硬件连接与识别
拿到一块陌生的板子,第一步是找到调试接口。通常板子上会有标注,比如“JTAG”、“SWD”、“J1”、“P2”之类的。如果没有标注,可以用万用表量一下——SWDIO和SWCLK通常上拉到3.3V,GND就是地。
连接好之后,用OpenOCD或者J-Flash试试能不能识别到芯片。命令很简单:
openocd -f interface/jlink.cfg -f target/stm32f1x.cfg
如果能看到类似这样的输出,说明连接成功了:
Info : STM32F1xx: 512 KiB Flash, 64 KiB RAM
Info : JTAG/SWD speed: 1000 kHz
嗯,这里要注意:速度别设太高。我刚开始做的时候,为了图快,把SWD频率设到了10MHz,结果死活连不上。后来降到1MHz,一次成功。有些板子布线不好,高频信号会出问题。
3.1.2 读取Flash内容
连接成功后,读取Flash就很简单了。用OpenOCD的flash read_bank命令:
flash read_bank 0 firmware.bin 0x08000000 0x80000
这条命令的意思是:从地址0x08000000开始,读取512KB(0x80000字节)的数据,保存到firmware.bin文件里。
解除读保护的方法,通常是用OpenOCD的stm32f1x unlock 0命令。但要注意,解除读保护会擦除整个Flash——也就是说,固件也会被擦掉。所以,如果芯片上了读保护,这条路就走不通了。
3.2 Bootloader提取
如果调试接口被锁了,或者压根没引出调试接口,怎么办?别急,还有Bootloader这条路。
STM32出厂时,ROM里就固化了一个Bootloader。通过设置BOOT0和BOOT1引脚,可以让芯片从系统存储器启动,进入Bootloader模式。这时候,可以通过USART、USB、CAN等接口跟芯片通信,读取Flash内容。
3.2.1 进入Bootloader模式
以STM32F103为例,进入Bootloader的方法:
- 把BOOT0拉高(接3.3V),BOOT1拉低(接GND)。
- 给芯片上电或复位。
- 芯片就会从系统存储器启动,运行Bootloader。
进入之后,用串口工具(比如PuTTY)连接USART1(PA9/PA10),波特率设为115200。发送0x7F,如果收到0x79(ACK),说明通信成功了。
关键点: Bootloader模式下,芯片的Flash是可以通过命令读取的。常用的命令有:
- 0x00 - 获取版本和命令列表
- 0x11 - 读取存储器内容
- 0x43 - 擦除页面
- 0x44 - 写入数据
3.2.2 用脚本自动提取
手动发命令太慢了。我写过一个Python脚本,用pyserial库自动跟Bootloader通信,批量读取Flash:
import serial
import struct
def read_flash(port, baud=115200):
ser = serial.Serial(port, baud, timeout=1)
# 发送0x7F同步
ser.write(b'\x7F')
if ser.read() != b'\x79':
print("同步失败")
return
# 发送读取命令0x11
ser.write(b'\x11\xEE') # 0x11 + 0xEE(校验和)
if ser.read() != b'\x79':
print("命令失败")
return
# 发送起始地址(0x08000000)
addr = 0x08000000
addr_bytes = struct.pack('>I', addr)
checksum = 0xFF & (addr_bytes[0] ^ addr_bytes[1] ^ addr_bytes[2] ^ addr_bytes[3])
ser.write(addr_bytes + bytes([checksum]))
if ser.read() != b'\x79':
print("地址失败")
return
# 读取256字节
ser.write(b'\xFF\x00') # 长度255 + 校验
data = ser.read(256)
return data
这个脚本只是个示例,实际用的时候要加上错误重试和分段读取。我在一次项目中,就是用这个脚本从一块没有引出SWD接口的板子上,成功读出了128KB的固件。
3.3 Flash直接读取与转储
如果调试接口和Bootloader都用不了,那就只能上硬核手段了——直接读取Flash芯片。
STM32的Flash,有的是内部集成的,有的是外挂的SPI Flash。内部Flash没法直接拆下来读,但外挂的SPI Flash可以。
3.3.1 识别Flash型号
先看板子上的Flash芯片,记下型号。常见的SPI Flash有W25Q64、W25Q128、MX25L系列。用热风枪吹下来,或者用夹子夹住,然后用编程器读取。
| Flash型号 | 容量 | 接口 | 常见封装 |
|---|---|---|---|
| W25Q64 | 8MB | SPI | SOP-8 |
| W25Q128 | 16MB | SPI | SOP-8 |
| MX25L256 | 32MB | SPI | SOP-16 |
3.3.2 用编程器读取
我常用的编程器是CH341A,便宜又好用。把Flash芯片夹好,打开软件(比如FlashROM),选择对应的型号,点击“读取”。
读取出来的数据,保存成.bin文件。但要注意,这个.bin文件是原始二进制数据,里面可能包含固件、文件系统、配置参数等。需要后续分析才能分离出来。
3.4 三种方法的对比
说了这么多,到底该用哪种方法?我整理了一个对比表,方便你根据实际情况选择:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| JTAG/SWD | 调试接口可用,未开启读保护 | 速度快,操作简单 | 读保护后无法读取 |
| Bootloader | 调试接口不可用,但Bootloader可用 | 不需要额外硬件 | 速度慢,需要协议分析 |
| Flash直接读取 | 外挂SPI Flash,可拆卸 | 不受读保护限制 | 需要拆芯片,有损坏风险 |
3.5 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个决策树,遇到具体问题时,按图索骥:
3.6 避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路:
- 读保护等级2(RDP2)不可逆:一旦设置了RDP2,芯片就永久锁死了,任何方法都读不出来。所以,拿到芯片后,先用
stm32f1x lock_read命令检查一下RDP等级。 - Bootloader的波特率不是固定的:有些芯片的Bootloader支持自动波特率检测,有些需要手动设置。我遇到过一块板子,默认波特率是57600,而不是常见的115200。
- SPI Flash的引脚顺序:拆芯片之前,先拍照记录引脚方向。我有个同事,拆下来之后忘了方向,装回去的时候焊反了,芯片直接冒烟了。
- 供电不足:用编程器读取Flash时,如果板子上的电源不稳定,可能会读到错误数据。建议用外部稳压电源给板子供电,别只靠USB口。
好了,这一章的内容就到这里。固件提取是逆向分析的第一步,也是最关键的一步。方法选对了,事半功倍;选错了,可能连数据都拿不到。希望你能根据实际情况,灵活选择最适合的方法。