Initial commit
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 code payloads from `pushad`, `mov eax, 0x1234`, etc. instead of raw bytes.
|
||||
|
||||
Existing code:
|
||||
- `BMThread.cs` has blocking `Execute(addr, param)` → waits 10s, returns exit code.
|
||||
- `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 common payload subset: mov, push, pop, call, jmp, ret, nop, pushad/popad, test, je/jne, inc, add, sub, xor, and, or, cmp, lea, nop, hlt).
|
||||
- x64 text assembly (keep x86 only; x64 stubs remain hand-assembled `byte[]`).
|
||||
- Reimplementing FASM's directive system (`org`, `use32`, macros).
|
||||
- Native DLL dependency.
|
||||
|
||||
## 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 payload is built fresh.
|
||||
- Put in `Injection/` → rejected: assembler is generic, not injection-specific.
|
||||
|
||||
### D2: Two-pass assembler (labels + bytes)
|
||||
|
||||
Pass 1: scan instructions, record label positions, emit bytes (reserving 4 bytes for near jumps). Pass 2: resolve label offsets, patch jump targets.
|
||||
|
||||
This handles forward references (`jmp @skip` before `@skip:` label is defined) without multiple iterations. `SetPassLimit()` caps the loop for safety but 2 passes is sufficient for all payload patterns.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Single-pass → rejected: can't resolve forward jumps.
|
||||
- FASM-style multi-pass → overkill: payloads don'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). Payloads rarely exceed this, but document the limit.
|
||||
- **No runtime validation of payloads**: the assembler produces bytes; it doesn't verify the result is safe to execute. This matches FASM's behavior — the assembler doesn't validate semantics.
|
||||
@@ -0,0 +1,23 @@
|
||||
## Why
|
||||
|
||||
BlackMagic replaced FASM for code-payload generation but lost two useful capabilities:
|
||||
|
||||
1. **Non-blocking remote execution** (`InjectAndExecuteEx`): FASM's managed wrapper returned a thread handle without waiting. BlackMagic only has blocking `Execute()`. For DLL injection, a non-blocking variant avoids hanging when the target is slow to load.
|
||||
|
||||
2. **Text-based assembly**: FASM allowed building code payloads from assembly text (`AddLine("pushad")`). BlackMagic requires hand-assembled `byte[]`. For prototyping, debugging, and one-off code payloads, text assembly is faster to write and easier to review. A managed assembler eliminates the native FASM DLL dependency while keeping the ergonomic benefit.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `InjectAndExecuteEx()` to `BlackMagic` and `BMThread`: inject code then create a remote thread without waiting, returning the thread handle.
|
||||
- Add `AsmBuilder` class: pure C# x86 text assembler that converts instruction text to `byte[]` machine code. Supports common payload instructions (mov, push, pop, call, jmp, ret, nop, pushad/popad, test, je, jne, inc, add, sub, xor, etc.).
|
||||
- Add `InjectAndExecute(string asm)` and `InjectAndExecuteEx(string asm)` overloads that accept assembly text, assemble via `AsmBuilder`, then inject+execute.
|
||||
- Add `SetPassLimit()` to `AsmBuilder` for label resolution iteration control.
|
||||
|
||||
## 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
|
||||
Reference in New Issue
Block a user