iOS通过逆向理解Block的内存模型

2020-01-18 20:02:15丽君

前言

正常情况下,通过分析界面以及 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 *...