作者: Qixun Zhao(@S0rryMybad) of Qihoo 360 Vulcan Team
博客:《IPC Voucher UaF Remote Jailbreak Stage 2》

在今年11月份的天府杯比賽中,我演示了iPhoneX 最新iOS系統的遠程越獄,這篇文章講述的是這個exploit chain的stage 2.這里我用到的是一個沙盒內可以直接到達的內核漏洞(I name it Chaos),所以在取得Safari的RCE以后,我們可以直接從Safari沙盒內觸發這個漏洞,最終達到遠程越獄.

在這里文章中,我將會放出Chaos的PoC,并且會詳細講解(面向初學者)如何在A12上取得tfp0的exploit細節,但是我不會放出exploit的代碼,如果你想要越獄,這需要你自己去完成exploit的代碼或者等待越獄社區去開發.同時,我不會提及post exploit的利用細節,請把這個任務交給越獄社區.

這是在比賽前夕錄取的在iPhoneX最新系統上利用Chaos進行rjb DEMO:http://v.youku.com/v_show/id_XNDAyNjM1Mjk0OA==.html

0x0 基礎知識:

如果你不是初學者或者對這部分不感興趣,請直接跳過.

0x01 關于port的概念

在蘋果的內核中,port是一個十分重要的概念,并且易學難精(特別是它的引用計數關系),如果已經完全弄懂了port到底是怎樣的東西,你已經是iOS內核中的佼佼者.

簡單來說,port是一個單向傳輸通道,在內核中對應的內核對象是ipc_port,只能有一個接收方,但是可以有多個發送方.請記住是單向,不是雙向,因為只能有一個接收方,如果你想發送消息給一個port,前提你需要有這個port的send right,這些right的信息保存在進程相關的ipc entry table中,所以right的信息是每個進程獨立的,即使它們表示的port是同一個.正因為這個原因,所以port的權限可以在每個進程隔離.但是需要注意的是ipc_port這個內核對象是共享的,如果表示的內核port是同一個,所有的ipc entry都是指向同一個port,這也方便了port進程間的共享.

port有兩種重要的作用,第一種是用于進程間通訊,第二種是用于表示一個內核對象,相當于windows中的句柄.第二種是第一種的特殊情況,也就是當port的接收方是|ipc_space_kernel|的時候.如果你想對一個內核對象進行操作,前提你需要有這個內核對象對應的port的send right.

所謂的tfp0就是task for pid 0, pid 0對應的是內核進程, 因為task也是一個內核對象,所以它可以用port來表示,如果取得了pid 0的task port的send right, 就可以利用這個task port調用各種進程的內核API,通過這些API可以達到內核任意地址讀寫.

0x02 關于MIG

在蘋果的代碼中,有一種稱為MIG的代碼,這是根據defs文件自動生成的代碼,里面一般會做一些內核間對象的轉換(例如port到內核對象)以及對象引用計數的管理,然后調用真正的內核函數.如果kernel代碼編寫人員不熟悉defs的含義或者MIG對對象引用計數的管理,在這個MIG包裹的真正內核API中不適當地管理內核對象的引用計數,是很容易產生引用計數的泄露或者double free.

0x1 漏洞發現過程與細節

在一開始的時候,我看到這樣一段代碼,注意這不是最終的漏洞:

image.png

我們可以發現,在semaphore非空的時候,每一個路徑都調用了|semaphore_dereference|,除了|!=task|那個路徑,所以直覺告訴我,無論MIG的代碼是怎樣的,這里面肯定會有一個路徑會發生引用計數的泄露.經過瀏覽MIG的函數后,我發現確實|!=task|的路徑發生了引用計數的泄露,這個在iOS12之前是可以利用的,并且在沙箱內可以出發,只不過需要很久的時間去觸發引用計數的溢出,意義不大.并且在最新版已經修復.

