加密与解密x64逆向——虚函数

4.整数的取模

取模运算可以通过除法指令实现。一般的优化做法是将其转换成等价的位运算或者除法运算,再由除法运算进行优化。

虚函数

C++的三大核心机制是封装,继承,多态,而虚函数就是多态的一种体现。软件逆向中,难免遇到使用面向对象思想设计的软件,而虚函数就是在实际软件逆向过程中的一种还原面向对象的重要手段。本章探讨编译器实现虚函数的原理。

1.虚表





首先在main的入口先申请了对象实例的内存空间。先调用构造函数,调用fun1,fun2.这些成员函数调用的第一个参数就是this指针,也就是rcx=this。C++语法规定在实例化对象时会自动调用构造函数,对象作用域会自动调用析构函数。
在逆向过程中,当一个对象是在某个作用域内调用的第一个函数,就可以怀疑是构造函数,如果是最后一个,就怀疑是析构函数的调用。
构造函数中首先初始化虚表指针,然后初始化数据成员,构造函数完成,返回this指针。

然而为什么析构函数还要赋值虚表,构造函数不是赋值了吗?这是因为C++语法规定,析构函数需要调用虚函数的多态性。在分辨哪一个是构造函数哪一个是析构的时候可以看调用的顺序。
虚标中的函数成员函数声明顺序依次放入的。需要注意的是函数分布顺序在某些情况,和声明顺序不一定相同。不过顺序对逆向还原也没有影响。本例中只写了一个虚析构函数吗,却生成了两个析构函数。其中一个是普通析构函数,一个是放在虚表里的。

2.单重继承虚表






为什么main函数使用new申请对象空间。因为虚表需要8字节虚表空间和8字节的内存对齐。这里可以看出派生类和基类共享一个虚表指针。

 

3.多重继承虚表

多重继承是指一个类同时继承多个父类。多重继承与单重继承相比,可以有多个父类。




可以看出main函数与单重继承无区别。在cderived构造函数中,首先按继承顺序调用两个基类的构造函数,然后执行自己的构造函数代码。
因为两个基类都有虚函数,在这种情况下编译器会为派生类生成两个虚表,在构造函数时初始化。虚表项的构造顺序和上一个例子基本一致,唯一的区别在于派生类新增的虚函数挂在了第一个虚表后面。

4.菱形继承虚表

两个子类继承自同一个父类,又有子类同时继承这两个子类(在子类的子类中出现)。在内存布局会出现两个相同的父类内存结构。







main函数中的构造函数调用有点特殊,多传递了一个标识是否调用基类构造函数的标志参数。按语法B的构造函数要先构造A,这样编译器就要在B类的构造函数里调用A类的构造函数。顺序没有问题,然而在BC的构造也要先构造B构造C,在BC的构造函数里调用了B类的构造函数和C的构造函数。这里就有问题,因为这样子A的构造函数就被调用了两次。而这是多传递了一个参数就是用来表示是否调用虚基类的构造这样就可以防止虚基类被调用两次。
在调用虚基类构造函数之前,出现了一个初始化虚基类偏离表的操作。这是因为存在徐继承,虚基类对象的内存在派生类的内存中只保留一份,所以在编译器方便定位虚基类在对象内存中的位置,做一个虚基类偏移表。
所以我们在逆向分析时,如果发现构造函数有初始化虚基类偏移表的操作就可以怀疑这个类继承层次带有虚继承。

5.抽象类虚表

在C++中,含有纯虚函数的类称为抽象类,它不能实例化对象。面向对象设计中常用抽象类给子类规范接口,接口的功能通常都是重要功能。它最大的特点就是没有实现代码。如果在逆向分析时找到一个类的抽象类那么就可以通过他的构造函数就可以定位它的所有子类,得到重要功能。



可以看出父类为抽象类的实现代码和单重继承没有太大区别,唯一的别就是虚表。由于纯虚函数没有实现代码,编译器默认填充_purecall函数地址。
如果发现一个类的虚表里面有_purecall虚表项。

小结

 

posted @ 2023-04-21 23:19  bonelee  阅读(46)  评论(0编辑  收藏  举报