Reflective DLL Injection
A DLL that loads itself into memory without calling LoadLibrary — the reflective loader algorithm, PEB-based API resolution, and why it bypasses the kernel's PsSetLoadImageNotifyRoutine in some configurations
A Cobalt Strike shellcode blob is injected into a remote process via VirtualAllocEx + WriteProcessMemory + CreateRemoteThread. The shellcode is not standalone code — it's the ReflectiveDLLInjection bootstrap: a tiny stub that resolves LoadLibrary and GetProcAddress from the PEB export table, then maps the embedded DLL PE image into the process's memory the same way the Windows loader would, but entirely in user space. The result: a fully functional DLL running inside the process, with no DLL load event, no PEB module list entry (unless the reflective loader adds one), and no file on disk.
The Concept
Traditional DLL injection requires a DLL file on disk and calls LoadLibrary on the target — which invokes the Windows loader, writes to the PEB module list, and fires the PsSetLoadImageNotifyRoutine kernel callback. Reflective DLL injection embeds the DLL as raw bytes (in the injector, in shellcode, or in a network payload) and replaces the Windows loader with a custom "reflective loader" function compiled into the DLL itself.
The reflective loader runs entirely in user space and performs every step the Windows loader would have done — without the kernel callbacks, without the PEB module list entry, and without any file system presence.
Reflective Loader Algorithm
Reflective Loader (runs inside the target process):
Step 1: Find own image base
The loader knows its own address (passed as a parameter or found via call/pop).
Walk backward from that address to find the MZ header (DOS signature).
Step 2: Resolve needed APIs without LoadLibrary
Walk PEB.Ldr.InLoadOrderModuleList to find kernel32.dll and ntdll.dll.
From their export tables, find: LoadLibraryA, GetProcAddress, VirtualAlloc,
VirtualProtect, FlushInstructionCache.
(This is the PEB traversal technique from Chapter 8.)
Step 3: Allocate memory for the DLL image
Call VirtualAlloc(NULL, SizeOfImage, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE).
Step 4: Copy PE headers and sections
Copy the DLL's PE headers to the new allocation.
For each section: copy section data to (base + VirtualAddress).
Step 5: Apply base relocations
If the DLL didn't load at its preferred ImageBase, calculate delta.
Walk .reloc section; apply HIGHLOW (or DIR64 on x64) fixups.
Step 6: Resolve imports
Walk IMAGE_IMPORT_DESCRIPTOR entries.
For each DLL: LoadLibraryA(dllName).
For each import: GetProcAddress(hDll, funcName); write to IAT slot.
Step 7: Set final page protections
VirtualProtect each section with its correct protection flags:
.text → PAGE_EXECUTE_READ
.data → PAGE_READWRITE
.rdata → PAGE_READONLY
Step 8: Call DllMain(DLL_PROCESS_ATTACH)
Find DllMain from AddressOfEntryPoint; call it.
DllMain initializes the DLL and starts the payload.
PEB Traversal for API Resolution
; x64 assembly: find kernel32.dll base via PEB traversal (reflective loader start)
; Same technique as ROR13 shellcode, embedded in the DLL itself
mov rbx, qword [gs:0x60] ; PEB
mov rbx, [rbx + 0x18] ; Ldr = PEB.Ldr
mov rbx, [rbx + 0x10] ; InLoadOrderModuleList.Flink = first entry
mov rbx, [rbx] ; skip first entry (the .exe itself)
mov rbx, [rbx] ; second entry = ntdll.dll on most systems
mov rbx, [rbx + 0x30] ; DllBase of ntdll
; ... walk exports to find needed functions ...
; Then repeat walk for kernel32 (third entry in typical order)
Cobalt Strike Beacon
Cobalt Strike's default payload is a reflective DLL. The beacon DLL is injected as a raw byte array (shellcode + DLL header) via one of several injection techniques. The reflective loader at the beginning of the shellcode performs steps 1-8 above, then DllMain launches the beacon's C2 communications thread. Key observable artifacts:
| Artifact | Where | Detection signal |
|---|---|---|
| MEM_PRIVATE RWX/RX region in target | VAD of target process | Cross-view shows PE header in MEM_PRIVATE region not in PEB list |
| Sleep with jitter + beacon HTTPS traffic | Network + process | Periodic HTTPS from explorer.exe / unusual host process |
| CreateRemoteThread to injected shellcode | Sysmon Event 8 | StartAddress in MEM_PRIVATE region, not a module |
| Named pipe for SMB beacon | Named pipe events | Unusual named pipe pattern: \\.\pipe\MSSE-{guid}-server |
| MZ header in MEM_PRIVATE allocation | Memory scan | YARA: MZ magic at start of a MEM_PRIVATE executable region |
Detection
"""
Scan all processes for PE headers in MEM_PRIVATE executable regions.
A PE header (MZ magic) in anonymous (not file-backed) executable memory
is a strong indicator of reflective DLL injection.
"""
import ctypes, ctypes.wintypes, struct
MEM_PRIVATE = 0x20000
MEM_COMMIT = 0x1000
PAGE_EXECUTE_READ = 0x20
PAGE_EXEC_RW = 0x40
class MBI(ctypes.Structure):
_fields_ = [
("BaseAddress", ctypes.c_uint64),
("AllocationBase", ctypes.c_uint64),
("AllocProtect", ctypes.c_uint32),
("_pad", ctypes.c_uint32),
("RegionSize", ctypes.c_uint64),
("State", ctypes.c_uint32),
("Protect", ctypes.c_uint32),
("Type", ctypes.c_uint32),
("_pad2", ctypes.c_uint32),
]
def scan_reflective_dlls(pid: int):
hProc = ctypes.windll.kernel32.OpenProcess(0x0410, 0, pid)
if not hProc: return
addr, mbi = 0, MBI()
while ctypes.windll.kernel32.VirtualQueryEx(
hProc, ctypes.c_void_p(addr),
ctypes.byref(mbi), ctypes.sizeof(mbi)):
exec_mask = (PAGE_EXECUTE_READ | PAGE_EXEC_RW | 0x10 | 0x80)
if (mbi.State == MEM_COMMIT and
mbi.Type == MEM_PRIVATE and
mbi.Protect & exec_mask):
# Read first 2 bytes; check for MZ signature
buf = (ctypes.c_ubyte * 2)()
read = ctypes.c_size_t()
ctypes.windll.kernel32.ReadProcessMemory(
hProc, ctypes.c_void_p(mbi.BaseAddress),
buf, 2, ctypes.byref(read))
if read.value == 2 and buf[0] == 0x4D and buf[1] == 0x5A:
print(f"[!] PID {pid}: MZ in MEM_PRIVATE at 0x{mbi.BaseAddress:016X}"
f" (size 0x{mbi.RegionSize:X})")
addr = mbi.BaseAddress + mbi.RegionSize
if addr >= (1 << 47): break
ctypes.windll.kernel32.CloseHandle(hProc)
Q & A
Does reflective DLL injection bypass PsSetLoadImageNotifyRoutine kernel callbacks?
It depends on the implementation. PsSetLoadImageNotifyRoutine fires when the Windows loader maps an image section into a process — specifically when NtMapViewOfSection is called with an image section. Classic reflective DLL injection uses VirtualAlloc (not NtMapViewOfSection with a section object) to allocate memory, then manually copies the PE into that allocation. VirtualAlloc does not trigger PsSetLoadImageNotifyRoutine because there's no section object and no image mapping event. So: pure reflective loading via VirtualAlloc bypasses the load image callback. However: (1) The initial shellcode delivery (VirtualAllocEx + WriteProcessMemory + CreateRemoteThread or equivalent) does generate ETW-TI events and process access events. (2) Modern EDRs using ETW-TI observe the NtAllocateVirtualMemory, NtWriteVirtualMemory, and NtCreateThreadEx calls with their stacks, regardless of whether PsSetLoadImageNotifyRoutine fires. (3) The MEM_PRIVATE PE-header scan (as in the code above) detects reflective DLLs post-injection. (4) Some variants of reflective injection do use NtMapViewOfSection with a properly constructed section object — these fire the load image callback. The detection landscape is: the initial injection is visible via ETW-TI; the resulting state is visible via memory scanning; only point-in-time "file scanning" type defenses miss it completely.
What is the difference between reflective DLL injection and position-independent shellcode?
Position-independent shellcode (PIC) is a flat blob of machine code with no PE structure, no imports, no sections, and no relocation table. It's self-contained: it resolves all its dependencies via PEB traversal at runtime, uses only relative addressing (no absolute addresses), and can execute directly from any memory location. Reflective DLL injection loads a full PE binary (with all PE structures intact: headers, sections, import directory, relocation table) into memory. The reflective loader is a bootstrap stub that initializes the PE properly, making it a real DLL with sections, exports, and initialized imports — just loaded without the Windows loader's involvement. The key differences: (1) Complexity: shellcode is simpler, but limited in size and cannot easily use standard Windows APIs without resolving them manually. A reflective DLL can be written in C/C++ with all normal function calls and libraries. (2) Detection surface: shellcode typically has no PE header (unless it's a reflective DLL); a reflective DLL has an MZ header in the private allocation. YARA rules for MZ in executable private memory catch reflective DLLs but not raw shellcode. (3) Capability: Cobalt Strike, Meterpreter, and other frameworks use reflective DLL because it supports complex C++ code, thread creation, COM usage, etc. Simple shellcode can only do what can be written in a small position-independent stub. (4) Update path: a DLL with a proper export table can be updated and its exports called remotely; shellcode is a one-shot execution with no ongoing callable interface.