Skip to content

I/O Ring Exploitation

Before I/O Ring arrived in Windows 11 21H2, achieving stable kernel read/write after an initial corruption required creative abuse of whichever kernel objects happened to be nearby. WNF state data provided a read-back mechanism but was cumbersome for writes. Palette and bitmap objects offered read/write on legacy systems but were mitigated by type isolation. Named pipe data queue entries gave relative reads and writes, but only within the scope of adjacent pool memory. Each technique had tradeoffs in stability, reusability, and mitigation exposure.

I/O Ring changed the equation. Introduced as a high-performance asynchronous I/O mechanism modeled after Linux's io_uring, the Windows I/O Ring maintains a kernel-side array of registered buffer entries that the kernel trusts without re-validating against user-mode address space. Corrupting a single entry in this array redirects I/O Ring read and write operations to arbitrary kernel addresses. Both read and write flow through a single corrupted structure. The operations use standard kernel memcpy code paths, requiring no shellcode, no ROP chains, no SMEP or SMAP bypasses. A single corruption yields a reusable, HVCI-compatible, data-only kernel R/W primitive. On Windows 11 22H2 and later, I/O Ring has become the standard kernel R/W technique, largely replacing WNF state data corruption and palette/bitmap abuse.

Architecture

The kernel creates an _IORING_OBJECT in NonPagedPoolNx for each I/O Ring instance. This object manages the submission and completion queues (shared with user mode for low-latency operation) and, critically, a registered buffer table: an array of _IOP_MC_BUFFER_ENTRY structures that describe pre-registered I/O buffers by base address and length.

_IORING_OBJECT
  +0x000  Type / Size
  +0x010  SubmissionQueue     // mapped into user space
  +0x018  CompletionQueue     // mapped into user space
  +0x020  RegBuffersCount     // number of registered buffers
  +0x028  RegBuffers          // pointer to _IOP_MC_BUFFER_ENTRY array
  +0x030  RegFilesCount
  +0x038  RegFiles            // pointer to registered file objects

_IOP_MC_BUFFER_ENTRY
  +0x000  Address             // virtual address of registered buffer
  +0x008  Length              // buffer length
  +0x010  Type                // buffer type flags
  +0x018  Mdl                 // MDL for the buffer (if locked)

During normal operation, the user-mode process creates an I/O Ring with NtCreateIoRing, registers buffers with IORING_OP_REGISTER_BUFFERS, and then submits I/O operations through the submission queue. Each Submission Queue Entry (SQE) references a registered buffer by index. When the kernel processes an SQE, it looks up _IOP_MC_BUFFER_ENTRY[index].Address and performs the I/O operation using that address. The kernel does not re-validate that the address still points to user-mode memory. This design choice, made for performance, is the security-relevant property that enables exploitation.

The exploitation technique

The attacker needs an initial corruption primitive (pool overflow, UAF, TOCTOU race, or write-what-where) and the kernel address of the _IORING_OBJECT or its buffer table. The exploitation proceeds through a clear sequence.

First, the attacker creates an I/O Ring instance with NtCreateIoRing and registers several user-mode buffers to populate the buffer table. This creates the _IOP_MC_BUFFER_ENTRY array in NonPagedPoolNx, with entries pointing to legitimate user-mode addresses.

Second, the attacker locates the _IORING_OBJECT in kernel memory. The most reliable method is NtQuerySystemInformation(SystemHandleInformation), which returns the kernel object address for each handle in the system. The attacker creates an I/O Ring handle, scans the handle table for entries matching the IoRing object type, and extracts the kernel address. Alternatively, if the initial vulnerability involves a pool overflow, the attacker can position the _IORING_OBJECT adjacent to the vulnerable allocation through pool spray and corrupt it directly without needing its absolute address.

Third, the attacker uses the initial corruption primitive to overwrite the Address field of one _IOP_MC_BUFFER_ENTRY to point to a target kernel address. The Length field should be set to cover the desired read/write range.

Fourth, the attacker performs reads and writes through standard I/O Ring operations. For an arbitrary read, submitting an IORING_OP_WRITE SQE with the corrupted buffer index and a file descriptor causes the kernel to read from the corrupted address (the target kernel memory) and write to the file. The attacker reads the file from user mode to obtain the kernel data. For an arbitrary write, submitting an IORING_OP_READ SQE with a file containing attacker-controlled data causes the kernel to read from the file and write to the corrupted address.

The naming is counterintuitive: IORING_OP_READ reads from a file into the buffer (which is now a kernel address, so it writes to kernel memory), and IORING_OP_WRITE writes from the buffer to a file (which reads from the kernel address). Keeping this straight is important when building exploit logic.

