PCIeHardwareMitM 精读:在 PCIe 链路上插一块 FPGA,读包、改包、复制包

把一块 FPGA 板插进 PCIe 链路中间、分别扮演链路两端的角色,它就能在不打断通信、不被两端察觉的前提下,把经过的每个包的类型、地址、长度记下来,还能在一个时钟周期里改写包的内容,或者把某个地址上的读请求替换成自己提前存好的副本——整套硬件逻辑占用不到 FPGA 资源的 1%,跑在 250 MHz、16 Gb/s 上。

论文:Toward a hardware man-in-the-middle attack on PCIe bus(Microprocessors and Microsystems 77 (2020) 103198)

作者:Mohamed Amine Khelif、Jordane Lorandel、Olivier Romain(CY Cergy Paris Université,ETIS 实验室)、Matthieu Regnery、Denis Baheux(法国宪兵刑事研究所 IRCGN)、Guillaume Barbu(IDEMIA)

在 PCIe 链路上做观测这件事,已有工作(比如 Hermes)已经证明能从明文流量里还原出整个模型,不过那些实验用的是商用协议分析仪(专门串在链路上抓包分析的仪器),回答的是能看见什么。这篇论文补的是另一半:如果要自己造一个能看见、能改写、还能复制的东西插进链路,它得满足什么条件,代价有多大。顺着这个问题的答案往下读,也能看清一件和后续所有 PCIe 侧信道有关的事——包头从来不加密。

📚 基本概念速读

名称 定义 记忆点
PCIe CPU 与 GPU、NVMe、网卡这类外设之间的高速串行总线 今天的显卡和 SSD 都挂在这条链路上
Lane(通道) PCIe 由若干条双向串行链路组成,每条 4 根线(两组差分线,一发一收) x1、x4、x8、x16 说的就是 lane 的条数
TLP(Transaction Layer Packet) 事务层的包,真正承载读写请求与数据的那一类 有 header(包头,3 或 4 个 DW 长;1 DW 是个 32 位的字),可能带 payload(载荷,真正要传的数据)
DLLP(Data Link Layer Packet) 数据链路层的包,负责流控、电源管理与确认 不承载用户数据
IP core(IP 核) FPGA 里可以拿来即用的现成硬件功能模块 论文的架构就是几块 IP 拼起来的
MitM(Man-in-the-Middle,中间人) 把自己插进两端通信之间,转发之余读写内容 对两端不可见是它的前提,不是附加要求
Endpoint / Root Port PCIe 的两个端点角色:设备侧与主机侧 做中间人要同时扮演这两个角色
Shadow copy(影子拷贝) 把数据另存一份,需要时用这份顶替真实数据 论文用它冻结密码尝试计数器
Interposer(转接板) 插在两个器件之间、把信号引出来的小板子 真机上做物理插入要靠它
SignalTap / ILA FPGA 内部抓信号的逻辑分析仪 论文用它验证中间人的时序

🎯 起点:为了一次能试一百万个密码

这篇论文的出发点不是攻击,而是取证。作者里有两位来自法国宪兵刑事研究所(IRCGN),他们要打交道的是加密手机。

手机的存储可以简化成两块芯片。一块是 SoC(System on Chip,片上系统),也就是那颗主控芯片,处理器、加密引擎、内存控制器都集成在里面;另一块是 NAND 闪存,用户数据、照片、聊天记录都写在它上面,可以把它理解成手机里的硬盘。两块芯片之间由一条 PCIe 链路连着——这正是这套方案要找的插入点。

iPhone 这类设备把两样东西看得很紧。一样是用户数据,由 SoC 里的加密引擎加密之后才落到 NAND 上;另一样是密码尝试计数器,通常存两份,一份在 SoC 内部,一份在一块独立的小存储芯片(EEPROM,断电也不丢数据的那类)里,两份对不上手机就自动锁死。按下几次错误密码之后,手机会把密钥材料一起抹掉,数据就再也取不出来了。

