Add whitemagic-foundation OpenSpec design; isolate reference libs
Design-only foundation for WhiteMagic, a .NET 8 x64 library unifying the four studied process-manipulation libs. Adds proposal, design (7 decisions), 7 capability specs, and TDD task breakdown; all validate strict. Move Blackmagic, Blackmagic-old, GreyMagic, MemorySharp, fasm into reference/ (gitignored) — studied, not built here; each has its own upstream repo and nested .git. Rewrite plan doc paths to reference/. Corrects two factual defects found in review: - current BlackMagic has no D3D EndScene hook; MainThreadPump is net-new built on DetourManager, not a port - no BlackMagic.slnx exists; task 1.3 creates a fresh solution Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-21
|
||||
@@ -0,0 +1,74 @@
|
||||
## Context
|
||||
|
||||
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.
|
||||
|
||||
Existing code:
|
||||
- `BMThread.cs` has blocking `Execute(addr, param)` → waits 10s, returns exit code.
|
||||
- `BMThread.cs` has `CreateRemoteThread(addr, param)` → returns `SafeMemoryHandle?`.
|
||||
- `SInject.cs` has `InjectCode(addr, bytes)` and `InjectCode(bytes)` → allocate + write.
|
||||
- `BlackMagicTest/` uses xUnit, `net8.0-windows`, pure-logic tests (no process needed).
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Add `InjectAndExecuteEx()`: inject code + create remote thread, return handle, do NOT wait.
|
||||
- Add `AsmBuilder`: pure C# x86 text assembler. Convert `"pushad\nmov eax, 1\npopad"` → `byte[]`.
|
||||
- Add `SetPassLimit(int)` on `AsmBuilder` for label resolution iteration control.
|
||||
- Add convenience overloads: `InjectAndExecute(string asm)`, `InjectAndExecuteEx(string asm)`.
|
||||
- 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).
|
||||
- x64 text assembly (keep x86 only; x64 stubs remain hand-assembled `byte[]`).
|
||||
- Reimplementing FASM's directive system (`org`, `use32`, macros).
|
||||
- Native DLL dependency.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: AsmBuilder lives in `BlackMagic/Asm/AsmBuilder.cs`
|
||||
|
||||
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.
|
||||
- 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.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Single-pass → rejected: can't resolve forward jumps.
|
||||
- FASM-style multi-pass → overkill: shellcode doesn't need complex expression evaluation.
|
||||
|
||||
### D3: Instruction encoding via switch + helper methods
|
||||
|
||||
Each instruction maps to hand-coded byte emission. Helper methods: `EmitU8()`, `EmitU32()`, `EmitModRM()`, `EmitSIB()`. Same pattern as `SInject.cs`'s existing `BuildStub()` methods.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Lookup table / dictionary → rejected: instruction encoding is irregular (ModRM, SIB, displacement, immediate). Switch statements are clearer.
|
||||
|
||||
### D4: InjectAndExecuteEx returns `SafeMemoryHandle`
|
||||
|
||||
Matches existing `CreateRemoteThread()` return type. Caller is responsible for disposing the handle. No auto-wait, no auto-close.
|
||||
|
||||
### D5: TDD approach
|
||||
|
||||
Write xUnit tests in `BlackMagicTest/AsmBuilderTests.cs` first:
|
||||
- Each instruction: known input text → expected `byte[]` output.
|
||||
- Label resolution: forward jump, backward jump, multiple labels.
|
||||
- Error cases: unknown instruction, missing operand, invalid register.
|
||||
- `SetPassLimit`: verify iteration cap is respected.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,23 @@
|
||||
## Why
|
||||
|
||||
BlackMagic replaced FASM for shellcode 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.
|
||||
|
||||
## 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 `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.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `non-blocking-execute`: Non-blocking remote thread creation that returns a handle without waiting for exit.
|
||||
- `text-assembler`: Pure C# x86 text assembler converting assembly source to byte arrays without native dependencies.
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- None — additive features only. -->
|
||||
@@ -0,0 +1,41 @@
|
||||
## ADDED 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
|
||||
@@ -0,0 +1,84 @@
|
||||
## ADDED 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
|
||||
@@ -0,0 +1,44 @@
|
||||
## 1. AsmBuilder — Core (TDD: tests first)
|
||||
|
||||
- [ ] 1.1 Create `BlackMagicTest/AsmBuilderTests.cs` with tests for single-instruction assembly: `nop` → `[0x90]`, `pushad` → `[0x60]`, `popad` → `[0x61]`, `ret` → `[0xC3]`, `hlt` → `[0xF4]`
|
||||
- [ ] 1.2 Create `BlackMagic/Asm/AsmBuilder.cs` with `Assemble(string source)` entry point and instruction table. Implement `nop`, `pushad`, `popad`, `ret`, `hlt` to make step 1.1 tests pass
|
||||
- [ ] 1.3 Add tests for `mov reg, imm32` (eax, ecx, edx, ebx, esp, ebp, esi, edi) — each register maps to `0xB8 + reg` encoding with 4-byte little-endian immediate
|
||||
- [ ] 1.4 Implement `mov reg, imm32` in AsmBuilder to make step 1.3 tests pass
|
||||
- [ ] 1.5 Add tests for `mov reg, reg` (register-to-register via ModRM byte 0xC0 + (src<<3 | dst))
|
||||
- [ ] 1.6 Implement `mov reg, reg` to make step 1.5 tests pass
|
||||
- [ ] 1.7 Add tests for `push reg` (`0x50 + reg`), `pop reg` (`0x58 + reg`)
|
||||
- [ ] 1.8 Implement `push reg`, `pop reg` to make step 1.7 tests pass
|
||||
- [ ] 1.9 Add tests for `add reg, imm8` (`0x83 /0` with sign-extended byte), `sub reg, imm8` (`0x83 /5`), `xor reg, reg` (`0x31 /r`), `and reg, imm8`, `or reg, imm8`, `cmp reg, imm8` (`0x83 /7`)
|
||||
- [ ] 1.10 Implement `add`, `sub`, `xor`, `and`, `or`, `cmp` to make step 1.9 tests pass
|
||||
- [ ] 1.11 Add tests for `inc reg` (`0x40 + reg`), `dec reg` (`0x48 + reg`), `test reg, reg` (`0x85 /r`)
|
||||
- [ ] 1.12 Implement `inc`, `dec`, `test` to make step 1.11 tests pass
|
||||
- [ ] 1.13 Add tests for `jmp @label`, `je @label`, `jne @label` with forward and backward references
|
||||
- [ ] 1.14 Implement two-pass label resolution in AsmBuilder (pass 1: record label offsets, pass 2: patch jump targets). Make step 1.13 tests pass
|
||||
- [ ] 1.15 Add tests for `call @label` (near call, E8 rel32) and `nop` multi-instruction sequences
|
||||
- [ ] 1.16 Implement `call` to make step 1.15 tests pass
|
||||
- [ ] 1.17 Add tests for error cases: unknown instruction → `ArgumentException` with line number, missing operand → `ArgumentException`, invalid register → `ArgumentException`
|
||||
- [ ] 1.18 Implement error reporting to make step 1.17 tests pass
|
||||
|
||||
## 2. AsmBuilder — Pass Limit
|
||||
|
||||
- [ ] 2.1 Add tests for `SetPassLimit()`: default is 10, custom value is respected, exceeded limit throws `InvalidOperationException`
|
||||
- [ ] 2.2 Implement `SetPassLimit(int limit)` and pass-limit enforcement in AsmBuilder to make step 2.1 tests pass
|
||||
|
||||
## 3. InjectAndExecuteEx — Non-blocking Execution
|
||||
|
||||
- [ ] 3.1 Add tests for `BlackMagic.InjectAndExecuteEx(IntPtr, IntPtr)`: returns `null` when no process open, returns valid handle when process open (mock or integration)
|
||||
- [ ] 3.2 Implement `InjectAndExecuteEx(IntPtr startAddress, IntPtr parameter)` in `BMThread.cs` to make step 3.1 tests pass
|
||||
- [ ] 3.3 Add test for single-parameter overload: `InjectAndExecuteEx(IntPtr)` passes `IntPtr.Zero`
|
||||
- [ ] 3.4 Implement `InjectAndExecuteEx(IntPtr startAddress)` overload in `BMThread.cs`
|
||||
|
||||
## 4. Convenience Overloads (text → inject → execute)
|
||||
|
||||
- [ ] 4.1 Add tests for `InjectAndExecute(string asm)`: assembles text, allocates remote memory, writes bytes, executes, returns exit code. Test assembly failure → `ArgumentException`
|
||||
- [ ] 4.2 Implement `InjectAndExecute(string asm)` in `BMInject.cs` to make step 4.1 tests pass
|
||||
- [ ] 4.3 Add tests for `InjectAndExecuteEx(string asm)`: assembles text, allocates remote memory, writes bytes, returns thread handle. Test assembly failure → `ArgumentException`
|
||||
- [ ] 4.4 Implement `InjectAndExecuteEx(string asm)` in `BMInject.cs` to make step 4.3 tests pass
|
||||
|
||||
## 5. Build Verification
|
||||
|
||||
- [ ] 5.1 Run full test suite: `"C:\Program Files\dotnet\dotnet.exe" test BlackMagicTest/BlackMagicTest.csproj` — all tests pass
|
||||
- [ ] 5.2 Run full solution build: `"C:\Program Files\dotnet\dotnet.exe" build BlackMagic.slnx` — zero errors
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-21
|
||||
@@ -0,0 +1,126 @@
|
||||
## Context
|
||||
|
||||
This repo contains four C# process-manipulation libraries studied in `docs/memory-library-comparison.md`:
|
||||
|
||||
- **BlackMagic-old** (`reference/Blackmagic-old/`) — .NET FW 4.0, x86, FASM as a public `Asm` property.
|
||||
- **MemorySharp** (`reference/MemorySharp/`) — .NET FW, x86, FASM behind `Execute<T>`; rich high-level API (PEB/TEB, window, input, calling conventions) but unmaintained since 2016.
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**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.
|
||||
- Dual memory access: out-of-process (RPM/WPM) and in-process (direct deref) 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.
|
||||
- Port DLL injection (CreateThread + thread-hijack, x86/x64) and pattern scanning + cache from current BlackMagic.
|
||||
- Provide MemorySharp-grade ergonomics: `RemotePointer` indexer, `Module["fn"].Execute(...)`, PEB/TEB, window mutation, input simulation, async wrappers, helpers.
|
||||
- Test-first for all pure logic (assembler encodings, marshal cache, pattern matching, stub building, pump queue semantics).
|
||||
|
||||
**Non-Goals:**
|
||||
- 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.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: Layered architecture with an abstract `MemoryBase` (from GreyMagic)
|
||||
|
||||
`MemoryBase` defines abstract `ReadBytes`/`WriteBytes`/`Read<T>`/`Write<T>`, relative/absolute addressing, and hosts the `PatchManager`. Two concrete readers:
|
||||
- `ExternalReader : MemoryBase` — `ReadProcessMemory`/`WriteProcessMemory` over a `SafeMemoryHandle`. Owns allocation, injection, and the remote-thread + main-thread executors.
|
||||
- `InProcessReader : MemoryBase` — `unsafe` direct pointer deref; owns the `DetourManager` and `InProcessInvoker`.
|
||||
|
||||
**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 is the fast/crash-free path once injected.
|
||||
|
||||
**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.
|
||||
|
||||
### D2: Three-tier execution, crash-safe by default (the headline)
|
||||
|
||||
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 |
|
||||
| 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`.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
### D3: `IAssembler` seam replacing FASM
|
||||
|
||||
```
|
||||
IAssembler { byte[] Assemble(string asm, ulong origin = 0); }
|
||||
```
|
||||
Two backends:
|
||||
- `StubAssembler` (default) — hand-emits the fixed calling-convention trampolines (cdecl/stdcall/thiscall/fastcall: push args, call, cleanup, ret) and the injection stubs, using an `EmitU8`/`EmitU32`/`EmitU64` byte emitter (the pattern already in `SInject.cs`). No parsing, deterministic, x64-capable, zero dependency.
|
||||
- `IcedAssembler` (optional) — wraps Iced for arbitrary user-supplied mnemonics when a caller genuinely needs runtime text assembly.
|
||||
|
||||
**Why**: The comparison established that the only thing FASM did was runtime text→bytecode, needed solely for stubs and (rarely) arbitrary asm. Stubs are a small fixed set best hand-emitted; arbitrary asm is better served by a modern, managed, x64, MIT-licensed assembler (Iced) than by a native C++/CLI FASM DLL. Retiring FASM removes the x86-only mixed-mode constraint.
|
||||
|
||||
**Alternatives considered**: keep FASM/Fasm.NET — rejected: native dependency, x86-only, unmaintained. Hand-emit everything including arbitrary asm — rejected: reimplementing an assembler is out of scope; Iced already exists.
|
||||
|
||||
### D4: `MarshalCache<T>` for typed IO (from GreyMagic)
|
||||
|
||||
A `static class MarshalCache<T>` computes and caches `Marshal.SizeOf`, `TypeCode`, `TypeRequiresMarshal`, and `IsIntPtr` once per type. `Read<T>`/`Write<T>` branch on the cached flags: blittable types round-trip through `Span`/`MemoryMarshal`; marshal-required types use `Marshal.PtrToStructure`.
|
||||
|
||||
**Why**: GreyMagic's signature perf win — avoids per-call reflection. Current BlackMagic's `where unmanaged` constraint is faster still for blittable types but cannot express marshalled structs; the cache gives both.
|
||||
|
||||
### D5: Reversible hooking and patching with auto-restore (from GreyMagic)
|
||||
|
||||
`DetourManager`/`Detour` (inline `E9` jmp over a prologue, `CallOriginal`, `Apply`/`Remove`) and `PatchManager`/`Patch` (named byte patch, `Apply`/`Remove`/`IsApplied`). Both register into a manager that restores all live modifications on `MemoryBase.Dispose`. Detours are in-process only (they require executing the hook delegate in the target); patches work in both modes.
|
||||
|
||||
**Safety addition**: before splicing a detour, `StubAssembler`/`IcedAssembler` disassembles the target prologue to confirm the overwrite lands on instruction boundaries, avoiding the mid-instruction-splice crash class. When only `StubAssembler` is present, a minimal length-disassembler covers the common prologue shapes; full validation requires the Iced backend.
|
||||
|
||||
**Why**: MemorySharp promised Hook/Patch "coming soon" and never shipped them; GreyMagic shipped both and they are the cleanest available. Auto-restore prevents leaving the target corrupted after a crash of the host.
|
||||
|
||||
### D6: High-level surface as opt-in factories (from MemorySharp)
|
||||
|
||||
`RemotePointer` (indexer `sharp[addr]`), `RemoteModule`/`RemoteFunction` (`sharp["user32"]["MessageBoxA"].Execute(...)`), `ManagedPeb`/`ManagedTeb`, `WindowFactory` (move/resize/title/flash/activate), `Keyboard`/`Mouse` (PostMessage + SendInput), and async wrappers over the executors. These sit above `MemoryBase` and the execution tiers; none is required for core memory work.
|
||||
|
||||
**Why**: This is the ergonomic layer that made MemorySharp pleasant. It is pure P/Invoke over the core — no assembler, no FASM.
|
||||
|
||||
### D7: New projects, additive, test-first
|
||||
|
||||
`WhiteMagic/` (library, `net8.0-windows`, `AllowUnsafeBlocks`) and `WhiteMagicTest/` (xUnit). No solution file exists in the repo root today; task 1.3 creates a fresh `WhiteMagic.sln` (or builds the csproj directly). BlackMagic lives in a nested subdir with its own git and is not part of this build. Pure-logic components (assembler encodings, marshal cache, pattern matcher, stub builders, pump queue) get tests before implementation, mirroring the existing `inject-and-assemble` change's TDD discipline. Live-process behavior (RPM/WPM, injection, detours) is validated in integration tests gated on an available target.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Scope is large** → Deliver in vertical slices (see Migration Plan). The minimum crash-safe slice is `MemoryBase` + `ExternalReader` + `DetourManager` + `MainThreadPump`; everything else layers on without reworking it.
|
||||
- **In-process tier needs a managed loader not in scope** → Ship `InProcessReader`'s API and delegate-call path now; mark the actual CLR-host injection as a follow-up. External + pump deliver crash-safety without it.
|
||||
- **Detour prologue splicing can crash if misaligned** → Validate instruction boundaries before writing (D5); require Iced backend for full validation; keep `Remove`/auto-restore so a bad detour is recoverable.
|
||||
- **Iced adds a NuGet dependency** → Isolated behind `IAssembler`; default `StubAssembler` keeps the library dependency-free. Only callers needing arbitrary asm opt in.
|
||||
- **`MainThreadPump` adds per-frame overhead and a hook on a hot function** → Keep the drained-per-frame work bounded; the hook is a thin queue check when idle. Document that a wedged work item stalls the frame.
|
||||
- **x64 detours need larger/absolute jumps (14-byte `push/ret` or RIP-relative)** → `StubAssembler` emits the correct form per bitness; covered by encoding tests.
|
||||
- **Blittable vs marshalled ambiguity** in `MarshalCache` → Explicit flags and tests per type category; document that `[StructLayout]` is required for non-blittable remote structs.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
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.
|
||||
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.
|
||||
6. **Optional Iced backend** — `IcedAssembler`, arbitrary-asm inject, full prologue validation.
|
||||
|
||||
**Rollback**: WhiteMagic is a separate assembly; removing its project reference reverts consumers with no effect on existing libraries.
|
||||
|
||||
## 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.
|
||||
- **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>`.
|
||||
@@ -0,0 +1,39 @@
|
||||
## 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.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Introduce a new library, **WhiteMagic**, as a fresh .NET 8 project (`WhiteMagic/`) with an xUnit test project (`WhiteMagicTest/`). Additive — existing BlackMagic is untouched.
|
||||
- **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.
|
||||
- `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.**
|
||||
- **DLL injection**: `CreateRemoteThread` + thread-hijack strategies, x86 and x64 stubs (ported from current BlackMagic).
|
||||
- **Discovery & allocation**: pattern scanning with cache, PE-header parsing, and named-chunk `AllocatedMemory`.
|
||||
- **High-level ergonomics**: `RemotePointer` indexer, `Module["fn"].Execute(...)`, `ManagedPeb`/`ManagedTeb`, window mutation, keyboard/mouse simulation, async execution wrappers, and helper utilities.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `memory-access`: Dual external/in-process readers over an abstract `MemoryBase`, with `MarshalCache`-backed typed, array, and string read/write and relative/absolute addressing.
|
||||
- `memory-discovery`: Pattern/signature scanning (with cache), PE-header parsing, and named-chunk remote allocation.
|
||||
- `managed-assembler`: `IAssembler` seam producing machine code with no native dependency — hand-emitted convention stubs plus an optional Iced backend for arbitrary assembly.
|
||||
- `remote-execution`: Three-tier execution (remote-thread, crash-safe main-thread pump, in-process delegate) with calling-convention-aware `Execute<T>` and parameter marshalling.
|
||||
- `function-hooking`: Reversible inline detours (`CallOriginal`) and named byte patches with lifecycle management and auto-restore.
|
||||
- `dll-injection`: DLL injection via `CreateRemoteThread` and thread-hijack redirection for x86 and x64 targets.
|
||||
- `high-level-api`: Ergonomic surface — `RemotePointer` indexer, module/function access, PEB/TEB, window and input simulation, async wrappers, and helpers.
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- None — WhiteMagic is a new library; no existing WhiteMagic specs exist in openspec/specs/. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **New code**: `WhiteMagic/` library, `WhiteMagicTest/` xUnit project, both added to the solution.
|
||||
- **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.
|
||||
@@ -0,0 +1,53 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: DLL injection via remote thread
|
||||
|
||||
WhiteMagic SHALL inject a DLL into an open target process by creating a remote thread on `LoadLibrary`, returning the base address of the injected module on success and reporting failure without throwing for expected failure conditions.
|
||||
|
||||
#### Scenario: successful injection
|
||||
- **WHEN** a valid DLL path is injected into an open process of matching bitness
|
||||
- **THEN** the returned base address MUST be non-zero and the module MUST be loaded in the target
|
||||
|
||||
#### Scenario: bitness mismatch rejected
|
||||
- **WHEN** the target process bitness differs from the caller
|
||||
- **THEN** injection MUST fail with a clear error rather than corrupt the target
|
||||
|
||||
#### Scenario: missing file
|
||||
- **WHEN** the DLL path does not exist
|
||||
- **THEN** injection MUST report an argument error
|
||||
|
||||
### Requirement: DLL injection via thread hijack
|
||||
|
||||
WhiteMagic SHALL inject a DLL by hijacking an existing thread — saving its context, redirecting execution through a `LoadLibrary` stub, and restoring the original context — returning the injected module base address.
|
||||
|
||||
#### Scenario: hijack loads the module
|
||||
- **WHEN** a valid DLL is injected by hijacking a running thread
|
||||
- **THEN** the module MUST be loaded and the hijacked thread's original context MUST be restored
|
||||
|
||||
#### Scenario: exit code reports load result
|
||||
- **WHEN** the redirect stub completes
|
||||
- **THEN** the stub MUST record `LoadLibrary`'s result so the caller can detect load success or failure
|
||||
|
||||
### Requirement: x86 and x64 stubs
|
||||
|
||||
Injection stubs SHALL be emitted correctly for both x86 and x64 targets, including proper x64 addressing.
|
||||
|
||||
#### Scenario: x86 stub
|
||||
- **WHEN** injecting into a 32-bit target
|
||||
- **THEN** a 32-bit redirect stub MUST be emitted
|
||||
|
||||
#### Scenario: x64 stub
|
||||
- **WHEN** injecting into a 64-bit target
|
||||
- **THEN** a 64-bit redirect stub with correct absolute/RIP-relative addressing MUST be emitted
|
||||
|
||||
### Requirement: Raw code injection
|
||||
|
||||
WhiteMagic SHALL inject raw machine-code bytes into an open process, either at a caller-supplied address or into freshly allocated remote memory whose address is returned.
|
||||
|
||||
#### Scenario: inject at address
|
||||
- **WHEN** raw bytes are injected at a given address
|
||||
- **THEN** memory at that address MUST equal the injected bytes
|
||||
|
||||
#### Scenario: inject into fresh allocation
|
||||
- **WHEN** raw bytes are injected without an address
|
||||
- **THEN** remote memory MUST be allocated, the bytes written, and the allocation address returned
|
||||
@@ -0,0 +1,57 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Reversible inline detours
|
||||
|
||||
WhiteMagic SHALL provide a `DetourManager` that creates named inline detours redirecting a target function to a managed hook, supporting `Apply`, `Remove`, and calling the original function. Detours operate in-process.
|
||||
|
||||
#### Scenario: apply redirects the target
|
||||
- **WHEN** a detour from a target function to a hook delegate is applied
|
||||
- **THEN** calling the target MUST invoke the hook delegate
|
||||
|
||||
#### Scenario: call original
|
||||
- **WHEN** the hook invokes `CallOriginal(args)`
|
||||
- **THEN** the original target behavior MUST execute with those arguments and its result returned
|
||||
|
||||
#### Scenario: remove restores original bytes
|
||||
- **WHEN** an applied detour is removed
|
||||
- **THEN** the target's original prologue bytes MUST be restored and calling the target MUST no longer invoke the hook
|
||||
|
||||
#### Scenario: named lookup
|
||||
- **WHEN** a detour is created with a name
|
||||
- **THEN** it MUST be retrievable from the manager by that name
|
||||
|
||||
### Requirement: Instruction-boundary validation before splicing
|
||||
|
||||
Before overwriting a target prologue, the detour SHALL verify the overwrite covers whole instructions so that no instruction is split.
|
||||
|
||||
#### Scenario: aligned splice permitted
|
||||
- **WHEN** the bytes required for the jump cover a whole number of prologue instructions
|
||||
- **THEN** the detour MUST apply
|
||||
|
||||
#### Scenario: misaligned splice rejected
|
||||
- **WHEN** the required overwrite would end in the middle of an instruction and boundary information is available
|
||||
- **THEN** the detour MUST refuse to apply rather than corrupt the target
|
||||
|
||||
### Requirement: Named reversible byte patches
|
||||
|
||||
WhiteMagic SHALL provide a `PatchManager` that creates named byte patches with `Apply`, `Remove`, and `IsApplied`, usable in both external and in-process modes.
|
||||
|
||||
#### Scenario: apply writes patch bytes
|
||||
- **WHEN** a patch is applied at an address
|
||||
- **THEN** memory at that address MUST equal the patch bytes
|
||||
|
||||
#### Scenario: remove restores original
|
||||
- **WHEN** an applied patch is removed
|
||||
- **THEN** memory at that address MUST equal the original bytes captured at creation
|
||||
|
||||
#### Scenario: is-applied reflects state
|
||||
- **WHEN** `IsApplied` is queried
|
||||
- **THEN** it MUST return true only when the current bytes equal the patch bytes
|
||||
|
||||
### Requirement: Auto-restore on dispose
|
||||
|
||||
All live detours and patches SHALL be reverted when their owning `MemoryBase` is disposed.
|
||||
|
||||
#### Scenario: dispose reverts modifications
|
||||
- **WHEN** a `MemoryBase` with active detours and patches is disposed
|
||||
- **THEN** every modified region MUST be restored to its pre-modification bytes
|
||||
@@ -0,0 +1,69 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Remote pointer indexer
|
||||
|
||||
WhiteMagic SHALL expose a `RemotePointer` obtained by indexing the memory facade with an address, offering read/write/execute operations relative to that base address.
|
||||
|
||||
#### Scenario: read via indexer
|
||||
- **WHEN** `sharp[addr].Read<int>(offset)` is called
|
||||
- **THEN** it MUST read an int at `addr + offset`
|
||||
|
||||
#### Scenario: write via indexer
|
||||
- **WHEN** `sharp[addr].WriteString("text")` is called
|
||||
- **THEN** the string MUST be written starting at `addr`
|
||||
|
||||
### Requirement: Module and function access
|
||||
|
||||
WhiteMagic SHALL expose modules and their exported functions by name, allowing a resolved function to be executed with a calling convention and arguments.
|
||||
|
||||
#### Scenario: resolve function by name
|
||||
- **WHEN** `sharp["user32"]["MessageBoxA"]` is resolved
|
||||
- **THEN** it MUST return a function bound to the export address of `MessageBoxA` in `user32`
|
||||
|
||||
#### Scenario: execute resolved function
|
||||
- **WHEN** a resolved function is executed with a calling convention and arguments
|
||||
- **THEN** it MUST invoke the target through the chosen execution strategy with those arguments
|
||||
|
||||
### Requirement: PEB and TEB access
|
||||
|
||||
WhiteMagic SHALL expose managed reads of the target's Process Environment Block and a thread's Thread Environment Block.
|
||||
|
||||
#### Scenario: read PEB field
|
||||
- **WHEN** a PEB field (e.g. being-debugged flag) is read
|
||||
- **THEN** it MUST reflect the target's actual PEB value
|
||||
|
||||
#### Scenario: read TEB field
|
||||
- **WHEN** a TEB field is read for a given thread
|
||||
- **THEN** it MUST reflect that thread's actual TEB value
|
||||
|
||||
### Requirement: Window mutation
|
||||
|
||||
WhiteMagic SHALL enumerate and mutate target windows — position, size, title, activation, and flashing.
|
||||
|
||||
#### Scenario: move and resize
|
||||
- **WHEN** a window's X, Y, width, and height are set
|
||||
- **THEN** the window MUST move and resize to those values
|
||||
|
||||
#### Scenario: query by class name
|
||||
- **WHEN** windows are queried by class name
|
||||
- **THEN** matching windows MUST be returned
|
||||
|
||||
### Requirement: Keyboard and mouse simulation
|
||||
|
||||
WhiteMagic SHALL simulate keyboard and mouse input to a target window, including input delivered without activating the window where the mechanism allows.
|
||||
|
||||
#### Scenario: write text to a window
|
||||
- **WHEN** text is written to a target window's keyboard interface
|
||||
- **THEN** the window MUST receive the corresponding key input
|
||||
|
||||
#### Scenario: mouse click
|
||||
- **WHEN** a click at a coordinate is issued to a window's mouse interface
|
||||
- **THEN** the window MUST receive the corresponding mouse input
|
||||
|
||||
### Requirement: Asynchronous execution wrappers
|
||||
|
||||
WhiteMagic SHALL provide `Task`-based asynchronous wrappers over its execution strategies.
|
||||
|
||||
#### Scenario: async execute returns a task
|
||||
- **WHEN** an async execute is invoked
|
||||
- **THEN** it MUST return a `Task<T>` that completes with the execution result
|
||||
@@ -0,0 +1,49 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: IAssembler abstraction with no native dependency
|
||||
|
||||
WhiteMagic SHALL define an `IAssembler` seam that produces machine code, with a default backend that has no native or third-party dependency. FASM MUST NOT be referenced by the default configuration.
|
||||
|
||||
#### Scenario: default backend is dependency-free
|
||||
- **WHEN** WhiteMagic is built in its default configuration
|
||||
- **THEN** no reference to FASM or `ManagedFasm` MUST be present in the output
|
||||
|
||||
#### Scenario: backend is replaceable
|
||||
- **WHEN** an alternate `IAssembler` implementation is supplied
|
||||
- **THEN** execution and injection MUST use it without other code changes
|
||||
|
||||
### Requirement: Hand-emitted calling-convention stubs
|
||||
|
||||
The default `StubAssembler` SHALL emit call trampolines for the cdecl, stdcall, thiscall, and fastcall conventions — pushing/placing arguments, calling the target, cleaning the stack per convention, and returning — for both x86 and x64 targets.
|
||||
|
||||
#### Scenario: cdecl stub encoding
|
||||
- **WHEN** a cdecl call stub for a function with N 4-byte arguments is emitted (x86)
|
||||
- **THEN** the bytes MUST push the arguments in reverse order, `call` the target, `add esp, N*4`, and `ret`
|
||||
|
||||
#### Scenario: stdcall omits caller cleanup
|
||||
- **WHEN** a stdcall stub is emitted
|
||||
- **THEN** it MUST NOT emit a caller-side stack cleanup (the callee cleans)
|
||||
|
||||
#### Scenario: x64 uses register argument order
|
||||
- **WHEN** an x64 call stub is emitted
|
||||
- **THEN** the first integer arguments MUST be placed in the platform argument registers before the call
|
||||
|
||||
### Requirement: Byte emitter primitives
|
||||
|
||||
`StubAssembler` SHALL provide little-endian emit primitives (`EmitU8`, `EmitU32`, `EmitU64`) used to hand-assemble stubs deterministically.
|
||||
|
||||
#### Scenario: little-endian 32-bit emit
|
||||
- **WHEN** `EmitU32(0x11223344)` is called
|
||||
- **THEN** the appended bytes MUST be `[0x44, 0x33, 0x22, 0x11]`
|
||||
|
||||
### Requirement: Optional Iced backend for arbitrary assembly
|
||||
|
||||
WhiteMagic SHALL provide an optional `IcedAssembler` backend that assembles arbitrary x86/x64 mnemonic text to machine code for callers who require runtime text assembly.
|
||||
|
||||
#### Scenario: arbitrary mnemonics assembled
|
||||
- **WHEN** the Iced backend assembles `"push 0\nadd esp, 4\nret"` at a given origin
|
||||
- **THEN** it MUST return the corresponding machine code bytes
|
||||
|
||||
#### Scenario: origin-relative encoding
|
||||
- **WHEN** assembly containing a relative jump is assembled at a specified origin address
|
||||
- **THEN** the encoded relative offsets MUST be correct for that origin
|
||||
@@ -0,0 +1,57 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Abstract memory base with two readers
|
||||
|
||||
WhiteMagic SHALL expose an abstract `MemoryBase` type defining `ReadBytes`, `WriteBytes`, generic `Read<T>`/`Write<T>`, array read/write, and string read/write, with two concrete implementations: `ExternalReader` (out-of-process via ReadProcessMemory/WriteProcessMemory) and `InProcessReader` (in-process via direct pointer dereference).
|
||||
|
||||
#### Scenario: external read round-trip
|
||||
- **WHEN** an `ExternalReader` opens a target process and writes a value with `Write<int>(addr, 0x1234)` then reads it back with `Read<int>(addr)`
|
||||
- **THEN** the returned value MUST equal `0x1234`
|
||||
|
||||
#### Scenario: in-process read of own memory
|
||||
- **WHEN** an `InProcessReader` reads a known address in its own process
|
||||
- **THEN** the value MUST match a direct managed read of the same address
|
||||
|
||||
#### Scenario: shared API surface
|
||||
- **WHEN** code is written against the `MemoryBase` abstract type
|
||||
- **THEN** it MUST operate unchanged against both `ExternalReader` and `InProcessReader`
|
||||
|
||||
### Requirement: Typed read/write via marshal cache
|
||||
|
||||
`MemoryBase` SHALL support generic `Read<T>`/`Write<T>` for blittable and marshalled struct types, using a per-type `MarshalCache<T>` that caches size, type code, and marshalling requirements to avoid per-call reflection.
|
||||
|
||||
#### Scenario: blittable struct round-trip
|
||||
- **WHEN** a blittable `[StructLayout(LayoutKind.Sequential)]` struct is written and read back
|
||||
- **THEN** all fields MUST be preserved exactly
|
||||
|
||||
#### Scenario: marshal cache computed once
|
||||
- **WHEN** `Read<T>` is invoked repeatedly for the same type `T`
|
||||
- **THEN** `Marshal.SizeOf` and type inspection for `T` MUST be computed at most once and reused
|
||||
|
||||
#### Scenario: array read
|
||||
- **WHEN** `Read<T>(addr, count)` is called
|
||||
- **THEN** it MUST return an array of exactly `count` elements read contiguously from `addr`
|
||||
|
||||
### Requirement: String read and write with encoding
|
||||
|
||||
`MemoryBase` SHALL read and write strings with a caller-specified `Encoding` and a maximum length, terminating reads at a null terminator or the maximum length.
|
||||
|
||||
#### Scenario: ASCII write then read
|
||||
- **WHEN** `WriteString(addr, "hello", Encoding.ASCII)` is called then `ReadString(addr, Encoding.ASCII)`
|
||||
- **THEN** the result MUST equal `"hello"`
|
||||
|
||||
#### Scenario: read stops at null terminator
|
||||
- **WHEN** a null-terminated string shorter than `maxLength` is read
|
||||
- **THEN** the returned string MUST exclude the terminator and everything after it
|
||||
|
||||
### Requirement: Relative and absolute addressing
|
||||
|
||||
`MemoryBase` SHALL convert between addresses relative to the module image base and absolute addresses via `GetAbsolute` and `GetRelative`, and accept an `isRelative` flag on read/write operations.
|
||||
|
||||
#### Scenario: relative resolves against image base
|
||||
- **WHEN** `GetAbsolute(relative)` is called with the process image base known
|
||||
- **THEN** the result MUST equal `imageBase + relative`
|
||||
|
||||
#### Scenario: read with isRelative
|
||||
- **WHEN** `Read<int>(offset, isRelative: true)` is called
|
||||
- **THEN** the read MUST occur at `GetAbsolute(offset)`
|
||||
@@ -0,0 +1,57 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Pattern scanning with mask
|
||||
|
||||
WhiteMagic SHALL scan process memory for a byte signature with a wildcard mask, returning the address of the first match or `IntPtr.Zero` when no match is found. Scans SHALL be available over an explicit range, a single module, and all modules.
|
||||
|
||||
#### Scenario: pattern found
|
||||
- **WHEN** a known byte sequence is scanned for with a matching mask over a range containing it
|
||||
- **THEN** the returned address MUST point at the first occurrence
|
||||
|
||||
#### Scenario: wildcard mask
|
||||
- **WHEN** the mask marks positions as wildcards (e.g. `"xx?x"`)
|
||||
- **THEN** those byte positions MUST be ignored during matching
|
||||
|
||||
#### Scenario: pattern not found
|
||||
- **WHEN** a pattern absent from the range is scanned for
|
||||
- **THEN** the result MUST be `IntPtr.Zero`
|
||||
|
||||
### Requirement: Pattern scan cache
|
||||
|
||||
The scanner SHALL cache resolved pattern results keyed by pattern and mask, returning the cached address on repeat lookups, and SHALL expose an operation to clear the cache.
|
||||
|
||||
#### Scenario: repeat lookup served from cache
|
||||
- **WHEN** the same pattern and mask are scanned twice without clearing the cache
|
||||
- **THEN** the second lookup MUST return the same address without rescanning memory
|
||||
|
||||
#### Scenario: cache cleared
|
||||
- **WHEN** the cache is cleared
|
||||
- **THEN** the next lookup MUST rescan memory
|
||||
|
||||
### Requirement: PE header parsing
|
||||
|
||||
WhiteMagic SHALL parse the PE headers of a module to expose its sections and entry point without executing the module.
|
||||
|
||||
#### Scenario: sections enumerated
|
||||
- **WHEN** a valid PE module is parsed
|
||||
- **THEN** its section names, virtual addresses, and sizes MUST be enumerable
|
||||
|
||||
#### Scenario: entry point located
|
||||
- **WHEN** a valid PE module is parsed
|
||||
- **THEN** the parsed entry-point RVA MUST match the module's header
|
||||
|
||||
### Requirement: Named remote allocation
|
||||
|
||||
WhiteMagic SHALL allocate a chunk of remote memory subdivided into named regions, allowing typed read/write and address lookup by name, and freeing the whole chunk on dispose.
|
||||
|
||||
#### Scenario: write and read by name
|
||||
- **WHEN** a named region is allocated and `Write<int>("count", 5)` then `Read<int>("count")` is called
|
||||
- **THEN** the result MUST equal `5`
|
||||
|
||||
#### Scenario: address by name
|
||||
- **WHEN** a region named `"buffer"` is allocated
|
||||
- **THEN** requesting its address MUST return `chunkBase + regionOffset`
|
||||
|
||||
#### Scenario: freed on dispose
|
||||
- **WHEN** the allocation is disposed
|
||||
- **THEN** the underlying remote memory MUST be released
|
||||
@@ -0,0 +1,57 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Three-tier execution model
|
||||
|
||||
WhiteMagic SHALL provide three execution strategies selected by payload safety: `RemoteThreadExecutor` (via `CreateRemoteThread`), `MainThreadPump` (work marshalled onto the target's own thread), and `InProcessInvoker` (direct native-delegate calls when injected in-process).
|
||||
|
||||
#### Scenario: strategies are distinct and selectable
|
||||
- **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
|
||||
- **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`
|
||||
|
||||
### Requirement: Remote-thread execution for thread-agnostic payloads
|
||||
|
||||
`RemoteThreadExecutor` SHALL create a remote thread at a target address using a calling-convention-aware stub, wait for completion, and return the typed exit value. Its documentation MUST state that it is safe only for thread-agnostic payloads.
|
||||
|
||||
#### Scenario: execute with parameters and convention
|
||||
- **WHEN** `Execute<int>(addr, CallingConvention.Cdecl, arg1, arg2)` is called on a safe self-contained function
|
||||
- **THEN** the target MUST be called with the arguments laid out per cdecl and the typed return value returned
|
||||
|
||||
#### Scenario: parameters marshalled and freed
|
||||
- **WHEN** a `string` or struct parameter is passed to `Execute`
|
||||
- **THEN** it MUST be allocated in the remote process, passed by pointer, and freed after the call completes
|
||||
|
||||
#### Scenario: no process open
|
||||
- **WHEN** `Execute` is called with no process open
|
||||
- **THEN** it MUST fail deterministically rather than crash
|
||||
|
||||
### Requirement: Crash-safe main-thread pump
|
||||
|
||||
`MainThreadPump` SHALL install a hook on a per-frame function in the target and, each time that function runs, drain a thread-safe queue of work items, executing each on the target's own thread and returning its result or exception to the requesting caller.
|
||||
|
||||
#### Scenario: work runs on the hooked thread
|
||||
- **WHEN** a work item is queued and the hooked per-frame function next executes
|
||||
- **THEN** the work item MUST run in the context of the thread that calls the per-frame function
|
||||
|
||||
#### Scenario: result returned to caller
|
||||
- **WHEN** a caller queues a function returning a value and awaits its completion
|
||||
- **THEN** the caller MUST receive the returned value
|
||||
|
||||
#### Scenario: exception propagated, pump survives
|
||||
- **WHEN** a queued work item throws
|
||||
- **THEN** the exception MUST be surfaced to the requesting caller AND subsequent queued items MUST still be processed
|
||||
|
||||
#### Scenario: uninstall restores the frame function
|
||||
- **WHEN** the pump is disposed
|
||||
- **THEN** the hooked per-frame function MUST be restored to its original bytes
|
||||
|
||||
### Requirement: In-process delegate invocation
|
||||
|
||||
`InProcessInvoker` SHALL convert a function address to a typed managed delegate and call it directly, without creating a thread or crossing a thread boundary.
|
||||
|
||||
#### Scenario: call as delegate
|
||||
- **WHEN** `CreateFunction<TDelegate>(addr)` is called in-process and the returned delegate is invoked
|
||||
- **THEN** the native function at `addr` MUST be called directly on the current thread with the delegate's marshalled arguments
|
||||
@@ -0,0 +1,86 @@
|
||||
## 1. Project Setup
|
||||
|
||||
- [ ] 1.1 Create `WhiteMagic/WhiteMagic.csproj` targeting `net8.0-windows`, `AllowUnsafeBlocks=true`, nullable enabled
|
||||
- [ ] 1.2 Create `WhiteMagicTest/WhiteMagicTest.csproj` (xUnit, `net8.0-windows`) referencing `WhiteMagic`
|
||||
- [ ] 1.3 No solution file exists in the repo root. Create one (`dotnet new sln -n WhiteMagic`) and add both projects, OR skip the solution and build csproj directly (decide before 1.5). Do NOT reference `BlackMagic.slnx` — it does not exist.
|
||||
- [ ] 1.4 Add `WhiteMagic/Native/` P/Invoke surface (`LibraryImport`): OpenProcess, Read/WriteProcessMemory, VirtualAllocEx/FreeEx/ProtectEx, CreateRemoteThread, Wow64Get/SetThreadContext, Get/SetThreadContext, LoadLibrary, GetProcAddress; add `SafeMemoryHandle`
|
||||
- [ ] 1.5 Verify empty projects build: `dotnet build WhiteMagic.sln` (or `dotnet build WhiteMagic/WhiteMagic.csproj` if no solution) — zero errors
|
||||
|
||||
## 2. Core Memory Access (spec: memory-access)
|
||||
|
||||
- [ ] 2.1 Add tests for `MarshalCache<T>`: blittable size, marshal-required flag, IsIntPtr, computed-once behavior
|
||||
- [ ] 2.2 Implement `WhiteMagic/MarshalCache.cs` to pass 2.1
|
||||
- [ ] 2.3 Add tests for `MemoryBase` abstract contract + `ExternalReader` round-trip (`Read<T>`/`Write<T>`, arrays) using the current process as target
|
||||
- [ ] 2.4 Implement `WhiteMagic/MemoryBase.cs` (abstract) and `WhiteMagic/ExternalReader.cs` to pass 2.3
|
||||
- [ ] 2.5 Add tests for string read/write with encoding, null-terminator stop, and max length
|
||||
- [ ] 2.6 Implement `ReadString`/`WriteString` on `MemoryBase` to pass 2.5
|
||||
- [ ] 2.7 Add tests for relative/absolute addressing (`GetAbsolute`/`GetRelative`, `isRelative` flag)
|
||||
- [ ] 2.8 Implement addressing helpers to pass 2.7
|
||||
- [ ] 2.9 Add tests + `unsafe` implementation for `InProcessReader` (direct deref against own process); verify shared `MemoryBase` API works for both readers
|
||||
|
||||
## 3. Managed Assembler (spec: managed-assembler)
|
||||
|
||||
- [ ] 3.1 Add tests for `EmitU8`/`EmitU32`/`EmitU64` little-endian primitives
|
||||
- [ ] 3.2 Implement `WhiteMagic/Assembly/StubAssembler.cs` emitters + `IAssembler` interface to pass 3.1
|
||||
- [ ] 3.3 Add tests for x86 cdecl stub encoding (reverse push, call, `add esp, N*4`, ret) with known byte expectations
|
||||
- [ ] 3.4 Implement x86 cdecl stub to pass 3.3
|
||||
- [ ] 3.5 Add tests for stdcall (no caller cleanup), thiscall (ecx = this), fastcall (ecx/edx) x86 stubs
|
||||
- [ ] 3.6 Implement x86 stdcall/thiscall/fastcall stubs to pass 3.5
|
||||
- [ ] 3.7 Add tests for x64 stub argument-register placement and call
|
||||
- [ ] 3.8 Implement x64 stub to pass 3.7
|
||||
- [ ] 3.9 Confirm no FASM/`ManagedFasm` reference exists in `WhiteMagic` output (assert via a test that scans loaded references)
|
||||
|
||||
## 4. Crash-Safe Execution Slice (spec: remote-execution, function-hooking)
|
||||
|
||||
- [ ] 4.1 Add tests for `PatchManager`/`Patch`: apply writes bytes, remove restores original, `IsApplied` reflects state
|
||||
- [ ] 4.2 Implement `WhiteMagic/Hooking/PatchManager.cs` + `Patch.cs` to pass 4.1
|
||||
- [ ] 4.3 Add tests for `DetourManager`/`Detour` in-process: apply redirects, `CallOriginal`, remove restores, named lookup
|
||||
- [ ] 4.4 Implement `WhiteMagic/Hooking/DetourManager.cs` + `Detour.cs` (inline jmp, x86/x64 form) to pass 4.3
|
||||
- [ ] 4.5 Add tests for instruction-boundary validation (aligned splice permitted, misaligned rejected when boundary info available)
|
||||
- [ ] 4.6 Implement minimal prologue length-decoder in `Detour.Apply` to pass 4.5. Default `StubAssembler` covers ONLY the common x86/x64 prologue shapes — enumerate the covered opcodes in code + XML doc (e.g. `push reg` 0x50-0x57, `mov edi,edi` 8B FF, `push ebp`/`mov ebp,esp` 55 8B EC, `sub esp,imm` 83 EC / 81 EC, REX-prefixed forms). On any opcode outside the set, refuse the splice (do not guess). Full arbitrary-prologue validation is gated on the optional Iced backend (task 8.3) — document that slices 2-5 ship partial boundary safety.
|
||||
- [ ] 4.7 Add tests for auto-restore: disposing a `MemoryBase` reverts all active patches and detours
|
||||
- [ ] 4.8 Wire manager registration + `MemoryBase.Dispose` restore to pass 4.7
|
||||
- [ ] 4.9 Add tests for `MainThreadPump` queue semantics: item runs on hooked thread, result returned, throwing item surfaces exception and pump survives, dispose uninstalls hook (use a self-hosted frame-loop harness in-process)
|
||||
- [ ] 4.10 Implement `WhiteMagic/Execution/MainThreadPump.cs` (frame-function detour + thread-safe work queue + completion handles) to pass 4.9
|
||||
- [ ] 4.11 Add tests for `RemoteThreadExecutor.Execute<T>` (convention stub + wait + typed exit; no-process failure is deterministic)
|
||||
- [ ] 4.12 Implement `WhiteMagic/Execution/RemoteThreadExecutor.cs` and parameter marshalling (string/struct → remote alloc → free) to pass 4.11
|
||||
|
||||
## 5. Injection & Discovery (spec: dll-injection, memory-discovery)
|
||||
|
||||
- [ ] 5.1 Add tests for pattern scanning: found (range/module/all-modules), wildcard mask, not-found returns Zero
|
||||
- [ ] 5.2 Implement `WhiteMagic/Discovery/PatternScanner.cs` to pass 5.1
|
||||
- [ ] 5.3 Add tests + implement scan result cache (repeat served from cache, clear rescans)
|
||||
- [ ] 5.4 Add tests + implement `WhiteMagic/Discovery/PeHeaderParser.cs` (sections, entry point)
|
||||
- [ ] 5.5 Add tests + implement `WhiteMagic/Memory/AllocatedMemory.cs` (named regions, typed read/write by name, address by name, free on dispose)
|
||||
- [ ] 5.6 Add tests + implement raw code injection (`InjectCode` at address and into fresh allocation)
|
||||
- [ ] 5.7 Add tests + implement DLL injection via remote thread (LoadLibrary), including bitness-mismatch and missing-file failures
|
||||
- [ ] 5.8 Add tests + implement DLL injection via thread-hijack (save/redirect/restore context) with x86 and x64 stubs
|
||||
|
||||
## 6. In-Process Tier (spec: remote-execution)
|
||||
|
||||
- [ ] 6.1 Add tests for `InProcessInvoker.CreateFunction<TDelegate>` calling a known in-process function directly
|
||||
- [ ] 6.2 Implement `WhiteMagic/Execution/InProcessInvoker.cs` (`Marshal.GetDelegateForFunctionPointer`) + vtable-entry helper to pass 6.1
|
||||
- [ ] 6.3 Document that the CLR-host managed loader (injecting `InProcessReader` into a foreign process) is a separate follow-up change
|
||||
|
||||
## 7. High-Level Ergonomics (spec: high-level-api)
|
||||
|
||||
- [ ] 7.1 Add tests + implement `RemotePointer` indexer (`sharp[addr].Read/Write/Execute` relative to base)
|
||||
- [ ] 7.2 Add tests + implement `RemoteModule`/`RemoteFunction` (`sharp["mod"]["fn"]`) resolving export addresses and executing via a chosen strategy
|
||||
- [ ] 7.3 Add tests + implement `ManagedPeb`/`ManagedTeb` field reads
|
||||
- [ ] 7.4 Add tests + implement `WindowFactory`/`RemoteWindow` (enumerate, move/resize/title/activate/flash, query by class)
|
||||
- [ ] 7.5 Add tests + implement keyboard/mouse simulation (PostMessage + SendInput) to a target window
|
||||
- [ ] 7.6 Add tests + implement `Task`-based async execution wrappers over the executors and pump
|
||||
- [ ] 7.7 Add minimal facade (`WhiteMagic` entry type) exposing `Open`, readers, executors, managers, and the indexer
|
||||
|
||||
## 8. Optional Iced Backend (spec: managed-assembler)
|
||||
|
||||
- [ ] 8.1 Add `Iced` package reference behind an `IcedAssembler : IAssembler` in a way that keeps the default `StubAssembler` dependency-free
|
||||
- [ ] 8.2 Add tests + implement `IcedAssembler.Assemble(text, origin)` for arbitrary mnemonics and origin-relative encoding
|
||||
- [ ] 8.3 Add tests + wire full prologue instruction-boundary validation (D5) using the Iced disassembler when present
|
||||
|
||||
## 9. Verification
|
||||
|
||||
- [ ] 9.1 Run full test suite: `dotnet test WhiteMagicTest/WhiteMagicTest.csproj` — all pass
|
||||
- [ ] 9.2 Run full build (`dotnet build WhiteMagic.sln` or the WhiteMagic csproj, per 1.3) — zero errors, zero new warnings in `WhiteMagic`
|
||||
- [ ] 9.3 Confirm existing BlackMagic/its tests are unchanged and still green
|
||||
- [ ] 9.4 Update `docs/memory-library-comparison.md` "WhiteMagic — synthesis" section with any deviations discovered during implementation
|
||||
@@ -0,0 +1 @@
|
||||
schema: spec-driven
|
||||
Reference in New Issue
Block a user