前置了解:
what AFD is and what is does?
AFD (Ancillary Function Driver)是Windows操作系统中的一个内核模式驱动程序,它也是套接字(Socket) 通信的核心模块之一。
它提供了操作系统与网络协议栈之间的接口,让应用程序能够进行网络通信。支持WinSock,而WinSock是在Windows中访问网络服务的编程接口。
afd.sys 实现了套接字的管理,套接字之间的数据传输,监控套接字上的事件,afd.sys 还负责报告和处理网络通信错误。其实afd.sys功能基本上都是围绕网络套接字。它是网络上程序之间通信通道的端点。而套接字允许程序通过网络连接发送和接收数据。
通过这张图也可以更好的理解一下,而也让理解到winsock是user model下,afd.sys位于Kernel Mode,所以这种一般就需要一个桥梁将其联系来才能到达。
漏洞原理:
该漏洞存在于AFD驱动程序处理用户模式输入/输出(I/O)操作的方式中。
具体来说,该漏洞允许攻击者向AFD驱动程序发送恶意输入/输出控制(IOCTL)请求,这可能导致以提升的权限执行任意代码。
这里从两个方面来看此漏洞,首先是通过补丁对比,其次再去通过公开的利用代码来进行分析和学习最后的利用过程。
从Winbindex拿到打补丁和未打补丁版本
Windows 11 22H2 KB5017389 (+6) x64 10.0.22621.608(未打补丁)
Windows 11 22H2 KB5022303 (+2) x64 10.0.22621.1105(已打补丁)
可以看见基本没有大改,基本都是细小的差距。基本就可以确定不是特别大的逻辑问题需要重写模块。而且看上去基本就一个函数需要去看一下AfdNotifyRemoveIoCompletion
其实从汇编代码这里就蛮明显可以看见,补丁处明显是增加了一处代码。来看一下f5之后的样子
原来是在之前加了一处if判断,再通过ProbeForWrite来进行检查a3。因为在原版的时候
**(_DWORD **)(a3 + 24) ``` 赋值给v20的时候,是没有任何检查机制。 (ProbeForWrite 的作用是检查用户模式缓冲区是否实际驻留在地址空间的用户模式部分、是否可写以及是否正确对齐。其参数含义如下 ```cpp void ProbeForWrite( [in, out] volatile VOID *Address, //指定用户模式缓冲区的开头 [in] SIZE_T Length, //指定用户模式缓冲区的长 [in] ULONG Alignment //指定用户模式缓冲区开头的所需对齐方式 );)
现在通过函数的作用和diff的结果,确认了漏洞点是出现在AfdNotifyRemoveIoCompletion函数里,那么下一步就是要思考如何触发让我们的poc流程能触发到这个函数里边。
那么先来看一下此函数的交叉引用,来分析其调用序列
AfdNotifySock-->AfdNotifyRemoveIoCompletion
AfdNotifySock中调用AfdNotifyRemoveIoCompletion,而v7传入的就是后边a3的值。往上追踪代码发现
在到达函数之前很明显有几处条件判断,首先inputbufferlength要等于0x30,要不就会跳到分支LABEL_45就不会走到AfdNotifyRemoveIoCompletion函数那里。还需要Outputbuffer==0
继续往下边的流程走的话,会发现还经过了ObReferenceObjectByHandle函数,通过gpt的解释理解一下
所以在这里,我们要用NtCreateIoCompletion函数来创建有效的IO完成对象的句柄。而v11这块就是一个句柄HandleIoCompletion,再来让v10是个有效值来过下边的条件判断,这一处再后边的exp代码中也有体现。
while ( v13 < Inputbuffer->dwCounter) ) //这里要满足这个条件让其进入循环,将其counter 设为1 { if ( pre_mode ) { v24 = 0i64; v25 = 0i64; v15 = v13; pData1 = Inputbuffer->pData1; if ( v12 ) { v17 = pData1 + 16 * v13; //这里是一个循环.上述 v13 < Counter就会循环,然后累加,这样需要的空间就变大了 v31 = v17; if ( (v17 & 3) != 0 ) ExRaiseDatatypeMisalignment(); if ( v17 + 16 > *v14 || v17 + 16 < v17 ) *(_BYTE *)*v14 = 0; *(_QWORD *)&v24 = *(unsigned int *)v17; *((_QWORD *)&v24 + 1) = *(unsigned int *)(v17 + 4); LOWORD(v25) = *(_WORD *)(v17 + 8); BYTE2(v25) = *(_BYTE *)(v17 + 10); } 最后Counter为1将pData1设置为一块申请出的空间,流程就会走到了AfdNotifyRemoveIoCompletion函数里
我们再来看 AfdNotifyRemoveIoCompletion 函数里还存在一个条件判断:
那就是当我们将dwLen 设置为1的时候,让IoRemoveIoCompletion返回0,if就会跳到pdata2 + 24 =v20那里,而pData2是一块申请出的内存。
这里最主要的就是来添加已完成的I/O操作,从而使IoRemoveIoCompletion正常返回。
使用NtSetIoCompletion ,此函数用于向 I/O 完成端口的完成队列中添加一个 I/O 完成包,这样就可以让IoRemoveIoCompletion返回0。从而最后走到漏洞触发点了。
总得来说就是dwLen 为 1,然后pData2指向一块可写的内存空间。
然后继续往上跟踪AfdNotifySock,看看是否还有调用
没有发现对该函数的直接调用,但是发现AfdNotifySock的地址在AfdIrpCallDispatch的函数指针表上方。
这张表包含了AFD驱动程序的调度例程,里面的函数都是AFD驱动程序的调度函数。调度例程用于通过调用DeviceIoControl来处理来自Win32应用程序的请求。每个函数的控制代码在AfdIoctlTable中找到。
从recon2015逆向AFD.sys的pdf文章中,可以知道afd其实是有两个调度表,还有一个是AfdImmediateCallDispatch
其实两个表里面的函数都是AFD驱动程序的调度函数,言归正传我们要找到怎么触发到AfdNotifySock函数,
首先需要通过AfdIoctlTable去获取ioctl_code
那么就需要计算AfdImmediateCallDispatch表的起点和指向AfdNotifySock的指针存储位置之间的距离
我们可以计算AfdIoctlTable的索引 在最后一位最后 查找发现AfdNotifySock函数的ioctl_code为12127h
知道了ioctl_code,就可以在用户层调用DeviceIoControl来访问此函数了。
通过外佬x86matthew发布的代码能让我们很方便的利用起来。他在文章中提到NtCreateFile和NtDeviceIoControlFile 这两个函数,是Winsock库用来与AFD驱动通信使用的。
将代码编译运行的时候,下断点到afd!AfdNotifySock就会发现能触发到了。比较主要的就是下边两个函数的构造和喂参。
代码通过直接调用AFD驱动程序来执行套接字操作,为TCP套接字创建句柄,向AFD驱动程序发出IOCTL请求。这样我们就能通过获得的ioctl_code让我们能够触发目标函数。
代码分析:
现在让我们来梳理一下,我们已经知道了怎么触发到AfdNotifySock函数,然后又知道了怎么设置参数让其通过AfdNotifySock函数里的条件判断走到我们的漏洞函数AfdNotifyRemoveIoCompletion里边,在其里边的最后一个条件判断,我们也知道如何进行绕过。然后此时我们也掌握了如何使用代码调用afd程序(也就是触发AfdNotifySock)那么我们就来看看公开的exp,来分析验证我们的结论。
代码里有两个函数NtCreateIoCompletion,NtSetIoCompletion 一个是用来创建IO完成端口对象并返回其句柄
一个是用来将完成包添加到 I/O 完成端口的完成队列中,正好对上了我们分析的两个函数(IoCompletionObjectType,ObReferenceObjectByHandle)要求绕过的条件。
再到下边引用x86matthew的代码NtCreateFile去触发afd驱动 ObjectFilePath.Buffer = (PWSTR)L"\\Device\\Afd\\Endpoint"; ObjectFilePath.Length = (USHORT)wcslen(ObjectFilePath.Buffer) * sizeof(wchar_t); ObjectFilePath.MaximumLength = ObjectFilePath.Length; ObjectAttributes.Length = sizeof(ObjectAttributes); ObjectAttributes.ObjectName = &ObjectFilePath; ObjectAttributes.Attributes = 0x40; ret = _NtCreateFile(&hSocket, MAXIMUM_ALLOWED, &ObjectAttributes, &IoStatusBlock, NULL, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, 1, 0, bExtendedAttributes, sizeof(bExtendedAttributes));
这里就是上述分析漏洞时得出的条件,hCompletion 为句柄,data1 data2 为两块内存空间,counter为1
len为1,以此来进行条件判断的绕过。
使用NtDeviceIoControlFile与AFD驱动程序通信,用触发到我们的函数
_NtDeviceIoControlFile(hSocket, hEvent, NULL, NULL, &IoStatusBlock, AFD_NOTIFYSOCK_IOCTL, &Data, 0x30, NULL, 0); //AFD_NOTIFYSOCK_IOCTL 0x12127
这里的AFD_NOTIFYSOCK_IOCTL 就是上方我们计算得到的ioctl_code为12127,从而完成漏洞的触发。
I/O Ring
然后剩下的就是用于在Windows 11 22H2 独有的后利用原语 I/O Ring,原作写的非常细节易懂,这里俺就不过多叙述,简单陈述一下过程让整个漏洞利用串联起来。主要是 对I/O Ring逆向非常细致,以此他挖掘出一套利用I/O Ring 的读写机制从而达成漏洞利用原语。 它是一个异步 I/O 机制,该机制是仿照 Linux 的io_uring
这个东西是一个提交队列,而它是一个环形的结构,正好对应下图的Submission Queue
图中就是Submission Queue Entry的结构,而下图是准备提交给内核的提交队列。
这三个图就基本上含括了基本的核心。而当上述的情况发生会有如下的情况
最后漏洞复现截图
通过IoRing 的任意地址读写 获取system token 和 本进程 token,从而进行替换操作即可。
PS: 从看雪师傅那里的文章发现有一些细节的问题(如怎么完成这个函数的需求条件)问chatgpt也是很好的选择,但如果网上资料较少的gpt就很容易胡言乱语导致我理解错的一个东西,,,还是要多方面参考好一点
然后因为此漏洞影响的型号有限,tmd 期间拿Windows 11 装的VS 2022生成项目,出现的错误真是一堆屎一样。避坑! 先后改了无数个项目配置,
先是死活CreateIoRing(IORING_VERSION_3, ioRingFlags, 0x10000, 0x20000, &hIoRing); 这句代码有红色浪线,最后把IntelliSense禁用
又是 error C2065: “IORING_VERSION_3”: 未声明的标识符。明明头文件都引入了,最后我把<ioringapi.h>这个头文件放第一行竟然就好了。
error no target architecture winnt.h 然后又爆这个错误,查一下最后为为预处理器定义添加宏,最后竟然又回去了!!!神经病啊我曹。然后折腾了半小时突然感觉是不是傻逼win11的问题,毕竟win11有一些奇怪bug不止一次了,然后拿win10重新了安装了一下vs2022编译一下就好了。。。
只想说一句,去你*的win11
参考:
x86matthew
i-o-ring
exp
kanxue
patch-tuesday-exploit-wednesday-pwning-windows-ancillary-function-driver-winsock