CVE-2021-1732 for win10-1809

0x00 前言

漏洞名称:Windows Win32k权限提升漏洞 漏洞编号: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

0x01 前置知识

1.1 窗口类

使用窗口前要先注册一个窗口类 WNDCLASSEX,其结构如下:

typedef struct tagWNDCLASSEXW {
    UINT        cbSize;
    /* Win 3.x */
    UINT        style;
    WNDPROC     lpfnWndProc;
    int         cbClsExtra; // 窗口类的扩展内存
    int         cbWndExtra; // 窗口的扩展内存
    HINSTANCE   hInstance;
    HICON       hIcon;
    HCURSOR     hCursor;
    HBRUSH      hbrBackground;
    LPCWSTR     lpszMenuName;
    LPCWSTR     lpszClassName;
    /* Win 4.0 */
    HICON       hIconSm;
} WNDCLASSEXW, *PWNDCLASSEXW, NEAR *NPWNDCLASSEXW, FAR *LPWNDCLASSEXW;

1.2 窗口类的扩展内存

在应用程序注册一个窗口类时,可以让系统分配一定大小的内存空间,作为该窗口类的扩展内存,之后属于该窗口类的每个窗口都共享这片内存区域,每个窗口都可以通过 Windows 提供的 API 来读写这片扩展内存,如 GetClassLong、GetClassLongPtr、SetClassLong、SetClassLongPtr 等(Ptr 后缀是为了兼容 32 和 64 位),以此实现同窗口类的窗口间通信,而 cbClsExtra 字段就记录了这片内存的大小。

1.3 窗口的扩展内存

当窗口创建时,可以让系统分配一定大小的内存空间,作为该窗口的扩展内存,这片内存每个窗口独享,也可以通过 API 来读写(GetWindowLong、GetWindowLongPtr、SetWindowLong、SetWindowLongPtr),该机制提供了一种窗口数据暂存的方式,这片内存的大小由 cbWndExtra 字段记录。

1.4 tagWND

Windows 用 tagWND 结构体来描述每个窗口(该结构体在加载了官方 pdb 文件的 Win7 win32k.sys 模块可以找到,Win7 之后需要结合逆向来了解该结构体)。

在 Win10 中,对于每个窗口,系统为用户层和内核层各维护了一个 tagWND 结构体,用户层的 &tagWND + 0x28 处的 8 字节为一个指针,指向内核层 tagWND 结构体。

