作者:LarryS@青藤實驗室
原文鏈接:https://mp.weixin.qq.com/s/dy4ytn6cgKxp7avu-WNSHg

0. 前言

這篇文章介紹了 CVE-2023-24949 漏洞,該漏洞英文全稱是 “Windows Kernel Elevation of Privilege Vulnerability”,由此判斷漏洞位于 ntoskrnl.exe 中。根據官方說明,攻擊者利用該漏洞可以實現本地提權。

本文按照個人的分析流程對漏洞進行了介紹,通過補丁對比、功能分析以及函數調試確定了漏洞原理以及漏洞的觸發方法,并成功對漏洞進行觸發,最后進行了簡單的總結。

1. 補丁對比

檢查補丁修復前后的 ntoskrnl.exe,發現一共修改了三個函數。

其中

IoInitSystemPreDrivers 調整了部分代碼結構,減少了條件語句的嵌套,同時將部分代碼整合成了新的函數,沒有功能性變化;

PopTransitionSystemPowerStateEx 同樣未發現功能性變化;

問題出現在 MiCaptureRetpolineRelocationTables 函數:

注:為了方便理解,我在這里直接貼出進行完函數功能分析后,已經整理了變量名和數據結構的函數版本。

注意黃框圈起的位置就是補丁修復位置,在補丁修復之前,首先進行三個數的相加,然后通過檢查加法結果是否大于其中一個加數來判斷是否溢出。補丁修復后,三個數相加的操作被分成兩步進行,每次的加法操作都通過調用 RtlULongAdd 實現,該函數有一個檢查加法結果是否大于被加數的操作:

NTSTATUS __stdcall RtlULongAdd(ULONG ulAugend, ULONG ulAddend, ULONG *pulResult)
{
  ULONG v3; // eax
  ULONG result; // edx
  NTSTATUS status; // eax

  v3 = ulAugend + ulAddend;
  result = -1;
  if ( v3 >= ulAugend )
    result = v3;
  status = v3 < ulAugend ? 0xC0000095 : 0;
  *pulResult = result;
  return status;
}

由此可以看出這其實是一個整數溢出問題,在補丁修復之前,只檢查了加數 imageDynamicRelocationRva 和其他兩個數相加之后是否溢出,但是沒有檢查加數 baseRelocSize 和 加數 12 相加后是否溢出。

注意截圖中的綠框,在后續代碼中使用了 baseRelocSize + 12 作為空間分配和數據復制的大小參數。在截圖中無法看出,進行加法操作的三個加數原本都是四字節數據,加法的結果也是四字節數據,但是在 64 位系統上,實際上存儲這些數據的寄存器是 64 位的,而進行空間分配的 MiAllocatePool 函數和進行數據復制的 memmove 函數,表示數據大小的參數是 size_t 類型,在 64 位系統上同樣表示 64 位數據。因此,如果表示數據大小的參數 baseRelocSize + 12 發生溢出,就可以繞過上方的 curRetRelocRva > endOfTable 檢查,從而在 memmove 函數中訪問到遠超出合法數據范圍數據。

2. 函數功能分析

上面的截圖已經對變量進行了重命名,并且導入了相關數據結構,但在分析之初,所有變量名都還是 v* 的格式,完全看不出函數功能,因此在進行了補丁分析之后,我首先對函數功能進行了分析。

2.1 初步的嘗試

從函數名來看,這個函數和 Retpoline 有關,Retpline 是用于解決 Spectre 漏洞變體的一種機制。同時根據函數引用,MiParseImageLoadConfig 函數對其進行了調用,看起來好像和可執行文件的加載有關,但是不確定。

我對這兩類漏洞有一些淺薄的認識,但是不知道 Retpoline 和可執行文件加載有什么關系,于是開始了一些漫無目的的搜索。

在對 Retpoline、relocation table 等關鍵詞進行搜索時,我發現了 Dynamic Value Relocation Table(DVRT) 的介紹【1】,及其鏈接的 Load Configuration Directory 的相關文檔【2】,事情一下子變得清晰了起來。

