通过 CVE-2021-40449 初探 Windows 内核 POOL 分配与缓解措施

语言: CN / TW / HK

因为工作需要,需要将此漏洞适配到其他版本的系统。

CVE-2021-40449 简介

Microsoft Windows 内核模块 win32k 中存在 UAF 漏洞,成功利用此漏洞可实现本 地权限提升。 影响自 W indows 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 主体思路如下:

  1. 使用 CreateDCW 创建了一个 DCW

  2. 对用户态传入 DC 结构体中 UMPD Callback 的 DrvEnablePDEV 进行了 Hook

  3. Hook 替换的函数中,在第一次执行的情况下, 对传入的 DC 再次调用 ResetDC, 并在之后使用 CreatePalette 来复用之前释放掉的空间。

  4. 对第 0 步创建的 DCW 进行 ResetDC,从而触发 Hook 的函数。

  5. 使用其他方法 leak 出内核地址,结合此 UAF 漏洞进行利用。

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

  1. 漏洞触发点的位置

  2. 第一次分配空间的大小

  3. 分配指定大小的空间

调试关键点

对于上述 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 的内容

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

  1. 实际内存中无查询资料中的_PALETTE64 结构体中的头部分

  2. 该地址很可能是 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

之上的版本。使用其他方式去泄露可控内存即可。

最终效果如图。

总结

至此工作全部完成。本以为本次适配是一个非常简单的工作,结果过程中发现对于 近几年出现的一些利用时需要的基础知识,比如 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