APC Injection
Asynchronous Procedure Calls — the kernel mechanism, user-mode APC injection via NtQueueApcThread, Early Bird APC for pre-EP execution, and why no new thread event fires
Classic injection via CreateRemoteThread fires Sysmon Event ID 8 (Create Remote Thread) — a high-confidence detection. APC injection avoids this by not creating a thread: instead, the attacker queues an APC to an existing thread in the target process. When that thread next enters an alertable wait (SleepEx, WaitForSingleObjectEx with bAlertable=TRUE, etc.), the kernel dispatches the APC. No new thread; no thread creation event; payload runs inside an existing thread. This is why APC injection appears in several commercial C2 frameworks as an alternative to CreateRemoteThread.
APC Internals
An Asynchronous Procedure Call (APC) is a function queued for execution in the context of a specific thread. The kernel maintains an APC queue per thread (actually three: kernel APC list, user APC list, and special user APC list). User-mode APCs run only when the thread is in an alertable state.
Thread states for APC delivery:
Normal execution:
Thread runs → APCs queued but NOT delivered
The thread must enter an alertable wait to drain the queue
Alertable wait:
SleepEx(n, bAlertable=TRUE)
WaitForSingleObjectEx(h, t, TRUE)
WaitForMultipleObjectsEx(...)
NtWaitForSingleObject(h, alertable=TRUE)
SignalObjectAndWait(...)
On wake from alertable wait:
Kernel checks user APC queue
Delivers queued APCs one by one (runs APC function in thread context)
Thread resumes from the wait only after APC queue is drained
This is different from kernel APCs, which can interrupt a thread even during normal execution. User APCs are voluntary — the thread must cooperate by calling an alertable wait function.
APC Injection
// APC injection: queue shellcode as an APC to threads in the target process
// Requires PROCESS_VM_OPERATION + PROCESS_VM_WRITE + THREAD_SET_CONTEXT
BOOL InjectAPC(DWORD pid, PBYTE shellcode, SIZE_T scLen)
{
HANDLE hProc = OpenProcess(
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_QUERY_INFORMATION,
FALSE, pid);
if (!hProc) return FALSE;
// Step 1: write shellcode to the target process
LPVOID pRemote = VirtualAllocEx(hProc, NULL, scLen,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProc, pRemote, shellcode, scLen, NULL);
// Step 2: enumerate threads of the target process
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { sizeof(te) };
Thread32First(hSnap, &te);
do {
if (te.th32OwnerProcessID != pid) continue;
HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, te.th32ThreadID);
if (!hThread) continue;
// Step 3: queue APC to this thread
// The APC function signature is: VOID CALLBACK ApcFunc(ULONG_PTR param)
// We point to our shellcode; shellcode must be callable with rcx = 0
QueueUserAPC((PAPCFUNC)pRemote, hThread, 0);
CloseHandle(hThread);
} while (Thread32Next(hSnap, &te));
CloseHandle(hSnap);
CloseHandle(hProc);
// Payload runs when a target thread next enters an alertable wait
return TRUE;
}
Most threads in most processes are never in an alertable wait — they use WaitForSingleObject without the alertable flag, or they use non-alertable message loops. Queuing an APC to a non-alertable thread means the APC may never run. The attacker's workaround: queue APCs to every thread in the target, hoping at least one becomes alertable. Or target known alertable threads: threads waiting in SleepEx, RPC threads, thread pool worker threads. The Early Bird technique (below) solves this by targeting the process's main thread before its own entry point runs.
Early Bird APC Injection
Early Bird queues the APC to a newly-created suspended process's main thread before any of the process's own code has run. When the thread is resumed, the loader initializes ntdll and calls NtTestAlert (which drains the APC queue) as part of the initialization sequence — before the process's entry point. The payload runs before the host process even starts:
// Early Bird: queue APC before process entry point runs
BOOL EarlyBirdAPC(const wchar_t *hostPath, PBYTE shellcode, SIZE_T scLen)
{
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi;
// Step 1: create the host process SUSPENDED
CreateProcessW(hostPath, NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// Step 2: write shellcode into the new process
LPVOID pRemote = VirtualAllocEx(pi.hProcess, NULL, scLen,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, pRemote, shellcode, scLen, NULL);
// Step 3: queue APC to the ONE suspended thread (the main thread)
// This thread is alertable during loader initialization
QueueUserAPC((PAPCFUNC)pRemote, pi.hThread, 0);
// Step 4: resume — ntdll loader runs, calls NtTestAlert,
// APC is dispatched, shellcode runs BEFORE host EP
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return TRUE;
}
Special User APC (NtQueueApcThreadEx)
A newer undocumented API NtQueueApcThreadEx with a "Special User APC" flag can queue an APC that is delivered even to non-alertable threads — the thread doesn't need to call an alertable wait function. This essentially gives the power of kernel APC delivery from user mode. Used by some advanced implants and post-exploitation frameworks when targeting threads that are never alertable:
// NtQueueApcThreadEx with Special APC flag (undocumented, Win10+)
typedef NTSTATUS (NTAPI *pNtQueueApcThreadEx)(
HANDLE ThreadHandle,
HANDLE UserApcReserveHandle, // 0 for special APC
PPS_APC_ROUTINE ApcRoutine,
PVOID SystemArgument1,
PVOID SystemArgument2,
PVOID SystemArgument3
);
// With UserApcReserveHandle = (HANDLE)0x1 (QUEUE_USER_APC_SPECIAL_USER_APC),
// the APC is delivered without requiring alertable state.
Detection
| Event | Source | Signal |
|---|---|---|
| No thread creation event | – | APC injection doesn't create a new thread; absence of Sysmon Event 8 is NOT a sign of clean injection |
| Process Access (Sysmon 10) | Sysmon | GrantedAccess with THREAD_SET_CONTEXT on target process threads; same memory write signals as classic injection |
| ETHREAD.Win32StartAddress | Memory forensics | Normal threads have Win32StartAddress pointing to a known module; APC-delivered code may change the effective start address if checked post-execution |
| ETW-TI QueueUserApcThread | ETW Threat Intelligence | ETW-TI fires on NtQueueApcThread calls from suspicious processes; includes target thread, source process, APC function pointer |
| MEM_PRIVATE executable region executed | Memory scan / ETW-TI | The APC runs in a MEM_PRIVATE RWX allocation — same indicator as classic shellcode injection |
Q & A
Why does the Windows loader call NtTestAlert during initialization, making Early Bird work?
During process initialization (in LdrInitializeThunk), after the initial setup of the process environment, the loader calls NtTestAlert. NtTestAlert is a simple kernel function that checks if there are any pending user-mode APCs in the current thread's queue and, if so, returns to user mode in a way that allows those APCs to be dispatched. This is designed to handle legitimate use cases where an APC is queued to a thread before it starts executing user code — for example, a debugger or profiler attaching to a process before its entry point. The loader calls NtTestAlert because it's in an "alertable-compatible" state at that moment, having just transitioned from kernel mode initialization. For the Early Bird injection: the malicious APC was queued to the main thread while it was suspended (before the loader even ran). When ResumeThread is called, the thread starts the kernel-to-user-mode initialization path, LdrInitializeThunk runs, calls NtTestAlert, the APC queue has the shellcode entry, and the shellcode executes — all before LdrInitializeThunk calls the process entry point. This is why Early Bird is considered particularly powerful: the shellcode runs at a trusted point in process initialization where security tools haven't yet fully initialized their own hooks in the process.
How does APC injection into a GUI application's main thread work if that thread is stuck in a message loop?
The standard Windows message loop (GetMessage / TranslateMessage / DispatchMessage) is not alertable — it uses the Win32 message queue, not alertable waits. An APC queued to a thread in a standard message loop will never be dispatched by that loop. However, many GUI applications occasionally call alertable waits: (1) They may call SleepEx(0, TRUE) or WaitForSingleObjectEx internally for event handling. (2) Applications that use MsgWaitForMultipleObjectsEx with MWMO_ALERTABLE will drain APCs as part of message processing. (3) Thread pool threads (which most modern applications use for background work) use alertable waits when idle. (4) COM-based applications that use CoWaitForMultipleHandles are in an alertable-compatible state. The practical approach in an APC injection: enumerate all threads of the target and queue the APC to all of them. Eventually, at least one thread will enter an alertable state (a background thread, a network-waiting thread, a timer thread) and dispatch the APC. The Special User APC technique (NtQueueApcThreadEx with QUEUE_USER_APC_SPECIAL_USER_APC) sidesteps this entirely — it forces APC delivery to a non-alertable thread by interrupting it at the next safe point (similar to how kernel APCs work). This is why Special User APCs are more reliable but also more suspicious — they interrupt threads in ways they didn't opt into.