媒體框架是安卓系統(tǒng)組件中經(jīng)常被發(fā)現(xiàn)安全漏洞的組件,所以每次谷歌發(fā)布月度例行更新時(shí)經(jīng)常會有它的身影。Google最近發(fā)現(xiàn)的媒體框架的漏洞是遠(yuǎn)程代碼執(zhí)行漏洞,攻擊者可以制作特定的文件利用特權(quán)進(jìn)程執(zhí)行任意代碼。目前Google已將其命名為CVE-2018-9411,危險(xiǎn)等級定位危急,并在7月安全更新(2018-07-01補(bǔ)丁)中對其進(jìn)行了修補(bǔ),包括9月安全更新(2018-09-01補(bǔ)丁)中的一些附加補(bǔ)丁。
我還為此漏洞編寫了一個(gè)概念驗(yàn)證利用,演示了如何使用它來從常規(guī)非特權(quán)應(yīng)用程序的上下文中提升權(quán)限。
本文,我將介紹該漏洞和利用此漏洞的技術(shù)細(xì)節(jié)。首先我將介紹與漏洞相關(guān)的一些背景信息,然后再詳細(xì)介紹漏洞本身。在介紹如何利用此漏洞的過程中,我將選擇一個(gè)特定服務(wù)作為攻擊目標(biāo),而不是受漏洞影響的其他服務(wù)。另外,我還將分析與漏洞相關(guān)的一些服務(wù)。最后,我將介紹我編寫的概念驗(yàn)證漏洞利用的詳細(xì)信息。
Project Treble
什么是Project Treble?簡單的說就是谷歌為了整理安卓的碎片化,為了讓手機(jī)廠商適配安卓版本更輕松,推出的新架構(gòu)。
Project Treble對Android內(nèi)部運(yùn)作方式進(jìn)行了大量更改,其中的一個(gè)巨大的變化是許多系統(tǒng)服務(wù)的分離。以前,Android服務(wù)包含AOSP(Android開源項(xiàng)目)和供應(yīng)商代碼。在Project Treble出現(xiàn)之后,這些服務(wù)會被分為一個(gè)AOSP服務(wù)和一個(gè)或多個(gè)供應(yīng)商服務(wù),稱為HAL服務(wù)。更多背景信息,請點(diǎn)此。
HIDL
Project Treble的服務(wù)的分離增加了IPC(進(jìn)程間通信)的量,以前在AOSP和供應(yīng)商代碼之間的同一進(jìn)程中傳遞的數(shù)據(jù),現(xiàn)在必須通過AOSP和HAL服務(wù)之間通過IPC。由于Android中的大多數(shù)IPC都要經(jīng)過Binder,谷歌決定新的IPC也應(yīng)該這樣做。
但僅僅使用現(xiàn)有的Binder代碼是滿足不了新的IPC的,Google決定對其進(jìn)行一些修改。首先,Google引入了多個(gè)Binder域,以便將這種新型IPC與其他域分開。更重要的是,他們引入了HIDL,這是一種通過Binder IPC傳遞的數(shù)據(jù)的全新格式。這種新格式由一組新的庫支持,專用于AOSP和HAL服務(wù)之間的IPC新Binder域,其他Binder域仍使用舊格式。
與舊的HIDL格式相比,新HIDL格式的操作有點(diǎn)像層,新舊兩種情況下的底層都是Binder內(nèi)核驅(qū)動程序,但頂層是不同的。對于HAL和AOSP服務(wù)之間的通信,使用新的庫;對于其他類型的通信,使用舊的庫。這兩種庫包含的代碼都非常相似,以至于新的HIDL庫中某些原始代碼會直接從舊庫中復(fù)制到。雖然每個(gè)庫的用法并不完全相同(你不能簡單地用一個(gè)替換另一個(gè)),但它們?nèi)匀环浅O嗨啤?/span>
這兩組庫都以c++對象的形式表示Binder事務(wù)中傳輸?shù)臄?shù)據(jù),從相對簡單的對象(比如表示字符串的對象)到更復(fù)雜的實(shí)現(xiàn)(比如文件描述符或?qū)ζ渌⻊?wù)的引用),這意味著HIDL為許多類型的對象引入了新的實(shí)現(xiàn)方式。
共享內(nèi)存
Binder IPC的一個(gè)重要功能就是可以共享內(nèi)存,為了保持簡單性和良好性能,Binder將每個(gè)事務(wù)限制為最大1MB。對于進(jìn)程希望通過Binder在彼此之間共享大量數(shù)據(jù)的情況,使用共享內(nèi)存。
為了通過Binder共享內(nèi)存,進(jìn)程利用Binder的共享文件描述符的功能。使用mmap可以將文件描述符映射到內(nèi)存,這允許多個(gè)進(jìn)程通過共享一個(gè)文件描述符來共享同一個(gè)內(nèi)存區(qū)域,常規(guī)Linux(非Android)的一個(gè)問題是,文件描述符通常由文件支持,如果進(jìn)程想要共享匿名內(nèi)存區(qū)域怎么辦?出于這個(gè)原因,Android采用了Ashmem匿名共享內(nèi)存機(jī)制,它允許進(jìn)程在沒有涉及實(shí)際文件的情況下分配內(nèi)存,來備份文件描述符。
是否是通過Binder共享內(nèi)存處理對象,是HIDL和舊庫之間的一個(gè)區(qū)別。在這兩種情況下,最終操作都是相同的,一個(gè)進(jìn)程將ashmem文件描述符映射到其內(nèi)存空間,通過Binder將該文件描述符傳輸?shù)搅硪粋(gè)進(jìn)程,而另一個(gè)進(jìn)程將其映射到自己的內(nèi)存空間。不過,在處理對象的實(shí)現(xiàn)方式上是不同的。
在HIDL的情況下,共享內(nèi)存的一個(gè)重要對象是hidl_memory,如源代碼中所述:“hidl_memory是一種結(jié)構(gòu),可以用于在進(jìn)程之間傳輸共享內(nèi)存”。
漏洞介紹
讓我們來看看hidl_memory的組成內(nèi)容:

