该漏洞存在于win32k的xxxPaintSwitchWindow函数中,函数会将窗口对象扩展区域最开始八字节保存的内容取出,将其作为内存地址进行读写。可是这最开始的八字节中保存的内容可以通过SetWindowLong函数进行更改,而函数没有验证保存的内容是否指向合法的地址就进行读写,如果地址不合法,则会产生BSOD错误。通过设置,可以利用函数对指向地址进行读写的操作来扩大窗口的cbwndExtra,通过内存布局,在被扩大cbwndExtra的窗口高地址不远处布置一个窗口对象,通过修改窗口对象的成员实现任意地址读写,最终实现提权。
操作系统:Win7 x64 sp1 专业版
编译器:Visual Studio 2017
调试器:IDA Pro,WinDbg
漏洞函数xxxPaintSwitchWindow只有一个参数,就是窗口对象的tagWND结构体,在win7 x64系统下,该结构体共占128字节,定义如下:
xxxPaintSwitchWindow函数会将扩展区域中保存的内容赋给rdi:
验证保存的地址是否为0,以及保存的地址偏移0x6C的内容是否为0:
之后还会对rdi保存的地址偏移0x5C到0x6C这段区域进行增减操作,这里不难看出,增减的数值由GetDPIMetrics函数的返回值决定,不过这不是重点,重点是这些操作是会扩大这些与rdi偏移的地址中保存的内容:
这里的问题就很明显,函数取出扩展区域最开始八字节保存的地址,仅验证这个地址是否为0,对于它是否合法没有验证。而扩展区域中的内容又可以在用户层通过SetWindowLong函数修改,只要将这个地址修改为一个非法地址,函数对非法地址的读写就会产生BSOD。如果把这个地址指向窗口对象的cbwndExtra成员地址偏移-0x60处,最后面的增减操作就会扩大cbwndExtra。
触发该的函数调用链为:NtUserMessageCall -> NtUserfnINLPDRAWITEMSTRUCT -> xxxWrapSwitchWndProc -> xxxSwitchWndProc -> xxxPaintSwitchWindow。
首先是NtUserMessageCall函数,该函数在msg < 0x400的时候就会调用gapfnMessageCall数组中保存的函数地址:
gapfnMessageCall数组保存了一系列的函数,其中就有NtUserfnINLPDRAWITEMSTRUCT:
NtUserfnINLPDRAWITEMSTRUCT函数通过参数dwType计算偏移,调用gpsi偏移中保存的函数:
而gpsi偏移地址保存的函数在InitFunctionTables函数中初始化,其中偏移0x40处保存了xxxWrapSwitchWndProc,所以参数dwType需要为0,这样8 * 6 + 0x10 = 0x30 + 0x10 = 0x40,NtUserfnINLPDRAWITEMSTRUCT就会调用xxxWrapSwitchWndProc。
xxxWrapSwitchWndProc会调用xxxSwitchWndProc函数:
xxxSwitchWndProc主要分为两部分,第一部分如下图所示,会对tagWND->fnid进行判断,而一个新创建的窗口fnid为0,所以最外面的if语句会成立。在if成立的情况下,会有三个地方可能导致函数返回。
第一处是由于tagWND->fnid为0,所以就是在判断tagWND->cbwndExtra是否小于gpsi + 0x154中保存的数值。因为要用到扩展区域最开始八字节保存的内容,所以这里cbwndExtra至少为8。那么,这里就是在判断gpsi偏移0x154保存的内容大于等于至少0x130,而不通过其他操作,[gpsi + 0x154] < 0x130,所以这个条件不会成立。
第二处在传递的参数msg为WM_CREATE的时候,函数就不会返回:
第三处只要通过SetWindowLong设置扩展区域起始的八字节不为0就不会返回。如果这三处都不成立,就会将tagWND->fnid设置为0x2A0。
xxxSwitchWndProc第二处是调用漏洞函数xxxPaintSwitchWindow:
当消息为WM_ERASEBKGND的时候,xxxSwitchWndProc就会调用xxxPaintSwitchWindow:
想要触发漏洞函数,msg就不为1,可是msg不为1的时候,在xxxSwitchWndProc的第一部分的第二处代码又会返回。所以就需要两次进入xxxSwitchWndProc函数,第一次的时候msg要为WM_CREATE(0x1),这样函数会将tagWND->fnid设置为0x2A0。第二次在调用的时候,msg就可以指定为WM_ERASEBKGND(0x14),此时由于第一次进入将tagWND->fnid设置为0x2A0,xxxSwitchWndProc就会绕过第一部分的代码,直接在第二部分判断消息,调用漏洞函数xxxPaintSwitchWindow。
在xxxPaintSwitchWindow函数中,在第4处获取扩展区域最开始八字节保存的地址之前,也有三个地方需要绕过:
第一处的绕过,只需要创建窗口的时候,指定窗口可见就可以。在xxxSwitchWndProc函数中,已经设置了tagWND->fnid为0x2A0,所以不需要绕过。第三处还是在判断[gpsi + 0x154]和cbwndExtra + 0x128的数值大小,创建窗口的时候,cbwndExtra只需要设置为8就可以完成触发和利用。所以这里就是在判断[gpsi + 0x154]是否等于0x130。而创建类名为"#32771"窗口的时候,会将[gpsi + 0x154]设置为0x130,所以只需要通过创建这样一个窗口就可以绕过。
因此,漏洞触发步骤如下:
创建一个可见的带有八字节扩展区域的窗口用来触发漏洞
调用NtUserMessageCall,参数msg为WM_CREATE(0x1),将tagWND->fnid设置为0x2A0
将扩展区域最开始八字节保存的地址设置为一个不合法的地址
创建类名为"32771"的窗口,将[gpsi + 0x154]设置为0x130
调用NtUserMessageCall函数,参数msg为WM_ERASEBKGND(0x14),这样就会指向漏洞函数
相应POC代码如下:
在上图的第四处,也就是取出扩展区域最开始八字节保存的地址代码处下断点,编译运行POC,程序就会绕过上面的判断成功执行到这里:
继续向下运行,函数会对扩展区域保存的八字节地址进行读取,而这个地址已经被设置为一个不合法的地址:
继续运行就会因为对不合法地址进行读取造成BSOD:
要利用这个漏洞,需要在触发漏洞的窗口高地址不远处布置另一个用来攻击的窗口,所以需要首先创建一些用来攻击的窗口,然后释放掉中间的一部分,这样触发漏洞时候创建的窗口就会占用释放的某个窗口,高地址就会保存其他的窗口,相应代码如下:
在漏洞触发的时候,只要找到高地址处最近的窗口就可以完成攻击。此时,窗口扩展区域最开始八字节保存的地址应当是触发漏洞窗口的cbwndExtra地址减去0x60偏移的地址,因为xxxPaintSwitchWindow进行加减入操作的时候,是从偏移0x5C开始的。另外,在到达对内存进行加减操作之前,函数还会判断Alt键是否被按下,所以触发漏洞之前,需要模拟Alt的按键消息才能成功的完成加减的操作。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2022-7-19 18:14
被1900编辑
,原因: