天天看點

棧溢出攻擊

 本文的實驗來源于《Computer Systems A Programmer's Perspective》(深入了解計算機系統》一書中第三章的一個實驗。 

    作者給出了一個含有緩沖區溢出的程式bufbomb.c,你需要做的,就是注入給緩沖區些特殊的資料,到底利用緩沖區的目的。

  1. //bufbomb.c 
  2. #include <stdio.h> 
  3. #include <stdlib.h> 
  4. #include <ctype.h> 
  5. char *getxs(char *dest) 
  6.   int c; 
  7.   int even = 1;  
  8.   int otherd = 0;  
  9.   char *sp = dest; 
  10.   while ((c = getchar()) != EOF && c != '/n') { 
  11.     if (isxdigit(c)) { 
  12.       int val; 
  13.       if ('0' <= c && c <= '9') 
  14.     val = c - '0'; 
  15.       else if ('A' <= c && c <= 'F') 
  16.     val = c - 'A' + 10; 
  17.       else 
  18.     val = c - 'a' + 10; 
  19.       if (even) { 
  20.     otherd = val; 
  21.     even = 0; 
  22.       } else { 
  23.     *sp++ = otherd * 16 + val; 
  24.     even = 1; 
  25.       } 
  26.     } 
  27.   } 
  28.   *sp++ = '/0'; 
  29.   return dest; 
  30. int getbuf() 
  31.     char buf[12]; 
  32.     getxs(buf); 
  33.     return 1; 
  34. void test() 
  35.   int val; 
  36.   printf("Type Hex string:"); 
  37.   val = getbuf(); 
  38.   printf("getbuf returned 0x%x/n", val); 
  39. int main() 
  40.   int buf[16]; 
  41.   test(); 
  42.   return 0; 

        函數getxs的功能類似于庫函數gets的功能,除了它是以十六進制數字對的編碼方式讀入的字元。例如,要讀入字元串“0123”,你得給出輸入字元串“30 31 32 33”,這個函數會忽略空格。 

        分析這個程式,可以得知,正常情況下,這個函數會在getbuf中,調入getxs函數讀入數字對,然後不管任何情況下,都會對test函數傳回0x1,然後由test中的printf函數列印處getbuf的傳回值。 

        現在你的任務,就是,利用緩沖區溢出的漏洞,輸入些特殊的數字,使得螢幕中列印的是0xdeadbeef。

        我是在WindowsXP,visual c++6.0環境解決這個問題的。

        在做這個題目之前,你當然要知道什麼是幀棧結構(請參閱《深入了解計算機系統》第三章),了解%ebp和%esp的含義。

        題目中已經說了,“分析這個程式,可以得知,正常情況下,這個函數會在getbuf中,調入getxs函數讀入數字對,然後不管任何情況下,都會對 test函數傳回0x1,”那我們該怎麼辦了?我們馬上可以想到在getbuf這個函數裡定義的char buf[12]上做手腳,可以看到在getxs函數裡的while循環,結束條件隻是以回車或者是eof結束符為判斷标準,是以,根本沒對char輸入的數量做判斷!這樣的話,我們可以輸入多于12個的數,進而緩沖區溢出!

        在這裡還是提一下幀棧結構,如下:

+-------------------------------+高位址

|函數參數 n 個                   |

+-------------------------------+

|函數參數第 n-1 個            |

+-------------------------------+

|                    . . .                   |

|                    . . .                   |

|                    . . .                   |

+-------------------------------+

|函數參數第1個                  |

+-------------------------------+

|return 傳回位址                 |

+-------------------------------+

|ebp指針入棧                      | 

+-------------------------------+

|local var(局部變量)      |

+-------------------------------+

|                            others     |

+-------------------------------+低位址

        按照上面說的函數棧的存放情況,在getbuf這個函數裡,函數參數沒有,我們不管,然後就是return傳回位址,然後就是ebp指針,然後就是char buf[12]。

+-------------------------------+低位址

|return 傳回位址                 |

+-------------------------------+

|ebp指針入棧                      | 

+-------------------------------+

|                buf[11]                |

+-------------------------------+

|                buf[10]                |

+-------------------------------+

                    :

                    :

                    :

+-------------------------------+

|                 buf[0]                 |

+-------------------------------+

|                others                  |

+-------------------------------+高位址

         如果我們對buf溢出,能改寫ebp和return位址!下面看看,ebp是多少,return位址是多少。

         要知道這裡的%ebp存的是test函數的%ebp,因而我們在調試的時候就可以在test函數得到%ebp的值,它應該是我們寫入的 buf[12]-buf[15]的值,而且它要保持原來的值,不然傳回之後就亂套了,在我機器上是0x0012efa0。這個很容易,解決了第一步。

         下面我們再來看傳回位址,先看一段彙編碼(不同的機器有所不同):