论文把正常情况和自己的方案摆在一起做了对比(§2.3):

尝试次数 正常情况的耗时 这套方案的耗时
4 次 320 ms 320 ms
5 次 1 分钟 400 ms
6 次 6 分钟 480 ms
8 次 36 分钟 640 ms
9 次 1 小时 36 分 720 ms
10 次 25 小时 36 分 800 ms
11 次 数据已被抹掉 880 ms
1,000,000 次 不可能 约 22 小时 5 分

扛住一百万次尝试,差不多等于把 6 位数字密码的整个空间跑一遍。做法并不复杂:把计数器冻住。中间人记住计数器的初值,之后主机每次来读,它就把初值重新递回去。主机那边看到计数器一直没变,也就不解锁、不抹数据。

这里有个关键点值得先记住:做这件事不需要破解任何加密。数据是不是密文无所谓,中间人只认地址——哪个地址被读了、读了多少次,这就足够把计数器找出来。

🧩 中间人能插在哪,为什么两端不容易发现

PCIe 是一条点对点的串行链路,两端直接相连,中间本来没有一条可供搭线的共享总线,这一点和早期的并行 PCI 不同。要插进去,就得物理上把链路断开,中间接一块板子,由这块板子分别扮演两端的角色。

1
2
3
4
5
6
7
8
原来的连接
SoC / CPU ──────────────────── NAND / NVMe
(一条 PCIe 链路)

插了中间人之后
SoC / CPU ───── FPGA 板 ────── NAND / NVMe
↑ ↑
装作内存 装作主机

板子要同时装成两个样子:对主机一侧,它就是那块 NAND;对 NAND 一侧,它就是主机。这样它才既能转发、又能顺手读写。

图里的 NAND 是手机上的存储芯片;换到 PC 上,这个位置一般是 NVMe 固态硬盘(NVMe 是跑在 PCIe 上的硬盘协议)。两端挂的是什么设备不影响做法,因为中间人利用的是链路本身。

论文给这套架构列了五条硬要求(§3.1):

  • PCIe 3.0,传输率 8 GT/s(每秒 80 亿次信号跳变),单 lane 0.985 GB/s;
  • 响应时间不超过 1 ms;
  • 物理接口保持信号完整性(插进去的板子不能让信号波形变形),否则链路直接降速甚至断开;
  • 对通信双方不可见,不能触发任何安全机制;
  • 能访问经过的包。

第三条和第四条是真正卡人的地方。PCIe 是高速串行链路,板子插进去时自己引入的延迟必须小到让两端察觉不出来,论文反复讲的不可见就是指这件事。注意这里的不可见说的是不打断通信、不触发安全机制,而不是电学上查不到——作者后来在真机上干脆直接拿探头去量信号了。

🛠️ 中间人怎么搭:四块 IP 拼出来

论文没有直接在手机上动手,而是先做了一个 PC 规模的仿真器:用一台 PC 假装手机 SoC,用 FPGA 上的一个 IP(可以理解成一块在 FPGA 里现成可用的硬件功能模块)假装 NAND。这样验证成本低,调试也方便。

1
2
3
4
5
6
7
8
9
10
11
12
┌──────────────────────────────────────────┐
│ Host PC (plays the role of SoC) │ ← 主机,扮演手机 SoC
└──────────────────────────────────────────┘
│ PCIe 3.0 x2
↓
┌──────────────────────────────────────────┐
│ ST-Avalon IP │ ← 物理接口:只处理链路层
│ │
│ 1. MitM IP │ ← 核心:分析 / 过滤 / 改包
│ 2. Xillybus IP │ ← 假装成内存,自带驱动
│ 3. NIOS II softcore │ ← 把结果报给攻击者的电脑
└──────────────────────────────────────────┘

