一.实验题目及目的
1.实验题目
程序运行在linux环境中,从输入获得一个字符串,将这个字符串放入指定的buf处,buf的大小为32,需要分析栈帧、buf位置等信息,通过输入字符串使缓冲区溢出,完成指定的函数调用等目标。
2.实验目的
通过缓冲区溢出,熟悉函数调用的过程和栈帧结构的建立等。
二.实验内容
1.实验文件及实验准备
实验文件共有三个,bufbomb,hex2raw,makecookie。bufbomb是需要进行缓冲区溢出攻击的文件。实验过程需要用到userid和对应的cookie,cookie使用makecookie生成,hex2raw用于将16进制数据转换为字符串输入。
在实验中使用学号202004061409作为userid,cookie为0x73b099ff,并将bufbomb反汇编保存到文件中进行分析。
2.实验过程
getbuf函数如下,缓冲区大小为32,调用了Gets()。
(1)Level0:Candle
要求输入字符串使getbuf()不正常返回,而是执行smoke()函数.
getbuf()函数在test中调用。
getbuf的汇编代码如下:
getbuf中调用了Gets,并将缓冲区地址作为参数传入,根据getbuf的汇编代码,buf的起始位置为ebp-40的位置,而根据栈帧建立的规则,返回地址的位置在ebp+4,故只需要向缓冲区写入48个字节的内容,就可以覆盖修改返回地址,为了调用smoke()函数,将返回地址修改为smoke()的地址,即08048e0a。
因此输入的数据最后四个字节为0a 8e 04 08。0a会被当作\n,因此修改为smoke的第二条指令地址,即0a修改为0b。
新建文件输入16进制数据如下:
使用hex2raw将其转换为字符串数据输入,成功调用smoke()。
(2)Level1:Sparkler
Level1要求输入字符串后调用fizz()函数,并需要将cookie值作为参数传入,代码如下:
Fizz()的汇编代码如图所示,userid对应的cookie值位于0x804d104,传入的参数cookie在ebp+8的位置。需要找到fizz建立的栈帧的ebp的值。在getbuf返回前,执行了leave指令,leave指令等同于mov ebp,esp;pop ebp。然后执行ret指令进入fizz还会执行pop eip,因此在进入fizz()前,esp的值为原getbuf()函数的ebp+8。由于fizz不是由call指令调用的,没有返回地址入栈的过程,进入fizz()后,ebp入栈使esp的值-4,因此getbuf的ebp+8-4就是新的ebp的值,即新的ebp为getbuf的ebp+4。所以只要将getbuff的ebp +12的位置写入正确的cookie值,就可以向fizz()传入正确的cookie值。
因此向缓冲区写入56个字节的内容,45-48字节为fizz()的地址,53-56字节为0x73b099ff,新建文件,16进制数据如下:
转换为字符串输入,成功调用了fizz()并传入了正确的cookie值。
(3)Level2:Firecracker
这一次需要调用的是bang()函数,如下:
bang函数使用了全局变量,需要将全局变量global_val设置为cookie值。
需要在getbuf返回时先跳转到一段代码,在这段代码中设置global_val为cookie值,然后将bang的地址入栈,通过ret指令跳转执行bang()。最后的跳转使用ret是因为call和jmp指令使用PC相对寻址,难以设置正确。
bang()的汇编代码如下,global_value的地址为0x804d10c,需要把cookie值写入这个地址。
把cookie值写入global_value并跳转执行bang()需要执行以下指令:
将这段代码编译,再反汇编得到这段代码的机器码:
只需要将机器码写入buf,在getbuf返回时跳转到buf的起始位置,执行这些指令就可以了。buf的位置是esp-40,只能通过gdb调试找到运行时的位置。
gdb调试将断点打在0x804926b的位置,此时正在向Gets()传参,eax中的值为esp-40。这个值为0x556834e8。
同(1)中相同,向缓冲区写入48个字节的数据,开头为需要执行指令的机器码,,且机器码不需要考虑大小端,44-47字节为buf的首地址0x556834e8:
转换为字符输入,调用bang()成功。
(4)Level3:Dynamite
Level3需要完成的任务是在执行getbuf()后,将getbuf()的返回值修改为cookie值,并返回到test()函数,同时恢复被破坏的栈。
类似于level2,通过缓冲区溢出在getbuf()返回时跳转到一段代码,在这段代码中完成getbuf返回值的修改,栈的恢复,最后使用ret指令返回到test()当中。恢复到test()函数的栈帧,需要恢复ebp的值,在getbuf()返回前会在leave指令中pop ebp恢复旧的ebp值,但是这个值被复制进缓冲区的字符覆盖掉了,因此需要找到ebp的值并恢复。
修改返回值只需要将cookie值放入eax寄存器就可以了。恢复test()函数的ebp的值需要使用gdb调试找到,将断点设置在0x8048e40处,此处test()函数的栈帧已经建立。找到ebp的值为0x55683540。在指令中给ebp赋值或者在输入的字符串直接将这个值写入存放旧的ebp的位置都可以完成栈的恢复。
最后是使用ret指令返回test(),返回到call getbuf的下一条指令处0x8048e50。最终需要执行的汇编代码如下:
反汇编得到机器码如下:
最终输入的字符串的16进制形式如下:
转换为字符输入,结果如下:
(5)Level4:Nitroglycerin
本关与level3中任务相同,不同的是使用-n标志运行程序,会调用testn(),在testn()中调用getbufn()五次,且缓冲区的大小为512字节。每次getbufn()的栈空间随机,因此不可以再使用level3中的方式找到buf的起始位置,但栈帧结构是相同的,可以利用这一点找到buf的大致位置。
buf的地址
首先先找到buf的大致位置,确保可以跳转到这里执行需要执行的指令。
getbufn的汇编代码如下,buf的首地址是ebp-0x208。
接下来进入gdb调试,将断点打在0x8049247,此处getbufn的栈帧已经建立完毕:
五次调用getbufn,buf的首地址如下:
0x55683308
0x556832c8
0x55683318
0x55683308
0x55683358
因此可以找到buf首地址的大致范围0x556832c8-0x55683358。
改写getbufn的返回地址只能跳转到一个固定的地址,如何在跳转到固定地址后能够执行需要执行的指令,需要使用nop指令,利用一种nop sled的方式实现。nop sled就是在实际攻击代码前插入多条nop指令,nop指令只对PC+1,只要可以跳转到nop指令的部分,就可以一直执行到实际的攻击代码处并执行攻击代码。以上已经试探过了buf首地址的大致范围,选取首地址最大的0x55683358处,跳转到这里就可以保证进入缓冲区,然后在写入的数据中先插入nop指令,把攻击代码放到后面就可以了。
需要执行的代码
能够保证跳转到合适的位置,接下来就可以准备需要执行的代码了。需要完成的工作为修改getbufn的返回值,恢复testn的ebp,返回testn。其中修改返回值和返回testn与level3相同。
恢复testn的ebp可以通过观察testn与getbufn的栈帧情况进行处理。testn的部分汇编代码如下:
结合getbufn的汇编代码,可以得出getbufn的栈帧关系如下:
在getbufn返回前执行的leave指令将ebp的值赋给esp,然后弹出testn的ebp(已被覆盖),然后通过ret指令pop eip跳转执行指定的指令,esp+8。getbuf调用前后,esp的值是不变的。因此要恢复testn的ebp,只需要将esp+0x28赋值给ebp。
最终需要执行的代码如下:
编译后反汇编如下:
因此最终输入的数据如下,共528个字节:
由于getbufn执行5次,需要5次以上数据的输入,使用0a(\n)分隔。转换为字符后输入,目标完成。
三.实验总结
通过本次实验,加深了对缓冲区溢出的理解,并学习了解了缓冲区溢出攻击的方法,更加熟悉了函数调用时栈帧建立的过程和栈帧情况。其中最主要的是理解了缓冲区溢出攻击的方式,即通过溢出覆盖返回地址跳转到一段设定的程序进行执行,并可以通过恢复寄存器的方式恢复栈帧,以及应对动态堆栈的nop sleds方法。