C++逆向
C++逆向总结
一、call前的this指针参数传递
Windows和Linux下的大部分编译器将this指针存放到ecx寄存器中传递,就是在call某个对象成员函数时。但是,也有部分编译器使用rdi寄存器传递的。
如上图所示,第一个参数是用ecx寄存器传递的,后面的两个参数都是栈传递。我们可以通过观察发现,这里ida识别出现错误,没有第四个参数,a1就是this指针,里面的this其实是第二个运算参数。
二、C++函数名编译后的编码
编译器会对C++编写的源程序中的函数名进行编码,又称为C++ Name Mangling。
那么我们还原函数名的方法有:
- 由编译器提供的函数进行解码
#include <cxxabi.h> abi::__cxa_demangle(...);
- 使用NDK工具链中的C++ filt命令
cxxfilt 函数名
三、虚函数表
如果类对象中存在虚函数,则类对象在内存中的空间增加4个字节,用于存放虚函数表指针,且虚函数表指针默认为第一个成员变量。
虚函数表指针指向虚函数表,也可以理解为一个函数地址数组。
测试代码如下:
#include <iostream> using namespace std; class BaseObject{ public: virtual int sub() = 0; }; class MyObject:public BaseObject { public: int x; int y; public: MyObject(int a,int b) { x = a; y = b; } ~MyObject() { x = 0; y = 0; } int add() { return x + y; } int sub(){ if(x>y) return x-y; else return y-x; } }; int main() { BaseObject* obj = new MyObject(1,2); cout << "Hello!" << endl; cout << obj->sub() << endl; return 0; }
我们可以发现new的内存空间多了4个字节(2 * 4 + 4 = 0xC),不然应该申请8个字节的内存。
我们可以发现在MyObject类的构造函数中首先调用了父类BaseObject的构造函数。
在BaseObject的构造函数中,就做了一件事,将off_4C1470赋值给this[0],off_4C1470是BaseObject的虚函数表地址。
而MyObject类的构造函数在完成父类的构造函数后,将自己的虚函数表地址off_4C147C地址赋值给this[0],也就是覆盖了父类的虚函数表。
四、虚函数的调用
虚函数的调用是通过虚函数表指针来调用的。
测试代码如下:
class BaseObject{ public: virtual int add() = 0; virtual int sub() = 0; }; class MyObject:public BaseObject { public: int x; int y; public: MyObject(int a,int b) { x = a; y = b; } ~MyObject() { x = 0; y = 0; } int add() { return x + y; } int sub(){ if(x>y) return x-y; else return y-x; } }; int main() { BaseObject* obj = new MyObject(1,2); cout << "Hello!" << endl; cout << obj->add() << endl; cout << obj->sub() << endl; return 0; }
我们可以看到其通过虚函数表以及虚函数在表中的偏移来调用add函数与sub函数。
五、构造函数
C++类的构造函数在编译为汇编后和普通函数一样,只是多了个this指针作为参数。对象作为局部变量则在栈中提前留出空间。
如果是使用new函数创建的类对象,则先调用new函数完成内存申请创建对象,再将this指针传递给构造函数初始化。
六、线程同步问题
Windows 平台上, Interlock系列函数、atomic模板类、CriticalSection、Event、Mutex、Semphore、atomic等API 函数以及两个重要的函数 WaitForSingleObject、WaitForMultipleObjects,常被用于多线程同步。
Interlock系列函数:进行原子操作,通过volatile修饰符实现的,直接操作内存,不作缓存。
引:C/C++作为系统级语言,它们与硬件的联系是很紧密的。volatile的意思是“易变的”,这个关键字最早就是为了针对那些“异常”的内存操作而准备的。它的效果是让编译器不要对这个变量的读写操作做任何优化,每次读的时候都直接去该变量的内存地址中去读,每次写的时候都直接写到该变量的内存地址中去,即不做任何缓存优化。
atomic模板类参考资料: https://github.com/forhappy/Cplusplus-Concurrency-In-Practice/blob/master/zh/chapter7-Atomic/7.3%20Atomic-tutorial.md
CriticalSection对象的使用:https://learn.microsoft.com/en-us/windows/win32/sync/using-critical-section-objects
在linux平台上,使用 mutex、semphore、condition_variable、read-write-lock 等操作系统API。