緩沖區溢出
在大緩沖區的數據向小緩沖區復制的過程中,由於沒注意小緩沖區的邊界,“撐爆”了較小的緩沖區,從而沖掉了和小緩沖區相鄰內存區域的其他數據而引起的內存問題。
無論什么計算機架構,進程使用的內存都可以按照功能大致分為4個部分:
(1)代碼區:這個區域存儲着被裝入的執行的二進制代碼,處理器會到這個區域取指並執行。
(2)數據區:用於存儲局部變量。
(3)堆區:進程可以在堆區中動態的請求一定大小的內存,並在用完之后歸還個堆區。動態分配和回收是堆區的特點。
(4)棧區:用於動態的存儲函數之間的調用關系。以保證被調用函數在返回時恢復到母函數中繼續執行。
棧與系統棧
棧:指的是一種數據結構,是一種先入后出的數據表。系統棧:指的是內存中的棧,由系統自動維護,他用於實現高級語言中的函數調用。
- 系統棧:指的是內存中的棧,由系統自動維護,他用於實現高級語言中的函數調用。
函數調用過程
當函數被調用時,系統棧會為這個函數新開辟一個棧幀,並把它壓入棧中,這個棧幀的內存空間被它所屬的函數獨占,正常情況下是不會和別的函數共享的。
函數調用大致包括以下幾個步驟:
(1)參數入棧:將參數從右向左依次壓入系統棧。
(2)返回地址入棧:將當前代碼區調用的下一條指令地址壓入棧中,供函數返回時繼續執行。
(3)代碼區跳轉:處理器從當前代碼區跳到被執行函數入口。
(3)棧幀調整:1.保存當前棧幀狀態,已被后面恢復本棧幀使用(push ebp)
2.將當前棧幀切換到新的棧幀(mov ebp,esp)
3.給新棧幀分配空間(把ESP減去所需空間大小,抬高棧頂)
例如:
對於_stdcall 調用約定,函數調用時用到的指令序列如下:
;調用前 push 參數3; ;假設該函數有3個參數,將從右向左依次入棧 push 參數2; push 參數1; call 函數地址; ; 該指令同時完成兩件事:(a)向棧中壓入當前指令在內存中 ;的位置,及保存返回地址 ;(b)跳轉到函數地址 push ebp mov ebp,esp sub esp,xxx
類似的函數返回步驟:
(1)保存返回值,通常將函數返回值保存在寄存器eax中。
(2)彈出當前棧幀,恢復上一個棧幀
add esp,xxx ; 降低棧頂,回收當前棧幀 pop ebp ; 將上一個棧幀底部位置ebp恢復 retn ; 這個指令有兩個作用 (a)彈出當前棧頂元素,即彈出棧幀中的返回地址, ;至此,棧幀恢復。 ;(b)讓處理器跳轉到彈出的返回地址,恢復調用前的代碼
寄存器與函數棧幀
每一個函數獨占自己的棧幀空間。當前運行的函數的棧幀總在棧頂。Win32系統提供兩個特殊的寄存器用於標識位於系統的棧頂端的棧幀。
(1)ESP:棧指針寄存器,其內存中是一個指針,該指針永遠指系統棧最上面的一個棧幀的棧頂。
(2)EBP:基址指針寄存器,其內存中是一個指針,該指針永遠指向系統棧最上面的要給棧幀的底部。
(3)EIP:指令寄存器,其內存中是一個指針,該指針永遠指向下一條等待執行的指令地址。
函數調用約定
調用約定 |
_cdecl |
_fastcall |
_stdcall |
參數入棧順序 |
右→左 |
右→左 |
右→左 |
恢復平衡的位置 |
調用者 |
函數本身 |
函數本身 |
修改鄰接變量
通過上面的知識我們知道,函數的調用細節和棧中的數據分布情況,函數的局部變量在棧中一個挨着一個排着,如果這些局部變量中有數組之類的緩沖區,並且程序中存在數
組越界的缺陷,那么越界的數組元素就有可能破壞棧中相鄰變量的值,甚至破壞棧幀中保存的ebp的值、返回地址等重要數據。
下面舉個例子,來說明一下破壞棧內局部變量對程序安全性的影響:
1 #include <IOSTREAM> 2 using namespace std; 3 #define PASS_WORD "1234567" 4 5 int verify_password(char* password) 6 { 7 int authentitated; 8 char buffer[8]; 9 authentitated = strcmp(password,PASS_WORD); 10 strcpy(buffer,password); 11 return authentitated; 12 } 13 14 int main() 15 { 16 int valid_flag = 0; 17 char password[1024] = {0}; 18 while (1) 19 { 20 printf("please input password:"); 21 scanf("%s",password); 22 valid_flag = verify_password(password); 23 if(valid_flag) 24 { 25 printf("incorrect password!\r\n"); 26 } 27 else 28 { 29 printf("Congratulation! you have passed the verification!\r\n"); 30 } 31 } 32 return 0; 33 }
當我們輸入的是qqqqqqq,上述代碼verify_password棧幀布局:
由此可知,在verify_password棧幀中,局部變量authenticated,位於緩沖區buffer[8]的下方,authenticated是int型,在內存中占4個字節,所以,如果能讓buffer數組越界,就能夠影
響到authenticated。在程序中,authenticated為0表示驗證成功,為1表示驗證失敗,我們通過讓buffer數組越界,達到修改authenticated值得目的。
通過我們輸入可以造成緩沖區溢出,導致authenticated的值被修改。所以當我們輸入8個字符,第9個字符,作為結尾的NULL字符,將剛好寫到authenticated內存的低位上去,導致
authenticated由 0x00000001 變為 0x00000000,驗證通過。
修改函數返回地址
上述的的修改鄰接變量的方法是很有用的,但是這種漏洞利用對代碼的環境要求相對苛刻,更強大、更通用的攻擊通過緩沖區溢出改寫的目標往往不是一個變量,而是瞄准棧幀的
最下方的EBP和函數返回地址等棧幀狀態。
也就是說,我們繼續增加輸入的字符串長度,超出buffer[8]邊界,一次淹沒authenticated、前棧幀EBP、返回地址。也就是說,控制好字符串長度就可以讓字符串中相應的位置字符
的ASCII碼覆蓋這些棧幀的狀態。
當我們輸入一個足夠長的字符串是,程序崩潰,這是由於字符串足夠的長,淹沒了程序的返回地址,我們知道,當我們程序執行完畢之后,在執行retn指令時,棧頂恰好就是源程
序的返回地址,”retn”指令會把這個地址pop,彈入eip寄存器中,之后跳轉到這個地址去執行。
程序崩潰的原因是因為,函數返回地址裝入eip ,但是eip由於緩沖區溢出,淹沒了,將值改變,程序執行找不到對應地址的指令,導致程序崩潰。但是,如果我們給出一個有效
的地址,就可以讓處理器跳轉到任意的代碼去執行,也就是說,我們可以通過淹沒返回地址從而控制程序的執行。
下面舉個例子,通過緩沖區溢出,淹沒eip,修改eip寄存器,從而控制程序執行:
1 #include <IOSTREAM> 2 using namespace std; 3 #define PASS_WORD "1234567" 4 int verify_password(char* password) 5 { 6 int authentitated; 7 char szBuffer[8]; 8 authentitated = strcmp(password,PASS_WORD); 9 strcpy(szBuffer,password); 10 return authentitated; 11 } 12 int main() 13 { 14 int valid_flag = 0; 15 char password[1024] = {0}; 16 FILE* fp ; 17 fp=fopen("password.txt","rw+"); 18 19 if(fp==NULL) 20 { 21 exit(0); 22 } 23 24 fscanf(fp,"%s",password); 25 valid_flag = verify_password(password); 26 27 if(valid_flag) 28 { 29 printf("incorrect password!\r\n"); 30 } 31 else 32 { 33 printf("Congratulation! you have passed the verification!\r\n"); 34 } 35 fclose(fp); 36 37 getchar(); 38 return 0; 39 }
通過OD分析可得:
沒有淹沒時,verify_password棧幀如下:
當淹沒之后,verify_password棧幀如下:
eip已經被修改,成功。很開心!
當我們可以利用棧溢出這一漏洞,修改eip,我們就可以干一些更牛的事情,讓進程執行輸入的數據的代碼。
下面舉個例子,通過我們向password里添加一些機器指令,實現彈MessageBox。
1 #include <IOSTREAM> 2 #include <Windows.h> 3 using namespace std; 4 #define PASS_WORD "1234567" 5 int verify_password(char* password) 6 { 7 int authentitated; 8 char szBuffer[44]; 9 authentitated = strcmp(password,PASS_WORD); 10 strcpy(szBuffer,password); 11 return authentitated; 12 } 13 14 int main() 15 { 16 int valid_flag = 0; 17 char password[1024] = {0}; 18 FILE* fp ; 19 fp=fopen("password.txt","rw+"); 20 21 HMODULE h = LoadLibrary("user32.dll"); 22 printf("%x\r\n",h); 23 //0x77760000 24 //0x000774C0 25 //0x777D74C0 //MessageBox地址 26 //0x0018FA88 //buffer 的地址 27 28 if(fp==NULL) 29 { 30 exit(0); 31 } 32 fscanf(fp,"%s",password); 33 valid_flag = verify_password(password); 34 35 if(valid_flag) 36 { 37 printf("incorrect password!\r\n"); 38 } 39 else 40 { 41 printf("Congratulation! you have passed the verification!\r\n"); 42 } 43 fclose(fp); 44 return 0; 45 }
直接同過buffer中寫入代碼,這次的例子中,buffer定義的足夠大,就是為了能將我們自己彈窗的代碼完整的存放在里面,當我們執行拷貝,棧溢出,淹沒了棧幀,將返回地址設
置為buffer的首地址,此時,當函數棧retn之后,到返回地址繼續執行,這就實現了我們的目的。
通過OD分析可得,在沒發生拷貝前,沒有棧溢出時,verify_password棧幀如下:
在發生拷貝后,產生了棧溢出,verify_password棧幀如下:
在retn之后,eip指向buffer的基地址,進行程序的向下執行:
最終彈出對話框:
本文的實現,主要是通過參考《0day安全_軟件漏洞分析技術(第二版)》進行學習,本文中的代碼實現如下,點擊下載: