向量化异常处理(VEH) Hook

前言

在上两个Hook文章中,我实现了虚函数表钩子(VMT Hook)导入地址表(IAT)Hook

VMT Hook和IAT Hook的本质上都修改了内存(重定向了函数指针),所以也比较容易被检测

今天介绍一个不需要修改内存且几乎不会被检测的Hook,也就是向量化异常处理(VEH) Hook

什么是VEH?

在Windows上有着许多的异常处理方法,帮进程在遇到异常时免于崩溃。比如C++原生异常、结构化异常处理SEH(__try__finally__except就是VC++对SEH的封装)以及本文中要使用的向量化异常处理(VEH)

Windwos中的异常处理机制,当程序执行过程中发生异常时,ring0会使用KiDispatchException判断是否有内核调试器。如果没有调试器,ring0转交给ring3的KiDispatchException,开始找是否有异常处理程序(SEH、VEH)。如果已安装,会调用异常处理程序进行处理

异常处理流程:异常产生->调试器->VEH->SEH->TopLevelEH->调试器

由于SEH有很多缺点(基于线程、基于栈、处理优先级低),而VEH弥补了这些缺点(基于进程、且优先级高于SEH)

而我们可以用AddVectoredExceptionHandler函数注册自定义的VEH。当捕获到异常时,我们会拿到一个_EXCEPTION_POINTERS类型的参数。而这个参数的成员ContextRecord保存着异常触发时CPU各种寄存器的值,这就意味着我们可以直接修改EIP/RIP寄存器,从而达到指令跳转的目的

如何引发异常?

  1. PAGE_GUARD:有PAGE_GUARD标志的内存页面,如果程序尝试访问保护页内的地址,则引发STATUS_GUARD_PAGE_VIOLATION异常。且系统还清除PAGE_GUARD标志,从而删除内存页的保护页状态。

  2. NO_ACCESS: 有NO_ACCESS标志的内存页面,如果程序尝试访问保护页内的地址,则引发STATUS_ACCESS_VIOLATION的异常。

  3. INT3断点:也可以使用INT3断点并捕获BREAKPOINT异常。但INT3断点需要修改指令,不够隐蔽

  4. 硬件断点:最隐蔽。但只作用于单个线程,且需要线程的句柄

这四种都能可靠的引发异常并被VEH捕获,但使用前两种异常的同时也必须使用STATUS_SINGLE_STEP(单步异常)

如果你熟悉断点的原理,那么会很容易理解。STATUS_SINGLE_STEP实际上并不是严格意义上的异常,而是一种检测CPU的EFlags寄存器中的TF(跟踪标志位)的机制。当TF为1时,CPU每次仅执行一条指令,然后就引发一个STATUS_SINGLE_STEP异常

上图所示TF在Flags寄存器16位中的第9个bit,所以我们可以通过将ContextRecordEFlag按位或上二进制000100000000也就是0x100实现

为什么需要单步异常?

Windows系统在保护模式下使用内存分页机制。而一页内存是可以物理分配的最小内存量,通常是4K大小。

当我们用VirtualProtect在页面上设置保护标志时,不能作用于页面中某1个字节,而是只能作用于整个页面。

而绝大多数情况下,目标函数起始地址位于页面的中间。所以我们要想在这个函数起始地址触发异常,就必须使用单步异常

这样就可以一次在页中一条一条的执行指令,直到异常触发到目标函数起始地址的地址为止,然后修改EIP/RIP来跳转到我们的Hook函数

假设红色地址是目标函数的起始地址。STATUS_SINGLE_STEP允许我们一步步的到达那里。而如果执行了另一个函数,但和目标函数在同一个页中,那么STATUS_SINGLE_STEP会慢慢单步直到另一个函数结束,跳出该页,且不会到达红色地址,可以有效避免钩错函数

实现步骤

  1. 注册VEH
  2. 使用VirtualProtect将PAGE_GUARD标志添加到目标函数的地址页
  3. 等待目标函数被调用,从而引发PAGE_GUARD_VIOLATION异常
  4. VEH捕获异常,并跳转到Hook函数

封装

声明
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
31
32
33
34
35
36
37
38
39
40
41
#include <stdexcept>
#include <Windows.h>

#ifdef _WIN64
#define IP Rip
#else
#define IP Eip
#endif

template <typename F>
class veh_hook
{
static_assert(std::is_function<F>::value, "veh hook require the function");
public:
veh_hook(F* original_func, F* hook_func);

static void enable();
static void disable();
private:
static bool are_in_same_page();
static LONG WINAPI handler(EXCEPTION_POINTERS* exception_info);

static F* m_original_func;
static F* m_hook_func;
static void* m_veh_handle;
static DWORD m_old_protection;
};

template <typename F>
F* veh_hook<F>::m_original_func = nullptr;
template <typename F>
F* veh_hook<F>::m_hook_func = nullptr;
template <typename F>
void* veh_hook<F>::m_veh_handle = nullptr;
template <typename F>
DWORD veh_hook<F>::m_old_protection = 0;

