mirror of
https://github.com/youssefnoob003/SindriKit
synced 2026-07-07 21:57:09 +00:00
76 lines
6.3 KiB
Markdown
76 lines
6.3 KiB
Markdown
# 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.
|