踐踏堆棧-緩衝區溢位漏洞

來源:互聯網
上載者:User

標籤: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編譯器堆棧保護技術
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.