NtWaitForAlertByThreadId
Parque le thread appelant jusqu'à ce que NtAlertThreadByThreadId le réveille — la face noyau de WaitOnAddress.
Prototype
NTSTATUS NtWaitForAlertByThreadId( PVOID Address, PLARGE_INTEGER Timeout );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| Address | PVOID | in | Pointeur opaque utilisé comme clé de hachage dans la table d'attente par processus — typiquement l'adresse du mot de synchro user-mode. |
| Timeout | PLARGE_INTEGER | in | Intervalle 100-ns optionnel. Négatif = timeout relatif. NULL = attente infinie. Renvoie STATUS_TIMEOUT à l'expiration. |
IDs de syscalls par version de Windows
| Version 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 |
Module noyau
APIs liées
Stub du 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
Notes non documentées
Pendant de `NtAlertThreadByThreadId`. L'argument `Address` n'est *pas* déréférencé par le noyau — c'est simplement une clé de hachage routée dans la table d'attente de l'EPROCESS (KiWaitChainList / RtlpHashAddress). Quand `NtAlertThreadByThreadId` est appelé avec le TID correspondant, le noyau parcourt le bucket, trouve le thread correspondant et le signale. Le hachage par adresse permet la sémantique économique sans objet de `WaitOnAddress`. La plage de SSN (0x1B3 → 0x1E3) est l'une des plus volatiles de ntoskrnl — elle bouge presque à chaque mise à jour parce qu'elle se trouve en queue de la SST et que la table grandit derrière.
Usage courant par les malwares
Utilisé par les implémentations modernes de sleep-mask (style Ekko et successeurs) qui veulent une primitive d'attente *non* adossée à un objet Event/Mutex. Garez le thread beacon dormant dans ce syscall avec un long timeout ; signalez-le avec `NtAlertThreadByThreadId` quand du travail arrive. Aucune entrée `\BaseNamedObjects\…`, aucun handle visible dans la vue handles de System Informer, aucun événement `Microsoft-Windows-Kernel-Object` de création. Combiné à un polling style `WaitOnAddress` sur un mot d'état, l'implant peut se faire passer pour n'importe quel processus Win32 moderne utilisant SRWLOCK ou les nouveaux internals de std::mutex. Le volume légitime en fait une cible primaire de détection médiocre, ce qui en fait justement l'attrait pour les auteurs.
Opportunités de détection
Microsoft-Windows-Threat-Intelligence n'émet pas d'événement par appel. La seule anomalie réaliste est *fréquence × contexte* : un thread passant l'essentiel de son temps dans `NtWaitForAlertByThreadId` avec un timeout relatif non NULL, dans un processus avec egress réseau, mapping RX-only, créé par un enfant d'explorer.exe ou d'une appli Office. Les scanners mémoire (Moneta, Hunt-Sleeping-Beacons) attrapent la région RX environnante ; le syscall n'est qu'une couleur. Les EDR signalent parfois le pattern d'appel inhabituel (alert/wait depuis une DLL non système) mais le taux de faux positifs est élevé car les runtimes C++ modernes empruntent le même chemin.
Exemples de syscalls directs
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)
}Mappings MITRE ATT&CK
Last verified: 2026-05-20