Windows I/O Ring 技术演进:一年来的关键变化与更新

本文详细解析了Windows I/O Ring在一年内的技术演进,包括新增的写入和刷新操作、用户完成事件机制、Drain Preceding Operations标志、WoW64兼容性修复,以及内部数据结构的公开和版本更新。

Windows I/O Ring 一年来的变化:技术深度解析

自I/O Ring首次引入Windows以来已过一年。初始版本发布于Windows 21H2,本文旨在跟进其显著变化和更新,详细记录并解释这些改进。

新增支持的操作

最明显的变化是新增了两个操作:写入(write)和刷新(flush)。这些操作允许使用I/O Ring执行写入和刷新功能,处理方式与自第一版即支持的读取操作类似,并转发至相应的I/O函数。KernelBase.dll中添加了新的包装函数来排队这些操作的请求:BuildIoRingWriteFileBuildIoRingFlushFile,其定义可在ioringapi.h头文件(预览SDK中可用)中找到:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
STDAPI
BuildIoRingWriteFile (
    _In_ HIORING ioRing,
    IORING_HANDLE_REF fileRef,
    IORING_BUFFER_REF bufferRef,
    UINT32 numberOfBytesToWrite,
    UINT64 fileOffset,
    FILE_WRITE_FLAGS writeFlags,
    UINT_PTR userData,
    IORING_SQE_FLAGS sqeFlags
);

STDAPI
BuildIoRingFlushFile (
    _In_ HIORING ioRing,
    IORING_HANDLE_REF fileRef,
    FILE_FLUSH_MODE flushMode,
    UINT_PTR userData,
    IORING_SQE_FLAGS sqeFlags
);

类似于BuildIoRingReadFile,这两个函数构建提交队列条目(SQE)并添加至提交队列。新操作需要不同的标志和选项,如flushMode或writeFlags。为此,NT_IORING_SQE结构现在包含一个联合体,根据请求的OpCode解释输入数据。

支持写入操作的一个小内核更改可见于IopIoRingReferenceFileObject,包括新参数和额外调用ObReferenceFileObjectForWrite。不同函数中的缓冲区探测也根据操作类型发生了变化。

用户完成事件

另一个有趣的变化是能够注册用户事件以通知每个新完成的操作。与I/O Ring的CompletionEvent(仅在所有操作完成时发出信号)不同,新的可选用户事件会在每个新操作完成时发出信号,允许应用程序在处理完成队列时即时处理结果。

为支持此功能,创建了新的系统调用NtSetInformationIoRing

1
2
3
4
5
6
7
NTSTATUS
NtSetInformationIoRing (
    HANDLE IoRingHandle,
    ULONG IoRingInformationClass,
    ULONG InformationLength,
    PVOID Information
);

该函数接收IoRing对象句柄、信息类、长度和数据。目前仅一个信息类有效:1(暂称IoRingRegisterUserCompletionEventClass)。函数使用全局数组IopIoRingSetOperationLength检索每个信息类的预期长度。

输入检查后,函数引用I/O Ring并调用IopIoRingUpdateCompletionUserEvent设置完成用户事件。该函数主要包含同步代码,确保仅一个线程可编辑CompletionUserEvent字段。

IopCompleteIoRingEntry中,CompletionUserEvent的发出信号条件不同:每当操作完成并写入完成队列时,若未处理条目数为1,则发出信号用户事件。这允许应用程序通过专用线程等待事件并处理每个新完成条目,确保完成队列头尾一致。

KernelBase.dll中的SetIoRingCompletionEvent函数用于注册用户完成事件:

1
2
3
4
5
STDAPI
SetIoRingCompletionEvent (
    _In_ HIORING ioRing,
    _In_ HANDLE hEvent
);

示例应用代码展示了如何使用此API创建事件和线程处理完成操作。

Drain Preceding Operations

用户完成事件不是唯一的等待相关改进。查看NT_IORING_SQE_FLAGS枚举可见新标志NT_IORING_SQE_FLAG_DRAIN_PRECEDING_OPS

IopProcessIoRingEntry开始时检查此标志。若设置,调用IopIoRingSetupCompletionWait设置等待参数。该函数计算已提交但未完成的操作数,并设置CompletionWaitUntil。随后IopIoRingWaitForCompletionEvent等待CompletionEvent发出信号。

IopCompleteIoRingEntry中,若SignalCompletionEvent设置且完成事件数等于CompletionWaitUntil,则发出信号CompletionEvent。

此标志确保所有前置操作完成后再处理当前条目,适用于依赖先前I/O操作的场景。

等待机制也出现在提交I/O Ring时。NtSubmitIoRing的第三个参数WaitOperations用于请求等待的操作数。若不为0,函数调用IopIoRingSetupCompletionWait进行健全性检查,随后等待完成事件。

应用通常设置WaitOperations为0或提交操作总数,但也可选择等待部分操作。

漏洞分析

比较不同构建的代码是发现已修复漏洞的有趣方式。此处关注一个功能性问题:阻止WoW64进程使用部分I/O Ring功能。

IopIoRingDispatchRegisterBuffersIopIoRingDispatchRegisterFiles中,新构建添加了检查WoW64进程的代码。问题源于PVOID类型的大小差异:64位系统为8字节,32位系统为4字节。WoW64进程使用32位结构,导致内核直接读取未转换的缓冲区数组,造成偏移错误。

新构建中,内核识别WoW64进程的结构差异,正确解释条目大小(8字节而非0x10),读取前4字节为地址,后4字节为长度。同样修复适用于预注册文件句柄。

其他变化

  • 成功创建I/O Ring对象会生成ETW事件,包含初始化信息。
  • IoringObject->CompletionEvent从NotificationEvent升级为SynchronizationEvent。
  • 当前I/O Ring版本为3,新创建环应使用此版本。
  • KernelBase.dll导出新函数IsIoRingOpSupported,检查操作是否支持。

数据结构

Windows 11 22H2(build 22577)中,几乎所有内部I/O Ring结构在公共符号中可用,无需逆向工程。结构自21H2有重大变化,建议从符号获取最新版本。

部分结构示例(来自build 22598):

1
2
3
4
typedef struct _NT_IORING_INFO { ... };
typedef struct _NT_IORING_SUBMISSION_QUEUE { ... };
typedef struct _NT_IORING_SQE { ... };
typedef struct _IORING_OBJECT { ... };

HIORING结构未在符号中,逆向工程版本如下:

1
typedef struct _HIORING { ... };

结论

I/O Ring虽刚发布数月,但已接收有趣增改,旨在吸引I/O密集型应用。当前版本为3,预计未来将支持新操作类型或扩展功能。尚未有桌面应用使用此机制,但随着Windows 11普及,值得关注其使用(或滥用)情况。作为Windows 11的有趣新增,它仍存一些漏洞,需持续关注。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计