不焊拆芯片也能抽固件:用Scapy从SPI/QSPI抓包重哈镜像

admin 2026-09-28 05:02:53 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍一种不焊拆芯片的固件提取方法,通过夹住SPINORFlash并用逻辑分析仪抓取启动流量,用Scapy建立协议层解析,从READ命令重建固件镜像。文章详述SPI协议基础、信号识别、Scapy建模、镜像重建流程,并发现QSPI模式切换导致rootfs丢失的问题,最后给出四步操作流程,适用于硬件安全评估和供应链审计。 综合评分: 95 文章分类: 渗透测试,逆向分析,安全工具


不焊拆芯片也能抽固件:用 Scapy 从 SPI/QSPI 抓包重哈镜像

黑卷 黑卷

赛博安全攻防日记

2026年9月26日 17:19 上海

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

不焊拆芯片也能抽固件:用 Scapy 从 SPI/QSPI 抓包重建镜像

很多人用 Scapy 拆以太网包、甚至汽车总线,却忘了它其实是一套通用二进制协议框架:不管字节从网卡、CAN 还是逻辑分析仪来,字段定义与层叠解析都一样好用。VoidStar 这篇实战写的是:只 clip 一片 SPI NOR、抓开机流量,用 Scapy 建层解析,再把 READ 回填成整片固件镜像——不用焊下 Flash。

为什么不直接拆芯片?

目标是一台「只有一台」的嵌入式设备。焊下 NOR 有物理损伤风险,别的产品还可能触发防拆。于是他们夹住 Winbond W25Q128JV(16 MiB SPI NOR,SOIC8),用 Saleae 逻辑分析仪抓启动期总线流量。

W25Q 族 SOIC8 引脚(含本文 W25Q128JV)

UART/调试口基本静默;在线读 Flash 又撞上经典的总线争用。客户也不给固件——黑盒 SPI 嗅探成了主路径。

SPI 速览(站 Flash 视角)

关心四根线:CLK、CS#(低有效,框定一笔事务)、MOSI/DI、MISO/DO。CS# 拉低开新命令,拉高结束。窗口内主机依次送出 opcode、可选地址、可选 dummy,再收/发数据。SPI 全双工:读的时候 MOSI 仍在移位「无关」字节,真正数据在 MISO。

抓包:先认出哪根是哪根

夹子 + 上电,先看全景:

开机 SPI 抓包(夹子和飞线在高速下容易伤信号完整性)

放大后:约 16 MHz 的周期线是 CLK;另一根线拉低后才有活动——那是 CS#。另外两根在同一窗口里跑数据。

单笔 SPI 事务放大

标注:CS 框事务;一根先发命令,另一根回数据

命令线上有一段「毛刺」,脉宽比一个时钟周期还窄——按当前采样率采不到真数据,当作 SI 噪声即可。

噪声区放大——亚时钟脉宽 ⇒ 不是有效 SPI 采样

挂上 Saleae SPI 分析器,对照手册:首字节 0x03 就是 READ。

| Opcode | 含义 |

| — | — |

| 0x9F | JEDEC_ID |

| 0x03 | READ |

| 0x0B | FAST_READ |

| 0x6B | FAST_READ_QUAD_OUT |

| 0x05 | RDSR1 |

| 0x06 / 0x02 | WREN / PAGE_PROGRAM |

| 0x20 / 0xD8 | SECTOR_ERASE / BLOCK_ERASE |

Saleae 里把 CLK/CS/MOSI/MISO 对上通道

分析器给出的字节流;首字节 0x03 对上 READ

手册里的 Read Data (03h) 时序:opcode → 24 位地址 → 数据

WaveDrom:单线 READ 的结构示意

地址 0 的 READ;响应以 03 00 00 08 开头;阴影 MOSI 字节为 don’t-care

把 Saleae 导出成 CSV(enable / result / disable 行)。每个 CS 窗口就是一笔事务的 MOSI/MISO 字节流——正好喂给 Scapy 模型。

用 Scapy 给 Flash 建层

薄层设计:SPI(哪条 CS)、SPIFlashCmd(opcode + 用 ConditionalField 挂可选 24 位地址 / dummy)、再加上读数据 / 状态寄存器响应层。核心摘录:

class SPIFlashCmd(Packet):
    COMMANDS = {
        0x03: "READ", 0x0B: "FAST_READ", 0x6B: "FAST_READ_QUAD_OUT",
        0x02: "PAGE_PROGRAM", 0x05: "RDSR1", 0x06: "WREN",
        0x20: "SECTOR_ERASE", 0xD8: "BLOCK_ERASE", 0x9F: "JEDEC_ID",
    }
    CMD_HAS_ADDR  = {0x03, 0x0B, 0x6B, 0x02, 0x20, 0xD8}
    CMD_HAS_DUMMY = {0x0B, 0x6B}
    fields_desc = [
        ByteEnumField("cmd", 0x03, COMMANDS),
        ConditionalField(ThreeBytesField("addr", 0),
                         lambda p: p.cmd in p.CMD_HAS_ADDR),
        ConditionalField(ByteField("dummy", 0),
                         lambda p: p.cmd in p.CMD_HAS_DUMMY),
    ]

CSV 拆成 {mosi, miso} 事务后,MOSI 交给 SPIFlashCmd;头部长度直接标出 MISO 有效载荷起点。0x05 状态字节用 FlagsField 解成 BUSY / WEL / BP0…,比魔法数字好读得多。

附带发现:开机在擦写 Flash

