首页
社区
课程
招聘
[原创]更好理解:CVE-2021-1732漏洞分析报告与利用
发表于: 6小时前 97

[原创]更好理解:CVE-2021-1732漏洞分析报告与利用

6小时前
97

本次报告的标题定为《更好理解:CVE-2021-1732漏洞分析报告与利用》。在撰写过程中,我参考了多篇已有的分析文章,但由于该漏洞是在野样本中被首次发现,很少可用的PoC。为此,本文从零编写了一套完整的PoC与Exp,旨在帮助读者通过可运行的PoC直观地理解漏洞原理及其利用过程。此外,本文也对部分公开博客中的不准确之处进行了修正。

参考博客:

ExploitCN师傅的博客ExploitCN的博客

in1t师傅的博客in1t的博客

漏洞分析

概述

CVE-2021-1732 是蔓灵花(BITTER)APT组织在某次被披露的攻击行动中所使用的 0day 漏洞。该漏洞属于本地权限提升类型,攻击者利用该漏洞可将普通用户进程的权限提升至最高的 SYSTEM 权限,具有较高的危害性。

该漏洞属于逻辑型漏洞,与传统的内核漏洞存在明显区别。它并非 UAF(释放后使用)类型,也不涉及内存耗尽后的指针异常,因此没有堆喷射、内存消耗等常见的攻击特征,检测难度较大。漏洞的成因需要通过代码审计、深入分析函数执行流程才能定位,属于较为经典的逻辑漏洞类型。

正因为其逻辑型漏洞的特性,这类缺陷往往隐蔽性更强,难以通过传统的模糊测试或动态检测手段发现。它不依赖于内存布局的破坏,也不触发常见的内核崩溃信号,更多时候只是隐藏在系统正常执行路径中的一处“分支错误”或“状态遗漏”。因此,发现此类漏洞通常需要依赖逆向工程与静态代码分析,开发者或研究员必须逐层追踪内核函数的调用链,理解数据结构的使用方式,才能在看似合规的逻辑中找到可被利用的缝隙。也正因如此,这类漏洞在实际攻击场景中更具价值,能够绕过许多基于行为特征的防御机制。

注:如果读者预先了解CVE-2021-1732,并在复现方面遇到了困难,可以参考本篇博客的"坑点"部分

受影响的版本

Windows Server, version 20H2 (Server Core Installation)
Windows 10 Version 20H2 for ARM64-based Systems
Windows 10 Version 20H2 for 32-bit Systems
Windows 10 Version 20H2 for x64-based Systems
Windows Server, version 2004 (Server Core installation)
Windows 10 Version 2004 for x64-based Systems
Windows 10 Version 2004 for ARM64-based Systems
Windows 10 Version 2004 for 32-bit Systems
Windows Server, version 1909 (Server Core installation)
Windows 10 Version 1909 for ARM64-based Systems
Windows 10 Version 1909 for x64-based Systems
Windows 10 Version 1909 for 32-bit Systems
Windows Server 2019 (Server Core installation)
Windows Server 2019
Windows 10 Version 1809 for ARM64-based Systems
Windows 10 Version 1809 for x64-based Systems
Windows 10 Version 1809 for 32-bit Systems
Windows 10 Version 1803 for ARM64-based Systems
Windows 10 Version 1803 for x64-based Systems

详细分析

tagWND结构体

tagWND 是 Windows 内核中用于描述窗口的核心数据结构。它由内核模块 win32k.sys 在窗口创建时分配并维护,包含了窗口的全部信息:从最基本的窗口句柄(HWND)、窗口类名、窗口过程地址,到复杂的父子窗口关系、窗口样式与扩展样式、菜单句柄,再到窗口的额外数据指针(pExtraBytes)及其大小(cbWndExtra)。

这个结构体是窗口管理器实现一切操作的基础——当你向窗口发送消息、查询或修改属性时,系统会根据句柄找到对应的 tagWND,然后直接读取或修改其成员。值得注意的是,虽然 tagWND 本身位于内核地址空间,但为了高效的用户态交互,部分窗口数据通过桌面堆(Desktop Heap)映射到用户态,这使得 HMValidateHandle 这类函数能够返回一个指向 tagWNDk 某些字段的用户态指针。

用户态程序无法通过常规 API 修改其内容,必须借助系统调用(如 NtUserConsoleControl)让内核完成状态切换,这正是 CVE-2021-1732 漏洞利用的关键切入点:通过精心构造的调用,改变 tagWND 的标志位( dwExtraFlag0x800),使 pExtraBytes 的寻址模式从直接指针切换为桌面堆偏移,进而实现任意内核地址读写。

