iOS Jailbreak Principles - Undecimus 分析(二)通過 String XREF 定位核心資料

Soulghost發表於2019-12-29

系列文章

  1. iOS Jailbreak Principles - Sock Port 漏洞解析(一)UAF 與 Heap Spraying
  2. iOS Jailbreak Principles - Sock Port 漏洞解析(二)通過 Mach OOL Message 洩露 Port Address
  3. iOS Jailbreak Principles - Sock Port 漏洞解析(三)IOSurface Heap Spraying
  4. iOS Jailbreak Principles - Sock Port 漏洞解析(四)The tfp0 !
  5. iOS Jailbreak Principles - Undecimus 分析(一)Escape from Sandbox

前言

在核心中有許多關鍵變數和校驗,為獲得這些變數和繞過校驗就要求我們在記憶體中定位這些地址。本文將介紹 Undecimus 中基於 String XREF 定位關鍵記憶體地址的方法,通過該方法不僅可以準確定位核心中的特定元素,也能為自行設計二進位制分析工具帶來很好的啟發。

定位 Kernel Task

為了獲取核心資訊,我們需要定位到 Kernel Task 的地址,再通過 tfp0 的 kread 讀取內容。要定位 Kernel Task,關鍵是找到獲取 Kernel Task 的程式碼,然後嘗試從記憶體中定位這段程式碼,再分析指令解出變數的檔案偏移即可。

查詢使用 Kernel Task 的函式

xnu-4903.221.2 中可以找到訪問 Kernel Task 的如下程式碼:

int
proc_apply_resource_actions(void * bsdinfo, __unused int type, int action)
{
    proc_t p = (proc_t)bsdinfo;

    switch(action) {
        case PROC_POLICY_RSRCACT_THROTTLE:
        	/* no need to do anything */
        	break;
        
        case PROC_POLICY_RSRCACT_SUSPEND:
        	task_suspend(p->task);
        	break;
        
        case PROC_POLICY_RSRCACT_TERMINATE:
        	psignal(p, SIGKILL);
        	break;
        
        case PROC_POLICY_RSRCACT_NOTIFY_KQ:
        	/* not implemented */
        	break;
        
        case PROC_POLICY_RSRCACT_NOTIFY_EXC:
        	panic("shouldn't be applying exception notification to process!");
        	break;
	}
	return(0);
}
複製程式碼

這裡有一段字串 "shouldn't be applying exception notification to process!" 可用於輔助定位,它在編譯後會被儲存在 __TEXT,__cstring 段,通過在記憶體中搜尋 __TEXT,__cstring 段即可找到字串地址,我們稱之為 location_str

定位到函式中的 String XREF

由於 ARM 的取址常常需要 2 條指令完成,為了定位使用 location_str 的程式碼,我們需要對程式碼段進行靜態分析。當發現暫存器中的值等於 location_str 時即發現了一個交叉引用(XREF),通過這種手段我們便能在記憶體中定位到語句 panic("shouldn't be applying exception notification to process!") 對應的指令地址。

回溯找到 Kernel Task XREF

最快定位到 Kernel Task 的方法是回溯到 task_suspend(p->task),在 task_suspend 第一次訪問 p->task 時一定會對 task 定址,我們可以從定址指令中解出 task 的檔案偏移,再加上核心在記憶體中的基地址即可得到 Kernel Task 的地址。

