Fix API documentation and examples; add comprehensive documentation suite
- Example5: Fix format string bugs (alignment specifier placement, SafeMemoryHandle formatting) - Example4: Fix DetourManager.Detour() → Create() in all doc strings - Example3: Fix MemoryBase.CreateFunction() → InProcessInvoker.CreateFunction() in doc strings - Examples 1-5: Correct all runtime errors and API mismatches vs actual WhiteMagic API - README: Fix DetourManager.Detour() examples to use Create(); add missing code fence markers - Add WhiteMagic.Examples project with 5 comprehensive example files (40+ sub-examples) - Add docfx.json and toc.md for DocFX API reference generation - Add 5 conceptual guides: architecture, memory-access, execution-models, hooking, troubleshooting - Ensure zero errors, zero warnings across all projects (net8.0-windows) Doc strings now teach correct APIs; runtime format bugs eliminated; build succeeds.
This commit is contained in:
@@ -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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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>
|
||||||
|
|||||||
+61
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
# WhiteMagic Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
WhiteMagic is built as a layered architecture that provides multiple levels of abstraction over Windows process-introspection APIs. This design allows consumers to choose the right level of control for their use case, from low-level memory operations to high-level ergonomic APIs.
|
||||||
|
|
||||||
|
## Layer Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Magic (Facade) │
|
||||||
|
│ High-level entry point: Magic.Open(), Magic.OpenInProcess() │
|
||||||
|
└───────────────────────┬──────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────┴───────────────┐
|
||||||
|
│ │
|
||||||
|
┌───────▼─────────┐ ┌────────▼────────┐
|
||||||
|
│ MemoryBase │ │ High-Level API │
|
||||||
|
│ (Abstract) │ │ │
|
||||||
|
├─────────────────┤ ├─────────────────┤
|
||||||
|
│ ExternalReader │ │ RemotePointer │
|
||||||
|
│ InProcessReader │ │ RemoteModule │
|
||||||
|
│ │ │ RemoteFunction │
|
||||||
|
│ ┌──────────────┐│ │ ManagedPeb/TEB │
|
||||||
|
│ │ Hooking ││ │ Window/Input │
|
||||||
|
│ │ DetourMgr ││ └─────────────────┘
|
||||||
|
│ │ PatchMgr ││
|
||||||
|
│ └──────────────┘│
|
||||||
|
└─────────┬───────┘
|
||||||
|
│
|
||||||
|
┌─────▼─────┬───────────┬───────────┐
|
||||||
|
│ │ │ │
|
||||||
|
┌───▼────┐ ┌────▼──┐ ┌──────▼──┐ ┌──────▼───┐
|
||||||
|
│Native │ │Discov.│ │Execution│ │Assembly │
|
||||||
|
│P/Invoke│ │Scan/PE│ │3-tier │ │IAssembler│
|
||||||
|
└────────┘ └───────┘ └─────────┘ └──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Layer
|
||||||
|
|
||||||
|
### MemoryBase (Abstract)
|
||||||
|
|
||||||
|
The foundation of WhiteMagic is the `MemoryBase` abstract class, which defines the contract for all memory operations:
|
||||||
|
|
||||||
|
**Key responsibilities:**
|
||||||
|
- Abstract memory read/write operations
|
||||||
|
- Host for `MarshalCache<T>` optimization
|
||||||
|
- Owner of `PatchManager` and `DetourManager`
|
||||||
|
- Relative/absolute addressing support
|
||||||
|
|
||||||
|
**Design decision (D1):** Dual-mode readers share the same abstract interface, allowing high-level code (pattern scanning, patching, etc.) to work in both external and in-process modes without changes.
|
||||||
|
|
||||||
|
### ExternalReader
|
||||||
|
|
||||||
|
Implements `MemoryBase` for out-of-process operations:
|
||||||
|
|
||||||
|
**Mechanism:** Uses `ReadProcessMemory`/`WriteProcessMemory` via P/Invoke
|
||||||
|
**Handle:** `SafeMemoryHandle` to target process
|
||||||
|
**Use case:** Primary mode for automation hosts
|
||||||
|
|
||||||
|
### InProcessReader
|
||||||
|
|
||||||
|
Implements `MemoryBase` for injected code:
|
||||||
|
|
||||||
|
**Mechanism:** Uses `ReadProcessMemory`/`WriteProcessMemory` on self-handle
|
||||||
|
**Handle:** `SafeMemoryHandle` to current process
|
||||||
|
**Use case:** Once a managed DLL is injected, enables delegate calls and detours
|
||||||
|
|
||||||
|
**Design decision (D1 revision):** InProcessReader uses RPM-on-self instead of unsafe direct pointer deref because .NET cannot catch `AccessViolationException`, so a bad deref kills the host with no soft-failure path. The in-process speed win moves to the delegate-call and detour paths, not the reader.
|
||||||
|
|
||||||
|
## Discovery Layer
|
||||||
|
|
||||||
|
### PatternScanner
|
||||||
|
|
||||||
|
Scans target memory for byte patterns with wildcard support:
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- IDA-style hex patterns (`48 8B ? ? ? ? ?`)
|
||||||
|
- Optional caching to avoid repeated scans
|
||||||
|
- Module-relative or absolute addressing
|
||||||
|
|
||||||
|
### PeHeaderParser
|
||||||
|
|
||||||
|
Parses PE headers to extract export information:
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Export table parsing
|
||||||
|
- Forwarder resolution (e.g., `kernel32!HeapAlloc` → `NTDLL.RtlAllocateHeap`)
|
||||||
|
- Ordinal export support
|
||||||
|
|
||||||
|
## Execution Layer (Three-Tier Model)
|
||||||
|
|
||||||
|
### RemoteThreadExecutor
|
||||||
|
|
||||||
|
**Mechanism:** `CreateRemoteThread` + hand-assembled convention stubs
|
||||||
|
**Use for:** Thread-agnostic payloads (pure WinAPI, self-contained code, DLL injection)
|
||||||
|
**Risk:** NOT crash-safe for single-threaded target state
|
||||||
|
|
||||||
|
### MainThreadPump
|
||||||
|
|
||||||
|
**Mechanism:** Detour on per-frame function + work queue
|
||||||
|
**Use for:** State-sensitive calls (default for target-state access)
|
||||||
|
**Safety:** Crash-safe — runs on target's own thread
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
1. Installs a detour on a per-frame function (e.g., D3D `EndScene`)
|
||||||
|
2. Each frame, the hook drains a thread-safe queue
|
||||||
|
3. Work items run synchronously in target's context
|
||||||
|
4. Results/exceptions returned via `TaskCompletionSource`
|
||||||
|
|
||||||
|
### InProcessInvoker
|
||||||
|
|
||||||
|
**Mechanism:** `Marshal.GetDelegateForFunctionPointer`
|
||||||
|
**Use for:** In-process delegates after injection
|
||||||
|
**Benefit:** Zero thread-crossing overhead
|
||||||
|
|
||||||
|
## Hooking Layer
|
||||||
|
|
||||||
|
### DetourManager
|
||||||
|
|
||||||
|
**Scope:** In-process only
|
||||||
|
**Features:**
|
||||||
|
- Inline `E9 jmp` detours over function prologues
|
||||||
|
- `CallOriginal` support via trampoline
|
||||||
|
- `Apply`/`Remove` operations
|
||||||
|
- Auto-restore on `Dispose`
|
||||||
|
|
||||||
|
**Safety:** Prologue validation via `IAssembler.GetPrologueLength` (optional Iced backend)
|
||||||
|
|
||||||
|
### PatchManager
|
||||||
|
|
||||||
|
**Scope:** Both external and in-process
|
||||||
|
**Features:**
|
||||||
|
- Named byte patches
|
||||||
|
- `Apply`/`Remove`/`IsApplied`
|
||||||
|
- Auto-restore on `Dispose`
|
||||||
|
|
||||||
|
## Assembly Layer
|
||||||
|
|
||||||
|
### IAssembler Seam
|
||||||
|
|
||||||
|
Abstracts text → machine code generation:
|
||||||
|
|
||||||
|
**Implementations:**
|
||||||
|
- `StubAssembler` (default): Hand-emits calling-convention stubs, no dependency
|
||||||
|
- `IcedAssembler` (optional): Wraps Iced for arbitrary assembly
|
||||||
|
|
||||||
|
**Design decision (D3):** Keeps default configuration dependency-free; only callers needing arbitrary asm opt into Iced.
|
||||||
|
|
||||||
|
### Convention Stubs
|
||||||
|
|
||||||
|
`StubAssembler` emits x86/x64 calling-convention trampolines:
|
||||||
|
|
||||||
|
**Conventions supported:** cdecl, stdcall, thiscall, fastcall, x64 (Microsoft)
|
||||||
|
**Encoding:** Deterministic byte emission, no parsing required
|
||||||
|
|
||||||
|
## High-Level Layer
|
||||||
|
|
||||||
|
### RemotePointer
|
||||||
|
|
||||||
|
Indexer-based pointer arithmetic:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var ptr = magic[baseAddress];
|
||||||
|
int value = ptr.Read<int>(offset);
|
||||||
|
ptr.Write(999, offset);
|
||||||
|
```
|
||||||
|
|
||||||
|
### RemoteModule/RemoteFunction
|
||||||
|
|
||||||
|
Module and export resolution:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var module = magic["user32"];
|
||||||
|
var fn = module["MessageBoxA"];
|
||||||
|
int result = fn.Execute<int>(CallConvention.Stdcall, args...);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ManagedPeb/ManagedTeb
|
||||||
|
|
||||||
|
Managed views of Process Environment Block and Thread Environment Block:
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Typed field access
|
||||||
|
- No manual structure marshalling
|
||||||
|
|
||||||
|
### Window/Input
|
||||||
|
|
||||||
|
Window mutation and input simulation:
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Move/resize/title/activate/flash windows
|
||||||
|
- Keyboard/mouse input without focus (where supported)
|
||||||
|
|
||||||
|
## Optimization Layer
|
||||||
|
|
||||||
|
### MarshalCache<T>
|
||||||
|
|
||||||
|
Per-type metadata caching:
|
||||||
|
|
||||||
|
**Cached data:**
|
||||||
|
- `Size` (managed blittable width)
|
||||||
|
- `MarshalSize` (unmanaged interop width)
|
||||||
|
- `TypeRequiresMarshal` (needs `Marshal.PtrToStructure`)
|
||||||
|
- `IsIntPtr` (for special handling)
|
||||||
|
|
||||||
|
**Performance:** Avoids per-call reflection and `Marshal.SizeOf` overhead
|
||||||
|
|
||||||
|
## Thread Safety
|
||||||
|
|
||||||
|
### ExternalReader
|
||||||
|
|
||||||
|
**Thread-safe:** Yes (RPM/WPM are thread-safe)
|
||||||
|
**Synchronization:** None required
|
||||||
|
|
||||||
|
### InProcessReader
|
||||||
|
|
||||||
|
**Thread-safe:** Yes (RPM-on-self is thread-safe)
|
||||||
|
**Synchronization:** None required
|
||||||
|
|
||||||
|
### MainThreadPump
|
||||||
|
|
||||||
|
**Thread-safe:** Yes (uses `ConcurrentQueue`)
|
||||||
|
**Synchronization:** Lock-free queue + `TaskCompletionSource`
|
||||||
|
|
||||||
|
### DetourManager/PatchManager
|
||||||
|
|
||||||
|
**Thread-safe:** No
|
||||||
|
**Synchronization:** Caller must synchronize
|
||||||
|
|
||||||
|
## Lifetime Management
|
||||||
|
|
||||||
|
All disposable resources follow RAII:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var magic = Magic.Open(process);
|
||||||
|
// All handles, patches, and detours auto-restore on disposal
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resources cleaned up:**
|
||||||
|
- Process handles (`SafeMemoryHandle`)
|
||||||
|
- Applied patches (`PatchManager`)
|
||||||
|
- Active detours (`DetourManager`)
|
||||||
|
- Allocated memory (`AllocatedMemory`)
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
**Strategy:** Explicit failures, silent success
|
||||||
|
|
||||||
|
- **Read operations:** Throw on failure (Win32Exception)
|
||||||
|
- **Write operations:** Return `false` on failure
|
||||||
|
- **String reads:** Return `string.Empty` on failure
|
||||||
|
- **Invalid handles:** Throw `InvalidOperationException`
|
||||||
|
|
||||||
|
**Rationale:** Writes to another process can legitimately fail (protection changed, process exited); silent retry is often the right strategy. Reads are typically expected to succeed, so exceptions surface the problem immediately.
|
||||||
|
|
||||||
|
## Extensibility Points
|
||||||
|
|
||||||
|
### IAssembler
|
||||||
|
|
||||||
|
Plug in custom assemblers:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IAssembler
|
||||||
|
{
|
||||||
|
byte[] Assemble(string assemblyText, ulong origin = 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DetourManager.PrologueLengthResolver
|
||||||
|
|
||||||
|
Custom prologue validation:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
magic.DetourManager.PrologueLengthResolver = (address, minBytes) =>
|
||||||
|
// Custom validation logic
|
||||||
|
return requiredLength;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
### Memory Access
|
||||||
|
|
||||||
|
| Operation | Cost | Notes |
|
||||||
|
|-----------|------|-------|
|
||||||
|
| `Read<T>` (blittable) | Low | `MemoryMarshal.Read`, no alloc |
|
||||||
|
| `Read<T>` (marshalled) | Medium | `Marshal.PtrToStructure` |
|
||||||
|
| `Read<T>` (array) | Medium-High | Per-element marshalling |
|
||||||
|
| `ReadBytes` | Low | Direct buffer copy |
|
||||||
|
| `ReadString` | Medium | Encoding + allocation |
|
||||||
|
|
||||||
|
### Execution
|
||||||
|
|
||||||
|
| Method | Latency | Safety |
|
||||||
|
|--------|---------|--------|
|
||||||
|
| `RemoteThreadExecutor` | ~1-2ms | Thread-agnostic only |
|
||||||
|
| `MainThreadPump` | ~1 frame (16-33ms) | Crash-safe |
|
||||||
|
| `InProcessInvoker` | <1μs | In-process only |
|
||||||
|
|
||||||
|
### Pattern Scanning
|
||||||
|
|
||||||
|
| Mode | Cost | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| Uncached | High | Scans entire module |
|
||||||
|
| Cached | Low | O(1) after first scan |
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **Safety by default:** MainThreadPump prevents crashes from thread-affinity violations
|
||||||
|
2. **Bitness-agnostic:** Works on x86 and x64 without code changes
|
||||||
|
3. **No native dependencies:** Default configuration is pure managed
|
||||||
|
4. **Explicit operations:** Clear failure modes, no hidden retries
|
||||||
|
5. **Resource safety:** RAII-based cleanup prevents leaks
|
||||||
|
6. **Extensibility:** Seam points for assemblers, validators
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- [Execution Models](./execution-models.md) — Deep dive on the three-tier execution strategy
|
||||||
|
- [Memory Access](./memory-access.md) — MemoryBase readers and MarshalCache optimization
|
||||||
|
- [Function Hooking](./hooking.md) — DetourManager and PatchManager internals
|
||||||
|
- [Assembly Seam](./assembly-seam.md) — IAssembler abstraction and Iced integration
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
# Execution Models in WhiteMagic
|
||||||
|
|
||||||
|
WhiteMagic provides three distinct execution strategies, each designed for specific payload safety requirements. Choosing the right model is critical for avoiding crashes and ensuring reliable operation.
|
||||||
|
|
||||||
|
## The Problem: Thread-Affinity Crashes
|
||||||
|
|
||||||
|
When automating or debugging a target application, the most common failure mode is calling target functions on the wrong thread. Many applications (especially games, UI apps, and applications with scripting engines) have **single-threaded state**:
|
||||||
|
|
||||||
|
- Scripting VMs (Lua, Python, custom engines)
|
||||||
|
- Render contexts (Direct3D, OpenGL)
|
||||||
|
- Object models and state machines
|
||||||
|
- UI message pumps
|
||||||
|
|
||||||
|
If you call these functions from a thread you created via `CreateRemoteThread`, they race the target's main thread → memory corruption → crash.
|
||||||
|
|
||||||
|
**WhiteMagic's solution:** Split execution by payload safety, making the crash-safe path the default.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Model 1: RemoteThreadExecutor (CreateRemoteThread)
|
||||||
|
|
||||||
|
### Mechanism
|
||||||
|
|
||||||
|
Creates a new thread in the target process via `CreateRemoteThread`, executes a call stub, and waits for the exit code.
|
||||||
|
|
||||||
|
### Use Cases ✅
|
||||||
|
|
||||||
|
**SAFE for thread-agnostic payloads:**
|
||||||
|
- Pure WinAPI calls (`GetTickCount`, `GetCurrentProcessId`)
|
||||||
|
- Self-contained computations
|
||||||
|
- `LoadLibrary` (DLL injection)
|
||||||
|
- Code that touches only memory you own
|
||||||
|
|
||||||
|
### Avoid ❌
|
||||||
|
|
||||||
|
**UNSAFE for single-threaded target state:**
|
||||||
|
- Scripting engine entry points
|
||||||
|
- Render operations (Direct3D calls)
|
||||||
|
- Game state queries/modifications
|
||||||
|
- UI interactions
|
||||||
|
- Object model traversal
|
||||||
|
|
||||||
|
### Usage Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using WhiteMagic;
|
||||||
|
using WhiteMagic.Assembly;
|
||||||
|
|
||||||
|
using var magic = Magic.Open(targetProcess);
|
||||||
|
|
||||||
|
// Example: Call GetTickCount (thread-safe WinAPI)
|
||||||
|
var getTickCount = magic["kernel32"]["GetTickCount"];
|
||||||
|
uint ticks = getTickCount.Execute<uint>(CallConvention.Stdcall);
|
||||||
|
|
||||||
|
// Example: Call a function that doesn't touch thread-local state
|
||||||
|
int result = magic.RemoteThread.Execute<int>(
|
||||||
|
functionAddress,
|
||||||
|
CallConvention.Cdecl,
|
||||||
|
arg1, arg2, arg3
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- **Latency:** ~1-2ms (thread creation + execution + join)
|
||||||
|
- **Throughput:** Limited by thread creation overhead
|
||||||
|
- **Best for:** One-shot calls, initialization, DLL injection
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
|
||||||
|
- **Crash risk:** HIGH if touching single-threaded state
|
||||||
|
- **Detection:** Easily detected by anti-cheat (new thread creation)
|
||||||
|
- **Overhead:** Thread creation is not cheap
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Model 2: MainThreadPump (Crash-Safe) ⭐
|
||||||
|
|
||||||
|
### Mechanism
|
||||||
|
|
||||||
|
Installs a detour on a **per-frame function** (a function called every frame, like D3D's `EndScene`) and drains a thread-safe work queue there each frame.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. **Detour installation:** Hook a per-frame function at `frameAddress`
|
||||||
|
2. **Queue enqueue:** Caller enqueues a delegate via `Enqueue<T>()`
|
||||||
|
3. **Frame execution:** Target's main thread runs the hook each frame
|
||||||
|
4. **Queue drain:** Hook checks queue, executes pending work items
|
||||||
|
5. **Result return:** `TaskCompletionSource` delivers result/exception
|
||||||
|
|
||||||
|
### Use Cases ✅
|
||||||
|
|
||||||
|
**SAFE for state-sensitive calls:**
|
||||||
|
- Game state modifications (health, position, inventory)
|
||||||
|
- Scripting engine calls
|
||||||
|
- UI interactions
|
||||||
|
- Render operations
|
||||||
|
- Object model traversal
|
||||||
|
- **Any function that assumes main-thread context**
|
||||||
|
|
||||||
|
### Usage Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using WhiteMagic;
|
||||||
|
using WhiteMagic.Execution;
|
||||||
|
|
||||||
|
using var magic = Magic.Open(targetProcess);
|
||||||
|
|
||||||
|
// Create a pump that hooks a per-frame function
|
||||||
|
// (e.g., D3D9 EndScene, or any per-frame handler)
|
||||||
|
var pump = magic.CreateMainThreadPump(frameAddress);
|
||||||
|
|
||||||
|
// Enqueue work that runs on the target's main thread
|
||||||
|
int health = await pump.Enqueue(() =>
|
||||||
|
{
|
||||||
|
// Safe to touch game state here
|
||||||
|
return magic.Memory.Read<int>(healthAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modify game state safely
|
||||||
|
await pump.Enqueue(() =>
|
||||||
|
{
|
||||||
|
magic.Memory.Write(healthAddress, 999);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call a function that requires main-thread context
|
||||||
|
var result = await pump.Enqueue(() =>
|
||||||
|
{
|
||||||
|
var fn = magic["target"]["ProcessInput"];
|
||||||
|
return fn.Execute<int>(CallConvention.ThisCall, inputPtr);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finding a Frame Function
|
||||||
|
|
||||||
|
**Common per-frame functions:**
|
||||||
|
- Direct3D 9: `EndScene` (device + 0x44 vtable entry)
|
||||||
|
- Direct3D 11: Present callbacks
|
||||||
|
- OpenGL: SwapBuffers callbacks
|
||||||
|
- Custom: Many games have a `Update` or `Render` function per frame
|
||||||
|
|
||||||
|
**Helper for D3D9:**
|
||||||
|
```csharp
|
||||||
|
// Find D3D9 device and resolve EndScene
|
||||||
|
IntPtr d3dDevice = FindD3D9Device(magic);
|
||||||
|
IntPtr endScene = magic.Memory.Read<IntPtr>(d3dDevice + 0x44); // VTable
|
||||||
|
|
||||||
|
var pump = magic.CreateMainThreadPump(endScene);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- **Latency:** ~1 frame (16-33ms at 30-60 FPS)
|
||||||
|
- **Throughput:** Limited by frame rate and work item duration
|
||||||
|
- **Best for:** Repeated state-sensitive calls, game mods, automation
|
||||||
|
|
||||||
|
### Safety Features
|
||||||
|
|
||||||
|
- **Crash-safe:** Runs on target's own thread
|
||||||
|
- **Exception propagation:** Exceptions in work items propagate to caller
|
||||||
|
- **Timeout handling:** Can detect wedged work items
|
||||||
|
- **Queue bounded:** Prevents unlimited queue growth
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
|
||||||
|
- **Frame overhead:** Hook adds per-frame overhead (keep work items short)
|
||||||
|
- **Wedged work item:** A stuck work item stalls the frame (detectable via timeout)
|
||||||
|
- **Frame function needed:** Requires finding a per-frame function (application-specific)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Model 3: InProcessInvoker (Direct Delegates)
|
||||||
|
|
||||||
|
### Mechanism
|
||||||
|
|
||||||
|
Once a managed DLL is injected into the target, creates native delegates via `Marshal.GetDelegateForFunctionPointer` and calls them directly.
|
||||||
|
|
||||||
|
### Use Cases ✅
|
||||||
|
|
||||||
|
**When injected in-process:**
|
||||||
|
- Direct function calls with zero thread crossing
|
||||||
|
- High-performance repeated calls
|
||||||
|
- Full .NET interop capabilities
|
||||||
|
|
||||||
|
### Usage Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using WhiteMagic;
|
||||||
|
using WhiteMagic.Execution;
|
||||||
|
|
||||||
|
// Only works when injected in-process
|
||||||
|
using var magic = Magic.OpenInProcess();
|
||||||
|
|
||||||
|
// Create a delegate to a native function
|
||||||
|
var getName = magic.Memory.CreateFunction<GetNameDelegate>(
|
||||||
|
getNameAddress
|
||||||
|
);
|
||||||
|
|
||||||
|
// Call directly as a delegate
|
||||||
|
string name = getName(12345);
|
||||||
|
|
||||||
|
// Delegate signature
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
public delegate string GetNameDelegate(int id);
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
- **After injection:** Your managed DLL is already in the target
|
||||||
|
- **Performance-critical:** Need sub-microsecond call latency
|
||||||
|
- **Complex interop:** Need to pass complex structures or callbacks
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- **Latency:** <1μs (direct function call)
|
||||||
|
- **Throughput:** Highest (no thread crossing)
|
||||||
|
- **Best for:** In-process tools, profilers, injected helpers
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
- **In-process only:** Requires managed DLL injection
|
||||||
|
- **No crash-safety benefit:** Still subject to thread-affinity issues
|
||||||
|
- **Requires loader:** Need a CLR host or injection bootstrapper
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Choosing the Right Model
|
||||||
|
|
||||||
|
### Decision Flowchart
|
||||||
|
|
||||||
|
```
|
||||||
|
Are you injected in-process?
|
||||||
|
├─ Yes → Use InProcessInvoker (direct delegates)
|
||||||
|
└─ No → Does the call touch single-threaded state?
|
||||||
|
├─ Yes → Use MainThreadPump (crash-safe)
|
||||||
|
└─ No → Use RemoteThreadExecutor (CreateRemoteThread)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Practical Guidelines
|
||||||
|
|
||||||
|
| Scenario | Model | Reason |
|
||||||
|
|----------|-------|--------|
|
||||||
|
| DLL injection | RemoteThreadExecutor | `LoadLibrary` is thread-safe |
|
||||||
|
| Read health bar | MainThreadPump | Game state is main-thread-affine |
|
||||||
|
| Call GetTickCount | RemoteThreadExecutor | WinAPI, no thread affinity |
|
||||||
|
| Script engine call | MainThreadPump | Script VM is main-thread-only |
|
||||||
|
| Injected profiling | InProcessInvoker | Already in-process, max performance |
|
||||||
|
| Window mutation | RemoteThreadExecutor | WinAPI `SetWindowPos` is thread-safe |
|
||||||
|
|
||||||
|
### Common Mistakes
|
||||||
|
|
||||||
|
❌ **Wrong:** Using `RemoteThreadExecutor` for game state reads
|
||||||
|
```csharp
|
||||||
|
// UNSAFE: Will crash in most games
|
||||||
|
int health = magic.RemoteThread.Execute<int>(
|
||||||
|
readHealthFn,
|
||||||
|
CallConvention.Cdecl
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Right:** Using `MainThreadPump` for game state
|
||||||
|
```csharp
|
||||||
|
// SAFE: Runs on game's main thread
|
||||||
|
int health = await pump.Enqueue(() =>
|
||||||
|
magic.Memory.Read<int>(healthAddress)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comparison Summary
|
||||||
|
|
||||||
|
| Feature | RemoteThreadExecutor | MainThreadPump | InProcessInvoker |
|
||||||
|
|---------|---------------------|----------------|------------------|
|
||||||
|
| **Safety** | Thread-agnostic only | Crash-safe | In-process context |
|
||||||
|
| **Latency** | ~1-2ms | ~1 frame (16-33ms) | <1μs |
|
||||||
|
| **Detection risk** | High (new thread) | Low (detour) | None (in-process) |
|
||||||
|
| **Best for** | One-shot calls, DLL injection | State-sensitive calls | In-process tools |
|
||||||
|
| **Setup cost** | Low | Medium (need frame fn) | High (need injection) |
|
||||||
|
| **Throughput** | Low | Medium | Highest |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Topics
|
||||||
|
|
||||||
|
### Bypassing Anti-Cheat
|
||||||
|
|
||||||
|
**RemoteThreadExecutor** is easily detected (new thread creation). For stealth:
|
||||||
|
|
||||||
|
1. **Use MainThreadPump:** Detours are harder to detect than thread creation
|
||||||
|
2. **Thread hijacking:** For one-shot calls, hijack an existing thread (see `CodeInjector`)
|
||||||
|
|
||||||
|
### Combining Models
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Use RemoteThreadExecutor to inject DLL
|
||||||
|
magic.RemoteThread.Execute<IntPtr>(
|
||||||
|
loadLibraryAddress,
|
||||||
|
CallConvention.Stdcall,
|
||||||
|
dllPathPtr
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now injected, switch to InProcessInvoker
|
||||||
|
using var inProcess = Magic.OpenInProcess();
|
||||||
|
var fn = inProcess.Memory.CreateFunction<MyDelegate>(address);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int result = await pump.Enqueue(() =>
|
||||||
|
magic.Memory.Read<int>(address)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (AccessViolationException)
|
||||||
|
{
|
||||||
|
// Address not readable
|
||||||
|
}
|
||||||
|
catch (TimeoutException)
|
||||||
|
{
|
||||||
|
// Work item wedged (stuck the frame)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- [Architecture](./architecture.md) — Overall system design
|
||||||
|
- [Function Hooking](./hooking.md) — DetourManager internals
|
||||||
|
- [Memory Access](./memory-access.md) — MemoryBase and MarshalCache
|
||||||
+387
@@ -0,0 +1,387 @@
|
|||||||
|
# Function Hooking in WhiteMagic
|
||||||
|
|
||||||
|
WhiteMagic provides two types of runtime code modification: **inline detours** (function hooking) and **byte patches**. Both are reversible and automatically restored on disposal.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
| Feature | DetourManager | PatchManager |
|
||||||
|
|---------|---------------|--------------|
|
||||||
|
| **Scope** | In-process only | External + In-process |
|
||||||
|
| **Mechanism** | Inline `jmp` over prologue | Named byte patch |
|
||||||
|
| **Reversible** | ✅ Yes | ✅ Yes |
|
||||||
|
| **Auto-restore** | ✅ Yes | ✅ Yes |
|
||||||
|
| **Original call** | ✅ Via trampoline | ❌ No |
|
||||||
|
|
||||||
|
## DetourManager (Inline Function Hooking)
|
||||||
|
|
||||||
|
### What is a Detour?
|
||||||
|
|
||||||
|
An inline detour overwrites the first bytes of a function's prologue with a jump instruction (`jmp` or `push/ret`) that redirects execution to your hook delegate. The original bytes are saved in a **trampoline** that can call the original function.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
```
|
||||||
|
Original function:
|
||||||
|
│ push ebp
|
||||||
|
│ mov ebp, esp
|
||||||
|
│ sub esp, 0x10
|
||||||
|
│ ... rest of function
|
||||||
|
|
||||||
|
After detour:
|
||||||
|
│ jmp [hook_address] ← Overwrites prologue
|
||||||
|
Trampoline:
|
||||||
|
│ push ebp ← Saved original bytes
|
||||||
|
│ mov ebp, esp
|
||||||
|
│ jmp [original+5] ← Jumps back to original function
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
|
||||||
|
- **API interception:** Hook WinAPI functions (e.g., `CreateFileW` to monitor file access)
|
||||||
|
- **Function replacement:** Replace a game function with your own logic
|
||||||
|
- **Behavior modification:** Change parameters or return values
|
||||||
|
- **Profiling/instrumentation:** Count calls, measure execution time
|
||||||
|
|
||||||
|
### Usage Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using WhiteMagic;
|
||||||
|
using WhiteMagic.Hooking;
|
||||||
|
|
||||||
|
// In-process only
|
||||||
|
using var magic = Magic.OpenInProcess();
|
||||||
|
|
||||||
|
// Define your hook delegate
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public delegate void SleepDelegate(uint dwMilliseconds);
|
||||||
|
|
||||||
|
public static void MySleep(uint ms)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Sleep called with {ms}ms");
|
||||||
|
// Optionally call original
|
||||||
|
// originalSleep(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the detour
|
||||||
|
IntPtr sleepAddr = magic["kernel32"]["Sleep"].Address;
|
||||||
|
var detour = magic.DetourManager.Detour(
|
||||||
|
sleepAddr,
|
||||||
|
(SleepDelegate)MySleep
|
||||||
|
);
|
||||||
|
|
||||||
|
detour.Apply();
|
||||||
|
|
||||||
|
// ... use the hook
|
||||||
|
|
||||||
|
// Remove and restore original
|
||||||
|
detour.Remove();
|
||||||
|
```
|
||||||
|
|
||||||
|
### CallOriginal (Trampoline)
|
||||||
|
|
||||||
|
To call the original function from your hook:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
static MyDetourDelegate Original = null!;
|
||||||
|
|
||||||
|
static void MyHook(int arg1, float arg2)
|
||||||
|
{
|
||||||
|
// Do something before
|
||||||
|
Console.WriteLine($"Before: {arg1}, {arg2}");
|
||||||
|
|
||||||
|
// Call original
|
||||||
|
int result = Original(arg1, arg2);
|
||||||
|
|
||||||
|
// Do something after
|
||||||
|
Console.WriteLine($"After: {result}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create detour with original
|
||||||
|
var detour = magic.DetourManager.Detour(
|
||||||
|
targetAddress,
|
||||||
|
(MyDetourDelegate)MyHook,
|
||||||
|
out var original
|
||||||
|
);
|
||||||
|
|
||||||
|
Original = original;
|
||||||
|
detour.Apply();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prologue Safety
|
||||||
|
|
||||||
|
Before splicing a detour, WhiteMagic validates the prologue:
|
||||||
|
|
||||||
|
**Default (StubAssembler):**
|
||||||
|
- Covers common x86/x64 prologue shapes:
|
||||||
|
- `push ebp; mov ebp, esp`
|
||||||
|
- `mov edi, edi` (hot-patch padding)
|
||||||
|
- `sub rsp, XX` (x64 stack allocation)
|
||||||
|
- Single-byte instructions (`nop`, `int3`)
|
||||||
|
|
||||||
|
**Optional (IcedAssembler):**
|
||||||
|
- Full disassembly validation via Iced
|
||||||
|
- Handles arbitrary prologues
|
||||||
|
- Ensures splice lands on instruction boundaries
|
||||||
|
|
||||||
|
### Safety Considerations
|
||||||
|
|
||||||
|
**Risks:**
|
||||||
|
- **Mid-instruction splice:** Crashes if prologue validation fails
|
||||||
|
- **Race conditions:** Patching while code is running can crash
|
||||||
|
- **Anti-cheat:** Detours are easily detected
|
||||||
|
|
||||||
|
**Mitigations:**
|
||||||
|
- Validate prologue before patching
|
||||||
|
- Pause threads during application (use `DetourManager.Apply(options)`)
|
||||||
|
- Use `MainThreadPump` for crash-safe execution
|
||||||
|
- Auto-restore on `Dispose`
|
||||||
|
|
||||||
|
### Thread Safety
|
||||||
|
|
||||||
|
**DetourManager is NOT thread-safe.**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// WRONG: Concurrent modifications
|
||||||
|
Task.Run(() => detour1.Apply());
|
||||||
|
Task.Run(() => detour2.Apply());
|
||||||
|
|
||||||
|
// RIGHT: Synchronize
|
||||||
|
lock (magic.DetourManager)
|
||||||
|
{
|
||||||
|
detour1.Apply();
|
||||||
|
detour2.Apply();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Impact
|
||||||
|
|
||||||
|
- **Hook overhead:** ~5-10 CPU cycles (single jmp)
|
||||||
|
- **Trampoline call:** ~20-30 cycles (jmp + trampoline execution)
|
||||||
|
- **Best practice:** Keep hook delegates short
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PatchManager (Named Byte Patches)
|
||||||
|
|
||||||
|
### What is a Patch?
|
||||||
|
|
||||||
|
A patch is a named byte buffer that overwrites a region of memory. Unlike detours, patches can be any bytes and don't include a trampoline mechanism.
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
|
||||||
|
- **Patching constants:** Change a hardcoded value (e.g., max HP cap)
|
||||||
|
- **NOP-ing code:** Remove instructions (e.g., bypass a check)
|
||||||
|
- **Restoring bytes:** Undo anti-cheat modifications
|
||||||
|
- **Hot-patching:** Patch the hot-patch region (common in WinAPI)
|
||||||
|
|
||||||
|
### Usage Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using WhiteMagic;
|
||||||
|
using WhiteMagic.Hooking;
|
||||||
|
|
||||||
|
using var magic = Magic.Open(targetProcess);
|
||||||
|
|
||||||
|
// Create a patch to NOP 5 bytes
|
||||||
|
var patch = magic.PatchManager.Create(
|
||||||
|
"MaxHealthCapPatch",
|
||||||
|
address,
|
||||||
|
new byte[] { 0x90, 0x90, 0x90, 0x90, 0x90 } // NOP x5
|
||||||
|
);
|
||||||
|
|
||||||
|
patch.Apply();
|
||||||
|
|
||||||
|
// Check status
|
||||||
|
if (patch.IsApplied)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Patch applied");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove and restore
|
||||||
|
patch.Remove();
|
||||||
|
```
|
||||||
|
|
||||||
|
### VirtualProtect Dance
|
||||||
|
|
||||||
|
When applying a patch, `PatchManager` automatically:
|
||||||
|
|
||||||
|
1. Calls `VirtualProtectEx` to make region writable
|
||||||
|
2. Writes the patch bytes
|
||||||
|
3. Calls `VirtualProtectEx` to restore original protection
|
||||||
|
|
||||||
|
### Relative Addressing
|
||||||
|
|
||||||
|
Patches support relative addressing:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Apply relative to module base
|
||||||
|
var patch = magic.PatchManager.Create(
|
||||||
|
"DataPatch",
|
||||||
|
address, // Interpreted as relative if isRelative=true
|
||||||
|
patchBytes,
|
||||||
|
isRelative: true
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Thread Safety
|
||||||
|
|
||||||
|
**PatchManager is NOT thread-safe.** Synchronize concurrent modifications.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lifecycle Management
|
||||||
|
|
||||||
|
Both managers auto-restore on disposal:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// All applied detours and patches auto-restore
|
||||||
|
using (var magic = Magic.OpenInProcess())
|
||||||
|
{
|
||||||
|
var detour = magic.DetourManager.Detour(addr, hook);
|
||||||
|
detour.Apply();
|
||||||
|
|
||||||
|
var patch = magic.PatchManager.Create("Patch", addr, bytes);
|
||||||
|
patch.Apply();
|
||||||
|
|
||||||
|
// End of using block: detour.Remove() and patch.Remove() called automatically
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Explicit Disposal
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var detour = magic.DetourManager.Detour(addr, hook);
|
||||||
|
detour.Apply();
|
||||||
|
|
||||||
|
// Later
|
||||||
|
detour.Remove(); // Restores original bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced: Memory Protection
|
||||||
|
|
||||||
|
### Handling Read-Only Memory
|
||||||
|
|
||||||
|
If the target region is protected (e.g., `.text` section), both managers use:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
VirtualProtectEx(handle, address, size, PAGE_EXECUTE_READWRITE, out oldProt);
|
||||||
|
// Write bytes
|
||||||
|
VirtualProtectEx(handle, address, size, oldProt, out _);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
|
||||||
|
- **Anti-cheat:** May detect protection changes
|
||||||
|
- **Race conditions:** Other threads might execute during patch window
|
||||||
|
- **Crashes:** If code executes during the brief writable window
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced: Conditional Patching
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ConditionalPatch
|
||||||
|
{
|
||||||
|
private readonly Patch _patch;
|
||||||
|
private bool _enabled;
|
||||||
|
|
||||||
|
public bool Enabled
|
||||||
|
{
|
||||||
|
get => _enabled;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_enabled == value) return;
|
||||||
|
|
||||||
|
if (value)
|
||||||
|
_patch.Apply();
|
||||||
|
else
|
||||||
|
_patch.Remove();
|
||||||
|
|
||||||
|
_enabled = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced: Chain Hooking
|
||||||
|
|
||||||
|
Multiple hooks on the same function:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// First hook
|
||||||
|
var detour1 = magic.DetourManager.Detour(addr, Hook1);
|
||||||
|
detour1.Apply(out var trampoline1);
|
||||||
|
|
||||||
|
// Second hook (trampoline from first)
|
||||||
|
var detour2 = magic.DetourManager.Detour(
|
||||||
|
trampoline1.TrampolineAddress,
|
||||||
|
Hook2
|
||||||
|
);
|
||||||
|
detour2.Apply();
|
||||||
|
|
||||||
|
// Execution flow: Original → Hook2 → Hook1 → Trampoline1 → Original+5
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comparison to Other Libraries
|
||||||
|
|
||||||
|
| Library | Detours | Patches | Auto-restore | In-process |
|
||||||
|
|---------|---------|---------|---------------|------------|
|
||||||
|
| **WhiteMagic** | ✅ | ✅ | ✅ | ✅ Required |
|
||||||
|
| BlackMagic | ❌ | ❌ | ❌ | N/A |
|
||||||
|
| MemorySharp | ❌ (planned) | ❌ (planned) | N/A | N/A |
|
||||||
|
| GreyMagic | ✅ | ✅ | ✅ | ✅ Required |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Detour failed: prologue too short"
|
||||||
|
|
||||||
|
**Cause:** Function prologue is shorter than minimum required (5 bytes for x86 `jmp`, 14 bytes for x64 `push/ret`).
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Use a different function
|
||||||
|
- Patch deeper into the function (after prologue)
|
||||||
|
- For x86, use hot-patch area (`mov edi, edi` padding)
|
||||||
|
|
||||||
|
### "Patch failed: access violation"
|
||||||
|
|
||||||
|
**Cause:** Memory region is protected or invalid.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Verify address is valid (use `Magic.Memory.CanRead`)
|
||||||
|
- Ensure process has `PROCESS_VM_OPERATION` access
|
||||||
|
- Check if anti-cheat is blocking writes
|
||||||
|
|
||||||
|
### "Crash after detour"
|
||||||
|
|
||||||
|
**Cause:** Prologue validation failed or mid-instruction splice.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Enable Iced backend for full validation
|
||||||
|
- Pause threads during application
|
||||||
|
- Check if target is using code obfuscation
|
||||||
|
|
||||||
|
### "Hook not called"
|
||||||
|
|
||||||
|
**Cause:** Wrong function address or hook installed after target already called it.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Verify address with debugger
|
||||||
|
- Install hook early (before target uses function)
|
||||||
|
- Check if target is using a different implementation (e.g., forwarded export)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- [Architecture](./architecture.md) — Hooking layer design
|
||||||
|
- [Execution Models](./execution-models.md) — Safe hook execution
|
||||||
|
- [Memory Access](./memory-access.md) — Reading/writing memory
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
# Memory Access in WhiteMagic
|
||||||
|
|
||||||
|
WhiteMagic provides a dual memory-access model through an abstract `MemoryBase` class, supporting both external (out-of-process) and in-process readers with optimized typed I/O.
|
||||||
|
|
||||||
|
## MemoryBase Architecture
|
||||||
|
|
||||||
|
### Abstract Interface
|
||||||
|
|
||||||
|
`MemoryBase` defines the contract for all memory operations:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public abstract class MemoryBase : IDisposable
|
||||||
|
{
|
||||||
|
// Abstract raw I/O
|
||||||
|
public abstract byte[] ReadBytes(IntPtr address, int count, bool isRelative = false);
|
||||||
|
public abstract int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false);
|
||||||
|
|
||||||
|
// Typed I/O
|
||||||
|
public T Read<T>(IntPtr address, bool isRelative = false) where T : struct;
|
||||||
|
public bool Write<T>(IntPtr address, T value, bool isRelative = false) where T : struct;
|
||||||
|
|
||||||
|
// Array I/O
|
||||||
|
public T[] Read<T>(IntPtr address, int count, bool isRelative = false) where T : struct;
|
||||||
|
public bool Write<T>(IntPtr address, T[] values, bool isRelative = false) where T : struct;
|
||||||
|
|
||||||
|
// String I/O
|
||||||
|
public string ReadString(IntPtr address, Encoding encoding, int maxLength = 512);
|
||||||
|
public bool WriteString(IntPtr address, string value, Encoding encoding);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Concrete Implementations
|
||||||
|
|
||||||
|
#### ExternalReader
|
||||||
|
|
||||||
|
**Mechanism:** `ReadProcessMemory`/`WriteProcessMemory` via P/Invoke
|
||||||
|
**Handle:** `SafeMemoryHandle` to target process
|
||||||
|
**Use case:** Primary mode for automation hosts
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var magic = Magic.Open(targetProcess);
|
||||||
|
// Uses ExternalReader internally
|
||||||
|
```
|
||||||
|
|
||||||
|
#### InProcessReader
|
||||||
|
|
||||||
|
**Mechanism:** `ReadProcessMemory`/`WriteProcessMemory` on self-handle
|
||||||
|
**Handle:** `SafeMemoryHandle` to current process
|
||||||
|
**Use case:** Injected code, delegate calls, detours
|
||||||
|
|
||||||
|
**Design decision:** Uses RPM-on-self instead of unsafe direct pointer deref because .NET cannot catch `AccessViolationException`, so a bad deref kills the host with no soft-failure path. The in-process speed win moves to the delegate-call and detour paths, not the reader.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var magic = Magic.OpenInProcess();
|
||||||
|
// Uses InProcessReader internally
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Typed I/O with MarshalCache<T>
|
||||||
|
|
||||||
|
### Performance Optimization
|
||||||
|
|
||||||
|
`MarshalCache<T>` eliminates per-call reflection overhead by caching type metadata once:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class MarshalCache<T>
|
||||||
|
{
|
||||||
|
// Cached at static-constructor time
|
||||||
|
public static readonly int Size; // Managed blittable width
|
||||||
|
public static readonly int MarshalSize; // Unmanaged interop width
|
||||||
|
public static readonly bool TypeRequiresMarshal; // Needs PtrToStructure
|
||||||
|
public static readonly bool IsIntPtr; // Special handling for IntPtr
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Blittable vs Marshalled Types
|
||||||
|
|
||||||
|
**Blittable types** (no marshaling needed):
|
||||||
|
- Primitives: `int`, `byte`, `float`, `double`, `bool` (1 byte managed)
|
||||||
|
- Enums (if underlying type is blittable)
|
||||||
|
- Structs containing only blittable fields
|
||||||
|
- **Performance:** `MemoryMarshal.Read<T>` — zero allocation
|
||||||
|
|
||||||
|
**Marshalled types** (require `Marshal.PtrToStructure`):
|
||||||
|
- `bool` (4 bytes in Win32 interop)
|
||||||
|
- `char` (2 bytes ANSI marshaling)
|
||||||
|
- Structs with `[MarshalAs]` attributes
|
||||||
|
- Structs with inline `ByValTStr`/`ByValArray`
|
||||||
|
- **Performance:** `Marshal.PtrToStructure` — allocates temporary copy
|
||||||
|
|
||||||
|
### Size vs MarshalSize
|
||||||
|
|
||||||
|
For most types, `Size == MarshalSize`. They differ when:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Example: bool field
|
||||||
|
struct MyStruct
|
||||||
|
{
|
||||||
|
public bool Flag; // 1 byte managed, 4 bytes Win32 BOOL
|
||||||
|
public int Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
MarshalCache<MyStruct>.Size == 5; // Managed layout
|
||||||
|
MarshalCache<MyStruct>.MarshalSize == 8; // Win32 BOOL is 4 bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var magic = Magic.Open(process);
|
||||||
|
|
||||||
|
// Blittable read (fast path)
|
||||||
|
int health = magic.Memory.Read<int>(address);
|
||||||
|
|
||||||
|
// Marshalled read (slow path)
|
||||||
|
var gameState = magic.Memory.Read<MyStruct>(address);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Addressing Modes
|
||||||
|
|
||||||
|
### Absolute Addressing (Default)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Read at absolute address 0x12345678
|
||||||
|
int value = magic.Memory.Read<int>(0x12345678);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relative Addressing
|
||||||
|
|
||||||
|
Relative to module base (useful for ASRR):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Read at module_base + 0x1000
|
||||||
|
int value = magic.Memory.Read<int>(0x1000, isRelative: true);
|
||||||
|
// Equivalent to:
|
||||||
|
int value = magic.Memory.Read<int>(magic.Memory.ImageBase + 0x1000);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using RemotePointer
|
||||||
|
|
||||||
|
The `RemotePointer` indexer provides fluent relative addressing:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var basePtr = magic[moduleBase];
|
||||||
|
int offset1 = basePtr.Read<int>(0x1000);
|
||||||
|
int offset2 = basePtr.Read<int>(0x2000);
|
||||||
|
|
||||||
|
// Chained offsets
|
||||||
|
var nestedPtr = magic[basePtr.Read<IntPtr>(0x1000)];
|
||||||
|
int value = nestedPtr.Read<int>(0x50);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## String I/O
|
||||||
|
|
||||||
|
### Reading Strings
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Read null-terminated ANSI string
|
||||||
|
string ansi = magic.Memory.ReadString(
|
||||||
|
address,
|
||||||
|
Encoding.ASCII,
|
||||||
|
maxLength: 256
|
||||||
|
);
|
||||||
|
|
||||||
|
// Read null-terminated UTF-16 string
|
||||||
|
string unicode = magic.Memory.ReadString(
|
||||||
|
address,
|
||||||
|
Encoding.Unicode,
|
||||||
|
maxLength: 512
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation:** Reads byte-by-byte until null terminator or `maxLength`.
|
||||||
|
|
||||||
|
### Writing Strings
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Write null-terminated string
|
||||||
|
bool success = magic.Memory.WriteString(
|
||||||
|
address,
|
||||||
|
"Hello World",
|
||||||
|
Encoding.ASCII
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation:** Writes bytes + null terminator.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Array I/O
|
||||||
|
|
||||||
|
### Reading Arrays
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Read 10 integers
|
||||||
|
int[] values = magic.Memory.Read<int>(address, 10);
|
||||||
|
|
||||||
|
// Read struct array
|
||||||
|
var enemies = magic.Memory.Read<EnemyStruct>(enemyListPtr, 50);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Performance:**
|
||||||
|
- Blittable: `MemoryMarshal.Read<T>` in a loop (fast)
|
||||||
|
- Marshalled: `Marshal.PtrToStructure<T>` per element (slow)
|
||||||
|
|
||||||
|
### Writing Arrays
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Write integer array
|
||||||
|
int[] values = { 1, 2, 3, 4, 5 };
|
||||||
|
magic.Memory.Write(address, values);
|
||||||
|
|
||||||
|
// Write struct array
|
||||||
|
EnemyStruct[] enemies = GetEnemies();
|
||||||
|
magic.Memory.Write(enemyListPtr, enemies);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Raw Byte I/O
|
||||||
|
|
||||||
|
### Reading Bytes
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Read 100 bytes
|
||||||
|
byte[] buffer = magic.Memory.ReadBytes(address, 100);
|
||||||
|
|
||||||
|
// Read with relative addressing
|
||||||
|
byte[] code = magic.Memory.ReadBytes(offset, count, isRelative: true);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing Bytes
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Write byte array
|
||||||
|
byte[] patchBytes = { 0x90, 0x90, 0x90 }; // NOP x3
|
||||||
|
int written = magic.Memory.WriteBytes(address, patchBytes);
|
||||||
|
|
||||||
|
// Write with relative addressing
|
||||||
|
int written = magic.Memory.WriteBytes(
|
||||||
|
offset,
|
||||||
|
new byte[] { 0x01, 0x02, 0x03 },
|
||||||
|
isRelative: true
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Read Operations
|
||||||
|
|
||||||
|
**Strategy:** Explicit failures → throw exceptions
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int value = magic.Memory.Read<int>(address);
|
||||||
|
}
|
||||||
|
catch (Win32Exception ex)
|
||||||
|
{
|
||||||
|
// ReadProcessMemory failed (access violation, process exited, etc.)
|
||||||
|
Console.WriteLine($"Read failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
// Process handle is closed
|
||||||
|
Console.WriteLine($"Process not open: {ex.Message}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `default(T)` if fewer bytes read than expected (e.g., partial read).
|
||||||
|
|
||||||
|
### Write Operations
|
||||||
|
|
||||||
|
**Strategy:** Silent failures → return `false`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
bool success = magic.Memory.Write(address, 999);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
// Handle failure (retry, log, etc.)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reason:** Writes to another process can legitimately fail (protection changed, process exited); silent retry is often the right strategy.
|
||||||
|
|
||||||
|
### String Operations
|
||||||
|
|
||||||
|
**Read:** Returns `string.Empty` on failure.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
string text = magic.Memory.ReadString(address, Encoding.ASCII);
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
// Read failed or string is empty
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Write:** Returns `false` on failure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Thread Safety
|
||||||
|
|
||||||
|
### ExternalReader
|
||||||
|
|
||||||
|
**Thread-safe:** Yes
|
||||||
|
|
||||||
|
`ReadProcessMemory`/`WriteProcessMemory` are thread-safe at the OS level. No synchronization required.
|
||||||
|
|
||||||
|
### InProcessReader
|
||||||
|
|
||||||
|
**Thread-safe:** Yes
|
||||||
|
|
||||||
|
RPM-on-self is thread-safe. No synchronization required.
|
||||||
|
|
||||||
|
### High-Level Access
|
||||||
|
|
||||||
|
**Thread-safe:** Depends on usage
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// SAFE: Concurrent reads from multiple threads
|
||||||
|
int v1 = magic.Memory.Read<int>(addr1);
|
||||||
|
int v2 = magic.Memory.Read<int>(addr2);
|
||||||
|
|
||||||
|
// SAFE: Same address, concurrent reads (no race, just inconsistent value)
|
||||||
|
int v3 = magic.Memory.Read<int>(addr);
|
||||||
|
int v4 = magic.Memory.Read<int>(addr);
|
||||||
|
|
||||||
|
// UNSAFE: Concurrent writes (last write wins, no atomicity)
|
||||||
|
magic.Memory.Write(addr, 1); // Thread 1
|
||||||
|
magic.Memory.Write(addr, 2); // Thread 2 (may win)
|
||||||
|
```
|
||||||
|
|
||||||
|
For atomic read-modify-write, use `MainThreadPump` or implement locking.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
| Operation | Cost | Notes |
|
||||||
|
|-----------|------|-------|
|
||||||
|
| `Read<T>` (blittable) | Low | `MemoryMarshal.Read`, no alloc |
|
||||||
|
| `Read<T>` (marshalled) | Medium | `Marshal.PtrToStructure` + alloc |
|
||||||
|
| `Read<T>` (array, blittable) | Medium | Per-element `MemoryMarshal.Read` |
|
||||||
|
| `Read<T>` (array, marshalled) | High | Per-element marshalling + alloc |
|
||||||
|
| `ReadBytes` | Low | Direct buffer copy |
|
||||||
|
| `ReadString` | Medium | Byte-by-byte + encoding + alloc |
|
||||||
|
| `Write<T>` (blittable) | Low | `MemoryMarshal.Write` + buffer alloc |
|
||||||
|
| `Write<T>` (marshalled) | Medium | `Marshal.StructureToPtr` + buffer alloc |
|
||||||
|
| `WriteBytes` | Low | Direct buffer copy |
|
||||||
|
| `WriteString` | Medium | Encoding + null terminator + buffer alloc |
|
||||||
|
|
||||||
|
### Optimization Tips
|
||||||
|
|
||||||
|
1. **Prefer blittable types:** Use `int` instead of `bool` where possible
|
||||||
|
2. **Batch reads:** Read arrays instead of individual elements
|
||||||
|
3. **Reuse buffers:** For repeated reads, reuse byte arrays
|
||||||
|
4. **Cache offsets:** Compute addresses once, reuse them
|
||||||
|
5. **Use MarshalCache:** Automatic via `Read<T>`/`Write<T>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern: Reading a Nested Structure
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Assume structure: GameManager -> PlayerList -> Player[i] -> Health
|
||||||
|
IntPtr gameManagerPtr = magic.Memory.ImageBase + 0x1000;
|
||||||
|
IntPtr playerListPtr = magic.Memory.Read<IntPtr>(gameManagerPtr + 0x20);
|
||||||
|
IntPtr playerPtr = magic.Memory.Read<IntPtr>(playerListPtr + (playerIndex * 8));
|
||||||
|
int health = magic.Memory.Read<int>(playerPtr + 0x4);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using RemotePointer:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
int health = magic[gameManagerPtr]
|
||||||
|
.Read<IntPtr>(0x20) // PlayerList
|
||||||
|
.Let(ptr => magic[ptr]
|
||||||
|
.Read<IntPtr>(playerIndex * 8)) // Player
|
||||||
|
.Let(ptr => magic[ptr]
|
||||||
|
.Read<int>(0x4)); // Health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Scanning for a Value
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Scan memory region for a specific value
|
||||||
|
IntPtr found = IntPtr.Zero;
|
||||||
|
byte[] region = magic.Memory.ReadBytes(baseAddr, size);
|
||||||
|
|
||||||
|
for (int i = 0; i < region.Length - 4; i++)
|
||||||
|
{
|
||||||
|
int value = BitConverter.ToInt32(region, i);
|
||||||
|
if (value == targetValue)
|
||||||
|
{
|
||||||
|
found = baseAddr + i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Better:** Use `PatternScanner` (see [Discovery](./discovery.md)).
|
||||||
|
|
||||||
|
### Pattern: Safe Retry Loop
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Retry write with exponential backoff
|
||||||
|
int attempts = 0;
|
||||||
|
bool success = false;
|
||||||
|
while (attempts < 5 && !success)
|
||||||
|
{
|
||||||
|
success = magic.Memory.Write(address, value);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
attempts++;
|
||||||
|
Thread.Sleep(100 * (1 << attempts)); // 100ms, 200ms, 400ms, ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Read returns default value"
|
||||||
|
|
||||||
|
**Possible causes:**
|
||||||
|
1. Address is invalid
|
||||||
|
2. Process has exited
|
||||||
|
3. Memory protection doesn't allow read
|
||||||
|
4. Fewer bytes read than expected (partial read)
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
- Verify address with debugger
|
||||||
|
- Check `magic.Memory.Handle.IsInvalid`
|
||||||
|
- Use `CanRead` helper (if available)
|
||||||
|
- Validate `ReadBytes` length
|
||||||
|
|
||||||
|
### "Write returns false"
|
||||||
|
|
||||||
|
**Possible causes:**
|
||||||
|
1. Memory protection is read-only
|
||||||
|
2. Process has exited
|
||||||
|
3. Address is invalid
|
||||||
|
4. Anti-cheat blocking writes
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
- Verify address with debugger
|
||||||
|
- Check memory protection (`VirtualQueryEx`)
|
||||||
|
- Ensure process has `PROCESS_VM_WRITE` access
|
||||||
|
- Retry after `VirtualProtectEx` (if you have rights)
|
||||||
|
|
||||||
|
### "Performance is slow"
|
||||||
|
|
||||||
|
**Possible causes:**
|
||||||
|
1. Reading individual elements in a loop
|
||||||
|
2. Using marshalled types extensively
|
||||||
|
3. Small reads/writes (not batching)
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
- Read arrays instead of loops
|
||||||
|
- Use blittable types where possible
|
||||||
|
- Batch reads/writes
|
||||||
|
- Cache frequently accessed values
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- [Architecture](./architecture.md) — MemoryBase layer design
|
||||||
|
- [Discovery](./discovery.md) — Pattern scanning and PE parsing
|
||||||
|
- [Execution Models](./execution-models.md) — Safe execution in target process
|
||||||
@@ -0,0 +1,579 @@
|
|||||||
|
# Troubleshooting Guide
|
||||||
|
|
||||||
|
This guide covers common issues, errors, and solutions when using WhiteMagic.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Process Access Issues](#process-access-issues)
|
||||||
|
- [Memory Operation Failures](#memory-operation-failures)
|
||||||
|
- [Execution Errors](#execution-errors)
|
||||||
|
- [Hooking Problems](#hooking-problems)
|
||||||
|
- [Pattern Scanning Issues](#pattern-scanning-issues)
|
||||||
|
- [Performance Problems](#performance-problems)
|
||||||
|
- [Crash and Stability Issues](#crash-and-stability-issues)
|
||||||
|
- [Build and Compilation Errors](#build-and-compilation-errors)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Process Access Issues
|
||||||
|
|
||||||
|
### "Process is not open for read/write"
|
||||||
|
|
||||||
|
**Symptom:**
|
||||||
|
```
|
||||||
|
System.InvalidOperationException: Process is not open for read/write.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Process has exited
|
||||||
|
2. Process handle is closed
|
||||||
|
3. Insufficient permissions
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
```csharp
|
||||||
|
// Check process state
|
||||||
|
if (magic.Memory.Handle.IsInvalid || magic.Memory.Handle.IsClosed)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Process handle is invalid or closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-open the process
|
||||||
|
using var magic = Magic.Open(process);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prevention:**
|
||||||
|
- Keep `Process` object alive
|
||||||
|
- Don't dispose `Magic` while using it
|
||||||
|
- Use `using` statement for automatic cleanup
|
||||||
|
|
||||||
|
### "OpenProcess failed: Access Denied"
|
||||||
|
|
||||||
|
**Symptom:**
|
||||||
|
```
|
||||||
|
Process.Open returns false, or Magic.Open throws Win32Exception
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Insufficient privileges (not running as administrator)
|
||||||
|
2. Target process is protected (anti-cheat, system process)
|
||||||
|
3. 32-bit/64-bit mismatch
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
```csharp
|
||||||
|
// Run as administrator
|
||||||
|
// Right-click → Run as Administrator
|
||||||
|
|
||||||
|
// Check process bitness matches host
|
||||||
|
if (Environment.Is64BitProcess != Is64BitProcess(targetProcess))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Bitness mismatch between host and target");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prevention:**
|
||||||
|
- Run with admin privileges
|
||||||
|
- Check target process protection level
|
||||||
|
- Ensure bitness compatibility
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Memory Operation Failures
|
||||||
|
|
||||||
|
### "Read returns default value"
|
||||||
|
|
||||||
|
**Symptom:**
|
||||||
|
```csharp
|
||||||
|
int value = magic.Memory.Read<int>(address);
|
||||||
|
// value is 0 (or default), but expected different value
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Address is invalid
|
||||||
|
2. Memory protection prevents reading
|
||||||
|
3. Fewer bytes read than expected
|
||||||
|
4. Process has exited
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
```csharp
|
||||||
|
// Verify address with debugger
|
||||||
|
// Check if address is readable
|
||||||
|
byte[] test = magic.Memory.ReadBytes(address, 4);
|
||||||
|
if (test.Length < 4)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Cannot read from address");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate handle
|
||||||
|
if (magic.Memory.Handle.IsInvalid)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Process handle is invalid");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Write returns false"
|
||||||
|
|
||||||
|
**Symptom:**
|
||||||
|
```csharp
|
||||||
|
bool success = magic.Memory.Write(address, value);
|
||||||
|
if (!success) { /* Write failed */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Memory is read-only (e.g., `.text` section)
|
||||||
|
2. Address is invalid
|
||||||
|
3. Process has exited
|
||||||
|
4. Anti-cheat blocking writes
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
```csharp
|
||||||
|
// Retry with delay
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
if (magic.Memory.Write(address, value))
|
||||||
|
break;
|
||||||
|
Thread.Sleep(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if memory is protected
|
||||||
|
// Use VirtualProtectEx if you have rights (advanced)
|
||||||
|
```
|
||||||
|
|
||||||
|
**For code patches, use `PatchManager` instead:**
|
||||||
|
```csharp
|
||||||
|
var patch = magic.PatchManager.Create("Patch", address, bytes);
|
||||||
|
patch.Apply(); // Handles VirtualProtect automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Errors
|
||||||
|
|
||||||
|
### "CreateRemoteThread failed"
|
||||||
|
|
||||||
|
**Symptom:**
|
||||||
|
```
|
||||||
|
Win32Exception: CreateRemoteThread failed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Target process is protected
|
||||||
|
2. Insufficient permissions
|
||||||
|
3. Target process has exited
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
```csharp
|
||||||
|
// Ensure process is still running
|
||||||
|
if (process.HasExited)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Process has exited");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
// Run as administrator
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Crash when calling target function via RemoteThreadExecutor"
|
||||||
|
|
||||||
|
**Symptom:** Target application crashes after `RemoteThreadExecutor.Execute<T>`
|
||||||
|
|
||||||
|
**Cause:** Calling single-threaded function from remote thread (thread-affinity violation)
|
||||||
|
|
||||||
|
**Solution:** Use `MainThreadPump` instead:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// WRONG (crashes game):
|
||||||
|
int health = magic.RemoteThread.Execute<int>(
|
||||||
|
readHealthFn,
|
||||||
|
CallConvention.Cdecl
|
||||||
|
);
|
||||||
|
|
||||||
|
// RIGHT (crash-safe):
|
||||||
|
var pump = magic.CreateMainThreadPump(frameAddress);
|
||||||
|
int health = await pump.Enqueue(() =>
|
||||||
|
magic.Memory.Read<int>(healthAddress)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Explanation:** Game state, scripting engines, and render contexts are often main-thread-only. Use `MainThreadPump` for these.
|
||||||
|
|
||||||
|
### "MainThreadPump work item wedged"
|
||||||
|
|
||||||
|
**Symptom:** `Enqueue<T>()` hangs or times out
|
||||||
|
|
||||||
|
**Cause:** Work item threw exception or entered infinite loop
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int result = await pump.Enqueue(() =>
|
||||||
|
{
|
||||||
|
// Keep work items short!
|
||||||
|
return magic.Memory.Read<int>(address);
|
||||||
|
}, TimeSpan.FromSeconds(1)); // Add timeout
|
||||||
|
}
|
||||||
|
catch (TimeoutException)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Work item wedged (stuck the frame)");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prevention:**
|
||||||
|
- Keep pump work items short (< 1ms)
|
||||||
|
- Avoid blocking calls in work items
|
||||||
|
- Use timeout on `Enqueue<T>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hooking Problems
|
||||||
|
|
||||||
|
### "Detour failed: prologue too short"
|
||||||
|
|
||||||
|
**Symptom:**
|
||||||
|
```
|
||||||
|
InvalidOperationException: Prologue is too short for detour (minimum 5 bytes required)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause:** Function prologue is shorter than minimum required for jmp instruction
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
```csharp
|
||||||
|
// Option 1: Hook a different function
|
||||||
|
var detour = magic.DetourManager.Detour(alternativeAddress, hook);
|
||||||
|
|
||||||
|
// Option 2: Patch deeper into function (after prologue)
|
||||||
|
// (Advanced, risky)
|
||||||
|
|
||||||
|
// Option 3: For WinAPI, use hot-patch area
|
||||||
|
if (IsHotPatchPadded(functionAddress))
|
||||||
|
{
|
||||||
|
// Can use 2-byte jmp at [address-2]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Patch failed: access violation"
|
||||||
|
|
||||||
|
**Symptom:**
|
||||||
|
```
|
||||||
|
AccessViolationException when applying patch
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause:** Memory is protected or invalid
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
```csharp
|
||||||
|
// Verify address is valid
|
||||||
|
byte[] test = magic.Memory.ReadBytes(address, 1);
|
||||||
|
|
||||||
|
// Use PatchManager (handles VirtualProtect automatically)
|
||||||
|
var patch = magic.PatchManager.Create("Patch", address, bytes);
|
||||||
|
patch.Apply();
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Hook not called"
|
||||||
|
|
||||||
|
**Symptom:** Detour applied successfully, but hook delegate never executes
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Wrong function address
|
||||||
|
2. Target already called function before hook installed
|
||||||
|
3. Target is using a different implementation (e.g., forwarded export)
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
```csharp
|
||||||
|
// Verify address with debugger
|
||||||
|
// Install hook early (before target uses function)
|
||||||
|
// Check for forwarded exports
|
||||||
|
var fn = magic["module"]["function"];
|
||||||
|
Console.WriteLine($"Function address: {fn.Address}");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pattern Scanning Issues
|
||||||
|
|
||||||
|
### "FindPattern returns IntPtr.Zero"
|
||||||
|
|
||||||
|
**Symptom:** Pattern scan finds no matches
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Pattern is incorrect
|
||||||
|
2. Module not loaded
|
||||||
|
3. Memory layout changed (ASLR, update)
|
||||||
|
4. Pattern wildcards too broad
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
```csharp
|
||||||
|
// Verify pattern syntax
|
||||||
|
// Use IDA-style: "48 8B ? ? ? ? ?"
|
||||||
|
// ? = wildcard byte
|
||||||
|
|
||||||
|
// Narrow search range
|
||||||
|
// Scan only relevant module, not entire memory
|
||||||
|
|
||||||
|
// Check if module is loaded
|
||||||
|
var module = magic["moduleName"];
|
||||||
|
if (module.BaseAddress == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Module not loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with known pattern
|
||||||
|
var test = magic.Memory.FindPattern("48 8B 05 ? ? ? ?", module.BaseAddress, 0x1000);
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Pattern scanner is slow"
|
||||||
|
|
||||||
|
**Symptom:** `FindPattern` takes several seconds
|
||||||
|
|
||||||
|
**Cause:** Scanning large memory regions without cache
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```csharp
|
||||||
|
// Use pattern cache (if available in your version)
|
||||||
|
// Or cache results manually
|
||||||
|
private static readonly Dictionary<string, IntPtr> PatternCache = new();
|
||||||
|
|
||||||
|
IntPtr FindPatternCached(string pattern, IntPtr baseAddr, int size)
|
||||||
|
{
|
||||||
|
string key = $"{pattern}_{baseAddr}_{size}";
|
||||||
|
if (PatternCache.TryGetValue(key, out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
IntPtr result = magic.Memory.FindPattern(pattern, baseAddr, size);
|
||||||
|
PatternCache[key] = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Problems
|
||||||
|
|
||||||
|
### "Memory reads are slow"
|
||||||
|
|
||||||
|
**Symptom:** `Read<T>` takes several milliseconds
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Reading individual elements in a loop
|
||||||
|
2. Using marshalled types extensively
|
||||||
|
3. Target process is heavily loaded
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
```csharp
|
||||||
|
// WRONG: Slow loop
|
||||||
|
for (int i = 0; i < 1000; i++)
|
||||||
|
{
|
||||||
|
values[i] = magic.Memory.Read<int>(baseAddr + i * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RIGHT: Batch read
|
||||||
|
values = magic.Memory.Read<int>(baseAddr, 1000);
|
||||||
|
|
||||||
|
// RIGHT: Use blittable types
|
||||||
|
// Use int instead of bool where possible
|
||||||
|
```
|
||||||
|
|
||||||
|
### "High CPU usage"
|
||||||
|
|
||||||
|
**Symptom:** WhiteMagic causes high CPU in target process
|
||||||
|
|
||||||
|
**Cause:** Polling loops, tight read loops in pump
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
```csharp
|
||||||
|
// WRONG: Tight loop in pump
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int health = magic.Memory.Read<int>(healthAddr);
|
||||||
|
if (health == 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RIGHT: Event-driven or throttled
|
||||||
|
// Use MainThreadPump with delays
|
||||||
|
// Or poll at reasonable interval (e.g., 60 Hz)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Crash and Stability Issues
|
||||||
|
|
||||||
|
### "Target crashes when attached"
|
||||||
|
|
||||||
|
**Symptom:** Target application crashes shortly after `Magic.Open`
|
||||||
|
|
||||||
|
**Cause:** Thread-affinity violation (calling function from wrong thread)
|
||||||
|
|
||||||
|
**Solution:** Use `MainThreadPump` for state-sensitive calls:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Identify the crash-safe execution model
|
||||||
|
var pump = magic.CreateMainThreadPump(frameAddress);
|
||||||
|
|
||||||
|
// All state-sensitive calls go through pump
|
||||||
|
await pump.Enqueue(() => { /* safe code */ });
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Random crashes during operation"
|
||||||
|
|
||||||
|
**Symptom:** Intermittent crashes, hard to reproduce
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Race conditions (concurrent memory access)
|
||||||
|
2. Anti-cheat interference
|
||||||
|
3. Memory protection changes mid-operation
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
```csharp
|
||||||
|
// Add synchronization
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
magic.Memory.Write(address, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle access violations gracefully
|
||||||
|
try
|
||||||
|
{
|
||||||
|
magic.Memory.Read<int>(address);
|
||||||
|
}
|
||||||
|
catch (AccessViolationException)
|
||||||
|
{
|
||||||
|
// Retry or handle gracefully
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for anti-cheat
|
||||||
|
// Some anti-cheat tools detect and block memory manipulation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build and Compilation Errors
|
||||||
|
|
||||||
|
### "XML documentation errors"
|
||||||
|
|
||||||
|
**Symptom:** Build fails with `CS1591` or `CS0419` errors
|
||||||
|
|
||||||
|
**Cause:** Missing XML comments or ambiguous cref references
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```xml
|
||||||
|
<!-- In WhiteMagic.csproj -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<NoWarn>$(NoWarn);CS1591</NoWarn> <!-- Suppress missing warnings -->
|
||||||
|
</PropertyGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
Or add missing XML comments (see [Documentation Best Practices](../README.md#documentation)).
|
||||||
|
|
||||||
|
### "Type or namespace not found"
|
||||||
|
|
||||||
|
**Symptom:** `WhiteMagic` namespace not found after adding reference
|
||||||
|
|
||||||
|
**Cause:** Project not referencing `WhiteMagic.dll`
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
dotnet add reference ../WhiteMagic/WhiteMagic.csproj
|
||||||
|
# OR
|
||||||
|
dotnet add package WhiteMagic
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debugging Tips
|
||||||
|
|
||||||
|
### Enable Detailed Logging
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Add diagnostic logging
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
Debug.WriteLine($"Reading from 0x{address:X}");
|
||||||
|
int value = magic.Memory.Read<int>(address);
|
||||||
|
Debug.WriteLine($"Read result: {value}");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify with External Tools
|
||||||
|
|
||||||
|
- **Cheat Engine:** Verify memory addresses and values
|
||||||
|
- **x64dbg/windbg:** Verify function addresses and disassembly
|
||||||
|
- **Process Hacker:** Check process handles and permissions
|
||||||
|
|
||||||
|
### Common Pitfalls
|
||||||
|
|
||||||
|
1. ❌ **Forgetting `isRelative` for module-relative addresses**
|
||||||
|
```csharp
|
||||||
|
// WRONG
|
||||||
|
int value = magic.Memory.Read<int>(0x1000);
|
||||||
|
|
||||||
|
// RIGHT (if 0x1000 is module-relative)
|
||||||
|
int value = magic.Memory.Read<int>(0x1000, isRelative: true);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. ❌ **Using `RemoteThreadExecutor` for game state**
|
||||||
|
```csharp
|
||||||
|
// WRONG (crashes)
|
||||||
|
int health = magic.RemoteThread.Execute<int>(fn, CallConvention.Cdecl);
|
||||||
|
|
||||||
|
// RIGHT (crash-safe)
|
||||||
|
int health = await pump.Enqueue(() => magic.Memory.Read<int>(addr));
|
||||||
|
```
|
||||||
|
|
||||||
|
3. ❌ **Not disposing `Magic`**
|
||||||
|
```csharp
|
||||||
|
// WRONG (leaks handles)
|
||||||
|
var magic = Magic.Open(process);
|
||||||
|
// ... use magic
|
||||||
|
// Forgot to dispose!
|
||||||
|
|
||||||
|
// RIGHT
|
||||||
|
using (var magic = Magic.Open(process))
|
||||||
|
{
|
||||||
|
// ... use magic
|
||||||
|
} // Auto-disposes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
If you're still stuck:
|
||||||
|
|
||||||
|
1. **Check documentation:**
|
||||||
|
- [Architecture](./architecture.md)
|
||||||
|
- [Execution Models](./execution-models.md)
|
||||||
|
- [Memory Access](./memory-access.md)
|
||||||
|
- [Function Hooking](./hooking.md)
|
||||||
|
|
||||||
|
2. **Search issues:** Check existing GitHub issues
|
||||||
|
|
||||||
|
3. **Create minimal reproduction:**
|
||||||
|
```csharp
|
||||||
|
// Minimal code that reproduces the issue
|
||||||
|
using var magic = Magic.Open(process);
|
||||||
|
int value = magic.Memory.Read<int>(address);
|
||||||
|
// What happens vs. what you expect
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Include system info:**
|
||||||
|
- WhiteMagic version
|
||||||
|
- .NET version
|
||||||
|
- Target process (if applicable)
|
||||||
|
- Windows version
|
||||||
|
- x86 or x64
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Error Codes
|
||||||
|
|
||||||
|
| Win32 Error | Meaning | Solution |
|
||||||
|
|-------------|---------|----------|
|
||||||
|
| `ERROR_ACCESS_DENIED` (5) | Insufficient permissions | Run as administrator |
|
||||||
|
| `ERROR_INVALID_HANDLE` (6) | Handle is invalid/closed | Re-open process |
|
||||||
|
| `ERROR_NOT_ENOUGH_MEMORY` (8) | Insufficient memory | Reduce buffer size |
|
||||||
|
| `ERROR_NOACCESS` (998) | Invalid access | Check memory protection |
|
||||||
|
| `ERROR_PARTIAL_COPY` (299) | Partial read/write | Retry or check address |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*For more information, see the main [README](../README.md) and [architecture documentation](./architecture.md).*
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user