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 下载时除了chromium本体以外还需求将其pdb符号也一同下载 下载好后直接将pdb符号文件与exe履行文件解压放在一同即可 最终用windbg验证是否能够正常查找函数 注:下载以上内容都需求署理
缝隙剖析
POC
因为poc目录结构比较复杂,直接给出完好poc下载地址(需求署理):bugs.chromium.org
下载解压后能够得到两个html文件,其中trigger.html为咱们需求的poc
然后测验触发缝隙,依据阐明得知chrome默认不会启用mojo,想要启用有两种办法:
一、在命令行发动chromium时加上 --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进行调试
在调试开端前因为当时工作目录的问题需求将poc代码中以下两处途径进行一些改动
然后用.childdbg 1
敞开子进程调试
之后经过几个ntdll!LdrpDoDebuggerBreak
后就会触发crash
缝隙剖析
经过调查反常信息可判别此处并非缝隙触发的榜首现场,运用gflags.exe敞开页堆(+hpa)与仓库跟踪(+ust)并在发动chrome时增加–no-sandbox参数进行调试剖析会发现溃散点会转移到前一句代码 再结合代码能够判别产生溃散的地方是在获取render_frame_host_目标虚表 运用!address检查该render_frame_host_目标内存信息会发现该内存已被开释 经过调查发现render_frame_host_目标在InstalledAppProviderImpl目标在结构时被初始化 对content::InstalledAppProviderImpl::Create函数下断,当履行到以下内容时将会创立InstalledAppProviderImpl目标 而render_frame_host_保存在InstalledAppProviderImpl目标0x8偏移处 再结合poc能够确认InstalledAppProviderImpl目标是在sub frame调用bindInterface进行接口绑守时创立的 在之后的poc履行中,父帧会经过MojoInterfaceInterceptor拦截并获取子帧的句柄 获取后便会调用body.removeChild删除子帧 最终会经过filterInstalledApps函数去调用现已被开释的render_frame_host_目标的虚函数 总结poc的履行顺序大致为:
- 经过window.location.hash判别是否是子帧
- 如果是子帧就去履行Mojo.bindInterface
- 如果是父帧就去创立子帧并用MojoInterfaceInterceptor拦截子帧的Mojo.bindInterface到并将其句柄传递给父帧
- 开释子帧
- 运用filterInstalledApps去调用现已被开释但却仍然还留有悬挂指针的render_frame_host_虚函数
缝隙运用
敞开Mojo
上文中提到过chrome默认不能直接调用mojo,所以此处运用cve 2021-21224来合作敞开mojo。 经过剖析可知mojoJS的敞开与封闭主要由RenderFrameImpl类成员变量enabled_bindings_与IsMainFrame函数来决议 IsMainFrame函数的逻辑很简单就仅仅将一个类成员变量回来 而经过调试也可知当enabled_bindings_ & 2不为0时即可满意条件 也就是说此刻只需求将enabled_bindings_修正为2,再将is_main_frame_修正为1即可满意条件敞开mojo。 而在一个页面中可能会存在多个frame,而这些frame所对应的RenderFrameImpl目标都存储在一个全局变量g_frame_map中 要查找到全局变量g_frame_map,就需求先获取到chrome.dll的基址,运用21224结构的地址走漏函数与读写原语,走漏window目标地址,再从window目标中获取到一个坐落chrome.dll模块中的地址,再用该地址减去必定的偏移来得到chrome.dll模块基址,除此以外还能够用特征码查找的办法,这种办法兼容性会更好,但在我的环境下读写原语在进行频繁的读写操作时会产生反常产生溃散,详细原因暂时未知,所以权且运用减去固定偏移获取基址的办法。 之后因为无法直接经过g_frame_map符号在windbg中运用x来查找其地址,那就经过查找调用过该全局变量的函数来查找 之后在windbg中查找RenderFrame::ForEach并检查其汇编代码获取到g_frame_map地址为00007ffe`3d927888,用此值减去chrome基址得到偏移为0x7627888,只需运用chrome基址加0x7627888即可得到g_frame_map地址 g_frame_map变量8-16偏移处存放着一个链式结构,当只要一个frame时 创立sub frame后 而其对应的RenderFrameImpl目标保存在红线划出内存地址的0x28偏移处 再经过调查content::RenderFrameImpl::DidCreateScriptContext函数来获取相关变量在目标中的偏移,enabled_bindings_偏移为0x560IsMainFrame函数中用到的have_context_变量偏移为0x88将g_frame_map中保存的一切RenderFrameImpl目标相应偏移修正为对应的值即可。但要留意的是在我的缝隙环境( 81.0.4044.0)中,在获取成员变量enabled_bindings_时需求将g_frame_map中拿到的RenderFrameImpl目标地址加0x68再加enabled_bindings_地点偏移,而IsMainFrame中用到的成员变量就在g_frame_map中拿到的RenderFrameImpl目标的0x88偏移处。
内存收回
关于uaf缝隙运用的榜首步肯定是将此内存进行收回,而进行内存收回的条件就是先需求知道被开释的render_frame_host_占多大内存,经过前面的调试剖析得知render_frame_host_为RenderFrameHostImpl类实例,所以能够先对RenderFrameHostImpl结构函数下断,而实例巨细从结构函数是看不出来的,但能够从调用该实例结构函数的函数中看到。 经过kb栈回溯检查调用RenderFrameHostImpl结构函数的函数为RenderFrameHostFactory::Create 经过检查该函数可知render_frame_host_目标巨细为0xC38字节 在知道了要收回的内存巨细后就能够经过创立一系列的Blob来收回该内存
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所以此刻仍然会触发溃散 所以此刻需求填入相应的函数地址,确保在履行GetProcess与GetBrowserContest两个虚函数时不会产生溃散,并在履行IsOffTheRecord时能够走漏堆地址。 经过查找能够首先找到一个符合条件的函数ChromeMainDelegate::CreateContentClient,此函数会将this+8处地址回来给调用者,能够将此函数地址填入堆喷占位的数据中,在调用GetProcess与GetBrowserContext虚函数时就回去调用此函数。 再查找到ChromeMainDelegate类虚表检查虚表得知ChromeMainDelegate::CreateContentClient函数地址存放在起虚表的0x70偏移处。 而InstalledAppProviderImpl::FilterInstalledApps在调用虚函数GetProcess时会从内存中获取一个地址将其加0x48并在此处获取一个函数去履行,所以能够将ChromeMainDelegate虚表地址+(0x70-0x48)填入堆喷数据中,当InstalledAppProviderImpl::FilterInstalledApps去调用GetProcess时就会转入ChromeMainDelegate::CreateContentClient函数 在ChromeMainDelegate::CreateContentClient函数履行后会将堆喷数据地址+8偏移处的地址读出并再读出该地址0xD0偏移处的地址并调用,此处对应GetBrowserContext虚函数调用。于是能够将ChromeMainDelegate虚表地址-(0xD0-0x70)填入堆喷数据中当GetBrowserContext被调用时会再次转入ChromeMainDelegate::CreateContentClient函数 最终在调用虚函数IsOffTheRecord时需求找到一个能够走漏堆地址的函数填入相应方位,经过查找找到符合条件的虚函数content::WebContentsImpl::GetWakeLockContext,因为此函数还会将this指针填入堆地址+0x8偏移处,所以也能够为后续的this地址走漏提供方便。 此函数会创立一块内存用作目标内存,并会将此内存地址写入this+0x10+0x650偏移处,也就是堆喷占位数据的0x660偏移处
但要留意的是content::WebContentsImpl::GetWakeLockContext函数会先去判别this+0x10+0x650偏移处是否为0,如果为0才干够进行创立堆内存并写入this+0x10+0x650的操作
经过以上操作,在经过render_frame_host_->GetProcess()->GetBrowserContext()->IsOffTheRecord()
后就能够在堆喷占位数据的0x660偏移处得到一个需求的堆地址
this地址走漏
因为在上一步操作中现已走漏了堆地址而且还将this指针写入了堆地址+0x8偏移处,所以能够运用前面走漏堆地址的思路将UAF缝隙再触发一次,并把之前拿到的走漏的堆地址写入堆喷占位数据的对应偏移处即可获取到this指针,因为前面的缝隙运用this指针正好指向咱们可控的堆喷占位数据,拿到了this地址也就得到了当时可控数据的地址。
继续将ChromeMainDelegate::CreateContentClient函数放入GetProcess与GetBrowserContext函数对应的调用方位,现在只需求再找到一个符合条件能够将this指针从堆地址中获取到的函数,经过查找找到anonymous namespace'::DictionaryIterator::Start
函数正好符合要求。
结合调试再经过与走漏堆地址相同再次触发UAF缝隙便可得到this指针
沙盒逃逸
沙河逃逸的思路比较简单,经过回调去履行SetCommandLineFlagsForSandboxType函数将–no-sandbox参数增加到current_process_commandline_中。 首先需求找到一个能够调用回调函数的虚函数,经过查找找到content::responsiveness::MessageLoopObserver::DidProcessTask函数 现在再找到一个能够传递多个参数的回调函数,相似如下方式的 然后将SetCommandLineFlagsForSandboxType函数地址填入被走漏了地址的buffer的相应偏移处就能够将沙箱封闭,但调用SetCommandLineFlagsForSandboxType函数还需求先得到全局变量current_process_commandline_ 经过extensions::SizeConstraints::set_minimum_size函数将current_process_commandline_中保存的指针复制进前文中现已被走漏地址的可控地址中。 最终调用SetCommandLineFlagsForSandboxType函数,将–no-sandbox(0)标志增加进全局变量current_process_commandline_中 最终生成新的渲染器过程(例如,运用iframe到其他受控原点或敞开新的Tab),并再次运用渲染器缝隙运用(改写)即可成功。
总结
- 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,但此办法直接在本地履行时会出现跨域的问题需求起一个服务器去拜访履行。