2.2 相關知識點介紹

Spectre 是和計算機硬件相關的一種漏洞,利用了 CPU 的分支預測機制實現敏感數據的泄露。Retpoline 就是谷歌為了解決該問題開發的一種 CPU 補丁機制。

在原有的 CPU 分支預測機制中,CPU 會根據之前執行的指令預測分支跳轉的目的地址并提前進行預處理,但是攻擊者可以通過構造特定的代碼序列促使 CPU 猜錯分支,從而獲取受保護的數據。

Retpoline 機制采用一種類似于 jump table 的模式,將分支跳轉操作轉換為一個間接的非分支跳轉,從而避免了 CPU 分支預測機制的使用。

Retpoline 的轉換是在將內核模塊加載到內存時臨時執行的,但是操作系統是怎么知道在哪里進行轉換呢?答案是所有的信息都保存在 Dynamic Value Relocation Table(DVRT) 中,可以通過二進制文件的 load config 目錄獲得。

DVRT 會在編譯階段放入二進制文件中,編譯器會把所有和間接跳轉/調用有關的元數據保存到文件的 .reloc 段。當文件運行時,內核會對 DVRT 數據進行解析,并對保存的所有分支跳轉進行轉換。

2.3 DVRT 相關數據結構

首先是 DVRT 本身的格式,我只列出了兩個數據結構,因為這兩個數據結構可以導入 IDA,有助于對漏洞函數的分析:

// DVRT 頭部
struct ImageDynamicRelocationTable
{
    uint32_t version;   // 目前只有一個版本 1
    uint32_t size;      // 頭部后面的 retpoline 信息數據大小
};

// 根據不同類型的 retpoline 類型,每塊的起始部位有一個頭部
struct ImageDynamicRelocation
{
    uint64_t symbol;          // 表示 retpoline 類型,可選數值為 3,4,5
    uint32_t baseRelocSize;   // 頭部后面的該類型 retpoline 信息數據大小
};

然后是 _IMAGE_LOAD_CONFIG_DIRECTORY64 結構,該結構可以用來對 DVRT 進行定位,同樣可以導入 IDA,后面在使用到該數據結構的時候,會對相關字段進行說明:

struct _IMAGE_LOAD_CONFIG_CODE_INTEGRITY {
   WORD Flags;           
   WORD Catalog;         
   DWORD CatalogOffset;  
   DWORD Reserved;       
};

struct _IMAGE_LOAD_CONFIG_DIRECTORY64 {
   DWORD Size;           
   DWORD TimeDateStamp; 
   WORD MajorVersion;    
   WORD MinorVersion;    
   DWORD GlobalFlagsClear;
   DWORD GlobalFlagsSet;   
   DWORD CriticalSectionDefaultTimeout; 
   ULONGLONG DeCommitFreeBlockThreshold; 
   ULONGLONG DeCommitTotalFreeThreshold;
   ULONGLONG LockPrefixTable;
   ULONGLONG MaximumAllocationSize;
   ULONGLONG VirtualMemoryThreshold; 
   ULONGLONG ProcessAffinityMask;
   DWORD ProcessHeapFlags; 
   WORD CSDVersion;     
   WORD DependentLoadFlags;
   ULONGLONG EditList;        
   ULONGLONG SecurityCookie;  
   ULONGLONG SEHandlerTable;   
   ULONGLONG SEHandlerCount;   
   ULONGLONG GuardCFCheckFunctionPointer; 
   ULONGLONG GuardCFDispatchFunctionPointer;
   ULONGLONG GuardCFFunctionTable;
   ULONGLONG GuardCFFunctionCount;
   DWORD GuardFlags;     
   _IMAGE_LOAD_CONFIG_CODE_INTEGRITY CodeIntegrity;
   ULONGLONG GuardAddressTakenIatEntryTable;
   ULONGLONG GuardAddressTakenIatEntryCount;
   ULONGLONG GuardLongJumpTargetTable;
   ULONGLONG GuardLongJumpTargetCount;
   ULONGLONG DynamicValueRelocTable;
   ULONGLONG CHPEMetadataPointer;
   ULONGLONG GuardRFFailureRoutine;
   ULONGLONG GuardRFFailureRoutineFunctionPointer;
   DWORD DynamicValueRelocTableOffset;
   WORD DynamicValueRelocTableSection;
   WORD Reserved2;
   ULONGLONG GuardRFVerifyStackPointerFunctionPointer;
   DWORD HotPatchTableOffset;
   DWORD Reserved3;
   ULONGLONG EnclaveConfigurationPointer;
   ULONGLONG VolatileMetadataPointer;
   ULONGLONG GuardEHContinuationTable;
   ULONGLONG GuardEHContinuationCount;
};

2.4 小結

根據以上信息,可以確定漏洞函數用于在 load config 階段獲取 DVRT,同時根據其調用者,以及調用之前執行的其他函數參數,就可以確定漏洞函數中大部分變量的類型及功能,最終得到文章開頭截圖中的函數狀態。

3. 漏洞觸發

3.1 確定方向

想要觸發漏洞,首先需要知道怎樣到達漏洞函數。在查看 MiCaptureRetpolineRelocationTables 函數的引用情況時,有兩個函數對該函數進行了調用:

ChatGPT 對這兩個函數的解釋如下:

MiParseImageLoadConfig 是 Windows 操作系統 NT 內核中的一個函數,用于解析可執行文件的加載配置信息。在 Windows 中,可執行文件通常包含有加載配置信息,這些信息用于指示操作系統如何將該文件加載到內存中,并且如何初始化其中的數據和代碼。 MiParseImageLoadConfig 函數主要是在內核模塊加載過程中使用的,它會從可執行文件中讀取加載配置信息,并且根據這些信息對內存進行合適的分配和初始化。具體來說,MiParseImageLoadConfig 函數可以完成以下幾個任務: 1. 解析可執行文件中的加載配置信息,包括基址、大小、校驗和等。 2. 根據加載配置信息計算出需要分配的內存大小和位置,并且分配相應的虛擬內存區域。 3. 將可執行文件中的代碼、數據等部分復制到分配的內存區域中,并且完成必要的重定位和初始化工作。

需要注意的是,MiParseImageLoadConfig 函數是 Windows 內核中的一個底層函數,通常不會直接被應用程序調用。它主要是由其他內核模塊相關的函數來調用,以完成可執行文件的加載和初始化工作。

MiCaptureBootDriverRetpolineInfo 是 Windows 操作系統 NT 內核中的一個函數,用于捕獲引導驅動程序的 Retpoline 信息。在 Windows 中,Retpoline 是一種特殊的代碼轉換技術,用于緩解 Spectre 及類似的 CPU 漏洞所導致的安全風險。 具體來說,當一個驅動程序被加載到內存中時,MiCaptureBootDriverRetpolineInfo 函數會掃描它的代碼段,并且為其中包含的 Retpoline 調用生成相應的跳轉目標地址表。這個跳轉目標地址表可以用于替代原本的 Retpoline 跳轉,從而提高代碼的執行效率和安全性。 需要注意的是,MiCaptureBootDriverRetpolineInfo 函數只會在引導過程中被調用,它主要是用于捕獲引導驅動程序的 Retpoline 信息,以便在后續的操作中使用。其他驅動程序的 Retpoline 信息則會在加載時進行捕獲和處理。

看起來一個函數是在可執行文件加載的時候被調用,一個是在系統引導的過程中被調用,顯然前者比較好分析。

雖然是這么想的,但是我并不確定上面信息的準確度,同時也不知道自己理解的是不是正確,因此我對這兩個函數分別下了斷點,嘗試操作系統看看會不會發生中斷。

之后我發現,在 Explorer 中對文件進行瀏覽時,可以輕易的多次中斷在 MiParseImageLoadConfig 函數上,但是并沒有出現 MiCaptureBootDriverRetpolineInfo 中斷的情況,因此選擇 MiParseImageLoadConfig 這條調用線作為分析對象。