58:       val = getbuf();

004011C5   call        @ILT+10(getbuf) (0040100f)

004011CA   mov         dword ptr [ebp-4],eax

59:       printf("getbuf returned 0x%x/n", val);

004011CD   mov         eax,dword ptr [ebp-4]

004011D0   push        eax

004011D1   push        offset string "getbuf returned 0x%x/n" (0042001c)

004011D6   call        _printf (00401510)

004011DB   add         esp,8

          在getbuf()傳回後,肯定會接着執行004011CA ,我們能讓它從這執行嗎?當然不行!不然就要push eax,那是我們不想看到的,因為eax的值就是1。因而我們會想到能不能跳過這?當然能,改傳回位址啊!順水推舟,我們通過buf數組來覆寫傳回位址。 此時,我們想要它直接跳到004011D1處,因而可以通過設定buf[16]-buf[19]的值來覆寫傳回位址。

          到了考慮如何加進deadbeef了,在傳回後,将直接執行push        offset string "getbuf returned 0x%x/n" (0042001c),沒eax怎麼行了?不然printf函數就少了參數。再回到幀棧結構一下,printf在調用之前,要壓入參數,先壓入val,再 壓入offset string "getbuf returned 0x%x/n",也就是說參數val(等同那個eax)在offset string "getbuf returned 0x%x/n“之上,而且緊挨着。此時我們可以想到,既然傳回之後(傳回位址及其以下的元素都已彈出,傳回位址的上一個位元組成了棧頂)就執行 push        offset string "getbuf returned 0x%x/n" (0042001c)進行壓棧,那此時棧頂一定是參數val,而它原來就在傳回位址上面,因而我們可以通過設定buf[20]-buf[23]的值來覆寫 這個地方。

          綜上所述,%ebp的值為0x0012efa0,修改後的傳回位址為0x004011D1,因而我們可以輸入

          00000000  00000000  00000000  a0ef1200  d1114000  efbeadde

          這24個0可以輸入别的,不影響,關鍵是後面24個數。

棧幀也叫過程活動記錄,是編譯器用來實作過程/函數調用的一種資料結構 

從邏輯上講,棧幀就是一個函數執行的環境:函數參數、函數的局部變量、函數執行完後傳回到哪裡 … 。 

實作上有硬體方式和軟體方式(有些體系不支援硬體棧)

緩沖區溢出攻擊主要是利用棧幀的構成機制,利用疏忽的程式破壞棧幀進而使程式轉移到攻擊者期望或設定的代碼上。 

詳細分析過程調用的相關操作和彙編指令的作用。

    還是以代碼來分析,非常簡單的c程式,不過對于我們所關注的問題,已經足以說明問題了:

int f(int *a, int *b)

{

 int c;

 c = a + b;

 return c;

}

int main(void)

{

 int a, b;

 a = 1, b = 2;

 f(&a, &b);

 return 0;

}

    這個程式生成的彙編代碼如下(省略了不相關的部分,專注于兩個函數的函數體部分):

_f:

 pushl %ebp

 movl %esp, %ebp

 subl $4, %esp

 movl 8(%ebp), %edx

 movl 12(%ebp), %eax

 movl (%eax), %eax

 addl (%edx), %eax

 movl %eax, -4(%ebp)

 movl -4(%ebp), %eax

 leave

 ret

_main:

 pushl %ebp

 movl %esp, %ebp

 subl $24, %esp

 andl $-16, %esp

 movl $0, %eax

 movl %eax, -12(%ebp)

 movl -12(%ebp), %eax

 call __alloca

 call ___main

 movl $1, -4(%ebp)

 movl $2, -8(%ebp)

 leal -8(%ebp), %eax

 movl %eax, 4(%esp)

 leal -4(%ebp), %eax

 movl %eax, (%esp)

 call _f

 movl $0, %eax

 leave

 ret

    用幾幅圖來說明問題:

   1)函數的建立部分:

    任何一個函數在開始調用之前都要建立起函數體的棧幀(stack frame)結構,使得ebp作為幀指針(frame pointer),

而esp作為棧指針(stack pointer),需要注意的是esp是移動的,因為esp指針儲存的是棧頂元素的位址,壓棧或者退棧操作都将影響到esp指

針的值,而ebp則是不動的,是以關于過程的許多資訊比如局部變量的位置等等都是相對于ebp來給出的,ebp和esp這兩個指針是如此的重要以

