CVE-2021-1732 内核提权详细分析
对 Windows 内核一直挺感兴趣的,打算先复现CVE并熟悉各种模块......
在 CVE-2021-1732 中,真正的高危点并非单纯的未初始化内存,而是 win32k 通过 KeUserModeCallback 主动执行用户态回调函数这一设计。KernelCallback 机制使内核在关键对象构造和状态迁移阶段依赖用户态返回的数据,一旦回调入口或回调协议的完整性被破坏(如 KernelCallbackTable 可被劫持、返回数据语义校验不足),将导致内核执行流和对象状态被用户态间接控制。这类漏洞的危害在于其结构性信任失效,而非单一实现缺陷,所以 KernelCallback 路径本身是极具攻击价值的研究方向。
这类漏洞好似 WEB 中前端给后端传值,我抓包修改了值,后端无条件信任了传入的值,导致漏洞产生,所以在这种类似接收参数的接口中一定要做好校验,无论是内核还是WEB,都不要轻易相信任何传来的数据。
漏洞核心不只是“未初始化内存”本身,而是 win32k 的 KernelCallback 信任边界失效:在创建窗口并分配 WndExtra 的流程中,内核会通过 KeUserModeCallback 调用用户态回调(如 xxxClientAllocWindowClassExtraBytes,函数指针来自 PEB->KernelCallbackTable),并将用户态 NtCallbackReturn 返回的数据写回内核对象字段。攻击者在回调窗口期内调用 NtUserConsoleControl 切换窗口的关键标志位(常见描述为 0x800 的 ConsoleWindow 相关语义),导致同一个字段(如 tagWND 中保存 WndExtra 的值)在后续被内核 按“offset(相对 kernel desktop heap base 的偏移)”而非“pointer(用户态指针)”解释。当攻击者再通过 NtCallbackReturn 返回可控数值时,就会出现 字段值与解释语义不同步(out-of-sync) 的类型/语义混淆,最终在 kernel desktop heap 相关地址计算中产生 越界读写(OOB R/W)。利用上通常先把相邻窗口对象的关键字段(如 cbWndExtra、spmenu 等)打坏/扩展读写范围,将 OOB 放大为稳定的 任意读 + 任意写,最后通过遍历 EPROCESS->ActiveProcessLinks 找到 PID 4 的 SYSTEM Token 并替换当前进程 Token,实现本地提权到 SYSTEM。

Exp
mowenroot/Kernel
环境搭建
HelloWindows.cn - 精校 完整 极致 Windows系统下载仓储站

虚拟机vm搭建的时候添加低权限用户,其他默认

安装完后设置一些基本配置

下载 dControl v2.1(完全禁用 Windows Defender) - 吾爱破解 - 52pojie.cn 并关闭杀毒,防止Exp 被Defender 删了

编译exp
下载exp
90dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6E0L8%4N6W2L8Y4u0G2L8%4c8Q4x3V1k6w2k6i4u0F1k6h3I4Q4x3V1k6@1M7X3g2W2i4K6u0r3L8h3q4K6N6r3g2J5i4K6u0r3g2$3W2F1k6r3!0%4M7#2)9J5c8V1y4h3c8g2)9J5k6o6t1H3x3U0q4Q4x3X3b7I4y4K6x3J5
使用 Visual Studio 2019 创建一个新项目,选择 Windows 桌面向导

应用程序类型选择桌面应用程序,并勾选空项目

源文件处右击,添加一个现有项,选择刚下载的 CVE-2021-1732_Exploit.cpp

选择 Debug X64

右击属性,关闭优化

代码生成 -> 运行库选择 多线程调试(/MTd),将运行库静态链接到可执行文件中,否则在虚拟机中运行时可能会报找不到 dll 的错。

链接器 -> 调试 -> 生成经过优化以共享和发布的调试信息 (/DEBUG:FULL),为了后面用 ida 加载 pdb 时有符号。

链接器 -> 高级 -> 随机基址 设置为 否(/DYNAMICBASE:NO),固定基址 设置为 是(/FIXED),这是为了动调的时候方便下断点。

将上诉所有修改应用后,右击项目,然后生成

在项目文件夹中的 X64 -> Debug 文件夹,就能看到生成的 exe 和 pdb 了。

