Skip to content
λmaldev wiki/
pagesReflective DLL Loading
T1620Windowsx64 / x86C / C++

Reflective DLL Loading

updated 2026-07-127 min readthehackersbrain
Authorized use only. This material is published for detection engineering, malware analysis and authorized red-team engagements. Running these techniques against systems you do not own or have written permission to test is illegal.

Overview

A reflective DLL carries its own loader. Rather than asking Windows to map it, an exported bootstrap function replicates what the loader does — copy headers and sections, apply relocations, resolve imports, run TLS callbacks, call DllMain — from inside the target process.

The payoff is the absence of a module list entry. GetModuleHandle, the loaded-module inventory in your EDR console, and Sysmon image-load events all miss it, because from the operating system’s point of view no image was ever loaded.

Note.

Import resolution still goes through LoadLibraryA for dependencies. A loader that pulls in an unusual DLL — ws2_32, wininet — into a process that has no business with networking is a decent second-order signal.

The call chain

  1. 1
    (bootstrap stub)
    Position-independent prologue locates its own base by walking backwards to the MZ header.
  2. 2
    resolve kernel32 by hash
    Walk the PEB loader list and hash export names to find LoadLibraryA and GetProcAddress.
  3. 3
    allocate + copy sections
    Map headers and each section at the chosen base with the right protection per section.
  4. 4
    process relocations + imports
    Apply the base delta and build the IAT by hand, exactly as the real loader would.
  5. 5
    call DllMain(DLL_PROCESS_ATTACH)
    Hand control to the image; the module never appears in PEB_LDR_DATA.

Reference implementation

Abridged: the relocation and import loops are the bulk of a real loader and are omitted here.

reflective_loader.cC
// resolve exports by hash so no strings survive in the payload
PVOID k32 = find_module_by_hash(0x6A4ABC5B);          // kernel32.dll
fnLoadLibraryA  pLoadLibraryA  = find_export_by_hash(k32, 0xEC0E4E8E);
fnVirtualAlloc  pVirtualAlloc  = find_export_by_hash(k32, 0x91AFCA54);

// map at the preferred base if it is free, anywhere otherwise
PVOID base = pVirtualAlloc((PVOID)nt->OptionalHeader.ImageBase,
                         nt->OptionalHeader.SizeOfImage,
                         MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

copy_headers(base, raw);
copy_sections(base, raw, nt);
apply_relocations(base, nt);      // delta = base - OptionalHeader.ImageBase
resolve_imports(base, nt, pLoadLibraryA);
protect_sections(base, nt);       // per-section RX / R / RW — never blanket RWX

((fnDllMain)(base + nt->OptionalHeader.AddressOfEntryPoint))(base, DLL_PROCESS_ATTACH, NULL);

Detection

MEMORY SCAN
A committed region carrying an MZ/PE header that has no entry in the process module list.
YARA
The bootstrap prologue that hashes export names — stable across most public loader lineages.
ETW-TI
Protection changes to RX on private memory in a process that has just received a remote write.
SYSMON EID 8
Remote thread whose start address falls in private memory rather than a mapped image.

Scanning committed private memory for MZ at page granularity finds the naive implementations cheaply. Loaders that erase their headers after mapping defeat that check, which is why the YARA signature targets the bootstrap code rather than the PE structure.

Was this page useful?edit this page ↗