《C++反汇编与逆向分析技术揭秘》读书总结——虚函数

在C++中,使用关键字virtual声明函数为虚函数。当类中定义有虚函数时,编译器会将该类中所有虚函数的首地址保存在一张地址表中,这张表被称为虚函数地址表,简称虚表。同时,编译器还会在类中添加一个隐藏数据成员,称为虚表指针。该指针中保存着虚函数的首地址。

虚表是一个函数指针的数组。

例1:

class CVirtual
{
    public:
        virtual int GetNumber()
        {
            return m_nNmuber;
        }
        
        virtual void SetNumber(int nNumber)
        {
            m_nNumber = nNumber;
        }
    private:
        int m_nNumber;
};

如果这个类没有定义虚函数,则其长度为4(int类型),定义了虚函数后,由于还有隐藏数据成员(虚表指针),因此大小为8。 虚表指针被定义在对象首地址的前4字节处。

对象的多态性需要通过虚表和虚表指针来完成。由于非成员函数没有this指针,因此无法获得虚表指针,进而无法获取虚表,也就无法访问虚函数。

虚表指针实现过程:

对象的虚表指针初始化是通过编译器在构造函数内插入代码来完成的。用户没有编写构造函数时,由于必须初始化虚表指针,因此编译器会提供默认的构造函数,以完成虚表指针的初始化。

虚表中虚函数地址的排列顺序一句虚函数在类中的声明顺序而定,先声明的虚函数的地址会排在虚表中靠前的位置。第一个被声明的虚函数的地址在虚表的首地址处。

这种通过虚表间接寻址访问(虚函数)的情况只有在适用对象的指针或引用来调用虚函数的时候才会出现。

(比如

Test* pTest,test;

pTest = &test;

pTest->SetNumber();)

当直接使用对象调用自身的虚函数时,没有必要查表访问。这是因为已经明确调用的是自身的成员函数,根本没有构成多态性。

(比如

Test test;

test.SetNumber();)

比如下面的汇编代码在调用虚函数时,没有查虚表:

由构造函数初始化虚表指针引出一个识别构造函数的方法,如果排除开发者伪造编译器生成的代码来误导分析人员的可能,可以总结出结论:

对于单线继承的类结构,在其某个成员函数中,将this的地址初始化为虚表首地址时,可以判定这个成员函数为构造函数。

析构函数对虚表指针的操作:

构造函数和析构函数都是将虚表指针设置为当前对象所属类中的虚表首地址,两者看似相同,事实上差别很大。构造函数中完成的初始化虚表指针的工作,此时虚表指针没有指向虚表地址,而执行析构函数时,其对象的虚表指针已经指向了某个虚表首地址。这里实际上是在还原虚表指针,让其指向自身的虚表首地址,防止在析构函数中调用虚函数时取到非自身虚表,从而导致函数调用错误。

结合IDA中的引用参考可以得知,只要确定一个构造函数或者析构函数,我们就能顺藤摸瓜找到其他的构造函数以及类之间的关系。

虚函数的识别

鉴别类中是否出现了以下特征:

  • 类中隐式定义了一个数据成员;
  • 该数据成员在首地址处,占4字节;
  • 构造函数会将此数据成员初始化未某个数组(虚表)的首地址;
  • 这个地址属于数据区,是相对固定的地址;
  • 在这个数组内,每个元素都是函数指针;
  • 仔细观察这些函数,它们被调用时,第一个参数必然是this指针(要注意调用约定);
  • 在这些函数内部,很有可能对this指针使用相对间接的访问方式;

 

虚函数的识别最终转变成识别构造函数或者析构函数。在分析析构函数时,应重点考察对象首地址前4字节被赋予的值。

查询this指针所指向的地址中前4字节的内存数据,跟踪并分析其数据是否为地址信息,是否对这4字节的内容进行了赋值操作,赋值后的数据是否指向了某个地址表,表中各单元项是否为函数首地址。有了这一系列的鉴定流程后,就可得知此成员函数是否为一个构造函数。识别出构造函数后,即可顺藤摸瓜找到所有的虚函数。

总结成特征代码如下:

;具有成员函数特征,传递对象首地址作为this指针
lea ecx,[ebp-8]    ;获取对象首地址(貌似对象首地址常在[ebp-8]中)
call XXXXXXXXh     ;调用函数

;调用函数的实现代码
mov reg,this       ;某寄存器得到对象首地址
;向对象首地址处写入4字节数据,查看并确认此4字节数据是否为函数地址表的首地址

如遇以上代码,可高度怀疑为一个构造函数或者析构函数。查看并确认此4字节数据是否为函数地址表的首地址,即可判断是否为构造或析构函数。

如何区分构造函数与析构函数:

1、构造函数一定出现在析构函数之前,且构造函数执行前虚表指针没有指向虚表的首地址;

2、析构函数出现在所有成员函数之后,在实现过程中,虚表指针以及指向了某一个虚表的首地址。

 

识别出了虚表的首地址后,利用IDA的引用参考功能得到所有引用此虚表首地址的函数所在的地址标号。只有构造函数和析构函数中存在对虚表指针的修改操作,借此可以定位引用此虚表的所有构造和析构函数。(IDA会自动分析并标识出来)

 

《本章小结》中总结虚函数调用的识别:

如何确定[edx+4]一定是虚函数的地址呢?证实edx是虚函数表的首地址是关键,于是识别构造函数和析构函数尤为重要。先假设edx是虚表指针,然后结合IDA去查询应用参考,如果能找到所有的构造函数和唯一的析构函数(结合构造函数和析构函数的充要条件),就可以判断[edx+4]是否是虚函数了。如果假设不成立,那就应该怀疑是开发者自定义的函数指针数组。

 

atexit函数是寻找全局对象析构函数的指路灯,回头再补充。

 

 

本文为对原著内容的总结,非我原创。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值