一般情况下,为了获取函数之间的调用关系都是通过对源码进行静态分析得到。例如doxygen就是通过分析源码来获取函数调用关系链的,但是却存在一个缺点---需要依赖于源码,而且在跨模块的调用关系的获取上存在缺陷。本文提出一种通过逆向二进制文件的方式,通过反汇编的指令获取函数之间的调用关系。
站在逆向二进制的角度观察函数的调用关系,可以将函数分为以下几种类型:
1、普通函数的调用,分为两种一个是call指令调用,另一个是跳转指令调用。
2、函数指针的调用,指的是将函数作为参数进行传递,通过参数/变量进行调用。
3、类中虚函数的调用,通过虚表指针间接调用具体的子类函数。
先通过流程图描述核心思想,再一一详细介绍:
图1
首先,来介绍一下普通函数调用的情况:
这里所谓的普通函数的调用,指的是可以直接通过函数的虚拟地址进行直接的调用。从C/C++语言的角度来看,这个函数可以是一个纯C函数或者类成员非虚函数(补充:对于宏,在编译时就已将其替换为其所代表的项,所以在逆向的角度而言,若要获取宏的调用关系还需要进一步的将替换者变为宏,这个......)。从PE文件的角度考虑这个函数可能存储在.text的代码区,导入表,导出表三个地方中。
对于普通函数而言,在汇编层面直接调用的使其所在的函数地址,ida所在的加载器会将这个调用的实际函数地址替换成对应的函数名称,如下图1所示:
图2
通过对逆向汇编的分析,C/C++代码中的函数调用在编译成二进制之后,逆向成汇编语言,从普通函数的角度观察,调用函数的指令有两类:一类是call指令。另一类是跳转指令。如图2所示:
图3
汇编角度而言,普通函数的调用是最常用的一种形式,也最容易解析。
其次,介绍函数指针的具体情况:
函数指针一种使用形式就是回调函数(把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数)。函数指针的主旨是:作为参数,不管是函数的参数,还是作为一个成员变量。如GF皮肤库中的消息传递:
从汇编的角度而言,参数的传递主要通过以下两个指令:mov/push指令。以函数作为参数/变量进行传递的两种情况如图3所示:
图4
对于函数指针,我们只需要判断push/mov指令传递的地址是否是一个函数实际地址,若判断为真,就将其标明为一个函数指针的调用情况。
最后,介绍虚函数调用的具体情况:
虚函数基本概念的描述:作为面向对象最具特色的概念,对象的多态性需要通过虚表和虚表指针来完成,虚函数指针被定义在对象首地址的前4个字节处,因此虚函数必须作为成员函数使用。由于非成员函数没有this指针,因此无法获得虚函数表指针,进而无法获取虚表,也就无法访问虚函数。
在C++中,使用关键字virtual声明函数为虚函数,当类中定义有虚函数时,编译器会将该类中所有虚函数的首地址保存在一张地址表中,这张表被称为虚函数地址表,简称虚表。同时,编译器还会在类中添加一个隐藏数据成员,称为虚表指针。该指针中保存着虚表的首地址,用于记录和查找虚函数。如图5所示:包含虚函数的类的定义
图5
int nsize = sizeof(CVirtual);大小为8字节数据,多出了4字节数据,这4字节数据用于保存虚表指针。在虚表指针所指向的函数指针数组中,保存了虚函数GetNumber和SetNumber的首地址。对于开发者而言,虚表和虚函数指针都是隐藏的,在常规的开发过程中感觉不到她们的存在。对象中的虚表和虚函数指针的关系如图5所示:
图6
虚表指针的初始化是通过编译器在构造函数内插入代码来完成的。在虚表指针初始化的过程中,对象执行了构造函数后,就得到了虚表指针,当其他代码访问这个对象的虚函数时,会根据对象的首地址,取出对应虚表元素。当函数被调用时,会间接访问虚表,得到对应的虚函数首地址,并执行调用。此种调用是一个间接调用的过程,需要多次寻址才可以完成。
这种通过虚表间接寻址访问的情况只有在使用对象的指针或者引用来调用虚函数的时候才会出现。当直接使用对象调用自身的虚函数时,没有必要查表访问。这是已经明确调用的是自身成员函数,根本没有构成多态性,查询虚表只会画蛇添足,降低程序执行效率。
在逆向静态分析中虚函数缺失父调用函数关系,那么为什么会缺失父函数呢?看图:
图7
从图中我们可以知道子调用关系,却不知道父调用关系。那么为什么会产生这个问题呢?让我们一起看看一个有虚函数调用的函数的汇编实现:
图8
从上图可以很明白的知道,为什么虚函数父调用的关系缺失了,因为在汇编中这其实是一个地址的调用,要建立寄存器与具体虚表的关系是很困难的(或许本身就不可为)。一个解决方案是对IDA逆向C/C++伪码去获取虚函数名称(数据流指令的分析),然后通过虚函数名称去补全父函数调用关系, 但是通过对管家不同模块使用逆向伪码的功能,发现ida在逆向虚函数的时候准确率只能达到30%多,并且对不同版本IDA PRO的逆向虚函数伪码的功能进行的测试(除了最新的6.6未提供下载),发现准确率都很低。并且寻找了多个处理面向对象语言的插件效果也都不佳。在ida做了这么多年逆向虚函数的工作来看,这块工作耗时而且收效甚微。那么我们就退而求其次,做到现在可以确定做的事情:1、对于普通的非虚函数变更可以精确到函数级别的调用关系链的影响。 2、对于虚函数当其发生了变更,因为影响不能精确到函数级别,但是可以做到类级别。类之间的调用关系可以通过构造函数去确定,因为构造函数不是虚函数,这个前提是肯定的。
那么对于类调用关系的获取在管家有大致两种情况需要处理:
第一种是没有经过封装的直接的类之间的调用(包括模块内与模块间)。另一种是COM类跨模块间的类调用关系的处理,用如下流程图来表述:
图9
对于COM组件中数据的逆向处理因为比较复杂,这里不详细展开(后面特别用一篇文章描述)。
对于虚函数的处理,因为在静态逆向分析的情况下不能获取实际函数的调用,在万不得已的情况下,只能用类调用关系类弥补这方面数据的缺失。对于虚函数展示类调用关系,也可满足我们的业务需求。
下面是二进制变更/调用链与doxygen的对比图:
图10
上述整体描述了如何逆向分析获取函数调用关系链的方方面面,若是有讲的有误的地方,请大家指点改进,或者对虚函数的处理有更好的方法,要不吝赐教哦。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。