3.2 定位 DVRT

目前并沒有公開文檔說明包含 DVRT 信息的 PE 文件結構,而且在上面測試系統是否中斷時,我也嘗試在漏洞函數上下了斷點,但是并沒有發生中斷在漏洞函數的情況,因此我沒有辦法通過分析已有文件確定 DVRT 信息的位置。

注:后來我發現其實在系統文件夾還是比較容易能夠中斷在漏洞函數的,但是因為文件太多,不確定到底是哪個文件包含 DVRT 信息。

一開始我嘗試直接使用 VS 的功能,讓編譯后的程序具有 DVRT,但是沒有成功,不確定 VS 是否具有相關功能,因此我決定任意創建一個程序,然后使用十六進制編輯器直接修改文件內容,使系統在處理該文件時能夠到達漏洞所在位置。

使用 visual studio 創建一個 Empty Project(C++),程序內容任意,我只寫了一個空的 main 函數,編譯生成 exe 文件。

先使用該文件進行測試,看系統代碼能夠執行到哪里。在 MiCaptureRetpolineRelocationTables 函數設置斷點,系統不會發生中斷,但是如果在 MiParseImageLoadConfig 設置斷點,系統會發生多次中斷,無法判斷哪次中斷是測試文件導致的。

通過查看 IDA,發現系統在調用 MiCaptureRetpolineRelocationTables 之前,首先調用了 MiCaptureDynamicRelocationTableRva 函數,如果該函數執行成功,才會繼續往下執行,因此我在該函數的調用位置設置斷點,并且將測試文件單獨放在一個文件夾中,在使用資源管理器打開該文件夾時,系統中斷在了 MiCaptureDynamicRelocationTableRva 函數的調用處,說明這個正常生成的測試文件至少是可以執行到 MiCaptureDynamicRelocationTableRva 函數的。

MiCaptureDynamicRelocationTableRva 函數定義如下:

注:configDirec 這個變量的類型是我根據函數功能以及和 DVRT 有關的數據結構進行的猜測,設置后發現 _IMAGE_LOAD_CONFIG_DIRECTORY64 類型確實能夠滿足函數功能。

注意圖片中的注釋信息,為了讓這個函數執行成功,我們需要讓 DynamicValueRelocTableSection 這個字段的數值不為 0。

但是如何確定 DynamicValueRelocTableSection 在文件中的位置呢?可以使用 VS 提供的 dumpbin.exe 工具,輸出結果如下:

PS C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx64\x86> ./dumpbin.exe /loadconfig 【手動馬賽克文件路徑】
Microsoft (R) COFF/PE Dumper Version 14.35.32216.1
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file 【手動馬賽克文件路徑】

File Type: EXECUTABLE IMAGE

  Section contains the following load config:

            00000140 size
                   0 time date stamp
                0.00 Version
                   0 GlobalFlags Clear
                   0 GlobalFlags Set
                   0 Critical Section Default Timeout
                   0 Decommit Free Block Threshold
                   0 Decommit Total Free Threshold
    0000000000000000 Lock Prefix Table
                   0 Maximum Allocation Size
                   0 Virtual Memory Threshold
                   0 Process Affinity Mask
                   0 Process Heap Flags
                   0 CSD Version
                0000 Dependent Load Flag
    0000000000000000 Edit List
    0000000140003008 Security Cookie
    0000000140002190 Guard CF address of check-function pointer
    00000001400021A0 Guard CF address of dispatch-function pointer
    0000000000000000 Guard CF function table
                   0 Guard CF function count
            00000100 Guard Flags
                       CF instrumented
                0000 Code Integrity Flags
                0000 Code Integrity Catalog
            00000000 Code Integrity Catalog Offset
            00000000 Code Integrity Reserved
    0000000000000000 Guard CF address taken IAT entry table
                   0 Guard CF address taken IAT entry count
    0000000000000000 Guard CF long jump target table
                   0 Guard CF long jump target count
    0000000000000000 Dynamic value relocation table
    0000000000000000 Hybrid metadata pointer
    0000000000000000 Guard RF address of failure-function
    0000000000000000 Guard RF address of failure-function pointer
            00000000 Dynamic value relocation table offset
                0000 Dynamic value relocation table section
.....

注意到文件中的 Dynamic value relocation table offset 和 Dynamic value relocation table section 都是 0,這里就是我們打算修改的內容。

為了能夠定位該內容在文件中的位置,可以搜索 0000000140003008 Security Cookie 這個字段,因為它的值比較特殊,不容易發生重復。最終定位到如下位置:

根據 _IMAGE_LOAD_CONFIG_DIRECTORY64 的結構確定紅框中配置信息的起始位置,同時定位要修改的 DVRT 信息所在位置,其中前四個字節是 offset,后四個字節是 section。我們需要修改的是后四個字節,讓其不等于0,同時因為這個二進制文件有 6 個 section,因此數值也不能大于 6。這里可以直接將這個數值修改成 1。

修改后的文件還不能到達漏洞點,但是此時可以先調試確定一下相關變量的數值。結合上面的 MiCaptureDynamicRelocationTableRva 函數截圖,當系統計算完 sectionHeader 之后,檢查該數值發現定位到的是第一個 section header .text

// sectionHeader
nt!MiCaptureDynamicRelocationTableRva+0xeb:
fffff805`7e9249f7 493bf8          cmp     rdi,r8
0: kd> db r8
fffff805`d4090208  2e 74 65 78 74 00 00 00-0c 0d 00 00 00 10 00 00  .text...........
fffff805`d4090218  00 0e 00 00 00 04 00 00-00 00 00 00 00 00 00 00  ................
fffff805`d4090228  00 00 00 00 20 00 00 60-2e 72 64 61 74 61 00 00  .... ..`.rdata..
fffff805`d4090238  b4 0e 00 00 00 20 00 00-00 10 00 00 00 12 00 00  ..... ..........
fffff805`d4090248  00 00 00 00 00 00 00 00-00 00 00 00 40 00 00 40  ............@..@
fffff805`d4090258  2e 64 61 74 61 00 00 00-38 06 00 00 00 30 00 00  .data...8....0..
fffff805`d4090268  00 02 00 00 00 22 00 00-00 00 00 00 00 00 00 00  ....."..........
fffff805`d4090278  00 00 00 00 40 00 00 c0-2e 70 64 61 74 61 00 00  ....@....pdata..
// 計算得到的 DVRT RVA
0: kd> p
nt!MiCaptureDynamicRelocationTableRva+0x100:
fffff805`7e924a0c 41890e          mov     dword ptr [r14],ecx
0: kd> r rcx
rcx=0000000000001000

繼續向下執行可以進入 MiCaptureRetpolineRelocationTables 函數,根據上面計算得到的 RVA 數值得到的 DVRT 如下:

0: kd> p
nt!MiCaptureRetpolineRelocationTables+0x76:
fffff805`7e9254de 4a8b043b        mov     rax,qword ptr [rbx+r15]
0: kd> p
nt!MiCaptureRetpolineRelocationTables+0x7a:
fffff805`7e9254e2 4889442420      mov     qword ptr [rsp+20h],rax
2: kd> db rbx+r15
fffff805`d4091000  b8 01 00 00 00 c3 cc cc-cc cc cc cc cc cc cc cc  ................
fffff805`d4091010  cc cc cc cc cc cc 66 66-0f 1f 84 00 00 00 00 00  ......ff........
fffff805`d4091020  48 3b 0d e1 1f 00 00 75-10 48 c1 c1 10 66 f7 c1  H;.....u.H...f..
fffff805`d4091030  ff ff 75 01 c3 48 c1 c9-10 e9 aa 02 00 00 cc cc  ..u..H..........
fffff805`d4091040  40 53 48 83 ec 20 b9 01-00 00 00 e8 be 0b 00 00  @SH.. ..........
fffff805`d4091050  e8 db 06 00 00 8b c8 e8-e8 0b 00 00 e8 cb 06 00  ................
fffff805`d4091060  00 8b d8 e8 0c 0c 00 00-b9 01 00 00 00 89 18 e8  ................
fffff805`d4091070  44 04 00 00 84 c0 74 73-e8 37 09 00 00 48 8d 0d  D.....ts.7...H..

在測試文件中搜索這段數據,定位發現這段數據就是 .text 段的起始內容:

根據上面對 DVRT 數據結構的介紹,我們知道,如果按照 DVRT 解析這段數據,這里保存的首先是 8 字節的 ImageDynamicRelocationTable 結構,然后是 12 字節的 ImageDynamicRelocation 結構。

分析到這里,雖然我們不知道具有 DVRT 的 PE 文件究竟是什么結構,但是我們已經可以欺騙系統讓它認為測試文件具有 DVRT,而且也知道了系統認為的 DVRT 在文件中的位置。

3.3 分析觸發條件 & 嘗試觸發漏洞

我們看一下 MiCaptureRetpolineRelocationTables 函數的細節,確定 DVRT 需要滿足的要求:

注意黃框圈起的部分,按照要求修改測試文件中的數據:

到這里測試文件應該是修改好了,重新測試的時候發現系統并沒有崩潰,調試器提示信息:

SXS: BasepCreateActCtx() NtCreateSection() failed. Status = 0xc000009a

重新在 MiCaptureRetpolineRelocationTables 單步調試,發現當系統執行到三個數值的加法處時,確實發生了整數溢出:

3: kd> p
nt!MiCaptureRetpolineRelocationTables+0xfe:
fffff805`7e925566 4503f4          add     r14d,r12d
3: kd> r r14d
r14d=1014
3: kd> r r12d
r12d=ffffffff
3: kd> p
nt!MiCaptureRetpolineRelocationTables+0x101:
fffff805`7e925569 443bf2          cmp     r14d,edx
3: kd> r r14d
r14d=1013

但是當系統執行到 MiAllocatePool 函數時,空間分配失敗了。仔細看一下這個函數的調用參數:

3: kd> p
nt!MiCaptureRetpolineRelocationTables+0x181:
fffff805`7e9255e9 e832ddb1ff      call    nt!MiAllocatePool (fffff805`7e443320)
3: kd> r rcx
rcx=0000000000000100
3: kd> r rdx
rdx=000000010000000b

rdx 應該就是分配空間大小,正如我們所猜測的那樣,由于整數溢出,這里會分配一個超大的空間。問題就在這里,因為我的虛擬機內存不足,所以這個函數調用失敗了。

調高虛擬機內存后,空間分配成功,繼續執行數據復制操作:

0: kd> p
nt!MiCaptureRetpolineRelocationTables+0x19d:
fffff805`7e925605 e83619d0ff      call    nt!memcpy (fffff805`7e626f40)
0: kd> r rcx
rcx=ffffd6001c000000
0: kd> r rdx
rdx=fffff805e5631008
0: kd> r r8
r8=000000010000000b

同樣會嘗試復制大量數據,數據源地址 fffff805e5631008,數據大小 10000000b,由于訪問到非法內存空間,系統崩潰。

4. 總結

通過以上分析可知,CVE-2023-24949 是一個整數溢出漏洞,到我分析的位置為止,該漏洞可以實現內存越界讀,如果想要實現本地提權,還需要進一步分析。

值得注意的是,這個漏洞的觸發條件十分簡單,在保證內存足夠的前提下,只要在資源管理器看到測試文件就能觸發漏洞,如果將測試文件放在桌面,系統將無法正常啟動。如果配合釣魚攻擊,該漏洞可以實現更多功能。鑒于此,這次的 poc 文件不會直接發布在 github 上,感興趣的朋友可以根據文章自己進行復現。

5. 參考資料

  1. Dynamic Value Relocation Table (DVRT) details
  2. Windows PE32 load config directory
  3. How Meltdown and Spectre haunt Anti-Cheat
  4. https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-24949

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2080/