当C++类中函数虚函数时,为了实现多态,C++类最开始地方会包含一个指针,该指针指向虚函数表,当我们修改这虚函数表里地址就达到了实现虚函数表Hook的目的。
代码如下:
#include "VTblHook.h"
#include <iostream>
#include <Windows.h>
class MyBase
{
public:
MyBase(int a,int b):m_ia(a),m_ib(b){ }
virtual void __cdecl Test(int a){ }
void Print()
{
std::cout << "a:" << m_ia<< std::endl;
}
private:
int m_ia;
int m_ib;
};
class MyHook
{
public:
virtual void __cdecl Hook(int a)
{
MyBase* pBase = (MyBase*)(this);
pBase->Print();
std::cout << "Hook:" << a << std::endl;
}
virtual void __cdecl Hook1(int b)
{
std::cout << "Hook1:" << b << std::endl;
}
};
void __cdecl HookTest(void* p, int a)
{
std::cout << "HookTest sucess:"<< a << std::endl;
}
template<class T,class U>
void VtlHook(T* pT, int Tidx, U* pU, int Uidx)
{
auto pTVtAddr = (size_t*)*(size_t*)pT;
auto pUVtAddr = (size_t*)*(size_t*)pU;
DWORD dwProct = 0;
VirtualProtect(pTVtAddr, sizeof(size_t), PAGE_READWRITE, &dwProct);
pTVtAddr[Tidx] = pUVtAddr[Uidx];
VirtualProtect(pTVtAddr, sizeof(size_t), dwProct, nullptr);
};
template<class T>
void VtlHookFun(T* pT, int Tidx,size_t funcAdd)
{
auto pTVtAddr = (size_t*)*(size_t*)pT;
DWORD dwProct = 0;
VirtualProtect(pTVtAddr, sizeof(size_t), PAGE_READWRITE, &dwProct);
pTVtAddr[ Tidx ] = funcAdd;
VirtualProtect(pTVtAddr, sizeof(size_t), dwProct, nullptr);
}
void VTblHook()
{
MyBase* pb = new MyBase(1,2);
MyHook* pHook = new MyHook();
pb->Test(1);
HookTest(NULL,1);
//VtlHook(pb,0,pHook,0);
VtlHookFun(pb,0, (size_t)HookTest);
pb->Test(125);
delete pb;
delete pHook;
return;
}
这里面需要注意的地方有两个:
对于x86,我们写入的函数地址的调用方式,参数类型一定要设置好。
如果我们填入的是一个虚函数的地址,就相对简单一点,只需要填入虚函数地址的函数声明与被替换函数的声明一致即可。因为同一个程序里,是采用同一种调用方式的。示例代码里可以把两个虚函数里的__cdecl去掉,也是可以正常执行的。
如果我们填入的是一个普通函数的地址,该普通函数比虚函数需要新增一个参数,就是第一个参数需要是一个指针来接收,同时该函数需要采用__cdecl调用方式,虚函数也要采用__cdecl进行调用。因为如果采用__stdcall,由于用于hook的函数多一个参数,此时普通函数被以虚函数的方式进行调用,平栈的时候会有问题。因此需要统一采用__cdecl进行调用,由主调函数进行平栈。
如果虚函数采用__stdcall进行调用,而我们也没有办法修改函数调用方式的话,只有参数为空的时候才能hook成功,虚函数参数为空的时候普通函数参数也需要为空(还是因为平栈的问题),但是这样子就无法获取到类的指针了,并且无法有参数的传递,局限较大,否则就无法进行hook。
对于X64,由于去除了x86那么多混乱的调用方式,而且也是由主调函数进行平栈,因此没有这个问题。