通过CVE-2021-40449初探Windows内核POOL分配与缓解措施
因为工作需要,需要将此漏洞适配到其他版本的系统。
CVE-2021-40449简介
Microsoft Windows 内核模块win32k中存在UAF漏洞,成功利用此漏洞可实现本地权限提升。影响自Windows 7以来的所有版本, 包含:
- Microsoft:Windows:
- Microsoft:Windows 10
- Microsoft:Windows 7
- Microsoft:Windows 8.1
- Microsoft:Windows Server:
- Microsoft:Windows Server 2008
- Microsoft:Windows Server 2012
- Microsoft:Windows Server 2016
- Microsoft:Windows Server 2019
成因
上月月初,Kaspersky已经将细节公布出来了,该漏洞为已在野使用的漏洞,且内容较为详细。该CVE为一典型的UAF。
下图为该文章的原文。
文章内容简单总结下,该漏洞问题出在ResetDC: ResetDC函数会对传入句柄的结构体进行Free。ResetDC不会校验传入句柄是否可用;且在ResetDC的函数中会调用User-Mode的回调。 使得我们可以在ResetDC调用的回调中再次对同一句柄调用ReseetDC函数进行Free来释放, 从而造成未预期的结果。
后来上个月月末就出了验证的POC, 本月月初更新为可以直接用的EXP,但是仅限特定的两个版本。于是本人就以EXP作为蓝本,对其他系统进行适配。
基本流程
对EXP进行编译,在指定版本上可以运行并成功获取System权限。
快速查看Exp, 发现Expolit主体思路如下:
- 使用CreateDCW创建了一个DCW
- 对用户态传入DC结构体中UMPD Callback的
DrvEnablePDEV
进行了Hook- Hook替换的函数中,在第一次执行的情况下, 对传入的DC再次调用
ResetDC
, 并在之后使用CreatePalette
来复用之前释放掉的空间。- 对第0步创建的DCW进行
ResetDC
,从而触发Hook的函数。- 使用其他方法leak出内核地址,结合此UAF漏洞进行利用。
没有什么问题,那么适配到其他系统,从而成功利用此UAF漏洞的关键在于如下三点:
0. 漏洞触发点的位置
1. 第一次分配空间的大小
2. 分配指定大小的空间
调试关键点
对于上述UAF漏洞的三个关键点一条条确认,并对UAF后的利用方法进行了确认。
漏洞触发点的位置
对于该点,直接将填充的空间填充为0xcc,等待BSoD即可
查看寄存器
在根据堆栈,发现漏洞点在GreResetDCInterna1
内。大概就是在这里。
调用函数和第一个参数方便可控。又因为是64bits
,使用寄存器传参,所以就算只传了一个参数也不会爆炸。
计算一下偏移,没有遇到坑。
第一次分配空间的大小
直接在断点的位置,对可控的相关地址按经验使用!pool
命令,发现命令根本不能用,直接报错。
换种思路,对pool分配的关键函数nt!ExAllocatePoolWithTag
打了断点, 发现分配的大小为0xe10
,pooltag为GDev
.
又因为是win32k分配的DC的pool, 所以分配在了SessionPool
上。这一点亦可以通过_EPROCESS->SESSION
来确认
分配指定大小的空间
思路即是使用Palette
的来在SessionPool
中分配同样的大小,这样就很容易会复用到之前Free掉同样大小的空间了。
这个结构之前没怎么见过, 或者说实际上根本没用过,快速的查了波资料,发现和常用Bitmap
很像,只不过该项支持更高的新版本。
因为CreatePalette的函数原型为:
传入参数的结构体为LOGPALETTE
其中数组palPalEntry的数量由palNumentries决定
所使用的CreatePalette函数最终在内核中根据该结构体会分配 _PALETTE64
, pooltag为Gl?8
其中两者的最后一项完全相等, 只不过一个在内核空间一个在用户空间。
那么如果我们要分配指定大小的空间, 只需要对所需容减去内核中_PALETTE64
头的大小, 算出所需palPalEntry
的大小, 根据其数组成员的大小,进而算出palNumentries
的值,既可以在SessionPool
上任意分配指定大小(准确的说大小需要是4的倍数)的内核空间了。
以需要大小0xe10为例,我们只需要进行如下计算即可:
palNumentries=(0xe10-0x90)/0x4=0x360
纯理论完了后,对照着exp去看,发现计算方法一致,没什么问题。
其他
根据漏洞触发点,调用函数和第一个参数可控,配合内核地址泄露的手法绕过SMEP,将两值赋值为nt!RtlSetAllBits,与指向Token->Privileges
的RTL_BITMAP结构体的内核指针。
该函数可以根据后面结构体使其指向的内存bit都被置为1,因此即可成功利用提升权限。
大小问题
如上文所述,适配其他系统直接改个偏移,最多改个UAF需要的第一次分配的Pool大小就行了,岂不是有手就行。
然而,当我进行EXP细节验证的时候,出了意料之外的情况。
这个意外情况是:调试中发现第一次分配的时候分配了0xe10的大小, 但是Exploit后面分配的时候大小却是0xe20。
要么是Expolit错了, 要么是我查的资料错了。直觉告诉我是前者,但是Exp又能用,这是为什么呢?
所以索性就再调了下,看看内核中利用Palette分配的大小到底是多大。没想到这一看,问题就更大了。
使用0xe20/0xe10这两个大小来对分配的pool进行过滤:没有结果。
使用Gl?8这个pooltag来对分配的pool进行过滤:没有结果。
思考后决定将堆栈打印出来,查看情况。他总不可能不分配吧?
因为nt!ExAllocatePoolWithTag
是关键函数,所以会被系统疯狂的调用,印象中跑一次出结果需要至少半个多小时。
多方面排查下来发现是这个:
他的大小并不是我们预期中的0xe10,亦不是exp中的0xe20,而是奇怪的0xd94。pooltag也很奇怪,是Gapl
, 根本没见过。
以上两者与预期都不一样。
静下心来根据调用堆栈,对这个流程进行了分析,分析结果如下:
其中下图中数量(palNumentries)为0x364=(0xe20-0x90)/0x4,与exp中由0xe20的计算方式一样,所以看传入的参数是没问题的。
看完调用函数参数的同时打印出分配的地址,准备根据结构体查看内容,意外发现分配的地址更加奇怪。
随便打印个结尾为0x10的地址,发现内容中只有tagPALETTEENTRY数组的部分
随便打印个结尾为0x50的地址,发现与上述0x10结尾地址的内存差不多类似,但是多出了前0x30的内容
简单总结一下,奇怪的点在于:
0. 实际内存中无查询资料中的_PALETTE64结构体中的头部分
1. 该地址很可能是0x1000对齐的,实际返回指针并不是开头地址,是+0x10/0x50的地址。
那么这些点的原因是什么呢?
风水的大小问题
分配与缓解机制简介
- Type Isolation
- Segment Heap
Type Isolation
windows 10 1709(准确的说是16288)引入了TypeIsolation功能,该功能拆分了kernel-mode
中BITMAP
等的内部的数据(GDI Objects等)组织方式, 该技术后来也套用到了PALETTE
中。
其将GDI Objects的头部与后面的数据块进行了分离,并由双向链表将GDI Objects的头部统一管理。
该机制缓解了类似以下的攻击方法:修改结构体内的成员,使得可控的可变结构体大小任意变化,从而进一步利用。
按照微软官方的说法:该项技术实际上并不能阻止UAFs,它只是使它们很难被利用,只是一个缓解措施。
Segment Heap
Segment Heap最初是先用到R3上的Windows Store中的APPX应用,后面拓展到了一些关键进程与应用程序(如EDGE)。直到Windows 10 rs5引入了内核Segment Heap,这才将此技术应用到了R0上。
Segment Heap出现之前,Windows有且仅有一种Heap类型,统称为NT Heap。
Segment Heap与NT Heap完全不同,简单来说是引入了类似heap的分配机制,大部分结构体完全改变了。了解新的Segment Heap的结构,对之后版本的漏洞分析来说是及有意义的。
Segment Heap是根据分配内存所需大小,分为LFH,VS与BigPool三种类型,从而使用不同的分配机制来分配内存。
另外这也是windbg中!pool
拓展不能使用的原因,因为该拓展只支持传统的Pool。
分配与缓解机制的影响
对于前面所述的第一点,_PALETTE64
无结构体中头的部分,因为Type Isolation
的关系,导致头与数据分离了。
对于前面所述的第二点,实际上是因为内核中pool的分配机制变了,变成了所谓的Segment Heap
了。分配的地址均为base+0x10或base+0x50为该项的特性。
又因为我们所需空间的大小为0xe10, 实际分配的大小为0xd94,其大小在使用VS(Variable Size)类型的分配器的范围内.
而Backend在_HEAP_PAGE_SEGMENT
后的对齐大小为0x1000, 所以我们分配的地址亦是0x1000对齐的。
下面放一些具体的过程来分析一下。
Type Isolation的影响
我们以win7和win10 rs2(也叫15063或1703)来对比, 来看这些缓解措施给我们漏洞的利用造成了什么不同点:
第一次分配
这里是rs2第一次分配的pool
这里是win7第一次分配的pool
可以看到,除了大小改变了外,没有什么太大的差别,而且对于最终利用来说,并没有什么影响。
分配指定大小的空间
这里是rs2使用PALETTE
分配的pool
这里是win7使用PALETTE
分配的pool,明显看到有多0x90的大小。
可以看到, Type Isolation
机制发挥了作用。_PALETTE64
无结构体中头的部分,因为Type Isolation
的关系,导致头与数据分离了。(同时可以观察到pooltag也改变了)
在win7中,分配的大小与我们计算有PALETTE
头的大小一致,既是0xe28=0x90+0xd98字节。
Segment Heap的影响
同时我们来查看实际上使用Segment Heap分配方式来分配VS块的时候,实际返回地址前面偏移的相关信息。这是设计使然。
返回的偏移为0x10
地址,是因为之前的0x10
内容为_POOL_HEADER
而返回的偏移为0x50
的地址,是因为在_POOL_HEADER
之前还有_HEAP_VS_SUBSEGMENT
触发位置与CFG防护
这里是win7触发位置
这里是rs2触发位置
这里原本是CFG
的防护,但是看起来只是预留的,未开启的。__guard_dispatch_icall_fptr
暂时的实现只是空实现,为jmp rax
。
所以可以忽略。
Exp
在低版本中不能使用BigPool去泄露内核地址,因为该方法只能支持Win10 1607之上的版本。使用其他方式去泄露可控内存即可。
最终效果如图。
总结
在这次对Exp不同系统版本的适配中, 只能说是侥幸完成了。
本以为本次适配是一个非常简单的工作,结果过程中发现对于近几年出现的一些利用时需要的基础知识,比如Type Isolation和Segment Heap都不太了解(或者直接说后者之前都没听过), 于是花了大量的时间去查找相关的文档;尤其是后者查阅的过程很痛苦。
侥幸最后在可以接受的时间范围内搞定了, 也从中认识到了自己的不足, 像我这样对五年前的一些手法就了解一些,对近五年的发展没有详细跟进过,因为对新机制的不了解,导致了分析过程中的想当然。
之后应该紧跟版本更新,分析新技术,否则别说写了,甚至会连Exp都看不懂。
菜起来了。😒
以普遍理性而言,随着Windows的更新,内核中的缓解机制也愈发完善,能够及时修复或缓解已公开的通用利用方法。
好在新机制出现同时也会引进一些问题,比如Type Isolation缓解了类似可控的可变结构体大小任意变化的利用方式, 但是内核中Segment Heap的引入又使得前面Type Isolation机制的绕过方法更为简单:之前在Nt Heap中需要精心构造池风水,需要Alloc与Free多次,但是Segment Heap中由于分配方式的改变,对有些情况可以直接Spary就可以解决了,降低了某些情况下的利用的难度。
同时幸运的是,也有很多大佬愿意以论文或者议题的形式,分享自己所总结出来的最新的知识与技术。
参考资料
卡巴斯基的文章: MysterySnail attacks with Windows zero-day | Securelist
其中一个poc: https://github.com/KaLendsi/CVE-2021-40449-Exploit
与另一个poc: https://github.com/ly4k/CallbackHell 与其中的References
BlackHat中多个pdf
DEFCON中多个pdf
Windows Internals, 7th Edition
windows_kernel_heap_eng.pdf
leak方法: https://github.com/sam-b/windows_kernel_address_leaks