Back

/ 18 min read

c++逆向

类成员函数

thiscall约定

特定于 Microsoft __thiscall 的调用约定用于 x86 体系结构上的 C++ 类成员函数。 它是成员函数使用的默认调用约定,该约定不使用变量参数(vararg 函数)。

__thiscall 下,被调用方清理堆栈,这对于 vararg 函数是不可能的。 自变量将从右到左推送到堆栈中。 指针 this 通过注册 ECX 传递,而不是在堆栈上传递。

image-20250209205748660

类成员构造函数,会将类指针写入eax返回,而普通成员函数不会返回。

image-20250209211018431
// 在汇编层面上,函数签名大致为:
Class* __thiscall foo(Class* cl);

识别成员函数

构造函数

  • thiscall约定。
  • 返回this指针。(eax返回)

局部作用域

image-20250210122152168

new堆作用域

image-20250210122442680

new作用域更容易识别构造函数,因为构造函数的代码一定在检查之后,即使内联。

构造函数的返回值必须是this指针,局部作用域返回值在外部没用到。

堆作用域,返回值要在外部进行检查,如果为空,那就不调用构造函数。

析构函数

  • thiscall约定
  • 无参
  • 无返回值
  • 通过析构代理函数调用,通过参数决定是否调用释放

debug局部变量析构

Release版的关闭内联的汇编一致。

image-20250210124841658

Debug手动析构

  • delete会检查空指针。
  • 先调用析构,再调用delete释放内存。

显示调用析构不释放内存。

析构代理
// 函数签名
void scalar_deleting_destructor(bool is_free); // 控制是否释放内存
image-20250210130621645
  • push 1表示释放内存
  • push 0表示仅析构,不释放内存。

无参成员函数

image-20250209205527183
  1. 将结构体地址传递到ecx寄存器。
  2. 调用函数。

有参成员函数

image-20250209205639630
  1. 通过栈传递参数。
  2. 通过ecx传递this指针。
  3. 调用函数。

特殊

可以手动修改成员函数的调用约定。

手动修改成员函数调用约定在进行FFI的时候非常有用。比如想让rust使用c++类中的函数。

image-20250210124149823

类数组

Person a[10];

类数组在初始化的时候要批量调用构造函数,还需要让delete[]能够释放这一整块内存。所以类数组在申请内存的时候会多申请4个字节,用来保存长度。

image-20250211094742605

类构造迭代器

当使用Person ps[10];这种方式定义类数组时,编译器将优化为使用类构造迭代器来进行循环构造。

image-20250211110129309
void vector_constructor_iterator(void* ary_address, size_t objcet_size, size_t object_num, void (*)(void *), constructor);
// 它还有一个eh vector constructor iterator 这个包含一个析构函数的参数
void eh_vector_constructor_iterator(void* ary_address, size_t objcet_size, size_t object_num, void (*)(void *) constructor, void (*)(void *) destructor);
image-20250211111230191
#include <cstdio>
class Person{
public:
Person(){
std::printf("Person\n");
}
~Person(){
std::printf("~Person()\n");
}
};
int main(){
Person ps[10];
return 0;
}

对象传递

类对象作为参数

特征:

  1. 函数外构造。
  2. 函数内析构。

不包含拷贝构造

#include <cstdio>
class Person{
public:
int age;
char name[25];
Person(){
std::printf("Person\n");
}
Person(int age){
std::printf("Person(int age)\n");
this->age = age;
}
~Person(){
std::printf("~Person()\n");
}
// 拷贝构造
// Person(const Person& other){
// this->age = other.age;
// printf("Person(const Person& other)\n");
// }
};
void show_age(Person p){
printf("show_age\n");
printf("age: %d\n", p.age);
}
int main(){
Person p1(13);
show_age(p1); // 不产生新对象
show_age(12); // 产生一个临时对象
return 0;
}
  • 因为类不包含拷贝构造,所以传递的时候,show_age(p1);不进行新对象的产生。传递时,栈上需要用到类中的所有数据。
  • show_age(12)参数是int类型,所以需要在show_age(12)这一行进行一次构造,构造后,不清理栈,此时栈上数据仍是类构造的数据。
image-20250211112956752

包含拷贝构造

// 拷贝构造
Person(const Person& other){
this->age = other.age;
printf("Person(const Person& other)\n");
}
  • 拷贝构造的时机在函数调用之前。拷贝后,不清理堆栈。
    • image-20250211113212091

例外

临时对象函数外构造、函数外析构。

image-20250210181429502

类对象作为返回值

特征:

函数内构造。

函数外析构。

retrun Object匿名形式

#include <cstdio>
class Person{
int age;
public:
Person(){
std::printf("Person\n");
}
Person(int age){
std::printf("Person(int age)\n");
this->age = age;
}
~Person(){
std::printf("~Person()\n");
}
// 拷贝构造
Person(const Person& other){
this->age = other.age;
printf("Person(const Person& other)\n");
}
};
Person return_Person(int age){
printf("return_Person\n");
return Person(age);
}
int main(){
return_person(12);
Person p1 = return_person(15);
const Person& p_ref = return_person(16);
return 0;
}

1. return_person(12)

image-20250211113642303

首先,栈中没有分配空间。临时对象的生命周期在本行消失,也就是本行释放。

2. Person p1 = return_person(15)

image-20250211113753329

返回值保存到栈中的局部对象。此时传参时,将ecx作为this指针提前放置,在构造时,使用局部变量的地址构造,省去一次拷贝构造。

3. const Person& p_ref = return_person(16)

image-20250211114001458

临时变量延长生命周期,局部存在引用的空间。但是不发生拷贝构造,表现形式与上面相同。

虚函数

纯虚函数

class A{ // 至少拥有一个纯虚函数 => 抽象类
public:
virtual void foo() = 0; // 纯虚函数
};

pure-specifier

微软自己搞得语法,可以让纯虚析构函数,带有实现。派生类继承的时候,析构函数可以实现也可以不实现。

class Base{
public:
Base();
virtual void fun1() = 0;
virtual void fun2() = 0;
virtual ~Base() = 0 { // 带有默认实现的纯虚析构 微软自己搞得
};
};

纯虚函数在虚表中的表现是一个__purecall指针

image-20250215100841112

当项目使用静态链接的时候,其中的实现如下:

image-20250215101137205

核心代码是在这个函数中抛出一个异常,可以通过这个来识别纯虚函数的调用。

image-20250215101258813

虚表

虚表存在.rdata常量区,大小未知,只能通过偏移来猜大小。

image-20250212091325802

当存在RTTI时,常量区可以看到类的名称信息与作用域。

image-20250212091531347

带有虚函数的构造

构造中需要填入虚表指针,虚表指针位置在类对象的首地址。

image-20250212091020040

无内联

image-20250212084136514

内联

image-20250212082927234

如果基类的构造为默认,在内联时将被优化。

带有虚函数的析构

如果派生类带有虚析构,虚表指针中指向析构代理函数。

析构时,为了避免在析构函数中使用本类方法,所以需要先将虚表指针重新指向为本类虚表地址。

// Circle:Shape
virtual ~Circle(){ // 先调用派生类构造
this->xxx();
}
virtual ~Shape(){ // 再调用基类构造
// 如果不重置虚表指针,此时this->xxx()指向派生类xxx方法,因为派生类已经被析构,所以造成误解
this->xxx();
}
image-20250212090933443

继承

  • 不带虚表的继承,难以通过构造函数分辨继承顺序。
    • 调基类构造有可能是:基类成员对象变量初始化。
image-20250213133100890

构造顺序

带有虚表的构造函数,以虚表指针填写为界限

  • 虚表填写上方为基类构造。
  • 下方为本类构造中的代码。
image-20250213134648099

识别RTTI继承顺序

虚表指针->RTTI->RTTI第一项

image-20250213141557609 image-20250213141630474 image-20250213141651341

多继承

假设我们有A、B类,我们现在让AB类多继承AB

class AB: public A, public B;

AB类对象的内存结构

+-----------------------+ <-- 对象起始地址 (例如 &objAB, ptrA, ptrAB)
| AB::vptr (for A) | 指向 AB 类针对 A 基类的虚函数表 (vtable_AB_for_A) 的指针
+-----------------------+
| dataA | 来自基类 A 的数据成员 (int)
+-----------------------+
| AB::vptr (for B) | 指向 AB 类针对 B 基类的虚函数表 (vtable_AB_for_B) 的指针
+-----------------------+
| dataB | 来自基类 B 的数据成员 (int)
+-----------------------+
| dataAB | 来自派生类 AB 自身的数据成员 (int)
+-----------------------+ <-- 对象内存末尾

构造特征

构造顺序:

  1. 使用A的虚表构造A
  2. 使用B的虚表构造B
  3. 填写AB类对A基类的虚表指针,填写AB类对B基类的虚表指针,来构造AB。
image-20250215103748668

在构造AB的时候,虚表是AB对A或者AB对B的虚表,和A的虚表、B的虚表不是一个东西。

如果基类A存在虚函数`fun1`,派生类AB覆盖`fun1`,此时:
A的虚表:
{ A::fun1 }
AB对A的虚表:
{ AB::fun1() }
两者不同,需要注意

注意与成员变量的区分

如果让AB类存在A、B两个类的成员。

此时内存结构:

+-----------------------+ <-- 对象起始地址 (例如 &objAB, ptrA, ptrAB)
| A::vptr | 指向 A 基类的虚函数表 的指针
+-----------------------+
| dataA | 来自基类 A 的数据成员 (int)
+-----------------------+
| B::vptr | 指向 AB 类针对 B 基类的虚函数表 (vtable_AB_for_B) 的指针
+-----------------------+
| dataB | 来自基类 B 的数据成员 (int)
+-----------------------+
| dataAB | 来自派生类 AB 自身的数据成员 (int)
+-----------------------+ <-- 对象内存末尾

内存结构的分布差不多,但是这里因为虚表指向的基类本身的,并不会重新覆盖。

image-20250215105515832

还有一点:

派生类填写本身虚表之前为继承关系,之后为构造代码。

这里明显看到填写自身虚表之前不存在继承关系。

析构

image-20250215105048600

虚继承

#include <iostream>
#include <cstdio>
using namespace std;
// 多重继承
class A{
int a = 10;
public:
A(){
std::cout << "A()" << std::endl;
}
virtual ~A(){
std::cout << "A::~A()" << std::endl;
}
virtual void fun1() = 0;
};
class B: public A{
int b = 20;
public:
B(){
std::cout << "B()" << std::endl;
}
virtual void fun1(){
printf("B::fun1()\n");
}
virtual void funB(){
printf("B::funB()\n");
}
virtual ~B(){
std::cout << "B::~B()" << std::endl;
}
};
class C: public A {
int b = 20;
public:
C(){
std::cout << "C()" << std::endl;
}
virtual void fun1(){
printf("C::fun1()\n");
}
virtual void funC(){
printf("C::funC()\n");
}
virtual ~C(){
std::cout << "C::~C()" << std::endl;
}
};
class BC:public B, public C {
int bc = 66;
public:
BC(){
std::cout << "BC()" << std::endl;
}
virtual void fun1(){
printf("BC::fun1()\n");
}
virtual void funB(){
printf("BC::funB()\n");
}
virtual void funC(){
printf("BC::funC()\n");
}
virtual void funBC(){
printf("BC::funBC()\n");
}
virtual ~BC(){
std::cout << "BC::~BC()" << std::endl;
}
};
int main(){
BC* bc = new BC();
__asm{
nop;
nop;
nop;
nop;
}
bc->funC();
bc->funB();
bc->funBC();
delete bc;
return 0;
}

菱形继承

A
/ \
B C
\ /
BC

当出现菱形继承时,对BC来说,同时存在两份A的实例(由BC分别提供)。

此时BC的构造链:

  1. 按照继承顺序先构造B::B()
    1. B::B()中调用A::A(),构造一个A对象。
  2. 然后构造C::C()
    1. C::C()也由构造A::A(),出现第二个A对象。
  3. 最后填写BC::vfptr(for B)BC::vfptr(for C)构造自身BC::BC()

最终BC的内存结构如下:

+-----------------------+ <-- 对象起始地址 (例如 &objA, ptrA)
| BC::vfptr (for B) | BC对B的虚表指针
+-----------------------+
| B 子对象部分 |
| +---------------------+
| | A对象部分 | 来自B中的A对象部分
| +---------------------+
+-----------------------+
| BC::vfptr (for C) | BC对C的虚表指针
+-----------------------+
| C 子对象部分 |
| +---------------------+
| | A对象部分 | 来自C中的A对象部分
| +---------------------+
+-----------------------+
| BC 自身内部成员变量 | BC派生类自身成员变量部分
+-----------------------+

此时出现冗余的A对象,为了解决这个问题,我们可以让BC虚继承A

class B:virtual public A;
class C:virtual public A;

虚继承后的内存结构

  • 虚基类仅构造一次,由最远的派生类构造。
