天天看點

堆棧溢出攻擊原理

eax, ebx, ecx, edx, esi, edi, ebp, esp等都是X86 彙編語言中CPU上的通用寄存器的名稱,是32位的寄存器。如果用C語言來解釋,可以把這些寄存器當作變量看待。

比方說:add eax,-2 ;   //可以認為是給變量eax加上-2這樣的一個值。

這些32位寄存器有多種用途,但每一個都有“專長”,有各自的特别之處。

EAX 是"累加器"(accumulator), 它是很多加法乘法指令的預設寄存器。

EBX 是"基位址"(base)寄存器, 在記憶體尋址時存放基位址。

ECX 是計數器(counter), 是重複(REP)字首指令和LOOP指令的内定計數器。

EDX 則總是被用來放整數除法産生的餘數。

ESI/EDI分别叫做"源/目标索引寄存器"(source/destination index),因為在很多字元串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.

EBP是"基址指針"(BASE POINTER), 它最經常被用作進階語言函數調用的"架構指針"(frame pointer). 在破解的時候,經常可以看見一個标準的函數起始代碼:

push ebp ;儲存目前ebp

mov ebp,esp ;EBP設為目前堆棧指針

sub esp, xxx ;預留xxx位元組給函數臨時變量.

...

這樣一來,EBP 構成了該函數的一個架構, 在EBP上方分别是原來的EBP, 傳回位址和參數. EBP下方則是臨時變量. 函數傳回時作 mov esp,ebp/pop ebp/ret 即可.

ESP 專門用作堆棧指針,被形象地稱為棧頂指針,堆棧的頂部是位址小的區域,壓入堆棧的資料越多,ESP也就越來越小。在32位平台上,ESP每次減少4位元組。

esp:寄存器存放目前線程的棧頂指針

ebp:寄存器存放目前線程的棧底指針

eip:寄存器存放下一個CPU指令存放的記憶體位址,當CPU執行完目前的指令後,從EIP寄存器中讀取下一條指令的記憶體位址,然後繼續執行。

一般寄存器:AX、BX、CX、DX

AX:累積暫存器,BX:基底暫存器,CX:計數暫存器,DX:資料暫存器

索引暫存器:SI、DI 

SI:來源索引暫存器,DI:目的索引暫存器 

堆疊、基底暫存器:SP、BP 

SP:堆疊指標暫存器,BP:基底指標暫存器 

EAX、ECX、EDX、EBX:為ax,bx,cx,dx的延伸,各為32位元 

ESI、EDI、ESP、EBP:為si,di,sp,bp的延伸,32位元

棧的基本模型

參數N ↓高位址
參數… 函數參數入棧的順序與具體的調用方式有關
參數 3
參數 2
參數 1
EIP 傳回本次調用後,下一條指令的位址
EBP 儲存調用者的EBP,然後EBP指向此時的棧頂。
臨時變量1
臨時變量2
臨時變量3
臨時變量…
臨時變量5 ↓低位址

EIP,EBP,ESP都是系統的寄存器,裡面存的都是些位址。

 為什麼要說這三個指針,是因為我們系統中棧的實作上離不開他們三個。

 我們DC上講過棧的資料結構,主要有以下特點:

 後進先處。(這個強調過多)

其實它還有以下兩個作用:

 1.棧是用來存儲臨時變量,函數傳遞的中間結果。

 2.作業系統維護的,對于程式員是透明的。

我們可能隻強調了它的後進先出的特點,至于棧實作的原理,沒怎麼講?下面我們就通過一個小例子說說棧的原理。

先寫個小程式:

void fun(void)

{

   printf("hello world");

}

void main(void)

{

  fun()

  printf("函數調用結束");

}

這是一個再簡單不過的函數調用的例子了。

當程式進行函數調用的時候,我們經常說的是先将函數壓棧,當函數調用結束後,再出棧。這一切的工作都是系統幫我們自動完成的。

