重新审视针对 Cloudflare Workers 的远程 Spectre 攻击
在 2021 年,我们评估了 [远程 Spectre 攻击](https://blog.cloudflare.com/spectre-research-with-tu-graz/) 针对 Cloudflare Workers 的影响。根据评估结果,我们推出了一种名为 [动态进程隔离](https://blog.cloudflare.com/spectre-research-with-tu-graz/) (DyPrIs) 的生产防御措施,能够识别恶意看似的脚本并将其隔离到独立的进程中。此后,新的稳定化 Spectre 攻击技术被发现。为了了解这些新技术是否对我们的 Workers 生产环境构成威胁,我们决定重新评估远程 Spectre 攻击。通过在生产环境中构建更新的概念验证,我们能够从实证角度评估在生产工作负载下 Spectre 攻击的风险。
要在生产环境中发起成功的侧信道攻击,外部攻击者必须克服额外障碍,例如共享硬件资源上的活动、中断、上下文切换和粗粒度定时器。我们的研究发现了 DyPrIs 实现中的一个限制,并且我们成功展示了一种远程 Spectre 攻击,在 Cloudflare Workers 的生产环境中以 99% 的准确率可靠地泄露多达 12 bit/s 的数据。基于这项研究,我们改进了 DyPrIs,集成了 V8 沙箱和 [进程内隔离机制](https://blog.cloudflare.com/safe-in-the-sandbox-security-hardening-for-cloudflare-workers/),进一步降低内存泄露攻击的风险。
今天,我们 [发布了一篇论文](https://arxiv.org/pdf/2608.17043),描述了我们的发现,论文由 Albert Pedersen、Haocheng Xiao、Sam Ainsworth、Nigel Topham 和 Martin Schwarzl 联合署名。论文涵盖了 2024 年和 2025 年初的研究。
请注意,由于 Cloudflare Workers Runtime 团队采取的对策,所展示的攻击在生产系统中已经得到缓解。在过去三年中,我们没有发现任何活动利用的迹象。
## Cloudflare Workers 安全模型
Cloudflare Workers 在边缘运行不信任的 JavaScript。利用语言级隔离,采用 V8 隔离,数万名用户可以共享同一个操作系统进程。每个 Worker 都有自己独立的 JavaScript 堆。这一设计使得启动延迟保持在较低水平,相比完全进程隔离,能更有效地运行多个用户。我们在运行时周围有多层防御,例如自动 V8 补丁管道、由 Linux 命名空间和 seccomp 过滤器组成的双层沙箱、Cap’n Proto RPC 以及将某些脚本调度在独立的进程沙箱中的可能性。然而,Worker 进程中的一次任意读取漏洞仍然可能导致跨用户数据泄露。一个难以缓解的漏洞是利用了推测性执行的特性,即进程内的 **Spectre**。
## Spectre
你可以把推测性执行想象成徒步旅行。某个时刻,你来到了一个分叉口,必须预测该往哪个方向走。如果预测正确,你节省了时间,能够享受阳光并在山区小屋喝一杯清凉饮料。然而,如果你推测错了方向,就得返回。小径看似没有人走过,但你的脚印仍然留在泥土中。
CPU 中的推测性执行也是类似的工作方式。分支预测提前对分支的结果进行有根据的猜测,CPU 随后推测性地执行它。如果预测是正确的,推测性执行节省了一些时间。而如果预测错误,CPU 则需要丢弃结果,回退并执行另一条分支。由于这些推测性执行的指令只暂时存在于 CPU 流水线中,并且从未被永久退休或提交,文献将其称为过渡指令,并将概念泛化为过渡执行。
然而,由于过渡执行,微架构状态中仍然留有一些痕迹,例如 CPU 缓存。攻击者可以利用 Spectre 暂时性地访问越界内存,将单个位的信息编码到缓存状态中,并利用重新访问数据的延迟来推断该位是否被设置。
为了 [缓解进程内 Spectre 攻击](https://blog.cloudflare.com/spectre-research-with-tu-graz/),Cloudflare Workers 冻结本地定时器,禁止多线程和共享内存,并主动检测、定期洗牌内存,将看似恶意的脚本隔离到独立进程中。
## 攻击原理
Cloudflare Workers 平台故意 [限制定时器](https://blog.cloudflare.com/mitigating-spectre-and-other-security-threats-the-cloudflare-workers-security-model/)。在仅限 CPU 的执行期间,时间基本上被“冻结”。`Date.now()` 和 `performance.now()` 不提供持续前进的高分辨率时钟。没有共享内存,也没有多线程,因此经典的通过 `SharedArrayBuffer` 的计时器不可用。
要成功发动攻击,必须解决几个挑战。首先,Workers 运行时是有限的,并且必须确保攻击者与受害者之间的共址。其次,必须发现一个可靠的、理想情况下共址的远程定时器,以便进行稳定的时间测量。\n第三,攻击在生产条件下进行,这意味着需要额外的稳定性措施,例如可靠的 Spectre 小工具,以实现临时的 64 位越界访问,健壮的信号放大机制以应对系统和网络噪声,以及一个可靠驱逐缓存数据的原语。
### Spectre 小工具
*推测性类型混淆 Spectre 小工具*
拥有合适的 Spectre 小工具(上述代码段),攻击者可以临时访问越界内存并将一个位编码到缓存(`probeArray`)。攻击者随后测量内存访问延迟,以确认数据是已缓存的还是未缓存的。更快的访问意味着该行已被缓存,并且该位是 1。相反,较慢的访问意味着未被缓存,该位为 0。在我们的攻击中,我们使用两种不同的 Spectre 小工具类型。第一种泄露压缩堆指针,例如隔离的堆基地址(根),而另一种则利用推测性类型混淆来泄露一个任意的、攻击者构造的用户空间 64 位指针。在研究进行时,Cloudflare Workers 中尚未实现 V8 沙箱。在指针压缩下,大多数对象使用 32 位压缩指针。`TypedArray` 是几个例外之一,仍然存储对其后备存储的原始 64 位指针,这恰巧是我们的工具所利用的。
代码 `obj instanceof ObjP` 执行类型检查,即一个分支。为了错误训练分支预测,我们在真实的 `ObjP` 实例上多次调用小工具,然后在具有攻击者控制的内存布局的不同对象 `ObjI` 上调用它。CPU 会推测已采取的分支并跟踪 `obj.ptr[0]`,即使对象具有不同的类型。为了泄露单个位,我们掩盖一个位,并用它选择两个 `probeArray` 行中的一个。哪一行被缓存,就编码了该位。
利用堆泄露小工具,我们映射相邻对象并定位攻击者控制的数组。我们的第二个小工具混淆两个跨越多个缓存行的大对象,因此类型字段落在不同的缓存行中,而我们读取的字段却依然在缓存之中。驱逐类型字段打开了推测窗口,而目标字段则保持缓存,临时读取则遵循一值为攻击者控制的 64 位值。这使得泄露变成了任意地址读取。关于此技术的更详细描述可以在论文中找到。
**本地演示:泄露任意 64 位地址。**
### 信号放大
缓存命中和缓存未命中的差异在于数纳秒。此外,远程定时器在微秒到毫秒的范围内是有噪声的。因此,需要某种形式的信号放大,以区分缓存命中与未命中。Stephen Röttger 和 Artur Janc 发现了一种方法来 [放大单次内存访问](https://security.googleblog.com/2021/03/a-spectre-proof-of-concept-for-spectre.html),通过利用 L1 缓存中的树状伪最近最少使用 (PLRU) 替换政策。树状 PLRU 将每个缓存集合组织为二叉树,其节点指向最近未使用的边,因此 CPU 通过遵循这些指针进行驱逐。通过合适的访问模式,攻击者可以通过在指向目标时触及其树邻居,令目标行无限期保留在缓存中。这一行为相当优雅。利用这一特性,单次缓存事件的时序可以被任意放大,从而导致大量 L1 命中(更快)与相对情况下大量 L1 未命中。
下图展示了内存地址 X 是否被缓存。如果没有被缓存,访问模式会导致大量的缓存命中。如果存在,它占据树中的一个节点,随后四个缓存行尝试适应三个节点,造成大量的 L1 未命中。
### 远程定时器
只要信号能够放大,一个嘈杂的远程定时器就足够区分编码位。例如,WebSocket 连接到一个外部服务器,提供高分辨率时间戳就足够了。定时器可以位于 Cloudflare 或与运行 Worker 的目标数据中心位于同一位置的数据中心。Worker 请求远程定时器为某个事件标记时间戳,并在事件停止后计算另一个请求的时间差。
在论文中,我们评估了几种不同的定时器设置,并能够通过只少量样本,在较大的拓扑距离下可靠地实现亚毫秒分辨率。下图展示了使用树状 PLRU 放大的缓存事件。
### 可重复测量
单次测量不足以可靠地区分时间编码数据。生产机器存在噪声,因此攻击者必须至少重复每次测量几次并使用一些统计判别法。在我们的案例中,重复测量意味着重置缓存状态,在每轮之前必须驱逐两个值。推测性分支依赖的值必须被驱逐,因此分支解决方案必须停滞,打开推测窗口。编码泄露位的探针行也必须被驱逐,以便下一次临时访问能够重新缓存它。
由于 JavaScript 中没有可用的直接指令,经典的做法是构建驱逐集。驱逐集是一组地址,映射到与目标相同的缓存集。以正确的方式访问它们将目标推出缓存。在他们的攻击中,Stephen Röttger 和 Artur Janc 使用驱逐列表,将至少一些内容可靠地驱逐到 L2 缓存。这可行,但代价高昂。构建一个精确的驱逐集需要大量的定时测量,而我们的定时器是嘈杂的远程定时器。针对 Workers 的先前远程攻击通过遍历一个比 L1 和 L2 缓存大的数组,规避了搜索。这是一个选择,但速度较慢。
Dougall Johnson 在他的 [极好的博客文章](https://dougallj.wordpress.com/2021/03/16/another-approach-to-portable-javascript-spectre-exploitation/) 中描述了更优雅的方式。这个想法直接来源于抽屉原理。如果你分配的数据显示大于缓存能够容纳的量,随机选择的缓存行几乎肯定不在缓存中。对于 256 KB 的 L2 缓存,分配 64 MB,留下最多 **1/256** 的机会,使随机缓存行仍然位于 L2 中。因此与其将特定行驱逐,你干脆不驱逐任何行。你选择一个已被驱逐的全新随机位置,几乎相当于必然发生。频繁循环访问对象数组的额外好处是,自动驱逐效应的实现。
为了在 JavaScript 中利用此特性,我们分配一个大的攻击者和受害者对象对的池,超出了最后一级缓存的限制。每次测量轮次选择一对新随机对。对象的映射指针,推测性类型检查读取的隐含类描述符,因此几乎可以肯定已经被驱逐。
### 将攻击者和受害者隔离共址
为使攻击成功,攻击者和受害者的隔离必须在同一台边缘服务器上的同一进程中调度。直观上,你可能会认为这很困难,因为 Cloudflare 运行数万台边缘服务器,但这在 Cloudflare Workers 中实际上相当简单。因为 Cloudflare Workers 被设计成可以在任何 Cloudflare 边缘服务器上执行,因此从攻击者脚本调用受害者脚本的 `fetch(“https://victim.example”)` 通常会导致调度程序在确切的相同进程中启动一个受害者 Worker。通过定期向其发送子请求,可以保持受害者隔离的活跃状态。
更有甚者,由于攻击的稳定性高度依赖于运行 Worker 脚本的边缘服务器 CPU 负载,攻击者可以在低流量的时段(例如,欧洲工作时段内在澳大利亚的 colo)中具有战略性地运行攻击。
### 遏制隔离资源限制
Cloudflare Workers 运行时 [强制实施一组限制](https://developers.cloudflare.com/workers/platform/limits/),以保护平台并防止滥用。为了实施此攻击,相关限制为每次调用 30 秒 CPU 时间和 1,000 次子请求。这些限制虽然已经 [增加](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits),但以下原则仍然适用。
对于常规的 Worker,每个 HTTP 请求,即 fetch 事件,是一次新调用,将重置这些限制。关键在于将连续请求落在同一台边缘服务器上。负载均衡和网络条件的变化使得这一点不可靠。耐久对象为我们解决了这个问题。
[耐久对象](https://developers.cloudflare.com/durable-objects/) 被设计用于客户端之间的实时协作,因此运行时将每个传入的 WebSocket 消息视为一次调用,重置 CPU 时间和请求限制。攻击者打开一个持久的 WebSocket 连接到一个 Durable Object worker 并发送定期的保持连接消息。这保持着一个隔离的存活,并为我们提供了通过其进行攻击的持久双向通道。
有一个怪癖消耗了我们一些时间。由于隔离是单线程的,因此在脚本将控制权返回给事件循环之前,传入的 WebSocket 消息只有在脚本放弃控制后才能处理。在同步代码中,运行时永远无法看到保持连接的消息,因此无法重置 CPU 时间。如果线程阻塞超过 30 秒,运行时会终止隔离。这限制了我们在一次同步突发中可放大的数量。定期间隔的产出让我们能够保持隔离存活,从 5 到 20 多个小时。
### 综合提炼一切
先前的攻击主要依赖于重复来放大单次缓存访问,因此速度相对较慢,使得泄露速率达到 120 bit/h。我们将基于树的 PLRU 放大与测量循环相结合。每一轮都重新创建缓存状态,从而增加了更多的时序差异。如果一次中断在某一轮破坏了缓存状态,也无所谓,因为后续迭代可以抵消影响。这使得信号足够强,能够利用远程 WebSocket 定时器进行位分类。整体思路现在是进行结合。
我们在 Cloudflare Workers 生产环境中演示了完整的端到端攻击,针对我们控制的 Workers。我们首先从攻击者 Worker 泄露数据。然后,从一个位于相同位置的受害者 Worker 泄露数据,在其中我们故意放置了一个秘密。
首先,我们在攻击者 Worker、受害者 Worker 和远程定时器之间建立了共址。耐久对象为我们提供了一个长生命周期的执行环境。WebSocket 消息为我们提供了一个可重复的时序来源。`/cdn-cgi/trace` 端点帮助我们通过查看 `fl` 值来确认机器放置。
其次,我们添加了一个校准步骤,以使用推测性可达值探测定时器。此步骤很重要,因为生产机器存在噪声。按调用校准让我们能够通过零与一之间的相对差异来对位进行分类。最后的测试应该导致两个明确定 separable 的分布。
作为第一步,我们从一个 Worker 泄露隔离根,而在另一 Worker 中,我们使用推测性类型混淆,利用 64 位指针读取隔离根。
作为中间步骤,我们通过读取来自 vDSO 区域的内存,确认了 64 位泄漏。vDSO 是一个方便的目标,因为其中包含诸如 `gettimeofday` 的人类可读字符串。
**视频演示:从 JavaScript 堆泄露数据**
最后,我们在受害者 Worker 中放置了一个 JWT 令牌,并逐位泄露它。第一个字节是字符 e,表示为 0b01100101。下图展示了该字节的逐位分类。为了分类,我们使用双侧测试来检验两个结果。使用多数投票和基于百分位的阈值来推断该位。在生产中,我们达到了高达 12 bit/s 的泄露速率,准确率超过 99%。请注意,更高的泄露速率是可能的,但代价是降低准确率。
### 鲁棒性
根据一天中的时间,机器的利用率会显著增加。这会减缓攻击,因为需要采样更多数据。尽管如此,即使在高 CPU 利用率下,攻击仍然可行。
### 为什么未被检测到?
DyPrIs 监控硬件性能计数器,并且一旦某个脚本看起来像是 Spectre 攻击,就将其隔离到自身进程中。使得攻击未被发觉的两个原因。首先,DyPrIs 只在脚本调用结束后才隔离。我们在攻击中使用的耐久对象保持活跃的技巧,可以运行几个小时到一天。WebSocket 保持连接消息使得单次调用可以打开数小时,因此泄露在隔离之前完成。其次,DyPrIs 会根据 iTLB 中的分支误预测数量进行归一化。
---
原文链接:[点击查看](https://blog.cloudflare.com/revisiting-spectre-attacks-on-workers/)
要在生产环境中发起成功的侧信道攻击,外部攻击者必须克服额外障碍,例如共享硬件资源上的活动、中断、上下文切换和粗粒度定时器。我们的研究发现了 DyPrIs 实现中的一个限制,并且我们成功展示了一种远程 Spectre 攻击,在 Cloudflare Workers 的生产环境中以 99% 的准确率可靠地泄露多达 12 bit/s 的数据。基于这项研究,我们改进了 DyPrIs,集成了 V8 沙箱和 [进程内隔离机制](https://blog.cloudflare.com/safe-in-the-sandbox-security-hardening-for-cloudflare-workers/),进一步降低内存泄露攻击的风险。
今天,我们 [发布了一篇论文](https://arxiv.org/pdf/2608.17043),描述了我们的发现,论文由 Albert Pedersen、Haocheng Xiao、Sam Ainsworth、Nigel Topham 和 Martin Schwarzl 联合署名。论文涵盖了 2024 年和 2025 年初的研究。
请注意,由于 Cloudflare Workers Runtime 团队采取的对策,所展示的攻击在生产系统中已经得到缓解。在过去三年中,我们没有发现任何活动利用的迹象。
## Cloudflare Workers 安全模型
Cloudflare Workers 在边缘运行不信任的 JavaScript。利用语言级隔离,采用 V8 隔离,数万名用户可以共享同一个操作系统进程。每个 Worker 都有自己独立的 JavaScript 堆。这一设计使得启动延迟保持在较低水平,相比完全进程隔离,能更有效地运行多个用户。我们在运行时周围有多层防御,例如自动 V8 补丁管道、由 Linux 命名空间和 seccomp 过滤器组成的双层沙箱、Cap’n Proto RPC 以及将某些脚本调度在独立的进程沙箱中的可能性。然而,Worker 进程中的一次任意读取漏洞仍然可能导致跨用户数据泄露。一个难以缓解的漏洞是利用了推测性执行的特性,即进程内的 **Spectre**。
## Spectre
你可以把推测性执行想象成徒步旅行。某个时刻,你来到了一个分叉口,必须预测该往哪个方向走。如果预测正确,你节省了时间,能够享受阳光并在山区小屋喝一杯清凉饮料。然而,如果你推测错了方向,就得返回。小径看似没有人走过,但你的脚印仍然留在泥土中。
CPU 中的推测性执行也是类似的工作方式。分支预测提前对分支的结果进行有根据的猜测,CPU 随后推测性地执行它。如果预测是正确的,推测性执行节省了一些时间。而如果预测错误,CPU 则需要丢弃结果,回退并执行另一条分支。由于这些推测性执行的指令只暂时存在于 CPU 流水线中,并且从未被永久退休或提交,文献将其称为过渡指令,并将概念泛化为过渡执行。
然而,由于过渡执行,微架构状态中仍然留有一些痕迹,例如 CPU 缓存。攻击者可以利用 Spectre 暂时性地访问越界内存,将单个位的信息编码到缓存状态中,并利用重新访问数据的延迟来推断该位是否被设置。
为了 [缓解进程内 Spectre 攻击](https://blog.cloudflare.com/spectre-research-with-tu-graz/),Cloudflare Workers 冻结本地定时器,禁止多线程和共享内存,并主动检测、定期洗牌内存,将看似恶意的脚本隔离到独立进程中。
## 攻击原理
Cloudflare Workers 平台故意 [限制定时器](https://blog.cloudflare.com/mitigating-spectre-and-other-security-threats-the-cloudflare-workers-security-model/)。在仅限 CPU 的执行期间,时间基本上被“冻结”。`Date.now()` 和 `performance.now()` 不提供持续前进的高分辨率时钟。没有共享内存,也没有多线程,因此经典的通过 `SharedArrayBuffer` 的计时器不可用。
要成功发动攻击,必须解决几个挑战。首先,Workers 运行时是有限的,并且必须确保攻击者与受害者之间的共址。其次,必须发现一个可靠的、理想情况下共址的远程定时器,以便进行稳定的时间测量。\n第三,攻击在生产条件下进行,这意味着需要额外的稳定性措施,例如可靠的 Spectre 小工具,以实现临时的 64 位越界访问,健壮的信号放大机制以应对系统和网络噪声,以及一个可靠驱逐缓存数据的原语。
### Spectre 小工具
*推测性类型混淆 Spectre 小工具*
拥有合适的 Spectre 小工具(上述代码段),攻击者可以临时访问越界内存并将一个位编码到缓存(`probeArray`)。攻击者随后测量内存访问延迟,以确认数据是已缓存的还是未缓存的。更快的访问意味着该行已被缓存,并且该位是 1。相反,较慢的访问意味着未被缓存,该位为 0。在我们的攻击中,我们使用两种不同的 Spectre 小工具类型。第一种泄露压缩堆指针,例如隔离的堆基地址(根),而另一种则利用推测性类型混淆来泄露一个任意的、攻击者构造的用户空间 64 位指针。在研究进行时,Cloudflare Workers 中尚未实现 V8 沙箱。在指针压缩下,大多数对象使用 32 位压缩指针。`TypedArray` 是几个例外之一,仍然存储对其后备存储的原始 64 位指针,这恰巧是我们的工具所利用的。
代码 `obj instanceof ObjP` 执行类型检查,即一个分支。为了错误训练分支预测,我们在真实的 `ObjP` 实例上多次调用小工具,然后在具有攻击者控制的内存布局的不同对象 `ObjI` 上调用它。CPU 会推测已采取的分支并跟踪 `obj.ptr[0]`,即使对象具有不同的类型。为了泄露单个位,我们掩盖一个位,并用它选择两个 `probeArray` 行中的一个。哪一行被缓存,就编码了该位。
利用堆泄露小工具,我们映射相邻对象并定位攻击者控制的数组。我们的第二个小工具混淆两个跨越多个缓存行的大对象,因此类型字段落在不同的缓存行中,而我们读取的字段却依然在缓存之中。驱逐类型字段打开了推测窗口,而目标字段则保持缓存,临时读取则遵循一值为攻击者控制的 64 位值。这使得泄露变成了任意地址读取。关于此技术的更详细描述可以在论文中找到。
**本地演示:泄露任意 64 位地址。**
### 信号放大
缓存命中和缓存未命中的差异在于数纳秒。此外,远程定时器在微秒到毫秒的范围内是有噪声的。因此,需要某种形式的信号放大,以区分缓存命中与未命中。Stephen Röttger 和 Artur Janc 发现了一种方法来 [放大单次内存访问](https://security.googleblog.com/2021/03/a-spectre-proof-of-concept-for-spectre.html),通过利用 L1 缓存中的树状伪最近最少使用 (PLRU) 替换政策。树状 PLRU 将每个缓存集合组织为二叉树,其节点指向最近未使用的边,因此 CPU 通过遵循这些指针进行驱逐。通过合适的访问模式,攻击者可以通过在指向目标时触及其树邻居,令目标行无限期保留在缓存中。这一行为相当优雅。利用这一特性,单次缓存事件的时序可以被任意放大,从而导致大量 L1 命中(更快)与相对情况下大量 L1 未命中。
下图展示了内存地址 X 是否被缓存。如果没有被缓存,访问模式会导致大量的缓存命中。如果存在,它占据树中的一个节点,随后四个缓存行尝试适应三个节点,造成大量的 L1 未命中。
### 远程定时器
只要信号能够放大,一个嘈杂的远程定时器就足够区分编码位。例如,WebSocket 连接到一个外部服务器,提供高分辨率时间戳就足够了。定时器可以位于 Cloudflare 或与运行 Worker 的目标数据中心位于同一位置的数据中心。Worker 请求远程定时器为某个事件标记时间戳,并在事件停止后计算另一个请求的时间差。
在论文中,我们评估了几种不同的定时器设置,并能够通过只少量样本,在较大的拓扑距离下可靠地实现亚毫秒分辨率。下图展示了使用树状 PLRU 放大的缓存事件。
### 可重复测量
单次测量不足以可靠地区分时间编码数据。生产机器存在噪声,因此攻击者必须至少重复每次测量几次并使用一些统计判别法。在我们的案例中,重复测量意味着重置缓存状态,在每轮之前必须驱逐两个值。推测性分支依赖的值必须被驱逐,因此分支解决方案必须停滞,打开推测窗口。编码泄露位的探针行也必须被驱逐,以便下一次临时访问能够重新缓存它。
由于 JavaScript 中没有可用的直接指令,经典的做法是构建驱逐集。驱逐集是一组地址,映射到与目标相同的缓存集。以正确的方式访问它们将目标推出缓存。在他们的攻击中,Stephen Röttger 和 Artur Janc 使用驱逐列表,将至少一些内容可靠地驱逐到 L2 缓存。这可行,但代价高昂。构建一个精确的驱逐集需要大量的定时测量,而我们的定时器是嘈杂的远程定时器。针对 Workers 的先前远程攻击通过遍历一个比 L1 和 L2 缓存大的数组,规避了搜索。这是一个选择,但速度较慢。
Dougall Johnson 在他的 [极好的博客文章](https://dougallj.wordpress.com/2021/03/16/another-approach-to-portable-javascript-spectre-exploitation/) 中描述了更优雅的方式。这个想法直接来源于抽屉原理。如果你分配的数据显示大于缓存能够容纳的量,随机选择的缓存行几乎肯定不在缓存中。对于 256 KB 的 L2 缓存,分配 64 MB,留下最多 **1/256** 的机会,使随机缓存行仍然位于 L2 中。因此与其将特定行驱逐,你干脆不驱逐任何行。你选择一个已被驱逐的全新随机位置,几乎相当于必然发生。频繁循环访问对象数组的额外好处是,自动驱逐效应的实现。
为了在 JavaScript 中利用此特性,我们分配一个大的攻击者和受害者对象对的池,超出了最后一级缓存的限制。每次测量轮次选择一对新随机对。对象的映射指针,推测性类型检查读取的隐含类描述符,因此几乎可以肯定已经被驱逐。
### 将攻击者和受害者隔离共址
为使攻击成功,攻击者和受害者的隔离必须在同一台边缘服务器上的同一进程中调度。直观上,你可能会认为这很困难,因为 Cloudflare 运行数万台边缘服务器,但这在 Cloudflare Workers 中实际上相当简单。因为 Cloudflare Workers 被设计成可以在任何 Cloudflare 边缘服务器上执行,因此从攻击者脚本调用受害者脚本的 `fetch(“https://victim.example”)` 通常会导致调度程序在确切的相同进程中启动一个受害者 Worker。通过定期向其发送子请求,可以保持受害者隔离的活跃状态。
更有甚者,由于攻击的稳定性高度依赖于运行 Worker 脚本的边缘服务器 CPU 负载,攻击者可以在低流量的时段(例如,欧洲工作时段内在澳大利亚的 colo)中具有战略性地运行攻击。
### 遏制隔离资源限制
Cloudflare Workers 运行时 [强制实施一组限制](https://developers.cloudflare.com/workers/platform/limits/),以保护平台并防止滥用。为了实施此攻击,相关限制为每次调用 30 秒 CPU 时间和 1,000 次子请求。这些限制虽然已经 [增加](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits),但以下原则仍然适用。
对于常规的 Worker,每个 HTTP 请求,即 fetch 事件,是一次新调用,将重置这些限制。关键在于将连续请求落在同一台边缘服务器上。负载均衡和网络条件的变化使得这一点不可靠。耐久对象为我们解决了这个问题。
[耐久对象](https://developers.cloudflare.com/durable-objects/) 被设计用于客户端之间的实时协作,因此运行时将每个传入的 WebSocket 消息视为一次调用,重置 CPU 时间和请求限制。攻击者打开一个持久的 WebSocket 连接到一个 Durable Object worker 并发送定期的保持连接消息。这保持着一个隔离的存活,并为我们提供了通过其进行攻击的持久双向通道。
有一个怪癖消耗了我们一些时间。由于隔离是单线程的,因此在脚本将控制权返回给事件循环之前,传入的 WebSocket 消息只有在脚本放弃控制后才能处理。在同步代码中,运行时永远无法看到保持连接的消息,因此无法重置 CPU 时间。如果线程阻塞超过 30 秒,运行时会终止隔离。这限制了我们在一次同步突发中可放大的数量。定期间隔的产出让我们能够保持隔离存活,从 5 到 20 多个小时。
### 综合提炼一切
先前的攻击主要依赖于重复来放大单次缓存访问,因此速度相对较慢,使得泄露速率达到 120 bit/h。我们将基于树的 PLRU 放大与测量循环相结合。每一轮都重新创建缓存状态,从而增加了更多的时序差异。如果一次中断在某一轮破坏了缓存状态,也无所谓,因为后续迭代可以抵消影响。这使得信号足够强,能够利用远程 WebSocket 定时器进行位分类。整体思路现在是进行结合。
我们在 Cloudflare Workers 生产环境中演示了完整的端到端攻击,针对我们控制的 Workers。我们首先从攻击者 Worker 泄露数据。然后,从一个位于相同位置的受害者 Worker 泄露数据,在其中我们故意放置了一个秘密。
首先,我们在攻击者 Worker、受害者 Worker 和远程定时器之间建立了共址。耐久对象为我们提供了一个长生命周期的执行环境。WebSocket 消息为我们提供了一个可重复的时序来源。`/cdn-cgi/trace` 端点帮助我们通过查看 `fl` 值来确认机器放置。
其次,我们添加了一个校准步骤,以使用推测性可达值探测定时器。此步骤很重要,因为生产机器存在噪声。按调用校准让我们能够通过零与一之间的相对差异来对位进行分类。最后的测试应该导致两个明确定 separable 的分布。
作为第一步,我们从一个 Worker 泄露隔离根,而在另一 Worker 中,我们使用推测性类型混淆,利用 64 位指针读取隔离根。
作为中间步骤,我们通过读取来自 vDSO 区域的内存,确认了 64 位泄漏。vDSO 是一个方便的目标,因为其中包含诸如 `gettimeofday` 的人类可读字符串。
**视频演示:从 JavaScript 堆泄露数据**
最后,我们在受害者 Worker 中放置了一个 JWT 令牌,并逐位泄露它。第一个字节是字符 e,表示为 0b01100101。下图展示了该字节的逐位分类。为了分类,我们使用双侧测试来检验两个结果。使用多数投票和基于百分位的阈值来推断该位。在生产中,我们达到了高达 12 bit/s 的泄露速率,准确率超过 99%。请注意,更高的泄露速率是可能的,但代价是降低准确率。
### 鲁棒性
根据一天中的时间,机器的利用率会显著增加。这会减缓攻击,因为需要采样更多数据。尽管如此,即使在高 CPU 利用率下,攻击仍然可行。
### 为什么未被检测到?
DyPrIs 监控硬件性能计数器,并且一旦某个脚本看起来像是 Spectre 攻击,就将其隔离到自身进程中。使得攻击未被发觉的两个原因。首先,DyPrIs 只在脚本调用结束后才隔离。我们在攻击中使用的耐久对象保持活跃的技巧,可以运行几个小时到一天。WebSocket 保持连接消息使得单次调用可以打开数小时,因此泄露在隔离之前完成。其次,DyPrIs 会根据 iTLB 中的分支误预测数量进行归一化。
---
原文链接:[点击查看](https://blog.cloudflare.com/revisiting-spectre-attacks-on-workers/)
评论
暂无评论。