- Example5: Fix format string bugs (alignment specifier placement, SafeMemoryHandle formatting) - Example4: Fix DetourManager.Detour() → Create() in all doc strings - Example3: Fix MemoryBase.CreateFunction() → InProcessInvoker.CreateFunction() in doc strings - Examples 1-5: Correct all runtime errors and API mismatches vs actual WhiteMagic API - README: Fix DetourManager.Detour() examples to use Create(); add missing code fence markers - Add WhiteMagic.Examples project with 5 comprehensive example files (40+ sub-examples) - Add docfx.json and toc.md for DocFX API reference generation - Add 5 conceptual guides: architecture, memory-access, execution-models, hooking, troubleshooting - Ensure zero errors, zero warnings across all projects (net8.0-windows) Doc strings now teach correct APIs; runtime format bugs eliminated; build succeeds.
334 lines
9.4 KiB
Markdown
334 lines
9.4 KiB
Markdown
# 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
|