作者: 啟明星辰ADLab
原文鏈接:https://mp.weixin.qq.com/s/gqH0lqz1ey6IzT--UD9Jsg

01 研究背景

沙箱作為很多主流應用的安全架構的重要組成部分,將進程限制在一個有限的環境內,避免該進程對磁盤等系統資源進行直接訪問。Chromium 中的沙箱進程通過pipe等方式和具有I/O等高權限的進程交互來完成進一步的操作,因此利用IPC繞過沙箱成為一種常見的方式,而渲染進程和無沙箱的browser進程之間的通信也成為了被關注的重點。Chromium IPC 包括Legacy IPC和 Mojo,本文主要介紹Mojo IPC機制,同時對Mojo IPC經典案例進行跟蹤分析。

02 Mojo IPC介紹

在Mojo的文檔的System Overview,超鏈接[1]中給出了Mojo的定義。

圖片

Mojo 使得IPC通信成為可能。要想使用Mojo(參見超鏈接[2]),首先要定義一個Mojom的文件,這個文件中定義了接口(interface),每個接口中定義了消息(message)。定義mojom文件//services/db/public/mojom/db.mojom:

圖片

添加BUILD.GN目標//services/db/public/mojom/BUILD.gn:

圖片

向需要這個接口的目標添加依賴,這里添加到src/BUILD.gn:

圖片

使用bindings generators 處理mojom文件,默認生成C++代碼,通過指定后綴也可以生成其他語言(js或者java)的綁定。

圖片

mojom和生成的C++綁定代碼的對應如下:

圖片

生成接口文件后,需要定義pipe以及pipe兩端的handle 對象,這樣才能發送消息。接收方如果想對消息進行接收處理,需要對接口進行實現。下面是C++定義pipe的兩種方式:

圖片

logger和receiver分別是pipe兩端的handle對象,分別代表發送端和接收端。此時可以使用logger->Log(“Hello”) 發送消息。接收端想要接收消息,首先要對mojo::PendingReceiver<T>進行綁定,最常見的就是將其綁定為mojo::Receiver<T>。一旦pipe上有可讀的消息,Receiver 就會讀取消息,反序列化消息,然后將該消息派遣到T的實現上。下圖是T的實現:

圖片

如果發送一個消息希望得到返回信息,mojom文件應該像下面這樣:

圖片

生成的C++接口如下:

圖片

發送端在發送消息時,可發送一個回調函數,而接收端在調用該消息的實現時,會在內部調用該回調函數,將這個消息的處理結果再發送給發送端。

圖片

上面以C++綁定作為實例,同理其他語言的綁定。例如js綁定如下:

圖片

在上面的實例中,echoServicePtr 相當于C++的 mojo::Remote<T>為發送端,echoServiceRequest 相當于C++的 mojo::PendingReceiver<T>為接收端。echoServiceBinding 相當于C++的 mojo::Receiver<T>已綁定的接收端,即可以處理接收的消息。

03 沙箱繞過案例分析

chromium的實現采用多進程方式,渲染進程和browser進程間就可以通過使用mojo IPC的方式進行通信。由于browser 是無沙箱運行的,通過與browser 的漏洞的交互,渲染進程就可以穿越沙箱執行任意代碼。下面通過一個經典案例來詳細跟蹤沙箱繞過的過程。

首先,啟用blink 特征參數“ --enable-blink-features=MojoJS,MojoJSTest ”,該參數可以模擬被妥協的渲染進程,使得js 可以直接訪問Mojo。如果有一個真正的妥協的渲染進程,可以通過修改內存直接開啟此功能,使得渲染進程具有MojoJS的能力。

下面是漏洞觸發時對應的現場環境,以及源代碼情況,可以看到,該漏洞是由于render_frame_host 對象被釋放后,由于該對象在FilterInstalledApps方法中被引用并進行連續的方法調用導致的釋放后重用:

圖片

圖片

圖片

InstalledAppProviderImpl 是瀏覽器進程對接口InstalledAppProvider的實現:

圖片

接口的定義文件為third_party/blink/public/mojom/installedapp/installed_app_provider.mojom:

圖片

由此可見,瀏覽器進程中實現了InstalledAppProvider接口,渲染進程通過該接口與瀏覽器進程通信。在Create 靜態方法中接收渲染進程發送的mojo::PendingReceiver。

圖片

圖片

在該方法中使用mojo::MakeSelfOwnedReceiver函數進行接收的綁定。在C++綁定API中查看該函數的使用,該函數會將接口的實現以及Receiver進行綁定。使得實現對象的生命周期和pipe的生命周期相同。一旦綁定的一端檢測到錯誤或者pipe關閉的時候,就會將實現對象回收。

圖片

在綁定的過程中會創建接口的實現實例,實例中保留了RenderFrameHost 對象:

圖片

browser進程保留RenderFrameHost和渲染進程中的框架進行交互。當框架銷毀時對應的RenderFrameHost也會隨之銷毀。

圖片

browser進程在每個框架初始化時,會調用PopulateFrameBinders 將框架對應的接口的創建函數存入map中,表示當前框架可以使用的接口,當某個框架中使用創建某個接口的pipe時,就會在這個map中查找創建接口實現的函數:

圖片

當創建好pipe,并設置好兩端的handle對象,那該pipe就能正常使用收發消息。在處理FilterInstalledApp消息時,會引用接口實現的實例中保留的RenderFrameHost。

圖片

如果在發送消息前,將該RenderFrameHost 對應的框架釋放,會引起釋放后重用。這個漏洞的觸發可以通過navigator.getInstalledRelatedApps不斷的向pipe發送消息,使得析構的框架和消息處理之間競爭,使得某個消息的處理在析構之后。但是非頂端的框架不能直接調用這個API。

