在以前的彙編教程系列文章中,咱們在用戶態下探討了諸多原理。從今天開始,咱們將詳細分析歷代 iOS Jailbreak Exploits,並由此深刻 XNU 內核,並學習更多二進制安全攻防的知識。html
雖然國外大佬提供的 Exploit PoC 都有較爲詳細的 write-up,但這些 write-up 經常以以前出現的 PoC 爲基礎,並不詳細展開某些具體原理,這就致使初學者很難徹底讀懂。筆者的 Jailbreak Priciples 系列文章會將全部相關的 PoC 和 write-up 進行整合,並以讀者是內核小白(其實筆者也是)爲假設展開分析,目標是打造人人能讀懂的 XNU 漏洞分析系列文章。git
iOS 僅爲用戶提供了一個受限的 Unix 環境,常規狀況下咱們只能在用戶態藉助於合法的系統調用來與內核交互。相反的,用於電腦的 macOS 則有着很高的自由度。它們都基於 Darwin-XNU,但 Apple 在 iPhoneOS 上施加了諸多限制,越獄即解除這些限制使咱們能夠得到 iPhoneOS 的 root 權限,進而在必定程度上隨心所欲。github
Apple 採用了 Sandbox, Signature Checkpoints 等手段對系統進行保護,使得突破這些限制變得極爲困難。web
目前越獄主要分爲兩類,一類是以硬件漏洞爲基礎的 BootROM Exploit,另外一類則是基於軟件漏洞的 Userland Exploit。安全
這類漏洞相似於單片機中的 IC 解密,從硬件層面發現 iPhone 自己的漏洞,使得整個系統的 Secure Boot Chain 變得不可靠,這類漏洞的殺傷力極強,只能經過更新硬件解決。最近出現的 checkm8 及基於它開發的 checkra1n 就實現了 iPhone 5s ~ iPhone X 系列機型的硬件調試與越獄;markdown
這類漏洞每每是對開源的 Darwin-XNU 進行代碼審計發現的,基於這些漏洞每每能使咱們在用戶態將任意可執行代碼送入內核執行,咱們即將介紹的 Sock Port Exploit 便是對 XNU 中 socket options 的一個 UAF 漏洞的利用。數據結構
經過上文的分析咱們知道,Userland Exploit 的一個重要基礎是能將任意數據寫入內核的堆區,使之成爲有效地 Kernel 數據結構,進而從用戶態實施對內核的非法控制。遺憾的是,咱們沒法直接操做內核的內存數據,這是由於用戶態的應用程序沒有辦法獲取 kernel_task,也就沒法直接經過 vm_read
和 vm_write
等函數操做內核的堆棧。app
既然沒法直接操做內存,咱們就須要考慮間接操做內存的方式,事實上咱們有很是多的方式可以間接讀寫內核的數據,最多見方式有 Socket, Mach Message 和 IOSurface 等,這裏咱們先介紹最好理解的 Socket 方式,隨後對 Sock Port 的漏洞時分析會介紹其利用這三種方式打的組合拳。dom
因爲 Socket 的實現是操做系統層面的,在用戶態經過 socket 函數建立 sock 時內核會執行一些內存分配操做,例以下面的用戶態代碼:iphone
int sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); 複製代碼
在內核態會根據傳入的參數建立 struct socket
結構體:
/* * Kernel structure per socket. * Contains send and receive buffer queues, * handle on protocol and pointer to protocol * private data and error information. */ struct socket { int so_zone; /* zone we were allocated from */ short so_type; /* generic type, see socket.h */ u_short so_error; /* error affecting connection */ u_int32_t so_options; /* from socket call, see socket.h */ short so_linger; /* time to linger while closing */ short so_state; /* internal state flags SS_*, below */ void *so_pcb; /* protocol control block */ // ... } 複製代碼
這裏咱們能經過傳入 socket 的參數間接、受限的控制內核中的內存,但因爲系統只會返回 sock 的句柄(handle)給咱們,咱們沒法直接讀取內核的內存內容。
要讀取內核的內存,咱們能夠藉助於內核提供的 socket options 相關函數,他們可以修改 socket 的一些配置,例以下面的代碼修改了 IPV6 下的 Maximum Transmission Unit:
// set mtu int minmtu = -1; setsockopt(sock, IPPROTO_IPV6, IPV6_USE_MIN_MTU, &minmtu, sizeof(*minmtu)); // read mtu getsockopt(sock, IPPROTO_IPV6, IPV6_USE_MIN_MTU, &minmtu, sizeof(*minmtu)); 複製代碼
在內核態,系統會讀取 struct socket
的 so_pcb,並執行來自用戶態的讀寫操做,由此咱們透過 options 相關函數讀寫了內核中 socket 結構體的部份內容。
上述方式有一個明顯的限制,那就是咱們只能在內核受控的範圍內讀寫內存,單單經過這種方式是玩不出幺蛾子的。設想若是咱們能嘗試把一個僞造的 Socket 結構體分配到內核的其餘區段,是否是就能經過 setsockopt
和 getsockopt
來讀寫任意內存了呢?
Sock Port 是一個利用 Socket 函數集實現內核內存任意讀寫的漏洞,它主要基於 iOS 10.0 - 12.2 的內核代碼中 socket disconnect 時的一個漏洞,觀察以下的內核代碼:
if (!(so->so_flags & SOF_PCBCLEARING)) { struct ip_moptions *imo; struct ip6_moptions *im6o; inp->inp_vflag = 0; if (inp->in6p_options != NULL) { m_freem(inp->in6p_options); inp->in6p_options = NULL; // <- good } ip6_freepcbopts(inp->in6p_outputopts); // <- bad ROUTE_RELEASE(&inp->in6p_route); /* free IPv4 related resources in case of mapped addr */ if (inp->inp_options != NULL) { (void) m_free(inp->inp_options); inp->inp_options = NULL; // <- good } // ... } 複製代碼
能夠看到在清理 options 時只對 in6p_outputopts
進行了釋放,而沒有清理 in6p_outputopts
指針的地址,這就形成了一個 in6p_outputopts
懸垂指針。
幸運的是,經過某種設置後,咱們可以在 socket disconnect 後繼續經過 setsockopt
和 getsockopt
間接讀寫這個懸垂指針。隨着系統從新分配這塊內存,咱們依然可以經過懸垂指針對其進行訪問,所以問題轉化爲了如何間接控制系統對該區域的 Reallocation。
這類透過懸垂指針操做已釋放區域的漏洞被稱爲 UAF(Use After Free),而間接控制系統 Reallocation 的常見方式有 堆噴射(Heap Spraying) 和 堆風水(Heap feng-shui),整個 Sock Port 的漏洞利用較爲複雜,咱們將在接下來的幾篇文章中逐步講解,這裏只須要對這些概念有個初步的認識便可。
透過上述例子咱們對 UAF 有了一個初步的認識,如今咱們參考 Webopedia 給出明確的定義:
Use After Free specifically refers to the attempt to access memory after it has been freed, which can cause a program to crash or, in the case of a Use-After-Free flaw, can potentially result in the execution of arbitrary code or even enable full remote code execution capabilities.
即嘗試訪問已釋放的內存,這會致使程序崩潰,或是潛在的任意代碼執行,甚至獲取徹底的遠程控制能力。
UAF 的關鍵之一是獲取被釋放區域的內存地址,通常透過懸垂指針實現,而懸垂指針是因爲指針指向的內存區域被釋放,但指針未被清零致使的,這類問題在缺少二進制安全知識的開發者寫出的代碼中家常便飯。
對於跨進程的狀況下,只透過懸垂指針是沒法讀寫執行內存的,須要配合一些能間接讀取懸垂指針的 IPC 函數,例如上文中提到的 setsockopt
和 getsockopt
,此外爲了有效地控制 Reallocation 每每須要結合間接操做堆的相關技術。
下面咱們參考 Computer Hope 給出 Heap Spraying 的定義:
Heap spraying is a technique used to aid the exploitation of vulnerabilities in computer systems. It is called "spraying the heap" because it involves writing a series of bytes at various places in the heap. The heap is a large pool of memory that is allocated for use by programs. The basic idea is similar to spray painting a wall to make it all the same color. Like a wall, the heap is "sprayed" so that its "color" (the bytes it contains) is uniformly distributed over its entire memory "surface."
即在用戶態透過系統調用等方式在內核堆的不一樣區域分配大量內存,若是將內核的堆比做牆壁,堆噴射就是經過大量分配內存的方式將一樣顏色的油漆(一樣的字節)潑灑到堆上,這會致使其顏色(一樣的字節)均勻的分佈在整個內存平面上,即那些先前被釋放的區域幾乎都被 Reallocation 成了一樣的內容。
簡言之就是,好比咱們 alloc 了 1 個 8B 的區域,隨後將其釋放,接下來再執行 alloc 時早晚會對先前的區域進行復用,若是剛好被咱們 alloc 時佔用,則達到了內容控制的目的。透過這種技術咱們能夠間接控制堆上的 Reallocation 內容。
顯然若是咱們將上述 Socket UAF 與 Heap Spraying 組合,就有機會爲 Socket Options 分配僞造的內容,隨後咱們經過 setsockopt
和 getsockopt
執行讀寫和驗證,就能實現對內核堆內存的徹底控制。
綜合上述理論探討,咱們對堆內存的讀寫有了初步的認識,事實上事情沒有咱們想象的那麼簡單,整個 Sock Port 的利用是基於許多漏洞組合而來的,並不是三言兩語和一朝一夕可以徹底搞懂,所以本文先不展開具體漏洞的內容,而是在用戶態模擬一個 UAF 和 Heap Spraying 的場景讓你們先從工程上初步認識這兩個概念。
設想小明是一個初級頁面仔,他要開發一個任務執行系統,該系統根據任務的優先級順序執行任務,任務的優先級取決於用戶的 VIP 等級,該 VIP 等級被記錄在 task 的 options 中:
struct secret_options { bool isVIP; int vipLevel; }; struct secret_task { int tid; bool valid; struct secret_options *options; }; 複製代碼
小明參考了 Mach Message 的設計理念,在系統內部維護 Task 的內存結構,只對外暴露 Task 的句柄(tid),用戶能夠透過 create_secret_task
建立任務,任務的默認是沒有 VIP 等級的:
std::map<task_t, struct secret_task *> taskTable; task_t create_secret_task() { struct secret_task *task = (struct secret_task *)calloc(1, sizeof(struct secret_task)); task->tid = arc4random(); while (taskTable.find(task->tid = arc4random()) != taskTable.end()); taskTable[task->tid] = task; struct secret_options *options = (struct secret_options *)calloc(1, sizeof(struct secret_options)); task->options = options; options->isVIP = false; options->vipLevel = 0; return task->tid; } 複製代碼
在系統以外,用戶能作的只是建立任務、獲取 VIP 信息以及獲取任務優先級:
typedef int task_t; #define SecretTaskOptIsVIP 0 #define SecretTaskOptVipLevel 1 #define SecretTaskVipLevelMAX 9 int get_task_priority(task_t task_id) { struct secret_task *task = get_task(task_id); if (!task) { return (~0U); } return task->options->isVIP ? (SecretTaskVipLevelMAX - task->options->vipLevel) : (~0U); } bool secret_get_options(task_t task_id, int optkey, void *ret) { struct secret_task *task = get_task(task_id); if (!task) { return false; } switch (optkey) { case SecretTaskOptIsVIP: *(reinterpret_cast<bool *>(ret)) = task->options->isVIP; break; case SecretTaskOptVipLevel: *(reinterpret_cast<int *>(ret)) = task->options->vipLevel; break; default: break; } return true; } 複製代碼
在理想狀況下,不考慮逆向工程的方式,咱們只能拿到 Task 的句柄,沒法獲取 Task 地址,所以沒法任意修改 VIP 信息。
小明同時爲用戶提供了註銷任務的 API,他只對任務的 options 進行了釋放,同時將任務標記爲 invalid,缺少經驗的他忘記清理 options 指針,爲系統引入了一個 UAF Exploit:
bool free_task(task_t task_id) { struct secret_task *task = get_task(task_id); if (!task) { return false; } free(task->options); task->valid = false; return true; } 複製代碼
常規狀況下,咱們只能透過公共的 API 訪問系統:
// create task task_t task = create_secret_task(); // read options int vipLevel; secret_get_options(task, SecretTaskOptVipLevel, &vipLevel); // get priority int priority = get_task_priority(leaked_task); // release task free_task(task); 複製代碼
因爲 Task 默認是非 VIP 的,咱們只能拿到最低優先級 INTMAX。這裏咱們經過 task->options
的 UAF 能夠僞造 task 的 VIP 等級,方法以下:
task->options
的懸垂指針;task->options
指向的 struct secret_options
相同大小的內存區域,直到 task->options
懸垂指針指向的區域被 Reallocation 成咱們新申請的內存,驗證方式能夠僞造特定數據,隨後經過 secret_get_options
讀取驗證;struct secret_options
已經指向了咱們新申請的區域,能夠經過修改該區域實現對 Task Options 的修改。struct faked_secret_options { bool isVIP; int vipLevel; }; struct faked_secret_options *sprayPayload = nullptr; task_t leaked_task = -1; for (int i = 0; i < 100; i++) { // create task task_t task = create_secret_task(); // free to make dangling options free_task(task); // alloc to spraying struct faked_secret_options *fakedOptions = (struct faked_secret_options *)calloc(1, sizeof(struct faked_secret_options)); fakedOptions->isVIP = true; // to verify fakedOptions->vipLevel = 0x123456; // check by vipLevel int vipLevel; secret_get_options(task, SecretTaskOptVipLevel, &vipLevel); if (vipLevel == 0x123456) { printf("spray succeeded at %d!!!\n", i); sprayPayload = fakedOptions; leaked_task = task; break; } } // modify if (sprayPayload) { sprayPayload->vipLevel = 9; } 複製代碼
因爲是純用戶態、同一線程內的同步操做,這種方式的成功率極高。固然這種方式只能讓你們對 UAF 與 Heap Spraying 有一個大體認識,實際上這類漏洞利用都是跨進程的,須要很是複雜的操做,每每須要藉助於 Mach Message 和 IOSurface,且 Payload 構造十分複雜。
在下一個章節中咱們將開始着手分析 Sock Port 的源碼,瞭解來自 Ian Beer 大佬的 kalloc 系列函數以及利用 IOSurface 進行 Heap Spraying 的方式和原理。其中 kalloc 系列函數須要對 Mach Message 有深刻的認識,所以在下一篇文章中咱們也會從 XNU 源碼角度分析 mach port 的設計。