影響Android多個高許可權服務的嚴重漏洞詳情披露(CVE-2018-9411)
媒體框架是安卓系統元件中經常被發現安全漏洞的元件,所以每次谷歌釋出月度例行更新時經常會有它的身影。Google最近發現的媒體框架的漏洞是遠端程式碼執行漏洞,攻擊者可以製作特定的檔案利用特權程序執行任意程式碼。目前Google已將其命名為CVE-2018-9411,危險等級定位危急,並在7月安全更新( ofollow,noindex" target="_blank">2018-07-01補丁 )中對其進行了修補,包括9月安全更新( 2018-09-01補丁 )中的一些附加補丁。
我還為此漏洞編寫了一個概念驗證利用,演示瞭如何使用它來從常規非特權應用程式的上下文中提升許可權。
本文,我將介紹該漏洞和利用此漏洞的技術細節。首先我將介紹與漏洞相關的一些背景資訊,然後再詳細介紹漏洞本身。在介紹如何利用此漏洞的過程中,我將選擇一個特定服務作為攻擊目標,而不是受漏洞影響的其他服務。另外,我還將分析與漏洞相關的一些服務。最後,我將介紹我編寫的概念驗證漏洞利用的詳細資訊。
Project Treble
什麼是Project Treble?簡單的說就是谷歌為了整理安卓的碎片化,為了讓手機廠商適配安卓版本更輕鬆,推出的新架構。
Project Treble對Android內部運作方式進行了大量更改,其中的一個巨大的變化是許多系統服務的分離。以前,Android服務包含AOSP(Android開源專案)和供應商程式碼。在Project Treble出現之後,這些服務會被分為一個AOSP服務和一個或多個供應商服務,稱為HAL服務。更多背景資訊,請 點此 。
HIDL
Project Treble的服務的分離增加了IPC(程序間通訊)的量,以前在AOSP和供應商程式碼之間的同一程序中傳遞的資料,現在必須通過AOSP和HAL服務之間通過IPC。由於Android中的大多數IPC都要經過Binder,谷歌決定新的IPC也應該這樣做。
但僅僅使用現有的Binder程式碼是滿足不了新的IPC的,Google決定對其進行一些修改。首先,Google引入了多個Binder域,以便將這種新型IPC與其他域分開。更重要的是,他們引入了HIDL,這是一種通過Binder IPC傳遞的資料的全新格式。這種新格式由一組新的庫支援,專用於AOSP和HAL服務之間的IPC新Binder域,其他Binder域仍使用舊格式。
與舊的HIDL格式相比,新HIDL格式的操作有點像層,新舊兩種情況下的底層都是Binder核心驅動程式,但頂層是不同的。對於HAL和AOSP服務之間的通訊,使用新的庫;對於其他型別的通訊,使用舊的庫。這兩種庫包含的程式碼都非常相似,以至於新的HIDL庫中某些原始程式碼會直接從舊庫中複製到。雖然每個庫的用法並不完全相同(你不能簡單地用一個替換另一個),但它們仍然非常相似。
這兩組庫都以c++物件的形式表示Binder事務中傳輸的資料,從相對簡單的物件(比如表示字串的物件)到更復雜的實現(比如檔案描述符或對其他服務的引用),這意味著HIDL為許多型別的物件引入了新的實現方式。
共享記憶體
Binder IPC的一個重要功能就是可以共享記憶體,為了保持簡單性和良好效能,Binder將每個事務限制為最大1MB。對於程序希望通過Binder在彼此之間共享大量資料的情況,使用共享記憶體。
為了通過Binder共享記憶體,程序利用Binder的共享檔案描述符的功能。使用mmap可以將檔案描述符對映到記憶體,這允許多個程序通過共享一個檔案描述符來共享同一個記憶體區域,常規Linux(非Android)的一個問題是,檔案描述符通常由檔案支援,如果程序想要共享匿名記憶體區域怎麼辦?出於這個原因,Android採用了Ashmem匿名共享記憶體機制,它允許程序在沒有涉及實際檔案的情況下分配記憶體,來備份檔案描述符。
是否是通過Binder共享記憶體處理物件,是HIDL和舊庫之間的一個區別。在這兩種情況下,最終操作都是相同的,一個程序將ashmem檔案描述符對映到其記憶體空間,通過Binder將該檔案描述符傳輸到另一個程序,而另一個程序將其對映到自己的記憶體空間。不過,在處理物件的實現方式上是不同的。
在HIDL的情況下,共享記憶體的一個重要物件是hidl_memory,如 原始碼中 所述:“hidl_memory是一種結構,可以用於在程序之間傳輸共享記憶體”。
漏洞介紹
讓我們來看看hidl_memory的組成內容:
其中mHandle是一個控制代碼,它是一個HIDL物件,它包含檔案描述符(在本文所舉的樣本中只有一個檔案描述符)。mSize 表示要共享的記憶體大小,mName應該代表記憶體的型別,但是隻有ashmem型別與此相關。
當通過HIDL中的Binder傳輸這樣的結構時,複雜物件(比如hidl_handle或hidl_string)有自己的用於寫入和讀取資料的自定義程式碼,而簡單型別(比如整數)則沒有自定義程式碼。這意味著程式碼大小會被轉換為64位整數,而在舊的庫中,則使用32位整數。
這看起來很奇怪,為什麼記憶體的大小應該是64位?為什麼不像舊的庫那樣,用32位程序處理這個問題呢?讓我們看一下對映hidl_memory物件(用於ashmem型別)的程式碼:
其中,沒有任何關於32位程序的內容,甚至沒有提到64位程序。
那其中到底發生了什麼?mmap簽名中的length欄位的型別是size_t,這意味著它的位數與程序的位數相匹配。在64位程序中沒有問題,一切都只是64位。另一方面,在32位程序中,大小被截斷為32位,因此僅使用較低的32位。
這意味著,如果32位程序接收到大小大於UINT32_MAX(0xFFFFFFFF)的hidl_memory,則實際的對映記憶體區域將不夠用。例如,對於大小為0x100001000的hidl_memory,記憶體區域的大小將僅為0x1000。在這種情況下,如果32位程序是基於hidl_memory大小執行邊界檢查,它們將會失敗,因為它們將錯誤地表明記憶體區域跨越的範圍超過整個記憶體空間,這就是漏洞。
尋找攻擊目標
現在我們試著找到一個攻擊目標,尋找符合以下標準的HAL服務:
1.編譯為32位;
2.把對共享記憶體的接收作為輸入;
3.在共享記憶體上執行邊界檢查時,不會截斷大小。例如,以下程式碼不容易受到攻擊,因為它對截斷的size_t執行邊界檢查:
以上都是此漏洞的基本要求,但我認為還有一些更重要的要求:
4.在AOSP中有預設實現,雖然供應商最終會負責所有HAL服務,但AOSP確實包含某些供應商可以使用的預設實現。我發現在許多情況下,當存在這樣的實現時,供應商不願意修改它,只是按原樣使用它。這使得這樣的目標更有趣,因為它可能與多個供應商相關,而不是特定於某個供應商的服務。
你應該注意的一件事是,儘管HAL服務應該只能由其他系統服務訪問,但事實並非如此。有一些精選的HAL服務實際上可以由常規的非特權應用程式訪問。因此,最後一個要求是:
5.可以從無特權的應用程式直接訪問,否則漏洞利用將實現不了,下面我們將討論的一個漏洞,只有在你已經破壞了另一個服務的情況下才能訪問它。
幸運的是,我找到了一個滿足所有這些要求的HAL服務:android.hardware.cas,又稱為MediaCasService。
CAS
CAS代表條件訪問系統,簡單來說它與DRM類似。簡單地說,它的功能與DRM相同,有需要解密的加密資料。
MediaCasService
首先,MediaCasService確實允許應用程式解密加密資料。如果你閱讀我 以前的文章 ,就會知道我是如何利用名為MediaDrmServer的服務中的漏洞。你可能會奇怪,我為什麼要與DRM進行比較?因為MediaCasService與MediaDrmServer(負責解密DRM媒體的服務)從其API到內部執行方式都非常相似。
需要注意的是,MediaDrmServer這個API被稱為descramble,而不是decrypt(儘管它們最終也會在內部對其進行解密)。
讓我們看看descramble是如何運作的:
不出所料,資料通過共享記憶體共享,有一個緩衝區指示共享記憶體的相關部分(稱為srcBuffer,但是對於源和目標都是相關的)。在此緩衝區上,服務從其中讀取源資料以及將目標資料寫入的位置都存在偏移量。此時,源資料不是加密的,在這種情況下,服務只需將資料從源複製到目標,而無需修改它。
這看起來很脆弱,至少,如果服務僅使用hidl_memory大小來驗證它是否完全適合共享記憶體,而不是其他引數,則會如此。在這種情況下,通過讓服務相信我們的小記憶體區域跨越了它的整個記憶體空間,我們就可以繞過邊界檢查,並將源和目標偏移量放在我們喜歡的任何地方。這將使我們能夠對服務記憶體進行完整的讀寫訪問,因為我們可以從任何地方讀取到共享記憶體,從共享記憶體寫入任何地方。注意,負偏移量也應利用此漏洞,因為即使是0xFFFFFFFF(-1)也會小於hidl_memory大小。
讓我們通過檢視descramble的程式碼來驗證這一點,請注意,函式validateRangeForSize只檢查“first_param + second_param <= third_param”,而忽略可能的溢位。
可以看到,程式碼根據hidl_memory大小檢查srcBuffer是否位於共享記憶體中。在此之後,不再使用hidl_memory,其餘的檢查將針對srcBuffer本身執行。至此,為了實現完整的讀寫訪問,我們需要做的就是使用這個漏洞,然後將srcBuffer的大小設定為大於0xFFFFFFFF。這樣,源和目標偏移量的任何值都是有效的。
使用漏洞進行越界讀取
使用漏洞進行越界寫入
TEE裝置
在使用這個原語編寫漏洞之前,讓我們先想好這個漏洞要實現的目標。檢視此服務的 SELinux規則 ,就可以看到它實際上受到嚴格限制,並且沒有很多許可權。不過,它還有一個普通的非特權應用程式沒有的有趣許可權,就是對TEE(可信執行環境)裝置的訪問。
此許可權非常有趣,因為它允許攻擊者訪問各種各樣的內容,比如不同供應商的不同裝置驅動程式、不同的信任區域作業系統和大量信任。在我之前的 文章 中,我已經討論過這個許可權有多危險了。
雖然訪問TEE裝置確實可以驗證很多事情,但我只想證明我可以獲得此訪問許可權。因此,我的目標是執行一個需要訪問TEE裝置的簡單操作。在Qualcomm TEE裝置驅動程式中,有一個相當簡單的ioctl,用於查詢裝置上執行的QSEOS版本。因此,構建MediaCasService漏洞時的目標是執行此ioctl並獲取其結果。
漏洞利用
到目前為止,我們對目標程序記憶體進行了完全讀取和寫入。雖然這是一個很好的開始,但有兩個問題需要解決:
1.ASLR:雖然我們有完全的讀訪問許可權,但它只與共享記憶體對映的位置相關。我們並不知道它與記憶體中的哪些資料進行比較。理想情況下,我們希望找到共享記憶體的地址以及其他有趣資料的地址。
2.漏洞在每次執行時,共享記憶體都會被對映,然後在操作後取消對映。不能保證每次都將共享記憶體對映到同一個位置,在執行期間完全有可能會有另一個記憶體區域取代原來的對映位置。
讓我們看一下這個特定構建的服務記憶體空間中連結器的一些記憶體對映:
如上說示,連結器恰好在linker_alloc_small_objects和linker_alloc之間建立了2個記憶體頁(0x2000)的小差距。這些儲存器對映的地址相對較高,此程序載入的所有庫都對映到較低的地址。這意味著這個差距是記憶體中最高的差距。由於mmap的行為是嘗試將低地址對映到高地址,因此任何對映2頁或更少記憶體區域的嘗試都應對映到此差距中。幸運的是,該服務通常不會對映這麼小的內容,這意味著這個差距應該留在那裡。這就解決了我們的第二個問題,因為這是記憶體中的確定性位置,我們的共享記憶體將始終對映在這個位置。
讓我們直接檢視差距之後的linker_alloc中的資料:
這裡的連結器資料恰好對我們有用,它包含的地址可以很容易的指示linker_alloc記憶體區域的地址。由於漏洞提供了相對讀取,並且我們已經得出結論,共享記憶體將在linker_alloc之前被直接對映,因此我們可以使用它來確定共享記憶體的地址。如果我們取偏移量為0x40的地址並將其減少0x10,就將得到linker_alloc地址,減少共享記憶體本身的大小將導致共享記憶體地址。
到目前為止,我們解決了第二個問題,但第一個問題只是部分解決了。雖然我們確實有共享記憶體的地址,但沒有其他有趣資料的地址,我們感興趣的其他資料還有哪些呢?
劫持一個執行緒
MediaCasService API的一部分功能是客戶端為事件提供監測的能力,如果客戶端提供偵聽器,則會在發生不同CAS事件時通知它。客戶端也可以自己觸發事件,然後將其傳送回偵聽器。Binder和HIDL的工作方式是,當服務向偵聽器傳送事件時,它將等待偵聽器完成對事件的處理,等待偵聽器的執行緒將被阻塞。
觸發事件的流程
此時,我們可以在已知的預定執行緒中阻止服務中的執行緒發生阻塞。一旦我們有一個處於這種狀態的執行緒,就可以修改它的堆疊來劫持它,只有在我們完成後,才能通過完成處理事件來恢復執行緒。不過,我們如何在記憶體中找到執行緒堆疊?
由於我們的確定性共享記憶體地址很高,該地址與阻塞執行緒堆疊的可能位置之間的距離很大。由於ASLR的影響,試圖從確定性地址相對地查詢執行緒堆疊太不可靠,所以我們使用了另一種方法,即嘗試使用更大的共享記憶體,並在阻塞的執行緒堆疊之前對映它,這樣我們就能夠通過漏洞訪問它。
此時,我們得到多個(5)執行緒,而不是隻有一個執行緒處於阻塞狀態。這會導致建立更多執行緒,並分配更多執行緒堆疊。通過執行此操作,如果記憶體中存在少量執行緒堆疊大小的空白,則應填充它們,並且阻塞執行緒中的至少一個執行緒堆疊應對映到低地址,而不在其之前對映到任何庫。 mmap是在低地址之前對映高地址的區域,然後,理想情況下,如果我們使用大型共享記憶體,則應在此之前進行對映。
填充差距並對映共享記憶體後的MediaCasService記憶體對映
不過缺點是,有可能其他意想不到的內容(比如jemalloc heap)可能會被對映到其中,因此被阻塞的執行緒堆疊將不會是我們期望的。可能有多種方法可以解決這個問題。我決定簡單地利用服務崩潰(使用漏洞來寫入未對映的地址)再試一次,因為每次服務崩潰時它都會重新啟動。無論如何,這種情況通常不會發生,即使發生了,一次重試通常就足夠了。
一旦我們的共享記憶體在被阻塞的執行緒堆疊之前被對映,我們就可以使用該漏洞從執行緒堆疊中讀取兩種地址:
1.執行緒堆疊地址,使用pthread元資料,它位於堆疊本身之後的同一記憶體區域中。
2.libc對映到的地址,以便稍後使用libc中的gadget 框架和符號構建ROP鏈(libc具有足夠的gadget 框架)。我們通過讀取libc中特定位置的返回地址來實現這一點,libc位於執行緒堆疊中。
從執行緒堆疊讀取的資料
至此,我們就可以使用漏洞讀取和寫入執行緒堆疊。由於我們既有確定性共享記憶體位置的地址,也有執行緒堆疊的地址,因此通過使用地址之間的差異,我們可以從共享記憶體(具有確定性位置的小記憶體)到達執行緒堆疊。
ROP鏈
我們可以完全訪問我們可以恢復的被阻塞的執行緒堆疊,因此下一步是執行ROP鏈。由於我們要準確知道ROP鏈覆蓋堆疊的哪個部分,因此必須時刻關注執行緒被阻塞的確切狀態。覆蓋部分堆疊後,我們可以恢復執行緒,從而執行ROP鏈。
遺憾的是,SELinux對此過程的限制使我們無法將此ROP鏈完全轉換為任意程式碼來執行。沒有execmem許可權,因此無法將匿名記憶體對映為可執行檔案,並且我們無法控制可以對映為可執行檔案的檔案型別。在本文的示例中,目標非常簡單(執行單個ioctl),所以我只是編寫了一個ROP鏈來執行此操作。從理論上講,如果你想要執行更復雜的操作,那仍然可以利用這個原語。例如,如果你想根據函式的結果執行復雜的邏輯,你可以執行多階段ROP:執行一個執行該函式的ROP鏈並將其結果寫入某處,讀取結果,執行復雜的邏輯,然後基於此執行另一個ROP鏈。
如上所述,由於目標是獲得QSEOS版本,下面是ROP鏈執行的程式碼。
stack_addr是堆疊記憶體區域的地址,它只是一個我們知道的可寫的地址,不會被覆蓋(堆疊從底部開始,不靠近頂部),所以我們可以將結果寫入該地址然後使用此漏洞讀取它。在最後的休眠時,執行緒不會在執行ROP鏈後立即崩潰,所以我們可以讀取結果。
構建ROP鏈本身非常簡單, libc中有足夠的gadget來執行它,所有的符號也都在libc中,且我們已經擁有了libc的地址。
完成漏洞利用後,我們就完成了劫持一個執行緒來執行ROP鏈,因此程序處於一個不穩定的狀態。為了使所有內容都處於不被感染的狀態,我們只是使用漏洞(通過寫入未對映的地址)使服務崩潰,以便讓它重新啟動。
總結
正如我之前的文章所講的那樣,雖然谷歌宣稱Project Treble有利於Android的安全性,但我們在本文中所找到的這個漏洞,就可以說明Project Treble並不是無懈可擊的。這個漏洞本身就是Project Treble的一個組成部分,且它不存在於以前的原始碼庫中,僅僅出現在新庫中。由於這個漏洞會出現在一個常用的庫中,因此它影響了許多高許可權服務。
GitHub 上提供了完整的漏洞利用程式碼,注意:本文所講的漏洞僅用於教育或防禦目的,它不適用於任何惡意或攻擊性用途。
漏洞發現的時間脈絡
2018.5.3:發現漏洞;
2018.5.7:我們將漏洞詳情及 PoC反饋給Google;
2018.7.2: Google釋出了一組補丁 ;
2018.7.13:谷歌要求我們推遲釋出此文章;
2018.9.4: Google釋出了一組額外的補丁 ;