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——线少,不容易接错。

小技巧: 很多开发板上的SWD接口会跟其他功能复用引脚。如果读不出来,先查芯片手册,看看是不是有复用功能没配置好。我曾经在一块ST板子上折腾了半小时,最后发现是BOOT0引脚电平不对。

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文件里。

警告: 读取之前,一定要确认芯片有没有开启读保护(RDP)。如果RDP等级是1或2,直接读会得到全0或者全1的数据。我曾经遇到过一块板子,读出来全是0xFF,还以为是Flash坏了,后来才发现是读保护没解除。

解除读保护的方法,通常是用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的方法:

  1. 把BOOT0拉高(接3.3V),BOOT1拉低(接GND)。
  2. 给芯片上电或复位。
  3. 芯片就会从系统存储器启动,运行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文件是原始二进制数据,里面可能包含固件、文件系统、配置参数等。需要后续分析才能分离出来。

注意: 有些Flash芯片有写保护引脚(WP#),如果这个引脚被拉低了,编程器可能读不出来。检查一下板子上的WP#引脚,确保它是高电平。

3.4 三种方法的对比

说了这么多,到底该用哪种方法?我整理了一个对比表,方便你根据实际情况选择:

方法 适用场景 优点 缺点
JTAG/SWD 调试接口可用,未开启读保护 速度快,操作简单 读保护后无法读取
Bootloader 调试接口不可用,但Bootloader可用 不需要额外硬件 速度慢,需要协议分析
Flash直接读取 外挂SPI Flash,可拆卸 不受读保护限制 需要拆芯片,有损坏风险

3.5 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个决策树,遇到具体问题时,按图索骥:

固件提取技术决策树 固件提取 调试接口可用? Bootloader可用? Flash可拆卸? JTAG/SWD读取 Bootloader提取 Flash直接读取 决策说明 1. 优先尝试JTAG/SWD,最简单快捷 2. 如果调试接口被锁,试试Bootloader 3. 如果Bootloader也用不了,拆Flash芯片 4. 注意:拆芯片有损坏风险,操作要小心

3.6 避坑指南

最后,分享几个我踩过的坑,希望能帮你少走弯路:

  • 读保护等级2(RDP2)不可逆:一旦设置了RDP2,芯片就永久锁死了,任何方法都读不出来。所以,拿到芯片后,先用stm32f1x lock_read命令检查一下RDP等级。
  • Bootloader的波特率不是固定的:有些芯片的Bootloader支持自动波特率检测,有些需要手动设置。我遇到过一块板子,默认波特率是57600,而不是常见的115200。
  • SPI Flash的引脚顺序:拆芯片之前,先拍照记录引脚方向。我有个同事,拆下来之后忘了方向,装回去的时候焊反了,芯片直接冒烟了。
  • 供电不足:用编程器读取Flash时,如果板子上的电源不稳定,可能会读到错误数据。建议用外部稳压电源给板子供电,别只靠USB口。

好了,这一章的内容就到这里。固件提取是逆向分析的第一步,也是最关键的一步。方法选对了,事半功倍;选错了,可能连数据都拿不到。希望你能根据实际情况,灵活选择最适合的方法。