NtWaitForKeyedEvent
Bloque le thread appelant sur un keyed event jusqu'à ce qu'un autre thread libère la même paire (event, clé).
Prototype
NTSTATUS NtWaitForKeyedEvent( HANDLE KeyedEventHandle, PVOID Key, BOOLEAN Alertable, PLARGE_INTEGER Timeout );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| KeyedEventHandle | HANDLE | in | Handle vers un keyed event. NULL utilise le défaut par processus à \KernelObjects\CritSecOutOfMemoryEvent. |
| Key | PVOID | in | Valeur de rendez-vous de la taille d'un pointeur. Doit être alignée naturellement (bit faible à zéro). Conventionnellement l'adresse de la critical section / mot de verrou. |
| Alertable | BOOLEAN | in | TRUE rend l'attente interruptible par APC user-mode ; presque toujours FALSE pour les primitives de verrou. |
| Timeout | PLARGE_INTEGER | in | Délai optionnel (unités de 100 ns, négatif = relatif). NULL = attente infinie. Renvoie STATUS_TIMEOUT à expiration. |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0x1B5 | win10-1507 |
| Win10 1607 | 0x1BE | win10-1607 |
| Win10 1703 | 0x1C4 | win10-1703 |
| Win10 1709 | 0x1C8 | win10-1709 |
| Win10 1803 | 0x1CA | win10-1803 |
| Win10 1809 | 0x1CB | win10-1809 |
| Win10 1903 | 0x1CC | win10-1903 |
| Win10 1909 | 0x1CC | win10-1909 |
| Win10 2004 | 0x1D2 | win10-2004 |
| Win10 20H2 | 0x1D2 | win10-20h2 |
| Win10 21H1 | 0x1D2 | win10-21h1 |
| Win10 21H2 | 0x1D4 | win10-21h2 |
| Win10 22H2 | 0x1D4 | win10-22h2 |
| Win11 21H2 | 0x1DE | win11-21h2 |
| Win11 22H2 | 0x1E2 | win11-22h2 |
| Win11 23H2 | 0x1E2 | win11-23h2 |
| Win11 24H2 | 0x1E5 | win11-24h2 |
| Server 2016 | 0x1BE | winserver-2016 |
| Server 2019 | 0x1CB | winserver-2019 |
| Server 2022 | 0x1DA | winserver-2022 |
| Server 2025 | 0x1E5 | winserver-2025 |
Module noyau
APIs liées
Stub du syscall
4C 8B D1 mov r10, rcx B8 E5 01 00 00 mov eax, 0x1E5 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
La moitié wait de la paire keyed-event. Utilisée en interne par `ntdll!RtlEnterCriticalSection` comme **fallback bas mémoire** lorsque la contention d'une critical section exigerait normalement d'allouer un objet event et que le système est à court de pool. Dans ce chemin, `ntdll.dll` passe `peb->KeyedEventHandle` (le global `CritSecOutOfMemoryEvent`) et utilise l'adresse du champ `LockSemaphore` de la critical section comme clé, si bien que chaque thread en contention dans le système partage un unique keyed event global. `NtWaitForKeyedEvent` est aussi l'une des deux primitives derrière `WaitOnAddress` (l'autre étant `KeWaitForSingleObject` sur un event thread-local) ; sur les builds modernes, `WaitOnAddress` est le point d'entrée user-mode recommandé.
Usage courant par les malwares
Signal modeste. Les designs d'obfuscation de sommeil voulant une primitive de synchro sans nommer aucun objet noyau apparient `NtWaitForKeyedEvent` (dans le thread endormi) à `NtReleaseKeyedEvent` (dans un gadget ROP de timer de réveil ou un APC). Le handle global étant déjà ouvert dans chaque processus, l'ensemble n'exige aucun nouveau handle — esquivant les sandboxes rudimentaires qui scannent les tables de handles à la recherche d'events / sémaphores fraîchement créés. La famille Ekko et plusieurs démos publiques de sleep-mask s'appuient là-dessus. Un usage direct et délibéré de `NtWaitForKeyedEvent` depuis un binaire non système hors ce pattern est plutôt l'indice d'une implémentation de verrou maison qu'un signal de malware.
Opportunités de détection
Même disposition que `NtCreateKeyedEvent` : pas de surface ETW ou Sysmon utile. Le seul angle blue-team significatif est *comportemental* — un thread bloqué dans `NtWaitForKeyedEvent` sur le handle global anormalement longtemps, alors que son module hôte vit en mémoire non backed / RX-only (typique des beacons sleep-maskés), est suspect. ETW Threat Intelligence + introspection des régions mémoire (ex. `MemoryWorkingSetExInformation` montrant des pages `MEMORY_REGION_PRIVATE`) est plus informatif que tracer le syscall lui-même.
Exemples de syscalls directs
asmx64 direct stub (Win11 24H2)
; Direct syscall stub for NtWaitForKeyedEvent (SSN 0x1E5 on Win11 24H2)
NtWaitForKeyedEvent PROC
mov r10, rcx ; KeyedEventHandle
mov eax, 1E5h ; SSN — drifts per build
syscall
ret
NtWaitForKeyedEvent ENDPcCritical-section low-memory fallback (illustrative)
// Mirrors what ntdll!RtlpWaitOnCriticalSection does in the out-of-pool path:
// every contending thread parks on the global keyed event using the address
// of the critical section's LockSemaphore slot as the rendezvous key.
#include <windows.h>
#include <winternl.h>
typedef NTSTATUS (NTAPI *pNtWaitForKeyedEvent)(
HANDLE, PVOID, BOOLEAN, PLARGE_INTEGER);
static pNtWaitForKeyedEvent g_wait;
void ParkOnLockWord(volatile void **lockSlot) {
if (!g_wait) {
g_wait = (pNtWaitForKeyedEvent)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtWaitForKeyedEvent");
}
// NULL handle ⇒ kernel substitutes peb->KeyedEventHandle = global event.
g_wait(NULL, (PVOID)lockSlot, FALSE, NULL);
}cModern equivalent — WaitOnAddress (recommended)
// Microsoft's blessed user-mode primitive for the same pattern.
#include <windows.h>
#include <synchapi.h>
#pragma comment(lib, "Synchronization.lib")
void WaitForLockChange(volatile LONG *lock, LONG expected) {
WaitOnAddress((volatile VOID*)lock, &expected, sizeof(expected), INFINITE);
}Mappings MITRE ATT&CK
Last verified: 2026-05-20