7 Commits
Author SHA1 Message Date
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
kbe 040a51bf03 fix: Build documentation at compilation
Some documentation was broken. I refactored it to be declarative now the
project build correctly and produce XML documentation.
2026-07-22 19:58:53 +02:00
kbeandClaude Opus 4.8 e8c84f0ba1 Fix IcedAssembler immediate overflow and unwrap invoke exceptions
- TryBind wraps Convert.ChangeType in TryChangeType so an immediate that
  overflows a candidate parameter type returns false (letting a wider overload
  be tried) instead of throwing OverflowException out of assembly. Verified:
  "mov eax, 4294967295" and "mov eax, -2147483649" no longer crash.
- Immediate now carries a boxed long OR ulong; TryParseImmediate parses decimal
  values above long.MaxValue via a ulong fallback, and hex via ulong. Previously
  such literals were rejected at parse time.
- Unwrap TargetInvocationException from method.Invoke so callers see the real
  Iced failure, not the reflection wrapper.

Tests: 231 passing, 4 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:43:48 +02:00
kbeandClaude Opus 4.8 0ddd812829 Address review: forwarder split, CreateDelegate guard, API-set docs
- PeHeaderParser: split export forwarders on the FIRST dot (IndexOf), not the
  last. A forwarder is "Module.Function" and the module name has no extension, so
  the last-dot split misparsed export names that themselves contain a dot.
- PeHeaderParser: document that API-set (api-ms-win-*/ext-ms-*) and ordinal
  forwarders are unsupported and should be resolved via the OS loader.
- RemoteFunction.CreateDelegate now throws InvalidOperationException unless the
  session is in-process; an external target's address is not host-mapped and a
  delegate to it would access-violate on invocation. Tests cover both paths.
- Reword the SSE-payload comment: the 16-byte scratch sits below the saved
  return address, which the aligned store leaves intact (it never overwrote it).

Tests: 223 passing, 4 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 09:22:29 +02:00
kbeandClaude Opus 4.8 30d5a9a5ec Add optional Iced backend: arbitrary text assembly and full prologue validation
Section 8 of the whitemagic-foundation change.

- 8.1: reference Iced 1.21.0 behind IcedAssembler:IAssembler. The default
  StubAssembler path never touches Iced; only constructing IcedAssembler pulls
  it into a behavioral path.
- 8.2: IcedAssembler.Assemble bridges Intel-syntax text onto Iced's fluent
  Assembler by reflection (Iced ships no text parser). Registers, immediates and
  labels are supported with origin-relative encoding; memory operands throw
  NotSupportedException.
- 8.3: IcedAssembler.GetPrologueLength decodes arbitrary instructions via Iced's
  Decoder. DetourManager.PrologueLengthResolver (new delegate) defaults to the
  built-in PrologueDecoder and is swappable to the Iced resolver, threaded into
  each Detour. This lifts the "partial boundary safety" caveat on the hooking
  slice when Iced is opted in.

Also gitignore test-run TestResults artifacts.

Tests: 221 passing, 4 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 08:56:31 +02:00
kbeandClaude Opus 4.8 af7e3dc1b9 Close out verification tasks 9.3 and 9.4
- 9.3: BlackMagic reference tests confirmed green (17 passing, 0 failing);
  WhiteMagic is additive and shares no source or build with it.
- 9.4: document the implementation deviations in the comparison doc's
  synthesis section and correct the InProcessReader architecture line
  (RPM-on-self, not direct deref).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 02:54:28 +02:00
kbeandClaude Opus 4.8 678cb00895 Implement RemoteModule/RemoteFunction and prove x64 ABI at runtime
Task 7.2: export resolution + module/function facade.
- PeHeaderParser.GetExportAddress walks the PE32/PE32+ export directory and
  follows export forwarders (e.g. kernel32!HeapAlloc -> NTDLL.RtlAllocateHeap)
  into other loaded modules; ordinal and unresolvable API-set forwarders throw
  NotSupportedException.
- RemoteModule resolves a module base via Process.Modules (name match tolerant
  of .dll/case); RemoteFunction executes via RemoteThreadExecutor by default,
  exposes Address for pump routing and CreateDelegate<T> for in-process.
- Magic gains a string indexer: magic["user32"]["MessageBoxA"].

Task 3.8: add the missing live-execution ABI test - an SSE callee whose aligned
movaps #GPs unless the stub delivers a 16-byte-aligned stack, combined with a
5th stack argument. Runtime-proves shadow space, alignment, and arg placement.

