> Windows Syscalls
ntoskrnl.exeT1055T1106

NtAssociateWaitCompletionPacket

Binds a wait-completion packet to a dispatcher object so its signal posts an entry to an IOCP.

Prototype

NTSTATUS NtAssociateWaitCompletionPacket(
  HANDLE   WaitCompletionPacketHandle,
  HANDLE   IoCompletionHandle,
  HANDLE   TargetObjectHandle,
  PVOID    KeyContext,
  PVOID    ApcContext,
  NTSTATUS IoStatus,
  ULONG_PTR IoStatusInformation,
  PBOOLEAN AlreadySignaled
);

Arguments

NameTypeDirDescription
WaitCompletionPacketHandleHANDLEinHandle to a wait-completion packet from NtCreateWaitCompletionPacket.
IoCompletionHandleHANDLEinHandle to the IOCP that will receive a completion entry when the dispatcher signals.
TargetObjectHandleHANDLEinDispatcher object to wait on — event, semaphore, mutant, timer, or process/thread handle.
KeyContextPVOIDinCompletion-key value delivered as lpCompletionKey to GetQueuedCompletionStatus.
ApcContextPVOIDinOVERLAPPED pointer reported back to the IOCP consumer; threadpool stores its callback context here.
IoStatusNTSTATUSinStatus value to place in the IO_STATUS_BLOCK.Status of the resulting completion entry.
IoStatusInformationULONG_PTRinInformation value (bytes transferred / arbitrary scalar) for IO_STATUS_BLOCK.Information.
AlreadySignaledPBOOLEANoutSet to TRUE if the dispatcher was already signaled at association time (completion was queued immediately).

Syscall IDs by Windows version

Windows versionSyscall IDBuild
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

Kernel module

ntoskrnl.exeNtAssociateWaitCompletionPacket

Related APIs

NtCreateWaitCompletionPacketNtCancelWaitCompletionPacketTpSetWaitCreateThreadpoolWaitSetThreadpoolWaitNtSetIoCompletionGetQueuedCompletionStatus

Syscall stub

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

Undocumented notes

The eight-argument workhorse behind `CreateThreadpoolWait` / `SetThreadpoolWait`. After ntdll creates a packet with `NtCreateWaitCompletionPacket`, it calls `NtAssociateWaitCompletionPacket` to glue the packet to (a) the IOCP that backs the threadpool worker factory and (b) the dispatcher object the user-mode code wants to wait on. When the dispatcher signals, the kernel posts an `(IoStatus, IoStatusInformation, KeyContext, ApcContext)` quadruple into the IOCP — `GetQueuedCompletionStatus` then dequeues it on a worker thread, which `ntdll!TppWaitpCallbackEpilog` routes to the user's `WAITORTIMERCALLBACK`. The interesting parameters from an attacker's perspective are `KeyContext` and `ApcContext`: both are forwarded *unchecked* from user mode into kernel-issued IOCP entries.

Common malware usage

Heart of **PoolParty Variant 7 — "Worker Factory via Wait Completion"** (SafeBreach Labs, Black Hat EU 2023). The technique: duplicate the target process's threadpool IOCP and an existing `WAIT_COMPLETION_PACKET` into the attacker's process, then call `NtAssociateWaitCompletionPacket` with crafted `KeyContext`/`ApcContext`/`IoStatusInformation` values that, when `ntdll!TppWorkerThread` in the target consumes the forged completion, are interpreted as a `_TP_WAIT*` whose callback pointer aims at attacker-controlled shellcode already mapped into the target. Because the IOCP completion is *kernel-posted*, EDR user-mode hooks on `QueueUserAPC`, `CreateRemoteThread`, `NtSetIoCompletion`, etc., never observe the moment of dispatch — the worker thread "naturally" wakes up. The technique works across PPL boundaries that block APC and CRT injection.

Detection opportunities

Cross-process handle duplication of `WaitCompletionPacket` or `IoCompletion` objects (Sysmon Event 10 with object-type telemetry, or kernel-mode `ObRegisterCallbacks` on `*ObjectType` for these classes) is the upstream signal. Downstream, EDRs that periodically walk `_TP_POOL → _TP_WAIT → Callback` lists in instrumented processes and validate that callback addresses point into known modules (not into anonymous RWX VADs) will catch the dispatch. ETW Microsoft-Windows-Threading does not natively cover this. Forensically, a `WAIT_COMPLETION_PACKET` in process A whose `TargetObject` field points at an object in process B is pathognomonic of the technique.

Direct syscall examples

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

MITRE ATT&CK mappings

Last verified: 2026-05-20