> Windows Syscalls
ntoskrnl.exeT1547.001T1112T1106

NtNotifyChangeMultipleKeys

Registra una única solicitud de notificación que se dispara cuando cualquiera de varias claves de registro cambia.

Prototipo

NTSTATUS NtNotifyChangeMultipleKeys(
  HANDLE             MasterKeyHandle,
  ULONG              Count,
  OBJECT_ATTRIBUTES *SubordinateObjects,
  HANDLE             Event,
  PIO_APC_ROUTINE    ApcRoutine,
  PVOID              ApcContext,
  PIO_STATUS_BLOCK   IoStatusBlock,
  ULONG              CompletionFilter,
  BOOLEAN            WatchTree,
  PVOID              Buffer,
  ULONG              BufferSize,
  BOOLEAN            Asynchronous
);

Argumentos

NameTypeDirDescription
MasterKeyHandleHANDLEinHandle a la clave primaria vigilada — la que posee la notificación.
CountULONGinNúmero de entradas OBJECT_ATTRIBUTES en SubordinateObjects. Cero es válido y degenera en comportamiento de clave única.
SubordinateObjectsOBJECT_ATTRIBUTES*inArray de OBJECT_ATTRIBUTES que nombran las claves adicionales a vigilar junto a MasterKey.
EventHANDLEinEvento opcional señalado al completarse. Requerido si Asynchronous=TRUE y no se proporciona APC.
ApcRoutinePIO_APC_ROUTINEinAPC en modo usuario opcional entregada al completarse.
ApcContextPVOIDinContexto definido por el llamador pasado a ApcRoutine.
IoStatusBlockPIO_STATUS_BLOCKoutRecibe el estado final. El campo Information no lleva payload — reconsulte las claves para leer el estado.
CompletionFilterULONGinMáscara de bits REG_NOTIFY_CHANGE_* (NAME, ATTRIBUTES, LAST_SET, SECURITY).
WatchTreeBOOLEANinTRUE para vigilar recursivamente cada subclave bajo cada clave del conjunto.
BufferPVOIDoutReservado — actualmente sin uso por el kernel; pase NULL.
BufferSizeULONGinReservado — pasar 0.
AsynchronousBOOLEANinTRUE devuelve STATUS_PENDING de inmediato; FALSE bloquea al llamador hasta un cambio.

IDs de syscalls por versión de Windows

Versión de WindowsID de syscallBuild
Win10 15070x106win10-1507
Win10 16070x10Bwin10-1607
Win10 17030x10Fwin10-1703
Win10 17090x111win10-1709
Win10 18030x113win10-1803
Win10 18090x114win10-1809
Win10 19030x115win10-1903
Win10 19090x115win10-1909
Win10 20040x11Awin10-2004
Win10 20H20x11Awin10-20h2
Win10 21H10x11Awin10-21h1
Win10 21H20x11Bwin10-21h2
Win10 22H20x11Bwin10-22h2
Win11 21H20x121win11-21h2
Win11 22H20x122win11-22h2
Win11 23H20x122win11-23h2
Win11 24H20x124win11-24h2
Server 20160x10Bwinserver-2016
Server 20190x114winserver-2019
Server 20220x120winserver-2022
Server 20250x124winserver-2025

Módulo del kernel

ntoskrnl.exeNtNotifyChangeMultipleKeys

APIs relacionadas

RegNotifyChangeKeyValueNtNotifyChangeKeyNtOpenKeyNtOpenKeyExNtQueryKey

Stub del syscall

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

Hermano multi-clave de `NtNotifyChangeKey`. No existe wrapper Win32 — la API `RegNotifyChangeKeyValue` solo vigila una sola clave. La función la usa internamente el configuration manager (CM) de Windows para coordinar notificaciones entre segmentos de hive enlazados, y un puñado de snap-ins MMC. El kernel recorre la lista `SubordinateObjects`, abre cada clave y registra un único CM_NOTIFY_BLOCK que se dispara cuando *cualquiera* cambia — el IO_STATUS_BLOCK no indica cuál disparó; el llamador debe reconsultar cada clave para descubrir el delta. A pesar de su base de usuarios estrecha, está exportado por ntdll en todos los builds desde NT 4.0 con lista de parámetros estable.

Uso común por malware