至于如果在過程中被破壞了,那麼整個過程的棧幀結構也就破壞了,結果不堪設想。涉及到函數體建立的彙編代碼是:

pushl %ebp           ;儲存上一個函數的ebp

movl %esp, %ebp  ;使ebp成為目前這個過程的棧幀結構的幀指針

subl $24, %esp      ;為過程的棧幀結構配置設定空間,這樣ebp和esp之間的空間就

                            ;可以容納本過程的局部變量和調用下一個函數

                            ;時需要調用的參數了,注意這個空間的大小可能還要考慮到對齊等因素。

    執行完上面三個指令之後,函數的棧幀結構大緻如圖:

                 _____ebp(main)                     位址高位

   |           |

   |  ........ |

   |  ........ |ebp和esp相差24個位元組

   |           |____esp(main)                       位址低位

2)函數體的主體部分:

    具體每個函數主體部分可能有較大的差别,不過一般的,函數體的局部變量的都是放在緊跟着ebp的位址空間裡,在我們的例子裡在main函

數中有這樣的指令:

movl $1, -4(%ebp)  ;-4(%ebp)就是a的所在

movl $2, -8(%ebp)  ;-8(%ebp)就是b的所在

是以,此時的main函數的棧幀結構大緻如圖:

                         _____ebp(main)                     位址高位

   |這個空間屬于a|

   |   a              |____a的位址在ebp - 4處

   |這個空間屬于b|

   | b                |____b的位址在ebp - 8處           

   |  ........         |

   |  ........         |ebp和esp相差24個位元組

   |                   |____esp(main)                     位址低位

    需要注意的是圖中a和b指針的指向,需要提醒的是讀取資料的時候是從低位址開始讀取的,是以當我們說"a的位址是ebp-4"的時候意味着 ebp-4到ebp這32位的位址空間存放的是a的資料,而不是從ebp-4到ebp-8,也就是說存儲資料是從低到高的,而給出位址的時候都是給的最低 位的位址。

3)準備調用f函數時的參數準備:

在一個函數的棧幀結構中除了存放有本過程的局部變量之外,在棧幀的最低的位置還要存放着這個函數将要調用的函數要用到的參數,如在我們的例子中main函數要調用函數f要用到&a, &b,那麼相關的代碼是:

leal -8(%ebp), %eax  ;把b的位址送入eax寄存器中

movl %eax, 4(%esp)  ;再通過eax寄存器把b的位址存入esp + 4處

leal -4(%ebp), %eax  ;把a的位址送入eax寄存器中

movl %eax, (%esp)  ;再通過eax寄存器把a的位址存入esp處

這樣之後,main函數的棧幀結構大緻如圖所示:

                  _____ebp(main)                     位址高位

   |這個空間屬于a |

   |   a               |____a的位址在ebp - 4處

   |這個空間屬于b |

   | b                 |____b的位址在ebp - 8處           

   |  ........          |     

   |  ........          |ebp和esp相差24個位元組

   |                    |

   | &b的位址       |____esp(main) + 4

   |                    |

   | &a的位址       |____esp(main)                     位址低位

   這就是一個比較完整的main函數的棧幀結構的示意圖了,可以看出在最上面的是函數的局部變量,而在最下面的是為被調用函數準備好的參數,不同的機器,操 作系統壓棧順序也是不一樣的,在我的機子上調用f(&a, &b)的時候壓棧是從右到左的,也就是先壓入b再壓入a。

   再說一說lea這個指令,這個指令并不真正的通路存儲器,而是産生一個有效位址,假設在這裡ebp為0x12345678,那麼執行leal -8(%ebp), %eax之後eax的資料就是0x12345670了,這個指令非常有用,經常用于生成指針(指針其實就是位址嘛),還可以用于通路數組的元素,比如說我 們要通路數組E[i],假設E的起始位址存放在edx中,而索引也就是i存放在ecx中,再假設這個數組是int類型的,也就是4個位元組大小的,那麼通路 E[i]并且把它的指放入eax就相當于:

movl (%edx, %ecx, 4), %eax

注意這時eax中的是資料,類型是int類型的,如果要得到E[i]的位址,那麼我們可以:

leal (%edx, %ecx, 4), %eax

4)執行call指令後的棧幀結構:

call指令相當于下面的兩個操作,首先把調用函數中在執行完函數調用後的下一條指令的位址壓入棧,在這裡其實就是相當于把指令movl $0,

%eax的位址壓入棧中,然後修改eip指針使它指向被調用函數的起始處(我們知道eip指針存放的是下一條指令的執行位址),此時的棧幀結構

