API hooking is one of the most powerful techniques in Windows system programming. At its core, hooking redirects function calls from their intended destination to code under our control. This interception capability serves legitimate purposes like debugging, profiling, and security monitoring, while also enabling offensive techniques like credential theft and detection evasion.
When a program calls MessageBox, it doesn't know or care where that function's code actually resides. The CPU simply jumps to an address and begins executing. Hooking exploits this indirection by inserting ourselves into the call chain, examining or modifying behavior before (or instead of) the original function executing.
THE ESSENCE OF API HOOKING
==========================
Normal Execution Flow:
┌──────────────────┐ ┌──────────────────┐
│ Application │ │ MessageBoxA │
│ │ │ │
│ call MsgBoxA ──┼────────►│ Display box │
│ │ │ Return result │
└──────────────────┘ └──────────────────┘
Hooked Execution Flow:
┌──────────────────┐ ┌──────────────────┐
│ Application │ │ Our Hook │
│ │ │ │
│ call MsgBoxA ──┼────────►│ Log arguments │
│ │ │ Modify data │
│ │ │ Call original──┼────┐
└──────────────────┘ └──────────────────┘ │
│
┌──────────────────┐ │
│ MessageBoxA │◄───┘
│ │
│ (Original) │
│ Display box │
└──────────────────┘
The application is unaware of the interception.
We see and control everything passing through.
Different hooking techniques offer different trade-offs in complexity, stealth, and scope. Understanding these options helps choose the right approach for each situation.
HOOKING METHOD CHARACTERISTICS
==============================
┌────────────────────┬──────────────┬────────────┬────────────┬─────────────┐
│ Method │ Scope │ Complexity │ Detection │ Reliability │
├────────────────────┼──────────────┼────────────┼────────────┼─────────────┤
│ Inline/Trampoline │ Per-process │ Medium │ Medium │ High │
│ IAT Hooking │ Per-module │ Low │ Easy │ Medium │
│ EAT Hooking │ System-wide* │ Medium │ Medium │ Medium │
│ VEH/Hardware BP │ Per-process │ High │ Hard │ High │
│ Kernel Hooking │ System-wide │ Very High │ Hard │ High │
└────────────────────┴──────────────┴────────────┴────────────┴─────────────┘
* EAT hooking affects future GetProcAddress calls, not existing pointers
Hooking serves both sides of the security equation:
Offensive Uses:
Defensive Uses:
Inline hooking is the most direct approach: overwrite the first few bytes of the target function with a jump to our code. To call the original function later, we save those overwritten bytes in a "trampoline" that executes them before jumping back.
INLINE HOOK STRUCTURE
=====================
Before Hooking:
┌─────────────────────────────────────┐
│ Original Function │
│ ────────────────────────────────── │
│ 0x7FFE1234: mov rbp, rsp │
│ 0x7FFE1237: sub rsp, 0x20 │
│ 0x7FFE123B: mov [rsp+8], rcx │
│ 0x7FFE1240: ... │
│ (function body) │
└─────────────────────────────────────┘
After Hooking:
┌─────────────────────────────────────┐
│ Original Function (modified) │
│ ────────────────────────────────── │
│ 0x7FFE1234: jmp HookFunction ◄──────┼── 14 bytes replaced
│ 0x7FFE1242: <partial instruction> │
│ ... │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Hook Function (our code) │
│ ────────────────────────────────── │
│ - Examine/log arguments │
│ - Optionally modify data │
│ - Call trampoline if needed │
│ - Process return value │
│ - Return to caller │
└─────────────────────────────────────┘
│
│ (to call original)
▼
┌─────────────────────────────────────┐
│ Trampoline │
│ ────────────────────────────────── │
│ mov rbp, rsp ◄── saved │
│ sub rsp, 0x20 bytes │
│ jmp 0x7FFE123B ────────────────── │ jump past our hook
└─────────────────────────────────────┘
On x64 Windows, we need a 14-byte jump sequence because the address space is 64 bits wide. This jump uses an indirect jump through a memory location immediately following the opcode.
#include <windows.h>
// Structure to track a single hook
typedef struct _HOOK_ENTRY {
PVOID pOriginalFunction; // Address of function to hook
PVOID pHookFunction; // Our replacement function
PVOID pTrampoline; // Trampoline to call original
BYTE bOriginalBytes[16]; // Saved original bytes
SIZE_T sOriginalBytesCount; // Number of bytes we overwrote
BOOL bInstalled; // Is hook currently active?
} HOOK_ENTRY, *PHOOK_ENTRY;
// x64 jump instruction requires 14 bytes:
// FF 25 00 00 00 00 jmp qword ptr [rip+0]
// XX XX XX XX XX XX XX XX 8-byte absolute address
#define JMP_SIZE_X64 14
The trampoline creation copies the original bytes and appends a jump back to the instruction following our hook:
BOOL CreateTrampoline(PHOOK_ENTRY pHook) {
// Allocate executable memory for the trampoline
// We need space for original bytes plus another jump
pHook->pTrampoline = VirtualAlloc(
NULL,
64, // Generous allocation for bytes + jump
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
if (!pHook->pTrampoline) return FALSE;
// Copy the original bytes that we're about to overwrite
memcpy(pHook->pTrampoline, pHook->bOriginalBytes, pHook->sOriginalBytesCount);
// Calculate where to jump back to (original function + bytes we took)
PBYTE pJumpLocation = (PBYTE)pHook->pTrampoline + pHook->sOriginalBytesCount;
PVOID pReturnAddress = (PBYTE)pHook->pOriginalFunction + pHook->sOriginalBytesCount;
// Write the jump back: FF 25 00 00 00 00 [8-byte address]
pJumpLocation[0] = 0xFF;
pJumpLocation[1] = 0x25;
*(DWORD*)(pJumpLocation + 2) = 0; // RIP-relative offset of 0
*(PVOID*)(pJumpLocation + 6) = pReturnAddress;
return TRUE;
}
Installing the hook overwrites the function's prologue with a jump to our handler:
BOOL InstallHook(PHOOK_ENTRY pHook) {
// For x64, we need 14 bytes for our jump instruction
pHook->sOriginalBytesCount = JMP_SIZE_X64;
// Save the bytes we're about to overwrite
memcpy(pHook->bOriginalBytes, pHook->pOriginalFunction, pHook->sOriginalBytesCount);
// Create the trampoline for calling original
if (!CreateTrampoline(pHook)) return FALSE;
// Make the target memory writable (code is normally read-execute only)
DWORD dwOldProtect;
if (!VirtualProtect(
pHook->pOriginalFunction,
pHook->sOriginalBytesCount,
PAGE_EXECUTE_READWRITE,
&dwOldProtect
)) {
return FALSE;
}
// Write our jump instruction
PBYTE pTarget = (PBYTE)pHook->pOriginalFunction;
pTarget[0] = 0xFF; // jmp qword ptr [rip+0]
pTarget[1] = 0x25;
*(DWORD*)(pTarget + 2) = 0;
*(PVOID*)(pTarget + 6) = pHook->pHookFunction;
// Restore original memory protection
VirtualProtect(
pHook->pOriginalFunction,
pHook->sOriginalBytesCount,
dwOldProtect,
&dwOldProtect
);
pHook->bInstalled = TRUE;
return TRUE;
}
Removing the hook restores the original bytes:
BOOL RemoveHook(PHOOK_ENTRY pHook) {
if (!pHook->bInstalled) return FALSE;
DWORD dwOldProtect;
VirtualProtect(
pHook->pOriginalFunction,
pHook->sOriginalBytesCount,
PAGE_EXECUTE_READWRITE,
&dwOldProtect
);
// Restore the original function prologue
memcpy(pHook->pOriginalFunction, pHook->bOriginalBytes, pHook->sOriginalBytesCount);
VirtualProtect(
pHook->pOriginalFunction,
pHook->sOriginalBytesCount,
dwOldProtect,
&dwOldProtect
);
// Free the trampoline memory
if (pHook->pTrampoline) {
VirtualFree(pHook->pTrampoline, 0, MEM_RELEASE);
pHook->pTrampoline = NULL;
}
pHook->bInstalled = FALSE;
return TRUE;
}
Here's a complete example that intercepts MessageBoxA calls, logs the parameters, modifies the message, and then displays the modified version:
#include <windows.h>
#include <stdio.h>
// Global hook tracking structure
HOOK_ENTRY g_MessageBoxHook = { 0 };
// Function pointer type for the original function
typedef int (WINAPI* fnMessageBoxA)(HWND, LPCSTR, LPCSTR, UINT);
// Our hook handler
int WINAPI HookedMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
// Log the interception
printf("[HOOK] MessageBoxA intercepted!\n");
printf("[HOOK] Original text: %s\n", lpText);
printf("[HOOK] Caption: %s\n", lpCaption);
// Modify the message
LPCSTR szModifiedText = "This message was intercepted by our hook!";
// Call the original function through our trampoline
fnMessageBoxA pOriginal = (fnMessageBoxA)g_MessageBoxHook.pTrampoline;
return pOriginal(hWnd, szModifiedText, lpCaption, uType);
}
int main() {
// Initialize the hook structure
g_MessageBoxHook.pOriginalFunction = GetProcAddress(
GetModuleHandleA("user32.dll"),
"MessageBoxA"
);
g_MessageBoxHook.pHookFunction = HookedMessageBoxA;
// Install the hook
if (InstallHook(&g_MessageBoxHook)) {
printf("Hook installed successfully!\n");
// This call will be intercepted
MessageBoxA(NULL, "Original message", "Test", MB_OK);
// Remove the hook
RemoveHook(&g_MessageBoxHook);
printf("Hook removed.\n");
// This call goes directly to the real function
MessageBoxA(NULL, "No longer hooked", "Test", MB_OK);
}
return 0;
}
Manual inline hooking works but requires careful attention to instruction boundaries and architecture details. Microsoft Detours is an official library that handles these complexities, providing robust hooking across x86 and x64 with proper instruction-length detection and thread safety.
Detours works by rewriting the target function's prologue to jump to a "detour function" (our hook). It automatically creates a trampoline containing the original instructions and stores the trampoline address where you can call the original function.
#include <windows.h>
#include <detours.h>
#include <stdio.h>
#pragma comment(lib, "detours.lib")
// Pointer to original function - Detours fills this in
static int (WINAPI* TrueMessageBoxA)(
HWND hWnd,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType
) = MessageBoxA; // Initialize to original address
// Our detour function (hook handler)
int WINAPI DetourMessageBoxA(
HWND hWnd,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType
) {
printf("[Detours] Intercepted: %s\n", lpText);
// Call the original through the trampoline
return TrueMessageBoxA(hWnd, "[DETOURED] Modified!", lpCaption, uType);
}
// Install hooks
BOOL AttachHooks(void) {
// Begin a transaction (groups multiple hooks atomically)
DetourTransactionBegin();
// Important: update the current thread to handle hooks being installed
DetourUpdateThread(GetCurrentThread());
// Attach our detour to the target function
// This modifies TrueMessageBoxA to point to the trampoline
DetourAttach(&(PVOID&)TrueMessageBoxA, DetourMessageBoxA);
// Commit all changes
LONG lError = DetourTransactionCommit();
return (lError == NO_ERROR);
}
// Remove hooks
BOOL DetachHooks(void) {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
// Detach our detour, restoring the original function
DetourDetach(&(PVOID&)TrueMessageBoxA, DetourMessageBoxA);
LONG lError = DetourTransactionCommit();
return (lError == NO_ERROR);
}
Detours excels at installing multiple hooks in a single transaction, which is important for consistency and thread safety:
#include <windows.h>
#include <detours.h>
// Original function pointers
static BOOL (WINAPI* TrueCreateProcessW)(
LPCWSTR, LPWSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES,
BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION
) = CreateProcessW;
static HANDLE (WINAPI* TrueCreateFileW)(
LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE
) = CreateFileW;
// Detour functions
BOOL WINAPI DetourCreateProcessW(
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
) {
// Log process creation
wprintf(L"[PROCESS] Creating: %s\n",
lpApplicationName ? lpApplicationName : lpCommandLine);
// Call original
return TrueCreateProcessW(
lpApplicationName, lpCommandLine, lpProcessAttributes,
lpThreadAttributes, bInheritHandles, dwCreationFlags,
lpEnvironment, lpCurrentDirectory, lpStartupInfo,
lpProcessInformation
);
}
HANDLE WINAPI DetourCreateFileW(
LPCWSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
) {
// Log file operations
wprintf(L"[FILE] Accessing: %s\n", lpFileName);
return TrueCreateFileW(
lpFileName, dwDesiredAccess, dwShareMode,
lpSecurityAttributes, dwCreationDisposition,
dwFlagsAndAttributes, hTemplateFile
);
}
// Install all hooks atomically
BOOL AttachAllHooks(void) {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
// Attach all hooks in one transaction
DetourAttach(&(PVOID&)TrueCreateProcessW, DetourCreateProcessW);
DetourAttach(&(PVOID&)TrueCreateFileW, DetourCreateFileW);
return DetourTransactionCommit() == NO_ERROR;
}
MinHook is a lightweight alternative to Detours, providing a simpler API while remaining robust for both x86 and x64. It's popular in the game modding community and for quick prototyping.
#include <windows.h>
#include <stdio.h>
#include "MinHook.h"
#pragma comment(lib, "libMinHook.x64.lib")
// Pointer for the original function (filled by MinHook)
typedef int (WINAPI* fnMessageBoxW)(HWND, LPCWSTR, LPCWSTR, UINT);
fnMessageBoxW fpMessageBoxW = NULL;
// Our hook function
int WINAPI HookedMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) {
wprintf(L"[MinHook] Intercepted: %s\n", lpText);
// Call original through the provided function pointer
return fpMessageBoxW(hWnd, L"[MinHook] Hooked!", lpCaption, uType);
}
int main() {
// Initialize MinHook
if (MH_Initialize() != MH_OK) {
printf("MinHook initialization failed\n");
return 1;
}
// Create hook by API name
if (MH_CreateHookApi(
L"user32", // Module name (without .dll)
"MessageBoxW", // Function name
&HookedMessageBoxW, // Our hook function
(LPVOID*)&fpMessageBoxW // Receives trampoline pointer
) != MH_OK) {
printf("Hook creation failed\n");
return 1;
}
// Enable the hook
if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK) {
printf("Hook enable failed\n");
return 1;
}
// Test the hook
MessageBoxW(NULL, L"Original text", L"Test", MB_OK);
// Disable and cleanup
MH_DisableHook(MH_ALL_HOOKS);
MH_Uninitialize();
return 0;
}
Sometimes you need to hook functions not exported by name, or hook at specific addresses within a function. MinHook supports this:
#include <windows.h>
#include "MinHook.h"
// Function type for NtQueryInformationProcess
typedef NTSTATUS (NTAPI* fnNtQueryInformationProcess)(
HANDLE ProcessHandle,
ULONG ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
fnNtQueryInformationProcess fpNtQueryInformationProcess = NULL;
// Hook to hide debugger presence (anti-anti-debug)
NTSTATUS NTAPI HookedNtQueryInformationProcess(
HANDLE ProcessHandle,
ULONG ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
) {
// ProcessDebugPort = 7
if (ProcessInformationClass == 7) {
// Return 0 for debug port (pretend no debugger)
if (ProcessInformation && ProcessInformationLength >= sizeof(DWORD_PTR)) {
*(DWORD_PTR*)ProcessInformation = 0;
}
if (ReturnLength) {
*ReturnLength = sizeof(DWORD_PTR);
}
return 0; // STATUS_SUCCESS
}
// For other queries, call the original
return fpNtQueryInformationProcess(
ProcessHandle,
ProcessInformationClass,
ProcessInformation,
ProcessInformationLength,
ReturnLength
);
}
BOOL SetupAntiAntiDebugHook(void) {
MH_Initialize();
// Get the actual function address
FARPROC pFunction = GetProcAddress(
GetModuleHandleW(L"ntdll.dll"),
"NtQueryInformationProcess"
);
// Create hook by address
if (MH_CreateHook(
pFunction,
&HookedNtQueryInformationProcess,
(LPVOID*)&fpNtQueryInformationProcess
) != MH_OK) {
return FALSE;
}
return MH_EnableHook(pFunction) == MH_OK;
}
IAT hooking takes a different approach: instead of modifying function code, it modifies the table of function pointers that the calling module uses to find functions. When code calls an imported function, it reads the address from the IAT and jumps there. By changing the IAT entry, we redirect all calls from that module.
IAT HOOKING MECHANISM
=====================
Module Structure (simplified):
┌────────────────────────────────────────────────────────────┐
│ PE Header │
│ ├── Import Directory → Array of import descriptors │
│ │ │
│ │ kernel32.dll imports: │
│ │ ┌──────────────────────────────────────────────────┐ │
│ │ │ OriginalFirstThunk → Names/Ordinals │ │
│ │ │ FirstThunk → IAT entries │ │
│ │ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
IAT (FirstThunk array):
┌────────────────────────────────────────────────────────────┐
│ Before Hook: After Hook: │
│ │
│ [0] MessageBoxA → 0x7FFE1234 [0] MessageBoxA → 0x140000│
│ (user32) (our hook)│
│ │
│ [1] ExitProcess → 0x7FFE5678 [1] ExitProcess → 0x7FFE5│
│ (unchanged) │
└────────────────────────────────────────────────────────────┘
Code execution:
call [IAT + MessageBoxA_offset]
└──────────────────────────► jumps to whatever address is in IAT
#include <windows.h>
#include <stdio.h>
BOOL IATHook(
HMODULE hModule, // Module whose IAT we're modifying
LPCSTR szTargetDll, // DLL containing the function
LPCSTR szFunctionName, // Function to hook
PVOID pHookFunction, // Our hook function
PVOID* ppOriginalFunction // Receives original function address
) {
PBYTE pBase = (PBYTE)hModule;
// Navigate PE headers
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBase;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pBase + pDos->e_lfanew);
// Get import directory
DWORD dwImportRVA = pNt->OptionalHeader.DataDirectory[
IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
if (!dwImportRVA) return FALSE;
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pBase + dwImportRVA);
// Walk through import descriptors looking for our target DLL
while (pImport->Name) {
LPCSTR szDllName = (LPCSTR)(pBase + pImport->Name);
if (_stricmp(szDllName, szTargetDll) == 0) {
// Found the DLL - now search for the function
// OriginalFirstThunk points to names/ordinals (for lookup)
// FirstThunk points to actual addresses (what we modify)
PIMAGE_THUNK_DATA pOriginalThunk =
(PIMAGE_THUNK_DATA)(pBase + pImport->OriginalFirstThunk);
PIMAGE_THUNK_DATA pThunk =
(PIMAGE_THUNK_DATA)(pBase + pImport->FirstThunk);
while (pOriginalThunk->u1.AddressOfData) {
// Check if imported by name (not ordinal)
if (!(pOriginalThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)) {
PIMAGE_IMPORT_BY_NAME pImportName =
(PIMAGE_IMPORT_BY_NAME)(pBase + pOriginalThunk->u1.AddressOfData);
if (strcmp((LPCSTR)pImportName->Name, szFunctionName) == 0) {
// Found it! Save the original address
*ppOriginalFunction = (PVOID)pThunk->u1.Function;
// Make IAT entry writable
DWORD dwOldProtect;
VirtualProtect(
&pThunk->u1.Function,
sizeof(PVOID),
PAGE_READWRITE,
&dwOldProtect
);
// Replace with our hook address
pThunk->u1.Function = (ULONG_PTR)pHookFunction;
// Restore protection
VirtualProtect(
&pThunk->u1.Function,
sizeof(PVOID),
dwOldProtect,
&dwOldProtect
);
return TRUE;
}
}
pOriginalThunk++;
pThunk++;
}
}
pImport++;
}
return FALSE; // Function not found
}
// Store original function pointer
int (WINAPI* OriginalMessageBoxA)(HWND, LPCSTR, LPCSTR, UINT);
// Our hook
int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
printf("[IAT Hook] Intercepted: %s\n", lpText);
return OriginalMessageBoxA(hWnd, "[IAT HOOKED]", lpCaption, uType);
}
int main() {
// Hook MessageBoxA in our own module's IAT
IATHook(
GetModuleHandle(NULL), // Current module
"user32.dll", // DLL name
"MessageBoxA", // Function name
MyMessageBoxA, // Our hook
(PVOID*)&OriginalMessageBoxA
);
// This call is now hooked
MessageBoxA(NULL, "Test message", "Test", MB_OK);
return 0;
}
Limitation: IAT hooking only affects calls from the specific module whose IAT we modified. Other modules calling the same function are unaffected.
While IAT hooking affects a specific caller, EAT hooking modifies the DLL that exports the function. This affects future GetProcAddress calls for that function, potentially having system-wide effects.
#include <windows.h>
BOOL EATHook(
HMODULE hModule, // DLL whose exports we're modifying
LPCSTR szFunctionName, // Function to hook
PVOID pHookFunction, // Our hook function
PVOID* ppOriginalFunction // Receives original function address
) {
PBYTE pBase = (PBYTE)hModule;
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBase;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pBase + pDos->e_lfanew);
DWORD dwExportRVA = pNt->OptionalHeader.DataDirectory[
IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (!dwExportRVA) return FALSE;
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(pBase + dwExportRVA);
// Export table arrays
PDWORD pdwFunctions = (PDWORD)(pBase + pExport->AddressOfFunctions);
PDWORD pdwNames = (PDWORD)(pBase + pExport->AddressOfNames);
PWORD pwOrdinals = (PWORD)(pBase + pExport->AddressOfNameOrdinals);
// Search for the function
for (DWORD i = 0; i < pExport->NumberOfNames; i++) {
LPCSTR szName = (LPCSTR)(pBase + pdwNames[i]);
if (strcmp(szName, szFunctionName) == 0) {
WORD wOrdinal = pwOrdinals[i];
PDWORD pdwFunctionRVA = &pdwFunctions[wOrdinal];
// Save original function address
*ppOriginalFunction = (PVOID)(pBase + *pdwFunctionRVA);
// Calculate new RVA (offset from module base to our hook)
// Note: This only works if hook is within same module or
// if we allocate the hook in a special way
DWORD dwNewRVA = (DWORD)((PBYTE)pHookFunction - pBase);
// Make EAT writable
DWORD dwOldProtect;
VirtualProtect(pdwFunctionRVA, sizeof(DWORD), PAGE_READWRITE, &dwOldProtect);
// Replace RVA
*pdwFunctionRVA = dwNewRVA;
VirtualProtect(pdwFunctionRVA, sizeof(DWORD), dwOldProtect, &dwOldProtect);
return TRUE;
}
}
return FALSE;
}
Important Note: EAT hooking only affects future GetProcAddress calls. Code that already has the function pointer (including the IAT, which is populated at load time) will continue using the original address.
VEH hooking is a more stealthy technique that doesn't modify any code. Instead, it uses hardware breakpoints and exception handling to intercept execution. When the CPU hits a hardware breakpoint, it generates an exception that our handler catches and redirects.
VEH HOOKING ARCHITECTURE
========================
Setup:
1. Register Vectored Exception Handler
2. Set hardware breakpoint on target function (DR0-DR3)
Execution Flow:
┌─────────────────┐
│ Code calls │
│ MessageBoxA │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Hardware BP │
│ triggers at │
│ function start │
└────────┬────────┘
│
▼ (EXCEPTION_SINGLE_STEP)
┌─────────────────┐
│ VEH Handler │
│ ───────────────│
│ Check: Is RIP │
│ our target? │
│ │
│ Yes: Modify RIP │
│ to point to │
│ our hook │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Our Hook runs │
│ (no code │
│ modification!) │
└─────────────────┘
#include <windows.h>
// Hook tracking (4 hardware breakpoint slots available)
typedef struct _VEH_HOOK {
PVOID pFunction; // Original function address
PVOID pHookFunction; // Our hook
int nDrIndex; // Which debug register (0-3)
} VEH_HOOK, *PVEH_HOOK;
VEH_HOOK g_VehHooks[4] = { 0 };
// Vectored Exception Handler
LONG WINAPI VehHandler(PEXCEPTION_POINTERS pExInfo) {
// Hardware breakpoints generate EXCEPTION_SINGLE_STEP
if (pExInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
// Check if we hit one of our hooks
for (int i = 0; i < 4; i++) {
if (g_VehHooks[i].pFunction &&
(PVOID)pExInfo->ContextRecord->Rip == g_VehHooks[i].pFunction) {
// Redirect execution to our hook
pExInfo->ContextRecord->Rip = (DWORD64)g_VehHooks[i].pHookFunction;
// Continue execution at new address
return EXCEPTION_CONTINUE_EXECUTION;
}
}
}
// Not our exception - let other handlers try
return EXCEPTION_CONTINUE_SEARCH;
}
// Set a hardware breakpoint
BOOL SetHardwareBreakpoint(PVOID pAddress, int nIndex) {
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
HANDLE hThread = GetCurrentThread();
if (!GetThreadContext(hThread, &ctx)) return FALSE;
// Set breakpoint address in DR0-DR3
switch (nIndex) {
case 0: ctx.Dr0 = (DWORD64)pAddress; break;
case 1: ctx.Dr1 = (DWORD64)pAddress; break;
case 2: ctx.Dr2 = (DWORD64)pAddress; break;
case 3: ctx.Dr3 = (DWORD64)pAddress; break;
default: return FALSE;
}
// Enable breakpoint in DR7
// DR7 controls which breakpoints are active and their conditions
ctx.Dr7 |= (1ULL << (nIndex * 2)); // Local enable for DRn
return SetThreadContext(hThread, &ctx);
}
// Install VEH hook
BOOL InstallVehHook(PVOID pFunction, PVOID pHookFunction) {
// Find a free slot
int nSlot = -1;
for (int i = 0; i < 4; i++) {
if (g_VehHooks[i].pFunction == NULL) {
nSlot = i;
break;
}
}
if (nSlot == -1) return FALSE; // All slots used
// Register VEH handler (only once)
static BOOL bRegistered = FALSE;
if (!bRegistered) {
AddVectoredExceptionHandler(1, VehHandler); // 1 = first handler
bRegistered = TRUE;
}
// Set the hardware breakpoint
if (!SetHardwareBreakpoint(pFunction, nSlot)) {
return FALSE;
}
// Store hook info
g_VehHooks[nSlot].pFunction = pFunction;
g_VehHooks[nSlot].pHookFunction = pHookFunction;
g_VehHooks[nSlot].nDrIndex = nSlot;
return TRUE;
}
Advantages:
Limitations:
Understanding how to detect hooks is crucial both for defenders trying to identify malicious activity and for red teamers trying to avoid detection.
#include <windows.h>
#include <stdio.h>
// Check if a function's prologue has been modified
BOOL IsInlineHooked(PVOID pFunction) {
PBYTE pCode = (PBYTE)pFunction;
// Check for common hook patterns at function start
// JMP rel32 (5 bytes)
if (pCode[0] == 0xE9) return TRUE;
// JMP rel8 (2 bytes)
if (pCode[0] == 0xEB) return TRUE;
// JMP [addr] - FF 25 (6+ bytes on x86, 14 on x64)
if (pCode[0] == 0xFF && pCode[1] == 0x25) return TRUE;
// CALL rel32
if (pCode[0] == 0xE8) return TRUE;
// PUSH addr; RET (6 bytes on x86)
if (pCode[0] == 0x68 && pCode[5] == 0xC3) return TRUE;
// MOV RAX, addr; JMP RAX (12 bytes on x64)
if (pCode[0] == 0x48 && pCode[1] == 0xB8) return TRUE;
return FALSE;
}
BOOL CheckIATIntegrity(HMODULE hModule) {
PBYTE pBase = (PBYTE)hModule;
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBase;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pBase + pDos->e_lfanew);
DWORD dwImportRVA = pNt->OptionalHeader.DataDirectory[
IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pBase + dwImportRVA);
while (pImport->Name) {
LPCSTR szDllName = (LPCSTR)(pBase + pImport->Name);
HMODULE hDll = GetModuleHandleA(szDllName);
if (!hDll) {
pImport++;
continue;
}
PIMAGE_THUNK_DATA pOriginalThunk =
(PIMAGE_THUNK_DATA)(pBase + pImport->OriginalFirstThunk);
PIMAGE_THUNK_DATA pThunk =
(PIMAGE_THUNK_DATA)(pBase + pImport->FirstThunk);
while (pOriginalThunk->u1.AddressOfData) {
if (!(pOriginalThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)) {
PIMAGE_IMPORT_BY_NAME pName =
(PIMAGE_IMPORT_BY_NAME)(pBase + pOriginalThunk->u1.AddressOfData);
// Get what the address SHOULD be
FARPROC pExpected = GetProcAddress(hDll, (LPCSTR)pName->Name);
// Compare with what's actually in IAT
if ((FARPROC)pThunk->u1.Function != pExpected) {
printf("[!] IAT hooked: %s!%s\n", szDllName, pName->Name);
printf(" Expected: %p, Actual: %p\n",
pExpected, (PVOID)pThunk->u1.Function);
return TRUE;
}
}
pOriginalThunk++;
pThunk++;
}
pImport++;
}
return FALSE; // No hooks detected
}
rule Inline_Hook_Patterns {
meta:
description = "Detects common inline hook patterns"
strings:
// x64 jump via RIP-relative
$jmp_rip = { FF 25 00 00 00 00 }
// x64 mov rax, imm64; jmp rax
$mov_jmp = { 48 B8 ?? ?? ?? ?? ?? ?? ?? ?? FF E0 }
// x86 push addr; ret
$push_ret = { 68 ?? ?? ?? ?? C3 }
condition:
any of them
}
rule Detours_Library_Usage {
meta:
description = "Detects Microsoft Detours library"
strings:
$func1 = "DetourTransactionBegin" ascii
$func2 = "DetourAttach" ascii
$func3 = "DetourDetach" ascii
$func4 = "DetourTransactionCommit" ascii
condition:
2 of them
}
rule MinHook_Library_Usage {
meta:
description = "Detects MinHook library"
strings:
$func1 = "MH_Initialize" ascii
$func2 = "MH_CreateHook" ascii
$func3 = "MH_EnableHook" ascii
condition:
all of them
}
Each hooking technique suits different scenarios:
HOOKING METHOD SELECTION GUIDE
==============================
┌──────────────────────┬────────────────────────────────────────────────────┐
│ Scenario │ Recommended Method │
├──────────────────────┼────────────────────────────────────────────────────┤
│ Quick prototyping │ MinHook or Detours (simple API) │
├──────────────────────┼────────────────────────────────────────────────────┤
│ Production EDR │ Detours (reliable, officially supported) │
├──────────────────────┼────────────────────────────────────────────────────┤
│ Hook specific caller │ IAT hooking (affects only that module) │
├──────────────────────┼────────────────────────────────────────────────────┤
│ Affect future loads │ EAT hooking (modifies GetProcAddress results) │
├──────────────────────┼────────────────────────────────────────────────────┤
│ Maximum stealth │ VEH + hardware breakpoints (no code modification) │
├──────────────────────┼────────────────────────────────────────────────────┤
│ System-wide effect │ Kernel hooking (requires driver) │
└──────────────────────┴────────────────────────────────────────────────────┘
Best Practices: