CVE-2022-21882漏洞是Windows系统的一个本地提权漏洞,微软在2022年1月份安全更新中修补此漏洞。本文章对漏洞成因及利用程序进行了详细的分析。

漏洞介绍

CVE-2022-21882是对CVE-2021-1732漏洞的绕过,属于win32k驱动程序中的一个类型混淆漏洞。

攻击者可以在user_mode调用相关的GUI
API进行内核调用,如xxxMenuWindowProc、xxxSBWndProc、xxxSwitchWndProc、xxxTooltipWndProc等,这些内核函数会触发回调xxxClientAllocWindowClassExtraBytes。攻击者可以通过hook
KernelCallbackTable 中 xxxClientAllocWindowClassExtraBytes 拦截该回调,并使用
NtUserConsoleControl 方法设置 tagWNDK 对象的 ConsoleWindow 标志,从而修改窗口类型。

最终回调后,系统不检查窗口类型是否发生变化,由于类型混淆而引用了错误的数据。flag修改前后的区别在于,在设置flag之前,系统认为tagWNDK.pExtraBytes保存了一个user_mode指针;flag设置后,系统认为tagWNDK.pExtraBytes是内核桌面堆的偏移量,攻击者可以控制这个偏移量,从而导致越界R&W。

本篇文章分析了漏洞成因及漏洞利用手法分析,侧重动态调试及利用手法分析。

漏洞影响版本

Windows 10 Version 21H2 for x64-based Systems
Windows 10 Version 21H2 for ARM64-based Systems
Windows 10 Version 21H2 for 32-bit Systems
Windows 11 for ARM64-based Systems
Windows 11 for x64-based Systems
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 21H1 for ARM64-based Systems
Windows 10 Version 21H1 for x64-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 20H2 for x64-based Systems
Windows 10 Version 1909 for ARM64-based Systems
Windows Server 2022 (Server Core installation)
Windows Server 2022
Windows 10 Version 21H1 for 32-bit Systems

分析环境

Windows 10 21H2 19044.1415 x64
Vmware 16.2.1
VirtualKD-Redux 2020.4.0.0
Windbg 10.0.22000.194

背景知识

本节内容描述了创建窗口时需要用到的结构体及函数:

  1. 用户态的窗口数据结构体:WNDCLASSEXW,需要关注cbWndExtra。

  2. 窗口数据保存在内核态时使用:tagWND和tagWNDK结构体,需要关注tagWNDK。

  3. 用户态调用SetWindowLong可以设置窗口扩展内存数据,逆向分析SetWindowLong如何设置窗口扩展内存数据。

窗口类拥有如下属性结构,此处仅列出比较重要的结构:

typedef struct tagWNDCLASSEXW {UINT        cbSize;             //结构体的大小…UINT        style;              //窗口的风格WNDPROC     lpfnWndProc;        //处理窗口消息的回调函数地址int         cbClsExtra;         //属于此类窗口所有实例共同占用的内存大小int         cbWndExtra;         //窗口实例扩展内存大小LPCWSTR     lpszClassName;      //类名…
} WNDCLASSEXW

在用户态创建窗口时,需要调用RegisterClass注册窗口类,每个窗口类有自己的名字,调用CreateWindow创建窗口时传入类的名字,即可创建对应的窗口实例。
当cbWndExtra不为0时,系统会申请一段对应大小的空间,如果回调到用户态申请空间时,可能会触发漏洞。
内核中使用两个结构体来保存窗口数据tagWND和tagWNDK:

ptagWND             //内核中调用ValidateHwnd传入用户态窗口句柄可返回此数据指针0x18 unknown0x80 kernel desktop heap base   //内核桌面堆基址0x28 ptagWNDk   // 需要重点关注这个结构体,结构体在下方:0xA8 spMenu

tagWNDK结构体,需要重点关注此结构体:

struct tagWNDK
{ULONG64 hWnd;               //+0x00ULONG64 OffsetToDesktopHeap;//+0x08 tagWNDK相对桌面堆基址偏移ULONG64 state;              //+0x10DWORD dwExStyle;            //+0x18DWORD dwStyle;              //+0x1CBYTE gap[0x38];DWORD rectBar_Left;         //0x58DWORD rectBar_Top;          //0x5CBYTE gap1[0x68];ULONG64 cbWndExtra;         //+0xC8 窗口扩展内存的大小BYTE gap2[0x18];DWORD dwExtraFlag;          //+0xE8  决定SetWindowLong寻址模式BYTE gap3[0x10];            //+0xECDWORD cbWndServerExtra;     //+0xFCBYTE gap5[0x28];ULONG64 pExtraBytes;    //+0x128 模式1:内核偏移量 模式2:用户态指针
};

当WNDCLASSEXW
中的cbWndExtra值不为0时,创建窗口时内核会回调到用户态函数user32!_xxxClientAllocWindowClassExtraBytes申请一块cbWndExtra大小的内存区域,并且将返回地址保存在tagWNDK结构体的pExtraBytes变量中。

使用函数SetWindowLong和GetWindowLong,可对窗口扩展内存进行读写,进入内核后调用栈如下:

win32kfull!xxxSetWindowLong
win32kfull!NtUserSetWindowLong+0xc7
win32k!NtUserSetWindowLong+0x16
nt!KiSystemServiceCopyEnd+0x25
win32u!NtUserSetWindowLong+0x14
user32!_SetWindowLong+0x6e
CVE_2022_21882!wmain+0x25d

SetWindowLong函数形式如下:

第二个参数为index,含义为设置扩展内存偏移index处的内容。
在win32kfull!xxxSetWindowLong函数中,会对第二个参数index进行判断,防止越界:

137行代码判断index+4如果大于cbWndServerExtra+
cbWndExtra,表明越界,一般情况下cbWndServerExtra为0,如果越界,会跳转到117行LABEL_34,设置v18为1413,跳转到LABEL_55,调用UserSetLastError设置错误值,我们可以在cmd下查看此错误值的含义:

如果没有越界的话,接下来会根据不同的模式来使用pExtraBytes,如下:

