Kate Li (Taiwan)的部落格

首頁

通過poc來學習漏洞的原理

作者 sebastiani 時間 2020-03-05
all

本文介紹的是easyFTPServer 1.7.0.2‘Http’remote Buffer Overflow的漏洞執行流程,通過已知的POC來推斷程式的大概執行流程以及漏洞利用原理。

easyFTPServer 1.7.0.2 ‘Http’ remote Buffer Overflow

Poc和軟件的下載地址:

https://pan.baidu.com/s/1dHjKFCX (提取碼5h7h)

0x01瞭解FTP

FTP服務全稱為檔案傳輸通訊協定服務,其工作模式採用C/S模式,用戶想要通過FTP服務訪問到共亯的檔案資訊時,首先需要在自己的電腦系統上運行一個FTP用戶端,這個用戶端可以是一個FTP應用程序,也可以是作業系統自帶的命令列程式,然後在FTP用戶端中輸入用戶名和密碼來登入FTP服務程式,成功登陸後,用戶就可以獲取遠程電腦上共亯的檔案資訊了。

emmmm……,換句話說用戶端連接遠程FTP伺服器需要以下幾個步驟

(1)建立TCP連接

(2)用戶端向FTP服務程式發送USER命令以標識用戶自己的身份,然後服務程式要求用戶端輸入密碼

(3)用戶端發送PASS命令,同時將使用者密碼發送給遠程FTP服務程式

(4)服務端判斷並通過認證

(5)用戶端開始利用其它FTP協定進行檔案操作

(6)結束此次連接,用QUIT命令退出

0x02搭建實驗環境

FTP服務端:

win xp sp3(我用的是吾愛破解論壇的虛擬機器)

FTP用戶端:

win 7(要求Python環境)

把easyFTPServer 1.7.0.2解壓到xp上,點擊運行,目錄下會產生3個XML設定檔以及名為anonymous的資料夾,該程式默認情况下會設定一個名為anonymous的初始用戶,而anonymous資料夾就是該用戶使用的檔案目錄。

easyFTPServer 1.7.0.2 anonymous

好了,我們已經配寘完服務端了。

大家可能已經發現了,這個軟件除了提供21埠的FTP服務之外,還提供了8080埠的服務,這個8080埠就是今天我講解的重點,我們先訪問一下

http://192.168.1.106:8080

emmmm,輸入anonymous,anonymous之後介面如下

anonymous,anonymous

們就可以通過網頁對FTP伺服器上分享的檔案進行下載其本質是把FTP分享的檔案以web頁面的管道給大家呈現了出來。

0x03 poc的簡單介紹

本文我們不研究怎麼寫poc,當然我會在最後給大家介紹一下尋蛋(egghunter)這種exp開發科技,我打算通過已經寫好的exp來給大家反向講解一下這個漏洞的成因及原理,個人認為這樣更容易理解,具體怎麼樣要問各位看官了……我會在調試的時候把自己的思路給大家詳細說明一下

exp如下(Python2.7版本)

我大概講解一下這個exp的內容,本質是構造了一個get請求,給path參數一個超長的字串裡面有我們覆蓋返回地址的跳板地址,而HOST後面的字串是我們構造的shellcode,Authorization後接我們的用戶名和密碼。

HOST Authorization

這樣我們就可以簡單的登入並把惡意的數據發送給192.168.1.106:8080頁面所在的服務端上。

192.168.1.106:8080

這裡的shellcode是在win xp sp3上彈出小算盘,可以通過metasploit的msfvenom模塊進行生成,我就不演示了。

win xp sp3 msfvenom

0x04執行exp

一切準備就緒,我們開始執行exp

首先,在FTP服務端上用OD附加EasyFTP Server這款軟件,由於這個軟件運行的時候有兩個行程,我們附加它的FTP服務行程,也就是ftpbasicsvr這個行程(這裡儘量使用原版OD)

OD EasyFTP Server ftpbasicsvr

點擊附加