其中mHandle是一個(gè)句柄,它是一個(gè)HIDL對象,它包含文件描述符(在本文所舉的樣本中只有一個(gè)文件描述符)。mSize 表示要共享的內(nèi)存大小,mName應(yīng)該代表內(nèi)存的類型,但是只有ashmem類型與此相關(guān)。
當(dāng)通過HIDL中的Binder傳輸這樣的結(jié)構(gòu)時(shí),復(fù)雜對象(比如hidl_handle或hidl_string)有自己的用于寫入和讀取數(shù)據(jù)的自定義代碼,而簡單類型(比如整數(shù))則沒有自定義代碼。這意味著代碼大小會被轉(zhuǎn)換為64位整數(shù),而在舊的庫中,則使用32位整數(shù)。
這看起來很奇怪,為什么內(nèi)存的大小應(yīng)該是64位?為什么不像舊的庫那樣,用32位進(jìn)程處理這個(gè)問題呢?讓我們看一下映射hidl_memory對象(用于ashmem類型)的代碼:

其中,沒有任何關(guān)于32位進(jìn)程的內(nèi)容,甚至沒有提到64位進(jìn)程。
那其中到底發(fā)生了什么?mmap簽名中的length字段的類型是size_t,這意味著它的位數(shù)與進(jìn)程的位數(shù)相匹配。在64位進(jìn)程中沒有問題,一切都只是64位。另一方面,在32位進(jìn)程中,大小被截?cái)酁?2位,因此僅使用較低的32位。
這意味著,如果32位進(jìn)程接收到大小大于UINT32_MAX(0xFFFFFFFF)的hidl_memory,則實(shí)際的映射內(nèi)存區(qū)域?qū)⒉粔蛴谩@纾瑢τ诖笮?x100001000的hidl_memory,內(nèi)存區(qū)域的大小將僅為0x1000。在這種情況下,如果32位進(jìn)程是基于hidl_memory大小執(zhí)行邊界檢查,它們將會失敗,因?yàn)樗鼈儗㈠e(cuò)誤地表明內(nèi)存區(qū)域跨越的范圍超過整個(gè)內(nèi)存空間,這就是漏洞。
尋找攻擊目標(biāo)
現(xiàn)在我們試著找到一個(gè)攻擊目標(biāo),尋找符合以下標(biāo)準(zhǔn)的HAL服務(wù):
1.編譯為32位;
2.把對共享內(nèi)存的接收作為輸入;
3.在共享內(nèi)存上執(zhí)行邊界檢查時(shí),不會截?cái)啻笮 @纾韵麓a不容易受到攻擊,因?yàn)樗鼘財(cái)嗟膕ize_t執(zhí)行邊界檢查:
以上都是此漏洞的基本要求,但我認(rèn)為還有一些更重要的要求:
4.在AOSP中有默認(rèn)實(shí)現(xiàn),雖然供應(yīng)商最終會負(fù)責(zé)所有HAL服務(wù),但AOSP確實(shí)包含某些供應(yīng)商可以使用的默認(rèn)實(shí)現(xiàn)。我發(fā)現(xiàn)在許多情況下,當(dāng)存在這樣的實(shí)現(xiàn)時(shí),供應(yīng)商不愿意修改它,只是按原樣使用它。這使得這樣的目標(biāo)更有趣,因?yàn)樗赡芘c多個(gè)供應(yīng)商相關(guān),而不是特定于某個(gè)供應(yīng)商的服務(wù)。
你應(yīng)該注意的一件事是,盡管HAL服務(wù)應(yīng)該只能由其他系統(tǒng)服務(wù)訪問,但事實(shí)并非如此。有一些精選的HAL服務(wù)實(shí)際上可以由常規(guī)的非特權(quán)應(yīng)用程序訪問。因此,最后一個(gè)要求是:
5.可以從無特權(quán)的應(yīng)用程序直接訪問,否則漏洞利用將實(shí)現(xiàn)不了,下面我們將討論的一個(gè)漏洞,只有在你已經(jīng)破壞了另一個(gè)服務(wù)的情況下才能訪問它。
幸運(yùn)的是,我找到了一個(gè)滿足所有這些要求的HAL服務(wù):android.hardware.cas,又稱為MediaCasService。
CAS
CAS代表?xiàng)l件訪問系統(tǒng),簡單來說它與DRM類似。簡單地說,它的功能與DRM相同,有需要解密的加密數(shù)據(jù)。
MediaCasService
首先,MediaCasService確實(shí)允許應(yīng)用程序解密加密數(shù)據(jù)。如果你閱讀我以前的文章,就會知道我是如何利用名為MediaDrmServer的服務(wù)中的漏洞。你可能會奇怪,我為什么要與DRM進(jìn)行比較?因?yàn)镸ediaCasService與MediaDrmServer(負(fù)責(zé)解密DRM媒體的服務(wù))從其API到內(nèi)部運(yùn)行方式都非常相似。
需要注意的是,MediaDrmServer這個(gè)API被稱為descramble,而不是decrypt(盡管它們最終也會在內(nèi)部對其進(jìn)行解密)。
讓我們看看descramble是如何運(yùn)作的:

