NtReadFile
Reads bytes from a file, device, named pipe or mapped section into a user buffer — the kernel primitive behind ReadFile.
Prototype
NTSTATUS NtReadFile( 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 obtained from NtCreateFile/NtOpenFile with FILE_READ_DATA or GENERIC_READ access. |
| Event | HANDLE | in | Optional event handle signaled on async completion. NULL for synchronous I/O. |
| ApcRoutine | PIO_APC_ROUTINE | in | Optional user-mode APC routine queued on completion. NULL for synchronous calls. |
| ApcContext | PVOID | in | Context pointer passed to ApcRoutine. Usually NULL. |
| IoStatusBlock | PIO_STATUS_BLOCK | out | Receives final NTSTATUS and Information field set to bytes actually read. |
| Buffer | PVOID | out | User-mode buffer receiving the data. Must be at least Length bytes. |
| Length | ULONG | in | Number of bytes to read. Short reads are normal at EOF or for pipes. |
| ByteOffset | PLARGE_INTEGER | in | Optional file offset. NULL means use the current file pointer (synchronous file). |
| Key | PULONG | in | Optional lock key. Almost always NULL outside of byte-range locking. |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1507 | 0x6 | win10-1507 |
| Win10 1607 | 0x6 | win10-1607 |
| Win10 1703 | 0x6 | win10-1703 |
| Win10 1709 | 0x6 | win10-1709 |
| Win10 1803 | 0x6 | win10-1803 |
| Win10 1809 | 0x6 | win10-1809 |
| Win10 1903 | 0x6 | win10-1903 |
| Win10 1909 | 0x6 | win10-1909 |
| Win10 2004 | 0x6 | win10-2004 |
| Win10 20H2 | 0x6 | win10-20h2 |
| Win10 21H1 | 0x6 | win10-21h1 |
| Win10 21H2 | 0x6 | win10-21h2 |
| Win10 22H2 | 0x6 | win10-22h2 |
| Win11 21H2 | 0x6 | win11-21h2 |
| Win11 22H2 | 0x6 | win11-22h2 |
| Win11 23H2 | 0x6 | win11-23h2 |
| Win11 24H2 | 0x6 | win11-24h2 |
| Server 2016 | 0x6 | winserver-2016 |
| Server 2019 | 0x6 | winserver-2019 |
| Server 2022 | 0x6 | winserver-2022 |
| Server 2025 | 0x6 | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 06 00 00 00 mov eax, 0x6 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
NtReadFile carries the classic NT 9-argument I/O shape (Event/APC pair, IO_STATUS_BLOCK, byte offset, lock key) that's reused by NtWriteFile, NtDeviceIoControlFile and NtFsControlFile — once you can call one, you can call all four. SSN `0x6` has been pinned since Windows 7 because the call sits at the very base of the table. The most important caller-side detail is that for files opened *without* FILE_SYNCHRONOUS_IO_NONALERT, you must pass a valid ByteOffset or supply an Event handle and wait on it — Microsoft's user-mode `ReadFile` hides this by emulating synchronous behavior when neither FILE_FLAG_OVERLAPPED nor an OVERLAPPED structure is supplied.
Common malware usage
Used in three main offensive patterns. (1) **Token-theft / LSASS reading**: open a file handle to a section backed by lsass.exe memory or to a captured minidump on disk, then NtReadFile to pull credential material out without ever touching MiniDumpWriteDump or NtReadVirtualMemory. (2) **Config / clipboard exfil**: open `\Device\KsecDD`, `\??\C:\Users\<u>\AppData\Roaming\Mozilla\Firefox\Profiles\*\logins.json`, browser cookie SQLite files, Outlook PST/OST, then read in chunks. (3) **Reading reflective loaders' own backing file** to stage second-stage payloads from disk without the OS heuristics that flag NtMapViewOfSection of executable images. Combined with `NtCreateFile` using NT-namespace paths (`\??\GLOBALROOT\Device\...`), the read can sidestep minifilters that key on DOS paths.
Detection opportunities
NtReadFile is essentially never anomalous on its own — billions of calls per host per day. The signal lives in (a) *what file*, (b) *who opened it*, and (c) the IRP chain. Microsoft-Windows-Kernel-File ETW (`{EDD08927-9CC4-4E65-B970-C2560FB5C289}`) emits Read events with offset and length. EDR minifilter callbacks (IRP_MJ_READ pre/post) see every read regardless of user-mode hooking, so direct syscalls do not hide reads. High-value hunts: any non-MsMpEng/SearchProtocolHost process reading `*\Mozilla\*\logins.json`, `*\Chromium\User Data\*\Login Data`, `*\Microsoft\Credentials\*`, `*\Microsoft\Protect\*` (DPAPI masterkeys), or `\Device\HarddiskVolumeShadowCopy*` (VSC NTDS.dit extraction).
Direct syscall examples
cRead browser logins file synchronously
// Assumes hFile was opened with FILE_SYNCHRONOUS_IO_NONALERT and FILE_READ_DATA.
IO_STATUS_BLOCK iosb = {0};
BYTE buffer[0x4000];
LARGE_INTEGER off = {0}; // start of file
NTSTATUS s = NtReadFile(
hFile,
NULL, // Event
NULL, NULL, // APC routine + context
&iosb,
buffer,
sizeof(buffer),
&off,
NULL); // Key
if (NT_SUCCESS(s)) {
SIZE_T got = iosb.Information;
// got bytes copied into buffer
}asmDirect stub (SSN 0x6) — stack args 5..9
; NtReadFile has 9 args. RCX,RDX,R8,R9 hold the first four; the rest
; sit on the stack after 32-byte home space — caller is responsible.
NtReadFile PROC
mov r10, rcx
mov eax, 06h
syscall
ret
NtReadFile ENDPrustAsync read with event wait
// Cargo: ntapi = "0.4", winapi = { version = "0.3", features = ["synchapi", "ntdef"] }
use ntapi::ntioapi::{NtReadFile, IO_STATUS_BLOCK};
use winapi::um::synchapi::WaitForSingleObject;
use winapi::shared::ntdef::{HANDLE, LARGE_INTEGER};
pub unsafe fn read_async(h_file: HANDLE, h_event: HANDLE, buf: &mut [u8], off: i64) -> i32 {
let mut iosb: IO_STATUS_BLOCK = core::mem::zeroed();
let mut li: LARGE_INTEGER = core::mem::zeroed();
*li.QuadPart_mut() = off;
let s = NtReadFile(
h_file, h_event,
None, core::ptr::null_mut(),
&mut iosb,
buf.as_mut_ptr() as *mut _,
buf.len() as u32,
&mut li,
core::ptr::null_mut(),
);
if s == 0x103 /* STATUS_PENDING */ {
WaitForSingleObject(h_event, u32::MAX);
}
s
}MITRE ATT&CK mappings
Last verified: 2026-05-20