NtWaitForKeyedEvent
Blocks the calling thread on a keyed event until another thread releases the same (event, key) pair.
Prototype
NTSTATUS NtWaitForKeyedEvent( HANDLE KeyedEventHandle, PVOID Key, BOOLEAN Alertable, PLARGE_INTEGER Timeout );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| KeyedEventHandle | HANDLE | in | Handle to a keyed event. NULL uses the per-process default at \KernelObjects\CritSecOutOfMemoryEvent. |
| Key | PVOID | in | Pointer-sized rendezvous value. Must be naturally aligned (low bit zero). Conventionally the address of the critical section / lock word. |
| Alertable | BOOLEAN | in | TRUE makes the wait interruptible by user-mode APCs; almost always FALSE for lock primitives. |
| Timeout | PLARGE_INTEGER | in | Optional timeout (100-ns units, negative = relative). NULL = wait forever. Returns STATUS_TIMEOUT on expiry. |
Syscall IDs by Windows version
| Windows version | Syscall ID | 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 |
Kernel module
Related APIs
Syscall stub
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
Undocumented notes
The wait half of the keyed-event pair. Internally used by `ntdll!RtlEnterCriticalSection` as the **low-memory fallback** when a critical-section contention would normally require allocating an event object and the system is out of pool. In that path `ntdll.dll` passes `peb->KeyedEventHandle` (the global `CritSecOutOfMemoryEvent`) and uses the critical-section's `LockSemaphore` field address as the key, so every contending thread in the system can share one global keyed event. `NtWaitForKeyedEvent` is also one of two primitives behind `WaitOnAddress` (the other is `KeWaitForSingleObject` on a thread-local event); on modern builds `WaitOnAddress` is the recommended user-mode entry point.
Common malware usage
Modest signal. Sleep-obfuscation designs that want a sync primitive without naming any kernel object pair `NtWaitForKeyedEvent` (in the sleeping thread) with `NtReleaseKeyedEvent` (in a wake-up timer ROP gadget or APC). Because the global handle is already open in every process, the entire scheme requires zero new handles to be created — bypassing crude sandboxes that scan handle tables for newly-created events / semaphores. The Ekko family and several published sleep-mask demos rely on this. Direct, deliberate use of `NtWaitForKeyedEvent` from a non-system binary outside that pattern is mostly an indicator of a custom lock implementation rather than malware per se.
Detection opportunities
Same disposition as `NtCreateKeyedEvent`: no useful ETW or Sysmon surface. The only meaningful blue-team angle is *behavioural* — a thread that has been in `NtWaitForKeyedEvent` on the global handle for an unusually long time, while its containing module lives in unbacked / RX-only memory (typical of sleep-masked beacons), is suspicious. ETW Threat Intelligence + memory-region introspection (e.g. `MemoryWorkingSetExInformation` showing pages with `MEMORY_REGION_PRIVATE`) is more informative than tracing the syscall itself.
Direct syscall examples
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);
}MITRE ATT&CK mappings
Last verified: 2026-05-20