PCIeMonitor 精读:把规则匹配搬进 PCIe 链路,在硬件上查挖矿
把监测器做成 PCIe 端点里的一个旁听者:主机发往 GPU 的指令流被硬件复制一份,交给流水线化的多模式匹配引擎逐条扫过,命中挖矿程序的指令序列就报警。规则是数据挖掘从真实矿工的 GPU 汇编里挖出来的操作码序列,整套监测不占被监测程序的执行时间,代价是 2.3 瓦。
论文:PCIe Monitoring for Secure Code Execution in Heterogeneous System Architectures(2025 IEEE International Conference on Cyber Security and Resilience, CSR)
作者:Iasonas Georgakas、Eva Papadogiannaki、Konstantinos Georgopoulos、Sotiris Ioannidis(Technical University of Crete)
前面几篇讲的都是同一件事的两面:如何在 PCIe 链路上把数据拿走。这篇换了个立场——同一条链路,反过来也是一个检测点。
但它的站位和前面几篇都不一样,这一点从一开始就要分清。Hermes 用的是买来的商用协议分析仪,PCIeHardwareMitM 是把一块 FPGA 插进链路中间当中间人,而这篇把监测模块直接做进端点的 FPGA 里,作为端点自己的一部分。读完能带走两样东西:一套可以照着搭的端点侧旁路骨架,以及一份关于这套监测到底能查得多准的账。
📚 基本概念速读
| 名称 | 定义 | 记忆点 |
|---|---|---|
| PCIe | CPU 与 GPU、网卡这类外设之间的高速串行总线 | 显卡、SSD 都挂在这条链路上 |
| TLP(Transaction Layer Packet) | PCIe 上真正承载读写请求与数据的包 | 有包头(地址、长度、类型)与载荷;本文并不解析它 |
| Endpoint | PCIe 链路里设备侧的那一端 | 本文用一块 FPGA 板扮演 GPU |
| DMA(Direct Memory Access) | 设备不经 CPU 直接读写主存 | XDMA 里的 DMA 就是这件事 |
| XDMA | Xilinx 的 DMA IP 核,负责 PCIe 上的数据搬运 | 把主机发来的数据接进 FPGA |
| IP core | FPGA 里可复用的现成硬件功能模块 | 本文的监测引擎就是若干个 IP 核拼出来的 |
| AXI Stream / AXI Memory-Mapped | FPGA 内部两种总线接口形态:前者按拍连续送数据、不带地址;后者带地址,可以像读写寄存器那样访问某个位置 | 搬运数据用 Stream,配置寄存器用 Memory-Mapped |
| Broadcaster | 把一个流输入原样复制成多路输出的硬件模块 | 本文旁路监测的关键:一份流量同时送两边 |
| Aho-Corasick | 多模式匹配算法:一趟扫描就能同时找出一组关键词 | 杀毒软件和 IDS 的常用引擎,本文把它搬进硬件 |
| opcode(操作码) | 一条指令里说明做什么的部分,与之相对的是操作数(对哪个寄存器、用什么立即数) | 本文的规则只用操作码,丢掉操作数 |
| MPSoC | 自带处理器核的 FPGA 芯片 | 本文用 ZCU102 当主机 |
| BRAM | FPGA 内部的块状存储 | 本文拿一块 BRAM 充当 GPU 的显存 |
| Kepler / SM30 | NVIDIA 的一代 GPU 架构(compute capability 3.0) | 本文的指令模式就是针对它编译出来的 |
| SPMF / BIDE+ | 一个序列模式挖掘工具集,以及其中的频繁闭合序列模式算法 | 规则从矿工汇编里挖出来的那一步 |
| ILA(Integrated Logic Analyzer) | 在 FPGA 内部抓信号波形的逻辑分析仪 | 用来核对匹配引擎的内部信号 |
| GTX transceiver | FPGA 里负责高速串行收发的硬核 | PCIe 的物理层就靠它,也是这块板子上最大的耗电项 |
| YARA / SNORT | 两个常见的规则匹配工具 | 论文拿它们所在的生态当作对比基线 |
| 假阳性(false positive) | 良性程序被规则命中、误判成恶意 | 本文规则集的主要软肋 |
🎬 起点:一个很实际的运维问题
论文的开场不是学术动机,而是一类真实事故:有人在你不知情的情况下拿你的 GPU 挖加密货币(用显卡反复算哈希来赚币,以下简称挖矿)。微软和思科都报告过这类事件。GPU 被挖矿程序占满,租户的关键任务跑不动,可用性直接受损。
挖矿为什么会在指令层面留下痕迹?因为挖矿的核心是反复做哈希,而哈希内部大量是移位和逻辑运算,不是浮点乘加。所以矿工程序的 GPU 汇编里,这两类指令的密度会偏高。本文整套检测就建立在这个观察上——不看数据内容,只看指令的种类与顺序。
论文对现有手段的评价是三条:
- 软件工具:可以查,但随时可能被恶意用户绕过或关掉;
- TEE(可信执行环境,靠硬件隔离保证执行安全的机制):把代码隔离起来执行,安全性有了,但会给系统加上额外开销;
- 商用 PCIe 分析仪(专门串在链路上抓包的仪器):能看链路,但很贵。
于是论文提了一个折中的思路:不改变主机、也不改变 GPU,只在链路上放一个硬件监测器。形态上就是一个基于规则的入侵检测系统(IDS),只不过规则匹配的不是网络包,而是 GPU 指令。
🧱 平台:两块 FPGA 板演一台 CPU-GPU 机器
上一节给出的是思路,落到实验上先要解决一件事:链路两端不能想当然地存在。论文用两块 Xilinx 的 FPGA 板把主机侧和设备侧都造了出来。
主机侧是 ZCU102(MPSoC 板),上面有 4 个 ARM Cortex-A35 核,跑的是用 Petalinux(Xilinx 给自己家 FPGA 板子定制 Linux 的工具)做出来的嵌入式系统,再装上 Xilinx 官方 GitHub 仓库里的 PCIe 驱动,负责把指令数据发向 PCIe。
设备侧是 KC705(Kintex-7 系列 FPGA),扮演一块 GPU,同时把监测器一起装在里面。这块板的内部结构是整篇文章的核心:
1 | Host (ZCU102 / Linux) |
图里的箭头只有一条主线,那是因为匹配结果不走这条线:它由监测模块经另一条流通道直接回传主机,不混在数据流里。三大块各自实例化了一组 IP 核,分工如下。
| 位置 | 实例化的 IP / 模块 | 作用 |
|---|---|---|
| PCIe Interface | XDMA IP core | 与主机做 PCIe 通信、搬运数据 |
| AXI Stream FIFO | 缓冲流上来的数据 | |
| Utility Buffer | 接 PCIe 参考时钟 | |
| Monitoring Tool | Aho-Corasick core ×2 | 指令模式的匹配引擎 |
| AXI Stream Broadcaster ×2 | 把一路流量复制成两路 | |
| GPU Emulation | Block Memory Generator(BRAM) | 假装 GPU 的显存,存 GPU 二进制代码 |
| AXI Stream DMA engine ×2 | 在 Stream 与 Memory-Mapped 之间转换 | |
| AXI BRAM Controller ×2 | 把数据写进 BRAM | |
| 全局 | AXI Interconnect | 把上面这些模块连起来 |
旁路是怎么实现的,这是全文最值得记住的一处工程细节。 关键在于那 2 个 AXI Stream Broadcaster:它把一路输入原样复制成多路输出。主机的数据流进来之后,经过 Broadcaster 分成两条——
1 | Data from Host |
分成两条之后谁也不挡谁,所以被监测的程序既不会被拖慢,也感知不到旁边有人在看。这就是它敢说零运行开销的依据。
这里有一个和后面评价直接相关的站位问题,值得单独指出来:监测器并不是插在链路的中间,而是住在端点内部。它和扮演 GPU 的那一部分共享同一块 FPGA,因此它看到的不是链路上的信号,而是已经进到端点的数据。前面 PCIeHardwareMitM 那篇是把自己插进两端之间当中间人,两者的位置差别很大。
🔍 监测引擎:把 Aho-Corasick 装进流水线
平台交出来的是一条被复制的数据流,接下来要让引擎认识它。
选 Aho-Corasick 的理由很直接。常规做法是拿每条规则去扫一遍数据,扫 N 条规则就要过 N 遍;Aho-Corasick 把所有模式编成一棵树,一趟扫描就能同时匹配全部模式。它正是 SNORT 这类 IDS 的底座。
论文做的主要改造是换输入类型:原版 Aho-Corasick 处理的是字符流,这里要处理的是 GPU 指令流——每一个字符换成一条指令的操作码。
1 | Input Data (GPU instruction stream) |
几个工程细节值得单独拎出来:
- 流水线分两级,每级内部再拆两级。 原文只说每级内部拆成两个低层流水级,图里画到的就是 Stage 1 / Stage 2 这一层。流水线就像工厂的传送带:把处理拆成几段,不同数据同时处在不同段上,节拍固定,所以吞吐是恒定的。
- 规则存在核内的 local memory 里,由低层的一个有限状态机(只有有限几个状态、按输入在这些状态之间跳转的逻辑)沿着这棵树走,每进来一条指令就往前推一格。这棵树是按全部模式的公共前缀拼出来的状态图,所以走到哪个位置,就代表已经匹配上了哪一段前缀。
- 输出的是一个分数,表示匹配上了多少条恶意模式。注意是几条,不是哪几条。
- IP 核可以按 PCIe 流通道数复制多份并行跑(流通道就是一路独立的流数据)。论文测的就是 1 份和 2 份两种情况——这个数字在后面会决定它到底比 CPU 快还是慢。
🎯 规则从哪来:从矿工汇编里挖指令序列
引擎本身是通用的,它能查出什么,完全取决于喂给它的规则。这一节看规则是怎么从真实矿工里挖出来的,整条链是:
1 | nvcc 10.4 编译(目标 SM30 / Kepler) |
每一步都有理由:
- 为什么用 nvcc 10.4 编译、目标 SM30。Kepler 是当时分布很广的一代架构。这个选择后面会变成一个约束。
- 为什么只保留操作码。操作数里装的是寄存器号和立即数,随程序千变万化,留着就没法跨程序通用;丢掉之后剩下的就只有指令类型,也就是移位、逻辑运算这样的序列。
- 样本怎么选。矿工用了
xmrMiner、yadaGPUminer、一个简单的 CUDA 比特币矿工和一个
Blake2s(一种哈希算法)矿工;对照组从排序、哈希表、遗传算法到 A*
等常规程序里挑,目的是让两边的指令构成有足够差异。反汇编用的是 NVIDIA
自带的
cuobjdump。
统计先给出方向:SHF 和 LOP 这两类指令在常规程序里约占 20%,在矿工里约占 40%——换个好数的说法,常规程序里每 5 条指令约有 1 条属于这两类,矿工里每 5 条约有 2 条。于是挖掘就围绕这两类指令做——用 SPMF(一个序列模式挖掘工具集)配合 BIDE+ 算法,挖频繁闭合序列模式(在一堆序列里反复出现、且不包含在更长的同类模式里的那些序列)。
窗口大小定成 7 条指令,这个数字不是随便选的:Kepler 架构下,指令流里每 7 条指令配一组控制信息(控制码组),所以按 7 条一段来切,最贴近硬件的实际编码方式。挖出来的 8 个模式都是 SHF 与 LOP 的排列组合:
| 编号 | 模式 |
|---|---|
| 1 | SHF → SHF → LOP |
| 2 | SHF → LOP → SHF |
| 3 | SHF → LOP → LOP |
| 4 | LOP → SHF → SHF |
| 5 | LOP → SHF → LOP |
| 6 | LOP → LOP → SHF |
| 7 | LOP → LOP → LOP |
| 8 | LOP → LOP → LOP → LOP |
论文用这 8 条规则去逐条核对每个程序,结果全部写在 Table I 里。表里出现了两个矿工和八个常规程序(另外两个矿工因篇幅没有给数据)。这张表也是全文最值得细看的一张表,因为它同时给出了命中情况和假阳性:
| 程序 | 命中模式数 | 命中的模式 |
|---|---|---|
| xmrMiner | 8 | 全部 |
| yadaGPUMiner | 8 | 全部 |
| Astar | 0 | — |
| Compression | 0 | — |
| hash table | 0 | — |
| Radix sort | 0 | — |
| MergeSort | 1 | LOP → LOP → LOP |
| cumfals | 1 | LOP → LOP → LOP |
| genetic | 2 | LOP → LOP → LOP、LOP → LOP → LOP → LOP |
| Sieve | 5 | SHF → LOP → SHF、SHF → LOP → LOP、LOP → SHF → LOP、LOP → LOP → SHF、LOP → LOP → LOP |
两个矿工八条全中,四个良性程序一条都不中,这些都是好消息。但另外四个良性程序有命中:Sieve 在 8 条规则里中 5 条,genetic 中 2 条,MergeSort 和 cumfals 各中 1 条。而且四个程序共同的命中项是同一条 LOP → LOP → LOP——它是这组规则里最弱的一环。
论文对这张表的交代只有一句:表格也标出哪些模式可能出现在常规程序里、算作假阳性。它没有给出判据,也没有算误报率。 一条都没中的程序和中了 5 条的程序,在这套系统里分别会触发什么,论文没有交代。这一点留到后面细说。
📊 实测:那三个数字是怎么算出来的
规则和引擎都齐了,剩下的问题是它到底准不准、快不快、费不费电。论文把验证和评估分成两件事做。
功能验证用 System ILA(在 FPGA 内部抓信号的逻辑分析仪)核对匹配引擎的关键信号,把仿真结果和板子上的实际结果对照。输入样本里既有不含任何恶意模式的良性例子,也有分别含 1~8 个不同恶意模式的例子,覆盖了各种边界情况。
性能对比的基线是在 CPU 上跑同一套 Aho-Corasick 算法,具体取的是 SNORT 网络入侵检测系统内核里的那一份实现。两台机器是 Intel 第 13 代 i5(4.3 GHz)和 KC705(100 MHz)——主频差了 40 多倍。
两边的功耗是分别测的:CPU 侧用 Linux 的 perf 工具(借助
Intel CPU 自带的能耗计量,需要 root 权限才能读),FPGA 侧用 Xilinx
Vivado 套件估算。测出来的板级功耗拆成四项:
| 功耗项 | 1 个 AC 核 | 2 个 AC 核 |
|---|---|---|
| 整块板合计 | 2.267 W | 2.294 W |
| GTX transceiver | 1.253 W | 1.253 W |
| Hard IP | 0.058 W | 0.058 W |
| 动态功耗 | 0.773 W | 0.800 W |
这张表的读法比数字本身更重要。占比最大的一项是 GTX transceiver,1.253 W,占整块板的 55%——那是 PCIe 物理层收发链路在耗电,跟匹配算法没有任何关系。而两个 AC 核之间的差别只有 0.027 W(动态功耗从 0.773 W 涨到 0.800 W)。换句话说,几乎全部功耗都用在让这块板子接上 PCIe 这件事上,多跑一个匹配引擎几乎不要钱——只多占整板功耗的 1.2%。
把这些数字拼起来就是论文给出的三个结论:
| 指标 | 结果 |
|---|---|
| 执行时间 / 处理吞吐 | FPGA 用 2 个 AC 核时是 CPU 的 1.55× |
| 能耗 | 同等输入下,2 个 AC 核的 FPGA 比 CPU 少 543×;1 个核时少 275× |
| 功耗 | FPGA 2.267 W(1 核)/2.294 W(2 核);CPU 约 30 W |
| 对被监测程序的开销 | 零(纯硬件旁路,不参与主机与设备的执行路径) |
这里也有一处论文自己写得很坦白的细节:只放 1 个 AC 核的时候,FPGA 在延迟和吞吐上都不如 CPU。加上第 2 个核并行跑之后才反超,因为并行把计算时间砍了一半。275× 与 543× 之间正好也是约 2 倍的差距,来源就是这同一次并行。
至于为什么 100 MHz 的 FPGA 能反超 4.3 GHz 的 CPU,论文没有展开。这里有一条推论可以参考(属于我的解释,不是论文结论):两者的对比不是时钟频率的对比。硬件里的 Aho-Corasick 是一条固定级数的流水线,每条输入都在固定的几个时钟内走完,节拍确定;CPU 那边则是逐字节查表加跳转,还要一直应付分支预测失败(CPU 先猜下一条指令该走哪条路,猜错就要清空流水线重来)的代价。单位时钟能吃的输入量差得很远,所以 43 倍的主频差可以被追平甚至反超。
⚠️ 技术缺口:这份评估没说清的地方
上一节给的三个数字都能查到出处,问题在于它们各自覆盖了多少东西。把论文没写到的部分集中列出来,比零散地读更有用。
| 缺口 | 具体是什么 | 为什么重要 |
|---|---|---|
| 假阳性没有判据 | 8 个良性程序有 4 个命中规则,Sieve 命中 8 条中的 5 条。论文只说这些算假阳性,没有给阈值,也没有算误报率 | 一个程序中了 5 条时,按几条报警才算报警?没有这条判据,检测率就无法真正评价 |
| 模式只留操作码,可以被打乱 | 规则是 3~4 条连续指令的排列。攻击者只要在循环里插入若干无关指令,就能把连续窗口切碎 | 论文完全没有讨论对抗鲁棒性 |
| 7 条窗口绑死在 Kepler 上 | 窗口来自 SM30 每 7 条指令一段控制信息的编码方式。换到 Volta、Turing、Ampere,编译产物和编码方式都变了 | 同一套规则换架构就得重挖,跨架构迁移没有验证 |
| 对比口径内部不一致 | 贡献列表里写的是对比 YARA 这类工具,实际基线是 SNORT 里的同一套 Aho-Corasick(YARA 内部同样基于多模式匹配) | 引用 1.55× 这个数字时,要按 SNORT 的口径写 |
| 能效数字的构成 | 543× 的对比里,板子侧最大的一项是 PCIe 物理层,占 55%;两个 AC 核之间只差 0.027 W | 引用时要说明这是平台功耗之差,不是匹配算法效率之比 |
| 零开销缺一个量化对照 | 只声明不占主机与设备的执行路径,没有给出装了监测器与不装监测器的端到端时序对比 | 论证方向是对的,但少了一个具体数字;PCIeHardwareMitM 那篇就量了改包只要一个时钟周期 |
| 站位在端点内部,不是链路中间 | 它看到的是已经进到端点的数据,不是链路上的信号 | 与插在链路中间的中间人不是同一个威胁模型,也不能直接对比 |
| 平台是仿真的 | 主机是 ZCU102 板子,设备侧那块 GPU 其实是一块 BRAM,只用来存 GPU 二进制代码,并不执行 | 论文自己把它列为未来工作 |
| 规则是静态的 | 状态表在部署时就定好了,运行中改规则要增加流通道再做部分重配置(在 FPGA 运行期间只换掉其中一部分电路) | 同样列为未来工作;而且规则集只覆盖挖矿这一类 |
| 没有开源产物 | 正文与参考文献里只有用到的第三方工具链接(4 个矿工仓库、SPMF),没有论文自己的代码或 bitstream(FPGA 的配置文件) | 想复现只能自己从头搭 |
论文列的未来工作里,有一条和链路监测这件事的走向关系最大:用一块 PCIe interposer(转接板)加上可编程逻辑(PL,指 FPGA 上那块可以自由配置成各种电路的区域),插进真实的 CPU 与 GPU 之间,把监测器嵌进去。从两块 FPGA 板互相演,到插进真机,差的正是这一步——而这步也是所有链路侧工作的共同门槛。
🔭 换个位置用:这套骨架还能干什么
前面一直在批判地读,但它验证过的那套硬件骨架本身是干净可用的。把监测逻辑从框里拿掉,剩下的东西对任何想在链路上做观测的人都有参考价值。
一套可以直接照搬的旁路骨架。 论文验证过的组合是:XDMA 接链路,AXI Stream Broadcaster 复制,监控逻辑与下游设备各拿一份。把这个骨架里的监控逻辑换成别的,它的用途就跟着变:
- 换成记录器 → 一个透明的链路记录点;
- 换成包头解析器 → 一个流量画像工具,只读地址、长度与包类型;
- 换成别的匹配规则 → 另一类内容的检测器。
它看的是内容,不是包头。 本文的匹配引擎吃的是 GPU 指令流的内容,不解析 TLP 的地址与包类型。这和读包头做访问模式统计是两条完全不同的路:前者要认出内容,后者只要元数据。能看的东西不重叠,也就意味着两条路可以互相补位。
站位在端点内部,对评测方反而是便利。 从外部插中间人,门槛卡在信号完整性与延迟上(PCIeHardwareMitM 那篇的难点全在这里);而把观测逻辑做进端点自己,或者用 ILA 直接看端点内部信号(论文做功能验证时就是这么干的),是一条门槛低得多的观测路径。要观测的是端点收到了什么,这个位置已经够用。
✅ 总结
在 PCIe 端点上做纯硬件旁路监测,工程上是成立的:把主机发往 GPU 的指令流复制一份交给硬件多模式匹配引擎,就能在不影响被监测程序的前提下查出挖矿程序;而它付出的代价几乎全在平台本身上——2.3 瓦里 55% 是 PCIe 物理层,多跑一个匹配引擎只要 0.027 瓦。
三个可以带走的要点:
- 可复用的是骨架,不是规则。 Broadcaster 旁路加 XDMA 接入这套组合,换掉中间的监控逻辑就能变成记录器、包头解析器或其它检测器;
- 它的强项和软肋是同一件事:只看指令的操作码。 好处是跨程序通用、不需要看内容,坏处是规则可被无关指令切碎、窗口绑死在编译目标架构、假阳性还没判据——8 个良性程序里有 4 个命中规则,最弱的 LOP → LOP → LOP 一条就中了 4 个;
- 引用它的数字要连着口径一起引。 1.55× 是同一个 Aho-Corasick 算法在 100 MHz FPGA 与 4.3 GHz CPU 之间的对比,543× 里绝大部分是平台功耗之差,两者都不是端到端系统开销。
再把视线拉远一点:同一条 PCIe 链路,攻击方可以用它偷模型,防御方也可以用它查异常。 两边用的都是同一个动作,在链路中间或端点里插一个东西、把流量复制一份,差别只在拿走之后拿去做什么,以及站在链路的哪一侧。