閱讀本文之前,您最好了解Android中的Binder機制、用于圖形系統的BufferQueue原理、堆管理器je_malloc的基本原理。
此文介紹了如何利用libcutils庫中的堆破壞漏洞獲得system_server權限,此漏洞是研究Android圖形子系統時發現的,對應的CVE号為CVE-2015-1474和CVE-2015-1528。
1.漏洞代碼的位置
本文次涉及的漏洞位于建立native handle的函數,如下代碼片段所示[1],每一個GraphicBuffer對象都包含一個native handle 的指針,通過native handle,GraphicBuffer可以跨程序共享圖像系統所需的記憶體。
|
|
|
|
|
|
|
|
|
當傳入精心構造的numFds和numInts(如numFds=0xffffffff,numInts=2)到native_handle_create 時,可以導緻表達式”sizeof(native_handle_t) + sizeof(int)*(numFds+numInts)”整數溢出,接下來寫配置設定的緩沖區h則會導緻堆破壞。有兩個函數會調用native_handle_create并寫配置設定的堆記憶體導緻堆破壞。其中有一個影響Android的所有版本,另一個隻影響Android Lollipop以上的版本。
影響所有Android版本的函數如下[2]
|
|
|
|
|
|
|
|
當傳入的numFds和numInts被惡意構造後,h配置設定的緩沖區小于預期的大小,後續的memcpy會導緻堆破壞。隻影響Lollipop以上版本的函數如下,這個函數在所有的Android版本中都存在,不過隻在Android 5.0以上才會被調用,其它版本不能觸發。
|
|
|
|
|
|
|
|
|
|
|
|
read 函數類似與memcpy,後從Parcel中讀取sizeof(int)*numInts到h->data + numFds,因為配置設定給h的堆記憶體小于預期,是以read會導緻堆破壞. 因為unflatten堆破壞時破壞的大小不好控制,我将使用這個函數來介紹此漏洞的利用。這個漏洞能被利用來提權的關鍵是numFds,numInts以及拷貝到native handle中的内容都可以被跨程序控制,進而使得低權限的程序可以注入代碼到高權限程序而達到提權。
2.利用方法
如果A程序可以獲得B程序的帶IGraphicProducer接口的binder代理對象,那麼A程序可以通過跨程序的binder調用利用此漏洞可獲得B程序的權限。成功利用的關鍵函數是IGraphicProducer的setSidebandStream[4]函數。
|
|
|
|
|
|
|
|
|
|
上述代碼描述了BnGraphicBufferProducer如何處理其它程序發來的SET_SIDEBAND_STREAM 事件,從代碼中可以看到,從其他程序傳來的parcel中的資料沒有做任何檢查,直接傳給了readNativeHandle,進而導緻numFds,numInts和其它被拷貝到所配置設定的堆記憶體的資料可以被發起binder調用的程序任意構造,使得利用成為可能。
下圖是攻擊場景圖,低權限程序通過binder調用,利用此漏洞可獲得高權限程序的權限:

Figure1.攻擊場景
3.如何獲得system_server的權限
要獲得system_server的權限需要分三步走,順序不能亂:
第一步,從普通應用注入mediaserver。
第二步,從mediaserver注入surfaceflinger。
第三步,從surfaceflinger注入system_server.
必須按順序注入是因為隻有前一步成功了才能獲得注入後續程序的權限,例如,隻有拿下了mediaserver才能有權限通路surfaceflinger中的一些特殊接口,才有機會注入surfaceflinger
Figure2. SELinux domains
如上圖所示,雖然surfaceflinger也是已系統使用者運作的,但是因為SElinux的存在surfaceflinger是運作在u:r:surfaceflinger:s0域中,而此域除了通路圖形裝置外,其它的權限很少,這是為什麼已經獲得system使用者權限後還要繼續注入system_server的原因。system_server可以看在是Android的“核心“,可以通路的資源很多,如果把root權限比作神的,能注入system_server應該算是“半神“。下圖描述了我們通過調用哪些接口,一步步的獲得system_server的權限。
Figure 3. 三步走獲得system_server權限
如上圖所示,一個普通應用程式通過調用IMediaRecord的querySurfaceMediaSource函數能夠獲得mediaserver程序導出的IGraphicProducer,進而普通應用程式可以注入代碼到mediaserver(詳情見後續章節),因為mediaserver有ACCESS_SURFACE_FLINGER的權限,是以注入到mediaserver中的代碼可以通過調用ISurfaceComposer的createSurface函數獲得surfaceflinger導出的IGraphicProducer接口,然後同過setSidebandStream可以拿下surfaceflinger。surfaceflinger 通過調用IWindowsManager的screenShotApplication 觸發system_server 調用 ISurfaceComposer captureScreen , 這個binder調用會将system_server 導出的一個 IGraphicProducer作為參數傳給surfaceflinger,進而surfaceflinger可以獲得system_server的IGraphicProducer接口,進而拿下system_server.
4.mediaserver注入詳解
我們需要經過三步才能獲得system_server的權限,但是由于NX,ASLR,SELinux,je_malloc和多個binder 服務線程相結合帶來的障礙,每一步都很複雜,本節将以mediaserver為例詳細介紹如何通過libutils的漏洞注入mediaserver. 簡單來說,需要經過5步才能成功。
1)控制binder服務線程
當使用je_malloc時,每一個線程都與一個特定的arena關聯,不同的線程從堆上配置設定記憶體時将使用不同的arena,每個arena又關聯一個或多個chunk,故配置設定給不同的線程的小塊堆記憶體将使用不同的chunk, Android中每個chunk的大小是1MB。圖4展示了je_malloc堆的離散性。
Figure 4.je_malloc中堆的分布
binder 服務線程處理binder代理發起的binder調用,服務線程的調用随着并行的binder調用的增加而增多,一般都有一個最大值。就mediaserver而言,程序剛啟動時binder服務線程的數目是4,最多可以增加到17個,圖5顯示了mediaserver中binder服務線程最多時的狀況.
Figure 5.mediaserver的服務線程
當一個binder調用到達mediaserver時,系統會随機的選擇一個服務線程來處理這個調用,而我們知道,不同的線程配置設定的堆記憶體位于不同的chunk中,是以,如果所有的線程都處于active狀态時,通過binder調用配置設定的記憶體可能位于不同的chunk中,進而使得通過binder調用進行堆風水基本不可能。解決方法是隻讓一個binder服務線程處于active狀态,挂起其餘的所有binder服務線程,進而多次binder調用所配置設定的堆記憶體可以位于同一個chunk中。
IGraphicProducer接口可以通過attachBuffer函數将一個GraphicBuffer加入到bufferqueue.而每一個bufferqueue隻能存儲特定數量的GraphicBuffer, Lollipop中是64個,當bufferqueue中的GraphicBuffer已經達到64個後,如果還調用attachBuffer,處理attachBuffer的binder服務線程将會挂起,一直等到有bufferqueue中GraphicBuffer小于64為止,通過這種方法,可以挂起mediaserver中的16個binder線程,剩下的那個将服務我們發起的攻擊性binder調用。
2) 洩漏堆的内容
因為ASLR的存在,要想利用次漏洞,我們需要從mediaserver中洩漏位址資訊,在je_malloc中,同一個線程中配置設定的相同大小的小塊記憶體将占據相鄰的region(je_malloc中有對region的定義),與dl_malloc不用的是,相鄰region之間沒有任何中繼資料。我們可以通過attachBuffer在mediaserver的堆上生成很多的native handle, native handle的結構如下,native handle 的大小由numFds和numInts決定,正常的native handle中numFds=2,numInts=12,配置設定的堆的大小是80。
(gdb) pt native_handle_t
type = struct native_handle {
int version;
int numFds;
int numInts;
int data[4294967296];
}
我們先通過attachBuffer配置設定多個正常的native handle,然後通過setSidebandStream構造一個畸形的native handle(numFds=-35 ,numInt=64),readNativeHandle會将相鄰的一個正常的native handle的numInts修改為較大的值, 當通過requestBuffer請求傳回被修改的native handle 時,堆内容将會被洩漏。
Figure 6.從mediaserver中洩漏堆内容
3)洩漏棧的基位址
因為NX的存在,我們需要使用ROP來繞過NX,而ROP需要能控制棧内容,所有我們需要知道棧的位置。知道了棧的位置,我們可以通過此漏洞重寫棧,進而可以将堆破壞漏洞轉化為棧重寫進而執行ROP。
我們已經知道如何洩漏堆的内容,是以我們可以在堆上搜尋特定的資料結構來洩漏棧的基址。被搜尋的關鍵資料結構為pthread_internal_t.
0xb652ec8c: 0xb424a080 0xb3b7c080 0x00000b58 0x00000a53
0xb652ec9c: 0xae8dcdb0 0x00000001 0xae7df000 0x000fe000
0xb652ecac: 0x00001000 0x00000000 0x00000000 0x00000000
0xb652ecbc: 0xb6e4700d 0xb3d48960 0x00000000 0xae7dd000
0xb652eccc: 0x00000001 0x00000000 0x00000000 0x00000000
0xb652ecdc: 0x00000000 0x00000000 0x00000000 0x00000000
上述位址範圍是一個pthread_internal_t的内容,從彩色高亮的區域我們可以得知這個結構描述的線程的一些屬性,tid 是 0xb58, pid 是 0xa53, 線程的棧的基址在0xae7df000. 因為這個結構中包含了線程的棧的基址,我們可以在洩漏的堆記憶體中搜尋這樣的結構來得到棧的基址。搜尋的特征為棧大小(0x000fe000) 和guard_size(0x00001000)。每一個通過pthread_create建立出來的binder服務線程都會配置設定一個pthread_internal_t的對象,一般來說,新建立出來的線程會被配置設定一個較大的tid. 因為mediaserver中除了binder服務線程外還有别的線程,别的線程的狀态不好控制,我們需要找到一個binder server線程對應的pthread_internal_t, 我們應該還記得binder server線程可以被動态觸發建立的,這樣就使得binder線程可以有一個較大的tid,我們可以搜尋多個pthread_internal_t的對象,然後選擇其中tid最大的一個,則有很大的機率這個線程是處于挂起狀态的binder服務線程,處于挂起狀态的binder服務線程的調用棧是固定的,如圖7所示,我們可以通過重寫它的傳回位址來觸發ROP的執行.
Figure 7.處于阻塞狀态的binder服務線程
4)洩漏共享庫(so)基址
我們需要子產品的基值來來構造ROP。為了有更多子產品來搜尋ROP,我們可能需要洩漏多個子產品基址。洩漏libui.so的基址很容易。唯一需要做的是通過在洩漏的堆記憶體中搜尋GraphicBuffer對象,GraphicBuffer對象包含一個 android_native_base_的子結構。這個結構中的incRef和decRef是指向libui.so中相應函數的函數指針。找到這個結構就能計算出libui.so的基址。GraphicBuffer具有特定特征,很容易搜尋到。
(gdb) pt/m android_native_base_t
type = struct android_native_base_t {
int magic;
int version;
void *reserved[4];
void (*incRef)(android_native_base_t *);
void (*decRef)(android_native_base_t *);
}
為了比較友善的寫shellcode,最好是能找到libc.so的基址,這樣我們可以在shellcode中調用libc的函數,libui.so的GOT中有memcpy的位址,memcpy為libc.so中的函數,如果能洩漏libui.so的GOT表我們就能獲得libc.so的基位址。由于這個漏洞的局限性,要洩漏libui.so的GOT并不容易。從前面章節得知,我們可以通過修改numFds和numInts來洩漏堆記憶體,但這中方法隻能洩漏連續的記憶體,因為洩漏記憶體的的邏輯是memcpy,如果洩漏的位址空間中有沒有被映射的記憶體,則memcpy直接會發生段錯誤。令一個限制是binder調用隻能傳回小于1MB的記憶體。堆記憶體和libui.so直接一般情況都存在沒有映射的頁面,而且兩者直接的距離也超過了1MB,是以不能通過簡單的修改numFds和numInts來洩漏libui.so的GOT。因為GraphicBuffer包含了native handle的指針,我們可以修改這個指針,使其指向一個僞造的native handle對象,則可以洩漏位址空間中緊跟這個native handle對象後的記憶體内容。洩漏的大小由僞造的numFds和numInts決定。通過這樣的方法,我們同樣可以洩漏libc.so的GOT,進而得到dlopen和dlsym的位址,有了這兩個函數,我們可以直接用進階語言寫shellcode.
5)控制je_malloc下次配置設定記憶體的位置
通過構造特殊的numFds和numInts,我們隻能寫緊臨native handle的連續記憶體,因為je_malloc中棧和堆的位址空間通常不是相鄰的。所有我們需要找到一個重寫棧的方法,也就是将這個漏洞轉換為一個任意位址寫的漏洞。我們可以通過修改je_malloc中的tcache的指針表來實作. 當使用je_malloc并激活了tcache機制時,每一個線程都會為配置設定的小對象維護一個cache,這個cache存儲在一個叫tcache_t的結構中。對每一種特定大小區間的對象,tcache_t都為其維護了一個指針表。在Android的je_malloc實作中,共有31大小區間。見下示gdb輸出
(gdb) p je_small_bin2size_tab
$24 = {8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 640, 768, 896, 1024, 1280, 1536, 1792, 2048, 2560, 3072, 3584}
tcache的指針表存儲在堆上,與其它通過malloc配置設定的堆記憶體混合在一起,是以通過構造numInts 和 numFds可以重寫這些指針。下面所示的gdb輸出為,配置設定大于112,小于等于128的記憶體時所使用的tcache指針表。當通過je_malloc配置設定一個位于此區間的對象時,從avail開始索引為ncached-1的指針将是je_malloc傳回的指針,通過将這個值修改為棧位址,那麼,下一次配置設定特定大小對象,棧位址将被傳回,寫被配置設定的對象記憶體是,棧将被重寫。通過這種方法,我們可以控制棧内容來執行ROP。
(gdb) p je_arenas[0].tcache_ql.qlh_first.tbins[11]
$9 = {tstats = {nrequests = 17}, low_water = 62, lg_fill_div = 1, ncached = 63, avail = 0xb6003f60}
(gdb) x/63xw je_arenas[0].tcache_ql.qlh_first.tbins[11].avail
0xb6003f60: 0xb6057f80 0xb6057f00 0xb6057e80 0xb6057e00
0xb6003f70: 0xb6057d80 0xb6057d00 0xb6057c80 0xb6057c00
0xb6003f80: 0xb6057b80 0xb6057b00 0xb6057a80 0xb6057a00
0xb6003f90: 0xb6057980 0xb6057900 0xb6057880 0xb6057800
0xb6003fa0: 0xb6057780 0xb6057700 0xb6057680 0xb6057600
5.繞過SELinux加載so
因為SELinux的存在,普通應用建立的檔案一般被标記為app_data_file 或 apk_data_file. mediaserver 沒有權限執行帶這些标簽的檔案. 是以不能在shellcode裡使用system或dlopen執行普通應用提供的執行檔案。幸運的是,mediaserver 有execmem 權限。
allow mediaserver self:process execmem; ————>SELinux policy
我們可以通過mprotect修改匿名記憶體為可執行(這在surfaceflinger中是不行的),進而能執行shellcode. 然後在shellcode裡實作從記憶體中加載so的機制(詳見PoC),進而帶 app_data_file 或 apk_data_file的檔案可以以二進制流的形式通過漏洞傳給mediaserver,然後通過load so from memroy子產品加載執行。
Figure 8. 加載共享庫流程
因為mediaserver在Lollipop下沒有執行/system/bin/sh的權限,我通過注入busybox到mediaserver來執行shell指令,如果利用成功,可以得到一個從mediaserver反彈的shell(如圖9),細節請見PoC.
Figure 9. 帶mediaserver權限的shell
6.溢出surfaceflinger和system_server
溢出surfaceflinger 和 system_server與注入mediaserver相似,不夠有兩點需要注意:
1.surfaceflinger 沒有execmem權限,不能使用mprotect修改匿名記憶體為可執行,所有功能隻能用RoP實作.
2.因為system_server和一般應用都是從Zygote fork出來的,子產品位址一樣,不需要洩漏system_server的子產品基位址.
PoC 見https://github.com/secmob/PoCForCVE-2015-1528
[引用]
[1]http://androidxref.com/5.0.0_r2/xref/system/core/libcutils/native_handle.c#29
[2]http://androidxref.com/5.0.0_r2/xref/frameworks/native/libs/ui/GraphicBuffer.cpp#303
[3]http://androidxref.com/5.0.0_r2/xref/frameworks/native/libs/binder/Parcel.cpp#1210
[4]http://androidxref.com/5.0.0_r2/xref/frameworks/native/libs/gui/IGraphicBufferProducer.cpp#403
[5]http://www.phrack.org/issues/68/10.html
原文位址: http://blogs.360.cn/360mobile/2015/08/31/android-libcutils%E5%BA%93%E4%B8%AD%E6%95%B4%E6%95%B0%E6%BA%A2%E5%87%BA%E5%AF%BC%E8%87%B4%E7%9A%84%E5%A0%86%E7%A0%B4%E5%9D%8F%E6%BC%8F%E6%B4%9E%E7%9A%84%E5%8F%91%E7%8E%B0%E4%B8%8E%E5%88%A9%E7%94%A8/