函數調用棧分析

轉載請註明出處:http://blog.csdn.net/wangxiaolong_china

 

關於堆棧空間利用最核心的一點就是:函數調用棧。而要深入理解函數調用棧,最重要的兩點就是:棧的結構變化,ebp寄存器的作用。

首先要認識到這樣兩個事實:

1.      一個函數調用動作可分解爲:零到多個push指令(用於參數入棧),一個call指令。call指令內部其實還暗含了一個將eip返回地址(即call指令下一條指令的地址)壓棧的動作。

2.      幾乎所有本地編譯器都會在每個函數體之前插入類似的指令:push %ebp,mov %esp %ebp。

因此,在程序執行到一個函數的真正函數體的時候,已經有以下數據壓入到堆棧中:零到多個參數,返回地址eip,ebp。

由此得到如下的棧結構(其中參數入棧順序跟調用方式有關,這裏以C語言默認的CDECL爲例):

“push %ebp”“mov %esp %ebp”這兩條指令實在是太有深意了:首先將ebp入棧,然後將棧頂指針esp賦值給ebp。“mov %esp %ebp”這條指令表面上看是用esp把ebp原來的值覆蓋了,其實不然,因爲在給ebp賦值之前,原ebp值已經被壓棧(位於棧頂),esp賦值給ebp後,ebp恰好指向棧頂(即被壓棧的原esp的位置)。

此時,ebp寄存器就處在一個非常重要的地位,該寄存器中存儲着棧中的一個地址(原ebp入棧後的棧頂),以該地址爲基準,向上(棧底方向)能獲取返回地址,函數調用參數值;向下(棧頂方向)能獲取函數局部變量值;而該地址處又存儲着上一層函數調用時的ebp值!!一般而言,ss:[ebp+4]處爲返回地址,ss:[ebp+8]處爲第一個參數值(最後一個入棧的參數值,此處假設其佔用4字節內存),ss:[ebp-4]處爲第一個局部變量,ss:[ebp]處爲上一層ebp值。

由於ebp中的地址總是“上一層函數調用時的ebp值”,而在每一層函數調用中,都能通過當時的ebp值“向上(棧底方向)能獲取返回地址、函數調用參數,向下(棧頂方向)能獲取函數局部變量值”。如此形成遞歸,直至到達棧底。這就是函數調用棧。由此看見,編譯器對於ebp寄存器的使用實在是太精妙了。

此外,從當前ebp出發,逐層向上找到所有的ebp是非常容易的:

unsigned int _ebp;
__asm _ebp, ebp;
while (not stack bottom)
{
    //...
    _ebp = *(unsigned int*)_ebp;
}

下面通過一個簡單的C程序,簡要的分析一下函數調用棧的變化情況。通過對具體C程序函數調用過程中堆棧空間變化的分析,加深對於函數調用棧的理解。

要分析的C程序源碼如下:

  stack.c
1 #include <stdio.h>
  2 
  3 void func1() {
  4         printf("in func1.\n");
  5 }
  6 
  7 void func2() {
  8         printf("in func2.\n");
  9 }
 10 
 11 void func3() {
 12         int a = 1;
 13         *(&a + 2) = (int)func1;
 14 }
 15 
 16 int main(void) {
 17         int a_main = 1;
 18         *(&a_main - 3) = (int)func2;
 19         func3();
 20 
 21         return 0;
 22 }

程序執行結果:

root@linux:~/pentest# gcc -g -o stack stack.c
root@linux:~/pentest# ./stack
in func1.
in func2.
Segmentation fault

使用GDB反彙編stack程序:

root@linux:~/pentest# gdb stack
GNU gdb (Ubuntu/Linaro 7.2-1ubuntu11) 7.2
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/pentest/stack...done.
(gdb) disass main
Dump of assembler code for function main:
   0x080483f8 <+0>:	push   %ebp
   0x080483f9 <+1>:	mov    %esp,%ebp
   0x080483fb <+3>:	sub    {1}x10,%esp
   0x080483fe <+6>:	movl   {1}x1,-0x4(%ebp)
   0x08048405 <+13>:	lea    -0x4(%ebp),%eax
   0x08048408 <+16>:	sub    {1}xc,%eax
   0x0804840b <+19>:	mov    {1}x80483c8,%edx
   0x08048410 <+24>:	mov    %edx,(%eax)
   0x08048412 <+26>:	call   0x80483dc <func3>
   0x08048417 <+31>:	mov    {1}x0,%eax
   0x0804841c <+36>:	leave  
   0x0804841d <+37>:	ret    
End of assembler dump.
(gdb) disass func3 
Dump of assembler code for function func3:
   0x080483dc <+0>:	push   %ebp
   0x080483dd <+1>:	mov    %esp,%ebp
   0x080483df <+3>:	sub    {1}x10,%esp
   0x080483e2 <+6>:	movl   {1}x1,-0x4(%ebp)
   0x080483e9 <+13>:	lea    -0x4(%ebp),%eax
   0x080483ec <+16>:	add    {1}x8,%eax
   0x080483ef <+19>:	mov    {1}x80483b4,%edx
   0x080483f4 <+24>:	mov    %edx,(%eax)
   0x080483f6 <+26>:	leave  
   0x080483f7 <+27>:	ret    
End of assembler dump.
(gdb) disass func2
Dump of assembler code for function func2:
   0x080483c8 <+0>:	push   %ebp
   0x080483c9 <+1>:	mov    %esp,%ebp
   0x080483cb <+3>:	sub    {1}x18,%esp
   0x080483ce <+6>:	movl   {1}x80484ea,(%esp)
   0x080483d5 <+13>:	call   0x80482f0 <puts@plt>
   0x080483da <+18>:	leave  
   0x080483db <+19>:	ret    
End of assembler dump.
(gdb) disass func1
Dump of assembler code for function func1:
   0x080483b4 <+0>:	push   %ebp
   0x080483b5 <+1>:	mov    %esp,%ebp
   0x080483b7 <+3>:	sub    {1}x18,%esp
   0x080483ba <+6>:	movl   {1}x80484e0,(%esp)
   0x080483c1 <+13>:	call   0x80482f0 <puts@plt>
   0x080483c6 <+18>:	leave  
   0x080483c7 <+19>:	ret    
End of assembler dump.
(gdb)


 

下面將按照程序執行流程,分析函數調用棧的主要變化情況:

程序執行起點是main函數,其調用棧變化如下所示:

Main程序調用func3,故調用func3後,調用棧變化如下所示:

由於func1的地址覆寫了eip_main函數調用返回地址,故func3執行結束後,將返回到func1並繼續執行,程序調用棧變化如下所示:

程序從func1返回時,填入棧中的func2的地址將作爲返回地址使用,即程序返回後將跳轉到func2起始處執行,程序調用棧如下所示:

程序執行func2,在執行ret指令時,由於返回地址可能指向無效的段,從而導致程序執行結果出現Segmentationfault。

要使程序執行可以正常結束,而不會出現Segmentation fault,則需要main函數中18 *(&a_main- 3) = (int)func2;之後添加如下一行代碼即可:

*(&a_main -2) = *(&a_main + 2);

通過該實驗,對於函數調用棧在函數調用過程中的變化的理解進一步加深,有利於更好的理解棧溢出的原理。

 

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