- 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.
10 KiB
WhiteMagic Architecture
Overview
WhiteMagic is built as a layered architecture that provides multiple levels of abstraction over Windows process-introspection APIs. This design allows consumers to choose the right level of control for their use case, from low-level memory operations to high-level ergonomic APIs.
Layer Structure
┌──────────────────────────────────────────────────────────────┐
│ Magic (Facade) │
│ High-level entry point: Magic.Open(), Magic.OpenInProcess() │
└───────────────────────┬──────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
┌───────▼─────────┐ ┌────────▼────────┐
│ MemoryBase │ │ High-Level API │
│ (Abstract) │ │ │
├─────────────────┤ ├─────────────────┤
│ ExternalReader │ │ RemotePointer │
│ InProcessReader │ │ RemoteModule │
│ │ │ RemoteFunction │
│ ┌──────────────┐│ │ ManagedPeb/TEB │
│ │ Hooking ││ │ Window/Input │
│ │ DetourMgr ││ └─────────────────┘
│ │ PatchMgr ││
│ └──────────────┘│
└─────────┬───────┘
│
┌─────▼─────┬───────────┬───────────┐
│ │ │ │
┌───▼────┐ ┌────▼──┐ ┌──────▼──┐ ┌──────▼───┐
│Native │ │Discov.│ │Execution│ │Assembly │
│P/Invoke│ │Scan/PE│ │3-tier │ │IAssembler│
└────────┘ └───────┘ └─────────┘ └──────────┘
Core Layer
MemoryBase (Abstract)
The foundation of WhiteMagic is the MemoryBase abstract class, which defines the contract for all memory operations:
Key responsibilities:
- Abstract memory read/write operations
- Host for
MarshalCache<T>optimization - Owner of
PatchManagerandDetourManager - Relative/absolute addressing support
Design decision (D1): Dual-mode readers share the same abstract interface, allowing high-level code (pattern scanning, patching, etc.) to work in both external and in-process modes without changes.
ExternalReader
Implements MemoryBase for out-of-process operations:
Mechanism: Uses ReadProcessMemory/WriteProcessMemory via P/Invoke
Handle: SafeMemoryHandle to target process
Use case: Primary mode for automation hosts
InProcessReader
Implements MemoryBase for injected code:
Mechanism: Uses ReadProcessMemory/WriteProcessMemory on self-handle
Handle: SafeMemoryHandle to current process
Use case: Once a managed DLL is injected, enables delegate calls and detours
Design decision (D1 revision): InProcessReader uses RPM-on-self instead of unsafe direct pointer deref because .NET cannot catch AccessViolationException, so a bad deref kills the host with no soft-failure path. The in-process speed win moves to the delegate-call and detour paths, not the reader.
Discovery Layer
PatternScanner
Scans target memory for byte patterns with wildcard support:
Features:
- IDA-style hex patterns (
48 8B ? ? ? ? ?) - Optional caching to avoid repeated scans
- Module-relative or absolute addressing
PeHeaderParser
Parses PE headers to extract export information:
Features:
- Export table parsing
- Forwarder resolution (e.g.,
kernel32!HeapAlloc→NTDLL.RtlAllocateHeap) - Ordinal export support
Execution Layer (Three-Tier Model)
RemoteThreadExecutor
Mechanism: CreateRemoteThread + hand-assembled convention stubs
Use for: Thread-agnostic payloads (pure WinAPI, self-contained code, DLL injection)
Risk: NOT crash-safe for single-threaded target state
MainThreadPump
Mechanism: Detour on per-frame function + work queue Use for: State-sensitive calls (default for target-state access) Safety: Crash-safe — runs on target's own thread
How it works:
- Installs a detour on a per-frame function (e.g., D3D
EndScene) - Each frame, the hook drains a thread-safe queue
- Work items run synchronously in target's context
- Results/exceptions returned via
TaskCompletionSource
InProcessInvoker
Mechanism: Marshal.GetDelegateForFunctionPointer
Use for: In-process delegates after injection
Benefit: Zero thread-crossing overhead
Hooking Layer
DetourManager
Scope: In-process only Features:
- Inline
E9 jmpdetours over function prologues CallOriginalsupport via trampolineApply/Removeoperations- Auto-restore on
Dispose
Safety: Prologue validation via IAssembler.GetPrologueLength (optional Iced backend)
PatchManager
Scope: Both external and in-process Features:
- Named byte patches
Apply/Remove/IsApplied- Auto-restore on
Dispose
Assembly Layer
IAssembler Seam
Abstracts text → machine code generation:
Implementations:
StubAssembler(default): Hand-emits calling-convention stubs, no dependencyIcedAssembler(optional): Wraps Iced for arbitrary assembly
Design decision (D3): Keeps default configuration dependency-free; only callers needing arbitrary asm opt into Iced.
Convention Stubs
StubAssembler emits x86/x64 calling-convention trampolines:
Conventions supported: cdecl, stdcall, thiscall, fastcall, x64 (Microsoft) Encoding: Deterministic byte emission, no parsing required
High-Level Layer
RemotePointer
Indexer-based pointer arithmetic:
var ptr = magic[baseAddress];
int value = ptr.Read<int>(offset);
ptr.Write(999, offset);
RemoteModule/RemoteFunction
Module and export resolution:
var module = magic["user32"];
var fn = module["MessageBoxA"];
int result = fn.Execute<int>(CallConvention.Stdcall, args...);
ManagedPeb/ManagedTeb
Managed views of Process Environment Block and Thread Environment Block:
Features:
- Typed field access
- No manual structure marshalling
Window/Input
Window mutation and input simulation:
Features:
- Move/resize/title/activate/flash windows
- Keyboard/mouse input without focus (where supported)
Optimization Layer
MarshalCache
Per-type metadata caching:
Cached data:
Size(managed blittable width)MarshalSize(unmanaged interop width)TypeRequiresMarshal(needsMarshal.PtrToStructure)IsIntPtr(for special handling)
Performance: Avoids per-call reflection and Marshal.SizeOf overhead
Thread Safety
ExternalReader
Thread-safe: Yes (RPM/WPM are thread-safe) Synchronization: None required
InProcessReader
Thread-safe: Yes (RPM-on-self is thread-safe) Synchronization: None required
MainThreadPump
Thread-safe: Yes (uses ConcurrentQueue)
Synchronization: Lock-free queue + TaskCompletionSource
DetourManager/PatchManager
Thread-safe: No Synchronization: Caller must synchronize
Lifetime Management
All disposable resources follow RAII:
using var magic = Magic.Open(process);
// All handles, patches, and detours auto-restore on disposal
Resources cleaned up:
- Process handles (
SafeMemoryHandle) - Applied patches (
PatchManager) - Active detours (
DetourManager) - Allocated memory (
AllocatedMemory)
Error Handling
Strategy: Explicit failures, silent success
- Read operations: Throw on failure (Win32Exception)
- Write operations: Return
falseon failure - String reads: Return
string.Emptyon failure - Invalid handles: Throw
InvalidOperationException
Rationale: Writes to another process can legitimately fail (protection changed, process exited); silent retry is often the right strategy. Reads are typically expected to succeed, so exceptions surface the problem immediately.
Extensibility Points
IAssembler
Plug in custom assemblers:
public interface IAssembler
{
byte[] Assemble(string assemblyText, ulong origin = 0);
}
DetourManager.PrologueLengthResolver
Custom prologue validation:
magic.DetourManager.PrologueLengthResolver = (address, minBytes) =>
// Custom validation logic
return requiredLength;
Performance Characteristics
Memory Access
| Operation | Cost | Notes |
|---|---|---|
Read<T> (blittable) |
Low | MemoryMarshal.Read, no alloc |
Read<T> (marshalled) |
Medium | Marshal.PtrToStructure |
Read<T> (array) |
Medium-High | Per-element marshalling |
ReadBytes |
Low | Direct buffer copy |
ReadString |
Medium | Encoding + allocation |
Execution
| Method | Latency | Safety |
|---|---|---|
RemoteThreadExecutor |
~1-2ms | Thread-agnostic only |
MainThreadPump |
~1 frame (16-33ms) | Crash-safe |
InProcessInvoker |
<1μs | In-process only |
Pattern Scanning
| Mode | Cost | Notes |
|---|---|---|
| Uncached | High | Scans entire module |
| Cached | Low | O(1) after first scan |
Design Principles
- Safety by default: MainThreadPump prevents crashes from thread-affinity violations
- Bitness-agnostic: Works on x86 and x64 without code changes
- No native dependencies: Default configuration is pure managed
- Explicit operations: Clear failure modes, no hidden retries
- Resource safety: RAII-based cleanup prevents leaks
- Extensibility: Seam points for assemblers, validators
Further Reading
- Execution Models — Deep dive on the three-tier execution strategy
- Memory Access — MemoryBase readers and MarshalCache optimization
- Function Hooking — DetourManager and PatchManager internals
- Assembly Seam — IAssembler abstraction and Iced integration