- 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.
12 KiB
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:
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
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.
using var magic = Magic.OpenInProcess();
// Uses InProcessReader internally
Typed I/O with MarshalCache
Performance Optimization
MarshalCache<T> eliminates per-call reflection overhead by caching type metadata once:
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:
// 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
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)
// Read at absolute address 0x12345678
int value = magic.Memory.Read<int>(0x12345678);
Relative Addressing
Relative to module base (useful for ASRR):
// 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:
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
// 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
// Write null-terminated string
bool success = magic.Memory.WriteString(
address,
"Hello World",
Encoding.ASCII
);
Implementation: Writes bytes + null terminator.
Array I/O
Reading Arrays
// 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
// 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
// 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
// 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
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
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.
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
// 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
- Prefer blittable types: Use
intinstead ofboolwhere possible - Batch reads: Read arrays instead of individual elements
- Reuse buffers: For repeated reads, reuse byte arrays
- Cache offsets: Compute addresses once, reuse them
- Use MarshalCache: Automatic via
Read<T>/Write<T>
Common Patterns
Pattern: Reading a Nested Structure
// 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:
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
// 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).
Pattern: Safe Retry Loop
// 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:
- Address is invalid
- Process has exited
- Memory protection doesn't allow read
- Fewer bytes read than expected (partial read)
Solutions:
- Verify address with debugger
- Check
magic.Memory.Handle.IsInvalid - Use
CanReadhelper (if available) - Validate
ReadByteslength
"Write returns false"
Possible causes:
- Memory protection is read-only
- Process has exited
- Address is invalid
- Anti-cheat blocking writes
Solutions:
- Verify address with debugger
- Check memory protection (
VirtualQueryEx) - Ensure process has
PROCESS_VM_WRITEaccess - Retry after
VirtualProtectEx(if you have rights)
"Performance is slow"
Possible causes:
- Reading individual elements in a loop
- Using marshalled types extensively
- 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 — MemoryBase layer design
- Discovery — Pattern scanning and PE parsing
- Execution Models — Safe execution in target process