Files
kbe 6300bebe33 Fix API documentation and examples; add comprehensive documentation suite
- Example5: Fix format string bugs (alignment specifier placement, SafeMemoryHandle formatting)
- Example4: Fix DetourManager.Detour() → Create() in all doc strings
- Example3: Fix MemoryBase.CreateFunction() → InProcessInvoker.CreateFunction() in doc strings
- Examples 1-5: Correct all runtime errors and API mismatches vs actual WhiteMagic API
- README: Fix DetourManager.Detour() examples to use Create(); add missing code fence markers
- Add WhiteMagic.Examples project with 5 comprehensive example files (40+ sub-examples)
- Add docfx.json and toc.md for DocFX API reference generation
- Add 5 conceptual guides: architecture, memory-access, execution-models, hooking, troubleshooting
- Ensure zero errors, zero warnings across all projects (net8.0-windows)

Doc strings now teach correct APIs; runtime format bugs eliminated; build succeeds.
2026-07-22 22:26:07 +02:00

9.0 KiB

Function Hooking in WhiteMagic

WhiteMagic provides two types of runtime code modification: inline detours (function hooking) and byte patches. Both are reversible and automatically restored on disposal.

Overview

Feature DetourManager PatchManager
Scope In-process only External + In-process
Mechanism Inline jmp over prologue Named byte patch
Reversible Yes Yes
Auto-restore Yes Yes
Original call Via trampoline No

DetourManager (Inline Function Hooking)

What is a Detour?

An inline detour overwrites the first bytes of a function's prologue with a jump instruction (jmp or push/ret) that redirects execution to your hook delegate. The original bytes are saved in a trampoline that can call the original function.

How It Works

Original function:
│  push ebp
│  mov ebp, esp
│  sub esp, 0x10
│  ... rest of function

After detour:
│  jmp [hook_address]    ← Overwrites prologue
Trampoline:
│  push ebp             ← Saved original bytes
│  mov ebp, esp
│  jmp [original+5]     ← Jumps back to original function

Use Cases

  • API interception: Hook WinAPI functions (e.g., CreateFileW to monitor file access)
  • Function replacement: Replace a game function with your own logic
  • Behavior modification: Change parameters or return values
  • Profiling/instrumentation: Count calls, measure execution time

Usage Example

using WhiteMagic;
using WhiteMagic.Hooking;

// In-process only
using var magic = Magic.OpenInProcess();

// Define your hook delegate
[DllImport("kernel32.dll")]
public delegate void SleepDelegate(uint dwMilliseconds);

public static void MySleep(uint ms)
{
    Console.WriteLine($"Sleep called with {ms}ms");
    // Optionally call original
    // originalSleep(ms);
}

// Apply the detour
IntPtr sleepAddr = magic["kernel32"]["Sleep"].Address;
var detour = magic.DetourManager.Detour(
    sleepAddr,
    (SleepDelegate)MySleep
);

detour.Apply();

// ... use the hook

// Remove and restore original
detour.Remove();

CallOriginal (Trampoline)

To call the original function from your hook:

static MyDetourDelegate Original = null!;

static void MyHook(int arg1, float arg2)
{
    // Do something before
    Console.WriteLine($"Before: {arg1}, {arg2}");
    
    // Call original
    int result = Original(arg1, arg2);
    
    // Do something after
    Console.WriteLine($"After: {result}");
    return result;
}

// Create detour with original
var detour = magic.DetourManager.Detour(
    targetAddress,
    (MyDetourDelegate)MyHook,
    out var original
);

Original = original;
detour.Apply();

Prologue Safety

Before splicing a detour, WhiteMagic validates the prologue:

Default (StubAssembler):

  • Covers common x86/x64 prologue shapes:
    • push ebp; mov ebp, esp
    • mov edi, edi (hot-patch padding)
    • sub rsp, XX (x64 stack allocation)
    • Single-byte instructions (nop, int3)

Optional (IcedAssembler):

  • Full disassembly validation via Iced
  • Handles arbitrary prologues
  • Ensures splice lands on instruction boundaries

Safety Considerations

Risks:

  • Mid-instruction splice: Crashes if prologue validation fails
  • Race conditions: Patching while code is running can crash
  • Anti-cheat: Detours are easily detected

Mitigations:

  • Validate prologue before patching
  • Pause threads during application (use DetourManager.Apply(options))
  • Use MainThreadPump for crash-safe execution
  • Auto-restore on Dispose

Thread Safety

DetourManager is NOT thread-safe.

// WRONG: Concurrent modifications
Task.Run(() => detour1.Apply());
Task.Run(() => detour2.Apply());

// RIGHT: Synchronize
lock (magic.DetourManager)
{
    detour1.Apply();
    detour2.Apply();
}

Performance Impact

  • Hook overhead: ~5-10 CPU cycles (single jmp)
  • Trampoline call: ~20-30 cycles (jmp + trampoline execution)
  • Best practice: Keep hook delegates short

PatchManager (Named Byte Patches)

What is a Patch?

A patch is a named byte buffer that overwrites a region of memory. Unlike detours, patches can be any bytes and don't include a trampoline mechanism.

Use Cases

  • Patching constants: Change a hardcoded value (e.g., max HP cap)
  • NOP-ing code: Remove instructions (e.g., bypass a check)
  • Restoring bytes: Undo anti-cheat modifications
  • Hot-patching: Patch the hot-patch region (common in WinAPI)

Usage Example

using WhiteMagic;
using WhiteMagic.Hooking;

using var magic = Magic.Open(targetProcess);

// Create a patch to NOP 5 bytes
var patch = magic.PatchManager.Create(
    "MaxHealthCapPatch",
    address,
    new byte[] { 0x90, 0x90, 0x90, 0x90, 0x90 } // NOP x5
);

patch.Apply();

// Check status
if (patch.IsApplied)
{
    Console.WriteLine("Patch applied");
}

// Remove and restore
patch.Remove();

VirtualProtect Dance

When applying a patch, PatchManager automatically:

  1. Calls VirtualProtectEx to make region writable
  2. Writes the patch bytes
  3. Calls VirtualProtectEx to restore original protection

Relative Addressing

Patches support relative addressing:

// Apply relative to module base
var patch = magic.PatchManager.Create(
    "DataPatch",
    address,        // Interpreted as relative if isRelative=true
    patchBytes,
    isRelative: true
);

Thread Safety

PatchManager is NOT thread-safe. Synchronize concurrent modifications.


Lifecycle Management

Both managers auto-restore on disposal:

// All applied detours and patches auto-restore
using (var magic = Magic.OpenInProcess())
{
    var detour = magic.DetourManager.Detour(addr, hook);
    detour.Apply();
    
    var patch = magic.PatchManager.Create("Patch", addr, bytes);
    patch.Apply();
    
    // End of using block: detour.Remove() and patch.Remove() called automatically
}

Explicit Disposal

var detour = magic.DetourManager.Detour(addr, hook);
detour.Apply();

// Later
detour.Remove(); // Restores original bytes

Advanced: Memory Protection

Handling Read-Only Memory

If the target region is protected (e.g., .text section), both managers use:

VirtualProtectEx(handle, address, size, PAGE_EXECUTE_READWRITE, out oldProt);
// Write bytes
VirtualProtectEx(handle, address, size, oldProt, out _);

Risks

  • Anti-cheat: May detect protection changes
  • Race conditions: Other threads might execute during patch window
  • Crashes: If code executes during the brief writable window

Advanced: Conditional Patching

public class ConditionalPatch
{
    private readonly Patch _patch;
    private bool _enabled;
    
    public bool Enabled
    {
        get => _enabled;
        set
        {
            if (_enabled == value) return;
            
            if (value)
                _patch.Apply();
            else
                _patch.Remove();
            
            _enabled = value;
        }
    }
}

Advanced: Chain Hooking

Multiple hooks on the same function:

// First hook
var detour1 = magic.DetourManager.Detour(addr, Hook1);
detour1.Apply(out var trampoline1);

// Second hook (trampoline from first)
var detour2 = magic.DetourManager.Detour(
    trampoline1.TrampolineAddress,
    Hook2
);
detour2.Apply();

// Execution flow: Original → Hook2 → Hook1 → Trampoline1 → Original+5

Comparison to Other Libraries

Library Detours Patches Auto-restore In-process
WhiteMagic Required
BlackMagic N/A
MemorySharp (planned) (planned) N/A N/A
GreyMagic Required

Troubleshooting

"Detour failed: prologue too short"

Cause: Function prologue is shorter than minimum required (5 bytes for x86 jmp, 14 bytes for x64 push/ret).

Solution:

  • Use a different function
  • Patch deeper into the function (after prologue)
  • For x86, use hot-patch area (mov edi, edi padding)

"Patch failed: access violation"

Cause: Memory region is protected or invalid.

Solution:

  • Verify address is valid (use Magic.Memory.CanRead)
  • Ensure process has PROCESS_VM_OPERATION access
  • Check if anti-cheat is blocking writes

"Crash after detour"

Cause: Prologue validation failed or mid-instruction splice.

Solution:

  • Enable Iced backend for full validation
  • Pause threads during application
  • Check if target is using code obfuscation

"Hook not called"

Cause: Wrong function address or hook installed after target already called it.

Solution:

  • Verify address with debugger
  • Install hook early (before target uses function)
  • Check if target is using a different implementation (e.g., forwarded export)

Further Reading