不出所料,數(shù)據(jù)通過共享內(nèi)存共享,有一個(gè)緩沖區(qū)指示共享內(nèi)存的相關(guān)部分(稱為srcBuffer,但是對于源和目標(biāo)都是相關(guān)的)。在此緩沖區(qū)上,服務(wù)從其中讀取源數(shù)據(jù)以及將目標(biāo)數(shù)據(jù)寫入的位置都存在偏移量。此時(shí),源數(shù)據(jù)不是加密的,在這種情況下,服務(wù)只需將數(shù)據(jù)從源復(fù)制到目標(biāo),而無需修改它。
這看起來很脆弱,至少,如果服務(wù)僅使用hidl_memory大小來驗(yàn)證它是否完全適合共享內(nèi)存,而不是其他參數(shù),則會如此。在這種情況下,通過讓服務(wù)相信我們的小內(nèi)存區(qū)域跨越了它的整個(gè)內(nèi)存空間,我們就可以繞過邊界檢查,并將源和目標(biāo)偏移量放在我們喜歡的任何地方。這將使我們能夠?qū)Ψ⻊?wù)內(nèi)存進(jìn)行完整的讀寫訪問,因?yàn)槲覀兛梢詮娜魏蔚胤阶x取到共享內(nèi)存,從共享內(nèi)存寫入任何地方。注意,負(fù)偏移量也應(yīng)利用此漏洞,因?yàn)榧词故?xFFFFFFFF(-1)也會小于hidl_memory大小。
讓我們通過查看descramble的代碼來驗(yàn)證這一點(diǎn),請注意,函數(shù)validateRangeForSize只檢查“first_param + second_param

