You're Gonna Carry That Weight

This commit is contained in:
sibouzitoun
2026-06-22 12:38:04 +01:00
commit 5e26626615
162 changed files with 10836 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
---
BasedOnStyle: LLVM
IndentWidth: 4
TabWidth: 4
UseTab: Never
ColumnLimit: 120
AlignConsecutiveDeclarations: true
AlignConsecutiveAssignments: true
AlignConsecutiveMacros: true
PointerAlignment: Right
BreakBeforeBraces: Attach
AllowShortFunctionsOnASingleLine: Empty
AllowShortBlocksOnASingleLine: false
KeepEmptyLinesAtTheStartOfBlocks: false
MaxEmptyLinesToKeep: 1
SortIncludes: true
IncludeBlocks: Regroup
---
+9
View File
@@ -0,0 +1,9 @@
# Enforce Unix line endings for C/C++ source code
*.c text eol=lf
*.h text eol=lf
*.md text eol=lf
*.txt text eol=lf
*.cmake text eol=lf
# Keep batch files as CRLF for Windows compatibility
*.bat text eol=crlf
+28
View File
@@ -0,0 +1,28 @@
---
name: Bug report
about: Create a report to help us improve SindriKit
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is. (Note: "AV/EDR caught my payload" is not a bug. See SECURITY.md for details.)
**To Reproduce**
Steps to reproduce the behavior:
1. Compiler used (e.g. MSVC v143, GCC, Clang):
2. CMake arguments / build configuration:
3. Minimal code snippet causing the issue:
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment details:**
- OS Architecture: [e.g. Windows 11 x64]
- Payload Architecture: [e.g. x86 or x64]
- Build Tier: [DEBUG or SILENT]
**Additional context**
Add any other context about the problem here. (If it's an OpSec leak in SILENT tier, please open a Security Advisory instead of a public issue).
+19
View File
@@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen. Keep in mind the strict dependency injection architectural model of the toolkit.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+19
View File
@@ -0,0 +1,19 @@
## Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
## Checklist:
*Please verify all of the following before submitting your PR. SindriKit enforces strict engineering standards.*
- [ ] My code follows the style guidelines of this project (verified via `.clang-format`).
- [ ] I have performed a self-review of my own code.
- [ ] I have commented my code, particularly in hard-to-understand or low-level ASM areas.
- [ ] I have made corresponding changes to the documentation in `docs/`.
- [ ] My changes generate no new warnings when compiled with `/W4 /WX` on MSVC.
- [ ] I have successfully compiled and tested my changes on **both x86 and x64** targets.
- [ ] I have verified that compiling in `SILENT` mode (`SND_ENABLE_DEBUG=OFF`) introduces no new string artifacts.
+26
View File
@@ -0,0 +1,26 @@
name: Build Matrix
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: windows-latest
strategy:
matrix:
arch: [x64, Win32]
build_type: [Release]
steps:
- uses: actions/checkout@v4
- name: Configure CMake
# By default, Windows runners use MSVC. We compile for both x64 and x86 to catch pointer truncation bugs.
run: cmake -B ${{github.workspace}}/build -A ${{matrix.arch}} -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -DSND_ENABLE_DEBUG=OFF -DSND_BUILD_PAYLOADS=ON
- name: Build
# The build will automatically fail if /W4 or /WX catches any warnings, enforcing our strict standards.
run: cmake --build ${{github.workspace}}/build --config ${{matrix.build_type}}
Executable
+4
View File
@@ -0,0 +1,4 @@
build*/
.clangd
__pycache__
corkami
+27
View File
@@ -0,0 +1,27 @@
# Changelog
All notable technique additions, strategy improvements, and core architecture updates to SindriKit will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project attempts to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [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. 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.
### Core Features (v1.0)
- **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 to provide clean execution bases.
- **Algorithm Agility:** Compile-time API hashing (DJB2 or FNV1A). Hashing algorithms can be swapped across the entire project via a single CMake variable.
- **PE Parser:** A custom, bounds-checked PE32/PE32+ parser with explicit state tracking (`is_mapped`) for handling raw and memory-mapped files safely.
- **Dynamic FFI:** Custom MASM assembly bridges for executing arbitrary functions while adhering to strict x64 and x86 calling conventions.
- **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
- **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.
Executable
+178
View File
@@ -0,0 +1,178 @@
cmake_minimum_required(VERSION 3.16)
project(SindriKit VERSION 1.0.0 LANGUAGES C ASM_MASM)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded")
option(SND_ENABLE_DEBUG "Enable debug prints and tracking" OFF)
option(SND_USE_PRINTF "Use printf instead of OutputDebugString" OFF)
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)
# --- Configuration Guards ---
if(SND_CRTLESS)
if(SND_BUILD_TESTS)
message(WARNING "SindriKit: SND_BUILD_TESTS requires the CRT. Forcing SND_CRTLESS=OFF.")
set(SND_CRTLESS OFF CACHE BOOL "Build the framework for pure CRT-less operation" FORCE)
else()
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.")
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)
endif()
endif()
endif()
if(NOT WIN32)
message(FATAL_ERROR "SindriKit supports Windows targets only.")
endif()
# --- Architecture-specific assembly ---
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(SYSCALL_ASM src/primitives/syscalls/asm/invoke_x64.asm)
set(EXECUTION_ASM src/primitives/execution/ffi/ffi_x64.asm)
set(HEAVENSGATE_ASM "")
else()
set(SYSCALL_ASM src/primitives/syscalls/asm/invoke_x86.asm)
set(EXECUTION_ASM src/primitives/execution/ffi/ffi_x86.asm)
set(HEAVENSGATE_ASM src/primitives/execution/heavens_gate/heavens_gate_x86.asm)
endif()
# --- Compile-time hash generation ---
find_package(Python3 REQUIRED COMPONENTS Interpreter)
set(SND_HASH_ALGO "DJB2" CACHE STRING "Hashing algorithm (DJB2, FNV1A)")
string(TOUPPER "${SND_HASH_ALGO}" SND_HASH_ALGO)
set(HASH_MANIFEST "${CMAKE_CURRENT_SOURCE_DIR}/config/hashes.ini")
set(HASH_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_hashes.py")
set(GENERATED_HEADER "${CMAKE_CURRENT_BINARY_DIR}/generated/sindri_hashes.h")
# Generate hashes at configure time.
# We use execute_process so it only runs during CMake configure.
# The Python script natively skips writing if the content hasn't changed, preserving timestamps.
execute_process(
COMMAND ${Python3_EXECUTABLE} ${HASH_SCRIPT} ${HASH_MANIFEST} ${GENERATED_HEADER} ${SND_HASH_ALGO} ${SND_RANDOMIZE_SEED}
RESULT_VARIABLE HASH_RES
)
if(NOT HASH_RES EQUAL 0)
message(FATAL_ERROR "SindriKit: Failed to generate API hashes.")
endif()
# Force CMake to reconfigure automatically if the manifest or script changes
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${HASH_MANIFEST} ${HASH_SCRIPT})
# --- Static library ---
add_library(sindri_engine STATIC
# Common
src/common/status.c
src/common/helpers.c
src/common/buffer.c
src/common/hash.c
src/common/disk.c
# PE parser
src/parsers/pe/pe_parser.c
src/parsers/pe/pe_exports.c
src/parsers/pe/pe_imports.c
src/parsers/pe/pe_relocations.c
src/parsers/pe/pe_utils.c
src/parsers/pe/pe_section_utils.c
# Reflective loader
src/loaders/reflective/engine.c
src/loaders/reflective/chain.c
# KnownDlls loader
src/loaders/knowndlls/knowndlls.c
src/loaders/knowndlls/win.c
src/loaders/knowndlls/native.c
# Memory primitives
src/primitives/memory/win.c
src/primitives/memory/native.c
# Module primitives
src/primitives/modules/win.c
src/primitives/modules/native.c
src/primitives/modules/peb.c
src/primitives/modules/ntdll.c
# Syscall resolvers
src/primitives/syscalls/syscalls.c
src/primitives/syscalls/hellsgate.c
src/primitives/syscalls/halosgate.c
src/primitives/syscalls/tartarusgate.c
src/primitives/syscalls/velesreek.c
src/primitives/syscalls/internal/neighbor.c
# Execution
src/primitives/execution/ffi/ffi_invoke.c
src/primitives/execution/heavens_gate/heavens_gate.c
# Assembly & generated
${SYSCALL_ASM}
${EXECUTION_ASM}
${HEAVENSGATE_ASM}
${GENERATED_HEADER}
)
if(SND_CRTLESS)
target_sources(sindri_engine PRIVATE src/common/crt_manifest.c)
endif()
add_library(sindri::engine ALIAS sindri_engine)
target_include_directories(sindri_engine
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_BINARY_DIR}/generated
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
target_compile_features(sindri_engine PUBLIC c_std_99)
# --- Debug & Print Configuration ---
if(SND_USE_PRINTF)
set(DEF_USE_PRINTF 1)
set(PRINT_MODE "CONSOLE (printf)")
else()
set(DEF_USE_PRINTF 0)
set(PRINT_MODE "DEBUGGER (OutputDebugString)")
endif()
if(SND_ENABLE_DEBUG)
set(DEF_DEBUG 1)
message(STATUS "SindriKit: DEBUG | Output: ${PRINT_MODE} | Hash: ${SND_HASH_ALGO}")
else()
set(DEF_DEBUG 0)
message(STATUS "SindriKit: SILENT | Hash: ${SND_HASH_ALGO}")
endif()
target_compile_definitions(sindri_engine PUBLIC
SND_DEBUG=${DEF_DEBUG}
SND_USE_PRINTF=${DEF_USE_PRINTF}
SND_HASH_${SND_HASH_ALGO}=1
)
# --- Compiler warnings ---
if(MSVC)
target_compile_options(sindri_engine PRIVATE
$<$<COMPILE_LANGUAGE:C>:/W4>
$<$<COMPILE_LANGUAGE:C>:/WX>
)
if(NOT SND_ENABLE_DEBUG)
target_compile_options(sindri_engine PRIVATE
$<$<COMPILE_LANGUAGE:C>:/GS->
$<$<COMPILE_LANGUAGE:C>:/sdl->
)
endif()
else()
target_compile_options(sindri_engine PRIVATE
$<$<COMPILE_LANGUAGE:C>:-Wall>
$<$<COMPILE_LANGUAGE:C>:-Wextra>
$<$<COMPILE_LANGUAGE:C>:-Werror>
)
endif()
# --- Optional subdirectories ---
if(SND_BUILD_PAYLOADS)
add_subdirectory(pocs)
endif()
if(SND_BUILD_TESTS)
add_subdirectory(tests)
endif()
+115
View File
@@ -0,0 +1,115 @@
# Contributing to SindriKit
SindriKit is a foundational C library for building offensive security capabilities. The core philosophy governing the entire project is: **"One abstraction layer. Every offensive domain."**
> [!WARNING]
> **Scope & Intent Restriction**
> SindriKit is an architectural framework, not a malware repository. Pull requests containing fully weaponized implants, ransomware, destructive capabilities, or hyper-specific vendor bypasses (e.g., "Bypass for Vendor X") will be immediately closed and locked. Contributions must be generalized, operational primitives (e.g., new parsing logic, generic injection techniques, execution agnostic wrappers).
Before contributing, you must familiarize yourself with the framework's architecture, primarily its strict separation of *Intent* from *Execution Mechanics*.
## Development Setup
SindriKit is designed **strictly for Windows targets** (`_WIN32` or `_WIN64`). The core build system relies on modern CMake (`>= 3.16`), MSVC, and Python 3 (for compile-time API string hashing).
1. Clone the repository.
2. Ensure you have **Visual Studio** (with C++ build tools), **CMake**, and a **Python 3** interpreter installed and available in your `PATH`.
3. Use the provided `build.bat` wrapper script at the repository root to configure and compile the project.
### Using `build.bat`
Do not manually invoke CMake for local development unless you are explicitly testing the `add_subdirectory()` integration. For standalone development, framework testing, and PoC building, always use `build.bat`.
`build.bat` automatically configures and builds **both** x86 (`build32`) and x64 (`build64`) targets simultaneously, enforcing the `Release` configuration for MSVC compiler optimization:
```cmd
:: Build silently (production defaults: Release, SND_ENABLE_DEBUG=OFF)
build.bat
:: Build with debug prints via OutputDebugString (SND_ENABLE_DEBUG=ON)
build.bat debug
:: Build with printf to stdout instead of OutputDebugString
build.bat debug console
:: Change the compile-time string hashing algorithm
build.bat djb2
build.bat fnv1a
:: Build everything, including tests and PoC payloads (forces debug & console)
build.bat tests pocs
```
## Core Architectural Rules
I heavily scrutinize all pull requests against the following architectural constraints. Code that breaks these paradigms will not be merged.
### 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).
- **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.
### 2. The Status System (`snd_status_t`)
Never return raw integers, `NULL` pointers, or standard `NTSTATUS`/`DWORD` error codes from a framework function.
- All fallible functions must return a `snd_status_t`.
- Utilize the framework macros: `SND_OK`, `SND_ERR()`, `SND_ERR_W32()`, `SND_ERR_CTX()`, and `SND_ERR_W32_CTX()`.
- Error handling must compile cleanly for both the `DEBUG` tier (verbose context strings) and the `SILENT` tier (zero strings). The macros handle this truncation transparently.
### 3. Compile-Time API Hashing
Never use plaintext API or module names as string literals in the C source code.
1. Add the required module and API names to `config/hashes.ini`.
2. The build system invokes `scripts/generate_hashes.py` during compilation to dynamically generate `generated/sindri_hashes.h` based on the active `SND_HASH_ALGO`.
3. Use the generated constants (e.g., `SND_HASH_NTDLL_DLL`, `SND_HASH_NTALLOCATEVIRTUALMEMORY`) in your code.
### 4. Zero Footprint (`SILENT` Tier)
> [!CAUTION]
> **No plaintext strings. No standard I/O.**
> When compiled via `build.bat` (which defaults to `SND_ENABLE_DEBUG=OFF`), the compiled binary must have **zero** framework-related `.rdata` string artifacts.
- Do **not** use standard C library printing functions (`printf`) or Win32 debug APIs (`OutputDebugString`) directly.
- **Always** use the `SND_DEBUG_PRINT` macro, which safely compiles to nothing in production tiers.
- PRs that leak strings into the `SILENT` tier build will be instantly rejected.
### 5. Custom Assembly (MASM)
SindriKit utilizes custom MASM for FFI (`ffi_x64.asm`, `ffi_x86.asm`), cascading syscall resolution, and Heaven's Gate. If your feature requires low-level execution logic:
- You must provide both x86 and x64 implementations.
- You must adhere strictly to the respective calling conventions (e.g., 16-byte stack alignment and shadow space for Microsoft x64).
## Code Style & Warnings
> [!IMPORTANT]
> The build system enforces `/W4` and `/WX` (Warnings as Errors) on MSVC.
- **C Standard**: C99 (`c_std_99`).
- Your code must compile **warning-free**. Pay extremely close attention to implicit truncations, signed/unsigned mismatches, padding anomalies, and pointer-size differences between 32-bit and 64-bit compilation.
## Testing Guidelines
SindriKit is a broad framework encompassing multiple offensive domains. While tests are **not strictly required** for every contribution, if you are adding a highly complex technique or a new parser, it is strongly encouraged to include a dedicated `tests` directory.
If your contribution requires rigorous validation, you can look to the existing PE loader's testing methodology (`tests/loader/`) as a structural example of how to build robust tests:
- **Declarative Execution Matrices**: Defining test specifications that automatically expand across architectures (`x86`/`x64`) and execution mechanics (e.g., testing both `winapi` and `nowinapi` profiles).
- **Dynamic Fuzzing & Mutation**: Programmatically altering inputs (as seen in `pe_mutator.py`) to simulate edge cases and ensure your implementation handles structural corruption without crashing.
- **External Corpuses & Architecture Guards**: Integrating external test suites (like Corkami) and explicitly guarding against execution mismatches.
While not all domains (e.g., simple memory primitives, injection techniques, or syscall strategies) require this level of stress testing, ensuring stability is a core tenet of the framework.
To execute the existing framework tests, you must first build the required binaries, then run the Python test harness:
1. **Build the Binaries**: The test suite requires the loader executables (source in `pocs/`) and the test payloads (source in `tests/loader/src/`). Passing the `tests` flag to the build script automatically compiles both the test payloads and the required PoC loaders.
```cmd
build.bat tests
```
2. **Run the Matrix**: Execute the Python test runner to launch the testing matrix.
```cmd
python tests/loader/test_runner.py --mutate --corkami
```
## Pull Request Process
1. Fork the repo and create a feature branch.
2. Implement your feature, adhering to the architecture.
3. If adding a new capability, update the corresponding markdown documentation in `docs/`.
4. Run `build.bat tests pocs` to ensure both x86 and x64 builds complete successfully and all tests pass.
5. **Squash your commits** into logical, atomic units before submitting.
6. Submit a PR outlining the objective, the approach, and validating its OpSec profile.
Executable
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 charfeddine youssef
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+233
View File
@@ -0,0 +1,233 @@
<p align="center">
<img src="assets/banner.png" width="100%">
</p>
<h1 align="center">SindriKit</h1>
<p align="center">
<strong>The infrastructure offensive development never had.</strong><br>
<em>A foundational C library for building operationally credible offensive capabilities.</em>
</p>
<p align="center">
<img src="https://img.shields.io/badge/language-C-blue?style=flat-square"/>
<img src="https://img.shields.io/badge/arch-x86%20%7C%20x64-lightgrey?style=flat-square"/>
<img src="https://img.shields.io/badge/build-CMake-orange?style=flat-square"/>
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square"/>
</p>
---
## What SindriKit Actually Is
**SindriKit is not an implementation. It is the architecture underneath implementations.**
Most offensive libraries give you parts. A syscall wrapper. A reflective loader. A process injector. Useful individually, incompatible by default, and you inherit the task of rewriting them from scratch every time an EDR changes its mind.
The real problem is that offensive tools couple two things that should never be coupled: the logic of a technique and the mechanics of its execution. Your loader doesn't just load a PE image. It loads a PE image *using VirtualAlloc, LoadLibrary... or whatever Win32 calls you hardcoded months ago*. When that gets flagged, you rewrite. The target changes. You rewrite again. You are not fighting the problem. You are fighting your own architecture.
SindriKit applies a single fix to that problem at the design level: **any offensive technique is expressed as a context structure that consumes injected API tables**. The execution mechanics (e.g. Win32, direct syscall, driver-backed, anything an operator defines) are resolved at runtime through those tables, completely invisible to the technique's logic.
The consequence is immediate: swapping your entire execution strategy from Win32-visible to raw direct-syscall becomes one line of code without having to rewrite your entire toolchain. The technique never knew the difference.
---
## What You Get
| | What it means |
|---|---|
| **One mental model** | Every domain ships with the same context + API table contract, standardized across each and every technique. |
| **Zero rewrite cycle** | Swap functions pointers to shift your entire execution profile. The technique never changes. |
| **Zero integration friction** | One `add_subdirectory` and `target_link_libraries` to inherit the full engine. |
| **Zero production overhead** | Silent tier compiles away every diagnostic string, file reference, and line number. |
---
## 60-Second Drop-In
```cmake
cmake_minimum_required(VERSION 3.16)
project(MyTool C ASM_MASM)
set(SND_BUILD_PAYLOADS OFF CACHE BOOL "")
set(SND_ENABLE_DEBUG OFF CACHE BOOL "")
set(SND_HASH_ALGO "DJB2" CACHE STRING "")
set(SND_RANDOMIZE_SEED ON CACHE BOOL "")
add_subdirectory(libs/SindriKit)
add_executable(my_tool src/main.c)
target_link_libraries(my_tool PRIVATE sindri::engine)
```
```sh
cmake -B build && cmake --build build --config Release
```
Just two lines for your tool to inherit all of SindriKit's capabilities: PE parsing, syscall resolution, reflective loading...
---
## The Engine
### API Abstraction Layer
```
┌────────────────────────────────────────────────────────────────────────────┐
│ ANY OFFENSIVE INTENT │
│ Loader · Injector · Spoofer · Patcher · Bypasser · Harvester · ... │
├────────────────────────────────────────────────────────────────────────────┤
│ SINDRIKIT API ABSTRACTION LAYER │
│ snd_memory_api_t -> alloc · free · protect │
│ snd_module_api_t -> load_library · get_proc_address · ... │
│ [ future tables ] -> thread · process · object · ... │
├──────────────────┬──────────────────────┬──────────────────────────────────┤
│ Win32 Profile │ Native Profile │ Bring Your Own Mechanic │
│ VirtualAlloc │ NtAllocateVirtual │ Driver · ROP · Exotic │
│ LoadLibraryA │ PEB Walk + EAT │ Operator-defined functions │
└──────────────────┴──────────────────────┴──────────────────────────────────┘
```
In practice, this means every domain follows the same contract:
```c
// Reflective Loader
snd_loader_ctx_t loader = {0};
loader.raw_source = &payload;
loader.mem_api = &snd_mem_native;
loader.mod_api = &snd_mod_native;
snd_prepare_reflective_image(&loader);
snd_execute_reflective_image(&loader);
// Remote Injector (planned)
snd_injector_ctx_t injector = {0};
injector.mem_api = &snd_mem_native;
injector.target_pid = 1337;
snd_execute_injection(&injector);
// ETW Patcher, Stack Spoofer, Credential Harvester... Same contract, always.
```
### Cascading Syscall Pipeline
No single extraction technique works everywhere. The hook architecture inside an EDR is not what you'll find in another. Hell's Gate for example works until it doesn't.
SindriKit treats syscall resolution as an injectable mechanic. Stack strategies in priority order. The engine falls through until one succeeds:
```c
snd_set_syscall_strategy(snd_hell_extract_syscall);
snd_add_syscall_strategy(snd_halo_extract_syscall);
snd_add_syscall_strategy(snd_veles_extract_syscall);
snd_add_syscall_strategy(any_other_technique_you_want);
```
### Compile-Time Algorithm Agility
Every API name and module string is stripped from the final binary at compile time. Swap hashing algorithms across the entire codebase with a single CMake variable:
```cmake
set(SND_HASH_ALGO "FNV1A") # or DJB2 recomputes everything automatically
set(SND_RANDOMIZE_SEED ON) # generates a fresh 32-bit seed on next configure
```
Each hash is embedded alongside a randomly generated seed (if `SND_RANDOMIZE_SEED=ON`). Static footprint shifts completely between compilations without touching a line of C.
### Architecture-Aware Dynamic FFI
A custom MASM assembly bridge for arbitrary runtime function invocation. x64 builds follow the Microsoft x64 calling convention precisely (shadow space, register argument placement, stack alignment). x86 builds push arguments in reverse order with support for both `cdecl` and `stdcall` targets.
### Bounds-Checked PE Parser
A unified PE32/PE32+ parser with a critical `is_mapped` flag that correctly handles both raw on-disk images and memory-mapped views. Every data directory access is validated against tracked buffer bounds before dereferencing. Export resolution supports forwarder chains up to depth 4 with hash-based lookup.
Tested against inputs that actually break naive parsers:
- 40+ core test combinations targeting edge-case EXEs, DLLs, bad arguments, missing exports, and TLS callbacks across x86 and x64.
- 100+ dynamic PE mutations generated by the `pe_mutator` module: zeroed section names, integer overflows, invalid `e_lfanew` bounds, mangled imports.
- Full Corkami corpus: cleanly loads valid samples, cleanly rejects malformed ones With no crashes across 99% of the samples.
### State-Tracked Domain Contexts
Every offensive operation is managed through a discrete context structure with an explicit stage enumeration. Operations can be paused between stages for sleep obfuscation or staged deployment, resumed cleanly, and inspected for the exact failure point down to the subsystem and reason.
---
## The API Design Philosophy
> **A context structure owns the operation's state. Injected API tables own the execution mechanics. The two are completely separate.**
Bootstrap the execution subsystem once:
```c
PVOID clean_ntdll = NULL;
snd_map_knowndll(&snd_knowndlls_win, L"ntdll.dll", &clean_ntdll);
snd_set_ntdll(clean_ntdll);
snd_set_syscall_strategy(snd_hell_extract_syscall);
snd_add_syscall_strategy(snd_halo_extract_syscall);
```
One line to change your entire execution strategy:
```c
// Win32 profile
const snd_memory_api_t *mem_api = &snd_mem_win;
// 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`. The operator's algorithm never changes regardless of which profile is active.
---
## Build Tiers
### Debug Tier — `SND_ENABLE_DEBUG=ON`
For local development. `snd_status_t` expands to include `file`, `line`, and a 128-byte `context` string buffer. `SND_ERR_CTX` and `SND_DEBUG_PRINT` emit state machine transitions, parsed PE field values, and syscall resolution outcomes. Use `SND_USE_PRINTF=ON` to route output to `stdout` instead of the debug console.
### Silent Tier — `SND_ENABLE_DEBUG=OFF`
The only acceptable configuration for any binary destined for a target environment. Every diagnostic string, file reference, and line number compiles away completely. `snd_status_t` collapses to two integers. Nothing else.
```cmake
set(SND_ENABLE_DEBUG OFF CACHE BOOL "")
set(SND_BUILD_PAYLOADS OFF CACHE BOOL "")
set(SND_RANDOMIZE_SEED ON CACHE BOOL "")
set(SND_HASH_ALGO "DJB2" CACHE STRING "")
add_subdirectory(vendor/SindriKit)
target_link_libraries(my_tool PRIVATE sindri::engine)
```
---
## Documentation
- **[Getting Started](docs/getting_started/)** — Installation, CMake integration, and bootstrapping the engine.
- **[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.*
---
## Disclaimer
**SindriKit is built for educational, research, and authorized Red Teaming purposes only.** For the full legal disclaimer and information regarding OpSec considerations, see the [Security Policy](SECURITY.md).
---
## License
[MIT](LICENSE)
---
<p align="center">
<img src="assets/title.png" width="100%">
</p>
+44
View File
@@ -0,0 +1,44 @@
# Security Policy
SindriKit is a foundational C library designed exclusively for building offensive security capabilities (implants, loaders, evasion tooling). As such, the definition of a "security vulnerability" differs fundamentally from traditional software projects.
Vulnerabilities in SindriKit are evaluated strictly through the lens of **Operational Security (OpSec)** and **Implant Stability**.
## Legal Disclaimer
**SindriKit is built for educational, research, and authorized Red Teaming purposes only.**
The authors and contributors assume no liability and are not responsible for any misuse, damage, or illegal actions caused by utilizing this toolkit. By using SindriKit, you agree to abide by all applicable local, state, and federal laws.
## Pre-Compiled Binaries
For operational security (OpSec) reasons, **no pre-compiled binaries are provided** in GitHub Releases or anywhere else in this repository. Distributing raw executables facilitates trivial signature generation by automated defensive scanners. All operators must compile the framework and Proof-of-Concept loaders from source.
## Scope of Vulnerabilities
I consider the following to be legitimate security vulnerabilities within the scope of SindriKit:
- **OpSec Regressions (SILENT Tier):**
If the framework is compiled in the production `SILENT` tier (`SND_ENABLE_DEBUG=OFF`), and it unintentionally leaks framework strings, debug artifacts, plaintext API names, or internal error contexts into the final compiled binary (`.rdata` or `.data`), this is a critical OpSec vulnerability. Operators rely on the `SILENT` tier to produce zero-footprint payloads.
- **Memory Corruption & Stability:**
Buffer overflows, out-of-bounds reads/writes in the custom PE parser, or unhandled null pointer dereferences that lead to the crashing of an implant or host process. Implants must be rock-solid; arbitrary crashes generate highly visible process crash dumps and defensive telemetry that instantly burn an operation.
## Out of Scope
The following are **NOT** considered vulnerabilities:
- **AV/EDR Detections:** SindriKit provides the architecture and low-level evasion primitives; it does not guarantee evasion out-of-the-box against all modern telemetry. If an AV/EDR vendor authors a behavioral signature against a provided PoC loader, that is an implementation detection, not a framework vulnerability.
- **Issues in the DEBUG Tier:** The `DEBUG` tier (`SND_ENABLE_DEBUG=ON`) is explicitly designed to be verbose, string-heavy, and noisy for local development. OpSec failures and string artifacts in this tier are expected and intentional.
- **Operator Error:** Misconfiguration of the Dependency Injection (DI) layers, providing invalid architecture targets, or failure to properly bootstrap the execution primitives (e.g., forgetting to configure the `ntdll` base before calling resolvers).
## Reporting a Vulnerability
**Please do not report OpSec or stability vulnerabilities via public GitHub issues.**
If you discover a vulnerability that fits the critical criteria above, please report it by:
1. Opening a **GitHub Security Advisory** draft on this repository. You can do this by navigating to the **Security** tab, selecting **Advisories**, and clicking **Report a vulnerability**, or by going directly to [this link](https://github.com/youssefnoob003/SindriKit/security/advisories/new).
2. Including detailed steps to reproduce the issue.
3. Specifying your exact build configuration (e.g., specific CMake variables used, MSVC compiler version, target architecture x86/x64, and whether `SND_CRTLESS` was active).
4. If reporting an OpSec string leak, please attach the relevant reverse engineering artifacts (such as `strings` output, hex dumps, or a Compiler Explorer link proving the leak).
I will review the advisory and work with you to patch the framework securely while minimizing operational exposure.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Executable
+116
View File
@@ -0,0 +1,116 @@
@echo off
setlocal
set CMAKE_FLAGS=-DSND_ENABLE_DEBUG=OFF
set BUILD_TIER=SILENT
set BUILD_OUTPUT=DEBUGGER
set HASH_ALGO=DJB2
set BUILD_TESTS=OFF
set BUILD_POCS=OFF
set BUILD_CRTLESS=OFF
set CLEAN_BUILD=OFF
set RANDOMIZE=OFF
:parse_args
if "%~1"=="" goto :done_args
if /I "%~1"=="debug" (
set CMAKE_FLAGS=%CMAKE_FLAGS% -DSND_ENABLE_DEBUG=ON
set BUILD_TIER=DEBUG
) else if /I "%~1"=="console" (
set CMAKE_FLAGS=%CMAKE_FLAGS% -DSND_USE_PRINTF=ON
set BUILD_OUTPUT=CONSOLE
) else if /I "%~1"=="fnv1a" (
set HASH_ALGO=FNV1A
) else if /I "%~1"=="djb2" (
set HASH_ALGO=DJB2
) else if /I "%~1"=="tests" (
:: Tests automatically enable DEBUG, PRINTF, and POCS
set CMAKE_FLAGS=%CMAKE_FLAGS% -DSND_ENABLE_DEBUG=ON -DSND_USE_PRINTF=ON -DSND_BUILD_TESTS=ON -DSND_BUILD_PAYLOADS=ON
set BUILD_TIER=DEBUG
set BUILD_OUTPUT=CONSOLE
set BUILD_TESTS=ON
set BUILD_POCS=ON
) else if /I "%~1"=="pocs" (
set CMAKE_FLAGS=%CMAKE_FLAGS% -DSND_BUILD_PAYLOADS=ON
set BUILD_POCS=ON
) else if /I "%~1"=="crtless" (
set CMAKE_FLAGS=%CMAKE_FLAGS% -DSND_CRTLESS=ON
set BUILD_CRTLESS=ON
) else if /I "%~1"=="clean" (
set CLEAN_BUILD=ON
) else if /I "%~1"=="random" (
set CMAKE_FLAGS=%CMAKE_FLAGS% -DSND_RANDOMIZE_SEED=ON
set RANDOMIZE=ON
) else (
echo [!] Unknown argument: %~1
echo.
echo Usage: build.bat [debug] [console] [djb2^|fnv1a] [tests] [pocs] [crtless] [clean] [random]
echo.
echo debug Enable debug prints ^(default: silent^)
echo console Use printf output ^(default: OutputDebugString^)
echo djb2 Use DJB2 hashing ^(default^)
echo fnv1a Use FNV1A hashing
echo tests Build test fixtures ^(implies debug and console^)
echo pocs Build PoC binaries
echo crtless Build CRT-less
echo clean Delete build dirs before compiling
echo random Use randomized seed for compile-time hashes
exit /b 1
)
shift
goto :parse_args
:done_args
set CMAKE_FLAGS=%CMAKE_FLAGS% -DSND_HASH_ALGO=%HASH_ALGO%
echo [*] SindriKit Build
echo [*] Tier : %BUILD_TIER%
echo [*] Output : %BUILD_OUTPUT%
echo [*] Hash : %HASH_ALGO%
echo [*] Tests : %BUILD_TESTS%
echo [*] PoCs : %BUILD_POCS%
echo [*] CRTless : %BUILD_CRTLESS%
echo [*] Random : %RANDOMIZE%
echo.
if "%CLEAN_BUILD%"=="ON" (
echo [*] Cleaning previous builds...
if exist build32 rmdir /s /q build32
if exist build64 rmdir /s /q build64
echo [+] Clean.
echo.
)
echo [*] Configuring x86...
cmake -B build32 -A Win32 %CMAKE_FLAGS%
if %errorlevel% neq 0 (
echo [!] CMake configure failed ^(x86^).
exit /b %errorlevel%
)
echo [*] Building x86...
cmake --build build32 --config Release
if %errorlevel% neq 0 (
echo [!] Build failed ^(x86^).
exit /b %errorlevel%
)
echo.
echo [*] Configuring x64...
cmake -B build64 -A x64 %CMAKE_FLAGS%
if %errorlevel% neq 0 (
echo [!] CMake configure failed ^(x64^).
exit /b %errorlevel%
)
echo [*] Building x64...
cmake --build build64 --config Release
if %errorlevel% neq 0 (
echo [!] Build failed ^(x64^).
exit /b %errorlevel%
)
echo.
echo [+] Done. %BUILD_TIER% / %BUILD_OUTPUT% / %HASH_ALGO%
endlocal
+7
View File
@@ -0,0 +1,7 @@
# Build Configuration
This directory holds declarative configuration files consumed by the build system's pre-build scripts. These files define *what* the framework needs to hash, and the scripts in `scripts/` determine *how*.
## Table of Contents
- [hashes.ini](hashes.ini)
The single source of truth for all compile-time API string hashes. Organized into `[module::<dll_name>]` sections that map DLL names to their exported functions. The build system generates a hash for both the module name and every API listed beneath it.
+38
View File
@@ -0,0 +1,38 @@
# =============================================================================
# SindriKit Hash Manifest
# =============================================================================
# This file is the single source of truth for all compile-time string hashes.
# The build system runs scripts/generate_hashes.py against it at build time and
# produces include/generated/sindri_hashes.h.
#
# FORMAT
# ------
# Group API names under a [module::<dll_name>] section header.
# The build system automatically generates a hash for both the module name
# itself and every API listed beneath it.
#
# Standalone strings that do not belong to any module (e.g. registry key
# names, object paths) go under [extras]. No module hash is generated for
# that section.
#
# Lines beginning with # are comments and are ignored by the parser.
# Blank lines are ignored.
#
# ADDING A NEW DOMAIN
# -------------------
# Add a new [module::<dll_name>] section for the target DLL and list the
# APIs your domain implementation calls. One build regenerates everything.
# =============================================================================
# -- ntdll.dll ----------------------------------------------------------------
# Syscall targets resolved via Hell's / Halo's / Tartarus' / VelesReek Gate.
# Also provides LdrLoadDll for the native module loader.
[module::ntdll.dll]
NtAllocateVirtualMemory
NtProtectVirtualMemory
NtFreeVirtualMemory
NtOpenSection
NtMapViewOfSection
NtClose
LdrLoadDll
+31
View File
@@ -0,0 +1,31 @@
# 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.
## Table of Contents
### 1. Introduction & Onboarding
- [**getting_started/**](getting_started/)
Onboarding guides for building SindriKit, configuring CMake, and running the first reflective loader.
- [**examples/**](examples/)
Standalone PoC walkthroughs demonstrating the framework across three operational profiles.
### 2. The Core Framework
- [**architecture/**](architecture/)
Core design patterns governing the framework: Dependency Injection, State Machines, the Status System, and Red Team integration guidance.
- [**domains/**](domains/)
The actionable offensive capabilities: primitives, loaders, and planned future domains (evasion, injection).
### 3. Internal Engines & Utilities
- [**parsers/**](parsers/)
PE format parsing engines: header validation, export/import resolution, base relocations, and TLS callback execution.
- [**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
- [**config/**](config/)
Documentation for the declarative configuration files (hash manifests) consumed by the build system.
- [**scripts/**](scripts/)
Documentation for the pre-build automation scripts that generate compile-time hash headers.
- [**tests/**](tests/)
Documentation for the integration testing infrastructure: the data-driven test runner, PE mutation engine, and test payload fixtures.
+16
View File
@@ -0,0 +1,16 @@
# Core Architecture
This directory outlines the fundamental design patterns, execution models, and telemetry constraints governing the entire SindriKit framework.
> [!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.
## 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.
- [state_machines.md](state_machines.md)
Details how complex operations (like loading or injecting) are broken into discrete, pausable stages for robust error handling.
- [status_system.md](status_system.md)
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)
Guide on linking SindriKit as an embedded engine into larger C2 implants and staging pipelines.
+52
View File
@@ -0,0 +1,52 @@
# 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.
## The Problem with Hardcoded APIs
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
> [!IMPORTANT]
> SindriKit never makes direct OS calls from its core engine. Instead, it relies on function pointer tables injected into the operation's context.
These tables are defined in `include/sindri/primitives/os_api.h` via two primary structures:
### Memory Capabilities (`snd_memory_api_t`)
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`)
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.
## Usage in Context
When initializing an operation, such as a reflective loader, the operator explicitly injects the desired OS APIs into the context:
```c
snd_loader_ctx_t ctx = {0};
// Inject standard Win32 APIs (for diagnostic/standard profiles)
ctx.mem_api = &snd_mem_win;
ctx.mod_api = &snd_mod_win;
// 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.
## Custom Implementations
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.
+48
View File
@@ -0,0 +1,48 @@
# Redteam 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 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
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
# Add SindriKit subdirectory
add_subdirectory(vendor/SindriKit)
# Link your implant against the SindriKit engine
add_executable(implant src/main.c)
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.
## Algorithm Agility
SindriKit natively supports "Algorithm Agility", allowing operators to rapidly swap out underlying evasion techniques globally across the entire toolkit during the build process.
For instance, string hashing algorithms used for API resolution can be instantly changed via CMake variables:
```cmake
set(SND_HASH_ALGO "DJB2" CACHE STRING "Hashing algorithm for API resolution")
```
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.
## 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.
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.
## Silent Build Profiles
For production builds destined for target environments, SindriKit provides a SILENT build tier:
```cmake
set(SND_ENABLE_DEBUG OFF CACHE BOOL "Disable verbose tracking for production builds")
```
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.
+46
View File
@@ -0,0 +1,46 @@
# 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.
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.
## The Global Context Pattern
Every major domain within SindriKit revolves around a domain-specific context structure.
While the exact fields vary depending on the operation, every context struct adheres to a common pattern:
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
By decentralizing execution into trackable stages, the framework gains several critical capabilities:
### 1. Granular Error Handling
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.
### 2. Execution Pausing and Sleep Obfuscation
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
To demonstrate how this global pattern is implemented, the Reflective Loader domain—the first concrete system built into SindriKit—serves as the primary reference.
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:
- `SND_STAGE_UNINITIALIZED`: The context has been created but no PE parsing has occurred.
- `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.
As future domains (such as injectors and spoofers) are added to the toolkit, they will inherently inherit and implement this identical State Machine paradigm.
+48
View File
@@ -0,0 +1,48 @@
# 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.
The SindriKit status system is designed to provide maximum verbosity during development while guaranteeing absolute zero-footprint string literal emissions during production deployments.
## 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 structure tracks the following:
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)
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 (`SND_DEBUG=1`)
During development, the `snd_status_t` structure expands to include heavily detailed telemetry:
- `file`: The C source file where the error occurred (`__FILE__`).
- `line`: The exact line number (`__LINE__`).
- `context`: A dynamically formatted string buffer (up to 128 chars) providing hyper-specific details (e.g., "Failed to resolve API hash 0xDEADBEEF").
This allows operators to pinpoint exactly why an evasion primitive or loader sequence failed without relying on external debuggers.
### Silent Tier (`SND_DEBUG=0`)
> [!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.
When `SND_ENABLE_DEBUG` is disabled, the status macros dynamically shrink the `snd_status_t` struct:
- 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
Developers interact with the status system using a suite of context-aware macros:
- `SND_OK`: Returns a successful status.
- `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.
+9
View File
@@ -0,0 +1,9 @@
# 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.
## 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.
- [api_reference.md](api_reference.md)
Complete API documentation for buffers, hashing, CRT replacement primitives, disk I/O, and the debug output system.
+353
View File
@@ -0,0 +1,353 @@
# Common: API Reference
This page documents the public API surface of the shared infrastructure headers under `include/sindri/common/`.
---
## Buffer Management (`sindri/common/buffer.h`)
### `snd_buffer_t`
Tracked memory buffer structure. Pairs a data pointer with its size and an optional deallocation callback.
| 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`
Initializes a buffer structure with tracking metadata.
| Parameter | Type | Description |
|---|---|---|
| `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`
---
### `snd_buffer_free`
Invokes the buffer's assigned `free_routine` (if non-NULL) and zeroes the structure fields.
| Parameter | Type | Description |
|---|---|---|
| `buf` | `snd_buffer_t*` | Buffer to free and zero |
**Returns:** `void`
---
### 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`
Zeroes `size` bytes at `dest` using a `volatile BYTE*` loop to prevent compiler elision.
| Parameter | Type | Description |
|---|---|---|
| `dest` | `void*` | Destination pointer |
| `size` | `size_t` | Number of bytes to zero |
---
### `snd_memcpy`
Copies `count` bytes from `src` to `dest`. No alignment assumptions.
| Parameter | Type | Description |
|---|---|---|
| `dest` | `void*` | Destination pointer |
| `src` | `const void*` | Source pointer |
| `count` | `size_t` | Number of bytes to copy |
---
### `snd_strnlen`
Returns the length of `str`, up to a maximum of `max_len`.
| Parameter | Type | Description |
|---|---|---|
| `str` | `const char*` | Input string |
| `max_len` | `size_t` | Maximum characters to scan |
**Returns:** `size_t`
---
### `snd_strncpy`
Copies up to `max_src_len` characters from `src` into `dest`, always null-terminating. Never writes more than `dest_size` bytes.
| Parameter | Type | Description |
|---|---|---|
| `dest` | `char*` | Destination buffer |
| `dest_size` | `size_t` | Total size of the destination buffer |
| `src` | `const char*` | Source string |
| `max_src_len` | `size_t` | Maximum characters to copy from source |
---
### `snd_strncat`
Appends up to `max_src_len` characters from `src` to `dest`, respecting `dest_size` and always null-terminating.
| Parameter | Type | Description |
|---|---|---|
| `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 |
---
### `snd_strnchr`
Searches for character `c` within the first `max_len` characters of `str`.
| Parameter | Type | Description |
|---|---|---|
| `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`.
---
### `snd_strncmp`
Compares up to `max_len` characters of `s1` and `s2`.
| Parameter | Type | Description |
|---|---|---|
| `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`.
---
## Hashing (`sindri/common/hash.h`)
### `snd_hash`
Computes the configured hash of an ASCII string. Case-sensitive.
| Parameter | Type | Description |
|---|---|---|
| `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`
Computes the configured hash of an ASCII string after lowering each character.
| Parameter | Type | Description |
|---|---|---|
| `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`
Computes the configured hash of a wide (UTF-16) string after lowering each character.
| Parameter | Type | Description |
|---|---|---|
| `str` | `const wchar_t*` | Null-terminated wide string |
**Returns:** `DWORD` — computed hash value.
**Primary use:** Hashing module names from the PEB's `InMemoryOrderModuleList` (e.g., `L"ntdll.dll"`).
---
## Disk I/O (`sindri/common/disk.h`)
### `snd_buffer_load_from_disk`
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
PVOID target = SND_PTR_ADD(image_base, rva);
```
### `SND_FORCE_INLINE`
Compiler-agnostic macro for forcing function inlining. Expands to `static __forceinline` on MSVC and `static inline __attribute__((always_inline))` on GCC/Clang.
### `SND_BEGIN_EXTERN_C` / `SND_END_EXTERN_C`
C++ linkage compatibility wrappers. Expand to `extern "C" { ... }` when compiled as C++, empty otherwise.
+98
View File
@@ -0,0 +1,98 @@
# 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.
---
## 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.
The standard C library functions that the framework requires are reimplemented as compiler-intrinsic-safe inline functions inside `include/sindri/common/helpers.h`:
| CRT Function | SindriKit Replacement | Notes |
|---|---|---|
| `memset` | `snd_memzero` | Uses `volatile BYTE*` to prevent compiler optimization from eliding the zeroing loop |
| `memcpy` | `snd_memcpy` | Byte-by-byte copy with no alignment assumptions |
| `strnlen` | `snd_strnlen` | Bounded length evaluation |
| `strncpy` / `strncpy_s` | `snd_strncpy` | Truncation-safe; always null-terminates |
| `strcat` / `strcat_s` | `snd_strncat` | Bounded concatenation with null-termination guarantee |
| `strchr` | `snd_strnchr` | Bounded character search |
| `strcmp` / `strncmp` | `snd_strncmp` | Bounded comparison returning standard `<0 / 0 / >0` semantics |
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`.
> [!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.
---
## 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.
### `snd_buffer_t`
```c
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.
### Bounds Checking Pipeline
All bounds validation is implemented as force-inlined functions to eliminate call overhead:
1. `snd_bounds_check(total_size, offset, size)` — pure arithmetic validation.
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.
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.
---
## 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.
### Hash Variants
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_lower` | `const char*` | Lowercased | DLL names from the PE Import Directory (e.g., `KERNEL32.dll` → lowered) |
| `snd_hash_wide_lower` | `const wchar_t*` | Lowercased | Module names from the PEB `BaseDllName` (UTF-16) |
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.
---
## Debug Output System
SindriKit implements a two-tier debug output system controlled by the `SND_DEBUG` and `SND_USE_PRINTF` preprocessor macros:
| `SND_DEBUG` | `SND_USE_PRINTF` | Output Destination | CRT Required |
|---|---|---|---|
| `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:
- `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.
---
## 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.
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.
+7
View File
@@ -0,0 +1,7 @@
# Build Configuration
This directory documents the declarative configuration files consumed by the build system.
## 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.
+60
View File
@@ -0,0 +1,60 @@
# Hash Manifest (`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.
## Purpose
Every time SindriKit needs to resolve a Windows API — whether walking the PEB for a module base, parsing an export table for a function address, or matching an import descriptor's DLL name — it compares a runtime-computed hash against a compile-time constant. This manifest defines exactly which strings get pre-hashed.
## Format Specification
### Module Sections: `[module::<dll_name>]`
Groups API names under a specific DLL. The build system generates a hash for both the module name itself and every API listed beneath it.
```ini
[module::ntdll.dll]
NtAllocateVirtualMemory
NtProtectVirtualMemory
NtFreeVirtualMemory
NtOpenSection
NtMapViewOfSection
NtClose
LdrLoadDll
```
This produces:
- `SND_HASH_NTDLL_DLL` — hash of the module name `ntdll.dll`
- `SND_HASH_NTALLOCATEVIRTUALMEMORY` — hash of the export name
- ... one per API listed.
### Extras Section: `[extras]`
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.
```ini
[extras]
\\KnownDlls\\ntdll.dll
```
### Comments and Whitespace
Lines beginning with `#` are comments and are ignored by the parser. Blank lines are ignored.
## Adding a New Domain
When implementing a new SindriKit domain that requires runtime API resolution:
1. Add a new `[module::<dll_name>]` section for the target DLL.
2. List every API the domain implementation calls beneath it.
3. Rebuild. One `cmake --build` regenerates the entire header automatically.
```ini
# -- user32.dll ---------------------------------------------------------------
# APIs required by a future GUI evasion domain.
[module::user32.dll]
MessageBoxA
FindWindowA
```
+17
View File
@@ -0,0 +1,17 @@
# Operational Domains
This directory categorizes the actionable offensive capabilities of SindriKit into specialized functional areas.
> [!IMPORTANT]
> **Domain Independence**
> 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
- [primitives/](primitives/)
The foundational extraction, memory, and resolution techniques that all other domains rely on.
- [loaders/](loaders/)
Mechanisms for bootstrapping and executing code payloads entirely in memory (e.g., Reflective PE Loading).
- [evasion/](evasion/)
*Placeholder:* Future domain for heuristic evasion, memory scanning bypasses, and sleep obfuscation.
- [injection/](injection/)
*Placeholder:* Future domain for remote process targeting, thread hijacking, and payload injection.
+6
View File
@@ -0,0 +1,6 @@
# Evasion Domain
This directory is reserved for future capabilities focused on heuristic evasion, memory scanning bypasses, and sleep obfuscation.
## Table of Contents
- *No documentation currently exists. Check back later.*
+6
View File
@@ -0,0 +1,6 @@
# Evasion API Reference
> [!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.
See [techniques.md](techniques.md) for a high-level overview of planned capabilities.
+17
View File
@@ -0,0 +1,17 @@
# Evasion Techniques
> [!NOTE]
> The evasion domain is planned for a future release of SindriKit. This page will document defensive evasion primitives as they are implemented.
## Overview
The evasion domain will expose standalone, context-driven offensive evasion capabilities that follow the same State Machine and Dependency Injection patterns used throughout the toolkit.
Planned capabilities include:
- **ETW Patching** — Disabling Event Tracing for Windows telemetry at the userland level by patching `EtwEventWrite` within the process's own ntdll copy.
- **AMSI Bypass** — Neutralizing the Antimalware Scan Interface within a process to prevent in-memory script and buffer scanning.
- **Call Stack Spoofing** — Forging the userland call stack prior to making sensitive API calls (e.g., memory allocation) to avoid stack-based heuristics used by EDRs.
- **Sleep Obfuscation** — Encrypting the implant's memory regions while sleeping to evade periodic memory scanner heuristics.
Each evasion capability will be initialized via a dedicated context struct and will expose its execution mechanics through the standard injected `mem_api` / `mod_api` DI tables.
+6
View File
@@ -0,0 +1,6 @@
# Injection Domain
This directory is reserved for future capabilities focused on remote process targeting, thread hijacking, and inter-process payload injection.
## Table of Contents
- *No documentation currently exists. Check back later.*
+6
View File
@@ -0,0 +1,6 @@
# Injection API Reference
> [!NOTE]
> The injection domain is planned for a future release of SindriKit. This page will be populated when the injection context (`snd_injector_ctx_t`) and its associated primitives are implemented.
See [techniques.md](techniques.md) for a high-level overview of planned capabilities.
+17
View File
@@ -0,0 +1,17 @@
# Injection Techniques
> [!NOTE]
> The injection domain is planned for a future release of SindriKit. This page will document process injection capabilities as they are implemented.
## Overview
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:
- **Classic Remote Injection** — Standard `VirtualAllocEx` / `WriteProcessMemory` / `CreateRemoteThread` injection, exposed through the injected `mem_api` so the allocation step can be transparently replaced with a direct `NtAllocateVirtualMemory` syscall.
- **Process Hollowing** — Unmapping a legitimate process's image and replacing it with a payload image, applying relocations and resolving imports in the remote process context.
- **Thread Hijacking** — Suspending a target thread, overwriting its context to redirect execution to a shellcode or reflectively-loaded payload, then resuming.
- **APC Queue Injection** — Queuing an Asynchronous Procedure Call to an alertable thread in the target process using `NtQueueApcThread`.
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.
+9
View File
@@ -0,0 +1,9 @@
# Loaders Domain
This directory contains implementations for bootstrapping code into memory.
## Table of Contents
- [techniques.md](techniques.md)
Provides a stage-by-stage breakdown of the Reflective PE Loading pipeline and the KnownDlls mapping strategy.
- [api_reference.md](api_reference.md)
Public API documentation for the reflective loader engine, execution chain wrapper functions, and the KnownDlls loader configurations.
+264
View File
@@ -0,0 +1,264 @@
# Loaders: API Reference
This page documents the full public API surface of the loaders domain. Headers live under `include/sindri/loaders/`.
---
## Reflective Loader 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.
### `snd_prepare_reflective_image`
Executes the full allocation and fixup chain in one call:
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`.
| Parameter | Type | Description |
|---|---|---|
| `ctx` | `snd_loader_ctx_t*` | Initialized loader context with `raw_source`, `mem_api`, and `mod_api` set |
**Returns:** `snd_status_t``SND_OK` on success, or the status of the first failing stage.
---
### `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`
Structure tracking the state of the allocated target region during the loading process.
| Field | Type | Description |
|---|---|---|
| `virtual_base` | `LPVOID` | Allocated virtual memory base, populated after `SND_STAGE_MEM_ALLOCATED` |
| `delta_offset` | `LONG_PTR` | Relocation delta (actual base minus preferred `ImageBase`) |
| `entry_point` | `LPVOID` | Resolved entry point address |
| `allocated_size` | `SIZE_T` | Total size of the virtual allocation |
---
### `snd_loader_ctx_t`
The central context structure for a reflective load operation.
| Field | Type | Description |
|---|---|---|
| `raw_source` | `const snd_buffer_t*` | Raw payload buffer (the on-disk PE bytes) |
| `pe` | `snd_pe_parser_t` | Parsed PE context, populated after stage `SND_STAGE_PARSED` |
| `target` | `snd_pe_target_t` | Target state block containing allocated memory and delta data |
| `stage` | `snd_loader_stage_t` | Current state machine position |
| `mem_api` | `const snd_memory_api_t*` | Injected memory primitives |
| `mod_api` | `const snd_module_api_t*` | Injected module primitives |
---
### `snd_compatibility_check`
Validates that the payload's architecture matches the host process bitness.
| Parameter | Type | Description |
|---|---|---|
| `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`
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`.
| Parameter | Type | Description |
|---|---|---|
| `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
SND_CALL_EXPORT(ctx, name, signature, status_out, ...)
```
| Parameter | Description |
|---|---|
| `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 |
---
### `SND_CALL_EXPORT_RET`
Resolves and calls a named export, capturing its return value.
```c
SND_CALL_EXPORT_RET(ctx, name, signature, status_out, ret_out, ...)
```
| Parameter | Description |
|---|---|
| `ctx` | Pointer to the initialized loader context |
| `name` | Plaintext string name of the target export |
| `signature` | Function pointer type to cast the export to |
| `status_out` | A `snd_status_t` variable that receives the resolution result |
| `ret_out` | Variable to store the export's return value |
| `...` | Arguments to pass to the export |
---
## KnownDlls Loader (`sindri/loaders/knowndlls/knowndlls.h`)
### `SND_TARGET_KNOWNDLLS_DIR`
**Value:** `L"\\KnownDlls\\"` (x64) or `L"\\KnownDlls32\\"` (x86)
The architecture-dependent Object Manager directory path where pristine system DLL section objects are cached by `smss.exe`.
---
### `snd_knowndlls_config_t`
Configuration structure for the KnownDlls loader mapping technique. Two pre-built instances are exported globally:
- `snd_knowndlls_win`: Uses Win32 implementations (e.g., `NtOpenSection` via `GetProcAddress`).
- `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.
+75
View File
@@ -0,0 +1,75 @@
# 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.
---
## 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.
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)
### The Loading Pipeline
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.
The pipeline proceeds through the following stages, each tracked by `snd_loader_stage_t`:
#### 1. Parse (`SND_STAGE_PARSED`)
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
`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`)
`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.
#### 4. Apply Base Relocations (`SND_STAGE_RELOCATED`)
`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
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.
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.
The base address of this clean mapping is then fed directly to `snd_set_ntdll()` (from `sindri/primitives/ntdll.h`), ensuring that all subsequent SSN resolution and native module lookups operate on unmodified stubs.
```c
PVOID clean_ntdll = NULL;
snd_status_t status = snd_map_knowndll(&snd_knowndlls_win, L"ntdll.dll", &clean_ntdll);
if (status.code == SND_SUCCESS) {
snd_set_ntdll(clean_ntdll);
}
```
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]
> `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.
+30
View File
@@ -0,0 +1,30 @@
# 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.
> [!IMPORTANT]
> **Zero Footprint Constraint:** Primitive implementations must avoid introducing new dependencies. For example, module resolution must occur via PEB walking rather than invoking `GetModuleHandle`.
## Table of Contents
- [execution/](execution/)
Techniques and APIs responsible for executing arbitrary memory (FFI) or transitioning across execution boundaries (Heaven's Gate).
- [memory/](memory/)
The foundational paradigms for interacting with host process memory (Native vs Win32 allocation).
- [modules/](modules/)
Techniques for interacting with loaded DLLs and extracting function addresses via PEB traversal.
- [syscalls/](syscalls/)
The framework's advanced system for bypassing userland EDR hooks via a cascading fallback mechanism.
---
## Global Primitives (`sindri/primitives/ntdll.h`)
SindriKit maintains global state for the `ntdll.dll` base address. This address is used ubiquitously by native components (like the syscall resolver and the native module API) to extract clean stubs or parse export tables without relying on repetitive PEB lookups or the potentially hooked PEB-resident image.
### `snd_set_ntdll`
Sets the globally cached base address of `ntdll.dll`.
- **`ntdll_base`**: Base address to cache. Typically obtained via KnownDlls mapping or PEB walking.
### `snd_get_ntdll`
Retrieves the globally cached base address.
- **Returns:** `PVOID` The base address, or `NULL` if not set.
@@ -0,0 +1,11 @@
# Execution Primitives
This directory contains techniques and API references for executing arbitrary memory pointers and transitioning across OS bitness boundaries.
## Table of Contents
- [ffi.md](ffi.md)
Details the MASM assembly bridges used to invoke dynamically resolved function pointers.
- [heavens_gate.md](heavens_gate.md)
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 API documentation for the dynamic execution and Heaven's Gate functions.
@@ -0,0 +1,44 @@
# Execution: API Reference
This page documents the public API surface for execution boundary transitions and dynamic invocation.
---
## Dynamic Invocation (`sindri/primitives/ffi.h`)
### `snd_execute_dynamic`
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.
| Parameter | Type | Description |
|---|---|---|
| `pFunctionAddress` | `PVOID` | Target function pointer; returns 0 immediately if NULL |
| `dwArgCount` | `DWORD` | Number of arguments in `pArgs`; may be 0 |
| `pArgs` | `const UINT_PTR*` | Array of `dwArgCount` arguments; may be NULL if count is 0 |
**Returns:** `UINT_PTR` — the return value of the invoked function, or 0 on error.
---
## Heaven's Gate (`sindri/primitives/heavens_gate.h`)
### `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.
**Returns:** `BOOL``TRUE` if running under WoW64, `FALSE` otherwise.
---
### `snd_hg_execute_64`
Transitions to 64-bit mode via the `0x33` segment selector and invokes the specified 64-bit function.
| Parameter | Type | Description |
|---|---|---|
| `pFunctionAddress` | `UINT64` | 64-bit virtual address of the target function |
| `dwArgCount` | `DWORD` | Number of 64-bit arguments (Max: 6) |
| `pArgs` | `const UINT64*` | Array of 64-bit arguments |
| `pResult` | `UINT64*` | Receives the 64-bit return value (`RAX`) |
**Returns:** `snd_status_t``SND_OK` on success, or an error if the process is not running under WOW64.
+22
View File
@@ -0,0 +1,22 @@
# 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.
## Architecture-Specific Bridges
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
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
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
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.
@@ -0,0 +1,20 @@
# 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.
## The WoW64 Boundary
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.
## Segment Selector `0x33`
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]
> 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.
**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)
+12
View File
@@ -0,0 +1,12 @@
# Memory Primitives
This directory documents the paradigms used by the framework to allocate, protect, and free virtual memory within the host process.
> [!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.
## Table of Contents
- [techniques.md](techniques.md)
Contrasts the telemetry differences between standard Win32 allocation (`VirtualAlloc`) and Native direct syscalls.
- [api_reference.md](api_reference.md)
Public API documentation detailing the `snd_memory_api_t` interface and its pre-built instances.
@@ -0,0 +1,28 @@
# Memory: API Reference
This page documents the memory capabilities exported by the framework.
---
## 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.
| Symbol | Backend Paradigm |
|---|---|
| `snd_mem_win` | Standard Win32 API (`VirtualAlloc`, `VirtualProtect`) |
| `snd_mem_native` | Direct Syscalls (`NtAllocateVirtualMemory`, `NtProtectVirtualMemory`) |
---
## The Interface (`sindri/primitives/os_api.h`)
### `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).
| Field | Signature | Description |
|---|---|---|
| `alloc` | `snd_status_t (*)(PVOID* base, SIZE_T size, ULONG alloc_type, ULONG protect)` | Allocates virtual memory |
| `free` | `snd_status_t (*)(PVOID base, SIZE_T size, ULONG free_type)` | Frees virtual memory |
| `protect` | `snd_status_t (*)(PVOID base, SIZE_T size, ULONG new_protect, ULONG* old_protect)` | Modifies page protections |
@@ -0,0 +1,30 @@
# 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.
## 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`.
- **Allocation:** `VirtualAlloc`
- **Protection:** `VirtualProtect`
- **Deallocation:** `VirtualFree`
### 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.
## Paradigm 2: Direct Syscalls (`snd_mem_native`)
The `snd_mem_native` implementation bypasses `kernel32.dll` and `ntdll.dll` entirely by utilizing the framework's internal Syscall Resolution pipeline.
- **Allocation:** Direct syscall to `NtAllocateVirtualMemory`
- **Protection:** Direct syscall to `NtProtectVirtualMemory`
- **Deallocation:** Direct syscall to `NtFreeVirtualMemory`
### 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.
> [!WARNING]
> `snd_mem_native` requires the operator to manually configure the syscall pipeline (via `snd_set_syscall_strategy` and `sindri/primitives/ntdll.h`'s `snd_set_ntdll`) prior to invocation. If the pipeline is not bootstrapped, the native memory calls will immediately fail.
@@ -0,0 +1,9 @@
# Module Primitives
This directory covers techniques for resolving loaded module base addresses and extracting exported functions without relying on highly-monitored OS APIs.
## Table of Contents
- [peb_walking.md](peb_walking.md)
Explains how to manually traverse the `PEB->Ldr` lists to locate DLLs in memory.
- [api_reference.md](api_reference.md)
Public API documentation for PEB walking functions and the `snd_module_api_t` interface.
@@ -0,0 +1,82 @@
# Modules: API Reference
This page documents the module capabilities exported by the framework.
---
## 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.
| Symbol | Backend Paradigm |
|---|---|
| `snd_mod_win` | `LoadLibraryA` / `GetProcAddress` / `GetModuleHandleW` |
| `snd_mod_native` | Global cache (`snd_get_ntdll`) + hash-based export resolution (`snd_pe_get_export_address_by_hash`) + PEB walking (`snd_peb_get_module_base_by_hash`) |
---
## The Interface (`sindri/primitives/os_api.h`)
### `snd_module_api_t`
The function pointer table defining the module and import contract.
| Field | Signature | Description |
|---|---|---|
| `load_library` | `snd_status_t (*)(const char* module_name, PVOID* base)` | 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_module_base` | `snd_status_t (*)(const wchar_t* module_name, PVOID* base)` | Retrieves the base address of a loaded module |
---
## PEB Walking (`sindri/primitives/peb.h`)
### `snd_peb_get_module_base`
Locates a loaded module's base address by walking `PEB->Ldr->InMemoryOrderModuleList` and comparing `BaseDllName` case-insensitively.
| Parameter | Type | Description |
|---|---|---|
| `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`
Hash-based variant. Eliminates the plaintext module name string from the binary entirely.
| Parameter | Type | Description |
|---|---|---|
| `module_hash` | `DWORD` | Pre-computed hash of the target module name |
| `out_base` | `PVOID*` | Receives the base address of the located module |
**Returns:** `snd_status_t``SND_OK` on success.
---
## Global NTDLL State (`sindri/primitives/ntdll.h`)
SindriKit maintains global state for the `ntdll.dll` base address. This address is used ubiquitously by native components to extract clean stubs or parse export tables without relying on repetitive PEB lookups or the potentially hooked PEB-resident image.
### `snd_set_ntdll`
Sets the globally cached base address of `ntdll.dll`.
| Parameter | Type | Description |
|---|---|---|
| `ntdll_base` | `PVOID` | Base address to cache. Typically obtained via KnownDlls mapping or PEB walking. |
**Returns:** `void`
---
### `snd_get_ntdll`
Retrieves the globally cached base address.
**Returns:** `PVOID` — The base address, or `NULL` if not set.
@@ -0,0 +1,29 @@
# 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,14 @@
# Syscall Primitives
This directory details SindriKit's advanced system for extracting System Service Numbers (SSNs) and bypassing inline userland EDR hooks.
> [!WARNING]
> **Initialization Constraint:** The syscall pipeline requires setting up the NTDLL base address. `snd_set_ntdll()` (from `sindri/primitives/ntdll.h`) must be called prior to invoking any resolution function.
## Table of Contents
- [pipeline.md](pipeline.md)
Explains the cascading fallback mechanism used to evaluate multiple SSN resolution strategies sequentially.
- [gates.md](gates.md)
Provides technical breakdowns of the Hell's, Halo's, Tartarus', and VelesReek extraction techniques.
- [api_reference.md](api_reference.md)
Public API documentation for syscall resolvers, pipeline configuration, and the generic ASM invocation stub.
@@ -0,0 +1,93 @@
# 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`.
---
## Core Data Structures
### `snd_syscall_entry_t`
Structure holding the successfully resolved context of a target syscall.
| Field | Type | Description |
|---|---|---|
| `pAddress` | `PVOID` | Pointer to the original stub address in memory |
| `dwHash` | `DWORD` | The compile-time hash of the target NT function |
| `wSystemCall` | `WORD` | The resolved System Service Number (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]
> The `ntdll.dll` base address is no longer set locally per-subsystem. It is now managed globally via `snd_set_ntdll` and `snd_get_ntdll` from `sindri/primitives/ntdll.h`. You MUST set the global base before attempting to resolve syscalls.
---
## Execution
### `snd_resolve_syscall`
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 |
|---|---|---|
| `func_hash` | `DWORD` | Compile-time hash of the target NT function name |
| `entry_out` | `snd_syscall_entry_t*` | Output structure populated with the resolved SSN and stub address |
**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_invoke_syscall_asm`
ASM stub that performs the actual `syscall` instruction. Architecture-specific implementations exist for x64 and x86.
| Parameter | Type | Description |
|---|---|---|
| `args` | `snd_syscall_args_t*` | Pointer to a populated arguments structure |
**Returns:** `NTSTATUS` — the raw kernel return value.
+48
View File
@@ -0,0 +1,48 @@
# 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 45 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
@@ -0,0 +1,30 @@
# Cascading Syscall 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.
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.
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
SindriKit composes the various gate strategies into a single ordered pipeline.
- `snd_set_syscall_strategy()` sets the primary strategy and resets any existing chain.
- `snd_add_syscall_strategy()` appends fallbacks (maximum chain depth: 4).
- 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.
### 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`.
> [!IMPORTANT]
> `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.
+13
View File
@@ -0,0 +1,13 @@
# Examples & Proof of Concepts
This directory contains standalone execution wrappers built on top of the SindriKit library.
## Table of Contents
- [loader_winapi.md](loader_winapi.md)
Walkthrough of standard reflective loading using noisy Win32 APIs for pipeline validation.
- [loader_nowinapi.md](loader_nowinapi.md)
Walkthrough of evasive loading using direct syscalls, Hell's Gate, and PEB walking.
- [loader_noCRT_nowinapi.md](loader_noCRT_nowinapi.md)
Walkthrough of the ultimate evasive profile: stripping the MSVC C Runtime entirely for minimal footprint.
- [heavens_gate.md](heavens_gate.md)
Walkthrough of transitioning a 32-bit WoW64 process into native 64-bit execution.
+63
View File
@@ -0,0 +1,63 @@
# PoC: 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).
## What it demonstrates
- Checking whether the current process is running under WoW64.
- Passing a 64-bit function pointer and argument array across the boundary.
- Executing 64-bit shellcode from a 32-bit process and retrieving a 64-bit return value in `RAX`.
## 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.
### Step 1: Validate the Environment
```c
if (!snd_is_wow64()) {
printf("[-] Not running in WOW64... Heaven's Gate requires WOW64.\n");
return SND_STATUS_ARCH_MISMATCH;
}
```
The framework parses the PEB to determine if the 32-bit process is being emulated by the 64-bit OS.
### Step 2: Allocate 64-bit Payload
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
mov rax, 0x1122334455667788
ret
```
```c
unsigned char shellcode64[] = {
0x48, 0xB8, 0x88, 0x77, 0x66,
0x55, 0x44, 0x33, 0x22, 0x11,
0xC3
};
PVOID pExec = VirtualAlloc(NULL, sizeof(shellcode64), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(pExec, shellcode64, sizeof(shellcode64));
```
### Step 3: Transition and Execute
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.
```c
UINT64 result = 0;
// Invoke Heaven's Gate with 0 arguments
snd_status_t status = snd_hg_execute_64((UINT64)(ULONG_PTR)pExec, 0, NULL, &result);
if (status.code == SND_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.
+58
View File
@@ -0,0 +1,58 @@
# Evasive Loader (No-CRT / No-WinAPI)
**Path:** `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.
## 1. CRT Independence
When compiled in Release mode (`SND_ENABLE_DEBUG=OFF`), this PoC invokes strict CRT-stripping flags:
```cmake
target_link_options(loader_noCRT_nowinapi PRIVATE
/NODEFAULTLIB
/ENTRY:main
)
```
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
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.
```c
PVOID ntdll;
status = snd_peb_get_module_base_by_hash(SND_HASH_NTDLL_DLL, &ntdll);
```
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.
```c
snd_set_ntdll(ntdll);
```
## 3. Syscall Strategy
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
snd_set_syscall_strategy(snd_hell_extract_syscall);
```
## 4. Execution
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.
## Building and Running
To compile this PoC specifically, ensure you are building in Release mode:
```bash
# Debug must be OFF to strip the CRT
cmake -B build -DSND_BUILD_PAYLOADS=ON -DSND_ENABLE_DEBUG=OFF
cmake --build build --config Release
```
Execution:
```bash
build\pocs\loader_noCRT_nowinapi\Release\loader_noCRT_nowinapi.exe
```
+71
View File
@@ -0,0 +1,71 @@
# PoC: 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.
## What it demonstrates
- The full SindriKit bootstrapping sequence required before any native primitive can be used.
- The complete cascading syscall pipeline covering four EDR hook evasion strategies.
- That the loader's core logic is entirely unchanged — only the injected API tables differ from `loader_winapi`.
## Walkthrough
The bootstrapping phase must complete before the loader context is initialized.
### Step 1: Retrieve a clean `ntdll.dll` from KnownDlls
```c
PVOID clean_ntdll = NULL;
// Configure the KnownDlls loader to use Win32 wrappers for the initial bootstrap.
// (Native strategy is available once the syscall pipeline itself is armed.)
snd_status_t status = snd_map_knowndll(&snd_knowndlls_win, L"ntdll.dll", &clean_ntdll);
```
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.
### Step 2: Feed the clean base to the global cache
```c
snd_set_ntdll(clean_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.
### Step 3: Configure the cascading syscall pipeline
```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);
```
`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).
### Step 4: Load the payload and initialize the loader context
```c
snd_buffer_t payload = { ... }; // raw PE bytes from disk
snd_loader_ctx_t ctx = {0};
ctx.raw_source = &payload;
ctx.mem_api = &snd_mem_native; // -> NtAllocateVirtualMemory via direct syscall
ctx.mod_api = &snd_mod_native; // -> PEB walking + manual export table parsing
```
### Step 5: Execute
```c
snd_status_t s = snd_prepare_reflective_image(&ctx);
if (s.code != SND_SUCCESS) {
snd_status_print(s);
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.
+44
View File
@@ -0,0 +1,44 @@
# PoC: 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.
## What it demonstrates
- The baseline reflective loading pipeline: parse → alloc → map → relocate → resolve imports → protect → TLS → execute.
- That the context-based architecture works correctly before introducing evasion complexity.
- A clean baseline to diff against when debugging why a stealth variant fails.
## Walkthrough
No syscall bootstrapping is required. The setup reduces to injecting the Win32-backed API tables and calling the execution chain wrapper:
```c
// Read the raw PE payload from disk into a buffer
snd_buffer_t payload = { ... };
// Initialize the loader context with Win32 primitives
snd_loader_ctx_t ctx = {0};
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.
+9
View File
@@ -0,0 +1,9 @@
# Getting Started
This directory contains introductory guides and onboarding documentation for building and operating SindriKit.
## Table of Contents
- [building.md](building.md)
A guide on configuring CMake, selecting build tiers, and managing CRT-independence for OpSec.
- [basic_usage.md](basic_usage.md)
Walkthrough of initializing a reflective loader context, bootstrapping OS APIs, and executing payloads.
+106
View File
@@ -0,0 +1,106 @@
# 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.
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).
## 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`).
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`.
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.
## 2. Global State vs. Explicit Execution Primitives
> [!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.
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:
```c
PVOID ntdll = NULL;
// 1. Explicitly locate and map NTDLL (e.g., via KnownDlls)
snd_status_t status = snd_map_knowndll(&snd_knowndlls_win, L"ntdll.dll", &ntdll);
if (status.code != SND_SUCCESS) {
return status.os_error;
}
// 2. Feed the base address into the global execution primitive state
snd_set_ntdll(ntdll);
// 3. Configure the Cascading Syscall Pipeline
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);
```
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.
## 3. Case Studies: Standard vs. Stealth Profiles
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.
### Profile A: Standard / Diagnostic (`loader_winapi`)
This profile uses the standard, documented Win32 implementations (`snd_mem_win`, `snd_mod_win`).
```c
snd_loader_ctx_t ctx = {0};
ctx.mem_api = &snd_mem_win; // Backed by VirtualAlloc/VirtualProtect
ctx.mod_api = &snd_mod_win; // Backed by GetModuleHandle/GetProcAddress
snd_status_t status = snd_prepare_reflective_image(&ctx);
if (status.code != SND_SUCCESS) {
// Inspect status.code and status.os_error for failure reasons
return status.os_error;
}
status = snd_execute_reflective_image(&ctx);
if (status.code != SND_SUCCESS) {
return status.os_error;
}
```
**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`)
This profile entirely drops the Win32 API layer in favor of direct, dynamic kernel interaction.
```c
snd_loader_ctx_t ctx = {0};
ctx.mem_api = &snd_mem_native; // Backed by Direct Syscalls
ctx.mod_api = &snd_mod_native; // Backed by Manual PEB Walking
// ... (Requires the global syscall bootstrapping shown in Section 2)
snd_status_t status = snd_prepare_reflective_image(&ctx);
if (status.code != SND_SUCCESS) {
return status.os_error;
}
status = snd_execute_reflective_image(&ctx);
if (status.code != SND_SUCCESS) {
return status.os_error;
}
```
**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
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
// Future Extensibility Example:
snd_injector_ctx_t inj_ctx = {0};
inj_ctx.mem_api = &snd_mem_native;
inj_ctx.target_pid = 1337;
// Uses the previously configured Hell's Gate / Halo's Gate cascading pipeline
snd_execute_injection(&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.
+90
View File
@@ -0,0 +1,90 @@
# 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.
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.
## 1. CMake Integration
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.
Drop the SindriKit repository into the project directory (e.g., `vendor/SindriKit`) and integrate it via `add_subdirectory()`:
```cmake
# In your parent project's CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(AdvancedImplant C CXX ASM_MASM)
# 1. Configure SindriKit Build Options (Optional but Recommended)
set(SND_BUILD_PAYLOADS OFF CACHE BOOL "Disable standalone PoCs")
set(SND_ENABLE_DEBUG OFF CACHE BOOL "Disable verbose tracking for production builds")
set(SND_HASH_ALGO "DJB2" CACHE STRING "Hashing algorithm for API resolution (DJB2, FNV1A)")
# 2. Add SindriKit subdirectory
add_subdirectory(vendor/SindriKit)
# 3. Link your implant against the SindriKit engine
add_executable(implant src/main.c)
target_link_libraries(implant PRIVATE sindri::engine)
```
### Build Configuration Variables
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 |
|---|---|---|---|
| `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_USE_PRINTF` | `OFF` | Reroutes debug output from `OutputDebugStringA` to `printf`. | Only effective if `SND_ENABLE_DEBUG=ON`. Requires 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_HASH_ALGO` | `DJB2` | The algorithm used for compile-time API resolution hashing. | Valid options: `DJB2`, `FNV1A`. |
| `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_BUILD_PAYLOADS` | `OFF` | Compiles the bundled `loader_winapi` and `loader_nowinapi` executables. | Used primarily for PoC verification. |
| `SND_BUILD_TESTS` | `OFF` | Compiles the internal test suite and PE mutator binaries. | **Requires CRT**. Forcefully sets `SND_CRTLESS=OFF`. |
### Pre-Build Code Generation (Algorithm Agility)
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.
## 2. Compiler Configuration and OpSec
SindriKit aggressively forces strict compilation. If an operation causes compiler ambiguity, it is treated as a fatal error.
### MSVC Optimization and Footprint
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:
```cmake
# Recommended MSVC Release Flags for your implant
if(MSVC)
target_compile_options(implant PRIVATE
/O1 # Optimize for size
/GS- # Disable buffer security checks (reduces imports/CRT dependency)
/GR- # Disable RTTI
/Zc:threadSafeInit- # Disable magic statics (CRT dependency)
)
# Strip PDB paths and enforce dynamic base
target_link_options(implant PRIVATE
/PDBALTPATH:%_PDB% # Strips local PDB path from PE headers
/DYNAMICBASE # Enforce ASLR
/NXCOMPAT # Enforce DEP
/ENTRY:main # Avoid CRT entrypoint if using pure C (Optional)
)
endif()
```
> [!WARNING]
> **CRT Independence & Telemetry Leakage**
> 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`.
>
> 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.
## 3. Build Tiers
SindriKit supports two build tiers controlled by the `SND_ENABLE_DEBUG` flag:
- **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`.
- **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.
> [!CAUTION]
> The `SILENT` tier (`SND_ENABLE_DEBUG=OFF`) is the **only** acceptable configuration for compiling production artifacts destined for target environments.
+9
View File
@@ -0,0 +1,9 @@
# Parsers Domain
This directory contains strict parsing engines for interpreting complex file formats (like Portable Executables) purely in-memory.
## 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.
- [api_reference.md](api_reference.md)
Complete API documentation for `snd_pe_parse`, export/import resolution, relocations, TLS callbacks, and utility macros.
+232
View File
@@ -0,0 +1,232 @@
# 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.
+118
View File
@@ -0,0 +1,118 @@
# 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.
+7
View File
@@ -0,0 +1,7 @@
# Build Scripts
This directory documents the pre-build automation scripts that generate source code artifacts consumed during compilation.
## 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.
+71
View File
@@ -0,0 +1,71 @@
# `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.
## Purpose
Offensive tooling that resolves Windows APIs by plaintext name (e.g., `"NtAllocateVirtualMemory"`) leaves those strings embedded in the binary's `.rdata` section. Static analysis tools and YARA rules trivially flag these artifacts. SindriKit eliminates this surface by pre-computing hashes of all required strings at configure time. At runtime, the framework hashes encountered export names and compares the result against these compile-time constants. The plaintext string never appears in the binary.
## Anti-Analysis: Randomized Seed
The script accepts a `RANDOMIZE` flag (ON/OFF) as its final argument.
- **When ON:** The script generates a fresh, randomized 32-bit seed in the range `[0x10000000, 0xFFFFFFFF]`. This guarantees that hash values are unique per build, preventing signature-based detection and thwarting pre-computed rainbow tables targeting standard DJB2/FNV1A initialization constants.
- **When OFF:** The script uses a deterministic initialization constant (e.g., `0x5381` for DJB2). This ensures that the generated header remains identical across executions, preserving file system timestamps and preventing unnecessary recompilations during local development.
The generated header emits the seed as `SND_HASH_SEED`, and the runtime hashing functions in `hash.h` consume it dynamically.
## Supported Algorithms
| Algorithm | CMake Variable | Description |
|---|---|---|
| DJB2 | `SND_HASH_ALGO=DJB2` | Daniel J. Bernstein's hash. Shift-and-add (`h = (h << 5) + h + c`). |
| FNV-1a | `SND_HASH_ALGO=FNV1A` | FowlerNollVo variant 1a. XOR-then-multiply with prime `16777619`. |
Switching the algorithm across the entire codebase requires only changing the `SND_HASH_ALGO` CMake variable without any source code modifications.
## Usage
```
python scripts/generate_hashes.py <hashes.ini> <sindri_hashes.h> <ALGO> <RANDOMIZE>
```
| Argument | Description |
|---|---|
| `hashes.ini` | Path to the hash manifest (typically `config/hashes.ini`) |
| `sindri_hashes.h` | Output path for the generated C header |
| `ALGO` | Hashing algorithm to use (`DJB2` or `FNV1A`) |
| `RANDOMIZE` | Toggle randomized seeds (`ON` or `OFF`) |
### Build System Integration (Smart Caching)
Under normal operation, this script is never invoked manually. The CMake build system evaluates it at **configure time** via `execute_process`.
To support aggressive incremental builds without sacrificing opsec agility, the script employs "Smart Caching" logic:
1. It pre-calculates the exact header string in memory.
2. It attempts to read the existing `sindri_hashes.h` from disk.
3. If the contents are a perfect 1:1 match, the script cleanly exits **without touching the file**, preserving the disk modification timestamp.
Because the timestamp remains unmodified, ninja and MSBuild correctly deduce that dependent C files do not need to recompile, vastly improving developer velocity when `SND_RANDOMIZE_SEED=OFF`.
## Output Format
The generated header contains:
1. A `SND_HASH_SEED` define with the randomized seed value.
2. Grouped `SND_HASH_<NAME>` defines for each module and API.
```c
// AUTO-GENERATED by scripts/generate_hashes.py (DO NOT EDIT)
// Algorithm: DJB2
#define SND_HASH_SEED 0xA3F7B1C2U
// --------------------------------------------------------------------------
// ntdll.dll
// --------------------------------------------------------------------------
#define SND_HASH_NTDLL_DLL 0x1B2C3D4EU // module
#define SND_HASH_NTALLOCATEVIRTUALMEMORY 0x5A6B7C8DU
#define SND_HASH_NTPROTECTVIRTUALMEMORY 0x9E0F1A2BU
...
```
+11
View File
@@ -0,0 +1,11 @@
# Integration Tests
This directory documents the automated testing infrastructure for validating the SindriKit reflective loading pipeline.
## 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).
- [pe_mutator.md](pe_mutator.md)
Documentation for the PE mutation engine, covering both benign edge-case mutations and breaking stressor mutations.
- [test_payloads.md](test_payloads.md)
Documentation for the test payload source files, detailing the purpose and validation logic of each fixture.
+67
View File
@@ -0,0 +1,67 @@
# PE Mutation Engine (`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.
## Mutation Categories
Mutations are divided into two strict categories:
### Benign Edge-Cases
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.
| Mutation | Description | What It Tests |
|---|---|---|
| `unaligned_sections` | Forces `SectionAlignment` and `FileAlignment` to `0x200` | Loaders that hardcode `0x1000` page alignment assumptions |
| `strip_relocs_from_exe` | Strips the `.reloc` directory and sets `IMAGE_FILE_RELOCS_STRIPPED` | Loaders that aggressively demand 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` |
| `append_overlay` | Appends 4 KB of junk data after the last section | Loaders that map beyond declared section boundaries |
| `null_section_names` | Zeroes out every section name string | Loaders that match section names unsafely or log via unchecked `printf` |
### Breaking Stressors
These produce structurally corrupted PE files. The parser **must** detect the violation and reject the input gracefully — **never crash**.
| Mutation | Description | What It Tests |
|---|---|---|
| `corrupt_reloc_block_size` | Sets first reloc block's `SizeOfBlock` to `0xFFFFFFFF` | Unbounded `while` loops in relocation parsing |
| `massive_size_of_headers` | Sets `SizeOfHeaders` to `0xFFFFFFFF` | Unchecked `memcpy` using header-sourced sizes |
| `section_truncated_raw_data` | Inflates a section's `SizeOfRawData` beyond the physical file | Blind section mapping without file bounds checks |
| `section_integer_overflow` | Sets `VirtualSize` and `SizeOfRawData` to `0xFFFFFFFF` | Integer overflow in allocation arithmetic (`VA + ALIGN(VirtualSize)`) |
| `section_overlap_corruption` | Collapses all section file offsets to zero | File alignment normalization bugs |
| `corrupt_pe_signature` | Replaces `PE\0\0` with `0xDEADBEEF` | NT signature validation |
| `invalid_e_lfanew` | Points `e_lfanew` 1 MB past EOF | Offset bounds checking before NT headers access |
| `zero_size_of_image` | Sets `SizeOfImage` to 0 | Allocation size validation |
| `corrupt_import_rva` | Points import directory to `0xDEAD0000` | RVA bounds checking before import traversal |
| `corrupt_export_rva` | Points export directory to `0xDEAD0000` | RVA bounds checking before export parsing |
| `huge_sections_count` | Sets `NumberOfSections` to `0xFFFF` | Section table overflow protection |
| `unterminated_imports` | Overwrites the null-terminating import descriptor with `0xFF` | Import loop termination without boundary checks |
| `massive_export_count` | Sets `NumberOfFunctions` and `NumberOfNames` to `0xFFFFFFFF` | Export iteration bounds checking |
| `oob_entry_point` | Points `AddressOfEntryPoint` past `SizeOfImage` | Entry point RVA validation before execution transfer |
## API
### `Mutation` Dataclass
| Field | Type | Description |
|---|---|---|
| `name` | `str` | Short string identifier (used in output filenames) |
| `description` | `str` | Human-readable label |
| `apply` | `Callable` | The mutation function (`(src, dst) -> None`) |
| `expect_loadable` | `bool` | `True` = loader must succeed; `False` = loader must reject gracefully |
| `applies_to` | `str` | `"both"`, `"exe"`, or `"dll"` |
### `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.
## Dependency
Requires the `pefile` Python package for structured PE modifications:
```
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`.
+69
View File
@@ -0,0 +1,69 @@
# Test Payloads (`tests/loader/src/`)
**Location:** `tests/loader/src/`
These C source files are compiled into DLL and EXE payloads that the test runner feeds to the reflective loaders. Each payload is designed to validate a specific capability of the loading pipeline from basic FFI argument passing to TLS callback execution.
All DLL payloads export functions that return sentinel values (`0xFEEDC0DE`, `0x1337C0DE`, etc.) to signal success or specific failure modes. The test runner validates these return values across architecture boundaries.
---
## `test_dll.c`
The primary DLL test payload. Exercises three critical loader capabilities:
### Exports
- **`SayHello(s1, s2, n3)`**: Validates that the FFI bridge correctly passes three arguments. Returns `0xFEEDC0DE` if the arguments exactly match `("bonjour", "hello", 12)`, otherwise `0xDEADBEEF`.
- **`VerifyInit()`**: Validates two things simultaneously:
1. **DllMain execution**: `DllMain(DLL_PROCESS_ATTACH)` writes `0xA77AC4ED` into a global variable. `VerifyInit` reads it back.
2. **Relocation correctness**: The global lives in `.data`. If relocations were not applied, the pointer reference will read garbage.
Returns `0xC001D00D` on success.
---
## `test_dll_advanced.c`
Exercises complex loader capabilities: multi-DLL imports, heap allocation, and dynamic loading.
### Exports
- **`AdvancedExport(input_str)`**: Calls `GetSystemInfo` (kernel32), allocates heap memory via `malloc`, copies the input string, dynamically loads `user32.dll` via `LoadLibraryA`, and resolves `MessageBoxA` via `GetProcAddress`. Returns `0x1337C0DE` if all operations succeed with input `"advanced_test"`.
- **`VerifyImports()`**: Stress-tests IAT resolution by calling a diverse set of kernel32 APIs (`GetCurrentProcessId`, `GetCurrentThreadId`, `VirtualAlloc/Free`, `GetSystemTimeAsFileTime`). Also verifies `DllMain` was called exactly once. Returns `0xCA11AB1E` on success.
---
## `test_dll_empty.c`
A completely empty DLL with no export or custom imports beyond what the compiler forces for `DllMain`. Tests that the loader gracefully handles a missing Export Directory without crashing. The test runner expects the loader to report `"Error: Requested export was not found"`.
---
## `test_dll_tls.c`
Registers a TLS callback via the `.CRT$XLB` section using `#pragma section` and `__declspec(allocate)`. The callback writes a sentinel (`0x7153CA11`) into a global variable during `DLL_PROCESS_ATTACH`.
### Exports
- **`VerifyTLS()`**: Reads the sentinel back. Returns `0x71500C01` if the TLS callback fired, `0x715FA110` if it did not.
This validates that the reflective loader correctly parses `IMAGE_TLS_DIRECTORY` and iterates the callback array before invoking `DllMain`.
---
## `test_exe.c`
A basic EXE payload that validates relocations and imports:
1. Checks that a `.data` global (`g_reloc_canary = 0xCAFEBABE`) survived relocation.
2. Calls `GetCurrentProcessId()` through the resolved IAT.
Exits with code `0x7A` (122) on success.
---
## `test_exe_advanced.c`
An advanced EXE payload that validates CRT initialization:
1. Performs `memcpy`, `strlen`, `strcmp` to verify the CRT was bootstrapped.
2. Calls `malloc` and `printf` to exercise heap and stdio.
3. Calls `GetCurrentProcessId` and `GetCurrentThreadId` for multi-API IAT verification.
Exits with code `0x1337` on success. The test runner matches `"Successfully allocated and printed"` in stdout.
+62
View File
@@ -0,0 +1,62 @@
# Test Runner (`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.
## Usage
```
python tests/loader/test_runner.py [--corkami]
```
| Flag | Description |
|---|---|
| *(none)* | Runs the core functional test matrix |
| `--corkami` | Enables the Corkami PE fuzz corpus tests (requires extracting `tests/fixtures/pe/corkami_fixtures.zip`) |
## Architecture
### The Spec → TestCase Expansion
Test definitions are written as compact `Spec` objects that are loader- and architecture-agnostic. Each `Spec` declares:
| Field | Description |
|---|---|
| `loaders` | Tuple of loader variants to test against (e.g., `("nowinapi", "winapi")`) |
| `payload` | Test payload name (e.g., `"test_dll"`, `"test_exe_advanced"`) |
| `export` | Optional DLL export name to invoke via the FFI bridge |
| `args` | Arguments passed to the export function |
| `expect_stdout` | Expected substring in the process stdout |
| `expect_retval` | Expected FFI return value (auto-formatted per architecture pointer width) |
| `expect_rc` | Expected process exit code |
| `expect_fail` | If `True`, the test expects the loader to reject the payload 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.
### Test Categories
#### 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
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
- Runs the 32-bit PoC and expects WoW64 detection success.
- Runs the 64-bit PoC and expects a non-WoW64 rejection message.
#### 4. Corkami Fuzz Tests (opt-in via `--corkami`)
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`)
Generates structurally mutated PE variants at runtime and validates that the loader either successfully loads benign mutations or gracefully rejects breaking mutations without crashing.
+9
View File
@@ -0,0 +1,9 @@
#ifndef SINDRI_H
#define SINDRI_H
#include <sindri/common.h>
#include <sindri/loaders.h>
#include <sindri/parsers.h>
#include <sindri/primitives.h>
#endif // SINDRI_H
+10
View File
@@ -0,0 +1,10 @@
#ifndef SND_COMMON_H
#define SND_COMMON_H
#include <sindri/common/buffer.h>
#include <sindri/common/disk.h>
#include <sindri/common/hash.h>
#include <sindri/common/helpers.h>
#include <sindri/common/status.h>
#endif
+87
View File
@@ -0,0 +1,87 @@
#ifndef SND_COMMON_BUFFER_H
#define SND_COMMON_BUFFER_H
#include <sindri/common/helpers.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
// Forward declaration of the buffer structure
typedef struct snd_buffer_s snd_buffer_t;
/**
* @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);
/**
* @brief Represents a tracked, bounds-checked memory buffer.
*/
struct snd_buffer_s {
LPVOID data;
SIZE_T size;
snd_free_cb free_routine;
};
/**
* @brief Initializes a buffer structure with memory tracking context.
* @param buf The buffer structure to initialize.
* @param data Pointer to the memory block.
* @param size Size of the memory block.
* @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);
/**
* @brief Frees a tracked buffer using its assigned cleanup routine and zeroes
* the structure.
* @param buf Pointer to the buffer structure to free.
*/
void snd_buffer_free(snd_buffer_t *buf);
/**
* @brief Checks if a given offset and size are within the bounds of a buffer.
*
* @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) {
if (buf == NULL || buf->data == NULL || buf->size == 0)
return FALSE;
return snd_bounds_check(buf->size, offset, size);
}
/**
* @brief Default routine for buffers allocated via HeapAlloc on the process
* heap.
*/
SND_FORCE_INLINE void snd_buffer_free_heap(snd_buffer_t *buf) {
if (buf && buf->data) {
HeapFree(GetProcessHeap(), 0, buf->data);
}
}
/**
* @brief Default routine for buffers allocated via VirtualAlloc.
*/
SND_FORCE_INLINE void snd_buffer_free_virtual(snd_buffer_t *buf) {
if (buf && buf->data) {
VirtualFree(buf->data, 0, MEM_RELEASE);
}
}
/**
* @brief Default routine for buffers allocated via MapViewOfFile.
*/
SND_FORCE_INLINE void snd_buffer_free_mapped(snd_buffer_t *buf) {
if (buf && buf->data) {
UnmapViewOfFile(buf->data);
}
}
SND_END_EXTERN_C
#endif // SND_COMMON_BUFFER_H
+23
View File
@@ -0,0 +1,23 @@
#ifndef SND_COMMON_DISK_H
#define SND_COMMON_DISK_H
#include <sindri/common/buffer.h>
#include <sindri/common/helpers.h>
#include <sindri/common/status.h>
SND_BEGIN_EXTERN_C
/**
* @brief Reads an entire file into a heap-allocated buffer.
* @param path The null-terminated string of the file path to read.
* @param out_buf Pointer to a zeroed snd_buffer_t. On success, data and size
* are populated.
* @return SND_OK on success, otherwise an I/O related error code.
* @note The caller is responsible for freeing the buffer using
* snd_buffer_free(out_buf).
*/
snd_status_t snd_buffer_load_from_disk(const char *path, snd_buffer_t *out_buf);
SND_END_EXTERN_C
#endif // SND_COMMON_DISK_H
+32
View File
@@ -0,0 +1,32 @@
#ifndef SND_COMMON_HASH_H
#define SND_COMMON_HASH_H
#include <sindri/common/helpers.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
/**
* @brief Computes the configured hash of a standard ASCII string
* (Case-Sensitive).
* @note Use this for API names from the Export Directory.
*/
DWORD snd_hash(const char *str);
/**
* @brief Computes the configured hash of a standard ASCII string, converting to
* lowercase.
* @note Use this for DLL names found in the PE Import Directory.
*/
DWORD snd_hash_lower(const char *str);
/**
* @brief Computes the configured hash of a wide string, converting to lowercase
* (Case-Insensitive).
* @note Use this for finding DLL module names in the PEB.
*/
DWORD snd_hash_wide_lower(const wchar_t *str);
SND_END_EXTERN_C
#endif // SND_COMMON_HASH_H
+305
View File
@@ -0,0 +1,305 @@
#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
+159
View File
@@ -0,0 +1,159 @@
#ifndef SND_COMMON_STATUS_H
#define SND_COMMON_STATUS_H
#include <windows.h>
#if SND_DEBUG
#include <stdarg.h>
#include <stdio.h>
#endif
#include <sindri/common/helpers.h>
SND_BEGIN_EXTERN_C
// Maximum length for error context strings (including null terminator)
#define SND_MAX_CTX_LEN 128
typedef enum _SND_STATUS_CODE {
SND_SUCCESS = 0,
SND_ERROR_GENERIC = -1,
SND_STATUS_INVALID_PARAMETER = -2,
SND_STATUS_INTEGER_OVERFLOW = -3,
SND_STATUS_UNSUPPORTED = -4,
SND_STATUS_NOT_INITIALIZED = -5,
// Command-line argument errors
SND_STATUS_MISSING_COMMAND_LINE_ARGS = 0x100,
SND_STATUS_INVALID_COMMAND_LINE_ARG,
// File / input errors
SND_STATUS_INVALID_PATH = 0x200,
SND_STATUS_FILE_CREATE_FAILED,
SND_STATUS_FILE_SIZE_QUERY_FAILED,
SND_STATUS_FILE_TOO_LARGE,
SND_STATUS_FILE_TOO_SMALL,
SND_STATUS_ALLOC_FAILED,
SND_STATUS_FILE_READ_FAILED,
// PE parsing / validation errors
SND_STATUS_DOS_HEADER_TRUNCATED = 0x300,
SND_STATUS_INVALID_DOS_SIGNATURE,
SND_STATUS_NT_HEADERS_TRUNCATED,
SND_STATUS_INVALID_NT_SIGNATURE,
SND_STATUS_INVALID_NT_HEADER_OFFSET,
SND_STATUS_UNSUPPORTED_OPTIONAL_HEADER_MAGIC,
SND_STATUS_INVALID_SECTION_COUNT,
SND_STATUS_SECTION_TABLE_OVERFLOW,
SND_STATUS_INVALID_SECTION_TABLE,
SND_STATUS_ARCH_MISMATCH,
SND_STATUS_ENTRY_POINT_NOT_FOUND,
SND_STATUS_INVALID_IMPORT_TABLE_SIZE,
SND_STATUS_INVALID_FILE_OFFSET,
SND_STATUS_DIRECTORY_NOT_FOUND,
// Reflective loading errors
SND_STATUS_SECTION_COPY_FAILED = 0x400,
SND_STATUS_RELOCATION_DIRECTORY_INVALID,
SND_STATUS_RELOCATION_PATCH_OUT_OF_RANGE,
SND_STATUS_RELOCATION_TYPE_UNSUPPORTED,
SND_STATUS_IMPORT_DIRECTORY_INVALID,
SND_STATUS_IMPORT_DLL_LOAD_FAILED,
SND_STATUS_IMPORT_THUNK_INVALID,
SND_STATUS_IMPORT_SYMBOL_RESOLVE_FAILED,
SND_STATUS_PROTECTION_UPDATE_FAILED,
SND_STATUS_DLL_INITIALIZATION_FAILED,
SND_STATUS_EXPORT_DIRECTORY_INVALID,
SND_STATUS_EXPORT_NOT_FOUND,
SND_STATUS_EXPORT_FORWARDER_UNSUPPORTED,
SND_STATUS_MISSING_DLL_EXPORT_NAME,
SND_STATUS_INVALID_STAGE_SEQUENCE,
SND_STATUS_CORRUPTED_STATE,
SND_FAILED_TO_EXECUTE,
// Syscall resolution errors
SND_STATUS_SSN_NOT_FOUND,
SND_STATUS_SSN_BUFFER_TOO_SMALL,
// PEB
SND_STATUS_PEB_MODULE_NOT_FOUND,
} snd_status_code_t;
typedef struct _SND_STATUS {
snd_status_code_t code;
int os_error;
#if SND_DEBUG
const char *file;
int line;
char context[SND_MAX_CTX_LEN];
#endif
} snd_status_t;
#if SND_DEBUG
static inline snd_status_t _snd_make_err(snd_status_code_t code, int os_error, const char *file, int line,
const char *fmt, ...) {
snd_status_t status = {code, os_error, file, line};
if (fmt != NULL) {
va_list args;
va_start(args, fmt);
vsnprintf(status.context, SND_MAX_CTX_LEN, fmt, args);
va_end(args);
} else {
status.context[0] = '\0';
}
return status;
}
static inline snd_status_t _snd_make_success() {
snd_status_t status = {SND_SUCCESS, 0, NULL, 0, {0}};
return status;
}
#define SND_OK _snd_make_success()
#define SND_ERR(c) _snd_make_err(c, 0, __FILE__, __LINE__, NULL)
#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_CTX(code, ...) _snd_make_err(code, GetLastError(), __FILE__, __LINE__, __VA_ARGS__)
#else
static inline snd_status_t _snd_make_success() {
snd_status_t status = {SND_SUCCESS, 0};
return status;
}
static inline snd_status_t _snd_make_err(snd_status_code_t code, int os_error) {
snd_status_t status = {code, os_error};
return status;
}
#define SND_OK _snd_make_success()
#define SND_ERR(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_CTX(code, ...) _snd_make_err(code, GetLastError())
#endif
/**
* @brief Converts a status structure into a human-readable string.
* @param status The status object to evaluate.
* @return Null-terminated string describing the status code.
*/
const char *snd_status_to_string(snd_status_t status);
/**
* @brief Prints a status structure, including context if available, to stderr.
* @param status The status object to print.
*/
void snd_status_print(snd_status_t status);
SND_END_EXTERN_C
#endif // SND_COMMON_STATUS_H
+92
View File
@@ -0,0 +1,92 @@
#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
+8
View File
@@ -0,0 +1,8 @@
#ifndef SND_LOADERS_H
#define SND_LOADERS_H
#include <sindri/loaders/knowndlls/knowndlls.h>
#include <sindri/loaders/reflective/chain.h>
#include <sindri/loaders/reflective/engine.h>
#endif // SND_LOADERS_H
@@ -0,0 +1,57 @@
#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
+36
View File
@@ -0,0 +1,36 @@
#ifndef SND_LOADERS_REFLECTIVE_CHAIN_H
#define SND_LOADERS_REFLECTIVE_CHAIN_H
#include <sindri/common/helpers.h>
#include <sindri/loaders/reflective/engine.h>
SND_BEGIN_EXTERN_C
/**
* @brief Executes the entire allocation and fixup chain.
*
* @param ctx Initialized loader context.
* @return SND_OK on success, otherwise the failing stage status.
* @note Wraps compatibility check, copy, relocation, imports, protections, and
* TLS.
*/
snd_status_t snd_prepare_reflective_image(snd_loader_ctx_t *ctx);
/**
* @brief Executes the loaded image entry point.
*
* @param ctx Prepared loader context.
* @return SND_OK on success, otherwise execution error.
*/
snd_status_t snd_execute_reflective_image(snd_loader_ctx_t *ctx);
/**
* @brief Cleans up and detaches the reflective image state.
*
* @param ctx Reflective loader context.
*/
void snd_detach_reflective_image(snd_loader_ctx_t *ctx);
SND_END_EXTERN_C
#endif // SND_LOADERS_REFLECTIVE_CHAIN_H
+150
View File
@@ -0,0 +1,150 @@
#ifndef SND_LOADERS_REFLECTIVE_ENGINE_H
#define SND_LOADERS_REFLECTIVE_ENGINE_H
#include <sindri/common/buffer.h>
#include <sindri/common/helpers.h>
#include <sindri/common/status.h>
#include <sindri/parsers/pe/pe_parser.h>
#include <sindri/primitives/os_api.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
typedef struct {
LPVOID virtual_base;
LONG_PTR delta_offset;
LPVOID entry_point;
SIZE_T allocated_size;
} snd_pe_target_t;
typedef enum {
SND_STAGE_UNINITIALIZED = 0,
SND_STAGE_PARSED,
SND_STAGE_MEM_ALLOCATED,
SND_STAGE_SECTIONS_MAPPED,
SND_STAGE_RELOCATED,
SND_STAGE_IMPORTS_RESOLVED,
SND_STAGE_READY_FOR_EXECUTION,
SND_STAGE_EXECUTED,
} snd_loader_stage_t;
typedef struct {
const snd_buffer_t *raw_source;
snd_pe_parser_t pe;
snd_pe_target_t target;
snd_loader_stage_t stage;
const snd_memory_api_t *mem_api;
const snd_module_api_t *mod_api;
} snd_loader_ctx_t;
/**
* @brief Portable macro to resolve and call a reflectively loaded DLL export
* with arbitrary arguments.
* @param ctx Pointer to the initialized loader context.
* @param name Plaintext string name of the target export function.
* @param signature The function pointer type signature to cast the export to.
* @param status_out A variable of type 'snd_status_t' that will receive the
* status result.
* @param ... The comma-separated arguments to pass directly to the
* target function.
*/
#define SND_CALL_EXPORT(ctx, name, signature, status_out, ...) \
do { \
FARPROC _proc = NULL; \
(status_out) = snd_get_proc_address((ctx), (name), &_proc); \
if ((status_out).code == SND_SUCCESS && _proc != NULL) { \
((signature)_proc)(__VA_ARGS__); \
} \
} while (0)
/**
* @brief Portable macro to resolve and call a reflectively loaded DLL export
* and capture its return value.
* @param ctx Pointer to the initialized loader context.
* @param name Plaintext string name of the target export function.
* @param signature The function pointer type signature to cast the export to.
* @param status_out A variable of type 'snd_status_t' that will receive the
* status result.
* @param ret_out Variable to store the return value of the export.
* @param ... The comma-separated arguments to pass directly to the
* target function.
*/
#define SND_CALL_EXPORT_RET(ctx, name, signature, status_out, ret_out, ...) \
do { \
FARPROC _proc = NULL; \
(status_out) = snd_get_proc_address((ctx), (name), &_proc); \
if ((status_out).code == SND_SUCCESS && _proc != NULL) { \
(ret_out) = ((signature)_proc)(__VA_ARGS__); \
} \
} while (0)
/**
* @brief Validates payload architecture.
* @param ctx The loader context with parsed PE.
* @return SND_OK on match, otherwise SND_STATUS_ARCH_MISMATCH.
*/
snd_status_t snd_compatibility_check(snd_loader_ctx_t *ctx);
/**
* @brief Allocates memory and copies sections.
* @param ctx The loader context. ctx->virtual_base will be populated on
* success.
* @return SND_OK on success, otherwise an allocation error.
*/
snd_status_t snd_allocate_and_copy_image(snd_loader_ctx_t *ctx);
/**
* @brief Applies base relocation fixups.
* @param ctx The loader context containing the mapped virtual base.
* @return SND_OK on success, otherwise a relocation error.
*/
snd_status_t snd_apply_relocations(snd_loader_ctx_t *ctx);
/**
* @brief Resolves imports and patches IAT.
* @param ctx The loader context.
* @return SND_OK on success, otherwise a resolution error.
*/
snd_status_t snd_resolve_imports(snd_loader_ctx_t *ctx);
/**
* @brief Applies final section page protections.
* @param ctx The loader context.
* @return SND_OK on success, otherwise a protection error.
*/
snd_status_t snd_apply_memory_protections(snd_loader_ctx_t *ctx);
/**
* @brief Executes TLS callbacks from a loaded image.
* @param ctx The loader context.
* @param reason The reason for the call (e.g., DLL_PROCESS_ATTACH).
*/
void snd_execute_tls_callbacks(snd_loader_ctx_t *ctx, DWORD reason);
/**
* @brief Resolves the entry point pointer.
* @param ctx The loader context.
* @return SND_OK on success, error code on failure.
*/
snd_status_t snd_get_entry_point(snd_loader_ctx_t *ctx);
/**
* @brief Resolves an exported symbol address.
* @param ctx The loader context.
* @param func_name Export name to resolve.
* @param func_addr_out Receives resolved export pointer.
* @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);
/**
* @brief Frees memory associated with the mapped reflective image.
* @param ctx The loader context.
*/
void snd_free_mapped_image(snd_loader_ctx_t *ctx);
SND_END_EXTERN_C
#endif // SND_LOADERS_REFLECTIVE_ENGINE_H
+6
View File
@@ -0,0 +1,6 @@
#ifndef SND_PARSERS_H
#define SND_PARSERS_H
#include <sindri/parsers/pe.h>
#endif // SND_PARSERS_H
+10
View File
@@ -0,0 +1,10 @@
#ifndef SND_PARSERS_PE_H
#define SND_PARSERS_PE_H
#include <sindri/parsers/pe/pe_exports.h>
#include <sindri/parsers/pe/pe_imports.h>
#include <sindri/parsers/pe/pe_parser.h>
#include <sindri/parsers/pe/pe_relocations.h>
#include <sindri/parsers/pe/pe_utils.h>
#endif
+63
View File
@@ -0,0 +1,63 @@
#ifndef SND_PARSERS_PE_EXPORTS_H
#define SND_PARSERS_PE_EXPORTS_H
#include "sindri_hashes.h"
#include <sindri/common/helpers.h>
#include <sindri/common/status.h>
#include <sindri/primitives/os_api.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
/**
* @brief Maximum recursion depth for resolving export forwarders.
* Prevents stack overflows from malicious or circular forwarder chains.
*/
#define SND_FWD_MAX_DEPTH 4
/**
* @brief Resolves the memory address of an exported function by name or
* ordinal.
*
* This function parses the Export Address Table (EAT). It supports resolving
* functions by name, by raw ordinal (using `INTRESOURCE`), and automatically
* handles NT Export Forwarders (e.g., forwarding a call to another DLL).
*
* @param base_address The base address of the mapped PE image.
* @param size The size of the mapped PE image. Can be
* `SND_SYS_DLL_SIZE_DEFAULT` to dynamically infer the size from the PE
* headers.
* @param func_name The name of the exported function, or a pointer-sized
* ordinal generated via the `MAKEINTRESOURCE` macro.
* @param func_addr_out Pointer that receives the resolved memory address on
* success.
* @param resolver Callback function used to load external dependencies if the
* requested export is a forwarder. If NULL, forwarder resolution will fail.
* @returns SND_OK on success, or an error status.
*/
snd_status_t snd_pe_get_export_address(PVOID base_address, SIZE_T size, const char *func_name, FARPROC *func_addr_out,
snd_module_resolver_cb resolver);
/**
* @brief Resolves the memory address of an exported function using a DJB2 hash.
*
* An API-hashing alternative to `snd_pe_get_export_address` for use in
* stealthy or position-independent contexts where plain-text strings are
* avoided.
*
* @param base_address The base address of the mapped PE image.
* @param size The size of the mapped PE image.
* @param func_hash The custom hash of the exported function name.
* @param func_addr_out Pointer that receives the resolved memory address on
* success.
* @param resolver Callback function used to resolve external dependencies via
* hash if the requested export is a forwarder. If NULL, forwarders fail.
* @returns SND_OK on success, or an error status.
*/
snd_status_t snd_pe_get_export_address_by_hash(PVOID base_address, SIZE_T size, DWORD func_hash, FARPROC *func_addr_out,
snd_module_resolver_hash_cb resolver);
SND_END_EXTERN_C
#endif // SND_PARSERS_PE_EXPORTS_H
+43
View File
@@ -0,0 +1,43 @@
#ifndef SND_PARSERS_PE_IMPORTS_H
#define SND_PARSERS_PE_IMPORTS_H
#include <sindri/common/helpers.h>
#include <sindri/common/status.h>
#include <sindri/parsers/pe/pe_parser.h>
#include <sindri/primitives/os_api.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
/**
* @name PE Ordinal Flag Masks
* @brief Utilities for parsing `IMAGE_THUNK_DATA` entries within the Import
* Name Table (INT). According to the Portable Executable specification, if the
* highest-order bit (MSB) of a thunk entry is set, the function is imported by
* its numerical ordinal rather than by a string name.
*/
// Checks if a 32-bit thunk entry matches an import-by-ordinal (MSB is set).
#define SND_PE_SNAP_BY_ORDINAL32(ordinal) ((ordinal) & 0x80000000)
// Checks if a 64-bit thunk entry matches an import-by-ordinal (MSB is set).
#define SND_PE_SNAP_BY_ORDINAL64(ordinal) ((ordinal) & 0x8000000000000000ULL)
// Strips the ordinal flag bit to extract the clean, 16-bit numerical ordinal
// ID.
#define SND_PE_ORDINAL(ordinal) ((ordinal) & 0xFFFF)
/**
* @brief Resolves the imports of a PE image by loading the required modules and
* resolving their function addresses.
* @param base_address The base memory address where the PE is mapped.
* @param mod_api Pointer to the module API functions to use for resolving
* imports.
* @param parser Pointer to the PE parser to use for resolving imports.
* @return SND_OK on success, otherwise an error status.
*/
snd_status_t snd_pe_resolve_imports(PVOID base_address, const snd_module_api_t *mod_api, const snd_pe_parser_t *parser);
SND_END_EXTERN_C
#endif // SND_PARSERS_PE_IMPORTS_H
+120
View File
@@ -0,0 +1,120 @@
#ifndef SND_PARSERS_PE_PARSER_H
#define SND_PARSERS_PE_PARSER_H
#include <sindri/common/buffer.h>
#include <sindri/common/helpers.h>
#include <sindri/common/status.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
/**
* @brief Access utility for clean cross-bitness reads inside the
* implementation.
*/
#define SND_PE_GET_NT_FIELD(parser, field) ((parser)->is_64bit ? (parser)->nt.nt64->field : (parser)->nt.nt32->field)
/**
* @warning SAFE HEADER BOOTSTRAP WINDOW & RESIDUAL RISK CAUTIONS
* This macro establishes a conservative 4KB (one virtual page) size limit.
* It serves as a secure bootstrap boundary for parsing the initial PE headers
* of in-memory or system DLLs where the total virtual size is not yet verified.
*
* Enforcing a 4KB window guarantees that if the target headers are
* intentionally truncated, malformed, or weaponized, the parser will fail
* cleanly inside the first page instead of blindly wandering out-of-bounds into
* unmapped memory.
*
* ! CRITICAL MEMORY FOOTPRINT REQUISITE:
* This approach assumes the underlying memory region has AT LEAST 4KB of valid
* backing allocation. If the source buffer was allocated with exact bounds
* smaller than 4KB (e.g., exactly 3KB) and a malformed header field references
* an offset within the unallocated window (e.g., 3.5KB), this 4KB boundary
* check will erroneously pass, triggering a physical Out-of-Bounds read.
*
* ! TRUST PARADOX (SizeOfImage Assumption):
* By implementing this bootstrap, the loader is forced to expand its tracking
* boundaries by trusting the payload's own `OptionalHeader.SizeOfImage` field.
* If an adversary feeds the loader an image with a fake, hyper-inflated size
* field, the parser will adopt it blindly. You must ensure downstream consumers
* of this parser handle an oversized virtual layout gracefully.
*
* ! CRITICAL LIFECYCLE MANDATE:
* Immediately after `snd_pe_parse` succeeds, you MUST extract the actual
* `OptionalHeader.SizeOfImage` and update the parser context's buffer size
* to encompass the full image layout. Leaving it clamped at 0x1000 will cause
* all subsequent directory parsing (imports, exports, relocations) to be
* rejected as out-of-bounds.
*/
#define SND_SYS_DLL_SIZE_DEFAULT 0x1000
/**
* @brief Context structure for the PE parser.
* @note Pointers inside this structure (dos, nt, section_head) point directly
* into the memory space backed by the `source` buffer. They do not own memory
* and their lifecycles are tied entirely to the validity of the underlying
* `source`.
*/
typedef struct {
snd_buffer_t source; // Backing buffer containing the raw or mapped PE data.
PIMAGE_DOS_HEADER dos;
union {
PIMAGE_NT_HEADERS32 nt32;
PIMAGE_NT_HEADERS64 nt64;
} nt;
PIMAGE_SECTION_HEADER
section_head; // Points to the first element in the section header array.
/**
* @brief Points to the COFF String Table (used for long section names).
* @note This is NULL for mapped/loaded images, as the string table is rarely
* copied to virtual memory space during manual mapping.
*/
BYTE *string_table;
BOOL is_64bit;
BOOL is_dll;
/**
* @brief Tracks the structural state of the `source` buffer.
* TRUE if the payload is aligned to its virtual memory layout (PE Sections
* mapped to VirtualAddresses). FALSE if the payload is in its raw disk layout
* (PE Sections mapped to PointerToRawData).
*/
BOOL is_mapped;
DWORD sections_count;
DWORD imports_rva;
DWORD import_size;
} snd_pe_parser_t;
/**
* @brief Parses a raw or mapped PE file buffer into a parsed PE representation.
*
* This function serves as the primary entry point for payload validation. It
* executes the initial bootstrap parse, validating the Magic DOS signature (MZ)
* and the NT headers signature (PE). Based on the headers, it dynamically sets
* the context bitness (32-bit vs 64-bit) and maps internal pointers differently
* depending on whether the source buffer is formatted as a raw disk file or an
* already aligned virtual image.
*
* @note This function performs **zero-copy parsing**. The pointers populated
* inside the `parser` context point directly into the memory space owned by the
* input `buf`. No heap allocations are performed. Consequently, the lifecycle
* of the populated `parser` structure is strictly bound to the lifetime and
* validity of the underlying `buf`.
*
* @param buf Raw buffer containing the PE file data.
* @param is_mapped TRUE if the buffer represents an already virtually aligned
* image, FALSE if it is a raw disk file layout.
* @param parser Pointer to the parser context to populate.
* @return SND_OK on success, or a contextual error status.
*/
snd_status_t snd_pe_parse(const snd_buffer_t *buf, BOOL is_mapped, snd_pe_parser_t *parser);
SND_END_EXTERN_C
#endif // SND_PARSERS_PE_PARSER_H
@@ -0,0 +1,35 @@
#ifndef SND_PARSERS_PE_RELOCATIONS_H
#define SND_PARSERS_PE_RELOCATIONS_H
#include <sindri/common/helpers.h>
#include <sindri/common/status.h>
#include <sindri/parsers/pe/pe_parser.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
/**
* @brief Applies base relocations to a virtually mapped PE image.
*
* This function iterates through the Base Relocation Table (if present) and
* patches hardcoded absolute memory addresses to align with the actual runtime
* base address where the image was mapped.
*
* @note The memory mapped at `base_address` must have at least `PAGE_READWRITE`
* permissions before calling this function, as it physically overwrites memory
* inside the `.text` and `.data` sections.
*
* @param base_address The actual runtime base memory address where the PE is
* currently mapped.
* @param delta_offset The mathematical difference between the actual mapped
* base address and the payload's preferred base address
* (`OptionalHeader.ImageBase`). If this is 0, the function exits early.
* @param parser Pointer to the validated PE parser context.
* @return SND_OK on success, or a contextual error if the payload cannot be
* relocated (e.g., RELOCS_STRIPPED is set).
*/
snd_status_t snd_pe_apply_relocations(PVOID base_address, LONG_PTR delta_offset, const snd_pe_parser_t *parser);
SND_END_EXTERN_C
#endif // SND_PARSERS_PE_RELOCATIONS_H
+69
View File
@@ -0,0 +1,69 @@
#ifndef SND_PARSERS_PE_UTILS_H
#define SND_PARSERS_PE_UTILS_H
#include <sindri/common/helpers.h>
#include <sindri/common/status.h>
#include <sindri/parsers/pe/pe_parser.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
#define SND_PE_MIN_FILE_ALIGNMENT 512
/**
* @brief Validates payload architecture compatibility.
* @param is_64bit TRUE if the payload is 64-bit, FALSE otherwise.
* @return TRUE if the architectures match, FALSE otherwise.
*/
BOOL snd_pe_compatibility_check(BOOL is_64bit);
/**
* @brief Retrieves the absolute memory address of the PE Entry Point.
*
* @param parser The parsed PE image.
* @return Pointer to the entry point, or NULL if invalid/not found.
*/
void *snd_pe_get_entry_point(const snd_pe_parser_t *parser);
/**
* @brief Retrieves the TLS callbacks from the PE image.
*
* @param base_address The base memory address of the PE image.
* @param parser The parsed PE image.
* @return A pointer to the TLS callbacks, or NULL if not found.
*/
PVOID snd_pe_get_tls_callbacks(PVOID base_address, const snd_pe_parser_t *parser);
/**
* @brief Executes TLS callbacks.
* @param virtual_base The base address where the image is mapped.
* @param parser The parsed PE image.
* @param reason The reason for calling the TLS callbacks (e.g.,
* DLL_PROCESS_ATTACH).
* @return A status indicating success or failure.
*/
snd_status_t snd_pe_execute_tls_callbacks(PVOID virtual_base, const snd_pe_parser_t *parser, DWORD reason);
/**
* @brief Abstracted address translation. Converts an RVA to a mapped or file
* pointer.
* @param parser The parsed PE image.
* @param rva The Relative Virtual Address to convert.
* @param size The size of the expected data at the RVA for bounds checking.
* @return A direct pointer to the data, or NULL if out of bounds/invalid.
*/
PVOID snd_pe_rva_to_ptr(const snd_pe_parser_t *parser, DWORD rva, SIZE_T size);
/**
* @brief Safely retrieves a PE Data Directory.
* @param parser The parsed PE image.
* @param index The directory index (e.g. IMAGE_DIRECTORY_ENTRY_IMPORT).
* @param dir_out Output pointer to store the retrieved directory.
* @return SND_OK on success, SND_STATUS_DIRECTORY_NOT_FOUND or other
* errors.
*/
snd_status_t snd_pe_get_directory(const snd_pe_parser_t *parser, DWORD index, IMAGE_DATA_DIRECTORY *dir_out);
SND_END_EXTERN_C
#endif // SND_PARSERS_PE_UTILS_H
+13
View File
@@ -0,0 +1,13 @@
#ifndef SND_PRIMITIVES_H
#define SND_PRIMITIVES_H
#include <sindri/primitives/ffi.h>
#include <sindri/primitives/heavens_gate.h>
#include <sindri/primitives/memory.h>
#include <sindri/primitives/modules.h>
#include <sindri/primitives/ntdll.h>
#include <sindri/primitives/os_api.h>
#include <sindri/primitives/peb.h>
#include <sindri/primitives/syscalls.h>
#endif // SND_PRIMITIVES_H
+31
View File
@@ -0,0 +1,31 @@
#ifndef SND_PRIMITIVES_FFI_H
#define SND_PRIMITIVES_FFI_H
#include <sindri/common/helpers.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
/**
* @brief Invokes an arbitrary function pointer at runtime using a generic
* argument array.
*
* @param pFunctionAddress Pointer to the target function. Must not be NULL;
* passing NULL causes an immediate return of 0.
* @param dwArgCount Number of entries in @p pArgs. May be 0 for
* zero-argument calls.
* @param pArgs Array of @p dwArgCount UINT_PTR-sized arguments.
* Ignored (and may be NULL) when @p dwArgCount is 0.
*
* @return The value returned by the invoked function as a UINT_PTR, or 0 if
* @p pFunctionAddress is NULL or the current architecture is unsupported.
*
* @note Callee-saved registers (RBX, RBP, RDI, RSI, R12-R15) are preserved
* by the assembly bridge. The caller is responsible for ensuring that
* the argument types and count are compatible with the target function.
*/
UINT_PTR snd_execute_dynamic(PVOID pFunctionAddress, DWORD dwArgCount, const UINT_PTR *pArgs);
SND_END_EXTERN_C
#endif // SND_PRIMITIVES_FFI_H
+33
View File
@@ -0,0 +1,33 @@
#ifndef SND_PRIMITIVES_HEAVENS_GATE_H
#define SND_PRIMITIVES_HEAVENS_GATE_H
#include <sindri/common/helpers.h>
#include <sindri/common/status.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
/**
* @brief Checks if the current 32-bit process is running under WOW64 (64-bit
* OS).
* @note This reads the WOW32Reserved field directly from the TEB.
* @return TRUE if running under WOW64, FALSE otherwise.
*/
BOOL snd_is_wow64(void);
/**
* @brief Invokes a 64-bit function from a 32-bit WOW64 process using Heaven's
* Gate.
*
* @param pFunctionAddress The 64-bit virtual address of the target function.
* @param dwArgCount Number of arguments to pass to the 64-bit function (Max:
* 6).
* @param pArgs Array of 64-bit arguments.
* @param pResult Pointer to store the 64-bit return value (RAX).
* @return SND_OK on success, or an appropriate error code if unsupported.
*/
snd_status_t snd_hg_execute_64(UINT64 pFunctionAddress, DWORD dwArgCount, const UINT64 *pArgs, UINT64 *pResult);
SND_END_EXTERN_C
#endif // SND_PRIMITIVES_HEAVENS_GATE_H
+15
View File
@@ -0,0 +1,15 @@
#ifndef SND_PRIMITIVES_MEMORY_H
#define SND_PRIMITIVES_MEMORY_H
#include <sindri/common/helpers.h>
#include <sindri/primitives/os_api.h>
SND_BEGIN_EXTERN_C
// Expose globally available, ready-to-use WinAPI capabilities
extern const snd_memory_api_t snd_mem_win;
extern const snd_memory_api_t snd_mem_native;
SND_END_EXTERN_C
#endif // SND_PRIMITIVES_MEMORY_H
+15
View File
@@ -0,0 +1,15 @@
#ifndef SND_PRIMITIVES_MODULES_H
#define SND_PRIMITIVES_MODULES_H
#include <sindri/common/helpers.h>
#include <sindri/primitives/os_api.h>
SND_BEGIN_EXTERN_C
// Expose globally available, ready-to-use WinAPI capabilities
extern const snd_module_api_t snd_mod_win;
extern const snd_module_api_t snd_mod_native;
SND_END_EXTERN_C
#endif // SND_PRIMITIVES_MODULES_H
+23
View File
@@ -0,0 +1,23 @@
#ifndef SND_PRIMITIVES_NTDLL_H
#define SND_PRIMITIVES_NTDLL_H
#include <sindri/common/helpers.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
/**
* @brief Sets the global base address of the ntdll.dll module.
* @param ntdll_base The base address of the ntdll module.
*/
void snd_set_ntdll(PVOID ntdll_base);
/**
* @brief Retrieves the global base address of the ntdll.dll module.
* @return The base address of the ntdll module, or NULL if not set.
*/
PVOID snd_get_ntdll(void);
SND_END_EXTERN_C
#endif // SND_PRIMITIVES_NTDLL_H
+52
View File
@@ -0,0 +1,52 @@
#ifndef SND_PRIMITIVES_OS_API_H
#define SND_PRIMITIVES_OS_API_H
#include <sindri/common/helpers.h>
#include <sindri/common/status.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
// Memory Capabilities
typedef snd_status_t(WINAPI *snd_memory_alloc_cb)(LPVOID address, SIZE_T size, DWORD allocation_type, DWORD protect,
LPVOID *out_address);
typedef snd_status_t(WINAPI *snd_memory_free_cb)(LPVOID address, SIZE_T size, DWORD free_type);
typedef snd_status_t(WINAPI *snd_memory_protect_cb)(LPVOID address, SIZE_T size, DWORD new_protect, DWORD *old_protect);
// Module Capabilities
typedef snd_status_t(WINAPI *snd_module_load_cb)(const char *module_name, HMODULE *out_module);
typedef snd_status_t(WINAPI *snd_module_get_proc_cb)(HMODULE hModule, const char *proc_name, FARPROC *out_proc);
typedef snd_status_t(WINAPI *snd_module_resolver_cb)(const wchar_t *module_name, PVOID *out_base);
// Module Capabilities (Hashes)
typedef snd_status_t(WINAPI *snd_module_load_hash_cb)(DWORD module_hash, HMODULE *out_module);
typedef snd_status_t(WINAPI *snd_module_get_proc_hash_cb)(HMODULE hModule, DWORD proc_hash, FARPROC *out_proc);
typedef snd_status_t(WINAPI *snd_module_resolver_hash_cb)(DWORD module_hash, PVOID *out_base);
/**
* @brief Local Memory Management API table.
*/
typedef struct {
snd_memory_alloc_cb alloc;
snd_memory_free_cb free;
snd_memory_protect_cb protect;
} snd_memory_api_t;
/**
* @brief Module and Import Resolution API table.
*/
typedef struct {
// String-based resolution
snd_module_load_cb load_library;
snd_module_get_proc_cb get_proc_address;
snd_module_resolver_cb get_module_base;
// Hash-based resolution
snd_module_load_hash_cb load_library_hash;
snd_module_get_proc_hash_cb get_proc_address_hash;
snd_module_resolver_hash_cb get_module_base_hash;
} snd_module_api_t;
SND_END_EXTERN_C
#endif // SND_PRIMITIVES_OS_API_H
+29
View File
@@ -0,0 +1,29 @@
#ifndef SND_PEB_H
#define SND_PEB_H
#include "sindri_hashes.h"
#include <sindri/common/helpers.h>
#include <sindri/common/status.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
/**
* @brief Safely locates the base address of a loaded module by walking the PEB.
* @param module_name Case-insensitive target name (e.g., L"ntdll.dll")
* @return Base pointer to the module, or NULL if not found.
*/
snd_status_t WINAPI snd_peb_get_module_base(const wchar_t *module_name, PVOID *out_base);
/**
* @brief Safely locates the base address of a loaded module by walking the PEB
* and comparing hashes.
* @param module_hash The hash of the target name.
* @return Base pointer to the module, or NULL if not found.
*/
snd_status_t WINAPI snd_peb_get_module_base_by_hash(DWORD module_hash, PVOID *out_base);
SND_END_EXTERN_C
#endif // SND_PEB_H
+81
View File
@@ -0,0 +1,81 @@
#ifndef SND_PRIMITIVES_SYSCALLS_COMMON_H
#define SND_PRIMITIVES_SYSCALLS_COMMON_H
#include <sindri/common/helpers.h>
#include <sindri/common/status.h>
#include <sindri/internal/nt_defs.h>
#include <sindri/primitives/os_api.h>
#include <windows.h>
SND_BEGIN_EXTERN_C
/**
* @brief Syscall entry structure holding the address, hash, and syscall number.
*/
typedef struct {
PVOID pAddress;
DWORD dwHash;
WORD wSystemCall;
} snd_syscall_entry_t;
/**
* @brief Arguments struct passed to the generic ASM syscall invoker.
*/
typedef struct {
WORD ssn;
PVOID arg1;
PVOID arg2;
PVOID arg3;
PVOID arg4;
PVOID arg5;
PVOID arg6;
PVOID arg7;
PVOID arg8;
PVOID arg9;
PVOID arg10;
} snd_syscall_args_t;
/**
* @brief Universal function signature for any syscall resolution strategy.
* 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);
snd_status_t snd_hell_extract_syscall(PVOID ntdll_base, DWORD func_hash, snd_syscall_entry_t *entry_out);
snd_status_t snd_halo_extract_syscall(PVOID ntdll_base, DWORD func_hash, snd_syscall_entry_t *entry_out);
snd_status_t snd_tartarus_extract_syscall(PVOID ntdll_base, DWORD func_hash, snd_syscall_entry_t *entry_out);
snd_status_t snd_veles_extract_syscall(PVOID ntdll_base, DWORD func_hash, snd_syscall_entry_t *entry_out);
/**
* @brief Sets the primary syscall resolution strategy using a function pointer.
* @param resolver The resolver function to use as the primary strategy.
*/
void snd_set_syscall_strategy(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_add_syscall_strategy(snd_syscall_resolver_t resolver);
/**
* @brief Core resolution entrypoint (evaluates the internal fallback chain
* automatically).
*
* @param func_hash The hash of the target syscall function name.
* @param entry_out Pointer to the entry structure to populate.
* @return SND_OK on success, or an error code if resolution fails.
*/
snd_status_t snd_resolve_syscall(DWORD func_hash, snd_syscall_entry_t *entry_out);
/**
* @brief ASM stub for invoking syscalls.
* @param args Pointer to the syscall arguments structure.
* @return The NTSTATUS returned by the syscall.
*/
extern NTSTATUS snd_invoke_syscall_asm(snd_syscall_args_t *args);
SND_END_EXTERN_C
#endif // SND_PRIMITIVES_SYSCALLS_COMMON_H

Some files were not shown because too many files have changed in this diff Show More