NtQueryDirectoryFile
Énumère un répertoire au niveau IRP — utilisé par les rootkits pour cacher des fichiers en altérant la liste retournée.
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 vers le répertoire, ouvert avec FILE_LIST_DIRECTORY | SYNCHRONIZE. |
| Event | HANDLE | in | Évènement optionnel pour la complétion async. NULL pour handles synchrones. |
| ApcRoutine | PIO_APC_ROUTINE | in | Routine APC optionnelle à la complétion. Habituellement NULL. |
| ApcContext | PVOID | in | Contexte pour ApcRoutine. NULL sans APC. |
| IoStatusBlock | PIO_STATUS_BLOCK | out | Reçoit le statut et le total d'octets écrits dans FileInformation. |
| FileInformation | PVOID | out | Tampon de sortie rempli d'un ou plusieurs enregistrements FILE_*_INFORMATION. |
| Length | ULONG | in | Taille de FileInformation en octets. STATUS_BUFFER_OVERFLOW si trop petit. |
| FileInformationClass | FILE_INFORMATION_CLASS | in | Disposition : FileDirectoryInformation=1, FileFullDirectoryInformation=2, FileBothDirectoryInformation=3, FileNamesInformation=12, FileIdBothDirectoryInformation=37. |
| ReturnSingleEntry | BOOLEAN | in | TRUE retourne au plus un enregistrement par appel (boucle plus simple, plus lent). |
| FileName | PUNICODE_STRING | in | Masque de recherche optionnel supportant `*` et `?` (ex. `*.exe`). NULL = toutes les entrées. |
| RestartScan | BOOLEAN | in | TRUE rembobine l'énumération. TRUE au premier appel, FALSE ensuite. |
IDs de syscalls par version de Windows
| Version 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 |
Module noyau
APIs liées
Stub du 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
Notes non documentées
NtQueryDirectoryFile est ce qu'appellent in fine `FindFirstFileW`/`FindNextFileW` user-mode, mais sensiblement plus rapide car il peut retourner plusieurs enregistrements en un seul aller-retour noyau — empaquetés en liste chaînée via `NextEntryOffset`. La classe d'information contrôle la disposition : `FileBothDirectoryInformation` (3) est le défaut de `FindFirstFile` et inclut les noms courts 8.3 ; `FileIdBothDirectoryInformation` (37) ajoute le numéro de référence (index MFT sous NTFS), permettant de croiser le journal USN sans réouverture. Le SSN `0x35` est stable depuis Windows 10 1507.
Usage courant par les malwares
Deux usages distincts. (1) **Énumération furtive** : beaucoup d'outils forensiques hookent `FindFirstFileW` user-mode, donc un appel direct à NtQueryDirectoryFile contourne ce hook ; combiné aux chemins NT et à `FileNamesInformation` (payload minimal), il parcourt des arborescences profondes avec peu d'allocations. (2) **Dissimulation rootkit (T1014)** — rootkits user-mode (style hook DLL : Necurs, Ramnit, ZeroAccess) et noyau (FU, TDL3/4, GrayFish d'Equation) interceptent cet appel pour exfiltrer leurs fichiers de la liste chaînée avant retour. L'astuce classique consiste à parcourir le buffer résultat et réécrire `NextEntryOffset` de chaque victime pour la sauter, ramenant l'offset du dernier à 0. Variantes : filtrer côté minifilter via callbacks post `IRP_MJ_DIRECTORY_CONTROL`.
Opportunités de détection
À lui seul, ce syscall est invoqué des dizaines de milliers de fois par seconde sur un hôte chargé (Explorer, Defender, indexeurs). Le volume seul ne dit rien. L'angle défensif est **l'intégrité, pas la télémétrie** : comparer une énumération haut niveau (`FindFirstFile`) à une basse niveau (lecture brute du MFT via `\\.\C:` parser `$Mft`, ou `FSCTL_GET_NTFS_FILE_RECORD`) — divergence = rootkit splicing. GMER, PowerForensics, l'artefact `parse_mft()` de Velociraptor font exactement cela. Les callbacks minifilter IRP_MJ_DIRECTORY_CONTROL peuvent aussi être audités via `fltmc instances` à la recherche de filtres non signés. Hooks noyau sur `NtQueryDirectoryFile`/`NtQueryDirectoryFileEx` en SSDT (32 bits) ou via patch inline (violation PatchGuard en x64) = signature directe de rootkit.
Exemples de syscalls directs
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
}Mappings MITRE ATT&CK
Last verified: 2026-05-20