在xxxSetWindowLong函数中:

正常情况下cbWndServerExtra为0,157行如果index+4<
cbWndServerExtra,那么修改的是窗口的保留属性,例如GWL_WNDPROC对应-4,含义为设置窗口的回调函数地址。我们需要设置的是窗口扩展内存,所以进入165行的代码区域。

在167行会判断dwExtraFlag属性是否包含0x800,如果包含,那么168行代码destAddress=pExtraBytes+index+内核桌面堆基址,此处pExtraBytes作为相对内核桌面堆基址的相对偏移量,(QWORD)(pTagWnd->field_18+128)为内核桌面堆基地址
,对应的汇编代码为

在171行处,dwExtraFlag属性不包含0x800,此时destAddress=index+pExtraBytes,此处pExtraBytes作为用户态申请的一块内存区域地址。

dwExtraFlag的含义:

dwExtraFlag&0x800 !=
0时,代表当前窗口是控制台窗口。调用AllocConsole申请控制台窗口时,调用程序会与conhost程序通信,conhost去创建控制台窗口,调用栈如下:

conhost获取到窗口句柄后,调用NtUserConsoleControl修改窗口为控制台类型,调用栈如下:

dwExtraFlag&0x800 ==0时,代表当前窗口是GUI窗口,调用CreateWindow时窗口就是GUI窗口。

总结:

  1. xxxSetWindowLong设置扩展内存数据时,有如下两种模式:
    模式1:tagWND的dwExtraFlag属性包含0x800,使用间接寻址模式,基址为内核桌面堆基地址,pExtraBytes作为偏移量去读写内存。
    模式2:tagWND的dwExtraFlag属性不包含0x800,使用直接寻址模式,pExtraBytes直接读写内存。

  2. xxxSetWindowLong会检查index,如果index+4超过cbWndExtra,那么返回索引越界错误。

漏洞成因

此漏洞是对CVE-2021-1732漏洞的绕过,此处简要介绍下CVE-2021-1732漏洞:

用户调用CreateWindow时,在对应的内核态函数中检查到窗口的cbWndExtra不为0,通过xxxCreateWindowEx->
xxxClientAllocWindowClassExtraBytes->调用回调表第123项用户态函数申请用户态空间,

1027行会调用user32!_xxxClientAllocWindowClassExtraBytes,EXP在回调函数中调用NtUserConsoleControl修改窗口的dwExtraFlag和pExtraBytes,修改窗口类型为控制台。

Windows修复代码在1039行,检查pExtraBytes是否被修改,此处查看汇编代码更为清晰


rdi+0x140-0x118 =
rdi+0x28,得到tagWNDK,偏移0x128得到pExtraBytes,判断是否不等于0,如果不等于0,1045行代码会跳转,最终释放窗口,漏洞利用失败。

也就是说:CVE-2021-1732的修复方法是在调用xxxClientAllocWindowClassExtraBytes函数后,在父函数CreateWindowEx中判断漏洞是否被利用了,这个修补方法之前是没有问题的。

但是在后续代码更新后,有了新的路径来触发xxxClientAllocWindowClassExtraBytes函数:

在xxxSwitchWndProc函数中调用xxxClientAllocWindowClassExtraBytes后也有检查pExtraBytes是否为0,如果不为0,那么就复制pExtraBytes内存数据到新申请的内存地址中,没有检查dwExtraFlag是否被修改。

总结:

由于CVE-2021-1732漏洞修补时是在父函数中修复的,虽然当时没有问题,但是当多了xxxClientAllocWindowClassExtraBytes函数的触发路径后,同样的漏洞又存在了,而且
CVE-2021-1732漏洞触发路径是在xxxCreateWindowEx中,此时窗口句柄还未返回给用户态,漏洞利用时需要更多的技巧,此漏洞利用时已经返回了窗口句柄,利用起来更加简单。

利用漏洞的流程

本节介绍了漏洞触发的流程,并介绍了触发漏洞及利用漏洞需要的各个知识点。

漏洞触发利用的流程:

要利用这个漏洞,需要以下背景知识:

6.1 触发用户态回调

本节描述如何触发用户态回调,使内核回调到user32!_xxxClientAllocWindowClassExtraBytes。

在IDA中查看xxxClientAllocWindowClassExtraBytes的引用,有多处地方调用到了此函数,

查看xxxSwitchWndProc代码如下:

98行代码有cbWndServerExtra变量赋值,而在调用SetWindowLong时会使用index-
cbWndServerExtra,所以我们真正想设置内存区域偏移index位置的变量时,参数2应该传入index+cbWndServerExtra。

103行代码调用xxxClientAllocWindowClassExtraBytes返回值赋值给了v20变量。

111行代码检查原来的pExtraBytes是否为0,如果不为0,那么就复制内存的数据,还会释放原来的pExtraBytes。

117、123行代码都会将v20变量赋值给pExtraBytes。

而xxxSwitchWndProc函数是可以通过win32u!
NtUserMessageCall函数来触发的,在用户态调用NtUserMessageCall函数会触发内核态函数xxxClientAllocWindowClassExtraBytes,函数调用栈如下:

win32kfull!xxxClientAllocWindowClassExtraBytes
win32kfull!xxxSwitchWndProc+0x167
win32kfull!xxxWrapSwitchWndProc+0x3c
win32kfull!NtUserfnINLPCREATESTRUCT+0x1c4
win32kfull!NtUserMessageCall+0x11d    内核态
…
win32u! NtUserMessageCall             用户态

在内核态的win32kfull!xxxClientAllocWindowClassExtraBytes函数中,会调用用户态的xxxClientAllocWindowClassExtraBytes函数。
win32kfull!xxxClientAllocWindowClassExtraBytes函数如下:

KernelCallbackTable第123项对应_xxxClientAllocWindowClassExtraBytes函数,使用IDA查看函数内容:

此函数中调用RtlAllocateHeap函数来申请*(a1)大小的内存,内存地址保存在addr变量中,然后调用NtCallbackReturn函数返回到内核态,返回的数据为addr变量的地址,对应在上面win32kfull!xxxClientAllocWindowClassExtraBytes函数中的v7变量,v7为addr变量的地址,*v7即为上图中的addr。

