保护个人安全和隐私是 Mozilla 使命的核心原则,因此我们始终致力于让用户在网络上更加安全。对于像 Firefox 这样复杂且高度优化的系统,内存安全是最大的安全挑战之一。Firefox 主要使用 C 和 C++ 编写。这些语言在安全使用方面臭名昭著,因为任何错误都可能导致程序完全崩溃。我们努力 寻找并消除内存安全漏洞,但我们也在不断发展 Firefox 代码库以在更深层面上解决这些攻击媒介。到目前为止,我们主要集中在两种技术上
一种新的方法
虽然我们在 Firefox 中继续广泛使用沙盒和 Rust,但每种方法都有其局限性。进程级沙盒适用于大型的现有组件,但会消耗大量系统资源,因此必须谨慎使用。Rust 非常轻量级,但重写数百万行现有的 C++ 代码是一个费力费时的过程。
例如 Graphite 字体形状库,Firefox 使用它来正确渲染某些复杂的字体。它太小,不能放到自己的进程中。即使发现内存安全漏洞,即使是站点隔离的进程体系结构也无法阻止恶意字体破坏加载它的页面。同时,重写和维护这种领域特定的代码并不是我们有限的工程资源的理想用途。
因此,今天我们将第三种方法加入到我们的武器库中。 RLBox 是由加州大学圣地亚哥分校、德克萨斯大学奥斯汀分校和斯坦福大学的研究人员开发的一种新的沙盒技术,它使我们能够快速有效地将现有的 Firefox 组件转换为在 WebAssembly 沙盒中运行。得益于 Shravan Narayan、Deian Stefan、Tal Garfinkel 和 Hovav Shacham 的不懈努力,我们已成功地将这项技术集成到我们的代码库中,并用它来对 Graphite 进行沙盒化。
这种隔离将在 Firefox 74 中发布给 Linux 用户,在 Firefox 75 中发布给 Mac 用户,Windows 支持将紧随其后。您可以在来自 UCSD 和 UT 奥斯汀 的新闻稿中以及联合 研究论文 中了解更多有关这项工作的信息。继续阅读以了解我们如何将其集成到 Firefox 的技术概述。
构建 wasm 沙盒
wasm 沙盒背后的核心实现思想是,您可以将 C/C++ 编译成 wasm 代码,然后您可以将该 wasm 代码编译成程序实际运行的机器的本地代码。这些步骤类似于您 在浏览器中运行 C/C++ 应用程序 时所做的操作,但我们是在 Firefox 自身构建时提前执行 wasm 到本地代码的转换。这两个步骤中的每一个都依赖于其自身的软件重要部分,我们添加了第三个步骤来使沙盒转换更加直接且不易出错。
首先,您需要能够将 C/C++ 编译成 wasm 代码。作为 WebAssembly 工作的一部分,wasm 后端被添加到 Clang 和 LLVM 中。但是,仅有编译器是不够的,您还需要 C/C++ 的标准库。此组件通过 wasi-sdk 提供。有了这些部分,我们就足以将 C/C++ 转换为 wasm 代码。
其次,您需要能够将 wasm 代码转换为本地目标文件。当我们刚开始实施 wasm 沙盒时,我们经常被问到:“为什么要进行这一步?您可以分发 wasm 代码,并在 Firefox 启动时在用户的机器上动态编译它。”我们本来可以这样做,但这需要为每个沙盒实例重新编译 wasm 代码。在每个来源都驻留在单独进程的世界中,每个沙盒编译的代码都是不必要的重复。我们选择的方法使多个进程之间能够共享编译后的本地代码,从而节省了大量的内存。这种方法还提高了沙盒的启动速度,这对于细粒度沙盒(例如,对与访问的每个字体或加载的每个图像相关的代码进行沙盒化)非常重要。
使用 Cranelift 及其伙伴进行提前编译
这种方法并不意味着我们必须编写自己的 wasm 到本地代码编译器!我们使用相同的编译器后端来实现这种提前编译,该后端最终将为 Firefox 的 JavaScript 引擎的 wasm 组件提供支持:Cranelift,通过 Bytecode Alliance 的 Lucet 编译器和运行时。这种代码共享确保改进可以同时惠及我们的 JavaScript 引擎和我们的 wasm 沙盒编译器。这两段代码目前出于工程原因使用 Cranelift 的不同版本。但是,随着我们的沙盒技术日趋成熟,我们预计将对其进行修改以使用完全相同的代码库。
现在,我们已经将 wasm 代码转换为本地目标代码,我们需要能够从 C++ 调用该沙盒代码。如果沙盒代码在单独的虚拟机中运行,此步骤将涉及在运行时查找函数名称以及管理与虚拟机相关的状态。但是,在上面的设置中,沙盒代码是尊重 wasm 安全模型的本地编译代码。因此,可以使用与调用常规本地代码相同的机制来调用沙盒函数。我们必须注意尊重所涉及的不同机器模型:wasm 代码使用 32 位指针,而我们的初始目标平台 x86-64 Linux 使用 64 位指针。但是,还有其他障碍需要克服,这将我们引向了转换过程的最后一步。
确保沙盒正确无误
使用与常规本地代码相同的机制调用沙盒代码很方便,但它隐藏了一个重要的细节。我们不能信任从沙盒中传出的任何内容,因为攻击者可能已经破坏了沙盒。
例如,对于沙盒函数
/* Returns values between zero and sixteen. */
int return_the_value();
我们无法保证此沙盒函数遵循其契约。因此,我们需要确保返回值落在我们期望的范围内。
类似地,对于返回指针的沙盒函数
extern const char* do_the_thing();
我们无法保证返回的指针实际指向沙盒控制的内存。攻击者可能已将返回的指针强制指向沙盒之外的应用程序中的某个位置。因此,我们在使用指针之前对其进行验证。
还有一些从源代码中看不出来的运行时约束。例如,上面返回的指针可能指向沙盒中的动态分配的内存。在这种情况下,指针应该由沙盒释放,而不是由主机应用程序释放。我们可以依靠开发人员始终记住哪些是应用程序值,哪些是沙盒值。经验表明,这种方法不可行。
污染数据
以上两个例子指出了一个一般原则:从沙盒返回的数据应该被专门标识为这样的数据。有了这种标识,我们可以确保数据以适当的方式处理。
我们将与沙盒相关的数据标记为“污染”。污染数据可以自由地进行操作(例如,指针运算、访问字段)以生成更多污染数据。但是,当我们将污染数据转换为非污染数据时,我们希望这些操作尽可能地明确。污染不仅对于管理从沙盒返回的内存非常有价值。它对于识别可能需要额外验证的从沙盒返回的数据也很有价值,例如指向某个外部数组的索引。
因此,我们将从沙盒公开的所有函数建模为返回污染数据。这些函数还将污染数据作为参数,因为它们操作的任何内容都必须以某种方式属于沙盒。一旦函数调用具有此接口,编译器就会成为一个污染检查器。当污染数据在需要非污染数据的上下文中使用,反之亦然时,就会发生编译器错误。这些上下文正是需要传播污染数据或需要验证数据的地方。RLBox 处理污染数据的所有细节,并提供使将库的接口逐步转换为沙盒接口变得简单的功能。
后续步骤
有了 wasm 沙盒的核心基础设施,我们可以专注于增加它在整个 Firefox 代码库中的影响,方法是将其引入到所有支持的平台,并将其应用于更多组件。由于这项技术轻量级且易于使用,我们预计在接下来的几个月里,在对 Firefox 的更多部分进行沙盒化方面取得快速进展。我们将最初的努力集中在与 Firefox 捆绑在一起的第三方库上。这些库通常具有明确定义的入口点,并且不会与系统中的其他部分广泛共享内存。但是,将来我们还计划将这项技术应用于第一方代码。
鸣谢
我们对 UCSD、UT 奥斯汀和斯坦福大学的研究合作伙伴的工作深表感谢,他们是这项努力的推动力量。我们还要特别感谢我们在 Bytecode Alliance 的合作伙伴,特别是 Fastly 的工程团队,他们开发了 Lucet 并帮助我们扩展了它的功能,使这个项目成为可能。
关于 Nathan Froyd
Nathan Froyd 是一位在 Firefox 上工作的软件工程师。他编写代码帮助其他人编写代码。在闲暇时间,他喜欢举重和阅读。
9 条评论