數據庫緩衝區溢出漏洞原理(棧)

背景

       在數據庫系統中已經發現了許多安全漏洞,其中比較嚴重且危害性比較大的有:緩衝區溢出和SQL注入2種。

       SQL注入主要依賴於結構化查詢語言,每種數據庫略有出入;SQL注入漏洞的利用,最主要的威脅是提權;後臺維護人員或******,可以藉此獲得DBA權限。需要說明的是,這裏所說的SQL注入並不是應用系統的SQL注入,而是數據庫自身的注入漏洞,這種漏洞比應用系統的注入漏洞危險性更高;對於SQL注入漏洞的存在,主要是由於數據庫中提供的系統或用戶函數存在的參數檢查不嚴和語句執行的缺陷。SQL注入漏洞不是本文的探討重點,會在安華金和數據庫安全實驗室發表的其它文章中進行探討。

       而對於緩衝區溢出的漏洞,風險性更高,因爲通過緩衝區溢出漏洞不僅可以危害數據庫,還可以控制操作系統,從而以數據庫服務器爲跳板,控制整個內網系統。緩衝區溢出的漏洞,不僅在數據庫系統中有,在操作系統上運行的相關應用軟件也有,但對於數據庫由於要提供大量外部訪問,防火牆、IPS等傳統不能對其進行禁止,這些***隱藏在數據庫的通訊協議中,具有更大的隱蔽性,更是難以防範。

       緩衝區溢出的***實現不僅需要注入點(注入點,是數據庫的漏洞提供的),同時還要依賴於操作系統的程序調用機制來實現;現在的操作系統對此,都在逐步加強防守,但***者總是能夠找出方法進行突破,這種突破主要依賴於shellcode的編寫。緩衝區溢出本質上是因爲操作系統無法區分數據和指令的區別,把數據當指令來執行,從而產生了無法預計的結果。安華金和數據庫安全實驗室將在本文將以windows XP爲依託,藉助代碼對如何利用緩衝區溢出做原理性介紹,以便於我們未來對其防範原理進行更好的討論。

 

緩衝區溢出

       緩衝區溢簡單說,是大的數據存入了小緩衝區,又不對存入數據進行邊界判斷,最終導致小緩衝區被撐爆。大的數據污染了小緩衝區附近的內存。污染的內存可能帶來改變程序控制流、奪取操作系統、禁止訪問等多種結果。下文所有的討論都是在windows xp上進行。

       緩衝區溢出主要可以分成三種:靜態數據溢出、棧溢出和堆溢出。產生這三種不同的溢出根源在於win的內存結構;win的內存可以被分成兩個層面:物理內存和虛擬內存。我們一般看到的其實只是windows的虛擬內存。在XP下windows會給所有進程都分配4G內存(無論物理內存真實多大);windows會把4G內存分成代碼區、數據區、堆區、棧區。數據區存儲的是進程的全局變量。如果利用這裏的數據進行緩衝區溢出那麼就被稱爲靜態數據溢出。同樣利用棧區和堆區進行緩衝區溢出,則相應被稱作棧溢出和堆溢出。靜態數據溢出雖然技術難度低但是靈活性和可以利用範圍低,所以本文就不介紹了。堆溢出相對複雜,將在別的文章介紹。本文介紹的是windows下的棧溢出,想要知道WINDOWS下的棧溢出如何利用,首先要理解windows下的棧結構。

 

棧結構

       爲了直觀的說清楚windows下的棧結構。我們構造一段代碼見下圖。這段代碼將完成3個任務

       1.演示WIN下棧的結構

       2.演示緩衝區溢出改變函數控制流程

       3.演示緩衝區溢出覆蓋返回地址(劫持函數)

       下面的程序包含一個主函數main和另外一個子函數re_choose。re_choose函數用於把從main函數中取得的輸入字符串和存儲的字符串liusicheng做對比。如果輸入的字符串和存儲的字符串一致則返回0。如果不一致則可能返回1或者-1。同時還人爲製造了一個緩衝區溢出點strcpy(buffer,input)。input有1024的空間,而buffer只有44的空間。只要input超過44則就會引發緩衝區溢出。main函數取re_choose返回值如果返回1或-1走if。如果返回0則走else。將用緩衝區溢出來讓返回1或者-1也走else。

#include <stdio.h>

#include <string.h>

#define ture_password"liusicheng"

int re_choose (char *input)

{

int result;

char buffer[44];

result = strcmp(input,ture_password);      

strcpy(buffer,input);                 //緩衝注入點

return result;

}

void main()

