NtFsControlFile
Envoie des codes FSCTL à un système de fichiers — utilisé pour planter des reparse points, accéder aux ADS et abuser des jonctions.
Prototype
NTSTATUS NtFsControlFile( HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, ULONG FsControlCode, PVOID InputBuffer, ULONG InputBufferLength, PVOID OutputBuffer, ULONG OutputBufferLength );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| FileHandle | HANDLE | in | Handle vers un fichier, répertoire ou volume. L'accès requis dépend du FSCTL. |
| Event | HANDLE | in | Évènement optionnel pour async. NULL pour synchrone. |
| ApcRoutine | PIO_APC_ROUTINE | in | Routine APC optionnelle à la complétion. Habituellement NULL. |
| ApcContext | PVOID | in | Contexte pour ApcRoutine. NULL si inutilisé. |
| IoStatusBlock | PIO_STATUS_BLOCK | out | Reçoit le statut et les octets retournés dans OutputBuffer. |
| FsControlCode | ULONG | in | Code FSCTL_*, ex. FSCTL_SET_REPARSE_POINT (0x900A4), FSCTL_GET_REPARSE_POINT (0x900A8), FSCTL_SET_SPARSE. |
| InputBuffer | PVOID | in | Payload d'entrée spécifique au FSCTL (ex. REPARSE_DATA_BUFFER pour set-reparse). |
| InputBufferLength | ULONG | in | Taille en octets d'InputBuffer. |
| OutputBuffer | PVOID | out | Tampon de sortie optionnel pour les FSCTL qui retournent des données. |
| OutputBufferLength | ULONG | in | Taille en octets d'OutputBuffer. 0 si OutputBuffer est NULL. |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0x39 | win10-1507 |
| Win10 1607 | 0x39 | win10-1607 |
| Win10 1703 | 0x39 | win10-1703 |
| Win10 1709 | 0x39 | win10-1709 |
| Win10 1803 | 0x39 | win10-1803 |
| Win10 1809 | 0x39 | win10-1809 |
| Win10 1903 | 0x39 | win10-1903 |
| Win10 1909 | 0x39 | win10-1909 |
| Win10 2004 | 0x39 | win10-2004 |
| Win10 20H2 | 0x39 | win10-20h2 |
| Win10 21H1 | 0x39 | win10-21h1 |
| Win10 21H2 | 0x39 | win10-21h2 |
| Win10 22H2 | 0x39 | win10-22h2 |
| Win11 21H2 | 0x39 | win11-21h2 |
| Win11 22H2 | 0x39 | win11-22h2 |
| Win11 23H2 | 0x39 | win11-23h2 |
| Win11 24H2 | 0x39 | win11-24h2 |
| Server 2016 | 0x39 | winserver-2016 |
| Server 2019 | 0x39 | winserver-2019 |
| Server 2022 | 0x39 | winserver-2022 |
| Server 2025 | 0x39 | winserver-2025 |
Module noyau
APIs liées
Stub du syscall
4C 8B D1 mov r10, rcx B8 39 00 00 00 mov eax, 0x39 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
Même forme à 10 arguments que NtDeviceIoControlFile ; le noyau partage même le code de dispatch (`IopXxxControlFile` avec flag `FsControl`). La différence est sémantique : un FSCTL est destiné à un *driver système de fichiers* (NTFS, ReFS, FAT, CDFS) ou à un redirecteur réseau (RDBSS, mrxsmb), et beaucoup de FSCTL exigent **SeManageVolumePrivilege** ou **SeRestorePrivilege**. SSN `0x39` stable de Win7 à Win11 24H2. Le catalogue de codes FSCTL est large (≈ 250) ; le sous-ensemble offensif est petit mais à fort impact : set/get de reparse point, marquage sparse, lecture du journal USN et récupération de bitmap de volume pour dump NTFS brut.
Usage courant par les malwares
Trois familles offensives. (1) **Abus reparse point / jonction (T1564.004 + T1574.x)** : `FSCTL_SET_REPARSE_POINT` écrit un tag de reparse NTFS sur un répertoire, le transformant en jonction ou point de montage. Exploits de contournement UAC et d'EoP s'en servent pour qu'un dossier inscriptible pointe vers un dossier SYSTEM (RedirectionGuard a atténué ça côté Win11), et les ransomwares l'utilisent pour rediriger les chemins de logs vers des volumes effacés. (2) **Découverte d'ADS (T1564.004)** : `FSCTL_QUERY_FILE_LAYOUT` et `FSCTL_GET_NTFS_FILE_RECORD` exposent chaque flux $DATA d'un fichier, permettant aux droppers de stocker payloads (scripts PowerShell, XML de tâches planifiées, EXE secondaires) dans des flux que `dir` ne montre pas par défaut. (3) **Extraction NTFS brute** : `FSCTL_GET_VOLUME_BITMAP` + `FSCTL_GET_RETRIEVAL_POINTERS` (runs de clusters du `$MFT`) est la recette de `RawCopy`, `EXTRACT-DiT` et la plupart des dumpers de NTDS.dit — ils lisent les runs directement depuis `\\.\PhysicalDriveN`, contournant totalement l'open verrouillé du fichier.
Opportunités de détection
L'ETW Microsoft-Windows-Ntfs (`{3FF37A1C-A68D-4D6E-8C9B-F79E8B16C482}`) et les fournisseurs du sous-système Storage émettent des évènements pour les sets de reparse point et le trafic FSCTL. Sysmon Event ID 11 se déclenche côté création de fichier lors d'une plantation de reparse. Les callbacks minifilter pre sur `IRP_MJ_FILE_SYSTEM_CONTROL` (FSCTL est son mineur) voient chaque code avec son input buffer — c'est là que la plupart des EDR s'enregistrent. Chasses : `FSCTL_SET_REPARSE_POINT` depuis des processus non-installeurs, `FSCTL_GET_VOLUME_BITMAP` depuis des binaires user-mode autres que `chkdsk`/`defrag`/AV, et tout processus ouvrant `\\.\PhysicalDriveN` puis `FSCTL_GET_RETRIEVAL_POINTERS` en séquence. PowerShell `Get-Item -Force` + énumération `:Zone.Identifier` expose les plantations ADS évidentes ; `Sysinternals streams.exe` est l'outil manuel. Les protections de liens symboliques Microsoft (`fsutil behavior set SymlinkEvaluation`) atténuent certaines classes.
Exemples de syscalls directs
cPlant a junction with FSCTL_SET_REPARSE_POINT
// REPARSE_DATA_BUFFER for IO_REPARSE_TAG_MOUNT_POINT, redirecting
// C:\Users\victim\Downloads\evil -> \??\C:\Windows\System32.
#define IO_REPARSE_TAG_MOUNT_POINT 0xA0000003
#define FSCTL_SET_REPARSE_POINT 0x900A4
typedef struct _REPARSE_MOUNT_POINT {
ULONG ReparseTag; // 0xA0000003
USHORT ReparseDataLength;
USHORT Reserved;
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
WCHAR PathBuffer[1];
} REPARSE_MOUNT_POINT, *PREPARSE_MOUNT_POINT;
// hDir = handle to empty directory opened with GENERIC_WRITE | DELETE
IO_STATUS_BLOCK iosb = {0};
NTSTATUS s = NtFsControlFile(
hDir, NULL, NULL, NULL, &iosb,
FSCTL_SET_REPARSE_POINT,
rd, REPARSE_DATA_BUFFER_HEADER_SIZE + rd->ReparseDataLength,
NULL, 0);asmDirect stub (SSN 0x39) — 10 args
; Identical 4-instr stub form; ten parameters from the caller, six on stack.
NtFsControlFile PROC
mov r10, rcx
mov eax, 39h
syscall
ret
NtFsControlFile ENDPrustQuery reparse point on a directory
// Cargo: ntapi = "0.4", winapi = { version = "0.3", features = ["ntdef"] }
use ntapi::ntioapi::{NtFsControlFile, IO_STATUS_BLOCK};
use winapi::shared::ntdef::HANDLE;
const FSCTL_GET_REPARSE_POINT: u32 = 0x900A8;
pub unsafe fn read_reparse(h_dir: HANDLE) -> Option<Vec<u8>> {
let mut buf = vec![0u8; 0x4000];
let mut iosb: IO_STATUS_BLOCK = core::mem::zeroed();
let s = NtFsControlFile(
h_dir, core::ptr::null_mut(),
None, core::ptr::null_mut(),
&mut iosb,
FSCTL_GET_REPARSE_POINT,
core::ptr::null_mut(), 0,
buf.as_mut_ptr() as *mut _, buf.len() as u32,
);
if s == 0 {
let n = *iosb.u.Status_mut() as usize;
buf.truncate(n);
Some(buf)
} else { None }
}Mappings MITRE ATT&CK
Last verified: 2026-05-20