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
| Name | Type | Dir | Description |
|---|---|---|---|
| FileHandle | HANDLE | in | Handle to the directory, opened with FILE_LIST_DIRECTORY | SYNCHRONIZE. |
| Event | HANDLE | in | Optional event for async completion. NULL for synchronous handles. |
| ApcRoutine | PIO_APC_ROUTINE | in | Optional APC routine queued on completion. Usually NULL. |
| ApcContext | PVOID | in | Context for ApcRoutine. NULL when no APC. |
| IoStatusBlock | PIO_STATUS_BLOCK | out | Receives status and total bytes written to FileInformation. |
| FileInformation | PVOID | out | Output buffer filled with one or more FILE_*_INFORMATION records. |
| Length | ULONG | in | Size of FileInformation in bytes. STATUS_BUFFER_OVERFLOW if too small. |
| FileInformationClass | FILE_INFORMATION_CLASS | in | Record layout: FileDirectoryInformation=1, FileFullDirectoryInformation=2, FileBothDirectoryInformation=3, FileNamesInformation=12, FileIdBothDirectoryInformation=37. |
| ReturnSingleEntry | BOOLEAN | in | TRUE returns at most one record per call (slower, simpler caller loop). |
| FileName | PUNICODE_STRING | in | Optional search mask supporting `*` and `?` (e.g. `*.exe`). NULL = all entries. |
| RestartScan | BOOLEAN | in | TRUE rewinds the enumeration. Pass TRUE on the first call, FALSE thereafter. |
Syscall IDs by Windows version
| Windows version | Syscall ID | 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 |
Kernel module
Related APIs
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 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
}MITRE ATT&CK mappings
Last verified: 2026-05-20