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.
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
# 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 `PatchManager` and `DetourManager`
|
||||
- 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:**
|
||||
1. Installs a detour on a per-frame function (e.g., D3D `EndScene`)
|
||||
2. Each frame, the hook drains a thread-safe queue
|
||||
3. Work items run synchronously in target's context
|
||||
4. 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 jmp` detours over function prologues
|
||||
- `CallOriginal` support via trampoline
|
||||
- `Apply`/`Remove` operations
|
||||
- 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 dependency
|
||||
- `IcedAssembler` (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:
|
||||
|
||||
```csharp
|
||||
var ptr = magic[baseAddress];
|
||||
int value = ptr.Read<int>(offset);
|
||||
ptr.Write(999, offset);
|
||||
```
|
||||
|
||||
### RemoteModule/RemoteFunction
|
||||
|
||||
Module and export resolution:
|
||||
|
||||
```csharp
|
||||
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<T>
|
||||
|
||||
Per-type metadata caching:
|
||||
|
||||
**Cached data:**
|
||||
- `Size` (managed blittable width)
|
||||
- `MarshalSize` (unmanaged interop width)
|
||||
- `TypeRequiresMarshal` (needs `Marshal.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:
|
||||
|
||||
```csharp
|
||||
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 `false` on failure
|
||||
- **String reads:** Return `string.Empty` on 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:
|
||||
|
||||
```csharp
|
||||
public interface IAssembler
|
||||
{
|
||||
byte[] Assemble(string assemblyText, ulong origin = 0);
|
||||
}
|
||||
```
|
||||
|
||||
### DetourManager.PrologueLengthResolver
|
||||
|
||||
Custom prologue validation:
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
1. **Safety by default:** MainThreadPump prevents crashes from thread-affinity violations
|
||||
2. **Bitness-agnostic:** Works on x86 and x64 without code changes
|
||||
3. **No native dependencies:** Default configuration is pure managed
|
||||
4. **Explicit operations:** Clear failure modes, no hidden retries
|
||||
5. **Resource safety:** RAII-based cleanup prevents leaks
|
||||
6. **Extensibility:** Seam points for assemblers, validators
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Execution Models](./execution-models.md) — Deep dive on the three-tier execution strategy
|
||||
- [Memory Access](./memory-access.md) — MemoryBase readers and MarshalCache optimization
|
||||
- [Function Hooking](./hooking.md) — DetourManager and PatchManager internals
|
||||
- [Assembly Seam](./assembly-seam.md) — IAssembler abstraction and Iced integration
|
||||
@@ -0,0 +1,333 @@
|
||||
# Execution Models in WhiteMagic
|
||||
|
||||
WhiteMagic provides three distinct execution strategies, each designed for specific payload safety requirements. Choosing the right model is critical for avoiding crashes and ensuring reliable operation.
|
||||
|
||||
## The Problem: Thread-Affinity Crashes
|
||||
|
||||
When automating or debugging a target application, the most common failure mode is calling target functions on the wrong thread. Many applications (especially games, UI apps, and applications with scripting engines) have **single-threaded state**:
|
||||
|
||||
- Scripting VMs (Lua, Python, custom engines)
|
||||
- Render contexts (Direct3D, OpenGL)
|
||||
- Object models and state machines
|
||||
- UI message pumps
|
||||
|
||||
If you call these functions from a thread you created via `CreateRemoteThread`, they race the target's main thread → memory corruption → crash.
|
||||
|
||||
**WhiteMagic's solution:** Split execution by payload safety, making the crash-safe path the default.
|
||||
|
||||
---
|
||||
|
||||
## Model 1: RemoteThreadExecutor (CreateRemoteThread)
|
||||
|
||||
### Mechanism
|
||||
|
||||
Creates a new thread in the target process via `CreateRemoteThread`, executes a call stub, and waits for the exit code.
|
||||
|
||||
### Use Cases ✅
|
||||
|
||||
**SAFE for thread-agnostic payloads:**
|
||||
- Pure WinAPI calls (`GetTickCount`, `GetCurrentProcessId`)
|
||||
- Self-contained computations
|
||||
- `LoadLibrary` (DLL injection)
|
||||
- Code that touches only memory you own
|
||||
|
||||
### Avoid ❌
|
||||
|
||||
**UNSAFE for single-threaded target state:**
|
||||
- Scripting engine entry points
|
||||
- Render operations (Direct3D calls)
|
||||
- Game state queries/modifications
|
||||
- UI interactions
|
||||
- Object model traversal
|
||||
|
||||
### Usage Example
|
||||
|
||||
```csharp
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Assembly;
|
||||
|
||||
using var magic = Magic.Open(targetProcess);
|
||||
|
||||
// Example: Call GetTickCount (thread-safe WinAPI)
|
||||
var getTickCount = magic["kernel32"]["GetTickCount"];
|
||||
uint ticks = getTickCount.Execute<uint>(CallConvention.Stdcall);
|
||||
|
||||
// Example: Call a function that doesn't touch thread-local state
|
||||
int result = magic.RemoteThread.Execute<int>(
|
||||
functionAddress,
|
||||
CallConvention.Cdecl,
|
||||
arg1, arg2, arg3
|
||||
);
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
- **Latency:** ~1-2ms (thread creation + execution + join)
|
||||
- **Throughput:** Limited by thread creation overhead
|
||||
- **Best for:** One-shot calls, initialization, DLL injection
|
||||
|
||||
### Risks
|
||||
|
||||
- **Crash risk:** HIGH if touching single-threaded state
|
||||
- **Detection:** Easily detected by anti-cheat (new thread creation)
|
||||
- **Overhead:** Thread creation is not cheap
|
||||
|
||||
---
|
||||
|
||||
## Model 2: MainThreadPump (Crash-Safe) ⭐
|
||||
|
||||
### Mechanism
|
||||
|
||||
Installs a detour on a **per-frame function** (a function called every frame, like D3D's `EndScene`) and drains a thread-safe work queue there each frame.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Detour installation:** Hook a per-frame function at `frameAddress`
|
||||
2. **Queue enqueue:** Caller enqueues a delegate via `Enqueue<T>()`
|
||||
3. **Frame execution:** Target's main thread runs the hook each frame
|
||||
4. **Queue drain:** Hook checks queue, executes pending work items
|
||||
5. **Result return:** `TaskCompletionSource` delivers result/exception
|
||||
|
||||
### Use Cases ✅
|
||||
|
||||
**SAFE for state-sensitive calls:**
|
||||
- Game state modifications (health, position, inventory)
|
||||
- Scripting engine calls
|
||||
- UI interactions
|
||||
- Render operations
|
||||
- Object model traversal
|
||||
- **Any function that assumes main-thread context**
|
||||
|
||||
### Usage Example
|
||||
|
||||
```csharp
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Execution;
|
||||
|
||||
using var magic = Magic.Open(targetProcess);
|
||||
|
||||
// Create a pump that hooks a per-frame function
|
||||
// (e.g., D3D9 EndScene, or any per-frame handler)
|
||||
var pump = magic.CreateMainThreadPump(frameAddress);
|
||||
|
||||
// Enqueue work that runs on the target's main thread
|
||||
int health = await pump.Enqueue(() =>
|
||||
{
|
||||
// Safe to touch game state here
|
||||
return magic.Memory.Read<int>(healthAddress);
|
||||
});
|
||||
|
||||
// Modify game state safely
|
||||
await pump.Enqueue(() =>
|
||||
{
|
||||
magic.Memory.Write(healthAddress, 999);
|
||||
});
|
||||
|
||||
// Call a function that requires main-thread context
|
||||
var result = await pump.Enqueue(() =>
|
||||
{
|
||||
var fn = magic["target"]["ProcessInput"];
|
||||
return fn.Execute<int>(CallConvention.ThisCall, inputPtr);
|
||||
});
|
||||
```
|
||||
|
||||
### Finding a Frame Function
|
||||
|
||||
**Common per-frame functions:**
|
||||
- Direct3D 9: `EndScene` (device + 0x44 vtable entry)
|
||||
- Direct3D 11: Present callbacks
|
||||
- OpenGL: SwapBuffers callbacks
|
||||
- Custom: Many games have a `Update` or `Render` function per frame
|
||||
|
||||
**Helper for D3D9:**
|
||||
```csharp
|
||||
// Find D3D9 device and resolve EndScene
|
||||
IntPtr d3dDevice = FindD3D9Device(magic);
|
||||
IntPtr endScene = magic.Memory.Read<IntPtr>(d3dDevice + 0x44); // VTable
|
||||
|
||||
var pump = magic.CreateMainThreadPump(endScene);
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
- **Latency:** ~1 frame (16-33ms at 30-60 FPS)
|
||||
- **Throughput:** Limited by frame rate and work item duration
|
||||
- **Best for:** Repeated state-sensitive calls, game mods, automation
|
||||
|
||||
### Safety Features
|
||||
|
||||
- **Crash-safe:** Runs on target's own thread
|
||||
- **Exception propagation:** Exceptions in work items propagate to caller
|
||||
- **Timeout handling:** Can detect wedged work items
|
||||
- **Queue bounded:** Prevents unlimited queue growth
|
||||
|
||||
### Risks
|
||||
|
||||
- **Frame overhead:** Hook adds per-frame overhead (keep work items short)
|
||||
- **Wedged work item:** A stuck work item stalls the frame (detectable via timeout)
|
||||
- **Frame function needed:** Requires finding a per-frame function (application-specific)
|
||||
|
||||
---
|
||||
|
||||
## Model 3: InProcessInvoker (Direct Delegates)
|
||||
|
||||
### Mechanism
|
||||
|
||||
Once a managed DLL is injected into the target, creates native delegates via `Marshal.GetDelegateForFunctionPointer` and calls them directly.
|
||||
|
||||
### Use Cases ✅
|
||||
|
||||
**When injected in-process:**
|
||||
- Direct function calls with zero thread crossing
|
||||
- High-performance repeated calls
|
||||
- Full .NET interop capabilities
|
||||
|
||||
### Usage Example
|
||||
|
||||
```csharp
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Execution;
|
||||
|
||||
// Only works when injected in-process
|
||||
using var magic = Magic.OpenInProcess();
|
||||
|
||||
// Create a delegate to a native function
|
||||
var getName = magic.Memory.CreateFunction<GetNameDelegate>(
|
||||
getNameAddress
|
||||
);
|
||||
|
||||
// Call directly as a delegate
|
||||
string name = getName(12345);
|
||||
|
||||
// Delegate signature
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate string GetNameDelegate(int id);
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
- **After injection:** Your managed DLL is already in the target
|
||||
- **Performance-critical:** Need sub-microsecond call latency
|
||||
- **Complex interop:** Need to pass complex structures or callbacks
|
||||
|
||||
### Performance
|
||||
|
||||
- **Latency:** <1μs (direct function call)
|
||||
- **Throughput:** Highest (no thread crossing)
|
||||
- **Best for:** In-process tools, profilers, injected helpers
|
||||
|
||||
### Limitations
|
||||
|
||||
- **In-process only:** Requires managed DLL injection
|
||||
- **No crash-safety benefit:** Still subject to thread-affinity issues
|
||||
- **Requires loader:** Need a CLR host or injection bootstrapper
|
||||
|
||||
---
|
||||
|
||||
## Choosing the Right Model
|
||||
|
||||
### Decision Flowchart
|
||||
|
||||
```
|
||||
Are you injected in-process?
|
||||
├─ Yes → Use InProcessInvoker (direct delegates)
|
||||
└─ No → Does the call touch single-threaded state?
|
||||
├─ Yes → Use MainThreadPump (crash-safe)
|
||||
└─ No → Use RemoteThreadExecutor (CreateRemoteThread)
|
||||
```
|
||||
|
||||
### Practical Guidelines
|
||||
|
||||
| Scenario | Model | Reason |
|
||||
|----------|-------|--------|
|
||||
| DLL injection | RemoteThreadExecutor | `LoadLibrary` is thread-safe |
|
||||
| Read health bar | MainThreadPump | Game state is main-thread-affine |
|
||||
| Call GetTickCount | RemoteThreadExecutor | WinAPI, no thread affinity |
|
||||
| Script engine call | MainThreadPump | Script VM is main-thread-only |
|
||||
| Injected profiling | InProcessInvoker | Already in-process, max performance |
|
||||
| Window mutation | RemoteThreadExecutor | WinAPI `SetWindowPos` is thread-safe |
|
||||
|
||||
### Common Mistakes
|
||||
|
||||
❌ **Wrong:** Using `RemoteThreadExecutor` for game state reads
|
||||
```csharp
|
||||
// UNSAFE: Will crash in most games
|
||||
int health = magic.RemoteThread.Execute<int>(
|
||||
readHealthFn,
|
||||
CallConvention.Cdecl
|
||||
);
|
||||
```
|
||||
|
||||
✅ **Right:** Using `MainThreadPump` for game state
|
||||
```csharp
|
||||
// SAFE: Runs on game's main thread
|
||||
int health = await pump.Enqueue(() =>
|
||||
magic.Memory.Read<int>(healthAddress)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comparison Summary
|
||||
|
||||
| Feature | RemoteThreadExecutor | MainThreadPump | InProcessInvoker |
|
||||
|---------|---------------------|----------------|------------------|
|
||||
| **Safety** | Thread-agnostic only | Crash-safe | In-process context |
|
||||
| **Latency** | ~1-2ms | ~1 frame (16-33ms) | <1μs |
|
||||
| **Detection risk** | High (new thread) | Low (detour) | None (in-process) |
|
||||
| **Best for** | One-shot calls, DLL injection | State-sensitive calls | In-process tools |
|
||||
| **Setup cost** | Low | Medium (need frame fn) | High (need injection) |
|
||||
| **Throughput** | Low | Medium | Highest |
|
||||
|
||||
---
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
### Bypassing Anti-Cheat
|
||||
|
||||
**RemoteThreadExecutor** is easily detected (new thread creation). For stealth:
|
||||
|
||||
1. **Use MainThreadPump:** Detours are harder to detect than thread creation
|
||||
2. **Thread hijacking:** For one-shot calls, hijack an existing thread (see `CodeInjector`)
|
||||
|
||||
### Combining Models
|
||||
|
||||
```csharp
|
||||
// Use RemoteThreadExecutor to inject DLL
|
||||
magic.RemoteThread.Execute<IntPtr>(
|
||||
loadLibraryAddress,
|
||||
CallConvention.Stdcall,
|
||||
dllPathPtr
|
||||
);
|
||||
|
||||
// Now injected, switch to InProcessInvoker
|
||||
using var inProcess = Magic.OpenInProcess();
|
||||
var fn = inProcess.Memory.CreateFunction<MyDelegate>(address);
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
int result = await pump.Enqueue(() =>
|
||||
magic.Memory.Read<int>(address)
|
||||
);
|
||||
}
|
||||
catch (AccessViolationException)
|
||||
{
|
||||
// Address not readable
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
// Work item wedged (stuck the frame)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Architecture](./architecture.md) — Overall system design
|
||||
- [Function Hooking](./hooking.md) — DetourManager internals
|
||||
- [Memory Access](./memory-access.md) — MemoryBase and MarshalCache
|
||||
+387
@@ -0,0 +1,387 @@
|
||||
# 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
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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.**
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
// 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:
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
- [Architecture](./architecture.md) — Hooking layer design
|
||||
- [Execution Models](./execution-models.md) — Safe hook execution
|
||||
- [Memory Access](./memory-access.md) — Reading/writing memory
|
||||
@@ -0,0 +1,481 @@
|
||||
# Memory Access in WhiteMagic
|
||||
|
||||
WhiteMagic provides a dual memory-access model through an abstract `MemoryBase` class, supporting both external (out-of-process) and in-process readers with optimized typed I/O.
|
||||
|
||||
## MemoryBase Architecture
|
||||
|
||||
### Abstract Interface
|
||||
|
||||
`MemoryBase` defines the contract for all memory operations:
|
||||
|
||||
```csharp
|
||||
public abstract class MemoryBase : IDisposable
|
||||
{
|
||||
// Abstract raw I/O
|
||||
public abstract byte[] ReadBytes(IntPtr address, int count, bool isRelative = false);
|
||||
public abstract int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false);
|
||||
|
||||
// Typed I/O
|
||||
public T Read<T>(IntPtr address, bool isRelative = false) where T : struct;
|
||||
public bool Write<T>(IntPtr address, T value, bool isRelative = false) where T : struct;
|
||||
|
||||
// Array I/O
|
||||
public T[] Read<T>(IntPtr address, int count, bool isRelative = false) where T : struct;
|
||||
public bool Write<T>(IntPtr address, T[] values, bool isRelative = false) where T : struct;
|
||||
|
||||
// String I/O
|
||||
public string ReadString(IntPtr address, Encoding encoding, int maxLength = 512);
|
||||
public bool WriteString(IntPtr address, string value, Encoding encoding);
|
||||
}
|
||||
```
|
||||
|
||||
### Concrete Implementations
|
||||
|
||||
#### ExternalReader
|
||||
|
||||
**Mechanism:** `ReadProcessMemory`/`WriteProcessMemory` via P/Invoke
|
||||
**Handle:** `SafeMemoryHandle` to target process
|
||||
**Use case:** Primary mode for automation hosts
|
||||
|
||||
```csharp
|
||||
using var magic = Magic.Open(targetProcess);
|
||||
// Uses ExternalReader internally
|
||||
```
|
||||
|
||||
#### InProcessReader
|
||||
|
||||
**Mechanism:** `ReadProcessMemory`/`WriteProcessMemory` on self-handle
|
||||
**Handle:** `SafeMemoryHandle` to current process
|
||||
**Use case:** Injected code, delegate calls, detours
|
||||
|
||||
**Design decision:** 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.
|
||||
|
||||
```csharp
|
||||
using var magic = Magic.OpenInProcess();
|
||||
// Uses InProcessReader internally
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typed I/O with MarshalCache<T>
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
`MarshalCache<T>` eliminates per-call reflection overhead by caching type metadata once:
|
||||
|
||||
```csharp
|
||||
public static class MarshalCache<T>
|
||||
{
|
||||
// Cached at static-constructor time
|
||||
public static readonly int Size; // Managed blittable width
|
||||
public static readonly int MarshalSize; // Unmanaged interop width
|
||||
public static readonly bool TypeRequiresMarshal; // Needs PtrToStructure
|
||||
public static readonly bool IsIntPtr; // Special handling for IntPtr
|
||||
}
|
||||
```
|
||||
|
||||
### Blittable vs Marshalled Types
|
||||
|
||||
**Blittable types** (no marshaling needed):
|
||||
- Primitives: `int`, `byte`, `float`, `double`, `bool` (1 byte managed)
|
||||
- Enums (if underlying type is blittable)
|
||||
- Structs containing only blittable fields
|
||||
- **Performance:** `MemoryMarshal.Read<T>` — zero allocation
|
||||
|
||||
**Marshalled types** (require `Marshal.PtrToStructure`):
|
||||
- `bool` (4 bytes in Win32 interop)
|
||||
- `char` (2 bytes ANSI marshaling)
|
||||
- Structs with `[MarshalAs]` attributes
|
||||
- Structs with inline `ByValTStr`/`ByValArray`
|
||||
- **Performance:** `Marshal.PtrToStructure` — allocates temporary copy
|
||||
|
||||
### Size vs MarshalSize
|
||||
|
||||
For most types, `Size == MarshalSize`. They differ when:
|
||||
|
||||
```csharp
|
||||
// Example: bool field
|
||||
struct MyStruct
|
||||
{
|
||||
public bool Flag; // 1 byte managed, 4 bytes Win32 BOOL
|
||||
public int Value;
|
||||
}
|
||||
|
||||
MarshalCache<MyStruct>.Size == 5; // Managed layout
|
||||
MarshalCache<MyStruct>.MarshalSize == 8; // Win32 BOOL is 4 bytes
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```csharp
|
||||
using var magic = Magic.Open(process);
|
||||
|
||||
// Blittable read (fast path)
|
||||
int health = magic.Memory.Read<int>(address);
|
||||
|
||||
// Marshalled read (slow path)
|
||||
var gameState = magic.Memory.Read<MyStruct>(address);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Addressing Modes
|
||||
|
||||
### Absolute Addressing (Default)
|
||||
|
||||
```csharp
|
||||
// Read at absolute address 0x12345678
|
||||
int value = magic.Memory.Read<int>(0x12345678);
|
||||
```
|
||||
|
||||
### Relative Addressing
|
||||
|
||||
Relative to module base (useful for ASRR):
|
||||
|
||||
```csharp
|
||||
// Read at module_base + 0x1000
|
||||
int value = magic.Memory.Read<int>(0x1000, isRelative: true);
|
||||
// Equivalent to:
|
||||
int value = magic.Memory.Read<int>(magic.Memory.ImageBase + 0x1000);
|
||||
```
|
||||
|
||||
### Using RemotePointer
|
||||
|
||||
The `RemotePointer` indexer provides fluent relative addressing:
|
||||
|
||||
```csharp
|
||||
var basePtr = magic[moduleBase];
|
||||
int offset1 = basePtr.Read<int>(0x1000);
|
||||
int offset2 = basePtr.Read<int>(0x2000);
|
||||
|
||||
// Chained offsets
|
||||
var nestedPtr = magic[basePtr.Read<IntPtr>(0x1000)];
|
||||
int value = nestedPtr.Read<int>(0x50);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## String I/O
|
||||
|
||||
### Reading Strings
|
||||
|
||||
```csharp
|
||||
// Read null-terminated ANSI string
|
||||
string ansi = magic.Memory.ReadString(
|
||||
address,
|
||||
Encoding.ASCII,
|
||||
maxLength: 256
|
||||
);
|
||||
|
||||
// Read null-terminated UTF-16 string
|
||||
string unicode = magic.Memory.ReadString(
|
||||
address,
|
||||
Encoding.Unicode,
|
||||
maxLength: 512
|
||||
);
|
||||
```
|
||||
|
||||
**Implementation:** Reads byte-by-byte until null terminator or `maxLength`.
|
||||
|
||||
### Writing Strings
|
||||
|
||||
```csharp
|
||||
// Write null-terminated string
|
||||
bool success = magic.Memory.WriteString(
|
||||
address,
|
||||
"Hello World",
|
||||
Encoding.ASCII
|
||||
);
|
||||
```
|
||||
|
||||
**Implementation:** Writes bytes + null terminator.
|
||||
|
||||
---
|
||||
|
||||
## Array I/O
|
||||
|
||||
### Reading Arrays
|
||||
|
||||
```csharp
|
||||
// Read 10 integers
|
||||
int[] values = magic.Memory.Read<int>(address, 10);
|
||||
|
||||
// Read struct array
|
||||
var enemies = magic.Memory.Read<EnemyStruct>(enemyListPtr, 50);
|
||||
```
|
||||
|
||||
**Performance:**
|
||||
- Blittable: `MemoryMarshal.Read<T>` in a loop (fast)
|
||||
- Marshalled: `Marshal.PtrToStructure<T>` per element (slow)
|
||||
|
||||
### Writing Arrays
|
||||
|
||||
```csharp
|
||||
// Write integer array
|
||||
int[] values = { 1, 2, 3, 4, 5 };
|
||||
magic.Memory.Write(address, values);
|
||||
|
||||
// Write struct array
|
||||
EnemyStruct[] enemies = GetEnemies();
|
||||
magic.Memory.Write(enemyListPtr, enemies);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Raw Byte I/O
|
||||
|
||||
### Reading Bytes
|
||||
|
||||
```csharp
|
||||
// Read 100 bytes
|
||||
byte[] buffer = magic.Memory.ReadBytes(address, 100);
|
||||
|
||||
// Read with relative addressing
|
||||
byte[] code = magic.Memory.ReadBytes(offset, count, isRelative: true);
|
||||
```
|
||||
|
||||
### Writing Bytes
|
||||
|
||||
```csharp
|
||||
// Write byte array
|
||||
byte[] patchBytes = { 0x90, 0x90, 0x90 }; // NOP x3
|
||||
int written = magic.Memory.WriteBytes(address, patchBytes);
|
||||
|
||||
// Write with relative addressing
|
||||
int written = magic.Memory.WriteBytes(
|
||||
offset,
|
||||
new byte[] { 0x01, 0x02, 0x03 },
|
||||
isRelative: true
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Read Operations
|
||||
|
||||
**Strategy:** Explicit failures → throw exceptions
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
int value = magic.Memory.Read<int>(address);
|
||||
}
|
||||
catch (Win32Exception ex)
|
||||
{
|
||||
// ReadProcessMemory failed (access violation, process exited, etc.)
|
||||
Console.WriteLine($"Read failed: {ex.Message}");
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
// Process handle is closed
|
||||
Console.WriteLine($"Process not open: {ex.Message}");
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:** `default(T)` if fewer bytes read than expected (e.g., partial read).
|
||||
|
||||
### Write Operations
|
||||
|
||||
**Strategy:** Silent failures → return `false`
|
||||
|
||||
```csharp
|
||||
bool success = magic.Memory.Write(address, 999);
|
||||
if (!success)
|
||||
{
|
||||
// Handle failure (retry, log, etc.)
|
||||
}
|
||||
```
|
||||
|
||||
**Reason:** Writes to another process can legitimately fail (protection changed, process exited); silent retry is often the right strategy.
|
||||
|
||||
### String Operations
|
||||
|
||||
**Read:** Returns `string.Empty` on failure.
|
||||
|
||||
```csharp
|
||||
string text = magic.Memory.ReadString(address, Encoding.ASCII);
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
// Read failed or string is empty
|
||||
}
|
||||
```
|
||||
|
||||
**Write:** Returns `false` on failure.
|
||||
|
||||
---
|
||||
|
||||
## Thread Safety
|
||||
|
||||
### ExternalReader
|
||||
|
||||
**Thread-safe:** Yes
|
||||
|
||||
`ReadProcessMemory`/`WriteProcessMemory` are thread-safe at the OS level. No synchronization required.
|
||||
|
||||
### InProcessReader
|
||||
|
||||
**Thread-safe:** Yes
|
||||
|
||||
RPM-on-self is thread-safe. No synchronization required.
|
||||
|
||||
### High-Level Access
|
||||
|
||||
**Thread-safe:** Depends on usage
|
||||
|
||||
```csharp
|
||||
// SAFE: Concurrent reads from multiple threads
|
||||
int v1 = magic.Memory.Read<int>(addr1);
|
||||
int v2 = magic.Memory.Read<int>(addr2);
|
||||
|
||||
// SAFE: Same address, concurrent reads (no race, just inconsistent value)
|
||||
int v3 = magic.Memory.Read<int>(addr);
|
||||
int v4 = magic.Memory.Read<int>(addr);
|
||||
|
||||
// UNSAFE: Concurrent writes (last write wins, no atomicity)
|
||||
magic.Memory.Write(addr, 1); // Thread 1
|
||||
magic.Memory.Write(addr, 2); // Thread 2 (may win)
|
||||
```
|
||||
|
||||
For atomic read-modify-write, use `MainThreadPump` or implement locking.
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
| Operation | Cost | Notes |
|
||||
|-----------|------|-------|
|
||||
| `Read<T>` (blittable) | Low | `MemoryMarshal.Read`, no alloc |
|
||||
| `Read<T>` (marshalled) | Medium | `Marshal.PtrToStructure` + alloc |
|
||||
| `Read<T>` (array, blittable) | Medium | Per-element `MemoryMarshal.Read` |
|
||||
| `Read<T>` (array, marshalled) | High | Per-element marshalling + alloc |
|
||||
| `ReadBytes` | Low | Direct buffer copy |
|
||||
| `ReadString` | Medium | Byte-by-byte + encoding + alloc |
|
||||
| `Write<T>` (blittable) | Low | `MemoryMarshal.Write` + buffer alloc |
|
||||
| `Write<T>` (marshalled) | Medium | `Marshal.StructureToPtr` + buffer alloc |
|
||||
| `WriteBytes` | Low | Direct buffer copy |
|
||||
| `WriteString` | Medium | Encoding + null terminator + buffer alloc |
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
1. **Prefer blittable types:** Use `int` instead of `bool` where possible
|
||||
2. **Batch reads:** Read arrays instead of individual elements
|
||||
3. **Reuse buffers:** For repeated reads, reuse byte arrays
|
||||
4. **Cache offsets:** Compute addresses once, reuse them
|
||||
5. **Use MarshalCache:** Automatic via `Read<T>`/`Write<T>`
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern: Reading a Nested Structure
|
||||
|
||||
```csharp
|
||||
// Assume structure: GameManager -> PlayerList -> Player[i] -> Health
|
||||
IntPtr gameManagerPtr = magic.Memory.ImageBase + 0x1000;
|
||||
IntPtr playerListPtr = magic.Memory.Read<IntPtr>(gameManagerPtr + 0x20);
|
||||
IntPtr playerPtr = magic.Memory.Read<IntPtr>(playerListPtr + (playerIndex * 8));
|
||||
int health = magic.Memory.Read<int>(playerPtr + 0x4);
|
||||
```
|
||||
|
||||
**Using RemotePointer:**
|
||||
|
||||
```csharp
|
||||
int health = magic[gameManagerPtr]
|
||||
.Read<IntPtr>(0x20) // PlayerList
|
||||
.Let(ptr => magic[ptr]
|
||||
.Read<IntPtr>(playerIndex * 8)) // Player
|
||||
.Let(ptr => magic[ptr]
|
||||
.Read<int>(0x4)); // Health
|
||||
```
|
||||
|
||||
### Pattern: Scanning for a Value
|
||||
|
||||
```csharp
|
||||
// Scan memory region for a specific value
|
||||
IntPtr found = IntPtr.Zero;
|
||||
byte[] region = magic.Memory.ReadBytes(baseAddr, size);
|
||||
|
||||
for (int i = 0; i < region.Length - 4; i++)
|
||||
{
|
||||
int value = BitConverter.ToInt32(region, i);
|
||||
if (value == targetValue)
|
||||
{
|
||||
found = baseAddr + i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Better:** Use `PatternScanner` (see [Discovery](./discovery.md)).
|
||||
|
||||
### Pattern: Safe Retry Loop
|
||||
|
||||
```csharp
|
||||
// Retry write with exponential backoff
|
||||
int attempts = 0;
|
||||
bool success = false;
|
||||
while (attempts < 5 && !success)
|
||||
{
|
||||
success = magic.Memory.Write(address, value);
|
||||
if (!success)
|
||||
{
|
||||
attempts++;
|
||||
Thread.Sleep(100 * (1 << attempts)); // 100ms, 200ms, 400ms, ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Read returns default value"
|
||||
|
||||
**Possible causes:**
|
||||
1. Address is invalid
|
||||
2. Process has exited
|
||||
3. Memory protection doesn't allow read
|
||||
4. Fewer bytes read than expected (partial read)
|
||||
|
||||
**Solutions:**
|
||||
- Verify address with debugger
|
||||
- Check `magic.Memory.Handle.IsInvalid`
|
||||
- Use `CanRead` helper (if available)
|
||||
- Validate `ReadBytes` length
|
||||
|
||||
### "Write returns false"
|
||||
|
||||
**Possible causes:**
|
||||
1. Memory protection is read-only
|
||||
2. Process has exited
|
||||
3. Address is invalid
|
||||
4. Anti-cheat blocking writes
|
||||
|
||||
**Solutions:**
|
||||
- Verify address with debugger
|
||||
- Check memory protection (`VirtualQueryEx`)
|
||||
- Ensure process has `PROCESS_VM_WRITE` access
|
||||
- Retry after `VirtualProtectEx` (if you have rights)
|
||||
|
||||
### "Performance is slow"
|
||||
|
||||
**Possible causes:**
|
||||
1. Reading individual elements in a loop
|
||||
2. Using marshalled types extensively
|
||||
3. Small reads/writes (not batching)
|
||||
|
||||
**Solutions:**
|
||||
- Read arrays instead of loops
|
||||
- Use blittable types where possible
|
||||
- Batch reads/writes
|
||||
- Cache frequently accessed values
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Architecture](./architecture.md) — MemoryBase layer design
|
||||
- [Discovery](./discovery.md) — Pattern scanning and PE parsing
|
||||
- [Execution Models](./execution-models.md) — Safe execution in target process
|
||||
@@ -0,0 +1,579 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
This guide covers common issues, errors, and solutions when using WhiteMagic.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Process Access Issues](#process-access-issues)
|
||||
- [Memory Operation Failures](#memory-operation-failures)
|
||||
- [Execution Errors](#execution-errors)
|
||||
- [Hooking Problems](#hooking-problems)
|
||||
- [Pattern Scanning Issues](#pattern-scanning-issues)
|
||||
- [Performance Problems](#performance-problems)
|
||||
- [Crash and Stability Issues](#crash-and-stability-issues)
|
||||
- [Build and Compilation Errors](#build-and-compilation-errors)
|
||||
|
||||
---
|
||||
|
||||
## Process Access Issues
|
||||
|
||||
### "Process is not open for read/write"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
System.InvalidOperationException: Process is not open for read/write.
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
1. Process has exited
|
||||
2. Process handle is closed
|
||||
3. Insufficient permissions
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// Check process state
|
||||
if (magic.Memory.Handle.IsInvalid || magic.Memory.Handle.IsClosed)
|
||||
{
|
||||
Console.WriteLine("Process handle is invalid or closed");
|
||||
}
|
||||
|
||||
// Re-open the process
|
||||
using var magic = Magic.Open(process);
|
||||
```
|
||||
|
||||
**Prevention:**
|
||||
- Keep `Process` object alive
|
||||
- Don't dispose `Magic` while using it
|
||||
- Use `using` statement for automatic cleanup
|
||||
|
||||
### "OpenProcess failed: Access Denied"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Process.Open returns false, or Magic.Open throws Win32Exception
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
1. Insufficient privileges (not running as administrator)
|
||||
2. Target process is protected (anti-cheat, system process)
|
||||
3. 32-bit/64-bit mismatch
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// Run as administrator
|
||||
// Right-click → Run as Administrator
|
||||
|
||||
// Check process bitness matches host
|
||||
if (Environment.Is64BitProcess != Is64BitProcess(targetProcess))
|
||||
{
|
||||
Console.WriteLine("Bitness mismatch between host and target");
|
||||
}
|
||||
```
|
||||
|
||||
**Prevention:**
|
||||
- Run with admin privileges
|
||||
- Check target process protection level
|
||||
- Ensure bitness compatibility
|
||||
|
||||
---
|
||||
|
||||
## Memory Operation Failures
|
||||
|
||||
### "Read returns default value"
|
||||
|
||||
**Symptom:**
|
||||
```csharp
|
||||
int value = magic.Memory.Read<int>(address);
|
||||
// value is 0 (or default), but expected different value
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
1. Address is invalid
|
||||
2. Memory protection prevents reading
|
||||
3. Fewer bytes read than expected
|
||||
4. Process has exited
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// Verify address with debugger
|
||||
// Check if address is readable
|
||||
byte[] test = magic.Memory.ReadBytes(address, 4);
|
||||
if (test.Length < 4)
|
||||
{
|
||||
Console.WriteLine("Cannot read from address");
|
||||
}
|
||||
|
||||
// Validate handle
|
||||
if (magic.Memory.Handle.IsInvalid)
|
||||
{
|
||||
Console.WriteLine("Process handle is invalid");
|
||||
}
|
||||
```
|
||||
|
||||
### "Write returns false"
|
||||
|
||||
**Symptom:**
|
||||
```csharp
|
||||
bool success = magic.Memory.Write(address, value);
|
||||
if (!success) { /* Write failed */ }
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
1. Memory is read-only (e.g., `.text` section)
|
||||
2. Address is invalid
|
||||
3. Process has exited
|
||||
4. Anti-cheat blocking writes
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// Retry with delay
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
if (magic.Memory.Write(address, value))
|
||||
break;
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
|
||||
// Check if memory is protected
|
||||
// Use VirtualProtectEx if you have rights (advanced)
|
||||
```
|
||||
|
||||
**For code patches, use `PatchManager` instead:**
|
||||
```csharp
|
||||
var patch = magic.PatchManager.Create("Patch", address, bytes);
|
||||
patch.Apply(); // Handles VirtualProtect automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Execution Errors
|
||||
|
||||
### "CreateRemoteThread failed"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Win32Exception: CreateRemoteThread failed
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
1. Target process is protected
|
||||
2. Insufficient permissions
|
||||
3. Target process has exited
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// Ensure process is still running
|
||||
if (process.HasExited)
|
||||
{
|
||||
Console.WriteLine("Process has exited");
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
// Run as administrator
|
||||
```
|
||||
|
||||
### "Crash when calling target function via RemoteThreadExecutor"
|
||||
|
||||
**Symptom:** Target application crashes after `RemoteThreadExecutor.Execute<T>`
|
||||
|
||||
**Cause:** Calling single-threaded function from remote thread (thread-affinity violation)
|
||||
|
||||
**Solution:** Use `MainThreadPump` instead:
|
||||
|
||||
```csharp
|
||||
// WRONG (crashes game):
|
||||
int health = magic.RemoteThread.Execute<int>(
|
||||
readHealthFn,
|
||||
CallConvention.Cdecl
|
||||
);
|
||||
|
||||
// RIGHT (crash-safe):
|
||||
var pump = magic.CreateMainThreadPump(frameAddress);
|
||||
int health = await pump.Enqueue(() =>
|
||||
magic.Memory.Read<int>(healthAddress)
|
||||
);
|
||||
```
|
||||
|
||||
**Explanation:** Game state, scripting engines, and render contexts are often main-thread-only. Use `MainThreadPump` for these.
|
||||
|
||||
### "MainThreadPump work item wedged"
|
||||
|
||||
**Symptom:** `Enqueue<T>()` hangs or times out
|
||||
|
||||
**Cause:** Work item threw exception or entered infinite loop
|
||||
|
||||
**Solution:**
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
int result = await pump.Enqueue(() =>
|
||||
{
|
||||
// Keep work items short!
|
||||
return magic.Memory.Read<int>(address);
|
||||
}, TimeSpan.FromSeconds(1)); // Add timeout
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
Console.WriteLine("Work item wedged (stuck the frame)");
|
||||
}
|
||||
```
|
||||
|
||||
**Prevention:**
|
||||
- Keep pump work items short (< 1ms)
|
||||
- Avoid blocking calls in work items
|
||||
- Use timeout on `Enqueue<T>`
|
||||
|
||||
---
|
||||
|
||||
## Hooking Problems
|
||||
|
||||
### "Detour failed: prologue too short"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
InvalidOperationException: Prologue is too short for detour (minimum 5 bytes required)
|
||||
```
|
||||
|
||||
**Cause:** Function prologue is shorter than minimum required for jmp instruction
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// Option 1: Hook a different function
|
||||
var detour = magic.DetourManager.Detour(alternativeAddress, hook);
|
||||
|
||||
// Option 2: Patch deeper into function (after prologue)
|
||||
// (Advanced, risky)
|
||||
|
||||
// Option 3: For WinAPI, use hot-patch area
|
||||
if (IsHotPatchPadded(functionAddress))
|
||||
{
|
||||
// Can use 2-byte jmp at [address-2]
|
||||
}
|
||||
```
|
||||
|
||||
### "Patch failed: access violation"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
AccessViolationException when applying patch
|
||||
```
|
||||
|
||||
**Cause:** Memory is protected or invalid
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// Verify address is valid
|
||||
byte[] test = magic.Memory.ReadBytes(address, 1);
|
||||
|
||||
// Use PatchManager (handles VirtualProtect automatically)
|
||||
var patch = magic.PatchManager.Create("Patch", address, bytes);
|
||||
patch.Apply();
|
||||
```
|
||||
|
||||
### "Hook not called"
|
||||
|
||||
**Symptom:** Detour applied successfully, but hook delegate never executes
|
||||
|
||||
**Causes:**
|
||||
1. Wrong function address
|
||||
2. Target already called function before hook installed
|
||||
3. Target is using a different implementation (e.g., forwarded export)
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// Verify address with debugger
|
||||
// Install hook early (before target uses function)
|
||||
// Check for forwarded exports
|
||||
var fn = magic["module"]["function"];
|
||||
Console.WriteLine($"Function address: {fn.Address}");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern Scanning Issues
|
||||
|
||||
### "FindPattern returns IntPtr.Zero"
|
||||
|
||||
**Symptom:** Pattern scan finds no matches
|
||||
|
||||
**Causes:**
|
||||
1. Pattern is incorrect
|
||||
2. Module not loaded
|
||||
3. Memory layout changed (ASLR, update)
|
||||
4. Pattern wildcards too broad
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// Verify pattern syntax
|
||||
// Use IDA-style: "48 8B ? ? ? ? ?"
|
||||
// ? = wildcard byte
|
||||
|
||||
// Narrow search range
|
||||
// Scan only relevant module, not entire memory
|
||||
|
||||
// Check if module is loaded
|
||||
var module = magic["moduleName"];
|
||||
if (module.BaseAddress == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Module not loaded");
|
||||
}
|
||||
|
||||
// Test with known pattern
|
||||
var test = magic.Memory.FindPattern("48 8B 05 ? ? ? ?", module.BaseAddress, 0x1000);
|
||||
```
|
||||
|
||||
### "Pattern scanner is slow"
|
||||
|
||||
**Symptom:** `FindPattern` takes several seconds
|
||||
|
||||
**Cause:** Scanning large memory regions without cache
|
||||
|
||||
**Solution:**
|
||||
```csharp
|
||||
// Use pattern cache (if available in your version)
|
||||
// Or cache results manually
|
||||
private static readonly Dictionary<string, IntPtr> PatternCache = new();
|
||||
|
||||
IntPtr FindPatternCached(string pattern, IntPtr baseAddr, int size)
|
||||
{
|
||||
string key = $"{pattern}_{baseAddr}_{size}";
|
||||
if (PatternCache.TryGetValue(key, out var cached))
|
||||
return cached;
|
||||
|
||||
IntPtr result = magic.Memory.FindPattern(pattern, baseAddr, size);
|
||||
PatternCache[key] = result;
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Problems
|
||||
|
||||
### "Memory reads are slow"
|
||||
|
||||
**Symptom:** `Read<T>` takes several milliseconds
|
||||
|
||||
**Causes:**
|
||||
1. Reading individual elements in a loop
|
||||
2. Using marshalled types extensively
|
||||
3. Target process is heavily loaded
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// WRONG: Slow loop
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
values[i] = magic.Memory.Read<int>(baseAddr + i * 4);
|
||||
}
|
||||
|
||||
// RIGHT: Batch read
|
||||
values = magic.Memory.Read<int>(baseAddr, 1000);
|
||||
|
||||
// RIGHT: Use blittable types
|
||||
// Use int instead of bool where possible
|
||||
```
|
||||
|
||||
### "High CPU usage"
|
||||
|
||||
**Symptom:** WhiteMagic causes high CPU in target process
|
||||
|
||||
**Cause:** Polling loops, tight read loops in pump
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// WRONG: Tight loop in pump
|
||||
while (true)
|
||||
{
|
||||
int health = magic.Memory.Read<int>(healthAddr);
|
||||
if (health == 0) break;
|
||||
}
|
||||
|
||||
// RIGHT: Event-driven or throttled
|
||||
// Use MainThreadPump with delays
|
||||
// Or poll at reasonable interval (e.g., 60 Hz)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Crash and Stability Issues
|
||||
|
||||
### "Target crashes when attached"
|
||||
|
||||
**Symptom:** Target application crashes shortly after `Magic.Open`
|
||||
|
||||
**Cause:** Thread-affinity violation (calling function from wrong thread)
|
||||
|
||||
**Solution:** Use `MainThreadPump` for state-sensitive calls:
|
||||
|
||||
```csharp
|
||||
// Identify the crash-safe execution model
|
||||
var pump = magic.CreateMainThreadPump(frameAddress);
|
||||
|
||||
// All state-sensitive calls go through pump
|
||||
await pump.Enqueue(() => { /* safe code */ });
|
||||
```
|
||||
|
||||
### "Random crashes during operation"
|
||||
|
||||
**Symptom:** Intermittent crashes, hard to reproduce
|
||||
|
||||
**Causes:**
|
||||
1. Race conditions (concurrent memory access)
|
||||
2. Anti-cheat interference
|
||||
3. Memory protection changes mid-operation
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// Add synchronization
|
||||
lock (syncLock)
|
||||
{
|
||||
magic.Memory.Write(address, value);
|
||||
}
|
||||
|
||||
// Handle access violations gracefully
|
||||
try
|
||||
{
|
||||
magic.Memory.Read<int>(address);
|
||||
}
|
||||
catch (AccessViolationException)
|
||||
{
|
||||
// Retry or handle gracefully
|
||||
}
|
||||
|
||||
// Check for anti-cheat
|
||||
// Some anti-cheat tools detect and block memory manipulation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build and Compilation Errors
|
||||
|
||||
### "XML documentation errors"
|
||||
|
||||
**Symptom:** Build fails with `CS1591` or `CS0419` errors
|
||||
|
||||
**Cause:** Missing XML comments or ambiguous cref references
|
||||
|
||||
**Solution:**
|
||||
```xml
|
||||
<!-- In WhiteMagic.csproj -->
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn> <!-- Suppress missing warnings -->
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
Or add missing XML comments (see [Documentation Best Practices](../README.md#documentation)).
|
||||
|
||||
### "Type or namespace not found"
|
||||
|
||||
**Symptom:** `WhiteMagic` namespace not found after adding reference
|
||||
|
||||
**Cause:** Project not referencing `WhiteMagic.dll`
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
dotnet add reference ../WhiteMagic/WhiteMagic.csproj
|
||||
# OR
|
||||
dotnet add package WhiteMagic
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Enable Detailed Logging
|
||||
|
||||
```csharp
|
||||
// Add diagnostic logging
|
||||
using System.Diagnostics;
|
||||
|
||||
Debug.WriteLine($"Reading from 0x{address:X}");
|
||||
int value = magic.Memory.Read<int>(address);
|
||||
Debug.WriteLine($"Read result: {value}");
|
||||
```
|
||||
|
||||
### Verify with External Tools
|
||||
|
||||
- **Cheat Engine:** Verify memory addresses and values
|
||||
- **x64dbg/windbg:** Verify function addresses and disassembly
|
||||
- **Process Hacker:** Check process handles and permissions
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
1. ❌ **Forgetting `isRelative` for module-relative addresses**
|
||||
```csharp
|
||||
// WRONG
|
||||
int value = magic.Memory.Read<int>(0x1000);
|
||||
|
||||
// RIGHT (if 0x1000 is module-relative)
|
||||
int value = magic.Memory.Read<int>(0x1000, isRelative: true);
|
||||
```
|
||||
|
||||
2. ❌ **Using `RemoteThreadExecutor` for game state**
|
||||
```csharp
|
||||
// WRONG (crashes)
|
||||
int health = magic.RemoteThread.Execute<int>(fn, CallConvention.Cdecl);
|
||||
|
||||
// RIGHT (crash-safe)
|
||||
int health = await pump.Enqueue(() => magic.Memory.Read<int>(addr));
|
||||
```
|
||||
|
||||
3. ❌ **Not disposing `Magic`**
|
||||
```csharp
|
||||
// WRONG (leaks handles)
|
||||
var magic = Magic.Open(process);
|
||||
// ... use magic
|
||||
// Forgot to dispose!
|
||||
|
||||
// RIGHT
|
||||
using (var magic = Magic.Open(process))
|
||||
{
|
||||
// ... use magic
|
||||
} // Auto-disposes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you're still stuck:
|
||||
|
||||
1. **Check documentation:**
|
||||
- [Architecture](./architecture.md)
|
||||
- [Execution Models](./execution-models.md)
|
||||
- [Memory Access](./memory-access.md)
|
||||
- [Function Hooking](./hooking.md)
|
||||
|
||||
2. **Search issues:** Check existing GitHub issues
|
||||
|
||||
3. **Create minimal reproduction:**
|
||||
```csharp
|
||||
// Minimal code that reproduces the issue
|
||||
using var magic = Magic.Open(process);
|
||||
int value = magic.Memory.Read<int>(address);
|
||||
// What happens vs. what you expect
|
||||
```
|
||||
|
||||
4. **Include system info:**
|
||||
- WhiteMagic version
|
||||
- .NET version
|
||||
- Target process (if applicable)
|
||||
- Windows version
|
||||
- x86 or x64
|
||||
|
||||
---
|
||||
|
||||
## Common Error Codes
|
||||
|
||||
| Win32 Error | Meaning | Solution |
|
||||
|-------------|---------|----------|
|
||||
| `ERROR_ACCESS_DENIED` (5) | Insufficient permissions | Run as administrator |
|
||||
| `ERROR_INVALID_HANDLE` (6) | Handle is invalid/closed | Re-open process |
|
||||
| `ERROR_NOT_ENOUGH_MEMORY` (8) | Insufficient memory | Reduce buffer size |
|
||||
| `ERROR_NOACCESS` (998) | Invalid access | Check memory protection |
|
||||
| `ERROR_PARTIAL_COPY` (299) | Partial read/write | Retry or check address |
|
||||
|
||||
---
|
||||
|
||||
*For more information, see the main [README](../README.md) and [architecture documentation](./architecture.md).*
|
||||
Reference in New Issue
Block a user