然後我們按F9讓程式繼續執行,即讓服務端接著監聽8080埠的請求。

我們回到用戶端,執行之前寫好的exp。

按下回車鍵,回到服務端看看發生了什麼?

emmmmm……小算盘彈了出來,說明exp沒有什麼問題。

那麼問題來了,我想看shellcode的詳細執行流程啊,而不是僅僅彈出一個小算盘。

接著我陷入了沉思:用戶端和FTP服務端進行通信的時候,本質上是socket通信,只不過是埠變成了8080罷了,有發送就會有接收,我們在recv()下中斷點不就可以截獲從用戶端發來的數據了嗎?

recv()

寫過c/ c++的人都知道,socket中的recv()函數是在WS2_32.dll中的。

c/c++ recv() WS2_32.dll

在OD上直接下斷bp recv,然後重啓FTP伺服器,重新附加行程,用戶端重新連接,OD的狀態如下:

bp recv

emmmmm……

看來我們的想法是對的,程式斷在了recv()函數的入口,注意看堆棧視窗有一堆aaaaaaaaaaa,那就是我們構造的get請求中path的參數,接下來我就要說明一下這個棧溢出漏洞的根本原因了,我們可以看到函數入口這樣一條彙編

recv() aaaaaaaaaaa

sub esp 124

就是開闢了一個292位元組的棧空間(124是十六進位),get請求中的path參數的值,也就是exp中buf的值,就存放在這個空間中。

而這個漏洞產生的原因就是沒有對path參數的值,也就是buf的值進行長度的校驗,以致於我們可以構造超長的字串從而覆蓋這個處理函數的返回地址進而對程式執行流程進行劫持。

具體怎麼實現的,我們拉到這個處理函數的末尾,在平棧的時候下斷也就是在0040b92D處下斷,按F9執行:

0040b92D

此時要注意堆棧,有一堆6161616161,而61正是a的十六進位表示,說明我們構造的超長字串已經入棧,F8單步執行到retn 8,我們看到返回地址被覆蓋成了跳板地址(這裡的跳板地址是ntdll.dll中的jmp esp,地址可以用OD挿件或者msf的模塊進行蒐索)

6161616161 retn 8 ntdll.dll jmp esp

執行完jmp esp後,F8向下執行,進入尋蛋(egghunter)部分(這裡就可以解釋一下py腳本中在跳板地址後面為什麼有8個\x63,因為retn 8嘛,返回的時候跳過了8個位元組)

retn 8

