虚函数表(VMT)Hook
前言
虚函数表(VMT)Hook,又叫指针重定向,是一种常见的Hook技术,在游戏外挂程序中最常见。且多用于在Direct3D / OpenGL引擎游戏里实现内置叠加层。
虚函数表(VMT)
本文中VMT就代指虚函数表。
虚函数表是C++实现多态的一种方式。
每一个有虚函数的类(或有虚函数类的派生类)都有一个VMT,VMT本质上就是一个函数指针数组,通常位于对象内存布局的开头或结尾。每当C++类声明虚(virtual
)函数时,编译器都会增加一个条目到VMT中。
例如,在x86系统上使用VS2019编译以下代码: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Base
{
public:
Base() { std::cout << "- Base::Base\n"; }
virtual ~Base() { std::cout << "- Base::~Base\n"; }
void A() { std::cout << "- Base::A\n"; }
virtual void B() { std::cout << "- Base::B\n"; }
virtual void C() { std::cout << "- Base::C\n"; }
};
class Derived final : public Base
{
public:
Derived() { std::cout << "- Derived::Derived\n"; }
~Derived() { std::cout << "- Derived::~Derived\n"; }
void B() override { std::cout << "- Derived::B\n"; }
void C() override { std::cout << "- Derived::C\n"; }
};Base
类有三个虚函数:~Base
、B
和C
. Derived
类派生自Base
,并重写了两个虚函数B
h和C
.
这里我们创建三个实例 1
2
3Base base;
Derived derived;
Base* pBase = new Derived();Base
实例的VMT包含了~Base
、B
和C

而两个Derived
实例的VMT包含了~Derived
、B
和C
。但VMT里的函数地址与Base
实例中的不一样(见下图)


那么应该如何使用这些函数呢?
以一个函数为例,该函数获取一个指向Base
的指针并调用函数A
,B
和C
: 1
2
3
4
5
6void Invoke(Base* const pBase)
{
pBase->A();
pBase->B();
pBase->C();
}1
2
3Invoke(&base);
Invoke(&derived);
Invoke(pBase);
将Invoke
函数反汇编,看看在汇编层面,VMT内的函数是如何被调用的: > 可以将RTC关闭(项目属性->C/C++->代码生成->基本运行时检查->默认值),省去__RTC_CheckEsp等检查,让反汇编代码更简洁 >
对于B
的调用,编译器将pBase
也就是对象的地址移入EAX
寄存器,然后间接获取VTM的基地址,并将其存储在EDX
寄存器中。通过EDX
作为索引+4将函数地址存储在EAX
寄存器中,然后调用EAX
对C
的调用如出一辙,只是VMT中函数地址的偏移量为8。
由此可见,VMT的底层实现就是一个函数指针数组。
明白了VMT调用的原理,我们就可以很轻松的写一个函数来打印VMT: 1
2
3
4
5
6
7
8void PrintVTable(Base* const pBase)
{
auto pVTableBase = *reinterpret_cast<void***>(pBase);
printf("First: %p\n"
"Second: %p\n"
"Third: %p\n",
pVTableBase[0] , pVTableBase[1], pVTableBase[2]);
}
我们只要覆盖掉需要Hook的函数在VMT中的地址即可,这也解释了为什么VMT Hook也叫指针重定向。 1
2
3
4
5
6
7
8
9void HookVMT(Base* const pBase)
{
auto pVTableBase = *reinterpret_cast<void***>(pBase);
unsigned long ulOldProtect = 0;
VirtualProtect(&pVTableBase[1], sizeof(void*), PAGE_EXECUTE_READWRITE, &ulOldProtect);
pVTableBase[1] = VMTHookFnc;
VirtualProtect(&pVTableBase[1], sizeof(void*), ulOldProtect, &ulOldProtect);
}
1 | void __fastcall VMTHookFnc(void* pEcx, void* pEdx) |
这里利用
__fastcall
调用约定用来获取this
指针
成功Hook住虚函数B
!

利用调试器,进入Hook函数中,可以看到this
指针VMT里的B
已经被替换成了VMTHookFuc

封装
剩下的就是封装了
这里的命名规则遵循STL标准库的小写规则
这里的实现是整个VMT替换,这样也可以方便的实现获取原函数。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class vmt_hook
{
public:
vmt_hook(void* obj, size_t num_funcs);
void hook(size_t index, void* func);
void unhook(size_t index);
template <typename T>
T get_original(size_t index);
void enable();
void disable();
private:
void*** m_object;
size_t m_num_funcs;
void** m_original_table;
std::unique_ptr<void*[]> m_new_table;
};
template<typename T>
inline T vmt_hook::get_original(size_t index)
{
return static_cast<T>(m_original_table[index]);
}
1 |
|
总结
- 执行速度:10
- 编写难度:3-5
- 检测率:3
VMT Hook是最好的Hook方法之一,因为没有API或者检测这类Hook的通用方法。 但大多数反作弊引擎都会在D3D渲染引擎上检测VMT Hook。当然,只要你有经验,你的Hook就不会被检测