前言
在之前的彙編教程系列文章中,我們在使用者态下探讨了諸多原理。從今天開始,我們将詳細分析曆代 iOS Jailbreak Exploits,并由此深入 XNU 核心,并學習更多二進制安全攻防的知識。
雖然國外大佬提供的 Exploit PoC 都有較為詳細的 write-up,但這些 write-up 常常以之前出現的 PoC 為基礎,并不詳細展開某些具體原理,這就導緻初學者很難完全讀懂。筆者的 Jailbreak Priciples 系列文章會将所有相關的 PoC 和 write-up 進行整合,并以讀者是核心小白(其實筆者也是)為假設展開分析,目标是打造人人能讀懂的 XNU 漏洞分析系列文章。
越獄的本質
iOS 僅為使用者提供了一個受限的 Unix 環境,正常情況下我們隻能在使用者态借助于合法的系統調用來與核心互動。相反的,用于電腦的 macOS 則有着很高的自由度。它們都基于 Darwin-XNU,但 Apple 在 iPhoneOS 上施加了諸多限制,越獄即解除這些限制使我們可以獲得 iPhoneOS 的 root 權限,進而在一定程度上為所欲為。
Apple 采用了 Sandbox, Signature Checkpoints 等手段對系統進行保護,使得突破這些限制變得極為困難。
越獄的分類
目前越獄主要分為兩類,一類是以硬體漏洞為基礎的 BootROM Exploit,另一類則是基于軟體漏洞的 Userland Exploit。
BootROM Exploit
這類漏洞類似于單片機中的 IC 解密,從硬體層面發現 iPhone 本身的漏洞,使得整個系統的
Secure Boot Chain變得不可靠,這類漏洞的殺傷力極強,隻能通過更新硬體解決。最近出現的
checkm8及基于它開發的
checkra1n就實作了 iPhone 5s ~ iPhone X 系列機型的硬體調試與越獄;
Userland Exploit
這類漏洞往往是對開源的
Darwin-XNU進行代碼審計發現的,基于這些漏洞往往能使我們在使用者态将任意可執行代碼送入核心執行,我們即将介紹的 Sock Port Exploit 即是對 XNU 中 socket options 的一個 UAF 漏洞的利用。
将使用者态資料送入核心
通過上文的分析我們知道,Userland Exploit 的一個重要基礎是能将任意資料寫入核心的堆區,使之成為有效地 Kernel 資料結構,進而從使用者态實施對核心的非法控制。遺憾的是,我們無法直接操作核心的記憶體資料,這是因為使用者态的應用程式沒有辦法擷取 kernel_task,也就無法直接通過
vm_read
vm_write
等函數操作核心的堆棧。
既然無法直接操作記憶體,我們就需要考慮間接操作記憶體的方式,事實上我們有非常多的方式能夠間接讀寫核心的資料,最常見方式有 Socket, Mach Message 和 IOSurface 等,這裡我們先介紹最好了解的 Socket 方式,随後對 Sock Port 的漏洞時分析會介紹其利用這三種方式打的組合拳。
基于 Socket 的間接核心記憶體讀寫
由于 Socket 的實作是作業系統層面的,在使用者态通過 socket 函數建立 sock 時核心會執行一些記憶體配置設定操作,例如下面的使用者态代碼:
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 讀寫核心的任意内容
上述方式有一個明顯的限制,那就是我們隻能在核心受控的範圍内讀寫記憶體,單單通過這種方式是玩不出幺蛾子的。設想如果我們能嘗試把一個僞造的 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 的漏洞利用較為複雜,我們将在接下來的幾篇文章中逐漸講解,這裡隻需要對這些概念有個初步的認識即可。
Use After Free
透過上述例子我們對 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 往往需要結合間接操作堆的相關技術。
Heap Spraying
下面我們參考 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
執行讀寫和驗證,就能實作對核心堆記憶體的完全控制。
一個純使用者态的 UAF & Heap Spraying 例子
綜合上述理論探讨,我們對堆記憶體的讀寫有了初步的認識,事實上事情沒有我們想象的那麼簡單,整個 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,并通過 free_task 函數将其釋放,這會構造一個
的懸垂指針;task->options
- 不斷配置設定與
指向的task->options
相同大小的記憶體區域,直到struct secret_options
懸垂指針指向的區域被 Reallocation 成我們新申請的記憶體,驗證方式可以僞造特定資料,随後通過task->options
讀取驗證;secret_get_options
- 此時
已經指向了我們新申請的區域,可以通過修改該區域實作對 Task Options 的修改。struct secret_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 的設計。

參考資料
- Andy Slye. What Is Jailbreaking? How a Jailbreak Works - https://www.youtube.com/watch?v=tYKfXNiA1wc
- Webopedia. Use After Free - https://www.webopedia.com/TERM/U/use-after-free.html
- Computer Hope. Heap spraying - https://www.computerhope.com/jargon/h/heap-spraying.htm
- GitHub. jakeajames/sock_port - https://github.com/jakeajames/sock_port/tree/sock_port_2