淺談PWN基礎-棧溢出

一、預備知識

緩衝區溢出簡單介紹

緩衝區溢出:簡單的說,緩衝區溢出就是超長的數據向緩衝區複製,導致數據超出了緩衝區,導致緩衝區其他的數據遭到破壞,這就是緩衝區溢出。而棧溢出是緩衝區溢出的一種,也是最常見的。只不過棧溢出發生在棧,堆溢出發生在堆,其實都是一樣的。

如果想用棧溢出來執行攻擊指令,就要在溢出數據內包含攻擊指令的內容或地址,並且要將程序控制權交給該指令。攻擊指令可以是自定義的指令片段,也可以利用系統內已有的函數及指令。

二、簡介棧

棧:
棧是一種計算機系統中的數據結構,它按照先進後出的原則存儲數據,先進入的數據被壓入棧底,最後的數據在棧頂,需要讀數據的時候從棧頂開始彈出數據(最後一個數據被第一個讀出來),是一種特殊的線性表。棧的操作常用的有進棧(PUSH),出棧(POP),還有常用的標識棧頂和棧底。

  • 可以把棧想象成一摞撲克牌一樣,一張一張疊加起來。

  • 進棧(PUSH):將一個數據放入棧裏叫進棧(PUSH),相當於在撲克牌的在最上面放了一張新的撲克牌。

  • 出棧(POP):將一個數據從棧裏取出叫出棧(POP),相當於在撲克牌的在最上面拿走了一張撲克牌。

  • 棧頂:常用寄存器ESP,ESP是棧指針寄存器,其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂。

  • 棧底:常用寄存器EBP,EBP是基址指針寄存器,其內存放着一個指針,該指針永遠指向系統棧最下面一個棧幀的底部。

函數狀態主要涉及這三個寄存器--esp,ebp,eip。esp 用來存儲函數調用棧的棧頂地址,在壓棧和退棧時發生變化。ebp 用來存儲當前函數狀態的基地址,在函數運行時不變,可以用來索引確定函數參數或局部變量的位置。eip 用來存儲即將執行的程序指令的地址,cpu 依照 eip 的存儲內容讀取指令並執行,eip 隨之指向相鄰的下一條指令,如此反覆,程序就得以連續執行指令。

三 函數調用棧的相關知識

在這裏插入圖片描述

發生函數調用時:

1,首先將被調用函數的參數按照逆序依次壓入棧內
2, 將被調用函數的返回地址壓入棧內。
3,將調用函數的基地址(ebp)壓入棧內,並將當前棧頂地址傳到 ebp 寄存器內。
4,將被調用函數的局部變量壓入棧內。

函數調用結束時:

1, 首先被調用函數的局部變量會從棧內直接彈出,棧頂會指向被調用函數(callee)的基地址。
2,將調用函數(caller)的基地址(ebp)彈出棧外,並存到 ebp 寄存器內
3,將被調用函數的返回地址彈出棧外,並存到 eip 寄存器內

至此調用函數(caller)的函數狀態就全部恢復了

三、 棧溢出原理及簡單案例分析

那麼,什麼是棧溢出呢?棧溢出是指向向棧中寫入了超出限定長度的數據,溢出的數據會覆蓋棧中其它數據,從而影響程序的運行。

如果我們計算好溢出的長度,編寫好溢出數據,讓我們想要的地址數據正好覆蓋到函數返回地址,那麼被調函數調用完返回主函數時,就會跳轉到我們覆蓋的地址上。通過這樣改變程序流程,接下來我們就可以幹很多壞事了!
接下來舉例說明

#include<stdio.h>
 
int fun1()
{
	int a;
	gets((char *)&a);
	return 0;
}
 
int fun2()
{
	printf("stackflow success!\n");
	return 0;
}
 
int main(int argc, char *argv[])
{
	fun1();
 
return 0;
}

gets()是C中的危險函數之一,它不進行邊界檢查。在我們的例子中,a是int型只有4字節大小的空間,所以當輸入的字符大於4字節時,就會發生溢出。而我們的目標就是,讓我們的溢出數據覆蓋fun1函數的返回地址,具體就是覆蓋爲fun2函數的地址,使程序的流程跳轉到fun2函數去執行。

  • 首先,我們用gcc對這段代碼進行編譯:

  • gcc -z execstack -fno-stack-protector -o stackflow-example ./stackflow-example.c

(其中-z execstack開啓堆棧可執行機制,-fno-stack-protector關閉堆棧保護機制)

  • 用gdb進行調試,可以直接在gets()函數下斷點,也可以使用next、step指令快速調試到gets()函數這,在輸入AAA後,查看堆棧數據。

  • 在執行完gets()函數並輸入AAA後,程序的棧分佈情況如下所示,0x00007fffffffe110即是上一函數(調用者main函數)的ebp,0x4005b4是fun1函數的返回地址。

  • 在輸入AAAAA後呢,溢出的數據就會存在0x00007fffffffe0f0開始的棧上

所以,我們只需要輸入AAAA+AAAAAAAA(覆蓋上一函數ebp)+fun2地址(覆蓋返回地址),就可以達到我們的目標。

在這裏插入圖片描述

可以使用examine命令(簡寫是x)來查看內存地址中的值。x命令的語法如下所示:

x/<n/f/u>

n、f、u是可選的參數。

  • n是一個正整數,表示需要顯示的內存單元的個數,也就是說從當前地址向後顯示幾個內存單元的內容,一個內存單元的大小由後面的u定義。

  • f 表示顯示的格式,參見下面。如果地址所指的是字符串,那麼格式可以是s,如果地十是指令地址,那麼格式可以是i。

  • u 表示從當前地址往後請求的字節數,如果不指定的話,GDB默認是4個bytes。u參數可以用下面的字符來代替,b表示單字節,h表示雙字節,w表示四字 節,g表示八字節。當我們指定了字節長度後,GDB會從指內存定的內存地址開始,讀寫指定字節,並把其當作一個值取出來。

  • 表示一個內存地址。

注意:嚴格區分n和u的關係,n表示單元個數,u表示每個單元的大小。

n/f/u三個參數可以一起使用。例如:

命令:x/3uh 0x54320 表示,從內存地址0x54320讀取內容,h表示以雙字節爲一個單位,3表示輸出三個單位,u表示按十六進制顯示。

緊接着,我們需要找到fun2函數的起始地址,來完成我們對程序流程的劫持。這個程序比較簡單,可以直接在調試的時候快速找到fun2函數的地址,正常我們可以使用如下命令查找。
例如:

   disass fun2,顯示fun2函數對應的彙編代碼 

在這裏插入圖片描述

fun2函數地址==0x400586

最後完成棧溢出,改變程序執行流程!
在這裏插入圖片描述
以上是我個人對於棧溢出的拙見,如有不足請大牛指正!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章