方法其实很简单。
1.打开VS自带的命令行工具。当然,你把cl.exe的目录写到环境变量中,直接在cmd中也能用。
这是VS2012的。
下面这个是VS2013的。都一样。
2.使用cl命令的/d1 reportAllClassLayout或reportSingleClassLayoutXXX选项。这里的reportAllClassLayout选项会打印大量相关类的信息,一般用处不大。而reportSingleClassLayoutXXX选项的XXX代表要编译的代码中类的名字(这里XXX类),打印XXX类的内存布局和虚函数表(如果代码中没有对应的类,则选项无效)。
其中,/d1reportSingleClassLayoutXXX 显示指定XXX类的内存布局
/d1reportAllClassLayou 显示所有类的内存布局
举个例子。
下述两个类存储在test.cpp文件中。
class A{
public:
void doX(){
std::cout << "A function of X";
}
private:
int a;
};
class B: public A{
public:
virtual void doX(){
std::cout << "B function of X";
}
};
在命令行中输入 cl /d1reportSingleClassLayoutB test.cpp
结果如下所示:
class B size(8):
+---
0 | {vfptr}
| +--- (base class A)
4 | | a
| +---
+---
B::$vftable@:
| &B_meta
| 0
0 | &B::doX
B::doX this adjustor: 0
在命令行中输入 cl /d1reportSingleClassLayoutA test.cpp
结果如下所示:
class A size(4):
+---
0 | a
+---
3.实际操作来验证
根据上面得到的虚函数表所在的位置,我们来进行一下hack行为。
#include
class A{
public:
void showMe(){
std::cout << "A function of X" << std::endl;
}
private:
int a;
};
class B : public A{
public:
virtual void showMe(){
std::cout << "B function of X" << std::endl;
}
};
int main(){
//获取基类成员函数所在的内存调用地址
typedef void (A::* Aptr)();
Aptr classPtr = &A::showMe;
void* iPtr = &classPtr;
//获取派生类对象中虚函数表的起始地址
B* bPtr = new B();
void* bAddr = bPtr;
int* BVTaddr = (int*)(bAddr); // + 1;
//因为只有一个虚函数,所以
//该虚函数的调用地址就直接存在了虚表地址上
//用基类A中的实函数地址来覆盖虚函数的地址
//这样调用时,就成了动态调用实函数了
*BVTaddr = (int)iPtr;
//通过指针调用虚函数,hack成功
//注意,只有通过指针或者引用,才能实现多态调用
bPtr->showMe();
//释放内存,避免泄漏
delete bPtr;
//system("pause");
}
使用Microsoft (R) C/C++ Optimizing Compiler Version 18.00.31101 for x86编译器和gcc version 4.7.1编译器,两个编译器输出结果相同,
结果均输出为 A function of X
表明这两个编译器,都是将虚表的起始地址,放在了对象的头部。如下所示:
class B size(8):
+---
0 | {vfptr}
| +--- (base class A)
4 | | a
| +---
+---
B::$vftable@:
| &B_meta
| 0
0 | &B::showMe
B::showMe this adjustor: 0