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:
kbe
2026-07-21 16:50:03 +02:00
co-authored by Claude Opus 4.8
commit 4405af15fd
44 changed files with 4495 additions and 0 deletions
@@ -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