四块的分工是这样的:

  • ST-Avalon:Intel 提供的 PCIe 接口 IP(底层基于 PIPE),负责和真实链路打交道。各种 DLLP 它自己就处理掉了,只把含有数据的 TLP 往上交;
  • MitM IP:论文自己写的核心。它夹在 ST-Avalon 和 Xillybus 之间,所以经过的每一个包都得从它手里过;
  • Xillybus:商业 IP,能把 FPGA 变成一个主机可以访问的外设。论文拿它来扮演那块要被读取的内存,它还自带驱动,省掉了自己写驱动的功夫;
  • NIOS II:Stratix V 这块板子上没有现成的处理器,于是用 FPGA 的逻辑资源搭了一个软核(softcore)进去,用来把分析结果送到攻击者的电脑,以及在几种攻击模式之间切换(切换不用重新综合 FPGA,也就是不必把硬件设计重新编一遍)。

🔍 装上之后能做三件事

论文把能力拆成三个递进的场景,前一个的结果是后一个的输入。

① 分析过滤,先把敏感数据找出来

第一件事是定位。论文从头到尾强调同一个前提:包的内容可能是密文,但包头永远不加密。包头里有包大小、读地址、写地址这些元数据,于是中间人可以统计某个地址被访问了多少次、访问得多频繁,再把这些统计和正在发生的事件对上号,比如用户正在尝试解锁。密码计数器就是这么被找出来的。

1
2
3
4
5
6
7
8
9
Host PC 发起一次 unlock
↓
PCIe 上出现一串 TLP
↓
MitM IP 只读 header
↓ packet type / address / length
统计某地址的访问次数与频率
↓
定位到密码尝试计数器所在的位置

② 在线改包,一个时钟周期就够

找到位置之后就能动手改。实测里,包从到达 MitM 到被改写后转发出去只花了一个时钟周期——芯片是按自己的节拍一跳一跳工作的,一跳就是一个时钟周期;它还会重新生成包的起止标记和有效信号,让改过的包在时序上看起来完全正常。

论文演示的是改 WMR(写内存请求)里的数据段,但同时说明能改的字段不止数据——包类型、写地址这些一样可以改。

③ 影子拷贝,把读请求截下来

第三个能力是为冻结计数器准备的。中间人把主机写往内存的数据全部复制一份存进自己的 shadow memory;之后主机每次来读那个地址,它就不再转发真实数据,而是把自己存的那一份递回去。

1
2
3
4
5
6
正常写:Host ── WMR ──→ Memory        MitM 顺手存一份进 shadow memory
正常读:Host ←── Cpl ─── Memory 读回来的是当前真实值

影子拷贝开启后:
Host ── RMR ──→ MitM 读请求被截住,不再往下走
Host ←── Cpl ─── MitM 递回来的是 shadow memory 里的初值

结果就是主机以为计数器一直没变,实际上它早被改过很多次了。

(上面两张图里的缩写:WMR = 写内存请求,RMR = 读内存请求,Cpl = 内存把数据送回来的完成包。)

📊 实测:代价有多小

论文用 Xillybus 的 StreamWrite / StreamRead 驱动发随机数据(payload 从 1 DW 到 1024 DW),把三种模式的资源占用和时序测了一遍。测试平台是一块 Intel Stratix V DSP 开发板,带 PCIe 3.0 x8 的 endpoint 接口、1 GB DDR3、939k 寄存器与 622k 逻辑单元(ALUT——FPGA 里实现逻辑的基本单元,下面表里的占比都按它算)。

模式 ALUT 寄存器 频率 延迟 吞吐
Logger(只记录与过滤) 25(< 1%) 137 250 MHz 1 cycle 16 Gb/s
MitM(可改包) 56(< 1%) 138 250 MHz 1 cycle 16 Gb/s
Shadow Copy 187(< 1%) 276 250 MHz 9 cycles 16 Gb/s