Finally, the attacker chains read and write operations to achieve privilege escalation. The typical sequence is to read the _EPROCESS of the SYSTEM process to obtain its token, then write that token to the current process's _EPROCESS, performing a token swap. Before closing the I/O Ring handle, the corrupted buffer table entry must be restored to its original value. Failing to do so causes the kernel to attempt cleanup on an invalid address during handle closure, resulting in a BSOD.

Why I/O Ring replaced earlier techniques

The shift from WNF state data and palette/bitmap objects to I/O Ring was driven by several practical advantages that compound in real-world exploitation.

Aspect I/O Ring WNF State Data Palette/Bitmap (Legacy)
Windows version 11 21H2+ 10+ 7/8/10 (patched)
Pool type NonPagedPoolNx PagedPool Session Pool
R/W capability Full read + write Read-back only (typically) Read + write
HVCI compatible Yes (data-only) Yes (data-only) No (function ptr)
Stability High Moderate Low on modern
Reusability Multiple R/W per corruption Limited Limited

The decisive advantage is reusability. Once a buffer table entry is corrupted, the attacker can perform unlimited reads and writes by simply updating the target address in the entry and submitting new SQEs. WNF state data typically supports a single read-back per corruption, and palette/bitmap objects have similar limitations. I/O Ring supports an entire exploitation session, including EPROCESS traversal, token location, token swap, and verification, through a single corrupted entry.

HVCI compatibility is equally important. I/O Ring exploitation is purely data-oriented: no function pointers are corrupted, no code is executed from writable memory, no ROP chains are constructed. HVCI's code integrity enforcement has nothing to block. This makes I/O Ring the standard technique on Windows 11 systems where HVCI is enabled by default.

Practical considerations

I/O Ring requires Windows 11 21H2 (build 22000) or later. It is not available on Windows 10 or Server 2019. Exploits targeting older builds must fall back to WNF state data, named pipe relative R/W, or other techniques.

Structure offsets are stable within major Windows versions but shift between builds. The RegBuffers offset within _IORING_OBJECT and the Address field offset within _IOP_MC_BUFFER_ENTRY must be correct for the target build. Exploits include a lookup table mapping build numbers to offsets.

The initial corruption must be capable of reaching NonPagedPoolNx, since both the _IORING_OBJECT and its buffer table are allocated there. Vulnerabilities that only corrupt PagedPool cannot directly target I/O Ring structures without an intermediate cross-pool technique.

I/O Ring operations require a valid file descriptor. Exploits typically open a temporary file or use a pipe as the I/O target. The file's content becomes either the source for kernel writes or the sink for kernel reads.

Clean teardown is critical. The kernel validates registered buffer entries during _IORING_OBJECT destruction. If a corrupted entry points to an inaccessible kernel address when the handle is closed, the cleanup code faults and the system crashes. Robust exploits always restore the original buffer entry values before closing the I/O Ring handle.

Microsoft has progressively restricted I/O Ring functionality in newer Windows 11 builds. In 24H2 and later, certain I/O Ring operations are disabled or require additional validation, potentially reducing the attack surface. The NtCreateIoRing API may face further restrictions in future builds. However, as of early 2026, the core buffer registration and I/O submission mechanisms remain functional on supported builds.

Mitigations and Limitations

HVCI provides no protection against I/O Ring exploitation because the technique is purely data-oriented. No code pointers are modified, no executable memory regions are created. HVCI's value lies in making the initial corruption step harder (by preventing function pointer abuse as an initial primitive), but once data-only corruption reaches the buffer table, HVCI is irrelevant.

KASLR complicates the initial step of locating the _IORING_OBJECT, since its kernel address must be known before the buffer table can be corrupted. However, NtQuerySystemInformation(SystemHandleInformation) provides this address directly on most builds. Restrictions on this API in newer builds may force attackers to use alternative address disclosure techniques, such as pipe attribute relative reads or ACL/SD manipulation to re-enable restricted information classes.

General pool corruption mitigations (Segment Heap isolation, pool cookies) make the initial corruption step harder but do not protect I/O Ring structures once corruption reaches them. The kernel does not validate that registered buffer entries still point to user-mode addresses before performing I/O, and adding such validation would impose a performance cost on every I/O Ring operation.

kCFI/XFG protections do not affect I/O Ring exploitation since no control flow is redirected. These protections matter only if the initial corruption primitive involves function pointer abuse, in which case they constrain the path to I/O Ring corruption rather than the I/O Ring technique itself.

CVE Driver Description
CVE-2023-21768 afd.sys Missing ProbeForWrite leading to I/O Ring buffer table corruption
CVE-2024-38193 afd.sys UAF exploited via I/O Ring R/W for token swap
CVE-2024-30085 cldflt.sys Pool overflow chained with I/O Ring for kernel R/W
CVE-2024-30088 ntoskrnl.exe TOCTOU race with I/O Ring exploitation
CVE-2024-26229 csc.sys Arbitrary write chained with I/O Ring

See Also