最近,在使用IDA Pro研究iOS应用的过程中,我发现,虽然IDA Pro和神奇的Decompiler插件能够以超高的还原度生成大部分的源代码,但如果想要针对某一个方法跟踪交叉引用(Cross Reference)的话,会发现其中缺失了许多实际存在的交叉引用,这对于静态分析工作的完整性造成了极大的挑战。
DUO Labs在https://duo.com/blog/reversing-objective-c-binaries-with-the-reobjc-module-for-ida-pro发布了一篇文章,提到了IDA Pro中的这个问题,解释了其中的原理,并开发了一款工具,帮助逆向研究者更全面地获取交叉引用的信息。
Objective-C与IDA Pro交叉引用
Objective-C是C语言的变体。用它开发的程序与Objective-C Runtime共享库链接。这个库实现了整个对象模型来支持Objective-C。
Runtime的目标之一是尽可能动态。这样的设计会对对象的函数调用产生影响。在Objective-C中,函数方法的调用被称为消息传递。对象一旦接收到这些消息,会调用其中的一个对象的方法。Runtime在运行时动态解析方法调用。Objective-C源代码中的方法调用由编译器转换为Runtime函数“objc_msgSend()”的调用。
在这里,我们将仔细研究一下IDA Pro模块REobjc,该模块将调用objc_msgSend()的正确交叉引用添加到被调用的实际函数中。
如果从一个方法到另一个方法的调用被编译为对objc_msgSend()的调用,这样运行时调用的实际函数将不会反映在IDA Pro的交叉引用中。objc_msgSend()的函数签名定义如下:
id objc_msgSend(id self, SEL op, …)
对于任何的Objective-C方法调用,前两个参数是对象的self指针以及selector,selector参数是调用方法对应的字符串表示。带参数的Objective-C方法将在selector之后按顺序传递参数。
编译Objective-C程序
下面的代码示例为一个简单的Objective-C源代码。此init方法包括4个Objective-C方法调用。
从概念上讲,编译器读取上面的Objective-C方法,并将它们编译成类似于以下内容的C代码。这个C代码示例实际上来自于IDA Pro的反编译器输出,它很好地说明了编译器如何将Objective-C调用转换为C。
如图所示,[super init]被转换为对objc_msgSendSuper2()的调用。这是初始化子类时的常见做法。[NSString string]被转换为objc_msgSend()调用,该调用被发送到NSString的对象。对于有参数的类方法的调用,[NSMutableData dataWithLength:_length]也被转换为对objc_msgSend()的调用。
示例中的最后一个Objective-C调用[[BTGattLookup alloc] init]展示了一个常见的allocate-initialize模式。首先,BTGattLookup类收到alloc消息,构造出了该类的实例。然后使用第二个objc_msgSend()调用init。
如果生成对应英特尔X64架构的二进制文件,调用将依照英特尔X64 ABI。函数参数按照RDI,RSI,RDX,RCX,R8,R9的顺序在寄存器中传递。这意味着RDI寄存器保存自指针,RSI寄存器保存选择器指针。如有参数,将从RDX寄存器开始保存。
如果要在IDA中添加从一个Objective-C函数到另一个Objective-C函数正确的交叉引用的话,就必须知道RDI和RSI寄存器中的值。对于大多数objc_msgSend()调用,这两个寄存器中的值通常很容易获取。
不过,除了上述方法调用方式之外,编译器可能也会使用其他方式。在X64上,编译器通常使用CALL和JMP指令生成函数调用。其他的可能性有使用条件跳转指令或直接分配方法给指令指针,但是概率很小。
编译器还可以将函数调用编码为间接调用或直接调用。在间接调用的情况下,指令参数是寄存器。在直接调用的情况下,指令参数是对内存位置的引用。不管哪种情况,都必须确定CALL或JMP是否引用了objc_msgSend()。
此外,为了正确生成交叉引用,必须跟踪函数调用的返回值。在X64中,函数调用的返回值存储在RAX寄存器中。如果在源码中首先alloc一个对象,然后对生成的对象实例执行方法调用,则需要跟踪存储在RAX中的对象指针的类型,以正确理解调用objc_msgSend()时传递的对象。
REobjc Idapython模块
REobjc idapython模块的主要目的是在Objective-C方法之间创建交叉引用。要使用REobjc,打开IDA Pro命令窗口并执行以下Python脚本:
idaapi.require(“reobjc”)
r = reobjc.REobjc(autorun = True)
REOBjc深入
要定位到我们关注的Objective-C Runtime调用,首先需要理解编译器可以通过多种方式在二进制中对调用进行编码。无论是直接的还是间接的调用,都有多种方式对调用指令进行编码。 所有Objective-C程序都链接了Objective-C Runtime,因此,所有调用最终都会进入导入的libobjc.dylib库。
通常,程序将包含一个插桩(stub)函数,它只执行无条件跳转到objc_msgSend()函数。这允许程序在任意地址加载并且正确调用库。
在REobjc模块中,将通过识别objc_msgSend()的所有调用形式来识别方法调用。调用的目标可能是_objc_msgSend,可能是__imp__objc_msgSend形式的导入指针。模块将查找当前数据库中所有相关的内容。
模块使用API idautils.Names()检索IDA数据库中所有的名称,然后通过正则表达式匹配目标函数,将匹配存储在数组中。分析时,每个候选调用或跳转指令都与Objective-C Runtime函数列表进行比较,记录使用任意形式的objc_msgSend()调用,并添加到交叉引用列表中。
随后模块迭代二进制文件中的所有函数,并且对于每个函数,迭代其中所有的指令。当识别出CALL或JMP指令时,模块确定指令的对象。如果对象是寄存器,则从CALL或JMP指令向前寻找寄存器的值。直接调用相比较为简单,可以立即获得CALL或JMP的对象。无论哪种情况,如果目标是objc_msgSend(),则CALL或JMP很有可能添加交叉引用。
当识别出objc_msgSend()的调用时,必须标识前两个函数参数。如上文所述,第一个参数指向接收Objective-C消息对象的指针。第二个参数是指向传递给对象的selector或消息的指针。由resolve_register_backwalk_ea()方法解析寄存器的值。
如上文所述,这里有两个寄存器需要特别关注。 RAX寄存器将保存先前CALL指令的返回值,因此REobjc.resolve_register_backwalk_ea()将通过CALL来跟踪RAX。此外,由于RDI寄存器用作Objective-C中的self指针,因此有些情况下RDI未在函数内显式设置。这是因为这些目标函数是在自己的self指针上调用方法。因此,当往回寻找时,如果RDI目标寄存器并且未显式设置,则代码将确定该方法来自当前类。
一旦为RDI和RSI寄存器解析了self和selector指针,模块将对可能方法尝试创建交叉引用。这是通过利用IDA Pro中现有的Objective-C支持来完成的。模块函数REobjc.objc_msgsend_xref将处理交叉引用的创建。该函数获取CALL的位置,以及设置RDI和RSI的位置,并尝试添加适当的交叉引用。
目前模块只能添加在当前二进制文件中方法的交叉引用,之后会尝试添加导入的库中方法的交叉引用。
REobjc:实践
接下来,我将通过搭建实际的iOS测试工程来实践不同方法调用方式的效果以及REobjc模块对完善交叉引用记录的效果。
首先,我们看一下最基础的一个调用。每一个ViweController都会默认调用父类中的viewDidLoad方法。
把工程编译后,在IDA Pro中看到的汇编代码如下:
使用Decompiler插件生成伪代码后:
可以发现,IDA Pro很好地还原了方法调用的逻辑。查看交叉引用信息,发现IDA也识别出了该调用。只不过,识别出来的被调用函数为msgSendSuper2,并不是我们想要的viewDidLoad。
我们运行REobjc后发现,交叉引用中添加了一条记录,表示ViewController的viewDidLoad方法被调用。DUO Labs作者在文章中也提到,目前的版本在正确识别父类方法调用上会有问题,可能会将子类的方法调用交叉引用添加到父类中,这就导致了我们在子类的交叉引用中看到了这条指向自己的记录。
接下来我们自己编写一个方法,在Lion类中新建一个实例方法lionFirstMethod如下:
可以看到方法中仅调用了NSLog用来打印调试语句。我们在工程其他方法中有两处引用了该方法,可以在搜索中看到:
接下来我们在IDA Pro中查看交叉引用信息,发现IDA Pro一个也没有识别出来(因为都识别成了msgSend)。
其实,这个结果是让我有点吃惊的,毕竟代码中写的还是非常直白的。
从生成的伪代码中也可以清晰地看出方法调用
随后我们尝试使用REobjc模块进行补完,发现交叉引用中正确地添加上了两条引用记录。
接下来,我们再做一个尝试。我们在Lion类中添加一个类方法,并且使用反射的方式引用类和该类方法。
不出意外,我们发现,IDA Pro并没有识别出该调用(仅识别出了msgSend,没有识别出performSelector,更不用说lionClassMethod了)。
当我们尝试使用REobjc进行补完,发现也没有识别出来最终的lionClassMethod。
总体来说,REobjc对一些基础的方法调用进行了补充,可以说完善了IDA Pro在Objective-C方面的一些缺陷,但是在更加细节的方面需要进行加强。另外,REobjc还没有针对arm架构的版本,我们使用模拟器进行编译的可执行文件可以使用REobjc,但是对于实际需要进行逆向分析的目标应用,通常无法获取非arm版本,所以后续我们也会尝试针对arm平台进行补充完善。
参考:https://duo.com/blog/reversing-objective-c-binaries-with-the-reobjc-module-for-ida-pro
发表评论
您还未登录,请先登录。
登录