iOS通过逆向理解Block的内存模型
前言
正常情况下,通过分析界面以及 class-dump 出来头文件就能对某个功能的实现猜个八九不离十。但是 block 这种特殊的类型在头文件中是看不出它的声明的,一些有 block 回调的方法名 dump 出来是类似这样的:
- (void)fm_getsubscribelist:(long long)arg1 pagesize:(long long)arg2 callback:(cdunknownblocktype)arg3;
因为这种回调看不到它的方法签名,我们无法知道这个 block 到底有几个参数,也不知道它函数体的具体地址,因此在使用 lldb 进行动态调试的时候也是困难重重。我也一度被这个困难所阻挡,以为调用到有 block 的方法就是进了死胡同,没办法继续跟踪下去了。我还因此放弃过好几次对某个功能的分析,特别受挫。
好在,我们还有 google 这个强大的武器。没有什么问题是一次 google 不能解决的。如果有,那就两次。
这篇文章就来讲讲如何通过 block 的内存模型来分析出它的函数体地址,以及函数签名。
block 的内存结构
在 llvm 文档中,可以看到 block 的实现规范,其中最关键的地方是对于 block 内存结构的定义:
struct block_literal_1 { void *isa; // initialized to &_nsconcretestackblock or &_nsconcreteglobalblock int flags; int reserved; void (*invoke)(void *, ...); struct block_descriptor_1 { unsigned long int reserved; // null unsigned long int size; // sizeof(struct block_literal_1) // optional helper functions void (*copy_helper)(void *dst, void *src); // iff (1<<25) void (*dispose_helper)(void *src); // iff (1<<25) // required abi.2010.3.16 const char *signature; // iff (1<<30) } *descriptor; // imported variables };
可以看到第一个成员是 isa,说明了 block 在 objective-c 当中也是一个对象。我们重点要关注的就是 void (*invode)(void *, ...);
和 descriptor 中的 const char *signature
,前者指向了 block 具体实现的地址,后者是表示 block 函数签名的字符串。
实战
注:本篇文章都是在 64 位系统下进行分析,如果是 32 位系统,整型与指针类型的大小都是与 64 位不一致的,请自行进行修改。
知道了 block 的内存模型后,就可以直接打开 hopper 和 lldb 进行调试了。
我这里使用了逻辑思维的得到 app 作为分析的例子。顺便说一句,得到上面的内容都相当不错,很多付费专栏的内容都是很赞的,值得一看。
准备
设备:iphone 5s ios 8.2 越狱
usbmuxd
$ tcprelay -t 22:2222 1234:1234 forwarding local port 2222 to remote port 22 forwarding local port 1234 to remote port 1234 ......
ssh 到 ios 设备并启动 debugserver:
$ ssh root@localhost -p 2222 iphone $ debugserver *:1234 -a "luojifm-ios" ebugserver-@(#)program:debugserver project:debugserver-320.2.89 for arm64. attaching to process luojifm-ios... listening to port 1234 for a connection from *...
本地打开 lldb 并远程附加进程,进行动态调试:
$ lldb (lldb) process connect connect://localhost:1234
找到偏移地址:
(lldb) image list -o -f [ 0] 0x0000000000074000 /private/var/mobile/containers/bundle/application/d106c0e3-d874-4534-aed6-a7104131b31d/luojifm-ios.app/luojifm-ios(0x0000000100074000) [ 1] 0x000000000002c000 /users/wordbeyond/library/developer/xcode/ios devicesupport/8.2 (12d508)/symbols/usr/lib/dyld
在 hopper 下找到需要断点的地址:
下断点:
(lldb) br s -a 0x0000000000074000+0x0000000100069700 breakpoint 2: where = luojifm-ios`_mh_execute_header + 407504, address = 0x00000001000dd700
然后在应用中点击订阅 tab ,此时会命中断点(如果没有命中,手动下拉刷新下)。
众所周知,objective-c 方法的调用都会转化成 objc_msgsend 调用,因此单步的时候看到 objc_msgsend 就可以停下来了:
-> 0x1000dd71c <+431900>: bl 0x100daa2bc ; symbol stub for: objc_msgsend 0x1000dd720 <+431904>: mov x0, x20 0x1000dd724 <+431908>: bl 0x100daa2ec ; symbol stub for: objc_release 0x1000dd728 <+431912>: mov x0, x21 (lldb) po $x0 <dataservicev2: 0x17400cea0> (lldb) po (char *)$x1 "fm_getsubscribelist:pagesize:callback:" (lldb) po $x4 <__nsstackblock__: 0x16fd88f88>
可以看到,第四个参数是个 stackblock 对象,但是 lldb 只为我们打印出了它的地址。接下来,就靠我们自己来找出它的函数体地址和函数签名了。
找出 block 的函数体地址
要找出 block 的函数体地址很简单,根据上面的内存模型,我们只到找到 invoke 这个函数指针的地址,它指向的就是这个 block 的实现。
在 64 位系统上,指针类型的大小是 8 个字节,而 int 是 4 个字节,如下:
因此,invoke 函数指针的地址就是在第 16 个字节之后。我们可以通过 lldb 的 memory 命令来打印出指定地址的内存,我们上面已经得到了 block 的地址,现在就打印出它的内存内容:
(lldb) memory read --size 8 --format x 0x16fd88f88 0x16fd88f88: 0x000000019b4d8088 0x00000000c2000000 0x16fd88f98: 0x00000001000dd770 0x0000000100fc6610 0x16fd88fa8: 0x000000017444c510 0x0000000000000001 0x16fd88fb8: 0x000000017444c510 0x0000000000000008
如前所述,函数指针的地址是在第 16 个字节之后,并占用 8 个字节,所以可以得到函数的地址是 0x00000001000dd770。
有了函数地址之后,就可以对这个地址进行反汇编:
(lldb) disassemble --start-address 0x00000001000dd770 luojifm-ios`_mh_execute_header: -> 0x1000dd770 <+431984>: stp x28, x27, [sp, #-96]! 0x1000dd774 <+431988>: stp x26, x25, [sp, #16] 0x1000dd778 <+431992>: stp x24, x23, [sp, #32] 0x1000dd77c <+431996>: stp x22, x21, [sp, #48] 0x1000dd780 <+432000>: stp x20, x19, [sp, #64] 0x1000dd784 <+432004>: stp x29, x30, [sp, #80] 0x1000dd788 <+432008>: add x29, sp, #80 ; =80 0x1000dd78c <+432012>: mov x22, x3
也可以直接在 lldb 当中下断点:
(lldb) br s -a 0x00000001000dd770 breakpoint 3: where = luojifm-ios`_mh_execute_header + 407616, address = 0x00000001000dd770
再次运行函数,就可以进到回调的 block 函数体内了。
但是,大多数情况下,我们并不需要进到 block 函数体内。在写 tweak 的时候,我们更需要的是知道这个 block 回调给了我们哪些参数。
接下来,我们继续进行探索。
找出 block 的函数签名
要找出 block 的函数签名,需要通过 descriptor 结构体中的 signature 成员,然后通过它得到一个 nsmethodsignature 对象。
首先,需要找到 descriptor 结构体。这个结构体在 block 中是通过指针持有的,它的位置正好在 invoke 成员后面,占用 8 个字节。可以从上面的内存打印中看到 descriptor 指针的地址是 0x0000000100fc6610。
接下来,就可以通过 descriptor 的地址找到 signature 了。但是,文档指出并不是每个 block 都是有方法签名的,我们需要通过 flags 与 block 中定义的枚举掩码进行与判断。还是在刚刚的 llvm 文档中,我们可以看到掩码的定义如下:
enum { block_has_copy_dispose = (1 << 25), block_has_ctor = (1 << 26), // helpers have c++ code block_is_global = (1 << 28), block_has_stret = (1 << 29), // iff block_has_signature block_has_signature = (1 << 30), };
再次使用 memory 命令打印出 flags 的值:
(lldb) memory read --size 4 --format x 0x16fd8a958 0x16fd8a958: 0x9b4d8088 0x00000001 0xc2000000 0x00000000 0x16fd8a968: 0x000dd770 0x00000001 0x00fc6610 0x00000001
由于 ((0xc2000000 & (1 << 30)) != 0),因此我们可以确定这个 block 是有签名的。
虽然在文档中指出并不是每个 block 都有函数签名的。但是我们可以在 clang 源码 中的 cgblocks.cpp 查看 codegenfunction::emitblockliteral
与 buildglobalblock 方法,可以看到每个 block 的 flags 成员都是被默认设置了 block_has_signature。因此,我们可以推断,所有使用 clang 编译的代码中的 block 都是有签名的。
为了找出 signature 的地址,我们还需要确认这个 block 是否拥有 copy_helper 和 disponse_helper 这两个可选的函数指针。由于 ((0xc2000000 & (1 << 25)) != 0)
,因此我们可以确认这个 block 拥有刚刚提到的两个函数指针。
现在可以总结下:signature 的地址是在 descriptor 下偏移两个 unsiged long 和两个指针后的地址,即 32 个字节后。现在让我们找出它的地址,并打印出它的字符串内容:
(lldb) memory read --size 8 --format x 0x0000000100fc6610 0x100fc6610: 0x0000000000000000 0x0000000000000029 0x100fc6620: 0x00000001000ddb64 0x00000001000ddb70 0x100fc6630: 0x0000000100dfec18 0x0000000000000001 0x100fc6640: 0x0000000000000000 0x0000000000000048 (lldb) p (char *)0x0000000100dfec18 (char *) $4 = 0x0000000100dfec18 "v28@?0q8@"nsdictionary"16b24"
看到这一串乱码是不是觉得有点崩溃,折腾了半天,怎么打印出这么一串鬼东西,虽然里面有一个熟悉的 nsdictionary,但是其它的东西完全看不懂啊。
不要慌,这确实就是一个函数签名,只是我们需要通过 nsmethodsignature 找出它的参数类型:
(lldb) po [nsmethodsignature signaturewithobjctypes:"v28@?0q8@\"nsdictionary\"16b24"] <nsmethodsignature: 0x174672940> number of arguments = 4 frame size = 224 is special struct return? no return value: -------- -------- -------- -------- type encoding (v) 'v' flags {} modifiers {} frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0} memory {offset = 0, size = 0} argument 0: -------- -------- -------- -------- type encoding (@) '@?' flags {isobject, isblock} modifiers {} frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0} memory {offset = 0, size = 8} argument 1: -------- -------- -------- -------- type encoding (q) 'q' flags {issigned} modifiers {} frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0} memory {offset = 0, size = 8} argument 2: -------- -------- -------- -------- type encoding (@) '@"nsdictionary"' flags {isobject} modifiers {} frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0} memory {offset = 0, size = 8} class 'nsdictionary' argument 3: -------- -------- -------- -------- type encoding (b) 'b' flags {} modifiers {} frame {offset = 24, offset adjust = 0, size = 8, size adjust = -7} memory {offset = 0, size = 1}
注意,字符串中的双引号需要对其进行转义。
对我们最有用的 type encoding 字段,这些符号对应的解释可以参考 type encoding 官方文档。
所以,总结来讲就是:这个方法没有返回值,它接受四个参数,第一个是 block (即我们自己的 block 的引用),第二个是 (long long) 类型的,第三个是一个 nsdictionary 对象,第四个是一个 bool 值。
最终,我们得到了这个 block 的函数参数。最初提到的那个方法签名的完整版就是:
- (void)fm_getsubscribelist:(long long)arg1 pagesize:(long long)arg2 callback:(void (^)(long long, nsdictionary *, bool)arg3;
小结
因为想使用真实的例子进行演示,所以本文直接使用逆向的动态分析进行说明。其实上面提到的所有过程,都可以直接在 xcode 通过自己写的代码进行操作。通过自己动手分析一遍,比看十篇文章来得更有效果。下次如果面试再有人问到 block 的实现和内存模型,你就可以跟它侃侃而谈了。
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。
推荐阅读