前言:本身剛開始看這一塊,加上本身的C語言基礎並很差,不少地方都是參考的其餘文章,因此可能會有不少錯誤的地方python
puts()
)來接受輸入的數據時,由於沒有考慮到數據的長度的合法性,可能會形成數據超過原本的應有長度,從而覆蓋掉後面的數據,以後程序讀取後面的數據時便會發生各類錯誤,引起風險內存劃分示意圖程序員
(1)代碼區(text segment)算法
(2)全局初始化數據區/靜態數據區(Data Segment)編程
char* s = "ABC"
存儲在常量區,所以只讀不可改;char[] s = "ABC"
,存儲在棧,所以是可改的const
修飾的全局變量也爲常量(3)未初始化數據區 (Block Started by Symbol,BSS)數組
(4)堆區安全
malloc
等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free
等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)(5)棧區數據結構
(1)意義編程語言
一個進程在運行過程當中,代碼是根據流程依次執行的,只須要訪問一次,固然跳轉和遞歸有可能使代碼執行屢次,而數據通常都須要訪問屢次,所以單獨開闢空間以方便訪問和節約空間。編輯器
臨時數據及須要再次使用的代碼在運行時放入棧區中,生命週期短。函數
局數據和靜態數據有可能在整個程序執行過程當中都須要訪問,所以單獨存儲管理。
堆區由用戶自由分配,以便管理
(1)棧幀包括
(2)重要的寄存器
EBP
:基址寄存器,指向棧底
ebp
用來存儲當前函數狀態的基地址,在函數運行時不變,能夠用來索引肯定函數參數或局部變量的位置ESP
:棧頂寄存器,指向棧頂
esp
用來存儲函數調用棧的棧頂地址,在壓棧和退棧時發生變化EIP
:程序計數器,指向的地址的值保存着下一條要進行的指令
eip
的存儲內容讀取指令並執行eip
隨之指向相鄰的下一條指令,如此反覆,程序就得以連續執行指令(3)操做棧的經常使用指令
push
:壓棧,
PUSH
指令會對ESP
/RSP
/SP
寄存器的值進行減法運算,使之減去4字節(32位)或8字節(64位),而後將操做數寫到上述寄存器裏的指針所指向的內存中pop
:彈棧
POP
指令是PUSH
指令的逆操做:它先從棧指針指向的內存中讀取數據,用以備用(一般是寫到其餘寄存器裏),而後再將棧指針的數值加上4字節或8字節(4)函數調用過程
main函數調用fun,稱main函數爲caller
,被調用函數fun稱爲callee
:
esp
寄存器的值不斷減少(對應於棧從內存高地址向低地址生長)。壓入棧內的數據包括調用參數、返回地址、調用函數的基地址,以及局部變量
callee
函數的參數逆序壓入棧,(若是被調用函數calle不須要參數,則沒有這一步驟)callee
壓入棧後,將調用函數caller
進行調用以後的下一條指令地址做爲返回地址壓入棧內(即壓入calle結束後須要執行的指令,以便告訴CPU這個函數調用完成以後該幹什麼,本例即返回到main函數的return
處),這樣調用函數(caller)
的eip(指令)
信息得以保存ebp
寄存器中的值(也就是調用函數的基地址)壓入棧內,並將ebp
寄存器的值更新爲當前棧頂的地址(即caller的esp
地址)
caller
的 ebp(基地址)
信息得以保存。同時,ebp
被更新爲被調用函數callee
的基地址(將當前棧頂地址傳到ebp
寄存器內)esp
的值減去一個字節數目值,實現esp
向低字節移動callee
的局部變量等數據壓入棧內eip
的指向的內存地址中的指令當被調用函數callee
完成以後,須要丟棄被調用函數callee
的狀態,並將棧頂恢復爲調用函數caller
的狀態
首先被調用函數的局部變量會從棧內直接彈出,棧頂會指向被調用函數callee
的基地址
而後將基地址內存存儲的調用函數caller
的基地址從棧內彈出,並存儲到ebp
寄存器內
caller
的 ebp(基地址)
信息得以恢復。此時棧頂會指向返回地址(即esp
寄存器的值更新爲被調用函數callee
執行時的ebp
的值)再將返回地址從棧內彈出,並存到eip
寄存器內。這樣調用函數caller
的eip(指令)
信息得以恢復。
至此caller的函數狀態就所有恢復了,以後就是繼續執行調用函數的指令
(1)利用棧溢出覆蓋函數的局部變量數據值
C語言中的gets()
從標準輸入設備讀字符串函數,其能夠無限讀取,不會判斷上限,因此會形成溢出,因此能夠利用這個漏洞來實現程序的數據以及流程的改變
如下是一個鍵盤輸入與內置的局部變量的值的判斷,而後決定是否執行特定程序的程序
#include <stdio.h> #include <stdlib.h> #include <string.h> void fun() { char password[6] = "ABCDE"; char str[6]; gets(str); str[5] = '\0'; if (strcmp(str, password) == 0){ //比較stryupassword的值是否相同 printf("開始執行python!\n"); system("python"); //開始啓動python程序 } else{ printf("NO!\n"); } } int main() { fun(); return 0; }
首先在fun()處設置一個斷點,表示下一步要進入fun()函數內,此時:
EIP = 0x006E18B0
ESP = 0x003EF850
EBP = 0x003EF920
fun函數開頭的彙編指令:
void fun() { 006E18B0 push ebp 006E18B1 mov ebp,esp 006E18B3 sub esp,0E0h
EIP
寄存器地址存儲的指令就是將ebp
寄存器內容(相應的地址)利用push
指令壓入棧mov
指令將esp
寄存器的內容複製到ebp
中sub
指令將esp寄存器的內容額外減去0x0e0h
,即esp
向低地址移動0x0e0h個字節當咱們的fun函數執行到gets(str)
時,查看變量的地址以及內存的值
變量的內存位置 attack 0x006e1840 {StackOverflow.exe!attack(...)} void (...) fun 0x006e18b0 {StackOverflow.exe!fun(...)} void (...) str 0x00cffacc "燙燙燙... char[0x00000006] password 0x00cffadc "ABCDE" char[0x00000006]
內存視圖 0x00CFFACC [cc cc cc cc cc cc]cc cc ???????? 0x00CFFAD4 cc cc cc cc cc cc cc cc ???????? 0x00CFFADC [41 42 43 44 45 00]cc cc ABCDE.?? 0x00CFFAE4 cc cc cc cc bc fb cf 00 ???????.
0x00CFFADC
開始即可以看出連續的6個字節對應的就是"ABCDE",即password的值,最後的是'\0'
爲結束符0x00CFFACC
對應的爲str的起始位置因爲puts函數不會限制輸入數據的長度,因此咱們能夠經過輸入特定字符在覆蓋掉password
輸入完畢後,運行到if語句時,再次查看內存
內存視圖 0x00CFFACC [41 41 41 41 41 00]41 41 AAAAA.AA 0x00CFFAD4 41 41 41 41 41 41 41 41 AAAAAAAA 0x00CFFADC [41 41 41 41 41 00]cc cc AAAAA.?? 0x00CFFAE4 cc cc cc cc bc fb cf 00 ???????.
結果
結束後,此時EIP內容爲main函數以後的指令地址,繼續執行main函數,程序完成
注:實際上可能因編譯器不一樣,環境不一樣,其str與password內存位置差距也不一樣,須要自行判斷
(2)利用棧溢出漏洞覆蓋參數值
再來看一個,經過寫入字符數據來覆蓋整形key,使得該key值與口令相等
#include <stdio.h> #include <stdlib.h> #include <string.h> void fun(int key) { char buffer[5]; puts(buffer); if (key == 0x41424344) { //對應的char爲a,b,c,d printf("開始執行python!\n"); system("python"); } else { printf("NO!\n"); } } int main() { fun(0x45464748); return 0; }
和以前相同,觀察&password
以及&key
的值
內存視圖 0x00D3F790 [a8 f7 d3 00]06 c0 6c 00 ???..?l. 0x00D3F798 f0 f7 d3 00 0d 1a 6c 00 ???...l. 0x00D3F7A0 [48 47 46 45]52 13 6c 00 HGFER.l. 變量內存位置 buffer 0x00D3F790 &key 0x00D3F7A0
key位於參數位置,因此經過寫入20個字節數據覆蓋掉key這個參數的值就能夠了,讓key的新值爲0x41424344(即輸入字符的最後四個字符爲DBCA)就能夠了
輸入16個字符+DCBA,結果:
(3)利用棧溢出漏洞修改返回地址,實現函數的跳轉
因爲EBP後的四個字節爲返回地址,即函數執行完以後的下一條執行的指令地址,能夠經過修改該字段來實現執行特定的函數
代碼以下
#include <stdio.h> #include <stdlib.h> #include <string.h> void attack() { printf("Attacked!\n"); system("python"); exit(0); } void fun() { char password[6] = "ABCDE"; char str[6]; FILE* fp; if (!(fp = fopen("H:\\SaftyTest\\StackOverflow\\password.txt", "r"))) { exit(0); } fscanf(fp, "%s", str); str[5] = '\0'; if (strcmp(str, password) == 0) printf("OK!\n"); else printf("NO!\n"); } int main(){ fun(); return 0; }
當執行到打開文本函數時,內存信息以下:
內存視圖 0x00F7F6C8 [00 10 d4 00 e4 f6]f7 00 ..?.???. 0x00F7F6D0 [41 42 43 44 45 00]a2 00 ABCDE.?. 0x00F7F6D8 [2c f7 f7 00|88 17 a2 00] ,??.?.?. 變量視圖 attack 0x00a21880 {StackOverflow.exe!attack(...)} void (...) fun 0x00a218e0 {StackOverflow.exe!fun(...)} void (...) str 0x00f7f6c8 "" char[0x00000006] password 0x00f7f6d0 "ABCDE" char[0x00000006]
此時attack函數地址爲0x00a21880
,因此須要讀入數據使得EBP
後的返回地址RET
覆蓋爲0x00a21880
修改password.txt的文本爲以下內容,並保存:
41414141414141414141414141414141414141418018A200
共24字節,最後四位爲0x8018a200
注:該內容爲16進制內容,實際打開文本看到的內容可能爲:
AAAAAAAAAAAAAAAAAAAA€?
這裏並無考慮兩個字符數組比較的問題,若是想要顯示OK,只需前str與password的相同位後的值改成‘/0’便可(對應文本的16進制內容爲00)
繼續執行程序,內存信息以下:
0x00F7F6C8 41 41 41 41 41 00 41 41 AAAAA.AA 0x00F7F6D0 [41 41 41 41]41 41 41 41 AAAAAAAA 0x00F7F6D8 [41 41 41 41|80 18 a2 00] AAAA€.?.
程序結果:
因爲本環境下C語言程序中採用大端存儲,即數據的低字節在內存中的字節地址更高,因此最後將幾個數據字節順序倒置
本例子運行完python後直接退出,若是不直接退出的話會發生如(2)同樣的結果,緣由也相同