NtSetIoCompletion
Postea un paquete de finalización a un puerto de finalización de E/S — el lado del kernel de PostQueuedCompletionStatus y el vector de entrega de los items falsificados de PoolParty.
Prototipo
NTSTATUS NtSetIoCompletion( HANDLE IoCompletionHandle, PVOID KeyContext, PVOID ApcContext, NTSTATUS IoStatus, ULONG_PTR IoStatusInformation );
Argumentos
| Name | Type | Dir | Description |
|---|---|---|---|
| IoCompletionHandle | HANDLE | in | Handle al puerto de finalización objetivo. Requiere IO_COMPLETION_MODIFY_STATE. |
| KeyContext | PVOID | in | CompletionKey definido por el llamador, devuelto al desencolador vía lpCompletionKey. |
| ApcContext | PVOID | in | Puntero definido por el llamador devuelto como lpOverlapped — normalmente un OVERLAPPED* en código legítimo, controlado por el atacante en PoolParty. |
| IoStatus | NTSTATUS | in | Código de estado escrito en el campo Status del IO_STATUS_BLOCK del paquete. |
| IoStatusInformation | ULONG_PTR | in | Valor definido por el llamador escrito en IO_STATUS_BLOCK.Information — típicamente un conteo de bytes transferidos. |
IDs de syscalls por versión de Windows
| Versión 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 |
Módulo del kernel
APIs relacionadas
Stub del 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
Notas no documentadas
Equivalente directo de `PostQueuedCompletionStatus`. El paquete se añade a la KQUEUE del puerto y se despierta un esperante (en NtRemoveIoCompletion, NtRemoveIoCompletionEx o NtWaitForWorkViaWorkerFactory). El kernel no interpreta `KeyContext` ni `ApcContext` — se devuelven literales. Esa opacidad hace al syscall muy versátil tanto para el despacho legítimo de E/S asíncrona como para la inyección maliciosa de paquetes forjados.
Uso común por malware
La variante PoolParty 'Worker via Completion Port': abrir el puerto de finalización del threadpool de la víctima (a menudo vía un handle filtrado o NtDuplicateObject), luego emitir NtSetIoCompletion con `ApcContext` apuntando a una estructura con forma `TP_DIRECT` — cuando ntdll!TppWorkerThread desencola el paquete, su dispatcher seguirá los punteros y llamará la función elegida por el atacante. Crucialmente, no necesita asignarse nueva memoria en la víctima si ya existe un gadget utilizable; la inyección se reduce a un solo syscall una vez obtenido el handle. Paquetes de finalización falsificados también han sido usados históricamente por cadenas de exploit contra fallos de callback de Win32k y por varias evasiones de sandbox en navegadores (hilo IO de Chromium, broker de Edge).
Oportunidades de detección
Como NtCreateIoCompletion, el syscall es demasiado común en código legítimo para alertar directamente. La variante inter-proceso es la señal accionable: NtSetIoCompletion sobre un handle cuyo objeto subyacente pertenece a un proceso distinto del llamador es esencialmente nunca legítimo. El ETW Microsoft-Windows-Kernel-IoCompletion puede exponer eventos post a alto coste; un enfoque más liviano es ObRegisterCallbacks sobre el tipo de objeto IoCompletion, condicionado a duplicaciones de handle inter-proceso. La inspección del contenido de paquetes forjados es teóricamente posible pero costosa — contextos con forma de TP_DIRECT desreferenciados en regiones VAD RWX son una heurística creíble.
Ejemplos de syscalls directos
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 ENDPMapeos MITRE ATT&CK
Last verified: 2026-05-20