打印本文 打印本文 关闭窗口 关闭窗口
缓冲区溢出的原理和实践(Phrack)
作者:武汉SEO闵涛  文章来源:敏韬网  点击数2164  更新时间:2009/4/22 23:33:33  文章录入:mintao  责任编辑:mintao
>        pushl $1
        call function
  
    以从后往前的顺序将function的三个参数压入栈中, 然后调用function(). 指令call
会把指令指针(IP)也压入栈中. 我们把这被保存的IP称为返回地址(RET). 在函数中所做
的第一件事情是例程的序幕工作:

        pushl %ebp
        movl %esp,%ebp
        subl $20,%esp

    将帧指针EBP压入栈中. 然后把当前的SP复制到EBP, 使其成为新的帧指针. 我们把这
个被保存的FP叫做SFP. 接下来将SP的值减小, 为局部变量保留空间.

    我们必须牢记:内存只能以字为单位寻址. 在这里一个字是4个字节, 32位. 因此5字节
的缓冲区会占用8个字节(2个字)的内存空间, 而10个字节的缓冲区会占用12个字节(3个字)
的内存空间. 这就是为什么SP要减掉20的原因. 这样我们就可以想象function()被调用时
堆栈的模样(每个空格代表一个字节):


内存低地址                                                         内存高地址
                                                              
           buffer2       buffer1   sfp   ret   a     b     c
<------   [            ][        ][    ][    ][    ][    ][    ]
           
堆栈顶部                                                            堆栈底部
                                                             

                                  
                                   缓冲区溢出
                                  ~~~~~~~~~~~~
    缓冲区溢出是向一个缓冲区填充超过它处理能力的数据所造成的结果. 如何利用这个
经常出现的编程错误来执行任意代码呢? 让我们来看看另一个例子:

example2.c
------------------------------------------------------------------------------
void function(char *str) {
   char buffer[16];

   strcpy(buffer,str);
}

void main() {
  char large_string[256];
  int i;

  for( i = 0; i < 255; i++)
    large_string[i] = 'A';

  function(large_string);
}
------------------------------------------------------------------------------
 
    这个程序的函数含有一个典型的内存缓冲区编码错误. 该函数没有进行边界检查就复
制提供的字符串, 错误地使用了strcpy()而没有使用strncpy(). 如果你运行这个程序就
会产生段错误. 让我们看看在调用函数时堆栈的模样:

内存低地址                                                         内存高地址

                  buffer            sfp   ret   *str
<------          [                ][    ][    ][    ]   

堆栈顶部                                                            堆栈底部

    这里发生了什么事? 为什么我们得到一个段错误? 答案很简单: strcpy()将*str的
内容(larger_string[])复制到buffer[]里, 直到在字符串中碰到一个空字符. 显然, 
buffer[]比*str小很多. buffer[]只有16个字节长, 而我们却试图向里面填入256个字节
的内容. 这意味着在buffer之后, 堆栈中250个字节全被覆盖. 包括SFP, RET, 甚至*str!
我们已经把large_string全都填成了A. A的十六进制值为0x41. 这意味着现在的返回地
址是0x41414141. 这已经在进程的地址空间之外了. 当函数返回时, 程序试图读取返回
地址的下一个指令, 此时我们就得到一个段错误.

    因此缓冲区溢出允许我们更改函数的返回地址. 这样我们就可以改变程序的执行流程.
现在回到第一个例子, 回忆当时堆栈的模样: 

内存低地址                                                         内存高地址
                                                              
           buffer2       buffer1   sfp   ret   a     b     c
<------   [            ][        ][    ][    ][    ][    ][    ]
           
堆栈顶部                                                            堆栈底部

    现在试着修改我们第一个例子, 让它可以覆盖返回地址, 而且使它可以执行任意代码.
堆栈中在buffer1[]之前的是SFP, SFP之前是返回地址. ret从buffer1[]的结尾算起是4个
字节.应该记住的是buffer1[]实际上是2个字即8个字节长. 因此返回地址从buffer1[]的开
头算起是12个字节. 我们会使用这种方法修改返回地址, 跳过函数调用后面的赋值语句
'x=1;', 为了做到这一点我们把返回地址加上8个字节. 代码看起来是这样的:

example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
   int *ret;

   ret = buffer1 + 12;
   (*ret) += 8;
}

void main() {
  int x;

  x = 0;
  function(1,2,3);
  x = 1;
  printf("%d\n",x);
}

上一页  [1] [2] 

打印本文 打印本文 关闭窗口 关闭窗口