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
| Name | Type | Dir | Description |
|---|---|---|---|
| FileHandle | HANDLE | in | Handle auf ein mit FILE_LIST_DIRECTORY-Zugriff geöffnetes Verzeichnis. |
| Event | HANDLE | in | Optionales Event, das bei Abschluss signalisiert wird. NULL bei Verwendung von APC oder alertable Wait. |
| ApcRoutine | PIO_APC_ROUTINE | in | Optionale User-Mode-APC, die bei Abschluss in einem alertable Thread zugestellt wird. |
| ApcContext | PVOID | in | Vom Aufrufer definierter Kontext, der an ApcRoutine übergeben wird. |
| IoStatusBlock | PIO_STATUS_BLOCK | out | Empfängt den finalen Status und die Anzahl der in Buffer geschriebenen Bytes. |
| Buffer | PVOID | out | DWORD-ausgerichteter Puffer, der eine Kette von FILE_NOTIFY_INFORMATION-Datensätzen empfängt. |
| Length | ULONG | in | Größe von Buffer in Bytes. Datensätze größer als Length lösen STATUS_NOTIFY_ENUM_DIR aus — der Aufrufer muss neu enumerieren. |
| CompletionFilter | ULONG | in | Bitmaske der FILE_NOTIFY_CHANGE_*-Flags (FILE_NAME, ATTRIBUTES, SIZE, LAST_WRITE, SECURITY, ...). |
| WatchTree | BOOLEAN | in | TRUE überwacht rekursiv alle Unterverzeichnisse; FALSE überwacht nur das unmittelbare Verzeichnis. |
Syscall-IDs pro Windows-Version
| Windows-Version | Syscall-ID | Build |
|---|---|---|
| Win10 1507 | 0x104 | win10-1507 |
| Win10 1607 | 0x109 | win10-1607 |
| Win10 1703 | 0x10D | win10-1703 |
| Win10 1709 | 0x10E | win10-1709 |
| Win10 1803 | 0x110 | win10-1803 |
| Win10 1809 | 0x111 | win10-1809 |
| Win10 1903 | 0x112 | win10-1903 |
| Win10 1909 | 0x112 | win10-1909 |
| Win10 2004 | 0x117 | win10-2004 |
| Win10 20H2 | 0x117 | win10-20h2 |
| Win10 21H1 | 0x117 | win10-21h1 |
| Win10 21H2 | 0x118 | win10-21h2 |
| Win10 22H2 | 0x118 | win10-22h2 |
| Win11 21H2 | 0x11E | win11-21h2 |
| Win11 22H2 | 0x11F | win11-22h2 |
| Win11 23H2 | 0x11F | win11-23h2 |
| Win11 24H2 | 0x121 | win11-24h2 |
| Server 2016 | 0x109 | winserver-2016 |
| Server 2019 | 0x111 | winserver-2019 |
| Server 2022 | 0x11D | winserver-2022 |
| Server 2025 | 0x121 | winserver-2025 |
Kernel-Modul
Verwandte APIs
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.
Erkennungsmö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 ENDPcWatchdog 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