NtSetIoCompletion
Poste un paquet de complétion sur un port de complétion d'E/S — le côté noyau de PostQueuedCompletionStatus et le vecteur de livraison des items forgés de PoolParty.
Prototype
NTSTATUS NtSetIoCompletion( HANDLE IoCompletionHandle, PVOID KeyContext, PVOID ApcContext, NTSTATUS IoStatus, ULONG_PTR IoStatusInformation );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| IoCompletionHandle | HANDLE | in | Handle vers le port de complétion cible. Nécessite IO_COMPLETION_MODIFY_STATE. |
| KeyContext | PVOID | in | CompletionKey défini par l'appelant, renvoyé au défileur via lpCompletionKey. |
| ApcContext | PVOID | in | Pointeur défini par l'appelant renvoyé en lpOverlapped — généralement un OVERLAPPED* en code légitime, contrôlé par l'attaquant dans PoolParty. |
| IoStatus | NTSTATUS | in | Code de statut écrit dans le champ Status de l'IO_STATUS_BLOCK du paquet. |
| IoStatusInformation | ULONG_PTR | in | Valeur définie par l'appelant écrite dans IO_STATUS_BLOCK.Information — typiquement un nombre d'octets transférés. |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0x185 | win10-1507 |
| Win10 1607 | 0x18E | win10-1607 |
| Win10 1703 | 0x194 | win10-1703 |
| Win10 1709 | 0x197 | win10-1709 |
| Win10 1803 | 0x199 | win10-1803 |
| Win10 1809 | 0x19A | win10-1809 |
| Win10 1903 | 0x19B | win10-1903 |
| Win10 1909 | 0x19B | win10-1909 |
| Win10 2004 | 0x1A1 | win10-2004 |
| Win10 20H2 | 0x1A1 | win10-20h2 |
| Win10 21H1 | 0x1A1 | win10-21h1 |
| Win10 21H2 | 0x1A3 | win10-21h2 |
| Win10 22H2 | 0x1A3 | win10-22h2 |
| Win11 21H2 | 0x1AC | win11-21h2 |
| Win11 22H2 | 0x1B0 | win11-22h2 |
| Win11 23H2 | 0x1B0 | win11-23h2 |
| Win11 24H2 | 0x1B3 | win11-24h2 |
| Server 2016 | 0x18E | winserver-2016 |
| Server 2019 | 0x19A | winserver-2019 |
| Server 2022 | 0x1A9 | winserver-2022 |
| Server 2025 | 0x1B3 | winserver-2025 |
Module noyau
APIs liées
Stub du syscall
4C 8B D1 mov r10, rcx B8 B3 01 00 00 mov eax, 0x1B3 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
Notes non documentées
Équivalent direct de `PostQueuedCompletionStatus`. Le paquet est ajouté à la KQUEUE du port et un attendeur (dans NtRemoveIoCompletion, NtRemoveIoCompletionEx ou NtWaitForWorkViaWorkerFactory) est réveillé. Le noyau n'interprète pas `KeyContext` ni `ApcContext` — ils sont renvoyés tels quels. Cette opacité rend le syscall très polyvalent pour le dispatch d'E/S asynchrone légitime comme pour l'injection malicieuse de paquets forgés.
Usage courant par les malwares
La variante PoolParty « Worker via Completion Port » : ouvrir le port de complétion du threadpool de la victime (souvent via un handle fuité ou NtDuplicateObject), puis émettre NtSetIoCompletion avec `ApcContext` pointant sur une structure de la forme `TP_DIRECT` — quand ntdll!TppWorkerThread défile le paquet, son dispatcher suit les pointeurs et appelle la fonction choisie par l'attaquant. Crucialement, aucune nouvelle mémoire ne doit être allouée chez la victime si un gadget utilisable existe déjà ; l'injection se résume à un seul syscall une fois le handle obtenu. Les paquets de complétion forgés ont aussi été historiquement utilisés par des chaînes d'exploitation contre des failles de callback Win32k et par diverses évasions de sandbox dans les navigateurs (thread IO de Chromium, broker d'Edge).
Opportunités de détection
Comme NtCreateIoCompletion, le syscall est bien trop courant en code légitime pour alerter dessus. La variante inter-processus est le signal actionnable : NtSetIoCompletion sur un handle dont l'objet sous-jacent appartient à un processus différent de l'appelant n'est essentiellement jamais légitime. ETW Microsoft-Windows-Kernel-IoCompletion peut exposer les événements post à coût élevé ; une approche moins coûteuse est ObRegisterCallbacks sur le type d'objet IoCompletion, conditionnée sur les duplications de handle inter-processus. L'inspection du contenu des paquets forgés est théoriquement possible mais coûteuse — les contextes en forme de TP_DIRECT déréférencés dans des régions VAD RWX sont une heuristique crédible.
Exemples de syscalls directs
cForged delivery — PoolParty 'Worker via Completion Port'
// hRemoteIocp is a handle in OUR address space that maps to the VICTIM's
// threadpool completion port (obtained via NtDuplicateObject or handle leak).
// pVictimGadget is a writable+executable address in the victim already shaped
// like a TP_DIRECT entry whose callback field points at shellcode.
NTSTATUS s = NtSetIoCompletion(
hRemoteIocp,
NULL, // KeyContext — irrelevant for TP_DIRECT path
pVictimGadget, // ApcContext — what TppWorkerThread will dispatch through
STATUS_SUCCESS,
0); // InformationcBenign use — wake a custom worker
// Plain PostQueuedCompletionStatus equivalent — every server you've written
// uses this under the hood.
NtSetIoCompletion(hIocp,
(PVOID)42, // your CompletionKey
(PVOID)pOverlapped, // your OVERLAPPED*
STATUS_SUCCESS,
bytesTransferred);asmx64 direct stub (Win11 24H2 SSN 0x1B3)
NtSetIoCompletion PROC
mov r10, rcx
mov eax, 1B3h
syscall
ret
NtSetIoCompletion ENDPMappings MITRE ATT&CK
Last verified: 2026-05-20