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
| Name | Type | Dir | Description |
|---|---|---|---|
| MasterKeyHandle | HANDLE | in | Handle a la clave primaria vigilada — la que posee la notificación. |
| Count | ULONG | in | Número de entradas OBJECT_ATTRIBUTES en SubordinateObjects. Cero es válido y degenera en comportamiento de clave única. |
| SubordinateObjects | OBJECT_ATTRIBUTES* | in | Array de OBJECT_ATTRIBUTES que nombran las claves adicionales a vigilar junto a MasterKey. |
| Event | HANDLE | in | Evento opcional señalado al completarse. Requerido si Asynchronous=TRUE y no se proporciona APC. |
| ApcRoutine | PIO_APC_ROUTINE | in | APC en modo usuario opcional entregada al completarse. |
| ApcContext | PVOID | in | Contexto definido por el llamador pasado a ApcRoutine. |
| IoStatusBlock | PIO_STATUS_BLOCK | out | Recibe el estado final. El campo Information no lleva payload — reconsulte las claves para leer el estado. |
| CompletionFilter | ULONG | in | Máscara de bits REG_NOTIFY_CHANGE_* (NAME, ATTRIBUTES, LAST_SET, SECURITY). |
| WatchTree | BOOLEAN | in | TRUE para vigilar recursivamente cada subclave bajo cada clave del conjunto. |
| Buffer | PVOID | out | Reservado — actualmente sin uso por el kernel; pase NULL. |
| BufferSize | ULONG | in | Reservado — pasar 0. |
| Asynchronous | BOOLEAN | in | TRUE devuelve STATUS_PENDING de inmediato; FALSE bloquea al llamador hasta un cambio. |
IDs de syscalls por versión de Windows
| Versión de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0x106 | win10-1507 |
| Win10 1607 | 0x10B | win10-1607 |
| Win10 1703 | 0x10F | win10-1703 |
| Win10 1709 | 0x111 | win10-1709 |
| Win10 1803 | 0x113 | win10-1803 |
| Win10 1809 | 0x114 | win10-1809 |
| Win10 1903 | 0x115 | win10-1903 |
| Win10 1909 | 0x115 | win10-1909 |
| Win10 2004 | 0x11A | win10-2004 |
| Win10 20H2 | 0x11A | win10-20h2 |
| Win10 21H1 | 0x11A | win10-21h1 |
| Win10 21H2 | 0x11B | win10-21h2 |
| Win10 22H2 | 0x11B | win10-22h2 |
| Win11 21H2 | 0x121 | win11-21h2 |
| Win11 22H2 | 0x122 | win11-22h2 |
| Win11 23H2 | 0x122 | win11-23h2 |
| Win11 24H2 | 0x124 | win11-24h2 |
| Server 2016 | 0x10B | winserver-2016 |
| Server 2019 | 0x114 | winserver-2019 |
| Server 2022 | 0x120 | winserver-2022 |
| Server 2025 | 0x124 | winserver-2025 |
Módulo del kernel
APIs relacionadas
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 ENDPcPersistence 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