> Windows Syscalls
ntoskrnl.exeT1547.001T1112T1106

NtNotifyChangeKey

Registra una notificación asíncrona para cambios en una clave del registro y (opcionalmente) su subárbol.

Prototipo

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
);

Argumentos

NameTypeDirDescription
KeyHandleHANDLEinHandle a una clave abierta con acceso KEY_NOTIFY.
EventHANDLEinEvento opcional señalado al cambiar. NULL si se usa ApcRoutine o espera síncrona.
ApcRoutinePIO_APC_ROUTINEinRutina APC de modo usuario opcional invocada cuando se dispara el cambio.
ApcContextPVOIDinValor de contexto pasado a ApcRoutine.
IoStatusBlockPIO_STATUS_BLOCKoutRecibe el estado final y los bytes Information devueltos.
CompletionFilterULONGinMáscara de bits de eventos REG_NOTIFY_CHANGE_* a vigilar (NAME, ATTRIBUTES, LAST_SET, SECURITY).
WatchTreeBOOLEANinSi TRUE, también reporta cambios en cualquier parte del subárbol enraizado en KeyHandle.
BufferPVOIDoutBúfer opcional reservado para futura expansión. Pasar siempre NULL hoy.
BufferSizeULONGinTamaño de Buffer en bytes. 0 cuando Buffer es NULL.
AsynchronousBOOLEANinSi TRUE, la llamada devuelve STATUS_PENDING de inmediato; en caso contrario, bloquea hasta que se dispare un cambio.

IDs de syscalls por versión de Windows

Versión de WindowsID de syscallBuild
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

Módulo del kernel

ntoskrnl.exeNtNotifyChangeKey

APIs relacionadas

RegNotifyChangeKeyValueNtNotifyChangeMultipleKeysNtNotifyChangeDirectoryFileCmRegisterCallbackEx

Stub del syscall

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

Notas no documentadas

NtNotifyChangeKey es el análogo de registro de NtNotifyChangeDirectoryFile y la implementación de kernel detrás de `RegNotifyChangeKeyValue`. El kernel asigna un CM_NOTIFY_BLOCK adjunto al KCB de la clave y señala el evento provisto / encola el APC la próxima vez que se alcanza CmpNotifyChangeKey durante una mutación de CM_KEY_CONTROL_BLOCK. Como la espera es de disparo único, los llamadores se re-arman llamando a NtNotifyChangeKey de nuevo desde el cuerpo del APC — produciendo un patrón reconocible de "bucle de notificación" en las pilas de llamadas en modo usuario.

Uso común por malware

Loaders preocupados por su persistencia se registran en `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` (o el árbol per-user `Image File Execution Options`) y *se auto-curan* en el momento en que un EDR o un sysadmin elimina el valor Run — el APC dispara, el implante reescribe el valor, y la eliminación parece nunca haber ocurrido. La misma primitiva permite vigilar `HKLM\SYSTEM\CurrentControlSet\Services\<EDR>\Start` y reaccionar si un defensor intenta deshabilitar un servicio competidor. Sysmon usa internamente NtNotifyChangeKey para alimentar Event IDs 12/13/14, así que la presencia del syscall no es anómala por sí sola.

Oportunidades de detección

Hookear NtNotifyChangeKey desde modo usuario es trivial pero eludible mediante syscalls directos. La detección de alto valor es *correlativa*: un Sysmon Event 13 (ValueSet) recreando un valor Run milisegundos después de un Event 12 (DeleteValue) por la misma imagen es un indicador casi perfecto de auto-curación dirigida por notificación. Telemetría de kernel: el proveedor ETW Microsoft-Windows-Kernel-Registry emite eventos RegistryNotify pero no está activo por defecto. Los EDR que hookean CmRegisterCallbackEx pueden observar el registro directamente. En hunt, buscar procesos con handles *de larga duración* sobre claves Run abiertas con KEY_NOTIFY — pocos programas legítimos lo hacen fuera de Explorer y SCM.

Ejemplos de syscalls directos

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
    );
}

Mapeos MITRE ATT&CK

Last verified: 2026-05-20