昨晚查找一些關于利用緩沖區(qū)溢出漏洞獲得系統(tǒng)root方面的資料,在IBM Developerworks上看到一篇很好的文章:Linux下緩沖區(qū)溢出攻擊的原理及對策
其中很詳細的講解了linux下關于緩沖區(qū)溢出的一些知識,其中有一段作為預備知識是關于linux下進程的地址空間的布局和堆棧幀相關資料,我仔細讀了好幾遍,才弄懂了其中的原理,現(xiàn)在把一些體會寫出來。
注:以下內(nèi)容部分和圖片摘自”Linux下緩沖區(qū)溢出攻擊的原理及對策“原文。
之前我所了解的linux下進程的地址空間的布局的知識,是從APUE第2版的P430得來的,之后上網(wǎng)查了一些資料,大概弄了明白。一個linux進程分為幾個部分(從一個進程的地址空間的低地址向高地址增長):
1.text段,就是存放代碼,可讀可執(zhí)行不可寫,也稱為正文段,代碼段。
2.data段,存放已初始化的全局變量和已初始化的static變量(不管是局部static變量還是全局static變量)
3.bss段,存放全局未初始化變量和未初始化的static變量(也是不區(qū)分局部還是全局static變量)
以上這3部分是確定的,也就是不同的程序,以上3部分的大小都各不相同,因程序而異,若未初始化的全局變量定義的多了,那么bss區(qū)就大點,反之則小點。
4.heap,也就是堆,堆在進程空間中是自低地址向高地址增長,你在程序中通過動態(tài)申請得到的內(nèi)存空間(c中一般為malloc\free,c++中一般為new\delete),就是在堆中動態(tài)分配的。
5.stack,棧,程序中每個函數(shù)中的局部變量,都是存放在棧中,棧是自高地址向低地址增長的。起初,堆和棧之間有很大一段空間,然后隨著,程序的運 行,堆不斷向高地址增長,棧不斷向高地址增長,這樣,堆跟棧之間的空間總有一個最大界限,超過這個最大界限,就會出現(xiàn)堆跟棧重疊,就會出錯,所以一般來 說,Linux下的進程都有其最大空間的。
6.再往上,也就是一個進程地址空間的頂部,存放了命令行參數(shù)和環(huán)境變量。
整個進程地址空間布局如下圖所示:

程序運行開始,由系統(tǒng)為進程地址空間中的text\data\bss段進行映射,由系統(tǒng)的缺頁異常處理程序按需將磁盤上程序文件中的真正代碼、數(shù)據(jù)寫入進程。此外,bss區(qū)域中的所有變量都會被清零。
通過上面的講述,我們知道了程序的代碼,全局的變量,static變量是怎么在進程空間中分配空間的,接下來講一下局部變量是怎么分配空間,函數(shù)是怎么調(diào)用的。其實也就是講解棧區(qū)的具體使用過程。
首先,我們要知道,棧中存放的是一個個被調(diào)函數(shù)所對應的堆棧幀,當函數(shù)fun1被調(diào)用,則fun1的堆棧幀入棧,fun1返回時,fun1的堆棧幀出棧。 什么是堆棧幀呢,堆棧幀其實就是保存被調(diào)函數(shù)返回時下一條執(zhí)行指令的指針、主調(diào)函數(shù)的堆棧幀的指針、主調(diào)函數(shù)傳遞給被調(diào)函數(shù)的實參(如果有的話)、被調(diào)函 數(shù)的局部變量等信息的一個結構。
堆棧幀結構如圖所示:

首先,我們要說明的是如何區(qū)分每個堆棧幀,或者說,如何知道我現(xiàn)在在使用哪個堆棧幀。和棧密切相關的有2個寄存器,一個是ebp,一個是esp,前 者可以叫作?;分羔?,后者可以叫棧頂指針。對于一個堆棧幀來說,ebp也叫堆棧幀指針,它永遠指向這個堆棧幀的某個固定位置(見上圖),所以可以根據(jù) ebp來表示一個堆棧幀,可以通過對ebp的偏移加減,來在堆棧幀中來來回回的訪問。esp則是隨著push和pop而不斷移動。因此根據(jù)esp來對堆棧 幀進行操作。
再來講一下上圖,一個堆棧幀的最頂部,是實參,然后是return address,這個值是由主調(diào)函數(shù)中的call命令在call調(diào)用時自動壓入的,不需要我們關心,previous frame pointer,就是主調(diào)函數(shù)的堆棧幀指針,也就是主調(diào)函數(shù)的ebp值。ebp偏移為正的都是被調(diào)函數(shù)的局部變量。
好了,說了這么多,很枯澀,我們通過一個實例來講下。
int function(int a, int b, int c)
{
char buffer[14]; int sum;
sum = a + b + c;
return sum;
}
void main()
{
int i;
i = function(1,2,3);
}
程序很簡單,不解釋。
在Linux下,我們通過 gcc -S example1.c 來生成匯編文件,然后我們查看上面這個程序對應的匯編程序:
1 .file "example1.c"
2 .version "01.01"
3 gcc2_compiled.:
4 .text
5 .align 4
6 .globl function
7 .type function,@function
8 function:
9 pushl %ebp //ebp這時指向的還是上一個堆棧幀
10 movl %esp,%ebp //上一步中已將ebp(上一個堆棧幀的ebp)壓入保存,
11 subl $20,%esp //esp下移20個字節(jié),就是申請20個字節(jié)的空間
12 movl 8(%ebp),%eax //ebp加8后,指向第1個實參,放入eax.
13 addl 12(%ebp),%eax //ebp+12后,指向第2個實參,與第一個實參相加.
14 movl 16(%ebp),%edx //第3個實參放入edx
15 addl %eax,%edx //就是3個實參相加,結果存入edx
16 movl %edx,-20(%ebp) //將結果放入剛才申請的sum中
17 movl -20(%ebp),%eax
18 jmp .L1
19 .align 4
20 .L1:
21 leave //leave和ret見下面的解析
22 ret
23 .Lfe1:
24 .size function,.Lfe1-function
25 .align 4
26 .globl main
27 .type main,@function
28 main:
29 pushl %ebp
30 movl %esp,%ebp
31 subl $4,%esp //申請4個字節(jié)(給局部變量i)
32 pushl $3 //壓入實參3
33 pushl $2 //壓入實參2
34 pushl $1 //壓入實參1
35 call function
36 addl $12,%esp //加上12就釋放了剛才壓入的3個實參
37 movl %eax,%eax //function返回值在eax中
38 movl %eax,-4(%ebp) //返回值賦值給了剛才申請的空間(變量i)
39 .L2:
40 leave
41 ret
42 .Lfe2:
43 .size main,.Lfe2-main
44 .ident "GCC: (GNU) 2.7.2.3"
其中函數(shù)function的堆棧幀

注釋:
1.function中,buffer是14個字節(jié),sum是4個字節(jié),照例應該是申請18個字節(jié),但是第11行,程序申請了20個字節(jié)。這是時間效率和 空間效率之間的一種折衷,因為Intel i386是32位的處理器,其每次內(nèi)存訪問都必須是4字節(jié)對齊的,而高30位地址相同的4個字節(jié)就構成了一個機器字。因此,如果為了填補 buffer[14]留下的兩個字節(jié)而將sum分配在兩個不同的機器字中,那么每次訪問sum就需要兩次內(nèi)存操作,這顯然是無法接受的。這些都是跟編譯器 相關的優(yōu)化技術。
2.我們再來看一下在函數(shù)function中是如何將a、b、c的和賦給sum的。前面已經(jīng)提過,在函數(shù)中訪問實參和局部變量時都是以堆棧幀指針為 基址,再加上一個偏移,而Intel i386體系結構下的堆棧幀指針就是ebp,為了清楚起見,我們在圖7中標出了堆棧幀中所有成分相對于堆棧幀指針ebp的偏移。這下圖6中12至16的計 算就一目了然了,8(%ebp)、12(%ebp)、16(%ebp)和-20(%ebp)分別是實參a、b、c和局部變量sum的地址,幾個簡單的 add指令和mov指令執(zhí)行后sum中便是a、b、c三者之和了。另外,在gcc編譯生成的匯編程序中函數(shù)的返回結果是通過eax傳遞的,因此在圖6中第 17行將sum的值拷貝到eax中。
3.我們再來看一下函數(shù)function執(zhí)行完之后與其對應的堆棧幀是如何彈出堆棧的。圖6中第21行的leave指令將堆棧幀指針 ebp拷貝到esp中,于是在堆棧幀中為局部變量buffer[14]和sum分配的空間就被釋放了;除此之外,leave指令還有一個功能,就是從堆棧 中彈出一個機器字并將其存放到ebp中,這樣ebp就被恢復為main函數(shù)的堆棧幀指針了。第22行的ret指令再次從堆棧中彈出一個機器字并將其存放到 指令指針eip中,這樣控制就返回到了第36行main函數(shù)中的addl指令處。addl指令將棧頂指針esp加上12,于是當初調(diào)用函數(shù) function之前壓入堆棧的三個實參所占用的堆??臻g也被釋放掉了。至此,函數(shù)function的堆棧幀就被完全銷毀了。前面剛剛提到過,在gcc編 譯生成的匯編程序中通過eax傳遞函數(shù)的返回結果,因此圖6中第38行將函數(shù)function的返回結果保存在了main函數(shù)的局部變量i中.
over.本人也是新手,有任何不正確的理解希望能指教,謝謝