CVE-2021-40449 win32kfull!GreResetDCInternal uaf 漏洞
0x01 TLDR
现有的 EXP,实现思路大致相同,只不过在一些细节的地方以及编码风格上稍有不同:
- https://github.com/KaLendsi/CVE-2021-40449-Exploit
- https://github.com/hakivvi/CVE-2021-40449/blob/main/CVE-2021-40449/exploit.cpp
- https://github.com/ly4k/CallbackHell
可以参考下面的漏洞分析文章详细了解漏洞原理:
- https://securelist.com/mysterysnail-attacks-with-windows-zero-day/104509/
- https://bbs.pediy.com/thread-269930.htm
- https://mp.weixin.qq.com/s/AcFS0Yn9SDuYxFnzbBqhkQ
大致的漏洞成因:
在调用
gdi32full!ReserDC时经系统调用NtGdiResetDC后最终进入win32kfull!GreResetDCInternal内,此函数通过HDC句柄获取DCOBJ对象,在使用过程中未检查该对象是否已经释放,导致普通用户通过某种方式篡改对象数据后被该函数重用发生提权,典型的 UAF 漏洞。
EXP 的漏洞触发过程:
- 泄露一些关键的内核地址:
ntoskrnl、token 以及RtlSetAllBits函数地址 - 伪造一个包含 token 的
BitMap,通过SystemBigPoolInformation拿到内核地址 - Hook 打印机驱动的用户模式回调函数
DrvEnablePDEV - 调用
CreateDCA创建一个新的包含 hook 回调函数指针的打印机设备上下文对象(DCOBJ) - 调用
ResetDC释放上一步创建的dco,当内部在调用hdcOpenDcW获取新的DC时会调用用户回调进入 hook 函数 - hook 函数中再次调用
ResetDC将旧的dco释放掉 - 后利用
Palette这个 GDI 对象构造与原dco大小差不多的 pool 空间,分别将RtlSetAllBits和 伪造的bitmap地址放到指定的偏移处,多次进行 pool spray,有几率将已经释放掉的 dco pool 回收利用 - 最终 hook 函数调用完毕之后,返回
ResetDC,此时 dco pool 已经被破坏,dco对象中保存的win32kfull!UMPDDrvResetPDEV函数指针及其参数都被覆盖 - 触发调用
RtlSetAllBits将token+0x40位置的 16 字节重置为 1,此时进程的SE_DEBUG_PRIVILEGE权限被打开 - 将 shellcode 注入到高权限进程完成提权
在 exp 中一些细节的地方看的不是很明白,主要在 pool spray 部分:
- 构造的 Palette pool chunk 大小 0xe2c 是怎么确定的,随便改了改还能利用,感觉不是计算出来的固定值,在一个范围里即可
- 为什么 chunk size 要 -0x90,0x90 应该是 Palette 内部对象本身的大小,具体原因需要分析创建过程,先跳过
RtlSetAllBits和fakedBitmap放的位置,我看那几个 exp 里写的都不是一样的,跟系统版本有关系
问题比较大的是第 3 个,因为和系统版本有直接联系,前面的 exp 在 20H2 版本测试均失败,所以为了适配其他版本这里需要确定偏移是如何确定的。
0x02 解决问题
偏移位置和漏洞触发点脱不开关系,使用 ida 查看漏洞函数如下:
DCOBJ::DCOBJ((XDCOBJ *)_pdco, hdc);
dco = _pdco[0];
if ( !_pdco[0] )
{
EngSetLastError(6u);
v13 = _pdco[0];
LABEL_38:
v16 = v26;
goto LABEL_19;
}
v7 = *((_DWORD *)_pdco[0] + 9) & 0x800;
if ( v7 )
{
DC::bMakeInfoDC(_pdco[0], 0);
dco = _pdco[0];
}
pDev = *((_QWORD *)dco + 6); // pdo = dco + 0x30
v12 = *(_QWORD *)(pDev + 1712);
*(_QWORD *)(pDev + 1712) = 0i64;
v13 = _pdco[0];
v26 = v12;
if ( (*((_DWORD *)_pdco[0] + 9) & 0x100) != 0
|| *((_DWORD *)_pdco[0] + 8) == 1
|| (*(_DWORD *)(pDev + 40) & 0x80u) == 0 )
{
goto LABEL_38;
}
v14 = *((_DWORD *)_pdco[0] + 27);
v15 = *((_QWORD *)_pdco[0] + 62) != 0i64;
v16 = v15;
if ( XDCOBJ::bCleanDC((XDCOBJ *)_pdco, 0) )
{
if ( *(_DWORD *)(pDev + 8) == 1 )
{
v17 = hdcOpenDCW(&word_1C02C5498, a2, 0i64, 0i64, *(_QWORD *)(pDev + 2560), v26, a4, a5, 0);
v8 = v17;
if ( v17 )
{
*(_QWORD *)(pDev + 2560) = 0i64;
DCOBJ::DCOBJ((XDCOBJ *)_newPdco, v17);
newDco = _newPdco[0];
if ( newDco )
{
PFN_DrvResetPDEV rfn = *(void (__fastcall **)(_QWORD, _QWORD))(pDev + 0xAB8); // 从 pdo + 0xab8 位置获取到的函数地址
if ( rfn )
{
PDEVOBJ newPDEV = *((_QWORD *)newDco + 6);
rfn(*(_QWORD *)(pDev + 0x708), *(_QWORD *)(newPDEV + 0x708)); // trigger execute, 参数在 pdo + 0x708 位置
}
}
}
}
可以看到,函数本意是要执行 PFN_DrvResetPDEV 释放 dco->pdev,但是该函数地址保存在 pdev+0xab8 位置,参数有两个,参数 1 取自 pdev+0x708 位置,参数 2 取自新的 pdev,exp 中就是利用池喷将这里替换 RtlSetAllBits 和 fakedBitmap 的指针达到修改 token 的目的。
因为 pdev 取自 dco+0x30 位置,所以:
- rfn = dco+0xae8
- arg1= dco+0x738
以 https://github.com/KaLendsi/CVE-2021-40449-Exploit 这个 exp 为例,其中构造 Palette 的方式:
HPALETTE createPaletteofSize2(int size) {
int pal_cnt = (size - 0x90) / 4;
int palsize = sizeof(LOGPALETTE) + (pal_cnt - 1) * sizeof(PALETTEENTRY);
LOGPALETTE* lPalette = (LOGPALETTE*)malloc(palsize);
DWORD64* p = (DWORD64*)((DWORD64)lPalette + 4);
memset(lPalette, 0xff, palsize);
p[0x15B] = GadgetAddr;
p[0xE5] = Fake_RtlBitMapAddr;
lPalette->palNumEntries = pal_cnt;
lPalette->palVersion = 0x300;
return CreatePalette(lPalette);
}
p 是一个 DWORD64 类型指针,所以做一下偏移换算:
- rfn = p[0x15B] = p+0xad8
- arg1 = p[0xE5] = p+0x728
算出来的竟然与之前相对 dco 的偏移各自差 0x10,差出来的是 _POOL_HEADER,每个池开头都会有 0x10 字节的头,保存了当前池的元数据信息。
在 win32kfull!GreResetDCInternal 位置下断单步跟,调用 DCOBJ::DCOBJ 初始化后的 dco 指针放在 rbx 里:

之后将 dco->pDev 指针放在 rbx:

看一下 rbx 地址所在的 pool 信息:

池类型为 PagedPool,池标签为 GDev,池块大小为 0xe80,这些信息就保存在开头的 0x10 字节里,通过 dt _POOL_HEADER ffff8150849b5000 查看:

首地址 ffff8150849b5000 和 dco->pDev 地址差正好 0x30,说明这也是 dco 的地址。
直接看一下 0xab8 偏移处指向的函数:

在利用上面构造的 Palette 多次喷射之后,可以成功覆盖此处内存:

通过 0xff 填充的位置可以看出来 exp 中的偏移计算需要将实际的偏移 -0x10,即:
- (0xab8+0x30-0x10)/8 = 0x15b
- (0x708+0x30-0x10)/8 = 0xe5
故得到 exp 中放置 RtlSetAllBits 和 fakedBitmap 的偏移:
p[0x15B] = GadgetAddr;
p[0xE5] = Fake_RtlBitMapAddr;
此时,测试的系统是 win10 1809(build 17763.1577)。
0x03 20H2 无法利用
在 20H2 版本上,偏移和 1809 上看到的一样:

但是发现利用不成功,测试后发现在 20H2 上 DCOBJ 并没有分配 GDev 池:

导致 exp 中 pool spray 无效。