VanHooks logo

VanHooks

Function hooking for Windows, Linux and macOS, on x86, x64 and ARM64.

VanHooks is a C++23 library for hooking functions on Windows, Linux and macOS. It has 17 kinds of hook, from the ordinary inline trampoline through import-table, vtable and mid-function hooks to hardware breakpoints, page-guard traps, syscall-gate patches and a kernel instrumentation callback. Every call that can fail returns std::expected, and every hook is an RAII object that restores the original bytes when it goes out of scope.

We wrote it because we were using MinHook on Windows and something else everywhere else, with different error handling in each. MinHook, EasyHook, SafetyHook and PolyHook2 each cover part of the ground. VanHooks covers all of it, so every TeamVanilla project hooks the same way.

Three API levels

The API is layered so simple tasks stay simple and advanced ones don't have to reinvent anything.

A first hook

The shortest form takes a target and a detour. Pass a third argument to get a pointer to the original function so the detour can call through to it.

#include <vh/vh.hpp>

static int (*orig_add)(int, int) = nullptr;

int my_add(int a, int b) {
    printf("add(%d, %d)\n", a, b);
    return orig_add(a, b);
}

auto r = vh::hook(&add, &my_add, &orig_add);
if (!r) {
    printf("failed: %s\n", vh::error_to_string(r.error()).data());
}

System functions can be hooked by module and name, without looking up the address yourself:

static decltype(&MessageBoxW) orig_mbw = nullptr;

int WINAPI hk_mbw(HWND h, LPCWSTR text, LPCWSTR cap, UINT type) {
    return orig_mbw(h, L"[Intercepted]", cap, type);
}

auto r = vh::api_hook("user32", "MessageBoxW", &hk_mbw, &orig_mbw,
                      { .tag = "User32.MessageBoxW" });

Hook kinds

Every kind returns the same vh::Hook object, with the same enable / disable / remove / chain interface.

Core hooks (all platforms)

KindCallWhat it does
Trampolinevh::inline_hookOverwrites the start of the function with a jump. 5 bytes on x86/x64, 16 on ARM64. The displaced prologue is copied into a trampoline stub so the original can still be called.
Import tablevh::iat_hook / iat_hook_allRedirects one import in one module, or in every loaded module. Windows.
PLTvh::plt_hookPatches PLT entries on Linux and lazy pointers on macOS — the POSIX equivalent of an IAT hook.
GOThook_gotPatches already-resolved GOT slots directly, covering symbols that PLT hooks miss. Linux and macOS.
VTablevh::vtable_hookSwaps one slot in a C++ virtual function table. The way to reach COM objects such as a Direct3D device.
Mid-functionvh::mid_hookStops at a byte offset inside a function, hands your callback a MidContext* with all GPRs and flags, and continues. Control flow isn't redirected.
Call-sitehook_callsitePatches a single CALL or JMP instruction's rel32 displacement. Only that one call site is redirected; all other callers of the function are unaffected.
Returnhook_returnIntercepts function return. An optional entry callback fires first; the return callback gets a ReturnContext* with the return-value registers and the real return address. x64 only.
Thread-localhook_thread_localInstalls a trampoline gated by a per-thread TLS flag. Only threads that call thread_local_enable(handle) see the detour; everyone else falls through to the original.

No-write hooks (no .text modification)

KindCallWhat it does
Hardware breakpointhook_hwbpUses DR0–DR3 and a VEH. No bytes are written to the target. Max 4 active hooks. Windows x64.
PAGE_GUARD traphook_page_guardSets PAGE_GUARD on the target's page and intercepts the exception in a VEH. Windows only.
Instrumentation callbackhook_instrumentation_callbackSets KPROCESS.InstrumentationCallback via NtSetInformationProcess. Fires on every kernel-to-user transition. Process-wide singleton. Windows x64.
Software breakpointhook_soft_bpWrites INT 3 (x86/x64) or BRK (ARM64) and catches the exception in a VEH.
Exceptionhook_exceptionA VEH that fires for an arbitrary exception code within an address range.

Table and gate hooks (Windows)

KindCallWhat it does
Delay-load IAThook_delay_iatWalks IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT and hooks delay-loaded DLLs that hook_iat can't reach, forcing resolution if the import hasn't been called yet.
EAThook_eatPatches an Export Address Table slot. Affects callers that resolve the symbol via GetProcAddress after the hook is installed.
Syscall gatehook_syscallLocates the syscall instruction inside an Nt* stub in ntdll and patches it with a call-site hook. Windows x64.
TLS callbackhook_tls_callbackPatches a slot in IMAGE_TLS_DIRECTORY. Fires before DllMain on thread attach and detach.

Mid-function hooks

A mid-function hook gets the CPU state at a chosen instruction boundary, and can change it before the function continues. On x64 the MidContext holds RAX through R15 and RFLAGS; on ARM64 it holds x0–x28, FP, LR, SP, NZCV, FPCR, FPSR and all 32 NEON/SVE registers.