EXP执行
下载 进程资源管理器 - Sysinternals | Microsoft Learn 然后拷贝 procexp 和 Exp 到虚拟机中
右键以管理员身份运行 ProcessExplorer,在上方空白处右键 -> Select Columns,勾选上 Integrity Level,就能看到进程的权限了。


拍摄快照(方便恢复),双击运行 ExploitTest.exe,查看进程权限

让程序继续执行,再看进程权限,已提升为 system

如果蓝屏了,就恢复快照。
动态调试
下载 dbgview : a5cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6H3j5h3&6Q4x3X3g2T1j5h3W2V1N6g2)9J5k6h3y4G2L8g2)9J5c8Y4y4Z5j5i4u0W2i4K6u0r3K9h3&6A6N6q4)9K6c8Y4y4#2M7X3I4Q4x3@1c8Y4P5h3S2V1x3V1#2z5P5f1N6Y4e0$3E0D9c8q4k6a6x3i4S2c8z5p5u0%4i4K6y4r3M7r3q4K6M7%4N6V1i4K6y4p5j5h3q4S2j5b7`.`.

下载 VirtualKD : Release VirtualKD-Redux 2024.3 · 4d61726b/VirtualKD-Redux
把 VirtualKD 拷贝到试验机,并管理员运行 vminstall.exe 进行下载。

下载选项,然后会自动重启。

按 F8选择禁用驱动签名强制,开机即可。


然后在主机上打开 vmmon64.exe,并执行 Run Debugger。

会弹出该窗口(假设Break是灰的且Win卡住了,那么需要点击一下Go继续运行Win)

Win10 设置永久禁用驱动程序强制签名。

tagWND
ptagWND(user layer)
0x10 unknown
0x00 pTEB
0x220 pEPROCESS(of current process)
0x18 unknown
0x80 kernel desktop heap base
0x28 ptagWNDk(kernel layer)
0x00 hwnd
0x08 kernel desktop heap base offset
0x18 dwStyle
0x58 Window Rect left
0x5C Window Rect top
0x98 spMenu(uninitialized)
0xC8 cbWndExtra
0xE8 dwExtraFlag
0x128 pExtraBytes
0x90 spMenu(analyzed by myself)
0x00 hMenu
0x18 unknown0
0x100 unknown
0x00 pEPROCESS(of current process)
0x28 unknown1
0x2C cItems(for check)
0x40 unknown2(for check)
0x44 unknown3(for check)
0x50 ptagWND
0x58 rgItems
0x00 unknown(for exploit)
0x98 spMenuk
0x00 pSelf
利用分析
窗口扩展内存
在用户态使用 SetWindowLong 可以设置扩展内存的数据,最终会在 win32kfull!xxxSetWindowLong 函数中实现,该函数目的 -> 把 dwvalue 写到窗口的扩展内存中,并返回旧值。这里有两种模式,一直是直接用户态空间,还有一种是对内核态空间偏移的内核模式注意这里的 pExtraBytes 为一个内核桌面堆的一个偏移值,原先并不是内存地址(相对于 KernelDesktopHeapBase 的偏移语义)。控制标志位为 ptagWNDK->dwExtraFlag & 0x800 。

创建窗口可以使用函数 CreateWindowEx 最终会到 win32kfull!xxxCreateWindowEx 使用 win32kbase!HMAllocObject 创建内存后使用tagWND->ptagWNDk->dwExtraFlag &= ~0x40000000u; 进行初始化,这里只是部分初始化,会存在脏数据影响的情况。

win32kbase!HMAllocObject 中使用 RtlAllocateHeap 进行创建空间,这里 flags 为 0,不会清空内存即这里的 ptagWNDK 没有初始化空,但是 ptagWND 是被初始化了在 调用 Win32AllocPoolZInit 手动使用 memset 进行初始化操作。


经过上面分析,不难发现 tagWND->ptagWNDk->dwExtraFlag 标志位存在脏数据干扰的情况,先看看窗口扩展内存是怎么申请的,然后尝试需要寻找能控制这个标志位的函数。
创建窗口扩展内存
创建扩展内存空间在 win32kfull!xxxCreateWindowEx 中

先使用重载方法 tagWND::RedirectedFieldcbwndExtra<int>::operator!= 进行判断 ptagWNDk->cbWndExtra != 0, 这里的 cbWndExtra 在注册窗口类的时候可控。

然后调用函数 xxxClientAllocWindowClassExtraBytes(ptagWNDk->cbWndExtra) 进行申请内存。
该函数先 call KernelCallback[123] 进行用户态的内存申请,然后判断返回长度是否为 0x18,并 check 是否可读。

KernelCallback 表,可以在 user32.dll 中寻找到。
UserClientDllInitialize 初始化的 PEB 的时候使用 NtCurrentPeb()->KernelCallbackTable = apfnDispatch;

这里的 123 项为 __xxxClientAllocWindowClassExtraBytes,

__xxxClientAllocWindowClassExtraBytes函数实现,就是通过 RtlAllocateHeap 申请用户态内存,但是这里使用 flags 为 8,会内存置零,然后经过 NtCallbackReturn 显式返回内核。内核通过 KernelCallback 进入内核态必须通过 NtCallbackReturn 函数返回。

现在知道窗口的扩展内存 ptagWNDk->cbWndExtra 是怎么申请的了,默认情况下是用户态空间的内存,并不是内核空间地址。先寻找有没有 Api更改扩展内存的标志位(ptagWNDk->dwExtraFlag)。
修改窗口扩展标志位
在 win32kfull!xxxConsoleControl 中最后的逻辑为:
1、判断了一些标志位和 cbWndExtra 长度
2、如果原本为内核偏移模式,那扩展内存地址 (ptagWNDk->cbWndExtra)直接基于 KernelDesktopHeapBase + pExtraBytes 的偏移,注意这里的 pExtraBytes 为一个内核桌面堆的一个偏移值,原先并不是内存地址(相对于 KernelDesktopHeapBase 的偏移语义)。
3、如果原本为用户内存寻址模式,使用 DesktopAlloc 创建新的桌面内核空间,然后拷贝原先的数据并计算到 KernelDesktopHeapBase 的偏移值写到 pExtraBytes(相对于 KernelDesktopHeapBase 的偏移语义) 。
4、最后会更新扩展内存的值,并更改模式。也就是说不管之前是什么模式,最终都会更改为内核桌面偏移的模式(KernelDesktopHeapBase + pExtraBytes + offset)。

利用分析
经过上面创建窗口扩展内存的分析,以上过程用 iamelli0t 师傅博客的一张图来总结:

创建窗口的时候并没有初始化对应的窗口扩展标志位,后调用了用户态的函数来创建窗口扩展内存,返回的窗口扩展地址写回pExtraBytes ,这时扩展内存还是为用户态的内存。
但是我们可以通过 user32!ConsoleControl 函数对 窗口扩展标志位 的 0x800 修改变为内核偏移模式,会设置 pExtraBytes为内核桌面堆的偏移值。
利用手法v1.0
那按照普通堆喷的手法应该是这样操作的:
1、申请一堆窗口(CreateWindowEx),并修改窗口标志位(NtUserConsoleControl),做到内核偏移模式,后进行批量释放。
2、然后申请一个窗口,这个时候因为标志位没初始化,本来模式应为用户空间堆模式,现劫持为内核偏移模式。
3、进行越界读写操作。
但是并没有这么简单就能操作,在默认情况下 pExtraBytes 是为用户空间内存的地址,这会导致在内核偏移模式下,偏移量巨大,而且使用可控的 offset 是四字节,并无法有效控制偏移。

所以我们之前分析扩展窗口空间是怎么创建就很重要了,扩展窗口空间使用 call KernelCallback[123] 从内核态进入用户态进行用户态空间的申请,如果这里的 KernelCallback 表被我们所篡改了,原本 RtlAllocateHeap -> NtCallbackReturn 的流程,我们可以 hook 为 NtUserConsoleControl(先修改模式) -> NtCallbackReturn(&hijack_offset,0x18,0),那这样原本 pExtraBytes 为用户态内存的地址就可以被 hook 为想要的任意偏移。这样在使用 SetWindowLong 设置扩展内存的时候,模式为内核偏移模式,偏移为我们劫持的任意值,就可以完成任意地址写。
借用 iamelli0t 师傅的图来直观感受

那到这里你就可以发现,如果 dwExtraFlag 在原位置正常初始化了,我们仍然可以利用,就是因为初始化的步骤在前,而 pExtraBytes 申请用户态空间在后,我们可以使用 win32u!NtUserConsoleControl 函数来进行劫持。所以如果 dwExtraFlag 初始化不在 call KernelCallback[123] 之后我们仍然是可以利用的。

