DevIOus 精读:用 GPU 和网卡从 IOMMU 里窥探隔壁设备
设备访问内存要先经 IOMMU 做地址翻译,翻译结果缓存在 IOTLB 里,而这块缓存被同一个 IOMMU 下的所有设备共用。于是攻击者不必跟受害者共享任何内存,只要让自己的 GPU 或网卡反复发 DMA 请求并给时延计时,就能从 IOTLB 的状态变化里看出隔壁设备在干什么——跨 VM 成立,隔着 RDMA 也能用。
论文:DevIOus: Device-Driven Side-Channel Attacks on the IOMMU(IEEE S&P 2023)
作者:Taehun Kim、Hyeongjin Park、Seokmin Lee、Junbeom Hur、Youngjoo Shin(Korea University)、Seunghee Shin(SUNY Binghamton)
CPU 缓存侧信道的前提通常写得很清楚:攻击者与受害者要共享内存、共享某一级 CPU 缓存(比如所有核共用的末级缓存)、共享某个地址空间。这篇论文把前提换掉了——不再从 CPU 发起请求,而是让设备自己发。这一换带来两个后果:攻击者不需要与受害者共享任何内存,而且整条信道不经过 CPU 缓存与 TLB。后面那一点,恰好是它能绕开一大堆现成防御的原因。
📚 基本概念速读
| 名称 | 定义 | 记忆点 |
|---|---|---|
| DMA(Direct Memory Access) | 设备不经 CPU 直接读写主存(也就是内存条) | 全文所有请求都是它 |
| 虚拟地址 / 物理地址 | 程序与设备用的是虚拟地址,真正落到内存条上的位置叫物理地址 | 两者之间的换算就是地址翻译 |
| 页(page) | 内存被切成大小固定的块,翻译和分配都以块为单位 | 常见 4 KB,大页 2 MB |
| TLB | CPU 侧缓存地址翻译结果的地方 | 设备侧的对应物就是 IOTLB |
| 组(set)/ 路(way) | 缓存不是一个大池子:地址先决定落在哪一组,每组只有固定几个位置,那些位置叫路 | 一个组填满后,要按替换策略挤掉旧的 |
| IOMMU | CPU 里管设备 DMA 的硬件,负责把设备的 I/O 地址翻译成物理地址并检查权限 | 本文的攻击面就在它内部 |
| DRHU | IOMMU 里真正做地址翻译的单元 | 每次 DMA 请求都要找它 |
| IOVA | 设备侧使用的虚拟地址 | 每个设备有自己的一份 |
| IOTLB | 存最近的 IOVA 到物理地址映射的缓存 | 翻译一次要查多级页表,太慢,所以要有它 |
| RDMA | 一台机器直接读写另一台机器内存的协议,两端操作系统都不参与 | 把它变成攻击工具以后,攻击半径从本机拉到网络 |
| RNIC | 支持 RDMA 的网卡 | 能精确地一次发一条 DMA 请求,逆向 IOTLB 全靠它 |
| mkey | 保护 RDMA 内存区域的密钥,随请求一起发送 | 让对方网卡能验证请求来源 |
| Eviction set | 一组地址,访问它们能把目标缓存组里的条目全挤掉 | 攻击的抓手,和 CPU 缓存侧信道用的是同一套方法 |
| Prime+Probe | 先用 eviction set 填满目标组,等一会儿,再访问一次并计时 | 不需要攻击者与受害者共享内存 |
| Flush+Reload / Evict+Reload | 先把目标从缓存里清掉或挤掉,等受害者执行,再读一次并计时 | 需要双方能碰到同一块内存 |
| DDIO | 让设备直接读写 CPU 末级缓存的技术 | 前人的同类攻击多数打的是它,这篇打的是别处 |
🧭 起点:让设备来发请求
先交代这篇和 CPU 侧侧信道的根本差别。
CPU 缓存与 TLB 攻击里,挑起事端的是 CPU:攻击者的进程去访问内存,制造缓存冲突。这类攻击有一个共同的前提——攻击者与受害者至少共享某一级存储。而现代云里这个前提常常不成立:每个租户分到独立的 GPU、独立的网卡,通过 PCI-passthrough(把整块物理设备直接划给一台虚拟机)直通进来,内存空间彼此隔离,操作系统层面看不到任何共享。
这篇论文的观察是:即使内存完全隔离,设备之间的地址翻译仍然是共享的。一个 IOMMU 管着一组设备,所有设备的翻译请求都落进同一个 IOTLB。而翻译命中与不命中之间存在 1000 个 CPU 周期量级的时延差,足以测量。
更关键的是攻击者的身份变了。既然请求由设备发出,整个攻击过程不碰 CPU 缓存,也不碰 TLB。论文反复强调这一点,因为它的直接后果是:那些靠 CPU 缓存行为做异常检测的防护,看不见这种攻击。
🔬 先把 IOTLB 拆开看
要攻击一块缓存,得先知道它长什么样。IOTLB 在此之前从没被公开逆向过,所以论文自己动手做了一遍。目标是一颗 Intel Xeon Silver 4316(第三代 Xeon 可扩展处理器)。
观测手段是 Intel 从第三代 Xeon 开始提供的 IOMMU 性能监控(PMON)——CPU 里的一组硬件计数器,可以指定监听哪类事件,同一个事件还能用 umask 这个掩码再细分。其中与 IOTLB 相关的三个事件:
| 事件 | umask | 含义 |
|---|---|---|
| FIRST_LOOKUP | 0x1 | 查 IOTLB 的请求数 |
| MISSES | 0x20 | 未命中后填入新条目的次数 |
| CTX_CACHE_LOOKUP | 0x40 | 未命中后去查上下文缓存(记录每个设备翻译上下文的表)的次数 |
论文选 MISSES 做判据,因为它的口径更严:只有页表里有有效翻译的未命中才会被计入。
而发起访问的工具选的是 RNIC。理由是它能精确地一次只发一条 DMA 请求,这一点对逐条试探 IOTLB 的组织结构至关重要——GPU 做不到这个粒度。
逆向出来的结果如下:
| 属性 | 4 KB 页 | 2 MB 页 |
|---|---|---|
| 组数(sets) | 32 | 16 |
| 路数(ways) | 8 | 8 |
| 映射方式 | 线性(直接拿地址的几位当组号) | 线性(同左) |
| 取地址哪几位当组号(中括号表示取第几位到第几位) | IOVA[16:12] | IOVA[24:21] |
| 替换策略 | LRU | LRU |
这张表里有两条值得单独说。
第一条是大页与小页用的是两套独立的硬件。论文先做了个对照实验:用 4 KB 页把整个 IOTLB 填满,然后拿一个 2 MB 页去挤——一个条目都没被挤掉。这说明两者互不干扰,想让请求之间产生冲突,页大小必须一致。
第二条是替换策略可以被完整确定下来,靠的是前人提出的置换向量方法:准备两个 eviction set,一个用来填满目标组,另一个用来逐条挤;数一数目标条目是在第几次才被挤掉,就能推出它在 LRU 序列里的位置。实验给出的置换向量显示,每个条目的相对位置都被严格维持,IOTLB 用的是完整的 LRU(Least Recently Used,最久没被访问的条目最先被挤出去)。
🎓 先补一课:探测缓存的三种套路
前面说清了 IOTLB 是一块共享缓存,也确认了它的命中与未命中之间有时延差。但有差和能用之间还隔着一步:具体怎么探。这篇论文用的正是业界三种经典套路里的两种,先把它们一次讲明白,后面读到 PP、ER、PPA 这些缩写才不会绊住。
先交代缓存扮演的角色,用图书馆打比方:
- 主存:图书馆的大书库,书最全,但取一次要走很远;
- 缓存:你桌子边的小书架,只能放几本,但伸手就能拿到;
- 命中 / 未命中:要的书在桌上就是命中(快),不在就得去书库翻(慢)。
三种套路建立在同一个前提上:命中快、未命中慢,而这个快慢攻击者能靠计时测出来。区别只在动手前先做什么、最后量什么:
1 | 1. Prime+Probe:先把自己的东西塞满书架,过一会儿再看还在不在 |
① Prime+Probe:查自己的地盘有没有被抢
三步:先用自己的数据把目标那一组缓存填满(Prime),隔一段时间让别人干活(Wait),再回来访问自己那批数据并计时(Probe)。如果变慢了,说明自己填进去的条目被挤出去了——而能挤掉它的,只有这期间用过同一组缓存的程序。
它最大的好处是不需要双方共享任何数据:攻击者不用碰到受害者的内存,只要两者的访问会落进同一个缓存组就够。
② Flush+Reload:查某一本书回没回来
三步:先把目标数据对应的缓存行清掉(Flush),等别人干活(Wait),再读一次同一份数据并计时(Reload)。读得快,说明这期间有人把它重新放回了缓存。
它比 Prime+Probe 更精准——盯的是指定的那份数据,而不是笼统的某一组被用过;代价是要求双方共享同一份数据,最常见的情形是共享库里的代码。
③ Evict+Reload:同上,只是不用专门的清除指令
三步和前一种一样,唯一区别在第一步:不用 CPU 提供的清除指令,而是访问一批会争抢同一组缓存的地址,把目标挤出去。好处是不依赖那条特殊指令(有些平台用不了),代价是要自己找出一批能挤掉目标的地址。
三种放在一起
| Prime+Probe | Flush+Reload | Evict+Reload | |
|---|---|---|---|
| 第一步 | 填满目标缓存组 | 清除指定缓存行 | 靠竞争把目标挤出去 |
| 最后量什么 | 自己那批数据的访问速度 | 目标数据的访问速度 | 目标数据的访问速度 |
| 需要共享目标数据吗 | 不需要 | 通常需要 | 通常需要 |
| 主要看出什么 | 哪一组缓存被人用过 | 指定数据有没有被访问 | 指定数据有没有被访问 |
| 特点 | 适用面广 | 精度高 | 不依赖专门的清除指令 |
回到本文:IOTLB 是设备侧的缓存,上面这三件事照样能做,只是发起访问的从 CPU 换成了 GPU 或网卡。至于这篇具体挑了哪几种、为什么挑它们,下一节说。
如果只记一句:Prime+Probe 看的是自己那批资源有没有被抢走,Flush+Reload 和 Evict+Reload 看的是指定的那份数据有没有被别人放回来。
🛠️ 两个原语:GPU 版与网卡版
有了 IOTLB 的结构参数,攻击就需要一个能持续发起 DMA 请求并计时的抓手。论文做了两套,分别用 GPU 和 RNIC。
GPU 版(DEVIOus_GPU)用 cudaMemcpy()
触发 GPU 到主存的 DMA(CUDA 是 NVIDIA 的 GPU
编程接口,cudaMemcpy 就是它用来在 CPU 与 GPU
之间搬数据的函数)。这里有个细节:CUDA
默认会用临时分配的内存做中转,地址每次都可能变,而攻击需要地址固定,所以必须用
cudaMallocHost() 显式申请固定内存。
而且 GPU 版只能用 Prime+Probe。原因是 GPU 驱动不允许两块内存被不同应用共享,而 Flush+Reload 和 Evict+Reload 都要求攻击者与受害者能碰到同一块内存。
网卡版(DEVIOus_RNIC)走 RDMA,用
ibv_post_send() 和 ibv_poll_cq()
发请求并确认完成。它比 GPU 版多两个好处:一是 RDMA
的内存区域可以在两个客户端之间共享,所以 Evict+Reload
也能用;二是它天然把攻击半径拉到了网络上,后面那两节远程攻击就是用它做的。
🔊 一个技巧:把 1 次未命中放大成 8 次
这部分是全文最精巧的设计,也是远程攻击能成立的关键。
本地测量噪声小,一次未命中带来的延迟差就够用了。远程不一样——隔着 RDMA 网络,时延抖动本身就可能吃掉信号。论文的应对办法不是做统计平滑,而是利用 LRU 的顺序,把信号本身放大。
1 | Prime 之后,一个组里的 8 个条目按 LRU 排好,8 是最老的那个: |
命名上,反序探测的叫 Prime+Probe(PP),正序探测的叫 Prime+Probe with Amplification(PPA)。用 PPA 的结果是:探测一轮能撞上 8 次未命中,而不是 1 次。实测里两者的差距很可观——网卡版 PP 的命中与未命中时延差是 1145 个 CPU 周期,PPA 则达到 6323 个周期。
代价也很实在:放大是拿攻击执行时间换来的,而且正是因为它把噪声也一起放大了,PPA 只在噪声大的远程场景里才划算。本地场景里,GPU 版 PP 的 1642 个周期已经比网卡版的任何一个都大,直接用 PP 就好。
| 原语 | 方法 | 命中时延 | 未命中时延 | 时延差 | 单次执行耗时 |
|---|---|---|---|---|---|
| DEVIOus_GPU | PP | 175,633 | 177,275 | 1,642 | 60.42 µs |
| DEVIOus_RNIC | Evict+Reload | 16,798 | 17,891 | 983 | 52.29 µs |
| DEVIOus_RNIC | PP | 147,196 | 148,341 | 1,145 | 45.43 µs |
| DEVIOus_RNIC | PPA | 152,374 | 158,697 | 6,323 | 47.45 µs |
(单位都是 CPU 周期,最后一列是单次攻击执行时间。ER 与 PP 的时延绝对值差了一个数量级,是因为两者的请求路径不同,有意义的是差值那一列。)
🎯 四个案例
论文用这两个原语做了四个攻击,从本机跨 VM 一路做到远程网络。
案例一:跨 VM 的隐蔽信道(本地)
两台虚拟机各自直通一张 GPU,共享同一个 IOTLB。发送方用 GPU 的 DMA 时序编码比特,接收方用另一张 GPU 做 Prime+Probe。论文报出的结果是:单个 IOTLB 组、时间窗口 250 µs 时吞吐 2.54 Kbps(千比特每秒)、误码率 2.18%;用上 16 个组以后吞吐升到 40.64 Kbps。
案例二:SSH 击键时序(本地跨 VM)
这个场景值得一提,因为它把攻击角度选得很刁。虚拟机 A 里的攻击者用 GPU 探测;虚拟机 B 里跑的是一台 SSH 服务器,网卡直通给它。攻击者不需要任何 SSH 账号,也不需要访问 B 的权限,只要共享 IOMMU 就够。
原理是利用 Linux 网卡的 RX ring buffer——收包描述符排成的环形队列。网卡每收到一个包,就要按描述符里的地址往主存做一次 DMA,写进接收缓冲区。这个 DMA 会查 IOTLB,于是攻击者能观测到。而且这个 ring buffer 是固定的 4 KB 页、用一致(coherent)方式映射(CPU 和设备看到的是同一份数据,硬件自动保持两边同步),所以活动始终落在同一个 IOTLB 组上——论文用 256 轮发包实验确认了这一点,活跃的永远是 0 号组。
剩下的就顺理成章了:反复探测 0 号组,每个包到达就在时间轴上留下一个峰值。论文的评估是:与抓包工具 tcpdump 的时间戳相比,平均差 0.81 ms,而人类击键的平均间隔约 240 ms——不到千分之四。论文据此认为,这种击键时序攻击已经和嗅探式攻击同等准确。
案例三:远程推断键值访问(RDMA 网络)
攻击者在远端 RDMA 客户端上,目标是服务器上跑着 Apache Crail 的键值存储,想推断另一个客户端访问了哪些键值。这里出现了 PPA 真正的用武之地:
| 方法 | 准确率 |
|---|---|
| PP | 即使收集 100 次探测记录(trace)也只有 62.66% |
| PPA | 收集 20 条 trace 就从 89.47% 升到 99.18% |
同一个噪声环境里,PP 怎么加数据都上不去,PPA 用五分之一的数据就接近满分——放大信号的价值就在这里。
案例四:远程网站指纹(RDMA 网络)
攻击者要推断远端用户在访问哪个网站。抓手是现代浏览器默认打开的 GPU 加速:把网页画成像素这一步(光栅化)和把各层画面拼到一起这一步(合成)现在都交给 GPU,而且一有可用元素就立刻送过去,这个过程产生的 DMA 访问模式与网页的内容和形状相关。
时间上也来得及:加载一个网页平均要 600 ms,而用 PPA 扫一遍整个 IOTLB 只要不到 2 ms,所以一次页面加载能采到 300 多条 trace。分类用的是带注意力机制的 Bi-LSTM(双向长短期记忆网络,一种专治时序数据的模型,论文把它叫作 Att-BLSTM),训练数据取自 Alexa 排名的前 300 个网站。
结果是对 Chrome 和 Firefox、在 Windows 与 Linux 上都做了测试,准确率最高 95.2%;即便换成 Tor 浏览器,仍能到 90.6%。论文另外测了用大页的情况:换成 2 MB 大页时准确率降到 89.52%,作者归因于大页在 IOTLB 里承载的信息粒度更粗。
🛡️ 为什么它比 CPU 侧信道难防
论文在防御部分给了一个很直接的判断:针对 CPU 缓存侧信道的那一整套检测手段,对这套攻击全都无效。
原因在前文已经交代过——攻击请求由设备发出,不经过 CPU 缓存,也不经过 TLB。异常检测器盯着的是 CPU 缓存与 TLB 的行为,自然什么也看不到。
论文给的两条出路:
- 异常检测要用对了事件。Intel 从第三代 Xeon 起提供了 IOTLB 的硬件事件,论文点名了四个:FIRST_LOOKUPS(umask 0x1)、4K_HITS(0x4)、2M_HITS(0x8)、MISSES(0x20)。用它们来统计异常模式,论文自评的运行时开销不到 5%;
- IOTLB 分区。既然风险来自共享,就把 IOTLB 按设备域切开来。论文在另一处(针对同类攻击的讨论里)也承认这条路的代价——隔离会限制规模:一台有 8 个满速插槽、却只有 4 块 IOMMU 的主板,配不出 8 个彼此隔离的单卡实例。
⚠️ 这篇的边界
| 位置 | 要注意的地方 |
|---|---|
| 前提条件 | 攻击者设备必须与受害者设备共享同一个 IOMMU。论文的原话是要求同一 root port(PCIe 从 CPU 出来的根端口),比同期一些工作要求共享 PCIe switch(接在 root port 下面、再分叉给多块设备的交换芯片)的假设更宽松 |
| 攻击者要有什么 | 一台能持续发起 DMA 并计时的设备——GPU、RNIC 都可以,或者一块可编程 FPGA。反过来,如果目标应用改用 MMIO(让 CPU 逐次去读写设备寄存器,而不是让设备自己搬数据)而不是 DMA 来驱动设备,这条信道就看不见它 |
| 具体数字都绑在平台上 | 32 组 8 路、LRU、983 到 6323 周期的时延差,全部测自 Intel Xeon Silver 4316 加特定 GPU/网卡组合。换 CPU、换设备都要重新标定 |
| 大页会变粗 | 大页在 IOTLB 里的组数更少(16 组对 32 组),承载的信息粒度更粗——网站指纹的准确率从 95.2% 掉到 89.52% 就是证据 |
| 准确率的口径 | Crail 那组 99.18% 是在攻击者能反复触发受害者请求的前提下得到的(论文明说这在真实 Crail 里未必可行);网站指纹用的是闭集设定,即受害者只在 300 个候选网站里浏览 |
✅ 总结
把发起请求的一方从 CPU 换成设备,IOTLB 就变成了一条不经过 CPU 缓存与 TLB 的共享信道:攻击者不需要与受害者共享任何内存,就能从翻译命中时延的变化里读出隔壁在收包、在访问键值、在渲染网页。
三个可以带走的要点:
- 共享点的清单需要更新。除了缓存和 TLB,设备侧的地址翻译缓存同样跨租户共享,而且它挡在每一条 DMA 路径上;
- 换成设备发起请求,就绕开了整类防御。既不碰 CPU 缓存也不碰 TLB,常用的一整套侧信道异常检测在这条路上失效;
- 噪声大反而催生了一个更聪明的技巧。PPA 不去做平滑,而是利用 LRU 的顺序把一次未命中放大成八次——远程攻击的数据里,PP 加多少数据都停在 62.66%,PPA 用五分之一的数据就到了 99.18%。