標籤:style blog http java get 使用
踐踏堆棧-緩衝區溢位漏洞
打算寫這篇文章是因為在網上看過一篇論文,講了緩衝區溢位破壞堆棧來執行惡意程式的漏洞。該論文請見參考資料1。這篇文章會涉及一些彙編的基礎知識,以及虛擬記憶體的一些基本概念等。當然用來偵錯工具的系統是linux,工具是gcc。很久沒有看過彙編和C語言了,錯漏之處,還請指正。
1.概要
文章標題有提到堆棧和緩衝區,那麼就先來探討下這幾個名詞的定義。這裡的緩衝區,指的就是電腦內一塊連續的記憶體地區,可以儲存相同資料類型的多個執行個體。C程式員最常見的緩衝區就是字元數組了。與C語言中其他變數一樣,數組也可以聲明為靜態或動態,靜態變數在程式載入時位於資料區段,動態變數位於堆棧之中(這一點我們可以很容易的寫個程式來驗證,見exmple1.c,使用命令gcc -m32 -S example1.c將其編譯成32位彙編代碼,查看example1.s即可看到數組a的資料分布在資料區段中,而數組b的資料則分布在堆棧中)。本文只探討動態緩衝區的溢出問題,即基於堆棧的緩衝區溢位。
exapmle1.c-------------------------------int main() { static int a[4] = {1, 2, 3, 4}; int b[4] = {5, 6, 7, 8};}
2.基礎知識
2.1 進程記憶體組織形式
既然本文要討論基於堆棧的緩衝區溢位,首先就來看看進程的記憶體組織圖。我們基本都知道,進程在記憶體中的結構可以簡單的分為程式碼片段,資料區段和堆棧段。程式碼片段位於記憶體低地址,而堆棧位於記憶體高地址。當然我們這裡說的記憶體位址是指虛擬位址,具體物理地址是需要經過MMU(記憶體管理單元)進行轉換得到。下面是一個進程的記憶體組織圖:
圖2.1 進程的記憶體組織圖
從圖2.1中可以看到,除了基本的程式碼片段,資料區段,還有未初始化資料區段bss,堆heap,記憶體映射地區等。當然我們這裡的段的概念跟程式載入時的段是不一樣的,具體區別可以參見《Linux C一站式編程》18.5 ELF檔案格式那一節的說明。
2.2 堆棧
堆棧是一種電腦中常用的抽象資料模型,其特徵就是先進先出,支援的操作主要就是PUSH和POP。PUSH操作是在堆棧頂部壓入一個元素,而POP操作則是彈出堆棧的頂部元素。
為什麼會使用堆棧則是跟現代電腦設計相關。在進階程式設計語言如C語言,JAVA語言,PYTHON語言等編寫程式時,經常會用到函數(function)或者過程(procedure)。通常,一個函數調用可以像跳轉命令那樣改變程式的執行流程,而函數執行完畢後,又需要把控制權返回給函數之後的代碼指令,這種實現需要依靠堆棧來實現。當然在函數的局部變數中,以及函數傳遞參數和返回值中都要用到堆棧。
堆棧是一塊連續的記憶體地區,堆棧既可以向上也可以向下增長,這個依賴於具體實現。在大部分的處理器如Intel,Motorola,SPARC和MIPS中,堆棧都是向下增長的,即堆棧指標SP指向堆棧的頂部,堆棧底部是一個固定的地址,堆棧大小在運行時由核心動態調整。CPU實現指令PUSH和POP,向堆棧中添加和移除元素。
除了堆棧指標SP,為了方便還有一個指向幀內固定地址的指標BP。從理論上來說,局部變數可以通過SP加位移量來引用,然而,當有字被壓入棧和出棧後,這些位移就變化了。儘管有些情況下編譯器能夠跟蹤棧內的操作變化,修正位移量,但是還有很多情況不能跟蹤,而且為了跟蹤位移量的變化需要引入額外的管理開銷。因此很多編譯器會使用第二個寄存器BP,局部變數和函數參數都可以引用它,因為局部變數和函數參數到BP的距離不受PUSH和POP操作的影響。
2.3 函數調用中棧幀分析
為了利用緩衝區溢位,需要知道函數調用中棧幀變化和布局情況,這裡就不分析了,已經有很好的文章詳細說過這個問題,參見宋勁松老師的《linux C一站式編程》19.1節函數調用。
3.緩衝區溢位
好了,做了一些準備工作後,可以來看看這個緩衝區溢位的問題了。 首先看下面的代碼example1.c,我們分析下函數棧幀的分布。
example1.c--------------------------------------------------void function(int a, int b, int c) { char buffer1[5]; char buffer2[10];}void main() { function(1,2,3);}
運行命令:gcc -S -fno-stack-protector example1.c,通過分析example1.s檔案得出 函數棧幀分布如下所示(我的運行環境是32位的ubuntu11.04):
棧幀分布 |
c (高地址) |
b |
a |
ret(返回地址) |
ebp |
buffer1 |
buffer2 (低地址) |
接下來看一個通過覆蓋返回地址造成段錯誤的情況。見example2.c。
example2.c---------------------------------------void function(char *str) { char buffer[16]; strcpy(buffer,str);}void main() { char large_string[256]; int i; for( i = 0; i < 255; i++) large_string[i] = 'A'; function(large_string);}
example2.c是一個典型的緩衝區溢位的例子,strcpy拷貝的資料超過了16個位元組,導致溢出代碼覆蓋了棧中儲存的ebp值以及返回地址ret,而函數返回時會從棧中取返回地址ret接著執行下一條指令,該地址不合法,從而導致段錯誤。而如果用一個合法地址來覆蓋返回地址ret,這樣就可以修改程式執行流程了。
接下來,修改example1.c,通過緩衝區溢位修改返回地址ret來修改程式執行流程。如example3.c所示。
example3.c--------------------------------void function(int a, int b, int c){ int *ret; char buffer1[5]; char buffer2[10]; ret = buffer1 + 13; (*ret) += 8;}void main(){ int x = 0; function(1,2,3); x = 1; printf("%d\n", x);}
使用命令gcc -o example3 -fno-stack-protector example3.c編譯,可以看到棧幀分布如下所示:
| 棧幀分布| | ------------ | | c (高地址)| | b | | a | | ret(返回地址) | | ebp | | ret (局部變數ret)| | buffer1 | | buffer2 (低地址)| 因此,通過ret=buffer1+13,可以獲得返回地址ret的地址。這裡之所以加13,是buffer1的5位元組+局部變數ret的4位元組+ebp的4位元組。調用function函數後,返回地址本應該是x=1指令地址,(*ret) += 8將返回地址ret加8,這樣就跳過了x=1這條指令,example3.c編譯後執行的結果是0。注意必須加上-fno-stack-protector,因為gcc預設存在堆棧保護技術,那樣會防止返回地址被改寫,如果返回地址被惡意修改,會報段錯誤。GCC編譯器堆棧保護技術詳見該文連結。
接下來可以通過緩衝區溢位來執行shell代碼,這個留待下一篇文章再說了,內容太長,現在還沒有看完。
4.參考資料
- stack smashing
- GCC編譯器堆棧保護技術