通过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。

下图为该文章的原文。

image.png

文章内容简单总结下,该漏洞问题出在ResetDC: ResetDC函数会对传入句柄的结构体进行Free。ResetDC不会校验传入句柄是否可用;且在ResetDC的函数中会调用User-Mode的回调。 使得我们可以在ResetDC调用的回调中再次对同一句柄调用ReseetDC函数进行Free来释放, 从而造成未预期的结果。

后来上个月月末就出了验证的POC, 本月月初更新为可以直接用的EXP,但是仅限特定的两个版本。于是本人就以EXP作为蓝本,对其他系统进行适配。

基本流程

对EXP进行编译,在指定版本上可以运行并成功获取System权限。

快速查看Exp, 发现Expolit主体思路如下:

  1. 使用CreateDCW创建了一个DCW
  2. 对用户态传入DC结构体中UMPD Callback的DrvEnablePDEV进行了Hook
  3. Hook替换的函数中,在第一次执行的情况下, 对传入的DC再次调用ResetDC, 并在之后使用CreatePalette来复用之前释放掉的空间。
  4. 对第0步创建的DCW进行ResetDC,从而触发Hook的函数。
  5. 使用其他方法leak出内核地址,结合此UAF漏洞进行利用。

没有什么问题,那么适配到其他系统,从而成功利用此UAF漏洞的关键在于如下三点:

    0. 漏洞触发点的位置
    1. 第一次分配空间的大小
    2. 分配指定大小的空间

调试关键点

对于上述UAF漏洞的三个关键点一条条确认,并对UAF后的利用方法进行了确认。

漏洞触发点的位置

对于该点,直接将填充的空间填充为0xcc,等待BSoD即可
image.png

查看寄存器
image.png

在根据堆栈,发现漏洞点在GreResetDCInterna1内。大概就是在这里。

image.png

调用函数和第一个参数方便可控。又因为是64bits,使用寄存器传参,所以就算只传了一个参数也不会爆炸。

计算一下偏移,没有遇到坑。
image

第一次分配空间的大小

直接在断点的位置,对可控的相关地址按经验使用!pool命令,发现命令根本不能用,直接报错。

换种思路,对pool分配的关键函数nt!ExAllocatePoolWithTag打了断点, 发现分配的大小为0xe10,pooltag为GDev.
image

又因为是win32k分配的DC的pool, 所以分配在了SessionPool上。这一点亦可以通过_EPROCESS->SESSION来确认

分配指定大小的空间

思路即是使用Palette的来在SessionPool中分配同样的大小,这样就很容易会复用到之前Free掉同样大小的空间了。

这个结构之前没怎么见过, 或者说实际上根本没用过,快速的查了波资料,发现和常用Bitmap很像,只不过该项支持更高的新版本。

因为CreatePalette的函数原型为:

image.png

传入参数的结构体为LOGPALETTE

image.png

其中数组palPalEntry的数量由palNumentries决定

所使用的CreatePalette函数最终在内核中根据该结构体会分配 _PALETTE64, pooltag为Gl?8

image.png

其中两者的最后一项完全相等, 只不过一个在内核空间一个在用户空间。

image.png

那么如果我们要分配指定大小的空间, 只需要对所需容减去内核中_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是关键函数,所以会被系统疯狂的调用,印象中跑一次出结果需要至少半个多小时。

多方面排查下来发现是这个:

image

他的大小并不是我们预期中的0xe10,亦不是exp中的0xe20,而是奇怪的0xd94。pooltag也很奇怪,是Gapl, 根本没见过。
以上两者与预期都不一样。

静下心来根据调用堆栈,对这个流程进行了分析,分析结果如下:

其中下图中数量(palNumentries)为0x364=(0xe20-0x90)/0x4,与exp中由0xe20的计算方式一样,所以看传入的参数是没问题的。

image

看完调用函数参数的同时打印出分配的地址,准备根据结构体查看内容,意外发现分配的地址更加奇怪。

image

随便打印个结尾为0x10的地址,发现内容中只有tagPALETTEENTRY数组的部分

image.png

随便打印个结尾为0x50的地址,发现与上述0x10结尾地址的内存差不多类似,但是多出了前0x30的内容

image.png

简单总结一下,奇怪的点在于:

0. 实际内存中无查询资料中的_PALETTE64结构体中的头部分
1. 该地址很可能是0x1000对齐的,实际返回指针并不是开头地址,是+0x10/0x50的地址。

那么这些点的原因是什么呢?

风水的大小问题

分配与缓解机制简介

  • Type Isolation
  • Segment Heap

Type Isolation

windows 10 1709(准确的说是16288)引入了TypeIsolation功能,该功能拆分了kernel-modeBITMAP等的内部的数据(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

image.png

这里是win7第一次分配的pool

image.png

可以看到,除了大小改变了外,没有什么太大的差别,而且对于最终利用来说,并没有什么影响。

分配指定大小的空间

这里是rs2使用PALETTE分配的pool

image.png

这里是win7使用PALETTE分配的pool,明显看到有多0x90的大小。

image.png

可以看到, Type Isolation机制发挥了作用。_PALETTE64无结构体中头的部分,因为Type Isolation的关系,导致头与数据分离了。(同时可以观察到pooltag也改变了)

在win7中,分配的大小与我们计算有PALETTE头的大小一致,既是0xe28=0x90+0xd98字节。

Segment Heap的影响

同时我们来查看实际上使用Segment Heap分配方式来分配VS块的时候,实际返回地址前面偏移的相关信息。这是设计使然。

返回的偏移为0x10地址,是因为之前的0x10内容为_POOL_HEADER

image.png

而返回的偏移为0x50的地址,是因为在_POOL_HEADER之前还有_HEAP_VS_SUBSEGMENT

image.png

触发位置与CFG防护

这里是win7触发位置
image

这里是rs2触发位置

image.png
这里原本是CFG的防护,但是看起来只是预留的,未开启的。
__guard_dispatch_icall_fptr暂时的实现只是空实现,为jmp rax
所以可以忽略。

Exp

在低版本中不能使用BigPool去泄露内核地址,因为该方法只能支持Win10 1607之上的版本。使用其他方式去泄露可控内存即可。
最终效果如图。
image.png

总结

在这次对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