函数调用栈分析

时间:2021-12-10 13:51:10

转载请注明出处: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);

通过该实验,对于函数调用栈在函数调用过程中的变化的理解进一步加深,有利于更好的理解栈溢出的原理。