VehGuardHook.hpp
1#pragma once
2#define WIN32_LEAN_AND_MEAN
3#include <Windows.h>
4#include <cstdint>
5
6#ifdef _WIN64
7 #define XIP Rip
8#else
9 #define XIP Eip
10#endif
11
12namespace rubyeex {
13
14// PAGE_GUARD + VEH redirect hook.
15// Notes:
16// - This is a "best-effort" usermode hook; it is noisy (exceptions) and can be impacted by debuggers.
17// - Only supports 1 active hook instance in this minimal version.
18class VehGuardHook {
19public:
20 static bool Install(void* target, void* detour);
21 static bool Remove();
22 static bool IsInstalled() { return s_active; }
23
24private:
25 struct HookState {
26 void* target = nullptr;
27 void* detour = nullptr;
28 void* pageBase = nullptr;
29 SIZE_T pageSize = 0;
30 DWORD origProt = 0;
31 PVOID vehHandle = nullptr;
32 };
33
34 static HookState s;
35
36 static bool QueryPage(void* addr, void*& base, SIZE_T& size, DWORD& prot);
37 static bool IsExecutableProt(DWORD prot);
38 static LONG WINAPI Handler(EXCEPTION_POINTERS* info);
39
40 // Per-thread flag: did THIS thread just hit our guard page?
41 static DWORD s_tlsIndex;
42 static bool s_active;
43};
44
45inline VehGuardHook::HookState VehGuardHook::s{};
46inline DWORD VehGuardHook::s_tlsIndex = TLS_OUT_OF_INDEXES;
47inline bool VehGuardHook::s_active = false;
48
49inline bool VehGuardHook::IsExecutableProt(DWORD prot)
50{
51 prot &= 0xFF; // strip guard/nocache/wc bits
52 return prot == PAGE_EXECUTE ||
53 prot == PAGE_EXECUTE_READ ||
54 prot == PAGE_EXECUTE_READWRITE ||
55 prot == PAGE_EXECUTE_WRITECOPY;
56}
57
58inline bool VehGuardHook::QueryPage(void* addr, void*& base, SIZE_T& size, DWORD& prot)
59{
60 MEMORY_BASIC_INFORMATION mbi{};
61 if (!VirtualQuery(addr, &mbi, sizeof(mbi)))
62 return false;
63
64 if (mbi.State != MEM_COMMIT)
65 return false;
66
67 base = mbi.BaseAddress;
68 size = mbi.RegionSize;
69 prot = mbi.Protect;
70 return true;
71}
72
73inline bool VehGuardHook::Install(void* target, void* detour)
74{
75 if (!target || !detour)
76 return false;
77
78 if (s_active)
79 return false;
80
81 void* base = nullptr;
82 SIZE_T size = 0;
83 DWORD prot = 0;
84
85 if (!QueryPage(target, base, size, prot))
86 return false;
87
88 if (!IsExecutableProt(prot))
89 return false;
90
91 // TLS for per-thread single-step gating
92 s_tlsIndex = TlsAlloc();
93 if (s_tlsIndex == TLS_OUT_OF_INDEXES)
94 return false;
95
96 s.target = target;
97 s.detour = detour;
98 s.pageBase = base;
99 s.pageSize = size;
100 s.origProt = prot;
101
102 s.vehHandle = AddVectoredExceptionHandler(/*first=*/1, Handler);
103 if (!s.vehHandle) {
104 TlsFree(s_tlsIndex);
105 s_tlsIndex = TLS_OUT_OF_INDEXES;
106 s = {};
107 return false;
108 }
109
110 DWORD oldProt = 0;
111 if (!VirtualProtect(s.pageBase, s.pageSize, s.origProt | PAGE_GUARD, &oldProt)) {
112 RemoveVectoredExceptionHandler(s.vehHandle);
113 TlsFree(s_tlsIndex);
114 s_tlsIndex = TLS_OUT_OF_INDEXES;
115 s = {};
116 return false;
117 }
118
119 s_active = true;
120 return true;
121}
122
123inline bool VehGuardHook::Remove()
124{
125 if (!s_active)
126 return false;
127
128 // Best-effort restore
129 DWORD tmp = 0;
130 VirtualProtect(s.pageBase, s.pageSize, s.origProt, &tmp);
131
132 if (s.vehHandle)
133 RemoveVectoredExceptionHandler(s.vehHandle);
134
135 if (s_tlsIndex != TLS_OUT_OF_INDEXES) {
136 TlsFree(s_tlsIndex);
137 s_tlsIndex = TLS_OUT_OF_INDEXES;
138 }
139
140 s = {};
141 s_active = false;
142 return true;
143}
144
145inline LONG WINAPI VehGuardHook::Handler(EXCEPTION_POINTERS* info)
146{
147 const auto code = info->ExceptionRecord->ExceptionCode;
148
149 if (code == STATUS_GUARD_PAGE_VIOLATION)
150 {
151 // Only handle exceptions originating from our guarded page.
152 // (Guard-page violations include the faulting address in ExceptionInformation[1] sometimes,
153 // but it's not consistently useful; using RIP/EIP check is simple and deterministic.)
154 void* ip = (void*)info->ContextRecord->XIP;
155
156 if (ip == s.target) {
157 // mark this thread so SINGLE_STEP knows it's ours
158 if (s_tlsIndex != TLS_OUT_OF_INDEXES)
159 TlsSetValue(s_tlsIndex, (LPVOID)1);
160
161 info->ContextRecord->XIP = (uintptr_t)s.detour;
162 }
163
164 // ensure we get a SINGLE_STEP after next instruction to re-arm PAGE_GUARD
165 info->ContextRecord->EFlags |= 0x100;
166 return EXCEPTION_CONTINUE_EXECUTION;
167 }
168
169 if (code == STATUS_SINGLE_STEP)
170 {
171 // Only re-guard if THIS thread previously hit our guard-page
172 if (s_active && s_tlsIndex != TLS_OUT_OF_INDEXES) {
173 if (TlsGetValue(s_tlsIndex)) {
174 TlsSetValue(s_tlsIndex, nullptr);
175
176 DWORD tmp = 0;
177 VirtualProtect(s.pageBase, s.pageSize, s.origProt | PAGE_GUARD, &tmp);
178 }
179 }
180 return EXCEPTION_CONTINUE_EXECUTION;
181 }
182
183 return EXCEPTION_CONTINUE_SEARCH;
184}
185
186}
187
example.cpp
1#include <Windows.h>
2#include <cstdio>
3#include "veh_guard_hook.hpp"
4
5static int __stdcall Target(int a, int b)
6{
7 return a + b;
8}
9
10static int __stdcall Detour(int a, int b)
11{
12 std::printf("Detour called! a=%d b=%d\n", a, b);
13 // You can call original only if you have another path to it (this technique redirects execution).
14 return a + b + 1000;
15}
16
17int main()
18{
19 std::printf("Target(2,3) before hook = %d\n", Target(2,3));
20
21 if (!rubyeex::VehGuardHook::Install((void*)&Target, (void*)&Detour)) {
22 std::printf("Hook install failed\n");
23 return 1;
24 }
25
26 std::printf("Target(2,3) after hook = %d\n", Target(2,3));
27
28 rubyeex::VehGuardHook::Remove();
29
30 std::printf("Target(2,3) after remove= %d\n", Target(2,3));
31 return 0;
32}
33