Module 1: Process Injection Landscape
Classic injection techniques, why they fail against modern EDRs, and the motivation behind PoolParty’s thread pool approach.
Module Objective
Understand the traditional process injection techniques (DLL injection, shellcode injection, APC injection), the specific Windows API call patterns that EDRs hook and monitor, why these techniques are increasingly detected, and how PoolParty reframes the injection problem by targeting the Windows Thread Pool as an execution primitive.
1. What Is Process Injection?
Process injection is the act of executing arbitrary code within the address space of a separate, running process. Attackers use it to:
- Evade detection — code runs under a legitimate process name (e.g.,
svchost.exe,explorer.exe) - Bypass security boundaries — inherit the target process’s tokens, privileges, and network access
- Persist stealthily — no new process created, no suspicious command lines logged
- Access process-specific resources — read credentials from LSASS, manipulate browser memory
MITRE ATT&CK catalogs process injection under T1055, with over a dozen sub-techniques. Despite decades of research, injection remains a cornerstone of offensive tradecraft because executing code in another process is fundamentally useful.
2. Classic Injection Techniques
2.1 DLL Injection via CreateRemoteThread
The oldest and most well-known technique. The attacker writes the path of a malicious DLL into the target process and creates a remote thread that calls LoadLibraryA:
C++// Classic DLL Injection - highly detected
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);
// Allocate memory in target for DLL path
LPVOID remoteBuf = VirtualAllocEx(hProcess, NULL, dllPathLen,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// Write DLL path into target process
WriteProcessMemory(hProcess, remoteBuf, dllPath, dllPathLen, NULL);
// Create remote thread calling LoadLibraryA with our DLL path
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("kernel32.dll"),
"LoadLibraryA"),
remoteBuf, 0, NULL);
2.2 Shellcode Injection via VirtualAllocEx + WriteProcessMemory
Instead of a DLL path, raw shellcode is written to allocated memory with executable permissions:
C++// Shellcode injection - allocate RWX, write, execute
LPVOID remoteBuf = VirtualAllocEx(hProcess, NULL, shellcodeLen,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, remoteBuf, shellcode, shellcodeLen, NULL);
CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)remoteBuf, NULL, 0, NULL);
2.3 APC Injection
Asynchronous Procedure Calls queue code to execute in the context of a specific thread. The target thread must enter an alertable wait state for the APC to fire:
C++// APC injection - queue shellcode as APC to alertable thread
LPVOID remoteBuf = VirtualAllocEx(hProcess, NULL, shellcodeLen,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, remoteBuf, shellcode, shellcodeLen, NULL);
// Queue APC to each thread in the target process
QueueUserAPC((PAPCFUNC)remoteBuf, hThread, 0);
2.4 Other Variants
| Technique | Execution Trigger | Key APIs |
|---|---|---|
| Process Hollowing | Replace suspended process image | NtUnmapViewOfSection, SetThreadContext |
| Thread Hijacking | Modify RIP/EIP of existing thread | SuspendThread, SetThreadContext, ResumeThread |
| Atom Bombing | Abuse global atom table + APC | GlobalAddAtom, NtQueueApcThread |
| Early Bird | APC to newly created suspended process | CreateProcess(SUSPENDED), QueueUserAPC |
| Callback Injection | Abuse API callbacks (e.g., EnumWindows) | EnumWindows, EnumDesktops, etc. |
3. The API Call Pattern Problem
Every classic injection technique follows the same fundamental three-step pattern that EDRs exploit for detection:
Universal Injection Pattern
VirtualAllocEx
WriteProcessMemory
CreateRemoteThread
QueueUserAPC
SetThreadContext
This pattern — allocate, write, execute — is the Achilles’ heel of all traditional injection. EDRs monitor every step:
| Step | Hooked APIs | What EDR Detects |
|---|---|---|
| Allocate | NtAllocateVirtualMemory | Cross-process memory allocation, RWX permissions requested |
| Write | NtWriteVirtualMemory | Cross-process memory writes, shellcode byte patterns |
| Execute | NtCreateThreadEx, NtQueueApcThread, NtSetContextThread | Remote thread creation, suspicious thread start addresses, APC targeting |
The Execution Trigger Is the Weakest Link
Allocating memory and writing bytes are relatively common operations. The execution trigger — the API call that causes the injected code to actually run — is what EDRs focus on most heavily. CreateRemoteThread, QueueUserAPC, and SetThreadContext targeting a remote process are high-fidelity indicators of injection. PoolParty’s insight is to find execution triggers that EDRs do not monitor.
4. How EDRs Hook These APIs
Modern EDRs use multiple hooking layers to intercept injection-related API calls:
4.1 User-Mode Hooks (ntdll.dll)
EDRs patch the prologue of ntdll.dll functions with a JMP to their monitoring DLL. When your code calls NtAllocateVirtualMemory, execution is redirected to the EDR first:
ASM; Original ntdll!NtAllocateVirtualMemory
mov r10, rcx
mov eax, 18h ; syscall number
syscall
ret
; After EDR hooking
jmp edr_monitor.dll+0x1234 ; redirected to EDR
nop
nop
syscall ; never reached via normal path
ret
4.2 Kernel Callbacks
Windows provides kernel-mode callback mechanisms that EDR drivers register for:
- PsSetCreateProcessNotifyRoutine — notified on process creation
- PsSetCreateThreadNotifyRoutine — notified on thread creation (catches
CreateRemoteThread) - ObRegisterCallbacks — intercepts handle operations (catches
OpenProcess) - ETW TI (Threat Intelligence) — kernel telemetry on memory operations
4.3 ETW (Event Tracing for Windows)
The Microsoft-Windows-Threat-Intelligence ETW provider reports on cross-process memory operations directly from the kernel, bypassing any user-mode unhooking attempts.
5. Why Traditional Techniques Are Failing
The detection landscape has matured to the point where classic injection is reliably caught:
| Evasion Attempt | Why It No Longer Works |
|---|---|
| Direct syscalls (bypass ntdll hooks) | Kernel callbacks and ETW TI still report the operation |
| Indirect syscalls (jump to ntdll syscall instruction) | Thread call stack analysis reveals non-ntdll callers |
| Unhooking ntdll (overwrite with clean copy) | ETW TI operates from kernel, unaffected by user-mode changes |
| Using NtMapViewOfSection instead of Write | Mapped sections still detected via VAD (Virtual Address Descriptor) analysis |
| Callback-based execution | EDRs now monitor the specific callbacks (e.g., hooking KiUserApcDispatcher) |
The Core Insight
All classic techniques are detected because they require a known execution trigger API. PoolParty sidesteps this entirely by using the Windows Thread Pool as the execution primitive — the target process’s own thread pool worker threads execute the injected code as part of their normal callback dispatch, with no suspicious API call required for the execution step.
6. PoolParty’s Approach
Alon Leviev’s research (SafeBreach Labs, Black Hat EU 2023) identified a fundamentally different injection surface: the Windows Thread Pool. Instead of creating a remote thread or queuing an APC, PoolParty injects work items into the target process’s thread pool. The thread pool’s own worker threads then execute the callback — no CreateRemoteThread, no QueueUserAPC, no SetThreadContext.
PoolParty vs Classic Injection
Alloc → Write → CreateRemoteThread
Alloc → Write → Insert work item into thread pool
The 8 PoolParty variants each target a different thread pool entry point:
| Variant | Target Structure | Mechanism |
|---|---|---|
| 1 | Worker Factory StartRoutine | Overwrite start routine via NtSetInformationWorkerFactory |
| 2 | TP_WORK | Insert crafted work item into target’s thread pool |
| 3 | TP_WAIT | Insert wait item into target’s thread pool |
| 4 | TP_IO | Insert I/O completion item into target’s thread pool |
| 5 | TP_ALPC | Insert ALPC item into target’s thread pool |
| 6 | TP_JOB | Insert job item into target’s thread pool |
| 7 | TP_DIRECT | Insert direct item via NtSetIoCompletionEx |
| 8 | TP_TIMER | Insert timer item into target’s thread pool |
7. Course Roadmap
What Comes Next
| Module | Topic | Focus |
|---|---|---|
| 1 (this) | Process Injection Landscape | Why classic techniques fail |
| 2 | Thread Pool Architecture | TP_POOL, work items, IOCP fundamentals |
| 3 | Thread Pool Internals | TppWorkerThread, callback dispatch, kernel side |
| 4 | Worker Factory & TP_WORK | Variants 1–2: StartRoutine hijack and TP_WORK insertion |
| 5 | Wait, I/O & ALPC | Variants 3–5: TP_WAIT, TP_IO, and TP_ALPC |
| 6 | Job & Direct Variants | Variants 6–7: TP_JOB and TP_DIRECT |
| 7 | TP_TIMER Insertion | Variant 8: Timer-based callback injection |
| 8 | Full Chain & Detection | EDR results, detection strategies, IOCP monitoring |
Knowledge Check
Q1: What is the three-step pattern that EDRs use to detect classic process injection?
Q2: Why do direct syscalls no longer fully evade EDR detection?
Q3: What makes PoolParty fundamentally different from classic injection techniques?