CSAPP-buflab
buflab
实验目的
详细了解IA-32调用惯例和堆栈结构。它涉及对lab目录中的可执行文件bufbomb应用一系列缓冲区溢出攻击。
实验环境和工具
ubuntu 12.04.5 (32位) ;
gdb 7.4 ;
实验内容及操作步骤
准备工作
阅读Readme.txt和buflab-writeup.pdf的前几页
按照Readme.txt的要求,任意输入一个字符作为userid。使用makecookie生成cookie。我这里输入的字符为h,得到cookie:0x20083f2f
。
在linux下解压buflab-handout.tar.gz
Level 0: Candle (10 pts)
1 |
|
目标:
让BUFBOMB在getbuf
执行其return
语句时执行smoke
的代码,而不是返回test
。
注意:利用漏洞字符串还可能损坏堆栈中与此阶段不直接相关的部分,但这不会导致问题,因为冒烟会导致程序直接退出。
分析:
getbuf()
的反汇编代码:
1 |
|
-
由
lea -0x28(%ebp),%eax
和mov %eax,(%esp)
可知,ebp-0x28
的地址为Gets()
函数的参数。Gets()
将以该地址为起点向地址增大的方向保存字符。getbuf()
的部分栈帧示意图如下: -
因此需要将
getbuf()
的返回地址覆盖为smoke()第一条语句的地址
。smoke()
的汇编代码如下:1
2
3
4
5
6
7
8
9
10
11
12Dump of assembler code for function smoke:
0x08048e0a <+0>: push %ebp
0x08048e0b <+1>: mov %esp,%ebp
0x08048e0d <+3>: sub $0x18,%esp
0x08048e10 <+6>: movl $0x804a2fe,0x4(%esp)
0x08048e18 <+14>: movl $0x1,(%esp)
0x08048e1f <+21>: call 0x8048990 <__printf_chk@plt>
0x08048e24 <+26>: movl $0x0,(%esp)
0x08048e2b <+33>: call 0x8049280 <validate>
0x08048e30 <+38>: movl $0x0,(%esp)
0x08048e37 <+45>: call 0x80488d0 <exit@plt>
End of assembler dump.首地址为
0x08048e0a
。由于0x0a
为'\n'
,故选用0x08048e0b
注入。构造的字符串为(40+4)个字符(除了0x0a以外的任意字符),再加上
0b 8e 04 08
(小端法)。txt文件如下:
结果:
Level 1: Sparkler (10 pts)
1 |
|
目标:
与Level 0类似,让BUFBOMB
执行fizz
的代码,而不是返回test
。但是,您必须使它看起来像fizz,就好像传递了cookie作为它的参数。
分析:
fizz
的反汇编代码:
1 |
|
-
由
mov 0x8(%ebp),%eax
可知,此时ebp+0x8
的地址保存的是fizz()
的参数。其余类似Level0。更改getbuf()
的返回地址为fizz()
的入口地址后,进入fizz()
前的部分栈帧示意图如下。进入fizz()后,esp的值加4,之后push %ebp
,esp的值减4,再由mov %esp,%ebp
,我们可以确定fizz()
的参数的地址。 -
构造字符串为(40+4)个字符(除了0x0a以外的任意字符),加上
af 8d 04 08
(fizz()
的入口地址,小端法),再加上4个字符
(除了0x0a以外的任意字符),最后加上cookie:2f 3f 08 20
(小端法)。txt文件如下:
结果:
Level 2: Firecracker (15 pts)
1 |
|
目标:
与级别0和1类似,让BUFBOMB
执行bang
的代码,而不是返回test
。但在此之前,必须将全局变量global_value
设置为用户id的cookie
。攻击代码应该设置全局变量,将bang
的地址推送到堆栈上,然后执行ret
指令以跳转到bang
的代码。
分析:
bang
的反汇编代码:
1 |
|
- 查看地址0x804d10c的值和0x804d104的值,得到它们分别为
global_value
和cookie
的值。
-
而
getbuf
中的ebp
位置为0x55683610
(如下图),显然无法直接覆盖。 -
故可以编写汇编代码,然后把它们转换为字符编码放入堆栈中,以完成需要的操作。汇编代码如下:
1
2
3
4
5/*bang.s*/
mov 0x804d104,%eax /*将cookie保存到eax*/
mov %eax,0x804d10c /*将global_value设置为cookie的值*/
push $0x08048d52 /*bang的函数入口地址入栈*/
ret /*返回,进入bang函数*/ -
通过指令将.s文件编译为.o文件,查看反汇编代码,共16个字节:
-
我们可以把这段代码从
buf
的起始位置开始存放,而把getbuf
的返回地址更改为buf
的起始地址,以执行这段代码。经调试getbuf
,buf
的起始地址为:0x556835b8
。 -
更改后的栈帧示意图如下:
-
故注入的字符串为代码的字符编码(共16个字节)+28个字节(除了0x0a以外的任意字符)+
b8 35 68 55
(buf的起始地址的小端法表示)。txt文件如下:
结果:
Level 3: Dynamite (20 pts)
test
的c代码如下:
1 |
|
test
的汇编代码如下:
1 |
|
目标:
提供一个漏洞字符串,该字符串将导致getbuf
将cookie
返回到test
,而不是值1。可以在test
的代码中看到,这将导致程序运行“Boom!”。
漏洞字符串将cookie
设置为返回值的同时,应恢复任何损坏的状态,在堆栈上设定正确的返回地址,并执行ret
指令以真正返回test
。
分析:
-
getbuf
的返回值保存在eax
中,故注入的字符串应执行操作将getbuf中eax
的值设为cookie
的值。同时返回到test
的call 0x8049262 <getbuf>
之后的位置,同时注入buf
时应让保存的旧ebp
保持原值不变。 -
编写汇编代码如下:
1
2
3mov $0x20083f2f,%eax /*将cookie的值保存在eax中*/
push $0x08048e50 /*test的call <getbuf>之后的地址入栈*/
ret /*返回*/ -
输入指令,反汇编得机器码如下,共11个字节:
-
我们可以把这段代码从
buf
的起始位置开始存放,而把getbuf
的返回地址更改为buf
的起始地址,以执行这段代码。与Level 2一样,buf
的起始地址为:0x556835b8
。 -
同时旧
ebp
应保持原值不变,调试查看得getbuf
保存的ebp
的值为0x55683610
-
故更改后的
getbuf
的部分栈帧如下: -
故注入的字符串为代码的字符编码(共11个字节)+29个字节(除了0x0a以外的任意字符)+
10 36 68 55
(原ebp的值,小端法表示)+b8 35 68 55
(buf的起始地址的小端法表示)。txt文件如下:
结果:
Level 4: Nitroglycerin (10 pts)
testn
的汇编代码如下
1 |
|
目标:
在Nitro模式下运行时,BUFBOMB要求提供字符串5次,它将执行getbufn
5次,每次都有不同的堆栈偏移量。
与Level3相同,Level4要求提供一个漏洞字符串,该字符串将导致getbufn
将cookie
返回到testn
,而不是值1。可以在testn
的代码中看到,这将导致程序进入“KABOOM!”。攻击代码需要将cookie
设置为返回值,同时应恢复任何损坏的状态,在堆栈上设定正确的返回地址,并执行ret
指令以真正返回testn
。
分析:
getbufn
的汇编代码如下:
1 |
|
-
ebp-0x208
的地址为Gets()
函数的参数。Gets()
将以该地址为起点向地址增大的方向保存字符。 -
通过调试,观察每次执行
testn
时的ebp
,以及对应的getbufn
的ebp
的变化。 -
观察到
testn
的ebp
是变化的,最大值为0x55683680
,最小值为0x556835a0
,差值为0xE0(224)。getbufn
的ebp
同样是变化的,最大值为0x55683650
,最小值为0x55683570
。对应的buf
起始地址最大值为0x55683448
,最小值为0x55683368
。由于我们注入的返回地址是固定的,故我们注入的返回地址须不小于0x55683468
,否则可能出现buf
覆盖的地址都大于设定的返回地址,从返回地址向高地址执行命令时执行了未知命令的情况。 -
类似于Level3,我们从更改后的返回地址开始执行指令。由于设定的返回地址不小于
0x55683448
,当buf
的起始地址小于设定的返回地址时,就需要想办法使注入的攻击代码出现在返回地址的高处。我们就设定返回地址为0x55683448
,则buf
起始地址的最小值相差了224个字节,这就需要至少填充224个字节的nop指令(nop指令只使程序计数器加1),从而在任何情况下都能使CPU将指令至少执行到注入的攻击代码(若填充00,则CPU无法识别,无法进行后续操作)。 -
而
testn
的ebp
是不断的变化的,无法像Level3一样在内存中注入固定的值恢复保存的ebp
。但我们可以找到getbufn
的ebp
与testn
的ebp
的关系,即前者比后者小了0x30
。我们的攻击代码是在getbufn
的leave
、ret
指令之后执行的。在这两次指令后,esp
的值变为getbufn
的ebp
+0x8
,而本身的ebp
变为保存的ebp
的值(但被buf溢出覆盖)。故此时,我们可以根据这个关系:testn
的ebp
=esp
+0x28
编写注入的代码。 -
注入的代码如下:
1
2
3
4mov $0x20083f2f,%eax /*将cookie的值保存在eax中*/
lea 0x28(%esp),%ebp /*恢复保存的ebp的值*/
push $0x08048ce2 /*testn的call <getbuf>之后的地址入栈*/
ret /*返回*/ -
输入指令,反汇编得机器码如下,共15个字节:
-
可以得到栈帧的示意图:
-
getbufn
的ebp-0x208为buf的起始地址,0x208为520。故注入的字符串为509个nop(0x90)+15个字节的攻击代码+48 34 68 55
(修改的返回地址,小端法表示)。txt文件如下:
结果:
实验总结
-
这次实验的难度随级别的提高而增加,引导我们如何利用缓冲区存在的漏洞实现一些目的:
Level0:利用直接覆盖返回地址,在调用函数getbuf时直接返回smoke函数,让我们初步认识缓冲区溢出攻击的原理。
Level1:在Level0的基础上,多了修改函数参数的操作,这需要我们结合汇编代码找到参数的位置。
Level2:开始需要我们自己编写汇编代码段去实现操作:修改返回值、设置全局变量、跳转。同时也需要利用缓冲区溢出,跳转至这段代码的起始地址。
Level3:同时利用自己编写的代码设置返回值并返回至test函数,需要覆盖buf时要保持函数保存的旧ebp不变。
Level4:每次调用getbufn的目的与Level3一致,不同的是它的ebp不断变化,需要找到等式关系去编写代码以修正而ebp。难点还在于多次调用使栈基址随机化,这需要利用弄nop_sled的技术。
通过学习、理解如何实现缓冲区溢出攻击,我对函数调用、栈帧空间的分配、nop_sled的使用等相关知识有了更加深刻的理解。
-
在实验的部分地方需要对运行过程进行调试,查看某个寄存器的值及其变化。所以gdb工具的使用是不可或缺的。通过完成这次实验,我对gdb工具的使用更加熟练。
-
进行实验,细心和耐心也是很重要的品质。有时候会因为不够细心而耽误时间,如Level4中我因为看错了ebp的值,使得第一次尝试没有通过,但好在能够通过调试发现错误之处,并加以改正。有了细心和耐心的加持,才能更好地完成一个个实验,收获知识,提升技能。
本博客所有文章除特别声明外,均为博客作者本人编写整理,转载请联系作者!