作者selph
前言
窥探Ring0漏洞世界:缓冲区溢出之池溢出
实验环境:
•虚拟机:Windows 7 x86
•物理机:Windows 10 x64
•软件:IDA,Windbg,VS2022
漏洞分析
本次实验内容是PoolOverflow,IRP分发函数通过跳转表进行跳转,两项之间的控制码相差4,所以本次实验使用的控制码是:0x22200f,漏洞触发代码:
int __stdcall TriggerBufferOverflowNonPagedPool(void *UserBuffer, unsigned int Size)
{
PVOID PoolWithTag; // ebx
_DbgPrintEx(0x4Du, 3u, “[+] Allocating Pool chunk\n”);
PoolWithTag = ExAllocatePoolWithTag(NonPagedPool, 0x1F8u, ‘kcaH’);// 申请非分页池内存
if ( PoolWithTag ) // 申请成功打印相关信息
{
_DbgPrintEx(0x4Du, 3u, “[+] Pool Tag: %s\n”, “‘kcaH’”);
_DbgPrintEx(0x4Du, 3u, “[+] Pool Type: %s\n”, “NonPagedPool”);
_DbgPrintEx(0x4Du, 3u, “[+] Pool Size: 0x%zX\n”, 0x1F8u);
_DbgPrintEx(0x4Du, 3u, “[+] Pool Chunk: 0x%p\n”, PoolWithTag);
ProbeForRead(UserBuffer, 0x1F8u, 1u); // 确保输入参数地址可读
_DbgPrintEx(0x4Du, 3u, “[+] UserBuffer: 0x%p\n”, UserBuffer);
_DbgPrintEx(0x4Du, 3u, “[+] UserBuffer Size: 0x%zX\n”, Size);
_DbgPrintEx(0x4Du, 3u, “[+] KernelBuffer: 0x%p\n”, PoolWithTag);
_DbgPrintEx(0x4Du, 3u, “[+] KernelBuffer Size: 0x%zX\n”, 0x1F8u);
_DbgPrintEx(0x4Du, 3u, “[+] Triggering Buffer Overflow in NonPagedPool\n”);
memcpy(PoolWithTag, UserBuffer, Size); // 复制输入参数到申请的内存里
_DbgPrintEx(0x4Du, 3u, “[+] Freeing Pool chunk\n”);
_DbgPrintEx(0x4Du, 3u, “[+] Pool Tag: %s\n”, “‘kcaH’”);
_DbgPrintEx(0x4Du, 3u, “[+] Pool Chunk: 0x%p\n”, PoolWithTag);
ExFreePoolWithTag(PoolWithTag, ‘kcaH’); // 释放内存
return 0;
}
else
{
_DbgPrintEx(0x4Du, 3u, “[-] Unable to allocate Pool chunk\n”);
return 0xC0000017;
}
}
乍看之下好像没啥问题,填充缓冲区,同时也限制大小了,仔细一看,emmm,申请内存的大小是0x1F8字节,复制的时候复制大小来自用户输入,是个经典的缓冲区溢出,不过缓冲区是位于非分页池内存
漏洞利用
池风水
内核池类似于用户层的堆,也是用来动态分配内存的。因为是动态分配,所以分配的内存位置就会不固定,在用户层有堆喷射这样的技术来辅助突破动态地址,这里则需要在内核里也找到一种方法来修改内存池,以便在内存区域精准调用shellcode
本例中的程序将用户缓冲区分配在了非分页内存池里,所以需要找到一种方法对非分页池中的地址进行操作以便辅助定位shellcode的执行
Windows提供了一种Event对象,存储在非分页池中,使用API-CreateEventA创建。
根据参考资料[2]中论文的介绍,我们可知:
内核池空闲池块保存在一个链表结构里,当进行申请该池的内存的时候,会从链表里找到合适大小的池块进行分配,如果找不到,则会寻找相近大小的池块进行切割然后再分配;
当空闲链表里有位置相邻的空闲池块,则会进行合并操作,合并成一个大的池块
通过大量申请Event对象,然后通过CloseHandle释放一部分Event对象留出合适的空间给用户缓冲区,那么用户缓冲区很可能就会出现在我们挖出的空缺位置上,并且同时紧紧挨着一个Event对象,也就是说,可以固定让用户缓冲区后面紧挨着一个Event对象
这里需要创建两个足够大的Event对象数组,一个用来消耗小尺寸空闲内存块,一个用来挖出空缺提供给用户缓冲区
在空出的空闲块中,我们将有漏洞的用户缓冲区插入,
图示如下:(参考资料[7])
利用原理&Event对象结构
这里的利用方式与之前的堆溢出覆盖堆块链表指针不同,这里通过伪造对象结构来通过堆溢出利用伪造的对象进行执行shellcode(一句话概括:控制缓冲区紧挨着一个Event对象,通过覆盖伪造一个OBJECT_TYPE头,覆盖指向OBJECT_TYPE_INITIALIZER中的一个过程的指针,通过执行该过程从而执行shellcode)具体分析往下看即可
先给一个刚好大小的正常输入看看池的情况:
#include
#include
int main()
{
ULONG UserBufferSize = 0x1f8;
char* UserBuffer = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, UserBufferSize);
RtlFillMemory(UserBuffer, UserBufferSize, 0x66);
HANDLE hDevice = ::CreateFileW(L"\\.\HacksysExtremeVulnerableDriver", GENERIC_ALL, FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
ULONG WriteRet = 0;
DeviceIoControl(hDevice, 0x222003 + 4 * 3, (LPVOID)UserB