Release v1.1.0: Process Injection & Architecture Hardening

- Implement complete cross-process injection engine via snd_inj_ctx_t
- Track explicit remote_entry_point for safe thread hijacking and PE execution
- Refactor standalone syscall resolvers into a unified pipeline (scan/sort)
- Perfect status code system by purging generic fallbacks in favor of highly-specific, domain-aware error codes
- Fix minor pointer validation and parsing bugs in the reflective loader
This commit is contained in:
sibouzitoun
2026-06-28 14:14:54 +01:00
parent afffc17dfe
commit aea2eb6cfb
180 changed files with 8387 additions and 3659 deletions
+50 -2
View File
@@ -1,6 +1,54 @@
# Injection Domain
This directory is reserved for future capabilities focused on remote process targeting, thread hijacking, and inter-process payload injection.
Remote process injection: open target, allocate remote memory, write payload, set protections, create a remote thread. Cross-process operations route through an injected `snd_process_api_t` table.
## Shared context vs loader contexts
Unlike loaders, **all injection techniques share** `snd_inj_ctx_t` (`sindri/injection/context.h`). Stage machine, handles, remote fields, and `proc_api` are identical across classic and future techniques.
Each technique adds engine functions and chains under a subdirectory but mutates the same context:
```
include/sindri/injection/
├── context.h ← shared across ALL techniques
└── classic/
├── engine.h ← per-stage classic engine
└── chain.h ← snd_inj_classic_shell, snd_inj_classic_pe
```
Loader contexts are **per-technique** (`snd_ldr_pe_ctx_t` today).
## Header map
| Header | Role |
|---|---|
| `sindri/injection.h` | Umbrella include |
| `sindri/injection/context.h` | `snd_inj_ctx_t`, stages, `snd_inj_cleanup` |
| `sindri/injection/classic.h` | Classic technique umbrella |
| `sindri/injection/classic/engine.h` | Per-stage engine functions |
| `sindri/injection/classic/chain.h` | `snd_inj_classic_shell`, `snd_inj_classic_pe` |
## Implemented techniques
| Technique | Chain | Payload |
|---|---|---|
| Classic shellcode | `snd_inj_classic_shell` | Raw buffer in `inj_ctx.payload` |
| Classic PE | `snd_inj_classic_pe` | Local bake + remote execute (requires `snd_ldr_pe_ctx_t`) |
## PoCs
| PoC | Chain | Profile |
|---|---|---|
| `pocs/inject_shell/main.c` | `snd_inj_classic_shell` | KnownDlls bootstrap + `snd_proc_win` |
| `pocs/inject_pe/main.c` | `snd_inj_classic_pe` | `snd_mem_sys`, `snd_mod_nt`, `snd_proc_sys` |
## Table of Contents
- *No documentation currently exists. Check back later.*
- [techniques.md](techniques.md) — classic pipelines, stage machine, loader interop
- [api_reference.md](api_reference.md) — context, engine, and chain API
## Related documentation
- [Loaders domain](../loaders/README.md) — local PE bake for classic PE path
- [Process primitives](../primitives/process/README.md)
- [Examples](../../examples/README.md)
+163 -4
View File
@@ -1,6 +1,165 @@
# Injection API Reference
# Injection: API Reference
> [!NOTE]
> The injection domain is planned for a future release of SindriKit. This page will be populated when the injection context (`snd_injector_ctx_t`) and its associated primitives are implemented.
Public injection API under `include/sindri/injection/`. Include `sindri/injection.h` or `sindri.h` for the full surface.
See [techniques.md](techniques.md) for a high-level overview of planned capabilities.
---
## Shared Context (`sindri/injection/context.h`)
All injection techniques operate on `snd_inj_ctx_t`. Defined in `context.h`, not in technique-specific headers.
### `snd_inj_ctx_t`
| Field | Type | Description |
|---|---|---|
| `target_pid` | `DWORD` | PID of the target process |
| `target_process` | `HANDLE` | Handle opened via `proc_api->open_process` |
| `remote_base` | `PVOID` | Remote allocation base |
| `remote_entry_point` | `PVOID` | Optional. If set, thread starts here instead of `remote_base` (PE path) |
| `remote_size` | `SIZE_T` | Size of the remote allocation |
| `remote_thread` | `HANDLE` | Handle from `proc_api->create_remote_thread` |
| `stage` | `snd_inj_stage_t` | Current pipeline stage |
| `payload` | `const snd_buffer_t *` | Source buffer (shellcode or locally baked PE image) |
| `proc_api` | `const snd_process_api_t *` | Injected remote process API |
**Required before any chain:** `target_pid`, `proc_api`, and (for alloc/write) a valid `payload`.
---
### `snd_inj_stage_t`
| Constant | Value | Description |
|---|---|---|
| `SND_INJ_STAGE_UNINITIALIZED` | 0 | Not started |
| `SND_INJ_STAGE_TARGET_ACQUIRED` | 1 | Process handle obtained |
| `SND_INJ_STAGE_MEMORY_ALLOCATED` | 2 | Remote memory allocated (RW) |
| `SND_INJ_STAGE_PAYLOAD_WRITTEN` | 3 | Payload written remotely |
| `SND_INJ_STAGE_PROTECTIONS_SET` | 4 | Remote memory set to RX |
| `SND_INJ_STAGE_EXECUTED` | 5 | Remote thread created |
---
### `snd_inj_stage_to_string`
Returns a human-readable stage name for logging and error context.
```c
const char *snd_inj_stage_to_string(snd_inj_stage_t stage);
```
**Source:** `src/injection/context.c`
---
### `snd_inj_cleanup`
Closes `remote_thread` and `target_process` through `proc_api->close_handle`, clears remote fields, resets stage to `UNINITIALIZED`.
```c
void snd_inj_cleanup(snd_inj_ctx_t *ctx);
```
Does not free loader-local mappings. No-op if `ctx` or `proc_api` is NULL.
---
## Classic Engine (`sindri/injection/classic/engine.h`)
Per-stage functions with strict stage validation. Each advances `ctx->stage` on success.
| Function | Required stage | `proc_api` callback | Advances to |
|---|---|---|---|
| `snd_inj_classic_open_target` | `UNINITIALIZED` | `open_process` | `TARGET_ACQUIRED` |
| `snd_inj_classic_alloc_remote` | `TARGET_ACQUIRED` | `alloc_remote` | `MEMORY_ALLOCATED` |
| `snd_inj_classic_write_payload` | `MEMORY_ALLOCATED` | `write_remote` | `PAYLOAD_WRITTEN` |
| `snd_inj_classic_set_protections` | `PAYLOAD_WRITTEN` | `protect_remote` | `PROTECTIONS_SET` |
| `snd_inj_classic_execute` | `PROTECTIONS_SET` | `create_remote_thread` | `EXECUTED` |
### `snd_inj_classic_open_target`
Opens the target with `PROCESS_ALL_ACCESS` (`0x001FFFFF`). Requires non-zero `target_pid`.
**Returns:** `SND_OK`, `SND_STATUS_PROCESS_OPEN_FAILED`, `SND_STATUS_INVALID_STAGE_SEQUENCE`, `SND_STATUS_INVALID_PARAMETER`
---
### `snd_inj_classic_alloc_remote`
Allocates `payload->size` bytes remotely with `MEM_COMMIT | MEM_RESERVE` and `PAGE_READWRITE`. Sets `remote_size = payload->size`.
**Returns:** `SND_OK`, allocation errors from `proc_api`, `SND_STATUS_INVALID_STAGE_SEQUENCE`
---
### `snd_inj_classic_write_payload`
Writes `payload->data` (`remote_size` bytes) to `remote_base`.
**Returns:** `SND_OK`, `SND_STATUS_VIRTUAL_WRITE_FAILED`, `SND_STATUS_INVALID_STAGE_SEQUENCE`
---
### `snd_inj_classic_set_protections`
Transitions the full remote region to `PAGE_EXECUTE_READ`.
**Returns:** `SND_OK`, `SND_STATUS_VIRTUAL_PROTECT_FAILED`, `SND_STATUS_INVALID_STAGE_SEQUENCE`
---
### `snd_inj_classic_execute`
Creates a remote thread at `remote_base` with parameter `NULL`. Stores handle in `remote_thread`.
**Returns:** `SND_OK`, `SND_STATUS_THREAD_CREATE_FAILED`, `SND_STATUS_INVALID_STAGE_SEQUENCE`
**Source:** `src/injection/classic/engine.c`
---
## Classic Chain (`sindri/injection/classic/chain.h`)
### `snd_inj_classic_shell`
Runs the full shellcode pipeline: open → alloc → write → protect → execute.
```c
snd_status_t snd_inj_classic_shell(snd_inj_ctx_t *ctx);
```
| Parameter | Description |
|---|---|
| `ctx` | Context with `target_pid`, `payload`, and `proc_api` set |
**Returns:** `SND_OK` or the failing stage's status
**Source:** `src/injection/classic/chain.c`
---
### `snd_inj_classic_pe`
Orchestrates local PE preparation via `snd_ldr_pe_ctx_t` and remote injection via `snd_inj_ctx_t`. See [techniques.md](techniques.md) for the interleaved step order.
```c
snd_status_t snd_inj_classic_pe(snd_ldr_pe_ctx_t *ldr_ctx, snd_inj_ctx_t *inj_ctx);
```
| Parameter | Description |
|---|---|
| `ldr_ctx` | Loader context with `raw_source`, `mem_api`, and `mod_api` set |
| `inj_ctx` | Injection context with `target_pid` and `proc_api` set |
On success, `inj_ctx` reaches `SND_INJ_STAGE_EXECUTED` with `remote_entry_point` pointing at the remote entry point and `remote_base` pointing at the allocation base.
**Returns:** `SND_OK` or the failing loader/injection stage status
**Source:** `src/injection/classic/chain.c`
---
## Related Documentation
- [Loader API](../loaders/api_reference.md) — `snd_ldr_pe_ctx_t` used by `snd_inj_classic_pe`
- [Process primitives](../primitives/process/api_reference.md) — `snd_process_api_t` backends
- [State machines](../../architecture/state_machines.md) — global stage pattern
+150 -10
View File
@@ -1,17 +1,157 @@
# Injection Techniques
> [!NOTE]
> The injection domain is planned for a future release of SindriKit. This page will document process injection capabilities as they are implemented.
Injection operations follow the same Dependency Injection and state machine patterns used throughout SindriKit. Every technique advances a shared `snd_inj_ctx_t` through discrete stages and delegates all cross-process work to `ctx->proc_api`.
## Overview
**Prerequisite reading:** [Process primitives](../primitives/process/techniques.md), [Dependency Injection](../../architecture/dependency_injection.md)
The injection domain will expose process injection capabilities that follow the same State Machine and Dependency Injection patterns used throughout the toolkit. A `snd_injector_ctx_t` context will manage the progression of injection operations through discrete, trackable stages.
---
Planned capabilities include:
## Architecture: One Context, Many Techniques
- **Classic Remote Injection** — Standard `VirtualAllocEx` / `WriteProcessMemory` / `CreateRemoteThread` injection, exposed through the injected `mem_api` so the allocation step can be transparently replaced with a direct `NtAllocateVirtualMemory` syscall.
- **Process Hollowing** — Unmapping a legitimate process's image and replacing it with a payload image, applying relocations and resolving imports in the remote process context.
- **Thread Hijacking** — Suspending a target thread, overwriting its context to redirect execution to a shellcode or reflectively-loaded payload, then resuming.
- **APC Queue Injection** — Queuing an Asynchronous Procedure Call to an alertable thread in the target process using `NtQueueApcThread`.
```mermaid
flowchart TB
subgraph shared ["Shared (all techniques)"]
CTX["snd_inj_ctx_t"]
PROC["proc_api → snd_process_api_t"]
end
Each technique will rely on the existing PE parser and reflective loading pipeline from the loaders domain, reusing all established primitives rather than re-implementing them.
subgraph classic ["Classic technique (implemented)"]
ENG["snd_inj_classic_* engine"]
SH["snd_inj_classic_shell"]
PE["snd_inj_classic_pe"]
end
subgraph future ["Future techniques"]
APC["APC queue …"]
HIJ["Thread hijack …"]
end
CTX --> ENG
ENG --> SH
ENG --> PE
CTX -.-> APC
CTX -.-> HIJ
PROC --> ENG
```
Future techniques will add their own engine headers (e.g. `injection/apc/engine.h`) but continue to mutate the same `snd_inj_ctx_t`. Technique-specific metadata, if ever needed, lives in technique-local structures passed alongside the shared context — not in a forked injection context type.
---
## Shared Stage Machine (`snd_inj_stage_t`)
| Stage | Set by | Meaning |
|---|---|---|
| `SND_INJ_STAGE_UNINITIALIZED` | — | Context created, not started |
| `SND_INJ_STAGE_TARGET_ACQUIRED` | `snd_inj_classic_open_target` | Handle to target process |
| `SND_INJ_STAGE_MEMORY_ALLOCATED` | `snd_inj_classic_alloc_remote` | RW region reserved in remote process |
| `SND_INJ_STAGE_PAYLOAD_WRITTEN` | `snd_inj_classic_write_payload` | Payload bytes copied remotely |
| `SND_INJ_STAGE_PROTECTIONS_SET` | `snd_inj_classic_set_protections` | Remote region transitioned to RX |
| `SND_INJ_STAGE_EXECUTED` | `snd_inj_classic_execute` | Remote thread created |
Each engine function validates the current stage and returns `SND_STATUS_INVALID_STAGE_SEQUENCE` on mismatch. This ordering is enforced for all classic paths and will be reused by future techniques that build on the same remote write/execute primitives.
---
## Classic Technique: Shellcode (`snd_inj_classic_shell`)
The baseline **Alloc → Write → Protect → Execute** pattern. The payload buffer is treated as opaque shellcode — the thread starts at `remote_base` (allocation base), not at an PE entry point.
### Pipeline
1. **Open target**`proc_api->open_process(target_pid, PROCESS_ALL_ACCESS, …)`
2. **Allocate remote**`payload->size` bytes, `MEM_COMMIT | MEM_RESERVE`, `PAGE_READWRITE`
3. **Write payload**`proc_api->write_remote` copies the full shellcode buffer
4. **Protect** — single `proc_api->protect_remote` call: `PAGE_READWRITE``PAGE_EXECUTE_READ` over the entire allocation
5. **Execute**`proc_api->create_remote_thread` at `remote_entry_point` (or `remote_base` if NULL), parameter `NULL`
### OpSec notes
- Avoids allocating `PAGE_EXECUTE_READWRITE` directly (RW then RX is a common evasion pattern).
- The shellcode path does not parse PE structures or touch the loader domain.
- Backend choice (`snd_proc_win` / `_nt` / `_sys`) determines telemetry surface — see [process primitives](../primitives/process/techniques.md).
### Example (`pocs/inject_shell/main.c`)
```c
snd_inj_ctx_t inj_ctx = {0};
inj_ctx.target_pid = target_pid;
inj_ctx.payload = &shellcode_buf;
inj_ctx.proc_api = &snd_proc_win; // or snd_proc_sys after syscall bootstrap
snd_status_t status = snd_inj_classic_shell(&inj_ctx);
snd_inj_cleanup(&inj_ctx);
```
---
## Classic Technique: PE (`snd_inj_classic_pe`)
High-level orchestrator linking a **reflective loader context** (`snd_ldr_pe_ctx_t`) with the **shared injection context** (`snd_inj_ctx_t`). The PE is parsed, mapped, relocated, and import-fixed **locally**, then the fixed image bytes are written into the remote process. Execution starts at the remote entry point (`remote_base + AddressOfEntryPoint`).
This is not a full in-remote reflective load — fixups happen in local memory using the loader engine, then the baked image is marshaled cross-process.
### Interleaved pipeline
The PE chain deliberately interleaves loader and injection stages so relocations use the **remote base** as the execution address:
| Step | Component | Action |
|---|---|---|
| 1 | Loader | `snd_pe_parse(ldr_ctx->raw_source, FALSE, &ldr_ctx->pe)``SND_STAGE_PARSED` |
| 2 | Loader | `snd_ldr_pe_compatibility_check` |
| 3 | Loader | `snd_ldr_pe_allocate_and_copy_image` — local RW mapping |
| 4 | Injection | `inj_ctx->payload` ← local mapped buffer (`local_base`, `allocated_size`) |
| 5 | Injection | `snd_inj_classic_open_target` |
| 6 | Injection | `snd_inj_classic_alloc_remote` — remote RW region sized to `allocated_size` |
| 7 | Loader | `ldr_ctx->target.execution_base = inj_ctx->remote_base` |
| 8 | Loader | `snd_ldr_pe_apply_relocations` — delta = remote_base ImageBase |
| 9 | Loader | `snd_ldr_pe_resolve_imports` — IAT patched locally |
| 10 | Injection | `snd_inj_classic_write_payload` — writes baked image to remote |
| 11 | Injection | `snd_inj_classic_set_protections` — flat `PAGE_EXECUTE_READ` on remote region |
| 12 | Injection | `remote_entry_point = remote_base + ep_rva`; `snd_inj_classic_execute` |
**Key behaviors:**
- **`execution_base`** on the loader context is set to the remote allocation address *before* relocations so the delta matches where the image will run.
- **Per-section protections** (`snd_ldr_pe_apply_memory_protections`) are **not** called — the injection path applies a single RX protection over the entire remote allocation.
- **TLS callbacks** are **not** invoked in the current PE injection chain.
- **Local execution** (`snd_ldr_pe_execute_image`) is blocked when `local_base != execution_base`; detach/free similarly refuse remote-prepared images.
For DLL payloads, the remote thread starts at `AddressOfEntryPoint` (the DLL entry symbol, typically `DllMain`) with **`NULL` thread parameter** — not a typed `DllMain(hinst, DLL_PROCESS_ATTACH, NULL)` call. Do not assume `DLL_PROCESS_ATTACH` semantics; this differs from local `snd_ldr_pe_execute_image` in `chain.c`.
### Example (`pocs/inject_pe/main.c`)
```c
snd_ldr_pe_ctx_t ldr_ctx = {0};
snd_inj_ctx_t inj_ctx = {0};
ldr_ctx.mem_api = &snd_mem_sys;
ldr_ctx.mod_api = &snd_mod_nt;
ldr_ctx.raw_source = &file_buf;
inj_ctx.target_pid = target_pid;
inj_ctx.proc_api = &snd_proc_sys;
snd_status_t status = snd_inj_classic_pe(&ldr_ctx, &inj_ctx);
snd_inj_cleanup(&inj_ctx);
```
---
## Cleanup
`snd_inj_cleanup` closes `remote_thread` and `target_process` via `proc_api->close_handle`, clears remote fields, and resets stage to `UNINITIALIZED`. It does not free the local loader mapping — callers manage `snd_ldr_pe_free_mapped_image` separately if a local `snd_ldr_pe_ctx_t` was used.
---
## Planned Techniques
Future injection techniques will reuse `snd_inj_ctx_t` and `proc_api`:
| Technique | Description |
|---|---|
| **APC queue** | Queue user APC to an alertable thread via `NtQueueApcThread` |
| **Thread hijack** | Suspend thread, rewrite context, resume |
| **Process hollowing** | Replace remote image in situ (loader + injection coordination) |
Each will add technique-specific engine headers under `include/sindri/injection/<technique>/` without forking the shared context type.