总结:
触发回调函数的路径为:
Win32u!NtUserMessageCall(用户态)->win32kfull!NtUserMessageCall(内核态)->
win32kfull!xxxSwitchWndProc(内核态)->
win32kfull!xxxClientAllocWindowClassExtraBytes(内核态)->
nt!KeUserModeCallback(内核态)->
user32!_xxxClientAllocWindowClassExtraBytes(用户态,HOOK此函数)
本节讲了如何从用户态进入到内核,又回调到user32!_xxxClientAllocWindowClassExtraBytes函数的方法。

6.2 HOOK回调函数

上一小节讲了触发到user32!_xxxClientAllocWindowClassExtraBytes函数的流程,我们还需要hook此回调函数,在回调函数中触发漏洞。下面代码可以将回调函数表项第123、124分别修改为MyxxxClientAllocWindowClassExtraBytes、MyxxxClientFreeWindowClassExtraBytes。

6.3 修改窗口模式为模式1

上一小节讲了如何进入到用户态自定义的函数,本节讲述在自定义的函数中通过用户态未公开函数NtUserConsoleControl修改窗口模式为模式1,本节对NtUserConsoleControl函数进行逆向分析。

函数win32u!
NtUserConsoleControl可以设置模式为内核桌面堆相对寻址模式,此函数有三个参数,第一个参数为功能号,第二个参数为一个结构体的地址,结构体内存中第一个QWORD为窗口句柄,第三个参数为结构体的大小。

NtUserConsoleControl函数会调用到内核态win32kfull模块的NtUserConsoleControl函数,调用栈如下:

win32kfull!NtUserConsoleControl         内核态
win32k!NtUserConsoleControl+0x16        内核态
nt!KiSystemServiceCopyEnd+0x25
win32u!NtUserConsoleControl+0x14        用户态
CVE_2022_21882!wmain+0x3f4              用户态

win32kfull模块NtUserConsoleControl判断参数,然后调用xxxConsoleControl如下:

17行判断参数index不大于6

22行判断参数length小于0x18

26行判断参数2指针不为空且length不为0

以上条件满足时会调用xxxConsoleControl函数,传入参数为index、变量的地址,传入数据的长度,
xxxConsoleControl函数会对index及len进行判断:

110行代码可知,index必须为6,113行代码可知len必须为0x10,115行到119行代码可知,传入参数地址指向的第一个QWORD数据必须为一个合法的窗口句柄,否则此函数会返回。

134、136行判断是否包含0x800属性,如果包含,v23赋值为内核桌面堆基地址+偏移量pExtraBytes,得到的v23为内核地址。

140行代码,如果不包含0x800属性,那么调用DesktopAlloc申请一段cbWndExtra大小的内存保存在v23中。

149到156行代码判断原来的pExtraBytes指针不为空,就拷贝数据到刚申请的内存中,并调用xxxClientFreeWindowClassExtraBytes->user32!_xxxClientFreeWindowClassExtraBy释放内存。

159、160行代码使用内核地址v23减去内核桌面堆基址得到偏移量v21,将v21赋值给pExtraBytes变量。

使用如下代码可以修改窗口模式为模式1:

ULONG64 buff[2]={hwnd};
NtUserConsoleControl(6, &buff, sizeof(buff));即可将hwnd对应的窗口模式设置为模式1。

总结:
在自定义回调函数中调用win32u!NtUserConsoleControl可以设置窗口模式为模式1,传入参数需要符合下列要求:

  1. 参数1 index必须为6

  2. 参数2指向一段缓冲区,缓冲区第一个QWORD必须为一个合法的窗口句柄

  3. 参数3 len必须为0x10

6.4 回调返回伪造偏移量

在_xxxClientAllocWindowClassExtraBytes 函数中调用NtCallBackReturn回调函数可以返回到内核态:

伪造一个合适的偏移量Offset,然后应该取Offset地址传给NtCallbackReturn函数,可以将offset赋值给pExtraBytes变量。

由于之前已经切换窗口为模式1,pExtraBytes含义为相对于内核桌面堆基址的偏移,再查看tagWNDK结构体,关注以下字段:

+0x08   ULONG64 OffsetToDesktopHeap;    //窗口tagWNDK相对桌面堆基址偏移
+0xE8   DWORD dwExtraFlag;              //包含0x800即为模式1
+0x128  ULONG64 pExtraBytes;            //模式1:内核桌面堆偏移量 模式2:用户态指针

OffsetToDesktopHeap为窗口本身地址tagWNDK相对于内核桌面堆基址的偏移,可以使用如下方法来伪造合适的偏移量:

  1. 创建多个窗口,如窗口0和窗口2(为了与EXP匹配),窗口2触发回调函数,返回窗口0的OffsetToDesktopHeap ,赋值给窗口2的pExtraBytes变量。

  2. 对窗口2调用SetWindowLong时,写入的目标地址为:内核桌面堆基址+pExtraBytes+index,此时pExtraBytes为窗口0的地址偏移,对窗口2调用SetWindowLong可以写窗口0的tagWNDK结构数据,这是第一次越界写。

总结:

调用NtCallbackReturn可以返回到内核中,伪造偏移量为窗口0的OffsetToDesktopHeap,赋值给窗口2的pExtraBytes,当对窗口2调用SetWindowLong时即可修改到窗口0的tagWNDK结构体。
接下来我们需要获取窗口0的OffsetToDesktopHeap。

6.5 泄露内核窗口数据结构

上一小节中我们在用户态中要返回窗口0的OffsetToDesktopHeap到内核态,OffsetToDesktopHeap是内核态的数据,要想获取这个数据还需要一些工作。

调用CreateWindow只能返回一个窗口句柄,用户态无法直接看到内核数据,但是系统把tagWNDK的数据在用户态映射了一份只读数据,只需要调用函数HMValidateHandle即可,动态库中没有导出此函数,需要通过IsMenu函数来定位:

定位user32!HMValidateHandle的代码如下:

定位到user32!HMValidateHandle函数地址后,传入hwnd即可获取tagWNDK数据地址。

tagWNDK* p = HMValidateHandle(hwnd),通过tagWNDK指针即可获取到OffsetToDesktopHeap数据。

6.6 如何布局内存

通过上面的知识,我们可以通过窗口2修改窗口0的tagWNDK结构体数据,本节描述如何布局内存,构造写原语。

应该通过NtUserConsoleControl修改窗口0切换到模式1,这样对窗口0调用SetWindowLong即可修改内核数据,但是调用SetWindowLong时index有范围限制,所以通过窗口2将窗口0的tagWNDK.
cbWndExtra修改为0xFFFFFFFF,扩大窗口0可读写的范围。

现在我们开始内存布局:

1.创建窗口0,窗口0切换到模式1,pExtraBytes为扩展内存相对内核桌面堆基址的偏移量

窗口2触发回调后,回调函数中对窗口2调用NtUserConsoleControl,所以窗口2也处于模式1,pExtraBytes为扩展内存相对内核桌面堆基址的偏移量。

2.回调函数中返回窗口0的OffsetToDesktopHeap,此时内存如下:

图中红色线条,此时窗口2的pExtraBytes为窗口0的OffsetToDesktopHeap,指向了窗口0的结构体地址,此时对窗口2调用SetWindowLong即可修改窗口0的内核数据结构

3.通过窗口2修改窗口0的cbWndExtra

SetWindowsLong(窗口2句柄,
0xC8(此处还有一个偏移量),0xFFFFFFFF),即可修改窗口0的cbWndExtra为极大值,且此时窗口0处于模式1,如果传入一个较大的index且不大于0xFFFFFFFF,那么就可以越界修改到内存处于高地址处的其他窗口的数据。

4.再次创建一个窗口1,窗口1处于模式2,不用修改模式

窗口1刚开始pExtraBytes指向用户态地址,使用模式2直接寻址。
由于窗口0的pExtraBytes是相对于内核桌面堆基址的偏移量,窗口1的OffsetToDeskTopHeap是当前tagWNDK结构体与内核桌面堆基址的偏移量,所以这两个值可以计算一个差值,对窗口0调用SetWindowLong时传入这个差值即可写入到窗口1的结构体,再加上pExtraBytes相对于tagWNDK结构体的偏移即可设置窗口1的pExtraBytes为任意值。

5.由于此时窗口1处于模式1直接寻址,且我们可以设置窗口1扩展内存地址pExtraBytes为任意地址,所以对窗口1调用SetWindowLong即可向任意内核地址写入数据。

总结:

内存布局的关键在于窗口0的pExtraBytes必须小于窗口1和窗口2的OffsetToDesktopHeap,这样的话在绕过了窗口0的cbWndExtra过小的限制后,对窗口0调用SetWindowLong传入的第二个参数,传入一个较大值,即可向后越界写入到窗口1和窗口2的tagWNDK结构体。

我们来设想一下不满足内存布局的情况,假如窗口1的OffsetToDesktopHeap小于窗口0的pExtraBytes,即窗口1的tagWNDK位于低地址,窗口0的扩展内存位于高地址,那从窗口0越界往低地址写内容时,SetWindowLong的index必须传入一个64位的负数,但是SetWindowLong的第二个参数index是一个32位的值,调用函数时64位截断为32位数据,在内核中扩展到64位后高位为0还是个正数,所以窗口0无法越界写到低地址。

EXP分析调试

首先动态定位多个函数地址,接下来需要调用

创建窗口类:

#define MAGIC_CB_WND_EXTRA 0x1337

调用函数RegisterClassEx创建两个窗口类:

类名为NormalClass的窗口,窗口的cbWndExtra大小为0x20。

类名为MagicClass的窗口,窗口的cbWndExtra大小为0x1337,使用MagicClass类创建的窗口会利用漏洞构造一个内核相对偏移量。

内存布局的代码如下:

第241行到244行,创建了菜单,之后创建窗口使用此菜单。

第245行到250行,使用NormalClass类名创建了50个窗口存放在g_hWnd数组中,然后销毁后面的48个窗口,这样是为了后面创建窗口时可以占用被销毁窗口的区域,缩短窗口之间的间距,此时g_hWnd[0]和g_hWnd[1]存放句柄,将这两个窗口称为窗口0和窗口1,其中247行调用HMValidateHandle函数传入句柄得到对应窗口在用户态映射的tagWNDK数据内存地址保存在g_pWndK数组中。

第245行到255行,调用NtUserConsoleControl函数设置窗口0由用户态直接寻址切换为内核态相对偏移寻址,并且窗口0的pExtraBytes是相对于内核桌面堆基址的偏移。

第257行到258行,使用MagicClass类名创建窗口2保存在g_hWnd[2]中,称为窗口2,然后调用HMValidateHandle获得窗口2的tagWNDK数据映射地址保存在g_pWndK[2]中。

第260和278行代码判断内存布局是否成功,此时窗口0处于内核模式,所以窗口0的pExtraBytes为申请的内核内存空间(不是窗口内核对象地址)相对于内核桌面堆基地址的偏移,窗口1和窗口2为用户态模式,OffsetToDesktopHeap为窗口内核对象地址相对于内核桌面堆基地址的偏移,内存布局必须满足:

窗口0的pExtraBytes小于窗口1的OffsetToDesktopHeap,计算差值extra_to_wnd1_offset,为正数。

窗口0的pExtraBytes小于窗口2的OffsetToDesktopHeap,计算差值extra_to_wnd2_offset,为正数。

如果布局失败,那就销毁窗口继续布局,如果最后一次布局失败,就退出。

布局完成后,程序运行到此处:

程序在虚拟机中运行到DebugBreak()函数时,如果有内核调试器,调试器会自动中断:

此时指令位于DebugBreak函数中,输入k,栈回溯只显示了地址,没有显示符号表,输入

gu;.reload /user

