20155306 白皎 0day漏洞——漏洞利用原理之棧溢出利用
一、系統棧的工作原理
1.1內存的用途
根據不同的操作系統,一個進程可能被分配到不同的內存區域去執行。但是不管什么樣的操作系統、什么樣的計算機架構,進程使用的內存都可以按照功能大致分為以下4個部分:
-
代碼區:這個區域存儲着被裝入執行的二進制機器代碼,處理器會到這個區域取指並執行。
-
數據區:用於存儲全局變量等。
-
堆區:進程可以在堆區動態地請求一定大小的內存,並在用完之后歸還給堆區。動態分配和回收是堆區的特點。
-
棧區:用於動態地存儲函數之間的關系,以保證被調用函數在返回時恢復到母函數中繼續執行。
在Windows平台下,高級語言寫出的程序經過編譯鏈接,最終會變成PE文件。當PE文件被裝載運行后,就成了所謂的進程。四個區有着各自的功能,在進程運行中缺一不可,大致過程如下:
PE文件代碼段中包含的二進制級別的機器代碼會被裝入內存的**代碼區**(.text),處理器將到內存的這個區域一條一條地取出指令和在**數據區**存放的全局變量等操作數,並送入運算邏輯單元進行運算;如果代碼中請求開辟動態內存,則會在內存的**堆區**分配一塊大小合適的區域返回給代碼區的代碼使用;當函數調用發生時,函數的調用關系等信息會動態地保存在內存的**棧區**,以供處理器在執行完被調用函數的代碼時,返回母函數。
1.2系統棧
棧指的是一種數據結構,是一種先進后出的數據表。內存中的戰區實際上指的就是系統棧。
棧的最常見操作有兩種:
壓棧(PUSH)、彈棧(POP)。
用於標識棧的屬性也有兩個:
棧頂(TOP):push操作時,top增1;pop操作時,top減一。
棧底(BASE):與top正好相反,標識最下面的位置,一般不會變動的。
1.3寄存器與函數棧幀
每一個函數獨占自己的棧幀空間。當前正在運行的函數的棧幀總是在棧頂。Win32系統提供兩個特殊的寄存器用於標識位於系統棧頂端的棧幀。
-
ESP:棧指針寄存器(extended stack pointer),其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂。
-
EBP:基址指針寄存器(extended base pointer),其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的底部,並非系統棧的底部。
除此之外,還有一個很重要的寄存器。
- EIP:指令寄存器(extended instruction pointer),其內存放着一個指針,該指針永遠指向下一條等待執行的指令地址。 可以說如果控制了EIP寄存器的內容,就控制了進程——我們讓EIP指向哪里,CPU就會去執行哪里的指令。這里不多說EIP的作用,我個人認為王爽老是的匯編里面講EIP講的已經是挺好的了~這里不想多寫關於EIP的事情。
1.4函數調用約定與相關指令
函數調用大概包括以下幾個步驟:
(1)參數入棧:將參數從右向左依次壓入系統棧中。
(2)返回地址入棧:將當前代碼區調用指令的下一條指令地址壓入棧中,供函數返回時繼續執行。
(3)代碼區跳轉:處理器從當前代碼區跳轉到被調用函數的入口處。
(4)棧幀調整:具體包括:
<1>保存當前棧幀狀態值,已備后面恢復本棧幀時使用(EBP入棧)。
<2>將當前棧幀切換到新棧幀(將ESP值裝入EBP,更新棧幀底部)。
<3>給新棧幀分配空間(把ESP減去所需空間的大小,抬高棧頂)。
<4>對於_stdcall調用約定,函數調用時用到的指令序列大致如下:
push 參數3 ;假設該函數有3個參數,將從右向做依次入棧
push 參數2
push 參數1
call 函數地址 ;call指令將同時完成兩項工作:a)向棧中壓入當前指令地址的下一個指令地址,即保存返回地址。 b)跳轉到所調用函數的入口處。
push ebp ;保存舊棧幀的底部
mov ebp,esp ;設置新棧幀的底部 (棧幀切換)
sub esp,xxx ;設置新棧幀的頂部 (抬高棧頂,為新棧幀開辟空間)
函數返回的步驟如下:
<1>保存返回值,通常將函數的返回值保存在寄存器EAX中。
<2>彈出當前幀,恢復上一個棧幀。具體包括:
(1)在堆棧平衡的基礎上,給ESP加上棧幀的大小,降低棧頂,回收當前棧幀的空間。
(2)將當前棧幀底部保存的前棧幀EBP值彈入EBP寄存器,恢復出上一個棧幀。
(3)將函數返回地址彈給EIP寄存器。
<3>跳轉:按照函數返回地址跳回母函數中繼續執行。
add esp,xxx ;降低棧頂,回收當前的棧幀
pop ebp ;將上一個棧幀底部位置恢復到ebp
retn ;該指令有兩個功能:a)彈出當前棧頂元素,即彈出棧幀中的返回地址,至此,棧幀恢復到上一個棧幀工作完成。b)讓處理器跳轉到彈出的返回地址,恢復調用前代碼區
二、棧溢出利用之修改鄰接變量
-原理分析
本實驗目的:是研究如何通過非法的超長密碼去修改buffer的鄰接變量authenticated來繞過密碼驗證。
原理:一般情況下函數的局部變量在棧中一個挨着一個排列,如果這些局部變量中有數組之類的緩沖區,並且程序中存在數組越界的缺陷,那么越界的數組元素就有可能破壞棧中相鄰變量的值,甚至破壞棧幀中所保存的EBP值、返回地址等重要數據。
實驗代碼:
#include <stdio.h>
#include <string.h>
#define PASSWORD "1234567"
int verify_password(char *password){
int authenticated;
char buffer[8];
authenticated= strcmp(password,PASSWORD);
strcpy(buffer,password);
return flag;
}
void main(){
int valid_flag;
char password[1024];
while(1){
printf("Please input password: ");
scanf("%s",password);
valid_flag = verify_password(password);
if(valid_flag){
printf("Incorrect password!\n");
}
else{
printf("Congratulations!\n");
break;
}
}
}
通過代碼,我們可以想象出代碼執行到verify_password時候的棧幀狀態,如圖所示:
我們分析一下:局部變量authenticated正好位於緩沖區buffer的下方,為int型,占用4字節。因此,如果buffer越界,則buffer[8]——buffer[11]正好寫入相鄰的authenticated中。同時,通過源碼,我們可以發現當authenticated為0時,驗證成功;反之則不成功。所以,我們只要做到讓越界的ASCII碼修改authenticated的值為0,則繞過了密碼認證。
-實驗步驟
2.1 首先驗證程序運行結果,只有正確輸入“1234567”才可以通過驗證:
2.2假設我們輸入的密碼為7個“qqqqqqq”,按照字符串的關系大於1234567,strcmp返回1,因此authenticated值為1,通過ollydbg調試的實際內存如圖:【0x71是"q"的ASCII碼表示】
2.3下面我們試試輸入超過7個字符,輸入“qqqqqqqqrst”,如圖所示,正好從第9個字符開始,開始寫入authenticated中,因此authenticated的值為0x00747372:
2.4 我們知道,字符串數據最后都有座位結束標志的NULL(0),當我們嘗試輸入8個“q”,正好第九個字符0被寫入authenticated中,我們看一下:
果然密碼驗證成功了:
最后,我們可以明白只要輸入一個大於1234567的8個字符的字符串,那么隱藏的第九個截斷符就能將authenticated覆蓋為0,從而繞過驗證。
三、棧溢出利用之修改函數返回地址
-原理分析
上一個實驗介紹的改寫鄰接變量的方法似然很管用,但是並不太通用,本節介紹一個相對更通用的辦法,修改棧幀最下方的EBP和函數返回地址等棧幀狀態值。
下面,我們分析一下本實驗的原理:如果繼續增加輸入的字符,那么超出buffer[8]邊界的字符將依次淹沒authenticated、前棧幀EBP、返回地址。也就是說,控制好字符串的長度就可以讓字符串中相應位置字符的ASCII碼覆蓋掉這些棧幀狀態值。
因此,本實驗的目的是:我們通過溢出來覆蓋返回地址從而控制程序的執行流程。
我們大致可以得出以下結論:
可以得出以下的結論:
-
輸入11個'q',第9-11個字符連同NULL結束符將authenticated沖刷為0x00717171。
-
輸入15個'q',第9-12個字符將authenticated沖刷為0x71717171;第13-15個字符連同NULL結束符將前棧幀EBP沖刷為0x00717171。
-
輸入19個'q',第9-12字符將authenticated沖刷為0x71717171;第13-16個字符連同NULL結束符將前棧幀EBP沖刷為0x71717171;第17-19個字符連同NULL結束符將返回地址沖刷為0x00717171。
這里用19個字符作為輸入,看看淹沒返回地址會對程序產生什么影響。出於雙字對齊的目的,我們輸入的字符串按照"4321"為一個單元進行組織,最后輸入的字符串為"4321432143214321432"進行測試,用OD分析如下圖所示:
實際的內存狀況和我們分析的結論一致,此時的棧狀態見下表的內容:
由於鍵盤輸入ASCII碼范圍有限,所以將代碼稍作改動改為從文件讀取字符串。源碼如下:
#include <stdio.h>
#define PASSWORD "1234567"
int verify_password (char *password)
{
int authenticated;
char buffer[8];
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}
main()
{
int valid_flag=0;
char password[1024];
FILE * fp;
if(!(fp=fopen("password.txt","rw+")))
{
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}
-實驗步驟
3.1 用OD加載可執行文件,通過閱讀反匯編代碼,可以知道通過驗證的程序分支的指令地址為:0x00401028
3.2正常的執行是調用verify_password函數,然后進行比較來決定跳轉到錯誤或正確的分支。如果我們直接把返回地址覆蓋為驗證通過的地址,而不進入需要比較判斷的分支,豈不是可以繞過密碼驗證了。首先創建一個password.txt的文件,寫入5個“4321”后保存到與實驗程序同名的目錄下,如圖:
【buffer[8]需要2個“4321”,authenticated需要一個,EBP需要一個,因此要覆蓋返回地址,需要5個“4321”】
3.3保存后,用Ultra_32打開,切換到十六進制編輯模式:
3.4將最后4個字節改為新的返回地址【由於“大端機”的緣故,為了使最終數據為0x00401128,我們需要逆序輸入】
3.5切換為文本格式,這時也就驗證了為什么我們不再用鍵盤輸入字符串。
3.6將psaaword.txt保存后,用OD重新加載程序並調試,首先可以看到成功繞過密碼驗證:
3.7我們再回頭看一下最終的棧狀態:authenticated和EBP被覆蓋后均為0x31323334,返回地址被覆蓋后為0x00401128(正好為驗證成功的地址)
四、棧溢出利用之代碼植入
-原理分析
本實驗目的:在buffer中植入我們想讓他做的代碼,然后通過返回地址讓程序跳轉到系統棧中執行。這樣我們就可以讓進程去干本來干不了的事情啦!
為了在buffer中植入代碼,我們擴充了buffer的容量,來承載我們即將要植入的代碼!簡單的對代碼進行的修改,源碼如下:
#include<stdio.h>
#include<windows.h>
#define PASSWORD "1234567"
int verify_password(char * password)
{
int authenticated;
char buffer[44];
authenticated = strcmp(password,PASSWORD);
strcpy(buffer,password);
return authenticated;
}
int main()
{
int valid_flag = 0;
char password[1024];
FILE * fp;
LoadLibrary("user32.dll");//prepare for messagebox
if(!(fp = fopen("password.txt", "rw+")))
{
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}
同樣的,我們簡單分析一下棧的布局:如果buffer中有44個字符,那么第45個字符null正好覆蓋掉authenticated低字節中的1,從而可以突破密碼的限制。
-實驗步驟
4.1我們仍然以“4321”為一個單元,在password.txt中寫入44個字符,如圖:
4.2果然通過了驗證。
4.3通過OD可以看到,authenticated低字節被覆蓋。同時,我們可以知道buffer的起始地址為0x0012FB7C。因此password.txt中的第53-56個字符的ASCII碼值將寫入棧幀的返回地址中,成為函數返回后執行的指令。
4.4接下來,我們給password.txt植入機器代碼。
用匯編語言調用MessageboxA需要3個步驟:
(1)裝載動態鏈接庫user32.dll。MessageBoxA是動態鏈接庫user32.dll的導出函數。
(2)在匯編語言中調用這個函數需要獲得這個函數的入口地址。【 MessageBoxA的入口參數可以通過user32.dll在系統中加載的基址和MessageBoxA在庫中的偏移相加得到。(具體可以使用vc自帶工具“Dependency Walker“獲得這些信息) 】
(3)在調用前需要向棧中按從右向左的順序壓入MessageBoxA。
- 通過下圖,我們可以得知user32.dll的基地址為0x77D10000,MessageBoxA的偏移地址為0x000407EA,基地址加上偏移地址得到入口地址為0x77D507EA。
- 開始編寫函數調用的匯編代碼,這里我們可以先把字符串“failwest”壓入棧區,寫出的匯編代碼和指令對應的機器代碼如圖:
- 將上述匯編代碼一十六進制形式抄入password.txt,,但是要注意!第53~56字節為自己的buffer的起始地址。
4.5程序運行情況如下:
4.6我們可以對壓入的字符串進行修改,哈哈,改成自己的學號。
4.7在單擊彈框“ok”之后,程序會報錯崩潰,因為MessageA調用的代碼執行完成后,我們沒有寫安全退出的代碼。
這兩天學習0day漏洞原理利用之棧溢出,哈哈,整體非常開心。開始慢慢適應新的工具去幫助我解決一些問題,通過查看內存地址,寄存器的值以及棧中的數值變化來判斷出程序運行目前的狀態還有等等感覺自己之前數據結構學習棧或者l老師的課學習,還是沒能夠深入進去,只是知道程序運行和棧息息相關,但是具體什么關系,又不是很明確,但是這次學習就不一樣,給了自己壓力,需要一個人去完成一項任務,就會好好去看書,不懂查資料,會努力把一個東西弄明白,因為感覺自己連知識點都不會還怎么實踐呀哈哈。結果是順利的呀,學會了三種不同的方式來進行攻擊。對了,如果看到有不一樣的圖,那時因為我用了不同版本的OD去調試噠
參考文獻:
[1]王清.《0day安全:軟件漏洞分析技術》[M].中國:電子工業出版社,2008.
[2]王清.《0day安全:軟件漏洞分析技術(第2版)》[M].中國:電子工業出版社,2011.