可以看到,代碼根據(jù)hidl_memory大小檢查srcBuffer是否位于共享內(nèi)存中。在此之后,不再使用hidl_memory,其余的檢查將針對srcBuffer本身執(zhí)行。至此,為了實(shí)現(xiàn)完整的讀寫訪問,我們需要做的就是使用這個(gè)漏洞,然后將srcBuffer的大小設(shè)置為大于0xFFFFFFFF。這樣,源和目標(biāo)偏移量的任何值都是有效的。

使用漏洞進(jìn)行越界讀取

使用漏洞進(jìn)行越界寫入
TEE設(shè)備
在使用這個(gè)原語編寫漏洞之前,讓我們先想好這個(gè)漏洞要實(shí)現(xiàn)的目標(biāo)。查看此服務(wù)的SELinux規(guī)則,就可以看到它實(shí)際上受到嚴(yán)格限制,并且沒有很多權(quán)限。不過,它還有一個(gè)普通的非特權(quán)應(yīng)用程序沒有的有趣權(quán)限,就是對TEE(可信執(zhí)行環(huán)境)設(shè)備的訪問。
此權(quán)限非常有趣,因?yàn)樗试S攻擊者訪問各種各樣的內(nèi)容,比如不同供應(yīng)商的不同設(shè)備驅(qū)動程序、不同的信任區(qū)域操作系統(tǒng)和大量信任。在我之前的文章中,我已經(jīng)討論過這個(gè)權(quán)限有多危險(xiǎn)了。
雖然訪問TEE設(shè)備確實(shí)可以驗(yàn)證很多事情,但我只想證明我可以獲得此訪問權(quán)限。因此,我的目標(biāo)是執(zhí)行一個(gè)需要訪問TEE設(shè)備的簡單操作。在Qualcomm TEE設(shè)備驅(qū)動程序中,有一個(gè)相當(dāng)簡單的ioctl,用于查詢設(shè)備上運(yùn)行的QSEOS版本。因此,構(gòu)建MediaCasService漏洞時(shí)的目標(biāo)是運(yùn)行此ioctl并獲取其結(jié)果。
漏洞利用
到目前為止,我們對目標(biāo)進(jìn)程內(nèi)存進(jìn)行了完全讀取和寫入。雖然這是一個(gè)很好的開始,但有兩個(gè)問題需要解決:
1.ASLR:雖然我們有完全的讀訪問權(quán)限,但它只與共享內(nèi)存映射的位置相關(guān)。我們并不知道它與內(nèi)存中的哪些數(shù)據(jù)進(jìn)行比較。理想情況下,我們希望找到共享內(nèi)存的地址以及其他有趣數(shù)據(jù)的地址。
2.漏洞在每次執(zhí)行時(shí),共享內(nèi)存都會被映射,然后在操作后取消映射。不能保證每次都將共享內(nèi)存映射到同一個(gè)位置,在執(zhí)行期間完全有可能會有另一個(gè)內(nèi)存區(qū)域取代原來的映射位置。
讓我們看一下這個(gè)特定構(gòu)建的服務(wù)內(nèi)存空間中鏈接器的一些內(nèi)存映射:

如上說示,鏈接器恰好在linker_alloc_small_objects和linker_alloc之間創(chuàng)建了2個(gè)內(nèi)存頁(0x2000)的小差距。這些存儲器映射的地址相對較高,此進(jìn)程加載的所有庫都映射到較低的地址。這意味著這個(gè)差距是內(nèi)存中最高的差距。由于mmap的行為是嘗試將低地址映射到高地址,因此任何映射2頁或更少內(nèi)存區(qū)域的嘗試都應(yīng)映射到此差距中。幸運(yùn)的是,該服務(wù)通常不會映射這么小的內(nèi)容,這意味著這個(gè)差距應(yīng)該留在那里。這就解決了我們的第二個(gè)問題,因?yàn)檫@是內(nèi)存中的確定性位置,我們的共享內(nèi)存將始終映射在這個(gè)位置。讓我們直接查看差距之后的linker_alloc中的數(shù)據(jù):

