> Windows Syscalls
ntoskrnl.exeT1547.001T1112T1106

NtNotifyChangeKey

Registriert eine asynchrone Benachrichtigung für Änderungen an einem Registry-Schlüssel und (optional) seinem Teilbaum.

Prototyp

NTSTATUS NtNotifyChangeKey(
  HANDLE             KeyHandle,
  HANDLE             Event,
  PIO_APC_ROUTINE    ApcRoutine,
  PVOID              ApcContext,
  PIO_STATUS_BLOCK   IoStatusBlock,
  ULONG              CompletionFilter,
  BOOLEAN            WatchTree,
  PVOID              Buffer,
  ULONG              BufferSize,
  BOOLEAN            Asynchronous
);

Argumente

NameTypeDirDescription
KeyHandleHANDLEinHandle auf einen mit KEY_NOTIFY-Zugriff geöffneten Schlüssel.
EventHANDLEinOptionales Event, das bei Änderung signalisiert wird. NULL bei ApcRoutine oder synchronem Warten.
ApcRoutinePIO_APC_ROUTINEinOptionale User-Mode-APC-Routine, die bei der Änderung aufgerufen wird.
ApcContextPVOIDinKontextwert, der an ApcRoutine übergeben wird.
IoStatusBlockPIO_STATUS_BLOCKoutEmpfängt den Endstatus und die zurückgegebenen Information-Bytes.
CompletionFilterULONGinBitmaske der zu beobachtenden REG_NOTIFY_CHANGE_*-Ereignisse (NAME, ATTRIBUTES, LAST_SET, SECURITY).
WatchTreeBOOLEANinWenn TRUE, werden auch Änderungen im gesamten Teilbaum unter KeyHandle gemeldet.
BufferPVOIDoutOptionaler Puffer für zukünftige Erweiterung. Heute stets NULL übergeben.
BufferSizeULONGinGröße von Buffer in Bytes. 0, wenn Buffer NULL ist.
AsynchronousBOOLEANinWenn TRUE, kehrt der Aufruf sofort mit STATUS_PENDING zurück; andernfalls blockiert er bis zu einer Änderung.

Syscall-IDs pro Windows-Version

Windows-VersionSyscall-IDBuild
Win10 15070x105win10-1507
Win10 16070x10Awin10-1607
Win10 17030x10Ewin10-1703
Win10 17090x110win10-1709
Win10 18030x112win10-1803
Win10 18090x113win10-1809
Win10 19030x114win10-1903
Win10 19090x114win10-1909
Win10 20040x119win10-2004
Win10 20H20x119win10-20h2
Win10 21H10x119win10-21h1
Win10 21H20x11Awin10-21h2
Win10 22H20x11Awin10-22h2
Win11 21H20x120win11-21h2
Win11 22H20x121win11-22h2
Win11 23H20x121win11-23h2
Win11 24H20x123win11-24h2
Server 20160x10Awinserver-2016
Server 20190x113winserver-2019
Server 20220x11Fwinserver-2022
Server 20250x123winserver-2025

Kernel-Modul

ntoskrnl.exeNtNotifyChangeKey

Verwandte APIs

RegNotifyChangeKeyValueNtNotifyChangeMultipleKeysNtNotifyChangeDirectoryFileCmRegisterCallbackEx

Syscall-Stub

4C 8B D1            mov r10, rcx
B8 23 01 00 00      mov eax, 0x123
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

Undokumentierte Hinweise

NtNotifyChangeKey ist das Registry-Pendant zu NtNotifyChangeDirectoryFile und die Kernel-Implementation hinter `RegNotifyChangeKeyValue`. Der Kernel allokiert einen an den KCB des Schlüssels gehängten CM_NOTIFY_BLOCK und signalisiert das übergebene Event / reiht den APC ein, sobald CmpNotifyChangeKey während einer CM_KEY_CONTROL_BLOCK-Mutation erreicht wird. Da das Warten einmalig ist, scharfen Aufrufer aus dem APC-Body neu mit erneutem NtNotifyChangeKey — daraus entsteht ein erkennbares „Notify-Loop"-Muster in User-Mode-Callstacks.

