栈内存控制

来源:互联网 发布:数据分析培训课程 编辑:程序博客网 时间:2024/06/05 15:18


栈内存控制

所谓栈内存控制就是如何能够随意改变栈上的内容,或者重新利用上个函数栈上的内容。先来看一个简单的例子:

#include<stdio.h>

int foo(int initial,int val)

{

int local;

if(initial)

       {

       local=val;

       printf("foo local=%d\n",local);

       return 0;   

       }

printf("foo local=%d\n",local);

}

int bar(int initial,int val)

{

int local;

if(initial)

       {

       local=val;

       printf("bar local=%d\n",local);

       return 0;

       }

printf("bar local=%d\n",local);

return 1;

}

int main()

{

       foo(1,10);

       foo(0,0);

       bar(1,12);

       foo(0,0);

}

运行结果图:

这段代码很简单,函数foo和bar具有完全相同的函数体,调用bar(1,12),然后再调用foo(0,0),结果打印的是bar设置的12。为什么 foo会打印出bar里面的值,而不是之前foo里面的值?因为他们具有完全相同的函数体,从而使他们的函数栈相同,在main函数中每次调用他们,都会 使用相同的栈,而栈内的内容不会在函数返回时清空,而是保持原样,这样在foo(0,0)的时候,没有去赋值,而是直接使用栈上的值,这个栈刚刚被bar 使用过,所以就造成了foo获取到了bar内的值。这就是一个简单的栈上内容的控制,当然,我们要控制内核栈要比这复杂的多。如果您没有明白,那么我想您需要学习一下C中的局部变量的分配与栈的关系。

补充:上面只是对栈内存控制进行了一个理论上的分析。然而真正是怎样实现的,下面我们来做一个分析。

我在这里假设各位读者已经具有使用gdb调试和objdump反汇编工具的基本操作的能力。如果没有,希望还是学习一下。因为这两个工具不管是在应用程序开发还是在内核学习中都会经常遇到。特别是在遇到segment fault时对于错误的定位有很好的帮助。

首先我们运行 :gdb ./stack_contral 然后通过b 25在25行处设置断点。接着r指令运行至断点处,info frame查看当前栈帧情况。从图中我们可以发现,在执行foo(1,10)时栈帧esp指针在0xbffff118,而调用者的栈帧指针在

0xbffff120处。然后继续进行 n单步运行,并查看各被调用函数的帧栈信息。发现之后调用foo(0,0)以及bar(1.12)和foo(0,0)都使用的同一个帧栈结构。因此无论是在bar还是foo中存放的局部变量是存放在用户栈中的同一地址处的。因此,即使第二次调用foo(0,0)的时候,即使函数中没有给局部变量int c赋值,对应栈地址中仍然存放的是10,因为这是上次foo(1,10)函数调用遗留在栈结构中的数据。同理可以解释调用bar(1,12)后再调用foo(0,0)却输出了foo local=12的结果。


如果你还没有理解,那好吧我再介绍一个讲解函数调用栈帧结构的例子,方便你先了解基础知识,完了再回过头理解如何进行栈内存控制。

从逻辑上讲进程的堆栈是由多个堆栈帧构成的,其中每个堆栈帧都对应一个函数调用。当函数调用发生时,新的堆栈帧被压入堆栈;当函数返回时,相应的堆栈帧从堆栈中弹出。

任何一个程序通常都包括代码段和数据段,这些代码和数据本身都是静态的。程序要想运行,首先要由操作系统负责为其创建进程,并在进程的虚拟地址空间中为其代码段和数据段建立映射。光有代码段和数据段是不够的,进程在运行过程中还要有其动态环境,其中最重要的就是堆栈。图3所示为 Linux下进程的地址空间布局:

3 Linux下进程地址空间的布局


首先,execve(2)会负责为进程代码段和数据段建立映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。另外,execve(2)还会将bss段清零,这就是为什么未赋初值的全局变量以及static变量其初值为零的原因。进程用户空间的最高位置是用来存放程序运行时的命令行参数及环境变量的,在这段地址空间的下方和bss段的上方还留有一个很大的空洞,而作为进程动态运行环境的堆栈和堆就栖身其中,其中堆栈向下伸展,堆向上伸展。

