CVE-2021-1732 LPE漏洞分析


概述

  CVE-2021-1732是一個發生在windows內核win32kfull模塊的LPE漏洞,並且由於創建窗口時調用win32kfull!xxxCreateWindowEx過程中會進行用戶模式回調(KeUserModeCallback),從而給了用戶態進程利用的機會。

  該漏洞由安恆信息在2020年12月在野外攻擊樣本中發現,在2021年2月份公開披露。相關樣本在2020年APT組織蔓靈花針對國內的一次攻擊中作為提權組件被發現。

分析

  Windows中創建窗口時,會調用API CreateWindowEx,最終在內核會調用至win32kfull!xxxCreateWindowEx。在win10 1909上調試時調用堆棧回溯如下:

  ... ...     win32kfull!xxxCreateWindowEx+0x1259

  ... ...     win32kfull!NtUserCreateWindowEx+0x6a0

  ... ...     nt!KiSystemServiceCopyEnd+0x25

  ... ...     win32u!NtUserCreateWindowEx+0x14

  ... ...     USER32!VerNtUserCreateWindowEx+0x211

  ... ...     USER32!CreateWindowInternal+0x1b4

  ... ...     USER32!CreateWindowExW+0x82

  win32kfull模塊的xxxCreateWindowEx函數為最終負責窗口對象創建的過程。CVE-2021-1732主要是在win32kfull!xxxCreateWindowEx調用win32kfull! xxxClientAllocWindowClassExtraBytes進行窗口擴展內存時觸發。xxxClientAllocWindowClassExtraBytes函數中會調用KeUserModeCallback進行用戶模式回調,以在用戶模式執行回調。該函數中指定的回調ApiNumber為0x7B,即為user32! _xxxClientAllocWindowClassExtraBytes。相關回調函數表可在PEB->KernelCallBackTable中查看。

  查看user32! _xxxClientAllocWindowClassExtraBytes,只是在用戶模式當前進程堆中分配了指定大小的空間,並將分配的堆地址通過NtCallbackReturn傳回內核。

  由於用戶模式回調函數的執行是在用戶態進行,因此用戶可以直接從進程中對該函數進行Hook,改變執行流程。

  分析時使用POC為https://github.com/KaLendsi/CVE-2021-1732-Exploit,經過和原始樣本的比對,可以發現該POC是對原始樣本的完全還原,僅是在部分變量名含義上不正確。

  漏洞首先對user32! _xxxClientAllocWindowClassExtraBytes進行Hook,在之后進程每次調用CreateWindowExW創建窗口時將會走到Hook函數處。替換后的KernelCallBackTable如下所示:

  接着創建多個普通窗口,后續都會經過Hook函數。對於普通窗口,Hook函數仍舊按照舊流程,為其調用user32! _xxxClientAllocWindowClassExtraBytes。判斷依據是傳入的參數值,即tagWnd. cbwndExtra,相關細節在創建利用窗口時再說。

  不過雖然普通窗口的創建仍是走的正常流程,但是會記錄每個創建窗口的對象地址。窗口對象地址利用HMValidateHandle進行泄露。該函數未導出,不過可以通過調用了該函數的其他API進行搜尋,比如IsMenu。

  調用方式為HMValidateHandle(HANDLE h, int type),傳入窗口句柄和type值,如果句柄類型和參數type一致,返回句柄對應的對象在用戶態內存的地址,值得注意的是,該調用成功返回值實際為poi(tagWnd+0x28)。窗口傳入type為1。

  1:TYPE_WINDOW

  如此連續創建多個窗口,查詢(VirtualQuery)每個窗口對象所在內存塊的基址,記錄其中最小的基址。接着除了窗口0和1,調用DestroyWindow銷毀其余窗口。保留下的窗口0和1將結合后續將創建的magicWnd進行漏洞利用,而記錄的最小基址將用於搜尋magicWnd。

  對比窗口0和1分別相對於桌面堆的偏移,較小者和較大者分別記為WndMin、WndMax。偏移值位於窗口對象tagWnd對象偏移0x08處。

  tagWnd對象結構部分偏移如下:

  +0x00         Handle

  +0x08         cLockObj

  +0x10         unk

    ++0x00    ETHREAD

      ... ...

      +++0x220    EPROCESS

        ... ...

        ++++0x2e8    UniqueProcessId

        ++++0x2f0    ActiveProcessLinks

        ++++0x360    Token

        ++++0x3e8    InheritedFromUniqueProcessId

        ... ...

      ... ...

  +0x18

    ++0x80    桌面堆基址

  ... ...

  +0x20         pSelf

  +0x28

    ++0x00    Handle

    ++0x08    *(tagWnd+0x28)相對於桌面堆基址的偏移

    ++0x18    exStyle

    ++0x1c    dwStyle

    ++0x98    spMenu

      ... ...

      +++0x50    tagWnd

      ... ...

    ++0xc8    cbwndExtra,指定Extrabytes字節數

    ++0xe8    不明flag,flag|=0x800可指定pExtrabytes屬性為偏移

    ++0x128   pExtrabytes,指向分配的Extrabytes內存

  ... ...

  +0xa8    spMenu

    ... ...

    ++0x50    tagWnd

    ... ...

  窗口銷毀后調用NtUserConsoleControl,指定參數ConsoleControl為6,ConsoleCtrlInfoLength為0x10,將窗口WndMin對象pExtrabytes(0x128)字段屬性設置為偏移,設置成功后pExtrabytes字段值為相對於桌面堆的偏移值,而0xe8處的flag將|=0x800。重新申請后的Extrabytes內存大小由poi(poi(tagWnd+0x28)+0xc8)指定。

(由於中間反復調試過幾次,截圖之間的數據可能有些對不上)

  然后創建一個magic窗口WndMagic,同之前一樣,會執行到xxxClientAllocWindowClassExtraBytes的Hook函數處。此時將進入另一分支,觸發Hook函數真正作用流程。判斷方式是傳入的參數值,之前創建的普通窗口和現在的magic窗口指定的cbWndExtra值是不同的,普通窗口固定為32字節,magic窗口為一個隨機值。

  而wndClass.cbWndExtra值將被賦值到窗口對象poi(tagWnd+0x28)+0xc8處,並作為ExtraBytes內存分配時的大小指定值,然后進行用戶模式回調。用戶態回調函數執行結束后返回內存地址到內核,賦值到poi(tagWnd+0x28)+0x128處。而Hook函數的目的就是為了返回一個虛假偏移,指向其他地址,實現可任意地址寫的功能。

  窗口創建過程中,執行到Hook函數中,通過比對傳入的參數值和隨機值,可確定此次創建是WndMagic。不過此時win32kfull! xxxCreateWindowEx尚未執行完畢,所以HWND句柄值還未返回,尚不可知。然而在進行額外內存進行創建時,窗口對象部分屬性已經完成初始化,比如句柄值、窗口屬性、擴展屬性等。

  所以通過匹配cbWndExtra值,再比對窗口擴展屬性值exStyle(此次利用中所有窗口屬性值都設置為了WS_EX_NOACTIVATE [0x8000000]),一致的情況下可以大概率確認WndMagic位置,自然可通過偏移獲取到相應屬性值。

  獲取WndMagic窗口句柄后,調用NtUserConsoleControl設置magic窗口pExtrabytes屬性為相對於桌面堆的偏移。接着再借助NtCallbackReturn將普通窗口WndMin對象poi(tagWnd+0x28)+0x08處的值傳回內核,從而結束回調。而poi(tagWnd+0x28)+0x08的值為poi(tagWnd+0x28)基於桌面堆基址的內存偏移,因此這里將導致WndMagic對象pExtrabytes值實際是指向WndMin窗口對象的偏移。

  之后調用SetWindowLongW,指定參數為(WndMagic句柄、Index=0x128、WndMin對象在內存中的偏移),返回數據應為原偏移處的舊數據,所以此處返回值為Hook函數中返回的WndMin虛假偏移。

    LONG SetWindowLongW(

        [in] HWND hWnd,

        [in] int  nIndex,

        [in] LONG dwNewLong

    );

  調用API SetWindowLongW最終執行到win32kfull! xxxSetWindowLong。Index大於等於0的情況下會執行到下圖所示的位置。而此次利用中wndClass.cbClsExtra指定為0 ,poi(tagWnd+0x28)+0xfc也持續為0,可以忽略。因為poi(tagWnd+0x28)+0xe8已被設置0x800屬性,所以poi(poi(tagWnd+0x28)+0x128)+DesktopHeapBaseAddr+Index=tagWnd_WndMin+0x128。也就是說雖是對WndMagic進行的操作,實際上實對WndMin對象pExtrabytes字段的寫入,值為自身WndMin在桌面堆中的偏移。

  然后執行SetWindowLongW(hWndMagic, offset_0xc8, 0xFFFFFFF),設置WndMin對象poi(tagWnd+0x28)+0xc8處cbwndExtra值設為0xFFFFFFF,擴大可以寫入的范圍,在xxxSetWindowLong和xxxSetWindowLongPtr中都存在對該值和Index的大小比較判定。

  現在WndMagic可控制WndMin,而WndMax對象偏移已知,因此也可控制,可以實現任意位置寫。接着就是對任意位置數據讀,這里采用的的是API GetMenuBarInfo,對Menu Bat信息的獲取,這種利用一次可以讀取8字節內容。

  BOOL GetMenuBarInfo(

      [in]      HWND         hwnd,

      [in]      LONG         idObject,

      [in]      LONG         idItem,

      [in, out] PMENUBARINFO pmbi

  );

  利用中構造了一個fakeMenu,將復制給WndMax,SetWindowLongPtr指定Index為-12,且窗口dwStyle為WS_CHILDWINDOW(0x40000000L),那么窗口spMenu字段可以被設置為指定的值。spMenu字段有兩處位置,poi(tagWnd+0x28)+0x98tagWnd+0xa8。而SetWindowLongPtr成功調用后返回的值為窗口的原spMenu,記錄該值。

  但是此時窗口並不是子窗口類型,所以在這之前需要對該字段手動進行設置。調用SetWindowLongPtrA,參數為(hWndMin, offset_0x18+WndMax_offset-WndMin_offset, poi(poi(tagWnd+0x28)+0x18)^0x4000000000000000),可以將WndMax窗口類型添加上WS_CHILDWINDOW屬性,從而通過檢測。

  為WndMax設置WS_CHILDWINDOW屬性,並添加spMenu后,再次調用SetWindowLongPtrA恢復其dwStyle,去除WS_CHILDWINDOW屬性,原因是后續在使用GetMenuBarInfo讀取指定地址數據時,窗口不能為子窗口類型。

  WndMax的fake spMenu設置完成,且已獲取了舊spMenu,記為old_spMenu。而在spMenu結構的0x50偏移處是spMenu所屬窗口對象地址,即poi(spMenu+0x50)==tagWnd。

  了解以上信息后,需要對指定地址進行讀,該漏洞利用對GetMenuBarInfo進行了封裝,傳入地址,封裝函數返回該地址下的內容。

  對GetMenuBarInfo的利用核心主要是指定idObject為-3,idItem為1,pmbi接收數據。API最終會走到win32kfull! xxxGetMenuBarInfo函數,傳參數據同GetMenuBarInfo。對該函數分析,可以看到需要對一些特殊的位置進行偽造,從而進入目的代碼處。其中poi(tagWnd+0x28)+0x58和poi(tagWnd+0x28)+0x5C處的值常為0,忽略。

  最終讀取時,可以看到pmbi->left讀取值為poi(poi(poi(poi(menu)+0x58))+0x40),pmbi->top為poi(poi(poi(poi(menu)+0x58))+0x44),其中poi(poi(poi(menu)+0x58))值可由用戶進行控制,令其為X,也就意味着我們通過控制X值,可以讀取X+0x40處的8字節內容,即pmbi.rcBar.left+(pmbi.rcBar.top<<32)。那么只需要控制X為欲要讀取的目的地址減去0x40,即可獲取相應數據。

  回到漏洞利用時封裝的讀取函數中,函數中首先向X指向的內存中每4個字節填寫一個相對於X基址的偏移值,這樣GetMenuBarInfo讀取回的pmbi.rcBar.left即為目標讀取地址應減去的差值。這么做的目的可能是為了防止系統版本的不同導致的差值不同,比如此次調試時win10 1909就為0x40。

  然后第二次調用GetMenuBarInfo,傳入(目的讀取地址- pmbi.rcBar.left),即可獲取目的地址8字節內容。

  這么一步步通過讀取,可以獲取到EPROCESS,然后通過ActiveProcessLinks,遍歷找到當前進程和system進程EPROCESS位置。

  再次兩次調用SetWindowLongPtrA,替換當前進程Token為system進程,獲取system權限。第一次將當前進程Token地址寫入WndMax對象pExtrabytes處,第二次將system進程Token寫入當前進程Token中。完成提權。

 

參考

https://ti.dbappsecurity.com.cn/blog/articles/2021/02/10/windows-kernel-zero-day-exploit-is-used-by-bitter-apt-in-targeted-attack-cn/

https://www.freebuf.com/vuls/271177.html

https://github.com/KaLendsi/CVE-2021-1732-Exploit

https://xiaodaozhi.com/exploit/29.html

https://theevilbit.github.io/posts/a_simple_protection_against_hmvalidatehandle_technique/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM