最近在做C++虚函数Hook的时候想起了一点比较有趣的东西,不是什么高深的技术,就是简单的记录一下。
做C++的对于虚函数和纯虚函数应该不陌生,简单来说就是用虚表的形式实现的,网上关于虚函数的理论知识和图太多,我这里就不贴了,直接用一段测试代码上调试器实际看一下。
类定义如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #include <Windows.h> #include <iostream> class A { public: 	virtual void test1(int iPrint) = 0; 	virtual void test2(int iPrint) = 0; }; class B : public A { public: 	virtual void test1(int iPrint) 	{ 		std::cout << "test1:"<<iPrint << std::endl; 	} 	virtual void test2(int iPrint) 	{ 		std::cout << "test2:"<<iPrint << std::endl; 	} }; | 
| 1 2 3 4 5 6 7 8 | int main(int argc,char *argv[]) { 	B *ptest=new B; 	ptest->test1(1); 	ptest->test2(2); 	delete ptest; 	return 0; } | 
通过调试的结果可以直观的看出,对象this指针所指向的(*this)正好是虚表的地址,拿到了虚表的地址,我们就可以对虚表中的函数地址进行修改,或者直接调用里面的函数,反正能想到什么都能做。
先搞一个有意思的东西,直接调用一下虚表中的函数。
在这里要插一嘴,关于三种常见函数调用约定的东西,虽然这个也是老生常谈了。
__cdecl: C/C++默认方式,参数从右向左入栈,主调函数负责栈平衡。
__stdcall: Windows API默认方式,参数从右向左入栈,被调函数负责栈平衡。
__thiscall : C++成员函数调用方式,参数从右到左入栈,被调函数负责栈平衡,this指针存放于CX/ECX寄存器中。
C++成员函数都是__thiscall调用约定,又需要特别提到的,成员函数默认第一个参数是this指针【其实这都是基础我也不想多说
所以按下面的方式声明函数类型:
| 1 | typedef void(__thiscall *PFUN)(PVOID pthis, int iPrint); | 
main中代码如下:
| 1 2 3 4 5 6 7 8 9 10 11 | int main(int argc,char *argv[]) { 	B *ptest=new B; 	LPDWORD pVtable = reinterpret_cast<LPDWORD>(*reinterpret_cast<LPDWORD>(ptest)); 	PFUN ptest1 = reinterpret_cast<PFUN>(*pVtable); 	PFUN ptest2 = reinterpret_cast<PFUN>(*(pVtable + 1)); 	ptest1(ptest, 1); 	ptest2(ptest, 2); 	delete ptest; 	return 0; } | 
执行结果如下:
如果想要Hook虚表中的函数,这样是不够的,C风格函数无法定义__thiscall调用约定,所以要构造的函数第一个参数必须是this指针,但是__thiscall调用约定this指针是通过ECX传入的,我们通过栈传入this指针,实际上相当于多传了一个参数。如果对于无其他参数的函数,可以用__cdecl声明替换的函数,在函数外把多传的this指针平衡调;但是对于多参数的函数,无论用__cdecl还是__stdcall调用约定,都无法正常平衡堆栈,在Debug模式编译CheckEsp就会直接报栈不平,在Release编译模式下虽不会报错,但实际上栈是没有平衡的。所以我在这里选择构造一个类,在类中实现一个__thiscall调用约定的函数,然后替换进虚表中。
| 1 2 3 4 5 6 7 8 9 10 | class hook { public: 	virtual void  testhook( 			int iPrint) 	{ 		B* pB = reinterpret_cast<B*>(this); 		std::cout << "testhook:" << pB->a << std::endl; 	} }; | 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | int main(int argc,char *argv[]) { 	hook* ptesthook=new hook; 	B *ptest=new B; 	LPDWORD pVtable = reinterpret_cast<LPDWORD>(*reinterpret_cast<LPDWORD>(ptest)); 	LPDWORD pVtableHook = reinterpret_cast<LPDWORD>(*reinterpret_cast<LPDWORD>(ptesthook)); 	DWORD dwOldProtect; 	if (!VirtualProtect(pVtable, sizeof(DWORD) * 2, PAGE_READWRITE, &dwOldProtect)) 	{ 		return 0; 	} 	pVtable[1] = (*pVtableHook); 	if (!VirtualProtect(pVtable, sizeof(DWORD) * 2, dwOldProtect, &dwOldProtect)) 	{ 		return 0; 	} 	ptest->test2(1); 	delete ptest; 	delete ptesthook; 	return 0; } | 
有了方法之后怎么利用就根据场景去做变通吧,其实这片真的很水,只是遇到了随便写一下记录一下。