Windows SMB Ghost(CVE-2020-0796)漏洞分析

發布時間 2020-04-09

漏洞介紹


2020年3月10日,微軟在其官方SRC發布了CVE-2020-0796的安全公告(ADV200005,Microsoft Guidance for Disabling SMBv3 Compression),公告表示在Windows SMBv3版本的客戶端和服務端存在遠程代碼執行漏洞。同時指出該漏洞存在于MicroSoft Server Message Block 3.1.1協議處理特定請求包的功能中,攻擊者利用該漏洞可在目標SMB Server或者Client中執行任意代碼。


啟明星辰ADLab安全研究人員在對該漏洞進行研究的過程中發現目前流傳的一些漏洞分析存在某些問題,因此對該漏洞進行了深入的分析,并在Windows 10系統上進行了復現。



漏洞復現


采用Windows 10 1903版本進行復現。在漏洞利用后,驗證程序提權結束后創建了一個system權限的cmd shell,如圖1所示。


圖1 CVE-2020-0796本地提權


漏洞基本原理


CVE-2020-0796漏洞存在于受影響版本的Windows驅動srv2.sys中。Windows SMB v3.1.1 版本增加了對壓縮數據的支持。圖2所示為帶壓縮數據的SMB數據報文的構成。


圖2 帶壓縮數據的SMB數據報文結構


根據微軟MS-SMB2協議文檔,SMB Compression Transform Header的結構如圖3所示。


圖3 SMB Compression Transform Header數據結構


ProtocolId:4字節,固定為0x424D53FC

OriginalComressedSegmentSize:4字節,原始的未壓縮數據大小

CompressionAlgorithm:2字節,壓縮算法

Flags :2字節,詳見協議文檔

Offset/Length:根據Flags的取值為Offset或者Length,Offset表示數據包中壓縮數據相對于當前結構的偏移

srv2.sys中處理SMBv3壓縮數據包的解壓函數Srv2DecompressData未嚴格校驗數據包中OriginalCompressedSegmentSize和Offset/Length字段的合法性。而這兩個字段影響了Srv2DecompressData中內存分配函數SrvNetAllocateBuffer的參數。如圖4所示的Srv2DecompressData函數反編譯代碼,SrvNetAllocateBuffer實際的參數為OriginalCompressedSegmentSize+Offset。這兩個參數都直接來源于數據包中SMB Compression Transform Header中的字段,而函數并未判斷這兩個字段是否合法,就直接將其相加后作為內存分配的參數(unsigned int類型)。


圖4 Srv2DecompressData函數的關鍵代碼


這里,OriginalCompressedSegmentSize+Offset可能小于實際需要分配的內存大小,從而在后續調用解壓函數SmbCompressionDecompress過程中存在越界讀取或者寫入的風險。


提權利用過程


目前已公開的針對該漏洞的本地提權利用包含如下的主要過程:

(1)驗證程序首先創建到SMS server的會話連接(記為session)。

(2)驗證程序獲取自身token數據結構中privilege成員在內核中的地址(記tokenAddr)。

(3)驗證程序通過session發送畸形壓縮數據(記為evilData)給SMB server觸發漏洞。其中,evilData包含tokenAddr、權限數據、溢出占位數據。

(4)SMS server收到evilData后觸發漏洞,并修改tokenAddr地址處的權限數據,從而提升驗證程序的權限。

(5)驗證程序獲取權限后對winlogon進行控制,來創建system用戶shell。


漏洞內存分配分析


首先,看一下已公開利用的evilData數據包的內容,如圖5所示。


圖5 提權poc發送的帶壓縮數據的SMB數據包


數據包的內容很簡單,其中幾個關鍵字段數據如下:

OriginalSize:0xffffffff

Offset:0x10

Real compressed data:13字節的壓縮數據,解壓后應為1108字節’A’加8字節的token地址。

SMB3 raw data:實際上是由2個8字節的0x1FF2FFFFBC(總長0x10)加上0x13字節的壓縮數據組成。

從上面的漏洞原理分析可知,漏洞成因是Srv2DecompressData函數對報文字段缺乏合法性判斷造成內存分配不當。在該漏洞數據包中,OriginalSize 是一個畸形值。OriginalSize + Offset = 0xffffffff + 0x10 = 0xf 是一個很小的值,其將會傳遞給SrvNetAllocateBuffer進行調用,下面具體分析內存分配情況。SrvNetAllocateBuffer的反編譯代碼如圖6。


圖6 SrvNetAllocateBuffer內存分配過程


由于傳給SrvNetAllocateBuffer的參數為0xf,根據SrvNetAllocateBuffer的處理流程可知,該請求內存將從SrvNetBufferLookasides表中分配。這里需要注意的是,變量SrvDisableNetBufferLookAsideList跟注冊表項相關,系統默認狀態下SrvDisableNetBufferLookAsideList為0。


圖7 SrvDisableNetBufferLookAsideList變量初始化過程


SrvNetBufferLookasides表通過函數SrvNetCreateBuffer初始化,實際SrvNetCreateBuffer循環調用了SrvNetBufferLookasideAllocate分配內存,調用SrvNetBufferLookasideAllocate的參數分別為[‘0x1100’, ‘0x2100’, ‘0x4100’, ‘0x8100’, ‘0x10100’, ‘0x20100’, ‘0x40100’, ‘0x80100’, ‘0x100100’]。在這里,內存分配參數為0xf,對應的lookaside表為0x1100大小的表項。