.reload /user会自动加载用户态符号,pdb文件位于本地对应目录,再次输入k,显示栈回溯,可以看到显示正常。
我们先查看三个窗口的内核数据结构
使用命令 dt tagWNDK
poi(CVE_2022_21882!g_pWndK+0)可以以结构体方式查看窗口0的tagWNDK结构,在内存布局时已经对窗口0切换了模式,如下:

在调用NtUserMessageCall之前,窗口0处于模式1,窗口1和2处于模式2。
接下来调用HookUserModeCallBack 来Hook回调函数,代码如下:

动态调试时查看KernelCallbackTable表:

kd> !peb
PEB at 0000001eb0c75000
kd> dt ntdll!_PEB KernelCallbackTable 0000001eb0c75000
+0x058 KernelCallbackTable : 0x00007ffe`bc6f2070 Void

查看KernelCallbackTable表项

我们需要查看123项的内容,如下:

调试运行HookUserModeCallBack函数后,再次查看:

在自定义的回调函数MyxxxClientAllocWindowClassExtraBytes中

接着下断点:

并且在MyxxxClientAllocWindowClassExtraBytes函数中下断点:

在调试器中输入g运行,现在运行到如下位置:

在运行NtUserConsoleControl前后分别查看窗口2的模式:

继续按g运行,中断在SetWindowLong函数前

此时窗口2处于模式1,并且pExtraBytes为窗口0的OffsetToDesktopHeap,再调用SetWindowLong函数:

这是第一次越界写,第一个参数为窗口2的句柄,第二个参数为index,为cbWndExtra相对tagWNDK结构体首地址的偏移量+cbWndServerExtra,由于窗口2调用了NtUserMessageCall,所以cbWndServerExtra为0x10,调用SetWindowLong时会使用index-
cbWndServerExtra,所以此处要加上cbWndServerExtra来抵消,可参考前文SetWindowLong函数的分析。

单步运行后

可以看到窗口0的cbWndExtra变成了0xFFFFFFFF,接下来对窗口0调用SetWindowLong时传入index可以传入之前计算得到的extra_to_wnd1_offset和extra_to_wnd2_offset来分别修改窗口1和窗口2的窗口内核数据。

此时窗口1处于直接寻址模式,对窗口0调用SetWindowLongPtr修改窗口1的pExtraBytes为任意值,使用SetWindowLongPtr是因为此函数第三个参数可以传入64位数据,将窗口1的pExtraBytes设置为任意值,接下来对窗口1调用SetWindowLong即可实现任意地址写数据。

两种提权方式

8.1 设置token

第一种为设置当前进程的token为system进程的token,将当前进程提升到system权限,这种需要读取进程的EPROCESS结构,再定位到token变量的地址,修改token,公开的EXP中使用GetMenuBarInfo函数来实现内核任意地址读原语。

我们先分析这种方式,先看下Menu内核结构体:

ptagWND0x10 THREADINFO0x1A0 PROCESSINFO0x00 EPROCESS0x18 unknown0x80 kernel desktop heap base0x28 ptagWNDk0xA8 spMenu0x28 obj280x2C cItems(forcheck)       设置为10x40 cxMenu(forcheck)           设置为10x44 cyMenu(forcheck)           设置为10x50 ptagWND0x58 rgItems0x00 unknown(forexploit)    //要读的地址-0x400x98 ppMenu0x00 pSelf                  //指向spMenu

在EXP中先构造一个假的Menu

其中401行设置ppMenu偏移0x00处的值为spMenu,404、408、409设置spMenu结构体内部数据是为了绕过GetMenuBarInfo的验证,GetMenuBarInfo函数会调用内核中的NtUserGetMenuBarInfo,最终调用到xxxGetMenuBarInfo,GetMenuBarInfo对应有四个参数,对应xxxGetMenuBarInfo的四个参数,其中参数2为idObject,参数3为idItem。
xxxGetMenuBarInfo对参数有校验:

164行判断idObject!=-3如果满足,就不能触发到下面读内存的代码路径,所以idObject必须为-3。

316行代码判断dwStyle不能包含WS_CHILD属性。

322行代码从spMenu中偏移0x98取值,赋值给ppMenu。

325行代码判断idItem不能小于0。

328行代码判断idItem不能大于spMenu偏移0x28取值再偏移0x2c取值。

335行代码判断spMenu偏移0x40取值不为0并且偏移0x44取值不为0。

338行到344行,如果idItem不为0,可以让idItem为1,那么_readAddrSub40的值为spMenu偏移0x58取值。

接下来程序进入353行

v5是传入的第四个参数,用作保存读取到的数据。

在353、354行,可以读取传入地址的数据+窗口RECT的left坐标。

在357、358行,可以读取传入地址的数据+4+窗口RECT的top坐标。

所以只要我们可以绕过构造一个假的Menu,绕过上述限制,在Menu偏移0x58再偏移0x00的地址处存放想读取的地址-0x40,当GetMenuBarInfo返回时left和top中保存的就是目标地址处的8字节数据。

要想替换窗口的Menu为假的Menu,还是需要用到SetWindowLong函数,在内核态win32kfull!xxxSetWindowLong函数中会调用xxxSetWindowData函数:

xxxSetWindowData函数如下:

134、136行,判断如果index为0xFFFFFFF4,为-12,对应为GWLP_ID。

138行判断如果dwStyle是否包含WS_CHILD属性。

140行取出原来的menu指针,赋值给retValue,最终会作为用户态SetWindowLong函数的返回值。

142行修改spMenu为SetWindowLong传入第三个参数newValue值。

所以我们需要如下步骤才能完成任意地址读:

  1. 先对窗口0使用内核越界写修改窗口1的dwStyle值为包含WS_CHILD,这样调用SetWindowLong时即可绕过上面138行的判断。

  2. 对窗口1使用SetWindowLong函数传入index为GWLP_ID,修改窗口1的Menu为构造的假的Menu,并且SetWindowLong会返回原先的Menu的地址。

  3. 使用原先的Menu通过内核数据结构即可定位到当前进程的EPROCESS,进而定位到token的地址。

  4. 再次对窗口0使用内核越界写修改窗口1的dwStyle值为不包含WS_CHILD,这样调用GetMenuBarInfo时可以绕过xxxGetMenuBarInfo中316行代码的判断。

  5. 需要读取数据时,将目标地址-0x40赋值给假的Menu偏移0x58对应的内存空间中,再调用GetMenuBarInfo函数。

单步运行413行代码,窗口1的dwStyle就包含了WS_CHILD属性。


可以看到修改完成后,窗口1的dwStyle包含了WS_CHILD属性。

继续执行415行代码:

在416行下断点后运行:

此时SetWindowLong函数刚执行完毕,返回值rax为0xfffffa49c0821e60,保存的是旧的spMenu指针,而根据之前的数据结构,可以使用spMenu定位到当前进程的EPROCESS。

执行419行代码,移除窗口1的WS_CHILD属性,为接下来调用GetMenuBarInfo做准备


窗口1的dwStyle移除了WS_CHILD属性。
然后构造读原语如下:

根据之前的数据结构

ptagWND0x10 THREADINFO0x1A0 PROCESSINFO0x00 EPROCESS0x18 unknown0x80 kernel desktop heap base0x28 ptagWNDk0xA8 spMenu0x50 ptagWND

所以获取到spMenu后可以使用如下代码来获取当前进程的EPROCESS

在调试器中查看如下:

上图中可以看到通过spMenu取偏移和使用命令.process两种方式获取到的EPROCESS值是一致的。

查看当前进程的token

kd> !token
…
Privs:
19 0x000000013 SeShutdownPrivilege               Attributes -
23 0x000000017 SeChangeNotifyPrivilege           Attributes - Enabled Default
25 0x000000019 SeUndockPrivilege                 Attributes -
33 0x000000021 SeIncreaseWorkingSetPrivilege     Attributes -
34 0x000000022 SeTimeZonePrivilege               Attributes -
…

我们直接运行到454行,此时当前进程的token被替换为系统token

EPROCESS中token结构体为_EX_FAST_REF

kd> dt _EX_FAST_REF
ntdll!_EX_FAST_REF
+0x000 Object           : Ptr64 Void
+0x000 RefCnt           : Pos 0, 4 Bits
+0x000 Value            : Uint8B

调试运行到454行,重新运行一次,所以EPROCESS值与之前不一样。

可以看到此时调用到if(iCount<5000),_EX_FAST_REF结构体中的object值已经修改了。

查看system进程的EPROCESS

kd> dt nt!_EX_FAST_REF ffffe504`89885080+0x4b8
+0x000 Object           : 0xffffbe09`9a242744 Void
+0x000 RefCnt           : 0y0100
+0x000 Value            : 0xffffbe09`9a242744