ptagWND(来源于ExploitCN师傅)
    0x10 unknown
        0x00 pTEB
            0x220 pEPROCESS(of current process)
    0x18 unknown
        0x80 kernel desktop heap base
    0x28 ptagWNDk
        0x00 hwnd
        0x08 kernel desktop heap base offset
        0x18 dwExStyle
        0x1C dwStyle
        0x58 Window Rect left
        0x5C Window Rect top
        0x98 spMenu(uninitialized)
        0xC8 cbWndExtra
        0xE8 dwExtraFlag (是寻址模式,还是offset模式)
        0x128 pExtraBytes
    0xA8 g_pMem4(spMenu)(根据EXP代码分析)
        0x00 hMenu
        0x18 unknown
            0x100 unknown
                0x00 pEPROCESS(of current process)
        0x50 ptagWND
        0x58 rgItems
            0x00 unknown(for exploit)
        0x98 g_pMem3
            0x00 g_pMem1
                0x28 g_pMem2
                    0x2C cItems(for check)
                0x40 unknown1
                0x44 unknown2
                0x58 g_pMem5
                    0x00 DestAddr-0x40

CreateWindowExW介绍

这里借用in1t师傅博客里的图片,在CreateWindowEx创建窗口的过程中,如果注册的窗口类指定了cbWndExtra(即为窗口实例分配的额外字节数),内核会通过KeUserModeCallback回调机制调用用户层的user32!_xxxClientAllocWindowClassExtraBytes函数,由它在用户态系统堆中申请内存。内核将这块内存的地址赋值给窗口对象tagWNDpExtraBytes成员后,并未重新设置dwExtraFlagtagWNDk.dwExtraFlag &= ~0x800),此时pExtraBytes被解释为指向用户态内存的绝对指针。

两种存储模式:实际上,窗口扩展数据存在两种存储方式:

  • 模式1(用户态系统堆)pExtraBytes直接保存用户态堆指针,dwExtraFlag不含0x800标志;
  • 模式2(内核态桌面堆)pExtraBytes保存的是相对于内核桌面堆基址的偏移量,dwExtraFlag会设置0x800标志位。

根本原因在于:内核在处理窗口扩展内存分配时,未能保证“存储模式切换”与“指针/偏移量赋值”这两个操作的原子性和一致性,形成了状态不同步的经典逻辑漏洞。

图片描述

POC 讲解

思路讲解

如果我们能够主动控制内核中 tagWND 结构体的 pExtraBytes 成员与 dwExtraFlag 的值,就可以实现内核桌面堆的相对地址写。

控制 pExtraBytes

win32kfull.sys 中的 xxxClientAllocWindowClassExtraBytes 函数里,pExtraBytes 指向的内存来源于用户态申请。因此,通过 Hook 用户态的 xxxClientAllocWindowClassExtraBytes 函数,即可实现对该成员的控制。 图片描述

控制 dwExtraFlag

NtUserConsoleControl 函数会将 dwExtraFlag 设置为间接寻址模式。 图片描述

基于以上两点,便可理解后续 PoC 的构造逻辑。

难点NtUserConsoleControl 函数需要传入窗口句柄,而该句柄需要在 CreateWindowExW 返回之前获取。好在内核会将部分窗口数据映射到用户态,我们可以通过内存读取的方式找到已生成但尚未返回的窗口句柄(具体通过自定义的 GetHwnd 函数实现)。另外,NtCallbackReturn函数返回的时候,尽量返回一个很大的数,这样方便触发内存越界访问,导致蓝屏.

EXP 讲解

在实现对内核桌面堆的相对地址写入后,由于普通窗口的 pExtraBytes 指向的是绝对地址,我们可以借助这一特性,将相对地址写入转化为绝对地址写入。再通过GetMenuBarInfo泄露信息,实现任意地址读

任意地址写

整个实现过程分为以下几步:

  1. 使 Magic 窗口的 pExtraBytes 指向 NormalMin 窗口;
  2. 通过 Magic 窗口修改 NormalMin 窗口的 pExtraBytes,使其指向自身;
  3. 再通过 NormalMin 窗口修改 NormalMax 窗口的 pExtraBytes,使其指向任意目标地址;
  4. 最后,通过 NormalMax 窗口实现对任意地址的写入操作。

效果: 图片描述

任意地址读

在获得任意地址写能力之后,提权操作就相对简单了。本文选择通过 GetMenuBarInfo 函数实现任意地址读。

spMenu

首先需要控制 ptagWND 结构体中的 spMenu 字段,使其指向我们预先分配的 g_pMem4 内存区域。通过调用 SetWindowLongPtrW 并将 nIndex 参数设置为 -12,即可将 spMenu 填入指定的值。需要注意的是,该调用要求窗口的ExStyle满足特定条件,而 ExStyle 位于 ptagWNDk 中且可以被修改。因此,我们可以先修改 ExStyle,再成功设置 spMenu 的值。

初次任意地址读

读者在阅读 Exp 源码时,可能会对 g_initRead 这个变量的作用产生疑问:为什么初次读取与后续读取的处理方式有所不同?这里贴出一段 IDA 反编译代码,重点可以关注其中的 v42 和 v43 部分。

