Hooking, interception, and patching crate for Windows x64
Find a file
2026-07-03 18:10:10 +02:00
src Add HookSlot for static hook storage 2026-07-03 18:10:10 +02:00
tests Add HookSlot for static hook storage 2026-07-03 18:10:10 +02:00
.gitignore init 2026-06-29 11:35:54 +02:00
Cargo.lock Add HookSlot for static hook storage 2026-07-03 18:10:10 +02:00
Cargo.toml Add HookSlot for static hook storage 2026-07-03 18:10:10 +02:00
LICENSE init 2026-06-29 11:35:54 +02:00
README.md Add HookSlot for static hook storage 2026-07-03 18:10:10 +02:00
rustfmt.toml init 2026-06-29 11:35:54 +02:00

rehook

Hooking, interception, and patching crate for Windows x64.

This crate focuses mainly on this features:

  • Replace a function and keep a trampoline for the original
  • Hook an address inside a function and edit the saved register context
  • Patch raw bytes when you already know exactly what you want to write

The default build is no_std.

Install

[dependencies]
rehook = { git = "..." }

Optional features:

rehook = { git = "...", features = ["std"] }
rehook = { git = "...", features = ["iced-reloc"] }
rehook = { git = "...", features = ["xmm-registers"] }
  • std: implements standard error traits.
  • iced-reloc: uses iced-x86 for relocation instead of the built-in decoder.
  • xmm-registers: saves/restores XMM0-XMM15 for inline hooks.

The built-in relocator is small on purpose. If a hook site fails with UnsupportedInstruction, try iced-reloc feature before assuming the address is bad.

Quick replacement hook

use rehook::prelude::*;

type TargetFn = unsafe extern "system" fn(i32) -> i32;

static ORIGINAL: Original<TargetFn> = Original::new();

unsafe extern "system" fn detour(value: i32) -> i32 {
    let original = ORIGINAL.expect("original function is not installed");

    original(value) + 10
}

unsafe {
    let hook = TypedReplacementHook::<TargetFn>::install(
        target_fn as TargetFn,
        detour,
        HookOptions::default(),
    )?;

    ORIGINAL.set(hook.original())?;

    // Keep `hook` alive for as long as the hook should stay installed.
}

Dropping the hook restores the original bytes. If the hook is meant to stay installed for the rest of the process, leak it explicitly:

hook.forget();

There is also a macro if you like the static-original pattern:

rehook::define_original! {
    static ORIGINAL: TargetFn;
}

unsafe {
    let hook = rehook::install_typed_hook!(
        ty: TargetFn,
        target: target_fn,
        detour: detour,
        original: ORIGINAL,
    )?;
}

For raw addresses, use TypedReplacementHook::install_at or ReplacementHook::install.

One-shot replacement hook

Use TypedOneShotReplacementHook when a replacement is only needed for the first call. A common case is waiting for an initialization function, calling the original, then never intercepting that function again.

use core::sync::atomic::{AtomicBool, Ordering};
use rehook::prelude::*;

type InitFn = unsafe extern "system" fn(*const i8) -> i32;

static ORIGINAL_INIT: Original<InitFn> = Original::new();
static INIT_HOOK: HookSlot<TypedOneShotReplacementHook<InitFn>> = HookSlot::new();
static READY: AtomicBool = AtomicBool::new(false);

unsafe extern "system" fn init_detour(name: *const i8) -> i32 {
    let original = ORIGINAL_INIT.expect("original init is not installed");
    let result = unsafe { original(name) };

    READY.store(true, Ordering::Release);
    INIT_HOOK.take();

    result
}

unsafe {
    let hook = TypedOneShotReplacementHook::<InitFn>::install(
        init as InitFn,
        init_detour,
        HookOptions::default(),
    )?;

    ORIGINAL_INIT.set(hook.original())?;
    INIT_HOOK.set(hook)?;
}

On the first hit, the generated bridge restores the original bytes before jumping to the detour. Future calls go directly to the original function. HookSlot is a small static storage helper for hook handles, so you do not need static mut Option<Hook>.

Inline hook

Inline hooks run a callback before the overwritten instruction span is executed. The callback receives an InlineContext.

use rehook::prelude::*;

extern "win64" fn on_hit(ctx: &mut InlineContext) {
    ctx.regs.rcx = 123;
}

unsafe {
    let hook = InlineHook::install(
        address_inside_function,
        on_hit,
        HookOptions::default(),
    )?;
}

Normal flow is:

callback -> relocated original bytes -> code after the stolen span

If your callback is replacing the stolen instruction(s), call skip_original():

extern "win64" fn on_hit(ctx: &mut InlineContext) {
    ctx.set_zero_flag(true);
    ctx.skip_original();
}

That is not an early return. It only skips the instruction span overwritten by the hook patch. To leave the function early, redirect to a valid epilogue or return path yourself:

extern "win64" fn on_hit(ctx: &mut InlineContext) {
    ctx.regs.rax = 0;
    ctx.set_resume_address(return_site);
}

With xmm-registers, scalar float arguments can be edited through the low slot of the XMM register:

extern "win64" fn on_hit(ctx: &mut InlineContext) {
    ctx.regs.xmm0.set_f32(30.59);
}

For packed values:

ctx.regs.xmm0.set_f32_at(2, 30.59);
ctx.regs.xmm1.set_f64_at(1, 12.5);

If the callback needs caller state, use install_with_user_data.

One-shot inline hook

OneShotInlineHook is for break-once style hooks. It restores the original bytes on the first hit before execution continues.

let hook = unsafe {
    OneShotInlineHook::install(address_inside_function, on_hit, HookOptions::default())?
};

Keep the handle alive until it fires. Drop or disable it if you want to cancel it before then.

Thread strategy

The default is CallerEnsuresSafety: no suspension, no callbacks. It is the fast path, but it assumes you are not racing another thread that may execute the bytes being patched.

For built-in best-effort suspension:

let options = HookOptions::new()
    .with_thread_strategy(ThreadStrategy::SuspendOtherThreads);

This enumerates current process threads, suspends every thread it can except the current one, patches, then resumes them. Threads that cannot be opened or suspended are skipped.

If you already have your own synchronization/suspend code, wrap patching with a callback strategy instead:

let options = HookOptions::new()
    .with_thread_strategy(ThreadStrategy::Callback(ThreadCallback::new(
        pre,
        post,
        user_data,
    )));

For raw patch batches, with_thread_strategy(strategy, || { ... }) is public.

Patch kind

By default, PatchKind::Auto uses a 5-byte relative jump when the destination is within rel32 range. Otherwise it uses a 14-byte absolute-indirect sequence.

Trampolines and inline bridges try to allocate near the hook site so the 5-byte path is usually available.

You can force either form:

let options = HookOptions::new().with_patch_kind(PatchKind::Relative5);
let options = HookOptions::new().with_patch_kind(PatchKind::Absolute14);

Relative5 fails if the destination is not reachable.

Patterns And Raw Patches

The pattern scanner is just a utility. It does not install hooks.

let pattern = Pattern::parse("48 8B 4? ?F E8 ? ? ? ?")?;
let offset = pattern.find_in(bytes);

Scan executable sections in a loaded module:

let address = unsafe {
    find_pattern_in_module(module_base, "48 8B ?? ?? E8 ? ? ? ?")?
};

For direct byte patches:

unsafe {
    write_memory(address.cast::<u8>(), &[0x90, 0x90, 0x90])?;
}

MemoryPatch is the RAII version:

let patch = unsafe {
    MemoryPatch::install(address.cast::<u8>(), &[0x90, 0x90])?
};

Dropping patch restores the old bytes.

Helpers

The common imports are in:

use rehook::prelude::*;

Public modules are still available if you prefer explicit imports:

  • rehook::hook
  • rehook::memory
  • rehook::module
  • rehook::options
  • rehook::original
  • rehook::patch
  • rehook::pattern
  • rehook::registers
  • rehook::address

There are also helpers for:

  • module/export lookup
  • rel32 target calculation
  • typed raw addresses
  • grouped hook installation through HookBatch

They are convenience APIs. The core pieces are still replacement hooks, inline hooks, and memory writes.

Things To Keep In Mind

  • Hook handles are guards. Dropping them disables the hook.
  • forget() intentionally leaks the handle/generated code and leaves the hook installed.
  • Inline hook addresses must be exact instruction boundaries.
  • skip_original() skips the overwritten instruction span, not the whole function.
  • Registers::rsp is exposed for inspection, but edits to rsp are not applied by the restore path.
  • Thread suspension reduces patching races. It does not make a bad hook site safe.
  • The built-in relocator handles common x64 hook sites, not every possible encoding.
  • Pooled trampoline/bridge pages are switched back to RX after code emission, but individual chunks are not freed.

License

MPL-2.0. See LICENSE.