Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da342d355e | ||
|
|
1169fdb994 | ||
|
|
3e294dc846 | ||
|
|
8f988768fe | ||
|
|
9aef9c21e3 | ||
|
|
f0faca3112 |
@@ -1,190 +1,3 @@
|
||||
# WhiteMagic
|
||||
|
||||
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.
|
||||
Wite Magic is a C# library to read, write and execute remote code into a target process for analysis, debuging and mod creation.
|
||||
@@ -1,321 +0,0 @@
|
||||
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!");
|
||||
}
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
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!");
|
||||
}
|
||||
}
|
||||
@@ -1,401 +0,0 @@
|
||||
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!");
|
||||
}
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -1,531 +0,0 @@
|
||||
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!");
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,14 +0,0 @@
|
||||
<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,5 +1,4 @@
|
||||
<Solution>
|
||||
<Project Path="WhiteMagic.Examples/WhiteMagic.Examples.csproj" />
|
||||
<Project Path="WhiteMagic/WhiteMagic.csproj" />
|
||||
<Project Path="WhiteMagicTest/WhiteMagicTest.csproj" />
|
||||
</Solution>
|
||||
|
||||
@@ -30,10 +30,8 @@ public sealed class StubAssembler : IAssembler
|
||||
|
||||
// ── Emit primitives ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Emits a single byte into the buffer.</summary>
|
||||
public void EmitU8(List<byte> buffer, byte value) => buffer.Add(value);
|
||||
|
||||
/// <summary>Emits a 32-bit little-endian integer into the buffer.</summary>
|
||||
public void EmitU32(List<byte> buffer, uint value)
|
||||
{
|
||||
buffer.Add((byte)value);
|
||||
@@ -42,7 +40,6 @@ public sealed class StubAssembler : IAssembler
|
||||
buffer.Add((byte)(value >> 24));
|
||||
}
|
||||
|
||||
/// <summary>Emits a 64-bit little-endian integer into the buffer.</summary>
|
||||
public void EmitU64(List<byte> buffer, ulong value)
|
||||
{
|
||||
EmitU32(buffer, (uint)value);
|
||||
|
||||
@@ -472,46 +472,43 @@ public sealed class RemoteThreadExecutor
|
||||
nuint mask = AllocationGranularity - (nuint)1;
|
||||
nuint aligned = (preferred + AllocationGranularity - (nuint)1) & ~mask;
|
||||
|
||||
for (int i = 0; i < NearAllocationAttempts; i++)
|
||||
for (long delta = 0; delta <= (long)0x7FFF; delta++)
|
||||
{
|
||||
nuint candidate;
|
||||
if (i == 0)
|
||||
long signedOffset = delta * (long)AllocationGranularity;
|
||||
|
||||
// Try above, then below the target. Keep the original address as the first attempt.
|
||||
for (int sign = 0; sign < 2; sign++)
|
||||
{
|
||||
candidate = aligned;
|
||||
}
|
||||
else if ((i & 1) == 1)
|
||||
{
|
||||
candidate = aligned + (nuint)i * AllocationGranularity;
|
||||
}
|
||||
else
|
||||
{
|
||||
nuint offset = (nuint)i * AllocationGranularity;
|
||||
if (offset > aligned)
|
||||
{
|
||||
if (delta == 0 && sign != 0)
|
||||
continue;
|
||||
|
||||
long offset = sign == 0 ? signedOffset : -signedOffset;
|
||||
nuint candidate = (nuint)((long)aligned + offset);
|
||||
|
||||
// Avoid underflow to zero on below-target search.
|
||||
if (offset < 0 && candidate >= aligned)
|
||||
continue;
|
||||
|
||||
IntPtr result = NativeMethods.VirtualAllocEx(
|
||||
handle,
|
||||
(IntPtr)(nint)candidate,
|
||||
size,
|
||||
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
|
||||
MemoryProtectionType.ExecuteReadWrite);
|
||||
|
||||
if (result != IntPtr.Zero)
|
||||
{
|
||||
long distance = (long)(nuint)(nint)result - (long)(nuint)(nint)preferredAddress;
|
||||
if (distance >= int.MinValue && distance <= int.MaxValue)
|
||||
return result;
|
||||
|
||||
// The allocator gave us a nearby candidate but on the wrong side
|
||||
// of the 2 GiB boundary; treat it as unusable and keep searching.
|
||||
NativeMethods.VirtualFreeEx(handle, result, 0, MemoryFreeType.Release);
|
||||
}
|
||||
|
||||
candidate = aligned - offset;
|
||||
}
|
||||
|
||||
IntPtr result = NativeMethods.VirtualAllocEx(
|
||||
handle,
|
||||
(IntPtr)(nint)candidate,
|
||||
size,
|
||||
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
|
||||
MemoryProtectionType.ExecuteReadWrite);
|
||||
|
||||
if (result != IntPtr.Zero)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return NativeMethods.VirtualAllocEx(
|
||||
handle,
|
||||
IntPtr.Zero,
|
||||
size,
|
||||
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
|
||||
MemoryProtectionType.ExecuteReadWrite);
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +383,7 @@ public sealed class DllInjector
|
||||
if (value != IntPtr.Zero)
|
||||
return value;
|
||||
|
||||
Thread.Sleep(5);
|
||||
System.Threading.Thread.Sleep(5);
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
|
||||
+48
-1
@@ -1,7 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Process = System.Diagnostics.Process;
|
||||
using WhiteMagic.Execution;
|
||||
using WhiteMagic.Hooking;
|
||||
using WhiteMagic.Memory;
|
||||
using WhiteMagic.ProcessDiscovery;
|
||||
using WhiteMagic.Thread;
|
||||
using WhiteMagic.Windows;
|
||||
|
||||
namespace WhiteMagic;
|
||||
|
||||
@@ -24,6 +29,21 @@ public sealed class Magic : IDisposable
|
||||
/// <summary>Inline-detour manager (in-process only).</summary>
|
||||
public DetourManager DetourManager => Memory.DetourManager;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the memory region that contains <paramref name="address"/>.
|
||||
/// </summary>
|
||||
public MemoryRegion QueryRegion(IntPtr address) => Memory.QueryRegion(address);
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the committed and reserved regions of the target process address space.
|
||||
/// </summary>
|
||||
public IEnumerable<MemoryRegion> Regions => Memory.EnumerateRegions();
|
||||
|
||||
/// <summary>
|
||||
/// Factory for discovering and operating on the target process's threads.
|
||||
/// </summary>
|
||||
public ThreadFactory Threads => new ThreadFactory(Memory);
|
||||
|
||||
private Magic(MemoryBase memory)
|
||||
{
|
||||
Memory = memory;
|
||||
@@ -31,11 +51,38 @@ public sealed class Magic : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Opens an external process for reading, writing, and execution.</summary>
|
||||
public static Magic Open(System.Diagnostics.Process process)
|
||||
public static Magic Open(Process process)
|
||||
{
|
||||
return new Magic(new ExternalReader(process));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a target process by its image name. Throws if zero or more than one match.
|
||||
/// </summary>
|
||||
public static Magic Open(string processName)
|
||||
{
|
||||
using Process process = ApplicationFinder.OpenProcess(processName);
|
||||
return Open(process);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the process that owns the top-level window with the specified title.
|
||||
/// </summary>
|
||||
public static Magic OpenByWindowTitle(string title)
|
||||
{
|
||||
using Process process = ApplicationFinder.OpenByWindowTitle(title);
|
||||
return Open(process);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the process that owns the specified window handle.
|
||||
/// </summary>
|
||||
public static Magic OpenByWindowHandle(IntPtr handle)
|
||||
{
|
||||
using Process process = ApplicationFinder.OpenByWindowHandle(handle);
|
||||
return Open(process);
|
||||
}
|
||||
|
||||
/// <summary>Creates an in-process session for the current process.</summary>
|
||||
public static Magic OpenInProcess()
|
||||
{
|
||||
|
||||
@@ -6,8 +6,8 @@ namespace WhiteMagic;
|
||||
|
||||
/// <summary>
|
||||
/// Caches the widths marshalling decisions for type <typeparamref name="T"/>
|
||||
/// once, at static-constructor time. <see cref="MemoryBase.Read{T}(nint, bool)"/> and
|
||||
/// <see cref="MemoryBase.Write{T}(nint, T, bool)"/> branch on <see cref="TypeRequiresMarshal"/>
|
||||
/// once, at static-constructor time. <see cref="MemoryBase.Read{T}"/> and
|
||||
/// <see cref="MemoryBase.Write{T}"/> branch on <see cref="TypeRequiresMarshal"/>
|
||||
/// and pick the appropriate width from this cache.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to cache metadata for.</typeparam>
|
||||
@@ -24,8 +24,8 @@ public static class MarshalCache<T>
|
||||
public static readonly int Size;
|
||||
|
||||
/// <summary>
|
||||
/// The unmanaged (interop) width via <see cref="Marshal.SizeOf(System.Type)"/>. The marshal
|
||||
/// path (<see cref="Marshal.PtrToStructure(nint, System.Type)"/>/<see cref="Marshal.StructureToPtr"/>)
|
||||
/// The unmanaged (interop) width via <see cref="Marshal.SizeOf"/>. The marshal
|
||||
/// path (<see cref="Marshal.PtrToStructure"/>/<see cref="Marshal.StructureToPtr"/>)
|
||||
/// reads/writes this many bytes. Exceeds <see cref="Size"/> whenever a struct
|
||||
/// carries inline unmanaged data that the marshaler expands — inline
|
||||
/// <c>ByValTStr</c>/<c>ByValArray</c> buffers, <c>bool</c> fields (4 bytes per
|
||||
@@ -46,7 +46,7 @@ public static class MarshalCache<T>
|
||||
/// <summary>
|
||||
/// <see langword="true"/> when <typeparamref name="T"/> cannot be copied through
|
||||
/// the blittable <see cref="System.Runtime.InteropServices.MemoryMarshal"/> path
|
||||
/// and must fall back to <see cref="Marshal.PtrToStructure(nint, System.Type)"/> /
|
||||
/// and must fall back to <see cref="Marshal.PtrToStructure"/> /
|
||||
/// <see cref="Marshal.StructureToPtr"/>. This is the case when a top-level field
|
||||
/// carries <see cref="MarshalAsAttribute"/>, or when <typeparamref name="T"/>
|
||||
/// contains a managed reference
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagic.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// An immutable snapshot of a memory region as reported by <c>VirtualQueryEx</c>.
|
||||
/// </summary>
|
||||
public readonly record struct MemoryRegion
|
||||
{
|
||||
/// <summary>The base address of the region of pages.</summary>
|
||||
public IntPtr BaseAddress { get; }
|
||||
|
||||
/// <summary>The size of the region, in bytes.</summary>
|
||||
public nuint Size { get; }
|
||||
|
||||
/// <summary>The access protection of the pages in the region.</summary>
|
||||
public MemoryProtectionType Protection { get; }
|
||||
|
||||
/// <summary>The state of the pages in the region.</summary>
|
||||
public MemoryState State { get; }
|
||||
|
||||
/// <summary>The type of pages in the region.</summary>
|
||||
public MemoryType Type { get; }
|
||||
|
||||
/// <summary>The base address of a range of pages allocated by VirtualAllocEx.</summary>
|
||||
public IntPtr AllocationBase { get; }
|
||||
|
||||
/// <summary>The memory protection option when the region was initially allocated.</summary>
|
||||
public MemoryProtectionType AllocationProtect { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="MemoryRegion"/> from explicit values.
|
||||
/// </summary>
|
||||
public MemoryRegion(
|
||||
IntPtr baseAddress,
|
||||
nuint size,
|
||||
MemoryProtectionType protection,
|
||||
MemoryState state,
|
||||
MemoryType type,
|
||||
IntPtr allocationBase,
|
||||
MemoryProtectionType allocationProtect)
|
||||
{
|
||||
BaseAddress = baseAddress;
|
||||
Size = size;
|
||||
Protection = protection;
|
||||
State = state;
|
||||
Type = type;
|
||||
AllocationBase = allocationBase;
|
||||
AllocationProtect = allocationProtect;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="MemoryRegion"/> from a raw <c>MEMORY_BASIC_INFORMATION</c>.
|
||||
/// </summary>
|
||||
internal MemoryRegion(MemoryBasicInformation info)
|
||||
{
|
||||
BaseAddress = info.BaseAddress;
|
||||
Size = info.RegionSize;
|
||||
AllocationBase = info.AllocationBase;
|
||||
AllocationProtect = (MemoryProtectionType)info.AllocationProtect;
|
||||
Protection = (MemoryProtectionType)info.Protect;
|
||||
State = (MemoryState)info.State;
|
||||
Type = (MemoryType)info.Type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if <paramref name="address"/> is inside the region,
|
||||
/// defined as <c>[BaseAddress, BaseAddress + Size)</c>.
|
||||
/// </summary>
|
||||
public bool Contains(IntPtr address)
|
||||
{
|
||||
return (nuint)(address - BaseAddress) < Size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagic.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// A scope that temporarily changes page protection via <c>VirtualProtectEx</c> and
|
||||
/// restores the original protection when disposed, including when the guarded body throws.
|
||||
/// </summary>
|
||||
public sealed class ProtectionScope : IDisposable
|
||||
{
|
||||
private readonly MemoryBase _memory;
|
||||
private readonly IntPtr _address;
|
||||
private readonly nint _size;
|
||||
private readonly MemoryProtectionType _originalProtection;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new protection scope, applying <paramref name="newProtection"/> to the
|
||||
/// specified range immediately.
|
||||
/// </summary>
|
||||
internal ProtectionScope(MemoryBase memory, IntPtr address, nint size, MemoryProtectionType newProtection)
|
||||
{
|
||||
_memory = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||
|
||||
if (address == IntPtr.Zero)
|
||||
throw new ArgumentException("Address cannot be zero.", nameof(address));
|
||||
|
||||
if (size <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive.");
|
||||
|
||||
_address = address;
|
||||
_size = size;
|
||||
|
||||
if (!NativeMethods.VirtualProtectEx(
|
||||
memory.Handle,
|
||||
address,
|
||||
size,
|
||||
newProtection,
|
||||
out _originalProtection))
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException(
|
||||
$"VirtualProtectEx failed to change protection: error {error}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Restores the original page protection if it has not already been restored.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
NativeMethods.VirtualProtectEx(_memory.Handle, _address, _size, _originalProtection, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using WhiteMagic.Hooking;
|
||||
using WhiteMagic.Memory;
|
||||
using WhiteMagic.Native;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
@@ -7,7 +9,7 @@ namespace WhiteMagic;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base for all memory-access readers and writers. Provides typed
|
||||
/// <see cref="Read{T}(nint, bool)"/>/<see cref="Write{T}(nint, T, bool)"/>, array IO, string IO, and
|
||||
/// <see cref="Read{T}"/>/<see cref="Write{T}"/>, array IO, string IO, and
|
||||
/// relative/absolute addressing. Subclasses implement the concrete
|
||||
/// <see cref="ReadBytes"/> and <see cref="WriteBytes"/> methods.
|
||||
/// </summary>
|
||||
@@ -268,6 +270,62 @@ public abstract class MemoryBase : IDisposable
|
||||
return (IntPtr)((nint)absolute - (nint)ImageBase);
|
||||
}
|
||||
|
||||
// ── Memory region query ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Queries the memory region that contains <paramref name="address"/> in the target
|
||||
/// process using <c>VirtualQueryEx</c>.
|
||||
/// </summary>
|
||||
/// <returns>An immutable snapshot of the region.</returns>
|
||||
/// <exception cref="InvalidOperationException">The query fails.</exception>
|
||||
public MemoryRegion QueryRegion(IntPtr address)
|
||||
{
|
||||
nuint bufferSize = (nuint)Marshal.SizeOf<MemoryBasicInformation>();
|
||||
nuint result = NativeMethods.VirtualQueryEx(Handle, address, out MemoryBasicInformation info, bufferSize);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException($"VirtualQueryEx failed for address 0x{address:X}: error {error}.");
|
||||
}
|
||||
|
||||
return new MemoryRegion(info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the memory regions of the target process from the lowest address upward.
|
||||
/// The walk is lazy; callers can stop early without walking the entire address space.
|
||||
/// </summary>
|
||||
public IEnumerable<MemoryRegion> EnumerateRegions()
|
||||
{
|
||||
IntPtr address = IntPtr.Zero;
|
||||
nuint bufferSize = (nuint)Marshal.SizeOf<MemoryBasicInformation>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
nuint result = NativeMethods.VirtualQueryEx(Handle, address, out MemoryBasicInformation info, bufferSize);
|
||||
if (result == 0)
|
||||
yield break;
|
||||
|
||||
yield return new MemoryRegion(info);
|
||||
IntPtr next = info.BaseAddress + (nint)info.RegionSize;
|
||||
if (next.ToInt64() <= address.ToInt64())
|
||||
yield break;
|
||||
|
||||
address = next;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the page protection on a region of memory and returns a disposable scope
|
||||
/// that restores the original protection on dispose, including when an exception escapes
|
||||
/// the guarded body.
|
||||
/// </summary>
|
||||
public ProtectionScope ChangeProtection(IntPtr address, nint size, MemoryProtectionType protection)
|
||||
{
|
||||
return new ProtectionScope(this, address, size, protection);
|
||||
}
|
||||
|
||||
// ── Lifecycle ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -156,3 +156,61 @@ public static class ContextFlags
|
||||
/// <summary>AMD64: control, integer, and segment registers.</summary>
|
||||
public const uint Amd64Full = Amd64Control | Amd64Integer | Amd64Segments;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Values that describe the state of memory pages returned by <c>VirtualQueryEx</c>.
|
||||
/// </summary>
|
||||
public enum MemoryState : uint
|
||||
{
|
||||
/// <summary>Indicates committed pages for which physical storage has been allocated.</summary>
|
||||
Commit = 0x1000,
|
||||
|
||||
/// <summary>Indicates reserved pages where a range of the virtual address space is reserved without any physical storage being allocated.</summary>
|
||||
Reserve = 0x2000,
|
||||
|
||||
/// <summary>Indicates free pages not accessible to the calling process and available to be allocated.</summary>
|
||||
Free = 0x10000,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Values that describe the type of memory pages returned by <c>VirtualQueryEx</c>.
|
||||
/// </summary>
|
||||
public enum MemoryType : uint
|
||||
{
|
||||
/// <summary>Indicates that the memory pages within the region are private.</summary>
|
||||
Private = 0x20000,
|
||||
|
||||
/// <summary>Indicates that the memory pages within the region are mapped into the view of a section.</summary>
|
||||
Mapped = 0x40000,
|
||||
|
||||
/// <summary>Indicates that the memory pages within the region are mapped into the view of an image section.</summary>
|
||||
Image = 0x1000000,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flags used by <c>CreateToolhelp32Snapshot</c> to specify the portions of the system to include in the snapshot.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum SnapshotFlags : uint
|
||||
{
|
||||
/// <summary>Enumerate the heap list.</summary>
|
||||
HeapList = 0x00000001,
|
||||
|
||||
/// <summary>Enumerate the process list.</summary>
|
||||
Process = 0x00000002,
|
||||
|
||||
/// <summary>Enumerate the thread list.</summary>
|
||||
Thread = 0x00000004,
|
||||
|
||||
/// <summary>Enumerate the module list.</summary>
|
||||
Module = 0x00000008,
|
||||
|
||||
/// <summary>Enumerate the 32-bit module list for the specified process.</summary>
|
||||
Module32 = 0x00000010,
|
||||
|
||||
/// <summary>Include all processes and threads in the system.</summary>
|
||||
All = 0x0000001F,
|
||||
|
||||
/// <summary>Indicate that the snapshot handle is to be inheritable.</summary>
|
||||
Inherit = 0x80000000,
|
||||
}
|
||||
|
||||
@@ -173,4 +173,46 @@ internal static partial class NativeMethods
|
||||
SafeMemoryHandle handle,
|
||||
uint milliseconds);
|
||||
|
||||
// ── Memory query ───────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Retrieves information about a range of pages in the virtual address space of a specified process.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
internal static partial nuint VirtualQueryEx(
|
||||
SafeMemoryHandle process,
|
||||
IntPtr address,
|
||||
out MemoryBasicInformation buffer,
|
||||
nuint length);
|
||||
|
||||
// ── Thread enumeration ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Takes a snapshot of the specified processes, as well as the heaps, modules, and threads used by these processes.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
internal static partial SafeMemoryHandle CreateToolhelp32Snapshot(
|
||||
SnapshotFlags dwFlags,
|
||||
int th32ProcessID);
|
||||
|
||||
/// <summary>Retrieves information about the first thread of any process encountered in a system snapshot.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool Thread32First(
|
||||
SafeMemoryHandle hSnapshot,
|
||||
ref ThreadEntry32 lpte);
|
||||
|
||||
/// <summary>Retrieves information about the next thread of any process encountered in a system snapshot.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool Thread32Next(
|
||||
SafeMemoryHandle hSnapshot,
|
||||
ref ThreadEntry32 lpte);
|
||||
|
||||
/// <summary>Retrieves timing information for the specified thread.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetThreadTimes(
|
||||
SafeMemoryHandle thread,
|
||||
out long creationTime,
|
||||
out long exitTime,
|
||||
out long kernelTime,
|
||||
out long userTime);
|
||||
|
||||
}
|
||||
|
||||
@@ -205,3 +205,61 @@ public unsafe struct Context64
|
||||
/// <summary>The source RIP of the last exception.</summary>
|
||||
public ulong LastExceptionFromRip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Layout matches <c>MEMORY_BASIC_INFORMATION</c>. Uses pointer-sized fields so the
|
||||
/// structure is 28 bytes on x86 and 48 bytes on x64, matching the layout the OS expects
|
||||
/// from a caller of those bitnesses.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct MemoryBasicInformation
|
||||
{
|
||||
/// <summary>A pointer to the base address of the region of pages.</summary>
|
||||
public nint BaseAddress;
|
||||
|
||||
/// <summary>A pointer to the base address of a range of pages allocated by the VirtualAllocEx function.</summary>
|
||||
public nint AllocationBase;
|
||||
|
||||
/// <summary>The memory protection option when the region was initially allocated.</summary>
|
||||
public uint AllocationProtect;
|
||||
|
||||
/// <summary>The size of the region beginning at the base address, in bytes.</summary>
|
||||
public nuint RegionSize;
|
||||
|
||||
/// <summary>The state of the pages in the region.</summary>
|
||||
public uint State;
|
||||
|
||||
/// <summary>The access protection of the pages in the region.</summary>
|
||||
public uint Protect;
|
||||
|
||||
/// <summary>The type of pages in the region.</summary>
|
||||
public uint Type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Layout matches <c>THREADENTRY32</c> used by <c>Thread32First</c>/<c>Thread32Next</c>.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct ThreadEntry32
|
||||
{
|
||||
/// <summary>The size of the structure, in bytes.</summary>
|
||||
public uint dwSize;
|
||||
|
||||
/// <summary>This member is no longer used and is always zero.</summary>
|
||||
public uint cntUsage;
|
||||
|
||||
/// <summary>The thread identifier.</summary>
|
||||
public uint th32ThreadID;
|
||||
|
||||
/// <summary>The identifier of the process that owns the thread.</summary>
|
||||
public uint th32OwnerProcessID;
|
||||
|
||||
/// <summary>The kernel base priority level assigned to the thread.</summary>
|
||||
public int tpBasePri;
|
||||
|
||||
/// <summary>This member is no longer used.</summary>
|
||||
public int tpDeltaPri;
|
||||
|
||||
/// <summary>This member is reserved.</summary>
|
||||
public uint dwFlags;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic.Native;
|
||||
using WhiteMagic.Windows;
|
||||
|
||||
namespace WhiteMagic.ProcessDiscovery;
|
||||
|
||||
/// <summary>
|
||||
/// Discovers running processes by name, window title, or window handle so they can be
|
||||
/// attached through a <see cref="Magic"/> session.
|
||||
/// </summary>
|
||||
public static class ApplicationFinder
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumerates processes whose image name matches <paramref name="processName"/>
|
||||
/// (extension optional).
|
||||
/// </summary>
|
||||
public static IEnumerable<Process> Enumerate(string processName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(processName);
|
||||
|
||||
return Process.GetProcessesByName(GetNameWithoutExtension(processName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the unique process whose image name matches <paramref name="processName"/>.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Zero or multiple processes match.</exception>
|
||||
public static Process OpenProcess(string processName)
|
||||
{
|
||||
Process[] candidates = Enumerate(processName).ToArray();
|
||||
|
||||
if (candidates.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No process named '{processName}' was found.");
|
||||
}
|
||||
|
||||
if (candidates.Length > 1)
|
||||
{
|
||||
string list = string.Join(", ", candidates.Select(p => $"{p.ProcessName}:{p.Id}"));
|
||||
foreach (Process candidate in candidates)
|
||||
candidate.Dispose();
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Process name '{processName}' is ambiguous ({candidates.Length} matches): {list}");
|
||||
}
|
||||
|
||||
Process result = candidates[0];
|
||||
for (int i = 1; i < candidates.Length; i++)
|
||||
candidates[i].Dispose();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates processes that own a top-level window whose title equals
|
||||
/// <paramref name="title"/>.
|
||||
/// </summary>
|
||||
public static IEnumerable<Process> FindByWindowTitle(string title)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(title);
|
||||
|
||||
var seen = new HashSet<int>();
|
||||
foreach (RemoteWindow window in WindowFactory.GetWindows())
|
||||
{
|
||||
if (!string.Equals(window.Text, title, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
uint pid = window.ProcessId;
|
||||
if (pid == 0 || !seen.Add((int)pid))
|
||||
continue;
|
||||
|
||||
Process? process;
|
||||
try
|
||||
{
|
||||
process = global::System.Diagnostics.Process.GetProcessById((int)pid);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return process;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the unique process that owns a top-level window titled <paramref name="title"/>.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Zero or multiple windows match.</exception>
|
||||
public static Process OpenByWindowTitle(string title)
|
||||
{
|
||||
Process[] candidates = FindByWindowTitle(title).ToArray();
|
||||
|
||||
if (candidates.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No top-level window titled '{title}' was found.");
|
||||
}
|
||||
|
||||
if (candidates.Length > 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Window title '{title}' is ambiguous ({candidates.Length} matches): " +
|
||||
string.Join(", ", candidates.Select(p => $"{p.ProcessName}:{p.Id}")));
|
||||
}
|
||||
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the process that owns the specified window handle.
|
||||
/// </summary>
|
||||
public static Process OpenByWindowHandle(IntPtr handle)
|
||||
{
|
||||
if (handle == IntPtr.Zero)
|
||||
throw new ArgumentException("Window handle cannot be zero.", nameof(handle));
|
||||
|
||||
uint tid = NativeMethods.GetWindowThreadProcessId(handle, out uint processId);
|
||||
if (tid == 0 || processId == 0)
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException(
|
||||
$"GetWindowThreadProcessId failed for handle {handle:X}: error {error}.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return global::System.Diagnostics.Process.GetProcessById((int)processId);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Process {processId} owning window {handle:X} is no longer running.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetNameWithoutExtension(string name)
|
||||
{
|
||||
if (name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
|
||||
return name[..^4];
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace WhiteMagic.Thread;
|
||||
|
||||
/// <summary>
|
||||
/// A disposable scope that tracks a set of threads frozen by <see cref="ThreadFactory.Freeze"/>.
|
||||
/// Disposing the scope resumes exactly those threads, in reverse order, even if the guarded
|
||||
/// body throws, and then disposes the underlying thread handles.
|
||||
/// </summary>
|
||||
public sealed class FrozenThread : IDisposable
|
||||
{
|
||||
private readonly IReadOnlyList<RemoteThread> _threads;
|
||||
private bool _disposed;
|
||||
|
||||
internal FrozenThread(IReadOnlyList<RemoteThread> threads)
|
||||
{
|
||||
_threads = threads ?? throw new ArgumentNullException(nameof(threads));
|
||||
}
|
||||
|
||||
/// <summary>The threads suspended by this freeze scope.</summary>
|
||||
public IEnumerable<RemoteThread> Threads => _threads;
|
||||
|
||||
/// <summary>
|
||||
/// Resumes the frozen threads in reverse order, then disposes every thread handle.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
|
||||
foreach (RemoteThread thread in _threads.Reverse())
|
||||
{
|
||||
try
|
||||
{
|
||||
thread.Resume();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Resume-on-dispose is best-effort; the handle is still disposed below.
|
||||
}
|
||||
}
|
||||
|
||||
foreach (RemoteThread thread in _threads)
|
||||
{
|
||||
thread.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic.Native;
|
||||
using WhiteMagic.ThreadEnvironment;
|
||||
|
||||
namespace WhiteMagic.Thread;
|
||||
|
||||
/// <summary>
|
||||
/// A handle to an existing thread in the target process. Provides suspend/resume,
|
||||
/// context read/write, and TEB query.
|
||||
/// </summary>
|
||||
public sealed class RemoteThread : IDisposable
|
||||
{
|
||||
private readonly MemoryBase _memory;
|
||||
private readonly SafeMemoryHandle _handle;
|
||||
private readonly int _id;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>The operating-system identifier of this thread.</summary>
|
||||
public int Id => _id;
|
||||
|
||||
/// <summary>The native thread handle.</summary>
|
||||
internal SafeMemoryHandle Handle => _handle;
|
||||
|
||||
internal RemoteThread(MemoryBase memory, int threadId, SafeMemoryHandle handle)
|
||||
{
|
||||
_memory = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||
_id = threadId;
|
||||
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the thread specified by <paramref name="threadId"/> in the target process
|
||||
/// represented by <paramref name="memory"/>.
|
||||
/// </summary>
|
||||
public RemoteThread(MemoryBase memory, int threadId)
|
||||
: this(memory, threadId, OpenHandle(threadId))
|
||||
{
|
||||
}
|
||||
|
||||
private static SafeMemoryHandle OpenHandle(int threadId)
|
||||
{
|
||||
if (threadId <= 0)
|
||||
throw new ArgumentException("Thread ID must be positive.", nameof(threadId));
|
||||
|
||||
const ThreadAccess requiredAccess =
|
||||
ThreadAccess.SuspendResume |
|
||||
ThreadAccess.GetContext |
|
||||
ThreadAccess.SetContext |
|
||||
ThreadAccess.QueryInformation;
|
||||
|
||||
SafeMemoryHandle handle = NativeMethods.OpenThread(requiredAccess, false, threadId);
|
||||
if (handle.IsInvalid)
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException($"OpenThread failed for thread {threadId}: error {error}.");
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suspends the thread and returns its previous suspend count.
|
||||
/// </summary>
|
||||
public uint Suspend()
|
||||
{
|
||||
uint result = NativeMethods.SuspendThread(_handle);
|
||||
if (result == 0xFFFFFFFF)
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException($"SuspendThread failed for thread {_id}: error {error}.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resumes the thread and returns its previous suspend count.
|
||||
/// </summary>
|
||||
public uint Resume()
|
||||
{
|
||||
uint result = NativeMethods.ResumeThread(_handle);
|
||||
if (result == 0xFFFFFFFF)
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException($"ResumeThread failed for thread {_id}: error {error}.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the 64-bit native context of the thread. Valid only for 64-bit targets.
|
||||
/// </summary>
|
||||
public unsafe void GetContext64(out Context64 context)
|
||||
{
|
||||
nint size = Marshal.SizeOf<Context64>();
|
||||
void* ptr = NativeMemory.AlignedAlloc((nuint)size, 16);
|
||||
try
|
||||
{
|
||||
Unsafe.InitBlock(ptr, 0, (uint)size);
|
||||
((Context64*)ptr)->ContextFlags = ContextFlags.Amd64Full;
|
||||
|
||||
if (!NativeMethods.GetThreadContext(_handle, ref *(Context64*)ptr))
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException($"GetThreadContext failed for thread {_id}: error {error}.");
|
||||
}
|
||||
|
||||
context = *(Context64*)ptr;
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMemory.AlignedFree(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the 64-bit native context of the thread. Valid only for 64-bit targets.
|
||||
/// </summary>
|
||||
public unsafe void SetContext64(ref Context64 context)
|
||||
{
|
||||
nint size = Marshal.SizeOf<Context64>();
|
||||
void* ptr = NativeMemory.AlignedAlloc((nuint)size, 16);
|
||||
try
|
||||
{
|
||||
*(Context64*)ptr = context;
|
||||
if (!NativeMethods.SetThreadContext(_handle, ref *(Context64*)ptr))
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException($"SetThreadContext failed for thread {_id}: error {error}.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMemory.AlignedFree(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the 32-bit native context of the thread. Valid only for 32-bit targets.
|
||||
/// </summary>
|
||||
public void GetContext32(out Context32 context)
|
||||
{
|
||||
if (_memory.Is64Bit)
|
||||
{
|
||||
context = default;
|
||||
throw new InvalidOperationException(
|
||||
"Use GetContext64 for 64-bit targets; GetContext32 is valid for 32-bit targets only.");
|
||||
}
|
||||
|
||||
context = new Context32 { ContextFlags = ContextFlags.X86Full };
|
||||
if (!NativeMethods.GetThreadContext(_handle, ref context))
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException($"GetThreadContext failed for thread {_id}: error {error}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the 32-bit native context of the thread. Valid only for 32-bit targets.
|
||||
/// </summary>
|
||||
public void SetContext32(ref Context32 context)
|
||||
{
|
||||
if (_memory.Is64Bit)
|
||||
throw new InvalidOperationException(
|
||||
"Use SetContext64 for 64-bit targets; SetContext32 is valid for 32-bit targets only.");
|
||||
|
||||
if (!NativeMethods.SetThreadContext(_handle, ref context))
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException($"SetThreadContext failed for thread {_id}: error {error}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a managed reader for this thread's Thread Environment Block.
|
||||
/// </summary>
|
||||
public ManagedTeb GetTeb()
|
||||
{
|
||||
return new ManagedTeb(_memory, _id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
_handle.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagic.Thread;
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates and selects threads belonging to the target process.
|
||||
/// </summary>
|
||||
public sealed class ThreadFactory
|
||||
{
|
||||
private readonly MemoryBase _memory;
|
||||
|
||||
/// <summary>Creates a factory bound to the target process represented by <paramref name="memory"/>.</summary>
|
||||
public ThreadFactory(MemoryBase memory)
|
||||
{
|
||||
_memory = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates every thread that belongs to the target process.
|
||||
/// </summary>
|
||||
public IEnumerable<RemoteThread> Enumerate()
|
||||
{
|
||||
foreach (int threadId in CollectThreadIds())
|
||||
{
|
||||
SafeMemoryHandle handle = NativeMethods.OpenThread(
|
||||
ThreadAccess.SuspendResume |
|
||||
ThreadAccess.GetContext |
|
||||
ThreadAccess.SetContext |
|
||||
ThreadAccess.QueryInformation,
|
||||
false,
|
||||
threadId);
|
||||
|
||||
if (handle.IsInvalid)
|
||||
continue;
|
||||
|
||||
yield return new RemoteThread(_memory, threadId, handle);
|
||||
}
|
||||
}
|
||||
|
||||
private int[] CollectThreadIds()
|
||||
{
|
||||
using SafeMemoryHandle snapshot = NativeMethods.CreateToolhelp32Snapshot(SnapshotFlags.Thread, 0);
|
||||
if (snapshot.IsInvalid)
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException($"CreateToolhelp32Snapshot failed: error {error}.");
|
||||
}
|
||||
|
||||
var entry = new ThreadEntry32
|
||||
{
|
||||
dwSize = (uint)Marshal.SizeOf<ThreadEntry32>()
|
||||
};
|
||||
|
||||
var ids = new List<int>();
|
||||
|
||||
if (!NativeMethods.Thread32First(snapshot, ref entry))
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
if (error == 18 || error == 259) // ERROR_NO_MORE_FILES / ERROR_NO_MORE_ITEMS
|
||||
return ids.ToArray();
|
||||
|
||||
throw new InvalidOperationException($"Thread32First failed: error {error}.");
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
if (entry.th32OwnerProcessID == (uint)_memory.ProcessId)
|
||||
ids.Add((int)entry.th32ThreadID);
|
||||
}
|
||||
while (NativeMethods.Thread32Next(snapshot, ref entry));
|
||||
|
||||
return ids.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the thread with the specified operating-system identifier if it belongs
|
||||
/// to the target process.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The thread does not belong to the target process.</exception>
|
||||
public RemoteThread GetThreadById(int threadId)
|
||||
{
|
||||
if (threadId <= 0)
|
||||
throw new ArgumentException("Thread ID must be positive.", nameof(threadId));
|
||||
|
||||
const ThreadAccess requiredAccess =
|
||||
ThreadAccess.SuspendResume |
|
||||
ThreadAccess.GetContext |
|
||||
ThreadAccess.SetContext |
|
||||
ThreadAccess.QueryInformation;
|
||||
|
||||
SafeMemoryHandle handle = NativeMethods.OpenThread(requiredAccess, false, threadId);
|
||||
if (handle.IsInvalid)
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException($"OpenThread failed for thread {threadId}: error {error}.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var info = new ThreadBasicInformation();
|
||||
int status = NativeMethods.NtQueryInformationThread(
|
||||
handle,
|
||||
0,
|
||||
ref info,
|
||||
(uint)Marshal.SizeOf<ThreadBasicInformation>(),
|
||||
out _);
|
||||
|
||||
if (status < 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"NtQueryInformationThread failed for thread {threadId} (NTSTATUS {status:X8}).");
|
||||
}
|
||||
|
||||
if ((uint)(nint)info.ClientId.UniqueProcess != (uint)_memory.ProcessId)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Thread {threadId} does not belong to process {_memory.ProcessId}.");
|
||||
}
|
||||
|
||||
// Ownership of the validated handle transfers to the RemoteThread.
|
||||
return new RemoteThread(_memory, threadId, handle);
|
||||
}
|
||||
catch
|
||||
{
|
||||
handle.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the earliest-created thread of the target process.
|
||||
/// </summary>
|
||||
public RemoteThread MainThread
|
||||
{
|
||||
get
|
||||
{
|
||||
RemoteThread? earliest = null;
|
||||
long earliestTime = long.MaxValue;
|
||||
|
||||
foreach (RemoteThread thread in Enumerate())
|
||||
{
|
||||
long creationTime = GetCreationTime(thread.Id);
|
||||
if (creationTime < earliestTime)
|
||||
{
|
||||
earliestTime = creationTime;
|
||||
earliest?.Dispose();
|
||||
earliest = thread;
|
||||
}
|
||||
else
|
||||
{
|
||||
thread.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (earliest is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Process {_memory.ProcessId} has no observable threads.");
|
||||
}
|
||||
|
||||
return earliest;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suspends the supplied threads and returns a disposable scope that resumes exactly
|
||||
/// those threads when disposed, including when an exception escapes the guarded body.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Do not freeze the target's threads while executing target code through a remote
|
||||
/// thread or main-thread pump; doing so can deadlock because the frozen thread is the
|
||||
/// one responsible for running the code.
|
||||
/// </remarks>
|
||||
public FrozenThread Freeze(IEnumerable<RemoteThread> threads)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(threads);
|
||||
|
||||
var suspended = new List<RemoteThread>();
|
||||
try
|
||||
{
|
||||
foreach (RemoteThread thread in threads)
|
||||
{
|
||||
thread.Suspend();
|
||||
suspended.Add(thread);
|
||||
}
|
||||
|
||||
return new FrozenThread(suspended);
|
||||
}
|
||||
catch
|
||||
{
|
||||
foreach (RemoteThread thread in suspended)
|
||||
{
|
||||
try
|
||||
{
|
||||
thread.Resume();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort unwind.
|
||||
}
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suspends all target threads selected by <paramref name="predicate"/>.
|
||||
/// </summary>
|
||||
public FrozenThread Freeze(Func<RemoteThread, bool> predicate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
|
||||
var selected = new List<RemoteThread>();
|
||||
try
|
||||
{
|
||||
foreach (RemoteThread thread in Enumerate())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (predicate(thread))
|
||||
selected.Add(thread);
|
||||
else
|
||||
thread.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
thread.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return Freeze(selected);
|
||||
}
|
||||
catch
|
||||
{
|
||||
foreach (RemoteThread thread in selected)
|
||||
thread.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private long GetCreationTime(int threadId)
|
||||
{
|
||||
using SafeMemoryHandle handle = NativeMethods.OpenThread(ThreadAccess.QueryInformation, false, threadId);
|
||||
if (handle.IsInvalid)
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException($"OpenThread failed for thread {threadId}: error {error}.");
|
||||
}
|
||||
|
||||
if (!NativeMethods.GetThreadTimes(handle, out long creationTime, out _, out _, out _))
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException($"GetThreadTimes failed for thread {threadId}: error {error}.");
|
||||
}
|
||||
|
||||
return creationTime;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Platforms>x86;x64;AnyCPU</Platforms>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -113,7 +113,6 @@ public sealed class RemoteWindow
|
||||
return NativeMethods.FlashWindowEx(ref info);
|
||||
}
|
||||
|
||||
/// <summary>Returns a string representation of this window, including its handle, class name, and title.</summary>
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Thread = System.Threading.Thread;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Injection;
|
||||
using WhiteMagic.Native;
|
||||
@@ -69,7 +70,7 @@ public class DllInjectorTests
|
||||
int osThreadId = 0;
|
||||
Exception? threadError = null;
|
||||
|
||||
var helper = new Thread(() =>
|
||||
var helper = new System.Threading.Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -80,7 +81,7 @@ public class DllInjectorTests
|
||||
// also be stopped once its original context is restored.
|
||||
while (!stopEvent.IsSet)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
System.Threading.Thread.Sleep(10);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Linq;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Memory;
|
||||
using WhiteMagic.Native;
|
||||
using WhiteMagic.Thread;
|
||||
using Xunit;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the convenience accessors exposed directly on <see cref="Magic"/>.
|
||||
/// </summary>
|
||||
public sealed class MagicFacadeTests
|
||||
{
|
||||
[Fact]
|
||||
public void QueryRegion_returns_region_containing_image_base()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
MemoryRegion region = magic.QueryRegion(magic.Memory.ImageBase);
|
||||
Assert.True(region.Contains(magic.Memory.ImageBase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Regions_enumerates_region_containing_image_base()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
|
||||
bool found = magic.Regions.Any(r => r.Contains(magic.Memory.ImageBase));
|
||||
|
||||
Assert.True(found);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Threads_factory_enumerates_current_thread()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
ThreadFactory factory = magic.Threads;
|
||||
|
||||
int currentOsId = (int)NativeMethods.GetCurrentThreadId();
|
||||
bool found = factory.Enumerate().Any(t => t.Id == currentOsId);
|
||||
|
||||
Assert.True(found);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Memory;
|
||||
using WhiteMagic.Native;
|
||||
using Xunit;
|
||||
|
||||
namespace WhiteMagicTest.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for memory-region query, enumeration and scoped protection (tasks 1.2, 1.4, 1.6, 1.8).
|
||||
/// </summary>
|
||||
public sealed class MemoryRegionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Contains_returns_true_for_addresses_inside_half_open_range()
|
||||
{
|
||||
var region = new MemoryRegion(
|
||||
new IntPtr(0x10000),
|
||||
0x1000,
|
||||
MemoryProtectionType.ReadWrite,
|
||||
MemoryState.Commit,
|
||||
MemoryType.Private,
|
||||
new IntPtr(0x10000),
|
||||
MemoryProtectionType.ReadWrite);
|
||||
|
||||
Assert.True(region.Contains(new IntPtr(0x10000)));
|
||||
Assert.True(region.Contains(new IntPtr(0x10FFF)));
|
||||
Assert.False(region.Contains(new IntPtr(0x11000)));
|
||||
Assert.False(region.Contains(new IntPtr(0x0FFF)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueryRegion_returns_region_containing_committed_address()
|
||||
{
|
||||
using var reader = new InProcessReader();
|
||||
nint pageSize = Environment.SystemPageSize;
|
||||
|
||||
IntPtr block = NativeMethods.VirtualAllocEx(
|
||||
reader.Handle,
|
||||
IntPtr.Zero,
|
||||
pageSize,
|
||||
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
|
||||
MemoryProtectionType.ReadWrite);
|
||||
|
||||
Assert.NotEqual(IntPtr.Zero, block);
|
||||
|
||||
try
|
||||
{
|
||||
MemoryRegion region = reader.QueryRegion(block);
|
||||
|
||||
Assert.Equal(block, region.BaseAddress);
|
||||
Assert.True(region.Contains(block));
|
||||
Assert.True(region.Contains(block + (int)pageSize - 1));
|
||||
Assert.Equal(MemoryState.Commit, region.State);
|
||||
Assert.Equal(MemoryType.Private, region.Type);
|
||||
Assert.Equal(MemoryProtectionType.ReadWrite, region.Protection);
|
||||
Assert.Equal(MemoryProtectionType.ReadWrite, region.AllocationProtect);
|
||||
Assert.Equal(block, region.AllocationBase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMethods.VirtualFreeEx(reader.Handle, block, 0, MemoryFreeType.Release);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnumerateRegions_yields_ascending_non_overlapping_regions()
|
||||
{
|
||||
using var reader = new InProcessReader();
|
||||
|
||||
MemoryRegion[] regions = reader.EnumerateRegions().Take(5).ToArray();
|
||||
Assert.True(regions.Length > 0);
|
||||
|
||||
for (int i = 1; i < regions.Length; i++)
|
||||
{
|
||||
Assert.True(
|
||||
(nuint)regions[i].BaseAddress >=
|
||||
(nuint)regions[i - 1].BaseAddress + regions[i - 1].Size);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnumerateRegions_is_lazy_and_stops_early()
|
||||
{
|
||||
using var reader = new InProcessReader();
|
||||
|
||||
// Taking a single item must not force a full address-space walk.
|
||||
MemoryRegion first = reader.EnumerateRegions().First();
|
||||
Assert.True(first.Size > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangeProtection_applies_new_protection_inside_scope_and_restores_on_dispose()
|
||||
{
|
||||
using var reader = new InProcessReader();
|
||||
nint pageSize = Environment.SystemPageSize;
|
||||
|
||||
IntPtr block = NativeMethods.VirtualAllocEx(
|
||||
reader.Handle,
|
||||
IntPtr.Zero,
|
||||
pageSize,
|
||||
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
|
||||
MemoryProtectionType.ReadWrite);
|
||||
|
||||
Assert.NotEqual(IntPtr.Zero, block);
|
||||
|
||||
try
|
||||
{
|
||||
Assert.Equal(MemoryProtectionType.ReadWrite, reader.QueryRegion(block).Protection);
|
||||
|
||||
using (reader.ChangeProtection(block, pageSize, MemoryProtectionType.ExecuteReadWrite))
|
||||
{
|
||||
Assert.Equal(MemoryProtectionType.ExecuteReadWrite, reader.QueryRegion(block).Protection);
|
||||
}
|
||||
|
||||
Assert.Equal(MemoryProtectionType.ReadWrite, reader.QueryRegion(block).Protection);
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMethods.VirtualFreeEx(reader.Handle, block, 0, MemoryFreeType.Release);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangeProtection_restores_original_protection_when_body_throws()
|
||||
{
|
||||
using var reader = new InProcessReader();
|
||||
nint pageSize = Environment.SystemPageSize;
|
||||
|
||||
IntPtr block = NativeMethods.VirtualAllocEx(
|
||||
reader.Handle,
|
||||
IntPtr.Zero,
|
||||
pageSize,
|
||||
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
|
||||
MemoryProtectionType.ReadWrite);
|
||||
|
||||
Assert.NotEqual(IntPtr.Zero, block);
|
||||
|
||||
try
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(new Action(() =>
|
||||
{
|
||||
using (reader.ChangeProtection(block, pageSize, MemoryProtectionType.ExecuteReadWrite))
|
||||
{
|
||||
Assert.Equal(MemoryProtectionType.ExecuteReadWrite, reader.QueryRegion(block).Protection);
|
||||
throw new InvalidOperationException("Intentional failure inside scope.");
|
||||
}
|
||||
}));
|
||||
|
||||
Assert.Equal(MemoryProtectionType.ReadWrite, reader.QueryRegion(block).Protection);
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMethods.VirtualFreeEx(reader.Handle, block, 0, MemoryFreeType.Release);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Native;
|
||||
using WhiteMagic.ProcessDiscovery;
|
||||
using Xunit;
|
||||
|
||||
namespace WhiteMagicTest.ProcessDiscovery;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for process discovery via <see cref="ApplicationFinder"/> and the matching
|
||||
/// <see cref="Magic.Open"/> overloads.
|
||||
/// </summary>
|
||||
public sealed class ApplicationFinderTests
|
||||
{
|
||||
[Fact]
|
||||
public void Enumerate_finds_current_process_by_name()
|
||||
{
|
||||
string currentName = Process.GetCurrentProcess().ProcessName;
|
||||
|
||||
Process[] found = ApplicationFinder.Enumerate(currentName).ToArray();
|
||||
|
||||
Assert.True(found.Length >= 1);
|
||||
Assert.Contains(found, p => p.Id == Process.GetCurrentProcess().Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Open_by_name_returns_current_process_when_unique()
|
||||
{
|
||||
string currentName = Process.GetCurrentProcess().ProcessName;
|
||||
|
||||
using Process process = ApplicationFinder.OpenProcess(currentName);
|
||||
|
||||
Assert.Equal(Process.GetCurrentProcess().Id, process.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Open_throws_when_name_is_ambiguous()
|
||||
{
|
||||
// Look for a multi-instance system process; skip if the environment is not typical.
|
||||
Process[] candidates = Process.GetProcessesByName("svchost");
|
||||
if (candidates.Length <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InvalidOperationException ex = Assert.Throws<InvalidOperationException>(
|
||||
() => ApplicationFinder.OpenProcess("svchost"));
|
||||
|
||||
Assert.Contains("svchost", ex.Message);
|
||||
Assert.Contains("ambiguous", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Open_throws_when_no_process_matches()
|
||||
{
|
||||
InvalidOperationException ex = Assert.Throws<InvalidOperationException>(
|
||||
() => ApplicationFinder.OpenProcess("probably-not-loaded-xyz.exe"));
|
||||
|
||||
Assert.Contains("No process", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenByWindowHandle_returns_owning_process()
|
||||
{
|
||||
IntPtr handle = Process.GetCurrentProcess().MainWindowHandle;
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using Process process = ApplicationFinder.OpenByWindowHandle(handle);
|
||||
Assert.Equal(Process.GetCurrentProcess().Id, process.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenByWindowHandle_throws_for_zero_handle()
|
||||
{
|
||||
Assert.Throws<ArgumentException>("handle", () => ApplicationFinder.OpenByWindowHandle(IntPtr.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Magic_Open_by_name_attaches_to_current_process()
|
||||
{
|
||||
string currentName = Process.GetCurrentProcess().ProcessName;
|
||||
|
||||
using var magic = Magic.Open(currentName);
|
||||
|
||||
Assert.Equal(Process.GetCurrentProcess().Id, magic.Memory.ProcessId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Magic_OpenByWindowHandle_attaches_to_owning_process()
|
||||
{
|
||||
IntPtr handle = Process.GetCurrentProcess().MainWindowHandle;
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var magic = Magic.OpenByWindowHandle(handle);
|
||||
|
||||
Assert.Equal(Process.GetCurrentProcess().Id, magic.Memory.ProcessId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using SysThread = System.Threading.Thread;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Native;
|
||||
using WhiteMagic.Thread;
|
||||
using Xunit;
|
||||
|
||||
namespace WhiteMagicTest.Thread;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for scoped thread freeze via <see cref="FrozenThread"/> and <see cref="ThreadFactory.Freeze"/>.
|
||||
/// </summary>
|
||||
public sealed class FrozenThreadTests
|
||||
{
|
||||
[Fact]
|
||||
public void Freeze_suspends_selected_workers_until_disposed()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
var factory = new ThreadFactory(magic.Memory);
|
||||
|
||||
using var cts1 = new CancellationTokenSource();
|
||||
using var cts2 = new CancellationTokenSource();
|
||||
var started1 = new ManualResetEventSlim(false);
|
||||
var started2 = new ManualResetEventSlim(false);
|
||||
int osThreadId1 = 0;
|
||||
int osThreadId2 = 0;
|
||||
|
||||
var worker1 = new SysThread(() =>
|
||||
{
|
||||
osThreadId1 = (int)NativeMethods.GetCurrentThreadId();
|
||||
started1.Set();
|
||||
while (!cts1.IsCancellationRequested)
|
||||
SysThread.Sleep(10);
|
||||
});
|
||||
|
||||
var worker2 = new SysThread(() =>
|
||||
{
|
||||
osThreadId2 = (int)NativeMethods.GetCurrentThreadId();
|
||||
started2.Set();
|
||||
while (!cts2.IsCancellationRequested)
|
||||
SysThread.Sleep(10);
|
||||
});
|
||||
|
||||
worker1.Start();
|
||||
worker2.Start();
|
||||
started1.Wait();
|
||||
started2.Wait();
|
||||
|
||||
int[] targetIds = [osThreadId1, osThreadId2];
|
||||
|
||||
try
|
||||
{
|
||||
var selected = factory.Enumerate().Where(t => targetIds.Contains(t.Id)).ToList();
|
||||
Assert.Equal(2, selected.Count);
|
||||
|
||||
using (factory.Freeze(selected))
|
||||
{
|
||||
cts1.Cancel();
|
||||
cts2.Cancel();
|
||||
|
||||
Assert.False(worker1.Join(100));
|
||||
Assert.False(worker2.Join(100));
|
||||
}
|
||||
|
||||
Assert.True(worker1.Join(1000));
|
||||
Assert.True(worker2.Join(1000));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (worker1.IsAlive)
|
||||
{
|
||||
cts1.Cancel();
|
||||
using var t = new RemoteThread(magic.Memory, osThreadId1);
|
||||
t.Resume();
|
||||
worker1.Join(1000);
|
||||
}
|
||||
|
||||
if (worker2.IsAlive)
|
||||
{
|
||||
cts2.Cancel();
|
||||
using var t = new RemoteThread(magic.Memory, osThreadId2);
|
||||
t.Resume();
|
||||
worker2.Join(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_resumes_only_frozen_threads_leaving_external_suspends_intact()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
var factory = new ThreadFactory(magic.Memory);
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var started = new ManualResetEventSlim(false);
|
||||
int osThreadId = 0;
|
||||
|
||||
var worker = new SysThread(() =>
|
||||
{
|
||||
osThreadId = (int)NativeMethods.GetCurrentThreadId();
|
||||
started.Set();
|
||||
while (!cts.IsCancellationRequested)
|
||||
SysThread.Sleep(10);
|
||||
});
|
||||
|
||||
worker.Start();
|
||||
started.Wait();
|
||||
|
||||
try
|
||||
{
|
||||
// Suspend the worker externally first.
|
||||
using (var external = new RemoteThread(magic.Memory, osThreadId))
|
||||
{
|
||||
external.Suspend();
|
||||
|
||||
var selected = factory.Enumerate().Where(t => t.Id == osThreadId).ToList();
|
||||
using (factory.Freeze(selected))
|
||||
{
|
||||
// Frozen scope adds one more suspend count.
|
||||
}
|
||||
|
||||
// After the freeze scope disposes, the worker was resumed once.
|
||||
// Because it was already externally suspended, it should still be suspended.
|
||||
cts.Cancel();
|
||||
Assert.False(worker.Join(100));
|
||||
|
||||
external.Resume();
|
||||
}
|
||||
|
||||
Assert.True(worker.Join(1000));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (worker.IsAlive)
|
||||
{
|
||||
cts.Cancel();
|
||||
using var t = new RemoteThread(magic.Memory, osThreadId);
|
||||
t.Resume();
|
||||
worker.Join(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exception_in_body_still_resumes_frozen_threads()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
var factory = new ThreadFactory(magic.Memory);
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var started = new ManualResetEventSlim(false);
|
||||
int osThreadId = 0;
|
||||
|
||||
var worker = new SysThread(() =>
|
||||
{
|
||||
osThreadId = (int)NativeMethods.GetCurrentThreadId();
|
||||
started.Set();
|
||||
while (!cts.IsCancellationRequested)
|
||||
SysThread.Sleep(10);
|
||||
});
|
||||
|
||||
worker.Start();
|
||||
started.Wait();
|
||||
|
||||
try
|
||||
{
|
||||
var selected = factory.Enumerate().Where(t => t.Id == osThreadId).ToList();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(new Action(() =>
|
||||
{
|
||||
using (factory.Freeze(selected))
|
||||
{
|
||||
throw new InvalidOperationException("Intentional failure inside freeze scope.");
|
||||
}
|
||||
}));
|
||||
|
||||
cts.Cancel();
|
||||
Assert.True(worker.Join(1000));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (worker.IsAlive)
|
||||
{
|
||||
cts.Cancel();
|
||||
using var t = new RemoteThread(magic.Memory, osThreadId);
|
||||
t.Resume();
|
||||
worker.Join(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System.Threading;
|
||||
using Thread = System.Threading.Thread;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Native;
|
||||
using WhiteMagic.Thread;
|
||||
using Xunit;
|
||||
|
||||
namespace WhiteMagicTest.Thread;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="RemoteThread.GetContext64"/> / <see cref="RemoteThread.SetContext64"/>.
|
||||
/// 32-bit/WOW64 context is tested on a 32-bit host run.
|
||||
/// </summary>
|
||||
public sealed class RemoteThreadContextTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetContext64_SetContext64_round_trip_on_suspended_self_thread()
|
||||
{
|
||||
if (!Environment.Is64BitProcess)
|
||||
return;
|
||||
|
||||
using var magic = Magic.OpenInProcess();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var started = new ManualResetEventSlim(false);
|
||||
int osThreadId = 0;
|
||||
|
||||
var worker = new System.Threading.Thread(() =>
|
||||
{
|
||||
osThreadId = (int)NativeMethods.GetCurrentThreadId();
|
||||
started.Set();
|
||||
while (!cts.IsCancellationRequested)
|
||||
System.Threading.Thread.Sleep(10);
|
||||
});
|
||||
|
||||
worker.Start();
|
||||
started.Wait();
|
||||
|
||||
try
|
||||
{
|
||||
using var thread = new RemoteThread(magic.Memory, osThreadId);
|
||||
thread.Suspend();
|
||||
System.Threading.Thread.Sleep(100);
|
||||
|
||||
thread.GetContext64(out Context64 context);
|
||||
Assert.NotEqual(0uL, context.Rip);
|
||||
|
||||
const ulong sentinel = 0x123456789ABCDEF0uL;
|
||||
ulong originalRax = context.Rax;
|
||||
context.Rax = sentinel;
|
||||
thread.SetContext64(ref context);
|
||||
|
||||
thread.GetContext64(out context);
|
||||
Assert.Equal(sentinel, context.Rax);
|
||||
|
||||
// Restore the original register before resuming so the worker keeps running.
|
||||
context.Rax = originalRax;
|
||||
thread.SetContext64(ref context);
|
||||
|
||||
thread.Resume();
|
||||
cts.Cancel();
|
||||
Assert.True(worker.Join(1000));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (worker.IsAlive)
|
||||
{
|
||||
cts.Cancel();
|
||||
using var thread = new RemoteThread(magic.Memory, osThreadId);
|
||||
thread.Resume();
|
||||
worker.Join(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetContext32_SetContext32_round_trip_on_suspended_self_thread()
|
||||
{
|
||||
if (Environment.Is64BitProcess)
|
||||
return;
|
||||
|
||||
using var magic = Magic.OpenInProcess();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var started = new ManualResetEventSlim(false);
|
||||
int osThreadId = 0;
|
||||
|
||||
var worker = new System.Threading.Thread(() =>
|
||||
{
|
||||
osThreadId = (int)NativeMethods.GetCurrentThreadId();
|
||||
started.Set();
|
||||
while (!cts.IsCancellationRequested)
|
||||
System.Threading.Thread.Sleep(10);
|
||||
});
|
||||
|
||||
worker.Start();
|
||||
started.Wait();
|
||||
|
||||
try
|
||||
{
|
||||
using var thread = new RemoteThread(magic.Memory, osThreadId);
|
||||
thread.Suspend();
|
||||
|
||||
thread.GetContext32(out Context32 context);
|
||||
Assert.NotEqual(0u, context.Eip);
|
||||
|
||||
const uint sentinel = 0x89ABCDEFu;
|
||||
uint originalEax = context.Eax;
|
||||
context.Eax = sentinel;
|
||||
thread.SetContext32(ref context);
|
||||
|
||||
thread.GetContext32(out context);
|
||||
Assert.Equal(sentinel, context.Eax);
|
||||
|
||||
context.Eax = originalEax;
|
||||
thread.SetContext32(ref context);
|
||||
|
||||
thread.Resume();
|
||||
cts.Cancel();
|
||||
Assert.True(worker.Join(1000));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (worker.IsAlive)
|
||||
{
|
||||
cts.Cancel();
|
||||
using var thread = new RemoteThread(magic.Memory, osThreadId);
|
||||
thread.Resume();
|
||||
worker.Join(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Threading;
|
||||
using Thread = System.Threading.Thread;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Native;
|
||||
using WhiteMagic.Thread;
|
||||
using Xunit;
|
||||
|
||||
namespace WhiteMagicTest.Thread;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="RemoteThread"/> open/suspend/resume and context round-trip.
|
||||
/// </summary>
|
||||
public sealed class RemoteThreadTests
|
||||
{
|
||||
[Fact]
|
||||
public void Open_by_id_succeeds_for_current_thread()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
int currentId = (int)NativeMethods.GetCurrentThreadId();
|
||||
|
||||
using var thread = new RemoteThread(magic.Memory, currentId);
|
||||
Assert.Equal(currentId, thread.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Suspend_returns_prior_count_and_stops_worker()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var started = new ManualResetEventSlim(false);
|
||||
int osThreadId = 0;
|
||||
|
||||
var worker = new System.Threading.Thread(() =>
|
||||
{
|
||||
osThreadId = (int)NativeMethods.GetCurrentThreadId();
|
||||
started.Set();
|
||||
while (!cts.IsCancellationRequested)
|
||||
System.Threading.Thread.Sleep(10);
|
||||
});
|
||||
|
||||
worker.Start();
|
||||
started.Wait();
|
||||
|
||||
try
|
||||
{
|
||||
using var thread = new RemoteThread(magic.Memory, osThreadId);
|
||||
|
||||
uint prior = thread.Suspend();
|
||||
Assert.True(prior < 0xFFFFFFFF);
|
||||
|
||||
cts.Cancel();
|
||||
// Worker cannot observe cancellation while suspended.
|
||||
Assert.False(worker.Join(100));
|
||||
|
||||
thread.Resume();
|
||||
Assert.True(worker.Join(1000));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (worker.IsAlive)
|
||||
{
|
||||
cts.Cancel();
|
||||
using var thread = new RemoteThread(magic.Memory, osThreadId);
|
||||
thread.Resume();
|
||||
worker.Join(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resume_restarts_a_suspended_worker()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var started = new ManualResetEventSlim(false);
|
||||
var resumed = new ManualResetEventSlim(false);
|
||||
int osThreadId = 0;
|
||||
|
||||
var worker = new System.Threading.Thread(() =>
|
||||
{
|
||||
osThreadId = (int)NativeMethods.GetCurrentThreadId();
|
||||
started.Set();
|
||||
while (!cts.IsCancellationRequested)
|
||||
{
|
||||
resumed.Set();
|
||||
System.Threading.Thread.Sleep(10);
|
||||
}
|
||||
});
|
||||
|
||||
worker.Start();
|
||||
started.Wait();
|
||||
|
||||
try
|
||||
{
|
||||
using var thread = new RemoteThread(magic.Memory, osThreadId);
|
||||
thread.Suspend();
|
||||
resumed.Reset();
|
||||
|
||||
uint prior = thread.Resume();
|
||||
Assert.True(prior < 0xFFFFFFFF);
|
||||
|
||||
// Worker must reach the resumed flag again.
|
||||
Assert.True(resumed.Wait(1000));
|
||||
cts.Cancel();
|
||||
Assert.True(worker.Join(1000));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (worker.IsAlive)
|
||||
{
|
||||
cts.Cancel();
|
||||
using var thread = new RemoteThread(magic.Memory, osThreadId);
|
||||
thread.Resume();
|
||||
worker.Join(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTeb_returns_managed_teb_for_thread()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
int currentId = (int)NativeMethods.GetCurrentThreadId();
|
||||
|
||||
using var thread = new RemoteThread(magic.Memory, currentId);
|
||||
using var teb = thread.GetTeb();
|
||||
|
||||
Assert.NotEqual(IntPtr.Zero, teb.ReadTebAddress());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using SysThread = System.Threading.Thread;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Native;
|
||||
using WhiteMagic.Thread;
|
||||
using Xunit;
|
||||
|
||||
namespace WhiteMagicTest.Thread;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ThreadFactory"/> enumeration and main-thread selection.
|
||||
/// </summary>
|
||||
public sealed class ThreadFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Enumerate_returns_only_target_threads()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
var factory = new ThreadFactory(magic.Memory);
|
||||
|
||||
int currentOsId = (int)NativeMethods.GetCurrentThreadId();
|
||||
var ids = factory.Enumerate().Select(t => t.Id).ToList();
|
||||
|
||||
Assert.True(ids.Count > 0);
|
||||
Assert.Contains(currentOsId, ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetThreadById_returns_matching_thread()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
var factory = new ThreadFactory(magic.Memory);
|
||||
|
||||
int currentOsId = (int)NativeMethods.GetCurrentThreadId();
|
||||
using RemoteThread thread = factory.GetThreadById(currentOsId);
|
||||
Assert.Equal(currentOsId, thread.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetThreadById_throws_for_nonexistent_thread()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
var factory = new ThreadFactory(magic.Memory);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => factory.GetThreadById(0x7FFFFFFF));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MainThread_returns_a_thread_belonging_to_the_target()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
var factory = new ThreadFactory(magic.Memory);
|
||||
|
||||
using RemoteThread main = factory.MainThread;
|
||||
Assert.NotNull(main);
|
||||
|
||||
var ids = factory.Enumerate().Select(t => t.Id).ToList();
|
||||
Assert.Contains(main.Id, ids);
|
||||
}
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
# 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
|
||||
@@ -1,333 +0,0 @@
|
||||
# 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
@@ -1,387 +0,0 @@
|
||||
# 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
|
||||
@@ -1,481 +0,0 @@
|
||||
# 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
|
||||
@@ -102,5 +102,5 @@ The design held, but building it surfaced corrections worth recording (each is d
|
||||
- **`RemoteModule`/`RemoteFunction` follow PE export forwarders** — `kernel32!HeapAlloc` → `NTDLL.RtlAllocateHeap` and similar resolve into the real target module; ordinal and API-set forwarders throw `NotSupportedException` rather than returning a wrong address (task 7.2).
|
||||
- **Detour prologue safety is tiered** — the default `StubAssembler` length-decoder covers only the common x86/x64 prologue shapes and refuses any opcode outside that set (zero dependency); the optional `IcedAssembler.GetPrologueLength` decodes arbitrary prologues and is plugged in via `DetourManager.PrologueLengthResolver` when full validation is wanted (tasks 4.6, 8.3).
|
||||
- **Iced has no text parser** — the design assumed arbitrary text assembly could be delegated to Iced, but Iced ships only a *fluent* code assembler and a decoder. `IcedAssembler.Assemble` bridges Intel-syntax text onto the fluent API by reflection (registers, immediates, labels; memory operands unsupported), rather than depending on a parser that does not exist (task 8.2).
|
||||
- **Injection bitness corrections** — the thread-hijack injector enforces matching host/target bitness, so the 32-bit path always runs from a 32-bit caller and uses native `GetThreadContext`/`SetThreadContext`; the WOW64 context APIs (for 64-bit callers inspecting WOW64 targets) never apply here and were removed. `ExternalReader` validates `QueryInformation`/`QueryLimitedInformation` access and surfaces `IsWow64Process` failures instead of silently assuming host bitness.
|
||||
- **Thread-control, memory-region, and process-discovery gaps are closed** — this change adds `MemoryBase.QueryRegion`/`EnumerateRegions`/`ChangeProtection`, `RemoteThread`/`ThreadFactory`/`FrozenThread`, and `ApplicationFinder` with `Magic.Open` overloads. WhiteMagic now covers the public surfaces of all four reference libraries.
|
||||
- **Bounds and protection hardening** — `AllocatedMemory` range-checks typed IO against region size; `Patch` mirrors the detour's `VirtualProtectEx` dance; `MainThreadPump` guards the completion race on an already-completed `TaskCompletionSource`.
|
||||
|
||||
@@ -1,579 +0,0 @@
|
||||
# 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,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-22
|
||||
@@ -0,0 +1,86 @@
|
||||
## Context
|
||||
|
||||
`whitemagic-foundation` shipped the core (dual `MemoryBase`, execution tiers, hooking, injection, discovery, high-level surface). The post-implementation review confirmed parity with GreyMagic and current BlackMagic but flagged three MemorySharp capabilities still absent: public thread control, memory-region query, and process discovery. This change closes those gaps. It is additive; nothing in `whitemagic-foundation` is reworked.
|
||||
|
||||
The consuming use case is unchanged — an external automation host over a legacy x86 desktop app — so every surface here must work **out-of-process** over a `SafeMemoryHandle`, and honor target bitness where the OS structures differ (thread `CONTEXT`).
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Public thread surface: enumerate the target's threads, suspend/resume, read/write `CONTEXT`, read a thread's TEB, and a **scoped freeze** (`IDisposable`) that suspends a thread set and resumes on dispose even if the body throws.
|
||||
- Memory-region surface: query the region containing an address, enumerate all mapped regions, and a **scoped protection change** (`IDisposable`) that restores original protection on dispose.
|
||||
- Process discovery: attach a target by process name, window title, or window handle; enumerate candidate processes.
|
||||
- Reuse existing `NativeMethods` (`OpenThread`, `Suspend`/`ResumeThread`, `Get`/`SetThreadContext`, `VirtualProtectEx`, `SafeMemoryHandle`) rather than duplicating them.
|
||||
- Test-first for pure logic (region-contains math, freeze/dispose ordering, name/handle matching) with live-process integration tests gated on an available target (self-process).
|
||||
|
||||
**Non-Goals:**
|
||||
- Managed-loader / in-process pump reachability (separate follow-up).
|
||||
- Thread creation — `RemoteThreadExecutor` already owns `CreateRemoteThread`; `RemoteThread` here wraps *existing* target threads.
|
||||
- Writing to arbitrary regions found by enumeration beyond what `MemoryBase` read/write already offers.
|
||||
- Kernel-level or hidden-thread discovery — toolhelp/`NtQueryInformationThread` visibility is sufficient for the automation use case.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: `RemoteThread` wraps an existing target thread over a `SafeMemoryHandle`
|
||||
|
||||
`RemoteThread` opens a thread by TID via `OpenThread(THREAD_ALL_ACCESS...)` into a `SafeMemoryHandle` and exposes `Suspend()`/`Resume()`, `GetContext()`/`SetContext()` (Wow64 variant selected by target bitness, mirroring `DllInjector`'s hijack path), `GetTeb()` (via `NtQueryInformationThread`/`ThreadBasicInformation` → `ManagedTeb`), and `Id`. `Suspend`/`Resume` return the prior suspend count so nested suspends are observable.
|
||||
|
||||
**Why**: Mirrors the proven bitness handling already in `DllInjector`; keeps thread handles inside a `SafeMemoryHandle` for deterministic cleanup like every other native handle in the library.
|
||||
|
||||
**Alternatives**: expose raw `System.Diagnostics.ProcessThread` — rejected: no suspend/resume/context and no soft handle ownership.
|
||||
|
||||
### D2: `ThreadFactory` enumerates via toolhelp snapshot
|
||||
|
||||
`ThreadFactory.Enumerate()` walks `CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD)` + `Thread32First`/`Thread32Next`, filtering by owning PID, yielding `RemoteThread`. `MainThread` returns the thread with the earliest creation time (via `GetThreadTimes`), matching MemorySharp's definition. `GetThreadById(id)` opens directly.
|
||||
|
||||
**Why**: toolhelp is the documented, x86/x64-uniform thread walk and needs no undocumented structures.
|
||||
|
||||
**Alternatives**: `NtQuerySystemInformation(SystemProcessInformation)` — rejected: larger undocumented surface for no gain here.
|
||||
|
||||
### D3: Scoped freeze is the default ergonomic
|
||||
|
||||
`ThreadFactory.Freeze(predicate = all-but-caller?)` suspends the selected threads and returns a `FrozenThread : IDisposable` whose `Dispose()` resumes exactly the threads it suspended, in reverse order. Individual `RemoteThread.Suspend/Resume` remain available for manual control.
|
||||
|
||||
**Why**: The dominant use ("freeze the target while I read/write a consistent snapshot") is a scope. An `IDisposable` makes leak-on-exception impossible: `using (factory.Freeze()) { ...edit... }`.
|
||||
|
||||
**Trade-off**: Freezing the target's own threads while calling *into* the target (pump/remote-thread) can deadlock. Documented: freeze is for passive read/write snapshots, not while executing target code.
|
||||
|
||||
### D4: `MemoryRegion` is an immutable `VirtualQueryEx` snapshot; enumeration is lazy
|
||||
|
||||
`MemoryRegion` holds `BaseAddress`, `Size`, `Protection`, `State`, `Type`, `AllocationBase`, `AllocationProtect`, and `Contains(address)`. `MemoryBase.QueryRegion(address)` returns the single region containing an address; `MemoryBase.EnumerateRegions()` yields regions from address 0 upward by repeatedly calling `VirtualQueryEx(base + size)` until it fails (end of address space). Enumeration is `IEnumerable<MemoryRegion>` (lazy) so a caller can stop early.
|
||||
|
||||
**Why**: `VirtualQueryEx` already returns contiguous non-overlapping regions; walking `base+size` is the canonical enumeration. Lazy avoids materializing the whole address space.
|
||||
|
||||
### D5: Protection change is a scoped, auto-restoring helper
|
||||
|
||||
`MemoryBase.ChangeProtection(address, size, newProtect)` calls `VirtualProtectEx`, captures the old protection, and returns a `ProtectionScope : IDisposable` that restores it on dispose. This is the same protect/restore pattern already inlined in `Detour.Apply`; extracting it lets callers guard their own writes: `using (mem.ChangeProtection(a, n, ExecuteReadWrite)) { mem.WriteBytes(a, patch); }`.
|
||||
|
||||
**Why**: Removes a foot-gun (leaving a page writable) and de-duplicates the pattern. `Detour`/`Patch` may later adopt it, but that refactor is out of scope here.
|
||||
|
||||
### D6: Process discovery via `System.Diagnostics.Process` + Win32 window queries
|
||||
|
||||
`ApplicationFinder` wraps `Process.GetProcessesByName`, a `GetWindow`/`EnumWindows` + `GetWindowThreadProcessId` path for window-title/handle attach, and exposes them as `Magic.Open(string processName)`, `Magic.OpenByWindowTitle(string)`, `Magic.OpenByWindowHandle(IntPtr)` overloads plus `ApplicationFinder.Enumerate()`. Ambiguous matches (multiple processes) throw with the candidate list rather than guessing.
|
||||
|
||||
**Why**: Managed `Process` covers name/PID; the existing `WindowFactory`/`RemoteWindow` P/Invoke already resolves windows, so window→PID reuses it. Throwing on ambiguity avoids attaching to the wrong instance.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Freeze-while-executing deadlock** → Documented non-use; `Freeze` default predicate can exclude the caller's own thread, but cross-process it cannot exclude the *target's* pump thread — caller must not freeze while the pump runs. (D3)
|
||||
- **Suspend count skew** → `Suspend`/`Resume` return prior counts; `FrozenThread` tracks exactly what it suspended and resumes only those, so external suspends are not clobbered. (D3)
|
||||
- **`VirtualQueryEx` over a 64-bit address space is large** → enumeration is lazy and `IEnumerable`; callers filtering by `State == Commit` or a range stop early. (D4)
|
||||
- **Ambiguous process match** → throw with candidates, never auto-pick. (D6)
|
||||
- **Bitness of thread `CONTEXT`** → reuse the exact Wow64/native selection already validated in `DllInjector`. (D1)
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Additive; nothing to migrate. Suggested slices:
|
||||
1. **Memory-region** — `MemoryRegion`, `QueryRegion`, `EnumerateRegions`, `ChangeProtection`/`ProtectionScope`. Smallest, unlocks safe writes immediately.
|
||||
2. **Thread-control** — `RemoteThread`, `ThreadFactory`, `FrozenThread`.
|
||||
3. **Process discovery** — `ApplicationFinder`, `Magic.Open*` overloads.
|
||||
|
||||
**Rollback**: remove the new files and the additive `Magic` members; no existing type is modified.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **`Freeze` default predicate**: all target threads, or all-but-main? Leaning all-but-none (freeze everything the caller selects; no implicit exclusion cross-process). To confirm during slice 2.
|
||||
- **TEB read for a thread**: `NtQueryInformationThread(ThreadBasicInformation)` (undocumented-ish but stable) vs. deriving from `GetThreadContext`. Leaning the former to match `ManagedTeb`'s existing shape.
|
||||
@@ -0,0 +1,34 @@
|
||||
## Why
|
||||
|
||||
The `whitemagic-foundation` review found WhiteMagic is a superset of GreyMagic and current BlackMagic, but **not yet of MemorySharp**. Three genuinely useful capabilities MemorySharp (and, for threads, current BlackMagic's `SThread`) shipped are missing from WhiteMagic:
|
||||
|
||||
1. **Thread control** — WhiteMagic calls `SuspendThread`/`ResumeThread` only *internally* inside `DllInjector` thread-hijack. There is no public surface to enumerate a target's threads, suspend/resume them, or **freeze** them for the duration of an edit. Freezing threads is table-stakes for memory editing/trainers (MemorySharp: `ThreadFactory`/`RemoteThread`/`FrozenThread`; BlackMagic: `SThread`).
|
||||
2. **Memory-region query** — WhiteMagic changes page protection inline inside `Detour` but exposes no `VirtualQueryEx` region walk, no query-region-at-address, and no reusable scoped protection helper (MemorySharp: `RemoteRegion`/`MemoryProtection`). Callers cannot inspect what is mapped, its protection, or safely flip protection around a write.
|
||||
3. **Process discovery** — no way to open a target by name/window/title; the caller must obtain a PID out of band (MemorySharp: `ApplicationFinder`).
|
||||
|
||||
These are all **additive, low-risk** surfaces that sit on the existing `MemoryBase`/`SafeMemoryHandle` and native P/Invoke layer. None requires the deferred managed-loader work.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Thread control** (new capability `thread-control`): `RemoteThread` (open by id, suspend/resume, get/set context, get TEB, join), `ThreadFactory` (enumerate the target's threads, get main thread, get-by-id), and `FrozenThread`/`Freeze()` returning an `IDisposable` scope that suspends a set of threads and resumes them on dispose.
|
||||
- **Memory-region query** (new capability `memory-region`): `MemoryRegion` (a queried `VirtualQueryEx` result — base, size, protection, state, type), region enumeration across the target's address space, query-region-containing-an-address, and a `ChangeProtection(...)` helper returning an `IDisposable` scope that restores the original protection on dispose.
|
||||
- **Process discovery** (added to existing capability `high-level-api`): an `ApplicationFinder`/`Magic.Open` overloads to attach by process name, window title, or window handle, plus enumeration of candidate processes.
|
||||
|
||||
No behavior of existing WhiteMagic types changes; these are new types plus additive `Magic` facade members and new native imports.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `thread-control`: Enumerate, suspend/resume, freeze (scoped), and read/write the context of a target process's threads.
|
||||
- `memory-region`: Query and enumerate mapped memory regions (`VirtualQueryEx`) and change page protection through a scoped, auto-restoring helper.
|
||||
|
||||
### Modified Capabilities
|
||||
- `high-level-api`: Adds process discovery — attach a target by name/window/handle and enumerate candidates.
|
||||
|
||||
## Impact
|
||||
|
||||
- **New code**: `WhiteMagic/Thread/RemoteThread.cs`, `ThreadFactory.cs`, `FrozenThread.cs`; `WhiteMagic/Memory/MemoryRegion.cs`, `MemoryRegionEnumerator` (or methods on `MemoryBase`), `ProtectionScope`; `WhiteMagic/Process/ApplicationFinder.cs`; additive `Magic` facade members.
|
||||
- **New native imports**: `Thread32First`/`Thread32Next` + `CreateToolhelp32Snapshot` (or `NtQueryInformationProcess` thread walk), `VirtualQueryEx`, `MEMORY_BASIC_INFORMATION`. `OpenThread`/`Suspend`/`Resume`/`Get`/`SetThreadContext` already exist in `NativeMethods`.
|
||||
- **No dependency change**: pure P/Invoke over the existing core. No FASM, no Iced, no managed loader.
|
||||
- **No changes** to BlackMagic/MemorySharp/GreyMagic or their tests.
|
||||
- **Platform**: unchanged — bitness-agnostic (x86 + x64); thread context read honors the target's bitness like the existing hijack path.
|
||||
@@ -0,0 +1,25 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Process discovery
|
||||
|
||||
WhiteMagic SHALL attach to a target process discovered by process name, window title, or window handle, and SHALL enumerate candidate processes. An ambiguous match MUST fail deterministically rather than attaching to an arbitrary candidate.
|
||||
|
||||
#### Scenario: open by process name
|
||||
- **WHEN** a target is opened by a unique process name
|
||||
- **THEN** it MUST attach to that process
|
||||
|
||||
#### Scenario: open by window title
|
||||
- **WHEN** a target is opened by a window title
|
||||
- **THEN** it MUST attach to the process owning the window with that title
|
||||
|
||||
#### Scenario: open by window handle
|
||||
- **WHEN** a target is opened by a window handle
|
||||
- **THEN** it MUST attach to the process that owns that window
|
||||
|
||||
#### Scenario: ambiguous match is rejected
|
||||
- **WHEN** more than one process matches the given name or title
|
||||
- **THEN** the open MUST fail and surface the set of candidate processes rather than picking one
|
||||
|
||||
#### Scenario: enumerate candidates
|
||||
- **WHEN** candidate processes are enumerated
|
||||
- **THEN** the result MUST list the processes eligible to be opened
|
||||
@@ -0,0 +1,41 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Query the region containing an address
|
||||
|
||||
WhiteMagic SHALL return the mapped memory region that contains a given address, including its base, size, protection, state, and type.
|
||||
|
||||
#### Scenario: query a committed address
|
||||
- **WHEN** the region containing a known committed address is queried
|
||||
- **THEN** it MUST return a region whose base and size bracket that address and whose protection reflects the page's actual protection
|
||||
|
||||
#### Scenario: region membership test
|
||||
- **WHEN** a region is asked whether it contains an address
|
||||
- **THEN** it MUST return true only for addresses within `[base, base + size)`
|
||||
|
||||
### Requirement: Enumerate mapped regions
|
||||
|
||||
WhiteMagic SHALL enumerate the mapped memory regions of the target from the lowest address upward, lazily.
|
||||
|
||||
#### Scenario: enumeration walks the address space
|
||||
- **WHEN** the target's regions are enumerated
|
||||
- **THEN** the sequence MUST yield contiguous, non-overlapping regions ascending by base address until the end of the queryable address space
|
||||
|
||||
#### Scenario: early stop
|
||||
- **WHEN** a caller stops consuming the enumeration after the first match
|
||||
- **THEN** enumeration MUST NOT query the entire address space
|
||||
|
||||
### Requirement: Scoped protection change
|
||||
|
||||
WhiteMagic SHALL change the protection of a region and restore the original protection when the returned scope is disposed.
|
||||
|
||||
#### Scenario: protection is applied within the scope
|
||||
- **WHEN** a protection-change scope is created for a region with a new protection
|
||||
- **THEN** the region's protection MUST be the requested value for the duration of the scope
|
||||
|
||||
#### Scenario: protection is restored on dispose
|
||||
- **WHEN** the protection-change scope is disposed
|
||||
- **THEN** the region's protection MUST be restored to the value it had before the scope was created
|
||||
|
||||
#### Scenario: restore on exception
|
||||
- **WHEN** the guarded body throws before the scope is disposed
|
||||
- **THEN** the original protection MUST still be restored as the scope unwinds
|
||||
@@ -0,0 +1,57 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Enumerate target threads
|
||||
|
||||
WhiteMagic SHALL enumerate the threads belonging to the target process and expose each as a controllable thread handle.
|
||||
|
||||
#### Scenario: enumerate returns the target's threads
|
||||
- **WHEN** the threads of an open target are enumerated
|
||||
- **THEN** the result MUST contain a handle for each thread owned by the target process and none owned by other processes
|
||||
|
||||
#### Scenario: resolve the main thread
|
||||
- **WHEN** the main thread is requested
|
||||
- **THEN** it MUST return the earliest-created thread of the target process
|
||||
|
||||
#### Scenario: get a thread by id
|
||||
- **WHEN** a thread is requested by its thread id
|
||||
- **THEN** it MUST return a handle bound to that thread, or fail deterministically if the id is not a thread of the target
|
||||
|
||||
### Requirement: Suspend and resume a thread
|
||||
|
||||
WhiteMagic SHALL suspend and resume an individual target thread and report the prior suspend count.
|
||||
|
||||
#### Scenario: suspend increments the suspend count
|
||||
- **WHEN** a running thread is suspended
|
||||
- **THEN** the thread MUST stop executing and the returned prior suspend count MUST reflect its state before the call
|
||||
|
||||
#### Scenario: resume restores execution
|
||||
- **WHEN** a previously suspended thread is resumed to a zero suspend count
|
||||
- **THEN** the thread MUST resume executing
|
||||
|
||||
### Requirement: Read and write thread context
|
||||
|
||||
WhiteMagic SHALL read and write a target thread's register context, selecting the context layout that matches the target's bitness.
|
||||
|
||||
#### Scenario: round-trip a register value
|
||||
- **WHEN** a thread's context is read, a register is modified, and the context is written back
|
||||
- **THEN** a subsequent read MUST reflect the modified register value
|
||||
|
||||
#### Scenario: bitness-correct context
|
||||
- **WHEN** the target is a 32-bit (WOW64) process
|
||||
- **THEN** the WOW64 context layout MUST be used, and for a 64-bit target the native layout MUST be used
|
||||
|
||||
### Requirement: Scoped thread freeze
|
||||
|
||||
WhiteMagic SHALL provide a scoped freeze that suspends a selected set of target threads and resumes exactly those threads when the scope is disposed, including when the guarded body throws.
|
||||
|
||||
#### Scenario: freeze suspends selected threads
|
||||
- **WHEN** a freeze scope is created over a set of threads
|
||||
- **THEN** each of those threads MUST be suspended for the duration of the scope
|
||||
|
||||
#### Scenario: dispose resumes only the frozen threads
|
||||
- **WHEN** the freeze scope is disposed
|
||||
- **THEN** exactly the threads it suspended MUST be resumed, and threads suspended by other callers MUST be left unchanged
|
||||
|
||||
#### Scenario: exception in the body still resumes
|
||||
- **WHEN** the guarded body throws before the scope is disposed
|
||||
- **THEN** the frozen threads MUST still be resumed as the scope unwinds
|
||||
@@ -0,0 +1,39 @@
|
||||
## 1. Memory-region query (spec: memory-region)
|
||||
|
||||
- [x] 1.1 Add `VirtualQueryEx` `LibraryImport` and `MEMORY_BASIC_INFORMATION` to `Native/` (32/64-bit-correct layout)
|
||||
- [x] 1.2 Add tests for `MemoryRegion.Contains` (in-range true, boundary `[base, base+size)`, out-of-range false)
|
||||
- [x] 1.3 Implement `WhiteMagic/Memory/MemoryRegion.cs` (immutable: BaseAddress, Size, Protection, State, Type, AllocationBase, AllocationProtect, Contains) to pass 1.2
|
||||
- [x] 1.4 Add tests for `MemoryBase.QueryRegion(address)` against a known committed address in the current process
|
||||
- [x] 1.5 Implement `QueryRegion` to pass 1.4
|
||||
- [x] 1.6 Add tests for `EnumerateRegions()`: ascending non-overlapping bases, lazy (early stop does not walk whole space — assert via a bounded take)
|
||||
- [x] 1.7 Implement lazy `EnumerateRegions()` (walk `base+size` until `VirtualQueryEx` fails) to pass 1.6
|
||||
- [x] 1.8 Add tests for `ChangeProtection`/`ProtectionScope`: protection applied in scope, restored on dispose, restored on exception
|
||||
- [x] 1.9 Implement `MemoryBase.ChangeProtection` returning `ProtectionScope : IDisposable` to pass 1.8
|
||||
|
||||
## 2. Thread control (spec: thread-control)
|
||||
|
||||
- [x] 2.1 Add `CreateToolhelp32Snapshot`/`Thread32First`/`Thread32Next` + `THREADENTRY32`, and `GetThreadTimes`, to `Native/` (reuse existing `OpenThread`/`Suspend`/`Resume`/`Get`/`SetThreadContext`)
|
||||
- [x] 2.2 Add tests for `RemoteThread`: open by id, `Suspend` returns prior count and stops the thread, `Resume` restarts it (self-process worker thread)
|
||||
- [x] 2.3 Implement `WhiteMagic/Thread/RemoteThread.cs` (OpenThread → `SafeMemoryHandle`, Suspend/Resume, Id) to pass 2.2
|
||||
- [x] 2.4 Add tests for `GetContext`/`SetContext` round-trip on a suspended self-thread; assert WOW64 vs native selection by target bitness
|
||||
- [x] 2.5 Implement context read/write reusing `DllInjector`'s bitness selection to pass 2.4
|
||||
- [x] 2.6 Add tests + implement `RemoteThread.GetTeb()` (via `NtQueryInformationThread`/`ThreadBasicInformation` → `ManagedTeb`)
|
||||
- [x] 2.7 Add tests for `ThreadFactory`: `Enumerate()` returns only target threads, `MainThread` = earliest-created, `GetThreadById`
|
||||
- [x] 2.8 Implement `WhiteMagic/Thread/ThreadFactory.cs` (toolhelp walk filtered by PID; `GetThreadTimes` for main) to pass 2.7
|
||||
- [x] 2.9 Add tests for `FrozenThread`/`Freeze()`: suspends selected set, dispose resumes exactly those, body-throws still resumes, external suspends untouched
|
||||
- [x] 2.10 Implement `WhiteMagic/Thread/FrozenThread.cs` + `ThreadFactory.Freeze(...)` (reverse-order resume on dispose) to pass 2.9
|
||||
|
||||
## 3. Process discovery (spec: high-level-api)
|
||||
|
||||
- [x] 3.1 Add tests for `ApplicationFinder.Enumerate()` and open-by-name against the current process
|
||||
- [x] 3.2 Implement `WhiteMagic/Process/ApplicationFinder.cs` (`Process.GetProcessesByName`; window-title/handle via existing `WindowFactory` + `GetWindowThreadProcessId`)
|
||||
- [x] 3.3 Add tests for ambiguous-match rejection (multiple candidates → throws with candidate list) and open-by-window-handle
|
||||
- [x] 3.4 Add `Magic.Open(string processName)`, `Magic.OpenByWindowTitle(string)`, `Magic.OpenByWindowHandle(IntPtr)` overloads delegating to `ApplicationFinder`; add tests
|
||||
- [x] 3.5 Wire new surface into the `Magic` facade (expose `Threads` factory and `Regions`/`QueryRegion` accessors) and document freeze-while-executing deadlock caveat in XML docs
|
||||
|
||||
## 4. Verification
|
||||
|
||||
- [x] 4.1 Run full test suite: `dotnet test WhiteMagicTest/WhiteMagicTest.csproj` — all pass
|
||||
- [x] 4.2 Run full build (`dotnet build WhiteMagic.slnx`) — zero errors, zero new warnings in `WhiteMagic`
|
||||
- [x] 4.3 Update `docs/memory-library-comparison.md` — mark thread-control, memory-region, and process-discovery gaps closed; note WhiteMagic is now a superset of MemorySharp's public surface (or list any remaining minor helpers deliberately skipped)
|
||||
- [x] 4.4 `openspec validate add-thread-region-finder --strict` passes
|
||||
@@ -1,45 +0,0 @@
|
||||
# non-blocking-execute Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change inject-and-assemble. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: InjectAndExecuteEx creates remote thread without waiting
|
||||
|
||||
`BlackMagic.InjectAndExecuteEx(IntPtr startAddress, IntPtr parameter)` injects code at `startAddress` into the opened process, creates a remote thread with `parameter`, and returns the thread handle immediately without waiting for the thread to exit.
|
||||
|
||||
#### Scenario: successful non-blocking execution
|
||||
- **WHEN** a process is open and `InjectAndExecuteEx(addr, param)` is called with a valid code address
|
||||
- **THEN** a remote thread is created in the target process and a valid `SafeMemoryHandle` is returned
|
||||
|
||||
#### Scenario: no process open
|
||||
- **WHEN** no process is open and `InjectAndExecuteEx(addr, param)` is called
|
||||
- **THEN** `null` is returned
|
||||
|
||||
### Requirement: InjectAndExecuteEx single-parameter overload
|
||||
|
||||
`BlackMagic.InjectAndExecuteEx(IntPtr startAddress)` calls `InjectAndExecuteEx(startAddress, IntPtr.Zero)`.
|
||||
|
||||
#### Scenario: parameter-less non-blocking execution
|
||||
- **WHEN** `InjectAndExecuteEx(addr)` is called with a valid address
|
||||
- **THEN** the thread is created with parameter `IntPtr.Zero`
|
||||
|
||||
### Requirement: InjectAndExecuteEx from assembly text
|
||||
|
||||
`BlackMagic.InjectAndExecuteEx(string asm)` assembles the text via `AsmBuilder`, allocates remote memory, writes the bytes, calls `InjectAndExecuteEx` on the allocated address, and returns the thread handle.
|
||||
|
||||
#### Scenario: execute assembly text non-blocking
|
||||
- **WHEN** `InjectAndExecuteEx("nop")` is called with a process open
|
||||
- **THEN** the text is assembled to bytes, written to remote memory, a thread is started, and the handle is returned
|
||||
|
||||
#### Scenario: assembly failure
|
||||
- **WHEN** `InjectAndExecuteEx("invalidinstruction")` is called
|
||||
- **THEN** `ArgumentException` is thrown with the assembly error
|
||||
|
||||
### Requirement: InjectAndExecute from assembly text (blocking convenience)
|
||||
|
||||
`BlackMagic.InjectAndExecute(string asm)` assembles the text, allocates remote memory, writes the bytes, calls `Execute` (blocking, 10s timeout), and returns the exit code.
|
||||
|
||||
#### Scenario: execute assembly text blocking
|
||||
- **WHEN** `InjectAndExecute("mov eax, 42\nret")` is called with a process open
|
||||
- **THEN** the text is assembled, injected, executed, and the thread exit code is returned
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# text-assembler Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change inject-and-assemble. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: AsmBuilder assembles x86 instruction text to byte array
|
||||
|
||||
`AsmBuilder.Assemble(string source)` parses x86 assembly text and returns the corresponding `byte[]` machine code.
|
||||
|
||||
#### Scenario: single instruction
|
||||
- **WHEN** `AsmBuilder.Assemble("nop")` is called
|
||||
- **THEN** the result is `[0x90]`
|
||||
|
||||
#### Scenario: multiple instructions
|
||||
- **WHEN** `AsmBuilder.Assemble("pushad\npopad")` is called
|
||||
- **THEN** the result is `[0x60, 0x61]`
|
||||
|
||||
#### Scenario: instruction with immediate operand
|
||||
- **WHEN** `AsmBuilder.Assemble("mov eax, 1")` is called
|
||||
- **THEN** the result is `[0xB8, 0x01, 0x00, 0x00, 0x00]`
|
||||
|
||||
### Requirement: AsmBuilder supports register operands
|
||||
|
||||
Supported registers: `eax`, `ecx`, `edx`, `ebx`, `esp`, `ebp`, `esi`, `edi` (and 8-bit: `al`, `cl`, `dl`, `bl`, `ah`, `ch`, `dh`, `bh`).
|
||||
|
||||
#### Scenario: register-to-register move
|
||||
- **WHEN** `AsmBuilder.Assemble("mov eax, ecx")` is called
|
||||
- **THEN** the result is `[0x89, 0xC8]` (mov eax, ecx encoding)
|
||||
|
||||
#### Scenario: register encoding
|
||||
- **WHEN** registers are used in instructions
|
||||
- **THEN** each register maps to its correct 3-bit encoding (eax=0, ecx=1, edx=2, ebx=3, esp=4, ebp=5, esi=6, edi=7)
|
||||
|
||||
### Requirement: AsmBuilder supports labels and jumps
|
||||
|
||||
Labels are defined with `@name:` and referenced with `jmp @name` or `je @name`. Forward and backward references are resolved in a second pass.
|
||||
|
||||
#### Scenario: forward jump
|
||||
- **WHEN** `AsmBuilder.Assemble("jmp @skip\nnop\n@skip:\nret")` is called
|
||||
- **THEN** the jump skips exactly over the `nop` (2 bytes) and lands on `ret`
|
||||
|
||||
#### Scenario: backward jump
|
||||
- **WHEN** `AsmBuilder.Assemble("@loop:\nnop\njmp @loop")` is called
|
||||
- **THEN** the jump targets the earlier label correctly
|
||||
|
||||
#### Scenario: multiple labels
|
||||
- **WHEN** multiple labels are used in one source
|
||||
- **THEN** each label resolves to its correct byte offset
|
||||
|
||||
### Requirement: AsmBuilder SetPassLimit controls iteration
|
||||
|
||||
`AsmBuilder.SetPassLimit(int limit)` sets the maximum number of assembly passes for label resolution. Default is 10. If the limit is exceeded before all labels resolve, `InvalidOperationException` is thrown.
|
||||
|
||||
#### Scenario: default pass limit
|
||||
- **WHEN** no `SetPassLimit` is called
|
||||
- **THEN** the assembler uses 10 passes maximum
|
||||
|
||||
#### Scenario: custom pass limit
|
||||
- **WHEN** `SetPassLimit(20)` is called
|
||||
- **THEN** the assembler uses 20 passes maximum
|
||||
|
||||
#### Scenario: pass limit exceeded
|
||||
- **WHEN** forward references cannot resolve within the pass limit
|
||||
- **THEN** `InvalidOperationException` is thrown with label resolution details
|
||||
|
||||
### Requirement: AsmBuilder reports clear errors
|
||||
|
||||
Unknown instructions, missing operands, and invalid register names produce `ArgumentException` with the line number and offending text.
|
||||
|
||||
#### Scenario: unknown instruction
|
||||
- **WHEN** `AsmBuilder.Assemble("xyzw")` is called
|
||||
- **THEN** `ArgumentException` is thrown mentioning line 1 and "xyzw"
|
||||
|
||||
#### Scenario: missing operand
|
||||
- **WHEN** `AsmBuilder.Assemble("mov")` is called (no operands)
|
||||
- **THEN** `ArgumentException` is thrown mentioning missing operand
|
||||
|
||||
### Requirement: AsmBuilder supported instruction set
|
||||
|
||||
The following x86 instructions are supported:
|
||||
- **Data movement**: `mov`, `push`, `pop`, `pushad`, `popad`, `lea`
|
||||
- **Arithmetic**: `add`, `sub`, `inc`, `dec`, `xor`, `and`, `or`, `cmp`, `test`
|
||||
- **Control flow**: `jmp`, `je`, `jne`, `call`, `ret`, `nop`, `hlt`
|
||||
|
||||
#### Scenario: all instructions produce valid bytes
|
||||
- **WHEN** each supported instruction is assembled individually
|
||||
- **THEN** it produces the correct x86 machine code encoding
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# 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