天天看點

棧溢出原理和利用學習1. 什麼是棧?2. 函數調用棧3. 棧溢出原理

做PWN題經常遇到棧溢出,有時一些棧的基礎知識總是記不清楚,腦子卡頓,是以整理一番,讓自己徹底記住它!

1. 什麼是棧?

棧,即堆棧,是一種具有一定規則的資料結構,它按照先進後出的原則存儲資料,先進入的資料被壓入棧底,最後的資料在棧頂。

堆棧資料結構的兩種基本操作:

  • PUSH:将資料壓入棧頂
  • POP  :将棧頂資料彈出

知道了什麼是棧,我們看看棧在記憶體中是怎樣一個分布情況。

以Linux 32位平台為例,程序有4GB大小的虛拟位址空間,其中1GB留給系統核心,3GB是程序自身擁有。一個程序大緻的記憶體布局如下圖所示。

棧溢出原理和利用學習1. 什麼是棧?2. 函數調用棧3. 棧溢出原理

簡單說明一下

代碼段:存放可執行程式的代碼,可讀不可寫

資料段:存放程式中已經初始化的靜态(全局)變量,可讀寫

bss段:存放程式中未初始化的靜态(全局)變量,可讀寫

堆(heap):存放動态配置設定的内容,需要程式猿手動配置設定和釋放

棧(stack):存放局部變量,如函數的參數、傳回位址、局部變量等,有系統自動配置設定和釋放

2. 函數調用棧

棧的基本概念和程序記憶體布局get,讓我們繼續看看函數調用棧,C語言函數調用過程中棧的PUSH和POP了解一下。

2.1 背景知識

棧增長方向:高位址->低位址

ESP:棧指針寄存器,指向棧頂的低位址

EBP:基址指針寄存器,指向棧底的高位址

EIP:指令指針,存儲即将執行的程式指令的位址

函數調用約定:

調用方式 cdecl stdcall fastcall
參數傳遞 從右到左壓棧 從右到左壓棧

左邊兩個參數

分别放在ECX

和EDX寄存器,

其餘的參數從

右到左壓棧

棧清理 調用者 函數自身 函數自身

(C語言預設的函數調用方式為cdecl)

2.2 函數調用開始

在調用一個函數時,系統會為這個函數配置設定一個棧幀,棧幀空間為該函數所獨有。

調用者調用一個函數的過程大緻如下:

  • 函數參數從右到左入棧
  • 傳回位址入棧
  • 上一函數ebp入棧
  • ...

在上一函數ebp入棧後,就開辟了被調函數的新棧幀,接下來便是被調函數臨時變量入棧等操作,如果被調函數裡有繼續調用新函數的操作,将繼續開始上述的一系列操作,不斷循環嵌套下去。下圖表示函數調用過程中棧的布局情況。

棧溢出原理和利用學習1. 什麼是棧?2. 函數調用棧3. 棧溢出原理

2.3 函數調用結束

函數調用結束時的變化,主要就是按相反的順序将資料彈出棧:

  • 彈出臨時變量
  • 彈出調用函數的ebp值,存到ebp寄存器中
  • 彈出傳回位址,存到eip寄存器中

傳回位址即是用call指令調用函數時下一條指令的位址,存到eip中,程式就知道在調用完後繼續執行下一條指令。

我們會有一個疑惑,調用函數時将函數參數從右到左入棧,調用結束時怎麼沒有将它們彈出?

在這裡,系統并不是用POP指令将它們彈出,而是通常通過ADD ESP讓它們從棧中“消失”。

3. 棧溢出原理

那麼,什麼是棧溢出呢?棧溢出是指向向棧中寫入了超出限定長度的資料,溢出的資料會覆寫棧中其它資料,進而影響程式的運作。

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

我們以一個執行個體(64位程式)對上述棧溢出原理和利用做一個說明。

#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位址(覆寫傳回位址),就可以達到我們的目标。

棧溢出原理和利用學習1. 什麼是棧?2. 函數調用棧3. 棧溢出原理

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

fun2函數位址==0x400586

棧溢出原理和利用學習1. 什麼是棧?2. 函數調用棧3. 棧溢出原理

最後完成棧溢出,改變程式執行流程!(注意位址的小端位元組序)

棧溢出原理和利用學習1. 什麼是棧?2. 函數調用棧3. 棧溢出原理

當然,在實際的棧溢出中,我們劫持程式的流程,一般會修改傳回位址到shellcode或者記憶體中已經有的一段指令(ROP),一些具體的攻擊利用技術,這裡暫時先不做詳細介紹了。

最後列一些C中會發生緩沖區溢出的危險函數:

  • gets()
  • strcpy
  • strcat
  • sprintf
  • ......