【二进制漏洞- HEVD03-07】此文章归类为:二进制漏洞。
前言
本文将围绕 HEVD 中的两种漏洞类型展开,它们均与非分页内存池有关:释放后使用(UAF)与池溢出。在低版本系统上,这类漏洞的利用相对直接;对于高版本下的利用方式,我原本也有一些初步的设想,但仔细考虑后,觉得有必要说明一下——HEVD 毕竟是一个模拟环境,而在真实场景中,针对内核池的利用几乎总是围绕真实的内核对象展开,很少有机会让我们主动申请一块池内存来存放受控数据(不考虑 BYOVD 的情况)。既然这是学习环境,我们就不在复杂的版本对抗上过多纠缠了,先把基础手法梳理清楚。
UAF
漏洞成因
NTSTATUS
FreeUaFObjectNonPagedPool(
VOID
)
{
NTSTATUS Status = STATUS_UNSUCCESSFUL;
PAGED_CODE();
__try
{
if (g_UseAfterFreeObjectNonPagedPool)
{
ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
Status = STATUS_SUCCESS;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
}
return Status;
}
NTSTATUS
UseUaFObjectNonPagedPool(
VOID
)
{
NTSTATUS Status = STATUS_UNSUCCESSFUL;
PAGED_CODE();
__try
{
if (g_UseAfterFreeObjectNonPagedPool)
{
if (g_UseAfterFreeObjectNonPagedPool->Callback)
{
g_UseAfterFreeObjectNonPagedPool->Callback();
}
Status = STATUS_SUCCESS;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
全局池块在释放后并没有对指针进行清零,我们仍然可以在UseUaFObjectNonPagedPool中使用,所以,我们使用AllocateFakeObjectNonPagedPool把刚刚释放的池块重新申请出来,覆盖Callback指针为我们用户态的shellecode,完成利用。
高版本思考
- windows内核的非分页内存池是可以执行的,我们只需要把shellcode放在我们申请出来的内核池对象上,想办法泄露池块地址就行。
- 在我的上一篇文章,我发现
NtQuerySystemInformation函数在win11 24H2之前几乎可以泄露所有的内核对象地址,结合本题思考,我们在较高版本下,仍然是可以利用的。
非分页内存池溢出
漏洞成因
NTSTATUS
TriggerBufferOverflowNonPagedPool(
_In_ PVOID UserBuffer,
_In_ SIZE_T Size
)
{
PVOID KernelBuffer = NULL;
NTSTATUS Status = STATUS_SUCCESS;
PAGED_CODE();
__try
{
DbgPrint("[+] Allocating Pool chunk\n");
KernelBuffer = ExAllocatePoolWithTag(
NonPagedPool,
(SIZE_T)POOL_BUFFER_SIZE,
(ULONG)POOL_TAG
);
if (!KernelBuffer)
{
DbgPrint("[-] Unable to allocate Pool chunk\n");
Status = STATUS_NO_MEMORY;
return Status;
}
else
{
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
DbgPrint("[+] Pool Size: 0x%zX\n", (SIZE_T)POOL_BUFFER_SIZE);
DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);
}
ProbeForRead(UserBuffer, (SIZE_T)POOL_BUFFER_SIZE, (ULONG)__alignof(UCHAR));
DbgPrint("[+] Triggering Buffer Overflow in NonPagedPool\n");
RtlCopyMemory(KernelBuffer, UserBuffer, Size);
if (KernelBuffer)
{
ExFreePoolWithTag(KernelBuffer, (ULONG)POOL_TAG);
KernelBuffer = NULL;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
很明显,我们可以申请一个总大小为0x200的池块,然后进行池溢出。
这个低版本下的利用思路前辈们https://bbs.kanxue.com/thread-252665-1.htm#msg_header_h1_3已经讲的很详细了,我总结一下吧:
- 首先是对零页内存的巧妙利用,申请地址空间为零的内存,填充我们的shellcode。
- 对于TypeIndex为0的对象(原本不存在),我们就能劫持他的Close指针。
- 申请0x1000个Event对象,每个对象会在内核申请0x40的非分页内核池空间。
- 释放8个Event对象,触发合并,合并的池块大小为0x200
- DeviceIoControl实现池溢出,通过调试修改下面的池块(也就是Event)的
_OBJECT_HEADER的TypeIndex为0.
- CloseHandle关闭Event句柄,执行ObjectType的关闭例程。因为我们设置的TypeIndex是0,执行的是我们的Shellcode
高版本思考
看了一些文章,高版本的内核池结构将变得非常复杂,【译】Scoop the Windows 10 Pool - 敬渊's Blog倾向于转化为任意地址递减漏洞,从而获得SeDebugPrivilege权限。
在我的文章[原创] 学习笔记:CVE-2014-1767漏洞利用思路与我的理解-二进制漏洞-看雪安全社区|专业技术交流与安全研究论坛中也有类似的思路,总的来说,就是利用内核对象保存的指针,获得在内核任意读写的能力,完成利用.
代码
放附件了,Github
更多【二进制漏洞- HEVD03-07】相关视频教程:www.yxfzedu.com