NtAssociateWaitCompletionPacket
Lie un wait-completion packet à un dispatcher object pour que son signal poste une entrée dans un IOCP.
Prototype
NTSTATUS NtAssociateWaitCompletionPacket( HANDLE WaitCompletionPacketHandle, HANDLE IoCompletionHandle, HANDLE TargetObjectHandle, PVOID KeyContext, PVOID ApcContext, NTSTATUS IoStatus, ULONG_PTR IoStatusInformation, PBOOLEAN AlreadySignaled );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| WaitCompletionPacketHandle | HANDLE | in | Handle vers un wait-completion packet issu de NtCreateWaitCompletionPacket. |
| IoCompletionHandle | HANDLE | in | Handle vers l'IOCP qui recevra une entrée de completion quand le dispatcher signalera. |
| TargetObjectHandle | HANDLE | in | Dispatcher object à attendre — event, sémaphore, mutant, timer, ou handle de processus/thread. |
| KeyContext | PVOID | in | Valeur de completion-key délivrée comme lpCompletionKey à GetQueuedCompletionStatus. |
| ApcContext | PVOID | in | Pointeur OVERLAPPED rapporté au consommateur de l'IOCP ; le threadpool y stocke son contexte de callback. |
| IoStatus | NTSTATUS | in | Valeur de statut à placer dans IO_STATUS_BLOCK.Status de l'entrée de completion résultante. |
| IoStatusInformation | ULONG_PTR | in | Valeur d'information (octets transférés / scalaire arbitraire) pour IO_STATUS_BLOCK.Information. |
| AlreadySignaled | PBOOLEAN | out | Mis à TRUE si le dispatcher était déjà signalé au moment de l'association (completion immédiatement mise en file). |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0x8C | win10-1507 |
| Win10 1607 | 0x8C | win10-1607 |
| Win10 1703 | 0x8D | win10-1703 |
| Win10 1709 | 0x8D | win10-1709 |
| Win10 1803 | 0x8E | win10-1803 |
| Win10 1809 | 0x8E | win10-1809 |
| Win10 1903 | 0x8E | win10-1903 |
| Win10 1909 | 0x8E | win10-1909 |
| Win10 2004 | 0x90 | win10-2004 |
| Win10 20H2 | 0x90 | win10-20h2 |
| Win10 21H1 | 0x90 | win10-21h1 |
| Win10 21H2 | 0x90 | win10-21h2 |
| Win10 22H2 | 0x90 | win10-22h2 |
| Win11 21H2 | 0x90 | win11-21h2 |
| Win11 22H2 | 0x90 | win11-22h2 |
| Win11 23H2 | 0x90 | win11-23h2 |
| Win11 24H2 | 0x92 | win11-24h2 |
| Server 2016 | 0x8C | winserver-2016 |
| Server 2019 | 0x8E | winserver-2019 |
| Server 2022 | 0x90 | winserver-2022 |
| Server 2025 | 0x92 | winserver-2025 |
Module noyau
APIs liées
Stub du syscall
4C 8B D1 mov r10, rcx B8 92 00 00 00 mov eax, 0x92 ; Win11 24H2 SSN 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
Le cheval de trait à huit arguments derrière `CreateThreadpoolWait` / `SetThreadpoolWait`. Après que ntdll crée un paquet avec `NtCreateWaitCompletionPacket`, il appelle `NtAssociateWaitCompletionPacket` pour coller le paquet à (a) l'IOCP qui soutient la worker factory du threadpool et (b) le dispatcher object que le code user-mode veut attendre. Quand le dispatcher signale, le noyau poste un quadruplet `(IoStatus, IoStatusInformation, KeyContext, ApcContext)` dans l'IOCP — `GetQueuedCompletionStatus` le dépile alors sur un thread worker, que `ntdll!TppWaitpCallbackEpilog` route vers le `WAITORTIMERCALLBACK` de l'utilisateur. Les paramètres intéressants du point de vue d'un attaquant sont `KeyContext` et `ApcContext` : tous deux sont transmis *sans vérification* depuis user mode jusque dans les entrées IOCP émises par le noyau.
Usage courant par les malwares
Cœur de **PoolParty Variant 7 — « Worker Factory via Wait Completion »** (SafeBreach Labs, Black Hat EU 2023). La technique : dupliquer l'IOCP du threadpool du processus cible et un `WAIT_COMPLETION_PACKET` existant dans le processus de l'attaquant, puis appeler `NtAssociateWaitCompletionPacket` avec des valeurs `KeyContext`/`ApcContext`/`IoStatusInformation` forgées qui, lorsque `ntdll!TppWorkerThread` dans la cible consomme la completion falsifiée, sont interprétées comme un `_TP_WAIT*` dont le pointeur de callback vise un shellcode contrôlé par l'attaquant déjà mappé dans la cible. Comme la completion IOCP est *postée par le noyau*, les hooks user-mode EDR sur `QueueUserAPC`, `CreateRemoteThread`, `NtSetIoCompletion`, etc., n'observent jamais le moment du dispatch — le thread worker se « réveille » naturellement. La technique fonctionne à travers les frontières PPL qui bloquent l'injection APC et CRT.
Opportunités de détection
La duplication de handle cross-process d'objets `WaitCompletionPacket` ou `IoCompletion` (Sysmon Event 10 avec télémétrie de type d'objet, ou `ObRegisterCallbacks` kernel-mode sur les `*ObjectType` de ces classes) est le signal amont. En aval, les EDR qui parcourent périodiquement les listes `_TP_POOL → _TP_WAIT → Callback` dans les processus instrumentés et valident que les adresses de callback pointent dans des modules connus (pas dans des VAD RWX anonymes) attraperont le dispatch. ETW Microsoft-Windows-Threading ne couvre pas cela nativement. Forensique : un `WAIT_COMPLETION_PACKET` dans le processus A dont le champ `TargetObject` pointe sur un objet du processus B est pathognomonique de la technique.
Exemples de syscalls directs
cPoolParty Variant 7 skeleton
// Assumes earlier steps duplicated hVictimIocp and hVictimPacket from
// the target process into our own, and shellcodeRemoteVA is RX in the target.
BOOLEAN alreadySignaled = FALSE;
// KeyContext/ApcContext are forwarded raw — used by ntdll!TppWorkerThread
// to locate a _TP_WAIT whose Callback we already overwrote to shellcodeRemoteVA.
PVOID keyContext = (PVOID)pForgedTpWaitInTarget;
PVOID apcContext = (PVOID)pForgedOverlappedInTarget;
NTSTATUS st = NtAssociateWaitCompletionPacket(
hVictimPacket,
hVictimIocp,
hSignaledEvent, // any already-signaled event we own
keyContext,
apcContext,
STATUS_SUCCESS,
0,
&alreadySignaled);
// At this point the kernel posts a completion into the *victim's* IOCP.
// The victim's threadpool worker thread wakes naturally and dispatches
// into shellcodeRemoteVA — no APC, no CreateRemoteThread, no NtSetContext.cLegitimate threadpool-style use
// Roughly what ntdll!TpAllocWait + TpSetWait do internally.
HANDLE hPacket = NULL;
OBJECT_ATTRIBUTES oa = { sizeof(oa) };
NtCreateWaitCompletionPacket(&hPacket, GENERIC_ALL, &oa);
BOOLEAN already = FALSE;
NtAssociateWaitCompletionPacket(
hPacket,
hThreadpoolIocp, // pool's internal IOCP
hUserEvent, // what the user wants to wait on
(PVOID)pTpWait, // identifies our _TP_WAIT for dispatch
(PVOID)&tpWait->Overlapped,
STATUS_SUCCESS, 0,
&already);asmx64 direct stub
; NtAssociateWaitCompletionPacket direct syscall (Win11 24H2 SSN 0x92)
; 8 args: 4 in RCX/RDX/R8/R9, the remaining 4 spilled on the stack at [rsp+28h..40h].
NtAssociateWaitCompletionPacket PROC
mov r10, rcx
mov eax, 92h
syscall
ret
NtAssociateWaitCompletionPacket ENDPMappings MITRE ATT&CK
Last verified: 2026-05-20