system进程_EX_FAST_REF的Object也为0xffffbe09`9a242744,当前进程修改成功,使用!token命令验证下:

修改token的代码如下:

  1. EPROCESS结构体中有一个进程链表,保存了当前系统的所有进程,我们主要关注ActiveProcessLinks和UniqueProcessId属性
kd> dt nt!_EPROCESS
+0x000 Pcb                : _KPROCESS
+0x438 ProcessLock        : _EX_PUSH_LOCK
+0x440 UniqueProcessId    : Ptr64 Void     //进程ID
+0x448 ActiveProcessLinks : _LIST_ENTRY    //进程链表

通过遍历进程链表ActiveProcessLinks,找到进程PID
UniqueProcessId为4的system进程,偏移0x4b8得到_EX_FAST_REF结构体地址,取出Object的值。

  1. 之前eprocess变量中保存了当前进程的EPROCESS地址,定位到_EX_FAST_REF结构体地址

  2. 通过窗口0越界写窗口1的pExtraBytes,传入第二步找到的地址,下面448行代码。

449行通过窗口1调用SetWindowLong设置Object修改值为第一步找到的Object。

450行代码恢复窗口1的pExtraBytes。

恢复内核数据:

407行到414行都是为了恢复内核窗口内容,防止蓝屏。

408行设置窗口2的pExtraBytes为正常的用户态指针。

409行设置窗口2的dwExtraFlag不包含0x800属性,即从模式1修改为模式2。

411到414行恢复窗口1的Menu指针。

418行恢复KernelCallbackTable表项。

自定义的释放内存的回调函数MyxxxClientFreeWindowClassExtraBytes,判断如果是特定窗口,就不释放内存,直接返回。

最终在回调函数表中恢复此项,释放窗口2的pExtraBytes,之前恢复内核数据代码处设置了窗口2的pExtraBytes为RtlAllocateHeap返回的指针。

8.2 修改Privileges

第二种漏洞利用要修改token的变量Privileges,这种实现相对来说简单,不需要构造写原语,为当前进程添加SE_DEBUG权限并启用,遍历进程,过滤与当前进程位于同一session下的winlogon登录进程,此进程是system权限,打开此进程并注入代码执行。

背景知识:

要打开系统安全进程和服务进程,并且有写入数据权限,需要当前进程拥有SeDebugPrivilege权限,这个是调试进程会用到的权限,当一个进程启动后,正常情况下,是无法提升权限的,正向开发时使用的AdjustTokenPrivileges函数只能是启用某个权限或者禁用某个权限。

之前我们已经实现了任意地址写数据,窗口1本身为用户态直接寻址模式,通过设置窗口1的pExtraBytes值为任意值,调用SetWindowLongPtr时即可对任意地址写数据,上一种利用手法是调用SetWindowsLong来构造写原语,调用GetMenuBarInfo来构造读原语,然后通过EPROCESS的ActiveProcessLinks链遍历进程,当进程号为4时,认为是system进程,获取system的Token变量覆盖到当前进程的Token,当前进程就提权到了system级别。

漏洞利用思路为:使用OpenProcessToken打开当前进程调整权限的句柄,使用NtQuerySystemInformation函数泄露句柄在内核中的地址,泄露出的地址为进程Token在内核中的地址,然后偏移0x40:

0: kd> dt _TOKEN
nt!_TOKEN
…
+0x040 Privileges       : _SEP_TOKEN_PRIVILEGES
…

在EPROCESS结构体中的token变量类型为nt!_EX_FAST_REF

kd> dt nt!_EX_FAST_REF
+0x000 Object           : Ptr64 Void
+0x000 RefCnt           : Pos 0, 4 Bits
+0x000 Value            : Uint8B

其实这个结构体中Object才属于TOKEN结构体,但Object的值不是简单的对应TOKEN结构体,而是需要经过计算,上面的结构体中RefCnt也是位于偏移0x00,只占4位,这四位表示了Object对象的引用计数,这里我们使用上面第一种利用方法利用成功后的数据

kd> dt nt!_EX_FAST_REF ffffe504`89885080+0x4b8
+0x000 Object           : 0xffffbe09`9a242744 Void
+0x000 RefCnt           : 0y0100
+0x000 Value            : 0xffffbe09`9a242744