這里的鏈接器數(shù)據(jù)恰好對我們有用,它包含的地址可以很容易的指示linker_alloc內(nèi)存區(qū)域的地址。由于漏洞提供了相對讀取,并且我們已經(jīng)得出結(jié)論,共享內(nèi)存將在linker_alloc之前被直接映射,因此我們可以使用它來確定共享內(nèi)存的地址。如果我們?nèi)∑屏繛?x40的地址并將其減少0x10,就將得到linker_alloc地址,減少共享內(nèi)存本身的大小將導(dǎo)致共享內(nèi)存地址。
到目前為止,我們解決了第二個(gè)問題,但第一個(gè)問題只是部分解決了。雖然我們確實(shí)有共享內(nèi)存的地址,但沒有其他有趣數(shù)據(jù)的地址,我們感興趣的其他數(shù)據(jù)還有哪些呢?
劫持一個(gè)線程
MediaCasService API的一部分功能是客戶端為事件提供監(jiān)測的能力,如果客戶端提供偵聽器,則會在發(fā)生不同CAS事件時(shí)通知它。客戶端也可以自己觸發(fā)事件,然后將其發(fā)送回偵聽器。Binder和HIDL的工作方式是,當(dāng)服務(wù)向偵聽器發(fā)送事件時(shí),它將等待偵聽器完成對事件的處理,等待偵聽器的線程將被阻塞。

觸發(fā)事件的流程
此時(shí),我們可以在已知的預(yù)定線程中阻止服務(wù)中的線程發(fā)生阻塞。一旦我們有一個(gè)處于這種狀態(tài)的線程,就可以修改它的堆棧來劫持它,只有在我們完成后,才能通過完成處理事件來恢復(fù)線程。不過,我們?nèi)绾卧趦?nèi)存中找到線程堆棧?
由于我們的確定性共享內(nèi)存地址很高,該地址與阻塞線程堆棧的可能位置之間的距離很大。由于ASLR的影響,試圖從確定性地址相對地查找線程堆棧太不可靠,所以我們使用了另一種方法,即嘗試使用更大的共享內(nèi)存,并在阻塞的線程堆棧之前映射它,這樣我們就能夠通過漏洞訪問它。
此時(shí),我們得到多個(gè)(5)線程,而不是只有一個(gè)線程處于阻塞狀態(tài)。這會導(dǎo)致創(chuàng)建更多線程,并分配更多線程堆棧。通過執(zhí)行此操作,如果內(nèi)存中存在少量線程堆棧大小的空白,則應(yīng)填充它們,并且阻塞線程中的至少一個(gè)線程堆棧應(yīng)映射到低地址,而不在其之前映射到任何庫。 mmap是在低地址之前映射高地址的區(qū)域,然后,理想情況下,如果我們使用大型共享內(nèi)存,則應(yīng)在此之前進(jìn)行映射。

填充差距并映射共享內(nèi)存后的MediaCasService內(nèi)存映射
不過缺點(diǎn)是,有可能其他意想不到的內(nèi)容(比如jemalloc heap)可能會被映射到其中,因此被阻塞的線程堆棧將不會是我們期望的。可能有多種方法可以解決這個(gè)問題。我決定簡單地利用服務(wù)崩潰(使用漏洞來寫入未映射的地址)再試一次,因?yàn)槊看畏⻊?wù)崩潰時(shí)它都會重新啟動。無論如何,這種情況通常不會發(fā)生,即使發(fā)生了,一次重試通常就足夠了。
一旦我們的共享內(nèi)存在被阻塞的線程堆棧之前被映射,我們就可以使用該漏洞從線程堆棧中讀取兩種地址:
1.線程堆棧地址,使用pthread元數(shù)據(jù),它位于堆棧本身之后的同一內(nèi)存區(qū)域中。
2.libc映射到的地址,以便稍后使用libc中的gadget 框架和符號構(gòu)建ROP鏈(libc具有足夠的gadget 框架)。我們通過讀取libc中特定位置的返回地址來實(shí)現(xiàn)這一點(diǎn),libc位于線程堆棧中。

