Every technique we've explored so far—syscall evasion, memory obfuscation, sleep encryption—operates within the confines of a single process. The implant runs in its own address space, managing its own memory, making its own API calls. But this isolation creates inherent limitations. Security products can identify and scrutinize individual processes. A suspicious executable running from an unusual location draws attention regardless of how well it hides its internal behavior. Process injection shatters these boundaries by executing code within another process's address space, fundamentally changing the relationship between malicious code and the processes that host it.
This chapter explores the techniques that allow code to cross process boundaries. From classic DLL injection to sophisticated hollowing techniques, these methods form the foundation of advanced offensive operations. Understanding them is essential for both red teams seeking to evade detection and blue teams working to identify and prevent these attacks.
Process injection serves multiple strategic purposes, each creating significant challenges for defenders:
PROCESS INJECTION BENEFITS
Defense Evasion:
┌─────────────────────────────────────────────────────────────────────┐
│ Hiding in Plain Sight │
│ ├── Code executes in trusted system process (svchost.exe) │
│ ├── Network traffic originates from browser (chrome.exe) │
│ ├── No suspicious standalone process to investigate │
│ └── Inherits target process's reputation and trust level │
└─────────────────────────────────────────────────────────────────────┘
Privilege Operations:
┌─────────────────────────────────────────────────────────────────────┐
│ Accessing Protected Resources │
│ ├── Inject into elevated process for higher privileges │
│ ├── Access process-specific resources (tokens, handles) │
│ ├── Inject into LSASS for credential access │
│ └── Bypass per-process security restrictions │
└─────────────────────────────────────────────────────────────────────┘
Persistence:
┌─────────────────────────────────────────────────────────────────────┐
│ Surviving Process Termination │
│ ├── Original process can exit after injection │
│ ├── Payload survives in long-running processes │
│ ├── Multiple injection targets provide redundancy │
│ └── Some processes restart automatically if killed │
└─────────────────────────────────────────────────────────────────────┘
Capability Extension:
┌─────────────────────────────────────────────────────────────────────┐
│ Leveraging Target Process Features │
│ ├── Browser injection enables credential capture │
│ ├── Outlook injection accesses email │
│ ├── SSH client injection captures keys │
│ └── Application-specific memory access │
└─────────────────────────────────────────────────────────────────────┘
These benefits explain why process injection remains central to sophisticated attacks despite extensive defensive investment in detecting it.
Before injecting into a process, an attacker must find suitable targets. Windows provides several mechanisms for enumerating running processes, each with different trade-offs between simplicity and stealth.
The most common approach uses the Toolhelp library, which creates a snapshot of system state including all running processes:
TOOLHELP ENUMERATION
┌─────────────────────────────────────────────────────────────────────┐
│ CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) │
│ └── Returns handle to snapshot of all processes │
│ │
│ Process32First/Process32Next │
│ └── Iterate through PROCESSENTRY32 structures │
│ ├── th32ProcessID: Process ID │
│ ├── szExeFile: Process name (e.g., "notepad.exe") │
│ ├── th32ParentProcessID: Parent process ID │
│ └── cntThreads: Thread count │
│ │
│ For each process: │
│ └── Compare szExeFile against target name │
│ └── OpenProcess() to verify access before returning │
└─────────────────────────────────────────────────────────────────────┘
This approach is straightforward but creates observable artifacts. The Toolhelp functions are commonly monitored, and the snapshot operation can be detected.
A stealthier approach uses NtQuerySystemInformation directly, bypassing the higher-level Toolhelp wrapper:
NATIVE API ENUMERATION
NtQuerySystemInformation(SystemProcessInformation, ...)
│
├── First call with NULL buffer → Returns required size
│
└── Second call with allocated buffer → Returns process list
│
└── SYSTEM_PROCESS_INFORMATION structures:
├── UniqueProcessId: Process ID (HANDLE)
├── ImageName: UNICODE_STRING with process name
├── NumberOfThreads: Thread count
├── HandleCount: Open handle count
├── SessionId: Session ID
└── NextEntryOffset: Offset to next entry (0 = last)
Advantages:
├── Bypasses Toolhelp API hooks
├── Provides more detailed information
└── Less commonly monitored than Toolhelp
Using native APIs requires more code but reduces the detection surface. Combined with direct syscalls, this approach can enumerate processes without touching any user-mode hooks.
Not all processes are equally suitable for injection:
TARGET SELECTION CRITERIA
Good Targets:
┌─────────────────────────────────────────────────────────────────────┐
│ svchost.exe │
│ ├── Multiple instances always running │
│ ├── Expected to have network activity │
│ ├── High privilege instances available │
│ └── One more instance doesn't stand out │
│ │
│ explorer.exe │
│ ├── Always running for logged-in users │
│ ├── Expected to spawn processes │
│ ├── Network activity plausible │
│ └── High visibility to user (be careful) │
│ │
│ Browser processes (chrome.exe, firefox.exe) │
│ ├── Network traffic expected │
│ ├── Multiple processes normal │
│ ├── Heavy resource use doesn't stand out │
│ └── Credential capture opportunities │
└─────────────────────────────────────────────────────────────────────┘
Poor Targets:
┌─────────────────────────────────────────────────────────────────────┐
│ Security products (avoid detection/crashes) │
│ Single-instance processes (injection visible) │
│ Processes about to exit (waste of effort) │
│ Protected processes (PPL) - injection blocked │
│ Critical system processes - BSOD risk │
└─────────────────────────────────────────────────────────────────────┘
The ideal target matches the implant's needs (network access, privilege level, longevity) while not drawing attention from security monitoring.
The oldest and most straightforward injection technique loads a DLL into the target process. Since DLLs can contain arbitrary code that executes in DllMain, this provides a simple mechanism for cross-process code execution.
The technique exploits a fundamental Windows behavior: when LoadLibrary executes, it loads the specified DLL and runs its DllMain function. By creating a thread in the target process that calls LoadLibrary with a path to an attacker-controlled DLL, the attacker's code executes in the target's context.
DLL INJECTION FLOW
Injector Process Target Process
──────────────── ──────────────
│ │
│ 1. OpenProcess(PROCESS_ALL_ACCESS) │
│ ─────────────────────────────────────▶ │
│ (Obtain handle to target) │
│ │
│ 2. VirtualAllocEx() │
│ ─────────────────────────────────────▶ │ Memory allocated
│ (Allocate for DLL path) │ for path string
│ │
│ 3. WriteProcessMemory() │
│ ─────────────────────────────────────▶ │ "C:\path\to\
│ (Write DLL path string) │ malicious.dll"
│ │
│ 4. GetProcAddress(LoadLibraryW) │
│ (Get LoadLibrary address) │
│ Same in all processes due to │
│ ASLR applying system-wide │
│ │
│ 5. CreateRemoteThread() │
│ ─────────────────────────────────────▶ │ Thread created
│ (Thread starts at LoadLibrary │ at LoadLibraryW
│ with DLL path as argument) │ │
│ │ ▼
│ │ LoadLibraryW()
│ │ loads DLL
│ │ │
│ │ ▼
│ │ DllMain()
│ │ EXECUTES!
│ │
The technique relies on several Windows behaviors:
ASLR Consistency: While ASLR randomizes module base addresses at system boot, the same module (like kernel32.dll) loads at the same address in all processes. This means the address of LoadLibraryW obtained in the injector is valid in the target.
Thread Flexibility: CreateRemoteThread allows creating a thread in another process with an arbitrary start address. The thread function signature LPTHREAD_START_ROUTINE matches LoadLibraryW's parameter (a single pointer argument).
DllMain Execution: When a DLL loads, Windows calls its DllMain with DLL_PROCESS_ATTACH. This provides automatic code execution simply by loading the DLL.
DLL injection is heavily monitored:
Detection Points:
OpenProcess with injection-capable access rights:
├── PROCESS_VM_OPERATION
├── PROCESS_VM_WRITE
└── PROCESS_CREATE_THREAD
VirtualAllocEx cross-process allocation:
└── New memory region in target process
WriteProcessMemory cross-process write:
└── Data written to target process memory
CreateRemoteThread:
└── Specifically monitored by most EDRs
DLL loading:
└── New module appears in target process
└── Sysmon Event ID 7 (Image Loaded)
The DLL file must exist on disk, creating an artifact for forensic recovery. The DLL loading itself is visible through image load callbacks and can be blocked by policy. Despite these limitations, DLL injection remains common due to its simplicity.
Rather than loading a DLL file, shellcode injection writes position-independent code directly to the target process and executes it. This eliminates the need for a file on disk and provides more flexibility in what code executes.
The pattern is similar to DLL injection but instead of writing a path and calling LoadLibrary, we write raw shellcode and create a thread pointing directly to it:
SHELLCODE INJECTION FLOW
1. Open target process with necessary permissions
2. Allocate memory in target:
└── VirtualAllocEx(hProcess, NULL, shellcodeSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE)
↑
RWX permission is suspicious!
3. Write shellcode to allocated memory:
└── WriteProcessMemory(hProcess, pRemote,
pShellcode, shellcodeSize, NULL)
4. Execute shellcode:
└── CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)pRemote,
NULL, 0, NULL)
Allocating RWX (read-write-execute) memory is a major detection indicator. A better approach separates the write and execute phases:
RW → RX TRANSITION
Step 1: Allocate RW (no execute)
┌─────────────────────────────────────────────────────────────────────┐
│ VirtualAllocEx(..., PAGE_READWRITE) │
│ └── Memory is writable but not executable │
│ └── Less suspicious than RWX │
└─────────────────────────────────────────────────────────────────────┘
Step 2: Write shellcode
┌─────────────────────────────────────────────────────────────────────┐
│ WriteProcessMemory(...) │
│ └── Shellcode written while memory is RW │
└─────────────────────────────────────────────────────────────────────┘
Step 3: Change to RX (no write)
┌─────────────────────────────────────────────────────────────────────┐
│ VirtualProtectEx(..., PAGE_EXECUTE_READ) │
│ └── Memory becomes executable │
│ └── Write permission removed │
│ └── Mimics legitimate code sections │
└─────────────────────────────────────────────────────────────────────┘
Step 4: Execute
┌─────────────────────────────────────────────────────────────────────┐
│ CreateRemoteThread(...) pointing to RX memory │
│ └── Thread executes in read-execute memory │
│ └── More closely matches legitimate behavior │
└─────────────────────────────────────────────────────────────────────┘
This sequence still involves the suspicious combination of cross-process allocation, writing, and thread creation, but avoids the RWX indicator that immediately flags malicious activity.
CreateRemoteThread is heavily monitored. Alternative methods to trigger shellcode execution include:
Alternative Execution Triggers:
NtCreateThreadEx:
├── Lower-level API, less commonly hooked
└── Provides more control over thread creation
RtlCreateUserThread:
├── Even lower level
└── Used by some legitimate Windows components
QueueUserAPC + Thread Alertable Wait:
├── Queue shellcode as APC to existing thread
└── Executes when thread enters alertable wait
└── Covered in detail in Chapter 14
Thread Hijacking:
├── Suspend existing thread
├── Modify its context to point to shellcode
└── Resume thread
└── Also covered in Chapter 14
SetWindowsHookEx:
├── Install message hook targeting process
├── DLL loads when hook triggers
└── Indirect execution path
Each alternative trades off complexity against detection. The choice depends on the target environment and security products deployed.
Memory mapping provides an alternative to VirtualAllocEx that can evade some detection mechanisms. Instead of explicitly allocating memory, this technique creates a file mapping (backed by the pagefile) and maps views of it into both the local and remote processes.
MAPPING INJECTION ARCHITECTURE
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ Section Object (Pagefile-Backed) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Shared Memory │ │
│ │ [Shellcode bytes written here...] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ▲ ▲ │
│ │ MapViewOfFile │ MapViewOfFile2 │
│ │ (FILE_MAP_WRITE) │ (FILE_MAP_EXECUTE) │
│ │ │ │
│ ┌──────────┴──────────┐ ┌──────────┴──────────┐ │
│ │ Local Process │ │ Target Process │ │
│ │ │ │ │ │
│ │ Write shellcode │ │ Execute shellcode │ │
│ │ to local view │ │ from remote view │ │
│ │ │ │ │ │
│ │ Changes visible │ │ Same memory, │ │
│ │ in both views! │ │ just mapped RX │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Several factors make this technique harder to detect:
No WriteProcessMemory: The shellcode is written to the local view, not directly to the target process. WriteProcessMemory is a heavily monitored API; avoiding it removes a major detection point.
Section Object Semantics: The section object approach is used by many legitimate Windows operations (shared memory, memory-mapped files). The activity blends with normal system behavior.
Different API Pattern: Security products tuned to detect VirtualAllocEx + WriteProcessMemory + CreateRemoteThread may miss the mapping variation.
Windows provides several ways to implement mapping injection:
MapViewOfFile2: Available on Windows 10 1803+, this function allows mapping a view into another process directly.
NtMapViewOfSection: The underlying native API works on all Windows versions and provides fine-grained control over the mapping.
The key is creating a section with PAGE_EXECUTE_READWRITE capabilities, mapping it locally for writing, then mapping it into the target for execution.
Module stomping takes a different approach: instead of allocating new memory, it overwrites the code section of an already-loaded DLL. This places shellcode in memory that's already backed by a legitimate signed module.
When you allocate new executable memory, that memory is "unbacked"—it doesn't correspond to any file on disk. Security products can identify unbacked executable regions as suspicious. Module stomping eliminates this indicator:
UNBACKED VS BACKED MEMORY
Standard Injection:
┌─────────────────────────────────────────────────────────────────────┐
│ VirtualAllocEx() creates NEW memory region │
│ ├── No backing file │
│ ├── Flagged as unbacked executable │
│ └── Memory scanners identify as suspicious │
└─────────────────────────────────────────────────────────────────────┘
Module Stomping:
┌─────────────────────────────────────────────────────────────────────┐
│ Overwrite .text section of loaded DLL │
│ ├── Memory backed by signed DLL file │
│ ├── Appears as legitimate module code │
│ ├── Memory protection already RX │
│ └── Only temporary RW needed for writing │
└─────────────────────────────────────────────────────────────────────┘
Not every DLL is suitable for stomping:
Good Stomping Targets:
Requirements:
├── .text section larger than shellcode
├── Rarely called after initial load
├── Won't be integrity-checked by application
└── Common enough not to stand out
Examples:
┌──────────────────────────────────────────────────────────────────┐
│ Module │ Why Suitable │
├──────────────────────────────────────────────────────────────────┤
│ amsi.dll │ Often already disabled for bypass │
│ dbghelp.dll │ Debugging helper, large, rarely used │
│ version.dll │ Common, predictable location │
│ setupapi.dll │ Large unused functions │
│ msvcp140.dll │ Runtime with many unused exports │
└──────────────────────────────────────────────────────────────────┘
Poor Targets:
├── ntdll.dll (critical, integrity-checked)
├── kernel32.dll (critical, heavily used)
├── Security product DLLs (monitored, crashes)
└── Small DLLs (insufficient space)
MODULE STOMPING STEPS
1. Load target DLL (or find already-loaded):
└── LoadLibraryEx(..., DONT_RESOLVE_DLL_REFERENCES)
└── Loads without running DllMain
2. Locate .text section:
└── Parse PE headers
└── Find section with IMAGE_SCN_CNT_CODE flag
└── Verify size sufficient for shellcode
3. Make section writable:
└── VirtualProtect(pTextSection, size, PAGE_READWRITE, &oldProtect)
└── Temporarily remove execute restriction
4. Overwrite with shellcode:
└── memcpy(pTextSection, pShellcode, shellcodeSize)
└── Original code destroyed
5. Restore execution permission:
└── VirtualProtect(pTextSection, size, PAGE_EXECUTE_READ, &oldProtect)
└── Now executable, matches original permissions
6. Execute:
└── Call into the stomped region
└── Or create thread pointing there
Module stomping creates detection challenges for defenders:
However, EDRs are increasingly implementing memory integrity checks that compare loaded modules against their on-disk versions.
Process hollowing creates a new process in a suspended state, removes its original code, and replaces it with malicious code. The result is a process that appears to be one thing (based on its name and initial executable) but actually runs something completely different.
PROCESS HOLLOWING FLOW
Step 1: Create Suspended Process
┌─────────────────────────────────────────────────────────────────────┐
│ CreateProcess("svchost.exe", ..., CREATE_SUSPENDED) │
│ ├── Process created but main thread not running │
│ ├── Original svchost.exe image loaded │
│ └── Ready for manipulation before execution starts │
└─────────────────────────────────────────────────────────────────────┘
│
▼
Step 2: Hollow Out Original Image
┌─────────────────────────────────────────────────────────────────────┐
│ NtUnmapViewOfSection(hProcess, imageBase) │
│ ├── Removes the legitimate svchost.exe image │
│ ├── Memory at ImageBase now free │
│ └── Process structure intact but empty │
└─────────────────────────────────────────────────────────────────────┘
│
▼
Step 3: Allocate for Malicious PE
┌─────────────────────────────────────────────────────────────────────┐
│ VirtualAllocEx(hProcess, preferredBase, sizeOfImage, ...) │
│ ├── Allocate at payload's preferred ImageBase │
│ ├── If unavailable, allocate anywhere (requires relocation) │
│ └── Size matches malicious PE's SizeOfImage │
└─────────────────────────────────────────────────────────────────────┘
│
▼
Step 4: Write Malicious PE
┌─────────────────────────────────────────────────────────────────────┐
│ WriteProcessMemory(headers) │
│ WriteProcessMemory(sections[0]) │
│ WriteProcessMemory(sections[1]) │
│ ... │
│ ├── Write PE headers and each section │
│ ├── Map sections to correct virtual addresses │
│ └── Malicious code now in process │
└─────────────────────────────────────────────────────────────────────┘
│
▼
Step 5: Update Entry Point
┌─────────────────────────────────────────────────────────────────────┐
│ GetThreadContext(hThread, &ctx) │
│ ctx.Rcx = newImageBase + AddressOfEntryPoint (x64) │
│ SetThreadContext(hThread, &ctx) │
│ ├── Point thread's start address to malicious entry point │
│ └── Also update PEB.ImageBaseAddress │
└─────────────────────────────────────────────────────────────────────┘
│
▼
Step 6: Resume Execution
┌─────────────────────────────────────────────────────────────────────┐
│ ResumeThread(hThread) │
│ ├── Process starts running │
│ ├── But running malicious code, not svchost.exe │
│ └── Process name still shows as svchost.exe │
└─────────────────────────────────────────────────────────────────────┘
Process hollowing essentially performs manual PE loading. Several considerations apply:
Relocation: If the malicious PE can't load at its preferred address, relocations must be applied. The .reloc section contains fixup information.
Import Resolution: Unlike normal loading, the loader doesn't process imports. The malicious PE must either resolve imports itself or be position-independent shellcode.
TLS Callbacks: Thread Local Storage callbacks won't execute automatically. If the payload relies on them, they must be manually triggered.
Process hollowing has characteristic indicators:
Hollowing Detection Indicators:
Process Creation:
├── CREATE_SUSPENDED flag (legitimate but notable)
└── Sysmon Event ID 1 shows suspended creation
Memory Operations:
├── NtUnmapViewOfSection on main image (very unusual)
├── VirtualAllocEx + WriteProcessMemory sequence
└── Memory protection changes
Context Manipulation:
├── SetThreadContext modifying RIP/RCX (entry point)
└── PEB modifications visible
Behavioral:
├── Process image on disk doesn't match memory
├── Execution from unexpected memory regions
└── Parent-child relationship anomalies
Modern Detection:
├── EDRs track hollowing-specific API sequences
├── Memory scanning compares disk vs memory
└── Callback-based monitoring catches operations
Beyond the core techniques, several variations exist:
Instead of creating a new thread, hijack an existing one:
This avoids CreateRemoteThread monitoring but disrupts the hijacked thread's normal execution.
Queue an Asynchronous Procedure Call to an existing thread:
QueueUserAPCCovered in detail in Chapter 14.
Abuse Windows atom tables to write data cross-process:
GlobalGetAtomNameLargely mitigated in modern Windows versions.
Inject into a process during its initialization before security products attach:
The APC executes before the process's main code runs, potentially before EDR instrumentation.
Process injection is one of the most scrutinized attack techniques. Understanding detection methods helps both attackers and defenders.
INJECTION DETECTION APPROACHES
API Monitoring:
┌─────────────────────────────────────────────────────────────────────┐
│ Hook critical APIs: │
│ ├── OpenProcess (with VM_WRITE, CREATE_THREAD access) │
│ ├── VirtualAllocEx / NtAllocateVirtualMemory │
│ ├── WriteProcessMemory / NtWriteVirtualMemory │
│ ├── CreateRemoteThread / NtCreateThreadEx │
│ ├── SetThreadContext / NtSetContextThread │
│ └── NtMapViewOfSection (cross-process) │
│ │
│ Correlate: Process A opens B → A writes to B → A creates thread │
└─────────────────────────────────────────────────────────────────────┘
Kernel Callbacks:
┌─────────────────────────────────────────────────────────────────────┐
│ PsSetCreateProcessNotifyRoutine │
│ └── Track process creation, especially suspended │
│ │
│ PsSetCreateThreadNotifyRoutine │
│ └── Identify cross-process thread creation │
│ │
│ PsSetLoadImageNotifyRoutine │
│ └── Detect DLL loading in target processes │
│ │
│ ObRegisterCallbacks │
│ └── Monitor handle operations (opening processes) │
└─────────────────────────────────────────────────────────────────────┘
Memory Analysis:
┌─────────────────────────────────────────────────────────────────────┐
│ ├── Identify unbacked executable regions │
│ ├── Compare loaded modules to disk versions │
│ ├── Detect RWX memory (should be rare) │
│ ├── Track VirtualProtect changes on code sections │
│ └── Identify PE structures in unusual locations │
└─────────────────────────────────────────────────────────────────────┘
Behavioral Analysis:
┌─────────────────────────────────────────────────────────────────────┐
│ ├── Process X performing operations it normally doesn't │
│ ├── Network traffic from unexpected processes │
│ ├── Process behavior changing after certain events │
│ └── Parent-child relationships that don't match image │
└─────────────────────────────────────────────────────────────────────┘
Modern injection must account for these detection mechanisms:
Use Direct/Indirect Syscalls: Bypass user-mode API hooks by invoking syscalls directly.
Avoid RWX Memory: Use RW→RX transitions instead of RWX allocations.
Target Selection: Choose injection targets that normally exhibit similar behavior.
Timing: Inject during periods of high system activity when monitoring may be noisier.
Callback-Based Execution: Use APCs, timer callbacks, or other indirect execution methods instead of CreateRemoteThread.
Clean Up: Remove injection artifacts after execution completes.
Process injection enables code execution across process boundaries, providing stealth, privilege access, and persistence capabilities. Each technique offers different trade-offs:
| Technique | Stealth | Complexity | Detection Risk |
|---|---|---|---|
| DLL Injection | Low | Low | High (well-known) |
| Shellcode Injection | Medium | Low | Medium |
| Mapping Injection | High | Medium | Medium |
| Module Stomping | High | Medium | Medium |
| Process Hollowing | Medium | High | Medium |
Key principles for effective injection:
The next chapter explores thread-level manipulation and Asynchronous Procedure Calls—techniques that provide alternatives to traditional injection by working with existing threads rather than creating new ones.