《C++反汇编与逆向分析技术揭秘》之12——继承

时间:2023-03-09 15:56:06
《C++反汇编与逆向分析技术揭秘》之12——继承
  • 识别类和类之间的关系

在父类中声明为私有的成员,虽然子类对象无法直接访问,但是在子类对象的内存结构中,父类私有的成员数据依然存在。

在没有提供构造函数的时候,系统会尝试提供默认的构造函数:

当子类中没有构造函数或析构函数,而它的父类却需要构造函数与析构函数是,编译器会为这个子类提供默认的构造函数与析构函数。这是为了帮助父类对象完成初始化。同理,如果是子类对象的成员对象需要构造函数,那么系统也会为这个子类对象提供一个默认的构造函数。

如果子类中存在构造函数,而父类中不存在构造函数。那么,如果父类中没有虚函数,就不存在初始化虚表的任务,那么编译器就不会为父类提供默认的构造函数。反之,如果父类中存在虚函数,需要初始化虚表指针,那么就会为父类提供默认的构造函数。

子类对象被销毁时,为了能够调用父类的析构函数,编译器会为子类提供默认的析构函数。

编译器提供默认的构造函数是基于两种需要:1、父类、成员对象、本类需要初始化虚表;2、父类、成员对象定义了构造函数,需要调用。

子类对象在内存中的数据排列为:先安排父类数据,后安排子类新定义的数据。构造顺序为:先构造父类,后按照声明顺序构造成员对象和初始化列表中指定的成员,最后才是执行自身构造函数中的内容。

  • 构造函数内部虚函数失效问题

举例:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

子类的构造函数会先调用父类的构造函数,并以子类对象的首地址作为this指针传递给父类的构造函数

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

父类的构造函数中会有一步,把子类的虚表指针赋值为父类的虚表地址(目的是避免调用到子类的ShowSpeak,而是确保调用到父类的ShowSpeak):

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

编译器发现这时虚表是父类的虚表,而这时的作用域也是在父类构造函数之内,所以就转换成了直接调用

《C++反汇编与逆向分析技术揭秘》之12——继承

当你重新回到子类构造函数作用域中的时候,会重新给子类对象的虚表指针赋值:

《C++反汇编与逆向分析技术揭秘》之12——继承

以上是构造函数中出现虚函数的调用。如果是成员函数中出现虚函数的调用呢?

举例:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

两个Test中的ShowSpeak都是通过间接查找虚表来调用到虚函数的:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

这也解释了,为什么借助成员函数调用到的虚函数都是自身的那个虚函数了。

如果是在构造函数中调用了一个成员函数,而这个成员函数中又调用了虚函数呢?

举例:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

一上来调用CChinese的构造函数:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

调用父类的构造函数:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

调用父类构造函数中的Test时仍然使用的是子类对象的地址,但是会把子类对象的虚表指针的值赋值为父类的虚表地址:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

所以,尽管Test中的ShowSpeak是间接查虚表调用,但是仍调用到的是父类的虚函数:

《C++反汇编与逆向分析技术揭秘》之12——继承

考虑另外一种情况,类对象直接调用虚函数:

《C++反汇编与逆向分析技术揭秘》之12——继承

这如同调用普通 的成员函数,不会出现间接调用的情况,因为很明确是调用的哪个对象的函数:

《C++反汇编与逆向分析技术揭秘》之12——继承

但是如果是类对象调用了一个虚函数,而虚函数中又调用了虚函数呢?比如:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

直接调用虚函数:

《C++反汇编与逆向分析技术揭秘》之12——继承

虚函数内部间接调用另外一个虚函数:

《C++反汇编与逆向分析技术揭秘》之12——继承

这时的Test()并没有传递指针参数,内部也不是通过指针调用的ShowSpeak(),所以就直接通过ecx查找到虚表并进行间接调用。否则,会设置一个虚表指针,根据虚表指针间接调用到正确的ShowSpeak。

  • 多重继承

举例:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

构建CSofaBed的时候,先后构建两个父类:

《C++反汇编与逆向分析技术揭秘》之12——继承

构建CSofaBed时前两个数据分别是CSofa的虚表地址和m_nColor,后三个数据分别是CBed的虚表地址和m_nLength、m_nWidth:

《C++反汇编与逆向分析技术揭秘》之12——继承

两个虚表指向的内容分别如下:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

构建完两个父类之后,会重写这两个虚表地址的内容,并写入CSofaBed中独有的数据成员:

《C++反汇编与逆向分析技术揭秘》之12——继承

三个数据被更改了:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

因为CSofaBed中覆写了CSofa中的两个虚函数,所以0x00d8cad4中所指的函数地址数组中有两个地址与0x00d8ca58中所指的三个函数地址有所不同,而第三个红线下边的函数是CSofaBed自定义的虚函数GetHeight:

《C++反汇编与逆向分析技术揭秘》之12——继承

再来看0x00d8caec所指向的内容

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

其实这里也是跳转到0x00d83a60.和第一个虚表的第一个虚函数(CSofaBed的虚析构函数)最终要执行的位置是一样的。

再来看对CSofaBed进行析构的过程:

先填写了CSofaBed中的两个虚表地址:

《C++反汇编与逆向分析技术揭秘》之12——继承

然后再调用两个虚函数:

《C++反汇编与逆向分析技术揭秘》之12——继承

~CBed和~CSofa中又各自重新填写了虚表:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

提问,作为成员的类对象和继承下来的父类对象,在内存中的表现形式有什么区别?

如果类没有虚函数,那么作为成员和作为父类,在内存中的表现形式几乎没有区别。但是如果有虚函数存在,就会有区别。举例:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

调用完两个父类的构造函数之后,会有两个虚表指针的写入:

《C++反汇编与逆向分析技术揭秘》之12——继承

而随后的成员对象的构造函数被调用之后,没有虚表指针写入的步骤:

《C++反汇编与逆向分析技术揭秘》之12——继承

  • 抽象类

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

在构建成员对象MemberSon的时候会先构建其父类Member:

《C++反汇编与逆向分析技术揭秘》之12——继承

写入的虚表指针的第一项就是纯虚函数purefunc:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

来到了纯虚函数调用处:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

  • 菱形继承

举例:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

1、构造过程:

构造CSofaBed会先压入一个1:

《C++反汇编与逆向分析技术揭秘》之12——继承

进入CSofaBed的构造函数中发现,这里会有一个判断,这里的判断是为了防止重复构造Furniture。判断后,写入两个offset域到CSofaBed结构中,然后调用Furniture的构造函数:

《C++反汇编与逆向分析技术揭秘》之12——继承

101DB60:

《C++反汇编与逆向分析技术揭秘》之12——继承

101DB6C:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

调用完Furniture的构造函数之后,祖父类的内容在CSofaBed的内存结构中显现出来:

《C++反汇编与逆向分析技术揭秘》之12——继承

随后构造Sofa:

《C++反汇编与逆向分析技术揭秘》之12——继承

由于这里push的是0,所以Sofa的构造函数中会跳过对Furniture的构造,避免了重复构造:

《C++反汇编与逆向分析技术揭秘》之12——继承

随后Sofa写入了自己的虚表地址和数据内容,同时借助offset更改了Furniture位置上的虚表指针的值(毕竟Sofa也重写了Furniture的虚函数):

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

随后调用的CBed的构造函数同理,写入了自己的虚表地址,写入了自己的数据,覆写了Furniture位置上的虚表指针和数据:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

调用完CSofa和CBed的构造函数之后,写入自己的虚表指针,毕竟SofaBed也覆写了Sofa和Bed类中的虚函数:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

当然也要修改Furniture位置上的虚表指针的值:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

2、执行过程

借助offset找到Furniture的位置:

《C++反汇编与逆向分析技术揭秘》之12——继承

直接定位到Sofa的位置:

《C++反汇编与逆向分析技术揭秘》之12——继承

同样也是直接定位到Bed的位置:

《C++反汇编与逆向分析技术揭秘》之12——继承

借助offset调用到Furniture位置上的虚表中的虚函数:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

3、析构函数的调用

现在CSofaBed中析构CSofa和CBed,然后再在后边析构CFurniture:

《C++反汇编与逆向分析技术揭秘》之12——继承

  • 虚继承多个父类的情况

举例:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

内存布局如下:

《C++反汇编与逆向分析技术揭秘》之12——继承

而且offset处会记录多个父类的偏移

《C++反汇编与逆向分析技术揭秘》之12——继承

但是,如果有一个父类不是虚继承而来的,比如:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

那么存放offset的位置也会变化(不再是紧接在虚表指针之后):

《C++反汇编与逆向分析技术揭秘》之12——继承

先构建虚基类CFurniture:

《C++反汇编与逆向分析技术揭秘》之12——继承

随后构建CSofa:

《C++反汇编与逆向分析技术揭秘》之12——继承

《C++反汇编与逆向分析技术揭秘》之12——继承

这里可以看出,offset实际上是跟在CSofa的虚表以及父类CFurniture2的数据成员7之后的。因为实际上这个虚表指针0eda74和数据成员7是其父类Furniture2的成员,只不过CSofa覆写了这个虚表指针而已。

随后会构造CBed:

《C++反汇编与逆向分析技术揭秘》之12——继承

同样,offset是排在CBed中的父类CFurniture2的内容(一个Furniture2的虚表指针和一个Furniture2的数据成员)的后边。

最后写入CSofaBed的数据成员,构建完毕:

《C++反汇编与逆向分析技术揭秘》之12——继承

而此时查看offset中的内容:

《C++反汇编与逆向分析技术揭秘》之12——继承

只记录了一个父类的偏移。所以我们可以得出结论:offset中只记录虚基类的偏移,不记录父类的偏移