> Windows Syscalls
ntoskrnl.exeT1055T1106

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

NameTypeDirDescription
WaitCompletionPacketHandleHANDLEinHandle a un wait-completion packet de NtCreateWaitCompletionPacket.
IoCompletionHandleHANDLEinHandle al IOCP que recibirá una entrada de completion cuando el dispatcher señale.
TargetObjectHandleHANDLEinDispatcher object a esperar — event, semáforo, mutant, timer, o handle de proceso/hilo.
KeyContextPVOIDinValor completion-key entregado como lpCompletionKey a GetQueuedCompletionStatus.
ApcContextPVOIDinPuntero OVERLAPPED reportado al consumidor del IOCP; el threadpool guarda aquí su contexto de callback.
IoStatusNTSTATUSinValor de estado a colocar en IO_STATUS_BLOCK.Status de la entrada de completion resultante.
IoStatusInformationULONG_PTRinValor de información (bytes transferidos / escalar arbitrario) para IO_STATUS_BLOCK.Information.
AlreadySignaledPBOOLEANoutPuesto 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 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

Módulo del kernel

ntoskrnl.exeNtAssociateWaitCompletionPacket

APIs relacionadas

NtCreateWaitCompletionPacketNtCancelWaitCompletionPacketTpSetWaitCreateThreadpoolWaitSetThreadpoolWaitNtSetIoCompletionGetQueuedCompletionStatus

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 ENDP

Mapeos MITRE ATT&CK

Last verified: 2026-05-20