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
| Name | Type | Dir | Description |
|---|---|---|---|
| FileHandle | HANDLE | in | Handle a un directorio abierto con acceso FILE_LIST_DIRECTORY. |
| Event | HANDLE | in | Evento opcional señalado al completarse. NULL si se usa un APC o una espera alertable. |
| ApcRoutine | PIO_APC_ROUTINE | in | APC en modo usuario opcional entregada al completarse en un hilo alertable. |
| ApcContext | PVOID | in | Contexto definido por el llamador pasado a ApcRoutine. |
| IoStatusBlock | PIO_STATUS_BLOCK | out | Recibe el estado final y el número de bytes escritos en Buffer. |
| Buffer | PVOID | out | Buffer alineado a DWORD que recibe una cadena de registros FILE_NOTIFY_INFORMATION. |
| Length | ULONG | in | Tamaño de Buffer en bytes. Registros mayores que Length producen STATUS_NOTIFY_ENUM_DIR — el llamador debe reenumerar. |
| CompletionFilter | ULONG | in | Máscara de bits FILE_NOTIFY_CHANGE_* (FILE_NAME, ATTRIBUTES, SIZE, LAST_WRITE, SECURITY, ...). |
| WatchTree | BOOLEAN | in | TRUE para vigilar todos los subdirectorios recursivamente; FALSE para vigilar solo el directorio inmediato. |
IDs de syscalls por versión de Windows
| Versión de Windows | ID de syscall | 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 |
Módulo del kernel
APIs relacionadas
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 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)
}Mapeos MITRE ATT&CK
Last verified: 2026-05-20