棧溢出原理
馬上軟考了,補一補
寄存器分配 ESP、EBP、EIP
以32位x86架構為基礎,Windows提供三個寄存器幫助定位棧和函數調用--ESP、EBP、EBP
ESP
ESP 用來存儲函數調用棧的棧頂指針
,指向棧區中最上一個棧幀的棧頂
EBP
EBP 用來存儲當前函數狀態的基地址,即棧底指針
,指向棧區中最上一個棧幀的棧底
EIP
EIP 用來存放下一個執行語句的地址的指令寄存器
函數調用步驟
下面讓我們來看看發生函數調用時,棧頂函數狀態以及上述寄存器的變化。變化的核心任務是將調用函數(caller)的狀態保存起來,同時創建被調用函數(callee)的狀態。
所謂被調用函數,就是函數中的函數 Caller's Caller's ebp=Callee's ebp
void caller(arg1,arg2,....argn){
callee(arg1,arg1,arg2,....argn);
}
Step1.參數入棧
首先將被調用函數(callee)的參數按照逆序依次壓入棧內 argn......arg2,arg1
這個棧是從下往上壓的,也就是esp位置的先出棧
Step2.返回地址入棧
將調用函數(caller)進行調用之后的下一條指令地址作為返回地址壓入棧內。這樣調用函數(caller)的 EIP(指令地址)保存在 Return Address
中。這里的地址就是callee()運行完接着caller()下一條指令執行的地址
Step3.調用函數的EBP入棧
將當前的 EBP 寄存器的值(也就是調用函數caller()的基地址)壓入棧內
並將EBP(棧底)的值更新為當前ESP(棧頂)的地址 → mov esp, ebp
Step4.被調用函數局部變量入棧
到這一步,被調用函數callee的函數幀結構就完整了
在壓棧的過程中,esp 寄存器的值不斷減小,棧內數據不斷變大(對應於棧從內存高地址向低地址生長)。
壓入棧內的數據包括:調用參數(argn...arg1)、返回地址(Return Address)、調用函數的基地址(新EBP),以及局部變量(Local Variables)
其中調用參數
以外的數據共同構成了"被調用函數(callee)的狀態"。在發生調用時,程序還會將被調用函數(callee)的指令地址存到 EIP 寄存器內,這樣程序就可以依次執行被調用函數的指令了。
看過了函數調用發生時的情況,就不難理解函數調用結束時的變化。變化的核心就是丟棄被調用函數(callee)的狀態,並將棧頂彈出恢復為調用函數(caller)的狀態。
Step5.將被調用函數的局部變量彈出棧外
首先被調用函數的局部變量會從棧內直接彈出,棧頂會指向調用函數(caller)的基地址。
Step6.彈出調用函數EBP
然后將基地址內存儲的調用函數(caller)的基地址從棧內彈出,並存到 ebp 寄存器內。這樣調用函數(caller)的 ebp(基地址)信息得以恢復。此時棧頂會指向返回地址。下圖容易誤解,其實這里的Caller's Caller's ebp已經變成了Caller's ebp
Step7.彈出返回地址,存入EIP
再將返回地址從棧內彈出,並存到 eip 寄存器內。這樣調用函數(caller)的 eip(指令)信息得以恢復到caller()的執行狀態。
至此調用函數的狀態恢復,之后繼續執行調用函數的指令
棧溢出攻擊
什么是棧溢出呢?
棧溢出是指向向棧中寫入了超出限定長度的數據,溢出的數據會覆蓋棧中其它數據,從而影響程序的運行。如果我們計算好溢出的長度,編寫好溢出數據,讓我們想要的地址數據正好覆蓋到函數返回地址
那么被調函數調用完返回主函數時,就會跳轉到我們覆蓋的地址上。所以控制程序執行指令最關鍵的寄存器就是 EIP
,我們的目標就是讓EIP
載入攻擊指令的地址。通過這樣改變程序流程,接下來我們就可以干很多壞事了!
該怎么構造呢,這里有個簡單的緩沖區溢出的例子
void f(char *str){
char buf[16];
strcpy(buf,str);
}
void main(){
char buf[128];
for(i=0;i<=127;i++)
buf[i]='A';
f(buf);
print("It's Buffer OverFlow!");
}
這是執行被調用函數f()的stack
EIP(Return Address)
其中的buf是局部變量,但是我們從程序中可以看出,128位的*str輸入明顯越界,會導致數據溢出,溢出就會覆蓋EBP、EIP
我們通過精心構造一個位數精准的數據,使得EIP處正好被我們溢出的那幾位數據覆蓋為AAAA,其中A對應ASCII 0x41
AAAA對應EIP地址為0x41414141
當0x41414141
覆蓋了原來EIP的位置時,f函數在返回時就會將0x41414141
彈出來給EIP,假如這時shell程序調用地址是0x41414141
,那么main()接下來就會去執行shell,我們就能運行shell實施攻擊
(這里全填A是為了方便理解,實際情況構造shell地址肯定不是AAAA哈,自己根據shell地址構造就行了)
防范緩沖區溢出策略
常用的函數有strcpy()
、sprintf()
、strcat()
、vsprintf()
、gets()
、scanf()
,以及在循環內的函數getc()
、fgetc()
、getchat()
等都非常容易導致溢出
防范策略有:
系統管理防范策略:關閉不必要的特權程序、及時打系統補丁
開發時的防范策略:注意危險函數的調用、緩沖區不允許執行、靜態分析檢查指針、堆棧向高地址方向增長等