2、固件提取技术:硬件调试接口(JTAG/SWD)、SPI Flash读取、BGA芯片拆焊与读取

好,我们进入第二章。这一章讲的是固件提取,说白了就是怎么把芯片里的程序“掏”出来。你可能会想,直接读Flash不就行了?嗯,事情没那么简单。不同的芯片、不同的封装、不同的保护机制,决定了你得用不同的手段。我这些年折腾下来,总结出三条路:调试接口、SPI Flash直读、以及BGA拆焊。咱们一条一条说。

2.1 硬件调试接口:JTAG与SWD

JTAG和SWD,是嵌入式世界里最常用的调试接口。JTAG是老祖宗,SWD是ARM后来搞的简化版。我个人习惯,能走SWD就走SWD,因为线少,稳定,不容易出错。

2.1.1 JTAG接口

JTAG全称是Joint Test Action Group,标准是IEEE 1149.1。它用了5根线:

  • TMS:模式选择
  • TCK:时钟
  • TDI:数据输入
  • TDO:数据输出
  • TRST:复位(可选)

JTAG的核心思想是通过边界扫描链,把芯片内部的寄存器串起来。你可以通过TDI往里面塞数据,再从TDO读出来。说白了,就是一条串行高速公路。

我在项目中遇到过一款路由器,JTAG接口被厂家物理割断了。嗯,他们以为这样我就读不了固件了。其实只要找到CPU的JTAG引脚,飞几根线就能搞定。你想想看,厂家能割断PCB走线,但割不断芯片内部的扫描链。

小技巧: 找JTAG引脚,可以用万用表量一下。JTAG的TCK和TMS通常有上拉或下拉电阻,对地阻值在1kΩ~10kΩ之间。我一般先找TCK,因为它频率固定,用示波器能看到方波。

2.1.2 SWD接口

SWD是Serial Wire Debug的缩写,ARM Cortex-M系列标配。它只需要两根线:

  • SWDIO:数据线(双向)
  • SWCLK:时钟线

为什么ARM要搞SWD?因为省引脚啊!很多Cortex-M芯片只有20个引脚,JTAG占5个太奢侈了。SWD两根线就能完成调试和编程,效率还比JTAG高。

我记得有一次,客户拿了个变砖的智能门锁过来。主控是STM32F103,SWD接口被固件锁死了。我试了常规的拉低BOOT0引脚进系统存储器模式,结果发现厂家把BOOT0引脚也禁用了。最后怎么搞的?我用高压编程器,直接给芯片的复位引脚送了一个12V脉冲,强行解锁了SWD接口。嗯,这招有点暴力,但管用。

警告: 高压解锁有风险,可能会烧坏芯片。我建议你先试试常规方法:上电瞬间按住复位键,同时用调试器连接,再释放复位。很多芯片的SWD锁就是这么破的。

2.2 SPI Flash读取

SPI Flash是嵌入式系统里最常见的固件存储介质。它便宜、容量大、接口简单。但读取它,你得先找到它。

2.2.1 识别SPI Flash

SPI Flash通常是8个引脚的小芯片,封装有SOIC-8、WSON-8等。引脚定义很固定:

引脚号 名称 功能
1 CS# 片选(低电平有效)
2 DO 数据输出
3 WP# 写保护
4 GND
5 DI 数据输入
6 CLK 时钟
7 HOLD# 保持
8 VCC 电源

你拿到一块板子,先找这种8脚芯片。用万用表量一下第8脚对地电压,如果是3.3V或1.8V,基本就是SPI Flash了。

2.2.2 在线读取 vs 离线读取

读取SPI Flash有两种方式:

  • 在线读取:用夹子夹住芯片,通过编程器直接读。好处是不用拆芯片,坏处是如果板子上有其他设备干扰,可能读出来的数据是错的。
  • 离线读取:把芯片拆下来,放到编程器座子里读。数据最可靠,但需要拆焊。

我个人习惯,先在线读一次。如果读出来的数据校验不过,再拆下来离线读。你想想看,拆芯片是有风险的,尤其是多层板,焊盘很容易掉。

核心命令: SPI Flash读取固件,最常用的命令是0x03(读数据)和0x9F(读ID)。比如你想读Winbond W25Q64,发送0x9F,它会返回三个字节:EF 40 18,这就是它的唯一ID。

下面是一个用Python通过SPI读取Flash的示例:

import spidev
import time

spi = spidev.SpiDev()
spi.open(0, 0)  # 打开SPI总线0,片选0
spi.max_speed_hz = 1000000  # 1MHz

# 读取芯片ID
spi.xfer2([0x9F])  # 发送读ID命令
id_bytes = spi.xfer2([0x00, 0x00, 0x00])  # 读取3个字节
print(f"Flash ID: {id_bytes[0]:02X} {id_bytes[1]:02X} {id_bytes[2]:02X}")

# 读取固件(从地址0开始,读256字节)
addr = 0x000000
spi.xfer2([0x03, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF])
firmware = spi.xfer2([0x00] * 256)  # 读取256字节
print(f"Firmware first 16 bytes: {firmware[:16].hex()}")

spi.close()

我曾经遇到过一个问题:在线读取时,读出来的数据全是0xFF。折腾了半天,发现是板子上的电源没断干净,Flash处于复位状态。嗯,这里要注意:在线读取时,一定要确保板子完全断电,或者Flash的VCC引脚有稳定的电压。

2.3 BGA芯片拆焊与读取

BGA封装,是嵌入式安全分析里最头疼的东西。它没有引脚,只有底部的焊球。想读里面的固件?要么拆下来,要么用飞线。

2.3.1 什么时候需要拆BGA

一般来说,只有以下情况才需要拆BGA:

  • 芯片内部集成了Flash(比如ESP32、MT7688等)
  • SPI Flash被封装在芯片内部,外部无法直接访问
  • 芯片有读保护,且无法通过调试接口绕过

我拆过最多次的是ESP32。它的Flash是内置的,想读固件只能把芯片拆下来,放到编程器上读。但ESP32的Flash是加密的,拆下来读出来的也是密文。嗯,那是另一章的内容了。

2.3.2 拆焊工具与步骤

拆BGA,工具很重要。我推荐以下配置:

  • 热风枪:温度控制在300~350°C,风速中等
  • 助焊剂:BGA专用助焊剂,不要用松香
  • 镊子:防静电镊子,尖头的最好
  • 预热台:如果板子比较大,建议用预热台先加热到100°C

步骤其实不复杂:

  1. 在芯片四周涂上助焊剂
  2. 用热风枪均匀加热,先预热整个芯片区域
  3. 等助焊剂开始冒烟,用镊子轻轻推一下芯片
  4. 如果芯片能滑动,说明焊球已经融化,用镊子夹起来
  5. 清理焊盘,用吸锡带把残留的锡吸干净
避坑指南: 我曾经因为心急,温度调到了400°C,结果芯片外壳都鼓包了。BGA芯片很脆弱,温度超过350°C就可能内部损坏。我建议你从300°C开始,慢慢往上加,直到芯片能滑动为止。

2.3.3 读取BGA芯片中的固件

拆下来之后,怎么读?有两种方法:

  • 直接编程器读取:如果芯片是标准的SPI Flash封装(比如WSON-8),直接放到编程器座子里读。
  • 飞线读取:如果芯片是BGA封装,没有现成的座子,就得飞线。找到芯片的SPI引脚(CS、CLK、DI、DO),焊上细漆包线,再连到编程器上。

飞线是个技术活。BGA芯片的焊球间距只有0.5mm甚至0.4mm,你得用显微镜才能看清。我一般用0.1mm的漆包线,先在焊盘上涂一点助焊剂,然后用烙铁轻轻一碰,线就焊上去了。

我记得有一次,客户拿了个工业路由器过来,主控是MT7621。它的Flash是内置的,BGA封装。我拆下来之后,发现芯片底部有176个焊球,但只有4个是SPI接口。我花了两个小时,用万用表一个一个量,才找到CS、CLK、DI、DO这四个引脚。嗯,那感觉就像在海底捞针。

小技巧: 找BGA芯片的引脚定义,最快的方法是查数据手册。如果找不到,可以去网上搜“芯片型号+ball map”,很多芯片厂商会提供引脚分布图。我一般先搜,搜不到再自己量。

2.4 总结

固件提取,说白了就是一场攻防博弈。厂家会想尽办法保护固件,比如割断JTAG、锁死SWD、加密Flash。但作为安全分析人员,我们也有自己的手段。JTAG和SWD是最优雅的方式,SPI Flash直读是最可靠的方式,BGA拆焊是最后的手段。

我个人建议,初学者先从SPI Flash读取开始练手。找一块旧的路由器或者开发板,用夹子夹住Flash,用编程器读出来。等你能熟练读出固件了,再尝试JTAG和SWD。至于BGA拆焊,嗯,那需要一定的焊接功底,建议你先在废板上练练手。

下一章,我们会讲固件分析的第一步:文件系统识别与提取。到时候你会看到,读出来的固件其实是一个大杂烩,里面有文件系统、内核、引导程序等等。怎么把它们分开?我们到时候细说。