NtWaitForAlertByThreadId
Parks the calling thread until NtAlertThreadByThreadId wakes it — the kernel side of WaitOnAddress.
Prototype
NTSTATUS NtWaitForAlertByThreadId( PVOID Address, PLARGE_INTEGER Timeout );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| Address | PVOID | in | Opaque pointer used as the hash key in the per-process wait table — typically the address of the user-mode synchronization word. |
| Timeout | PLARGE_INTEGER | in | Optional 100-ns interval. Negative = relative timeout. NULL = wait forever. Returns STATUS_TIMEOUT on expiry. |
Syscall IDs by Windows version
| Windows version | Syscall ID | 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 |
Kernel module
Related APIs
Syscall stub
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
Undocumented notes
Counterpart of `NtAlertThreadByThreadId`. The `Address` argument is *not* dereferenced by the kernel — it is purely a hash key bucketed into the EPROCESS wait table (KiWaitChainList / RtlpHashAddress). When `NtAlertThreadByThreadId` is called with the corresponding TID, the kernel walks the bucket, finds the matching thread and signals it. The address-based hashing is what enables the cheap, object-less semantics of `WaitOnAddress`. The SSN range (0x1B3 → 0x1E3) is one of the most volatile in ntoskrnl — it shifts almost every feature update because it lives at the tail of the SST and the table grows behind it.
Common malware usage
Used by modern sleep-mask implementations (Ekko-style and successor variants) that want a wait primitive *not* backed by an Event/Mutex object. Park the dormant beacon thread in this syscall with a long timeout; signal it with `NtAlertThreadByThreadId` when work arrives. No `\BaseNamedObjects\…` entry, no handle visible in System Informer's handle view, no `Microsoft-Windows-Kernel-Object` ETW event for creation. Combined with `WaitOnAddress`-style polling of a status word, the implant can pretend to be any modern Win32 process using SRWLOCK or the new std::mutex internals. Volume of legitimate calls makes this a poor primary detection target, which is exactly why it is attractive to authors.
Detection opportunities
Microsoft-Windows-Threat-Intelligence does not emit a per-call event. The only realistic anomaly is *frequency × context*: a thread spending most of its time in `NtWaitForAlertByThreadId` with a non-NULL relative timeout, in a process that performs network egress, has an RX-only mapping, and was created by a child of explorer.exe or a Microsoft Office app. Memory scanners (Moneta, Hunt-Sleeping-Beacons) catch the surrounding RX region; the syscall itself is just colour. EDRs sometimes flag the unusual call-site pattern (alert/wait from non-system DLL) but false-positive rates are high because mid-modern C++ runtimes hit the same code path.
Direct syscall examples
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)
}MITRE ATT&CK mappings
Last verified: 2026-05-20