流量并不只有读:启动中段出现 两千多次 PAGE_PROGRAM和一次BLOCK_ERASE(0xD8)。把重复的RDSR1轮询折叠后,顺序和手册一致:空闲 →WREN→ 擦0x730000→ 轮询BUSY约 21 万次 → 再WREN→ 每次 32 字节写满该 64 KiB 块(约 400 ms)——典型配置区重写。下文重建只回放READ;若要看写入内容,数据在 MOSI 的Raw 载荷里。

从 READ 重建镜像

16 MiB 缓冲先填 0xFF(擦除态 NOR 默认),把每笔 READ 画到对应地址并记覆盖率:

def reconstruct(path, flash_size=0x1000000, fill=0xFF):
    image = bytearray([fill]) * flash_size
    coverage = bytearray(flash_size)
    for tx in parse_transactions(path):
        pkt, resp = transaction_to_packets(tx)
        if not isinstance(resp, SPIFlashReadResp):
            continue
        addr = pkt[SPIFlashCmd].addr
        data = resp.data
        end = min(addr + len(data), flash_size)
        image[addr:end] = data[:end - addr]
        coverage[addr:end] = b"\x01" * (end - addr)
    return bytes(image), coverage

单线开机抓包能拼出 bootloader + kernel(binwalk 可见 gzip/LZMA、DRAM 初始化字符串),但 没有 rootfs。常见解释:抓包窗口不够长,或控制器已经 切到 QSPI。

QSPI:rootfs 为什么丢了?

Quad 模式下,原来的 /WP、/HOLD 变成 IO2/IO3;命令/地址/数据可走 1~4 线。判据:这两根脚突然开始跑数据,时钟从约 16 MHz → 50 MHz(四线合计吞吐大约 12 倍)。

四线读:opcode/地址仍可走单线,数据阶段占满 IO0–IO3

切换前:标准 SPI,时钟大约 16 MHz

切换后:同一时间尺度下边沿密了约三倍,再乘四线,吞吐大约一个数量级

切换后读命令是 0x6B Fast Read Quad Output(1-1-4)。Saleae 自带 SPI 分析器不懂 QSPI——要挂 QSPI 插件,并把抓包拆成「单线段 + 四线段」。导出表结构不同(command/address/dummy/data + Lines Used),但逻辑事务仍可落进同一套 SPIFlashCmd / reconstruct——Scapy 建的是事务模型,不是线数模型。

只跑四线重建约覆盖 67%,能看到 SquashFS @ 0x2D0000 和 JFFS2 @ 0xDE0000,而 0x2D0000 以下仍是 0xFF(boot/kernel 在切换前用单线读完了)。把单线镜像与四线镜像合并(dd 或 spidump --merge)后 binwalk 齐活;再 unsquashfs 就是正常 Linux 根目录(bin、etc、web…)。

开源工具:wrongbaud/spidump。合并时注意丢掉「用错线宽解出来的脏读」——例如单线分析器误解的四线事务,否则覆盖率和哈希都会漂。

实战里还可以交叉验证:对重建镜像跑 binwalk -e、对比已知 OTA(若有)、或把 SquashFS 里的版本字符串、证书、默认口令清单拉出来做资产盘点。单线阶段若已看到 PAGE_PROGRAM 轨迹,把 MOSI 载荷另存一份「启动期写入日志」,往往比静态固件更能解释「设备为什么每次开机都要改那 64 KiB」。

信号完整性提醒:文中已注明夹子与飞线在高速下容易出问题。若解码偶发错位,优先缩短线长、降低采样前端负载,或在更低时钟的冷启动窗口先抓一轮,再专门抓 QSPI 高速段——两段覆盖合并,比硬扛一次满速长抓更稳。

这条链对评估意味着什么

实操上可以把流程收成四步,方便复用到别的 SOIC8 NOR:

  1. 夹住再上电

    :优先确认 CLK/CS,再辨 DI/DO;亚时钟毛刺先当 SI,不要硬解成协议。

  2. 导出按 CS 切事务

    :enable→字节→disable 就是一帧;MOSI 喂 Scapy,MISO 按头部长度切片。

  3. 先单线拼 boot/kernel

    :覆盖率不够或 binwalk 缺 rootfs 时,拉长窗口看 /WP、/HOLD 是否突然有数据、时钟是否跳档。

  4. 四线另开分析会话再 merge

    :同一套 reconstruct,只是输入多了一份 QSPI 导出。

对防守与供应链审计:若产线/现场只能「只读探测」、不能破坏样品,这种 clip + 重建比热风拆片更可控;同时也提醒——「焊盘没露 UART 就安全」并不成立,总线侧仍可能在开机窗口把整片布局读给你看。配置区若在启动时整块擦写,抓包轨迹本身就是一条高价值行为指纹(谁在写、写哪、写多久)。

同一套字段模型以后还能挂到 TPM、显示屏、私有传感器帧;作者也预告了 I2C 方向的 Scapy 插件。对公众号读者更实用的点是:把「协议层」从网线搬到飞线,工具链不用换脑子。

作者:Matthew Alt / VoidStar Security


免责声明:

本人所有文章均为技术分享,均用于防御为目的的记录,请勿用于其他用途,否则后果自负。

更多 IoT / 车联网 / 机器人 / AI 安全资料在星球里,扫码进「车联网攻防日记」。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。

本文转载自:赛博安全攻防日记 黑卷 黑卷《不焊拆芯片也能抽固件:用 Scapy 从 SPI/QSPI 抓包重哈镜像》

评论:0   参与:  0