auto r = vh::mid_hook(game_update_fn,
    [](vh::MidContext* ctx) noexcept {
        player_health = static_cast<int>(ctx->rax);
    },
    { .offset = 0x1C, .tag = "Game.HealthReadback" });

The offset is rounded up to the nearest instruction boundary — VanHooks never splits an instruction. The Zydis disassembler is compiled into the library for this.

Lifetime, groups and chains

A vh::Hook is move-only. It can be disabled and re-enabled, removed early, and asked whether it's installed and active. When it's destroyed, the original bytes go back. The object also exposes its kind, target, detour, trampoline address and tag for introspection.

A group owns several hooks and switches them in one thread-suspension window, which costs far less than switching each one separately. Hooks in a group can be found again by tag.

auto grp = vh::group("UI");
grp.add(vh::api_hook("user32", "MessageBoxW", &hk_mbw, &orig_mbw,
                     { .tag = "User32.MessageBoxW" }))
   .add(vh::api_hook("user32", "SetWindowTextW", &hk_swt, &orig_swt,
                     { .tag = "User32.SetWindowTextW" }));

grp.disable();                              // both at once
grp.at("User32.MessageBoxW").enable();      // just one

A second detour can be put in front of an existing hook. Calls then run through the new detour, the old one, and finally the real function. Links come off in the reverse order they went on.

auto base = vh::inline_hook(&fn, &detour1, &orig1).value();
auto link = base.chain(&detour2, &chain_orig).value();

link.remove();
base.remove();

When several DLLs in one process each install their own hooks, vh::HookRegistry::global() keeps their groups by name, and one remove_all() at shutdown clears every one of them.

Batch queue

Installing or removing hooks one at a time means one thread-suspension per hook. The batch queue amortises the cost: queue up any mix of enables, disables and removes, then flush them all in a single Toolhelp32 / ptrace / task_threads window.

auto& eng = vh::advanced::engine();
eng.queue_enable(h1);
eng.queue_enable(h2);
eng.queue_disable(h3);
eng.apply_queued();   // one suspension, all patches applied

Groups use this internally — calling grp.enable() queues every hook in the group and flushes once.

Introspection

Every hook can report its kind, target address, detour address, trampoline address, enabled state and tag. The engine itself can list every installed hook, describe one by handle, find a hook by its target address, and return a total count.

for (auto& desc : eng.list_hooks()) {
    printf("[%s] %s → %p (enabled: %d)\n",
        desc.tag.c_str(),
        to_string(desc.kind),
        desc.target,
        desc.enabled);
}

Errors

Every fallible call returns vh::Result<T>, which is std::expected<T, vh::Error>. The error enum has named values grouped by category — general, memory, hook lifecycle, disassembly, module/symbol lookup, thread management, batch/chain, PE introspection, breakpoints and call-stack capture. vh::error_to_string turns any of them into a compile-time string view.

Compared with MinHook

 VanHooksMinHook
PlatformsWindows, Linux, macOSWindows
Architecturesx86, x64, ARM64x86, x64
Hook kinds17 (trampoline, call-site, IAT, delay-IAT, EAT, PLT, GOT, vtable, mid-function, return, thread-local, hardware breakpoint, page-guard, software breakpoint, exception, syscall gate, instrumentation callback, TLS callback)1 (inline)
Errorsstd::expectedC status codes
RAIIYes — hook removes itself on destructionNo — you call MH_RemoveHook
BatchingGroups and a queue, one thread-suspension for allA queue
ChainingYes — linked detour listNo
IntrospectionList, describe, find-by-target, countNo
Thread-local hooksYes — TLS-gated detourNo

Shutdown and DLL usage

If VanHooks is loaded inside a DLL, call vh::shutdown() before returning from DllMain(DLL_PROCESS_DETACH). This removes all hooks and tears down the engine before static destructors run. Failure to do so is safe in simple cases but can cause use-after-free if a detour references a module-level object that is destroyed first. vh::shutdown() is idempotent.

BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID) {
    if (reason == DLL_PROCESS_DETACH)
        vh::shutdown();
    return TRUE;
}

Adding it to a project

Copy include/ into your project and link the prebuilt library for your target. With CMake:

if(CMAKE_SIZEOF_VOID_P EQUAL 8)
    set(VH_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/lib/win-x64")
else()
    set(VH_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/lib/win-x86")
endif()

add_library(VanHooks::vanhooks STATIC IMPORTED)
set_target_properties(VanHooks::vanhooks PROPERTIES
    IMPORTED_LOCATION_RELEASE "${VH_LIB_DIR}/Release/vanhooks.lib"
    IMPORTED_LOCATION_DEBUG   "${VH_LIB_DIR}/Debug/vanhooks.lib"
    INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(my_target PRIVATE VanHooks::vanhooks)

Or add the full source tree with CMake's add_subdirectory:

set(VH_BUILD_TESTS    OFF CACHE BOOL "" FORCE)
set(VH_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
add_subdirectory(../VanHooks ${CMAKE_BINARY_DIR}/vanhooks_build)
target_link_libraries(my_target PRIVATE vanhooks)