滲透測試之一:緩衝區原理分析

在學習滲透測試過程中,一定要看shellcode編程揭祕,在該書中存在很多自己需要掌握的基礎的知識,其中一個難點溢出分析,其中重點與難點是緩衝區溢出,轉摘一篇寫的很好的文章,分享給大家。

緩衝區溢出(Buffer Overflow)是計算機安全領域內既經典而又古老的話題。隨着計算機系統安全性的加強,傳統的緩衝區溢出攻擊方式可能變得不再奏效,相應的介紹緩衝區溢出原理的資料也變得“大衆化”起來。其中看雪的《0day安全:軟件漏洞分析技術》一書將緩衝區溢出攻擊的原理闡述得簡潔明瞭。本文參考該書對緩衝區溢出原理的講解,並結合實際的代碼實例進行驗證。不過即便如此,完成一個簡單的溢出代碼也需要解決很多書中無法涉及的問題,尤其是面對較新的具有安全特性的編譯器——比如MSVisual Studio2010。接下來,我們結合具體代碼,按照對緩衝區溢出原理的循序漸進地理解方式去挖掘緩衝區溢出背後的底層機制。

一、代碼 <=> 數據

顧名思義,緩衝區溢出的含義是爲緩衝區提供了多於其存儲容量的數據,就像往杯子裏倒入了過量的水一樣。通常情況下,緩衝區溢出的數據只會破壞程序數據,造成意外終止。但是如果有人精心構造溢出數據的內容,那麼就有可能獲得系統的控制權!如果說用戶(也可能是黑客)提供了水——緩衝區溢出攻擊的數據,那麼系統提供了溢出的容器——緩衝區。

緩衝區在系統中的表現形式是多樣的,高級語言定義的變量、數組、結構體等在運行時可以說都是保存在緩衝區內的,因此所謂緩衝區可以更抽象地理解爲一段可讀寫的內存區域,緩衝區攻擊的最終目的就是希望系統能執行這塊可讀寫內存中已經被蓄意設定好的惡意代碼。按照馮·諾依曼存儲程序原理,程序代碼是作爲二進制數據存儲在內存的,同樣程序的數據也在內存中,因此直接從內存的二進制形式上是無法區分哪些是數據哪些是代碼的,這也爲緩衝區溢出攻擊提供了可能。

進程地址空間分佈

1是進程地址空間分佈的簡單表示。代碼存儲了用戶程序的所有可執行代碼,在程序正常執行的情況下,程序計數器(PC指針)只會在代碼段和操作系統地址空間(內核態)內尋址。數據段內存儲了用戶程序的全局變量,文字池等。棧空間存儲了用戶程序的函數棧幀(包括參數、局部數據等),實現函數調用機制,它的數據增長方向是低地址方向。堆空間存儲了程序運行時動態申請的內存數據等,數據增長方向是高地址方向。除了代碼段和受操作系統保護的數據區域,其他的內存區域都可能作爲緩衝區,因此緩衝區溢出的位置可能在數據段,也可能在堆、棧段。如果程序的代碼有軟件漏洞,惡意程序會“教唆”程序計數器從上述緩衝區內取指,執行惡意程序提供的數據代碼!本文分析並實現棧溢出攻擊方式。

二、函數棧幀

棧的主要功能是實現函數的調用。因此在介紹棧溢出原理之前,需要弄清函數調用時棧空間發生了怎樣的變化。每次函數調用時,系統會把函數的返回地址(函數調用指令後緊跟指令的地址),一些關鍵的寄存器值保存在棧內,函數的實際參數和局部變量(包括數據、結構體、對象等)也會保存在棧內。這些數據統稱爲函數調用的棧幀,而且是每次函數調用都會有個獨立的棧幀,這也爲遞歸函數的實現提供了可能。

函數棧幀

如圖所示,我們定義了一個簡單的函數function,它接受一個整形參數,做一次乘法操作並返回。當調用function(0)時,arg參數記錄了值0入棧,並將call function指令下一條指令的地址0x00bd16f0保存到棧內,然後跳轉到function函數內部執行。每個函數定義都會有函數頭和函數尾代碼,如圖綠框表示。因爲函數內需要用ebp保存函數棧幀基址,因此先保存ebp原來的值到棧內,然後將棧指針esp內容保存到ebp。函數返回前需要做相反的操作——將esp指針恢復,並彈出ebp。這樣,函數內正常情況下無論怎樣使用棧,都不會使棧失去平衡。

sub esp,44h指令爲局部變量開闢了棧空間,比如ret變量的位置。理論上,function只需要再開闢4字節空間保存ret即可,但是編譯器開闢了更多的空間(這個問題很詭異,你覺得呢?)。函數調用結束返回後,函數棧幀恢復到保存參數0時的狀態,爲了保持棧幀平衡,需要恢復esp的內容,使用add esp,4將壓入的參數彈出。

之所以會有緩衝區溢出的可能,主要是因爲棧空間內保存了函數的返回地址。該地址保存了函數調用結束後後續執行的指令的位置,對於計算機安全來說,該信息是很敏感的。如果有人惡意修改了這個返回地址,並使該返回地址指向了一個新的代碼位置,程序便能從其它位置繼續執行。

三、棧溢出基本原理

上邊給出的代碼是無法進行溢出操作的,因爲用戶沒有“插足”的機會。但是實際上很多程序都會接受用戶的外界輸入,尤其是當函數內的一個數組緩衝區接受用戶輸入的時候,一旦程序代碼未對輸入的長度進行合法性檢查的話,緩衝區溢出便有可能觸發!比如下邊的一個簡單的函數。

 

void fun(unsigned char *data)
{
    unsigned char buffer[BUF_LEN];
    strcpy((char*)buffer,(char*)data);//溢出點
}

這個函數沒有做什麼有“意義”的事情(這裏主要是爲了簡化問題),但是它是一個典型的棧溢出代碼。在使用不安全的strcpy庫函數時,系統會盲目地將data的全部數據拷貝到buffer指向的內存區域。buffer的長度是有限的,一旦data的數據長度超過BUF_LEN,便會產生緩衝區溢出。

緩衝區溢出

由於棧是低地址方向增長的,因此局部數組buffer的指針在緩衝區的下方。當把data的數據拷貝到buffer內時,超過緩衝區區域的高地址部分數據會“淹沒”原本的其他棧幀數據,根據淹沒數據的內容不同,可能會有產生以下情況:

1、淹沒了其他的局部變量。如果被淹沒的局部變量是條件變量,那麼可能會改變函數原本的執行流程。這種方式可以用於破解簡單的軟件驗證。

2、淹沒了ebp的值。修改了函數執行結束後要恢復的棧指針,將會導致棧幀失去平衡。

3、淹沒了返回地址。這是棧溢出原理的核心所在,通過淹沒的方式修改函數的返回地址,使程序代碼執行“意外”的流程!

4、淹沒參數變量。修改函數的參數變量也可能改變當前函數的執行結果和流程。

5、淹沒上級函數的棧幀,情況與上述4點類似,只不過影響的是上級函數的執行。當然這裏的前提是保證函數能正常返回,即函數地址不能被隨意修改(這可能很麻煩!)。

如果在data本身的數據內就保存了一系列的指令的二進制代碼,一旦棧溢出修改了函數的返回地址,並將該地址指向這段二進制代碼的其實位置,那麼就完成了基本的溢出攻擊行爲。

基本棧溢出攻擊

通過計算返回地址內存區域相對於buffer的偏移,並在對應位置構造新的地址指向buffer內部二進制代碼的其實位置,便能執行用戶的自定義代碼!這段既是代碼又是數據的二進制數據被稱爲shellcode,因爲攻擊者希望通過這段代碼打開系統的shell,以執行任意的操作系統命令——比如下載病毒,安裝木馬,開放端口,格式化磁盤等惡意操作。

四、棧溢出攻擊