tagWND 结构(https://www.anquanke.com/post/id/241804#h3-12):

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
        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

0x02 漏洞分析

2.1 流程分析

IDA 打开 win32kfull.sys。

NtUserCreateWindowEx 会调用 win32kfull!xxxCreateWindowEx

查看 win32kfull!xxxCreateWindowEx

这里有一个关键参数 v32,查看该参数调用情况

可知v32 = v31,查看v31 调用情况

HMAllocObject 创建了一个tagWND 结构体并返回其指针。

接着查看 win32kfull!xxxClientAllocWindowsClassExtraBytes 附近代码:

可以看到 win32kfull!xxxCreateWindowEx 在调用过程中会调用 win32kfull!xxxClientAllocWindowsClassExtraBytes,用来给当前线程窗口分配额外内存,传入的参数为 [[tagWnd + 0x28h] + 0xc8h],即tagWND-­>cbWndExtra,返回值传给 [[tagWnd + 0x28h] + 0x128h],即 tagWND-­> pExtraBytes。

跟进该函数

可发现 win32kfull!xxxClientAllocWindowsClassExtraBytes 调用 KeUserModeCallback,在《KernelCallbackTable》中记录了 KeUserModeCallback 的结构以及调用过程。

NTSTATUS
KeUserModeCallback (
     IN ULONG ApiNumber,
     IN PVOID InputBuffer,
     IN ULONG InputLength,
     OUT PVOID *OutputBuffer,
     IN PULONG OutputLength
     );

可知此时会回调 KernelCallbackTable 中 ApiNumber 为 0x7B 处的用户层函数,偏移则为 0x3d8,即 user32.dll!_xxxClientAllocWindowClassExtraBytes,传入的参数为 InputBuffer 和 InputLength,即 v11 的地址 和 4。

查看 user32.dll!_xxxClientAllocWindowClassExtraBytes

可以看到会调用 RtlAllocateHeap 分配一个大小为 0x18 的堆空间,将返回大小和指针返回给了 v12 和 v7。

win32kfull!xxxClientAllocWindowsClassExtraBytes 第26行起,表明在KeUserModeCallback 执行完成后,需要具备的几个运行条件:

  • v12(OutputLength) 必须要等于 24(0x18)

  • v7(OutputBuffer)+8 < MmUserProbeAddress,即 v7 在用户空间

综上,通过层层调用 user32.dll!_xxxClientAllocWindowClassExtraBytes 创建的用户堆空间地址指针最后会返回给 tagWND-­> pExtraBytes。

套个图 https://iamelli0t.github.io/images/CVE-2021-1732/1.png

2.2 利用函数

2.2.1 win32kfull!NtUserConsoleControl

可以看到 win32kfull!NtUserConsoleControl 会调用 win32kfull!xxxConsoleControl,但是需要满足一些条件:

  • 传入的第一个参数 ≤ 6

  • 传入的第三个参数 ≤ 18

2.2.2 win32kfull!xxxConsoleControl

跟进 win32kfull!xxxConsoleControl

可以看到这里会调用 DesktopAlloc 在内核态桌面堆分配内存

往前追溯查看调用条件。得到:

  • 传入的第一个参数 -5 = 1,即传入的第一个参数 =6

  • 传入的第三个参数 = 0x10

  • (ValidateHwnd(传入的第二个参数) = tagWnd)->dwExtraFlag ≠ 0x800

分配完内存后,tagWndk->pExtraBytes = 分配的内核态桌面堆地址 - 桌面内核堆基址 = 偏移 ,并将 tagWndk- > dwExtraFlag 修改为 0x800,这样操作后,此窗口的额外数据会采用桌面堆+偏移的寻址方式

这类操作会直接影响如 setWindowsLong 类函数对窗口的设置。

0x03 EXP

3.1 流程

基于上述过程,已经能得到 ptagWnd->pExtraBytes 的两种赋值流程:

  1. 用户空间系统堆(ptagWnd->dwExtraFlag & 0x800 == 0):直接寻址

  2. 内核空间桌面堆(ptagWnd->dwExtraFlag & 0x800 != 0):offset 间接寻址

因此利用过程应为:

对 user32!_xxxClientAllocWindowClassExtraBytes 进行 Hook,调用 win32kfull!NtUserConsoleControl,将 tagWnd->pExtraBytes 改为基于内核桌面堆空间的 offset 寻址模式,然后调用 ntdll!NtCallbackReturn 向内核返回一个能过读写检查的可控值,用于设置 ptagWndk->pExtraBytes,然后调用 setWindowsLong 写附加空间,实现基于内核空间桌面堆基址的可控偏移量越界写。

SetWindowLong最终调用 win32kfull!xxxSetWindowLong,传入根据窗口句柄找到的 tagWND 结构体地址(ptagWND)、写入的扩展内存的偏移(nIndex)、要写入的值(value)。 nIndex的值必须小于 ptagWNDk->cbWndExtra(窗口扩展内存大小,注册窗口类时指定)。 当 ptagWNDk->dwExtraFlag & 0x800 != 0 时,内核桌面堆起始地址 + pExtraBytes + nIndex 处的 4 字节会被赋值成 value

再套个图 https://iamelli0t.github.io/images/CVE-2021-1732/7.png

3.2 问题

3.2.1 窗口句柄

    win32kfull!xxxConsoleControl 需要传入窗口句柄,使用6号功能,EXP 需要在 CreateWindowEx 创建窗口过程中调用 user32!ConsoleControl,但此时 CreateWindowsEx 还没有返回窗口句柄 HWND。

    xxxCreateWindowEx 会调用 win32k!HMAllocObject 创建一个 tagWND 结构体,并返回其指针,窗口句柄存储在 &tagWNDk+0,&tagWNDk+8 保存了 tagWNDk 相对于桌面堆基址的偏移。且 win32k!HMAllocObject 调用位置在 user32!_xxxClientAllocWindowClassExtraBytes 之前,因此获得创建的窗口句柄需要通过 user32!HMValidateHandle 泄露 tagWNDk。

    user32!HMValidateHandle:只要把窗口句柄传递给这个函数,就会返回 tagWNDk 在用户空间的只读映射指针(HMAllocObject 创建了桌面堆类型句柄后,会把tagWNDk 对象放入到内核模式到用户模式的映射内存中)

3.2.2 构造任意地址读写原语

  1. 任意地址写

    漏洞可以获得一次基于内核空间桌面堆基址的可控偏移量越界写 的机会,因此可以将一个正常窗口的 tagWNDk 相对于桌面堆基址的偏移 (&tagWNDk+8) 放入漏洞窗口的 ptagWndk->pExtraBytes,调用 setWindowsLong 就可以对漏洞窗口进行操作就可以修改正常窗口的 tagWNDk->cbWndExtra,也就可以解除 nIndex 限制,再用这个解除限制的窗口操作 nIndex,就可以对其他窗口桌面堆实现越界写入。

  1. 任意地址读

    http://ryze-t.com/posts/2021/10/20/GetMenuBarInfo-实现任意地址读.html

  1. 进程提升

    在完成任意地址读后,同样获取了 spMenu,[spMenu+0x18]+0x100 存储的是当前进程的 EPROCESS,因此可以通过任意地址读进程 EPROCESS 链表的循环遍历,查看 EPROCESS 结构

    几个关键属性:

    - 0x2e8 ActiveProcessLinks 进程 EPROCESS 链表

    - 0x2e0 UniqueProcessId 进程ID

    - 0x358 Token 进程 Token

    因此只需要利用读写权限,遍历进程链表找到 pid =4 的进程,将 Token 复制到当前进程,即可完成提权

0x04 参考链接

https://www.anquanke.com/post/id/241804

https://github.com/k-k-k-k-k/CVE-2021-1732