-
-
[原创]更好理解:CVE-2021-1732漏洞分析报告与利用
-
发表于: 2026-3-23 10:44 1112
-
本次报告的标题定为《更好理解:CVE-2021-1732漏洞分析报告与利用》。在撰写过程中,我参考了多篇已有的分析文章,但由于该漏洞是在野样本中被首次发现,很少可用的PoC。为此,本文从零编写了一套完整的PoC与Exp,旨在帮助读者通过可运行的PoC直观地理解漏洞原理及其利用过程。此外,本文也对部分公开博客中的不准确之处进行了修正。
参考博客:
ExploitCN师傅的博客ExploitCN的博客
in1t师傅的博客in1t的博客
CVE-2021-1732 是蔓灵花(BITTER)APT组织在某次被披露的攻击行动中所使用的 0day 漏洞。该漏洞属于本地权限提升类型,攻击者利用该漏洞可将普通用户进程的权限提升至最高的 SYSTEM 权限,具有较高的危害性。
该漏洞属于逻辑型漏洞,与传统的内核漏洞存在明显区别。它并非 UAF(释放后使用)类型,也不涉及内存耗尽后的指针异常,因此没有堆喷射、内存消耗等常见的攻击特征,检测难度较大。漏洞的成因需要通过代码审计、深入分析函数执行流程才能定位,属于较为经典的逻辑漏洞类型。
正因为其逻辑型漏洞的特性,这类缺陷往往隐蔽性更强,难以通过传统的模糊测试或动态检测手段发现。它不依赖于内存布局的破坏,也不触发常见的内核崩溃信号,更多时候只是隐藏在系统正常执行路径中的一处“分支错误”或“状态遗漏”。因此,发现此类漏洞通常需要依赖逆向工程与静态代码分析,开发者或研究员必须逐层追踪内核函数的调用链,理解数据结构的使用方式,才能在看似合规的逻辑中找到可被利用的缝隙。也正因如此,这类漏洞在实际攻击场景中更具价值,能够绕过许多基于行为特征的防御机制。
注:如果读者预先了解CVE-2021-1732,并在复现方面遇到了困难,可以参考本篇博客的"坑点"部分
tagWND 是 Windows 内核中用于描述窗口的核心数据结构。它由内核模块 win32k.sys 在窗口创建时分配并维护,包含了窗口的全部信息:从最基本的窗口句柄(HWND)、窗口类名、窗口过程地址,到复杂的父子窗口关系、窗口样式与扩展样式、菜单句柄,再到窗口的额外数据指针(pExtraBytes)及其大小(cbWndExtra)。
这个结构体是窗口管理器实现一切操作的基础——当你向窗口发送消息、查询或修改属性时,系统会根据句柄找到对应的 tagWND,然后直接读取或修改其成员。值得注意的是,虽然 tagWND 本身位于内核地址空间,但为了高效的用户态交互,部分窗口数据通过桌面堆(Desktop Heap)映射到用户态,这使得 HMValidateHandle 这类函数能够返回一个指向 tagWNDk 某些字段的用户态指针。
用户态程序无法通过常规 API 修改其内容,必须借助系统调用(如 NtUserConsoleControl)让内核完成状态切换,这正是 CVE-2021-1732 漏洞利用的关键切入点:通过精心构造的调用,改变 tagWND 的标志位( dwExtraFlag 的 0x800),使 pExtraBytes 的寻址模式从直接指针切换为桌面堆偏移,进而实现任意内核地址读写。
这里借用in1t师傅博客里的图片,在CreateWindowEx创建窗口的过程中,如果注册的窗口类指定了cbWndExtra(即为窗口实例分配的额外字节数),内核会通过KeUserModeCallback回调机制调用用户层的user32!_xxxClientAllocWindowClassExtraBytes函数,由它在用户态系统堆中申请内存。内核将这块内存的地址赋值给窗口对象tagWND的pExtraBytes成员后,并未重新设置dwExtraFlag(tagWNDk.dwExtraFlag &= ~0x800),此时pExtraBytes被解释为指向用户态内存的绝对指针。
两种存储模式:实际上,窗口扩展数据存在两种存储方式:
根本原因在于:内核在处理窗口扩展内存分配时,未能保证“存储模式切换”与“指针/偏移量赋值”这两个操作的原子性和一致性,形成了状态不同步的经典逻辑漏洞。

如果我们能够主动控制内核中 tagWND 结构体的 pExtraBytes 成员与 dwExtraFlag 的值,就可以实现内核桌面堆的相对地址写。
在 win32kfull.sys 中的 xxxClientAllocWindowClassExtraBytes 函数里,pExtraBytes 指向的内存来源于用户态申请。因此,通过 Hook 用户态的 xxxClientAllocWindowClassExtraBytes 函数,即可实现对该成员的控制。 
NtUserConsoleControl 函数会将 dwExtraFlag 设置为间接寻址模式。 
基于以上两点,便可理解后续 PoC 的构造逻辑。
难点:NtUserConsoleControl 函数需要传入窗口句柄,而该句柄需要在 CreateWindowExW 返回之前获取。好在内核会将部分窗口数据映射到用户态,我们可以通过内存读取的方式找到已生成但尚未返回的窗口句柄(具体通过自定义的 GetHwnd 函数实现)。另外,NtCallbackReturn函数返回的时候,尽量返回一个很大的数,这样方便触发内存越界访问,导致蓝屏.
在实现对内核桌面堆的相对地址写入后,由于普通窗口的 pExtraBytes 指向的是绝对地址,我们可以借助这一特性,将相对地址写入转化为绝对地址写入。再通过GetMenuBarInfo泄露信息,实现任意地址读
整个实现过程分为以下几步:
效果: 