case -3:
      if ( (*(_BYTE *)(ptagWNDk + 0x1F) & 0x40) != 0 )// check1
        goto LABEL_9;
      g_pMem4 = *(_QWORD *)(ptagWND + 0xA8);
      if ( !g_pMem4 )
        goto LABEL_9;
      v75 = 0;
      SmartObjStackRefBase<tagMENU>::operator=(&g_pMem3, g_pMem4);
      if ( !SmartObjStackRef<tagMENU>::operator bool((__int64)&g_pMem3)
        || (int)_a3 < 0
        || (unsigned int)_a3 > *(_DWORD *)(*(_QWORD *)(*(_QWORD *)g_pMem3 + 0x28LL) + 0x2CLL) )// check2
      {
        goto LABEL_9;
      }
      v30 = v75;
      if ( !v75 )
        v30 = *(_QWORD **)g_pMem3;
      *(_QWORD *)(a4 + 0x18) = *v30;
      if ( *(_DWORD *)(*(_QWORD *)g_pMem3 + 0x40LL) && *(_DWORD *)(*(_QWORD *)g_pMem3 + 0x44LL) )
      {
        if ( (_DWORD)_a3 ) //This One
        {
          ptagWNDk2 = *(_QWORD *)(ptagWND + 40);
          v38 = 0x60 * _a3;
          g_pMem5 = *(_QWORD *)(*(_QWORD *)g_pMem3 + 0x58LL);
          g_pMem5_1 = *(_QWORD *)(0x60 * _a3 + g_pMem5 - 96);
          if ( (*(_BYTE *)(ptagWNDk2 + 0x1A) & 0x40) != 0 )// no
          {
            ...
          }
          else
          {
            v42 = *(_DWORD *)(g_pMem5_1 + 0x40) + *(_DWORD *)(ptagWNDk2 + 0x58);
            *(_DWORD *)(a4 + 4) = v42;          // ArbitraryRead
            *(_DWORD *)(a4 + 12) = v42 + *(_DWORD *)(*(_QWORD *)(v38 + g_pMem5 - 96) + 0x48LL);
          }
          v43 = *(_DWORD *)(*(_QWORD *)(v38 + g_pMem5 - 96) + 0x44LL) + *(_DWORD *)(*(_QWORD *)(ptagWND + 40) + 0x5CLL);
          *(_DWORD *)(a4 + 8) = v43;
          v36 = v43 + *(_DWORD *)(*(_QWORD *)(v38 + g_pMem5 - 96) + 0x4CLL);

现有部分公开的 Exp 实现中存在一处不准确的地方:在计算 v42 = *(_DWORD *)(g_pMem5_1 + 0x40) + *(_DWORD *)(ptagWNDk2 + 0x58); 时,并不需要强制将 g_pMem5_1 的 0x40 偏移设置为 0x40。真正需要初始化 g_initRead 的原因在于,我们需要获取 *(_DWORD *)(ptagWNDk2 + 0x58) 的默认值。同理,下方v43的 *(_DWORD *)(*(_QWORD *)(ptagWND + 40) + 0x5CLL); 同样需要先获取其默认值。因此,初次读取的核心目的是保存这些默认值,以确保后续任意地址读操作的准确性。

坑点

在本次 Exp 的实现过程中,需要特别注意数据类型扩展的问题,尤其是从 LONGLONGLONG 等类型的转换。例如在获取 g_pfnHMValidateHandle 时,就涉及此类扩展操作。此外,在进行任意地址读并返回值时,务必确保低 4 字节的值不会发生符号扩展,否则可能导致地址计算错误,影响后续利用流程的稳定性。

总结

本文围绕 CVE-2021-1732 这一经典的内核逻辑漏洞,从零开始构建了完整的 PoC 与 Exp,重点梳理了漏洞触发、利用构建及调试过程中的关键细节。

在漏洞分析部分,我们通过追踪 win32kfull.sys 中的关键函数,逐步揭示了如何控制 tagWND 结构体中的 pExtraBytesdwExtraFlag,从而实现内核桌面堆的相对地址写入。利用环节则基于普通窗口 pExtraBytes 指向绝对地址的特性,通过窗口链的巧妙转换,将相对地址写转化为绝对地址写,并进一步利用 spMenuGetMenuBarInfo 完成任意地址读写。

此外,本文还特别指出了实现过程中容易踩到的坑点,例如数据类型扩展时低 4 字节的符号扩展问题,以及初次读取时对默认值保存的必要性。

尽管该漏洞已有不少公开分析,但现有资料大多未提供完整的 PoC。本文从实际代码出发,填补了这一空白,希望能帮助读者更直观地理解逻辑型漏洞的挖掘与利用思路,同时也为后续研究同类漏洞提供一份可复现的参考。

如果你在阅读过程中有任何疑问,或对实现细节有更好的改进建议,欢迎交流讨论。本文的代码已经上传Github:AO031的Github,也会放在附件里面

图片描述


[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

上传的附件:
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回