这里就触发思考,在 CVE-2021-1732 中,真正的高危点并非单纯的未初始化内存,实际在初始化空间的时候 dwExtraFlag 会被重置,而是 win32k 通过 KeUserModeCallback 主动执行用户态回调函数这一设计。KernelCallback 机制使内核在关键对象构造和状态迁移阶段依赖用户态返回的数据,一旦回调入口或回调协议的完整性被破坏(如 KernelCallbackTable 可被劫持、返回数据语义校验不足),将导致内核执行流和对象状态被用户态间接控制。这类漏洞的危害在于其结构性信任失效,而非单一实现缺陷,所以 KernelCallback 路径本身是极具攻击价值的研究方向。
这类漏洞好似 WEB 中前端给后端传值,我抓包修改了值,后端无条件信任了传入的值,导致漏洞产生,所以在这种类似接收参数的接口中一定要做好校验,无论是内核还是WEB,都不要轻易相信任何传来的数据。
利用手法v2.0
所以经过上面的分析,我们可以这样操作完成任意地址写:
1、申请 50 个窗口A(CreateWindowEx,cbWndExtra=0x20),保留窗口0和1其他全部进行批量释放。
2、hook KernelCallback[123] 为我们自定义的劫持流,可以为这样:修改模式(NtUserConsoleControl(hwnd,...))->劫 持返回参数为窗口0到KernelDesktopHeapBase偏移(NtCallbackReturn(&hijack_offset,0x18,0)) 。
3、申请一个窗口B,这个时候创建扩展内存空间申请被我们hook返回了一个 窗口0到KernelDesktopHeapBase偏移值
4、对窗口B进行扩展内存写(SetWindowLong),这样修改的就是窗口0的内核内存,可以完成越界写。
但是上面利用的时候会出现一个问题,KernelDesktopHeapBaseOffset(窗口0到KernelDesktopHeapBase偏移) 是属于内核上下文中的,我们只有_TAGWND_USER(CreateWindowEx返回的hWnd)用户态下的窗口句柄,没有内核模式中的窗口句柄,无法知道任何窗口的KernelDesktopHeapBaseOffset 偏移,也就是NtCallbackReturn返回不了我想控制的值。

HMValidateHandle
user32!HMValidateHandle 函数会把一个用户态的 HANDLE,解析后返回对应的内核对象指针。

通过调试也可以进一步验证返回的 ptagWNDK,第一个8字节为 hwnd 句柄。

那现在我们可以通过 user32!HMValidateHandle 泄露出来 内核窗口句柄就可以拿到 KernelDesktopHeapBaseOffset 偏移值。
利用手法v2.1
进一步更新利用手法:
1、申请 50 个窗口A(CreateWindowEx,cbWndExtra=0x20)并保存hWnd、hWndKernel(user32!HMValidateHandle),保留 窗口0和1其他全部进行批量释放,利用hWndKernel[0]获取窗口0到KernelDesktopHeapBase偏移。
2、hook KernelCallback[123] 为我们自定义的劫持流,可以为这样:
a.利用 hWndKernel[i]->cbWndExtra 匹配 cbWndExtra=0x1234 的窗口B,这里匹配上的 hWndA 就是新建的窗口
b.修改模式(NtUserConsoleControl(hWndA ,...))
c.劫持返回参数为窗口0到KernelDesktopHeapBase偏移(NtCallbackReturn(&hijack_offset,0x18,0)))
3、申请一个窗口B(cbWndExtra=0x1234,和窗口A能区分开就行),这个时候创建扩展内存空间申请被我们hook了,会执行2的步骤
4、对窗口B进行扩展内存写(SetWindowLong),这样修改的就是窗口0的内核内存,可以完成越界写。
借用 in1t 师傅文章中的图片

现在只能完成内核桌面堆上的任意地址写,也只有特定句柄泄露的内核地址,还没有能力进行内核任意地址读的能力,所以想要提权的话,我们还需要泄露任意内核地址的能力。并且想要窃取 system 进程的token 得 pEPROCESS 来完成。
回顾最开始的 ptagWND 结构体,pEPROCESS 有两处可以获取。
ptagWND(user layer)
0x10 unknown
0x00 pTEB
0x220 pEPROCESS(of current process)
0x28 ptagWNDk(kernel layer)
0x00 hwnd
0x90 spMenu(analyzed by myself)
0x00 hMenu
0x18 unknown0
0x100 unknown
0x00 pEPROCESS(of current process)
读原语
可以使用 GetMenuBarInfo 函数,进行菜单的信息泄露。在 xxxGetMenuBarInfo 中,不为 WS_CHILD 的情况下,是特别好控制的数据返回,可以伪造 spMenu 并构造 spMenuk->pSelf->rgItems.unknown_for_exploit 为想要读取的地址。