template <typename F>
veh_hook<F> make_veh_hook(F* original_func, F* hook_func) {
return veh_hook<F>(original_func, hook_func);
}
实现
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
template<typename F>
veh_hook<F>::veh_hook(F* original_func, F* hook_func) {
m_original_func = original_func;
m_hook_func = hook_func;
}

template<typename F>
void veh_hook<F>::enable() {
if (!m_veh_handle) {
if (!m_original_func || !m_hook_func)
throw std::runtime_error("Failed to enable veh hook : original/hook function can't be nullptr!");

// 不能Hook在同一页面的两个函数,会无限回调
if (are_in_same_page())
throw std::runtime_error("Failed to enable veh hook : two functions in the same page!");

// 添加VEH
m_veh_handle = AddVectoredExceptionHandler(true, handler);
if (!m_veh_handle)
throw std::runtime_error("Failed to enable veh hook : add veh failed!");

// 给目标函数的页面加上PAGE_GUARD标志
if (!VirtualProtect(m_original_func, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &m_old_protection))
throw std::runtime_error("Failed to enable veh hook : modify memory protection failed!");
}
}

template<typename F>
void veh_hook<F>::disable() {
if (m_veh_handle) {
DWORD _;
if (!VirtualProtect(m_original_func, 1, m_old_protection, &_)) // 恢复内存保护
throw std::runtime_error("Failed to disable veh hook : modify memory protection failed!");

if (!RemoveVectoredExceptionHandler(m_veh_handle)) // 移除VEH
throw std::runtime_error("Failed to disable veh hook : remove veh failed!");

m_veh_handle = nullptr;
}
}

template<typename F>
bool veh_hook<F>::are_in_same_page() {
MEMORY_BASIC_INFORMATION mbi1;
if (!VirtualQuery(m_original_func, &mbi1, sizeof(mbi1)))
return true;

MEMORY_BASIC_INFORMATION mbi2;
if (!VirtualQuery(m_hook_func, &mbi2, sizeof(mbi2)))
return true;

if (mbi1.BaseAddress == mbi2.BaseAddress) // 看看页起始地址是否相同
return true; // 在同一页,返回true使enable函数抛出异常

return false;
}

template<typename F>
LONG WINAPI veh_hook<F>::handler(EXCEPTION_POINTERS* exception_info) {
// 捕获PAGE_GUARD_VIOLATION异常
if (exception_info->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) {
// 如果EIP/RIP是目标函数,就修改EIP/RIP来跳转到Hook函数
if (exception_info->ContextRecord->IP == reinterpret_cast<uintptr_t>(m_original_func)) {
exception_info->ContextRecord->IP = reinterpret_cast<uintptr_t>(m_hook_func);
}

exception_info->ContextRecord->EFlags |= 0x100; // 将TF置1,下一条指令执行完后会触发SINGLE_STEP异常
return EXCEPTION_CONTINUE_EXECUTION; // 继续下一条指令
} else if (exception_info->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) {// 捕获SINGLE_STEP异常
DWORD _;
VirtualProtect(m_original_func, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &_); // 重新加上PAGE_GUARD标志,因为每次触发过异常后系统都会清除它

return EXCEPTION_CONTINUE_EXECUTION; // 继续下一条指令
}

return EXCEPTION_CONTINUE_SEARCH; // 如果不是PAGE_GUARD也不是SINGLE_STEP,就拒绝处理
}

用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 钩子函数
void WINAPI my_sleep(DWORD ms) {
std::cout << "[?] Hooked Sleep Function Called!" << std::endl;
std::cout << "Sleeping for: " << ms << std::endl;
}

int main() {
std::cout << "VEH Hook Example by AmazingPP\n" << std::endl;

auto hooking = make_veh_hook(Sleep, my_sleep);
hooking.enable();
Sleep(1000);
hooking.disable();
Sleep(1000);

return 0;
}

输出:

注意:由于VEH Hook并没有修改目标函数,所以如果想在Hook函数里调用原函数需要先disable(),否则会无限循环调用

总结

  • 执行速度:1
  • 编写难度:8
  • 检测率:1

VEH Hook 不需要修改内存,所以非常隐蔽,而且几乎没有反作弊引擎可以检测到它

当然有小部分反作弊引擎用VirtualQuery检查PAGE_GUARD标志来检测VEH Hook,解决方法也很简单,先用VEH Hook把VirtualQuery钩上,筛掉PAGE_GUARD标志即可

但缺点也很明显,实现起来比较复杂,而且执行速度非常非常慢

如果对Windows的异常处理机制有更深入的了解,甚至可以钩上异常处理ring0返回ring3后调用的第一个用户态函数,也就是ntdll里的KiUserExceptionDispatcher,这样就可以比VEH更早的捕获到异常

引用

Vectored Exception Handling, Hooking Via Forced Exception