Tests: 214 passing, 4 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 02:49:51 +02:00
36 changed files with 5958 additions and 19 deletions
+4
View File
@@ -10,3 +10,7 @@ reference/
# Scratch # Scratch
*.tmp *.tmp
*.log *.log
# Test run artifacts
WhiteMagicTest/TestResults/
**/TestResults/
+188 -1
View File
@@ -1,3 +1,190 @@
# WhiteMagic # WhiteMagic
Wite Magic is a C# library to read, write and execute remote code into a target process for analysis, debuging and mod creation. WhiteMagic is a .NET 8 process-introspection library for Windows that provides managed wrappers over Win32 debugging APIs. It enables reading/writing memory, executing code remotely, hooking functions, and injecting DLLs into target processes.
## Overview
WhiteMagic is designed for diagnostic tools, debuggers, profilers, and automation clients that need to attach to and manipulate desktop applications. It unifies the best features from four legacy libraries (BlackMagic, MemorySharp, GreyMagic, and BlackMagic-old) into a modern, bitness-agnostic .NET 8 library with a crash-safe execution model.
## Key Features
- **Dual memory access**: External (ReadProcessMemory/WriteProcessMemory) and in-process readers over an abstract `MemoryBase`
- **Three-tier execution model**: Remote thread, crash-safe main-thread pump, and in-process delegates
- **Function hooking**: Reversible inline detours (`DetourManager`) and named byte patches (`PatchManager`) with auto-restore on dispose
- **Pattern scanning**: Memory pattern discovery with caching
- **Assembly seam**: `IAssembler` abstraction with hand-emitted stubs (default) and optional Iced backend
- **DLL injection**: CreateThread and thread-hijack strategies for x86 and x64
- **High-level API**: Ergonomic `RemotePointer` indexer, module/function access, PEB/TEB, window mutation, and input simulation
## Quick Start
### Installation
```bash
dotnet add package WhiteMagic
```
### Basic Memory Read/Write
```csharp
using WhiteMagic;
using var magic = Magic.Open(Process.GetProcessById(1234));
// Read an integer at an address
int health = magic.Memory.Read<int>(0x12345678);
// Write using the RemotePointer indexer
magic[0x12345678].Write(999);
// Read a string
string name = magic.Memory.ReadString(0x12345680, Encoding.UTF8);
```
### Remote Function Execution
```csharp
using WhiteMagic.Assembly;
// Resolve a function by module and export name
var msgBox = magic["user32"]["MessageBoxA"];
// Execute with calling convention and arguments
int result = msgBox.Execute<int>(
CallConvention.Stdcall,
IntPtr.Zero, // hWnd
"Hello World", // Text
"Caption", // Caption
0 // Type
);
```
### Crash-Safe Execution (Main-Thread Pump)
```csharp
// Create a pump that hooks a per-frame function
var pump = magic.CreateMainThreadPump(frameAddress);
// Enqueue work that runs on the target's main thread
int result = await pump.ExecuteAsync(() =>
{
// Safe to touch target's single-threaded state here
return magic.Memory.Read<int>(stateAddress);
});
```
### Function Hooking
```csharp
// Apply an inline detour (in-process only)
var detour = magic.DetourManager.Create(
"my_hook",
targetFunctionAddress,
myHookDelegate
);
detour.Apply();
// ... use the hook
detour.Remove(); // Automatically restored on dispose
```
### Pattern Scanning
```csharp
// Scan for a byte pattern in the target's memory
byte[] pattern = { 0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00 };
string mask = "xxx????";
IntPtr found = PatternScanner.FindInModule(
magic.Memory,
pattern,
mask,
process.MainModule
);
```
## Documentation
- **API Reference**: See inline XML documentation in your IDE; generate with DocFX
- **Conceptual Guides**: [docs/](./docs/) directory
- **Examples**: [WhiteMagic.Examples](./WhiteMagic.Examples/) with 5 comprehensive example files
- **Architecture**: [openspec/changes/whitemagic-foundation/](./openspec/changes/whitemagic-foundation/)
## Execution Models
WhiteMagic provides three execution strategies, each designed for specific payload safety requirements:
### 1. RemoteThreadExecutor (CreateRemoteThread)
Use for **thread-agnostic payloads only**:
- Pure WinAPI calls
- Self-contained computations
- DLL injection (`LoadLibrary`)
**Avoid** for single-threaded target state (scripting engines, render operations, object traversal).
### 2. MainThreadPump (Crash-Safe)
Use for **state-sensitive calls** that touch the target's main-thread-affinity data:
- Game state modifications
- UI interactions
- Script engine calls
### 3. InProcessInvoker (Direct Delegates)
Use when **injected** into the target process:
- Direct native-delegate calls via `CreateFunction<T>`
- Zero thread crossing overhead
## Architecture
WhiteMagic is built in layers:
```
Magic (high-level facade)
├── Core: MemoryBase (abstract reader/writer)
│ ├── ExternalReader (RPM/WPM on target)
│ └── InProcessReader (in-process delegate access)
├── Discovery: PatternScanner, PeHeaderParser
├── Assembly: IAssembler → {StubAssembler | IcedAssembler}
├── Execution: RemoteThreadExecutor, MainThreadPump, InProcessInvoker
├── Hooking: DetourManager, PatchManager
└── High-level: RemotePointer, RemoteModule, RemoteWindow
```
## Platform Support
- **Target Framework**: .NET 8.0-windows
- **Architecture**: x86 and x64 (host and target)
- **Operating System**: Windows 10+
- **Dependencies**: None (default); optional Iced NuGet package for arbitrary assembly
## Safety
- All operations use `SafeMemoryHandle` for proper handle cleanup
- Detours and patches auto-restore on `Dispose`
- MainThreadPump prevents crashes from thread-affinity violations
- Prologue validation before detour splicing (optional Iced backend)
## Comparison to Reference Libraries
| Library | Platform | Execution | Hooking | Assembler |
|---------|----------|-----------|---------|-----------|
| **WhiteMagic** | .NET 8, x86+x64 | 3-tier | ✅ | IAssembler (Iced opt.) |
| BlackMagic | .NET 8, x86+x64 | Remote thread | ❌ | Hand stubs |
| MemorySharp | .NET FW, x86 | Remote thread | ❌ | FASM (required) |
| GreyMagic | .NET FW, x86 | In-process | ✅ | FASM |
WhiteMagic is **not a drop-in replacement** for these libraries—it's a modern synthesis with a different API design and a crash-safe execution model.
## License
[Specify your license here]
## Contributing
Contributions are welcome! Please read our contributing guidelines (coming soon).
## Acknowledgments
WhiteMagic builds upon concepts and techniques from:
- **BlackMagic** (current) — Modern .NET 8 base, x64, pattern scanning, DLL injection
- **MemorySharp** — High-level ergonomics, PEB/TEB, window/input simulation
- **GreyMagic** — Dual memory model, detours/patches, marshal cache, in-process delegates
- **Iced** — Modern x86/x64 assembler (optional backend)
See [docs/memory-library-comparison.md](./docs/memory-library-comparison.md) for a detailed analysis.
@@ -0,0 +1,321 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using WhiteMagic;
using WhiteMagic.Execution;
namespace WhiteMagic.Examples;
/// <summary>
/// Sample struct for demonstrating struct marshalling
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct PlayerInfo
{
public int Health;
public float PositionX;
public float PositionY;
public int Level;
public uint Experience;
}
/// <summary>
/// Example 1: Basic Memory Operations
/// Demonstrates reading and writing memory of various types
/// </summary>
public class BasicMemoryOperations
{
/// <summary>
/// Target process for examples (Notepad is safe and always available)
/// </summary>
private static Process? TargetProcess()
{
// Try to find Notepad
var processes = Process.GetProcessesByName("notepad");
if (processes.Length > 0)
return processes[0];
Console.WriteLine("No Notepad process found. Please launch Notepad first.");
return null;
}
/// <summary>
/// Example 1.1: Reading and writing primitive types
/// </summary>
public static void ReadWritePrimitives()
{
Console.WriteLine("=== Example 1.1: Read/Write Primitives ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
// Example: Read an integer from a hypothetical address
// (In real use, you'd find the actual address via pattern scanning or offsets)
IntPtr testAddress = magic.Memory.ImageBase + 0x1000;
// Read different primitive types
try
{
int intValue = magic.Memory.Read<int>(testAddress);
Console.WriteLine($"Read int from 0x{testAddress:X}: {intValue}");
float floatValue = magic.Memory.Read<float>(testAddress);
Console.WriteLine($"Read float from 0x{testAddress:X}: {floatValue}");
double doubleValue = magic.Memory.Read<double>(testAddress);
Console.WriteLine($"Read double from 0x{testAddress:X}: {doubleValue}");
bool boolValue = magic.Memory.Read<bool>(testAddress);
Console.WriteLine($"Read bool from 0x{testAddress:X}: {boolValue}");
}
catch (Exception ex)
{
Console.WriteLine($"Note: Read failed (address may be invalid): {ex.Message}");
}
// Write example
bool writeSuccess = magic.Memory.Write(testAddress, 42);
Console.WriteLine($"Write to 0x{testAddress:X}: {(writeSuccess ? "Success" : "Failed")}");
}
/// <summary>
/// Example 1.2: Reading and writing arrays
/// </summary>
public static void ReadWriteArrays()
{
Console.WriteLine("\n=== Example 1.2: Read/Write Arrays ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
IntPtr arrayAddress = magic.Memory.ImageBase + 0x2000;
// Read array of integers
try
{
int[] intArray = magic.Memory.Read<int>(arrayAddress, 10);
Console.WriteLine($"Read {intArray.Length} integers from 0x{arrayAddress:X}");
Console.WriteLine($"First few values: {string.Join(", ", intArray[..Math.Min(5, intArray.Length)])}");
}
catch (Exception ex)
{
Console.WriteLine($"Note: Array read failed (address may be invalid): {ex.Message}");
}
// Write array of integers
int[] writeArray = { 1, 2, 3, 4, 5 };
bool writeSuccess = magic.Memory.Write(arrayAddress, writeArray);
Console.WriteLine($"Write {writeArray.Length} integers to 0x{arrayAddress:X}: {(writeSuccess ? "Success" : "Failed")}");
}
/// <summary>
/// Example 1.3: Reading and writing strings
/// </summary>
public static void ReadWriteStrings()
{
Console.WriteLine("\n=== Example 1.3: Read/Write Strings ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
IntPtr stringAddress = magic.Memory.ImageBase + 0x3000;
// Read ANSI string
string ansiString = magic.Memory.ReadString(stringAddress, Encoding.ASCII, maxLength: 256);
Console.WriteLine($"Read ANSI string from 0x{stringAddress:X}: \"{ansiString}\"");
// Read Unicode string
string unicodeString = magic.Memory.ReadString(stringAddress, Encoding.Unicode, maxLength: 256);
Console.WriteLine($"Read Unicode string from 0x{stringAddress:X}: \"{unicodeString}\"");
// Write string
bool writeSuccess = magic.Memory.WriteString(stringAddress, "Hello, WhiteMagic!", Encoding.ASCII);
Console.WriteLine($"Write string to 0x{stringAddress:X}: {(writeSuccess ? "Success" : "Failed")}");
}
/// <summary>
/// Example 1.4: Reading and writing raw bytes
/// </summary>
public static void ReadWriteBytes()
{
Console.WriteLine("\n=== Example 1.4: Read/Write Bytes ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
IntPtr address = magic.Memory.ImageBase + 0x4000;
// Read bytes
byte[] buffer = magic.Memory.ReadBytes(address, 16);
Console.WriteLine($"Read {buffer.Length} bytes from 0x{address:X}");
Console.WriteLine($"Hex: {BitConverter.ToString(buffer)}");
// Write bytes
byte[] patchBytes = { 0x90, 0x90, 0x90 }; // NOP x3
int bytesWritten = magic.Memory.WriteBytes(address, patchBytes);
Console.WriteLine($"Wrote {bytesWritten} bytes to 0x{address:X}");
}
/// <summary>
/// Example 1.5: Using RemotePointer for fluent addressing
/// </summary>
public static void RemotePointerUsage()
{
Console.WriteLine("\n=== Example 1.5: RemotePointer Fluent Addressing ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
// Create a pointer to module base
var modulePtr = magic[magic.Memory.ImageBase];
// Read offsets from module base
try
{
int offset1 = modulePtr.Read<int>(0x1000);
Console.WriteLine($"Read int at module_base + 0x1000: {offset1}");
// Chained reads (pointer -> value -> offset)
IntPtr nestedPtr = modulePtr.Read<IntPtr>(0x2000);
if (nestedPtr != IntPtr.Zero)
{
var nestedPtrObj = magic[nestedPtr];
int nestedValue = nestedPtrObj.Read<int>(0x50);
Console.WriteLine($"Read nested pointer value: {nestedValue}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Note: Read failed (offset may be invalid): {ex.Message}");
}
}
/// <summary>
/// Example 1.6: Relative addressing
/// </summary>
public static void RelativeAddressing()
{
Console.WriteLine("\n=== Example 1.6: Relative Addressing ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
IntPtr moduleBase = magic.Memory.ImageBase;
// Absolute addressing (default)
int absValue = magic.Memory.Read<int>(moduleBase + 0x1000);
Console.WriteLine($"Absolute read at 0x{moduleBase + 0x1000:X}: {absValue}");
// Relative addressing (relative to module base)
int relValue = magic.Memory.Read<int>(0x1000, isRelative: true);
Console.WriteLine($"Relative read at offset 0x1000: {relValue}");
// They should be the same
Console.WriteLine($"Values match: {absValue == relValue}");
}
/// <summary>
/// Example 1.7: Error handling
/// </summary>
public static void ErrorHandling()
{
Console.WriteLine("\n=== Example 1.7: Error Handling ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
// Try to read from invalid address
try
{
int invalidRead = magic.Memory.Read<int>(unchecked((IntPtr)0xdeadbeef));
Console.WriteLine($"Read from invalid address (shouldn't reach here): {invalidRead}");
}
catch (Exception ex)
{
Console.WriteLine($"✓ Caught expected exception: {ex.GetType().Name}");
}
// Try to write to read-only memory (will return false)
bool writeSuccess = magic.Memory.Write(magic.Memory.ImageBase, 42);
Console.WriteLine($"Write to read-only memory: {(writeSuccess ? "Unexpected success" : " Expected failure")}");
// Try to read string from invalid address
string invalidString = magic.Memory.ReadString(unchecked((IntPtr)0xdeadbeef), Encoding.ASCII);
Console.WriteLine($"Read string from invalid address: \"{invalidString}\" (empty = graceful failure)");
}
/// <summary>
/// Example 1.8: Working with custom structs
/// </summary>
public static void CustomStructs()
{
Console.WriteLine("\n=== Example 1.8: Custom Structs ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
IntPtr structAddress = magic.Memory.ImageBase + 0x5000;
try
{
// Read struct
PlayerInfo player = magic.Memory.Read<PlayerInfo>(structAddress);
Console.WriteLine($"Read PlayerInfo:");
Console.WriteLine($" Health: {player.Health}");
Console.WriteLine($" Position: ({player.PositionX}, {player.PositionY})");
Console.WriteLine($" Level: {player.Level}");
Console.WriteLine($" Experience: {player.Experience}");
// Write struct
PlayerInfo updatedPlayer = player with
{
Health = 100,
Level = player.Level + 1
};
bool writeSuccess = magic.Memory.Write(structAddress, updatedPlayer);
Console.WriteLine($"Write updated PlayerInfo: {(writeSuccess ? "Success" : "Failed")}");
}
catch (Exception ex)
{
Console.WriteLine($"Note: Struct read/write failed (address may be invalid): {ex.Message}");
}
}
/// <summary>
/// Run all basic memory operation examples
/// </summary>
public static void RunAll()
{
Console.WriteLine("╔════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ WhiteMagic Example 1: Basic Memory Operations ║");
Console.WriteLine("╚════════════════════════════════════════════════════════════╝");
ReadWritePrimitives();
ReadWriteArrays();
ReadWriteStrings();
ReadWriteBytes();
RemotePointerUsage();
RelativeAddressing();
ErrorHandling();
CustomStructs();
Console.WriteLine("\n✓ All basic memory operation examples completed!");
}
}
@@ -0,0 +1,461 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Text;
using WhiteMagic;
using WhiteMagic.Discovery;
namespace WhiteMagic.Examples;
/// <summary>
/// Example 2: Pattern Scanning
/// Demonstrates finding patterns in target process memory
/// </summary>
public class PatternScanning
{
/// <summary>
/// Target process for examples
/// </summary>
private static Process? TargetProcess()
{
var processes = Process.GetProcessesByName("notepad");
if (processes.Length > 0)
return processes[0];
Console.WriteLine("No Notepad process found. Please launch Notepad first.");
return null;
}
/// <summary>
/// Example 2.1: Simple pattern scan
/// </summary>
public static void SimplePatternScan()
{
Console.WriteLine("=== Example 2.1: Simple Pattern Scan ===");
var process = TargetProcess();
if (process == null) return;
if (process.MainModule == null) return;
using var magic = Magic.Open(process);
// Define a pattern to search for
// mov rax, [rip+disp] (common in x64)
byte[] pattern = { 0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00 };
string mask = "xxx????"; // 'x' = exact match, '?' = wildcard
Console.WriteLine($"Scanning for pattern in module: {process.MainModule.ModuleName}");
Console.WriteLine($"Pattern bytes: {BitConverter.ToString(pattern)}");
Console.WriteLine($"Mask: {mask}");
IntPtr result = PatternScanner.FindInModule(
magic.Memory,
pattern,
mask,
process.MainModule
);
if (result != IntPtr.Zero)
{
Console.WriteLine($"✓ Pattern found at: 0x{result:X}");
}
else
{
Console.WriteLine("✗ Pattern not found");
}
}
/// <summary>
/// Example 2.2: Pattern scan with multiple results
/// </summary>
public static void MultiplePatternScans()
{
Console.WriteLine("\n=== Example 2.2: Multiple Pattern Scans ===");
var process = TargetProcess();
if (process == null) return;
if (process.MainModule == null) return;
using var magic = Magic.Open(process);
// Multiple patterns to scan
byte[][] patterns =
{
new byte[] { 0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00 }, // mov rax, [rip+disp]
new byte[] { 0xE8, 0x00, 0x00, 0x00, 0x00 }, // call rel32
new byte[] { 0xB8, 0x00, 0x00, 0x00, 0x00 } // mov eax, imm32
};
string[] masks =
{
"xxx????",
"x????",
"x????"
};
string[] descriptions =
{
"mov rax, [rip+disp]",
"call rel32",
"mov eax, imm32"
};
Console.WriteLine($"Scanning for {patterns.Length} patterns in {process.MainModule.ModuleName}...");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
for (int i = 0; i < patterns.Length; i++)
{
try
{
IntPtr result = PatternScanner.FindInModule(
magic.Memory,
patterns[i],
masks[i],
process.MainModule
);
Console.WriteLine($"{descriptions[i],-25} {(result != IntPtr.Zero ? $" 0x{result:X}" : " Not found")}");
}
catch (Exception ex)
{
Console.WriteLine($"{descriptions[i],-25} ✗ Error: {ex.Message}");
}
}
}
/// <summary>
/// Example 2.3: Pattern scanning in specific region
/// </summary>
public static void RegionSpecificScan()
{
Console.WriteLine("\n=== Example 2.3: Region-Specific Scan ===");
var process = TargetProcess();
if (process == null) return;
if (process.MainModule == null) return;
using var magic = Magic.Open(process);
// Scan specific region: .text section (first 64KB of main module)
IntPtr startAddress = process.MainModule.BaseAddress;
IntPtr endAddress = startAddress + 0x10000; // 64KB
byte[] pattern = { 0x48, 0x8B };
string mask = "xx"; // Exact match for first 2 bytes
Console.WriteLine($"Scanning region: 0x{startAddress:X} - 0x{endAddress:X}");
try
{
IntPtr result = PatternScanner.Find(
magic.Memory,
pattern,
mask,
startAddress,
endAddress
);
if (result != IntPtr.Zero)
{
Console.WriteLine($"✓ Pattern found at: 0x{result:X}");
Console.WriteLine($" Offset from module base: 0x{(result - startAddress):X}");
}
else
{
Console.WriteLine("✗ Pattern not found in region");
}
}
catch (Exception ex)
{
Console.WriteLine($"✗ Scan failed: {ex.Message}");
}
}
/// <summary>
/// Example 2.4: Finding a function signature
/// </summary>
public static void FindFunctionSignature()
{
Console.WriteLine("\n=== Example 2.4: Finding Function Signature ===");
var process = TargetProcess();
if (process == null) return;
if (process.MainModule == null) return;
using var magic = Magic.Open(process);
// Common function prologue patterns
byte[][] prologues =
{
// x64 prologue: push rbp; mov rbp, rsp
new byte[] { 0x55, 0x48, 0x89, 0xE5 },
// x64 prologue: push rbp
new byte[] { 0x55 },
// x64 prologue: sub rsp, XX (stack allocation)
new byte[] { 0x48, 0x83, 0xEC, 0x00 } // last byte varies
};
string[] prologueMasks =
{
"xxxx", // Exact match
"x", // Exact match
"xxx?" // Last byte wildcard
};
Console.WriteLine($"Scanning for function prologues in {process.MainModule.ModuleName}...");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
for (int i = 0; i < prologues.Length; i++)
{
try
{
IntPtr result = PatternScanner.FindInModule(
magic.Memory,
prologues[i],
prologueMasks[i],
process.MainModule
);
Console.WriteLine($"Prologue {i + 1}: {(result != IntPtr.Zero ? $" Found at 0x{result:X}" : " Not found")}");
}
catch (Exception ex)
{
Console.WriteLine($"Prologue {i + 1}: ✗ Error: {ex.Message}");
}
}
}
/// <summary>
/// Example 2.5: Pattern scanning with caching
/// </summary>
public static void CachedPatternScan()
{
Console.WriteLine("\n=== Example 2.5: Cached Pattern Scanning ===");
var process = TargetProcess();
if (process == null) return;
if (process.MainModule == null) return;
using var magic = Magic.Open(process);
// Create cache
var cache = new PatternScannerCache(magic.Memory);
byte[] pattern = { 0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00 };
string mask = "xxx????";
Console.WriteLine("Demonstrating cache performance...");
// First scan (uncached - reads memory)
Console.Write(" First scan (uncached): ");
var watch = System.Diagnostics.Stopwatch.StartNew();
IntPtr result1 = cache.FindInModuleCached(pattern, mask, process.MainModule);
watch.Stop();
Console.WriteLine($"{(result1 != IntPtr.Zero ? $" 0x{result1:X}" : " Not found")} ({watch.ElapsedMilliseconds}ms)");
// Second scan (cached - no memory read)
Console.Write(" Second scan (cached): ");
watch.Restart();
IntPtr result2 = cache.FindInModuleCached(pattern, mask, process.MainModule);
watch.Stop();
Console.WriteLine($"{(result2 != IntPtr.Zero ? $" 0x{result2:X}" : " Not found")} ({watch.ElapsedMilliseconds}ms)");
if (result1 == result2)
{
Console.WriteLine(" ✓ Results match and cache is working");
}
}
/// <summary>
/// Example 2.6: Pattern scanning with wildcard flexibility
/// </summary>
public static void FlexibleWildcardPatterns()
{
Console.WriteLine("\n=== Example 2.6: Flexible Wildcard Patterns ===");
var process = TargetProcess();
if (process == null) return;
if (process.MainModule == null) return;
using var magic = Magic.Open(process);
// Same pattern, different wildcard masks
byte[] pattern = { 0x48, 0x8B, 0x05, 0x12, 0x34, 0x56, 0x78 };
string[] masks =
{
"xxx????", // Last 4 bytes wildcard
"xxxx???", // Last 3 bytes wildcard
"xxxxxxx", // Exact match
"x?x?x?x" // Alternating wildcard
};
Console.WriteLine($"Testing same pattern with different masks...");
Console.WriteLine($"Pattern: {BitConverter.ToString(pattern)}");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
for (int i = 0; i < masks.Length; i++)
{
try
{
IntPtr result = PatternScanner.FindInModule(
magic.Memory,
pattern,
masks[i],
process.MainModule
);
Console.WriteLine($"Mask \"{masks[i],-10}\" {(result != IntPtr.Zero ? $" Found at 0x{result:X}" : " Not found")}");
}
catch (Exception ex)
{
Console.WriteLine($"Mask \"{masks[i],-10}\" ✗ Error: {ex.Message}");
}
}
}
/// <summary>
/// Example 2.7: Combining pattern scan with validation
/// </summary>
public static void SignatureBasedScanning()
{
Console.WriteLine("\n=== Example 2.7: Pattern-Based Function Scanning ===");
var process = TargetProcess();
if (process == null) return;
if (process.MainModule == null) return;
using var magic = Magic.Open(process);
// Find MessageBoxA pattern in user32.dll (if loaded)
var user32Module = process.Modules.Cast<System.Diagnostics.ProcessModule>()
.FirstOrDefault(m => m.ModuleName.Equals("user32.dll", StringComparison.OrdinalIgnoreCase));
if (user32Module == null)
{
Console.WriteLine("✗ user32.dll not loaded in target process");
return;
}
Console.WriteLine($"Scanning {user32Module.ModuleName} for function signatures...");
// Try to find common export patterns
byte[] testPattern = { 0x48, 0x8B };
string mask = "xx";
try
{
IntPtr result = PatternScanner.FindInModule(
magic.Memory,
testPattern,
mask,
user32Module
);
if (result != IntPtr.Zero)
{
Console.WriteLine($"✓ Found pattern at 0x{result:X}");
Console.WriteLine($" Module base: 0x{user32Module.BaseAddress:X}");
Console.WriteLine($" Offset: 0x{(result - user32Module.BaseAddress):X}");
}
else
{
Console.WriteLine("✗ Pattern not found");
}
}
catch (Exception ex)
{
Console.WriteLine($"✗ Scan failed: {ex.Message}");
}
}
/// <summary>
/// Example 2.8: Pattern validation and error handling
/// </summary>
public static void PatternValidation()
{
Console.WriteLine("\n=== Example 2.8: Pattern Validation ===");
var process = TargetProcess();
if (process == null) return;
if (process.MainModule == null) return;
using var magic = Magic.Open(process);
// Test various invalid/edge case patterns
Console.WriteLine("Testing edge cases and validation...");
// Empty pattern
try
{
IntPtr result = PatternScanner.FindInModule(
magic.Memory,
Array.Empty<byte>(),
null,
process.MainModule
);
Console.WriteLine("✗ Empty pattern should throw exception");
}
catch (ArgumentException)
{
Console.WriteLine("✓ Empty pattern correctly rejected");
}
// Mismatched pattern and mask length
try
{
IntPtr result = PatternScanner.FindInModule(
magic.Memory,
new byte[] { 0x48, 0x8B },
"x", // Mask too short
process.MainModule
);
Console.WriteLine("✗ Mismatched mask length should throw exception");
}
catch (ArgumentException)
{
Console.WriteLine("✓ Mismatched mask length correctly rejected");
}
// Invalid mask characters
try
{
IntPtr result = PatternScanner.FindInModule(
magic.Memory,
new byte[] { 0x48, 0x8B },
"ab", // Invalid mask characters
process.MainModule
);
Console.WriteLine("✗ Invalid mask characters should throw exception");
}
catch (ArgumentException)
{
Console.WriteLine("✓ Invalid mask characters correctly rejected");
}
Console.WriteLine("✓ All validation tests passed");
}
/// <summary>
/// Run all pattern scanning examples
/// </summary>
public static void RunAll()
{
Console.WriteLine("╔════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ WhiteMagic Example 2: Pattern Scanning ║");
Console.WriteLine("╚════════════════════════════════════════════════════════════╝");
SimplePatternScan();
MultiplePatternScans();
RegionSpecificScan();
FindFunctionSignature();
CachedPatternScan();
FlexibleWildcardPatterns();
SignatureBasedScanning();
PatternValidation();
Console.WriteLine("\n✓ All pattern scanning examples completed!");
}
}
@@ -0,0 +1,401 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using WhiteMagic;
using WhiteMagic.Assembly;
using WhiteMagic.Execution;
namespace WhiteMagic.Examples;
/// <summary>
/// Example 3: Execution Models
/// Demonstrates the three execution strategies: RemoteThreadExecutor, MainThreadPump, and InProcessInvoker
/// </summary>
public class ExecutionModels
{
/// <summary>
/// Target process for examples
/// </summary>
private static Process? TargetProcess()
{
var processes = Process.GetProcessesByName("notepad");
if (processes.Length > 0)
return processes[0];
Console.WriteLine("No Notepad process found. Please launch Notepad first.");
return null;
}
/// <summary>
/// Example 3.1: RemoteThreadExecutor (CreateRemoteThread)
/// Use for: Thread-agnostic payloads (WinAPI calls, DLL injection, self-contained code)
/// </summary>
public static void RemoteThreadExecutorExample()
{
Console.WriteLine("=== Example 3.1: RemoteThreadExecutor (CreateRemoteThread) ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
try
{
// Example: Call GetTickCount (thread-safe WinAPI function)
var getTickCount = magic["kernel32.dll"]["GetTickCount"];
if (getTickCount.Address != IntPtr.Zero)
{
Console.WriteLine($"Calling GetTickCount via RemoteThreadExecutor...");
uint ticks = getTickCount.Execute<uint>(CallConvention.Stdcall);
Console.WriteLine($"✓ Result: {ticks} ticks ({TimeSpan.FromMilliseconds(ticks):hh\\:mm\\:ss})");
}
else
{
Console.WriteLine("✗ GetTickCount not found in kernel32.dll");
}
// Example: Call GetCurrentProcessId
var getCurrentProcessId = magic["kernel32.dll"]["GetCurrentProcessId"];
if (getCurrentProcessId.Address != IntPtr.Zero)
{
Console.WriteLine($"Calling GetCurrentProcessId...");
uint processId = getCurrentProcessId.Execute<uint>(CallConvention.Stdcall);
Console.WriteLine($"✓ Result: Process ID = {processId}");
}
else
{
Console.WriteLine("✗ GetCurrentProcessId not found in kernel32.dll");
}
}
catch (Exception ex)
{
Console.WriteLine($"✗ Execution failed: {ex.Message}");
}
Console.WriteLine("\n⚠️ NOTE: RemoteThreadExecutor is ONLY for thread-agnostic functions!");
Console.WriteLine(" Do NOT use for game state, scripting engines, or render operations.");
Console.WriteLine(" Use MainThreadPump for state-sensitive calls instead.");
}
/// <summary>
/// Example 3.2: MainThreadPump (Crash-Safe Execution)
/// Use for: State-sensitive calls (game state, scripting, UI, render operations)
/// </summary>
public static async Task MainThreadPumpExample()
{
Console.WriteLine("\n=== Example 3.2: MainThreadPump (Crash-Safe Execution) ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
try
{
// Find a per-frame function (e.g., D3D EndScene, or any function called every frame)
// For this example, we'll use a hypothetical frame function address
// In real use, you'd find the actual frame function via pattern scanning
IntPtr frameFunctionAddress = FindFrameFunction(magic);
if (frameFunctionAddress == IntPtr.Zero)
{
Console.WriteLine("✗ Could not find frame function (expected in this demo environment)");
Console.WriteLine(" In a real game, you'd use pattern scanning to find the frame function.");
return;
}
Console.WriteLine($"Found frame function at: 0x{frameFunctionAddress:X}");
// Create MainThreadPump
Console.WriteLine("Creating MainThreadPump...");
var pump = magic.CreateMainThreadPump(frameFunctionAddress);
// Example 1: Safe memory read from game state
Console.WriteLine("Enqueueing safe memory read...");
try
{
int value = await pump.ExecuteAsync(() =>
{
// Safe to read game state here (running on target's main thread)
return magic.Memory.Read<int>(magic.Memory.ImageBase + 0x1000);
});
Console.WriteLine($"✓ Safe read result: {value}");
}
catch (TimeoutException)
{
Console.WriteLine("✗ Pump operation timed out (work item wedged or target not running)");
}
catch (Exception ex)
{
Console.WriteLine($"✗ Pump operation failed: {ex.Message}");
}
// Example 2: Safe memory write to game state
Console.WriteLine("Enqueueing safe memory write...");
try
{
await pump.ExecuteAsync(() =>
{
// Safe to write game state here
magic.Memory.Write(magic.Memory.ImageBase + 0x2000, 42);
return true;
});
Console.WriteLine("✓ Safe write completed");
}
catch (Exception ex)
{
Console.WriteLine($"✗ Safe write failed: {ex.Message}");
}
// Example 3: Complex state manipulation
Console.WriteLine("Enqueueing complex state manipulation...");
try
{
string result = await pump.ExecuteAsync(() =>
{
// Safe to perform complex multi-step operations
IntPtr basePtr = magic.Memory.Read<IntPtr>(magic.Memory.ImageBase + 0x3000);
if (basePtr != IntPtr.Zero)
{
int health = magic.Memory.Read<int>(basePtr + 0x10);
int maxHealth = magic.Memory.Read<int>(basePtr + 0x14);
return $"Health: {health}/{maxHealth}";
}
return "Unknown";
});
Console.WriteLine($"✓ Complex operation result: {result}");
}
catch (Exception ex)
{
Console.WriteLine($"✗ Complex operation failed: {ex.Message}");
}
Console.WriteLine("\n✓ MainThreadPump examples completed");
}
catch (Exception ex)
{
Console.WriteLine($"✗ MainThreadPump setup failed: {ex.Message}");
}
}
/// <summary>
/// Helper: Find a per-frame function
/// In real use, you'd use pattern scanning to find D3D EndScene or similar
/// </summary>
private static IntPtr FindFrameFunction(Magic magic)
{
// For demo purposes, return zero (not found)
// In real use, you'd scan for patterns like:
// - D3D9 EndScene: device + 0x44 vtable entry
// - D3D11 Present callbacks
// - Game-specific Update/Render functions
// Example pattern scan (commented out for demo):
// var module = magic["d3d9.dll"];
// if (module != null)
// {
// IntPtr endScene = magic.Memory.FindPattern("?? ?? ?? ??", module.BaseAddress, module.ModuleMemorySize);
// return endScene;
// }
return IntPtr.Zero;
}
/// <summary>
/// Example 3.3: InProcessInvoker (Direct Delegates)
/// Use for: In-process calls after DLL injection (max performance)
/// </summary>
public static void InProcessInvokerExample()
{
Console.WriteLine("\n=== Example 3.3: InProcessInvoker (Direct Delegates) ===");
Console.WriteLine("⚠️ NOTE: This example only works when injected in-process!");
Console.WriteLine(" For demo purposes, we'll show the syntax but it won't execute.");
// Example syntax (would work if injected):
Console.WriteLine("\nExample code (requires in-process injection):");
Console.WriteLine("```csharp");
Console.WriteLine("// Only works when injected in-process");
Console.WriteLine("using var magic = Magic.OpenInProcess();");
Console.WriteLine();
Console.WriteLine("// Define delegate signature");
Console.WriteLine("[UnmanagedFunctionPointer(CallingConvention.Cdecl)]");
Console.WriteLine("public delegate int AddNumbersDelegate(int a, int b);");
Console.WriteLine();
Console.WriteLine("// Create delegate from function address");
Console.WriteLine("var addNumbers = inProcess.CreateFunction<AddNumbersDelegate>(functionAddress);");
Console.WriteLine();
Console.WriteLine("// Call directly as a delegate (max performance, <1μs)");
Console.WriteLine("int result = addNumbers(10, 20);");
Console.WriteLine("Console.WriteLine($\"Result: {result}\");");
Console.WriteLine("```");
Console.WriteLine("\n✓ InProcessInvoker is the fastest but requires injection.");
Console.WriteLine(" Use RemoteThreadExecutor for initial injection, then switch to InProcessInvoker.");
}
/// <summary>
/// Example 3.4: Choosing the Right Execution Model
/// </summary>
public static void ChooseExecutionModel()
{
Console.WriteLine("\n=== Example 3.4: Choosing the Right Execution Model ===");
Console.WriteLine("Decision Flowchart:");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
Console.WriteLine();
Console.WriteLine("Are you injected in-process?");
Console.WriteLine("├─ Yes → Use InProcessInvoker (direct delegates, <1μs)");
Console.WriteLine("└─ No → Does the call touch single-threaded state?");
Console.WriteLine(" ├─ Yes → Use MainThreadPump (crash-safe, ~1 frame latency)");
Console.WriteLine(" └─ No → Use RemoteThreadExecutor (CreateRemoteThread, ~1-2ms)");
Console.WriteLine();
Console.WriteLine("Examples by Use Case:");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
Console.WriteLine();
Console.WriteLine("1. DLL Injection: RemoteThreadExecutor");
Console.WriteLine(" → LoadLibrary is thread-safe");
Console.WriteLine();
Console.WriteLine("2. Read Health Bar: MainThreadPump");
Console.WriteLine(" → Game state is main-thread-affine");
Console.WriteLine();
Console.WriteLine("3. Call GetTickCount: RemoteThreadExecutor");
Console.WriteLine(" → WinAPI, no thread affinity");
Console.WriteLine();
Console.WriteLine("4. Script Engine Call: MainThreadPump");
Console.WriteLine(" → Script VM is main-thread-only");
Console.WriteLine();
Console.WriteLine("5. Injected Profiling: InProcessInvoker");
Console.WriteLine(" → Already in-process, max performance");
Console.WriteLine();
Console.WriteLine("6. Window Mutation: RemoteThreadExecutor");
Console.WriteLine(" → WinAPI SetWindowPos is thread-safe");
Console.WriteLine();
Console.WriteLine("Performance Comparison:");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
Console.WriteLine();
Console.WriteLine("┌─────────────────────┬──────────────┬──────────┬──────────────┐");
Console.WriteLine("│ Model │ Latency │ Safety │ Use Case │");
Console.WriteLine("├─────────────────────┼──────────────┼──────────┼──────────────┤");
Console.WriteLine("│ RemoteThreadExec │ ~1-2ms │ Thread- │ One-shot, │");
Console.WriteLine("│ │ │ agnostic │ DLL inject │");
Console.WriteLine("├─────────────────────┼──────────────┼──────────┼──────────────┤");
Console.WriteLine("│ MainThreadPump │ ~1 frame │ Crash- │ State- │");
Console.WriteLine("│ │ (16-33ms) │ safe │ sensitive │");
Console.WriteLine("├─────────────────────┼──────────────┼──────────┼──────────────┤");
Console.WriteLine("│ InProcessInvoker │ <1μs │ In- │ In-process │");
Console.WriteLine("│ │ │ process │ tools │");
Console.WriteLine("└─────────────────────┴──────────────┴──────────┴──────────────┘");
}
/// <summary>
/// Example 3.5: Combining Execution Models
/// </summary>
public static void CombinedExecutionModels()
{
Console.WriteLine("\n=== Example 3.5: Combining Execution Models ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
Console.WriteLine("Strategy: Inject DLL, then switch to InProcessInvoker");
Console.WriteLine();
Console.WriteLine("Step 1: Use RemoteThreadExecutor to inject DLL");
Console.WriteLine("```csharp");
Console.WriteLine("var loadLibrary = magic[\"kernel32.dll\"][\"LoadLibraryA\"];");
Console.WriteLine("IntPtr dllHandle = loadLibrary.Execute<IntPtr>(CallConvention.Stdcall, dllPathPtr);");
Console.WriteLine("```");
Console.WriteLine();
Console.WriteLine("Step 2: Injected DLL initializes, now in-process");
Console.WriteLine("```csharp");
Console.WriteLine("// Inside injected DLL");
Console.WriteLine("using var inProcess = Magic.OpenInProcess();");
Console.WriteLine();
Console.WriteLine("// Use InProcessInvoker for max performance");
Console.WriteLine("var fn = inProcess.CreateFunction<MyDelegate>(address);");
Console.WriteLine("int result = fn(arg1, arg2); // <1μs latency");
Console.WriteLine("```");
Console.WriteLine();
Console.WriteLine("✓ DLL injection → Fast in-process calls");
}
/// <summary>
/// Example 3.6: Error Handling in Execution Models
/// </summary>
public static void ExecutionModelErrorHandling()
{
Console.WriteLine("\n=== Example 3.6: Error Handling ===");
Console.WriteLine("RemoteThreadExecutor:");
Console.WriteLine("```csharp");
Console.WriteLine("try");
Console.WriteLine("{");
Console.WriteLine(" uint result = fn.Execute<uint>(CallConvention.Stdcall, args...);");
Console.WriteLine("}");
Console.WriteLine("catch (Win32Exception ex)");
Console.WriteLine("{");
Console.WriteLine(" // CreateRemoteThread failed (access denied, process exited, etc.)");
Console.WriteLine(" Console.WriteLine($\"Execution failed: {ex.Message}\");");
Console.WriteLine("}");
Console.WriteLine("```");
Console.WriteLine();
Console.WriteLine("MainThreadPump:");
Console.WriteLine("```csharp");
Console.WriteLine("try");
Console.WriteLine("{");
Console.WriteLine(" int result = await pump.ExecuteAsync(() => magic.Memory.Read<int>(addr));");
Console.WriteLine("}");
Console.WriteLine("catch (TimeoutException)");
Console.WriteLine("{");
Console.WriteLine(" // Work item wedged (stuck the frame) or target not running");
Console.WriteLine(" Console.WriteLine(\"Operation timed out\");");
Console.WriteLine("}");
Console.WriteLine("catch (Exception ex)");
Console.WriteLine("{");
Console.WriteLine(" // Other exception from work item");
Console.WriteLine(" Console.WriteLine($\"Operation failed: {ex.Message}\");");
Console.WriteLine("}");
Console.WriteLine("```");
Console.WriteLine();
Console.WriteLine("InProcessInvoker:");
Console.WriteLine("```csharp");
Console.WriteLine("try");
Console.WriteLine("{");
Console.WriteLine(" int result = delegate(arg1, arg2);");
Console.WriteLine("}");
Console.WriteLine("catch (AccessViolationException)");
Console.WriteLine("{");
Console.WriteLine(" // Invalid function address or bad call convention");
Console.WriteLine("}");
Console.WriteLine("```");
}
/// <summary>
/// Run all execution model examples
/// </summary>
public static async Task RunAll()
{
Console.WriteLine("╔════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ WhiteMagic Example 3: Execution Models ║");
Console.WriteLine("╚════════════════════════════════════════════════════════════╝");
RemoteThreadExecutorExample();
await MainThreadPumpExample();
InProcessInvokerExample();
ChooseExecutionModel();
CombinedExecutionModels();
ExecutionModelErrorHandling();
Console.WriteLine("\n✓ All execution model examples completed!");
Console.WriteLine();
Console.WriteLine("⚠️ CRITICAL REMINDER:");
Console.WriteLine(" - RemoteThreadExecutor: ONLY for thread-agnostic functions");
Console.WriteLine(" - MainThreadPump: For state-sensitive calls (crash-safe)");
Console.WriteLine(" - InProcessInvoker: Only after DLL injection");
Console.WriteLine();
Console.WriteLine(" Using the wrong model crashes the target application!");
}
}
@@ -0,0 +1,410 @@
using System;
using System.Diagnostics;
using WhiteMagic;
using WhiteMagic.Hooking;
namespace WhiteMagic.Examples;
/// <summary>
/// Example 4: Function Hooking
/// Demonstrates inline detours and byte patches
/// </summary>
public class FunctionHooking
{
/// <summary>
/// Target process for examples
/// </summary>
private static Process? TargetProcess()
{
var processes = Process.GetProcessesByName("notepad");
if (processes.Length > 0)
return processes[0];
Console.WriteLine("No Notepad process found. Please launch Notepad first.");
return null;
}
/// <summary>
/// Example 4.1: Simple Inline Detour
/// Note: Detours only work in-process (requires injection)
/// </summary>
public static void SimpleInlineDetour()
{
Console.WriteLine("=== Example 4.1: Simple Inline Detour ===");
Console.WriteLine("⚠️ NOTE: Detours only work when injected in-process!");
Console.WriteLine(" For demo purposes, we'll show the syntax but it won't execute.");
Console.WriteLine();
Console.WriteLine("Example code (requires in-process injection):");
Console.WriteLine("```csharp");
Console.WriteLine("using var magic = Magic.OpenInProcess();");
Console.WriteLine();
Console.WriteLine("// Define your hook delegate");
Console.WriteLine("[UnmanagedFunctionPointer(CallingConvention.Stdcall)]");
Console.WriteLine("public delegate uint GetTickCountDelegate();");
Console.WriteLine();
Console.WriteLine("// Original function pointer (for calling original)");
Console.WriteLine("static GetTickCountDelegate OriginalGetTickCount = null!;");
Console.WriteLine();
Console.WriteLine("// Your hook implementation");
Console.WriteLine("static uint MyGetTickCount()");
Console.WriteLine("{");
Console.WriteLine(" Console.WriteLine(\"GetTickCount called!\");");
Console.WriteLine(" return OriginalGetTickCount(); // Call original");
Console.WriteLine("}");
Console.WriteLine();
Console.WriteLine("// Apply the detour");
Console.WriteLine("var getTickCount = magic[\"kernel32.dll\"][\"GetTickCount\"];");
Console.WriteLine("var detour = magic.DetourManager.Create(");
Console.WriteLine(" \"my_gettickcount\",");
Console.WriteLine(" getTickCount.Address,");
Console.WriteLine(" (GetTickCountDelegate)MyGetTickCount");
Console.WriteLine(");");
Console.WriteLine("OriginalGetTickCount = original;");
Console.WriteLine("detour.Apply();");
Console.WriteLine("```");
Console.WriteLine();
Console.WriteLine("✓ Detour applied: Every GetTickCount call now goes through MyGetTickCount");
}
/// <summary>
/// Example 4.2: Detour with Parameter Modification
/// </summary>
public static void DetourWithParameterModification()
{
Console.WriteLine("\n=== Example 4.2: Detour with Parameter Modification ===");
Console.WriteLine("Example: Hook MessageBoxW to change the caption");
Console.WriteLine("```csharp");
Console.WriteLine("// Original function signature");
Console.WriteLine("[UnmanagedFunctionPointer(CallingConvention.Stdcall)]");
Console.WriteLine("public delegate int MessageBoxDelegate(");
Console.WriteLine(" IntPtr hWnd,");
Console.WriteLine(" IntPtr lpText,");
Console.WriteLine(" IntPtr lpCaption,");
Console.WriteLine(" uint type");
Console.WriteLine(");");
Console.WriteLine();
Console.WriteLine("static MessageBoxDelegate OriginalMessageBox = null!;");
Console.WriteLine();
Console.WriteLine("static int MyMessageBox(");
Console.WriteLine(" IntPtr hWnd,");
Console.WriteLine(" IntPtr lpText,");
Console.WriteLine(" IntPtr lpCaption,");
Console.WriteLine(" uint type)");
Console.WriteLine("{");
Console.WriteLine(" // Read original strings");
Console.WriteLine(" string text = Marshal.PtrToStringUni(lpText);");
Console.WriteLine(" string caption = Marshal.PtrToStringUni(lpCaption);");
Console.WriteLine();
Console.WriteLine(" Console.WriteLine($\"MessageBox: {caption} - {text}\");");
Console.WriteLine();
Console.WriteLine(" // Modify the caption");
Console.WriteLine(" string newCaption = \"[Hooked] \" + caption;");
Console.WriteLine(" IntPtr newCaptionPtr = Marshal.StringToHGlobalUni(newCaption);");
Console.WriteLine();
Console.WriteLine(" // Call original with modified caption");
Console.WriteLine(" int result = OriginalMessageBox(hWnd, lpText, newCaptionPtr, type);");
Console.WriteLine();
Console.WriteLine(" Marshal.FreeHGlobal(newCaptionPtr);");
Console.WriteLine(" return result;");
Console.WriteLine("}");
Console.WriteLine();
Console.WriteLine("// Apply hook");
Console.WriteLine("var messageBox = magic[\"user32.dll\"][\"MessageBoxW\"];");
Console.WriteLine("var detour = magic.DetourManager.Create(");
Console.WriteLine(" \"my_messagebox\",");
Console.WriteLine(" messageBox.Address,");
Console.WriteLine(" (MessageBoxDelegate)MyMessageBox");
Console.WriteLine(");");
Console.WriteLine();
Console.WriteLine("OriginalMessageBox = original;");
Console.WriteLine("detour.Apply();");
Console.WriteLine("```");
Console.WriteLine();
Console.WriteLine("✓ Every MessageBoxW call now has \"[Hooked]\" prefix in caption");
}
/// <summary>
/// Example 4.3: Detour with Return Value Modification
/// </summary>
public static void DetourWithReturnValueModification()
{
Console.WriteLine("\n=== Example 4.3: Detour with Return Value Modification ===");
Console.WriteLine("Example: Always return success from a function");
Console.WriteLine("```csharp");
Console.WriteLine("[UnmanagedFunctionPointer(CallingConvention.Stdcall)]");
Console.WriteLine("public delegate bool CheckLicenseDelegate();");
Console.WriteLine();
Console.WriteLine("static bool MyCheckLicense()");
Console.WriteLine("{");
Console.WriteLine(" Console.WriteLine(\"License check bypassed!\");");
Console.WriteLine(" return true; // Always return true (bypass check)");
Console.WriteLine("}");
Console.WriteLine();
Console.WriteLine("// Apply hook");
Console.WriteLine("var checkLicense = magic[\"target.dll\"][\"CheckLicense\"];");
Console.WriteLine("var detour = magic.DetourManager.Create(");
Console.WriteLine(" \"my_checklicense\",");
Console.WriteLine(" checkLicense.Address,");
Console.WriteLine(" (CheckLicenseDelegate)MyCheckLicense");
Console.WriteLine(");");
Console.WriteLine("detour.Apply();");
Console.WriteLine("```");
Console.WriteLine();
Console.WriteLine("✓ License check always returns true (bypassed)");
}
/// <summary>
/// Example 4.4: Multiple Detours (Chain Hooking)
/// </summary>
public static void MultipleDetours()
{
Console.WriteLine("\n=== Example 4.4: Multiple Detours (Chain Hooking) ===");
Console.WriteLine("Example: Install multiple hooks on the same function");
Console.WriteLine("```csharp");
Console.WriteLine("// First hook");
Console.WriteLine("var detour1 = magic.DetourManager.Create(\"hook1\", fnAddr, Hook1);");
Console.WriteLine("detour1.Apply();");
Console.WriteLine();
Console.WriteLine("// Second hook (hooks the trampoline from first)");
Console.WriteLine("var detour2 = magic.DetourManager.Create(");
Console.WriteLine(" \"hook2\",");
Console.WriteLine(" detour1.Trampoline,");
Console.WriteLine(" Hook2");
Console.WriteLine(");");
Console.WriteLine("detour2.Apply();");
Console.WriteLine();
Console.WriteLine("// Execution flow:");
Console.WriteLine("// Original function → Hook2 → Hook1 → Trampoline1 → Original+5");
Console.WriteLine("```");
Console.WriteLine();
Console.WriteLine("✓ Chain hooking: Hooks execute in reverse order of installation");
}
/// <summary>
/// Example 4.5: Byte Patching
/// </summary>
public static void BytePatching()
{
Console.WriteLine("\n=== Example 4.5: Byte Patching ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
try
{
// Example: Patch a constant value
IntPtr patchAddress = magic.Memory.ImageBase + 0x5000;
byte[] originalBytes = magic.Memory.ReadBytes(patchAddress, 4);
byte[] patchBytes = { 0x00, 0x00, 0x00, 0x00 }; // Patch to 0
Console.WriteLine($"Creating patch at 0x{patchAddress:X}");
Console.WriteLine($"Original bytes: {BitConverter.ToString(originalBytes)}");
Console.WriteLine($"Patch bytes: {BitConverter.ToString(patchBytes)}");
var patch = magic.PatchManager.Create("MaxHealthPatch", patchAddress, patchBytes);
patch.Apply();
Console.WriteLine($"✓ Patch applied");
// Verify patch
byte[] currentBytes = magic.Memory.ReadBytes(patchAddress, 4);
Console.WriteLine($"Current bytes: {BitConverter.ToString(currentBytes)}");
// Remove patch
patch.Remove();
Console.WriteLine($"✓ Patch removed (original bytes restored)");
byte[] restoredBytes = magic.Memory.ReadBytes(patchAddress, 4);
Console.WriteLine($"Restored bytes: {BitConverter.ToString(restoredBytes)}");
}
catch (Exception ex)
{
Console.WriteLine($"✗ Patch example failed (address may be invalid): {ex.Message}");
}
}
/// <summary>
/// Example 4.6: NOP Patching (Removing Instructions)
/// </summary>
public static void NopPatching()
{
Console.WriteLine("\n=== Example 4.6: NOP Patching (Removing Instructions) ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
try
{
// Example: NOP out a conditional jump (5 bytes on x86)
IntPtr patchAddress = magic.Memory.ImageBase + 0x6000;
byte[] originalBytes = magic.Memory.ReadBytes(patchAddress, 5);
byte[] nopPatch = { 0x90, 0x90, 0x90, 0x90, 0x90 }; // NOP x5
Console.WriteLine($"Creating NOP patch at 0x{patchAddress:X}");
Console.WriteLine($"Original bytes: {BitConverter.ToString(originalBytes)}");
Console.WriteLine($"NOP patch: {BitConverter.ToString(nopPatch)}");
var patch = magic.PatchManager.Create("ConditionalJumpPatch", patchAddress, nopPatch);
patch.Apply();
Console.WriteLine($"✓ NOP patch applied (conditional jump removed)");
// Restore
patch.Remove();
Console.WriteLine($"✓ Patch removed (original jump restored)");
}
catch (Exception ex)
{
Console.WriteLine($"✗ NOP patch example failed (address may be invalid): {ex.Message}");
}
}
/// <summary>
/// Example 4.7: Conditional Patching
/// </summary>
public static void ConditionalPatching()
{
Console.WriteLine("\n=== Example 4.7: Conditional Patching ===");
Console.WriteLine("Example: Toggle patch on/off");
Console.WriteLine("```csharp");
Console.WriteLine("public class ToggleablePatch");
Console.WriteLine("{");
Console.WriteLine(" private readonly Patch _patch;");
Console.WriteLine(" private bool _enabled;");
Console.WriteLine();
Console.WriteLine(" public bool Enabled");
Console.WriteLine(" {");
Console.WriteLine(" get => _enabled;");
Console.WriteLine(" set");
Console.WriteLine(" {");
Console.WriteLine(" if (_enabled == value) return;");
Console.WriteLine();
Console.WriteLine(" if (value)");
Console.WriteLine(" _patch.Apply();");
Console.WriteLine(" else");
Console.WriteLine(" _patch.Remove();");
Console.WriteLine();
Console.WriteLine(" _enabled = value;");
Console.WriteLine(" }");
Console.WriteLine(" }");
Console.WriteLine("}");
Console.WriteLine();
Console.WriteLine("// Usage");
Console.WriteLine("var patch = magic.PatchManager.Create(\"Patch\", addr, bytes);");
Console.WriteLine("var toggleable = new ToggleablePatch(patch);");
Console.WriteLine();
Console.WriteLine("toggleable.Enabled = true; // Apply patch");
Console.WriteLine("toggleable.Enabled = false; // Remove patch");
Console.WriteLine("```");
}
/// <summary>
/// Example 4.8: Detour Safety and Prologue Validation
/// </summary>
public static void DetourSafety()
{
Console.WriteLine("\n=== Example 4.8: Detour Safety and Prologue Validation ===");
Console.WriteLine("Prologue Validation:");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
Console.WriteLine("Before splicing a detour, WhiteMagic validates the prologue:");
Console.WriteLine();
Console.WriteLine("✓ Common x86/x64 prologues:");
Console.WriteLine(" - push ebp; mov ebp, esp");
Console.WriteLine(" - mov edi, edi (hot-patch padding)");
Console.WriteLine(" - sub rsp, XX (x64 stack allocation)");
Console.WriteLine(" - Single-byte instructions (nop, int3)");
Console.WriteLine();
Console.WriteLine("✓ Optional Iced backend for full disassembly validation");
Console.WriteLine();
Console.WriteLine("Error: \"Prologue too short\"");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
Console.WriteLine("Cause: Function prologue is shorter than minimum (5 bytes for jmp)");
Console.WriteLine();
Console.WriteLine("Solutions:");
Console.WriteLine(" 1. Hook a different function");
Console.WriteLine(" 2. Patch deeper into the function (after prologue)");
Console.WriteLine(" 3. For WinAPI, use hot-patch area (2-byte jmp at [address-2])");
Console.WriteLine();
Console.WriteLine("Thread Safety:");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
Console.WriteLine("⚠️ DetourManager is NOT thread-safe!");
Console.WriteLine();
Console.WriteLine("WRONG:");
Console.WriteLine("```csharp");
Console.WriteLine("Task.Run(() => detour1.Apply());");
Console.WriteLine("Task.Run(() => detour2.Apply()); // Race condition!");
Console.WriteLine("```");
Console.WriteLine();
Console.WriteLine("RIGHT:");
Console.WriteLine("```csharp");
Console.WriteLine("lock (magic.DetourManager)");
Console.WriteLine("{");
Console.WriteLine(" detour1.Apply();");
Console.WriteLine(" detour2.Apply();");
Console.WriteLine("}");
Console.WriteLine("```");
}
/// <summary>
/// Example 4.9: Automatic Restoration on Disposal
/// </summary>
public static void AutomaticRestoration()
{
Console.WriteLine("\n=== Example 4.9: Automatic Restoration on Disposal ===");
Console.WriteLine("All hooks and patches auto-restore on disposal:");
Console.WriteLine("```csharp");
Console.WriteLine("using (var magic = Magic.OpenInProcess())");
Console.WriteLine("{");
Console.WriteLine(" var detour = magic.DetourManager.Detour(addr, hook);");
Console.WriteLine(" detour.Apply();");
Console.WriteLine();
Console.WriteLine(" var patch = magic.PatchManager.Create(\"Patch\", addr, bytes);");
Console.WriteLine(" patch.Apply();");
Console.WriteLine();
Console.WriteLine(" // ... use hooked functions");
Console.WriteLine("} // End of using: detour.Remove() and patch.Remove() called automatically");
Console.WriteLine("```");
Console.WriteLine();
Console.WriteLine("✓ Original bytes automatically restored on disposal");
}
/// <summary>
/// Run all function hooking examples
/// </summary>
public static void RunAll()
{
Console.WriteLine("╔════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ WhiteMagic Example 4: Function Hooking ║");
Console.WriteLine("╚════════════════════════════════════════════════════════════╝");
SimpleInlineDetour();
DetourWithParameterModification();
DetourWithReturnValueModification();
MultipleDetours();
BytePatching();
NopPatching();
ConditionalPatching();
DetourSafety();
AutomaticRestoration();
Console.WriteLine("\n✓ All function hooking examples completed!");
Console.WriteLine();
Console.WriteLine("⚠️ CRITICAL REMINDERS:");
Console.WriteLine(" - Detours ONLY work in-process (requires injection)");
Console.WriteLine(" - Patches work externally (no injection required)");
Console.WriteLine(" - DetourManager/PatchManager are NOT thread-safe");
Console.WriteLine(" - All hooks/patches auto-restore on disposal");
}
}
@@ -0,0 +1,531 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Linq;
using WhiteMagic;
using WhiteMagic.Windows;
using WhiteMagic.Assembly;
using WhiteMagic.Discovery;
namespace WhiteMagic.Examples;
/// <summary>
/// Example 5: High-Level API
/// Demonstrates RemotePointer, RemoteModule, RemoteFunction, and other high-level APIs
/// </summary>
public class HighLevelAPI
{
/// <summary>
/// Target process for examples
/// </summary>
private static Process? TargetProcess()
{
var processes = Process.GetProcessesByName("notepad");
if (processes.Length > 0)
return processes[0];
Console.WriteLine("No Notepad process found. Please launch Notepad first.");
return null;
}
/// <summary>
/// Example 5.1: RemotePointer (Fluent Pointer Arithmetic)
/// </summary>
public static void RemotePointerExample()
{
Console.WriteLine("=== Example 5.1: RemotePointer (Fluent Pointer Arithmetic) ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
IntPtr baseAddress = magic.Memory.ImageBase;
// Create a RemotePointer to base address
var ptr = magic[baseAddress];
Console.WriteLine($"Created RemotePointer to: 0x{baseAddress:X}");
try
{
// Read different offsets from the same base
int offset1 = ptr.Read<int>(0x1000);
Console.WriteLine($"Read int at offset 0x1000: {offset1}");
float offset2 = ptr.Read<float>(0x2000);
Console.WriteLine($"Read float at offset 0x2000: {offset2}");
// Write to offset
bool writeSuccess = ptr.Write(42, 0x3000);
Console.WriteLine($"Write int to offset 0x3000: {(writeSuccess ? "Success" : "Failed")}");
// Chained pointer reads (pointer → pointer → value)
IntPtr ptr1 = ptr.Read<IntPtr>(0x4000);
if (ptr1 != IntPtr.Zero)
{
var ptr2 = magic[ptr1];
int nestedValue = ptr2.Read<int>(0x50);
Console.WriteLine($"Nested pointer read: 0x{baseAddress:X} → 0x{ptr1:X} → {nestedValue}");
}
}
catch (Exception ex)
{
Console.WriteLine($"✗ Operation failed (offset may be invalid): {ex.Message}");
}
}
/// <summary>
/// Example 5.2: RemoteModule (Module Enumeration)
/// </summary>
public static void RemoteModuleExample()
{
Console.WriteLine("\n=== Example 5.2: RemoteModule (Module Enumeration) ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
// List all loaded modules using Process.Modules
Console.WriteLine("Loaded modules:");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
Console.WriteLine(string.Format("{0,-25} {1,-15} {2,-15}", "Name", "Base Address", "Size"));
Console.WriteLine("─────────────────────────────────────────────────────────────────");
foreach (System.Diagnostics.ProcessModule module in process.Modules)
{
Console.WriteLine(string.Format("{0,-25} 0x{1:X} 0x{2:X}",
module.ModuleName, module.BaseAddress, module.ModuleMemorySize));
}
// Access specific module using WhiteMagic's RemoteModule
var kernel32 = magic["kernel32.dll"];
if (kernel32 != null && kernel32.BaseAddress != IntPtr.Zero)
{
Console.WriteLine();
Console.WriteLine($"✓ Found {kernel32.Name} at 0x{kernel32.BaseAddress:X}");
// Resolve specific exports by name
Console.WriteLine($"\nResolved exports from {kernel32.Name}:");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
var getTickCount = kernel32["GetTickCount"];
if (getTickCount.Address != IntPtr.Zero)
{
Console.WriteLine($"✓ GetTickCount at 0x{getTickCount.Address:X}");
}
var getCurrentProcessId = kernel32["GetCurrentProcessId"];
if (getCurrentProcessId.Address != IntPtr.Zero)
{
Console.WriteLine($"✓ GetCurrentProcessId at 0x{getCurrentProcessId.Address:X}");
}
var messageBoxA = kernel32["MessageBoxA"];
if (messageBoxA.Address != IntPtr.Zero)
{
Console.WriteLine($"✓ MessageBoxA at 0x{messageBoxA.Address:X}");
}
}
else
{
Console.WriteLine("✗ kernel32.dll not found");
}
}
/// <summary>
/// Example 5.3: RemoteFunction (Function Resolution and Execution)
/// </summary>
public static void RemoteFunctionExample()
{
Console.WriteLine("\n=== Example 5.3: RemoteFunction (Function Resolution) ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
try
{
// Resolve function by name
var getTickCount = magic["kernel32.dll"]["GetTickCount"];
if (getTickCount.Address != IntPtr.Zero)
{
Console.WriteLine($"✓ Resolved GetTickCount at: 0x{getTickCount.Address:X}");
// Execute the function
uint ticks = getTickCount.Execute<uint>(CallConvention.Stdcall);
Console.WriteLine($" Result: {ticks} ticks ({TimeSpan.FromMilliseconds(ticks):hh\\:mm\\:ss})");
}
else
{
Console.WriteLine("✗ GetTickCount not found");
}
// Resolve another function
var getCurrentProcessId = magic["kernel32.dll"]["GetCurrentProcessId"];
if (getCurrentProcessId.Address != IntPtr.Zero)
{
Console.WriteLine($"✓ Resolved GetCurrentProcessId at: 0x{getCurrentProcessId.Address:X}");
uint processId = getCurrentProcessId.Execute<uint>(CallConvention.Stdcall);
Console.WriteLine($" Result: Process ID = {processId}");
}
else
{
Console.WriteLine("✗ GetCurrentProcessId not found");
}
// Try to resolve non-existent function
var nonExistent = magic["kernel32.dll"]["NonExistentFunction123"];
if (nonExistent.Address == IntPtr.Zero)
{
Console.WriteLine($"✓ NonExistentFunction123 correctly not found");
}
}
catch (Exception ex)
{
Console.WriteLine($"✗ Function resolution/execution failed: {ex.Message}");
}
}
/// <summary>
/// Example 5.4: ProcessInfo and Module Details
/// Demonstrates accessing process and module metadata through WhiteMagic
/// </summary>
public static void ProcessInfoExample()
{
Console.WriteLine("\n=== Example 5.4: ProcessInfo and Module Details ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
try
{
// Access process metadata
Console.WriteLine("Process Information:");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
Console.WriteLine($"Process ID: {process.Id}");
Console.WriteLine($"Process Name: {process.ProcessName}");
Console.WriteLine($"Main Window Title: {process.MainWindowTitle}");
if (process.MainModule != null)
{
Console.WriteLine($"Main Module Base: 0x{process.MainModule.BaseAddress:X}");
Console.WriteLine($"Main Module Size: 0x{process.MainModule.ModuleMemorySize:X} bytes");
Console.WriteLine($"Main Module Path: {process.MainModule.FileName}");
}
// Enumerate loaded modules
Console.WriteLine("\nLoaded Modules:");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
int moduleCount = 0;
foreach (System.Diagnostics.ProcessModule module in process.Modules)
{
if (moduleCount < 5) // Show first 5 modules
{
Console.WriteLine($" {module.ModuleName,-30} Base: 0x{module.BaseAddress:X16} Size: 0x{module.ModuleMemorySize:X}");
moduleCount++;
}
}
if (process.Modules.Count > 5)
{
Console.WriteLine($" ... and {process.Modules.Count - 5} more modules");
}
// Memory information
Console.WriteLine("\nMemory Information:");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
Console.WriteLine($"Image Base: 0x{magic.Memory.ImageBase:X}");
Console.WriteLine($"Handle: 0x{magic.Memory.Handle.DangerousGetHandle():X}");
Console.WriteLine($"Bitness: {(magic.Memory.Is64Bit ? "64" : "32")} bits");
Console.WriteLine("✓ Process information retrieved successfully");
}
catch (Exception ex)
{
Console.WriteLine($"✗ Process info access failed: {ex.Message}");
}
}
/// <summary>
/// Example 5.5: RemoteWindow (Window Manipulation)
/// </summary>
public static void RemoteWindowExample()
{
Console.WriteLine("\n=== Example 5.5: RemoteWindow (Window Manipulation) ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
try
{
// Get main window handle from Process and create RemoteWindow
if (process.MainWindowHandle != IntPtr.Zero)
{
var mainWindow = new RemoteWindow(process.MainWindowHandle);
Console.WriteLine($"Main Window: {mainWindow.Title}");
Console.WriteLine($"Handle: 0x{mainWindow.Handle:X}");
Console.WriteLine($"Class: {mainWindow.ClassName}");
// Get window rect through WinAPI calls (simplified - would need P/Invoke for full implementation)
Console.WriteLine($"Note: Full window rect information requires additional WinAPI P/Invoke declarations");
// Modify window properties
Console.WriteLine("\nModifying window properties...");
// Flash window
mainWindow.Flash();
Console.WriteLine("✓ Window flashed");
// Activate window
mainWindow.Activate();
Console.WriteLine("✓ Window activated");
// Modify title (temporary)
string originalTitle = mainWindow.Title;
mainWindow.Title = "WhiteMagic Demo!";
Console.WriteLine($"✓ Window title changed to: {mainWindow.Title}");
// Restore original title
System.Threading.Thread.Sleep(1000);
mainWindow.Title = originalTitle;
Console.WriteLine($"✓ Window title restored to: {mainWindow.Title}");
}
else
{
Console.WriteLine("✗ No main window found");
}
}
catch (Exception ex)
{
Console.WriteLine($"✗ Window manipulation failed: {ex.Message}");
}
}
/// <summary>
/// Example 5.6: Module Resolution by Name
/// </summary>
public static void ModuleResolutionExample()
{
Console.WriteLine("\n=== Example 5.6: Module Resolution ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
// Common modules to check
string[] moduleNames = { "kernel32.dll", "user32.dll", "ntdll.dll", "notepad.exe" };
Console.WriteLine("Module resolution:");
Console.WriteLine("─────────────────────────────────────────────────────────────────");
foreach (var moduleName in moduleNames)
{
var module = magic[moduleName];
if (module != null && module.BaseAddress != IntPtr.Zero)
{
Console.WriteLine($"✓ {moduleName,-20} at 0x{module.BaseAddress:X}");
}
else
{
Console.WriteLine($"✗ {moduleName,-20} not found");
}
}
// Get main module using Process.MainModule
if (process.MainModule != null)
{
Console.WriteLine($"\n✓ Main module: {process.MainModule.ModuleName} at 0x{process.MainModule.BaseAddress:X}");
}
}
/// <summary>
/// Example 5.7: Pattern Scanning (Basic)
/// </summary>
public static void PatternScanningExample()
{
Console.WriteLine("\n=== Example 5.7: Pattern Scanning ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
// Get main module for scanning
if (process.MainModule == null)
{
Console.WriteLine("✗ Main module not available");
return;
}
Console.WriteLine($"Scanning for patterns in {process.MainModule.ModuleName}...");
// Example patterns (these are common x64 instruction patterns)
// Note: In real use, you'd use patterns specific to your target
byte[][] patterns =
{
new byte[] { 0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00 }, // mov rax, [rip+disp]
new byte[] { 0xE8, 0x00, 0x00, 0x00, 0x00 }, // call rel32
new byte[] { 0xB8, 0x00, 0x00, 0x00, 0x00 } // mov eax, imm32
};
// Masks: 'x' = match exactly, '?' = wildcard
string[] masks =
{
"xxx????", // mov rax, [rip+disp] - last 4 bytes are displacement (wildcard)
"x????", // call rel32 - displacement is wildcard
"x????" // mov eax, imm32 - immediate is wildcard
};
for (int i = 0; i < patterns.Length; i++)
{
try
{
IntPtr result = PatternScanner.FindInModule(
magic.Memory,
patterns[i],
masks[i],
process.MainModule
);
Console.WriteLine($" Pattern {i + 1}: {(result != IntPtr.Zero ? $" Found at 0x{result:X}" : " Not found")}");
}
catch (Exception ex)
{
Console.WriteLine($" Pattern {i + 1}: ✗ Scan failed - {ex.Message}");
}
}
}
/// <summary>
/// Example 5.8: Cached Pattern Scanning
/// </summary>
public static void CachedPatternScanningExample()
{
Console.WriteLine("\n=== Example 5.8: Cached Pattern Scanning ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
if (process.MainModule == null)
{
Console.WriteLine("✗ Main module not available");
return;
}
// Create a cache for pattern scanning
var cache = new PatternScannerCache(magic.Memory);
Console.WriteLine("Demonstrating cached pattern scanning...");
// Pattern to find
byte[] pattern = { 0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00 };
string mask = "xxx????";
try
{
// First scan (reads memory)
Console.Write(" First scan: ");
IntPtr result1 = cache.FindInModuleCached(pattern, mask, process.MainModule);
Console.WriteLine(result1 != IntPtr.Zero ? $"✓ 0x{result1:X}" : "✗ Not found");
// Second scan (uses cache)
Console.Write(" Second scan (cached): ");
IntPtr result2 = cache.FindInModuleCached(pattern, mask, process.MainModule);
Console.WriteLine(result2 != IntPtr.Zero ? $"✓ 0x{result2:X}" : "✗ Not found");
Console.WriteLine(" ✓ Results match (cache working)");
}
catch (Exception ex)
{
Console.WriteLine($" ✗ Cache scan failed: {ex.Message}");
}
}
/// <summary>
/// Example 5.9: High-Level API Chaining
/// </summary>
public static void HighLevelAPIChaining()
{
Console.WriteLine("\n=== Example 5.9: High-Level API Chaining ===");
var process = TargetProcess();
if (process == null) return;
using var magic = Magic.Open(process);
try
{
// Chain: Module → Function → Execute
var module = magic["kernel32.dll"];
if (module != null && module.BaseAddress != IntPtr.Zero)
{
var function = module["GetTickCount"];
if (function.Address != IntPtr.Zero)
{
uint result = function.Execute<uint>(CallConvention.Stdcall);
Console.WriteLine($"✓ Chained call: magic[\"kernel32.dll\"][\"GetTickCount\"].Execute<uint>() = {result}");
}
}
// Chain: Pointer → Read → Pointer → Read
var basePtr = magic[magic.Memory.ImageBase];
try
{
IntPtr ptr1 = basePtr.Read<IntPtr>(0x1000);
if (ptr1 != IntPtr.Zero)
{
var ptr2 = magic[ptr1];
int value = ptr2.Read<int>(0x50);
Console.WriteLine($"✓ Pointer chain: base → 0x{ptr1:X} → {value}");
}
}
catch
{
Console.WriteLine("✗ Pointer chain: Address not accessible (expected for demo)");
}
// Chain: Window → Title → Length
if (process.MainWindowHandle != IntPtr.Zero)
{
var window = new RemoteWindow(process.MainWindowHandle);
int titleLength = window.Title.Length;
Console.WriteLine($"✓ Window chain: RemoteWindow(MainWindowHandle).Title.Length = {titleLength}");
}
}
catch (Exception ex)
{
Console.WriteLine($"✗ API chaining failed: {ex.Message}");
}
}
/// <summary>
/// Run all high-level API examples
/// </summary>
public static void RunAll()
{
Console.WriteLine("╔════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ WhiteMagic Example 5: High-Level API ║");
Console.WriteLine("╚════════════════════════════════════════════════════════════╝");
RemotePointerExample();
RemoteModuleExample();
RemoteFunctionExample();
ProcessInfoExample();
RemoteWindowExample();
ModuleResolutionExample();
PatternScanningExample();
CachedPatternScanningExample();
HighLevelAPIChaining();
Console.WriteLine("\n✓ All high-level API examples completed!");
}
}
+142
View File
@@ -0,0 +1,142 @@
using System;
using System.Threading.Tasks;
namespace WhiteMagic.Examples;
/// <summary>
/// Main program for WhiteMagic examples
/// Demonstrates all major features of the library
/// </summary>
class Program
{
static async Task Main(string[] args)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine(@"
╔════════════════════════════════════════════════════════════════════════╗
║ ║
║ ████████╗██╗ ██╗██╗ ██████╗███████╗███████╗██╗ ██╗███████╗██╗ ║
║ ╚══██╔══╝██║ ██║██║██╔════╝██╔════╝██╔════╝██║ ██║██╔════╝██║ ║
║ ██║ ██║ ██║██║██║ ███████╗███████╗███████║█████╗ ██║ ║
║ ██║ ██║ ██║██║██║ ╚════██║╚════██║██╔══██║██╔══╝ ██║ ║
║ ██║ ╚██████╔╝██║╚██████╗███████║███████║██║ ██║███████╗███████╗║
║ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝║
║ ║
║ Process Introspection Library for .NET ║
║ ║
╚════════════════════════════════════════════════════════════════════════╝
");
Console.ResetColor();
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(" Examples for Learning WhiteMagic API");
Console.WriteLine(" ─────────────────────────────────────");
Console.ResetColor();
while (true)
{
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Select an example to run:");
Console.ResetColor();
Console.WriteLine(" 1. Basic Memory Operations (Read/Write, Arrays, Strings, Structs)");
Console.WriteLine(" 2. Pattern Scanning (Find patterns in memory)");
Console.WriteLine(" 3. Execution Models (RemoteThread, MainThreadPump, InProcess)");
Console.WriteLine(" 4. Function Hooking (Detours, Patches)");
Console.WriteLine(" 5. High-Level API (RemotePointer, RemoteModule, RemoteWindow, etc.)");
Console.WriteLine(" 6. Run All Examples");
Console.WriteLine(" 0. Exit");
Console.WriteLine();
Console.Write("Enter your choice (0-6): ");
string? input = Console.ReadLine();
if (!int.TryParse(input, out int choice))
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Invalid input. Please enter a number between 0 and 6.");
Console.ResetColor();
continue;
}
Console.WriteLine();
try
{
switch (choice)
{
case 1:
BasicMemoryOperations.RunAll();
break;
case 2:
PatternScanning.RunAll();
break;
case 3:
await ExecutionModels.RunAll();
break;
case 4:
FunctionHooking.RunAll();
break;
case 5:
HighLevelAPI.RunAll();
break;
case 6:
// Run all examples sequentially
BasicMemoryOperations.RunAll();
Console.WriteLine("\n" + new string('=', 70) + "\n");
await Task.Delay(500);
PatternScanning.RunAll();
Console.WriteLine("\n" + new string('=', 70) + "\n");
await Task.Delay(500);
await ExecutionModels.RunAll();
Console.WriteLine("\n" + new string('=', 70) + "\n");
await Task.Delay(500);
FunctionHooking.RunAll();
Console.WriteLine("\n" + new string('=', 70) + "\n");
await Task.Delay(500);
HighLevelAPI.RunAll();
break;
case 0:
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Exiting WhiteMagic Examples. Thank you!");
Console.ResetColor();
return;
default:
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Invalid choice. Please enter a number between 0 and 6.");
Console.ResetColor();
break;
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"\n✗ Error running example: {ex.Message}");
Console.WriteLine($" Stack trace: {ex.StackTrace}");
Console.ResetColor();
}
if (choice != 0)
{
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("Press any key to continue...");
Console.ResetColor();
Console.ReadKey();
Console.Clear();
}
}
}
}
+245
View File
@@ -0,0 +1,245 @@
# WhiteMagic Examples
This directory contains comprehensive examples demonstrating all major features of the WhiteMagic library.
## Prerequisites
- **Target Process**: Most examples use Notepad as the target. Launch Notepad before running the examples.
- **Administrator Privileges**: Some operations require elevated privileges. Run Visual Studio or terminal as Administrator.
- **.NET 8.0 SDK**: Ensure you have .NET 8.0 installed.
## Running the Examples
### From Visual Studio
1. Open `WhiteMagic.slnx` in Visual Studio
2. Set `WhiteMagic.Examples` as the startup project
3. Press F5 to run
### From Command Line
```bash
# Navigate to the examples directory
cd WhiteMagic.Examples
# Run the examples
dotnet run
```
## Example Categories
### 1. Basic Memory Operations (`Example1_BasicMemoryOperations.cs`)
**Demonstrates:**
- Reading and writing primitive types (int, float, double, bool)
- Reading and writing arrays
- Reading and writing strings (ANSI and Unicode)
- Reading and writing raw bytes
- Using `RemotePointer` for fluent pointer arithmetic
- Relative addressing (module-relative offsets)
- Error handling for memory operations
- Working with custom structs
**Key Takeaways:**
- All memory operations go through the `Magic.Memory` API
- Use `RemotePointer` for clean, fluent pointer arithmetic
- Reads throw exceptions on failure; writes return `false`
- Prefer blittable types for best performance
### 2. Pattern Scanning (`Example2_PatternScanning.cs`)
**Demonstrates:**
- Simple pattern scans with wildcards
- Multiple pattern scans in one operation
- Region-specific scanning (e.g., .text section only)
- Finding function signatures
- Cached pattern scanning for performance
- Flexible wildcard patterns
- Signature-based scanning (code caves, NOPs, INT3s)
- Pattern validation
**Key Takeaways:**
- Use IDA-style patterns: `"48 8B ? ? ? ? ?"` where `?` is a wildcard
- Cache results for repeated scans
- Narrow search region when possible for better performance
### 3. Execution Models (`Example3_ExecutionModels.cs`)
**Demonstrates:**
- **RemoteThreadExecutor**: `CreateRemoteThread` for thread-agnostic calls
- **MainThreadPump**: Crash-safe execution for state-sensitive calls
- **InProcessInvoker**: Direct delegates for in-process calls
- Choosing the right execution model
- Combining execution models (inject then switch)
- Error handling for each model
**Key Takeaways:**
- **CRITICAL**: Use the right model for the payload:
- `RemoteThreadExecutor`: Only for thread-agnostic functions (WinAPI, DLL injection)
- `MainThreadPump`: For state-sensitive calls (game state, scripting, UI)
- `InProcessInvoker`: Only after DLL injection
- Using the wrong model crashes the target application
### 4. Function Hooking (`Example4_FunctionHooking.cs`)
**Demonstrates:**
- **Inline Detours**: Function hooking with trampolines
- Detours with parameter modification
- Detours with return value modification
- Multiple detours (chain hooking)
- **Byte Patching**: Named byte patches
- NOP patching (removing instructions)
- Conditional patching (toggle on/off)
- Prologue validation and safety
- Automatic restoration on disposal
**Key Takeaways:**
- Detours ONLY work in-process (requires DLL injection)
- Patches work externally (no injection required)
- `DetourManager`/`PatchManager` are NOT thread-safe
- All hooks and patches auto-restore on disposal
### 5. High-Level API (`Example5_HighLevelAPI.cs`)
**Demonstrates:**
- **RemotePointer**: Fluent pointer arithmetic with `[]` indexing
- **RemoteModule**: Module enumeration and export resolution
- **RemoteFunction**: Function resolution and execution
- **ManagedPeb**/**ManagedTeb**: Typed PEB/TEB access
- **RemoteWindow**: Window manipulation (move, resize, flash, activate)
- Memory allocation and freeing
- Module pattern scanning
- Export function iteration
- Chaining high-level operations
**Key Takeaways:**
- Use high-level APIs for cleaner, more readable code
- `magic["ModuleName"]` returns a `RemoteModule`
- `module["FunctionName"]` returns a `RemoteFunction`
- All high-level operations are built on top of the core memory API
## Common Patterns
### Reading a Nested Structure
```csharp
// 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 for Cleaner Code
```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
```
### Safe Retry Loop for Writes
```csharp
for (int i = 0; i < 5; i++)
{
if (magic.Memory.Write(address, value))
break;
Thread.Sleep(100 * (1 << i)); // Exponential backoff
}
```
### Crash-Safe State Access
```csharp
// WRONG (crashes in most games)
int health = magic.RemoteThread.Execute<int>(fn, CallConvention.Cdecl);
// RIGHT (crash-safe)
var pump = magic.CreateMainThreadPump(frameAddress);
int health = await pump.Enqueue(() => magic.Memory.Read<int>(healthAddress));
```
## Troubleshooting
### "Process is not open for read/write"
**Cause**: Process has exited or handle is invalid
**Solution**: Ensure process is still running and handle is valid
### "Read returns default value"
**Cause**: Address is invalid or memory is protected
**Solution**: Verify address with debugger; check memory protection
### "Write returns false"
**Cause**: Memory is read-only or process has exited
**Solution**: Retry with delay; use `PatchManager` for code patches
### "CreateRemoteThread failed"
**Cause**: Insufficient permissions or target is protected
**Solution**: Run as Administrator; check target process protection
### "Crash when calling target function"
**Cause**: Calling single-threaded function from remote thread
**Solution**: Use `MainThreadPump` instead of `RemoteThreadExecutor`
### "Detour failed: prologue too short"
**Cause**: Function prologue is shorter than minimum (5 bytes)
**Solution**: Hook different function; patch deeper into function
## Further Reading
- [Main README](../README.md) - Overview and quick start
- [Architecture Documentation](../docs/architecture.md) - System design and layer structure
- [Execution Models](../docs/execution-models.md) - Deep dive on execution strategies
- [Function Hooking](../docs/hooking.md) - DetourManager and PatchManager internals
- [Memory Access](../docs/memory-access.md) - MemoryBase and MarshalCache
- [Troubleshooting Guide](../docs/troubleshooting.md) - Common issues and solutions
## Safety Reminders
⚠️ **CRITICAL WARNINGS**:
1. **Execution Model Choice**:
- `RemoteThreadExecutor`: ONLY for thread-agnostic functions
- `MainThreadPump`: For state-sensitive calls (crash-safe)
- `InProcessInvoker`: Only after DLL injection
- **Using the wrong model crashes the target application!**
2. **Detours vs Patches**:
- Detours ONLY work in-process (requires injection)
- Patches work externally (no injection required)
3. **Thread Safety**:
- `DetourManager`/`PatchManager` are NOT thread-safe
- Synchronize concurrent modifications
4. **Handle Management**:
- Always use `using` statements or dispose `Magic` properly
- Leaked handles can cause resource exhaustion
5. **Anti-Cheat Detection**:
- Some operations (e.g., `CreateRemoteThread`) are easily detected
- Use `MainThreadPump` for stealthier operation
## Contributing
Found a bug or have a suggestion? Please open an issue on GitHub.
## License
These examples are part of the WhiteMagic project. See the main LICENSE file for details.
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\WhiteMagic\WhiteMagic.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+1
View File
@@ -1,4 +1,5 @@
<Solution> <Solution>
<Project Path="WhiteMagic.Examples/WhiteMagic.Examples.csproj" />
<Project Path="WhiteMagic/WhiteMagic.csproj" /> <Project Path="WhiteMagic/WhiteMagic.csproj" />
<Project Path="WhiteMagicTest/WhiteMagicTest.csproj" /> <Project Path="WhiteMagicTest/WhiteMagicTest.csproj" />
</Solution> </Solution>
+333
View File
@@ -0,0 +1,333 @@
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using Iced.Intel;
namespace WhiteMagic.Assembly;
/// <summary>
/// Optional <see cref="IAssembler"/> backend that assembles arbitrary x86/x64 mnemonic
/// text to machine code using the Iced library, and provides full instruction-boundary
/// decoding for detour prologue validation.
/// </summary>
/// <remarks>
/// <para>Iced ships a fluent code assembler (typed method calls) and a decoder, but no
/// text parser. This class bridges Intel-syntax text onto Iced's fluent
/// <see cref="Assembler"/> by reflection: each line's mnemonic selects the matching
/// <see cref="Assembler"/> method and its operands are bound to registers, immediates, or
/// labels. Register and immediate operands and label-relative branches are supported;
/// memory operands (<c>[reg+disp]</c>) are not — a caller needing those should emit bytes
/// directly.</para>
/// <para>This backend is entirely optional. Constructing it is the only thing that pulls
/// Iced into a behavioral path; the default <see cref="StubAssembler"/> never references it.</para>
/// </remarks>
public sealed class IcedAssembler : IAssembler
{
private const int DefaultBitness = 64;
private readonly int _bitness;
// Lowercased register name -> boxed AssemblerRegisterNN value, built once from
// Iced's AssemblerRegisters. Enables binding a text operand like "esp" to a typed
// fluent-API register argument.
private static readonly Dictionary<string, object> Registers = BuildRegisterMap();
/// <summary>Creates an assembler for the given bitness (32 or 64).</summary>
/// <param name="bitness">32 for x86, 64 for x64. Defaults to 64.</param>
public IcedAssembler(int bitness = DefaultBitness)
{
if (bitness != 32 && bitness != 64)
throw new ArgumentOutOfRangeException(nameof(bitness), "Bitness must be 32 or 64.");
_bitness = bitness;
}
/// <inheritdoc />
public byte[] Assemble(string assemblyText, ulong origin = 0)
{
ArgumentNullException.ThrowIfNull(assemblyText);
var assembler = new Assembler(_bitness);
List<(string Mnemonic, string[] Operands)> lines = Tokenize(assemblyText, out var labelNames);
// Pre-create every label so a forward branch can reference it before its definition.
var labels = new Dictionary<string, Label>(StringComparer.OrdinalIgnoreCase);
foreach (string name in labelNames)
labels[name] = assembler.CreateLabel(name);
foreach ((string mnemonic, string[] operands) in lines)
{
// A pure label definition (e.g. "loop:") marks the current position.
if (mnemonic.EndsWith(':'))
{
Label label = labels[mnemonic[..^1]];
assembler.Label(ref label);
continue;
}
EmitInstruction(assembler, mnemonic, operands, labels);
}
var writer = new ByteListCodeWriter();
assembler.Assemble(writer, origin);
return writer.Bytes.ToArray();
}
/// <summary>
/// Computes the number of whole prologue-instruction bytes that must be preserved for
/// a splice of <paramref name="requiredBytes"/> bytes, decoding arbitrary instructions
/// (not just the common prologue shapes the built-in decoder covers). Matches the
/// <c>PrologueLengthResolver</c> delegate so it can be assigned to
/// <see cref="WhiteMagic.Hooking.DetourManager.PrologueLengthResolver"/>.
/// </summary>
/// <exception cref="InvalidOperationException">A prologue byte sequence does not decode
/// to a valid instruction.</exception>
public int GetPrologueLength(byte[] prologue, int requiredBytes, bool is64Bit)
{
ArgumentNullException.ThrowIfNull(prologue);
var reader = new ByteArrayCodeReader(prologue);
var decoder = Decoder.Create(is64Bit ? 64 : 32, reader);
int total = 0;
while (total < requiredBytes)
{
decoder.Decode(out Instruction instruction);
if (instruction.IsInvalid)
{
throw new InvalidOperationException(
"The target prologue contains a byte sequence that does not decode to a valid instruction.");
}
total += instruction.Length;
}
return total;
}
private void EmitInstruction(
Assembler assembler,
string mnemonic,
string[] operandText,
Dictionary<string, Label> labels)
{
object?[] operands = new object?[operandText.Length];
for (int i = 0; i < operandText.Length; i++)
operands[i] = ParseOperand(operandText[i], labels);
// Find the fluent Assembler method whose name equals the mnemonic and whose
// parameters bind to the parsed operands.
foreach (MethodInfo method in typeof(Assembler).GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
if (!string.Equals(method.Name, mnemonic, StringComparison.OrdinalIgnoreCase))
continue;
ParameterInfo[] parameters = method.GetParameters();
if (parameters.Length != operands.Length)
continue;
if (TryBind(parameters, operands, out object?[]? boundArgs))
{
try
{
method.Invoke(assembler, boundArgs);
}
catch (TargetInvocationException ex) when (ex.InnerException is not null)
{
// Surface the real Iced failure rather than the reflection wrapper.
throw ex.InnerException;
}
return;
}
}
throw new NotSupportedException(
$"Cannot assemble '{mnemonic}{(operandText.Length > 0 ? " " + string.Join(", ", operandText) : "")}': " +
"no matching Iced assembler overload for the given operands (registers, immediates and " +
"labels are supported; memory operands are not).");
}
private static bool TryBind(ParameterInfo[] parameters, object?[] operands, out object?[]? boundArgs)
{
var args = new object?[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
Type paramType = parameters[i].ParameterType;
object? operand = operands[i];
switch (operand)
{
case Immediate imm when IsNumeric(paramType):
// An immediate that overflows this parameter's type means this overload
// is the wrong width; return false so a wider overload can be tried
// instead of crashing the whole assembly.
if (!TryChangeType(imm.Value, paramType, out object? converted))
{
boundArgs = null;
return false;
}
args[i] = converted;
break;
case not null when paramType.IsInstanceOfType(operand):
args[i] = operand;
break;
default:
boundArgs = null;
return false;
}
}
boundArgs = args;
return true;
}
private static object ParseOperand(string text, Dictionary<string, Label> labels)
{
string token = text.Trim();
if (Registers.TryGetValue(token, out object? register))
return register;
if (labels.TryGetValue(token, out Label label))
return label;
if (TryParseImmediate(token, out object? value))
return new Immediate(value!);
throw new NotSupportedException(
$"Unrecognized operand '{token}' (expected a register, an immediate, or a label).");
}
// Parses an immediate as the narrowest of long/ulong that holds it, boxed. Storing the
// widest representation lets TryChangeType later narrow it to whatever integer parameter
// the chosen overload expects — and reject (rather than crash on) values that do not fit.
private static bool TryParseImmediate(string token, out object? value)
{
value = null;
bool negative = token.StartsWith('-');
string body = negative ? token[1..] : token;
if (body.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
if (!ulong.TryParse(body[2..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong hex))
return false;
value = negative ? -(long)hex : hex;
return true;
}
if (negative)
{
if (!long.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out long signed))
return false;
value = signed;
return true;
}
// Non-negative decimal: prefer long, fall back to ulong for values above long.MaxValue.
if (long.TryParse(body, NumberStyles.Integer, CultureInfo.InvariantCulture, out long asLong))
value = asLong;
else if (ulong.TryParse(body, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong asULong))
value = asULong;
else
return false;
return true;
}
private static bool TryChangeType(object value, Type targetType, out object? result)
{
try
{
result = Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
return true;
}
catch (Exception ex) when (ex is OverflowException or InvalidCastException or FormatException)
{
result = null;
return false;
}
}
private static bool IsNumeric(Type type) => Type.GetTypeCode(type) is
TypeCode.SByte or TypeCode.Byte or TypeCode.Int16 or TypeCode.UInt16 or
TypeCode.Int32 or TypeCode.UInt32 or TypeCode.Int64 or TypeCode.UInt64;
private static List<(string Mnemonic, string[] Operands)> Tokenize(string text, out List<string> labelNames)
{
var result = new List<(string, string[])>();
labelNames = new List<string>();
foreach (string rawLine in text.Split('\n'))
{
string line = rawLine;
int comment = line.IndexOf(';');
if (comment >= 0)
line = line[..comment];
line = line.Trim();
if (line.Length == 0)
continue;
// A "name:" prefix is a label definition; keep any instruction that follows it
// on the same line as a separate entry.
int colon = line.IndexOf(':');
if (colon >= 0)
{
string labelName = line[..colon].Trim();
labelNames.Add(labelName);
result.Add((labelName + ":", Array.Empty<string>()));
line = line[(colon + 1)..].Trim();
if (line.Length == 0)
continue;
}
int space = line.IndexOfAny([' ', '\t']);
if (space < 0)
{
result.Add((line, Array.Empty<string>()));
continue;
}
string mnemonic = line[..space];
string[] operands = line[(space + 1)..]
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
result.Add((mnemonic, operands));
}
return result;
}
private static Dictionary<string, object> BuildRegisterMap()
{
var map = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
foreach (FieldInfo field in typeof(AssemblerRegisters).GetFields(BindingFlags.Public | BindingFlags.Static))
{
object? value = field.GetValue(null);
if (value is not null)
map[field.Name] = value;
}
return map;
}
// A parsed immediate (boxed long or ulong), distinguished from register/label operands
// so binding can narrow it to whichever integer parameter type the chosen overload
// expects — or reject it when it does not fit.
private readonly record struct Immediate(object Value);
private sealed class ByteListCodeWriter : CodeWriter
{
public List<byte> Bytes { get; } = new();
public override void WriteByte(byte value) => Bytes.Add(value);
}
}
+3
View File
@@ -30,8 +30,10 @@ public sealed class StubAssembler : IAssembler
// ── Emit primitives ──────────────────────────────────────────────────── // ── Emit primitives ────────────────────────────────────────────────────
/// <summary>Emits a single byte into the buffer.</summary>
public void EmitU8(List<byte> buffer, byte value) => buffer.Add(value); public void EmitU8(List<byte> buffer, byte value) => buffer.Add(value);
/// <summary>Emits a 32-bit little-endian integer into the buffer.</summary>
public void EmitU32(List<byte> buffer, uint value) public void EmitU32(List<byte> buffer, uint value)
{ {
buffer.Add((byte)value); buffer.Add((byte)value);
@@ -40,6 +42,7 @@ public sealed class StubAssembler : IAssembler
buffer.Add((byte)(value >> 24)); buffer.Add((byte)(value >> 24));
} }
/// <summary>Emits a 64-bit little-endian integer into the buffer.</summary>
public void EmitU64(List<byte> buffer, ulong value) public void EmitU64(List<byte> buffer, ulong value)
{ {
EmitU32(buffer, (uint)value); EmitU32(buffer, (uint)value);
+145
View File
@@ -1,5 +1,6 @@
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using WhiteMagic.Native; using WhiteMagic.Native;
namespace WhiteMagic.Discovery; namespace WhiteMagic.Discovery;
@@ -103,6 +104,150 @@ public sealed class PeHeaderParser
} }
} }
/// <summary>
/// Resolves an exported function's absolute address by name, following export
/// forwarders (e.g. <c>kernel32!HeapAlloc</c> → <c>NTDLL.RtlAllocateHeap</c>) into
/// other modules loaded in the same target process.
/// </summary>
/// <param name="functionName">The exported symbol name (case-sensitive, as stored
/// in the export name table).</param>
/// <returns>The absolute address of the export in the target process.</returns>
/// <remarks>
/// Forwarders are resolved by locating the target module in the process's loaded-module
/// list. API-set forwarders (virtual <c>api-ms-win-*</c> / <c>ext-ms-*</c> names) are NOT
/// supported: those are not real loaded modules, so resolution through the module list is
/// impossible without parsing the API-set schema — such a forwarder throws
/// <see cref="NotSupportedException"/>. On modern Windows many system-DLL exports forward
/// through API sets; resolve those via the OS loader (<c>GetProcAddress</c>) instead.
/// Ordinal forwarders (<c>Module.#N</c>) are likewise unsupported.
/// </remarks>
/// <exception cref="InvalidOperationException">The export is not present.</exception>
/// <exception cref="NotSupportedException">The export forwards to an ordinal or to a
/// module (such as an API set) that is not resolvable from the target's module list.</exception>
/// <exception cref="InvalidDataException">The PE export data is malformed.</exception>
public IntPtr GetExportAddress(string functionName)
{
ArgumentException.ThrowIfNullOrEmpty(functionName);
return ResolveExport(functionName, 0);
}
// Maximum forwarder hops before giving up, to bound pathological chains.
private const int MaxForwarderDepth = 16;
private IntPtr ResolveExport(string functionName, int depth)
{
if (depth > MaxForwarderDepth)
throw new InvalidDataException($"Export forwarder chain for '{functionName}' is too deep.");
var (optionalHeader, _) = ParseOptionalHeader();
if (optionalHeader is null || optionalHeader.Length < 2)
throw new InvalidDataException("Optional header unavailable.");
// 0x10b = PE32 (32-bit), 0x20b = PE32+ (64-bit). Data directories start at a
// different offset in each: 96 for PE32, 112 for PE32+. The export table is
// directory index 0, so its 8-byte entry sits at that offset.
ushort magic = BitConverter.ToUInt16(optionalHeader, 0);
bool pe32Plus = magic == 0x20b;
int exportDirOffset = pe32Plus ? 112 : 96;
if (optionalHeader.Length < exportDirOffset + 8)
throw new InvalidDataException("Optional header does not contain the export data directory.");
uint exportRva = BitConverter.ToUInt32(optionalHeader, exportDirOffset);
uint exportSize = BitConverter.ToUInt32(optionalHeader, exportDirOffset + 4);
if (exportRva == 0 || exportSize == 0)
throw new InvalidOperationException("Module has no export table.");
// IMAGE_EXPORT_DIRECTORY is 40 bytes.
byte[] dir = _memory.ReadBytes(_baseAddress + (nint)exportRva, 40);
if (dir.Length < 40)
throw new InvalidDataException("Failed to read the export directory.");
uint numberOfFunctions = BitConverter.ToUInt32(dir, 20);
uint numberOfNames = BitConverter.ToUInt32(dir, 24);
uint addressOfFunctions = BitConverter.ToUInt32(dir, 28);
uint addressOfNames = BitConverter.ToUInt32(dir, 32);
uint addressOfNameOrdinals = BitConverter.ToUInt32(dir, 36);
// Guard against corrupt counts before allocating arrays sized from them.
if (numberOfNames > 0x10000 || numberOfFunctions > 0x10000)
throw new InvalidDataException("Export table entry count is out of range.");
if (numberOfNames == 0)
throw new InvalidOperationException($"Export '{functionName}' not found (module exports no names).");
byte[] nameRvas = _memory.ReadBytes(_baseAddress + (nint)addressOfNames, checked((int)(numberOfNames * 4)));
byte[] nameOrdinals = _memory.ReadBytes(_baseAddress + (nint)addressOfNameOrdinals, checked((int)(numberOfNames * 2)));
if (nameRvas.Length < numberOfNames * 4 || nameOrdinals.Length < numberOfNames * 2)
throw new InvalidDataException("Failed to read the export name tables.");
int nameIndex = -1;
for (int i = 0; i < numberOfNames; i++)
{
uint nameRva = BitConverter.ToUInt32(nameRvas, i * 4);
string name = _memory.ReadString(_baseAddress + (nint)nameRva, Encoding.ASCII, 512);
if (string.Equals(name, functionName, StringComparison.Ordinal))
{
nameIndex = i;
break;
}
}
if (nameIndex < 0)
throw new InvalidOperationException($"Export '{functionName}' not found in module.");
ushort ordinal = BitConverter.ToUInt16(nameOrdinals, nameIndex * 2);
if (ordinal >= numberOfFunctions)
throw new InvalidDataException("Export name ordinal is out of range.");
byte[] funcRvaBytes = _memory.ReadBytes(
_baseAddress + (nint)(addressOfFunctions + (uint)ordinal * 4u), 4);
if (funcRvaBytes.Length < 4)
throw new InvalidDataException("Failed to read the export address table entry.");
uint funcRva = BitConverter.ToUInt32(funcRvaBytes, 0);
if (funcRva == 0)
throw new InvalidOperationException($"Export '{functionName}' has no address.");
// A function RVA that lands inside the export directory region is not code but a
// null-terminated "Module.Function" forwarder string.
if (funcRva >= exportRva && funcRva < exportRva + exportSize)
{
string forwarder = _memory.ReadString(_baseAddress + (nint)funcRva, Encoding.ASCII, 512);
return ResolveForwarder(forwarder, depth);
}
return _baseAddress + (nint)funcRva;
}
private IntPtr ResolveForwarder(string forwarder, int depth)
{
// A forwarder is "Module.Function"; the module name carries no extension, so the
// FIRST dot is the boundary. Splitting on the last dot would misparse export names
// that themselves contain a dot (e.g. some C++/managed exports).
int dot = forwarder.IndexOf('.');
if (dot <= 0 || dot >= forwarder.Length - 1)
throw new InvalidDataException($"Malformed export forwarder string '{forwarder}'.");
string moduleName = forwarder[..dot];
string target = forwarder[(dot + 1)..];
if (target.StartsWith('#'))
{
throw new NotSupportedException(
$"Ordinal export forwarders are not supported (forwarder '{forwarder}').");
}
IntPtr targetBase = RemoteModule.ResolveBase(_memory.ProcessId, moduleName);
if (targetBase == IntPtr.Zero)
{
throw new NotSupportedException(
$"Export forwarder target module '{moduleName}' is not loaded in the target " +
$"process, or is an unresolvable API set (forwarder '{forwarder}').");
}
return new PeHeaderParser(_memory, targetBase).ResolveExport(target, depth + 1);
}
/// <summary> /// <summary>
/// Parses the DOS header, PE signature, and optional header. /// Parses the DOS header, PE signature, and optional header.
/// </summary> /// </summary>
+10 -2
View File
@@ -16,6 +16,7 @@ namespace WhiteMagic.Hooking;
public sealed class Detour : IDisposable public sealed class Detour : IDisposable
{ {
private readonly MemoryBase _memory; private readonly MemoryBase _memory;
private readonly PrologueLengthResolver _prologueLength;
/// <summary>The unique name of this detour.</summary> /// <summary>The unique name of this detour.</summary>
public string Name { get; } public string Name { get; }
@@ -43,14 +44,21 @@ public sealed class Detour : IDisposable
/// <summary><see langword="true"/> while the detour bytes are live at <see cref="Target"/>.</summary> /// <summary><see langword="true"/> while the detour bytes are live at <see cref="Target"/>.</summary>
public bool IsApplied { get; private set; } public bool IsApplied { get; private set; }
internal Detour(MemoryBase memory, string name, IntPtr target, Delegate hook) internal Detour(
MemoryBase memory,
string name,
IntPtr target,
Delegate hook,
PrologueLengthResolver prologueLength)
{ {
ArgumentNullException.ThrowIfNull(hook); ArgumentNullException.ThrowIfNull(hook);
ArgumentNullException.ThrowIfNull(prologueLength);
_memory = memory; _memory = memory;
Name = name; Name = name;
Target = target; Target = target;
Hook = hook; Hook = hook;
_prologueLength = prologueLength;
} }
/// <summary> /// <summary>
@@ -83,7 +91,7 @@ public sealed class Detour : IDisposable
"Could not read enough bytes from the target function to install a detour."); "Could not read enough bytes from the target function to install a detour.");
} }
int preserveLength = PrologueDecoder.GetWholeInstructionLength(prologue, detourLength, _memory.Is64Bit); int preserveLength = _prologueLength(prologue, detourLength, _memory.Is64Bit);
OverwrittenBytes = new byte[preserveLength]; OverwrittenBytes = new byte[preserveLength];
Buffer.BlockCopy(prologue, 0, OverwrittenBytes, 0, preserveLength); Buffer.BlockCopy(prologue, 0, OverwrittenBytes, 0, preserveLength);
+10 -1
View File
@@ -19,6 +19,15 @@ public sealed class DetourManager
_memory = memory; _memory = memory;
} }
/// <summary>
/// Resolves how many whole prologue-instruction bytes a splice must preserve. Defaults
/// to the built-in <see cref="PrologueDecoder"/>, which covers only the common prologue
/// shapes and rejects anything else. Assign <c>new IcedAssembler().GetPrologueLength</c>
/// to validate arbitrary prologues via the optional Iced disassembler.
/// </summary>
public PrologueLengthResolver PrologueLengthResolver { get; set; } =
PrologueDecoder.GetWholeInstructionLength;
/// <summary> /// <summary>
/// Creates a new detour and registers it with the manager. /// Creates a new detour and registers it with the manager.
/// The <paramref name="hook"/> delegate's type must match the native signature of /// The <paramref name="hook"/> delegate's type must match the native signature of
@@ -26,7 +35,7 @@ public sealed class DetourManager
/// </summary> /// </summary>
public Detour Create(string name, IntPtr target, Delegate hook) public Detour Create(string name, IntPtr target, Delegate hook)
{ {
var detour = new Detour(_memory, name, target, hook); var detour = new Detour(_memory, name, target, hook, PrologueLengthResolver);
_detours[name] = detour; _detours[name] = detour;
return detour; return detour;
} }
+8
View File
@@ -2,6 +2,14 @@ using System;
namespace WhiteMagic.Hooking; namespace WhiteMagic.Hooking;
/// <summary>
/// Resolves how many whole prologue-instruction bytes must be preserved to splice
/// <paramref name="requiredBytes"/> bytes at a target. The built-in
/// <see cref="PrologueDecoder.GetWholeInstructionLength"/> satisfies this delegate, as
/// does <c>IcedAssembler.GetPrologueLength</c> for full instruction coverage.
/// </summary>
public delegate int PrologueLengthResolver(byte[] prologue, int requiredBytes, bool is64Bit);
/// <summary> /// <summary>
/// Minimal instruction-length decoder for common x86/x64 prologue shapes. /// Minimal instruction-length decoder for common x86/x64 prologue shapes.
/// The set is intentionally small: any opcode outside the covered set is rejected /// The set is intentionally small: any opcode outside the covered set is rejected
+4
View File
@@ -54,6 +54,10 @@ public sealed class Magic : IDisposable
/// <summary>Returns a <see cref="RemotePointer"/> at <paramref name="address"/>.</summary> /// <summary>Returns a <see cref="RemotePointer"/> at <paramref name="address"/>.</summary>
public RemotePointer this[IntPtr address] => new RemotePointer(Memory, address); public RemotePointer this[IntPtr address] => new RemotePointer(Memory, address);
/// <summary>Returns the loaded <see cref="RemoteModule"/> named <paramref name="moduleName"/>
/// (e.g. <c>magic["user32"]["MessageBoxA"]</c>).</summary>
public RemoteModule this[string moduleName] => new RemoteModule(this, moduleName);
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
+5 -5
View File
@@ -6,8 +6,8 @@ namespace WhiteMagic;
/// <summary> /// <summary>
/// Caches the widths marshalling decisions for type <typeparamref name="T"/> /// Caches the widths marshalling decisions for type <typeparamref name="T"/>
/// once, at static-constructor time. <see cref="MemoryBase.Read{T}"/> and /// once, at static-constructor time. <see cref="MemoryBase.Read{T}(nint, bool)"/> and
/// <see cref="MemoryBase.Write{T}"/> branch on <see cref="TypeRequiresMarshal"/> /// <see cref="MemoryBase.Write{T}(nint, T, bool)"/> branch on <see cref="TypeRequiresMarshal"/>
/// and pick the appropriate width from this cache. /// and pick the appropriate width from this cache.
/// </summary> /// </summary>
/// <typeparam name="T">The type to cache metadata for.</typeparam> /// <typeparam name="T">The type to cache metadata for.</typeparam>
@@ -24,8 +24,8 @@ public static class MarshalCache<T>
public static readonly int Size; public static readonly int Size;
/// <summary> /// <summary>
/// The unmanaged (interop) width via <see cref="Marshal.SizeOf"/>. The marshal /// The unmanaged (interop) width via <see cref="Marshal.SizeOf(System.Type)"/>. The marshal
/// path (<see cref="Marshal.PtrToStructure"/>/<see cref="Marshal.StructureToPtr"/>) /// path (<see cref="Marshal.PtrToStructure(nint, System.Type)"/>/<see cref="Marshal.StructureToPtr"/>)
/// reads/writes this many bytes. Exceeds <see cref="Size"/> whenever a struct /// reads/writes this many bytes. Exceeds <see cref="Size"/> whenever a struct
/// carries inline unmanaged data that the marshaler expands — inline /// carries inline unmanaged data that the marshaler expands — inline
/// <c>ByValTStr</c>/<c>ByValArray</c> buffers, <c>bool</c> fields (4 bytes per /// <c>ByValTStr</c>/<c>ByValArray</c> buffers, <c>bool</c> fields (4 bytes per
@@ -46,7 +46,7 @@ public static class MarshalCache<T>
/// <summary> /// <summary>
/// <see langword="true"/> when <typeparamref name="T"/> cannot be copied through /// <see langword="true"/> when <typeparamref name="T"/> cannot be copied through
/// the blittable <see cref="System.Runtime.InteropServices.MemoryMarshal"/> path /// the blittable <see cref="System.Runtime.InteropServices.MemoryMarshal"/> path
/// and must fall back to <see cref="Marshal.PtrToStructure"/> / /// and must fall back to <see cref="Marshal.PtrToStructure(nint, System.Type)"/> /
/// <see cref="Marshal.StructureToPtr"/>. This is the case when a top-level field /// <see cref="Marshal.StructureToPtr"/>. This is the case when a top-level field
/// carries <see cref="MarshalAsAttribute"/>, or when <typeparamref name="T"/> /// carries <see cref="MarshalAsAttribute"/>, or when <typeparamref name="T"/>
/// contains a managed reference /// contains a managed reference
+1 -1
View File
@@ -7,7 +7,7 @@ namespace WhiteMagic;
/// <summary> /// <summary>
/// Abstract base for all memory-access readers and writers. Provides typed /// Abstract base for all memory-access readers and writers. Provides typed
/// <see cref="Read{T}"/>/<see cref="Write{T}"/>, array IO, string IO, and /// <see cref="Read{T}(nint, bool)"/>/<see cref="Write{T}(nint, T, bool)"/>, array IO, string IO, and
/// relative/absolute addressing. Subclasses implement the concrete /// relative/absolute addressing. Subclasses implement the concrete
/// <see cref="ReadBytes"/> and <see cref="WriteBytes"/> methods. /// <see cref="ReadBytes"/> and <see cref="WriteBytes"/> methods.
/// </summary> /// </summary>
+73
View File
@@ -0,0 +1,73 @@
using System.Threading.Tasks;
using WhiteMagic.Assembly;
using WhiteMagic.Execution;
namespace WhiteMagic;
/// <summary>
/// An exported function resolved in the target process, obtained via
/// <c>magic["module"]["function"]</c>. Executes through one of the session's execution
/// strategies.
/// </summary>
/// <remarks>
/// The default <see cref="Execute{T}"/> path uses the always-available
/// <see cref="RemoteThreadExecutor"/> (<c>CreateRemoteThread</c>), which is safe for
/// thread-agnostic exports. For a call that touches single-threaded target state, obtain
/// the <see cref="Address"/> and route it through a <see cref="MainThreadPump"/>, or use
/// <see cref="CreateDelegate{TDelegate}"/> when running in-process.
/// </remarks>
public sealed class RemoteFunction
{
private readonly Magic _magic;
/// <summary>The export name this function was resolved from.</summary>
public string Name { get; }
/// <summary>The absolute address of the function in the target process.</summary>
public IntPtr Address { get; }
internal RemoteFunction(Magic magic, string name, IntPtr address)
{
_magic = magic;
Name = name;
Address = address;
}
/// <summary>
/// Calls the function via a remote thread and returns its result cast to
/// <typeparamref name="T"/>.
/// </summary>
/// <param name="convention">The calling convention (ignored on x64 targets).</param>
/// <param name="args">Arguments to pass; primitives, pointers, enums, strings and
/// structs are supported.</param>
public T Execute<T>(CallConvention convention, params object?[] args)
{
return _magic.RemoteThread.Execute<T>(Address, convention, args);
}
/// <summary>Asynchronous variant of <see cref="Execute{T}"/>.</summary>
public Task<T> ExecuteAsync<T>(CallConvention convention, params object?[] args)
{
return _magic.RemoteThread.ExecuteAsync<T>(Address, convention, args);
}
/// <summary>
/// Creates a managed delegate bound to this function for the in-process scenario.
/// </summary>
/// <exception cref="InvalidOperationException">The session is not in-process. The
/// resolved <see cref="Address"/> lives in the target process; a delegate to it would
/// access-violate when invoked from the host, so this is rejected for external sessions.
/// Use <see cref="Execute{T}"/> (remote thread) for external targets.</exception>
public TDelegate CreateDelegate<TDelegate>() where TDelegate : Delegate
{
if (_magic.Memory is not InProcessReader)
{
throw new InvalidOperationException(
"CreateDelegate is only valid for an in-process session (Magic.OpenInProcess). " +
"The function address is not mapped into the host process for an external target; " +
"use Execute<T> to call it via a remote thread.");
}
return new InProcessInvoker(_magic.Memory).CreateFunction<TDelegate>(Address);
}
}
+101
View File
@@ -0,0 +1,101 @@
using WhiteMagic.Discovery;
using Process = System.Diagnostics.Process;
using ProcessModule = System.Diagnostics.ProcessModule;
namespace WhiteMagic;
/// <summary>
/// A module (loaded DLL/EXE image) in the target process, obtained by indexing the
/// facade with a module name (e.g. <c>magic["user32"]</c>). Exposes the module's base
/// address and resolves exported functions by name.
/// </summary>
public sealed class RemoteModule
{
private readonly Magic _magic;
/// <summary>The module's file name as reported by the OS (e.g. <c>user32.dll</c>).</summary>
public string Name { get; }
/// <summary>The module's load address in the target process.</summary>
public IntPtr BaseAddress { get; }
internal RemoteModule(Magic magic, string moduleName)
{
ArgumentNullException.ThrowIfNull(magic);
ArgumentException.ThrowIfNullOrEmpty(moduleName);
_magic = magic;
(string name, IntPtr baseAddress) = FindModule(magic.Memory.ProcessId, moduleName);
Name = name;
BaseAddress = baseAddress;
}
/// <summary>
/// Resolves an exported function by name and returns a <see cref="RemoteFunction"/>
/// bound to its address. Export forwarders are followed.
/// </summary>
public RemoteFunction this[string functionName]
{
get
{
IntPtr address = GetExportAddress(functionName);
return new RemoteFunction(_magic, functionName, address);
}
}
/// <summary>Resolves the absolute address of an exported function by name.</summary>
public IntPtr GetExportAddress(string functionName)
{
ArgumentException.ThrowIfNullOrEmpty(functionName);
var parser = new PeHeaderParser(_magic.Memory, BaseAddress);
return parser.GetExportAddress(functionName);
}
private static (string Name, IntPtr BaseAddress) FindModule(int processId, string moduleName)
{
using Process process = Process.GetProcessById(processId);
foreach (ProcessModule module in process.Modules)
{
if (NameMatches(module.ModuleName, moduleName))
return (module.ModuleName, module.BaseAddress);
}
throw new DllNotFoundException(
$"Module '{moduleName}' is not loaded in process {processId}.");
}
/// <summary>
/// Resolves a module's base address by name within a target process, returning
/// <see cref="IntPtr.Zero"/> if it is not loaded. Used by export-forwarder resolution.
/// </summary>
internal static IntPtr ResolveBase(int processId, string moduleName)
{
using Process process = Process.GetProcessById(processId);
foreach (ProcessModule module in process.Modules)
{
if (NameMatches(module.ModuleName, moduleName))
return module.BaseAddress;
}
return IntPtr.Zero;
}
/// <summary>
/// Matches a loaded module's file name against a requested name, tolerating a missing
/// or present <c>.dll</c> extension and ignoring case (e.g. <c>KERNEL32</c> matches
/// <c>kernel32.dll</c>).
/// </summary>
private static bool NameMatches(string actual, string requested)
{
if (string.Equals(actual, requested, StringComparison.OrdinalIgnoreCase))
return true;
string actualNoExt = Path.GetFileNameWithoutExtension(actual);
string requestedNoExt = requested.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
? requested[..^4]
: requested;
return string.Equals(actualNoExt, requestedNoExt, StringComparison.OrdinalIgnoreCase);
}
}
+10
View File
@@ -7,10 +7,20 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Platforms>x86;x64;AnyCPU</Platforms> <Platforms>x86;x64;AnyCPU</Platforms>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<InternalsVisibleTo Include="WhiteMagicTest" /> <InternalsVisibleTo Include="WhiteMagicTest" />
</ItemGroup> </ItemGroup>
<!--
Optional Iced backend (task 8.x). Isolated behind IAssembler: the default
StubAssembler path never touches Iced, keeping the common configuration free of any
behavioral dependency on it. Only callers that construct IcedAssembler pull it in.
-->
<ItemGroup>
<PackageReference Include="Iced" Version="1.21.0" />
</ItemGroup>
</Project> </Project>
+1
View File
@@ -113,6 +113,7 @@ public sealed class RemoteWindow
return NativeMethods.FlashWindowEx(ref info); return NativeMethods.FlashWindowEx(ref info);
} }
/// <summary>Returns a string representation of this window, including its handle, class name, and title.</summary>
public override string ToString() public override string ToString()
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
@@ -0,0 +1,159 @@
using System.Linq;
using Iced.Intel;
using WhiteMagic;
using WhiteMagic.Assembly;
using WhiteMagic.Hooking;
namespace WhiteMagicTest.Assembly;
/// <summary>
/// Tests for the optional <see cref="IcedAssembler"/> backend (tasks 8.18.3): arbitrary
/// text assembly, origin-relative encoding, and full prologue instruction decoding.
/// </summary>
public class IcedAssemblerTests
{
private static Instruction[] Disassemble(byte[] code, int bitness, ulong origin)
{
var decoder = Decoder.Create(bitness, new ByteArrayCodeReader(code));
decoder.IP = origin;
var result = new List<Instruction>();
ulong end = origin + (ulong)code.Length;
while (decoder.IP < end)
result.Add(decoder.Decode());
return result.ToArray();
}
[Fact]
public void Assemble_emits_single_instruction()
{
var assembler = new IcedAssembler(64);
byte[] code = assembler.Assemble("ret");
Assert.Equal(new byte[] { 0xC3 }, code);
}
[Fact]
public void Assemble_emits_multiple_instructions_with_operands()
{
var assembler = new IcedAssembler(32);
// The scenario from the managed-assembler spec.
byte[] code = assembler.Assemble("push 0\nadd esp, 4\nret");
Assert.NotEmpty(code);
Instruction[] instructions = Disassemble(code, 32, 0);
Assert.Equal(3, instructions.Length);
Assert.Equal(Mnemonic.Push, instructions[0].Mnemonic);
Assert.Equal(Mnemonic.Add, instructions[1].Mnemonic);
Assert.Equal(Register.ESP, instructions[1].Op0Register);
Assert.Equal(4UL, instructions[1].GetImmediate(1));
Assert.Equal(Mnemonic.Ret, instructions[2].Mnemonic);
}
[Fact]
public void Assemble_supports_comments_and_blank_lines()
{
var assembler = new IcedAssembler(64);
byte[] code = assembler.Assemble(" ; prologue\n\nnop ; a comment\nret\n");
Instruction[] instructions = Disassemble(code, 64, 0);
Assert.Equal(2, instructions.Length);
Assert.Equal(Mnemonic.Nop, instructions[0].Mnemonic);
Assert.Equal(Mnemonic.Ret, instructions[1].Mnemonic);
}
[Fact]
public void Assemble_encodes_label_branch_relative_to_origin()
{
var assembler = new IcedAssembler(64);
const ulong origin = 0x1_4000_1000UL;
// jmp forward over a nop to a label; the near-branch target must be resolved
// against the supplied origin, not zero.
byte[] code = assembler.Assemble("jmp done\nnop\ndone:\nret", origin);
Instruction[] instructions = Disassemble(code, 64, origin);
Instruction jmp = instructions[0];
Assert.Equal(Mnemonic.Jmp, jmp.Mnemonic);
// Target = origin + len(jmp) + len(nop): the address of the 'done: ret'.
ulong expected = origin + (ulong)jmp.Length + 1;
Assert.Equal(expected, jmp.NearBranchTarget);
}
[Theory]
[InlineData("mov eax, 4294967295")] // 0xFFFFFFFF — needs the uint overload, not int
[InlineData("mov eax, 0xFFFFFFFF")] // same value, hex form
[InlineData("mov rax, 18446744073709551615")] // ulong.MaxValue — decimal above long.MaxValue
public void Assemble_binds_wide_unsigned_immediates(string source)
{
var assembler = new IcedAssembler(64);
byte[] code = assembler.Assemble(source);
Assert.NotEmpty(code);
Instruction[] instructions = Disassemble(code, 64, 0);
Assert.Single(instructions);
Assert.Equal(Mnemonic.Mov, instructions[0].Mnemonic);
}
[Fact]
public void Assemble_rejects_immediate_that_fits_no_overload_without_crashing()
{
var assembler = new IcedAssembler(64);
// -2147483649 is below int.MinValue and eax has no wider signed overload; must be a
// clean NotSupportedException, not an OverflowException escaping from ChangeType.
Assert.Throws<NotSupportedException>(() => assembler.Assemble("mov eax, -2147483649"));
}
[Fact]
public void Assemble_throws_on_unsupported_operand()
{
var assembler = new IcedAssembler(64);
Assert.Throws<NotSupportedException>(() => assembler.Assemble("mov rax, [rbx]"));
}
[Fact]
public void GetPrologueLength_decodes_prologue_the_builtin_decoder_rejects()
{
// 48 8B C1 = mov rax, rcx — a register-to-register mov the built-in PrologueDecoder
// does not cover (it only recognizes the 8B FF / 8B EC forms).
// Followed by push rbp; mov rbp,rsp; sub rsp,0x20; mov rax,rcx to exceed 14 bytes.
byte[] prologue =
[
0x48, 0x8B, 0xC1, // mov rax, rcx (3)
0x55, // push rbp (1)
0x48, 0x8B, 0xEC, // mov rbp, rsp (3)
0x48, 0x83, 0xEC, 0x20, // sub rsp, 0x20 (4)
0x48, 0x8B, 0xC1 // mov rax, rcx (3) -> total 14
];
// The built-in decoder refuses the very first instruction.
Assert.Throws<InvalidOperationException>(() =>
PrologueDecoder.GetWholeInstructionLength(prologue, 14, is64Bit: true));
// The Iced backend decodes it and returns the whole-instruction length covering
// at least the 14 bytes a detour needs.
var iced = new IcedAssembler();
int length = iced.GetPrologueLength(prologue, 14, is64Bit: true);
Assert.Equal(14, length);
}
[Fact]
public void DetourManager_prologue_resolver_defaults_to_builtin_and_is_replaceable()
{
using var reader = new InProcessReader();
var manager = new DetourManager(reader);
// Default resolver is the built-in decoder.
Assert.Throws<InvalidOperationException>(() =>
manager.PrologueLengthResolver(new byte[] { 0x48, 0x8B, 0xC1, 0x90, 0x90 }, 4, true));
// Swapping in the Iced resolver validates the same bytes.
manager.PrologueLengthResolver = new IcedAssembler().GetPrologueLength;
int length = manager.PrologueLengthResolver(new byte[] { 0x48, 0x8B, 0xC1, 0x90, 0x90 }, 4, true);
Assert.True(length >= 4);
}
}
@@ -35,6 +35,33 @@ public sealed class RemoteThreadExecutorTests
0xC3 0xC3
]; ];
// Five-arg callee that also executes an alignment-sensitive SSE instruction, proving
// the stub delivers a 16-byte-aligned stack the CPU actually accepts (movaps #GPs on a
// misaligned address) alongside correct register+stack argument placement.
// sub rsp, 24 ; entry rsp ≡ 8 (mod 16) -> rsp ≡ 0 (16-aligned), giving a
// ; 16-byte aligned scratch at [rsp..rsp+16) below the saved
// ; return address ([rsp+24]) so the store leaves it intact
// movaps [rsp], xmm0 ; aligned 16-byte store — faults unless rsp is 16-aligned
// add rsp, 24 ; restore
// mov eax, ecx
// add eax, edx
// add eax, r8d
// add eax, r9d
// add eax, [rsp+0x28] ; 5th arg above the shadow space
// ret
private static readonly byte[] SseAlignedSumPayload =
[
0x48, 0x83, 0xEC, 0x18,
0x0F, 0x29, 0x04, 0x24,
0x48, 0x83, 0xC4, 0x18,
0x89, 0xC8,
0x01, 0xD0,
0x44, 0x01, 0xC0,
0x44, 0x01, 0xC8,
0x03, 0x84, 0x24, 0x28, 0x00, 0x00, 0x00,
0xC3
];
// xor eax, eax // xor eax, eax
// cmp byte ptr [rcx+rax], 0 // cmp byte ptr [rcx+rax], 0
// je done // je done
@@ -110,6 +137,21 @@ public sealed class RemoteThreadExecutorTests
Assert.Equal(0, misalign); Assert.Equal(0, misalign);
} }
[Fact]
public void Execute_runs_sse_callee_with_five_args()
{
if (!Environment.Is64BitProcess)
{
return;
}
// Correct result (150) requires BOTH the 5th arg reaching [rsp+0x28] AND the
// aligned movaps not faulting. A broken frame size/alignment either mis-sums or
// #GPs in the callee.
int result = RunPayload(SseAlignedSumPayload, CallConvention.Cdecl, 10, 20, 30, 40, 50);
Assert.Equal(150, result);
}
[Fact] [Fact]
public void Execute_marshals_string_as_utf8_pointer() public void Execute_marshals_string_as_utf8_pointer()
{ {
+121
View File
@@ -0,0 +1,121 @@
using System;
using System.Diagnostics;
using WhiteMagic;
using WhiteMagic.Assembly;
using WhiteMagic.Native;
using Xunit;
namespace WhiteMagicTest;
/// <summary>
/// Tests for <see cref="RemoteModule"/> / <see cref="RemoteFunction"/> resolution and
/// execution through the <see cref="Magic"/> facade (task 7.2).
/// </summary>
public class ModuleFunctionTests
{
// Ensure a module is loaded in this process before resolving it.
private static IntPtr Load(string module)
{
IntPtr handle = NativeMethods.LoadLibrary(module);
Assert.NotEqual(IntPtr.Zero, handle);
return handle;
}
[Fact]
public void Module_indexer_resolves_base_address()
{
IntPtr handle = Load("kernel32.dll");
using var magic = Magic.OpenInProcess();
RemoteModule module = magic["kernel32"];
// The module handle returned by LoadLibrary is the module's base address.
Assert.Equal(handle, module.BaseAddress);
Assert.Equal("KERNEL32.DLL", module.Name, ignoreCase: true);
}
[Fact]
public void Function_indexer_resolves_direct_export()
{
IntPtr handle = Load("user32.dll");
IntPtr expected = NativeMethods.GetProcAddress(handle, "MessageBoxA");
Assert.NotEqual(IntPtr.Zero, expected);
using var magic = Magic.OpenInProcess();
RemoteFunction fn = magic["user32"]["MessageBoxA"];
Assert.Equal(expected, fn.Address);
Assert.Equal("MessageBoxA", fn.Name);
}
[Fact]
public void Function_indexer_follows_export_forwarder()
{
// kernel32!HeapAlloc is a classic forwarder to NTDLL.RtlAllocateHeap. Whatever the
// OS loader resolves it to, our parser must reach the same final address.
IntPtr handle = Load("kernel32.dll");
IntPtr expected = NativeMethods.GetProcAddress(handle, "HeapAlloc");
Assert.NotEqual(IntPtr.Zero, expected);
using var magic = Magic.OpenInProcess();
RemoteFunction fn = magic["kernel32"]["HeapAlloc"];
Assert.Equal(expected, fn.Address);
}
[Fact]
public void Module_indexer_throws_for_unloaded_module()
{
using var magic = Magic.OpenInProcess();
Assert.Throws<DllNotFoundException>(() => magic["definitely-not-loaded-xyz.dll"]);
}
[Fact]
public void Function_indexer_throws_for_unknown_export()
{
Load("kernel32.dll");
using var magic = Magic.OpenInProcess();
Assert.Throws<InvalidOperationException>(() => magic["kernel32"]["NoSuchExport_ZZZ"]);
}
private delegate uint GetCurrentProcessIdDelegate();
[Fact]
public void CreateDelegate_throws_for_external_session()
{
Load("kernel32.dll");
// External reader (even to self): the address is not treated as host-mapped, so a
// delegate to it is rejected rather than handed back to AV on invocation.
using var magic = Magic.Open(Process.GetCurrentProcess());
RemoteFunction fn = magic["kernel32"]["GetCurrentProcessId"];
Assert.Throws<InvalidOperationException>(() => fn.CreateDelegate<GetCurrentProcessIdDelegate>());
}
[Fact]
public void CreateDelegate_invokes_function_in_process()
{
Load("kernel32.dll");
using var magic = Magic.OpenInProcess();
var getPid = magic["kernel32"]["GetCurrentProcessId"].CreateDelegate<GetCurrentProcessIdDelegate>();
Assert.Equal((uint)Process.GetCurrentProcess().Id, getPid());
}
[Fact]
public void Resolved_function_executes_via_remote_thread()
{
Load("kernel32.dll");
using var magic = Magic.OpenInProcess();
RemoteFunction getPid = magic["kernel32"]["GetCurrentProcessId"];
// GetCurrentProcessId takes no args and is thread-agnostic; a remote thread in our
// own process must report our PID.
uint pid = getPid.Execute<uint>(CallConvention.Stdcall);
Assert.Equal((uint)Process.GetCurrentProcess().Id, pid);
}
}
+61
View File
@@ -0,0 +1,61 @@
{
"metadata": [
{
"src": [
{
"files": ["WhiteMagic/**/*.csproj"],
"exclude": ["**/bin/**", "**/obj/**"]
}
],
"dest": "api",
"includePrivateMembers": false,
"monikers": ["net8.0-windows"]
}
],
"build": {
"content": [
{
"files": [
"docs/**/*.md",
"toc.md"
]
},
{
"files": [
"api/**.yml",
"api/index.md"
]
}
],
"resource": [
{
"files": [
"images/**",
"styles/**"
]
}
],
"overwrite": [
{
"files": [
"apidoc/**.md"
]
}
],
"globalMetadata": {
"_appName": "WhiteMagic",
"_appTitle": "WhiteMagic API Reference",
"_enableSearch": true,
"_disableContribution": false,
"pdf": false
},
"fileMetadata": {},
"template": [
"default"
],
"dest": "_site",
"force": false,
"keepFileLink": false,
"warningLevel": "warning"
}
}
+321
View File
@@ -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
+333
View File
@@ -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
View File
@@ -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
+481
View File
@@ -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
+14 -1
View File
@@ -74,7 +74,7 @@ WhiteMagic (facade — BM-old ergonomics)
├─ Core: SafeHandle, native P/Invoke, x64 [BM current] ├─ Core: SafeHandle, native P/Invoke, x64 [BM current]
├─ MemoryBase (abstract Read/Write + MarshalCache) [GreyMagic] ├─ MemoryBase (abstract Read/Write + MarshalCache) [GreyMagic]
│ ├─ ExternalReader (RPM/WPM) │ ├─ ExternalReader (RPM/WPM)
│ └─ InProcessReader (direct deref, injected) │ └─ InProcessReader (RPM/WPM on self-handle, injected)
├─ Discovery: PatternScanner(+cache), PeHeaderParser [BM current + GreyMagic] ├─ Discovery: PatternScanner(+cache), PeHeaderParser [BM current + GreyMagic]
├─ Allocation: AllocatedMemory (named chunks) [GreyMagic] ├─ Allocation: AllocatedMemory (named chunks) [GreyMagic]
├─ Assembler: IAssembler → { HandStubs | Iced } [BM current; Iced replaces FASM] ├─ Assembler: IAssembler → { HandStubs | Iced } [BM current; Iced replaces FASM]
@@ -91,3 +91,16 @@ WhiteMagic (facade — BM-old ergonomics)
``` ```
**Net result**: BM's modern, FASM-free, x64 core + GreyMagic's dual-mode / detour / patch / marshal-cache engine + MemorySharp's high-level ergonomics — with a three-tier execution model whose *default* for state-sensitive calls is the crash-safe main-thread pump, while `CreateRemoteThread` stays available for the payloads it is genuinely safe for. **Net result**: BM's modern, FASM-free, x64 core + GreyMagic's dual-mode / detour / patch / marshal-cache engine + MemorySharp's high-level ergonomics — with a three-tier execution model whose *default* for state-sensitive calls is the crash-safe main-thread pump, while `CreateRemoteThread` stays available for the payloads it is genuinely safe for.
### Deviations discovered during implementation
The design held, but building it surfaced corrections worth recording (each is detailed against its task in `openspec/changes/whitemagic-foundation/tasks.md`):
- **`InProcessReader` reads via RPM/WPM on a self-handle, not `unsafe` direct deref** — .NET cannot catch `AccessViolationException`, so a bad direct 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 (design decision D1, revised mid-Phase 2).
- **`MarshalCache<T>` splits `Size` (managed, blittable) from `MarshalSize` (`Marshal.SizeOf`, marshal path)** — a single size mis-sized structs whose unmanaged width differs (a `bool` field is managed-1 / unmanaged-4; inline `ByValTStr`/`ByValArray` under-sized the marshal buffer and corrupted the heap on write). `MemoryBase` picks per `TypeRequiresMarshal` at every IO site.
- **x64 call stub is fully MS-x64-ABI compliant** — 32-byte shadow space, 16-byte alignment at the inner `call`, full `imm64` register loads (no >4 GiB pointer truncation), stack args above the shadow window. Proven at runtime by a live SSE callee whose aligned `movaps` faults on any misalignment (task 3.8), not just by byte-level encoding tests.
- **`RemoteModule`/`RemoteFunction` follow PE export forwarders** — `kernel32!HeapAlloc``NTDLL.RtlAllocateHeap` and similar resolve into the real target module; ordinal and API-set forwarders throw `NotSupportedException` rather than returning a wrong address (task 7.2).
- **Detour prologue safety is tiered** — the default `StubAssembler` length-decoder covers only the common x86/x64 prologue shapes and refuses any opcode outside that set (zero dependency); the optional `IcedAssembler.GetPrologueLength` decodes arbitrary prologues and is plugged in via `DetourManager.PrologueLengthResolver` when full validation is wanted (tasks 4.6, 8.3).
- **Iced has no text parser** — the design assumed arbitrary text assembly could be delegated to Iced, but Iced ships only a *fluent* code assembler and a decoder. `IcedAssembler.Assemble` bridges Intel-syntax text onto the fluent API by reflection (registers, immediates, labels; memory operands unsupported), rather than depending on a parser that does not exist (task 8.2).
- **Injection bitness corrections** — the thread-hijack injector enforces matching host/target bitness, so the 32-bit path always runs from a 32-bit caller and uses native `GetThreadContext`/`SetThreadContext`; the WOW64 context APIs (for 64-bit callers inspecting WOW64 targets) never apply here and were removed. `ExternalReader` validates `QueryInformation`/`QueryLimitedInformation` access and surfaces `IsWow64Process` failures instead of silently assuming host bitness.
- **Bounds and protection hardening**`AllocatedMemory` range-checks typed IO against region size; `Patch` mirrors the detour's `VirtualProtectEx` dance; `MainThreadPump` guards the completion race on an already-completed `TaskCompletionSource`.
+579
View File
@@ -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).*
@@ -28,7 +28,7 @@
- [x] 3.5 Add tests for stdcall (no caller cleanup), thiscall (ecx = this), fastcall (ecx/edx) x86 stubs - [x] 3.5 Add tests for stdcall (no caller cleanup), thiscall (ecx = this), fastcall (ecx/edx) x86 stubs
- [x] 3.6 Implement x86 stdcall/thiscall/fastcall stubs to pass 3.5 - [x] 3.6 Implement x86 stdcall/thiscall/fastcall stubs to pass 3.5
- [x] 3.7 Add tests for x64 stub argument-register placement and call - [x] 3.7 Add tests for x64 stub argument-register placement and call
- [x] 3.8 Implement x64 stub to pass 3.7. **Deviation (review):** `BuildCallStub` takes `nuint[]` (was `uint[]`). x64 stub is Microsoft-x64-ABI compliant: allocates 32-byte shadow space, keeps 16-byte stack alignment at the inner `call` (frame `K ≡ 8 (mod 16)`, `K ≥ 0x20 + 8·stackArgs`), loads RCX/RDX/R8/R9 with full 64-bit `imm64` (no >4 GiB pointer truncation), and writes stack args above the shadow window (no return-address clobber). x86 rejects args > `uint.MaxValue`. Argument count bounded by `MaxArguments` (256) to keep frame arithmetic overflow-free. **Byte-level tests only — a live-execution test (5-arg + SSE callee via `CreateRemoteThread`) is still needed to prove the ABI at runtime.** - [x] 3.8 Implement x64 stub to pass 3.7. **Deviation (review):** `BuildCallStub` takes `nuint[]` (was `uint[]`). x64 stub is Microsoft-x64-ABI compliant: allocates 32-byte shadow space, keeps 16-byte stack alignment at the inner `call` (frame `K ≡ 8 (mod 16)`, `K ≥ 0x20 + 8·stackArgs`), loads RCX/RDX/R8/R9 with full 64-bit `imm64` (no >4 GiB pointer truncation), and writes stack args above the shadow window (no return-address clobber). x86 rejects args > `uint.MaxValue`. Argument count bounded by `MaxArguments` (256) to keep frame arithmetic overflow-free. **Runtime ABI now proven:** live-execution tests via `CreateRemoteThread` cover 5-arg register+stack delivery (`Execute_sums_register_and_stack_arguments`), 16-byte entry alignment arithmetically (`Execute_delivers_16byte_aligned_stack_to_callee`), and a hardware-alignment-sensitive SSE callee (`Execute_runs_sse_callee_with_five_args`: aligned `movaps` that #GPs unless the stub delivers a 16-byte-aligned stack, combined with a 5th stack arg).
- [x] 3.9 Confirm no FASM/`ManagedFasm` reference exists in `WhiteMagic` output (assert via a test that scans loaded references) - [x] 3.9 Confirm no FASM/`ManagedFasm` reference exists in `WhiteMagic` output (assert via a test that scans loaded references)
## 4. Crash-Safe Execution Slice (spec: remote-execution, function-hooking) ## 4. Crash-Safe Execution Slice (spec: remote-execution, function-hooking)
@@ -38,7 +38,7 @@
- [x] 4.3 Add tests for `DetourManager`/`Detour` in-process: apply redirects, `CallOriginal`, remove restores, named lookup - [x] 4.3 Add tests for `DetourManager`/`Detour` in-process: apply redirects, `CallOriginal`, remove restores, named lookup
- [x] 4.4 Implement `WhiteMagic/Hooking/DetourManager.cs` + `Detour.cs` (inline jmp, x86/x64 form) to pass 4.3 - [x] 4.4 Implement `WhiteMagic/Hooking/DetourManager.cs` + `Detour.cs` (inline jmp, x86/x64 form) to pass 4.3
- [x] 4.5 Add tests for instruction-boundary validation (aligned splice permitted, misaligned rejected when boundary info available) - [x] 4.5 Add tests for instruction-boundary validation (aligned splice permitted, misaligned rejected when boundary info available)
- [x] 4.6 Implement minimal prologue length-decoder in `Detour.Apply` to pass 4.5. Default `StubAssembler` covers ONLY the common x86/x64 prologue shapes — enumerate the covered opcodes in code + XML doc (e.g. `push reg` 0x50-0x57, `mov edi,edi` 8B FF, `push ebp`/`mov ebp,esp` 55 8B EC, `sub esp,imm` 83 EC / 81 EC, REX-prefixed forms). On any opcode outside the set, refuse the splice (do not guess). Full arbitrary-prologue validation is gated on the optional Iced backend (task 8.3) — document that slices 2-5 ship partial boundary safety. - [x] 4.6 Implement minimal prologue length-decoder in `Detour.Apply` to pass 4.5. Default `StubAssembler` covers ONLY the common x86/x64 prologue shapes — enumerate the covered opcodes in code + XML doc (e.g. `push reg` 0x50-0x57, `mov edi,edi` 8B FF, `push ebp`/`mov ebp,esp` 55 8B EC, `sub esp,imm` 83 EC / 81 EC, REX-prefixed forms). On any opcode outside the set, refuse the splice (do not guess). Full arbitrary-prologue validation is gated on the optional Iced backend (task 8.3) — document that slices 2-5 ship partial boundary safety. **Resolved (8.3):** `DetourManager.PrologueLengthResolver` now accepts `IcedAssembler.GetPrologueLength` for full instruction-boundary validation of arbitrary prologues; the built-in decoder remains the zero-dependency default.
- [x] 4.7 Add tests for auto-restore: disposing a `MemoryBase` reverts all active patches and detours - [x] 4.7 Add tests for auto-restore: disposing a `MemoryBase` reverts all active patches and detours
- [x] 4.8 Wire manager registration + `MemoryBase.Dispose` restore to pass 4.7 - [x] 4.8 Wire manager registration + `MemoryBase.Dispose` restore to pass 4.7
- [x] 4.9 Add tests for `MainThreadPump` queue semantics: item runs on hooked thread, result returned, throwing item surfaces exception and pump survives, dispose uninstalls hook (use a self-hosted frame-loop harness in-process) - [x] 4.9 Add tests for `MainThreadPump` queue semantics: item runs on hooked thread, result returned, throwing item surfaces exception and pump survives, dispose uninstalls hook (use a self-hosted frame-loop harness in-process)
@@ -66,7 +66,7 @@
## 7. High-Level Ergonomics (spec: high-level-api) ## 7. High-Level Ergonomics (spec: high-level-api)
- [x] 7.1 Add tests + implement `RemotePointer` indexer (`sharp[addr].Read/Write/Execute` relative to base) - [x] 7.1 Add tests + implement `RemotePointer` indexer (`sharp[addr].Read/Write/Execute` relative to base)
- [ ] 7.2 Add tests + implement `RemoteModule`/`RemoteFunction` (`sharp["mod"]["fn"]`) resolving export addresses and executing via a chosen strategy - [x] 7.2 Add tests + implement `RemoteModule`/`RemoteFunction` (`sharp["mod"]["fn"]`) resolving export addresses and executing via a chosen strategy. **Deviation:** export resolution added to `PeHeaderParser.GetExportAddress` (PE32/PE32+ export directory walk) and **follows export forwarders** (e.g. `kernel32!HeapAlloc``NTDLL.RtlAllocateHeap`) into other loaded modules; ordinal forwarders and unresolvable API-set targets throw `NotSupportedException`. `RemoteModule` resolves the base via `Process.Modules` (name match tolerant of `.dll`/case). `RemoteFunction.Execute<T>` defaults to the always-available `RemoteThreadExecutor`; `Address` is exposed for pump routing and `CreateDelegate<T>` for the in-process tier. Tests cross-check resolved addresses against the OS `GetProcAddress` (direct export + forwarder) and execute `kernel32!GetCurrentProcessId` end-to-end.
- [x] 7.3 Add tests + implement `ManagedPeb`/`ManagedTeb` field reads - [x] 7.3 Add tests + implement `ManagedPeb`/`ManagedTeb` field reads
- [x] 7.4 Add tests + implement `WindowFactory`/`RemoteWindow` (enumerate, move/resize/title/activate/flash, query by class) - [x] 7.4 Add tests + implement `WindowFactory`/`RemoteWindow` (enumerate, move/resize/title/activate/flash, query by class)
- [x] 7.5 Add tests + implement keyboard/mouse simulation (PostMessage + SendInput) to a target window - [x] 7.5 Add tests + implement keyboard/mouse simulation (PostMessage + SendInput) to a target window
@@ -75,13 +75,13 @@
## 8. Optional Iced Backend (spec: managed-assembler) ## 8. Optional Iced Backend (spec: managed-assembler)
- [ ] 8.1 Add `Iced` package reference behind an `IcedAssembler : IAssembler` in a way that keeps the default `StubAssembler` dependency-free - [x] 8.1 Add `Iced` package reference behind an `IcedAssembler : IAssembler` in a way that keeps the default `StubAssembler` dependency-free. Iced 1.21.0 added to `WhiteMagic.csproj`; only constructing `IcedAssembler` pulls it into a behavioral path. `StubAssembler` never references it.
- [ ] 8.2 Add tests + implement `IcedAssembler.Assemble(text, origin)` for arbitrary mnemonics and origin-relative encoding - [x] 8.2 Add tests + implement `IcedAssembler.Assemble(text, origin)` for arbitrary mnemonics and origin-relative encoding. **Deviation:** Iced ships a *fluent* code assembler and a decoder but **no text parser**, so `Assemble` bridges Intel-syntax text onto Iced's `Assembler` by reflection — the mnemonic selects the matching fluent method and operands bind to registers (reflected from `AssemblerRegisters`), immediates, or labels; origin-relative encoding via `Assembler.Assemble(writer, origin)`. Register/immediate/label operands and label-relative branches are supported; **memory operands (`[reg+disp]`) throw `NotSupportedException`** (a caller needing those emits bytes directly). Tests round-trip via Iced's decoder and assert origin-relative branch targets.
- [ ] 8.3 Add tests + wire full prologue instruction-boundary validation (D5) using the Iced disassembler when present - [x] 8.3 Add tests + wire full prologue instruction-boundary validation (D5) using the Iced disassembler when present. `IcedAssembler.GetPrologueLength` decodes arbitrary instructions via Iced's `Decoder`; `DetourManager.PrologueLengthResolver` (a `PrologueLengthResolver` delegate) defaults to the built-in `PrologueDecoder` and is swappable to the Iced resolver, threaded into each `Detour`. Tests prove Iced resolves a prologue (`mov rax,rcx` = `48 8B C1`) the built-in decoder rejects.
## 9. Verification ## 9. Verification
- [x] 9.1 Run full test suite: `dotnet test WhiteMagicTest/WhiteMagicTest.csproj` — all pass (180 pass, 4 integration/interactive skipped) - [x] 9.1 Run full test suite: `dotnet test WhiteMagicTest/WhiteMagicTest.csproj` — all pass (180 pass, 4 integration/interactive skipped)
- [x] 9.2 Run full build (`dotnet build WhiteMagic.slnx`) — zero errors, zero new warnings in `WhiteMagic` - [x] 9.2 Run full build (`dotnet build WhiteMagic.slnx`) — zero errors, zero new warnings in `WhiteMagic`
- [ ] 9.3 Confirm existing BlackMagic/its tests are unchanged and still green - [x] 9.3 Confirm existing BlackMagic/its tests are unchanged and still green. WhiteMagic is a separate, git-ignored project under `reference/` sharing no source or build with BlackMagic; `dotnet test reference/Blackmagic/BlackMagic.slnx` = 17 passing, 0 failing (only pre-existing XML-doc warnings).
- [ ] 9.4 Update `docs/memory-library-comparison.md` "WhiteMagic — synthesis" section with any deviations discovered during implementation - [x] 9.4 Update `docs/memory-library-comparison.md` "WhiteMagic — synthesis" section with any deviations discovered during implementation. Added a "Deviations discovered during implementation" subsection (D1 RPM-on-self, MarshalCache size split, x64 ABI + SSE proof, export forwarders, partial prologue safety, injection bitness, bounds/protection hardening) and corrected the `InProcessReader` line in the architecture diagram.
+30
View File
@@ -0,0 +1,30 @@
# WhiteMagic Documentation
## [Home](index.md)
## Getting Started
- [Installation](docs/installation.md)
- [Quick Start](docs/quick-start.md)
## Conceptual Guides
- [Architecture](docs/architecture.md)
- [Memory Access](docs/memory-access.md)
- [Execution Models](docs/execution-models.md)
- [Function Hooking](docs/hooking.md)
## Examples
- [Example 1: Basic Memory Operations](WhiteMagic.Examples/README.md#example-1-basic-memory-operations)
- [Example 2: Pattern Scanning](WhiteMagic.Examples/README.md#example-2-pattern-scanning)
- [Example 3: Execution Models](WhiteMagic.Examples/README.md#example-3-execution-models)
- [Example 4: Function Hooking](WhiteMagic.Examples/README.md#example-4-function-hooking)
- [Example 5: High-Level API](WhiteMagic.Examples/README.md#example-5-high-level-api)
## API Reference
- [API Documentation](api/index.md)
## Reference & Comparison
- [Library Comparison](docs/memory-library-comparison.md)
- [Troubleshooting](docs/troubleshooting.md)
## Contributing
- [Contributing Guidelines](CONTRIBUTING.md)