写点什么

网络安全之内核提权漏洞深入分析

  • 2022 年 4 月 28 日
  • 本文字数:7991 字

    阅读完需:约 26 分钟

背景知识

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


  1. 用户态的窗口数据结构体:WNDCLASSEXW。

  2. 窗口数据保存在内核态时使用:tagWND 和 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 unknown        0x80 kernel desktop heap base   //内核桌面堆基址    0x28 ptagWNDk   // 需要重点关注这个结构体,结构体在下方:    0xA8 spMenu
复制代码


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


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


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


【一>所有资源获取<一】1、很多已经买不到的绝版电子书 2、安全大厂内部的培训资料 3、全套工具包 4、100 份 src 源码技术文档 5、网络安全基础入门、Linux、web 安全、攻防方面的视频 6、应急响应笔记 7、 网络安全学习路线 8、ctf 夺旗赛解析 9、WEB 安全入门笔记


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


win32kfull!xxxSetWindowLongwin32kfull!NtUserSetWindowLong+0xc7win32k!NtUserSetWindowLong+0x16nt!KiSystemServiceCopyEnd+0x25win32u!NtUserSetWindowLong+0x14USER32!_SetWindowLong+0x6eCVE_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!xxxClientAllocWindowClassExtraByteswin32kfull!xxxSwitchWndProc+0x167win32kfull!xxxWrapSwitchWndProc+0x3cwin32kfull!NtUserfnINLPCREATESTRUCT+0x1c4win32kfull!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+0x25win32u!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 可读写的范围。


现在我们开始内存布局:


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



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


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



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


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



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


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



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


由于此时窗口 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。

用户头像

我是一名网络安全渗透师 2021.06.18 加入

关注我,后续将会带来更多精选作品,需要资料+wx:mengmengji08

评论

发布
暂无评论
网络安全之内核提权漏洞深入分析_网络安全_网络安全学海_InfoQ写作社区