NtWaitForWorkViaWorkerFactory
Bloquea un worker de threadpool hasta que haya un item disponible en la cola de finalización de la factory — el bucle caliente secuestrado por PoolParty.
Prototipo
NTSTATUS NtWaitForWorkViaWorkerFactory( HANDLE WorkerFactoryHandle, PFILE_IO_COMPLETION_INFORMATION MiniPacket );
Argumentos
| Name | Type | Dir | Description |
|---|---|---|---|
| WorkerFactoryHandle | HANDLE | in | Handle a la worker factory a la que está vinculado el hilo llamador. |
| MiniPacket | PFILE_IO_COMPLETION_INFORMATION | out | Recibe el paquete de finalización desencolado — KeyContext, ApcContext, IoStatusBlock.Status, IoStatusBlock.Information. |
IDs de syscalls por versión de Windows
| Versión de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0x1B6 | win10-1507 |
| Win10 1607 | 0x1BF | win10-1607 |
| Win10 1703 | 0x1C5 | win10-1703 |
| Win10 1709 | 0x1C9 | win10-1709 |
| Win10 1803 | 0x1CB | win10-1803 |
| Win10 1809 | 0x1CC | win10-1809 |
| Win10 1903 | 0x1CD | win10-1903 |
| Win10 1909 | 0x1CD | win10-1909 |
| Win10 2004 | 0x1D3 | win10-2004 |
| Win10 20H2 | 0x1D3 | win10-20h2 |
| Win10 21H1 | 0x1D3 | win10-21h1 |
| Win10 21H2 | 0x1D5 | win10-21h2 |
| Win10 22H2 | 0x1D5 | win10-22h2 |
| Win11 21H2 | 0x1DF | win11-21h2 |
| Win11 22H2 | 0x1E3 | win11-22h2 |
| Win11 23H2 | 0x1E3 | win11-23h2 |
| Win11 24H2 | 0x1E6 | win11-24h2 |
| Server 2016 | 0x1BF | winserver-2016 |
| Server 2019 | 0x1CC | winserver-2019 |
| Server 2022 | 0x1DB | winserver-2022 |
| Server 2025 | 0x1E6 | winserver-2025 |
Módulo del kernel
APIs relacionadas
Stub del syscall
4C 8B D1 mov r10, rcx B8 E6 01 00 00 mov eax, 0x1E6 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
Internamente esto es un dequeue fusionado del IoCompletion subyacente más una actualización del `WaitingWorkerCount` de la factory. El paquete devuelto sigue la disposición de cuatro campos de `FILE_IO_COMPLETION_INFORMATION` que usa NtRemoveIoCompletion, pero la espera está *ligada* a la factory — solo los hilos cuya worker factory coincide pueden consumir de su cola, y el kernel usa esta atadura para hacer cumplir límites de hilos y timeouts de inactividad. Un retorno `STATUS_TIMEOUT` hace que el threadpool considere recoger al worker; un retorno exitoso lo lanza a su bucle de despacho.
Uso común por malware
Toda la propuesta de valor de PoolParty es que al kernel *no* le importa qué código corra en un hilo que invoca este syscall — una vez que un worker queda bloqueado aquí, cualquier cosa escrita en el puerto de finalización aguas arriba (o lo que apunten `KeyContext`/`ApcContext` si el dispatcher en modo usuario confía en ellos) se convierte en flujo de ejecución controlado por el usuario. La mayoría de variantes de PoolParty no emiten NtWaitForWorkViaWorkerFactory ellas mismas: dejan que el legítimo ntdll!TppWorkerThread siga llamándolo, y luego envenenan la StartRoutine de la factory o los items que desencola. La variante 'worker via completion port' forja explícitamente paquetes que, al ser consumidos por este syscall y despachados por TppWorkerThread, terminan invocando código controlado por el atacante a través de la indirección de callback del threadpool.
Oportunidades de detección
Como NtWorkerFactoryWorkerReady, imposible de marcar de forma aislada — todo proceso Windows moderno tiene en todo momento decenas de hilos durmiendo aquí. La detección debe centrarse en el *contenido* de los paquetes que devuelve. Los proveedores ETW IoCompletion pueden muestrear el tráfico del puerto de finalización; los desajustes entre los contextos de callback esperados (que el threadpool sigue internamente en estructuras `TP_DIRECT` / `TP_CALLBACK_INSTANCE`) y lo que realmente se entregó son la señal de mayor fidelidad. El escaneo de memoria de las pilas de hilos worker en busca de direcciones de retorno fuera de los rangos conocidos de ntdll/kernel32 es un control conductual razonable tras observar una pareja Create/Release inter-proceso sospechosa.
Ejemplos de syscalls directos
cMinimal worker loop
// Stripped-down clone of ntdll!TppWorkerThread for educational use.
VOID WINAPI MyWorker(PVOID context) {
HANDLE hWf = (HANDLE)context;
FILE_IO_COMPLETION_INFORMATION packet;
for (;;) {
NTSTATUS s = NtWaitForWorkViaWorkerFactory(hWf, &packet);
if (s == STATUS_TIMEOUT) break; // pool decided to reap us
if (!NT_SUCCESS(s)) break;
// packet.KeyContext is *not* trusted in legitimate ntdll —
// it dispatches through internal TP_CALLBACK_INSTANCE tables.
Dispatch(&packet);
NtWorkerFactoryWorkerReady(hWf);
}
}asmx64 direct stub (Win11 24H2 SSN 0x1E6)
NtWaitForWorkViaWorkerFactory PROC
mov r10, rcx
mov eax, 1E6h
syscall
ret
NtWaitForWorkViaWorkerFactory ENDPrustObserve a hijacked completion
// Diagnostic helper used in PoolParty research — drains one packet from a
// worker factory and prints the would-be dispatch context.
use ntapi::ntioapi::FILE_IO_COMPLETION_INFORMATION;
use ntapi::ntexapi::NtWaitForWorkViaWorkerFactory;
unsafe fn observe_one(h_wf: windows_sys::Win32::Foundation::HANDLE) {
let mut p: FILE_IO_COMPLETION_INFORMATION = core::mem::zeroed();
let s = NtWaitForWorkViaWorkerFactory(h_wf, &mut p);
if s == 0 {
eprintln!(
"KeyContext={:p} ApcContext={:p} Status={:#x} Info={:#x}",
p.KeyContext, p.ApcContext,
p.IoStatusBlock.u.Status, p.IoStatusBlock.Information
);
}
}Mapeos MITRE ATT&CK
Last verified: 2026-05-20