Cloudflare官方
25-11-13 17:56 微博认证:Cloudflare官方微博

沙箱安全:Cloudflare Workers 的安全加固(一)

作为无服务器云提供商,我们在全球分布的基础设施上运行您的代码 - 能够在我们的网络上运行客户代码意味着任何人都可以充分利用我们的全球覆盖和低延迟优势。Workers 不仅高效,更致力于为用户简化操作。简而言之:您只需专心编写代码,其余事务交由我们来处理。

“处理其余事务”的重要一环,就是尽可能保障 Workers 的安全性。我们此前已撰文介绍过自身的安全架构。确保 Workers 的安全性是一个颇具挑战性的问题,因为 Workers 的核心设计理念就是在我们的硬件上运行第三方代码。这是信息安全领域最为棘手的难题之一:攻击者在精心策划攻击时,能够充分利用目标系统上运行的编程语言的全部能力。

正因如此,我们始终致力于持续更新和优化 Workers 运行时环境,以便充分运用硬件和软件领域的最新改进。本文将为大家分享我们在保障 Workers 安全性方面所开展的部分最新工作。

首先了解一下背景知识:Workers 是基于 V8 JavaScript 运行时构建的,而 V8 最初是为 Chrome 等基于 Chromium 的浏览器开发的。这让我们有了一个良好的开端,因为 V8 诞生于充满对抗性的环境之中,始终面临着严峻的攻击和审查挑战。和 Workers 一样,Chromium 的设计目标也是安全地运行具有对抗性的代码。正因如此,V8 会持续接受最先进的模糊测试工具和净化器的检验,多年来,通过引入 Oilpan/cppgc 等新技术以及改进静态分析等方法,V8 的安全性得到了不断增强。

不过,我们使用 V8 的方式略有不同。因此,我们将在本文中介绍我们如何修改 V8 以提高我们用例中的安全性。

内存保护密钥带来的硬件辅助安全增强

Intel、AMD 和 ARM 的现代 CPU 都支持内存保护密钥,有时也称为 PKU,即用户空间保护密钥 (Protection Keys for Userspace)。这是一项非常强大的安全功能,可以增强虚拟内存和内存保护的能力。

传统上,您电脑或手机中 CPU 的内存保护功能主要用于保护内核以及保护不同进程之间的互通。在同一个进程内部,所有线程都能访问相同的内存区域。而内存保护密钥技术则让我们能够阻止特定线程访问其不应访问的内存区域。

V8 已经为即时编译器应用了内存保护密钥。对于以 JavaScript 为代表的编程语言,其即时编译器会在代码运行时动态生成经过优化、高度适配的代码版本。通常情况下,编译器会在独立的线程中运行,需要具备向代码区域写入数据的权限,以便部署优化后的代码。不过,编译器线程本身不需要能够执行这些代码。与之相对,常规的执行线程则需要能够运行这些代码,但不得对其进行修改。内存保护密钥为每个线程提供了其所需的权限,但仅此而已。Chromium 项目中的 V8 团队并未停下技术迭代的脚步,他们在此详细阐述了关于内存保护密钥的未来规划。

在 Workers 平台中,我们的安全需求与 Chromium 浏览器存在显著差异。Workers 的安全架构采用 V8 隔离实例来分隔运行在我们服务器上的不同脚本。(此外,我们还部署了额外的缓解措施,以增强系统对 Spectre 攻击的防御能力)。若 V8 引擎按预期运行,这些防护应该足够可靠,但我们更坚信纵深防御理念:通过多层相互叠加的安全控制机制来构建防护体系。

正因如此,我们对 V8 进行了内部修改,采用内存保护密钥实现各隔离实例之间的安全隔离。现代 x64 架构的 CPU 最多支持 15 个不同的密钥,其中部分密钥已用于 V8 中的其他功能,因此我们实际可用的密钥约有 12 个。我们为每个隔离实例分配一个随机密钥,该密钥用于保护其 V8 堆数据——即脚本运行时创建的 JavaScript 对象所处的内存区域。这意味着,以往可能导致攻击者跨隔离实例窃取数据的漏洞,在 92% 的情况下会直接触发硬件级防护机制(按 12 个密钥计算,92% 由 11/12 计算得出)。

图2

图2的插图展示了一个攻击者试图从其他隔离区读取数据的场景。大多数情况下,这种越界访问会被不匹配的内存保护密钥检测到——系统会立即终止攻击者的脚本并通知我们,以便我们及时调查和修复问题。图中用红色箭头表示攻击者“走运”的特殊情况:当攻击者碰巧遇到了具有相同内存保护密钥的隔离区(表现为图中隔离区颜色相同)时,攻击就可能得逞。
不过,我们仍有提升空间,可将 92% 的保护率进一步优化。在本文后续部分,我们将阐述如何针对某一常见场景将保护率提升至 100%。但在此之前,先来看看我们正在利用的 V8 软件加固特性。

V8 沙箱,一种基于软件的安全边界

过去几年间,V8 新增了另一个纵深防御功能:V8 沙箱(注意不要与 Workers 自上线起便采用的二级沙箱混淆)。V8 沙箱作为一项历经数年的技术攻坚项目,其防护能力已逐步趋于成熟。该项目的诞生源于一个关键发现:V8 引擎的诸多安全漏洞往往始于对堆内存中对象的非法篡改。攻击者以这种篡改作为跳板,进而渗透至进程的其他区域,最终可能实现权限提升,获取对受害者浏览器乃至整个系统的更高访问权限。

V8 沙箱项目是一项雄心勃勃的软件安全防护措施,旨在阻断攻击升级路径:让攻击者无法从 V8 堆内存的破坏行为进一步危害整个进程。这意味着需要采取多项措施,其中就包括彻底清除堆内存中的所有指针。但在此之前,让我们尽可能用通俗易懂的语言解释什么是内存破坏攻击。

内存破坏攻击

内存破坏攻击会诱使程序错误地使用自身内存。计算机内存本质上只是一系列整数的存储空间,每个整数都存放在特定位置。这些位置都有各自的地址(地址本身也只是一个数字)。程序会以不同方式解读这些位置中的数据,比如将其视为文本、像素,或是指针。指针实际上是用于标识其他内存位置的地址,因此它们就像某种箭头,指向其他数据片段。

下面是一个具体示例,该示例涉及缓冲区溢出攻击。这是一种历史上较为常见且相对容易理解的攻击形式:假设某个程序包含一个小缓冲区(比如一个 16 字符的文本框),紧随其后存放着一个 8 字节指针,该指针指向某些普通数据。攻击者可能会向该程序发送一个 24 字符的字符串,从而引发“缓冲区溢出”。由于程序存在漏洞,前 16 个字符会正常填入目标缓冲区,但剩余的 8 个字符则会溢出并覆盖相邻的指针。

图3

请参见下文,了解现在如何阻止此类攻击。

此时,该指针已被重定向至攻击者指定的敏感数据区域,而非其原本应当访问的常规数据。当程序试图调用它自以为正常的指针时,实际上访问的却是攻击者精心挑选的敏感数据。
此类攻击往往分阶段实施:首先制造一个小规模的混乱(例如缓冲区溢出),继而利用该混乱引发更大问题,最终获取本不应属于攻击者的数据访问权限或系统能力。攻击者最终可能通过这种误导手段,窃取敏感信息或在程序中植入恶意数据——而这些恶意数据将被程序误认为合法内容。

以上是对利用缓冲区溢出实施内存破坏攻击的简要抽象说明——这只是众多攻击手段中较为简单的一种。若想了解更为详细且时效性更强的实际案例,可参阅 Google 发布的说明文档,或这份 V8 漏洞分析报告。

V8 中的压缩指针

许多攻击都是基于篡改指针实施的,因此理想情况下我们会彻底移除程序内存中的所有指针。但由于面向对象语言的堆内存中充斥着大量指针,表面上看这似乎是一项不可能完成的任务——不过这项技术突破得益于早期的发展成果。自 2020 年起,V8 引擎就开始提供通过使用压缩指针来节省内存的选项。这意味着在 64 位系统上,堆内存仅使用 32 位的相对偏移量(相对于基准地址)。这将堆内存总量限制在最大 4GiB,这一限制对于浏览器环境来说是可以接受的,对于运行在 Cloudflare Workers 的 V8 隔离实例中的单个脚本来说同样适用。