大緻如圖:

                        _____ebp(main)                     位址高位

   |這個空間屬于a |

   |   a               |____a的位址在ebp - 4處

   |這個空間屬于b |

   | b                 |____b的位址在ebp - 8處           

   |  ........          |

   |  ........          |ebp和esp相差24個位元組

   |                    |

   | &b的位址       |____esp(main) + 4

   |                    |

   | &a的位址       |____esp(main)                     位址低位

   |main函數中下  |

   |一條指令的位址|____esp(此時esp指針已經不再指向main函數的棧幀結構了)

同時,可以看到的是push操作的相當于下面的兩條指令,比如說pushl %ebp,就相當于:

   subl $4, %esp

   movl %ebp, (%esp)

效果都是相同的,不同的是push指令的機器碼要比這兩個指令要簡單的多。

還需要注意的是在通路記憶體時的兩種不同操作,比如:

   movl $4, %esp

相當簡單,直接把4送進esp就是了。

   movl $4, (%esp)

就複雜一點,首先得到esp中的值,然後把這個值作為位址,然後把4送入這個位址。

簡而言之,沒有加括号的時候,寄存器相當于普通的整形變量,而加了括号以後寄存器就相當于指針了,存放着變量的位址。

5)進入f函數以後的棧幀分布情況:

同樣的,在進入f函數的時候也要建立起函數的棧幀結構,同樣要調用這樣的三條指令:

pushl %ebp     ;儲存上一個函數的ebp

movl %esp, %ebp   ;使ebp成為目前這個過程的棧幀結構的幀指針

subl $4, %esp    ;為過程的棧幀結構配置設定空間,

   ;這樣ebp和esp之間的空間就可以容納本過程的局部變量和調用下一個函數

   ;時需要調用的參數了,注意這個空間的大小可能還要考慮到對齊等因素。

這時的棧幀結構大緻如圖:

                        _____ebp(main)              位址高位

   |這個空間屬于a |

   |   a               |____a的位址在ebp - 4處

   |這個空間屬于b |

   | b                 |____b的位址在ebp - 8處           

   |  ........          |

   |  ........          |ebp和esp相差24個位元組

   |                    |

   | &b的位址       |____esp + 4

   |                    |

   | &a的位址       |____esp(main)                     

   |main函數中下  |

   |一條指令的位址 |____ebp + 4(f)

   |main函數中      |

   |ebp儲存在這裡 |____ebp(f)   位址低位

   注意,首先執行pushl %ebp指令不僅把main函數的ebp棧幀儲存在棧中,而且還使得esp指針減少了4,在執行movl %esp, %ebp完後,ebp和esp指向同一個位置了,是以這是ebp處儲存的就是main函數的ebp指針的值,而ebp + 4的地方存放的就是main函數中下一條指令的位址了,我們前面說過過程中的局部變量是緊跟着ebp指針的,一旦發生溢出,存儲的資料往位址高位走就會覆 蓋這兩個關鍵的值,如果程式修改指令的地

址讓它指向一段有害的代碼,或者修改ebp指針使得傳回的時候到一個有害的函數處,那麼後果是不堪設想的--這就是所謂的"緩沖區溢出"。

6)退出函數時的恢複堆棧準備:

一般的,在函數傳回的時候都有如下的兩條指令:

leave

ret

逐條來解釋,首先leave指令相當于以下的兩條代碼:

movl %ebp, %esp   ;恢複esp指針

popl %ebp     ;恢複ebp指針

    注意看5)的示意圖,在執行完movl %ebp, %esp之後,ebp和esp指向同一個位置,前面說過ebp處儲存的時main函數棧幀結構的ebp指針的值,再執行popl %ebp的時候,就可以順利的把調用函數(這裡時main函數)的ebp指針恢複了,由此再次強調在函數運作的過程中切記不可修改

ebp和esp指針,否則結果是不可預料的。

    同時也可以看出pop指令的相當于下面的兩條指令,如popl %ebp相當于

movl (%esp), %ebp

addl $4, %esp

   執行完leave指令之後,也就是在執行完popl %ebp後,上面說了esp指針已經加上4了,也就是說esp處存放的是main函數中下一條指令的位址了,執行ret指令就相當于popl %eip,也就是把這個位址送入eip指針中(實際并不能這樣做)。

   上面就是一個函數從建立到調用其它函數再傳回的具體分析了,說的應該夠清楚的了

分享到: 

  • 上一篇:應用程式架構本質,第 2 部分: 設計模式入門
  • 下一篇:C中不安全的函數

檢視評論

繼續閱讀