> Windows Syscalls
ntoskrnl.exeT1083T1014T1106

NtQueryDirectoryFile

Enumerates a directory at the IRP layer — used by rootkits to hide files by tampering with the returned list.

Prototype

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

Arguments

NameTypeDirDescription
FileHandleHANDLEinHandle to the directory, opened with FILE_LIST_DIRECTORY | SYNCHRONIZE.
EventHANDLEinOptional event for async completion. NULL for synchronous handles.
ApcRoutinePIO_APC_ROUTINEinOptional APC routine queued on completion. Usually NULL.
ApcContextPVOIDinContext for ApcRoutine. NULL when no APC.
IoStatusBlockPIO_STATUS_BLOCKoutReceives status and total bytes written to FileInformation.
FileInformationPVOIDoutOutput buffer filled with one or more FILE_*_INFORMATION records.
LengthULONGinSize of FileInformation in bytes. STATUS_BUFFER_OVERFLOW if too small.
FileInformationClassFILE_INFORMATION_CLASSinRecord layout: FileDirectoryInformation=1, FileFullDirectoryInformation=2, FileBothDirectoryInformation=3, FileNamesInformation=12, FileIdBothDirectoryInformation=37.
ReturnSingleEntryBOOLEANinTRUE returns at most one record per call (slower, simpler caller loop).
FileNamePUNICODE_STRINGinOptional search mask supporting `*` and `?` (e.g. `*.exe`). NULL = all entries.
RestartScanBOOLEANinTRUE rewinds the enumeration. Pass TRUE on the first call, FALSE thereafter.

Syscall IDs by Windows version

Windows versionSyscall IDBuild
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

Kernel module

ntoskrnl.exeNtQueryDirectoryFile

Related APIs

FindFirstFileWFindNextFileWFindFirstFileExWNtQueryDirectoryFileExNtQueryInformationFileNtCreateFile

Syscall stub

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

Undocumented notes

NtQueryDirectoryFile is what user-mode `FindFirstFileW`/`FindNextFileW` ultimately calls, but it's substantially faster because it can return many records in a single trip across the kernel boundary — the records are packed as a linked list via `NextEntryOffset`. The information class controls the per-record layout: `FileBothDirectoryInformation` (3) is the default for `FindFirstFile` and includes 8.3 short names; `FileIdBothDirectoryInformation` (37) adds the file reference number (MFT entry index on NTFS), which lets a caller cross-reference USN journal entries without a second open. SSN `0x35` has been stable since Windows 10 1507.

Common malware usage

Two distinct uses. (1) **Stealth enumeration**: many forensic / scanner tools hook user-mode `FindFirstFileW`, so a direct NtQueryDirectoryFile bypasses that hook entirely; combined with NT-namespace paths and `FileNamesInformation` (smallest payload), it walks deep trees with minimal allocation footprint. (2) **Rootkit hiding (T1014)** — both user-mode (DLL-hook style: Necurs, Ramnit, ZeroAccess) and kernel-mode (FU, TDL3/4, Equation's GrayFish) rootkits intercept this call to splice their own files out of the linked list before it returns to the caller. The classic trick is to walk the result buffer and rewrite each victim entry's `NextEntryOffset` to skip the entry to hide, fixing up the terminal record's offset to 0. Variants achieve the same by filtering at the minifilter layer with `IRP_MJ_DIRECTORY_CONTROL` post-callbacks.

Detection opportunities

On its own, this syscall is invoked tens of thousands of times per second on a busy host (Explorer, Defender, indexers). Volume alone is meaningless. The blue-team angle is **integrity, not telemetry**: compare results from a high-level enumeration (`FindFirstFile` chain) against a low-level one (raw MFT read via `\\.\C:` `$Mft` parser, or `FSCTL_GET_NTFS_FILE_RECORD`) — discrepancies indicate a rootkit splicing the list. Tools like GMER, PowerForensics, and Velociraptor's `parse_mft()` artefact do exactly this. The IRP_MJ_DIRECTORY_CONTROL minifilter callback can also be inspected for unsigned/unknown filters via `fltmc instances`. Kernel-mode hooks on `NtQueryDirectoryFile`/`NtQueryDirectoryFileEx` in SSDT (32-bit) or via inline patch (PatchGuard violation on x64) are direct rootkit indicators.

Direct syscall examples

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
}

MITRE ATT&CK mappings

Last verified: 2026-05-20