75 lines
4.0 KiB
Markdown
75 lines
4.0 KiB
Markdown
## 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.
|