NtFsControlFile
Sendet FSCTL-Codes an ein Dateisystem — eingesetzt für Reparse Points, ADS-Zugriff und Junction-Missbrauch.
Prototyp
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 );
Argumente
| Name | Type | Dir | Description |
|---|---|---|---|
| FileHandle | HANDLE | in | Handle auf Datei, Verzeichnis oder Volume. Benötigter Zugriff hängt vom FSCTL ab. |
| Event | HANDLE | in | Optionales Event für asynchron. NULL für synchron. |
| ApcRoutine | PIO_APC_ROUTINE | in | Optionale APC-Routine bei Fertigstellung. Üblicherweise NULL. |
| ApcContext | PVOID | in | Kontext für ApcRoutine. NULL bei Nichtgebrauch. |
| IoStatusBlock | PIO_STATUS_BLOCK | out | Erhält Status und in OutputBuffer zurückgegebene Bytes. |
| FsControlCode | ULONG | in | FSCTL_*-Code, z. B. FSCTL_SET_REPARSE_POINT (0x900A4), FSCTL_GET_REPARSE_POINT (0x900A8), FSCTL_SET_SPARSE. |
| InputBuffer | PVOID | in | FSCTL-spezifische Eingabe-Payload (z. B. REPARSE_DATA_BUFFER für Set-Reparse). |
| InputBufferLength | ULONG | in | Größe von InputBuffer in Bytes. |
| OutputBuffer | PVOID | out | Optionaler Ausgabe-Puffer für FSCTLs, die Daten zurückgeben. |
| OutputBufferLength | ULONG | in | Größe von OutputBuffer in Bytes. 0, wenn OutputBuffer NULL ist. |
Syscall-IDs pro Windows-Version
| Windows-Version | Syscall-ID | 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 |
Kernel-Modul
Verwandte APIs
Syscall-Stub
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
Undokumentierte Hinweise
Gleiche 10-Argument-Form wie NtDeviceIoControlFile; der Kernel teilt sich sogar den Dispatch-Code (`IopXxxControlFile` mit `FsControl`-Flag). Der Unterschied ist semantisch: ein FSCTL geht an einen *Dateisystem-Treiber* (NTFS, ReFS, FAT, CDFS) oder einen Netzwerk-Redirector (RDBSS, mrxsmb), und viele FSCTLs erfordern **SeManageVolumePrivilege** oder **SeRestorePrivilege**. SSN `0x39` ist von Win7 bis Win11 24H2 stabil. Der FSCTL-Code-Katalog ist groß (≈ 250); die offensiv relevante Teilmenge ist klein, aber wirkungsstark: Reparse-Point set/get, Sparse-Marker, USN-Journal-Lesen und Volume-Bitmap für Roh-NTFS-Dumps.
Häufige Malware-Nutzung
Drei offensive Familien. (1) **Reparse-Point/Junction-Missbrauch (T1564.004 + T1574.x)**: `FSCTL_SET_REPARSE_POINT` schreibt einen NTFS-Reparse-Tag auf ein Verzeichnis und macht es zu Junction oder Mount Point. UAC-Bypass und EoP-Exploits lassen damit einen schreibbaren Ordner auf einen SYSTEM-Ordner zeigen (RedirectionGuard entschärfte das nur unter Win11), Ransomware leitet Log-Pfade auf gelöschte Volumes um. (2) **ADS-Entdeckung (T1564.004)**: `FSCTL_QUERY_FILE_LAYOUT` und `FSCTL_GET_NTFS_FILE_RECORD` legen jeden $DATA-Stream einer Datei offen; Dropper verstecken so Payloads (PowerShell-Skripte, geplante Task-XMLs, sekundäre EXEs) in Streams, die `dir` standardmäßig nicht anzeigt. (3) **Roh-NTFS-Extraktion**: `FSCTL_GET_VOLUME_BITMAP` + `FSCTL_GET_RETRIEVAL_POINTERS` (Cluster-Runs der `$MFT`) ist das Rezept hinter `RawCopy`, `EXTRACT-DiT` und den meisten NTDS.dit-Dumpern — sie lesen Cluster-Runs direkt von `\\.\PhysicalDriveN` und umgehen die gesperrte Datei-Öffnung.
Erkennungsmöglichkeiten
Microsoft-Windows-Ntfs-ETW (`{3FF37A1C-A68D-4D6E-8C9B-F79E8B16C482}`) und Storage-Subsystem-Provider emittieren Events für Reparse-Point-Sets und FSCTL-Traffic. Sysmon Event ID 11 feuert beim File-Create eines Reparse-Plants. Minifilter-Pre-Callbacks auf `IRP_MJ_FILE_SYSTEM_CONTROL` (FSCTL ist dessen Minor) sehen jeden Code samt Input-Buffer — dort registrieren sich EDRs typischerweise. Hochwertige Hunts: `FSCTL_SET_REPARSE_POINT` von Nicht-Installer-Prozessen, `FSCTL_GET_VOLUME_BITMAP` von User-Mode-Binaries außer `chkdsk`/`defrag`/AV, und jeder Prozess, der `\\.\PhysicalDriveN` öffnet und danach sequenziell `FSCTL_GET_RETRIEVAL_POINTERS` aufruft. PowerShell `Get-Item -Force` mit `:Zone.Identifier`-Enumeration entlarvt offensichtliche ADS-Plants; `Sysinternals streams.exe` ist das manuelle Werkzeug. Microsofts Symlink-Schutz (`fsutil behavior set SymlinkEvaluation`) entschärft einige Reparse-Missbrauchs-Klassen.
Direkte Syscall-Beispiele
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 }
}MITRE ATT&CK-Mappings
Last verified: 2026-05-20