> Windows Syscalls
ntoskrnl.exeT1083T1497T1106

NtNotifyChangeDirectoryFile

Enregistre une demande de notification asynchrone des changements de fichiers dans un répertoire ouvert.

Prototype

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

Arguments

NameTypeDirDescription
FileHandleHANDLEinHandle vers un répertoire ouvert avec l'accès FILE_LIST_DIRECTORY.
EventHANDLEinÉvénement optionnel signalé à l'achèvement. NULL si un APC ou une attente alertable est utilisé.
ApcRoutinePIO_APC_ROUTINEinAPC usermode optionnel délivré à l'achèvement dans un thread alertable.
ApcContextPVOIDinContexte défini par l'appelant transmis à ApcRoutine.
IoStatusBlockPIO_STATUS_BLOCKoutReçoit le statut final et le nombre d'octets écrits dans Buffer.
BufferPVOIDoutBuffer aligné DWORD qui reçoit une chaîne d'enregistrements FILE_NOTIFY_INFORMATION.
LengthULONGinTaille de Buffer en octets. Des enregistrements plus grands que Length provoquent STATUS_NOTIFY_ENUM_DIR — l'appelant doit ré-énumérer.
CompletionFilterULONGinMasque de bits FILE_NOTIFY_CHANGE_* (FILE_NAME, ATTRIBUTES, SIZE, LAST_WRITE, SECURITY, ...).
WatchTreeBOOLEANinTRUE pour surveiller récursivement tous les sous-répertoires ; FALSE pour surveiller uniquement le répertoire immédiat.

IDs de syscalls par version de Windows

Version de WindowsID de syscallBuild
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

Module noyau

ntoskrnl.exeNtNotifyChangeDirectoryFile

APIs liées

ReadDirectoryChangesWReadDirectoryChangesExWNtNotifyChangeDirectoryFileExFindFirstChangeNotificationWFindNextChangeNotificationNtQueryDirectoryFile

Stub du syscall

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

Notes non documentées

C'est l'épine dorsale noyau de `ReadDirectoryChangesW` et de tous les frameworks de surveillance de fichiers sous Windows — `FileSystemWatcher` de .NET, `fs.watch` de Node, `fsnotify` de Go, `notify` de Rust, etc. L'appel est **asynchrone** : il renvoie STATUS_PENDING immédiatement et complète plus tard via l'IO_STATUS_BLOCK, un Event ou un APC. Le buffer résultat est une chaîne compacte d'enregistrements `FILE_NOTIFY_INFORMATION` (longueur variable, liés par `NextEntryOffset`). `WatchTree=TRUE` est relativement coûteux — l'IO manager doit faire remonter chaque changement de répertoire dans la liste de filtres du parent. NTFS implémente le journal de changements au niveau du volume ; pour les partages SMB, le redirecteur transmet la requête.

Usage courant par les malwares

Deux patterns d'abus principaux. D'abord, les **implants watchdog** ouvrent un handle vers un répertoire que l'opérateur risque d'utiliser (ex. `C:\Tools`, `%USERPROFILE%\Downloads`, `C:\Windows\Temp`) et utilisent NtNotifyChangeDirectoryFile avec `WatchTree=TRUE` pour réagir dès qu'un analyste pose Procmon.exe, x64dbg.exe, Wireshark.exe ou un installeur EDR — à ce moment-là l'implant s'auto-supprime, exfiltre moins agressivement, ou tue sa tâche planifiée. Ensuite, **C2 déclenché** : l'implant surveille un chemin fixe (souvent les téléchargements ou un dossier OneDrive synchronisé) et traite l'apparition d'un fichier nommé spécifiquement comme le signal de l'étape suivante — un dead-drop local discret qui évite tout trafic C2 jusqu'à activation. Certains ransomwares (Royal, variantes Black Basta) l'utilisent en plus pour détecter les nouveaux volumes USB ou réseau montés en cours de chiffrement, afin que le worker ramasse le nouveau volume sans redémarrage.

Opportunités de détection

Du point de vue défensif, `NtNotifyChangeDirectoryFile` est en très grande majorité bénin — Explorer, Search Indexer, OneDrive, les AV, les IDE et les moteurs antivirus l'appellent en permanence. Le signal réside dans *quels* répertoires sont surveillés et *qui* les surveille : un processus non-Explorer qui maintient une surveillance récursive sur `C:\` ou `%SystemRoot%` est inhabituel. Sysmon Event ID 11 (FileCreate) est la primitive défensive orthogonale — elle expose les événements fichier qu'il faudrait sinon dériver de ce syscall. Le provider ETW Microsoft-Windows-Kernel-FileFilter / Microsoft-Windows-Kernel-File expose les opérations sous-jacentes IRP_MJ_DIRECTORY_CONTROL mineur IRP_MN_NOTIFY_CHANGE_DIRECTORY. Les EDR capturant la télémétrie des handles peuvent pivoter d'une ouverture suspecte de répertoire (FILE_LIST_DIRECTORY sur un chemin sensible) vers les appels notify suivants.

Exemples de syscalls directs

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

Mappings MITRE ATT&CK

Last verified: 2026-05-20