**Raramente visto en malware** — casi todo módulo de monitorización de persistencia o anti-limpieza que necesita vigilar registro usa el más simple `NtNotifyChangeKey` (o RegNotifyChangeKeyValue) contra una sola Run key. Los pocos abusos documentados combinan Multiple Keys con `WatchTree=TRUE` para vigilar toda la familia `HKLM\Software\Microsoft\Windows\CurrentVersion\Run`, `RunOnce`, `Image File Execution Options` y `Winlogon` bajo un solo APC, de modo que el implante reinstale cualquier entrada de persistencia en cuanto un EDR o un sysadmin borre una. Como no hay wrapper Win32, la llamada en sí destaca un poco más en los stacks que la variante de clave única.

Oportunidades de detección

La señal telemétrica aquí es notablemente más fuerte que para `NtNotifyChangeKey` precisamente porque el software usermode legítimo casi nunca lo invoca — es en gran medida una ruta interna del CM. ETW Microsoft-Windows-Kernel-Registry expone la creación de solicitudes de notify de registro; combinado con la stack por hilo del EDR, un proceso no-sistema que haga esta llamada es sospechoso de por sí. Sysmon Event ID 12/13/14 (RegistryEvent) reporta los cambios en sí, no el registro de la vigilancia — los defensores que correlacionan patrones watch-luego-restore en un mismo TID típicamente atrapan los bucles de restauración de persistencia. Pivot defensivo útil: identificar procesos que tengan un handle sobre una clave de la familia Run más un notify asíncrono en curso; cruzar con la ruta de imagen y el firmante.

Ejemplos de syscalls directos

asmx64 direct stub (Win11 24H2)

; Direct syscall stub for NtNotifyChangeMultipleKeys (SSN 0x124 on Win11 24H2)
NtNotifyChangeMultipleKeys PROC
    mov  r10, rcx          ; syscall convention
    mov  eax, 124h         ; SSN — varies per build
    syscall
    ret
NtNotifyChangeMultipleKeys ENDP

cPersistence guard — watch all Run-family keys at once

// Implant module: re-install Run entry whenever any Run-family key is touched.
// Uses NtNotifyChangeMultipleKeys so a single APC covers Run + RunOnce + Winlogon.
#include <windows.h>
#include <winternl.h>

typedef NTSTATUS (NTAPI *pNtNotifyChangeMultipleKeys)(
    HANDLE, ULONG, POBJECT_ATTRIBUTES, HANDLE,
    PIO_APC_ROUTINE, PVOID, PIO_STATUS_BLOCK,
    ULONG, BOOLEAN, PVOID, ULONG, BOOLEAN);

VOID PersistenceWatch(HANDLE hRunKey,
                     POBJECT_ATTRIBUTES extras, ULONG nExtras) {
    pNtNotifyChangeMultipleKeys p = (pNtNotifyChangeMultipleKeys)
        GetProcAddress(GetModuleHandleA("ntdll.dll"),
                       "NtNotifyChangeMultipleKeys");
    HANDLE hEvt = CreateEventW(NULL, FALSE, FALSE, NULL);
    IO_STATUS_BLOCK iosb;
    for (;;) {
        NTSTATUS s = p(hRunKey, nExtras, extras, hEvt, NULL, NULL, &iosb,
            REG_NOTIFY_CHANGE_LAST_SET | REG_NOTIFY_CHANGE_NAME,
            TRUE,      // WatchTree
            NULL, 0,
            TRUE);     // Asynchronous
        if (s != STATUS_PENDING && s < 0) break;
        WaitForSingleObject(hEvt, INFINITE);
        ReinstallAllPersistence(); // re-write our Run values
    }
}

cSysadmin tool — coordinated reload of two hives

// Defensive use case: watch HKLM\SAM and HKLM\SECURITY together so a
// monitoring agent flushes its credential-policy cache in one wake-up.
#include <windows.h>
#include <winternl.h>

VOID PolicyWatcher(HANDLE hSam, HANDLE hSec) {
    OBJECT_ATTRIBUTES extras[1] = { 0 }; // SAM is master, SECURITY is subordinate
    UNICODE_STRING secPath; RtlInitUnicodeString(&secPath,
        L"\\Registry\\Machine\\Security");
    InitializeObjectAttributes(&extras[0], &secPath, OBJ_CASE_INSENSITIVE, NULL, NULL);

    IO_STATUS_BLOCK iosb;
    NtNotifyChangeMultipleKeys(hSam, 1, extras, NULL,
        &OnPolicyChange, NULL, &iosb,
        REG_NOTIFY_CHANGE_LAST_SET, TRUE,
        NULL, 0, TRUE);
}

Mapeos MITRE ATT&CK

Last verified: 2026-05-20