在逆向工程中,类的逆向分析非常重要,尤其逆向分析的对象为面向对象的编程语言(如 C++
)所编写的程序时类的分析就显得更加重要。这是因为类结构往往承载了程序的核心逻辑和设计模式。通过还原类的结构、继承关系和成员函数,逆向工程师能够理解程序的整体架构、功能实现以及开发者的设计意图,从而有效识别关键模块、提升分析效率。类的分析比结构体更复杂,因为类除了数据成员外,还包含了方法(函数)和继承关系。
对象的内存布局
在逆向分析中,熟悉对象的内存布局非常重要,尤其是在处理面向对象的代码时。对象的内存布局取决于类的成员、继承关系、虚函数以及编译器的实现。通常情况下与结构体一样对象的成员变量通常按它们在类定义中声明的顺序布局在内存中,但编译器可能会对变量进行内存对齐优化,以提高访问效率;这种对齐会导致对象中可能出现“填充字节”,即未被实际使用的数据区域。现有如下C++代码,,我们进行编译后生成exe
程序:
#include<stdio.h>
#include<stdlib.h>
struct Wbly {
double length;
int age;
};
class Wolven {
public:
Wolven() {
a = 100;
b = 200;
w.age = 20;
w.length = 300;
d = 400;
}
~Wolven() {
}
private:
int a;
int b;
Wbly w;
long d;
};
int main() {
Wolven cw;
system("pause");
return 0;
}
代码中声明了一个Wolven
类,并在main
函数中进行了实例化,在实例化过程中对象会调用构造函数用于初始化对象成员,大概说明代码内容后,我们将生成的exe
程序放入x96dbg
中进行动态分析,查看该对象的内存布局。
定位到main
函数后,跳过初始化代码,直接锁定相关代码:
lea ecx,dword ptr ss:[ebp-28]
call objrev.A8DBB1
lea ecx, dword ptr ss:[ebp-28]
:这条指令将栈中的一个局部变量的地址加载到 ecx
寄存器中。
call objrev.A8DBB1
:call
指令用于调用函数。
在逆向过程中,如果看到 ecx
寄存器用来传递指针且指向了一个对象实例,并且调用的函数对该地址进行了成员变量操作,就可能表明这是一个通过 __thiscall
调用约定传递 this
指针的成员函数调用。
thiscall调用约定
__thiscall
是一种调用约定,主要用于 C++ 的类成员函数调用。thiscall
是 C++ 编译器在调用非静态成员函数时的默认调用约定,作用是规范 this
指针和函数参数的传递方式。它确保每个类的成员函数在调用时知道调用的是哪个对象,并使成员函数能够访问这个对象的数据成员和其他方法。
在面向对象程序中,成员函数的第一个参数通常是 this
指针,它指向调用该成员函数的对象实例。在逆向工程中,可以通过观察寄存器或栈传递的第一个参数,来识别 this
指针的传递方式:
x86 平台
①this 指针通过 ecx 寄存器传递,因此不会消耗栈空间。
②其他参数(除了 this 以外的参数)按照从右到左的顺序压入栈中。
③调用结束后由调用者负责清理栈空间。
x64 平台
x64 上通常没有 __thiscall 调用约定,因为 Windows 系统上 x64 平台的标准调用约定会直接将 this 指针放在 rcx 寄存器中,而其他参数按顺序放在 rdx、r8、r9,这样可以满足 x64 平台的成员函数调用需求。
这个代码片段的含义就是:
-
将对象的地址加载到
ecx
中,作为this
指针传递给构造函数。 -
随后调用
objrev.A8DBB1
,它是该类的构造函数,通过ecx
中的地址进行对象的初始化。
this
指针在面向对象编程中用于指向当前对象实例,所以此时传入ecx
寄存器的数据就是当前对象的地址;
此时我们在x32dbg
中转到该对象对应的内存地址中进行查看,可以看到当前对象的内存地址空间全部被进行了初始化操作,也就是填充了cc
字符。VS 编译器会在未使用的内存或对齐填充中填充字节 0xCC
,这是 Visual Studio (VS) 编译器的一个常见特点。
接下去往下执行对应的指令,也就是调用构造函数。
执行完后我们再来观察一下内存中的数据内容:
可以看到在构造函数中做的初始化操作全部映现在内存中了:这个时候我们结合C++
源代码来查看一下内存空间中的数值,并观察其特征。
Wolven() {
a = 100;(0x64)
b = 200;(0xC8)
w.age = 20;(0x14)
w.length = 300;(85EB51...)
d = 400; (0190)
}
因为结构体在声明时age
成员在length
变量后,所以即使在构造函数中age
成员先于length
进行初始化,但是在内存中还是以age
变量的值还是在length
变量后。并且我们通过内存可以清楚的看到,对象与结构体一样,编译器可能会对其中的成员变量进行内存对齐优化,最显著的特征就是:对象的内存空间中就存在为了对齐而进行填充的CC
字符。
通过本文的深入分析,我们不仅了解了对象的内存布局以及在逆向工程中如何识别和解析它,还对 thiscall
调用约定的工作原理有了更加全面的理解。掌握这些概念对于逆向分析C++程序尤其重要,能够帮助我们更有效地定位和解读成员函数的调用。随着对不同编译器、操作系统和硬件架构特性的掌握,逆向分析变得更加精确和高效。
原文始发于微信公众号(风铃Sec):C/C++逆向:对象内存分布&__thiscall
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论