伪造 spMenu 并不难,但是还有一点是需要控制 Wnd->spMenu 为我们伪造的菜单才能读取里面的信息。
在 xxxSetWindowLongPtr() -> xxxSetWindowData() 中当 nIndex 为 -12(GWL_ID) 的时候并且 dwExtraFlag 为 WS_CHILD 的时候可以替换 WND->spMenu 为传入的指针,并且返回原来旧的 spMenu,这里我们即可以伪造 spMenu 又可以泄露原有 spMenu ,后续进一步利用原有 spMenu 泄露里面的 _EPROCESS 达到提权的目的。

利用手法 latest
1、申请 50 个窗口A(CreateWindowEx,cbWndExtra=0x20)并保存hWnd、hWndKernel(user32!HMValidateHandle),保留 窗口0和1其他全部进行批量释放,利用hWndKernel[0]获取窗口0到KernelDesktopHeapBase偏移。
2、hook KernelCallback[123] 为我们自定义的劫持流,可以为这样:
a.利用 hWndKernel[i]->cbWndExtra 匹配 cbWndExtra=0x1234 的窗口B,这里匹配上的 hWndA 就是新建的窗口
b.修改模式(NtUserConsoleControl(hWndA ,...))
c.劫持返回参数为窗口0到KernelDesktopHeapBase偏移(NtCallbackReturn(&hijack_offset,0x18,0)))
3、申请一个窗口B(cbWndExtra=0x1234,和窗口A能区分开就行),这个时候创建扩展内存空间申请被我们hook了,会执行2的步骤
4、对窗口B进行扩展内存写(SetWindowLong),这样修改的就是窗口0的内核内存,对 窗口0 篡改,修改 dwExtraFlag 为 WS_CHILD 。
5、伪造 spMenu ,使用 SetWindowLongPtr 进行替换 hWnd[0]->spMenu ,后续利用读原语获取原来 hWnd[0]->OrgSpMenu中的 _EPROCESS ,获取到 SYSTEM 的 TOKEN 。
6、继续利用窗口B的扩展内存写,修改 hWnd[0]的 pExtraBytes 为当前进程的 _EProcess ,并控制模式为用户态。

7、对hWnd[0]使用 SetWindowLong 等Api时,就会写入当前进程的 _EProcess,把之前获取的 SystemToken 替换进去即可完成提权。

参考链接
[0] CVE-2021-1732 Windows10 本地提权漏洞复现及详细分析-安全KER - 安全资讯平台
[1] MSRC: Windows Win32k 特权提升漏洞公告
[2] 0Day攻击!首次发现蔓灵花组织在针对国内的攻击活动中使用Windows内核提权0Day漏洞(CVE-2021-1732)
[3] Github: k-k-k-k-k/CVE-2021-1732
[4] Github: KaLendsi/CVE-2021-1732-Exploit
[5] [原创]CVE-2021-1732 Microsoft Windows10 本地提权漏洞研究及Exploit开发
[6] WNDCLASSEXA structure (winuser.h)
[7] CreateWindowExW function (winuser.h)
[8] Win10 tagWnd partial member reverse (window hidden, window protected)
[9] Part 18: Kernel Exploitation -> RS2 Bitmap Necromancy
[10] Microsoft Windows提权漏洞 (CVE-2021-1732) 分析
[11] [原创]KeUserModeCallback用法详解
[12] CVE-2021-1732: win32kfull xxxCreateWindowEx callback out-of-bounds
[13] Windows源代码阅读之 句柄算法
[14] A simple protection against HMValidateHandle technique
[15] GetMenuBarInfo function (winuser.h)
[16] MENUBARINFO structure (winuser.h)
[17] SetWindowLongPtrW function (winuser.h)
[18] 通过ActiveProcessLinks遍历进程
[19] 使用AllocConsole在Win32程序中调用控制台调试输出
[20] 使用VMware + win10 + VirtualKD + windbg从零搭建双机内核调试环境
[21] Github: VirtualKD-Redux/release
[22] 下载 Windows 调试工具
[23] 使用符号服务器
[24] Microsoft Update Catalog