1. 程式人生 > 實用技巧 >CVE-2020-0769逆向分析

CVE-2020-0769逆向分析

受影響版本:

系統 版本
Microsoft Windows 10
Windows 10 1607
Windows 10 1709
Windows 10 1803
Windows 10 1809
Windows 10 1903
Windows 10 1909
Windows 7 SP1
Windows 8.1
Windows RT 8.1
Windows Server 2008 SP2
Windows Server 2008 R2 SP1
Windows Server 2012
Windows Server 2012 R2
Windows Server 2016
Windows Server 2019
Windows Server 1803
Windows Server 1903
Windows Server 1909

此漏洞只會影響SMB v3.1.1
客戶端與服務端都存在此漏洞
服務端漏洞位於srv2.sys核心模組中,客戶端漏洞位於mrxxmb.sys模組

漏洞描述
從win10 1903/win server 1903開始對SMB v3.1.1進行資料壓縮的支援
包格式

翻譯後內容

此次漏洞觸發原因是因為客戶端/服務端在進行資料解壓是未對OriginalCompressedSegmentSize與Offset/Length 進行合理的長度檢查造成的
先來大致梳理一下函式的呼叫關係:
DriverEntry=>Srv2DeviceControl=>Srv2ProcessFsctl=>Srv2StartDriver=>Srv2StartInstance=>Srv2ReceiveHandler
然後Srv2ReceiveHandler函式會將Srv2DecompressMessageAsync函式放入SLIST_ENTRY連結串列中進行回撥非同步呼叫

然後Srv2DecompressMessageAsync函式會呼叫去呼叫Srv2DecompressData函式

Srv2DecompressData函式會根據OriginalCompressedSegmentSize與Offset/Length進行記憶體分配

_mm_srli_si128函式是一個與XMM暫存器相關的函式,此函式讓第一個引數v3邏輯運算向右移8個位元組,要注意他的移動單位是位元組不是位
此時Size指向ProtocolId,v4指向CompressionAlgorithm,Size偏移1個32位即4位元組便是OriginalCompressedSegmentSize,v4偏移一個4位元組便是Offset/Length
然後SrvNetAllocateBuffer函式申請記憶體空間其大小等於OriginalCompressedSegmentSize+Offset而這兩個值都是可控的,然後進入SrvNetAllocateBuffer檢視如何進行記憶體分配,要注意此函式與之後要看SmbCompressionDecompress的位於sysnet.sys模組中

進入SrvNetAllocateBuffe後他會先判斷SrvDisableNetBufferLookAsideList是否為真,或者,引數1即要分配的記憶體大小是否大於0x100100
如果一方成立就進入if在判斷引數1是否大於0x100100如果大於的話就返回失敗,如果僅僅是SrvDisableNetBufferLookAsideList為真那就呼叫SrvNetAllocateBufferFromPool進行記憶體分配,再來看看SrvDisableNetBufferLookAsideList是如何初始化的

可以看出SrvDisableNetBufferLookAsideList是在函式SrvNetRefreshLanmanServerParameters中進行初始化的

可以看出SrvDisableNetBufferLookAsideList的值肯定為一個布林值即真或假,SrvLibGetDWord函式會去呼叫ZwOpenKey開啟登錄檔鍵值然後使用ZwQueryValueKey去讀取登錄檔如果讀取成功則返回一個指定值,如果讀取失敗則返回ZwQueryValueKey的返回值即失敗原因,在我的系統裡沒有在登錄檔找到這個項,所以SrvDisableNetBufferLookAsideList的值預設為false,也就是說SrvNetAllocateBuffer的第一個if正常情況下不會去執行,順著流程往下走可以看到

他會先判斷引數1是否大於0x1100,然後求出到底用哪個值做SrvNetBufferLookasides的下標來獲取記憶體,如果不大於0x1100則預設下標為0,再來看看SrvNetBufferLookasides是如何初始化的

進入SrvNetCreateBufferLookasides函式,一直追下去會發現PplCreateLookasideList內部其實還是呼叫ExInitializeLookasideListEx函式來進行LookasideList列表的初始化,我們直接進入SrvNetBufferLookasideAllocate檢視分配了新的LookasideList列表的函式,這裡(1<<(v3+12))+256是要分配記憶體的大小,根據計算此大小依次為[0x900,0x1100,0x2100,0x4100,0x8100,0x10100......0x80100]

SrvNetBufferLookasideAllocate在內部又呼叫了SrvNetAllocateBufferFromPool函式

在SrvNetAllocateBufferFromPool函式中呼叫了ExAllocatePoolWithTag函式來分配指定型別的記憶體

分配大小v7我重新命名為size,然後會發現size=v6+v3=(2*(MmSizeOfMdl+8))+(lParam2 + 232)


最後要返回的資料我重新命名為backdata,剛剛從ExAllocatePoolWithTag函式獲取到的資料重新命名為ExAllocData
可以看出backdata=&ExAllocData[lParam2+0x57]&0xFFFFFFFFFFFFFFF8ui64

假設lparam2為0x1100,那0x1100+0x57=0x1157,0x1157&0xFFFFFFFFFFFFFFF8ui64=0x1150,也就是說返回的資料是從ExAllocatePoolWithTag函式獲取到的資料的0x1150偏移處開始的
根據上面可以總結出,SrvNetAllocateBuffer函式最後會建立一個‘結構體+資料’這種型別的一塊記憶體,這塊記憶體結構大致如下

回到srv2.sys中的Srv2DecompressData,在用SrvNetAllocateBuffer申請過記憶體後會呼叫SmbCompressionDecompress函式來解壓縮資料,此函式也在srvnet.sys中,其本質上是呼叫RtlDecompressBufferEx2函式來進行資料解壓縮的

這裡解釋一下幾個重要引數,方便與Srv2DecompressData中的傳入的引數一一對應

  • CompressionFormat:解壓縮演算法,此引數不用過多關注,他對應SmbCompressionDecompress的第一個引數
  • UncompressedBuffer:解壓後資料存放的緩衝區地址,對應SmbCompressionDecompress的第四個引數
  • UncompressedBufferSize:解壓資料緩衝區大小,對應SmbCompressionDecompress的第五個引數
  • CompressedBuffer:待解壓資料,對應SmbCompressionDecompress的第二個引數
  • CompressedBufferSize:待解壓資料大小,對應SmbCompressionDecompress的第三個引數
    返回值便是RtlDecompressBufferEx2函式的返回值

再回到Srv2DecompressData看看是如何呼叫SmbCompressionDecompress的

可以看出他會從*(_QWORD *)(*(_QWORD *)(v1 + 240) + 24i64) + Size.m128i_u32[3] + 16i64處獲取壓縮資料,經過解壓放入Size.m128i_u32[3] + *(_QWORD *)(backdata + 24)backdata + 24指向剛剛SrvNetAllocateBuffer申請記憶體的起始位置,在這裡也就是將解壓後資料放入‘記憶體起始位置+SMB資料包offset/length’處,第六個引數v11用於接收解壓後的資料大小,當SmbCompressionDecompress函式呼叫失敗或者解壓後的資料大小與SMB包中OriginalCompressedSegmentSize的值不一致時(不過如果RtlDecompressBufferEx2呼叫成功的話OriginalCompressedSegmentSize的值就會賦給v11),否則繼續往後執行,接著往後看

這段程式碼可以解釋為,如果offset/length不為0,則從(v1 + 240) + 24i64) + 16i64)處獲取資料後放入(v8 + 24)指向的地址,根據分析上面SmbCompressionDecompress函式的呼叫可知(v1 + 240) + 24i64) + 16i64)大致指向壓縮資料記憶體位置,(v8 + 24)指向記憶體起始的位置。

由於OriginalCompressedSegmentSize與Offset/Length長度我們可控,且SrvNetAllocateBuffer函式會根據他們倆來申請一塊‘資料+結構體’形式的記憶體,我們可以申請一塊較小的記憶體,將我們想要讓重新賦值的某塊記憶體的地址想辦法構造payload填充到(v8 + 24)處,然後在momove函式執行時就會將我們想要寫入的資料寫入到(v8 + 24)處