但是,如果你是一個老練的漏洞挖掘人員,你應該有敏銳的觸覺第一時間想到,這部分的代碼肯定是缺少review并且質量不怎么好,畢竟這里可是沙盒內能直達的代碼啊,也意味著內核編寫人員可能并不熟悉MIG代碼的生成規則.這個信息比找到上面那個雞肋的漏洞更加重要,于是我開始找這些MIG相關的內核函數,當然是沙盒直達的.這也啟示了我以后挖掘漏洞的一些方法.

image.png

接著,我在相關的代碼中看到一個平平無奇的內核函數task_swap_mach_voucher,也就是漏洞的核心所在:

image.png

如果不配合MIG函數看, 肯定是看不出這個平平無奇的函數所存在的問題,因此我們看看對應的MIG函數:

image.png

其中|convert_port_to_voucher|是會把對應的ipc_voucher 引用計數加一, |ipc_voucher_release|和|convert_voucher_to_port|會把引用計數減一.看起來沒有任何問題,無論|new_voucher|還是|old_voucher|都是先加一再減一,并且沒有任何賦值,所以引用計數也不需要變化.

但是我們再來回顧那個平平無奇的函數,里面把|new_voucher|賦值到|old_voucher|了!!!!!這意味著,當|task_swap_mach_voucher|調用出來后,|new_voucher|是等于|old_voucher|,換句話說,|new_voucher|會被double free,同時|old_voucher|不會有free.發生引用計數泄露,所以這里一共有兩個問題.當然double free的利用價值更加大,不需要等漫長的時間觸發引用計數溢出,所以最終我們得到的PoC如下:

image.png

首先通過|thread_set_mach_voucher|設置一個dangling pointer,然后通過漏洞釋放ipc voucher對象,然后通過|thread_get_mach_voucher|觸發crash.接下來就是如何在A12上利用.

0x2 get the tfp0 on A12

UaF的漏洞通常是要fake對應的漏洞對象,所以在利用這個漏洞之前,我們首先要搞清楚我們UaF的對象ipc_voucher到底是怎樣的數據結構:

image.png

好消息是ipc_voucher里面存在一個ipc_port_t iv_port,并且這個port是可以通過thread_get_mach_voucher => convert_voucher_to_port 傳回用戶態,意味著我們可以通過fake port的方法直接構造一個tfp0.關于fake port的利用有一篇寫得十分好的文章(via@s1guza ),強烈推薦閱讀: https://siguza.github.io/v0rtex/. 我的利用中參考這篇文章和代碼很多.

壞消息是我們都知道ipc_voucher是一個內核對象,意味著這個偽造的fake port我們沒有receive right, 這對于tfp0沒有任何影響,因為只需要有send right就可以完全控制這個內核對象,但是對利用過程有一定的影響.

0x21 Zone GC

在iOS的內核里, 不同的內核對象隔離在不同的zone, 這意味著即使ipc voucher對象釋放了,這個對象不是真正的釋放,只是放到對應的zone的free list,我們也只可能重新分配一個ipc voucher去填充.但是一般的UaF的漏洞我們都是需要轉換成Type Confusion去利用,也就是我們需要分配一個不同的內核對象去填充這個釋放的ipc voucher內存區域,在這里我們需要手動觸發內核的zone gc, 把對應的page釋放掉.

在這里我用到的方法是分配很多的ipc_voucher對象,這里起碼得超過一個page的大小,然后全部釋放掉.因為zone gc的最小單位是一個page, 如果一個page里面不是全部ipc_voucher被釋放,那么在zone gc的時候并不會釋放這個page(詳情參考MacOS X and iOS Internals:To the Apple’s Core Page 427 中文版):

image.png

在釋放完畢后,我們需要釋放zone gc,把對應的page釋放回操作系統管理,觸發的方法在ian beer以往的利用中已經有介紹過,利用分配大量的port并且發送消息即可:

image.png

這里有一個坑就是我們最好通過usleep稍微等待一些時間,因為zone gc需要一些時間,因為這個坑我調試的時候就發生了很詭異的bug:在調試器里運行得很好,但是一脫離調試器就panic.