圖片

在實際的trigger文件中,通過在子框架中創建pipe,并且接收端綁定為接口InstalledAppProvider,將發送端綁定為一個臨時的全局接口名pwn。

圖片

在主框架中,分配一個子框架,在子框架綁定pwn接口時,使用interceptor.oninterfacerequest 截獲該綁定,創建實際的InstalledAppProvider的發送端。這樣就創建好了pipe和兩端的綁定,也就創建了browser對該接口的實現。這樣主框架就能有效地控制了子框架的pipe消息發送。在實現對象中包含對子框架的引用。在子框架析構后,browser對子框架的引用還在。

圖片

當發送filterInstalledApps的消息時,觸發UAF:

圖片

每次創建一個框架,browser進程會為該框架注冊可使用的接口。當一個框架申請接口時,browser進程會根據注冊的接口查找該接口的創建函數。創建函數需要接收發送端的pendingReceiver,并對其綁定。對pendingReceiver的綁定有多種,在InstalledAppProvider接口中使用mojo::MakeSelfOwnedReceiver將接口的實現和接收端進行綁定。這個函數會將接口的實現對象的生命周期和pipe的生命周期進行捆綁。這個函數在調用的過程中會創建實現對象。在實現對象中保存了框架指針。當收到FilterInstalledApps的消息時,該函數調用了框架指針的虛函數。如果創建了pipe并且綁定完畢,刪除框架,但pipe依然存在。在這個時候發送消息,就會引發框架的釋放后重用。在漏洞觸發上,能夠保證框架在銷毀后,pipe還能保持連接很重要。這樣才有機會觸發漏洞。通過在子框架中創建pipe,并且截獲接口發送端的綁定,將子框架中的發送指針傳遞到主框架中,這樣就保證了pipe的連接。pipe的消息發送也就得到了有效控制。

chrome的每個進程都有一個全局對象CommandLine,這個對象保存了這個進程運行時傳遞的參數,如果能向這個對象中傳遞--no-sandbox參數,當渲染進程重新加載后就會在無沙箱的進程中執行程序。

圖片

從漏洞分析上可以漏洞利用需要首先控制對象的虛表指針。但在沒有泄漏任何地址的情況下,如何控制RenderFrameHost的虛表指針呢?由于在Windows上每次加載chrome.dll的基址基本不變,而這個庫包含了chrome的大部分代碼 ,作為渲染進程和瀏覽器進程共享的庫。實際上結合js的漏洞可以泄漏這個庫的基址。這個庫為漏洞基礎的搭建創造了條件。

對于RenderFrameHost對象的替換,使用RenderFrameHost在調試的版本中對象的大小是0xc38,使用可控大小的blob對象進行占用。

圖片

使用mojo bindings創建blob對象,實現了對blob對象的創建 、釋放、以及數據的讀取。

圖片

因為共有三次連續的虛函數調用,必須保證前一次調用的虛函數返回內容是可控的。這里找到可以返回對象成員的指針,這樣返回內容就是可控的。在這里找到的虛函數如下:

圖片

該函數返回的內容正是在對象偏移8的位置上:

圖片

調用流程如下:

圖片

三次調用的偏移分別為0x48,0x0d0和0x18。

圖片

allocReadable函數中觸發兩次漏洞。在觸發漏洞之前,會將要寫入緩存的內容寫入占位緩存buf1的末端:

圖片

第一次觸發漏洞,控制程序執行調用chrome!content::WebContentsImpl::GetWakeLockContext,該函數調用chrome!content::WakeLockContextHost::WakeLockContextHost創建WakeLockContextHost對象,并將WakeLockContextHost對象的地址寫入占位的buf1+0x10+0x650處。

圖片

圖片

第二次觸發漏洞,控制程序執行調用chrome!`anonymous namespace'::DictionaryIterator::Start,該函數將第一次觸發漏洞時創建的占位的buf1緩存地址寫回到第二次觸發漏洞的占位緩存buf2+0x10+0x18處,這樣就泄漏this指針,從而泄漏寫入內存內容的地址。

圖片

另一個重要的函數是callFunction實現任意函數調用。該函數觸發漏洞,調用函數chrome!content::responsiveness::MessageLoopObserver::DidProcessTask,該函數執行回調:

圖片

圖片

在調用任意函數之前需要偽造bindstate,bindstate的布局如下:

圖片

Polymorphic_invoke 是一個函數指針,該函數負責調用functor。Polymorphic_invoke 必須知道函數參數個數以及參數類型。找到一個可以調用多個參數的invoker實現任意函數調用。

圖片

在函數getCurrentProcessCommandLine中首先泄漏一個堆地址:

圖片

泄漏 current_process_commandline_全局變量:

圖片

圖片

圖片

調用一個具有拷貝功能的函數,將commandline的地址拷貝到泄漏的堆地址上,從而獲得commadline的地址。

圖片

調用SetCommandLineFlagsForSandboxType 關閉沙箱:

圖片

圖片

圖片

結合js漏洞,繞過沙箱,打開本地的記事本。

圖片

04 小結

有關Mojo IPC漏洞,最常見到的就是對象生命周期管理不當所帶來的安全問題。本文結合Mojo的背景知識,對照Mojo的安全問題,深入研究chrome的IPC機制。同時本文跟蹤了Mojo IPC的一個經典漏洞,漏洞的觸發思路從可能的條件競爭到有效控制消息的發送,通過在渲染進程的commandline全局變量中,添加關閉沙箱的選項。

參考鏈接:

[1]https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md#system-overview

[2]https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/mojo/public/cpp/bindings/README.md

[3]https://bugs.chromium.org/p/chromium/issues/detail?id=1062091


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/1947/