kern_return_t
task_suspend(task_t task)
{
    kern_return_t kr;
    mach_port_t port, send, old_notify;
    mach_port_name_t name;
    
    if (task == TASK_NULL || task == kernel_task)
    	return (KERN_INVALID_ARGUMENT);
    
    task_lock(task);
    // ...
複製程式碼

從上面的分析可以看出問題的關鍵在於 XREF 的定位,下面我們將分析一種 String Based XREF 定位演算法來解決上述問題。

在記憶體中載入 Kernelcache

根據 iPhone Wiki 給出的 Kernelcache 定義[1]:

The kernelcache is basically the kernel itself as well as all of its extensions (AppleImage3NORAccess, IOAESAccelerator, IOPKEAccelerator, etc.) into one file, then packed/encrypted in an IMG3 (iPhone OS 2.0 and above) or 8900 (iPhone OS 1.0 through 1.1.4) container.

即 kernelcache 就是將 kernel 和它的擴充套件打包在一個檔案中並以 IMG3 格式儲存(iOS 2 以上)。

上一篇文章 中我們介紹了基於 tfp0 的沙盒逃逸方法,通過沙盒逃逸我們可以從 /System/Library/Caches/com.apple.kernelcaches/kernelcache 讀取 kernelcache,它既是當前系統載入的映象。

讀者可開啟 Undecimus 的 jailbreak.m 檔案,搜尋 "Initializing patchfinder" 定位到 kernelcache 的載入程式碼,載入方法和普通的 Mach-O 檔案類似,也是先讀取 Mach HeaderLoad Commands,然後逐段記錄偏移量,具體程式碼在 init_kernel 函式中。

這裡不再贅述載入過程,只指出幾個關鍵的全域性變數:

  1. cstring_basecstring_size__TEXT,__cstring 段的虛擬地址和長度;
  2. xnucore_basexnucore_size__TEXT,__TEXT_EXEC 段,即程式碼段的虛擬地址和長度;
  3. kerndumpbase 是所有段中最小的虛擬地址,即 kernelcache 載入的虛擬基地址,在普通的 Mach-O 檔案中這個值一般是 __PAGEZERO 段的虛擬地址 0x100000000,在核心中似乎是 __TEXT 段的虛擬地址 0xFFFFFFF007004000;
  4. kernel 是 kernelcache 在使用者空間的完整對映,即一份完整載入的核心映象。

Find String Based XREF

在 Undecimus 中包含一個 find_strref 函式用於定位字串的 XREF:

addr_t
find_strref(const char *string, int n, enum string_bases string_base, bool full_match, bool ppl_base)
{
    uint8_t *str;
    addr_t base;
    addr_t size;
    enum text_bases text_base = ppl_base?text_ppl_base:text_xnucore_base;

    switch (string_base) {
        case string_base_const:
            base = const_base;
            size = const_size;
            break;
        case string_base_data:
            base = data_base;
            size = data_size;
            break;
        case string_base_oslstring:
            base = oslstring_base;
            size = oslstring_size;
            break;
        case string_base_pstring:
            base = pstring_base;
            size = pstring_size;
            text_base = text_prelink_base;
            break;
        case string_base_cstring:
        default:
            base = cstring_base;
            size = cstring_size;
            break;
    }
    addr_t off = 0;
    while ((str = boyermoore_horspool_memmem(kernel + base + off, size - off, (uint8_t *)string, strlen(string)))) {
        // Only match the beginning of strings
        // first_string || \0this_string
        if ((str == kernel + base || *(str-1) == '\0') && (!full_match || strcmp((char *)str, string) == 0))
            break;
        // find after str
        off = str - (kernel + base) + 1;
    }
    if (!str) {
        return 0;
    }
    // find xref
    return find_reference(str - kernel + kerndumpbase, n, text_base);
}
複製程式碼

它要求傳入字串 string,引用的序號 n,基準段 string_base,是否完全匹配 full_match,以及是否位於 __PPLTEXT 段,對於尋找 Kernel Task 的場景,我們的入參如下:

addr_t str = find_strref("\"shouldn't be applying exception notification", 2, string_base_cstring, false, false);
複製程式碼

即以 __TEXT,__cstring 為基準,不要求完全匹配,找到第 2 個交叉引用所在的地址。

定位字串地址

字串地址的定位邏輯在 boyermoore_horspool_memmem 函式中:

static unsigned char *
boyermoore_horspool_memmem(const unsigned char* haystack, size_t hlen,
                           const unsigned char* needle,   size_t nlen)
{
    size_t last, scan = 0;
    size_t bad_char_skip[UCHAR_MAX + 1]; /* Officially called:
                                          * bad character shift */

    /* Sanity checks on the parameters */
    if (nlen <= 0 || !haystack || !needle)
        return NULL;

    /* ---- Preprocess ---- */
    /* Initialize the table to default value */
    /* When a character is encountered that does not occur
     * in the needle, we can safely skip ahead for the whole
     * length of the needle.
     */
    for (scan = 0; scan <= UCHAR_MAX; scan = scan + 1)
        bad_char_skip[scan] = nlen;

    /* C arrays have the first byte at [0], therefore:
     * [nlen - 1] is the last byte of the array. */
    last = nlen - 1;

    /* Then populate it with the analysis of the needle */
    for (scan = 0; scan < last; scan = scan + 1)
        bad_char_skip[needle[scan]] = last - scan;

    /* ---- Do the matching ---- */

    /* Search the haystack, while the needle can still be within it. */
    while (hlen >= nlen)
    {
        /* scan from the end of the needle */
        for (scan = last; haystack[scan] == needle[scan]; scan = scan - 1)
            if (scan == 0) /* If the first byte matches, we've found it. */
                return (void *)haystack;

        /* otherwise, we need to skip some bytes and start again.
           Note that here we are getting the skip value based on the last byte
           of needle, no matter where we didn't match. So if needle is: "abcd"
           then we are skipping based on 'd' and that value will be 4, and
           for "abcdd" we again skip on 'd' but the value will be only 1.
           The alternative of pretending that the mismatched character was
           the last character is slower in the normal case (E.g. finding
           "abcd" in "...azcd..." gives 4 by using 'd' but only
           4-2==2 using 'z'. */
        hlen     -= bad_char_skip[haystack[last]];
        haystack += bad_char_skip[haystack[last]];
    }

    return NULL;
}
複製程式碼

我們首先根據呼叫分析入參:

addr_t base = cstring_base;
addr_t off = 0;
while ((str = boyermoore_horspool_memmem(kernel + base + off, size - off, (uint8_t *)string, strlen(string)))) {
    // Only match the beginning of strings
    // first_string || \0this_string
    if ((str == kernel + base || *(str-1) == '\0') && (!full_match || strcmp((char *)str, string) == 0))
        break;
    // find after str
    off = str - (kernel + base) + 1;
}
複製程式碼
  1. haystack = kernel + base + off,即 __TEXT,__cstring 段的起始地址;
  2. hlen = size - off,即 __TEXT,__cstring 段的長度;
  3. needle = string 即待查詢字串指標;
  4. nlen = strlen(string) 即待查詢字串的長度。

在函式的開頭首先維護了一個 bad_char_skip 陣列來記錄當匹配失敗時,應當跳過多少個字元來避免無意義的匹配。整個演算法採用了倒序掃描的方式,不斷從 haystack[needle_len - 1] 向前掃描並檢查 haystack[i] == needle[i],當匹配到 haystack[0] 時如果依然滿足條件,說明找到了字串的地址,否則根據匹配失敗的字元查 bad_char_skip 表將 haystack 指標後移繼續匹配。

需要注意的是,在匹配成功後得到的字串地址是相對於使用者空間的 kernelcache 對映 kernel 的,並非是字串在核心中的實際地址。

搜尋對字串所在地址的定址操作

在獲取到字串在使用者空間的地址 str 後,首先需要計算它在 kernelcache 中的虛擬地址:

addr_t str_vmaddr = str - kernel + kerndumpbase;
複製程式碼

核心程式碼中對 str 的引用一定涉及到對 str_vmaddr 的定址,主要的定址方式有以下幾種:

; 1
adrp xn, str@PAGE
add xn, xn, str@PAGEOFF

; 2
ldr xn, [xm, #imm]

; 3
ldr xn, =#imm

; 4
adr xn, #imm

; 5
bl #addr
複製程式碼

find_strref 的尾部呼叫了 return find_reference(str_vmaddr, n, text_base)find_reference__TEXT_EXEC,__text 進行了靜態分析,對定址相關的指令模擬了暫存器運算,主要邏輯在 xref64 函式中,當發現暫存器中的值等於 str_vmaddr 時即找到了一條對 str 的交叉引用。

這裡的程式碼主要是對機器碼的解碼和運算操作,篇幅較長不再貼出,讀者有興趣可以自行閱讀。

通過 String XREF 定位變數地址

上文中我們已經得到了目標函式 proc_apply_resource_actions 中對 str 的引用地址,隨後需要向上回溯定位 task_suspend 函式的呼叫指令:

addr_t find_kernel_task(void) {
    /**
             adrp x8,     str@PAGE
     str --> add  x8, x8, str@PAGEOFF
             bl   _panic
     */
    addr_t str = find_strref("\"shouldn't be applying exception notification", 2, string_base_cstring, false, false);
    if (!str) return 0;
    str -= kerndumpbase;

    // find bl _task_suspend
    addr_t call = step64_back(kernel, str, 0x10, INSN_CALL);
    if (!call) return 0;

    addr_t task_suspend = follow_call64(kernel, call);
    if (!task_suspend) return 0;

    addr_t adrp = step64(kernel, task_suspend, 20*4, INSN_ADRP);
    if (!adrp) return 0;

    addr_t kern_task = calc64(kernel, adrp, adrp + 0x8, 8);
    if (!kern_task) return 0;

    return kern_task + kerndumpbase;
}
複製程式碼

整個過程主要分 3 步:

  1. 回溯找到 bl _task_suspend 的呼叫點,解出 task_suspend 函式的地址;
  2. task_suspend 函式向後搜尋第一條 adrp 指令,即是對 Kernel Task 的定址;
  3. 從定址指令中解出 Kernel Task 地址。

我們再回過頭來看 proc_apply_resource_actions 函式片段:

switch(action) {
	case PROC_POLICY_RSRCACT_THROTTLE:
		/* no need to do anything */
		break;

	case PROC_POLICY_RSRCACT_SUSPEND:
		task_suspend(p->task);
		break;

	case PROC_POLICY_RSRCACT_TERMINATE:
		psignal(p, SIGKILL);
		break;

	case PROC_POLICY_RSRCACT_NOTIFY_KQ:
		/* not implemented */
		break;
	
	case PROC_POLICY_RSRCACT_NOTIFY_EXC:
		panic("shouldn't be applying exception notification to process!");
		break;
}
複製程式碼

編譯時不一定會按照 case 的順序生成機器碼,因此我們需要根據 str XREF 找到 kernelcache 中的實際表示,一個簡單地辦法是在 find_strref("\"shouldn't be applying exception notification", 2, string_base_cstring, false, false) 後打一個斷點來獲取 str XREF 的檔案偏移,再利用二進位制分析工具反彙編 kernelcache 中的這個部分。

通過斷點除錯可知 str XREF 位於 0x0000000000f9f084,這應該是一條 add 指令:

/**
         adrp x8,     str@PAGE
 str --> add  x8, x8, str@PAGEOFF
         bl   _panic
 */
複製程式碼

Mach-O 檢視器中開啟可以發現,0x0000000000f9f084 確實是一條 add 指令:

iOS Jailbreak Principles - Undecimus 分析(二)通過 String XREF 定位核心資料

要定位 task_suspend(p->task) 有兩種方式,其一是 p->task 是一個基於偏移量的結構體成員定址有明顯特徵,第二個是看函式呼叫前的引數準備。在 0xf9f074 處有一個 +16 的偏移量定址,顯然這是對 p->task 地址的計算,因此 0xf9f078 處即是 task_suspend(p->task) 的呼叫。

所以從 add 指令處向前回溯 3 條指令即可,找到這條 CALL 指令後,即可從中解出 task_suspend的地址:

// find bl _task_suspend
addr_t call = step64_back(kernel, str, 0x10, INSN_CALL);
if (!call) return 0;

addr_t task_suspend = follow_call64(kernel, call);
if (!task_suspend) return 0;
複製程式碼

隨後我們從 task_suspend 函式的起始地址開始向後搜尋第一個 adrp 指令即可找到對 Kernel Task 的 adrp 語句,靜態分析 adrp & add 即可計算出 Kernel Task 的地址:

addr_t adrp = step64(kernel, task_suspend, 20*4, INSN_ADRP);
if (!adrp) return 0;

addr_t kern_task = calc64(kernel, adrp, adrp + 0x8, 8);
if (!kern_task) return 0;
複製程式碼

注意這裡我們得到的依然是 fileoff,需要加上 kerndumpbase 得到虛擬地址:

return kern_task + kerndumpbase;
複製程式碼

需要注意的是,如果要在核心中讀取 Kernel Task,這個地址需要加上 kernel_slide 才可以。計算 kernel_slide 的程式碼緊跟在 tfp0 之後,讀者有興趣可以自行閱讀。

總結

本文詳細分析了 Undecimus 中基於 string 的交叉引用在記憶體中定位程式碼和變數的技術,通過該技術可以實現核心中變數地址的定位,隨後可通過讀寫實現繞過檢測和注入等操作。該技術不僅是完成 Jailbreak 的關鍵技術,也能給讀者帶來二進位制靜態分析的一些啟發。

iOS Jailbreak Principles - Undecimus 分析(二)通過 String XREF 定位核心資料

參考資料

  1. The iPhone Wiki: Kernelcache
  2. Apple: Darwin-XNU
  3. Github/pwn20wndstuff: Undecimus

相關文章