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,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
|
||||
Reference in New Issue
Block a user