NtWriteFile
Writes data to an open file, pipe, or device — the kernel companion to NtCreateFile for dropping payloads.
Prototype
NTSTATUS NtWriteFile( HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, ULONG Length, PLARGE_INTEGER ByteOffset, PULONG Key );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| FileHandle | HANDLE | in | Handle opened by NtCreateFile/NtOpenFile with GENERIC_WRITE or FILE_WRITE_DATA. |
| Event | HANDLE | in | Optional event signaled when async write completes. NULL for synchronous handles. |
| ApcRoutine | PIO_APC_ROUTINE | in | Optional APC delivered on completion. Almost always NULL outside of overlapped I/O. |
| ApcContext | PVOID | in | Context passed to ApcRoutine. NULL if ApcRoutine is NULL. |
| IoStatusBlock | PIO_STATUS_BLOCK | out | Receives final NTSTATUS and Information field with bytes written. |
| Buffer | PVOID | in | Pointer to the source data to write. |
| Length | ULONG | in | Number of bytes to write from Buffer. |
| ByteOffset | PLARGE_INTEGER | in | File offset, or FILE_USE_FILE_POINTER_POSITION (-2) to use the current position. |
| Key | PULONG | in | Optional byte-range lock key. Almost always NULL. |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1507 | 0x8 | win10-1507 |
| Win10 1607 | 0x8 | win10-1607 |
| Win10 1703 | 0x8 | win10-1703 |
| Win10 1709 | 0x8 | win10-1709 |
| Win10 1803 | 0x8 | win10-1803 |
| Win10 1809 | 0x8 | win10-1809 |
| Win10 1903 | 0x8 | win10-1903 |
| Win10 1909 | 0x8 | win10-1909 |
| Win10 2004 | 0x8 | win10-2004 |
| Win10 20H2 | 0x8 | win10-20h2 |
| Win10 21H1 | 0x8 | win10-21h1 |
| Win10 21H2 | 0x8 | win10-21h2 |
| Win10 22H2 | 0x8 | win10-22h2 |
| Win11 21H2 | 0x8 | win11-21h2 |
| Win11 22H2 | 0x8 | win11-22h2 |
| Win11 23H2 | 0x8 | win11-23h2 |
| Win11 24H2 | 0x8 | win11-24h2 |
| Server 2016 | 0x8 | winserver-2016 |
| Server 2019 | 0x8 | winserver-2019 |
| Server 2022 | 0x8 | winserver-2022 |
| Server 2025 | 0x8 | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 08 00 00 00 mov eax, 0x8 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
SSN `0x8` — one of the lowest, and unchanged since Windows 7 — making NtWriteFile easy to call without any dynamic SSN resolution. Behind the API are 9 parameters, of which only FileHandle, IoStatusBlock, Buffer, Length and ByteOffset are routinely non-NULL. If the handle was opened with `FILE_SYNCHRONOUS_IO_NONALERT` (the typical case), the call blocks until the write completes and the IoStatusBlock is filled before return. Routes through IopXxxControlFile → driver IRP_MJ_WRITE, so every filesystem and storage filter driver in the device stack sees the write.
Common malware usage
Combined with NtCreateFile this is the entirety of the on-disk payload-staging chain: open with FILE_OVERWRITE_IF, NtWriteFile the encrypted next stage, close. Ransomware uses it during encryption (open original, NtReadFile contents, encrypt, NtWriteFile back over the same file or to a `.locked` companion). Wipers use it to zero out `\Device\PhysicalDriveN` or to overwrite registry hive files (HermeticWiper, CaddyWiper). Loaders writing to `%APPDATA%\Microsoft\Templates\Normal.dotm` or to a Startup folder shortcut close the persistence loop without ever touching kernel32!WriteFile.
Detection opportunities
Unlike NtCreateFile, NtWriteFile rarely produces dedicated telemetry on its own — file *creation* fires Sysmon 11 / Kernel-File `Create`, but per-write events are too noisy to log by default. EDRs sit in the minifilter layer and see the data being written, which is how content-based YARA-on-write and ransomware-canary engines work. Microsoft-Windows-Kernel-File event 12 (`Write`) is available but typically only enabled during incident-response captures because of volume. The asymmetry matters operationally: defenders detect ransomware by *what* is being written (high-entropy content, swapped extensions, mass renames), not by NtWriteFile invocation counts. Correlate with NtSetInformationFile/FileDispositionInformation to catch the rename or unlink that usually follows.
Direct syscall examples
cWrite payload to a freshly-created file
// After NtCreateFile gave us a synchronous, writable handle:
IO_STATUS_BLOCK iosb = {0};
LARGE_INTEGER offset = { .QuadPart = 0 };
NTSTATUS s = NtWriteFile(
hFile,
NULL, NULL, NULL, // no event, no APC
&iosb,
payload, payloadLen,
&offset, // write at start of file
NULL); // no lock key
if (NT_SUCCESS(s)) {
// iosb.Information == bytes written
}asmDirect stub (SSN 0x8)
NtWriteFile PROC
mov r10, rcx
mov eax, 8
syscall
ret
NtWriteFile ENDPrustAppend to a log device via ntapi
use ntapi::ntioapi::{NtWriteFile, IO_STATUS_BLOCK};
pub unsafe fn write_all(handle: *mut core::ffi::c_void, data: &[u8]) -> i32 {
let mut iosb: IO_STATUS_BLOCK = core::mem::zeroed();
// -2 == FILE_USE_FILE_POINTER_POSITION
let mut offset: i64 = -2;
NtWriteFile(
handle,
core::ptr::null_mut(),
None,
core::ptr::null_mut(),
&mut iosb,
data.as_ptr() as *mut _,
data.len() as u32,
&mut offset as *mut _ as *mut _,
core::ptr::null_mut(),
)
}MITRE ATT&CK mappings
- T1486Data Encrypted for Impact
- T1561.001Disk Content Wipe
- T1106Native API
Last verified: 2026-05-20