> Windows Syscalls
ntoskrnl.exeT1083T1497T1106

NtNotifyChangeDirectoryFile

Registra una solicitud de notificación asíncrona para cambios del sistema de archivos en un directorio abierto.

Prototipo

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

Argumentos

NameTypeDirDescription
FileHandleHANDLEinHandle a un directorio abierto con acceso FILE_LIST_DIRECTORY.
EventHANDLEinEvento opcional señalado al completarse. NULL si se usa un APC o una espera alertable.
ApcRoutinePIO_APC_ROUTINEinAPC en modo usuario opcional entregada al completarse en un hilo alertable.
ApcContextPVOIDinContexto definido por el llamador pasado a ApcRoutine.
IoStatusBlockPIO_STATUS_BLOCKoutRecibe el estado final y el número de bytes escritos en Buffer.
BufferPVOIDoutBuffer alineado a DWORD que recibe una cadena de registros FILE_NOTIFY_INFORMATION.
LengthULONGinTamaño de Buffer en bytes. Registros mayores que Length producen STATUS_NOTIFY_ENUM_DIR — el llamador debe reenumerar.
CompletionFilterULONGinMáscara de bits FILE_NOTIFY_CHANGE_* (FILE_NAME, ATTRIBUTES, SIZE, LAST_WRITE, SECURITY, ...).
WatchTreeBOOLEANinTRUE para vigilar todos los subdirectorios recursivamente; FALSE para vigilar solo el directorio inmediato.

IDs de syscalls por versión de Windows

Versión 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

Módulo del kernel

ntoskrnl.exeNtNotifyChangeDirectoryFile

APIs relacionadas

ReadDirectoryChangesWReadDirectoryChangesExWNtNotifyChangeDirectoryFileExFindFirstChangeNotificationWFindNextChangeNotificationNtQueryDirectoryFile

Stub del 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

Notas no documentadas

Es la columna vertebral en kernel de `ReadDirectoryChangesW` y de todos los frameworks de vigilancia de archivos en Windows — `FileSystemWatcher` de .NET, `fs.watch` de Node, `fsnotify` de Go, `notify` de Rust, etc. La llamada es **asíncrona**: devuelve STATUS_PENDING de inmediato y completa después vía el IO_STATUS_BLOCK, un Event o un APC. El buffer de resultado es una cadena empaquetada de registros `FILE_NOTIFY_INFORMATION` (longitud variable, enlazados por `NextEntryOffset`). `WatchTree=TRUE` es comparativamente costoso — el IO manager debe propagar cada cambio por la lista de filtros del padre. NTFS implementa el journal de cambios a nivel de volumen; para recursos SMB el redirector reenvía la solicitud.

Uso común por malware

Dos patrones de abuso principales. Primero, **implantes watchdog** abren un handle a un directorio que el operador podría usar (p. ej. `C:\Tools`, `%USERPROFILE%\Downloads`, `C:\Windows\Temp`) y usan NtNotifyChangeDirectoryFile con `WatchTree=TRUE` para reaccionar en cuanto un analista suelta Procmon.exe, x64dbg.exe, Wireshark.exe o un instalador de EDR — en ese momento el implante se autodestruye, exfiltra menos agresivamente o mata su tarea programada. Segundo, **C2 disparado por evento**: el implante vigila una ruta fija (a menudo Descargas o una carpeta OneDrive sincronizada) y trata la aparición de un archivo nombrado específicamente como la señal de siguiente etapa — un dead-drop local sigiloso que evita todo tráfico C2 hasta su activación. Algunos ransomware (Royal, variantes de Black Basta) lo usan adicionalmente para detectar nuevos volúmenes USB o de red montados durante el cifrado, para que el worker recoja el nuevo volumen sin reiniciarse.

Oportunidades de detección

Desde la óptica defensiva, `NtNotifyChangeDirectoryFile` es abrumadoramente benigno — Explorer, Search Indexer, OneDrive, productos AV, watchers de IDE y motores antivirus lo invocan constantemente. La señal está en *qué* directorios se vigilan y *quién* los vigila: un proceso que no sea Explorer manteniendo una vigilancia recursiva sobre `C:\` o `%SystemRoot%` es inusual. Sysmon Event ID 11 (FileCreate) es la primitiva defensiva ortogonal — expone los eventos de archivo que de otro modo habría que derivar de este syscall. El proveedor ETW Microsoft-Windows-Kernel-FileFilter / Microsoft-Windows-Kernel-File expone las operaciones subyacentes IRP_MJ_DIRECTORY_CONTROL minor IRP_MN_NOTIFY_CHANGE_DIRECTORY. Los EDR que capturan telemetría de handles pueden pivotar de una apertura sospechosa de directorio (FILE_LIST_DIRECTORY sobre una ruta sensible) a las llamadas notify subsiguientes.

Ejemplos de syscalls directos

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

Mapeos MITRE ATT&CK

Last verified: 2026-05-20