Object为0xffffbe09`9a242744,RefCnt 为0y0100,需要经过如下换算才可以:

0xffffbe09`9a242744&0xFFFFFFFFFFFFFFF0=0xffffbe09`9a242740

Windbg中查看:


Token偏移0x40为Privileges,Privileges中Present和Enable分别表明进程当前是否可以启用对应权限和是否启用了对应权限,EnabledByDefault是默认启用了对应权限,EnabledByDefault这个变量不需要修改,都是8字节数据,如果将Present和Enable都修改为0xFFFFFFFFFFFFFFFF,

在windbg中可以看到位与权限对应关系如下:

其中2位到32位是有效数据,我们只需要启用第20位SeDebugPrivilege权限就可以打开winlogon进程,之后注入shellcode,运行shellcode启动一个system级别的cmd进程。

内存布局与之前的第一种利用方法一样,接着hook回调函数,对窗口2调用NtUserMessageCall,接下来就不一样了:

调用LeakEporcessKtoken泄露token的地址,

LeakEporcessKtoken函数调用OpenProcessToken打开自身进程的token,第二个参数访问掩码设置为TOKEN_ADJUST_PRIVILEGES,为调整令牌权限,然后调用GetKernelPointer泄露token的内核地址:


其中结构体SYSTEM_HANDLE_TABLE_ENTRY_INFO和SYSTEM_HANDLE_INFORMATION在移植到64位版本时,笔者有对结构体内容进行一些修正,结构体中都多了一个变量ULONG
xxxCDCDCD用来占位,保持8字节对齐。
泄露token地址后,token+0x40即可定位到Privileges变量地址,

313行通过窗口0越界写修改窗口1的pExtraBytes为token+0x40,定位到Privileges。

314到319行,设置新的权限值,其实只需要设置第20位,但是此处设置了第2到第36位都为1。

320行设置Present属性。

321行设置Enabled属性。

322行恢复窗口1的pExtraBytes值。

324行定位winlogon进程的pid,此处需要注意如果有多个用户登录那么存在多个winlogon进程,需要找到跟当前进程处于同一会话中的winlogon进程,否则最终启动的cmd当前用户无法看到。

325行写shellcode到winlogon进程中并执行。

328到331行是为了修复窗口内核数据。

总结两种漏洞利用方法的优劣:
第一种方法:对比第二种稍微有点复杂,要构造读写原语,优势在于不管是低权限进程还是中等权限进程都可以进行提权。

第二种方法:只需要构造一个写原语,然后开启各种权限,通过注入的方法来获取高权限,相对难度低点,但是要调用NtQuerySyetemInformation函数至少需要中等权限,对权限要求较高。

补丁分析

此漏洞对应的补丁为KB5009543,打补丁后调用NtUserMessageCall时触发到内核函数的调用栈如下:

win32kfull!xxxClientAllocWindowClassExtraBytes
win32kfull!xxxValidateClassAndSize+0x171
win32kfull!xxxSwitchWndProc+0x5a
win32kfull!xxxWrapSwitchWndProc+0x3c
win32kfull!NtUserfnINLPCREATESTRUCT+0x1c4
win32kfull!NtUserMessageCall+0x11d
win32k!NtUserMessageCall+0x3d

在函数xxxClientAllocWindowClassExtraBytes中调用回调函数后,内核函数对窗口的dwExtraFlag属性校验:

43行判断dwExtraFlag是否包含0x800属性,如果包含,说明用户态函数被hook,当前函数返回值不使用用户态申请的空间,而是返回0,返回到xxxValidateClassAndSize函数后,

判断返回值为0,直接返回,不会再去修改pExtraBytes为用户伪造的值。

最后

分享一个快速学习【网络安全】的方法,「也许是」最全面的学习方法:
1、网络安全理论知识(2天)
①了解行业相关背景,前景,确定发展方向。
②学习网络安全相关法律法规。
③网络安全运营的概念。
④等保简介、等保规定、流程和规范。(非常重要)

2、渗透测试基础(一周)
①渗透测试的流程、分类、标准
②信息收集技术:主动/被动信息搜集、Nmap工具、Google Hacking
③漏洞扫描、漏洞利用、原理,利用方法、工具(MSF)、绕过IDS和反病毒侦察
④主机攻防演练:MS17-010、MS08-067、MS10-046、MS12-20等

3、操作系统基础(一周)
①Windows系统常见功能和命令
②Kali Linux系统常见功能和命令
③操作系统安全(系统入侵排查/系统加固基础)

4、计算机网络基础(一周)
①计算机网络基础、协议和架构
②网络通信原理、OSI模型、数据转发流程
③常见协议解析(HTTP、TCP/IP、ARP等)
④网络攻击技术与网络安全防御技术
⑤Web漏洞原理与防御:主动/被动攻击、DDOS攻击、CVE漏洞复现

5、数据库基础操作(2天)
①数据库基础
②SQL语言基础
③数据库安全加固

6、Web渗透(1周)
①HTML、CSS和JavaScript简介
②OWASP Top10
③Web漏洞扫描工具
④Web渗透工具:Nmap、BurpSuite、SQLMap、其他(菜刀、漏扫等)

恭喜你,如果学到这里,你基本可以从事一份网络安全相关的工作,比如渗透测试、Web 渗透、安全服务、安全分析等岗位;如果等保模块学的好,还可以从事等保工程师。薪资区间6k-15k。

到此为止,大概1个月的时间。你已经成为了一名“脚本小子”。那么你还想往下探索吗?

想要入坑黑客&网络安全的朋友,给大家准备了一份:282G全网最全的网络安全资料包免费领取!
扫下方二维码,免费领取

有了这些基础,如果你要深入学习,可以参考下方这个超详细学习路线图,按照这个路线学习,完全够支撑你成为一名优秀的中高级网络安全工程师:

高清学习路线图或XMIND文件(点击下载原文件)

还有一些学习中收集的视频、文档资源,有需要的可以自取:
每个成长路线对应板块的配套视频:


当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料&工具,并且已经帮大家分好类了。

因篇幅有限,仅展示部分资料,需要的可以【扫下方二维码免费领取】

CVE-2022-21882 Win32k内核提权漏洞深入分析相关推荐

  1. [CVE-2021-1732] win32k内核提权漏洞分析

    CVE-2021-1732是今年二月份被披露的蔓灵花(BITTER)APT组织在某次攻击行动中使用的0Day漏洞[1][2][3].该漏洞利用Windows操作系统win32k内核模块一处用户态回调机 ...

  2. 从Dump到POC系列一:Win32k内核提权漏洞分析

    转载自:http://blogs.360.cn/blog/dump-to-poc-to-win32k-kernel-privilege-escalation-vulnerability/ 1.引言 近 ...

  3. Linux Privilege Escalation Kernel Exploits | Linux本地内核提权漏洞复现 CVE-2015-1328

    Linux Privilege Escalation Kernel Exploits | Linux本地内核提权漏洞复现 CVE-2015-1328 文章目录 Linux Privilege Esca ...

  4. Microsoft Windows Win32k本地提权漏洞分析

    漏洞信息 1 漏洞简介 漏洞名称:Microsoft Windows Win32k本地提权漏洞 漏洞编号:CVE-2015-2546 漏洞类型:UAF 影响范围:Windows 7 Service P ...

  5. 【经典漏洞回顾】Microsoft Windows Win32k本地提权漏洞分析(CVE-2015-0057)

    漏洞信息 1 漏洞简介 漏洞名称:Microsoft Windows Win32k本地提权漏洞 漏洞编号:CVE-2015-0057 漏洞类型:UAF 影响范围:Windows Server 2003 ...

  6. str045漏洞提权linux,Linux运维知识之CVE-2016-5195 Dirtycow: Linux内核提权漏洞

    本文主要向大家介Linux运维知识之CVE-2016-5195 Dirtycow: Linux内核提权漏洞绍了,通过具体的内容向大家展现,希望对大家学习Linux运维知识有所帮助. CVE-2016- ...

  7. linux漏洞知乎_Linux本地内核提权漏洞(CVE-2019-13272)

    0x00 简介 2019年07月20日,Linux正式修复了一个本地内核提权漏洞.通过此漏洞,攻击者可将普通权限用户提升为Root权限. 0x01 漏洞概述 当调用PTRACE_TRACEME时,pt ...

  8. CVE-2022-0847 Linux内核提权漏洞分析

    文章目录 前言 漏洞复现 1.1 文件覆写poc/exp 1.2 覆写/etc/passwd 漏洞分析 2.1 Linux管道机制 2.2 splice系统调用 2.3 漏洞利用流程 总结 前言 20 ...

  9. CVE-XX-XX:“Atom截胡”Windows内核提权漏洞分析

    可能要戴上眼镜好好看,图有点刺眼,有点不清 作者:PlayBoy23333 稿费:500RMB(不服你也来投稿啊!) 投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿 a)简介 前 ...

最新文章

  1. 在家想远程公司电脑?Python +微信一键连接
  2. leetcode算法第三题
  3. Zookeeper 入门
  4. HDU - 4461 The Power of Xiangqi
  5. boke练习: spring boot: security post数据时,要么关闭crst,要么添加隐藏域
  6. 新华计算机学校环境好吗,新华电脑校园环境好不好?(二)
  7. Linux下增加swap分区
  8. MD5消息摘要算法和SHA-1安全散列算法
  9. 国外计算机应用基础,计算机应用基础试题(国外英文资料).doc
  10. 计算机网络按照工作模式可分为什么和什么,计算机网络的分类有哪些?计算机网络工作模式...
  11. 电脑显示器黑屏故障全解析
  12. 斯皮尔曼相关(Spearman correlation)系数概述及其计算例
  13. 如何自动化入侵海康设备
  14. spss假设检验_SPSS有序Logistic回归的具体操作——「杏花开生物医药统计」
  15. 利用栈来完成表达式求值
  16. 软件测试记录包括哪些,bug记录里通常包括哪些内容?
  17. 区块链究竟有什么价值体现?
  18. 从零开始嵌入聊天机器人服务(小白适用)
  19. 让人们久等了的TCP BBR v2.0快要出炉了!
  20. CSS子元素选择父元素

热门文章

  1. linux数据泄露,Linux Kernel 5.9.1 及更早版本发现数据泄露和特权升级漏洞,需尽快升级...
  2. 优思学院|PDCA与PDSA到底有何分别?
  3. docker入门实践,制作属于自己的mysql5.7镜像
  4. 自考计算机原理知识点,(完整版)18版自考04741《计算机网络原理》知识点——第一章...
  5. 第四章 数据库应用系统功能设计与实施
  6. 部份AirPods Pro出现声音问题 Apple宣布召回计划
  7. 华为编程大赛之括号匹配
  8. matlab中RGB空间转化为LAB空间的颜色转换
  9. Linux - BBS项目学习 - 项目的完整部署(LNM+Python+Django+uwsgi+redis)
  10. 腾讯云服务器CVM(CentOS 7、Tencent Linux)手动搭建LNMP环境(linux+Nginx+Mariadb+PHP)