查看原文
其他

逆向 | 构造函数与析构函数

计算机与网络安全 计算机与网络安全 2022-06-01

一次性进群,长期免费索取教程,没有付费教程。

教程列表见微信公众号底部菜单

进微信群回复公众号:微信群;QQ群:460500587


微信公众号:计算机与网络安全

ID:Computer-network

一、快速识别出类


快速识别一个类的关键就是盯紧类所特有的“this指针”,在Visual Studio下,一般情况下会使用ecx寄存器传递this指针。我们都知道,每个成员函数在被调用时都要用this指针作为第一参数,因此我们在逆向工作中要时刻警惕使用ecx作为第一参数的函数,如果遇到这种情况,我们就有充足的理由怀疑它是一个类的成员函数。代码清单1就是一个简单的类调用示例。

代码清单1  简单的类调用示例

由代码清单1可知,在main()函数里调用了CObj类中的ShowMsg成员函数,并传入一个int型参数。


一般情况下,代码清单1会生成以下反汇编指令:

在阅读时要注意,后面的注释多数是手工添加上去的,也有IDA通过匹配符号文件自动添加的。之所以这样做是为了使您能够更加明了地对大段的反汇编代码做出一个基本判断,并有助于基础薄弱的朋友。但是这些注释在真实的逆向工作中是非常罕见的。因此在阅读这些代码及分析思路时,要尽可能地忽略这些内容,不到万不得已时不要以注释作为参照。


我想很多朋友看到这里会感觉到一头雾水,不过即便如此,这几行反汇编代码还是给了我们一些值得关注的信息:


通过第一行push 9与第三行call 00411440h可以看出一些疑点,因为一般情况下,除非要对参数做一些必要的取值处理,调用函数的参数应该都是紧邻call指令的,而这中间夹杂的lea ecx,[ebp-5h]指令似乎与call没有什么关系。


Visual Studio喜欢用ecx寄存器来传递this指针,而第三行lea ecx,[ebp-5h]恰好就是一个给ecx赋值的指令。而通过观察后面的代码我们发现,并没有用到ecx里的值。因此我们有理由猜测,之所以这样做是不是因为与其紧邻的call里会用到ecx呢?


如果以上猜测都成立的话,按照stdcall的调用规范,由于ecx是最紧邻call指令的,那么就证明ecx是被当作第一个参数传递进去的。


如果这些猜测都能成立,那么我们就能证明这个函数是某个类的成员函数的可能性就非常大了。


因此,既然这种简单的类的调用在汇编上显得无懈可击,我们就需要更深入地去观察,让我们分析一下成员函数CObj:ShowMsg(int)。

为了便于学习,我们提炼一下这段代码,其实真正做事的就是6条指令,剩下都是函数头或Debug版生成的东西。

从以上代码可以确定,ecx确实是被作为一个参数传递进来的,并进行了保存,然后紧跟着就是一些获取参数并执行相应功能的代码了。如果ecx中确实保存的是this指针的话,我们看到这个所谓的this指针似乎并没有什么用,像个鸡肋。


其实即便是到目前为止,我们仍然无法确定这就是一个成员函数的调用。我们通过前面的学习知道,如果函数是使用fastcall调用方式就会出现使用寄存器传参的情况。虽然Visual C++编译器在6.0以后的fastcall调用方式里也并没有使用ecx寄存器,但是在大多数情况下这些特征更像是一厢情愿的分析结果,因为编译器的不同,或使用了aux标记的程序都有可能生成这种代码。

所以,到这里我想您应该有所察觉了,如果要识别出一个类成员函数的调用,那么像这种简单的代码我们只能臆测,如果想拿到更多的证据,那么就只能期盼这是一个比较复杂的类。对于C++类的识别来说,复杂些的程序反而更有利于我们展开逆向工作。


既然如此,那我们就修改一下代码,让它变得更复杂些,如代码清单2所示。

代码清单2  稍复杂的类调用示例

这次我们为类加上了构造函数,并初始化了成员函数。下面再看看代码清单2的Debug版反汇编代码。

这里似乎仍然看不出什么,不过两次函数调用的参数似乎都是用到了ecx,且通过反汇编代码可知,ecx的值始终都没变化,遇到这种情况,我们其实已经有充分的理由怀疑这是两个成员函数了。


为了进一步确认,我们先看看第一个call里面是什么。

通过此段反汇编代码我们发现了很多值得怀疑的地方。


首先,ecx作为一个指针被当作参数传递了进来,并且还将ecx指向的地方当作了一个结构体,分别为其赋了两个值。


其次,保存有ecx值的[ebp-8]又被当作返回值传了回去。


现在就需要我们回忆一下C++的基础知识了。我们知道在C++的类中,this指针始终指向本类结构的起始地址,一般情况下类的结构如下所示。

因此,对this指针指向的内容进行操作,就是对类内部的成员进行操作。因此我们更加有理由怀疑这是一个类的成员函数了。


接着往下看,我们再进入第二个call中看看。

又是由进来的ecx寻址,并给一个地方赋了值,这非常可疑,而且与上一个call里面的内容几乎一样。因此结合上下文,我们大致就可以判定这是个类的成员函数了。


看到这里有的朋友很定会很疑惑,这是为什么?为什么感觉都是不确定的?难道就没有一些可以确定的特征吗?我要很遗憾地说,逆向本就是总结特征并且靠经验分析的一项技术,其实本来就没有什么绝对可靠的特征,只不过C++类的逆向在此处表现得尤为严重而已。


综上所述,我们得出以下两条经验:


紧盯采用ecx传参的函数调用。

留意以ecx为指针进行数据操作的代码。


二、识别构造函数


我们在上一个例子里使用构造函数初始化了类的两个数据成员,并且使得类变得更易识别。但是如果只给我们汇编代码的话,该怎样确定一个类中的构造函数呢?若之前你已确定了这是一个类,那么再去确定其中的构造函数就很简单了。


首先,从调用上来说,我们可以判定传入同一this指针的第一次调用就有可能是构造函数。就像上一个例子那样,两次call的ecx参数都来自同一位置,那么我们就要考虑验证一下它的第一个call调用的是否为构造函数了。


其次,从函数内部看,构造函数会将this指针作为返回值。让我们再回顾一下上面的那个例子。

我们可以看到,此函数将this指针作为返回值返回了,因此我们就可以大致判定这应该就是一个构造函数了。


综上所述,我们得出以下两条经验:


传入同一this指针的第一次调用有可能是构造函数。

将this指针作为返回值的函数有可能是构造函数。


三、识别析构函数


简单的类识别析构函数可能显得有些不可靠,因为只有一个特征,从调用上来说,同一this指针的最后一次被使用时调用的函数就有可能是析构函数。我们来看代码清单3。

代码清单3  带析构函数的类

这个例子与代码清单2中的例子几乎一样,唯一不同的就是我们增加了一个析构函数。下面让我们来看看它的调用。

我们发现加了析构函数后多了一个成员函数的调用,我们进一步看看。

我们无法在这个函数中找到任何有别于其他成员函数的特征,所以无从判断。如果你想以比较可靠的方式识别出一个类的析构函数,那么就需要那个类满足以下条件:


包含有虚函数。

包含有空间申请操作。


所以,为了减少逆向工作中的臆测成分,掌握虚函数的识别对于我们来说是至关重要的。

微信公众号:计算机与网络安全

ID:Computer-network

【推荐书籍】

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存