{

int choose=0;       

char input[1024];

scanf("%s",input);

choose=re_choose(input);

if (choose == 1 || choose == -1)

{

printf("error\n");

}

else

{

printf("ture\n");

}

 

}

 

       編譯出上面代碼的release版,放入IDA pro中得到反編譯代碼。下圖是MAIN函數的流程結構。清楚的看到main函數的整個控制流程和main函數的棧從建立到銷燬的全過程。棧主要用在函數調用上。進程調用的開始會調用大量系統函數,其中大量函數的地址是固定不變的(只和操作系統版本有關係),這些固定的函數將成爲以後用於跳轉的平臺。本文先不涉及這些函數。直接跳到main函數開始介紹。棧的結構是4個字節爲一層。如果超過4個字節。按照4個整數倍存儲。不足4個字節按照4個字節存儲。棧的主要操作只有2種push和POP。push是把寄存器的內容壓入到棧中,pop是把棧中的內容釋放掉。ebp是當前棧幀的棧底,esp是當前棧幀的棧頂。(注意由於棧是順序執行的所以同一時間只有一個棧頂和一個棧底。但棧底一般不是整個系統棧的棧底,而只是當前這個棧幀的棧底)。棧的結構採用先進先出,後進後出的原則。所以當創建一個棧的時候會遵循如下步驟:

       1、把上一個棧幀的棧底的指針壓入當前棧保存起來(push ebp)。這一步其實是2步:第一步壓入返回地址,第二步壓入當上一個棧幀的ebp。

       2、把上一個棧幀的棧底移動到上一個棧幀的棧頂(mov ebp,esp)。從此這個棧的棧底就確定且不會發生任何改變。棧頂esp會一直髮生變化。

       3、接着分配局部函數(subesp,404h)。本程序中2個變量1個是4字節1個是1024字節。加一起正好是0x404個字節。需要棧頂上移0x404。注意棧的方向和內存相反。數據進入內存是從低地址向高地址寫,而棧則是從高地址向低地址寫。正是這種結構,給了後來數據改寫之前數據的機會。棧頂的值會隨着棧中數據隨時進行調整。

wKioL1T4EarzM_g0AAEwc3ExzA8013.jpg 


注意:上圖中var_404= -404h、str1= -400h


       同樣棧撤銷的時候基本可以按照棧建立的逆操作進行。首先把棧底值覆蓋棧頂(mov esp,ebp)。接着棧中彈出當前ebp的值(pop ebp)。然後跳轉EIP中存儲的上個函數的返回地址(retn),回到前一個棧幀中(上一個函數中)刪除返回地址行(add esp 4)。到此棧被完全撤銷。至此一個棧從建立到撤銷的全部過程已經完成。我們除了關心一個棧的創建和消亡,更關心的就是棧是如何傳遞返回值和參數的。下圖是re_choose的反彙編圖:清楚的解釋了,在棧中是如何傳參和返回值的。

wKioL1T4EdLhONr-AADTwkVsUNk278.jpg 

註上圖中 var_4= -4 、str1 = 8

 

       main函數從 callsub_401000這句開始,創建子函數re_choose的棧幀,開始也是和main一樣的棧創建過程。直到執行到 mov eax,[ebp+str1],這句就是大家最關心的傳參。在棧中固定不動的是棧底(ebp)。利用棧底爲座標向高位內存移動8個字節取值。取到存在main中的input。放入eax寄存器中傳入re_choose用於計算。同樣的機制看後半段從re_choose中(mov eax,[ebp+var_4])取棧底向低地址偏移4個字節的內容。存在eax中,main把eax值存入ebp-404(mov [ebp+var_404],eax)這個地址中用於後續的判斷。至此棧的基本結構基本操作已經介紹完畢。棧緩衝區溢出的根源和棧的自身結構密切相關。正是由於棧中數據是先存入的在內存高地址,後入的在內存低地址。所以給了後入的機會,一旦超過棧原本分配的長度則會直接覆蓋原先存在內存高地址中的數據或指令。從而帶來不可預知的結果。

       至於函數的參數傳入的順序是從左到右還是從右到左(局部變量int a,b 是先壓a還是先壓b),函數返回時恢復棧平衡是讓母函數作還是子函數作。這部分和函數調用約定相關,主要的調用約定分爲,_cdecl、_fastcall和_stdcall。一般VS默認採用 _stdcall和windows api保持一致。stdcall規則要求:參數從右向左壓。(int a,b 先壓b)。函數退出的時候自己清理棧中的參數。(圖中經常會看到一個參數後面沒用直接被add esp 4了)

 

