You're Gonna Carry That Weight

This commit is contained in:
sibouzitoun
2026-06-22 12:38:04 +01:00
commit 5e26626615
162 changed files with 10836 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# Common Infrastructure
This directory documents the shared, domain-agnostic utilities that underpin the entire SindriKit framework. These headers live under `include/sindri/common/` and provide CRT-independent primitives, buffer lifecycle management, API hashing, and the diagnostic status system.
## Table of Contents
- [infrastructure.md](infrastructure.md)
Conceptual breakdown of how SindriKit achieves full CRT independence, how buffer bounds tracking prevents memory corruption, and how the compile-time hashing pipeline eliminates plaintext strings.
- [api_reference.md](api_reference.md)
Complete API documentation for buffers, hashing, CRT replacement primitives, disk I/O, and the debug output system.
+353
View File
@@ -0,0 +1,353 @@
# Common: API Reference
This page documents the public API surface of the shared infrastructure headers under `include/sindri/common/`.
---
## Buffer Management (`sindri/common/buffer.h`)
### `snd_buffer_t`
Tracked memory buffer structure. Pairs a data pointer with its size and an optional deallocation callback.
| Field | Type | Description |
|---|---|---|
| `data` | `LPVOID` | Pointer to the memory block |
| `size` | `SIZE_T` | Size of the memory block in bytes |
| `free_routine` | `snd_free_cb` | Optional callback invoked by `snd_buffer_free` to release the memory |
---
### `snd_buffer_init`
Initializes a buffer structure with tracking metadata.
| Parameter | Type | Description |
|---|---|---|
| `buf` | `snd_buffer_t*` | Buffer structure to initialize |
| `data` | `LPVOID` | Pointer to the memory block |
| `size` | `SIZE_T` | Size of the memory block |
| `free_routine` | `snd_free_cb` | Optional cleanup callback; may be NULL |
**Returns:** `void`
---
### `snd_buffer_free`
Invokes the buffer's assigned `free_routine` (if non-NULL) and zeroes the structure fields.
| Parameter | Type | Description |
|---|---|---|
| `buf` | `snd_buffer_t*` | Buffer to free and zero |
**Returns:** `void`
---
### Pre-built Free Routines
| Function | Backend | Use Case |
|---|---|---|
| `snd_buffer_free_heap` | `HeapFree(GetProcessHeap(), ...)` | Buffers allocated via `HeapAlloc` |
| `snd_buffer_free_virtual` | `VirtualFree(..., MEM_RELEASE)` | Buffers allocated via `VirtualAlloc` |
| `snd_buffer_free_mapped` | `UnmapViewOfFile(...)` | Buffers created via `MapViewOfFile` |
---
## Bounds Checking (`sindri/common/helpers.h`)
All bounds-checking functions are force-inlined to eliminate call overhead.
### `snd_bounds_check`
Pure arithmetic bounds validation. Returns `TRUE` if `offset + size` fits within `total_size`.
| Parameter | Type | Description |
|---|---|---|
| `total_size` | `SIZE_T` | Total size of the region |
| `offset` | `SIZE_T` | Offset from the start |
| `size` | `SIZE_T` | Size of the sub-region to validate |
**Returns:** `BOOL`
---
### `snd_buffer_bounds_check`
Validates an `(offset, size)` pair against a tracked `snd_buffer_t`. Returns `FALSE` if the buffer is NULL, empty, or the region exceeds bounds.
| Parameter | Type | Description |
|---|---|---|
| `buf` | `const snd_buffer_t*` | Tracked buffer to validate against |
| `offset` | `SIZE_T` | Offset from `buf->data` |
| `size` | `SIZE_T` | Size of the sub-region |
**Returns:** `BOOL`
---
### `snd_ptr_bounds_check`
Validates an arbitrary pointer against a known base region. Computes the offset from `base` to `ptr` and delegates to `snd_bounds_check`.
| Parameter | Type | Description |
|---|---|---|
| `base` | `const void*` | Base pointer of the known region |
| `total_size` | `SIZE_T` | Total size of the region |
| `ptr` | `const void*` | Target pointer to validate |
| `size` | `SIZE_T` | Size of the access at the target pointer |
**Returns:** `BOOL`
---
## CRT Replacement Primitives (`sindri/common/helpers.h`)
### `snd_memzero`
Zeroes `size` bytes at `dest` using a `volatile BYTE*` loop to prevent compiler elision.
| Parameter | Type | Description |
|---|---|---|
| `dest` | `void*` | Destination pointer |
| `size` | `size_t` | Number of bytes to zero |
---
### `snd_memcpy`
Copies `count` bytes from `src` to `dest`. No alignment assumptions.
| Parameter | Type | Description |
|---|---|---|
| `dest` | `void*` | Destination pointer |
| `src` | `const void*` | Source pointer |
| `count` | `size_t` | Number of bytes to copy |
---
### `snd_strnlen`
Returns the length of `str`, up to a maximum of `max_len`.
| Parameter | Type | Description |
|---|---|---|
| `str` | `const char*` | Input string |
| `max_len` | `size_t` | Maximum characters to scan |
**Returns:** `size_t`
---
### `snd_strncpy`
Copies up to `max_src_len` characters from `src` into `dest`, always null-terminating. Never writes more than `dest_size` bytes.
| Parameter | Type | Description |
|---|---|---|
| `dest` | `char*` | Destination buffer |
| `dest_size` | `size_t` | Total size of the destination buffer |
| `src` | `const char*` | Source string |
| `max_src_len` | `size_t` | Maximum characters to copy from source |
---
### `snd_strncat`
Appends up to `max_src_len` characters from `src` to `dest`, respecting `dest_size` and always null-terminating.
| Parameter | Type | Description |
|---|---|---|
| `dest` | `char*` | Destination buffer (must already be null-terminated) |
| `dest_size` | `SIZE_T` | Total size of the destination buffer |
| `src` | `const char*` | Source string to append |
| `max_src_len` | `SIZE_T` | Maximum characters to append |
---
### `snd_strnchr`
Searches for character `c` within the first `max_len` characters of `str`.
| Parameter | Type | Description |
|---|---|---|
| `str` | `const char*` | String to search |
| `c` | `char` | Character to find |
| `max_len` | `SIZE_T` | Maximum characters to scan |
**Returns:** `const char*` — pointer to the first occurrence, or `NULL`.
---
### `snd_strncmp`
Compares up to `max_len` characters of `s1` and `s2`.
| Parameter | Type | Description |
|---|---|---|
| `s1` | `const char*` | First string |
| `s2` | `const char*` | Second string |
| `max_len` | `SIZE_T` | Maximum characters to compare |
**Returns:** `int``<0` if `s1 < s2`, `0` if equal, `>0` if `s1 > s2`.
---
## Hashing (`sindri/common/hash.h`)
### `snd_hash`
Computes the configured hash of an ASCII string. Case-sensitive.
| Parameter | Type | Description |
|---|---|---|
| `str` | `const char*` | Null-terminated ASCII string |
**Returns:** `DWORD` — computed hash value.
**Primary use:** Hashing export names from the PE Export Address Table (e.g., `NtAllocateVirtualMemory`).
---
### `snd_hash_lower`
Computes the configured hash of an ASCII string after lowering each character.
| Parameter | Type | Description |
|---|---|---|
| `str` | `const char*` | Null-terminated ASCII string |
**Returns:** `DWORD` — computed hash value.
**Primary use:** Hashing DLL names from the PE Import Directory (e.g., `KERNEL32.dll`).
---
### `snd_hash_wide_lower`
Computes the configured hash of a wide (UTF-16) string after lowering each character.
| Parameter | Type | Description |
|---|---|---|
| `str` | `const wchar_t*` | Null-terminated wide string |
**Returns:** `DWORD` — computed hash value.
**Primary use:** Hashing module names from the PEB's `InMemoryOrderModuleList` (e.g., `L"ntdll.dll"`).
---
## Disk I/O (`sindri/common/disk.h`)
### `snd_buffer_load_from_disk`
Reads an entire file into a heap-allocated `snd_buffer_t`. The buffer's `free_routine` is set to `snd_buffer_free_heap`.
| Parameter | Type | Description |
|---|---|---|
| `path` | `const char*` | Null-terminated file path |
| `out_buf` | `snd_buffer_t*` | Output buffer; must be zeroed before the call |
**Returns:** `snd_status_t``SND_OK` on success, or an I/O error (`SND_STATUS_INVALID_PATH`, `SND_STATUS_FILE_CREATE_FAILED`, `SND_STATUS_FILE_READ_FAILED`, etc.).
> [!NOTE]
> The caller is responsible for releasing the buffer via `snd_buffer_free(out_buf)`.
---
## Debug Output (`sindri/common/helpers.h`)
### `SND_DEBUG_PRINT(fmt, ...)`
Macro that emits a formatted debug string. Compiles to a no-op when `SND_DEBUG=0`.
### `SND_FDEBUG_PRINT(stream, fmt, ...)`
Macro that emits a formatted debug string to a specific file stream. Only meaningful when `SND_USE_PRINTF=1`; otherwise routes to `OutputDebugStringA`.
### `snd_dump_hex`
Prints a combined hexadecimal and ASCII view of a byte buffer. Output is gated by `SND_DEBUG`.
| Parameter | Type | Description |
|---|---|---|
| `dat` | `const void*` | Pointer to the data to dump |
| `len_dat` | `size_t` | Number of bytes to dump |
| `base_off` | `ULONG_PTR` | Base address for computing printed offsets |
### `SND_DEBUG_MAX_LEN`
**Value:** `1024`
Maximum length of a formatted debug string buffer used internally by `snd_debug_print_internal`.
---
### `snd_debug_print_internal`
Internal function invoked by `SND_DEBUG_PRINT` and `SND_FDEBUG_PRINT` to safely format and print debug strings up to `SND_DEBUG_MAX_LEN`. Stripped when `SND_DEBUG=0`.
---
## Status System (`sindri/common/status.h`)
### `SND_MAX_CTX_LEN`
**Value:** `128`
Maximum length of the formatted error context string embedded within the `snd_status_t` structure.
---
### `snd_status_print`
Translates a `snd_status_t` code into a human-readable string using the framework's global status translation table. Also prints the context buffer if one is present.
| Parameter | Type | Description |
|---|---|---|
| `status` | `snd_status_t` | The status code/struct to translate and print |
**Returns:** `void`
---
## Internal NT Definitions (`sindri/internal/nt_defs.h`)
While primarily internal, the following constants and macros are explicitly defined to avoid dependency on the massive `<winternl.h>` header.
### `SND_PAGE_SIZE`
**Value:** `0x1000` (4096 bytes)
Standard x86/x64 memory page size.
### `SND_OBJ_CASE_INSENSITIVE`
**Value:** `0x00000040L`
Object Manager flag indicating case-insensitive string evaluation.
### `SND_InitializeObjectAttributes`
Macro that zeroes and initializes an `OBJECT_ATTRIBUTES` structure with the provided Root Directory, Object Name, and Attributes. Mirrors the standard `InitializeObjectAttributes` macro.
---
## Utility Macros (`sindri/common/helpers.h`)
### `SND_PTR_ADD(base, offset)`
Safely adds a byte offset to a base pointer by casting through `BYTE*`.
```c
PVOID target = SND_PTR_ADD(image_base, rva);
```
### `SND_FORCE_INLINE`
Compiler-agnostic macro for forcing function inlining. Expands to `static __forceinline` on MSVC and `static inline __attribute__((always_inline))` on GCC/Clang.
### `SND_BEGIN_EXTERN_C` / `SND_END_EXTERN_C`
C++ linkage compatibility wrappers. Expand to `extern "C" { ... }` when compiled as C++, empty otherwise.
+98
View File
@@ -0,0 +1,98 @@
# Common Infrastructure
The `include/sindri/common/` headers provide the shared utilities that every domain in the framework depends on. They solve three fundamental problems: eliminating the C Runtime dependency, tracking buffer bounds to prevent memory corruption, and removing plaintext API strings from the compiled binary.
---
## CRT Independence
SindriKit is designed to compile under `/NODEFAULTLIB` (MSVC), completely stripping the Microsoft C Runtime from the final binary. This is a hard requirement for position-independent shellcode and for minimizing the import table footprint of implants.
The standard C library functions that the framework requires are reimplemented as compiler-intrinsic-safe inline functions inside `include/sindri/common/helpers.h`:
| CRT Function | SindriKit Replacement | Notes |
|---|---|---|
| `memset` | `snd_memzero` | Uses `volatile BYTE*` to prevent compiler optimization from eliding the zeroing loop |
| `memcpy` | `snd_memcpy` | Byte-by-byte copy with no alignment assumptions |
| `strnlen` | `snd_strnlen` | Bounded length evaluation |
| `strncpy` / `strncpy_s` | `snd_strncpy` | Truncation-safe; always null-terminates |
| `strcat` / `strcat_s` | `snd_strncat` | Bounded concatenation with null-termination guarantee |
| `strchr` | `snd_strnchr` | Bounded character search |
| `strcmp` / `strncmp` | `snd_strncmp` | Bounded comparison returning standard `<0 / 0 / >0` semantics |
Additionally, `src/common/crt_manifest.c` provides the compiler-required `memcpy` and `memset` symbols that MSVC's code generation emits implicitly (e.g., for struct copies and zero-initialization). Without these symbols, linking under `/NODEFAULTLIB` produces unresolved external errors even if the source code never explicitly calls `memcpy`.
> [!CAUTION]
> Enabling `SND_DEBUG=1` or `SND_USE_PRINTF=1` pulls in `<stdio.h>` and `<stdarg.h>`, which reintroduce CRT dependencies. These flags **must** be disabled (`SND_DEBUG=0`) for any `/NODEFAULTLIB` build.
---
## Buffer Bounds Tracking
SindriKit wraps all raw memory pointers in a `snd_buffer_t` structure that pairs the data pointer with a tracked size. Every access into a buffer — whether parsing PE headers, walking export tables, or applying relocations — is routed through bounds-checking functions that validate `(offset, size)` against the tracked total before dereferencing.
### `snd_buffer_t`
```c
struct snd_buffer_s {
LPVOID data;
SIZE_T size;
snd_free_cb free_routine; // optional cleanup callback
};
```
The `free_routine` callback supports polymorphic deallocation: the same `snd_buffer_free` function correctly handles buffers backed by `HeapAlloc`, `VirtualAlloc`, or `MapViewOfFile` by dispatching through the stored callback.
### Bounds Checking Pipeline
All bounds validation is implemented as force-inlined functions to eliminate call overhead:
1. `snd_bounds_check(total_size, offset, size)` — pure arithmetic validation.
2. `snd_buffer_bounds_check(buf, offset, size)` — validates against a tracked `snd_buffer_t`.
3. `snd_ptr_bounds_check(base, total_size, ptr, size)` — validates an arbitrary pointer against a known base region.
These functions catch both overflow and underflow conditions. The PE parser's `snd_pe_rva_to_ptr` is the primary consumer, rejecting any RVA that would resolve outside the tracked image.
---
## API Hashing
To eliminate plaintext API strings (like `NtAllocateVirtualMemory` or `kernel32.dll`) from the binary's `.rdata` section, SindriKit pre-computes hashes of all required API names at configure time. The CMake build system runs `scripts/generate_hashes.py` against `config/hashes.ini`, which lists every module and function the framework resolves.
### Hash Variants
Three runtime hashing functions are provided to match the contexts in which strings are encountered:
| Function | Input | Casing | Use Case |
|---|---|---|---|
| `snd_hash` | `const char*` | Case-sensitive | Export names from the PE Export Directory (e.g., `NtAllocateVirtualMemory`) |
| `snd_hash_lower` | `const char*` | Lowercased | DLL names from the PE Import Directory (e.g., `KERNEL32.dll` → lowered) |
| `snd_hash_wide_lower` | `const wchar_t*` | Lowercased | Module names from the PEB `BaseDllName` (UTF-16) |
The hashing algorithm itself is swappable at configure time via the `SND_HASH_ALGO` CMake variable (e.g., `DJB2`, `FNV1A`). The Python script also generates a randomized compile-time seed per build run, ensuring that hash values are unique across builds and cannot be signature-matched.
---
## Debug Output System
SindriKit implements a two-tier debug output system controlled by the `SND_DEBUG` and `SND_USE_PRINTF` preprocessor macros:
| `SND_DEBUG` | `SND_USE_PRINTF` | Output Destination | CRT Required |
|---|---|---|---|
| `0` | N/A | All macros compile to no-ops; zero strings emitted | No |
| `1` | `1` | `stdout` / `stderr` via `fprintf` | Yes |
| `1` | `0` | Windows kernel debugger via `OutputDebugStringA` | Partial (`vsnprintf`) |
The two primary macros are:
- `SND_DEBUG_PRINT(fmt, ...)` — outputs to the default destination.
- `SND_FDEBUG_PRINT(stream, fmt, ...)` — outputs to a specific file stream (only meaningful when `SND_USE_PRINTF=1`).
When `SND_DEBUG=0`, both macros expand to empty `do { (void)0; } while(0)` blocks. The compiler strips all format string literals and argument evaluations from the binary entirely.
---
## Disk I/O
The `snd_buffer_load_from_disk` utility reads an entire file into a heap-allocated `snd_buffer_t`. It uses Win32 `CreateFileA` / `ReadFile` internally and sets the buffer's `free_routine` to `snd_buffer_free_heap` for automatic cleanup.
This function is used exclusively by the PoC executables to load raw PE payloads from disk. In a production implant, the payload would typically arrive over a network channel and be written directly into a pre-allocated buffer, bypassing disk I/O entirely.