標籤:資料庫安全 駭客攻擊 資料庫系統 漏洞攻擊 資料庫外網安全
背景
在資料庫系統中已經發現了許多安全性漏洞,其中比較嚴重且危害性比較大的有:緩衝區溢位和SQL注入2種。
SQL注入主要依賴於結構化查詢語言 (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。注意棧的方向和記憶體相反。資料進入記憶體是從低地址向高地址寫,而棧則是從高地址向低地址寫。正是這種結構,給了後來資料改寫之前資料的機會。棧頂的值會隨著棧中資料隨時進行調整。
650) this.width=650;" title="20150126-1.jpg" src="http://s3.51cto.com/wyfs02/M00/5A/28/wKioL1T4EarzM_g0AAEwc3ExzA8013.jpg" alt="wKioL1T4EarzM_g0AAEwc3ExzA8013.jpg" />
注意:中var_404= -404h、str1= -400h
同樣棧撤銷的時候基本可以按照棧建立的逆操作進行。首先把棧底值覆蓋棧頂(mov esp,ebp)。接著棧中彈出當前ebp的值(pop ebp)。然後跳轉EIP中儲存的上個函數的返回地址(retn),回到前一個棧幀中(上一個函數中)刪除返回地址行(add esp 4)。到此棧被完全撤銷。至此一個棧從建立到撤銷的全部過程已經完成。我們除了關心一個棧的建立和消亡,更關心的就是棧是如何傳遞傳回值和參數的。是re_choose的反組譯碼圖:清楚的解釋了,在棧中是如何傳參和傳回值的。
650) this.width=650;" title="20150126-2.jpg" src="http://s3.51cto.com/wyfs02/M02/5A/28/wKioL1T4EdLhONr-AADTwkVsUNk278.jpg" alt="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就表示通過密碼驗證了)。
650) this.width=650;" title="20150126-3.jpg" src="http://s3.51cto.com/wyfs02/M02/5A/2C/wKiom1T4EP7AI3I3AADJChVsMD4660.jpg" alt="wKiom1T4EP7AI3I3AADJChVsMD4660.jpg" />
buffer佔44個位元組也就是input需要至少輸入44個位元組來佔滿buffer,然後再輸入的位元組將會覆蓋result。修改choose的值,進而改變程式流程。由於這是字串最後會有一位null所以咱們輸入44個w來佔滿buffer把null擠到result中覆蓋原來的1.
輸入43個w
650) this.width=650;" title="20150126-4.jpg" src="http://s3.51cto.com/wyfs02/M01/5A/28/wKioL1T4EjrzeEgPAACVdwrF_Zs444.jpg" alt="wKioL1T4EjrzeEgPAACVdwrF_Zs444.jpg" />
沒有緩衝區溢位12FB6C未被修改
輸入44個w達到緩衝區溢位
650) this.width=650;" title="20150126-5.jpg" src="http://s3.51cto.com/wyfs02/M01/5A/28/wKioL1T4EnLgV6vuAACsKxPXzhA068.jpg" alt="wKioL1T4EnLgV6vuAACsKxPXzhA068.jpg" />
12FB6C正好被結束符null覆蓋掉從1改成0。最終跳轉到密碼驗證通過的支路。至此完成繞過密碼檢驗的全部步驟。這是緩衝區溢位的最基本用法。其實方法2隻是在方法1的基礎上更進一步而已。
650) this.width=650;" title="20150126-6.jpg" src="http://s3.51cto.com/wyfs02/M00/5A/2C/wKiom1T4EYOi54PLAACVJIu3bkY184.jpg" alt="wKiom1T4EYOi54PLAACVJIu3bkY184.jpg" />
2.直接劫持程式運行攻擊者的攻擊代碼。既然能通過緩衝區溢位覆蓋掉一些關鍵變數導致函數流程被改變。如果繼續向下溢出那麼就有可以覆蓋函數返回地址。改變函數返回地址到攻擊者需要返回的地方。本程式可以設計成在buffer記憶體入一個指令碼,然後用w填滿buffer中指令碼到返回地址這中間的空缺。最後把buffer的首地址覆蓋到函數返回地址。讓函數返回時,返回到buffer的初始地址,執行buffer記憶體儲的指令碼。
為了達到這個目標,首先要確定函數返回的地址和buffer的地址。前面已經得到了buffer的地址,看圖可知函數返回的地址。也就是需要從溢出點buffer到函數返回地址之間覆蓋我們的資訊。函數返回地址在12FB74buffer初始地址是12FB40,咱們需要覆蓋掉這56個位元組。
650) this.width=650;" title="20150126-7.jpg" src="http://s3.51cto.com/wyfs02/M01/5A/2C/wKiom1T4EanjJDrFAABZsH2Et3s869.jpg" alt="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上的緩衝區漏洞來和大家繼續分享棧緩衝區溢位這一話題。
本文出自 “資料庫安全” 部落格,請務必保留此出處http://schina.blog.51cto.com/9734953/1617670
資料庫緩衝區溢位漏洞原理(棧)