> Windows Syscalls
ntoskrnl.exeT1083T1497T1106

NtNotifyChangeDirectoryFile

Registriert eine asynchrone Benachrichtigungsanforderung für Dateisystemänderungen in einem geöffneten Verzeichnis-Handle.

Prototyp

NTSTATUS NtNotifyChangeDirectoryFile(
  HANDLE             FileHandle,
  HANDLE             Event,
  PIO_APC_ROUTINE    ApcRoutine,
  PVOID              ApcContext,
  PIO_STATUS_BLOCK   IoStatusBlock,
  PVOID              Buffer,
  ULONG              Length,
  ULONG              CompletionFilter,
  BOOLEAN            WatchTree
);

Argumente

NameTypeDirDescription
FileHandleHANDLEinHandle auf ein mit FILE_LIST_DIRECTORY-Zugriff geöffnetes Verzeichnis.
EventHANDLEinOptionales Event, das bei Abschluss signalisiert wird. NULL bei Verwendung von APC oder alertable Wait.
ApcRoutinePIO_APC_ROUTINEinOptionale User-Mode-APC, die bei Abschluss in einem alertable Thread zugestellt wird.
ApcContextPVOIDinVom Aufrufer definierter Kontext, der an ApcRoutine übergeben wird.
IoStatusBlockPIO_STATUS_BLOCKoutEmpfängt den finalen Status und die Anzahl der in Buffer geschriebenen Bytes.
BufferPVOIDoutDWORD-ausgerichteter Puffer, der eine Kette von FILE_NOTIFY_INFORMATION-Datensätzen empfängt.
LengthULONGinGröße von Buffer in Bytes. Datensätze größer als Length lösen STATUS_NOTIFY_ENUM_DIR aus — der Aufrufer muss neu enumerieren.
CompletionFilterULONGinBitmaske der FILE_NOTIFY_CHANGE_*-Flags (FILE_NAME, ATTRIBUTES, SIZE, LAST_WRITE, SECURITY, ...).
WatchTreeBOOLEANinTRUE überwacht rekursiv alle Unterverzeichnisse; FALSE überwacht nur das unmittelbare Verzeichnis.

Syscall-IDs pro Windows-Version

Windows-VersionSyscall-IDBuild
Win10 15070x104win10-1507
Win10 16070x109win10-1607
Win10 17030x10Dwin10-1703
Win10 17090x10Ewin10-1709
Win10 18030x110win10-1803
Win10 18090x111win10-1809
Win10 19030x112win10-1903
Win10 19090x112win10-1909
Win10 20040x117win10-2004
Win10 20H20x117win10-20h2
Win10 21H10x117win10-21h1
Win10 21H20x118win10-21h2
Win10 22H20x118win10-22h2
Win11 21H20x11Ewin11-21h2
Win11 22H20x11Fwin11-22h2
Win11 23H20x11Fwin11-23h2
Win11 24H20x121win11-24h2
Server 20160x109winserver-2016
Server 20190x111winserver-2019
Server 20220x11Dwinserver-2022
Server 20250x121winserver-2025

Kernel-Modul

ntoskrnl.exeNtNotifyChangeDirectoryFile

Verwandte APIs

ReadDirectoryChangesWReadDirectoryChangesExWNtNotifyChangeDirectoryFileExFindFirstChangeNotificationWFindNextChangeNotificationNtQueryDirectoryFile

Syscall-Stub

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

Dies ist das Kernel-Rückgrat von `ReadDirectoryChangesW` und jedes Dateisystem-Watcher-Frameworks unter Windows — .NETs `FileSystemWatcher`, Nodes `fs.watch`, Gos `fsnotify`, Rusts `notify` usw. Der Aufruf ist **asynchron**: er liefert sofort STATUS_PENDING und schließt später über den IO_STATUS_BLOCK, ein Event oder eine APC ab. Der Ergebnispuffer ist eine gepackte Kette aus `FILE_NOTIFY_INFORMATION`-Datensätzen (variable Länge, verkettet über `NextEntryOffset`). `WatchTree=TRUE` ist vergleichsweise teuer — der IO-Manager muss jede Verzeichnisänderung durch die Filterliste des Elternverzeichnisses propagieren. NTFS implementiert das Change-Journal auf Volume-Ebene; bei SMB-Shares leitet der Redirector die Anfrage weiter.

Häufige Malware-Nutzung

Zwei Haupt-Missbrauchsmuster. Erstens, **Watchdog-Implants** öffnen ein Handle auf ein Verzeichnis, das der Operator nutzen könnte (z. B. `C:\Tools`, `%USERPROFILE%\Downloads`, `C:\Windows\Temp`) und nutzen NtNotifyChangeDirectoryFile mit `WatchTree=TRUE`, um zu reagieren, sobald ein Analyst Procmon.exe, x64dbg.exe, Wireshark.exe oder ein EDR-Setup ablegt — dann löscht sich das Implant selbst, exfiltriert weniger aggressiv oder beendet seinen geplanten Task. Zweitens, **Trigger-getriebenes C2**: das Implant beobachtet einen festen Pfad (oft Downloads oder einen synchronisierten OneDrive-Ordner) und behandelt das Erscheinen einer speziell benannten Datei als Signal für die nächste Stufe — ein verdecktes lokales Dead-Drop, das jeglichen C2-Traffic vermeidet, bis es aktiviert wird. Manche Ransomware (Royal, Black-Basta-Varianten) verwendet es zusätzlich, um neu eingehängte USB- oder Netzlaufwerke während des Verschlüsselns zu erkennen, damit der Worker das neue Volume ohne Neustart aufnimmt.

Erkennungs­möglichkeiten

Aus Verteidigersicht ist `NtNotifyChangeDirectoryFile` überwältigend gutartig — Explorer, Search Indexer, OneDrive, AV-Produkte, IDE-Datei-Watcher und Antivirus-Engines rufen es ständig auf. Das Signal liegt darin, *welche* Verzeichnisse beobachtet werden und *wer* sie beobachtet: ein Nicht-Explorer-Prozess, der einen rekursiven Watch auf `C:\` oder `%SystemRoot%` hält, ist auffällig. Sysmon Event ID 11 (FileCreate) ist die orthogonale Verteidiger-Primitive — sie zeigt die Dateiereignisse, die man sonst aus diesem Syscall ableiten müsste. Der ETW-Provider Microsoft-Windows-Kernel-FileFilter / Microsoft-Windows-Kernel-File legt die zugrundeliegenden IRP_MJ_DIRECTORY_CONTROL minor IRP_MN_NOTIFY_CHANGE_DIRECTORY Operationen offen. EDRs, die Handle-Telemetrie erfassen, können von einem verdächtigen Verzeichnis-Open (FILE_LIST_DIRECTORY auf einem sensiblen Pfad) zu folgenden Notify-Aufrufen pivotieren.

Direkte Syscall-Beispiele

asmx64 direct stub (Win11 24H2)

; Direct syscall stub for NtNotifyChangeDirectoryFile (SSN 0x121 on Win11 24H2)
NtNotifyChangeDirectoryFile PROC
    mov  r10, rcx          ; syscall convention
    mov  eax, 121h         ; SSN — varies per build, prefer dynamic resolution
    syscall
    ret
NtNotifyChangeDirectoryFile ENDP

cWatchdog implant — watch for analyst tooling

// Open %USERPROFILE%\Downloads and react when known analyst tools land there.
// On hit, the implant flips to dormant mode (skips its next beacon round).
#include <windows.h>
#include <winternl.h>

static const WCHAR* k_targets[] = {
    L"procmon.exe", L"procmon64.exe", L"x64dbg.exe", L"x32dbg.exe",
    L"wireshark.exe", L"fakenet.exe", L"tcpview.exe", L"autoruns.exe"
};

VOID WatchdogThread(VOID) {
    HANDLE hDir = CreateFileW(L"C:\\Users\\Public\\Downloads",
        FILE_LIST_DIRECTORY,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        NULL, OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS, NULL);
    if (hDir == INVALID_HANDLE_VALUE) return;

    BYTE  buf[4096];
    DWORD got = 0;
    while (ReadDirectoryChangesW(hDir, buf, sizeof(buf), TRUE,
               FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_CREATION,
               &got, NULL, NULL)) {
        FILE_NOTIFY_INFORMATION* p = (FILE_NOTIFY_INFORMATION*)buf;
        for (;;) {
            for (int i = 0; i < ARRAYSIZE(k_targets); ++i) {
                if (wcsstr(p->FileName, k_targets[i])) {
                    g_dormant_until = GetTickCount64() + 24ULL*3600*1000;
                    break;
                }
            }
            if (!p->NextEntryOffset) break;
            p = (FILE_NOTIFY_INFORMATION*)((BYTE*)p + p->NextEntryOffset);
        }
    }
}

rustTrigger-driven C2 dead-drop via direct syscall

// Cargo: ntapi = "0.4", windows-sys = "0.59"
// Block on the directory until a file named "runme.bin" appears,
// then read it and pass to stage-2 loader.
use std::ptr::null_mut;
use ntapi::ntioapi::{NtNotifyChangeDirectoryFile, IO_STATUS_BLOCK,
                    FILE_NOTIFY_INFORMATION};
use windows_sys::Win32::Storage::FileSystem::*;

pub unsafe fn wait_for_trigger(h_dir: isize) -> Option<Vec<u16>> {
    let mut buf = vec![0u8; 4096];
    let mut iosb: IO_STATUS_BLOCK = std::mem::zeroed();
    let status = NtNotifyChangeDirectoryFile(
        h_dir as _, null_mut(), None, null_mut(),
        &mut iosb,
        buf.as_mut_ptr() as _, buf.len() as u32,
        FILE_NOTIFY_CHANGE_FILE_NAME, 0); // 0 = no subtree
    if status < 0 { return None; }
    let info = &*(buf.as_ptr() as *const FILE_NOTIFY_INFORMATION);
    let n = (info.FileNameLength as usize) / 2;
    let name: Vec<u16> = std::slice::from_raw_parts(info.FileName.as_ptr(), n).to_vec();
    Some(name)
}

MITRE ATT&CK-Mappings

Last verified: 2026-05-20