來到了我們的尋蛋指令,emmmmm關於這部分,你只需要知道的是蛋(也就是我們的shellcode)包含四個位元組的標誌頭.如果尋蛋開始,首先它會蒐索整個記憶體直至找到重複兩次找到這個標誌(如果標志是`”\x44\x7A\x32\x37”,那麼就蒐索”\x44\x7A\x32\x37\x44\x7A\x32\x37”)。當找到這個標誌,改變執行流跳轉到標誌後的shellcode執行。

`”\x44\x7A\x32\x37” ”\x44\x7A\x32\x37\x44\x7A\x32\x37”

接著我們對著00a2F196按F4,我們看寄存器edi的值變為了003D4960也就是尋蛋完成了,然後按F8執行,跳轉到了003D4960這段空間

00a2F196 003D4960 003D4960

明的你已經發現了,此時執行的shellcode正是py腳本中HOST:後邊的那一部分,拋開這個跳轉不談,此時,你或許有這樣的疑問

HOST:

HOST:後面的那些我們構造的shellcode到底存放在哪呢?

HOST:

我一開始想到的是棧,因為我們構造的path參數的值就在棧裡面,那麼HOST這一部分也應該在裡面,但我光速否决了,棧頂是00a2開頭的跟003D相差太大。

00a2 003D

那只能是堆了……

判斷棧地址還是堆地址直接用快速鍵ALT+M就可以看到了,其實這個方法是後來大佬給我說的,我當時判斷的方法是這樣的:

ALT+M

我先在資料視窗ctrl+G輸入003D0178

ctrl+G 003D0178

003D0178指向003D5178(chunk),我們轉到003D5178

003D0178 003D5178(chunk) 003D5178

發現003D5178指向003D0178,而shellcode的起始位置003D4960正好處於003D0688到003D5178之間,所以這段shellcode確實在堆中……

003D5178 003D0178 003D4960 003D0688 003D5178

emmmm……跟大佬的方法一比,還是對OD有點不熟練啊……

接下來我又產生了一個問題:FTP服務端是什麼時候把HOST:後面的shellcode寫到堆中的呢?

HOST:

要解决這個問題,我們要以一個開發者的思維來考慮,當一個get請求來的時候,我們肯定會創建一個堆區來保存path,Host,Authorization這些欄位的值,據我的開發經驗c/ c++對堆使用的函數

path,Host,Authorization c/c++

(1)一個是malloc(),動態分配,涉及到堆的分配

malloc()

(2)一個是HeapCreate(),創建一個堆,緊接著用HeapAlloc()方法分配堆空間

HeapCreate() HeapAlloc()

我傾向於第二種,所以我對HeapCreate()下了一個中斷點(bp HeapCreate),該函數位於kernel32.dll中,重新運行,用戶端建立連接,發現OD並沒有斷在該函數上……

HeapCreate() bp HeapCreate kernel32.dll

emmmm……難道我想錯了?

我接著陷入了沉思:HeapCreate()返回的控制碼會不會是一個全域變數,而且在我附加到行程之前就已經進行初始化了,所以才沒有斷下來,那麼我在HeapAlloc()下斷不就可以了嗎?因為開發者肯定會在數據到服務端的時候才進行堆分配並賦值的!

HeapCreate() HeapAlloc()

接著我把目標轉向了HeapAlloc()。這裡要注意一下在OD直接對HeapAlloc()下斷是不行的,因為kernel32.dll中的HeapAlloc()函數執行時緊接著會調用ntdll.dll中的RtlAllocateHeap()所以我們直接對RtlAllocateHeap()下斷(bp RtlAllocateHeap),重啓服務器,重新建立連接之後

HeapAlloc() HeapAlloc() kernel32.dll HeapAlloc() ntdll.dll RtlAllocateHeap() RtlAllocateHeap() bp RtlAllocateHeap

程式斷到了RtlAllocateHeap()的入口處,緊接著在資料視窗轉到003D4960,觀察資料視窗,一直按F9,會產生第一次突變

RtlAllocateHeap() 003D4960

此時已經把path參數的值寫入堆中,然後接著按F9,會產生第二次突變。

exp中Host:的值,也就是彈出小算盘的shellcode已經分配到堆中了,整個流程也就分析完了。

Host:

0x05小結

該漏洞是通過http的get請求提交的超長字串淹沒程式的返回地址,進而控制程式流程,再使用尋蛋科技使程式跳向堆中進行執行我們已經構造好的shellcode。

0x06關於尋蛋科技

我這裡粗略的講一下尋蛋科技的概要,通過前面的溯源我們應該都清楚了緩衝區溢位是怎麼工作的,以及我們怎麼劫持一個程式的執行流程,那麼問題來了,如果像這個漏洞一樣棧的空間不足放不下那麼大的shellcode怎麼辦?

很明顯我們通過把shellcode放到堆裡面,換句話說就是佈置shellcode在不同的記憶體區域,如果離的很近那麼我們直接用jmp offerset。

jmp offerset

如果離的遠,就像本例一樣,一個在棧,一個在堆。那麼我們就需要一個新的科技來找到它,這便是尋蛋科技的由來。蛋指的是shellcode的前四個位元組,就相當於一個標誌頭。尋蛋開始時,首先它會蒐索整個記憶體(棧/堆/…)直至找到重複兩次找到這個標誌。

當找到這個標誌,改變執行流跳轉到標誌後的shellcode執行。相信大家結合這個實例一定會對尋蛋科技有一個更深的體會。