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
2
3
4
5
6
7
8
9
10
11
12
13
14
           Host (ZCU102 / Linux)
│ PCIe Stream Channel ← 主机把 GPU 指令经 PCIe 发下来
↓
┌──────────────────────┐
│ 1. PCIe Interface │ ← 接入链路:XDMA、流 FIFO、参考时钟
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 2. Monitoring Tool │ ← 旁路:2 个 AC 核 + 2 个 Broadcaster
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 3. GPU Emulation │ ← 扮演 GPU:一块 BRAM 存 GPU 二进制代码
└──────────────────────┘

图里的箭头只有一条主线,那是因为匹配结果不走这条线:它由监测模块经另一条流通道直接回传主机,不混在数据流里。三大块各自实例化了一组 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
2
3
Data from Host
├──→ AC Core x2 ──────→ Match Score ──→ back to Host (复制一份送去匹配)
└──→ GPU Emulation (BRAM) (原件照常往下走)

分成两条之后谁也不挡谁,所以被监测的程序既不会被拖慢,也感知不到旁边有人在看。这就是它敢说零运行开销的依据。

这里有一个和后面评价直接相关的站位问题,值得单独指出来:监测器并不是插在链路的中间,而是住在端点内部。它和扮演 GPU 的那一部分共享同一块 FPGA,因此它看到的不是链路上的信号,而是已经进到端点的数据。前面 PCIeHardwareMitM 那篇是把自己插进两端之间当中间人,两者的位置差别很大。

🔍 监测引擎:把 Aho-Corasick 装进流水线

平台交出来的是一条被复制的数据流,接下来要让引擎认识它。

选 Aho-Corasick 的理由很直接。常规做法是拿每条规则去扫一遍数据,扫 N 条规则就要过 N 遍;Aho-Corasick 把所有模式编成一棵树,一趟扫描就能同时匹配全部模式。它正是 SNORT 这类 IDS 的底座。

论文做的主要改造是换输入类型:原版 Aho-Corasick 处理的是字符流,这里要处理的是 GPU 指令流——每一个字符换成一条指令的操作码。

1
2
3
4
5
6
7
8
9
10
11
Input Data (GPU instruction stream)
↓
┌────────────────────┐
│ Pipeline Stage 1 │ ← 高层流水级 1(内部再拆成两级)
└─────────┬──────────┘
↓
┌────────────────────┐
│ Pipeline Stage 2 │ ← 高层流水级 2(内部再拆成两级)
└─────────┬──────────┘
↓
Match Score ────→ back to Host

几个工程细节值得单独拎出来:

  • 流水线分两级,每级内部再拆两级。 原文只说每级内部拆成两个低层流水级,图里画到的就是 Stage 1 / Stage 2 这一层。流水线就像工厂的传送带:把处理拆成几段,不同数据同时处在不同段上,节拍固定,所以吞吐是恒定的。
  • 规则存在核内的 local memory 里,由低层的一个有限状态机(只有有限几个状态、按输入在这些状态之间跳转的逻辑)沿着这棵树走,每进来一条指令就往前推一格。这棵树是按全部模式的公共前缀拼出来的状态图,所以走到哪个位置,就代表已经匹配上了哪一段前缀。
  • 输出的是一个分数,表示匹配上了多少条恶意模式。注意是几条,不是哪几条。
  • IP 核可以按 PCIe 流通道数复制多份并行跑(流通道就是一路独立的流数据)。论文测的就是 1 份和 2 份两种情况——这个数字在后面会决定它到底比 CPU 快还是慢。

🎯 规则从哪来:从矿工汇编里挖指令序列

引擎本身是通用的,它能查出什么,完全取决于喂给它的规则。这一节看规则是怎么从真实矿工里挖出来的,整条链是:

1
2
3
4
5
6
7
8
9
nvcc 10.4 编译(目标 SM30 / Kepler)
↓
cuobjdump 反汇编,得到 GPU 汇编
↓
转成十六进制
↓
只保留操作码(把操作数全丢掉)
↓
统计 + 序列模式挖掘 → 规则

每一步都有理由:

  • 为什么用 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 瓦。

三个可以带走的要点:

  1. 可复用的是骨架,不是规则。 Broadcaster 旁路加 XDMA 接入这套组合,换掉中间的监控逻辑就能变成记录器、包头解析器或其它检测器;
  2. 它的强项和软肋是同一件事:只看指令的操作码。 好处是跨程序通用、不需要看内容,坏处是规则可被无关指令切碎、窗口绑死在编译目标架构、假阳性还没判据——8 个良性程序里有 4 个命中规则,最弱的 LOP → LOP → LOP 一条就中了 4 个;
  3. 引用它的数字要连着口径一起引。 1.55× 是同一个 Aho-Corasick 算法在 100 MHz FPGA 与 4.3 GHz CPU 之间的对比,543× 里绝大部分是平台功耗之差,两者都不是端到端系统开销。

再把视线拉远一点:同一条 PCIe 链路,攻击方可以用它偷模型,防御方也可以用它查异常。 两边用的都是同一个动作,在链路中间或端点里插一个东西、把流量复制一份,差别只在拿走之后拿去做什么,以及站在链路的哪一侧。