知道了堆栈在进程地址空间中的位置,我们再来看一看堆栈中都存放了什么。相信读者对C语言中的函数这样的概念都已经很熟悉了,实际上堆栈中存放的就是与每个函数对应的堆栈帧。当函数调用发生时,新的堆栈帧被压入堆栈;当函数返回时,相应的堆栈帧从堆栈中弹出。典型的堆栈帧结构如图4示。



堆栈帧的顶部为函数的实参,下面是函数的返回地址以及前一个堆栈帧的指针,最下面是分配给函数的局部变量使用的空间。一个堆栈帧通常都有两个指针,其中一个称为堆栈帧指针,另一个称为栈顶指针。前者所指向的位置是固定的,而后者所指向的位置在函数的运行过程中可变。因此,在函数中访问实参和局部变量时都是以堆栈帧指针为基址,再加上一个偏移。对照图4可知,实参的偏移为正,局部变量的偏移为负。

介绍了堆栈帧的结构,我们再来看一下在Intel i386体系结构上堆栈帧是如何实现的。图5和图6分别是一个简单的C程序及其编译后生成的汇编程序。

一个简单的C程序example1.c

int function(int a, int b, int c)
{
        char buffer[14];
        int     sum;
        sum = a + b + c;
        return sum;
}
void main()
{
        int     i;
        i = function(1,2,3);
}


 example1.c编译后生成的汇编程序example1.s

1      .file   "example1.c"
2      .version    "01.01"
3 gcc2_compiled.:
4 .text
5      .align 4
6 .globl function
7      .type    function,@function
8 function:
9      pushl %ebp    
 #先将调用者的栈帧指针压栈。

10     movl %esp,%ebp #esp是栈顶指针(个人理解就是进程全局的栈顶指针,根据esp就可知道现在栈的空间实用情况。)然后将esp赋给ebp,表示现在栈帧为空
11     subl  $20,%esp  #可以看出buffer[14]+int在四字节对齐下,为20字节空间。所以移动esp,为当前函数对应的栈帧分配20字节空间.

12     movl 8(%ebp),%eax #ebp地址加8字节中存放的是 1(返回地址、前一个ebp、实参+

13     addl 12(%ebp),%eax #ebp地址加12字节中存放的是2

14     movl 16(%ebp),%edx #ebp地址加16字节中存放的是3(当调用function的时候实参压栈是从右到左的,而先压入栈的在高地址,因此得出上面注释中结论。)