0x22 leak a receive port addr

把對應的內存釋放后,我們開始考慮應該填充什么東西.首先第一步我們肯定是需要泄漏一些內核的信息,例如一些堆地址,因為在fake ipc_voucher和port的時候需要用到,后續我們還要在這個內存區域填充任意數據去fake.所以第一時間我們想到的是OSString,因為利用OSString我們可以完全控制內核的數據,并且可以通過一些API把OSString的數據讀回來,從而泄漏內核的信息.

關于OSString的分配我們可以用IOSurfaceRootUserClient的接口|IOSURFACE_GET_VALUE|和|IOSURFACE_SET_VALUE|.

第一步我們可以泄漏的東西是一個port的地址,具體我們看代碼convert voucher to port:

image.png

如果iv_port為空,則內核會分配一個新的port,并且放在iv_port的位置.所以在第一步重新分配OSString去fake ipc_voucher的關鍵就在于令iv_port這個offset為空,在分配port完成后,通過API把OSString讀回來,就可以得到剛分配的一個port的地址.

還有很重要的一點是offset問題,如果分配的OSString的開始位置不能剛好對應ipc_voucher的開始位置,我們偽造的一切數據都會錯誤.這里因為在iPhone XS Max中(A12)一個page的大小是0x4000,而zone的分配是以page為單位的,也就是說第一個ipc_voucher必定是page對齊的,所以我們分配的OSString只需要page對齊即可以保證和ipc_voucher對齊.(這里可能有點難理解,原諒我的表達能力),分配代碼如下:

image.png

這里的padding我們暫時不用管是什么,后續會用到.因為port為0x0, 所以在port后續會賦值一個port的真實地址,然后通過查找找出port的地址和占據了對應內存的OSString index:

image.png

我們之前提到了這個port是沒有send right的,后續我們利用中需要用到receive right去泄漏kernel的slide,所以這里我用了一個trick,在它附近分配大量的帶有receive right的port,最后我們可以用這個地址減去sizeof(ipc_port_t)即可得到一個receive right port的地址.

image.png

image.png

0x23 Fake a port

因為SMAP的關系,我們需要在內核地址中偽造port,這里我們需要得到一個我們可控的內核地址,也就是上面分配的那么多的OSString的其中一個即可,通過heap spray大量分配內存可以令這個地址更加容易猜測.

一開始我打算用泄漏的port地址去計算相關的偏移得到這個地址,但是后來我發現iOS中的堆地址隨機化比較弱,所以這里我用了一個固定的地址:

image.png

然后我們重新分配上面提到的OSString,重新偽造ipc_voucher,令它的port指向我們的可控地址,還記得我們記錄下了對應的OSString的idx了嗎?通過它我們可以很快定位出需要reallocate的OSString:

image.png

這里的第三個參數就是需要偽造的port的地址,我們看到這里有一個magic offset 0x8,在Fake Voucher開始位置再減去magic offset,也指向在上文我們提到的padding第二個域:

image.png

在這里我把fake voucher和fake port的內存區域重疊起來了,在padding+0x8的地方其實是fake port的開始地址,再往后會返回到hash域,通過這樣的布局剛好可以滿足fake voucher和fake port的要求而且不panic.這里重疊起來實屬無奈之舉,因為我們只有一次重分配的機會,如果重分配兩次,第一次分配的OSString 用來fake voucher,第二次用來fake port,則我們猜測的地址有一半可能是指向fake voucher,現在這樣只有一種可能,就是指向fake port.

0x24 leak the idx OSString of fake port

由于后期需要多次重分配fake port內存區域的數據,所以需要找到fake port對應的OSString的index:

image.png

通過調用thread_get_mach_voucher=>convert_voucher_to_port, 我們可以得到兩個需要的東西.第一是OSString的index,因為convert_voucher_to_port會修改fake port區域的reference,通過這個不同可以找出index:

image.png

第二個得到的是指向我們可控地址的用戶態port, 也就是上圖中的| fake_port_to_addr_control |,通過它和修改fake port的數據,我們可以做很多事情.

