mirror of
https://github.com/youssefnoob003/SindriKit
synced 2026-07-07 21:57:09 +00:00
You're Gonna Carry That Weight
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.*
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.*
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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 4–5 contain the SSN as a little-endian `WORD` and are extracted directly. On x86 WOW64, the analogous check is a leading `0xB8` (`mov eax, <SSN>`) at offset 0.
|
||||
|
||||
**Original research:** [Hell's Gate](https://github.com/am0nsec/HellsGate) by am0nsec & smelly\_\_vx
|
||||
|
||||
---
|
||||
|
||||
## Halo's Gate (`snd_halo_extract_syscall`)
|
||||
|
||||
Hell's Gate fails when the target stub has been patched. A typical userland hook overwrites the first bytes of the stub with a `JMP` (`0xE9`) redirect to the EDR's trampoline.
|
||||
|
||||
When Halo's Gate detects this (`p[0] == 0xE9`), it abandons the hooked stub and searches adjacent stubs in memory. Syscall stubs for neighboring exports are allocated contiguously in the export table and their SSNs are sequential. By walking up and down from the hooked stub until a clean, pattern-matching neighbor is found, the correct SSN is inferred by applying the neighbor's index offset.
|
||||
|
||||
**Original research:** [Halo's Gate](https://blog.sektor7.net/#!res/2021/halosgate.md) by Sektor7
|
||||
|
||||
---
|
||||
|
||||
## Tartarus' Gate (`snd_tartarus_extract_syscall`)
|
||||
|
||||
Extends Halo's Gate to cover a second hook placement pattern. Some EDR products patch stubs with a short jump (`0xEB`, a single-byte relative `JMP`) rather than the full 5-byte `0xE9` variant.
|
||||
|
||||
Tartarus' Gate detects both (`p[0] == 0xE9 || p[0] == 0xEB`) before delegating to the same neighbor-search logic, ensuring coverage of EDR hooks that Halo's Gate alone would miss.
|
||||
|
||||
**Original research:** [Tartarus' Gate](https://github.com/trickster0/TartarusGate) by trickster0
|
||||
|
||||
---
|
||||
|
||||
## VelesReek (`snd_veles_extract_syscall`)
|
||||
|
||||
An alternative SSN recovery approach that locates the `syscall` instruction (`0x0F 0x05`) itself within the stub rather than relying solely on the function prologue pattern.
|
||||
|
||||
On hooked stubs where the prologue has been overwritten, the `syscall` instruction and its immediate context can still anchor SSN derivation. Like the Gate variants, SindriKit's implementation also covers `0xE9`, `0xEB`, and (on x86) `0xE8` jump prefixes by falling through to the shared neighbor-search routine.
|
||||
|
||||
**Original research:** [VelesReek](https://github.com/klezvirus/VelesReek) by klezvirus
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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` | Fowler–Noll–Vo 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
|
||||
...
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user