0x00 前言
CVE-2014-4113是Win32k下的释放后重引用漏洞(UAF),该漏洞位于win32k!xxxHandleMenuMessages
中,通过调用win32k!xxxMNFindWindowFromPoint
获取tagWnd指针,在此期间,执行流通过回调机制,返回用户侧,在用户侧释放目标菜单对象,并返回0xFFFFFFFB
。当执行流重新返回内核侧,
并没有针对返回值进行校验,直接使用该返回值作为参数传入win32k!xxxSendMessage
发送MN_BUTTONDOWN
消息,由此造成UAF。
0x01 原因分析
本文通过crash寻找漏洞触发的原理,实验环境为win7(x86)sp1。运行poc之后。在win32k!xxxSendMessageTimeout+0xb3
处crash。
通过栈回溯,可以清晰的看到漏洞如何从用户层到内核的执行流。显然,该漏洞是在win32k!xxxSendMessageTimeout
函数内部crash。
根据异常信息,然后esi值为fffffffb
,查看反汇编情况,esi的值源于win32k!xxxSendMessageTimeout
的第一个参数,并最终发现产生漏洞的原因在于xxxMNFindWindowFromPoint
函数的返回值,复制给ebx,并且没有进行必要的校验,就作为参数传入xxxSendMessage发送MN_BUTTONDOWN
消息。
0x02 poc构建
根据栈回溯以及微软提供的漏洞信息,可以知道该漏洞发生在win32k!xxxHandleMenuMessages
,在用户层触发该漏洞的函数是user32!TrackPopupMenu
。TrackPopupMenu
的函数原型如下,其作用是在任何地方显示快捷菜单和跟踪菜单的选择。
根据TrackPopupMenu
的函数原型,重要的参数hMemu和hWnd,hMenu是菜单句柄,该参数是由CreatePopupMenu生成,hWnd是窗口句柄,hWnd参数是由CreateWindows生成,构建的poc如下,但是这样肯定无法触发漏洞。
首先,在win32k!xxxHandleMenuMessages
打一个断点,接着根据上面分析,漏洞产生原因是没有对位于win32k!xxxHandleMenuMessages+0x99
的xxxMNFindWindowFromPoint
的返回值进行校验。该地址主要有两个引用点。
win32k!xxxHandleMenuMessages+0x5D
win32k!xxxHandleMenuMessages+0x25A
要想触发到v13 = xxxMNFindWindowFromPoint(v3, &tagPopupMenu, v7);
主要有两条路径,第一条路径,当发送WM_LBUTTONDOWN(0x201)消息给窗口的时候触发,第二条路径,发送WM_RBUTTONDOWN消息,并且(*tagPopupMenu & 0x40) != 0
需要符合这两个条件。
这里详细解释一下(*tagPopupMenu & 0x40) != 0
,其汇编代码如下,可以看到tagPOPUPMENU偏移为0的地址和40h进行test。40h的2进制表示为01000000,这里判断第一个字节是否含有 fRightButton 标志位。所以,在第二条路径上需要满足如下两个条件
- 发送WM_RBUTTONDOWN消息
- 设置fRightButton标志1234567891011121314151617181920212223242526272829303132.text:95AB7B36 test byte ptr [edi+tagPOPUPMENU._bf_0], 40h.text:95AB7B39 jz loc_95AB7E12.text:95AB7B36 test byte ptr [edi], 40h.text:95AB7B39 jz loc_95AB7E12//kd> dt tagPOPUPMENUwin32k!tagPOPUPMENU+0x000 fIsMenuBar : Pos 0, 1 Bit+0x000 fHasMenuBar : Pos 1, 1 Bit+0x000 fIsSysMenu : Pos 2, 1 Bit+0x000 fIsTrackPopup : Pos 3, 1 Bit+0x000 fDroppedLeft : Pos 4, 1 Bit+0x000 fHierarchyDropped : Pos 5, 1 Bit+0x000 fRightButton : Pos 6, 1 Bit+0x000 fToggle : Pos 7, 1 Bit+0x000 fSynchronous : Pos 8, 1 Bit+0x000 fFirstClick : Pos 9, 1 Bit+0x000 fDropNextPopup : Pos 10, 1 Bit+0x000 fNoNotify : Pos 11, 1 Bit+0x000 fAboutToHide : Pos 12, 1 Bit+0x000 fShowTimer : Pos 13, 1 Bit+0x000 fHideTimer : Pos 14, 1 Bit+0x000 fDestroyed : Pos 15, 1 Bit+0x000 fDelayedFree : Pos 16, 1 Bit+0x000 fFlushDelayedFree : Pos 17, 1 Bit+0x000 fFreed : Pos 18, 1 Bit+0x000 fInCancel : Pos 19, 1 Bit+0x000 fTrackMouseEvent : Pos 20, 1 Bit+0x000 fSendUninit : Pos 21, 1 Bit+0x000 fRtoL : Pos 22, 1 Bit+0x000 iDropDir : Pos 23, 5 Bits+0x000 fUseMonitorRect : Pos 28, 1 Bit
如何发送消息呢,可以在WndProc函数中调用PostMessage发送消息PostMessageA(hwnd, WM_LBUTTONDOWN, 0, 0);
随后进入win32k!xxxMNFindWindowFromPoint
中,就像函数名称存在xxx
前缀,说明在该函数中存在回调到用户侧的执行逻辑,首先,win32k!xxxMNFindWindowFromPoint
会判断是否存在spwndNextPopup的子菜单窗口对象,如果存在子菜单窗口对象,就会调用 xxxSendMessage发送MN_FINDMENUWINDOWFROMPOINT
(0x1EB)消息,用来查找菜单窗口对象。
win32k!xxxMNFindWindowFromPoint
用来获取tagWnd对象,该函数首先判断tagpopupmenu
对象的spwndNextPopup是否为空,如果不为空的话,就会调用xxxSendMessage对子弹出菜单发送MN_FINDMENUWINDOWFROMPOINT(0x1EB)消息。
win32k!xxxSendMessage
最终调用xxxSendMessageTimeout
函数,在xxxSendMessageTimeout
中,首先判断发送窗口是否属于当前进程,以及判断是否处于Hook状态,并最终调用win32k!xxxMenuWindowProc
。
win32k!xxxMenuWindowProc
首先检查了窗口对象的状态,然后根据message执行不同的操作。当message为MN_FINDMENUWINDOWFROMPOINT时,继续将弹出窗口ppopupmenu传入xxxMNFindWindowFromPoint,并最终返回窗口对象。
因为spwndNextPopup = tagpopupmenu->spwndNextPopup;
不为空,所以在构造poc的时候需要创建两个菜单对象,新构造的poc如下
随后,进入win32k!xxxSendMessageTimeout
,【在挖一个坑,学习xxxSendMessageTimeout机制】
在win32k!xxxSendMessageTimeout
中调用xxxSendMessageToClient
向用户侧发送MN_FINDMENUWINDOWFROMPOINT消息,此时执行流会回调到用户层,从而给了机会销毁菜单对象,从而有了触发漏洞前提。
要想执行流执行xxxSendMessageToClient
,需要满足一下几个条件:
- pwnd != -1 (已满足)
- gptiCurrent == pwnd->head.pti(判断发送的窗口是否属于当前线程)(已满足)
- gptiCurrent->fsHooks(使用SetWindowsHookEx即可满足)
- ptagWND->bServerSideWindowProc(满足)
- 在0x1EB消息发送后修改快捷菜单的窗口过程(ptagwnd->lpfnWndProc)(使用SetWindowLongPtr可以满足)
继续构造新的poc。使用SetWindowsHookEx添加一个钩子过程。
编写钩子处理函数,根据上面分析,xxxMNFindWindowFromPoint
调用 xxxSendMessage
发送MN_FINDMENUWINDOWFROMPOINT
(0x1EB)消息,用来查找菜单窗口对象。所以我们需要钩取MN_FINDMENUWINDOWFROMPOINT
消息。
钩子函数原型如下,nCode
可以指定挂钩函数是否必须处理消息,wParam
表示消息是否可以由当前线程发送,lparam
指向的是tagCWPSTRUCT结构体指针。tagCWPSTRUCT结构体第3个成员表示消息类型。可以根据*(DWORD*)(lParam + 8)
的值判断消息是否是MN_FINDMENUWINDOWFROMPOINT
。
在窗口钩子的处理函数中,如果处理的消息为MN_FINDMENUWINDOWFROMPOINT
,就会调用SetWindowLongA
修改菜单的消息处理函数,为什么要这样呢?我理解的是,当使用SetWindowsHook设置的是针对窗口的钩子函数,所以还需要通过SetWindowLongA
为菜单对象设置钩子函数。hWnd
是指窗口句柄。nIndex
是指偏移,选择GWL_WNDPROC
为窗口函数设置一个新地址。dwNewLong
在这里指的是函数地址。HookCallbackTwo
是针对菜单对象的处理函数,在这个函数中需要释放菜单,并且返回-5。
很显然,当菜单钩子函数返回值为-5的时候,win32k!xxxMNFindWindowFromPoint
的返回值为0xfffffffb
,由此造成UAF。
0x03 漏洞利用
上一节介绍了,CVE-2014-4113最终是在win32k!xxxSendMessageTimeout+0xb3
crash。我们再来分析一下,首先int __stdcall xxxSendMessageTimeout(tagWND *pwnd, UINT message, WPARAM wParam, LPARAM lParam, UINT fuFlags, UINT uTimeout, PLONG_PTR lpdwResult)
的*pwnd
是可控的,在CVE-2014-4113这个漏洞中,该值为0xFFFFFFFB.当执行流执行到0x95A44856的时候,此时esi为0xFFFFFFFB,则[esi+60] = [0xFFFFFFFB + 0x60] = 0x5B
即会调用地址为0x5B这个地址的函数。
|
|
根据上述分析,要想执行流执行到0x95A44856,需要满足两个条件,内存地址为3的地址存放的是当前线程信息。gptiCurrent是一个全局变量,其指向的是tagTHREADINFO结构,也就是线程环境块偏移0x40的Win32ThreadInfo结构。
第二,地址为4的内存的值为4
根据上述描述,最终通过call dword ptr [esi+60h]
执行shellcode,所以我们需要在0x5B
处提前设置Shllcode的地址。在《Windows核心编程》关于内存结构的章节中指出:Windows系统存在空指针赋值分区,其范围从0x00000000至0x0000FFFF,由于这部分内存位于地址空间的最开始,因此也称之为零页内存。可以通过NtAllocateVirtualMemory
分配内存空间,对于Windows系统,在进程的虚拟空间申请一块内存时,该块内存默认为64KB大小对齐(分配内存的起始地址必须为64KB的整数倍),因此,当我们设置分配内存的起始地址为0x00000100时,系统会强制决定起始地址为0x00000000,由于我们分配页面大小选择4KB,因此分配得到的内存空间为0x00000000~0x00001FFF。
为了能执行到漏洞利用处,需要满足上面两个条件,并设置shellcode地址。
构造Shellcode函数值得注意的是,首先Shellcode函数的函数原型必须和result = pwnd->lpfnWndProc(pwnd, message_1, wParam, lParam);
保持一致。而且尽量让编译器进行堆栈的平衡。
0x04 参考文献
- [1] CVE-2014-4113 漏洞利用分析 : 主要的poc构建框架
- [2] https://github.com/B2AHEX/cveXXXX/blob/master/CVE-2014-4113/exploit.cpp : CVE-2014-4113的exp
- [3] CVE-2014-4113提权漏洞学习笔记 : 详细的分析笔记
- [4] [技术分享]经典内核漏洞调试笔记
- 以及小刀师傅的详细解答