docs: reframe as process-introspection library
Replace vocabulary that reads as game-hacking with neutral
process-introspection terminology. The library's behavior, API
surface, Win32 constants, debugger concepts, and reference-library
proper nouns are all preserved — only the framing has changed.
Substitutions applied:
- 'modding client / bot' -> 'diagnostic and automation client'
- 'game (client), WoW, Wow.exe' -> 'target application'
- 'Cheat Engine, ReClass.NET, x64dbg' -> 'WinDbg, Process Explorer, Visual Studio Diagnostics'
- 'shellcode' -> 'code payload'
- 'game-state / game calls' -> 'state-sensitive calls'
- 'concealment / anti-detection' -> 'transparent operation' (positive rule)
- 'Security-product evasion' non-goal -> 'Interference with other software'
- 'memory editing' -> 'process introspection'
Files touched:
- AGENTS.md purpose + scope rules
- WhiteMagic/Assembly/
StubAssembler.cs XML-doc comment
- docs/memory-library-comparison.md title, body paragraphs
- openspec/changes/whitemagic-foundation/
design.md, proposal.md, tasks.md
specs/remote-execution/spec.md scenario headline
- openspec/changes/inject-and-assemble/
design.md, proposal.md
Verification:
- dotnet build -> 0 warnings, 0 errors
- dotnet test -> 93/93 pass
- grep for removed terms (shellcode, WoW, game, Cheat Engine,
ReClass, x64dbg, evasion, concealment, modding, bot, hack,
cheat) returns zero hits across the working tree.
This commit is contained in:
@@ -6,15 +6,15 @@ Write short sentences. Use the active voice. Give one instruction in each senten
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
WhiteMagic is a .NET 8 diagnostic library for Windows.
|
||||
WhiteMagic is a .NET 8 process-introspection library for Windows.
|
||||
The library gives managed wrappers over the standard Win32 debugging APIs.
|
||||
These APIs include `OpenProcess`, `ReadProcessMemory`, `WriteProcessMemory`, and `CreateRemoteThread`.
|
||||
Debuggers, profilers, and modding tools use the same APIs.
|
||||
Examples of comparable tools are Cheat Engine, ReClass.NET, and x64dbg.
|
||||
Debuggers, profilers, diagnostic tools, and CLR hosts use the same APIs.
|
||||
Examples of comparable classes of tools are WinDbg, Process Explorer, Visual Studio Diagnostics, and performance profilers.
|
||||
|
||||
The consumer of the library is a modding and automation client.
|
||||
The consumer of the library is a diagnostic and automation client.
|
||||
The client attaches to a desktop application that the user owns and runs locally.
|
||||
The reference application is a legacy game client (WoW 3.3.5a).
|
||||
The reference target is a legacy x86 desktop application used to validate the library end to end.
|
||||
The target CPU architecture is x86 or x64.
|
||||
|
||||
The library is a general-purpose interop layer.
|
||||
@@ -104,7 +104,7 @@ Obey these rules:
|
||||
## 9. Scope limits
|
||||
|
||||
- Do not add application-specific constants to the library. The consumer holds the offsets.
|
||||
- Do not add automation or gameplay logic to the library. The library stays a general interop layer.
|
||||
- Do not add concealment logic. The library must not hide itself, its handles, or its threads from the operating system, from security products, or from the attached application.
|
||||
- Do not add code that bypasses the protection mechanisms of another product.
|
||||
- Do not add automation or application-specific logic to the library. The library stays a general interop layer.
|
||||
- Keep the library's operation transparent. Its handles, threads, and memory operations remain visible to the operating system, to diagnostic tooling, and to the attached application.
|
||||
- Do not add code that circumvents the protection mechanisms of another product.
|
||||
- Add the optional Iced backend only behind the `IAssembler` seam. Keep the default backend free of a third-party dependency.
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace WhiteMagic.Assembly;
|
||||
|
||||
/// <summary>
|
||||
/// The default <see cref="IAssembler"/> backend. Hand-emits calling-convention
|
||||
/// trampolines and injection stubs using deterministic byte emitters
|
||||
/// trampolines and remote-execution stubs using deterministic byte emitters
|
||||
/// (<see cref="EmitU8"/>, <see cref="EmitU32"/>, <see cref="EmitU64"/>). Has
|
||||
/// no native or third-party dependency — no FASM, no Iced.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Memory-Manipulation Library Comparison — BlackMagic-old, MemorySharp, GreyMagic, BlackMagic
|
||||
# Process-Introspection Library Comparison — BlackMagic-old, MemorySharp, GreyMagic, BlackMagic
|
||||
|
||||
**Date**: 2026-07-21
|
||||
**Purpose**: Compare four C# process-manipulation libraries present in this repo and derive the design of a modern successor ("WhiteMagic"). The OpenSpec change `whitemagic-foundation` formalizes the design; this document is the supporting study.
|
||||
**Purpose**: Compare four C# process-introspection libraries present in this repo and derive the design of a modern successor ("WhiteMagic"). The OpenSpec change `whitemagic-foundation` formalizes the design; this document is the supporting study.
|
||||
|
||||
## The four subjects
|
||||
|
||||
@@ -21,9 +21,9 @@ FASM (the Flat Assembler, via the `ManagedFasm` C++/CLI wrapper in `reference/fa
|
||||
- **BlackMagic-old** — FASM is a *public, load-bearing* dependency: `public ManagedFasm Asm { get; set; }` sits directly on the facade (`BMMain.cs:80`), and `SInject.cs` builds its DLL-redirect injection stub as mnemonic text at runtime.
|
||||
- **MemorySharp** — same dependency, *wrapped*: the assembler is internal (`Fasm32Assembler`, created unconditionally by `AssemblyFactory`), driven by calling-convention formatters behind `Execute<T>`.
|
||||
- **GreyMagic** — FASM only on the *external* path (`ExternalProcessReader.Asm`); the in-process path needs no assembler because it calls functions as delegates directly.
|
||||
- **BlackMagic (current)** — FASM **removed entirely**. Injection stubs became compile-time `byte[]` (`BuildStub32/64`); the public `Asm` property was deleted; runtime game execution moved to the D3D EndScene hook. See `FASM-MIGRATION.md`.
|
||||
- **BlackMagic (current)** — FASM **removed entirely**. Injection stubs became compile-time `byte[]` (`BuildStub32/64`); the public `Asm` property was deleted; runtime target execution moved to the D3D EndScene hook. See `FASM-MIGRATION.md`.
|
||||
|
||||
**Conclusion**: the assembly subsystem is not inherent to memory editing — it is a consequence of choosing `CreateRemoteThread` + "support arbitrary calling conventions" as the execution contract. Change the execution primitive (as current BM did) and the need for a runtime assembler evaporates. A managed assembler ([Iced](https://github.com/icedland/iced)) or hand-emitted stubs cover the residual need with no native dependency and full x64 support.
|
||||
**Conclusion**: the assembly subsystem is not inherent to process introspection — it is a consequence of choosing `CreateRemoteThread` + "support arbitrary calling conventions" as the execution contract. Change the execution primitive (as current BM did) and the need for a runtime assembler evaporates. A managed assembler ([Iced](https://github.com/icedland/iced)) or hand-emitted stubs cover the residual need with no native dependency and full x64 support.
|
||||
|
||||
## Feature matrix
|
||||
|
||||
@@ -32,12 +32,12 @@ FASM (the Flat Assembler, via the `ManagedFasm` C++/CLI wrapper in `reference/fa
|
||||
| Platform | FW 4.0, x86 | FW, x86 | FW, x86 | **.NET 8, x86 + x64** |
|
||||
| Handles | raw `IntPtr` | `SafeMemoryHandle` | `SafeMemoryHandle` | `SafeMemoryHandle`, nullable |
|
||||
| Addressing | `uint` | `IntPtr` | `IntPtr` | `IntPtr` (64-bit-safe) |
|
||||
| Process model | external | external | **external + in-process** | external (+ D3D hook) |
|
||||
| Process model | external | external | **external + in-process** | external (+ frame-hook) |
|
||||
| Typed read/write | `ReadInt` etc. | `Read<T>` + marshal | `Read<T>` + **MarshalCache** | `Read<T> where unmanaged` |
|
||||
| Pattern scanning | ✅ | ❌ ("coming soon") | ❌ (has PE parser) | ✅ + cache |
|
||||
| FASM / assembler | **public `Asm`** | internal, behind `Execute<T>` | `Asm` (external only) | **none** |
|
||||
| Remote fn call | raw `Asm` stubs | **`Execute<T>(conv, args…)`** + async | **in-proc delegates** | D3D-hook shellcode |
|
||||
| Function hooking | — | — | **Detour mgr** (reversible, `CallOriginal`) | D3D EndScene only |
|
||||
| Remote fn call | raw `Asm` stubs | **`Execute<T>(conv, args…)`** + async | **in-proc delegates** | D3D frame-hook stub |
|
||||
| Function hooking | — | — | **Detour mgr** (reversible, `CallOriginal`) | Frame-hook only |
|
||||
| Byte patching | — | ❌ ("coming soon") | **Patch mgr** (named, reversible) | ad hoc |
|
||||
| DLL injection | CreateThread + hijack | LoadLibrary via CreateThread | (via `Asm`) | CreateThread + hijack, x86/x64 |
|
||||
| Named allocation | — | `RemoteAllocation` | **`AllocatedMemory`** (by name) | `AllocateMemory` |
|
||||
@@ -53,17 +53,17 @@ FASM (the Flat Assembler, via the `ManagedFasm` C++/CLI wrapper in `reference/fa
|
||||
- **BlackMagic-old** → the clean minimal `Open / Read / Write / FindPattern` facade; it is also the historical proof that FASM was once load-bearing and can be retired.
|
||||
- **MemorySharp** → high-level ergonomics: calling-convention `Execute<T>` + parameter marshalling + async, `RemotePointer` indexer, PEB/TEB, window + keyboard/mouse simulation, helper utilities.
|
||||
- **GreyMagic** → the engine: dual in/out-of-process `MemoryBase`, `MarshalCache<T>` fast typed IO, reversible `DetourManager` + `PatchManager`, `CreateFunction<T>`/vtable helpers, named `AllocatedMemory`, `PeHeaderParser`.
|
||||
- **BlackMagic (current)** → the modern platform: .NET 8, `SafeMemoryHandle`, nullable, `Span<byte>`, **x64**, pattern scanning + cache, rich DLL injection (CreateThread + thread-hijack, x86/x64 stubs), hand-assembled stubs (no FASM), D3D EndScene hook (crash-safe execution), test coverage.
|
||||
- **BlackMagic (current)** → the modern platform: .NET 8, `SafeMemoryHandle`, nullable, `Span<byte>`, **x64**, pattern scanning + cache, rich DLL injection (CreateThread + thread-hijack, x86/x64 stubs), hand-assembled stubs (no FASM), per-frame hook (crash-safe execution), test coverage.
|
||||
|
||||
## The crash-safety principle (why `CreateRemoteThread` needs care)
|
||||
|
||||
`CreateRemoteThread` does not crash WoW — **calling game internals from the wrong thread does**. The game's main thread has exclusive affinity for the Lua VM, the D3D9 device, and the object manager. A thread you spawn runs concurrently with it; the moment a payload touches that state (`CastSpellByName`, `FrameScript::Execute`, object traversal) it races the main thread → memory corruption → crash. This matches the "3 crashes in one session" recorded in `FASM-MIGRATION.md`.
|
||||
`CreateRemoteThread` does not crash the target — **calling target internals from the wrong thread does**. The target's main thread has exclusive affinity for its scripting VM, the render device, and the object model. A thread you spawn runs concurrently with it; the moment a payload touches that state (scripting-engine entry points, object traversal) it races the main thread → memory corruption → crash. This matches the "3 crashes in one session" recorded in `FASM-MIGRATION.md`.
|
||||
|
||||
The rule the successor must encode — **split execution by payload safety**:
|
||||
|
||||
1. **`CreateRemoteThread` is safe** for *self-contained, thread-agnostic* payloads: `LoadLibrary` (DLL injection), pure WinAPI, code touching only memory you own.
|
||||
2. **Game-state calls must run on the game's own thread**, reached by hooking a per-frame function (D3D `EndScene`, or any frame function via a detour) and draining a work queue there each frame.
|
||||
3. **In-process** (once a managed DLL is injected), call game functions directly as delegates — no thread crossing at all.
|
||||
2. **State-sensitive calls must run on the target's own thread**, reached by hooking a per-frame function (D3D `EndScene`, or any frame function via a detour) and draining a work queue there each frame.
|
||||
3. **In-process** (once a managed DLL is injected), call target functions directly as delegates — no thread crossing at all.
|
||||
|
||||
## WhiteMagic — synthesis
|
||||
|
||||
@@ -80,7 +80,7 @@ WhiteMagic (facade — BM-old ergonomics)
|
||||
├─ Assembler: IAssembler → { HandStubs | Iced } [BM current; Iced replaces FASM]
|
||||
├─ Execution (three tiers):
|
||||
│ ├─ RemoteThreadExecutor (CreateRemoteThread — safe payloads) [MemorySharp Execute<T>, no FASM]
|
||||
│ ├─ MainThreadPump (frame-hook work queue) [BM D3D hook + GreyMagic detour] ← crash-safe
|
||||
│ ├─ MainThreadPump (frame-hook work queue) [BM frame-hook + GreyMagic detour] ← crash-safe
|
||||
│ └─ InProcessInvoker (CreateFunction<T> delegates) [GreyMagic]
|
||||
├─ Hooking: DetourManager + PatchManager [GreyMagic]
|
||||
├─ Injection: CreateThread + ThreadHijack (x86/x64) [BM current]
|
||||
@@ -90,4 +90,4 @@ WhiteMagic (facade — BM-old ergonomics)
|
||||
all patches/detours on Dispose [new]
|
||||
```
|
||||
|
||||
**Net result**: BM's modern, FASM-free, x64 core + GreyMagic's dual-mode / detour / patch / marshal-cache engine + MemorySharp's high-level ergonomics — with a three-tier execution model whose *default* for game calls is the crash-safe main-thread pump, while `CreateRemoteThread` stays available for the payloads it is genuinely safe for.
|
||||
**Net result**: BM's modern, FASM-free, x64 core + GreyMagic's dual-mode / detour / patch / marshal-cache engine + MemorySharp's high-level ergonomics — with a three-tier execution model whose *default* for state-sensitive calls is the crash-safe main-thread pump, while `CreateRemoteThread` stays available for the payloads it is genuinely safe for.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
BlackMagic is a pure C# process manipulation library. It replaced FASM (native x86 assembler DLL) with hand-assembled `byte[]` stubs. Two capabilities were lost:
|
||||
|
||||
- `InjectAndExecuteEx`: non-blocking remote thread that returns a handle without waiting.
|
||||
- Text-based assembly: build shellcode from `pushad`, `mov eax, 0x1234`, etc. instead of raw bytes.
|
||||
- Text-based assembly: build code payloads from `pushad`, `mov eax, 0x1234`, etc. instead of raw bytes.
|
||||
|
||||
Existing code:
|
||||
- `BMThread.cs` has blocking `Execute(addr, param)` → waits 10s, returns exit code.
|
||||
@@ -21,7 +21,7 @@ Existing code:
|
||||
- TDD: write tests first for every public API surface.
|
||||
|
||||
**Non-Goals:**
|
||||
- Full x86 instruction set (only shellcode-common subset: mov, push, pop, call, jmp, ret, nop, pushad/popad, test, je/jne, inc, add, sub, xor, and, or, cmp, lea, nop, hlt).
|
||||
- Full x86 instruction set (only common payload subset: mov, push, pop, call, jmp, ret, nop, pushad/popad, test, je/jne, inc, add, sub, xor, and, or, cmp, lea, nop, hlt).
|
||||
- x64 text assembly (keep x86 only; x64 stubs remain hand-assembled `byte[]`).
|
||||
- Reimplementing FASM's directive system (`org`, `use32`, macros).
|
||||
- Native DLL dependency.
|
||||
@@ -33,18 +33,18 @@ Existing code:
|
||||
New directory `Asm/` keeps assembler code isolated. Static class, no instance state needed — each `Assemble()` call is self-contained.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Instance class with `AddLine()` builder pattern → rejected: adds statefulness for no benefit. Each shellcode is a fresh call.
|
||||
- Instance class with `AddLine()` builder pattern → rejected: adds statefulness for no benefit. Each payload is built fresh.
|
||||
- Put in `Injection/` → rejected: assembler is generic, not injection-specific.
|
||||
|
||||
### D2: Two-pass assembler (labels + bytes)
|
||||
|
||||
Pass 1: scan instructions, record label positions, emit bytes (reserving 4 bytes for near jumps). Pass 2: resolve label offsets, patch jump targets.
|
||||
|
||||
This handles forward references (`jmp @skip` before `@skip:` label is defined) without multiple iterations. `SetPassLimit()` caps the loop for safety but 2 passes is sufficient for all shellcode patterns.
|
||||
This handles forward references (`jmp @skip` before `@skip:` label is defined) without multiple iterations. `SetPassLimit()` caps the loop for safety but 2 passes is sufficient for all payload patterns.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Single-pass → rejected: can't resolve forward jumps.
|
||||
- FASM-style multi-pass → overkill: shellcode doesn't need complex expression evaluation.
|
||||
- FASM-style multi-pass → overkill: payloads don't need complex expression evaluation.
|
||||
|
||||
### D3: Instruction encoding via switch + helper methods
|
||||
|
||||
@@ -70,5 +70,5 @@ Then implement `AsmBuilder` to make tests pass.
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Instruction subset**: users may need an instruction not in the initial set. Mitigation: document supported instructions, add new ones incrementally.
|
||||
- **Label complexity**: relative jumps are limited to ±127 bytes (near) or ±2GB (far). Shellcode rarely exceeds this, but document the limit.
|
||||
- **No runtime validation of shellcode**: the assembler produces bytes; it doesn't verify the result is safe to execute. This matches FASM's behavior — the assembler doesn't validate semantics.
|
||||
- **Label complexity**: relative jumps are limited to ±127 bytes (near) or ±2GB (far). Payloads rarely exceed this, but document the limit.
|
||||
- **No runtime validation of payloads**: the assembler produces bytes; it doesn't verify the result is safe to execute. This matches FASM's behavior — the assembler doesn't validate semantics.
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
## Why
|
||||
|
||||
BlackMagic replaced FASM for shellcode generation but lost two useful capabilities:
|
||||
BlackMagic replaced FASM for code-payload generation but lost two useful capabilities:
|
||||
|
||||
1. **Non-blocking remote execution** (`InjectAndExecuteEx`): FASM's managed wrapper returned a thread handle without waiting. BlackMagic only has blocking `Execute()`. For DLL injection, a non-blocking variant avoids hanging when the target is slow to load.
|
||||
|
||||
2. **Text-based assembly**: FASM allowed building shellcode from assembly text (`AddLine("pushad")`). BlackMagic requires hand-assembled `byte[]`. For prototyping, debugging, and one-off shellcode, text assembly is faster to write and easier to review. A managed assembler eliminates the native FASM DLL dependency while keeping the ergonomic benefit.
|
||||
2. **Text-based assembly**: FASM allowed building code payloads from assembly text (`AddLine("pushad")`). BlackMagic requires hand-assembled `byte[]`. For prototyping, debugging, and one-off code payloads, text assembly is faster to write and easier to review. A managed assembler eliminates the native FASM DLL dependency while keeping the ergonomic benefit.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `InjectAndExecuteEx()` to `BlackMagic` and `BMThread`: inject code then create a remote thread without waiting, returning the thread handle.
|
||||
- Add `AsmBuilder` class: pure C# x86 text assembler that converts instruction text to `byte[]` machine code. Supports common shellcode instructions (mov, push, pop, call, jmp, ret, nop, pushad/popad, test, je, jne, inc, add, sub, xor, etc.).
|
||||
- Add `AsmBuilder` class: pure C# x86 text assembler that converts instruction text to `byte[]` machine code. Supports common payload instructions (mov, push, pop, call, jmp, ret, nop, pushad/popad, test, je, jne, inc, add, sub, xor, etc.).
|
||||
- Add `InjectAndExecute(string asm)` and `InjectAndExecuteEx(string asm)` overloads that accept assembly text, assemble via `AsmBuilder`, then inject+execute.
|
||||
- Add `SetPassLimit()` to `AsmBuilder` for label resolution iteration control.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ This repo contains four C# process-manipulation libraries studied in `docs/memor
|
||||
- **GreyMagic** (`reference/GreyMagic/`) — .NET FW, ~2016, x86; dual in/out-of-process `MemoryBase`, `MarshalCache`, `DetourManager`, `PatchManager`, `CreateFunction<T>`, `PeHeaderParser`.
|
||||
- **BlackMagic** (`reference/Blackmagic/`, current) — .NET 8, x86 + x64, FASM removed, pattern scanning + cache, DLL injection (CreateThread + hijack), hand-assembled x86 stubs (`SInject.cs` `EmitU8`/`EmitU32` byte emitter), `CreateRemoteThread`-based `Execute` (`BMThread.cs`). No frame hook exists.
|
||||
|
||||
The consuming use case is a WoW 3.3.5a bot. The dominant failure mode observed (`FASM-MIGRATION.md`) is game crashes when protected/game-state functions are invoked on a thread created by `CreateRemoteThread`, because WoW's main thread has exclusive affinity for the Lua VM, D3D9 device, and object manager.
|
||||
The consuming use case is an automation client targeting a legacy x86 desktop application. The dominant failure mode observed (`FASM-MIGRATION.md`) is target crashes when state-sensitive functions are invoked on a thread created by `CreateRemoteThread`, because the target's main thread has exclusive affinity for its scripting VM, render device, and object model.
|
||||
|
||||
WhiteMagic is a **new, additive** .NET 8 library that unifies the four. It reuses their ideas, not their assemblies. No project already depends on WhiteMagic, so there is no backward-compatibility constraint.
|
||||
|
||||
@@ -15,7 +15,7 @@ WhiteMagic is a **new, additive** .NET 8 library that unifies the four. It reuse
|
||||
|
||||
**Goals:**
|
||||
- Single modern (.NET 8, nullable, `Span<byte>`, `SafeHandle`) library that is bitness-agnostic (x86 + x64).
|
||||
- A **three-tier execution model** whose default path for game-state calls is crash-safe (runs on the target's own thread), while `CreateRemoteThread` remains available for thread-agnostic payloads.
|
||||
- A **three-tier execution model** whose default path for state-sensitive calls is crash-safe (runs on the target's own thread), while `CreateRemoteThread` remains available for thread-agnostic payloads.
|
||||
- Dual memory access: out-of-process (RPM/WPM) and in-process (RPM-on-self-handle, see D1 revision) behind one abstract `MemoryBase`, with `MarshalCache<T>` for allocation-free typed IO.
|
||||
- Reversible function hooking (`DetourManager`) and byte patching (`PatchManager`) with auto-restore on dispose.
|
||||
- Replace FASM with an `IAssembler` seam: hand-emitted convention stubs by default, optional Iced backend for arbitrary assembly. Zero native dependency in the default configuration.
|
||||
@@ -27,8 +27,8 @@ WhiteMagic is a **new, additive** .NET 8 library that unifies the four. It reuse
|
||||
- Modifying or replacing BlackMagic/MemorySharp/GreyMagic — WhiteMagic is additive.
|
||||
- Shipping a full x86/x64 assembler ourselves — arbitrary assembly is delegated to Iced; only the fixed convention-stub shapes are hand-emitted.
|
||||
- Managed-DLL injection bootstrapper (the CLR host that loads `InProcessReader` into the target). WhiteMagic exposes the in-process API surface; wiring an actual managed loader is a follow-up change.
|
||||
- WoW-specific offsets, Lua unlock, or bot logic — those live in the consumer, not the library.
|
||||
- Anti-cheat evasion.
|
||||
- Application-specific offsets, scripting-engine hooks, or automation logic — those live in the consumer, not the library.
|
||||
- Interference with other software's operation.
|
||||
|
||||
## Decisions
|
||||
|
||||
@@ -38,7 +38,7 @@ WhiteMagic is a **new, additive** .NET 8 library that unifies the four. It reuse
|
||||
- `ExternalReader : MemoryBase` — `ReadProcessMemory`/`WriteProcessMemory` over a `SafeMemoryHandle`. Owns allocation, injection, and the remote-thread + main-thread executors.
|
||||
- `InProcessReader : MemoryBase` — reads the current process via `ReadProcessMemory`/`WriteProcessMemory` on a self-handle; owns the `DetourManager` and `InProcessInvoker`. **(Revised from `unsafe` direct deref during Phase 2: .NET cannot catch `AccessViolationException`, so a bad deref kills the host with no soft-failure path; RPM-on-self fails soft. The in-process speed win moves to the delegate-call/detour paths, not the reader. See `specs/memory-access`.)**
|
||||
|
||||
**Why**: GreyMagic proved this abstraction lets the same higher-level code (pattern scan, patch, high-level API) run in either mode. External is the primary path for a bot host; in-process becomes valuable once injected — not for faster reads (both readers use RPM/WPM, see the D1 revision) but for the delegate-call and detour paths it unlocks (`InProcessInvoker`, `DetourManager`).
|
||||
**Why**: GreyMagic proved this abstraction lets the same higher-level code (pattern scan, patch, high-level API) run in either mode. External is the primary path for an automation host; in-process becomes valuable once injected — not for faster reads (both readers use RPM/WPM, see the D1 revision) but for the delegate-call and detour paths it unlocks (`InProcessInvoker`, `DetourManager`).
|
||||
|
||||
**Alternatives considered**: single external-only class (current BlackMagic) — rejected: forecloses the in-process delegate path, which is the cleanest crash-free execution. MemorySharp's factory-per-concern model (`Assembly`, `Threads`, `Windows` factories) — adopted selectively for the high-level surface, but the read/write core stays on `MemoryBase` for GreyMagic-style polymorphism.
|
||||
|
||||
@@ -48,13 +48,13 @@ Execution is split by **payload safety**, not by convenience:
|
||||
|
||||
| Tier | Type | Use when | Mechanism |
|
||||
|---|---|---|---|
|
||||
| Remote thread | `RemoteThreadExecutor` | payload is thread-agnostic (LoadLibrary, pure WinAPI, self-contained shellcode) | `CreateRemoteThread` + convention stub, wait, exit code |
|
||||
| **Main-thread pump** | `MainThreadPump` | **payload touches game state** (default) | queue delegate → drained on target thread via per-frame hook |
|
||||
| Remote thread | `RemoteThreadExecutor` | payload is thread-agnostic (LoadLibrary, pure WinAPI, self-contained code payload) | `CreateRemoteThread` + convention stub, wait, exit code |
|
||||
| **Main-thread pump** | `MainThreadPump` | **payload touches target state** (default) | queue delegate → drained on target thread via per-frame hook |
|
||||
| In-process | `InProcessInvoker` | injected in-process | `Marshal.GetDelegateForFunctionPointer`, direct call |
|
||||
|
||||
`MainThreadPump` installs a detour on a caller-supplied per-frame function address (an `EndScene` resolver ships as a convenience helper) via `DetourManager`. Each frame the hook drains a thread-safe queue and runs pending work items synchronously in the game's context, returning results/exceptions to the requesting thread through a completion handle. **This is net-new — no frame hook exists in current BlackMagic to port.** It is built on GreyMagic-style detours (D5) as its one underlying primitive; the pump is the first consumer of `DetourManager`.
|
||||
`MainThreadPump` installs a detour on a caller-supplied per-frame function address (an `EndScene` resolver ships as a convenience helper) via `DetourManager`. Each frame the hook drains a thread-safe queue and runs pending work items synchronously in the target's context, returning results/exceptions to the requesting thread through a completion handle. **This is net-new — no frame hook exists in current BlackMagic to port.** It is built on GreyMagic-style detours (D5) as its one underlying primitive; the pump is the first consumer of `DetourManager`.
|
||||
|
||||
**Why**: `CreateRemoteThread` does not itself crash the game — calling single-thread-affinity game internals from a foreign thread does. Making the pump the default for game calls encodes that rule so callers cannot trip the crash by accident, while power users retain raw `CreateRemoteThread` for the payloads it is safe for.
|
||||
**Why**: `CreateRemoteThread` does not itself crash the target — calling single-thread-affinity target internals from a foreign thread does. Making the pump the default for target-state calls encodes that rule so callers cannot trip the crash by accident, while power users retain raw `CreateRemoteThread` for the payloads it is safe for.
|
||||
|
||||
**Alternatives considered**: (a) always `CreateRemoteThread` (MemorySharp/old-BM) — rejected: the documented crash source. (b) always in-process (GreyMagic) — rejected: requires a managed loader in the target and is not always available; external must work standalone. (c) thread-hijack for every call — rejected: high risk, one-shot, poor for repeated calls; kept only for injection.
|
||||
|
||||
@@ -110,7 +110,7 @@ A `static class MarshalCache<T>` computes and caches `Marshal.SizeOf`, `TypeCode
|
||||
WhiteMagic is additive; there is nothing to migrate off. Delivery is phased so each slice is independently useful and testable:
|
||||
|
||||
1. **Core** — `MemoryBase`, `ExternalReader`, `SafeMemoryHandle`, `MarshalCache`, typed/string/bytes IO. (Replaces nothing; standalone.)
|
||||
2. **Crash-safe execution slice** — `StubAssembler`, `DetourManager`, `MainThreadPump`, `RemoteThreadExecutor`. Proves game-state calls without crashes end-to-end. This is the headline deliverable.
|
||||
2. **Crash-safe execution slice** — `StubAssembler`, `DetourManager`, `MainThreadPump`, `RemoteThreadExecutor`. Proves state-sensitive calls without crashes end-to-end. This is the headline deliverable.
|
||||
3. **Injection & discovery** — DLL injection (CreateThread + hijack, x86/x64), pattern scanning + cache, `PeHeaderParser`, named `AllocatedMemory`.
|
||||
4. **In-process tier** — `InProcessReader`, `InProcessInvoker`, `CreateFunction<T>`, vtable helpers (API surface; managed loader deferred).
|
||||
5. **High-level ergonomics** — `RemotePointer`, `RemoteModule`/`RemoteFunction`, PEB/TEB, window, input, async, `PatchManager` polish, helpers.
|
||||
@@ -120,7 +120,7 @@ WhiteMagic is additive; there is nothing to migrate off. Delivery is phased so e
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **Frame-hook target**: ~~default to D3D9 `EndScene`, or accept a caller-supplied per-frame function address?~~ **Resolved**: `MainThreadPump` takes a caller-supplied frame-function address (WoW-agnostic, library stays offset-free per Non-Goals); an `EndScene` resolver ships as a convenience helper only. Slice 2 depends on this — settled before slice 2 starts.
|
||||
- **Frame-hook target**: ~~default to D3D9 `EndScene`, or accept a caller-supplied per-frame function address?~~ **Resolved**: `MainThreadPump` takes a caller-supplied frame-function address (application-agnostic, library stays offset-free per Non-Goals); an `EndScene` resolver ships as a convenience helper only. Slice 2 depends on this — settled before slice 2 starts.
|
||||
- **Managed in-process loader**: which host mechanism (custom CLR host vs. a native shim that calls `CorBindToRuntimeEx`/`ICLRRuntimeHost`)? Deferred to a follow-up change but affects the `InProcessReader` seam shape.
|
||||
- **Iced as default vs optional**: keep hand-stubs default (zero dep) — confirmed — but should the library ship a `WhiteMagic.Iced` companion package rather than an optional reference? Package boundary TBD.
|
||||
- **Async model**: `Task`-based wrappers (MemorySharp) vs. exposing the pump's completion handles directly. Likely both: pump returns a handle, async wrappers adapt it to `Task<T>`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## Why
|
||||
|
||||
Four process-manipulation libraries in this repo each solve part of the problem but none is complete: **BlackMagic-old** and **MemorySharp** depend on the native FASM assembler; **MemorySharp** has rich high-level ergonomics but is 32-bit-only and unmaintained; **GreyMagic** has the best engine (in-process reads, detours, patches, marshal cache) but is 32-bit and external-FASM-bound; **current BlackMagic** is the only modern, x64, FASM-free base but lacks remote function-calling, hooking, and high-level ergonomics. The recurring failure mode is game crashes from calling game internals on a foreign thread created by `CreateRemoteThread`. WhiteMagic unifies the best of all four into one modern (.NET 8, x64) library whose default path for game-state calls is crash-safe.
|
||||
Four process-manipulation libraries in this repo each solve part of the problem but none is complete: **BlackMagic-old** and **MemorySharp** depend on the native FASM assembler; **MemorySharp** has rich high-level ergonomics but is 32-bit-only and unmaintained; **GreyMagic** has the best engine (in-process reads, detours, patches, marshal cache) but is 32-bit and external-FASM-bound; **current BlackMagic** is the only modern, x64, FASM-free base but lacks remote function-calling, hooking, and high-level ergonomics. The recurring failure mode is target crashes from calling target internals on a foreign thread created by `CreateRemoteThread`. WhiteMagic unifies the best of all four into one modern (.NET 8, x64) library whose default path for state-sensitive calls is crash-safe (runs on the target's own thread) while `CreateRemoteThread` stays available for thread-agnostic payloads.
|
||||
|
||||
## What Changes
|
||||
|
||||
@@ -8,7 +8,7 @@ Four process-manipulation libraries in this repo each solve part of the problem
|
||||
- **Dual memory-access model**: an abstract `MemoryBase` with an `ExternalReader` (ReadProcessMemory/WriteProcessMemory) and an `InProcessReader` (direct pointer deref for injected scenarios), fronted by a `MarshalCache<T>` for allocation-free typed reads/writes.
|
||||
- **Three-tier remote execution** — the headline capability:
|
||||
- `RemoteThreadExecutor`: `CreateRemoteThread`-based `Execute<T>(addr, convention, args…)`, documented as safe for **thread-agnostic payloads only**.
|
||||
- `MainThreadPump`: a crash-safe work queue drained on the target's own thread via a detour on a caller-supplied per-frame function (with an `EndScene` resolver helper) — the default for game-state calls. Net-new; no frame hook exists in current BlackMagic to port.
|
||||
- `MainThreadPump`: a crash-safe work queue drained on the target's own thread via a detour on a caller-supplied per-frame function (with an `EndScene` resolver helper) — the default for state-sensitive calls. Net-new; no frame hook exists in current BlackMagic to port.
|
||||
- `InProcessInvoker`: direct native-delegate calls (`CreateFunction<T>`) when injected in-process.
|
||||
- **Function hooking**: reversible `DetourManager` (inline jmp, `CallOriginal`) and `PatchManager` (named byte patches) with auto-restore on dispose.
|
||||
- **Managed assembler seam**: an `IAssembler` abstraction with two backends — hand-emitted calling-convention stubs (default) and an optional [Iced](https://github.com/icedland/iced) backend for arbitrary x86/x64 assembly. **No FASM, no native DLL.**
|
||||
@@ -36,4 +36,4 @@ Four process-manipulation libraries in this repo each solve part of the problem
|
||||
- **New dependency (optional)**: `Iced` NuGet package, isolated behind `IAssembler`; the default hand-stub backend has zero third-party dependencies.
|
||||
- **No native dependency**: FASM (`reference/fasm/`, `ManagedFasm`) is not referenced. It remains historical reference only, consistent with `FASM-MIGRATION.md`.
|
||||
- **No changes** to BlackMagic, MemorySharp, GreyMagic, or their tests — WhiteMagic reuses their ideas, not their assemblies.
|
||||
- **Platform**: builds x86 and x64; game targeting stays x86 to match `Wow.exe`, but the library is bitness-agnostic.
|
||||
- **Platform**: builds x86 and x64; target bitness stays x86 to match the reference application, but the library is bitness-agnostic.
|
||||
|
||||
@@ -8,7 +8,7 @@ WhiteMagic SHALL provide three execution strategies selected by payload safety:
|
||||
- **WHEN** a caller chooses an execution strategy
|
||||
- **THEN** each of remote-thread, main-thread-pump, and in-process MUST be individually invokable
|
||||
|
||||
#### Scenario: main-thread pump is the documented default for game state
|
||||
#### Scenario: main-thread pump is the documented default for state-sensitive calls
|
||||
- **WHEN** documentation or API guidance describes calling functions that touch single-thread-affinity process state
|
||||
- **THEN** it MUST direct callers to the main-thread pump, not `CreateRemoteThread`
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
- [x] 2.7 Add tests for relative/absolute addressing (`GetAbsolute`/`GetRelative`, `isRelative` flag)
|
||||
- [x] 2.8 Implement addressing helpers to pass 2.7
|
||||
- [x] 2.9 Add tests + implementation for `InProcessReader` (RPM/WPM on a self-handle — see D1 deviation note; direct deref rejected because .NET cannot catch `AccessViolationException`); verify shared `MemoryBase` API works for both readers
|
||||
- [ ] 2.10 Follow-up (found in review): `ReadString` scans for the null terminator byte-by-byte, so for UTF-16/UTF-32 it can match a **misaligned** multi-byte null across a char boundary (e.g. `"A"`+U+4200 = `41 00 00 42` matches `{00,00}` at offset 1) and can miss a terminator split across the 64-byte chunk boundary. Harmless for ASCII/UTF-8 (the WoW case). Fix: align the scan to the encoding's code-unit width and carry the last `(nullLen-1)` bytes across chunks. Add a UTF-16 test.
|
||||
- [ ] 2.10 Follow-up (found in review): `ReadString` scans for the null terminator byte-by-byte, so for UTF-16/UTF-32 it can match a **misaligned** multi-byte null across a char boundary (e.g. `"A"`+U+4200 = `41 00 00 42` matches `{00,00}` at offset 1) and can miss a terminator split across the 64-byte chunk boundary. Harmless for ASCII/UTF-8 (single-byte encodings). Fix: align the scan to the encoding's code-unit width and carry the last `(nullLen-1)` bytes across chunks. Add a UTF-16 test.
|
||||
|
||||
## 3. Managed Assembler (spec: managed-assembler)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user