> Windows Syscalls
ntoskrnl.exeT1083T1014T1106

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

NameTypeDirDescription
FileHandleHANDLEinHandle al directorio, abierto con FILE_LIST_DIRECTORY | SYNCHRONIZE.
EventHANDLEinEvento opcional para completar async. NULL para handles síncronos.
ApcRoutinePIO_APC_ROUTINEinRutina APC opcional al completar. Normalmente NULL.
ApcContextPVOIDinContexto para ApcRoutine. NULL sin APC.
IoStatusBlockPIO_STATUS_BLOCKoutRecibe el status y los bytes totales escritos en FileInformation.
FileInformationPVOIDoutBúfer de salida con uno o más registros FILE_*_INFORMATION.
LengthULONGinTamaño de FileInformation en bytes. STATUS_BUFFER_OVERFLOW si es insuficiente.
FileInformationClassFILE_INFORMATION_CLASSinFormato: FileDirectoryInformation=1, FileFullDirectoryInformation=2, FileBothDirectoryInformation=3, FileNamesInformation=12, FileIdBothDirectoryInformation=37.
ReturnSingleEntryBOOLEANinTRUE devuelve a lo sumo un registro por llamada (más lento, bucle más simple).
FileNamePUNICODE_STRINGinMáscara de búsqueda opcional con `*` y `?` (p. ej. `*.exe`). NULL = todas las entradas.
RestartScanBOOLEANinTRUE reinicia la enumeración. TRUE en la primera llamada, FALSE después.

IDs de syscalls por versión de Windows

Versión de WindowsID de syscallBuild
Win10 15070x35win10-1507
Win10 16070x35win10-1607
Win10 17030x35win10-1703
Win10 17090x35win10-1709
Win10 18030x35win10-1803
Win10 18090x35win10-1809
Win10 19030x35win10-1903
Win10 19090x35win10-1909
Win10 20040x35win10-2004
Win10 20H20x35win10-20h2
Win10 21H10x35win10-21h1
Win10 21H20x35win10-21h2
Win10 22H20x35win10-22h2
Win11 21H20x35win11-21h2
Win11 22H20x35win11-22h2
Win11 23H20x35win11-23h2
Win11 24H20x35win11-24h2
Server 20160x35winserver-2016
Server 20190x35winserver-2019
Server 20220x35winserver-2022
Server 20250x35winserver-2025

Módulo del kernel

ntoskrnl.exeNtQueryDirectoryFile

APIs relacionadas

FindFirstFileWFindNextFileWFindFirstFileExWNtQueryDirectoryFileExNtQueryInformationFileNtCreateFile

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 ENDP

rustIterator 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