缓冲区溢出攻击实验

缓冲区溢出攻击实验

0x00 示例代码

#include <stdio.h>
void mgets(char* buf){
	char* temp=buf;
	char c;
	while((c=getchar())!='\n'&&c!=EOF){
		*temp++=c;
	}
	*temp++='\0';
	return;
}
int main(){
	char buf[8];
	mgets(buf);
	printf("This string can't be displayed normally!");
	return 0;
}

0x01 代码分析

​ main函数里分配了8个字节的buf缓冲区,然后调用mgets过程,往缓冲区里写入用户的输入。但是mgets里没有对写入缓冲区的大小进行限制,导致写入越界。

​ 由于buf是局部变量,分配在栈里,而栈是向下扩展的,所以buf在 上一个栈帧里记录调用main函数后返回的地址 的下方,然后随着不断往缓冲区buf里写数据,会向上覆盖该返回地址。导致main函数返回时跳转到攻击者写入的地址,改变了程序的行为。

0x02 具体操作

​ gcc默认开启栈破坏检测机制,并且默认设置了栈不可执行,来防止程序的缓冲区溢出漏洞,因此为了实验我们应该关闭。

​ 使用下面命令编译上面的代码

gcc -fno-stack-protector -z execstack 1.c -o 1.out

​ 生成可执行文件1.out

​ 我们要写入的新的跳转地址,因此最好栈和代码的位置不变动,这样我们才能知道要跳转的地址,因此要关闭操作系统的ASLR(adress space layout randominaztion)

​ 使用下面命令关闭ASLR

sudo echo 0 > /proc/sys/kernel/randomize_va_space

​ 使用 objdump -d 1.out 命令 观察main函数的栈帧分配:

image-20200805171144958

​ 可以看到,先push 保存了%rbp,然后给buf实际分配了16字节。那么从buf[0]到上一个栈帧最后写入的返回地址之间有24字节空间,我们需要先输入16字节,填满给buf分配的空间,再写入8字节,作为%rbp的值,需要注意的是,这个值不能任意写,因为最后main里的leave指令会把%rbp传送到%rsp,因此这个值应该能被访问,最好和新的返回地址一致,然后再写入8字节作为新的返回地址,最后输入EOF或者 换行符0x0a作为输入结束。

​ 在这里,我让新地址为printf的语句。为了知道printf语句运行时的地址,我们使用gdb调试一下。

​ 用gdb 1.out对其进行调试,然后输入break main,对main函数下断点,再用run指令,此时在main函数断下,再用disas,反汇编当前函数的代码。

image-20200805172304047

​ 可以发现,和objdump -d 的地址一致都是0x00000000004005df。那么我们需要输入的数据如下:

image-20200805172459672

​ 一共33字节16+8+8+1,前面16字节随意,中间8字节是%rbp,最后8字节是新地址我们填写0x00000000004005df,注意字节序,最后一个是换行符,表示输入结束。

​ 我们当然不可能直接执行1.out再从终端输入,因为有些数值对应的ascii码无法从键盘打出,因此我们用python生成二进制文件,最后用重定向输入。

​ python代码如下:

import struct
list_d=[0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x30,0x31,0x32,0x33,0x34,0x35,0x36,0xdf,0x05,0x40,0x00,0x00,0x00,0x00,0x00,0xdf,0x05,0x40,0x00,0x00,0x00,0x00,0x00,0x0a]
with open('data.bin','wb') as fp:
        for x in list_d:
                a=struct.pack('B',x)
                fp.write(a)
print('done')

​ 重定向输入命令执行1.out

./1.out < data.bin

​ 提示segmentation fault

image-20200805173414161

​ 但是用gdb调试执行,发现已经成功,在mian函数执行retq时,到了0x4005df,之所以segmentation fault是因为还会继续运行到main函数里的leave和retq,但是这个时候栈里的数据可能不对了。

​ 但是我们的实验目的已经达到了,完成了更改程序的行为(虽然结果不直观)。