缓冲区溢出原理分析(一)

缓冲区溢出

什么是缓冲区溢出???先作下类比,如果某个人把一瓶啤酒全部倒入一个小杯子中,那装不下的啤酒就会四处冒出,流到桌子上。

同样的道理,在计算机内部,输入数据通常被存放在一个临时空间内,这个临时存放空间就被称为缓冲区,缓冲区的长度事先已经被程序或者操作系统定义好了。缓冲区就很像那个啤酒杯,用来装东西,而且大小固定。

向缓冲区内填充数据,如果数据的长度很长(如同那瓶啤酒),超过了缓冲区(那个啤酒杯)本身的容量,那么结果就如同啤酒一样,四处溢出,数据也会溢出存储空间!装不下的啤酒会流到桌子上,而装不下的数据则会覆盖在合法数据上,这就是缓冲区和缓冲区溢出的道理。当然在理想的情况下,程序检查每个数据的长度,并且不允许超过缓冲区的长度大小,就像在倒啤酒的时候,啤酒要冒出杯子时我们就停止。但有些程序会假设数据长度总是与所分配的存储空间相匹配,而不作检查,从而为缓冲区溢出埋下隐患。OK,那我们如何利用缓冲区溢出呢在一般情况下,就像啤酒会到处流满桌面一样,溢出的数据会覆盖掉任何数据、指针或内容。除了破坏之外,对攻击者来说没有任何好处。但我们可引导溢出的数据,使计算机执行我们想要的命令。这就是很多漏洞公告上说的:‘黑客可以用精心构造的数据……

​ - > 摘选自 王炜老师的《Q版缓冲区溢出教程》

实验环境

操作机: windows XP

工具: DA pro , OllyDbg,vc++6.0;

测试软件:test1.exe, test2.exe

我们下面的实验是通过编写两个C例程序,使用工具OD,IDA pro进行逆向分析。针对汇编语句中CALL机制剖析缓冲区溢出的基本原理 。

实验准备:

编写C代码程序,编译

test1.c,代码如下:

#include<stdio.h> 
#include<string.h> //引入头文件

char name[] = "betao";  //定义全局变量

int main()            //返回值 主函数main()
{
    char buffer[8];   //开辟8个字节的空间用来存储变量name
    strcpy(buffer,name);  //内置函数(作用):将变量name内容赋值给buffer变量
    printf("%s\n",buffer);  //输出
    getchar();               //方便观察 作用:等待用户输入按键
    return 0;                //返回值
}

以上test1.c代码中,因为'betao'为超过buffer的空间,所以不会引起缓冲区溢出

test2.c代码如下:

#include<stdio.h> 
#include<string.h> //引入头文件

char name[] = "betaobetaobetao";  //定义全局变量,!!! 注意,这里多了两个betao

int main()            //返回值 主函数main()
{
    char buffer[8];   //开辟8个字节的空间用来存储变量name
    strcpy(buffer,name);  //内置函数(作用):将变量name内容赋值给buffer变量
    printf("%s\n",buffer);  //输出
    getchar();               //方便观察 作用:等待用户输入按键
    return 0;                //返回值
}

以上test2.c代码,因为copy给buffer超出了空间,所以导致缓冲区溢出

将C语言代码使用vc++6.0编程成可执行文件,大概步骤

新建win32 console application -> 新建 C++ 源文件 -> 编写代码 -> 编译(选择win32 Debug) -> 生成exe可执行文件

编译选择win32 debug操作如下:

右键点击菜单空白处——选择“组建”——选择“Win32 Debug“——重新编译链接

首先,我们先来运行一些编译好了的可执行文件test1.exe and test2.exe,看看两者区别

image-20191220211457759

第一个程序,没有任何异常

可见程序已经得到了正确的执行与输出。但是我在程序中所创建出来的是一个8字节长度的数组,而我在程序中的输入是5个字节。如果我的输入超过八个字节会怎么样呢?

接下来,我们执行第二个程序

image-20191220211551187

可见,程序虽然也能够正确输出,但是却弹出了错误提示对话框。为什么会出现这种情况?我们接下来就来研究一下。

我们首先分析一下无异常的程序,也就是test1.exe,这里我们使用ollydbg来分析

test1.exe载入ollydbg中,ollydbg载入方式有两种:

  • 鼠标直接将exe执行程序拖入OD控件区域
  • 文件 -> 附加 -> 选择运行中的程序

image-20191220212336225

因为我们需要从main函数开始分析,而上图:004015B0地址为软件初始运行地址,并非main函数的起始地址。所以我们需要寻找main函数的内存地址。我们使用 IDA Pro 打开程序寻找,当然我们可以不断地按F8单步执行,通过观察获取,但是这样未免需要一定的经验,而且也比较麻烦。所以我们将程序拖入IDA中

image-20191220213005763

如果不是图上的情况,而是图下的情况,按一下空格键,即可调出图上内容

image-20191220212914884

双击进入如下页面:

按下空格键

image-20191220213345908.png

可见,IDA已经帮我们获取了main函数的入口地址,即0x00401010 ,此时,我们可以切换到OD中,Ctrl + G 输入401010跳到该位置,按F2设置一个断点( 某个内存地址下了F2断点,当程序运行到此处的时候就会断下来 )

image-20191220214324018

由上面的截图,我们除了可以知道main函数的位置外,我们还从下面那段话“跳转来自 00401005”(英文版的是:jump from 00401005)得知main函数是由位于0x00401005位置处的语句跳过来的。由于缓冲区溢出是与栈空间紧密相关的,所以我们现在应当分析调用(CALL)main函数前后,栈空间的情况,所以这里我们就需要定位究竟是哪条语句调用了main函数。如果仅仅通过OD,我们是比较难定位的,所以这里我还是使用IDA Pro。