但在完成的過程中,系統會用到下面三種寄存器:

1.EIP

2.ESP

3.EBP

當調用fun函數開始時,三者的作用。

1.EIP寄存器裡存儲的是CPU下次要執行的指令的位址。

 也就是調用完fun函數後,讓CPU知道應該執行main函數中的printf("函數調用結束")語句了。

2.EBP寄存器裡存儲的是是棧的棧底指針,通常叫棧基址,這個是一開始進行fun()函數調用之前,由ESP傳遞給EBP的。(在函數調用前你可以這麼了解:ESP存儲的是棧頂位址,也是棧底位址。)

3.ESP寄存器裡存儲的是在調用函數fun()之後,棧的棧頂。并且始終指向棧頂。

當調用fun函數結束後,三者的作用:

1.系統根據EIP寄存器裡存儲的位址,CPU就能夠知道函數調用完,下一步應該做什麼,也就是應該執行main函數中的printf(“函數調用結束”)。

2.EBP寄存器存儲的是棧底位址,而這個位址是由ESP在函數調用前傳遞給EBP的。等到調用結束,EBP會把其位址再次傳回給ESP。是以ESP又一次指向了函數調用結束後,棧頂的位址。

其實我們對這個隻需要知道三個指針是什麼就可以,可能對我們以後學習棧溢出的問題以及看棧這方面的書籍有些幫助。當有人再給你說EIP,ESP,EBP的時候,你不能一頭霧水,那你水準就顯得窪了許多。其實不知道我們照樣可以程式設計,因為我們是C級别的程式員,而不是ASM級别的程式員

通過堆棧溢出來獲得root權限是目前使用的相當普遍的一項黑客技術。事實上這是一個黑客在系統本地已經擁有了一個基本賬号後的首選攻擊方式。

他也被廣泛應用于遠端攻擊。通過對daemon程序的堆棧溢出來實作遠端獲得rootshell的技術,已經被很多執行個體實作。

在windows系統中,同樣存在着堆棧溢出的問題。而且,随着internet的普及,win系列平台上的internet服務程式越來越多,低水準的win程式就成為你系統上的緻命傷因為它們同樣會被遠端堆棧溢出,而且,由于win系統使用者和管理者普遍缺乏安全防範的意識,一台win系統上的堆棧溢出,如果被惡意利用,将導緻整個機器被敵人所控制。進而,可能導緻整個區域網路落入敵人之手。

本系列講座将系統的介紹堆棧溢出的機制,原理,應用,以及防範的措施。希望通過我的講座,大家可以了解和掌握這項技術。而且,會自己去尋找堆棧溢出漏洞,以提高系統安全。

堆棧溢出系列講座

入門篇

本講的預備知識:

首先你應該了解intel彙編語言,熟悉寄存器的組成和功能。

你必須有堆棧和存儲配置設定方面的基礎知識,有關這方面的計算機書籍很多,我将隻是簡單闡述原理,着重在應用。

其次,你應該了解linux,本講中我們的例子将在linux上開發。

1:首先複習一下基礎知識。

從實體上講,堆棧是就是一段連續配置設定的記憶體空間。在一個程式中,會聲明各種變量。靜态全局變量是位于資料段并且在程式開始運作的時候被加載。而程式的動态的局部變量則配置設定在堆棧裡面。

從操作上來講,堆棧是一個先入後出的隊列。他的生長方向與記憶體的生長方向正好相反。我們規定記憶體的生長方向為向上,則棧的生長方向為向下。壓棧的操作push=ESP-4,出棧的操作是pop=ESP+4.換句話說,堆棧中老的值,其記憶體位址,反而比新的值要大。請牢牢記住這一點,因為這是堆棧溢出的基本理論依據。