棧溢出利用原理

       由於棧中存儲數據和內存方向相反,導致很容易出現後面的數據覆蓋前面的數據。最終改變程序。改變的結果從漏洞***角度分爲2種:1改變程序邏輯,繞過一些判斷使得某些限制無效。2.直接劫持程序運行***者的***代碼。

       1.改變程序邏輯,還是上面的例子。上面的例子中假如liusicheng這個預設的密碼是某銀行的密碼,通過密碼檢查後可以獲取該密碼保護的敏感信息。那麼***者要想通過密碼檢查,要麼輸入正確的密碼,要麼就需要改變程序流程(輸入錯的密碼但是還能走回對的分支)。爲了達到這個目的,我們尋找下choose的地址(re_choose的返回值),看re_choose的圖發現局部變量空間是0x30也就是48個字節。在低地址的應該就是用於溢出的buffer在最靠近棧底的4個字節的應該是result的地址,也就是choose取值的地址。result的地址是0012FB6C(由於input輸入的是qwe所以返回值是1)值是1,此時密碼驗證過不去。看低地址的0012F840這個就是字符串buffer。buffer會拷入輸入的input值。buffer裏面存儲的就是657771(win是小端字節序所以全是反的也就是qwe)。到這裏,如果想讓密碼驗證通過,就需要修改0012FB6C的值。除了輸入正確的密碼外,還可以嘗試輸入過長的input,讓input向buffer拷數據的時候造成Buffer緩衝區溢出,用溢出的值覆蓋掉在0012FB6C的值,把值修改成希望的0(0就表示通過密碼驗證了)。

 wKiom1T4EP7AI3I3AADJChVsMD4660.jpg

       buffer佔44個字節也就是input需要至少輸入44個字節來佔滿buffer,然後再輸入的字節將會覆蓋result。修改choose的值,進而改變程序流程。由於這是字符串最後會有一位null所以咱們輸入44個w來佔滿buffer把null擠到result中覆蓋原來的1.

輸入43個w

 wKioL1T4EjrzeEgPAACVdwrF_Zs444.jpg

 

       沒有緩衝區溢出12FB6C未被修改

       輸入44個w達到緩衝區溢出

 wKioL1T4EnLgV6vuAACsKxPXzhA068.jpg

       12FB6C正好被結束符null覆蓋掉從1改成0。最終跳轉到密碼驗證通過的支路。至此完成繞過密碼檢驗的全部步驟。這是緩衝區溢出的最基本用法。其實方法2只是在方法1的基礎上更進一步而已。

 wKiom1T4EYOi54PLAACVJIu3bkY184.jpg

       2.直接劫持程序運行***者的***代碼。既然能通過緩衝區溢出覆蓋掉一些關鍵變量導致函數流程被改變。如果繼續向下溢出那麼就有可以覆蓋函數返回地址。改變函數返回地址到***者需要返回的地方。本程序可以設計成在buffer內存入一個腳本,然後用w填滿buffer中腳本到返回地址這中間的空缺。最後把buffer的首地址覆蓋到函數返回地址。讓函數返回時,返回到buffer的初始地址,執行buffer內存儲的腳本。

       爲了達到這個目標,首先要確定函數返回的地址和buffer的地址。前面已經得到了buffer的地址,看圖可知函數返回的地址。也就是需要從溢出點buffer到函數返回地址之間覆蓋我們的信息。函數返回地址在12FB74buffer初始地址是12FB40,咱們需要覆蓋掉這56個字節。

 wKiom1T4EanjJDrFAABZsH2Et3s869.jpg

      構造一個shellcode(介紹shellcode不再本文範圍之內)+填充數據+0012FB40。這樣當函數發生retn時,不會跳到0041064,而是跳轉到設定好的0012FB40中。後面就會按步執行存儲於0012FB40中的shellcode。至此就完成了整個溢出過程,通過緩衝區溢出劫持整個程序的方法除了這種直接的覆蓋返回值地址外,還有覆蓋SEH。

 

SEH

       SEH是windows下的異常處理機制的重要數據結構(c++的_try的異常處理其實本質就是調用的SEH)。保證windows在出現各種錯誤操作後給函數或系統一次call back的機會。SEH結構非常複雜,這裏只說和緩衝溢出有關的部分。每個SEH包含兩個DWORD指針:SEH鏈表指針和異常處理函數句柄,共8個字節存儲於棧中。當線程初始化時,會自動向棧中安裝1個SEH,作爲線程默認的異常處理。如果程序調用了-try()等異常處理機制。編譯器就是向當前函數棧中安裝1個SEH來處理異常的。棧中同時可以存在多個SEH。整個棧中的SEH通過鏈表指針形成一個貫穿整個棧的單向鏈表。當異常出現,操作系統中斷程序,沿着整個SEH鏈表依次查詢看是是否有能處理這個異常的SEH。如果程序加載的SEH都不能處理,則會到系統級的SEH,由他探出錯誤窗口,強制關閉程序。

       SEH存在於棧中,所以棧緩衝區溢出有機會覆蓋掉SEH。和覆蓋返回地址一樣,如果覆蓋後異常處理的函數入口被修改成上面的buffer的入口那麼就可以,通過shellcode+填充數據+buffer地址的手法。達到***的目的,但是要注意的是需要在填充數據中觸發異常來保證SEH被觸發。

       至此windows下緩衝區溢出的主要原理已經介紹完畢。安華金和數據庫安全實驗室將在下一篇文章中用一個oracle10g上的緩衝區漏洞來和大家繼續分享棧緩衝區溢出這一話題。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章