> Windows Syscalls
ntoskrnl.exeT1083T1014T1106

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

NameTypeDirDescription
FileHandleHANDLEinHandle vers le répertoire, ouvert avec FILE_LIST_DIRECTORY | SYNCHRONIZE.
EventHANDLEinÉvènement optionnel pour la complétion async. NULL pour handles synchrones.
ApcRoutinePIO_APC_ROUTINEinRoutine APC optionnelle à la complétion. Habituellement NULL.
ApcContextPVOIDinContexte pour ApcRoutine. NULL sans APC.
IoStatusBlockPIO_STATUS_BLOCKoutReçoit le statut et le total d'octets écrits dans FileInformation.
FileInformationPVOIDoutTampon de sortie rempli d'un ou plusieurs enregistrements FILE_*_INFORMATION.
LengthULONGinTaille de FileInformation en octets. STATUS_BUFFER_OVERFLOW si trop petit.
FileInformationClassFILE_INFORMATION_CLASSinDisposition : FileDirectoryInformation=1, FileFullDirectoryInformation=2, FileBothDirectoryInformation=3, FileNamesInformation=12, FileIdBothDirectoryInformation=37.
ReturnSingleEntryBOOLEANinTRUE retourne au plus un enregistrement par appel (boucle plus simple, plus lent).
FileNamePUNICODE_STRINGinMasque de recherche optionnel supportant `*` et `?` (ex. `*.exe`). NULL = toutes les entrées.
RestartScanBOOLEANinTRUE rembobine l'énumération. TRUE au premier appel, FALSE ensuite.

IDs de syscalls par version de Windows

Version 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

Module noyau

ntoskrnl.exeNtQueryDirectoryFile

APIs liées

FindFirstFileWFindNextFileWFindFirstFileExWNtQueryDirectoryFileExNtQueryInformationFileNtCreateFile

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 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
}

Mappings MITRE ATT&CK

Last verified: 2026-05-20