> Windows Syscalls
ntoskrnl.exeT1547.001T1112T1106

NtNotifyChangeMultipleKeys

Registers a single notification request that fires when any of several registry keys changes.

Prototype

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

Arguments

NameTypeDirDescription
MasterKeyHandleHANDLEinHandle to the primary key being watched — the one that owns the notification.
CountULONGinNumber of OBJECT_ATTRIBUTES entries in SubordinateObjects. Zero is legal and degenerates to single-key behavior.
SubordinateObjectsOBJECT_ATTRIBUTES*inArray of OBJECT_ATTRIBUTES naming the additional keys to watch alongside MasterKey.
EventHANDLEinOptional event signaled on completion. Required if Asynchronous=TRUE and no APC is supplied.
ApcRoutinePIO_APC_ROUTINEinOptional user-mode APC delivered on completion.
ApcContextPVOIDinCaller-defined context passed to ApcRoutine.
IoStatusBlockPIO_STATUS_BLOCKoutReceives final status. The Information field carries no payload — re-query keys to read state.
CompletionFilterULONGinBitmask of REG_NOTIFY_CHANGE_* flags (NAME, ATTRIBUTES, LAST_SET, SECURITY).
WatchTreeBOOLEANinTRUE to watch every subkey recursively under each key in the set.
BufferPVOIDoutReserved — currently unused by the kernel; pass NULL.
BufferSizeULONGinReserved — pass 0.
AsynchronousBOOLEANinTRUE returns STATUS_PENDING immediately; FALSE blocks the caller until a change occurs.

Syscall IDs by Windows version

Windows versionSyscall IDBuild
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

Kernel module

ntoskrnl.exeNtNotifyChangeMultipleKeys

Related APIs

RegNotifyChangeKeyValueNtNotifyChangeKeyNtOpenKeyNtOpenKeyExNtQueryKey

Syscall stub

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

Undocumented notes

Multi-key sibling of `NtNotifyChangeKey`. There is no Win32 wrapper — the `RegNotifyChangeKeyValue` API watches only a single key. The function is used internally by the Windows configuration manager (CM) to coordinate notifications across linked hive segments and by a handful of MMC snap-ins. The kernel walks the `SubordinateObjects` list, opens each key, and registers a single CM_NOTIFY_BLOCK that signals when *any* of them changes — the IO_STATUS_BLOCK does not tell you which key fired; the caller must re-query each key to discover the delta. Despite its narrow user base it is exported by ntdll on every build since NT 4.0, with a stable parameter list.

Common malware usage

**Rarely seen in malware** — almost every persistence-monitoring or anti-cleanup module that needs registry watch behavior uses the simpler `NtNotifyChangeKey` (or RegNotifyChangeKeyValue) against a single Run key. The few documented abuses combine Multiple Keys with `WatchTree=TRUE` to watch the entire `HKLM\Software\Microsoft\Windows\CurrentVersion\Run`, `RunOnce`, `Image File Execution Options`, and `Winlogon` family under a single APC, so the implant can re-install any persistence entry the moment an EDR or a sysadmin deletes one. Because there is no Win32 wrapper the call itself stands out a bit more in stack traces than the single-key variant.

Detection opportunities

Telemetry signal here is meaningfully stronger than for `NtNotifyChangeKey` precisely because legitimate user-mode software almost never calls this — it is largely a CM-internal path. ETW Microsoft-Windows-Kernel-Registry will surface registry-notify request creation; pair that with the EDR's per-thread call stack and a non-system process making this call is suspicious on its own. Sysmon Event ID 12/13/14 (RegistryEvent) reports the changes themselves, not the watch registration — defenders correlating watch-then-restore patterns across a single TID typically catch persistence-restore loops. A defender pivot worth running: identify processes that hold a handle on a Run-family key plus an outstanding async notify; cross-reference with image path and signer.

Direct syscall examples

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

MITRE ATT&CK mappings

Last verified: 2026-05-20