NtAssociateWaitCompletionPacket
Enlaza un wait-completion packet a un dispatcher object para que su señal postee una entrada en un IOCP.
Prototipo
NTSTATUS NtAssociateWaitCompletionPacket( HANDLE WaitCompletionPacketHandle, HANDLE IoCompletionHandle, HANDLE TargetObjectHandle, PVOID KeyContext, PVOID ApcContext, NTSTATUS IoStatus, ULONG_PTR IoStatusInformation, PBOOLEAN AlreadySignaled );
Argumentos
| Name | Type | Dir | Description |
|---|---|---|---|
| WaitCompletionPacketHandle | HANDLE | in | Handle a un wait-completion packet de NtCreateWaitCompletionPacket. |
| IoCompletionHandle | HANDLE | in | Handle al IOCP que recibirá una entrada de completion cuando el dispatcher señale. |
| TargetObjectHandle | HANDLE | in | Dispatcher object a esperar — event, semáforo, mutant, timer, o handle de proceso/hilo. |
| KeyContext | PVOID | in | Valor completion-key entregado como lpCompletionKey a GetQueuedCompletionStatus. |
| ApcContext | PVOID | in | Puntero OVERLAPPED reportado al consumidor del IOCP; el threadpool guarda aquí su contexto de callback. |
| IoStatus | NTSTATUS | in | Valor de estado a colocar en IO_STATUS_BLOCK.Status de la entrada de completion resultante. |
| IoStatusInformation | ULONG_PTR | in | Valor de información (bytes transferidos / escalar arbitrario) para IO_STATUS_BLOCK.Information. |
| AlreadySignaled | PBOOLEAN | out | Puesto a TRUE si el dispatcher ya estaba señalado al momento de asociar (completion encolada de inmediato). |
IDs de syscalls por versión de Windows
| Versión 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 |
Módulo del kernel
APIs relacionadas
Stub del 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
Notas no documentadas
El caballo de batalla de ocho argumentos detrás de `CreateThreadpoolWait` / `SetThreadpoolWait`. Después de que ntdll crea un paquete con `NtCreateWaitCompletionPacket`, llama a `NtAssociateWaitCompletionPacket` para pegar el paquete a (a) el IOCP que respalda la worker factory del threadpool y (b) el dispatcher object que el código user-mode quiere esperar. Cuando el dispatcher señala, el kernel postea un cuádruple `(IoStatus, IoStatusInformation, KeyContext, ApcContext)` en el IOCP — `GetQueuedCompletionStatus` lo desencola en un hilo worker, que `ntdll!TppWaitpCallbackEpilog` enruta al `WAITORTIMERCALLBACK` del usuario. Los parámetros interesantes desde la perspectiva del atacante son `KeyContext` y `ApcContext`: ambos se reenvían *sin verificar* desde modo usuario hasta las entradas IOCP emitidas por el kernel.
Uso común por malware
Corazón de **PoolParty Variant 7 — "Worker Factory via Wait Completion"** (SafeBreach Labs, Black Hat EU 2023). La técnica: duplicar el IOCP del threadpool del proceso objetivo y un `WAIT_COMPLETION_PACKET` existente al proceso del atacante, luego llamar a `NtAssociateWaitCompletionPacket` con valores `KeyContext`/`ApcContext`/`IoStatusInformation` forjados que, cuando `ntdll!TppWorkerThread` en el objetivo consume la completion falsificada, son interpretados como un `_TP_WAIT*` cuyo puntero de callback apunta a shellcode controlado por el atacante ya mapeado en el objetivo. Como la completion IOCP es *posteada por el kernel*, los hooks user-mode de EDR sobre `QueueUserAPC`, `CreateRemoteThread`, `NtSetIoCompletion`, etc., nunca observan el momento del dispatch — el hilo worker "naturalmente" se despierta. La técnica funciona a través de fronteras PPL que bloquean inyección APC y CRT.
Oportunidades de detección
La duplicación cross-process de handles `WaitCompletionPacket` o `IoCompletion` (Sysmon Event 10 con telemetría de tipo de objeto, o `ObRegisterCallbacks` en modo kernel sobre los `*ObjectType` de estas clases) es la señal aguas arriba. Aguas abajo, los EDRs que recorren periódicamente las listas `_TP_POOL → _TP_WAIT → Callback` en procesos instrumentados y validan que las direcciones de callback apuntan a módulos conocidos (no a VADs RWX anónimos) atraparán el dispatch. ETW Microsoft-Windows-Threading no cubre esto nativamente. Forense: un `WAIT_COMPLETION_PACKET` en el proceso A cuyo campo `TargetObject` apunta a un objeto del proceso B es patognomónico de la técnica.
Ejemplos de syscalls directos
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 ENDPMapeos MITRE ATT&CK
Last verified: 2026-05-20