> Windows Syscalls
ntoskrnl.exeT1055T1106

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

NameTypeDirDescription
WaitCompletionPacketHandleHANDLEinHandle vers un wait-completion packet issu de NtCreateWaitCompletionPacket.
IoCompletionHandleHANDLEinHandle vers l'IOCP qui recevra une entrée de completion quand le dispatcher signalera.
TargetObjectHandleHANDLEinDispatcher object à attendre — event, sémaphore, mutant, timer, ou handle de processus/thread.
KeyContextPVOIDinValeur de completion-key délivrée comme lpCompletionKey à GetQueuedCompletionStatus.
ApcContextPVOIDinPointeur OVERLAPPED rapporté au consommateur de l'IOCP ; le threadpool y stocke son contexte de callback.
IoStatusNTSTATUSinValeur de statut à placer dans IO_STATUS_BLOCK.Status de l'entrée de completion résultante.
IoStatusInformationULONG_PTRinValeur d'information (octets transférés / scalaire arbitraire) pour IO_STATUS_BLOCK.Information.
AlreadySignaledPBOOLEANoutMis à 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 WindowsID de syscallBuild
Win10 15070x8Cwin10-1507
Win10 16070x8Cwin10-1607
Win10 17030x8Dwin10-1703
Win10 17090x8Dwin10-1709
Win10 18030x8Ewin10-1803
Win10 18090x8Ewin10-1809
Win10 19030x8Ewin10-1903
Win10 19090x8Ewin10-1909
Win10 20040x90win10-2004
Win10 20H20x90win10-20h2
Win10 21H10x90win10-21h1
Win10 21H20x90win10-21h2
Win10 22H20x90win10-22h2
Win11 21H20x90win11-21h2
Win11 22H20x90win11-22h2
Win11 23H20x90win11-23h2
Win11 24H20x92win11-24h2
Server 20160x8Cwinserver-2016
Server 20190x8Ewinserver-2019
Server 20220x90winserver-2022
Server 20250x92winserver-2025

Module noyau

ntoskrnl.exeNtAssociateWaitCompletionPacket

APIs liées

NtCreateWaitCompletionPacketNtCancelWaitCompletionPacketTpSetWaitCreateThreadpoolWaitSetThreadpoolWaitNtSetIoCompletionGetQueuedCompletionStatus

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 ENDP

Mappings MITRE ATT&CK

Last verified: 2026-05-20