CVE-2020-0796 利用SMBGhost進行本地特權升級:Writeup + POC

利用SMBGhost(CVE-2020-0796)進行本地特權升級:Writeup + POC

介紹

CVE-2020-0796是SMBv3.1.1(也稱爲“ SMBGhost”)的壓縮機制中的錯誤。該錯誤影響Windows 10版本1903和1909,大約三週前由Microsoft 宣佈並修補。得知此消息後,我們將瀏覽所有細節並創建一個快速的POC(概念驗證),以演示如何通過引發BSOD(死亡藍屏)而無需身份驗證即可遠程觸發該錯誤。幾天前,我們返回此錯誤的不僅僅是遠程DoS。Microsoft安全公告將該錯誤描述爲遠程代碼執行(RCE)漏洞,但是沒有公共POC通過此錯誤來演示RCE。

初步分析

該錯誤是整數溢出錯誤,它發生在Srv2DecompressDatasrv2.sys SMB服務器驅動程序的函數中。這是該函數的簡化版本,省略了不相關的細節:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18歲
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
typedef struct _COMPRESSION_TRANSFORM_HEADER
{
    ULONG ProtocolId;
    ULONG OriginalCompressedSegmentSize;
    USHORT CompressionAlgorithm;
    USHORT Flags;
    ULONG Offset;
} COMPRESSION_TRANSFORM_HEADER, *PCOMPRESSION_TRANSFORM_HEADER;
 
typedef struct _ALLOCATION_HEADER
{
    // ...
    PVOID UserBuffer;
    // ...
} ALLOCATION_HEADER, *PALLOCATION_HEADER;
 
NTSTATUS Srv2DecompressData(PCOMPRESSION_TRANSFORM_HEADER Header, SIZE_T TotalSize)
{
    PALLOCATION_HEADER Alloc = SrvNetAllocateBuffer(
        (ULONG)(Header->OriginalCompressedSegmentSize + Header->Offset),
        NULL);
    If (!Alloc) {
        return STATUS_INSUFFICIENT_RESOURCES;
    }
 
    ULONG FinalCompressedSize = 0;
 
    NTSTATUS Status = SmbCompressionDecompress(
        Header->CompressionAlgorithm,
        (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset,
        (ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset),
        (PUCHAR)Alloc->UserBuffer + Header->Offset,
        Header->OriginalCompressedSegmentSize,
        &FinalCompressedSize);
    if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
        SrvNetFreeBuffer(Alloc);
        return STATUS_BAD_DATA;
    }
 
    if (Header->Offset > 0) {
        memcpy(
            Alloc->UserBuffer,
            (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
            Header->Offset);
    }
 
    Srv2ReplaceReceiveBuffer(some_session_handle, Alloc);
    return STATUS_SUCCESS;
}

Srv2DecompressData函數接收客戶端發送的壓縮消息,分配所需的內存量,然後解壓縮數據。然後,如果該Offset字段不爲零,則將放置在壓縮數據之前的數據照原樣複製到分配的緩衝區的開頭。

 

如果仔細看,我們會注意到第20和31行可能導致某些輸入的整數溢出。例如,大多數在發佈錯誤後不久並使系統崩潰的POC都使用0xFFFFFFFFOffset字段的值。使用該值0xFFFFFFFF會在第20行觸發整數溢出,結果分配的字節數減少了。

後來,它在第31行觸發了另一個整數溢出。崩潰的發生是由於在第30行計算的地址處的內存訪問,該地址與接收到的消息距離很遠。如果代碼在第31行驗證了計算結果,則由於緩衝區長度恰好爲負數且無法表示,因此它將提早解決,這也使第30行的地址本身也無效。

 

選擇要溢出的內容

我們只有兩個相關的字段可以控制以引起整數溢出:OriginalCompressedSegmentSizeOffset,因此沒有太多選擇。在嘗試了幾種組合之後,以下組合引起了我們的注意:如果我們發送合法Offset價值和巨大OriginalCompressedSegmentSize價值怎麼辦?讓我們看一下代碼將要執行的三個步驟:

  1. 分配:由於整數溢出,分配的字節數將小於兩個字段的總和。
  2. 解壓縮:解壓縮將獲得巨大的OriginalCompressedSegmentSize價值,將目標緩衝區視爲實際上具有無限大小。所有其他參數均不受影響,因此將按預期工作。
  3. 複製:如果將要執行(會嗎?),則複製將按預期工作。

無論是否要執行“複製”步驟,它都已經很有趣–我們可以在“解壓縮”階段觸發越界寫入,因爲我們設法分配了比“分配”階段所需的字節少的字節。

 

如您所見,使用這種技術,我們可以觸發任何大小和內容的溢出,這是一個很好的開始。但是什麼位於我們的緩衝區之外?讓我們找出答案!

深入SrvNetAllocateBuffer

爲了回答這個問題,在我們的案例中,我們需要查看分配函數SrvNetAllocateBuffer。這是函數的有趣部分:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18歲
19
20
21
22
23
PALLOCATION_HEADER SrvNetAllocateBuffer(SIZE_T AllocSize, PALLOCATION_HEADER SourceBuffer)
{
    // ...
 
    if (SrvDisableNetBufferLookAsideList || AllocSize > 0x100100) {
        if (AllocSize > 0x1000100) {
            return NULL;
        }
        Result = SrvNetAllocateBufferFromPool(AllocSize, AllocSize);
    } else {
        int LookasideListIndex = 0;
        if (AllocSize > 0x1100) {
            LookasideListIndex = /* some calculation based on AllocSize */;
        }
 
        SOME_STRUCT list = SrvNetBufferLookasides[LookasideListIndex];
        Result = /* fetch result from list */;
    }
 
    // Initialize some Result fields...
 
    return Result;
}

我們可以看到分配函數根據所需的字節數執行不同的操作。大型分配(大於約16 MB)只會失敗。中型分配(大於約1 MB)使用該SrvNetAllocateBufferFromPool功能進行分配。小分配(其餘)使用後備列表進行優化。

注意:還有一個SrvDisableNetBufferLookAsideList標誌會影響該功能的功能,但是它是由一個未記錄的註冊表設置來設置的,並且默認情況下處於禁用狀態,因此不是很有趣。

後備列表用於有效地爲驅動程序保留一組可重用的固定大小的緩衝區。後備列表的功能之一是定義自定義的分配/空閒功能,這些功能將用於管理緩衝區。查看SrvNetBufferLookasides數組的引用,我們發現它已在SrvNetCreateBufferLookasides函數中初始化,並且通過查看它,我們學到了以下內容:

  • 自定義分配函數定義爲SrvNetBufferLookasideAllocate,它僅調用SrvNetAllocateBufferFromPool
  • 我們使用Python快速計算出了9個後備列表,它們的大小如下:
    >>> [hex((1 <((1 <<(i + 12))+ 256)for range(9)中的i]
    ['0x1100',' 0x2100”,“ 0x4100”,“ 0x8100”,“ 0x10100”,“ 0x20100”,“ 0x40100”,“ 0x80100”,“ 0x100100”]
    這與我們的發現相符,即0x100100不使用後備列表就分配了大於字節的分配。

結論是每個分配請求最終都出現在SrvNetBufferLookasideAllocate函數中,因此讓我們來看一下。

SrvNetBufferLookasideAllocate和分配的緩衝區佈局

SrvNetBufferLookasideAllocate函數NonPagedPoolNx使用該ExAllocatePoolWithTag函數在池中分配一個緩衝區,然後用數據填充某些結構。分配的緩衝區的佈局如下:

 

對於我們的研究範圍,此佈局的唯一相關部分是用戶緩衝區和ALLOCATION_HEADER結構。我們可以立即看到,通過溢出用戶緩衝區,我們最終將覆蓋該ALLOCATION_HEADER結構。看起來很方便。

覆蓋ALLOCATION_HEADER結構

這時我們的第一個想法是,由於SmbCompressionDecompress調用之後的檢查:

<span style="color:#001c26">如果(狀態<0 || FinalCompressedSize!=標頭-> OriginalCompressedSegmentSize){
    SrvNetFreeBuffer(Alloc);
    返回STATUS_BAD_DATA;
}</span>

SrvNetFreeBuffer將被調用,並且該函數將失敗,因爲我們將其設計OriginalCompressedSegmentSize成一個很大的數字,並且FinalCompressedSize將成爲一個較小的數字,代表解壓縮的字節的實際數量。因此,我們分析了該SrvNetFreeBuffer函數,設法將分配指針替換爲一個幻數,然後等待free函數嘗試對其進行釋放,以期以後將其用於free-after-free或類似用途。但是令我們驚訝的是,該memcpy函數崩潰了。這使我們感到高興,因爲我們根本不希望到達那裏,但是我們必須檢查它爲什麼發生。可以在SmbCompressionDecompress函數的實現中找到說明:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18歲
19
20
21
22
NTSTATUS SmbCompressionDecompress(
    USHORT CompressionAlgorithm,
    PUCHAR UncompressedBuffer,
    ULONG  UncompressedBufferSize,
    PUCHAR CompressedBuffer,
    ULONG  CompressedBufferSize,
    PULONG FinalCompressedSize)
{
    // ...
 
    NTSTATUS Status = RtlDecompressBufferEx2(
        ...,
        FinalUncompressedSize,
        ...);
    if (Status >= 0) {
        *FinalCompressedSize = CompressedBufferSize;
    }
 
    // ...
 
    return Status;
}

基本上,如果解壓縮成功,則將其FinalCompressedSize更新以保留的值CompressedBufferSize,即緩衝區的大小。對FinalCompressedSize返回值的這種故意更新對於我們來說似乎非常可疑,因爲這個小細節以及分配的緩衝區佈局允許非常方便地利用此錯誤。

由於執行繼續到複製原始數據的階段,因此讓我們再次查看該調用:

<span style="color:#001c26">memcpy(
    Alloc-> UserBuffer,
    (PUCHAR)標題+ sizeof(COMPRESSION_TRANSFORM_HEADER),
    Header-> Offset);</span>

ALLOCATION_HEADER結構中讀取目標地址,我們可以覆蓋該結構。緩衝區的內容和大小也由我們控制。大獎!遠程寫入內核中的任何內容!

遠程“隨處寫”實現

我們做了一個快速實施的寫什麼,在哪裏CVE-2020-0796漏洞利用 Python中,這是基於對CVE-2020-0796的DoS POC maxpl0it。該代碼相當簡短。

本地特權升級

現在我們有了寫在哪裏的漏洞,我們該怎麼辦?顯然,我們可能會使系統崩潰。我們也許可以觸發遠程代碼執行,但是還沒有找到一種方法。如果我們在本地主機上使用該漏洞利用並泄漏其他信息,則可以將其用於本地特權升級,因爲已經通過多種技術證明了它的可行性。

我們嘗試的第一種技術是Morten Schenk在其《Black Hat USA 2017》演講中提出的。該技術涉及覆蓋驅動程序.data部分中的函數指針win32kbase.sys,然後從用戶模式調用適當的函數以執行代碼。j00ru撰寫了一篇有關在WCTF 2018中使用此技術的出色文章,並提供了他的利用源代碼。我們針對“在哪裏寫”漏洞進行了調整,但發現它不起作用,因爲處理SMB消息的線程不是GUI線程。因此,win32kbase.sys它沒有被映射,並且該技術也不相關(除非有一種使其成爲GUI線程的方法,而我們沒有研究過)。

我們最終在2012年的Black Hat演講“ Easy Local Windows Kernel Exploitation”中使用了cerarcer涵蓋的衆所周知的技術。該技術是關於通過使用API 泄漏當前進程令牌地址,然後對其進行覆蓋,授予當前進程令牌特權,該特權隨後可用於特權升級。該濫權令牌特權期末由布萊恩·亞歷山大(研究dronesec)和斯蒂芬·布林(breenmachine)(2017)展示了使用權限提升各種令牌特權的幾種方法。NtQuerySystemInformation(SystemHandleInformation)

我們的漏洞利用基於Alexandre Beaulieu在“ 利用任意寫入權限升級特權”文章中友善地共享的代碼。通過將DLL注入來修改進程的令牌特權後,我們完成了特權升級winlogon.exe。DLL的全部目的是啓動的特權實例cmd.exe。我們的完整本地特權升級概念證明可在此處找到,僅可用於研究/防禦目的。

摘要

我們設法證明可以利用CVE-2020-0796漏洞進行本地特權升級。請注意,我們的利用僅限於中等完整性級別,因爲它依賴於較低完整性級別不可用的API調用。我們可以做得更多嗎?也許可以,但是這需要更多的研究。我們可以在分配的緩衝區中覆蓋許多其他字段,也許其中之一可以幫助我們實現其他有趣的事情,例如遠程代碼執行。

POC源代碼

 

修復

  1. 我們建議將服務器和端點更新到最新的Windows版本,以修復此漏洞。如果可能,請阻塞端口445,直到部署更新爲止。無論CVE-2020-0796,我們建議在可能的情況下啓用主機隔離。
  2. 可以禁用SMBv3.1.1壓縮以避免觸發此錯誤,但是,如果可能的話,我們建議執行完全更新。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章