深入解析Firefox中的JavaScript进程间通信:攻击与防御
Firefox使用进程间通信(IPC)来实现权限分离,这是其安全架构的重要基石。之前的博客文章主要关注了IPC的C++端模糊测试,而本文将探讨JavaScript中的IPC,它被用于用户界面的各个部分。首先,我们将简要回顾多进程架构以及即将到来的Project Fission变化,这是Firefox实现站点隔离的方案。然后,我们将研究两种不同的JavaScript IPC模式,并解释如何调用它们。使用Firefox的开发者工具(DevTools),我们将能够调试浏览器本身。
掌握这些知识后,我们将重新审视一个沙箱逃逸漏洞,该漏洞在2019年被用于针对Coinbase的0day攻击,并报告为CVE-2019-11708。这个0day漏洞在博客文章和公开可用的漏洞利用中得到了广泛覆盖。我们认为这个漏洞提供了一个很好的案例研究,其底层技术将有助于识别类似问题。最终,通过发现更多沙箱逃逸漏洞,您可以帮助保护数亿Firefox用户,作为Firefox漏洞赏金计划的一部分。
多进程架构的现在与未来
截至2021年4月,Firefox使用一个特权进程来启动其他进程类型并协调活动。这些类型包括Web内容进程、半特权Web内容进程(用于特殊网站如accounts.firefox.com或addons.mozilla.org)以及四种实用进程,用于Web扩展、GPU操作、网络或媒体解码。在这里,我们将重点关注主进程(也称为“父进程”)与多个Web进程(或“内容”进程)之间的通信。
Firefox正在转向新的安全架构以实现站点隔离,从“每个标签页一个进程”转向“每个站点一个进程”架构。
左:当前Firefox通常将标签页分组到自己的进程中。右:启用Fission的Firefox,将每个站点分离到自己的进程中。
父进程充当代理和可信用户界面主机。某些功能,如我们的设置页面about:preferences,本质上是托管在父进程中的网页(使用HTML和JavaScript)。此外,各种控制功能,如模态对话框、表单自动填充或本机用户界面部分(例如,
让我们看看JSActors和MessageManager,这是从JavaScript使用进程间通信(IPC)的两种最常见模式:
JSActors
使用JSActor是JS代码在进程之间通信的首选方法。JSActors总是成对出现——一个实现在子进程中,对应部分在父进程中。每个对都有一个单独的父实例,以便紧密且一致地将消息与特定内容窗口(JSWindowActors)或子进程(JSProcessActors)关联。
由于所有JSActors都是懒加载的,我们建议至少执行一次实现的功能,以确保它们都存在,并允许平滑的测试和调试体验。
基于JSActors构建的进程间通信,实现为FooParent和FooChild。
上面的示例图显示了一对名为FooParent和FooChild的JSActors。通过调用FooChild发送的消息只会被FooParent接收。子实例可以通过sendAsyncMessage(“someMessage”, value)发送一次性消息。如果需要响应(包装在Promise中),它可以通过sendQuery(“someMessage”, value)发送查询。
父实例必须实现一个receiveMessage(msg)函数来处理所有传入消息。请注意,消息在特定actor之间是命名空间绑定的,因此FooChild可以发送名为Bar:DoThing的消息,但永远无法到达BarParent。以下是一些示例代码(永久链接,2021年3月25日修订版),说明了消息在父进程中是如何处理的。
JSActor中receiveMessage函数的代码示例。
如图所示,PromptParent有一个receiveMessage处理程序(第127行),并将消息数据传递给其他函数,这些函数将决定在何处以及如何从父进程打开提示。像这样的消息处理程序及其被调用者是不受信任数据流入父进程的来源,并为深入审计提供了逻辑入口点。
Message Managers
在Project Fission的架构更改之前,大多数父子IPC通过MessageManagers系统进行。有多个消息管理器,包括每进程消息管理器和每标签页加载的内容帧消息管理器。
在这个系统下,两个进程中的JS都会使用addMessageListener方法注册消息监听器,并使用sendAsyncMessage发送消息,这些消息有一个名称和实际内容。为了帮助在整个代码库中跟踪消息,它们的名称通常以使用的组件为前缀(例如,SessionStore:restoreHistoryComplete)。
与JSActors不同,Message Managers需要冗长的初始化,使用addMessageListener,并且不绑定在一起。这意味着消息对所有监听相同消息名称的类可用,并且可以分散在整个代码库中。
使用MessageManager的进程间通信。
截至2021年4月下旬,我们的AddonsManager——处理WebExtensions安装到Firefox的代码——正在使用MessageManager API:
使用MessageManager API的receiveMessage函数代码示例。
设置MessageManager的代码(永久链接到确切修订版)看起来与JSActor的设置非常相似,不同之处在于消息可以同步使用,如子进程中的sendSyncMessage调用所示。除了缺乏懒加载之外,您可以假设相同的安全考虑:就像上面的JSActors一样,receiveMessage函数是不受信任信息从子进程流入父进程的地方,因此应该成为额外审查的焦点。
最后,如果您想实时检查MessageManager流量,可以使用我们的日志框架,并在运行Firefox时将环境变量MOZ_LOG设置为MessageManager:5。这将把所有进程接收到的消息记录到shell中,让您更好地了解发送的内容和时间。
检查、调试和模拟JavaScript IPC
自然,源代码审计receiveMessage处理程序最好与测试相结合。因此,让我们讨论如何在子进程中调用这些函数,并将JavaScript调试器附加到父进程。这允许我们模拟一个我们已经完全控制子进程的场景。为此,我们建议您下载并测试Firefox Nightly,以确保您正在测试最新代码——它还将使您与https://searchfox.org上的最新修订版代码搜索保持同步。为了获得最佳体验,我们建议您立即下载Firefox Nightly,并逐步跟随博客文章的这部分内容。
DevTools设置——父进程
首先,设置您的Firefox Nightly以启用浏览器调试。请注意,启用浏览器调试的说明可能会随时间变化,因此最好与MDN上的调试浏览器说明进行交叉检查。
打开开发者工具,点击右上角的“···”按钮,找到设置。在右下角的高级设置中,勾选以下选项:
- 启用浏览器chrome和附加组件调试工具箱
- 启用远程调试
在Firefox开发者工具中启用浏览器调试。
重新启动Firefox Nightly并打开浏览器调试器(工具 -> 浏览器工具 -> 浏览器工具箱)。这将打开一个新窗口,看起来与常见的DevTools非常相似。
这是您的父进程调试器(即,浏览器工具箱 = 父工具箱)。
帧选择器按钮,位于三个球“···”的左侧,将允许您在窗口之间选择。选择browser.xhtml,这是主浏览器窗口。切换到调试窗格将让您搜索文件并找到要调试的父actor,只要它们已经被加载。为了确保PromptParent actor已正确初始化,在例如https://example.com上打开一个新标签页,并从正常的DevTools控制台调用alert(1)。
使用Firefox开发者工具在Firefox的父进程中命中断点(左)。
您现在应该能够找到PromptParent.jsm(Ctrl+P)并为所有未来的调用设置调试器断点(见上图)。这将允许您检查和复制传递给父进程中Prompt JSActor的典型参数。
注意:一旦命中断点,您可以在开发者控制台中输入代码,这些代码然后在当前拦截的函数中执行。
DevTools设置——子进程
现在我们知道如何检查和获取父进程为Prompt:Open期望的参数,让我们尝试从调试的子进程中触发它:确保您在一个典型的网页上,如https://example.com,这样您就能获得正确类型的内容子进程。然后,通过工具菜单,找到“浏览器内容工具箱”。这里的“内容”指的是子进程(内容工具箱 = 子工具箱)。
由于每个内容进程可能有许多相同站点的窗口与之关联,我们需要找到当前窗口。这个代码段假设它是第一个标签页,并获取该标签页的Prompt actor:
|
|
现在我们有了actor,我们可以使用在父进程中收集的数据发送完全相同的数据。或者,可能是一个变体:
|
|
从Firefox开发者工具调用JavaScript IPC(右下)并观察效果(右上)。
在这种情况下,我们完全没有发送合理的promptPrincipal值。这肯定不适用于所有消息处理程序。为了这篇博客文章,我们可以假设Principal是Origin的实现(对于背景阅读,我们推荐我们两系列博客文章“理解Firefox中的Web安全检查”中的Principal对象解释:参见第一部分和第二部分)。
如果您想知道为什么允许内容进程发送可能任意的Principal(例如,来源):这是当前已知的限制,将在我们实现完整站点隔离的过程中修复(bug 1505832)。
如果您想尝试发送另一个伪造的来源——可能来自不同的网站,或者可能是最特权的Principal——绕过所有安全检查的SystemPrincipal,您可以使用这些代码段替换IPC消息中的promptPrincipal:
|
|
请注意,在调试版本中已经强制执行进程和站点之间的关联验证。如果您编译了自己的Firefox,这将导致内容进程崩溃。
重新审视先前的安全问题
现在我们已经设置了环境,可以重新审视上面提到的安全漏洞:CVE-2019-11708。
问题本身是一个典型的逻辑错误:易受攻击的代码版本没有在父进程中切换打开哪个提示,而是接受了内部提示页面的URL,该页面实现为XHTML页面。但通过调用此消息,攻击者可以导致父进程打开任何Web托管的页面。这允许他们在父进程中重新打开他们的内容进程漏洞利用,并升级到完全妥协。
让我们看看安全修复的差异,以了解我们如何替换易受攻击的逻辑,并在父进程中处理提示类型切换(源代码永久链接)。
修复CVE-2019-11708前后处理不受信任的message.data。
您会注意到第140行以上曾经接受并使用名为uri的参数。这在多个补丁中得到了修复。除了只允许某些对话框在父进程中打开之外,我们还通常禁止在父进程中打开Web URL。
如果您想自己尝试,下载67.0.4之前的Firefox版本,并尝试发送带有任意URL的Prompt:Open消息。
下一步
在这篇博客文章中,我们介绍了使用JavaScript进行Firefox IPC的方法,以及如何分别使用内容工具箱和浏览器工具箱调试子进程和父进程。使用此设置,您现在能够模拟完全妥协的子进程,审计源代码中的消息传递,并分析跨多个进程的运行时行为。
如果您已经熟悉模糊测试,并想分析JavaScript中的高级概念如何序列化和反序列化以传递进程边界,请查看我们之前关于模糊测试Firefox IPC层的博客文章。
如果您有兴趣大规模测试和分析源代码,您可能还想查看我们为所有Firefox版本发布的CodeQL数据库。
如果您想了解更多关于我们的开发人员如何将旧版MessageManager接口移植到JSActors的信息,您可以再次查看我们的JSActors文档,以及Mike Conley如何在他的Joy of Coding直播流第204集中移植弹出窗口阻止程序。
最后,我们在Mozilla对您可能用这些技术发现的漏洞非常感兴趣——比如混淆代理攻击,其中父进程可以被欺骗以使用其特权,而内容进程不应该能够这样做(例如,读取/写入文件系统上的任意文件)或UXSS类型攻击,以及漏洞利用缓解措施的绕过。请注意,截至2021年4月,我们尚未强制执行完整的站点隔离。允许冒充另一个站点的漏洞尚不符合赏金条件。通过我们的漏洞赏金计划提交您的发现,并在@attackndefense Twitter账户上关注我们以获取更多更新。