從線程堆棧讀取的數(shù)據(jù)
至此,我們就可以使用漏洞讀取和寫入線程堆棧。由于我們既有確定性共享內(nèi)存位置的地址,也有線程堆棧的地址,因此通過使用地址之間的差異,我們可以從共享內(nèi)存(具有確定性位置的小內(nèi)存)到達(dá)線程堆棧。
ROP鏈
我們可以完全訪問我們可以恢復(fù)的被阻塞的線程堆棧,因此下一步是執(zhí)行ROP鏈。由于我們要準(zhǔn)確知道ROP鏈覆蓋堆棧的哪個(gè)部分,因此必須時(shí)刻關(guān)注線程被阻塞的確切狀態(tài)。覆蓋部分堆棧后,我們可以恢復(fù)線程,從而執(zhí)行ROP鏈。
遺憾的是,SELinux對此過程的限制使我們無法將此ROP鏈完全轉(zhuǎn)換為任意代碼來執(zhí)行。沒有execmem權(quán)限,因此無法將匿名內(nèi)存映射為可執(zhí)行文件,并且我們無法控制可以映射為可執(zhí)行文件的文件類型。在本文的示例中,目標(biāo)非常簡單(運(yùn)行單個(gè)ioctl),所以我只是編寫了一個(gè)ROP鏈來執(zhí)行此操作。從理論上講,如果你想要執(zhí)行更復(fù)雜的操作,那仍然可以利用這個(gè)原語。例如,如果你想根據(jù)函數(shù)的結(jié)果執(zhí)行復(fù)雜的邏輯,你可以執(zhí)行多階段ROP:執(zhí)行一個(gè)運(yùn)行該函數(shù)的ROP鏈并將其結(jié)果寫入某處,讀取結(jié)果,執(zhí)行復(fù)雜的邏輯,然后基于此運(yùn)行另一個(gè)ROP鏈。
如上所述,由于目標(biāo)是獲得QSEOS版本,下面是ROP鏈執(zhí)行的代碼。

stack_addr是堆棧內(nèi)存區(qū)域的地址,它只是一個(gè)我們知道的可寫的地址,不會被覆蓋(堆棧從底部開始,不靠近頂部),所以我們可以將結(jié)果寫入該地址然后使用此漏洞讀取它。在最后的休眠時(shí),線程不會在運(yùn)行ROP鏈后立即崩潰,所以我們可以讀取結(jié)果。
構(gòu)建ROP鏈本身非常簡單, libc中有足夠的gadget來執(zhí)行它,所有的符號也都在libc中,且我們已經(jīng)擁有了libc的地址。
完成漏洞利用后,我們就完成了劫持一個(gè)線程來執(zhí)行ROP鏈,因此進(jìn)程處于一個(gè)不穩(wěn)定的狀態(tài)。為了使所有內(nèi)容都處于不被感染的狀態(tài),我們只是使用漏洞(通過寫入未映射的地址)使服務(wù)崩潰,以便讓它重新啟動。
總結(jié)
正如我之前的文章所講的那樣,雖然谷歌宣稱Project Treble有利于Android的安全性,但我們在本文中所找到的這個(gè)漏洞,就可以說明Project Treble并不是無懈可擊的。這個(gè)漏洞本身就是Project Treble的一個(gè)組成部分,且它不存在于以前的源代碼庫中,僅僅出現(xiàn)在新庫中。由于這個(gè)漏洞會出現(xiàn)在一個(gè)常用的庫中,因此它影響了許多高權(quán)限服務(wù)。
GitHub上提供了完整的漏洞利用代碼,注意:本文所講的漏洞僅用于教育或防御目的,它不適用于任何惡意或攻擊性用途。
漏洞發(fā)現(xiàn)的時(shí)間脈絡(luò)
2018.5.3:發(fā)現(xiàn)漏洞;
2018.5.7:我們將漏洞詳情及 PoC反饋給Google;
2018.7.2:Google發(fā)布了一組補(bǔ)丁;
2018.7.13:谷歌要求我們推遲發(fā)布此文章;
2018.9.4:Google發(fā)布了一組額外的補(bǔ)丁;
|