虚函数表(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
20
class 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类有三个虚函数:~BaseBC.
Derived类派生自Base,并重写了两个虚函数Bh和C.

这里我们创建三个实例

1
2
3
Base base;
Derived derived;
Base* pBase = new Derived();

我们把代码跑起来,用调试器观察,发现Base实例的VMT包含了~BaseBC

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

那么应该如何使用这些函数呢?

以一个函数为例,该函数获取一个指向Base的指针并调用函数ABC

Invoke
1
2
3
4
5
6
void Invoke(Base* const pBase)
{
pBase->A();
pBase->B();
pBase->C();
}

以以下方式调用:

1
2
3
Invoke(&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:

PrintVTable
1
2
3
4
5
6
7
8
void 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]);
}

VMT Hook的实现

明白了VMT调用的原理,当然也就可以轻松的实现Hook了。

我们只要覆盖掉需要Hook的函数在VMT中的地址即可,这也解释了为什么VMT Hook也叫指针重定向。

HookVMT
1
2
3
4
5
6
7
8
9
void 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);
}
VMTHookFnc
1
2
3
4
5
6
void __fastcall VMTHookFnc(void* pEcx, void* pEdx)
{
Base* pThisPtr = (Base*)pEcx;

std::cout << "In VMTHookFnc\n";
}

这里利用__fastcall调用约定用来获取this指针

成功Hook住虚函数B!

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

封装

剩下的就是封装了

这里的命名规则遵循STL标准库的小写规则

这里的实现是整个VMT替换,这样也可以方便的实现获取原函数。

vmt_hook.h
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
#include<memory>

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]);
}
vmt_hook.cpp
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
29
30
#include "vmt_hook.h"

vmt_hook::vmt_hook(void* obj, size_t num_funcs) :
m_object(static_cast<void***>(obj)),
m_num_funcs(num_funcs + 1),
m_original_table(*m_object),
m_new_table(std::make_unique<void*[]>(m_num_funcs))
{
std::copy_n(m_original_table - 1, m_num_funcs, m_new_table.get());
}

void vmt_hook::hook(size_t index, void* func)
{
m_new_table[index + 1] = func;
}

void vmt_hook::unhook(size_t index)
{
m_new_table[index + 1] = m_original_table[index];
}

void vmt_hook::enable()
{
*m_object = m_new_table.get() + 1;
}

void vmt_hook::disable()
{
*m_object = m_original_table;
}

总结

  • 执行速度:10
  • 编写难度:3-5
  • 检测率:3

VMT Hook是最好的Hook方法之一,因为没有API或者检测这类Hook的通用方法。
但大多数反作弊引擎都会在D3D渲染引擎上检测VMT Hook。当然,只要你有经验,你的Hook就不会被检测