0x25 任意內存地址讀

通過在fake port中偽造一個task port, 然后通過調用pid_for_task(關于這個利用技巧網上已有大量討論,這里不再解釋), 我們可以任意地址讀,每一次是32位,但是弊端就在于每一次讀取我們都要重新分配OSString,因為我們需要修改fake port中需要讀取的內存地址.因為我們知道對應的OSString index,我們不需要全部OSString重新分配:

image.png

這里我不是單單只重分配對應的index,是設置了一個range 0x50,也就是把這個index前后0x50個OSString也重新分配,令我吃驚的是,這個重分配出奇的穩定,原本我會覺得這個exploit會挺不穩定.

在上文我們已經泄漏了一個帶有receive right的port 地址,利用這個地址加任意地址讀,我們可以最后得到kernel slide,關于這部分內容以及接下來的網上已有討論的我不再詳述,還是推薦看這篇文章https://siguza.github.io/v0rtex/

image.png

0x26 fake a map port

現在的我們每一次操作fake port都要重新分配OSString,這對于利用十分不友好,在得到了kernel的slide,我們下一步應該立刻把| fake_port_to_addr_control |對應的內核地址remap到我們進程的用戶態,這樣以后每一次修改fake port的數據就可以直接在用戶態修改,不需要通過重分配OSString:

image.png

通過remap后,用戶態對應地址和內核態對應地址共享一個物理內存區域,這樣通過修改用戶態的地址即可達到修改內核態對應地址的數據的目的(除非是COW)

0x27 Fake TFP0

由于在convert_port_to_task中會檢測port的ip_kobject,也就是task_t的地址是否等于kernel_task,所以我們不能直接把讀取出來的kernel_task地址賦值到fake port的ip_kobject中,而需要它先memcpy到另外一個內核地址,然后再賦值.

這里我分開兩步驟,第一用一個真實的內核對象port去初始化fake port的所有數據,因為tfp0和所有內核對象的port都是共享一個receiver | ipc_space_kernel |,這里我用了一個IOSurefaceRootUserClient的port去初始化.如果不這樣做在用tfp0調用內核API的時候會出錯,因為很多屬性值還沒有初始化,例如ip_messages.

image.png

接下來把原生的kernel task地址copy到另外一個內核地址,并且修改tfp0 port中一些與IOSurefaceRootUserClient port不同的部分:

image.png

最后一步,重新分配fake voucher中的port地址,指向我們最新fake tfp0的地址,然后通過thread_get_mach_voucher返回到用戶態,最終得到tfp0:

image.png

0x3 Cleaning the stuff

因為我們在程序結束的時候,還有一個danging Pointer在thread mach voucher中指向我們的danging Pointer,而danging Pointer是指向我們OSString分配的內存,這部分內存在IOSurfaceRootUserClient釋放的時候進行釋放的,也就是進程結束的時候。除此之外,還有眾多我們偽造的port,都是指向OSString分配的內存,所以都要在進程結束前一并回收.

image.png

最后,包括我們最終生成的tfp0,也是需要進行釋放的,所以如果想要保持tfp0的持久性,最好在post exploit階段重新自己構造一個新的tfp0.至此tfp0的利用已經結束,關于后續的post exploit, 根目錄讀寫,簽名bypass等等這里不會提及.

0x4 總結

我們都知道,在A12中引入了PAC的mitigation,很多人都覺得這是UaF甚至是越獄的終點.事實證明,UaF的洞還是可以在PAC的環境下利用,這需要看具體的情況,因為PAC只是針對間接調用控制pc寄存器這一方面。我們可以看到,在取得tfp0的整個過程中,我們不需要控制pc寄存器,這是因為我們釋放的對象ipc_voucher中存在一個port的屬性值.UaF漏洞的利用很大程度上依賴這個釋放的對象的數據結構以及這些數據結構怎么去使用,因為最終我們要轉換成type confusion.


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