几点值得留意:

  • 资源占用都在 1% 以下。论文提到,省下来的空间可以拿去做更复杂的实时处理,比如完整的协议解码;
  • 延迟小得惊人。改包只花 1 个时钟周期,影子拷贝只要 9 个周期,而这 9 个周期的来源很具体:要把 64 bit 的数据流用两个 FIFO(先进先出的数据缓冲队列)转成 32 bit 输出,好和 Xillybus 对接;
  • 影子拷贝的测试零错误、不丢数据,一路做到 PCIe gen3 x2;
  • 论文还测了不同 PCIe 配置下的资源占用(Gen1 x1 到 Gen3 x2),MitM 加上 ST-Avalon 一共用掉 601 到 821 个 ALUT,同样都在 1% 以下。

还有一条细节挺有意思:中间人的解码与记录比 Xillybus 自己还快 4 个时钟周期。也就是说,在真正的内存开始处理之前,它早就把数据存好了。

⚠️ 这篇论文的边界

读的时候要把已经做到的和打算做的分清楚,否则很容易高估它。

已经做到的:在一个 PC 加 FPGA 的仿真环境里,把 PCIe 3.0 上的中间人跑通了,三种场景都有实测数据,资源开销与延迟都量了出来。

还只是计划中的(§5 用 currently working on 开头):

  • 真机上一个都没跑。作者的下一步是逆向智能手机主板,找到 SoC 与 NAND 之间最合适的插入位置;
  • 真机上做插入要自制 interposer(转接板)。他们用有源差分探头(夹在信号线上把波形读出来的一种测量探头)测到了真实手机的时钟与数据信号,但论文说这些信号需要用更高频的探头来改进;
  • 需要换成双口 PCIe 的 FPGA 板(Intel Arria 10,两个 gen3.0 x4 口,一个 endpoint、一个 root port),这样才能一侧接手机主板、一侧接内存。现在的板子只有一个 endpoint 口;
  • 如果目标手机的存储走 NVMe,还要加一层 NVMe 解码 overlay(叠在现有处理之上、专门认 NVMe 包格式的一层逻辑),论文说这会抬高延迟;
  • 对策研究没做。论文最后只说之后会研究。

还有几处口径要在引用时注意:

位置 要注意的地方
它是什么 这是一篇期刊论文(Microprocessors and Microsystems),不是安全四大那类会议论文;作者来自大学与法国宪兵取证机构,目标是验证取证工具的可行性
PCIe 版本 只做到 3.0(8 GT/s)。今天 GPU、NVMe 常见的 PCIe 4.0 / 5.0 速率是它的 2 到 4 倍,这套设计能否直接沿用要重新评估
只到 x2 实测跑的是 gen3 x2 这个很窄的配置(论文说要贴近手机的接口),不是 x8 或 x16
不可见的含义 论文说的不可见指不打断通信、不触发安全机制,不是电学上无法察觉——作者自己就在真机上用探头量信号
加密无关的边界 论文明确的是包头不加密、可用来做访问模式统计;它并没有演示在密文通信里读出内容,只有定位

✅ 总结

把一块 FPGA 插进 PCIe 链路中间、扮演两端、转发之余读写经过的包,这件事在工程上是成立的:资源占用不到 1%,改包只花一个时钟周期,而且两端察觉不到。

三个可以带走的要点:

  1. 中间人的门槛在延迟与信号完整性,不在算力。论文反复强调不可见性,落实下来就是几项很具体的指标:8 GT/s、响应不超过 1 ms、每个包只许增加极少量的时钟周期;
  2. 包头不加密,这一条撑起了整个观测面。即使 payload 是密文,凭包头里的类型、地址、长度就能统计出访问模式,进而定位敏感数据;
  3. 它的价值在于把观测点做实。Hermes 那类工作证明的是链路上能拿到什么,这篇证明的是——用什么代价可以把观测点放进链路里,而且放进去不被发现。

从能看见什么,到能不能插进去还不被发现,这篇补的是后半句。