背景
CVE-2021-40449是一個存在於Win32k核心驅動中的UAF漏洞。該漏洞在2021年八月下旬九月上旬被Kaspersky發現用於野外攻擊活動中。通過Hook win32k驅動執行 NtGdiResetDC 過程中發生的使用者模式回撥,完成對目標物件的釋放和佔用,最終實現指定核心函式的呼叫,以進行核心記憶體的讀寫操作,修改利用物件的Token許可權,實現EOP。
分析
此次分析是在Windows 10 1809中進行。
首先在使用者模式呼叫 CreateDC 時,會執行至win32k核心呼叫 win32kfull!NtGdiResetDC ,再執行至 win32kbase!hdcOpenDCW ,呼叫堆疊如下:
...... win32kbase!PDEVOBJ::PDEVOBJ
...... win32kbase!hdcOpenDCW+0x240
...... win32kfull!GreResetDCInternal+0x11a
...... win32kfull!NtGdiResetDC+0xd6
...... nt!KiSystemServiceCopyEnd+0x25
...... win32u!NtGdiResetDC+0x14
...... gdi32full!ResetDCWInternal+0x16b
...... GDI32!ResetDCW+0x31
...... CVE_2021_40449!main
執行的使用者回撥主要發生在 win32kbase!PDEVOBJ::PDEVOBJ 中,該函式應是一個 PDEV 物件的初始化函式,和 win32kfull!NtGdiResetDC 傳入引數中的 HDC 有關聯。初始化函式中有兩個使用者回撥: PDEVOBJ::EnablePDEV 、 PDEVOBJ::CompletePDEV 。這兩個使用者回撥主要是對 HDC 中的 PDEV 物件進行操作, PDEV 物件通過 PDEV::Allocate 分配記憶體。
執行完初始化函式,回到 hdcOpenDCW ,繼續執行至 GreCreateDisplayDC ,該函式初始化一個 PDC 物件,並將上面初始化的 PDEV 物件的記憶體地址放到 PDC 偏移 +0x30 處。
然後返回 PDC 0 偏移處的 DC 控制程式碼值 HDC ,該值也作為 win32kbase!hdcOpenDCW 的返回值,返回值 win32kfull!GreResetDCInternal 。
hdcOpenDCW 返回的 HDC 傳入 DCOBJ::DCOBJ ,返回 hdcOpenDCW 初始化的 PDC 物件的記憶體地址。
接著讀取 PDEV 物件 0xAB8 偏移處的函式指標並執行,注意此處的 PDEV 並不是在上一步的 hdcOpenDCW 中初始化的,而是在使用者態呼叫 ResetDC 前,呼叫 CreateDC 生成的。為進行區分,本文中將其稱為 HDC_user 。
GreResetDCInternal 的函式引數 HDC_user ,同樣通過 DCOBJ::DCOBJ 返回 PDC_user 物件,該物件偏移 0x30 處為 PDEV_user 物件的記憶體地址。
取 PDEV_user 偏移 0xAB8 處函式指標,執行 UMPDDrvResetPDEV ,傳入引數分別為 PDEV_user 和 PDEV_kernel 偏移 0x708 處的指標,指向各自的 DEVMODE 結構,這裡同樣會發生一次使用者態函式回撥,不過該回撥不進行考慮,因為此漏洞利用範圍內,被利用的主要是該指標。
完成 UMPDDrvResetPDEV 回撥後,執行 win32kbase!HmgSwapLockedHandleContents ,該函式會將 PDC_user 和 PDC_kernel 首部的 HDC 值和 PDC 的 引用計數值 進行了互換,從而完成 devmode 修改的功能。
後面則是將兩個 PDC 物件的引用計數值分別減 1 ,並呼叫 win32kbase!bDeleteDCInternal 將 HDC_kernel 索引到的 PDC 物件偏移 0x30 處指標指向的 PDEV 物件引用計數值減 1 ,值變為 0 。而又因為之前的 HmgSwap 操作,這裡的 PDC 和 PDEV 實際都是使用者傳入的 HDC 原本指向的物件。
根據MSDN所說,“當該計數器降至零,該物件就會被釋放”、“一旦控制程式碼計數減為零,物件的名稱就會從物件管理器的名稱空間中刪除”。意味著該物件可以被佔用,而 hdcOpenDCW 中又存在使用者回撥,在使用者回撥中再對相同的 HDC 執行一次 ResetDC ,那麼該 HDC 對應 PDEV 物件引用值將減為 0 ,佔用該 PDEV 物件後結束回撥,回到核心。
至於漏洞的觸發點,在原本的 UMPDDrvResetPDEV 呼叫處,該呼叫發生在 hdcOpenDCW 之後,呼叫函式的地址從 PDEV_user 中獲取,通過佔用,可以獲取到修改器呼叫目標為一個核心讀寫函式。
利用
該UAF漏洞的利用主要為以下幾個步驟:
- 使用 NtQuerySystemInformation 獲取利用程式 Token.Privileges 在核心中的位置;
- 洩露出一個可以用於核心寫的核心函式,這裡比較通用是 nt!RtlSetAllBits ;
- 構造一個 Fake_RTL_BITMAP ,作為 nt!RtlSetAllBits 函式引數,大多使用 ThreadName 的方式進行構造,不過同樣也可以手動申請一片使用者態記憶體進行構造;
- HOOK使用者回撥 DrvEnablePDEV (Hook DrvCompletePDEV 雖然可以成功佔用,但執行不到漏洞觸發點),在Hook函式中對相同 HDC 再執行一次 ResetDC ,返回後使用構造的 Fake Palette 去佔用被釋放的 PDEV 物件,然後結束當前回撥;
- 漏洞觸發,當前程式許可權位全部被啟用,完成提權。
在Hook函式中完成佔用後的記憶體佈局前後對比如下所示:
PDEV物件佔用成功後,完成回撥,返回 GreResetDCInternal ,可以看到成功地呼叫到 nt!RtlSetAllBits 。
nt!RtlSetAllBits 中僅將 rcx 作為引數,而漏洞觸發處的第一個引數 rcx 同樣可以通過佔用指定。
nt!RtlSetAllBits 中取 rcx 地址 0x08 偏移處的 QWORD 作為寫入的目標地址,而 rcx 偏移 0 處的 DWORD 值整除 0x40 後作為計數值,每次向目標地址寫入 rax 暫存器的值, rax 固定為 0xffffffffffffffff 。
總結
這次我分析這個漏洞時嘗試儘量不看網上公開的POC,僅根據Kaspersky的文章尋找漏洞位置,結果花了很多時間,遇到挺多問題的。比如尋找漏洞點時,不會出現 BSOD ,並且 !pool 不能馬上看到物件記憶體狀態變成 free ,還是去瞄了一些公開的POC,確認自己方向沒問題。
emmm最後好歹自己完成了POC,雖然耗時長且程式碼拉胯,相比那些優秀的POC通用性低,但是收穫也很多,起碼漏洞前後附近的程式碼各個角落都翻了一遍,而且一些坑下次可以避免。
參考
[1] MysterySnail attacks with Windows zero-day
[2] CVE-2021-40449 Exploitation