- 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.
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.,
CreateFileWto 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, espmov 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
MainThreadPumpfor 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:
- Calls
VirtualProtectExto make region writable - Writes the patch bytes
- Calls
VirtualProtectExto 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, edipadding)
"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_OPERATIONaccess - 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
- Architecture — Hooking layer design
- Execution Models — Safe hook execution
- Memory Access — Reading/writing memory