NtQueryDirectoryFile
Enumera un directorio a nivel IRP — usado por rootkits para ocultar archivos manipulando la lista devuelta.
Prototipo
NTSTATUS NtQueryDirectoryFile( HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass, BOOLEAN ReturnSingleEntry, PUNICODE_STRING FileName, BOOLEAN RestartScan );
Argumentos
| Name | Type | Dir | Description |
|---|---|---|---|
| FileHandle | HANDLE | in | Handle al directorio, abierto con FILE_LIST_DIRECTORY | SYNCHRONIZE. |
| Event | HANDLE | in | Evento opcional para completar async. NULL para handles síncronos. |
| ApcRoutine | PIO_APC_ROUTINE | in | Rutina APC opcional al completar. Normalmente NULL. |
| ApcContext | PVOID | in | Contexto para ApcRoutine. NULL sin APC. |
| IoStatusBlock | PIO_STATUS_BLOCK | out | Recibe el status y los bytes totales escritos en FileInformation. |
| FileInformation | PVOID | out | Búfer de salida con uno o más registros FILE_*_INFORMATION. |
| Length | ULONG | in | Tamaño de FileInformation en bytes. STATUS_BUFFER_OVERFLOW si es insuficiente. |
| FileInformationClass | FILE_INFORMATION_CLASS | in | Formato: FileDirectoryInformation=1, FileFullDirectoryInformation=2, FileBothDirectoryInformation=3, FileNamesInformation=12, FileIdBothDirectoryInformation=37. |
| ReturnSingleEntry | BOOLEAN | in | TRUE devuelve a lo sumo un registro por llamada (más lento, bucle más simple). |
| FileName | PUNICODE_STRING | in | Máscara de búsqueda opcional con `*` y `?` (p. ej. `*.exe`). NULL = todas las entradas. |
| RestartScan | BOOLEAN | in | TRUE reinicia la enumeración. TRUE en la primera llamada, FALSE después. |
IDs de syscalls por versión de Windows
| Versión de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0x35 | win10-1507 |
| Win10 1607 | 0x35 | win10-1607 |
| Win10 1703 | 0x35 | win10-1703 |
| Win10 1709 | 0x35 | win10-1709 |
| Win10 1803 | 0x35 | win10-1803 |
| Win10 1809 | 0x35 | win10-1809 |
| Win10 1903 | 0x35 | win10-1903 |
| Win10 1909 | 0x35 | win10-1909 |
| Win10 2004 | 0x35 | win10-2004 |
| Win10 20H2 | 0x35 | win10-20h2 |
| Win10 21H1 | 0x35 | win10-21h1 |
| Win10 21H2 | 0x35 | win10-21h2 |
| Win10 22H2 | 0x35 | win10-22h2 |
| Win11 21H2 | 0x35 | win11-21h2 |
| Win11 22H2 | 0x35 | win11-22h2 |
| Win11 23H2 | 0x35 | win11-23h2 |
| Win11 24H2 | 0x35 | win11-24h2 |
| Server 2016 | 0x35 | winserver-2016 |
| Server 2019 | 0x35 | winserver-2019 |
| Server 2022 | 0x35 | winserver-2022 |
| Server 2025 | 0x35 | winserver-2025 |
Módulo del kernel
APIs relacionadas
Stub del syscall
4C 8B D1 mov r10, rcx B8 35 00 00 00 mov eax, 0x35 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
NtQueryDirectoryFile es lo que en última instancia llaman `FindFirstFileW`/`FindNextFileW` en modo usuario, pero sustancialmente más rápido porque puede devolver muchos registros en un único cruce de frontera del kernel — empaquetados como lista enlazada vía `NextEntryOffset`. La clase de información controla el layout: `FileBothDirectoryInformation` (3) es el predeterminado de `FindFirstFile` e incluye nombres cortos 8.3; `FileIdBothDirectoryInformation` (37) añade el número de referencia (índice MFT en NTFS), permitiendo cruzar con USN journal sin reabrir. El SSN `0x35` está estable desde Windows 10 1507.
Uso común por malware
Dos usos distintos. (1) **Enumeración sigilosa**: muchas herramientas forenses enganchan `FindFirstFileW` en modo usuario, así que llamar directo a NtQueryDirectoryFile esquiva ese hook; junto a rutas NT y `FileNamesInformation` (payload mínimo), recorre árboles profundos con poca asignación. (2) **Ocultación de rootkit (T1014)** — tanto user-mode (estilo hook DLL: Necurs, Ramnit, ZeroAccess) como kernel-mode (FU, TDL3/4, GrayFish de Equation) interceptan esta llamada para sacar sus archivos de la lista enlazada antes de retornar. El truco clásico es recorrer el buffer resultado y reescribir `NextEntryOffset` de cada víctima para saltarla, dejando el offset del último en 0. Variantes filtran a nivel minifilter vía callbacks post `IRP_MJ_DIRECTORY_CONTROL`.
Oportunidades de detección
Por sí solo, este syscall se invoca decenas de miles de veces por segundo en un host activo (Explorer, Defender, indexadores). El volumen por sí mismo no dice nada. El ángulo defensivo es **integridad, no telemetría**: comparar una enumeración de alto nivel (`FindFirstFile`) con una de bajo nivel (lectura cruda del MFT vía `\\.\C:` parser `$Mft`, o `FSCTL_GET_NTFS_FILE_RECORD`) — discrepancia = splicing de rootkit. GMER, PowerForensics y el artefacto `parse_mft()` de Velociraptor lo hacen exactamente así. Los callbacks minifilter de IRP_MJ_DIRECTORY_CONTROL pueden auditarse vía `fltmc instances` buscando filtros sin firma. Hooks kernel sobre `NtQueryDirectoryFile`/`NtQueryDirectoryFileEx` en SSDT (32 bits) o por parche inline (violación de PatchGuard en x64) = firma directa de rootkit.
Ejemplos de syscalls directos
cWalk a directory with FileBothDirectoryInformation
// Assumes hDir was opened with FILE_LIST_DIRECTORY | SYNCHRONIZE
// + FILE_SYNCHRONOUS_IO_NONALERT.
BYTE buffer[0x10000];
IO_STATUS_BLOCK iosb = {0};
BOOLEAN restart = TRUE;
for (;;) {
NTSTATUS s = NtQueryDirectoryFile(
hDir, NULL, NULL, NULL, &iosb,
buffer, sizeof(buffer),
FileBothDirectoryInformation, // class 3
FALSE, // return many entries
NULL, // no mask
restart);
restart = FALSE;
if (s == STATUS_NO_MORE_FILES) break;
if (!NT_SUCCESS(s)) break;
PFILE_BOTH_DIR_INFORMATION e = (PFILE_BOTH_DIR_INFORMATION)buffer;
for (;;) {
// e->FileName / e->FileNameLength / e->EndOfFile
if (!e->NextEntryOffset) break;
e = (PFILE_BOTH_DIR_INFORMATION)((BYTE*)e + e->NextEntryOffset);
}
}asmDirect stub (SSN 0x35) — 11 args
; NtQueryDirectoryFile has 11 args; first 4 via RCX/RDX/R8/R9,
; remaining 7 pushed onto the stack after the 32-byte home space.
NtQueryDirectoryFile PROC
mov r10, rcx
mov eax, 35h
syscall
ret
NtQueryDirectoryFile ENDPrustIterator over FILE_NAMES_INFORMATION records
// Cargo: ntapi = "0.4", winapi = { version = "0.3", features = ["ntdef"] }
use ntapi::ntioapi::{NtQueryDirectoryFile, IO_STATUS_BLOCK, FILE_NAMES_INFORMATION};
use winapi::shared::ntdef::HANDLE;
pub unsafe fn list_names(h_dir: HANDLE) -> Vec<String> {
let mut buf = vec![0u8; 0x4000];
let mut iosb: IO_STATUS_BLOCK = core::mem::zeroed();
let mut out = Vec::new();
let mut restart = 1u8;
loop {
let s = NtQueryDirectoryFile(
h_dir, core::ptr::null_mut(),
None, core::ptr::null_mut(),
&mut iosb,
buf.as_mut_ptr() as *mut _, buf.len() as u32,
12, /* FileNamesInformation */
0, core::ptr::null_mut(), restart,
);
restart = 0;
if s != 0 { break; }
let mut off = 0usize;
loop {
let e = &*(buf.as_ptr().add(off) as *const FILE_NAMES_INFORMATION);
let name = core::slice::from_raw_parts(
e.FileName.as_ptr(), (e.FileNameLength / 2) as usize);
out.push(String::from_utf16_lossy(name));
if e.NextEntryOffset == 0 { break; }
off += e.NextEntryOffset as usize;
}
}
out
}Mapeos MITRE ATT&CK
Last verified: 2026-05-20