15     addl %eax,%edx
16     movl %edx,-20(%ebp) #
ebp地址减20字节中存放的是局部变量int sum(局部变量-
17     movl -20(%ebp),%eax #通过eax来传递函数返回值
18     jmp .L1
19     .align 4
20 .L1:
21     leave #
相当于 1.movl  %ebp ,%esp    2.popl %ebp

 一、leave指令将堆栈帧指针 ebp拷贝到esp中,于是在堆栈帧中为局部变量buffer[14]sum分配的空间就被释放了;二、leave指令还有一个功能,就是从堆栈中弹出一个机器字并将其存放到ebp中,这样ebp就被恢复为main函数的堆栈帧指针(对应pushl %ebp这句压栈操作)了
22     ret  #
ret指令再次从堆栈中弹出一个机器字并将其存放到指令指针eip这样控制就返回到了第36main函数中的addl指令处
23 .Lfe1:
24     .size    function,.Lfe1-function
25     .align 4
26 .globl main
27     .type    main,@function
28 main:
29     pushl %ebp
#进入main函数时的esp压栈操作 esp地址减4esp指向地址中存入ebp中的值。
30        movl %esp,%ebp # main函数栈帧的初始化。
31     subl $4,%esp #
先分配给局部变量int i 4字节空间。
32     pushl $3 #
实参入栈,从右到左
33     pushl $2
34     pushl $1
35     call function #
call将控制转移到function,将call的下一条指令addl的地址,也就是function函数的返回地址压入堆栈。
36     addl $12,%esp
#function函数返回之后,将esp指针加12其实是释放三个实参。
37     movl %eax,%eax
38     movl %eax,-4(%ebp)
#将返回值通过int i返回。
39 .L2:
40     leave  #
相当于 1.movl  %ebp ,%esp(main中释放局部变量空间)  2.popl %ebp

(弹出调用main函数的函数的栈帧指针ebp)

41     ret #
从堆栈中弹出eip,返回调用出下条指令。
42 .Lfe2:
43     .size    main,.Lfe2-main
44     .ident  "GCC: (GNU) 2.7.2.3"


这里我们着重关心一下与函数function对应的堆栈帧形成和销毁的过程。从程序中可以看到,function是在main中被调用的,三个实参的值分别为123由于C语言中函数传参遵循反向压栈顺序,所以在反汇编3234行三个实参从右向左依次被压入堆栈。接下来35行的 call指令除了将控制转移到function之外,还要将call的下一条指令addl的地址,也就是function函数的返回地址压入堆栈。下面就进入function函数了,首先在第9行将main函数的堆栈帧指针ebp保存在堆栈中并在第10行将当前的栈顶指针esp保存在堆栈帧指针ebp最后在第11行为function函数的局部变量buffer[14]sum在堆栈中分配空间。至此,函数function的堆栈帧就构建完成了,其结构如图5所示。



读者不妨回过头去与图4对比一下。这里有几点需要说明。首先,Intel i386体系结构下,堆栈帧指针的角色是由ebp扮演的,而栈顶指针的角色是由esp扮演的。另外,函数function的局部变量buffer[14]14个字符组成,其大小按说应为14字节,但是在堆栈帧中却为其分配了16个字节。这是时间效率和空间效率之间的一种折衷,因为Intel i38632位的处理器,其每次内存访问都必须是4字节对齐的,而高30位地址相同的4个字节就构成了一个机器字。因此,如果为了填补 buffer[14]留下的两个字节而将sum分配在两个不同的机器字中,那么每次访问sum就需要两次内存操作,这显然是无法接受的。还有一点需要说明的是,正如我们在本文前言中所指出的,如果读者使用的是较高版本的gcc的话,您所看到的函数function对应的堆栈帧可能和图7所示有所不同。上面已经讲过,为函数function的局部变量buffer[14]sum在堆栈中分配空间是通过在反汇编中第11行对esp进行减法操作完成的,而sub指令中的20正是这里两个局部变量所需的存储空间大小。但是在较高版本的gcc中,sub指令中出现的数字可能不是20,而是一个更大的数字。应该说这与优化编译技术有关,在较高版本的gcc中为了有效运用目前流行的各种优化编译技术,通常需要在每个函数的堆栈帧中留出一定额外的空间。

下面我们再来看一下在函数function中是如何将abc的和赋给sum的。前面已经提过,在函数中访问实参和局部变量时都是以堆栈帧指针为基址,再加上一个偏移,而Intel i386体系结构下的堆栈帧指针就是ebp,为了清楚起见,我们在图7中标出了堆栈帧中所有成分相对于堆栈帧指针ebp的偏移。这下反汇编1216的计算就一目了然了,8(%ebp)12(%ebp)16(%ebp)-20(%ebp)分别是实参abc和局部变量sum的地址,几个简单的 add指令和mov指令执行后sum中便是abc三者之和了。另外,在gcc编译生成的汇编程序中函数的返回结果是通过eax传递的,因此在反汇编中第17行将sum的值拷贝到eax中。

最后,我们再来看一下函数function执行完之后与其对应的堆栈帧是如何弹出堆栈的。反汇编中第21行的leave指令将堆栈帧指针 ebp拷贝到esp中,于是在堆栈帧中为局部变量buffer[14]sum分配的空间就被释放了;除此之外,leave指令还有一个功能,就是从堆栈中弹出一个机器字并将其存放到ebp中,这样ebp就被恢复为main函数的堆栈帧指针了。第22行的ret指令再次从堆栈中弹出一个机器字并将其存放到指令指针eip这样控制就返回到了第36main函数中的addl指令处addl指令将栈顶指针esp加上12,于是当初调用函数 function之前压入堆栈的三个实参所占用的堆栈空间也被释放掉了。至此,函数function的堆栈帧就被完全销毁了。前面刚刚提到过,在gcc译生成的汇编程序中通过eax传递函数的返回结果,因此反汇编中第38行将函数function的返回结果保存在了main函数的局部变量i中。

好了,讲完这些我想大家一定收获不小。因为内核漏洞中很多缓冲区溢出都是和这些知识相关的。


1 0
原创粉丝点击