这是 Windows kernel exploit 系列的第四部分,前一篇我们讲了任意内存覆盖漏洞,这一篇我们讲内核池溢出漏洞,这一篇篇幅虽然可能不会很多,但是需要很多的前置知识,也就是说,我们需要对Windows内存分配机制有一个深入的理解,我的建议是先看《0day安全:软件漏洞分析技术第二版》中的第五章堆溢出利用,里面很详细的讲解了堆的一些机制,但是主要讨论的是 Windows 2000~Windows XP SP1 平台的堆管理策略,看完了之后,类比堆溢出利用你可以看 Tarjei Mandt 写的 Kernel Pool Exploitation on Windows 7 ,因为我们的实验平台是 Windows 7 的内核池,所以我们需要对内核池深入的理解,虽然是英文文档,但是不要惧怕,毕竟我花了一周的时间才稍微读懂了其中的一些内容(这也是这一篇更新比较慢的原因),总之这个过程是漫长的,并不是一两天就能搞定的,话不多说,进入正题,看此文章之前你需要有以下准备:
传送门:
[+] Windows Kernel Exploit 内核漏洞学习(0)-环境安装
[+] Windows Kernel Exploit 内核漏洞学习(1)-UAF
[+] Windows Kernel Exploit 内核漏洞学习(2)-内核栈溢出
[+] Windows Kernel Exploit 内核漏洞学习(3)-任意内存覆盖漏洞
我们暂时先不看源码,先用IDA分析HEVD.sys
,我们找到TriggerPoolOverflow
函数,先静态分析一下函数在干什么,可以看到,函数首先用ExAllocatePoolWithTag 函数分配了一块非分页内存池,然后将一些信息打印出来,又验证缓冲区是否驻留在用户模式下,然后用memcpy 函数将UserBuffer
拷贝到KernelBuffer
,这和内核栈溢出有点似曾相识的感觉,同样的拷贝,同样的没有控制Size的大小,只是一个是栈溢出一个是池溢出
漏洞的原理很简单,就是没有控制好传入Size的大小,为了更清楚的了解漏洞原理,我们分析一下源码文件BufferOverflowNonPagedPool.c
,定位到关键点的位置,也就是说,安全的操作始终对分配的内存有严格的控制
漏洞的原理我们已经清楚了,但是关键点还是在利用上,内核池这个东西利用起来就不像栈一样那么简单了,我们还是一步一步的构造我们的exploit吧,首先根据上一篇的经验我们知道如何计算控制码从而调用TriggerPoolOverflow
函数,首先找到HackSysExtremeVulnerableDriver.h
中定义IOCTL
的地方,找到我们对应的函数
然后我们用python计算一下控制码
我们验证一下我们的代码,我们先给buf一个比较小的值
运行一下如我们所愿调用了TriggerPoolOverflow
函数,另外我们可以发现 Pool Size 有 0x1F8(504) 的大小(如果你细心的话其实在IDA中也能看到,另外你可以尝试着多传入几个字节的大小破坏下一块池头的内容,看看是否会蓝屏)
我们现在需要了解内核池分配的情况,所以我们需要在拷贝函数执行之前下断点观察,我们把 buf 设为 0x1F8 大小
我们可以用!pool address
命令查看address周围地址处的池信息
我们查看我们申请到池的末尾,0x41414141之后就是下一个池的池首,我们待会主要的目的就是修改下一个池首的内容,从而运行我们shellcode
从上面的池分布信息可以看到周围的池分布是很杂乱无章的,我们希望是能够控制我们内核池的分布,从源码中我们已经知道,我们的漏洞点是产生在非分页池中的,所以我们需要一个函数像malloc一样申请在我们的内核非分页池中,我们这里使用的是CreateEventA ,函数原型如下
该函数会生成一个Event 事件对象,它的大小为 0x40 ,因为在刚才的调试中我们知道我们的池大小为 0x1f8 + 8 = 0x200
,所以多次申请就刚好可以填满我们的池,如果把池铺满成我们的Event对象,我们再用CloseHandle 函数释放一些对象,我们就可以在Event中间留出一些我们可以操控的空间,我们构造如下代码测试
可以发现,我们已经把内核池铺成了我们希望的样子
接下来我们加上CloseHandle
函数就可以制造一些空洞了
重新运行结果如下,我们已经制造了许多空洞
首先我们复习一下x86 Kernel Pool
的池头结构_POOL_HEADER
,_POOL_HEADER
是用来管理pool thunk的,里面存放一些释放和分配所需要的信息
我们在调试中查看下一个池的一些结构
你可能会疑惑_OBJECT_HEADER
和_OBJECT_HEADER_QUOTA_INFO
是怎么分析出来的,这里你需要了解 Windows 7 的对象结构不然可能听不懂图片下面的那几行字,最好是在NT4源码(private\ntos\inc\ob.h)中搜索查看这些结构,这里我放一张图片吧
这里我简单说一下如何识别这两个结构的,根据下一块池的大小是 0x40 ,在_OBJECT_HEADER_QUOTA_INFO
结构中NonPagedPoolCharge
的偏移为0x004刚好为池的大小,所以这里确定为_OBJECT_HEADER_QUOTA_INFO
结构,又根据InfoMask
字段在_OBJECT_HEADER
中的偏移,结合我们确定的_OBJECT_HEADER_QUOTA_INFO
结构掩码为0x8可以确定这里就是我们的InfoMask
,这样推出_OBJECT_HEADER
的位置在+0x18处,其实我们需要修改的也就是_OBJECT_HEADER
中的TypeIndex
字段,这里是0xc,我们需要将它修改为0,我们看一下_OBJECT_HEADER
的结构
Windows 7 之后 _OBJECT_HEADER
及其之前的一些结构发生了变化,Windows 7之前0×008处的指向_OBJECT_TYPE
的指针已经没有了, 取而代之的是在 0x00c 处的类型索引值。但Windows7中添加了一个函数ObGetObjectType
,返回Object_type
对象指针,也就是说根据索引值在ObTypeIndexTable
数组中找到对应的ObjectType
我们查看一下ObTypeIndexTable
数组,根据TypeIndex
的大小我们可以确定偏移 0xc 处的 0x865f0598 即是我们 Event 对象的OBJECT_TYPE
,我们这里主要关注的是TypeInfo
中的CloseProcedure
字段
我们的最后目的是把CloseProcedure
字段覆盖为指向shellcode的指针,因为在最后会调用这些函数,把这里覆盖自然也就可以执行我们的shellcode,我们希望这里能够将Event这个结构放在我们能够操控的位置,在 Windows 7 中我们知道是可以在用户模式下控制0页内存的,所以我们希望这里能够指到0页内存,所以我们想把TypeIndex
从0xc修改为0x0,在 Windows 7 下ObTypeIndexTable
的前八个字节始终为0,所以可以在这里进行构造,需要注意的是,这里我们需要申请0页内存,我们传入的第二个参数不能是0,如果是0系统就会随机给我们分配一块内存,我们希望的是分配0页,如果传入1的话由于内存对齐就可以申请到0页内存,然后就可以放入我们shellcode的位置了
最后我们整合一下代码就可以提权了,总结一下步骤
最后提权效果如下,详细代码参考这里
这里放一些调试的小技巧,以判断每一步是否正确,在memcpy
处下断点,p单步运行可观察下一个池是否构造完成,dd 0x0
观察零页内存查看0x60处的指针是否指向shellcode,然后在该处下断点运行可以观察到是否运行了我们的shellcode,源码中的调试就是用__debugbreak()
下断点观察即可,调试很重要,如果你会调试的话写出exp也只是时间问题
参考资料:https://media.blackhat.com/bh-dc-11/Mandt/BlackHat_DC_2011_Mandt_kernelpool-wp.pdf https://www.cnblogs.com/kuangke/p/5818839.html https://www.cnblogs.com/flycat-2016/p/5449738.html https://rootkits.xyz/blog/2017/11/kernel-pool-overflow/
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2019-7-17 15:36
被Thunder J编辑
,原因: