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)處