上述過程雖然理論上能完成棧溢出攻擊行爲,但是實際上很難實現。操作系統每次加載可執行文件到進程空間的位置都是無法預測的,因此棧的位置實際是不固定的,通過硬編碼覆蓋新返回地址的方式並不可靠。爲了能準確定位shellcode的地址,需要藉助一些額外的操作,其中最經典的是藉助跳板的棧溢出方式。

根據前邊所述,函數執行後,棧指針esp會恢復到壓入參數時的狀態,在圖4中即data參數的地址。如果我們在函數的返回地址填入一個地址,該地址指向的內存保存了一條特殊的指令jmp esp——跳板。那麼函數返回後,會執行該指令並跳轉到esp所在的位置——即data的位置。我們可以將緩衝區再多溢出一部分,淹沒data這樣的函數參數,並在這裏放上我們想要執行的代碼!這樣,不管程序被加載到哪個位置,最終都會回來執行棧內的代碼。

藉助跳板的棧溢出攻擊

藉助於跳板的確可以很好的解決棧幀移位(棧加載地址不固定)的問題,但是跳板指令從哪找呢?“幸運”的是,在Windows操作系統加載的大量dll中,包含了許多這樣的指令,比如kernel32.dllntdll.dll,這兩個動態鏈接庫是Windows程序默認加載的。如果是圖形化界面的Windows程序還會加載user32.dll,它也包含了大量的跳板指令!而且更“神奇”的是Windows操作系統加載dll時候一般都是固定地址,因此這些dll內的跳板指令的地址一般都是固定的。我們可以離線搜索出跳板執行在dll內的偏移,並加上dll的加載地址,便得到一個適用的跳板指令地址!

複製代碼
//查詢dll內第一個jmp esp指令的位置
int findJmp(char*dll_name)
{
    char* handle=(char*)LoadLibraryA(dll_name);//獲取dll加載地址
    for(int pos=0;;pos++)//遍歷dll代碼空間
    {
        if(handle[pos]==(char)0xff&&handle[pos+1]==(char)0xe4)//尋找0xffe4 = jmp  esp
        {
            return (int)(handle+pos);
        }
    }
}
複製代碼

這裏簡化了搜索算法,輸出第一個跳板指令的地址,讀者可以選取其他更合適位置。LoadLibraryA庫函數返回值就是dll的加載地址,然後加上搜索到的跳板指令偏移pos便是最終地址。jmp esp指令的二進制表示爲0xffe4,因此搜索算法就是搜索dll內這樣的字節數據即可。

雖然如此,上述的攻擊方式還不夠好。因爲在esp後繼續追加shellcode代碼會將上級函數的棧幀淹沒,這樣做並沒有什麼好處,甚至可能會帶來運行時問題。既然被溢出的函數棧幀內提供了緩衝區,我們還是把核心的shellcode放在緩衝區內,而在esp之後放上跳轉指令轉移到原本的緩衝區位置。由於這樣做使代碼的位置在esp指針之前,如果shellcode中使用了push指令便會讓esp指令與shellcode代碼越來越近,甚至淹沒自身的代碼。這顯然不是我們想要的結果,因此我們可以強制擡高esp指針,使它在shellcode之前(低地址位置),這樣就能在shellcode內正常使用push指令了。

調整shellcode與棧指針

調整代碼的內容很簡單:

add esp,-X
jmp esp

第一條指令擡高了棧指針到shellcode之前。X代表shellcode起始地址與esp的偏移。如果shellcode從緩衝區起始位置開始,那麼就是buffer的地址偏移。這裏不使用sub esp,X指令主要是避免X的高位字節爲0的問題,很多情況下緩衝區溢出是針對字符串緩衝區的,如果出現字節0會導致緩衝區截斷,從而導致溢出失敗。

第二條指令就是跳轉到shellcode的起始位置繼續執行。(又是jmp esp!)

通過上述方式便能獲得一個較爲穩定的棧溢出攻擊。

發佈了76 篇原創文章 · 獲贊 47 · 訪問量 24萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章