虚表hook
虚表hook是一种address hook,通过修改虚表中的项实现hook。实际操作的时候还是有一些需要注意的地方。
技术原理
对象指针中保存着虚表的地址,我们取出这个地址,就得到了虚表。
cpp
#include <Windows.h>
#include <iostream>
class TargetClass
{
public:
virtual void vfunc()
{
std::cout << "TargetClass vfunc: " << this->number << std::endl;
}
virtual void vfunc2()
{
std::cout << "TargetClass vfunc2" << std::endl;
}
protected:
int number = 10;
};
class SubClass :public TargetClass
{
public:
virtual void vfunc()
{
std::cout << "SubClass vfunc: " << this->number << std::endl;
}
virtual void vfunc2()
{
std::cout << "SubClass vfunc2" << std::endl;
}
};
// detour函数需要一个参数保存this
void MyVFunc(PVOID rcx)
{
PBYTE obj = (PBYTE)rcx;
int number = *(int*)(obj + sizeof(ULONGLONG)); // 跳过隐式的vtable成员,得到number
std::cout << "the number is " << number << std::endl;
}
void InstallHook()
{
}
void UninstallHook()
{
}
int main(int argc, char* argv[])
{
TargetClass* obj = new SubClass;
obj->vfunc();
// 取出虚表地址,这里直接把虚表当作数组来访问
PULONGLONG vtable = (PULONGLONG)(*(PULONGLONG)obj);
// 需要修改虚表所在页的权限
DWORD dwOld = 0;
BOOL res =VirtualProtect((PVOID)vtable, sizeof(ULONGLONG), PAGE_READWRITE, &dwOld);
if (!res)
{
std::cerr << "VirtualProtect Failed: " << GetLastError() << std::endl;
return -1;
}
vtable[0] = (ULONGLONG)MyVFunc; // 修改虚表中的项
res = VirtualProtect((PVOID)vtable, sizeof(ULONGLONG), dwOld, &dwOld);
if (!res)
{
std::cerr << "VirtualProtect Failed: " << GetLastError() << std::endl;
return -1;
}
obj->vfunc();
return 0;
}
运行效果:

在我们的detour函数中的第一个参数需要用于接收this,这点是需要注意的。
两种实现方式
虚表hook有两种方式:
- 修改虚表
- 创建代理虚表
修改虚表
就像例子中的代码一样,通过修改特定的虚表项实现hook。这种方式实现简单,直接修改目标虚表中的函数指针即可。但它的缺点是全局生效------一旦修改了虚表,所有使用该虚表的对象实例都会受到影响,因为虚表本身是类级别的共享资源。
此外,修改虚表需要先通过 VirtualProtect 修改虚表所在内存页的权限(通常为 PAGE_READWRITE),修改完成后再恢复原始权限,否则可能引发访问违例或安全软件告警。
适用场景:临时hook、调试、或明确知道目标对象是唯一实例时使用。
创建代理虚表
创建代理虚表(Proxy VTable)是一种更隐蔽、更灵活的hook方式。其核心思路是:
- 分配一块新的内存,大小与原始虚表相同。
- 将原始虚表的内容复制到新内存,形成一份"代理虚表"。
- 修改代理虚表中的目标函数指针,指向我们的detour函数。
- 将目标对象的虚表指针(vptr)指向代理虚表。
这样做的优势在于:只影响被修改了vptr的特定对象实例,而不会影响类中其他对象的虚函数调用,因此更加精准和隐蔽。
cpp
void InstallProxyHook(TargetClass* obj)
{
// 1. 获取原始虚表地址
PULONGLONG originalVtable = (PULONGLONG)(*(PULONGLONG)obj);
// 2. 计算虚表大小(假设有2个虚函数)
const int vfuncCount = 2;
SIZE_T vtableSize = sizeof(ULONGLONG) * vfuncCount;
// 3. 分配代理虚表内存
PULONGLONG proxyVtable = (PULONGLONG)VirtualAlloc(
NULL, vtableSize, MEM_COMMIT, PAGE_READWRITE);
if (!proxyVtable)
{
std::cerr << "VirtualAlloc Failed: " << GetLastError() << std::endl;
return;
}
// 4. 复制原始虚表内容
memcpy(proxyVtable, originalVtable, vtableSize);
// 5. 修改代理虚表中的目标函数
DWORD dwOld = 0;
VirtualProtect(proxyVtable, vtableSize, PAGE_READWRITE, &dwOld);
proxyVtable[0] = (ULONGLONG)MyVFunc; // hook vfunc
VirtualProtect(proxyVtable, vtableSize, dwOld, &dwOld);
// 6. 将对象的vptr指向代理虚表
*(PULONGLONG*)obj = (ULONGLONG)proxyVtable;
}
注意事项:
- 代理虚表需要手动释放(
VirtualFree),否则会造成内存泄漏。 - 卸载hook时,需要将对象的vptr恢复为原始虚表地址,再释放代理虚表内存。
- 如果对象在hook期间被析构,务必先恢复vptr,否则析构函数可能调用到错误的虚函数。
适用场景:需要精确控制单个对象行为、避免影响同类其他实例、或需要绕过某些反hook检测时使用。