Häufige Malware-Nutzung

Persistenz-bewusste Loader registrieren ein Notify auf `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` (oder den per-user `Image File Execution Options`-Baum) und *heilen sich selbst*, sobald ein EDR oder Admin den Run-Wert löscht — der APC feuert, der Implant schreibt den Wert neu, und die Löschung wirkt nie geschehen. Dieselbe Primitive lässt Implants `HKLM\SYSTEM\CurrentControlSet\Services\<EDR>\Start` beobachten und reagieren, falls ein Verteidiger versucht, einen konkurrierenden Dienst zu deaktivieren. Sysmon nutzt NtNotifyChangeKey intern für Event IDs 12/13/14 — die bloße Präsenz des Syscalls ist also nicht ungewöhnlich.

Erkennungs­möglichkeiten

Das Hooken von NtNotifyChangeKey aus dem User-Mode ist trivial, aber per direkten Syscalls umgehbar. Hochwertige Detection ist *korreliert*: ein Sysmon-Event 13 (ValueSet), das einen Run-Wert Millisekunden nach Event 12 (DeleteValue) durch dasselbe Image neu erzeugt, ist ein nahezu perfekter Indikator für Notify-gesteuertes Self-Heal. Kernel-Telemetrie: der ETW-Provider Microsoft-Windows-Kernel-Registry liefert RegistryNotify-Events, ist aber standardmäßig nicht aktiv. EDRs, die CmRegisterCallbackEx hooken, sehen die Registrierung direkt. Beim Hunting nach Prozessen mit *langlebigen* KEY_NOTIFY-Handles auf Run-Keys suchen — legitime Software macht das selten außerhalb von Explorer und SCM.

Direkte Syscall-Beispiele

cRun-key self-heal loop

// Watch HKCU\...\Run and rewrite our value the moment it's deleted or modified.
HANDLE hEvent = CreateEventW(NULL, FALSE, FALSE, NULL);
HANDLE hKey;  // already opened with KEY_NOTIFY | KEY_SET_VALUE
IO_STATUS_BLOCK iosb;

for (;;) {
    NTSTATUS st = NtNotifyChangeKey(
        hKey, hEvent, NULL, NULL, &iosb,
        REG_NOTIFY_CHANGE_LAST_SET | REG_NOTIFY_CHANGE_NAME,
        FALSE,           // WatchTree
        NULL, 0,         // Buffer / size — always NULL today
        TRUE             // Asynchronous
    );
    if (st != STATUS_PENDING) break;
    WaitForSingleObject(hEvent, INFINITE);
    // Re-write the value — appears as if the deletion was undone.
    RegSetValueExW(hKey, L"Updater", 0, REG_SZ,
                   (BYTE*)g_payloadPath, (DWORD)((wcslen(g_payloadPath)+1)*2));
}

asmx64 direct stub (Win11 24H2 SSN 0x123)

NtNotifyChangeKey PROC
    mov  r10, rcx
    mov  eax, 123h
    syscall
    ret
NtNotifyChangeKey ENDP

rustAPC-driven re-arm via ntapi

// Cargo: ntapi = "0.4", windows-sys = "0.59"
unsafe extern "system" fn on_change(_apc_ctx: *mut c_void, iosb: *mut IO_STATUS_BLOCK, _r: u32) {
    // APC fires in the registering thread when the key mutates.
    rewrite_run_value();
    rearm_watch();
}

unsafe fn arm_watch(h_key: HANDLE) {
    let mut iosb = zeroed();
    NtNotifyChangeKey(
        h_key, null_mut(),
        Some(on_change), null_mut(),
        &mut iosb,
        REG_NOTIFY_CHANGE_LAST_SET,
        0,                 // WatchTree = FALSE
        null_mut(), 0,
        1,                 // Asynchronous = TRUE
    );
}

MITRE ATT&CK-Mappings

Last verified: 2026-05-20