Skip to content

I/O Ring Exploitation

Modern kernel read/write primitive using the Windows I/O Ring API to achieve arbitrary memory access through corrupted buffer registration entries.

Description

I/O Ring was introduced in Windows 11 21H2 (build 22000) as a high-performance asynchronous I/O mechanism modeled after Linux's io_uring. The kernel maintains an _IORING_OBJECT for each I/O Ring instance, which includes a registered buffer table -- an array of _IOP_MC_BUFFER_ENTRY structures describing pre-registered I/O buffers by base address and length. Legitimate I/O Ring operations reference these buffers by index, and the kernel reads the address from the table without re-validating it against user-mode address space.

By corrupting one or more entries in the registered buffer table, an attacker redirects I/O Ring read and write operations to arbitrary kernel addresses. A corrupted IORING_OP_READ operation causes the kernel to write file data to the target kernel address (arbitrary write), while a corrupted IORING_OP_WRITE causes the kernel to read from the target kernel address into a file descriptor (arbitrary read). This provides stable, repeatable arbitrary kernel R/W through legitimate kernel code paths with no need for shellcode, ROP chains, or SMEP/SMAP bypasses.

I/O Ring exploitation has become the dominant R/W primitive on Windows 11 22H2 and later, replacing older techniques like WNF state data corruption and palette/bitmap abuse. Its advantages include simplicity (standard API calls after initial corruption), stability (kernel performs normal memcpy operations), and reusability (the corrupted ring can be used for multiple reads and writes within a single exploitation session).

I/O Ring Architecture

Core Components

_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)

Legitimate Operation Flow

  1. Create I/O Ring -- NtCreateIoRing allocates the _IORING_OBJECT in NonPagedPoolNx
  2. Register buffers -- IORING_OP_REGISTER_BUFFERS causes the kernel to allocate a _IOP_MC_BUFFER_ENTRY array and populate it with the addresses and lengths of user-supplied buffers
  3. Submit I/O -- user-mode process writes Submission Queue Entries (SQEs) to the shared submission ring, referencing registered buffer indices
  4. Kernel processes SQEs -- for each SQE, the kernel looks up _IOP_MC_BUFFER_ENTRY[index].Address and performs the I/O operation using that address
  5. Completion -- results are written to the completion ring, readable from user mode

Technique Details

Exploitation Steps

  1. Obtain initial corruption primitive -- via pool overflow, UAF, TOCTOU race, or other vulnerability in a driver
  2. Create I/O Ring instance -- call NtCreateIoRing and register several user-mode buffers to populate the buffer table
  3. Locate _IORING_OBJECT in kernel memory -- use NtQuerySystemInformation(SystemHandleInformation) to find the kernel address of the I/O Ring handle object, or use pool spray adjacency to place it next to the vulnerable allocation
  4. Corrupt _IOP_MC_BUFFER_ENTRY.Address -- use the initial write primitive to overwrite the Address field of one registered buffer entry to point to a target kernel address
  5. Arbitrary read -- submit an IORING_OP_WRITE SQE referencing the corrupted buffer index with a file descriptor; the kernel reads from the corrupted address (kernel memory) and writes to the file; read the file from user mode to obtain the kernel data
  6. Arbitrary write -- submit an IORING_OP_READ SQE referencing the corrupted buffer index with a file containing attacker-controlled data; the kernel reads from the file and writes to the corrupted address (kernel memory)
  7. Chain R/W operations -- use the read primitive to locate _EPROCESS, read the SYSTEM token, then use the write primitive for token swapping
  8. Clean teardown -- restore the corrupted buffer table entry to its original value before closing the I/O Ring handle to prevent BSOD during cleanup

Locating the IORING_OBJECT

Before corrupting the buffer table, the attacker must know its kernel address:

  • NtQuerySystemInformation: The SystemHandleInformation class returns the kernel object address for each handle. Create an I/O Ring, then scan handle entries for the IoRing object type to extract the _IORING_OBJECT kernel address.
  • Pool spray adjacency: Place the _IORING_OBJECT adjacent to the vulnerable allocation via pool spray, and corrupt it directly without needing its absolute address.
  • Offset calculation: The registered buffer table is at a fixed offset within _IORING_OBJECT. Once the base address is known, compute the buffer table address by adding the offset for the target Windows build.

Why I/O Ring Replaced Earlier Techniques

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

I/O Ring's key advantages: - Both read and write from a single corrupted entry - Uses existing kernel code paths -- no shellcode or function pointer corruption needed - HVCI compatible -- purely data-oriented exploitation - Long-lived registered buffers remain valid across multiple operations - Simple user-mode API for triggering reads and writes

Practical Considerations

  • Minimum version: Windows 11 21H2 (build 22000). Not available on Windows 10 or Server 2019.
  • Initial corruption required: I/O Ring is not a vulnerability itself -- it requires an initial write primitive to corrupt the buffer table. Common sources include pool overflows (pool spray chains), UAF reclaim, and TOCTOU races.
  • Buffer table in NonPagedPoolNx: The _IOP_MC_BUFFER_ENTRY array is allocated from NonPagedPoolNx. The initial corruption must be capable of reaching this pool region.
  • Offset stability: Structure offsets are stable within major Windows versions but should be verified per build. The RegBuffers pointer offset within _IORING_OBJECT and the Address field offset within _IOP_MC_BUFFER_ENTRY must be correct.
  • File descriptor requirement: I/O Ring operations require a valid file descriptor. Exploits typically open a temporary file or use a pipe for the I/O target.
  • Clean teardown is critical: Failing to restore the corrupted buffer table entry before closing the I/O Ring handle causes the kernel to attempt cleanup on an invalid address, resulting in BSOD.

Mitigations and Limitations

  • I/O Ring API restrictions: 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, reducing the attack surface. The NtCreateIoRing API may be restricted in future builds.
  • No integrity check on buffer entries: Currently, the kernel does not validate that registered buffer entries still point to legitimate user-mode addresses before performing I/O. Adding such validation would mitigate the technique but impact performance.
  • HVCI does not block: Because I/O Ring exploitation is purely data-oriented (no code pointer corruption), HVCI provides no protection against this technique.
  • KASLR: The attacker needs to know the _IORING_OBJECT kernel address, which requires an info leak or handle table query. KASLR does not protect pool addresses, but NtQuerySystemInformation restrictions in newer builds may limit address disclosure.
  • kCFI/XFG: Control flow integrity protections do not protect data structures like the buffer table, but they make alternative exploitation paths harder if I/O Ring is unavailable.
  • Pool hardening: General pool corruption mitigations (segment heap isolation, pool cookies) make the initial corruption step harder but do not protect the I/O Ring structures once corruption is achieved.
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