NtWaitForAlertByThreadId
Aparca el hilo llamante hasta que NtAlertThreadByThreadId lo despierte — la cara kernel de WaitOnAddress.
Prototipo
NTSTATUS NtWaitForAlertByThreadId( PVOID Address, PLARGE_INTEGER Timeout );
Argumentos
| Name | Type | Dir | Description |
|---|---|---|---|
| Address | PVOID | in | Puntero opaco usado como clave hash en la tabla de espera por proceso — típicamente la dirección de la palabra de sincronización en modo usuario. |
| Timeout | PLARGE_INTEGER | in | Intervalo de 100 ns opcional. Negativo = timeout relativo. NULL = espera infinita. Devuelve STATUS_TIMEOUT al expirar. |
IDs de syscalls por versión de Windows
| Versión de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0x1B3 | win10-1507 |
| Win10 1607 | 0x1BC | win10-1607 |
| Win10 1703 | 0x1C2 | win10-1703 |
| Win10 1709 | 0x1C6 | win10-1709 |
| Win10 1803 | 0x1C8 | win10-1803 |
| Win10 1809 | 0x1C9 | win10-1809 |
| Win10 1903 | 0x1CA | win10-1903 |
| Win10 1909 | 0x1CA | win10-1909 |
| Win10 2004 | 0x1D0 | win10-2004 |
| Win10 20H2 | 0x1D0 | win10-20h2 |
| Win10 21H1 | 0x1D0 | win10-21h1 |
| Win10 21H2 | 0x1D2 | win10-21h2 |
| Win10 22H2 | 0x1D2 | win10-22h2 |
| Win11 21H2 | 0x1DC | win11-21h2 |
| Win11 22H2 | 0x1E0 | win11-22h2 |
| Win11 23H2 | 0x1E0 | win11-23h2 |
| Win11 24H2 | 0x1E3 | win11-24h2 |
| Server 2016 | 0x1BC | winserver-2016 |
| Server 2019 | 0x1C9 | winserver-2019 |
| Server 2022 | 0x1D8 | winserver-2022 |
| Server 2025 | 0x1E3 | winserver-2025 |
Módulo del kernel
APIs relacionadas
Stub del syscall
4C 8B D1 mov r10, rcx B8 E3 01 00 00 mov eax, 0x1E3 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
Contraparte de `NtAlertThreadByThreadId`. El argumento `Address` *no* lo desreferencia el kernel — es solo una clave de hash que se enruta a la wait table del EPROCESS (KiWaitChainList / RtlpHashAddress). Cuando se invoca `NtAlertThreadByThreadId` con el TID correspondiente, el kernel recorre el bucket, encuentra el hilo coincidente y lo señala. El hashing por dirección habilita la semántica sin objeto barata de `WaitOnAddress`. El rango de SSN (0x1B3 → 0x1E3) es de los más volátiles de ntoskrnl — se mueve casi en cada actualización mayor porque está al final de la SST y la tabla crece detrás.
Uso común por malware
Lo usan las implementaciones modernas de sleep-mask (estilo Ekko y sucesoras) que quieren una primitiva de espera *no* respaldada por un objeto Event/Mutex. Aparque el hilo beacon dormido en este syscall con timeout largo; señale con `NtAlertThreadByThreadId` cuando llegue trabajo. Sin entrada `\BaseNamedObjects\…`, sin handle visible en la vista de handles de System Informer, sin evento `Microsoft-Windows-Kernel-Object` de creación. Combinado con un polling estilo `WaitOnAddress` sobre una palabra de estado, el implante puede pasarse por cualquier proceso Win32 moderno con SRWLOCK o los nuevos internals de std::mutex. El volumen legítimo lo convierte en mal objetivo primario de detección — y por eso atractivo para autores.
Oportunidades de detección
Microsoft-Windows-Threat-Intelligence no emite evento por llamada. La única anomalía realista es *frecuencia × contexto*: un hilo que pasa la mayor parte del tiempo en `NtWaitForAlertByThreadId` con timeout relativo no NULL, en un proceso con egress de red, mapping RX-only, creado por un hijo de explorer.exe o de una app Office. Los escáneres de memoria (Moneta, Hunt-Sleeping-Beacons) atrapan la región RX circundante; el syscall solo es colorido. Los EDR a veces marcan el patrón de llamada inusual (alert/wait desde DLL no sistema) pero los falsos positivos son altos porque runtimes C++ modernos usan la misma ruta.
Ejemplos de syscalls directos
cWaitOnAddress-style park
// Park a worker thread until a partner calls NtAlertThreadByThreadId
// with our TID. No event handle ever exists.
typedef NTSTATUS(NTAPI* fnWait)(PVOID, PLARGE_INTEGER);
void ParkWorker(volatile LONG* statusWord, ULONGLONG ns100) {
HMODULE n = GetModuleHandleA("ntdll.dll");
fnWait p = (fnWait)GetProcAddress(n, "NtWaitForAlertByThreadId");
LARGE_INTEGER t;
t.QuadPart = -(LONGLONG)ns100; // relative timeout
p((PVOID)statusWord, &t);
}asmx64 direct stub (Win11 24H2 SSN)
; SSN 0x1E3 on win11-24h2 / winserver-2025. Moves on nearly every feature update.
NtWaitForAlertByThreadId PROC
mov r10, rcx
mov eax, 1E3h
syscall
ret
NtWaitForAlertByThreadId ENDPrustSleep-mask park with timeout
// Park between beacon cycles using the address of a status word as hash key.
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
type NtWaitForAlertByThreadId = unsafe extern "system" fn(addr: *mut u8, to: *mut i64) -> i32;
pub unsafe fn park(status: *mut u32, hundred_ns: i64) -> i32 {
let n = GetModuleHandleA(b"ntdll.dll\0".as_ptr());
let addr = GetProcAddress(n, b"NtWaitForAlertByThreadId\0".as_ptr()).unwrap();
let f: NtWaitForAlertByThreadId = std::mem::transmute(addr);
let mut t: i64 = -hundred_ns;
f(status as *mut u8, &mut t)
}Mapeos MITRE ATT&CK
Last verified: 2026-05-20