NtRemoveIoCompletionEx
Retira hasta un número especificado por el llamador de paquetes de completion de un puerto de I/O completion en una sola syscall.
Prototipo
NTSTATUS NtRemoveIoCompletionEx( HANDLE IoCompletionHandle, PFILE_IO_COMPLETION_INFORMATION IoCompletionInformation, ULONG Count, PULONG NumEntriesRemoved, PLARGE_INTEGER Timeout, BOOLEAN Alertable );
Argumentos
| Name | Type | Dir | Description |
|---|---|---|---|
| IoCompletionHandle | HANDLE | in | Handle al objeto IoCompletion abierto con IO_COMPLETION_MODIFY_STATE. |
| IoCompletionInformation | PFILE_IO_COMPLETION_INFORMATION | out | Array asignado por el llamador de Count entradas que recibe tuplas (KeyContext, ApcContext, IoStatusBlock). |
| Count | ULONG | in | Número máximo de paquetes a retirar en esta llamada. |
| NumEntriesRemoved | PULONG | out | Recibe el número real de paquetes devueltos (1..Count). |
| Timeout | PLARGE_INTEGER | in | Timeout opcional en unidades de 100ns relativo (negativo) o absoluto (positivo). NULL espera indefinidamente. |
| Alertable | BOOLEAN | in | TRUE permite que la espera sea interrumpida por una APC en modo usuario (devuelve STATUS_USER_APC). |
IDs de syscalls por versión de Windows
| Versión de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0x156 | win10-1507 |
| Win10 1607 | 0x15D | win10-1607 |
| Win10 1703 | 0x163 | win10-1703 |
| Win10 1709 | 0x166 | win10-1709 |
| Win10 1803 | 0x168 | win10-1803 |
| Win10 1809 | 0x169 | win10-1809 |
| Win10 1903 | 0x16A | win10-1903 |
| Win10 1909 | 0x16A | win10-1909 |
| Win10 2004 | 0x170 | win10-2004 |
| Win10 20H2 | 0x170 | win10-20h2 |
| Win10 21H1 | 0x170 | win10-21h1 |
| Win10 21H2 | 0x172 | win10-21h2 |
| Win10 22H2 | 0x172 | win10-22h2 |
| Win11 21H2 | 0x17A | win11-21h2 |
| Win11 22H2 | 0x17D | win11-22h2 |
| Win11 23H2 | 0x17D | win11-23h2 |
| Win11 24H2 | 0x17F | win11-24h2 |
| Server 2016 | 0x15D | winserver-2016 |
| Server 2019 | 0x169 | winserver-2019 |
| Server 2022 | 0x178 | winserver-2022 |
| Server 2025 | 0x17F | winserver-2025 |
Módulo del kernel
APIs relacionadas
Stub del syscall
4C 8B D1 mov r10, rcx B8 7F 01 00 00 mov eax, 0x17F 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
Primo en batch de `NtRemoveIoCompletion`. Donde la syscall legacy devuelve a lo sumo un paquete de completion por llamada, `NtRemoveIoCompletionEx` retorna hasta `Count` paquetes en un solo round-trip — una ganancia significativa para servidores de alto throughput (IIS, SQL Server, el hilo I/O completion del ThreadPool .NET, todo thread-pool Win32 con `WT_EXECUTEINIOTHREAD`). El wrapper Win32 es `GetQueuedCompletionStatusEx`. Devuelve STATUS_TIMEOUT si Timeout expira sin paquete, STATUS_USER_APC si Alertable y se disparó una APC, STATUS_ABANDONED_WAIT_0 si el objeto IoCompletion se cierra durante la espera. Disponible desde Windows Vista — API totalmente estable desde entonces, solo el SSN ha cambiado.
Uso común por malware
**Señal de malware débil**. Este syscall es abrumadoramente una primitiva de servidor/runtime de alto rendimiento — el ThreadPool .NET, el worker I/O del thread-pool Win32, y todo producto servidor Windows lo invocan constantemente. Las pocas menciones de malware en la literatura involucran código C2 *del lado servidor* de alta concurrencia (paneles y bots que actúan como servidores), no implantes. Un pequeño número de implantes pesados en red (algunos puentes external-C2 de Cobalt Strike, ciertos transportes custom de Sliver construidos sobre IO completion ports) lo usan porque reutilizan un modelo de threading estilo servidor — y ahí en telemetría se ve idéntico al tráfico WCF / ASP.NET Core legítimo. Como primitiva ofensiva no añade sigilo sobre el ReadFile / WSARecv subyacente.
Oportunidades de detección
Sin señal de detección útil en la práctica — `NtRemoveIoCompletionEx` es uno de los syscalls de mayor volumen en cualquier servidor Windows y un zumbido de fondo constante en workstations (cada pestaña de navegador, cada agente de almacenamiento cloud, cada language server de IDE). No hay pivot telemétrico por llamada que separe malicioso de benigno de forma significativa. Los defensores deben enfocarse en los *vecinos* del puerto IO completion: qué handles se encolan (sockets a endpoints sospechosos, handles a rutas sensibles), y el patrón de threading del consumidor (un worker que hace lecturas en completion port seguidas inmediatamente de NtCreateThreadEx en un proceso remoto es la única bandera débil modelable). Los EDR no suelen hookear este syscall por volumen — las variantes con syscall directo no ganan nada.
Ejemplos de syscalls directos
asmx64 direct stub (Win11 24H2)
; Direct syscall stub for NtRemoveIoCompletionEx (SSN 0x17F on Win11 24H2)
NtRemoveIoCompletionEx PROC
mov r10, rcx ; syscall convention
mov eax, 17Fh ; SSN — varies per build
syscall
ret
NtRemoveIoCompletionEx ENDPcBatched dequeue via GetQueuedCompletionStatusEx
// Standard server-thread loop: pull up to 16 packets per syscall.
// This is what nearly every IOCP-based service does.
#include <windows.h>
VOID IocpWorker(HANDLE hPort) {
OVERLAPPED_ENTRY entries[16];
ULONG got = 0;
while (GetQueuedCompletionStatusEx(hPort, entries, 16, &got, INFINITE, FALSE)) {
for (ULONG i = 0; i < got; ++i) {
DispatchCompletion(&entries[i]);
}
}
}rustDirect-syscall wrapper
// Cargo: windows-sys = "0.59"
// Minimal wrapper around the Ex syscall — used by a custom server runtime.
use std::ptr::null_mut;
#[repr(C)]
pub struct IoCompletionInfo {
pub key_context: usize,
pub apc_context: usize,
pub io_status: [usize; 2], // status + information
}
extern "system" {
fn NtRemoveIoCompletionEx(h: isize, info: *mut IoCompletionInfo,
count: u32, removed: *mut u32, timeout: *mut i64, alertable: u8) -> i32;
}
pub unsafe fn drain(h: isize, batch: &mut [IoCompletionInfo]) -> u32 {
let mut n: u32 = 0;
NtRemoveIoCompletionEx(h, batch.as_mut_ptr(), batch.len() as u32,
&mut n, null_mut(), 0);
n
}Mapeos MITRE ATT&CK
Last verified: 2026-05-20