由于已经知道main函数的地址是0x00401010,那么我们在IDA中,用鼠标在该地址点一下,之后利用快捷键“Ctrl+X”打开“交叉引用窗口”,就来到了jmp到此的函数位置:

image-20191220214521642

image-20191220214544784

然后在0x00401005的地址处,再次利用“交叉引用”功能,我们就能够找到调用main函数的位置了:

image-20191220214634601

位于 0x00401694处的语句调用了main函数,那么接下来我们在OD中来分析堆栈的情况

继续Ctrl + G 输入 401694 跳转到执行main函数的内存地址,F2设置断点

image-20191220215539618

注意上图中 红色箭头和黄色箭头的地方

​ 红色箭头 CALL语句下面的地址是0x00401699。这个地址很重要,为什么重要呢?因为我们的程序在进入每一个CALL之前,都会首先将CALL下面那条语句的地址入栈,然后再执行CALL语句。这样当CALL执行完后,程序再将该地址出栈,这样就能够知道下一步应该执行哪条指令. 我们一般也将这个地址称为“返回地址”,它告诉程序:“CALL执行完后,请执行这个地址处的语句。”

我们按下F9,跳转到此位置,查看栈中内容:

image-20191220215928730

我们接下来按F7,进入这个CALL,此时再看一下栈空间(注:栈空间由下至上是高地址往低地址处走的 )

image-20191220220111419

是上上图CALL语句下一个内存地址哦,由此可见,返回地址0x00401669已经入栈 .

分析main函数的缓冲区情况

因为我们在源程序中创建了一个8个字节大小的数组,因此进入main函数后的首要工作就是为这个局部变量分配空间。由于我们的程序是以Debug形式编译的,所以它会多分配一些空间(Release版本则会分配正好的空间)。结合本程序可以看到,它为我们分配的局部变量空间大小为0x4C(7C - 34)->(F8一步步向下执行):

image-20191220220929691

image-20191220221935424

在上图中,比较重要的是最后两行。其中最后一行在之前已经说过了,是非常重要的返回地址,它决定了当main函数执行完毕后,程序所要执行的语句的地址,而倒数第二行是父函数的EBP 。

再往上,就是我们的main函数的局部变量空间。这里大家可能会有疑惑,既然是分配给我们的空间,那么为什么还会有其它的数据呢? 我们继续往下执行 ,当我们执行完0x401026

image-20191220222344195image-20191220222551168

0X4C的空间大小(7C - 34), 这段空间都被0xCC填充了。这是因为程序为了容错性与保持自身的健壮性,于是利用0xCC,即int 3断点来填充满这段区域,这样一来,如果有未知的程序跳到这片区域,就不会出现崩溃的情况,而是直接断下来了。当然,这个问题与我们的缓冲区溢出没什么关系。

继续执行:

image-20191220222811127

image-20191220222849625

这里将betao入栈了,说明执行到了strcpy(),这个函数,我们通过IDA来查看一下,strcpy()函数的位置

image-20191220223306866

image-20191220223344255

继续执行,查看堆栈中的情况

image-20191220223433751

发现strcpy函数的第二个参数,也就是接收字符串所保存的位置,其保存的位置为0X0012FF78(buffer变量)

到这里程序无异常,正常执行,继续F8,直至retn

image-20191220223754120

image-20191220223820151

这个地址是不是很熟悉呢?没错就是返回地址

对溢出程序分析

其余跟上面步骤类似,我们直接跳至执行strcpy函数的内存地址分析

image-20191220224251689

继续执行,执行至retn,查看堆栈此时的情况:

image-20191220224431503

我们右键显示一下ascii码:

image-20191220224531935

由上图可知: 原本返回值:0x00401699地址被占用 且指向错误的地址(或不应该的地址)

F8执行,结果如图

image-20191220224744720

此时我们发现了两件事,一件是OD中的反汇编代码窗口是空的,说明0x006F6174地址处不存在指令,或者说它就是一个无效地址。第二件事是OD弹出了错误对话框,提示我们该地址出错,这与我们直接执行程序时所弹出的错误对话框有几分类似。

这里先补充一下,为什么返回地址显示的是tao,而不是aob,因为buffer 空间只有8个字节,而我输入了15个字节,buffer后面的四个字节是ebp的地址.

betaobet    aobe    tao
拷贝字符      EBP     覆盖内容

补充:

我们也可以通过执行test2.exe程序

image-20191220230136615

image-20191220230205983

image-20191220230259576

总结:

缓冲区漏洞的原理,它就是因为我们输入了过长的字符,而缓冲区本身又没有有效的验证机制,导致过长的字符将返回地址覆盖掉了,当我们的函数需要返回的时候,由于此时的返回地址是一个无效地址,因此导致程序出错。

那么依据这个原理,假设我们所覆盖的返回地址是一个有效地址,而在该地址处又包含着有效的指令,那么我们的系统就会毫不犹豫地跳到该地址处去执行指令。因此,如果想利用缓冲区溢出的漏洞,我们就可以构造出一个有效地址出来,然后将我们想让计算机执行的代码写入该地址,这样一来,我们就通过程序的漏洞,让计算机执行了我们自己编写的程序。

本文例子参考来源: https://blog.csdn.net/ioio_jy/article/details/48316029

本文链接:

https://www.betao.cn/archives/bf01.html
1 + 9 =
快来做第一个评论的人吧~