win32kfull!xxxCreateWindowEx函数创建窗口的过程中,当创建的窗口对象存在扩展内存的时候,会通过函数KeUserModeCallback返回用户层,申请需要的内存。返回到内核继续执行的时候,会将用户层函数中指定的地址保存到窗口对象偏移0x128的pExtraBytes成员中。当用户层对窗口调用SetWindowLongPtr函数的时候,函数会将pExtraBytes用于指定要写入的目标地址。通过劫持用户层函数的执行,可以让SetWindowLongPtr函数对不合法地址进行写入会产生BSOD,也可以通过计算来扩大其他窗口的cbwndExtra,从而实现任意地址读写,最终实现提权。
操作系统:Win10 x64 1909 专业版
编译器:Visual Studio 2017
调试器:IDA Pro, WinDbg
新的Win10版本修改了比较多的win32k*中的结构体,并且没有导出。所以以下部分成员只能是通过推测得出,首先是保存线程信息的tagTHREADINFO结构体,其偏移0x1C0保存的是tagDESKTOP结构体:
tagDESKTOP偏移0x80处保存的是pheapDesktop,该成员保存的是桌面堆的基址:
tagWND有了比较大的变化,窗口的扩展内存不在直接跟在tagWND之后,当偏移0xE8的Flags不包含0x800标记的时候,扩展内存的地址直接保存在0x128的pExtraBytes中,当Flags包含0x800标记的时候,扩展内存存在于桌面堆中,与桌面堆基址的偏移保存在了0x128的pExtraBytes中。偏移0x28指向了tagWDNK结构体,偏移0x8和0x30处保存了0x28所指向的地址于桌面堆地址的偏移:
偏移0xA8指向的tagMENU,这里只需要知道tagMENU结构体偏移0x98的pSelf指向的是tagMENU本身,而0x28的tagWDNK结构体如下:
该函数定义如下,当第三个参数bType指定为TYPE_WINDOW(0x1)的时候,就会用于创建窗口对象:
函数的主要代码如下,
从gptiCurrent中获取rpdesk成员,之后调用HMAllocateObject函数来申请tagWND对象:
调用tagWND::RedirectedFieldcbwndExtra<int>::operator!=判断cbWndExtra来判断是否存在扩展内存,存在的话就会调用xxxClientAlloWindowClassExtraBytes来创建扩展内存:
xxxClientAllocWindowClassExtraBytes函数的主要代码如下,函数会调用KeUserModeCallback来发起用户层的回调来申请内存。从用户层返回之后,函数会对输出长度及输出地址进行判断,通过判断后,就会将申请的内存地址返回:
用户层函数的实现则下所示,函数通过RtlAllocateHeap来申请需要大小的内存,之后将其放入Result[0]中,之后会通过NtCallbackReturn函数将申请的内存通过Result数组来申请的内存返回到内核层,第二个参数用来指定返回的数据的长度:
xxxSetWindowLongPtr函数用来对窗口的扩展区域进行写入,当nIndex小于0的时候,函数会调用xxxSetWindowData来写入值:
如果nIndex大于等于0,函数就会判断nIndex + 8是否大于cbwndExtra,如果大于则会设置错误,之后退出函数:
如果nIndex + 8 <= cbwndExtra的时候,函数会判断窗口对象是否带有0x800标记,如果有0x800标记,则会将寄存器r8会赋值为nIndex + pExtraBytes:
如果不带有0x800标记,就会将寄存器r8赋值为pheapDesktop + nIndex + pExtraBytes:
无论是否带有0x800标记,对r8赋值完之后,函数接下来就会将r8所指向地址中的内容保存到局部变量中,在将dwNewLong赋值到r8所指的地址:
由于xxxCreateWindowEx函数没有对用户层通过NtCallbackReturn函数指定的地址进行合法性验证,就将其赋值到窗口对象的pExtraBytes中。而对相应窗口调用SetWindowLongPtr的时候,会直接将pExtraBytes用于来指定读写地址。所以,通过对用户层的xxxClientAllocWindowClassExtraBytes进行劫持,可以将pExtraBytes指定为特定的值来触发BSOD。
为了可以在指定的窗口来修改函数,首先创建触发漏洞窗口的时候,扩展内存的大小,即cbwndExtra要指定为一个特定的值:
接下来可以在劫持的函数中,通过要申请内存的大小判断是否为目标窗口:
此时可以在xxxSetWindowLongPtr中关键位置下断点,因为创建的窗口对象的Flags不会带有0x800标记,所以函数会直接取出pExtraBytes用于读写,此时的地址为指定的不合法的0x100。按道理,继续执行会出现BSOD,然而事实上继续运行,函数会直接退出(应该有什么处理机制)。
xxxSetWindowLongPtr会通过tagWND->Flags来选择不同方式来指定用于读写的地址,要在Flags中加入0x800标记,可以通过xxxConsoleControl函数来实现,该函数定义如下:
要到达修改标记的代码,需要参数nIndex等于6,参数nInLength等于0x10。满足这两条之后,xxxConsoleControl函数会从参数pInfo中取出窗口的句柄,通过ValidateHwnd来获取相应的窗口对象:
判断Flags是否带有0x800标记:
因为创建的窗口不带有0x800标记,函数就会调用DesktopAlloc来申请一块新的内存:
接下来将新创建的内存地址减去pheapDesktop得到的偏移赋值到ptagWNDK->pExtraBytes中:
之后就是在Flags中增加0x800标记:
想要成功触发漏洞,需要通过xxxConsoleControl函数在Flags中增加0x800标记,但是调用xxxConsoleControl的时候,需要传入窗口的句柄,而在用户层的xxxClientAllocWindowClassExtraBytes执行过程中,用户层的CreateWindow函数还未返回,因为还未拿到窗口的句柄。但,在xxxCreateWindowEx函数在调用xxxClientAllocWindowClassExtraBytes之前,已经将窗口句柄赋值到窗口对象偏移0x0处。
因此,可以首先创建大量的窗口,然后释放掉其中的部分窗口,这样之后创建触发漏洞的窗口占用的内存就会占用到这些释放的窗口。
此时,就可以从释放的窗口中搜索触发漏洞的窗口,之后就可以修改窗口标记,在返回指定的地址:
再次在xxxSetWindowLongPtr处下断点,此时窗口的Flags带有0x800标记,所以会通过不同的方法来计算要读写的内存地址,该地址是无效的:
继续运行就会产生BSOD错误:
根据上面的内容可以得出以下的内容:
tagWNDK + 8处保存的是ptagWNDK - pheapDesktop
通过xxxConsoleControl增加0x800标记的时候,会将窗口的pExtraBytes修改为新申请的内存地址减去pheapDesktop的值
当Flags包含0x800标记,SetWindowLongPtr要读写的地址是pheapDesktop + nIndex + pExtraBytes的值
当Flags不包含0x800标记,SetWindowLongPtr要读写的地址是pExtraBytes + nIndex
pheapDesktop的值是相同的,且现在可以修改pExtraBytes以及为Flags可以增加0x800标记。此时,实现任意地址写的思路如下:
创建两个窗口,分别为tagWND0,tagWND1
在tagWND0中增加0x800标记,这样tagWND0->pExtraBytes中保存的就是与pheapDesktop的偏移
在用户层的xxxClientAllocWindowClassExtraBytes函数中,在调用NtCallbackReturn函数返回的时候,将地址修改为tagWND0 + 8处保存的偏移,这样对触发漏洞的窗口调用SetWindowLongPtr,就可以直接扩大tagWND0中的cbwndExtra
因为tagWND0的pExtraBytes指向的是pheapDesktop的偏移,而tagWNDK1也保存在相对于pheapDesktop的偏移,而该值可以通过tagWND1 + 8处来获取,这样可以计算出tagWND0->pExtraBytes与tagWND1 + 8的偏移。又因为tagWND0的cbwndExtra被扩大了,这样就可以通过tagWND0直接修改tagWND1的pExtraBytes
因为tagWND1没有具有0x800标记,所以直接对tagWND1调用SetWindowLongPtr会直接对pExtraBytes指向的地址进行写入,由此就实现任意地址写
在之前释放窗口的时候,是从下标2开始释放窗口,就是因为要将创建的第一个和第二个窗口作为tagWND0和tagWND1用于之后利用。此时,在循环创建窗口的时候,需要对创建的第0个窗口加入0x800标记,且记录需要用到的偏移:
释放窗口以后,就可以计算第4步需要的偏移:
此时对于触发漏洞的窗口,需要将返回值修改为tagWDN0 + 8的保存的值:
当创建完用于触发漏洞的窗口之后,可以通过函数扩大tagWND0的cbwndExtra:
现在就可以通过tagWND0修改tagWND1的pExtraBytes来实现任意地址写入:
任意地址的读通过GetMenuBarInfo函数来实现,该函数定义如下,其中第三个参数的pmbi->rcBar用来记录读取到的值:
GetMenuBarInfo对应的内核函数是xxxGetMenuBarInfo函数,该函数的主要代码如下,先有三处验证,验证通过之后,会将*(spMenu + 0x58)中保存的地址用于读取相应值,在保存于pmbi的rcBar中:
第一处验证,只需调用参数时指定参数idObject为-3就行。第二处验证,在创建窗口的时候,对于用于利用的窗口需要设置spMenu:
伪造tagMENU结构体,伪造的时候,要绕过第三处验证:
把伪造的tagMENU设置到tagWND1中:
现在就可以通过设置*(spMenu + 0x58)的值,来实现任意地址的读取:
具有任意地址读写的能力,就可以通过替换Token实现提权:
提权完成之后,为了防止退出进程时发生BSOD,还需要将利用该漏洞过程中修改的窗口对象的成员修复回原来的数据:
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2022-8-15 15:30
被1900编辑
,原因: