最近在做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; } |
有了方法之后怎么利用就根据场景去做变通吧,其实这片真的很水,只是遇到了随便写一下记录一下。