6.3 KiB
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 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_READIMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE→PAGE_READWRITEIMAGE_SCN_MEM_READonly →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.
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_winor&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.