NtAssociateWaitCompletionPacket
Bindet ein Wait-Completion-Packet an ein Dispatcher-Object, sodass dessen Signal einen Eintrag in einen IOCP postet.
Prototyp
NTSTATUS NtAssociateWaitCompletionPacket( HANDLE WaitCompletionPacketHandle, HANDLE IoCompletionHandle, HANDLE TargetObjectHandle, PVOID KeyContext, PVOID ApcContext, NTSTATUS IoStatus, ULONG_PTR IoStatusInformation, PBOOLEAN AlreadySignaled );
Argumente
| Name | Type | Dir | Description |
|---|---|---|---|
| WaitCompletionPacketHandle | HANDLE | in | Handle auf ein Wait-Completion-Packet aus NtCreateWaitCompletionPacket. |
| IoCompletionHandle | HANDLE | in | Handle auf den IOCP, der bei Signalisierung des Dispatchers einen Completion-Eintrag erhält. |
| TargetObjectHandle | HANDLE | in | Dispatcher-Object zum Warten — Event, Semaphore, Mutant, Timer oder Prozess-/Thread-Handle. |
| KeyContext | PVOID | in | Completion-Key-Wert, der als lpCompletionKey an GetQueuedCompletionStatus übergeben wird. |
| ApcContext | PVOID | in | OVERLAPPED-Zeiger, der an den IOCP-Konsumenten zurückgemeldet wird; der Threadpool legt hier seinen Callback-Kontext ab. |
| IoStatus | NTSTATUS | in | In IO_STATUS_BLOCK.Status des resultierenden Completion-Eintrags zu setzender Statuswert. |
| IoStatusInformation | ULONG_PTR | in | Information-Wert (übertragene Bytes / beliebiger Skalar) für IO_STATUS_BLOCK.Information. |
| AlreadySignaled | PBOOLEAN | out | Wird auf TRUE gesetzt, falls der Dispatcher beim Assoziieren bereits signalisiert war (Completion sofort eingereiht). |
Syscall-IDs pro 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-Modul
Verwandte 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
Undokumentierte Hinweise
Das achtargumentige Arbeitspferd hinter `CreateThreadpoolWait` / `SetThreadpoolWait`. Nachdem ntdll ein Paket mit `NtCreateWaitCompletionPacket` erzeugt, ruft es `NtAssociateWaitCompletionPacket` auf, um das Paket an (a) den IOCP, der die Worker-Factory des Threadpools trägt, und (b) das Dispatcher-Object zu kleben, auf das der User-Mode-Code warten will. Wenn der Dispatcher signalisiert, postet der Kernel ein `(IoStatus, IoStatusInformation, KeyContext, ApcContext)`-Quadrupel in den IOCP — `GetQueuedCompletionStatus` dequeued es auf einem Worker-Thread, den `ntdll!TppWaitpCallbackEpilog` zum `WAITORTIMERCALLBACK` des Benutzers routet. Aus Angreifersicht sind `KeyContext` und `ApcContext` interessant: beide werden *ungeprüft* aus dem User-Mode in vom Kernel emittierte IOCP-Einträge weitergereicht.
Häufige Malware-Nutzung
Herzstück von **PoolParty Variant 7 — „Worker Factory via Wait Completion"** (SafeBreach Labs, Black Hat EU 2023). Die Technik: Den Threadpool-IOCP des Zielprozesses und ein bestehendes `WAIT_COMPLETION_PACKET` in den Angreiferprozess duplizieren, dann `NtAssociateWaitCompletionPacket` mit gefälschten `KeyContext`/`ApcContext`/`IoStatusInformation`-Werten aufrufen, die, sobald `ntdll!TppWorkerThread` im Ziel die gefälschte Completion konsumiert, als `_TP_WAIT*` interpretiert werden, dessen Callback-Zeiger auf bereits ins Ziel gemappten, angreiferkontrollierten Shellcode zielt. Da die IOCP-Completion *kernel-postet* ist, sehen EDR-User-Mode-Hooks auf `QueueUserAPC`, `CreateRemoteThread`, `NtSetIoCompletion` usw. den Moment des Dispatch nie — der Worker-Thread „erwacht" auf natürliche Weise. Die Technik funktioniert auch über PPL-Grenzen hinweg, die APC- und CRT-Injection blockieren.
Erkennungsmöglichkeiten
Prozessübergreifende Handle-Duplikation von `WaitCompletionPacket`- oder `IoCompletion`-Objects (Sysmon-Event 10 mit Object-Type-Telemetrie oder Kernel-Mode-`ObRegisterCallbacks` auf den `*ObjectType` dieser Klassen) ist das Upstream-Signal. Downstream fangen EDRs, die periodisch die Listen `_TP_POOL → _TP_WAIT → Callback` in instrumentierten Prozessen durchlaufen und validieren, dass Callback-Adressen in bekannte Module zeigen (nicht in anonyme RWX-VADs), den Dispatch ab. ETW Microsoft-Windows-Threading deckt das nicht nativ ab. Forensisch ist ein `WAIT_COMPLETION_PACKET` in Prozess A, dessen `TargetObject`-Feld auf ein Objekt in Prozess B zeigt, pathognomonisch für die Technik.
Direkte Syscall-Beispiele
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