近年来,通用计算领域对GPU的使用稳步增加。随着在GPU上执行像AES这样的安全关键计算变得越来越普遍,相应的审查也必须增强。同时,新技术如WebGPU使得每个Web浏览器都能轻松访问计算着色器。研究表明,GPU缓存容易受到与CPU相同的基于驱逐的攻击,例如来自本地代码的Prime+Probe。
本研究提出了从浏览器内部—即受限的WebGPU环境中发起GPU缓存侧信道攻击。通过利用新的WebGPU标准的特性创建着色器,可以实现缓存侧信道攻击所需的所有构建块,例如用于区分L2缓存命中与未命中技术。此外,还利用现代GPU的大规模并行性设计了第一个并行化的驱逐集构建算法。攻击不需要用户交互,并且在浏览互联网时很容易进行驱动攻击的时间范围内进行。
本文展示了三个案例研究:首先是一个具有高F1分数(在NVIDIA上即82%到98%)的按键计时攻击。其次,展示了对基于GPU的AES加密服务的一种通用、无需特定集合的端到端攻击,能在6分钟内泄露完整的AES密钥。第三,评估了一个本地到浏览器的数据泄露场景,使用Prime+Probe隐蔽信道实现了高达10.9 kB/s的传输速率。
0x01 简介
在过去的几十年中,图形处理单元 (GPU,Graphics Processing Unit) 经历了重要的演变。虽然它们最初是为图形渲染的特定目的而设计的,但大多数现代独立 GPU 都提供了通用计算的可能性。随着 NVIDIA 的 CUDA 于 2007 年和 OpenCL 于 2009 年的推出,GPU 已成为工作负载的常用工具,这些工作负载受益于它们可以提供的大规模并行性。虽然与最近的 CPU 相比,单个执行速度仍然很慢,但当前一代显卡提供了数千个核心,从而大大提高了可并行操作的性能。
通用 GPU 计算的用例越来越多,包括秘密信息计算,如神经网络或加密应用程序。因此,GPU 已成为侧信道攻击的经常性目标。此外,攻击者还可能利用 GPU 攻击其他系统组件。浏览器已成为一个有趣的攻击媒介,因为用户经常在浏览器中在其设备上运行不受信任的第三方代码。由于 GPU 计算也可以为网站内的计算提供优势,因此浏览器供应商决定通过 WebGL 和即将推出的 WebGPU 标准等 API 将 GPU 暴露给 JavaScript。
WebGPU 不仅可用于桌面浏览器,Chrome Canary 117 版本中也已在移动设备上得到部分支持。作为基于 Web 的通用 GPU 交互的未来标准,WebGPU 旨在为性能和安全性奠定坚实的基础。该标准已经针对时序侧信道提供了明确的缓解措施,例如禁用计时器访问(使其成为受信任的功能),并模仿 JavaScript 针对恶意使用 SharedArrayBuffer 的缓解措施。
目前,基于浏览器的 GPU 缓存侧信道攻击的可行性(针对在本机代码或其他浏览器窗口中运行的目标)以及即将推出的 WebGPU 标准的攻击可能性尚未得到证实。考虑到浏览器为攻击者提供的无处不在的攻击面,需要调查以下问题:
1)是否可以使用 WebGPU 等API受限的浏览器环境中发起 GPU 缓存侧信道攻击?
2)攻击是否可以通用到足以在各种 GPU 硬件上实现?
3)攻击者可以在多大程度上利用 GPU 并行化来增强?
本研究设计了浏览器内部的端到端缓存侧信道攻击,利用了新的 WebGPU 标准。尽管 JavaScript 和 WebGPU 环境存在固有限制,但通过构建新的攻击原语,可以使缓存侧信道攻击的有效性与传统的基于 CPU 的攻击相当。攻击是通用的和自动化的,因为2个攻击原语会自动确定攻击所需的 GPU 特定配置参数,即缓存命中/未命中阈值、缓存大小和缓存集数量。攻击适用于各种各样的设备:基本攻击原语适用于来自 5 代不同产品和 2 个供应商(NVIDIA 和 AMD)的 11 个桌面级 GPU。
本文介绍了 3 种攻击,通过 WebGPU 计算着色器利用 JavaScript 在独立 GPU 的 L2 缓存上进行缓存争用的技术。首先,通常由图形渲染引起的大量缓存驱逐可以使攻击者辨别重新渲染的情况。其次,在浏览器中实施模板攻击,旨在监控内存访问模式。最后,介绍了从浏览器执行的对独立 GPU 的第一次 Prime+Probe 攻击。对于所有 3 种攻击,都使用 GPU 的并行性来评估是否可以改进基本攻击。
攻击在 3 种不同的场景中进行评估,这些场景涵盖低频不可重复事件以及可重复和高频事件:按键时序攻击、AES 密钥提取和隐蔽通道建立,这些都是从浏览器(即通过攻击者控制的网站)发起的。攻击不需要用户交互,并且在用户可能在网站上花费的实际时间范围内进行,例如在几分钟内。因此,它们可以很容易地攻击,在浏览互联网时针对任意用户。由于攻击基于 WebGPU,因此它们适用于所有实现 WebGPU 标准的操作系统和浏览器以及广泛的 GPU 设备。
0x02 背景
A. GPU架构
GPU 由多个流式多处理器 (SM,Streaming Multiprocessor) 组成,在 AMD 显卡上称为计算单元 (CU,Compute Unit)。每个 SM 都有其专用的内存子系统,包括共享内存(SM-本地内存)、缓存和功能单元,以并行执行多个线程,在 SIMD 范式下运行。在 GPU 上线程被组织成线程块(在 WebGPU 上也称为工作组),执行时会分配自己的 SM。SM 由多个处理块组成(在最近的 NVIDIA 和 AMD GPU 上为 4 个)。每个处理块都是一个单独的 SIMD 执行单元,具有自己的加载和存储单元,能够并行运行 32 个线程。线程块被划分为纬束 (warp),即在处理块上调度的 32 个线程组。处理块具有 warp 调度程序,即调度 warp 进出处理块的硬件调度程序。当一个 warp 必须等待内存访问或寄存器依赖关系时,warp 调度程序会调度另一个准备执行的 warp,以保持 SIMD 单元忙碌。warp 的不断重新调度可以隐藏延迟,从而更有效地使用处理块。
在 Volta 架构之前,warp 中的所有线程都共享同一个指令单元和一个程序计数器,即指令以锁步方式执行。如果同一个 warp 中的线程发散,它们将被屏蔽,直到它们再次收敛。如果一些线程执行 if 分支,一些线程执行 else 分支,则整个 warp 会执行 if 分支和 else 分支,线程会相应地被屏蔽。 Volta 引入了独立的线程调度,每个线程都有一个程序计数器和调用堆栈,允许处理块交错执行不同的分支。
与 CPU 类似,GPU 使用缓存来减少内存访问的延迟。也就是说,每个 SM 都有一个专用的 L1 缓存,在处理块之间共享,每个处理块都可以访问一个较小的私有 L0 缓存。最后,GPU 在 SM 之间共享一个全局 L2 缓存 (LLC,L2 cache)。上图说明了 Nvidia Turing GPU 的缓存层次结构。然而,需要注意 GPU 缓存的一些相关特性。首先,缓存之间没有一致性协议,保持一致性是开发人员的责任。其次,与 CPU 中经典的 64 字节缓存行不同,GPU 的 LLC 通常有 128 字节缓存行。
B. GPU API
可根据上下文通过不同的 API 调用 GPU。需要区分两个主要的 API 系列:本机 API(例如 OpenGL、Vulkan 和 CUDA)和 Web API(例如 WebGL 和 WebGPU)。
本机 API:与 GPU 交互是通过专用的本机 API,它们允许使用 GPU 进行图形渲染或通用计算。OpenGL 于 1992 年推出,用于支持 Linux 和 Apple 平台上的 GPU 辅助渲染,而 Windows 使用 Direct3D 框架。对于通用计算,2008 年发布的 OpenCL 为所有主要 GPU 供应商提供支持并得到广泛使用。2007 年,NVIDIA 发布了 CUDA,这是一种专为 NVIDIA GPU 设计的计算语言。面向消费者和面向企业的 NVIDIA GPU 都支持 CUDA。NVIDIA GPU 的高市场份额和 CUDA 的易用性使其成为目前在 GPU 上进行通用计算最广泛使用的框架。Apple 最近放弃了对 OpenGL 的支持,转而支持 Metal。同样,Vulkan 于 2016 年发布,作为 OpenGL 的现代替代品。虽然这些 API 有差异,但典型的调用包括对纹理映射、光栅化和 GPU 上的内存管理的操作。
Web API:WebGL 是当前基线 JavaScript API,允许访问 GPU 渲染。顾名思义,它最初的设计目标是在浏览器中进行图形渲染。因此它的 API 是有限的,不提供对 GPU 上通用计算的支持。这就是 WebGL 2.0 Compute 计划背后的动机:通过 WebGL 渲染上下文为 Web 带来计算着色器支持。由于新的本机渲染 API 的出现、OpenGL 的重要性逐渐下降以及 WebGL 的已知限制,项目贡献者决定弃用它,转而使用更现代的替代方案,即 WebGPU。
与 WebGL 一样,WebGPU 允许在浏览器中访问 GPU 图形功能。然而,它不仅仅是 OpenGL 的包装器。更重要的是,它旨在实现跨平台并通过 JavaScript 支持现代图形 API,例如 Vulkan、Metal 和 DirectX。与 WebGL 相比,它提供了更简洁的 API、明显更好的性能和更通用的应用程序范围。然而,主流浏览器参与这一过程以及良好的性能预示着未来几年将得到广泛部署。Chrome、Chromium 和 Microsoft Edge 已经在其官方版本中支持 WebGPU,Firefox 在其 Nightly 版本中也支持它。对移动 GPU 的支持也在进行中,最近已在 Android 上部署。
开发人员可以使用 WebGPU 创建渲染管道和管理 GPU 资源。WebGPU 有自己的着色器语言,称为 WebGPU 着色语言 (WGSL,WebGPU Shading Language),用于编写在运行时编译的自定义着色器。虽然 WebGPU 通过本机 API 提供对 GPU 的访问,但出于安全原因,标准的实现可能会限制可用的 GPU 资源,例如内存和运行时。如果不加以限制,大型 WebGPU 工作负载可能会严重影响主机系统的可用性,因为大多数 GPU 一次只允许一个活动着色器。
C. Prime+Probe
在过去的几十年中,研究人员对微架构攻击进行了广泛的研究。Prime+Probe是一种基于缓存的攻击,它通过利用缓存争用泄露缓存集访问来暴露进程的内存访问模式。这种技术对于对目标机器控制有限的攻击者特别有用,因为它的要求很低,不需要共享内存或使用刷新指令直接控制缓存。由于这些假设很弱,它非常适合基于浏览器的攻击,攻击者可以控制网页上的 JavaScript。
假设攻击者可以在与攻击目标相同的处理器上执行代码,则攻击的工作原理如下。首先,攻击者通过用自己的数据填充精心选择的缓存集来准备缓存。然后,他们等待攻击目标进行内存访问。最后,攻击者探测他们的数据以访问与之前相同的缓存集。如果攻击目标访问了攻击者监视的其中一个集合,他们将驱逐攻击者的一些数据,从而导致探测阶段的延迟更长。在隐蔽通道的上下文中,攻击者将同时运行发送方和接收方,并使用缓存集上的争用来构建通道。
0x03 威胁模型
当攻击者针对 WebGPU 时,主要是具有 WebGPU 支持的浏览器,这包括自版本 112 以来的 Chrome、Chromium、Edge 和 Firefox Nightly。基于 Web 浏览器,威胁模型将包括浏览器在处理敏感信息时可能运行的任何场景。由于整个系统通常共享 GPU,因此这可以包括任何渲染的内容(例如网站或应用程序)和通用计算操作。本文的攻击可以以驱动方式进行,只需访问网站一段时间即可。
假设被攻击目标将访问攻击者的页面几分钟,例如,阅读带有恶意 WebGPU 代码的博客。不假设 WebGPU 提供任何硬件计时器接口,为了进一步限制攻击者,假设 WebGPU 不提供工作组内存,仅攻击专用的 NVIDIA 和 AMD GPU。
0x04 WEBGPU原语
要在 WebGPU 中构建高级缓存攻击,需要几个关键原语。第一个是足够精确的计时器,可以可靠地区分缓存命中和未命中。使用此计时器可以检测缓存大小、缓存活动并构建 Prime+Probe 驱逐集。虽然这些原语已经在 CPU 上得到了广泛的研究,但在 GPU 上构建它们会遇到一些困难,尤其是在浏览器中。
A. 无时钟计时
大多数关于 GPU 的先前工作都是本地运行的,并且依靠高精度计时器或相关性能计数器进行测量。但是,WebGPU 采取了明确的措施来阻止时序攻击,例如使时间戳查询成为可选项并限制通过共享缓冲区的交互,类似于浏览器中的 SharedArrayBuffer 缓解措施。着色器语言 WGSL 目前不包含任何计时器,为了展示通用原语,将构建没有 API 提供的计时器的攻击。
对于JavaScript中计时器不精确的问题,需要设置一个共享内存缓冲区并使用专用线程不断增加共享变量。然后另一个线程可以读取此变量并将其值解释为计时器。将这个概念应用于 WebGPU 时,将面临三个挑战:
挑战1 - 线程序列化:不同的计算着色器不能同时运行。因此,同一个着色器需要在一个线程上计数并在另一个线程上执行攻击代码,如下所示。虽然这在 CPU 上很简单,但在同一处理块上调度的 GPU 线程可能会在某些架构上同步运行。这意味着如果同一个 warp 中的线程需要执行不同的指令(warp 发散),它们将按顺序运行,从而妨碍将计数器用作计时器。
挑战2 - 内存一致性:与 CPU 不同,GPU 在内存层次结构中没有自动一致性保证。每个 SM 管理一个专用的内存子系统,因此 SM 可能在其 L1 缓存中包含相同数据的不同副本。通过同步数据来保持一致状态的任务留给了开发人员。因此如果没有一致性,计数线程将在其私有 L1 缓存中增加计时器,从外部无法观察到。
挑战3 - 优化:WGSL 编译器积极优化代码,这样计数 while 循环就可以用最终结果替换,内存访问也可以用寄存器替换。
解决方案:为了实现可移植且低假设攻击的目标,需要采用通用方法。为了解决挑战1,将着色器的工作组大小设置为 1,这可以防止在同一处理块上进行调度。解决方案表明,即使是像禁用共享内存这样的强大安全措施也无法阻止 WGSL 中的计时器。可以使用原子指令解决挑战2和挑战3。首先,它们保证内存访问不会转换为寄存器访问以进行优化。其次,原子指令执行的加载和存储绕过 L1 缓存直接访问 L2 缓存,从而加强一致性。挑战3提出了原子操作无法解决的额外问题,有时编译器会重新排序或删除测量的负载,通过在编译器不知道结果的条件下使用加载的值来防止这种情况。
前图中说明了这种方法的一个最小示例。通过全局调用 ID global_id 选择一个线程作为计时线程,而另一个线程可以执行攻击。为了尽可能少地花时间读取停止变量,计时线程将其大部分时间花在紧密的内部循环中。所有与其他线程交互的内存操作都是通过原子指令完成的。第 12 行中的条件阻止编译器优化加载顺序或消除目标加载。
上表显示了两种技术在11种不同GPU上的命中-未命中分离效果。在大多数显卡上,递增一个局部变量并使用atomicStore进行更新,比使用atomicAdd要快得多,因为后者是一个阻塞操作,需要等待数据被带到执行单元。然而,这种技术在AMD显卡上总体上似乎效果较差,同时在一些Linux配置上也是如此(标记为Lin,与默认的Windows相对)测试表明,这种优化可能取决于多种因素,例如操作系统和驱动程序版本。当然,这种优化仅适用于 L2 缓存中不打算计时的数据访问。下图也突出了这种差异,但证实了 L2 缓存命中和未命中在两种情况下都是可以明显区分的。最后,如果不删除原子操作,计时器原语就无法被阻止,并且其准确性仅受从 L2 缓存加载和存储所需的时间限制。
B. 缓存大小检测
为了证明几乎所有支持 WebGPU 的设备都会受到这些通用攻击的影响,因此要尝试对尽可能少的参数进行硬编码。所有后续部分的一个重要参数是缓存大小。使用标准 LRU并用 10 MB 的大数组填充缓存。缓冲区大小的选择是基于观察,即大多数 GPU 的 L2 缓存都低于 8 MB。在同一个着色器执行中,向前然后向后迭代数组,计算命中次数。如果命中率非常高(> 95%),就逐步将测试大小增加到 40 MB、80 MB 和 100 MB。这使得大多数显卡的测量时间保持较低水平,同时即使对于较大的缓存也能进行准确检测。最后将这个近似大小与已知大小列表中最接近的较大大小进行匹配。
上表中,大多数显卡可以在不到 400 毫秒的时间内可靠地确定缓存大小。在异常值中,NVIDIA RTX 3060 Ti可靠地返回 3 MB 的大小,尽管官方的 L2 缓存大小为 4 MB。一个简单的解释是,两个 NVIDIA RTX 3060 Ti 型号都只有 3 MB 的 L2 缓存。另一种可能性是这些显卡具有不同的映射功能,并且它们的某些部分缓存只有在总 VRAM 分配大得多的情况下才可访问。
C. 驱逐集构建
构建 Prime+Probe 攻击的下一步是找到一组映射到同一缓存集的地址。为了确保这组地址替换缓存集中的所有当前条目,该集合的基数应至少与缓存关联性 W 匹配,将其称为驱逐集。在 CPU 上已经有大量工作来逆向工程从虚拟到物理地址到缓存集的映射。然而,在 GPU 上的缓存集映射可能要复杂得多,测试表明这些哈希函数在新一代 NVIDIA GPU 中有不同。特别是,许多 GPU 需要采用不同的非线性(或线性,但地址范围不同)映射函数,因为它们的缓存和 VRAM 大小不是 2 的幂。此外,AMD 甚至移动 GPU 可能完全遵循不同的方案。
为了遵循通用方法,不应尝试依赖任何已知的映射函数或页面大小。本文设计了一种在 GPU 上计算快速可靠的驱逐集的新方法,实施的基础是组消除方法 (GEM,)。GEM 的目标是找到目标地址的驱逐集(eviction set)。为此,将驱逐目标地址的大量地址 S >> W 划分为 W + 1 个组。由于 W 个地址的完整驱逐集必须包含在 W 个组中的 ≤ W 个的某种组合中,因此可以消除(至少)一个组而不影响驱逐。GEM 尝试从集合 S 中删除 W +1 个组中的每一个,直到找到一个不影响目标驱逐的组。重复此操作,直到 S 中只剩下 W 个地址,形成目标地址的驱逐集。
由于目标是找到所有集合,因此应该尝试一次找到多个驱逐集。与 Prime+Prune+Probe类似,可利用 LRU 的可预测行为来实现这一点。其次,将算法的各部分并行化为多个线程。以下许多常量都是经验确定的值,可在各种 GPU 上运行,而不是最佳值。
并行集合构建:参见上图,当在 GPU(或 CPU)上并行访问许多地址时,它们之间的顺序无法保证。这意味着当一个集合被拆分到不同的线程之间时,不能再期望观察到源自 LRU 的效果。实际上,依赖访问顺序的驱逐测量变得毫无意义。因此,在驱逐集构建算法中添加了一个预处理步骤。
预处理的目标是将最初较大地址 S 的地址集 si = ∣S∣(128 B 中缓存大小的 1.5 倍)分成没有重叠集的存储桶(bucket)。这种分区有助于独立检查每个存储桶中的集合,从而避免线程间干扰。该过程遵循与 GEM 类似的方法,分为两个主要步骤。
从 B = S 开始,第一步涉及从集合中选择一个随机元素作为轴心。这个轴心地址保证存储桶中至少有一个完整的集合,尽管它可能包含更多。第二步包括移除集合的一部分,即一个组,并验证轴心元素(pivot element)是否仍被残差 B 逐出。如果没有观察到逐出,将用另一个组重复迭代。与 GEM 相反,发现消除 1/2W ∣B∣ 而不是 1/W + 1 可以更好地缓解后续步骤 ( 2b ) 中元素的过度移除。
此外,还结合了 GEM 中没有的几种优化。在 B 减小到 3/4si 之前,利用并行性使用 30 个线程同时访问所有集合元素,只在最后测量轴心元素。当 ∣B ∣ < 1/6si 时或每四次迭代时当 ∣B ∣ < 3/4si 时(满足 optCondition,第 12 行),不仅测量轴心元素还测量所有其他元素。除了访问元素之外,测量元素也会产生很大的开销,而且如上所述,在集合仍然未知的情况下,无法并行化与 LRU 相关的观察。但是,这可以实现一个关键的优化:删除所有注册缓存命中的集合元素 (2b)。给定一致的访问顺序和近似于 LRU 的缓存替换策略,B 中的所有持久元素现在都是完整驱逐集的一部分。
此过程可以迭代,直到 B 低于预定阈值。经验评估表明,存储桶大小为 3500(相当于 145-206 个集合)适用于大多数 GPU。完成后,从 S 中减去 B。剩下一个所需大小的存储桶,专门包含驱逐集。S 的残差部分不包括与存储桶重叠的集合。重复前面的步骤可确保最终的存储桶仅由不重叠的驱逐集组成,共同代表几乎所有的缓存集。
并行存储桶筛选:将完整的驱逐集排序到大致相等的存储桶中后,可以开始从中提取单个集合。由于存储桶中的缓存集之间不再重叠,现在可以并行进行测量。例对于 NVIDIA RTX 3080 及其 5 MB 缓存,前面提到的目标存储桶大小会产生大约 16 个存储桶,每个存储桶有 160 个集合,每个集合最多有 24 个地址。这意味着可以在 16 个输入存储桶上启动一个循环,对每个存储桶并行运行以下大致步骤。
首先,再次对每个存储桶 B 中的元素进行洗牌,并选择一个枢轴元素来找到驱逐集。其次,删除 1/2W ∣B∣个 元素组,直到找到一个不影响枢轴驱逐的元素。第三,为了确定驱逐,并行测量所有存储桶中剩余的所有地址的访问延迟。 第四,显示缓存命中的存储桶中的地址也被删除,这样所有剩余的地址仍然在 B 中形成驱逐集 。存储桶以这种方式并行缩小,直到存储桶的大小低于 1000 个元素。此时不是每次循环删除 1/2 W ∣B∣,而是每次循环只删除一个元素。这能够利用 LRU 替换策略的级联驱逐效果:当只删除一个地址时,该地址与 B 中的其他 W 个地址一起形成一个驱逐集,这 W 个地址现在将显示为缓存命中。
实际上,通过删除一个元素在一个大存储桶 B 中找到了整个驱逐集。这能够在修剪存储桶以找到枢轴元素的驱逐集的同时筛选出许多自由集合。继续减小存储桶大小,直到枢轴的完整驱逐集仍然存在,或者某些错误的测量留下了一个不完整集合。此时,将所有无法归属于完整集合的废弃地址重新填充到存储桶中,然后重新从第一步开始。当所有存储桶都为空或长时间未找到新集合时,算法终止。
这种筛选方法非常有效,它找到的集合数量远远多于所选枢轴的数量。例如在NVIDIA RTX 3080 上,可能会在 17 个桶中搜索具有 90 个选定枢轴的驱逐集,但在此过程中筛选出 2465 个集合。1000 个元素的变化代表了快速桶缩小和通过筛选找到大量集合之间的经验权衡。当数字太高时,查找集合的时间将不必要地增加,因为大多数集合以平均 24 个地址开始,但只有当桶中只剩下 16 个地址时才能检测到。当它太低时,许多集合会因删除许多元素而因筛选方法而丢失。结合这些优化,可以在合理的时间范围内将大多数集合映射到 WebGPU 中所有 NVIDIA GPU 的 L2 缓存中,如下表所示。
值得注意的是 NVIDIA RTX 4090,因为巨大的缓存大小带来了其他显卡没有的问题。同样,这两款 AMD 显卡都未能完成这一重要步骤,无法进行进一步攻击,因此不包括在更高级的攻击中。一种可能的解释是,与其他显卡的时间差异会导致更多噪音,因为命中和未命中分布更接近。虽然从基本的时间差异来看,所有的攻击都可以在这些显卡上运行,但只能对这些 GPU 进行临时和时间受限的远程访问,这无法分析潜在的问题。NVIDIA RTX 3060 Ti 也脱颖而出,因为它即使在寻找 2048 个集合时也能始终找到接近 1536 个集合。这与在实验中发现的 3 MB L2 缓存一致。此外,找到的集合百分比随时间而变化,但大多数集合几乎总能在 5 分钟内找到。有了这个额外的原语,攻击者可以实现 Prime+Probe 来构建隐蔽通道或者执行其他缓存攻击,例如 Rowhammer 。
D. 完全缓存驱逐
在测量 GPU 上的缓存命中率时,首先观察到的是某些事件会驱逐相当大一部分缓存。每当屏幕上的元素被重新绘制或帧缓冲区因其他原因刷新时,这都会占用很大一部分缓存。根据 L2 缓存的总大小和正在绘制的内容,这甚至可能会驱逐整个缓存。一方面,这在某些攻击中表现为噪音;绘制事件后发生的每次测量都受到污染。另一方面,这些驱逐是屏幕上活动的指标,因此可以用作用户活动的侧信道。
0x05 键盘输入监控
由于在屏幕上绘制元素会驱逐缓存的大部分数据,因此构建了一种攻击,该攻击通过观察缓存争用来记录按键间隔时间。按键间隔时间包含重要信息,并可能导致密码恢复。攻击设置反映了最普遍的终端用户场景:一台配备单独独立GPU并进行互联网浏览的计算机。
这种攻击方法充分利用了缓存侧信道,通过监控由于绘制屏幕元素而发生的缓存冲突和数据驱逐来推断按键操作。这种技术的关键在于能够识别出缓存中数据加载和清除的模式,这些模式与用户输入行为(如键盘按键)间接相关。通过精细地分析这些模式,攻击者可以重构用户的打字节奏和顺序,从而潜在地恢复出敏感信息,如密码和其他私密数据。
A. 构造
攻击基于以下观察:对于输入的每个字符,文本框都会重新渲染。可以将其测量为一定量的缓存(最多为整个缓存)的驱逐,与渲染区域的大小相关。为了看到这种效果,使用覆盖部分缓存大小的缓冲区并反复测量其命中率。每当看到命中率低于精心选择的阈值(例如 50%)时,都会将时间戳记录为事件。此攻击的时间分辨率取决于攻击着色器完成测量的速度,而这又取决于总缓冲区大小。虽然在某些 GPU 上看到一小部分缓存已经足以观察击键,但对于大多数 GPU 来说,35% 是检测和速度之间的良好权衡。屏幕分辨率、文本框的大小和缩放级别都会影响驱逐的缓存行数量。
记录原始轨迹后,根据两个观察结果进行过滤。首先,非常接近的测量值(<25 毫秒差异)不太可能是单独的击键事件。其次,在 Windows 上,在短暂的打字中断后,光标开始以 530 毫秒的间隔闪烁。过滤这些噪声源可以消除大多数误报。下图显示了以不同速度打字的轨迹。
B. 评估
使用文本框测试监听攻击,并生成了直接从 javascript 注入的输入。下表显示了测试的 GPU 及其 F1 分数和击键间时间误差。在此测试期间没有其他视觉噪音,类似于许多网站的静态登录页面。持续的高召回率表明,大多数显卡上几乎没有错过任何按键。然而即使经过过滤,召回率也表明大多数显卡的平均误报率很低。AMD 再次表现不同。尽管召回率很高,但由于精度低,可以认为这次攻击大多失败或严重退化。
时间分辨率的一个有趣例子是 NVIDIA RTX 4090。由于其 72 MB 的大型 L2 缓存,仅仅测量缓存争用就需要不成比例的大量测量集。这是因为文本框的缓存占用空间不会随着缓存大小而增加。虽然所有其他显卡的采样率都很容易达到 15 毫秒以下,但大缓冲区意味着每次测量都需要超过 200 毫秒,因此使用这种方法进行按键间时间攻击是不切实际的。此外,还观察到在 Windows 上光标闪烁导致的驱逐比打字字符略少。
0x06 AES密钥恢复
从T表(T-table)实现中恢复 AES 密钥已成为评估细粒度侧信道攻击和微架构攻击的基准。T 表是用于加速AES加密算法的一种优化技术。T表主要用于替代AES算法中的SubBytes和MixColumns步骤,通过预计算和存储操作结果来提高处理速度。
自 2007 年以来,AES 已被提议作为通用 GPU 计算的用例。本文采用了集合无关的方法,无需了解缓存集大小或映射。传统的基于集合的策略需要大量缓存集的归档和 T 表访问的映射,本文方法绕过了这个初始步骤,专注于定位与 T 表行一致的地址。
A. 威胁模型
假设攻击者在目标浏览的网页中嵌入了一些恶意 JavaScript,并持续几分钟。被攻击目标运行基于 GPU 的 AES 实现,可以使用选定的明文和密钥对其进行加密查询。攻击者的目的是恢复目标使用的 AES 密钥。这种情况可以在 SFTP 服务器的情况下找到,其中选定的明文和密钥代表下载的文件。为了实施最后一轮攻击,假设攻击者可以访问目标密文,但不能访问明文或密钥。
B. AES 实现
本机加密服务是 AES CUDA 实现,它对所有轮次使用组合 T 表。与最后一轮使用单独表的实现相比,这增加了攻击的难度,因为所有其他轮次都会影响表条目的缓存命中率。由于GPU缓存行宽通常为128B,每个表由256个4字节条目组成,每个表正好适合8条缓存行,总共有32条缓存行填充了表条目。
C. 攻击方法
策略类似于按键监听,涉及分配一个大缓冲区来占用大量缓存部分,执行 AES 加密后使用 Prime+Probe 识别被驱逐的缓冲区偏移量。在具有极简 AES 内核的理想情况下,被驱逐的偏移量将与表或加密的输入和输出相关,这是因为GPU 实现了确定性的 LRU 类驱逐策略。这意味着,当一个完整集合中的一个地址被驱逐时,按照将它们放入集合的相同顺序测量所有其他地址将导致一连串的缓存未命中,因为每次新的访问都会导致未命中,从而覆盖下一个地址。在实践中发现内核加载会引入大量缓存占用,从而导致测量噪声。主要挑战是在这种噪声中辨别表,并分析每个表的缓存行以跟踪它们的访问。使用选定的密钥和特制的明文来推断偏移量和表条目之间的关系。通过这种映射,可以执行传统的最后一轮攻击。
分析T表:初始分析阶段在浏览器中分配一个相当大的数组(最佳大小因型号而异,即使缓存大小相似),确保内核加载和加密驱逐特定偏移量,并模板化 AES 加密的内存访问。使用随机明文,需要期望对表的每个条目的访问随机分布,从而导致集合一致的偏移量的可预测驱逐频率(频率为 0.995)。差分访问模板使用固定密钥和选定的明文,提高了分析的可靠性和效率。
该策略包括使用固定密钥预生成 32 个明文 pi,确保每个明文访问表内除一个缓存行之外的所有缓存行的加密,以及引用明文 pr,该加密访问所有缓存行。比较 pi 和 pr 加密期间的内存访问,可以发现与缓存行 i 一致的偏移量。此过程识别与每个缓存行一致的偏移量,但由于内核加载噪声,某些缓存行可能未被发现。这个精炼的偏移量列表简化了攻击,专注于减少的偏移量子集。
最后一轮:最后一轮攻击的唯一要求是可以在目标加密期间进行测量,并观察密文。给定一组密文和加密过程中对 T 表条目的访问,通过查找过程中未访问的缓存行来删除密钥字节上的可能值。对于加密期间未访问的每个缓存行,可以根据密文值删除所有可能导致上一轮内存访问的最后一轮密钥字节。考虑到 GPU 的缓存行大小,每次未访问缓存行时,都会获得有关上一轮密钥的最多2^4位信息。能够监视的缓存行越多,就越有可能减少上一轮密钥的搜索空间。一旦得到低于2^40个候选,就会切换到对密钥的详尽搜索。
D. 评估
评估专注于基于 CUDA 的目标实现,使得在AMD显卡上进行评估不可行。最终在两款最新显卡上评估攻击:NVIDIA RTX 3060 Ti 和 NVIDIA RTX 3060 Mobile。在 Ubuntu 22.04 和 Chromium 117 上运行了所有实验,但观察到了 Chrome 版本 112 到 115 的一致结果。
上表突出显示了攻击主要步骤的平均持续时间和成功恢复密钥所需的平均加密次数。值得注意的是,两张显卡的结果相似,在 6 分钟内恢复密钥。所需的平均加密分别为 9300 和 9800。跨 GPU 的结果一致性,再加上低标准偏差,凸显了攻击的稳定性和可重复性。
对 T 表进行性能分析平均需要 13 秒。此阶段的变化主要源于性能分析的不一致重复,直到确定最佳缓冲区大小,从而实现足够的驱逐观察。通常单个性能分析会话持续 6 秒,性能分析完成后,可以重新利用同一会话来泄露多个 AES 密钥,从而将攻击持续时间缩短至最后步骤所需的样本收集时间。性能分析通常不会为每个缓存行提供完整的映射。上一轮攻击的测量值和分配时间的差异与可监视的缓存行数量直接相关。平均而言,可以监视 20/32 条缓存行。NVIDIA RTX 3060 Ti 的结果略有不同,偶尔会映射更少的缓存行,导致攻击持续时间延长和标准偏差增加。
0x07 隐蔽信道生成
隐蔽信道是建立在某些共享资源之上的通道,这些资源本身并不是用于数据传输的。这使得攻击者能够在两个本应被隔离或严格监控的领域之间传输数据。因为发送者和接收者合作传输数据,隐蔽信道是评估任何侧信道带宽的一个重要标准。在传统的Prime+Probe缓存隐蔽信道中,发送者通过准备(驱逐)缓存组来传输二进制1,接收者随后可以通过探测(测量)同一组中的自己的行来检测这一信号。
借助可靠的计时器和找到所需驱逐集的方法,现在可以为L2缓存构建一个Prime+Probe缓存隐蔽信道。发送者使用CUDA的C++应用程序。在这种情况下,它是一个没有网络权限但可以访问GPU的恶意应用程序。发送者的目标是通过GPU隐蔽信道泄露敏感数据。接收者运行在用户同时访问的网站上。这可能是一个被注入恶意JavaScript的合法网站,或者是用户以某种方式被引导访问的网站。
A. 构造
用C++和CUDA编写发送者 S,充分利用高分辨率计时器。基于浏览器的接收者 R 则使用JavaScript和WGSL的组合。利用已构建的驱逐集,R 首先开始映射所有缓存组。
设置 - CJAG:由于 R 和 S 都没有各自驱逐集的绝对标签,因此第一步是将共享集从 S 传达给 R。为此,需要实现缓存干扰协议 (CJAG,Cache Jamming Agreement) 的 GPU 友好版本。在 CJAG 中,S 交替干扰一个集合,即连续驱逐它一段时间,并探测该集合稍长一段时间。同时,R 持续探测所有集合,直到检测到被干扰的集合。然后,R 切换到更长的干扰周期,这样 S 就知道该集合已被接收并继续前进。
与 CPU 上的 CJAG 方法不同,不同的着色器不会在 GPU 上同时执行,因此无法同时进行检测和干扰。需要将它们分割成单个调用,而不是使用不断循环进行干扰或检测的着色器。根据驱动程序中断的频率,可能会看到很少相互交互的长着色器执行。
为了使用大量的集合(例如 1024),在 CJAG 中实现的串行传输是不切实际的。相反,通过利用 GPU 固有的并行性来增强 CJAG 框架,使 S 和 R 能够同时干扰和检测所有集合。在这里,复制到着色器和从着色器复制是主要瓶颈,平均需要 3 毫秒。因此,每次着色器调用测量单个集合与 64 个集合之间的时间差异很小。将两者结合起来,在 16 个线程上并行测量集合。
在此阶段,S 还会换出 R 干扰中未检测到的任何集合,从而确保所有集合对双方都完全正常运转。在传达了 1024 个集合的选择后,S 切换到仅对所有集合的一半进行干扰。特定的一半由其缓存集合索引号中的当前位决定。通过这种方式,S 可以通过对每个位干扰不同的 512 个集合,以 log^2 (1024) = 10 步传输所有 1024 个集合的顺序。在传达了所有集合后,就可以开始数据传输了。上表显示传输 1024 个集合需要 14 秒到 28 秒。
传输:在完成集合干扰协议后,传输完全是单向的。将通道设计为其中发送方和接收方与挂钟同步。在原生 C++ 中,这至少提供了 µs 的精度,而在浏览器中,这限制为 100 µs。为数据包选择 5 毫秒的默认传输窗口长度,此长度不仅受计时器的精度限制,还受着色器运行所需时间的限制。为了补偿较长的数据包持续时间,使用 GPU 的并行性在 1024 个集合上同时传输。
虽然 S 的驱逐就像在循环中并行访问许多地址一样简单,但 R 仍然需要测量时间。挑战1意味着需要将每个并行线程分成不同的工作组以防止锁步执行。此外,单个集合始终需要由同一线程测量,因为这些访问之间的顺序对于驱逐策略至关重要。
与 GUI 相关的事件可能会带来不良噪音。同样,操作系统可能会在一段时间内取消 S 的调度。为了减少这种噪音,采用以下策略。首先采用多数投票测量方法,其中在传输窗口内尽可能频繁地测量每个集合。通过计算驱逐和非驱逐,通过多数投票获得结果。其次,以交替顺序访问每个集合内的地址。这确保了从最近使用最多的缓存行到最近使用最少的缓存行的一致读取,从而避免了如果最旧的缓存行被驱逐而发生的级联自驱逐。这能够确定一组中有多少被驱逐,并轻松识别低级噪音。最后使用差分测量方案。一对集合传输 1 位,并且丢弃没有被驱逐或两个集合都被驱逐的测量值。在有效传输中,每对恰好有一组被逐出,从而有效地将传输速率减半,导致总数据包长度为 64 B。因此,原始传输速度由参数固定为默认值 12.8 kB/s。
B. 评估
在 3 个 NVIDIA GPU 上评估隐蔽通道;RTX 2070 SUPER、3060 Ti 和 3080。GTX 1070 和 Quadro P620 的 Pascal 架构不支持 CUDA 发送方使用的所有指令。AMD 显卡不支持 CUDA,由于 AMD 上的集合查找失败,攻击无论如何都不会奏效。GTX 1650 和 GTX 1660 Ti 都支持指令和集合查找,但由于 CUDA 中的干扰检测出现故障,无法可靠地建立通信。
随着传输窗口的缩小,窗口中的平均读取次数会下降,错误率也会增加。因此,在 4 毫秒时,NVIDIA RTX 3080 的真实通道容量会比 5 毫秒时有所下降。由于时钟速度更高,3080 比其他两款显卡支持更快的传输。因此,其最快配置下的平均真实带宽为 10.9 kB/s,BER 为 2.2%。虽然通道不是最优的,但它清楚地证明了使用嵌入在网站中的 WGSL 代码作为隐蔽通道接收器的可行性。
0x08 讨论
支持的设备:本研究主要针对最近的 NVIDIA GPU,导致 AMD 显卡上的结果更差,因为访问权限非常有限。尽管存在这些架构差异,但 WebGPU 显然支持来自浏览器的通用缓存攻击。WebGPU 已经集成到 Android 的 Chrome Canary 中,尽管某些功能尚不可用。一旦实现奇偶校验,基于浏览器的 GPU 攻击的可能性就会显著增加。
限制:使用 Chrome 和 Chromium 版本 112-117 在各种操作系统上评估了PoC。尽管确定了所有设备的功能组合,但 WebGPU 实现仍然不一致,这一点由已由实验证明。相同的代码可能在一个版本中成功,而在另一个版本中意外失败,这可能是由于 WebGPU 的代码编译存在用户无法控制的变化。此外,还观察到 Linux 和 Windows 之间存在显着差异。虽然确切的原因(无论是驱动程序、浏览器还是 WebGPU 的底层框架(例如 Vulkan 与 DirectX))仍不清楚,但根本的时间差异支持这些攻击的可行性。
缓解:本文中的攻击是通用的,仅依赖于一些假设。尽管如此,可以采取措施限制攻击面。正如当前 WebGPU 草案中已经建议的那样,计时器可以设置为可选的、非常粗略的,或者理想情况下完全删除。但是,只要并发线程之间有一致的内存,就可以构造一个计时器。如果一致性机制(例子中是原子操作)被改变,这样的计时器很快就会失效。当然,除非专门重新设计,否则这可能会导致正常工作负载发生故障。
针对驱动攻击场景的最简单、最有效的解决方案是将浏览器中的 GPU 访问视为敏感资源,例如麦克风或摄像头访问,在使用前需要获得许可。对于 WebGL 和 WebGPU,目前情况并非如此(Firefox 114、Chrome 115、Chromium 117)。这还可以防止恶意方偷偷使用本地计算资源进行加密挖掘。
0x09 结论
GPU 已成为一种无处不在的计算资源,因此需要加强安全审查。本文证明可以直接从浏览器内部发起强大的 GPU 缓存侧信道攻击。基本攻击原语是通用的和自动化的,于可以在一组来自 5 代不同产品和 2 家供应商的 11 个桌面 GPU 上进行攻击,这些 GPU 通过 WebGPU 在浏览器中运行。现代 GPU 的大规模并行性可以在并行驱逐集构造算法中得到利用。
本研究分析了三个案例:首先是按键计时攻击,F1 分数在 82% 到 98% 之间,将敏感的用户输入暴露给攻击者。其次是基于 GPU 的 AES 加密不可知端到端攻击,在 6 分钟内泄露了完整的 AES 密钥,表明加密秘密也暴露给了基于浏览器的攻击者。最后是原生浏览器 Prime+ Probe 隐蔽通道,该通道的带宽可以达到平均传输速率高达 10.9 kB/s。GPU 访问应被视为与其他需要明确用户同意的设备和资源类似的安全和隐私风险。