CVE-2021-1732分析

分析环境

win10_20H2_update_2021_jan

漏洞成因

在使用函数CreatWindowEx创建窗口时,如果所注册的窗口类wndclass->cbWndExtra不为0,则CreateWindowEx会调用函数win32kfull!xxxClientAllocWindowClassExtraBytes给pExtraBytes来分配大小为cbWndExtra的空间,该函数会通过用户模式回调调用user32!_xxxClientAllocWindowClassExtraBytes函数分配空间,并将分配空间的地址以结果返回。

文章中相关结构

漏洞的利用流程

关键函数逆向分析

xxxCreateWindowEx


该位置位于xxxCreateWindowEx,对(*(tagWnd+0x28)+0xC8)处,即cbWndExtra,进行判断,如果当前值为不为0,则调用xxxClientAllocWindowClassExtraBytes进行内存分配

xxxClientAllocWindowClassExtraBytes


接着进入xxxClientAllocWindowClassExtraBytes,第25行通过用户模式回调(调用号为0x7b),并返回结果第四个参数AllocAddress,下面对AllocAddress进行判断是否为溢出或者超过了用户模式的内存限制。

__xxxClientAllocWindowClassExtraBytes


可以看到当前函数通过RtlAllocateHeap从pUserHeap分配内存,并放入Result进行返回(注意这里Result是个结构体,返回的大小为0x18),如果Hook掉该函数,修改Result为任意值,并通过NtConsoleControl改变pExtraBytes的属性,就可以通过SetWindowLongPtr进行任意地址写操作了。

xxxConsoleControl


可以看到xxxConsoleControl首先会对pwnd->flags进行判断:

  1. 如果当前标志位存在(即flag&0x800!=0),则pExtraBytes表示为相对于DesktopHeapBase的偏移,并将计算之后的结果存入Object[3]中,
  2. 如果当前标志位不存在,则通过DesktopAlloc分配一块大小为cbWndExtra的空间,并在最后将pwnd->flag 进行添加标志位0x800。

注意:观察line143~152,如果此时pwnd->pExtraBytes存在,则会将其中的内存拷贝至新开辟的空间v22中,同时会将pwnd->pExtraBytes进行修过,所以必须在回调函数中调用xxxConsoleControl(xxxCreateWindow还没给pwnd->pExtraBytes进行赋值),否则会读取错误,且覆盖掉构造好的内容。(踩坑+1)

xxxSetWindowLongPtr


从154行起存在几个检查部分:

  1. 判断nIndex是否大于0
  2. 判断nIndex是否小于pwnd->cbWndExtra
  3. 其他无关紧要的

正常流程会进入188行,接着会判断pwnd->flag是否包含标志0x800来选择偏移寻值或内存地址寻值。由于之前调用了xxxConsoleControl函数,将flag已经进行修改,所以采用偏移寻值(此处的v27在调试过程中等于nIndex,所以无须在意somenumber)。
修改的值所在地址为:DesktopHeapBase+pExtraBytes+nIndex。

xxxGetMenuBarInfo


构造写原语。
从tagWnd中获取spMenu对象,并从319行开始进行几个判断:

  1. spMenu->0x40 && spMenu->0x44 的计算结果为TRUE
  2. idItem不为0
  3. 窗口的属性pwnd->0x1A&0x40!=0,即拥有WS_CHILD属性

以上条件满足则读取目标地址+0x40(329行)的值,并存入MENUBARINFO->rcBar(类型为RECT)中。
该部分读取是相当于将一个16字节的数据拆成4份放到一个矩阵描述的结构体中,所以需要进行重新排列才能还原出真实数据。

漏洞利用&Exp编写

按照漏洞利用流程来看,首先需要对xxxClientAllocWindowClassExtraBytes的用户回调进行Hook修改。

那么大致的框架如下:

NTSTATUS WINAPI MyxxxClientAllocWindowClassExtraBytes(unsigned int* a1)
{
    ULONG_PTR ConsoleCtrlInfo[2] = {0};
    ConsoleCtrlInfo[0] = (ULONG_PTR)hwndTarget;
    ConsoleCtrlInfo[1] = 0;
    NtUserConsoleControl(0x6,&ConsoleCtrlInfo,sizezof(ConsoleCtrl));
    
    ULONG_PTR Result[3];
    Result[0] = SomeAddressCanModify;
    Result[1] = 0;
    Result[2] = 0;

    return g_NtCallbackReturn(&Result,0x18,0);
}

但是会发现一个问题,当前函数是CreateWindowEx调用中的一部分,CreateWindowEx函数还未结束,不能返回HWND句柄,所以在ConsoleCtrlInfo中,对应的hwndTarget句柄值无法得到,陷入了“先有鸡还是先有蛋”的问题,此时,出现的场景与内存漏洞利用相似,也可以使用Spray的方式来获取当前句柄。
因为tagWnd的结构体大小是固定的,所以每次创建都会从堆中获取同样大小的空间。那么,如果创建N个窗口,并全部进行销毁,再次分配窗口时,可能就会将tagWnd分配到其中某一块空间中。而在调用__xxxClientAllocWindowClassExtraBytes之前,tagWnd结构体中的HWND已经填充完毕,且tagWnd->0x28的pwnd结构体会同时映射到内存空间中,所以通过pwnd->0x00,也是可以获取HWND句柄的。

在通过ConsoleControl设置完成之后,即可以通过SetWindowLongPtr进行读写,但是参见该函数分析小节,会出现一个限制,判断传入的参数nIndex是否小于pwnd->cbWndExtra,那么就需要对其重新进行设置,观察所修改的目标地址 = DesktopHeapBase + SomeAddressCanModify + nIndex;如果想要修改当前的cbWndExtra字段,就需要获得pwnd的内核地址,通过结构体的得知,pwnd->0x08为当前pwnd所在地址相对于DesktopHeapBase的偏移,那么只需要将SomAddressCanModify变为pwnd->0x08,即可设置nIndex为0xc8来修改cbWndExtra。

......
SomAddressCanModify = pwnd->0x08;
......
SetWindowLongPtr(hwndTarget,0xc8,0x0fffffff);

上述exp的编写过程中,仍然存在一个问题,如何通过HWND来获得用户态中pwnd结构体,通过查阅可以得知有个函数叫做HMValidateHAndle,该函数可以将HWND转换为pwnd的所在地址。定位该函数位于user32.dll中,但是该函数没有导出,在加载到内存后,通过已导出的函数地址动态定位到该函数是比较好的解决方案(针对于不同系统版本等问题)。下图为HMValidateHandle的交叉引用函数列表

为了了方便定位到函数地址,其调用时越靠前越好,可以采用GetMenuItemCount、IsMenu等函数,这里采用IsMenu来定位HMValidateHandle函数。

最后只剩下任意读的问题,其实如果能够修改tagWnd,使用相关的GetxxxxInfo这种读取信息的都可以使用。这里采用了GetMenuInfo的方式去读取,唯一需要注意的就是要通过xxxGetMenuInfo中的相关检查即可。

总结

该漏洞利用难度不高,需要对win32k相关结构和函数有前置知识的了解。

posted @ 2021-11-23 10:48  V1oet  阅读(183)  评论(0)    收藏  举报