iOS Jailbreak Principles - Sock Port 漏洞解析(一)UAF 與 Heap Spraying

Soulghost發表於2019-11-17

前言

在之前的彙編教程系列文章中,我們在使用者態下探討了諸多原理。從今天開始,我們將詳細分析歷代 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_readvm_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 結構體分配到核心的其他區段,是不是就能通過 setsockoptgetsockopt 來讀寫任意記憶體了呢?

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 後繼續通過 setsockoptgetsockopt 間接讀寫這個懸垂指標。隨著系統重新分配這塊記憶體,我們依然能夠通過懸垂指標對其進行訪問,因此問題轉化為了如何間接控制系統對該區域的 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 函式,例如上文中提到的 setsockoptgetsockopt,此外為了有效地控制 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 分配偽造的內容,隨後我們通過 setsockoptgetsockopt 執行讀寫和驗證,就能實現對核心堆記憶體的完全控制。

一個純使用者態的 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 等級,方法如下:

  1. 建立一個 Task,並通過 free_task 函式將其釋放,這會構造一個 task->options 的懸垂指標;
  2. 不斷分配與 task->options 指向的 struct secret_options 相同大小的記憶體區域,直到 task->options 懸垂指標指向的區域被 Reallocation 成我們新申請的記憶體,驗證方式可以偽造特定資料,隨後通過 secret_get_options 讀取驗證;
  3. 此時 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 的設計。

iOS Jailbreak Principles - Sock Port 漏洞解析(一)UAF 與 Heap Spraying

參考資料

  1. Andy Slye. What Is Jailbreaking? How a Jailbreak Works - www.youtube.com/watch?v=tYK…
  2. Webopedia. Use After Free - www.webopedia.com/TERM/U/use-…
  3. Computer Hope. Heap spraying - www.computerhope.com/jargon/h/he…
  4. GitHub. jakeajames/sock_port - github.com/jakeajames/…

相關文章