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
| Name | Type | Dir | Description |
|---|---|---|---|
| KeyHandle | HANDLE | in | Handle auf einen mit KEY_NOTIFY-Zugriff geöffneten Schlüssel. |
| Event | HANDLE | in | Optionales Event, das bei Änderung signalisiert wird. NULL bei ApcRoutine oder synchronem Warten. |
| ApcRoutine | PIO_APC_ROUTINE | in | Optionale User-Mode-APC-Routine, die bei der Änderung aufgerufen wird. |
| ApcContext | PVOID | in | Kontextwert, der an ApcRoutine übergeben wird. |
| IoStatusBlock | PIO_STATUS_BLOCK | out | Empfängt den Endstatus und die zurückgegebenen Information-Bytes. |
| CompletionFilter | ULONG | in | Bitmaske der zu beobachtenden REG_NOTIFY_CHANGE_*-Ereignisse (NAME, ATTRIBUTES, LAST_SET, SECURITY). |
| WatchTree | BOOLEAN | in | Wenn TRUE, werden auch Änderungen im gesamten Teilbaum unter KeyHandle gemeldet. |
| Buffer | PVOID | out | Optionaler Puffer für zukünftige Erweiterung. Heute stets NULL übergeben. |
| BufferSize | ULONG | in | Größe von Buffer in Bytes. 0, wenn Buffer NULL ist. |
| Asynchronous | BOOLEAN | in | Wenn TRUE, kehrt der Aufruf sofort mit STATUS_PENDING zurück; andernfalls blockiert er bis zu einer Änderung. |
Syscall-IDs pro Windows-Version
| Windows-Version | Syscall-ID | Build |
|---|---|---|
| Win10 1507 | 0x105 | win10-1507 |
| Win10 1607 | 0x10A | win10-1607 |
| Win10 1703 | 0x10E | win10-1703 |
| Win10 1709 | 0x110 | win10-1709 |
| Win10 1803 | 0x112 | win10-1803 |
| Win10 1809 | 0x113 | win10-1809 |
| Win10 1903 | 0x114 | win10-1903 |
| Win10 1909 | 0x114 | win10-1909 |
| Win10 2004 | 0x119 | win10-2004 |
| Win10 20H2 | 0x119 | win10-20h2 |
| Win10 21H1 | 0x119 | win10-21h1 |
| Win10 21H2 | 0x11A | win10-21h2 |
| Win10 22H2 | 0x11A | win10-22h2 |
| Win11 21H2 | 0x120 | win11-21h2 |
| Win11 22H2 | 0x121 | win11-22h2 |
| Win11 23H2 | 0x121 | win11-23h2 |
| Win11 24H2 | 0x123 | win11-24h2 |
| Server 2016 | 0x10A | winserver-2016 |
| Server 2019 | 0x113 | winserver-2019 |
| Server 2022 | 0x11F | winserver-2022 |
| Server 2025 | 0x123 | winserver-2025 |
Kernel-Modul
Verwandte APIs
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.
Erkennungsmö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 ENDPrustAPC-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