图4 一个包含多个字段的人工构造对象,用于展示压缩堆与未压缩堆在布局上的差异。图中的每个存储单元均为 64 位宽度。

如果整个堆都位于一个连续的 4 GiB 内存区域内,那么所有指针的高 32 位都将保持相同值,因此我们无需在每个对象的每个指针字段中重复存储这些高位。从上面的示意图中可以看到,所有对象指针都以 0x12345678 开头,因此这部分高位地址信息是冗余的,完全不需要存储。这意味着对象指针字段和整数字段可以从 64 位缩减至 32 位。

我们仍然需要为某些字段(例如双精度浮点数,以及缓冲区的沙箱偏移量)保留 64 位字段,这些字段通常是脚本用于输入和输出数据的。有关详细信息,请参阅下文。

未压缩堆中的整数存储在 64 位字段的高 32 位中。压缩堆则使用 32 位字段的高 31 位。两种情况下,最低位均设置为 0,以指示整数(而不是指针或偏移量)。

从概念上来说,我们有两种基于 4 GiB 整数倍基地址的压缩与解压缩方法:
图5

从压缩指针到沙箱

最大的 32 位无符号整数约为 40 亿,因此 Decompress() 函数无法生成任何超出 [base, base + 4 GiB] 范围的指针。您可以说指针被困在这个区域,因此它有时被称为指针隔离笼。V8 可以为指针笼保留 4 GiB 的虚拟地址空间,以便只有 V8 对象出现在这个范围内。通过消除所有来自这个范围的指针,并遵循一些其他严格的规则,V8 可以将攻击者造成的任何内存破坏控制在这个笼子里。即使攻击者破坏了笼子内的 32 位偏移量,它也仍然只是一个 32 位偏移量,只能用于创建仍然被困在指针笼内的新指针。

图6 之前提到的缓冲区溢出攻击不再奏效,因为在指针隔离笼内仅存有攻击者自身的数据。

为构建沙箱环境,我们以 4 GiB 的指针隔离笼为基础,再额外分配 4 GiB 用于缓冲区及其他数据结构,从而形成完整的 8 GiB 沙箱空间。这也解释了为何前文提到的缓冲区偏移量采用 33 位设计,使其能够访问沙箱后半段的缓冲区区域(在 Chromium 浏览器采用更大沙箱时为 40 位)。V8 引擎将这些缓冲区偏移量存储在高位 33 位中,并在使用左移 31 位进行处理,以此防范攻击者对低位比特位的潜在篡改。

Cloudflare Workers 早已采用 V8 的压缩指针技术,但要充分发挥沙箱的完整效能,我们必须进行一些关键改进。此前在使用 V8 的沙箱配置时,一个进程内的所有隔离实例都必须属于同一个沙箱。这将导致所有 V8 堆内存的总容量被限制在 4 GiB 以下。然而对于我们的架构而言,这种限制远远不够,因为我们需要在同一时间为成千上万个脚本提供服务。

正因如此,我们委托 Igalia 团队为 V8 新增了隔离组功能。每个隔离组都拥有独立的沙箱,且可包含 1 个或多个隔离实例。基于这一改进,我们现已能够启用沙箱机制,一举消除了一整类潜在的安全隐患。尽管技术上允许在同一个沙箱中放置多个隔离实例,但目前我们仅在每个沙箱中放置单个隔离实例。

图7 沙箱的布局。单个沙箱中可包含多个隔离实例,但所有堆内存页必须位于指针隔离笼(即沙箱的前 4 GiB 空间)。我们采用 32 位偏移量替代对象间的传统指针引用,而缓冲区则使用 33 位偏移量,因此它们可以到达整个沙箱,但不能到达沙箱外部。

未完待续...

Cloudflare 保护整个企业网络,帮助客户高效构建互联网规模的应用程序,加速任何网站或互联网应用程序,抵御 DDoS 攻击,防止黑客入侵,并能协助您实现 Zero Trust 的部署与实施。

从任何设备访问 1.1.1.1,使用我们的免费应用加速和保护您的互联网。

立即联系我们,获取更多相关信息http://t.cn/A6Tx4yTT

#Cloudflare##沙盒安全#

发布于 辽宁