圖8 SrvNetCreateBuffer反編譯代碼


SrvNetBufferLookasideAllocate函數實際是調用SrvNetAllocateBufferFromPool來分配內存,如圖9所示。


圖9 SrvNetBufferLookasideAllocate反編譯代碼


在函數SrvNetAllocateBufferFromPool中,對于用戶請求的內存分配大小,內部通過ExAllocatePoolWithTag函數分配的內存實際要大于請求值(多出部分用于存儲部分內存相關數據結構)。以請求分配0x1100大小為例,經過一系列判斷后,最后分配的內存大小allocate_size = 0x1100 + E8 + 2*(MmSizeOfMdl + 8)。


圖10 SrvNetAllocateBufferFromPool函數反編譯代碼


內存分配完畢之后,SrvNetAllocateBufferFromPool函數還對分配的內存進行了一系列初始化操作,最后返回了一個內存信息結構體指針作為函數的返回值。


圖11  SrvNetAllocateBufferFromPool初始化內存數據


這里需要注意如下的數據關系:SrvNetAllocateBufferFromPool函數返回值return_buffer指向一個內存數據結構,該內存數據結構起始地址同實際分配內存(函數ExAllocatePoolWithTag分配的內存)起始地址的的偏移為0x1150;return_buffer+0x18位置指向了實際分配內存起始地址偏移0x50位置處,而最終return_buffer會作為函數SrvNetAllocateBuffer的返回值。其內存布局關系如圖12。



圖12 SrvNetAllocateBuffer(0xf)返回的內存數據布局


漏洞內存破壞分析


回到漏洞解壓函數Srv2DecompressData,在進行內存分配之后,Srv2DecompressData調用函數SmbCompressionDecompress開始解壓被壓縮的數據。其函數邏輯如圖13所示。


圖13 Srv2DecompressData解壓壓縮數據


實際上,該函數調用了Windows庫函數RtlDecompressBufferEx2來實現解壓,根據RtlDecompressBufferEx2的函數原型來對應分析SmbCompressionDecompress函數的各個參數。


SmbCompressionDecompress(CompressAlgo,//壓縮算法

Compressed_buf,//指向數據包中的壓縮數據  

Compressed_size,//數據包中壓縮數據大小,計算得到  

UnCompressedBuf,//解壓后的數據存儲地址,*(alloc_buffer+0x18)+0x10    

UnCompressedSize,//壓縮數據原始大小,源于數據包OriginalCompressedSegmentSize  

FinalUnCompressedSize)//最終解壓后數據大小


從反編譯代碼可以看出,函數SmbCompressionDecompress中保存解壓后數據的地址為*(alloc_buffer+0x18)+0x10的位置,根據內存分配過程分析,alloc_buffer + 0x18指向了實際內存分配起始位置偏移0x50處,所以拷貝目的地址為實際內存分配起始地址偏移0x60位置處。


在解壓過程中,壓縮數據解壓后將存儲到這個地址指向的內存中。根據evilData數據的構造過程,解壓后的數據為占坑數據和tokenAddr??截惖皆撎幍刂泛?,tokenAddr將覆蓋原內存數據結構中alloc_buffer+0x18處的數據。也就是解壓縮函數SmbCompressionDecompress返回后,alloc_buffer+0x18將指向驗證程序的tokenAddr內核地址??截愡^程如圖14和15所示。


圖14 解壓拷貝過程


圖15解壓完成后內存布局


繼續看Srv2DecompressData的后續處理流程,解壓成功后,函數判斷offset的結果不為0。不為0則進行內存移動,內存拷貝的參數如下:


memmove(*(alloc_buffer+0x18),SMB_payload,offset)

此時,alloc_buffer+0x18已經指向驗證程序的tokenAddr內核地址,而SMB_payload此時指向evilData中的權限數據,offset則為0x10。因此,這個內存移動完成后,權限數據將寫入tokenAddr處。這意味著,SMS Server成功修改了驗證程序的權限,從而實現了驗證程序的提權!


還有一個細節需要注意,在解壓時,Srv2DecompressData函數會判斷實際的解壓后數據大小FinalUnCompressedSize是否和數據包中原始數據大小OriginalCompressedSegmentSize一致,如圖16所示。



圖16 Srv2DecompressData檢查壓縮數據大小


按理來說實際解壓后的數據大小為0x1100,不等于數據包中的原始壓縮數據大小0xffffffff,這里應該進入到后面內存釋放的流程。然而,實際上在函數SmbCompressionDecompress中,調用RtlDecompressBufferEx2成功后會直接將OriginalCompressedSegmentSize賦值給FinalUnCompressedSize。這也是該漏洞關于任意地址寫入成功的關鍵之一。


圖17 SmbCompressionDecompres賦值FinalUnCompressedSize


漏洞修復建議


CVE-2020-0796是內存破壞漏洞,精心利用可導致遠程代碼執行,同時網絡上已經出現該漏洞的本地提權利用代碼。在此,建議受影響版本Windows用戶及時根據微軟官方漏洞防護公告對該漏洞進行防護。


參考鏈接:

1.https://fortiguard.com/encyclopedia/ips/48773

2.https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/ADV200005

3.https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-0796

4.https://www.catalog.update.microsoft.com/Search.aspx?q=KB4551762

5.https://github.com/danigargu/CVE-2020-0796

6.https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5606ad47-5ee0-437a-817e-70c366052962

7.https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-rtldecompressbufferex2