虚继承的结构
假设class B: virtual public A
+-----------------------+ <-- 对象起始地址 (例如 &objA, ptrA)
| vbptr (for B) | 虚基类偏移表指针 (Virtual Base Pointer)
+-----------------------+ - 指向 A 类针对虚基类 B 的虚基类表 (vbtable_A_for_B)
| vptr (for A) | 虚函数表指针 (Virtual Table Pointer)
+-----------------------+ - 指向 A 类自身的虚函数表 (vtable_A_for_A)
| dataA_Derived | 派生类 A 自身的数据成员 (int)
+-----------------------+
| (padding) | 可能存在填充字节,为了内存对齐
+-----------------------+
| 虚基类 B 子对象部分 | **共享的虚基类 B 子对象 (在 A 对象内部间接包含)**
| +---------------------+
| | dataB_Base | 来自虚基类 B 的数据成员 (int)
| +---------------------+
| | vptr (for B) | 可能存在虚函数表指针 (vtable_A_for_B') for the B subobject within A (取决于编译器优化)
| +---------------------+
+-----------------------+ <-- 对象内存末尾

虚表结构:

A:
0x0 vfptr{A::~A(), A::fun1()}
0x4 int A::a
B:A
0x0 vfptr{B::~B(), B::fun1(), B::funB()}
0x4 int A::a
0x8 int B::b
image-20250217094859656

经过虚继承后:

A:
0x0 vfptr {A::~A(), A::fun1()}
0x4 int A::a
B:A
0x0 vfptr{B::~B(), B::fun1(), B::funB()}
0x4 int A::a
0x8 int B::b
0x0 vfptr(for B) {B::funB(void)}
0x4 vbptr B::vbtable
0x8 int B::b (B::member)
0xc B::`vftable'{for `A'} {B::~B(), B::fun1()}
0x10 int A::a (A::member)

虚继承后:

  • A不变
  • B增加B::vbtableB::vftable (for A)
  • B::vbtable中存放B::vftable (for A)在类中的偏移
    • image-20250217095455909
  • B中对A虚函数的覆盖,会写入到B::vftable (for A)
  • B新增加的虚函数会增加一个B::vftable (for B)的虚表指针,并在其中写入虚函数地址。

BC的内存结构

A:
0x0 A::vftable {A::~A(), A::fun1()}
0x4 int A::a
B:A
0x0 B::vftable (for B) {B::funB(void)}
0x4 vbptr B::vbtable (for A) { 0xc }
0x8 B::member { int B::b }
0xc 0000 0000 // 分割 有可能有,也可能没有
-------------- A 虚基类起始
0x10 vftable B::vftable (for A) {B::~B(), B::fun1()}
0x14 int A::a (A::member)
-------------- A 虚基类结束
C:A
0x0 vftable C::vftable (for C) {C::funC(void)}
0x4 vbptr C::vbtable (for A) { 0xc }
0x8 C::memeber { int C::c }
0xc 0000 0000 // 分割 有可能有,也可能没有
-------------- A 虚基类起始
0x10 vftable C::vftable (for A) {C::~C(), C::fun1()}
0x14 int A::a (A::member)
-------------- A 虚基类结束
BC:B,A
--------------B 基类开始
0x0 vftable BC::vftable(for B) {BC::funBC(void)}
0x4 vbptr B::vbtable { 0x1c } // 存储从此处到A虚基类的偏移量
0x8 B member {int B::b}
--------------B 基类结束
--------------C 基类开始
0xc vftable BC::vftable(for C)
0x10 vbptr C::vbtable { 0x10 } // 存储从此处到A虚基类的偏移量
0x14 C member { int C::c }
--------------C 基类开始
0x18 BC member { int BC::bc }
0x1c 0000 0000 // 分割 有可能有,也可能没有
--------------A 虚基类开始
0x20 vftable A::vftable {}
0x24 A member {int A::a}
--------------A 虚基类结束
image-20250217095956011 image-20250217104007071

重要结论

  • 虚基类一定在类的最后。
  • 虚偏移表中存放的是从此处到虚基类的距离。
  • 通过虚偏移表,我们可以计算出虚基类和派生类的空间大小。(仅限单继承)

虚继承后构造顺序

  1. 填写BC::vbtable (for B),填写BC::vbtable (for C)
  2. 构造基类
    1. 由最后的派生类构造虚基类A
    2. 构造B
      1. 如果B中有重写A中的虚函数,则先覆盖A虚表。
      2. 覆盖B虚表,调用B构造。
    3. 构造C
      1. 如果C中有重写A中的虚函数,则先覆盖A虚表。
      2. 覆盖C虚表,调用C构造。
    4. 构造BC
      1. 如果BC中有对BC中虚函数的覆盖,则覆盖BC的虚表。
      2. 如果BC中有对A中虚函数的覆盖,则覆盖A的虚表。
      3. 调用构造代码。

  1. 填写偏移表:填写偏移表
  2. 构造虚基类Aimage-20250217160815833
  3. 构造B,因为不再构造A,所以下面两个顺序可以乱。
    1. 覆盖A中虚表。
    2. 覆盖自身虚表,构造自身。
  4. 构造C,顺序也可以乱。image-20250217161508434
    1. 覆盖A中虚表。
    2. 覆盖自身虚表,并构造自身。
  5. 构造BC
    1. 覆盖B的虚函数表,覆盖C的虚函数表。
    2. 覆盖A中虚表。
    3. 构造自身。