在一次函數調用中,堆棧中将被依次壓入:參數,傳回位址,EBP。如果函數有局部變量,接下來,就在堆棧中開辟相應的空間以構造變量。函數執行結束,這些局部變量的内容将被丢失。但是不被清除。在函數傳回的時候,彈出EBP,恢複堆棧到函數調用的位址,彈出傳回位址到EIP以繼續執行程式。

在C語言程式中,參數的壓棧順序是反向的。比如func(a,b,c)。在參數入棧的時候,是:先壓c,再壓b,最後壓a.在取參數的時候,由于棧的先入後出,先取棧頂的a,再取b,最後取c。(PS:如果你看不懂上面這段概述,請你去看以看關于堆棧的書籍,一般的彙編語言書籍都會詳細的讨論堆棧,必須弄懂它,你才能進行下面的學習)

2:好了,繼續,讓我們來看一看什麼是堆棧溢出。

2.1:運作時的堆棧配置設定

堆棧溢出就是不顧堆棧中配置設定的局部資料塊大小,向該資料塊寫入了過多的資料,導緻資料越界。結果覆寫了老的堆棧資料。

比如有下面一段程式:

程式一:

#include 

int main ( )

{

char name[8];

printf("Please type your name: ");

gets(name);

printf("Hello, %s!", name);

return 0;

編譯并且執行,我們輸入ipxodi,就會輸出Hello,ipxodi!。程式運作中,堆棧是怎麼操作的呢?

在main函數開始運作的時候,堆棧裡面将被依次放入傳回位址,EBP。

我們用gcc -S 來獲得彙編語言輸出,可以看到main函數的開頭部分對應如下語句

pushl %ebp

movl %esp,%ebp

subl $8,%esp

首先他把EBP儲存下來,,然後EBP等于現在的ESP,這樣EBP就可以用來通路本函數的局部變量。之後ESP減8,就是堆棧向上增長8個位元組,用來存放name[]數組。現在堆棧的布局如下:

記憶體底部 記憶體頂部

name EBP ret 

<------ [ ][ ][ ]

^&name

堆棧頂部 堆棧底部 

執行完gets(name)之後,堆棧如下:

記憶體底部 記憶體頂部

name EBP ret 

<------ [ipxodi/0 ][ ][ ]

^&name

堆棧頂部 堆棧底部 

最後,main傳回,彈出ret裡的位址,指派給EIP,CPU繼續執行EIP所指向的指令。

2.2:堆棧溢出

好,看起來一切順利。我們再執行一次,輸入ipxodiAAAAAAAAAAAAAAA,執行完

gets(name)

之後,堆棧如下:

記憶體底部 記憶體頂部

name EBP ret 

<------ [ipxodiAA][AAAA][AAAA].......

^&name

堆棧頂部 堆棧底部 

由于我們輸入的name字元串太長,name數組容納不下,隻好向記憶體頂部繼續寫‘A’。由于堆棧的生長方向與記憶體的生長方向相反,這些‘A’覆寫了堆棧的老的元素。如圖我們可以發現,EBP,ret都已經被‘A’覆寫了。在main傳回的時候,就會把‘AAAA’的ASCII碼:0x41414141作為傳回位址,CPU會試圖執行0x41414141處的指令,結果出現錯誤。這就是一次堆棧溢出。

3:如何利用堆棧溢出

我們已經制造了一次堆棧溢出。其原理可以概括為:由于字元串處理函數(gets,strcpy等等)沒有對數組越界加以監視和限制,我們利用字元數組寫越界,覆寫堆棧中的老元素的值,就可以修改傳回位址。

在上面的例子中,這導緻CPU去通路一個不存在的指令,結果出錯。

事實上,當堆棧溢出的時候,我們已經完全的控制了這個程式下一步的動作。如果我們用一個實際存在指令位址來覆寫這個傳回位址,CPU就會轉而執行我們的指令。

在UINX系統中,我們的指令可以執行一個shell,這個shell将獲得和被我們堆棧溢出的程式相同的權限。如果這個程式是setuid的,那麼我們就可以獲得root shell。

繼續閱讀