在win32k!xxxMNEndMenuState函数中,函数会调用MNFreePopup函数释放tagPOPUPMENU对象,但是函数释放对象以后,没有清空指针。而在弹出窗口过程中,用户可以劫持相应的处理函数来实现两次调用xxxMNEndMenuState函数,因为双重释放导致BSOD的产生。通过内存布局,可以伪装tagPOPUPMENU对象在释放的内存空间中,通过解引用修改窗口对象的关键的标志位,可以通过SendMessage函数让窗口在内核态执行指定的处理函数实现提权操作。
操作系统:Win7 x86 sp1 专业版
编译器:Visual Studio 2017
调试器:IDA Pro, WinDbg
图1是win32k!xxxMNEndMenuState函数与本漏洞有关的关键代码,共有三个关键的地方。
图1. xxxMNEndMenuState函数
全局变量gptiCurrent保存的是线程信息结构体tagTHREADINFO,该结构体偏移0x104处保存的是tagMENUSTATE结构体:
tagMENUSTATE结构体定义了菜单的状态,该结构体偏移为0的地方,保存了弹出菜单结构体tagPOPUPMENU:
第一处的代码就是取出pGlobalPopupMenu,之后在第二处将其作为参数传入MNFreePopup函数。tagPOPUPMENU结构体定义如下,该结构体存储关联的弹出菜单相关的各个内核对象的指针:
图2是MNFreePopup函数的反汇编结果,该函数的第二处代码会将传入的参数tagPopupMenu释放,可是释放之后,却没有把tagPopupMenu的指针置为空,如果再次进入该函数,就会对相同的内存进行释放,就会导致BSOD的产生。
图2 MNFreePopup函数反汇编
漏洞的触发需要调用TrackPopupMenuEx函数,该函数通过调用内核函数xxxTrackPopupMenuEx函数实现,xxxTrackPopupMenuEx实现了很多功能,具体可以看参考链接中Leeqwind师傅的分析,这里只讲几个关键的步骤。
首先,函数会调用xxxCreateWindowEx创建类名"#32768"窗口:
在xxxCreateWindowEx函数中,在对创建的窗口成员赋值之后,就会调用xxxSendMessage向窗口发送WM_NCCREATE消息:
调用完xxxCreateWindowEx函数后,xxxTrackPopupMenuEx函数会调用xxxSetWindowPos函数将菜窗口显示到屏幕中:
相关窗口位置和状态完成改变之后,xxxSetWindowPos函数会调用xxxEndDeferWindowPosEx函数:
xxxEndDeferWindowPosEx函数会继续调用xxxSendChangedMsgs函数:
xxxSendChagedMsgs函数则根据SWP_HIDEWINDOW状态标志位来选择调用xxxRemoveShadow函数删除阴影窗口或调用xxxAddShadow函数来增加阴影窗口:
阴影窗口通过以下的结构体中的next指针连接,第一个结构体的地址保存在了全局变量gpshadowFirst中。
在xxxAddShadow函数中,函数会首先申请0x0C大小的内存用来保存SHADOWWINDOW结构体:
接着就会创建阴影窗口,从参数可以看出,阴影窗口没有自己的消息处理例程:
在申请的内存将结构体的成员赋值,并将申请的结构体加入到全局变量gpshadowFirst所指的单向链表中,此时新增的阴影窗口就会是第一个。
xxxRemoveShadow删除阴影窗口的功能,就会是从gpshadowFirst全局变量中找到第一个符合要求的阴影窗口,销毁阴影窗口,并将增加阴影窗口时候申请的用来保存信息的0x0C的内存释放掉。
xxxSetWindowPos执行完成后,xxxTrackPopupMenuEx函数会调用xxxWindowEvent函数发送EVENT_SYSTEM_MENUPOPUPSTART通知事件:
总结一下,xxxTrackPopupMenuEx函数会做的三件事情:
调用xxxCreateWindowEx创建类名为"#32768"的窗口,在创建过程中,会调用xxxSendMessage发送WM_NCCREATE消息
调用xxxSetWindowPos将窗口显示到屏幕中,在此过程中会创建阴影窗口,阴影窗口没有自己的处理例程,在用户层可以对其完成设置
调用xxxWindowEvent函数发送EVENT_SYSTEM_MENUPOPUPSTART通知事件
在xxxTrackPopupMenuEx函数执行过程中,完成窗口创建以后,会发送WM_NCCREATE消息和EVENT_SYSTEM_MENUPOPUPSTART通知事件,而用户可以通过HOOK操作实现对这两个操作的劫持,执行用户想要的代码,触发上述存在的双重释放带来的BSOD,此时对消息和事件的劫持的代码如下:
在执行的代码中就要实现两次调用xxxMNEndMenuState,该函数的调用可以通过发送MN_ENDMENU消息,以及NtUserMNDraLeave系统调用来实现。而在图1的第三处代码的最后,函数会将tagMENUSTATE的pmnsPrev赋值给tagTHREADINFO的pMenuState,pmnsPrev通常为0,一旦完成了这个设置,即使再次进入xxxMNEndMenuState,也会在第一处的代码的验证中跳过第二处代码,即跳过MNFreePopup函数的调用。
因此,需要在最后的赋值之前产生第二个函数的调用,而在赋值之前,函数会对tagMENUSTATE的uButtonDownHitArea调用UnlockMFMWFPWindow函数。
uButtonDownHitArea成员保存着当前鼠标按下的坐标区域所属的窗口对象地址,UnlockMFMWFPWindow函数会对窗口对象进行释放,当计数为0的时候会销毁窗口,此时会销毁与该窗口关联的阴影窗口。此外,通过发送WM_ENDMENU消息销毁触发漏洞函数的时候,会删除两个阴影窗口。
综上,漏洞触发的思路如下,首先在消息处理例程中,会创建三个阴影窗口,且为第三个阴影窗口设置消息处理例程。
在事件处理例程中,第一次进入的时候会发送WM_LBUTTONDOW消息,这样uButtonDowHitArea成员域就会存传当前鼠标按下的区域所属的窗口对象,系统在处理WM_LBUTTONDOWN消息的时候,会再次进入到事件处理例程中,此时就通过发送MN_ENDMENU消息来调用xxxMNEndMenuState函数。
在处理MN_ENDMUEN消息过程中,会删除两个阴影窗口,当xxxMNEndMenuState函数执行到图1中第三处对uButtonDownHitArea调用UnlockMFMWFPWindow函数的时候,又会删除第三个阴影窗口,就会触发为阴影窗口设置的处理函数。在处理函数中,通过NtUserMNDraLeave函数来再次调用xxxMNEndMenuState函数。
在xxxMNEndMenuState函数中下断点,编译运行POC,可以看到第一次是由发送WM_ENDMENU消息来调用函数:
第二次就是通过NtUserMNDraLeave来调用的:
继续向下运行,就会因为释放已经被释放的内存产生BSOD错误。
想要不产生BSOD错误,就需要在第二处释放之前,构造数据填入释放的内存。这里通过SetClassLong函数实现,该函数定义如下:
当第二个参数为GCL_MENUNAME的时候,函数会将第三个参数指定的数据写入到第一个参数指定的窗口的成员中。要找到这些数据,首先要获取第一个参数的窗口的pcls:
pcls对应的是tagCLS结构体,该结构体的lpszMenuName保存的地址就保存了调用SetClassLong时指定的第三个参数中的数据。
因此,在触发漏洞之前需要先创建一些窗口:
这样,当第二次调用xxxMNEndMenuState函数的时候,就可以通过伪造的数据来防止BSOD的产生。
但是,从图2可以看到,在释放tagPOPUPMENU之前,会对其成员进行解引用,所以伪装的数据所指向的地址应当符合窗口对象的要求,此时就通过创建一个新得窗口,并为其设置扩展区域,伪造的tagPOPUPMENU中的成员指向的就是扩展区域,通过设置扩展区域中的数据来让伪造的tagPOPUPMENU中的成员所指的窗口有效。
想要完成提权操作,需要ShellCode在内核模式下执行,这里需要用到bServerSideWindowProc标志位:
当调用SendMessage向窗口发送消息的时候,内核会通过xxxSendMessageTimeout来实现功能,而在该函数中,会判断bServerSideWindowProc是否置位,如果置为则会调用指定的消息处理例程。
所以,在伪造的tagPOPUPMENU对象的spwndPrevPopup成员赋值为bServerSideWindowProc标志位偏移-4的地址,这样在图2的第一处的代码中成员进行解引用的时候,会将偏移为4的clockObj减一,这样就会将bServerSideWindowProc置位。
因此,第二次释放内存之后,就会将bServerSideWindowProc置位,这样对窗口发送消息后,就会在内核模式下执行ShellCode实现提权。
但这里有个问题,第二次释放的伪造的tagPOPUOMENU内存在线程退出的时候,程序会对它进行释放,此时因为漏洞的第二次释放的时候已经释放过这块内存了,就导致线程退出时候的释放是一块已经被释放的内存,就会造成BSOD的产生:
因此,在执行ShellCode的时候,应当把tagCLS的lpszMenuName成员清空。此时的ShellCode是以结构体的形式定义的,定义如下,其中成员pfnWinProc保存了要执行的ShellCode,tagCLS保存了上面创建的大量窗口的tagCLS:
此时通过以下代码就可以指定向窗口发送消息时候,在内核模式下执行ShellCode,同时在ShellCode的上方保存了要用到的数据。
在ShellCode中要找到会被释放的tagPOPUPMENU,所以在事件处理函数中,要对其内存地址进行记录,此时的事件处理例程如下,保存的tagPOPUPMENU对象地址,在发送消息的时候将作为参数进行传递:
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2022-8-4 23:15
被1900编辑
,原因: