Lab2 缓冲炸弹
一 问题描述
本题利用程序留出的输入缓冲区,将输入数据转换成字符串存放在栈区中,当输入字符串长度大于预留的缓冲大小时,多出的部分将覆盖栈帧中的其他内容,从而达到改变程序运行顺序,甚至添加其他程序修改数据等目的。
二 解题思路
1、 level 0
实验要求:改变程序的原始执行顺序,使程序在执行完getbuf后返回到smoke函数处运行。
解决本题,重要的是理解栈帧的形成,以及栈区内存的组织结构,增长方式等。
观察汇编代码,可以发现,该字符串的长度为24字节,与栈基地址的偏移为0x18,而%ebp+4位置存放着上一栈帧的基地址,%ebp+8的位置存放着函数的返回地址,知道了函数的返回地址位置就好办了,我们只需要覆盖此处放入目标跳转函数的地址,程序就会自动跳转。
查看汇编代码,找到smoke函数的起始地址为0x8048f95。我们只要将这个地址放在%ebp+8位置处即可,其它地方的数据不影响程序的执行。总共需要输入32字节的数据,考虑到PC都是小段机,后面4个字节数据为95 f8 04 08,前28字节的数据任意即可。由此,得到我们的输入数据:
0011223344556677889900112233445566778899001122334455667795f80408
使用实验提供的sendstring工具将字节码转换成字符串,作为getbuf的输入数据,通过测试。
2、 level 1
实验要求:与实验1大同小异,都是让getbuf()的调用者test()执行一个代码里未调用的函数,实验2中是fizz()函数。并且传入我们的cookie作为参数,让fizz()打印出来。
高地址 |
本函数参数(cookie) |
返回地址 |
保存的ebp |
局部变量(buffer) |
调用其他函数参数建立区域 |
低地址 |
从fizz函数的汇编代码里,我们可以看出:cookie存放的位置应该是(ebp-0x8),以及fizz函数的首地址为:08048f3d;通过栈帧图,可以清楚的看出,我们要做的就是:
①修改参数1的值为我们的cookie;
②把getbuf返回地址修改为fizz函数首地址,达到函数跳转的目的.
那么,同第一题,先对文件写入24字节的数组内容;然后写入4字节ebp(任意,不影响结果);再写入getbuf返回地址,显然返回地址应该写入fizz函数的首地址:3d 8f04 08;然后写入fizz的返回地址(因为只要求我们调用fizz函数即可,所以fizz的返回地址为任意);最后四个字节就是我们的cookie啦.我的cookie应该输入:cb 5e f0 55.
综上,可以得到我的字节码:
通过测试,结果如下。
3、 level 2
实验要求:让getbuf()返回到bang()而非test(),并且在执行bang()之前将global_value的值修改为cookie。
首先我们观察bang的汇编代码,可以推算出cookie的地址为0x804a1e0,global_value的地址为0x804a1d0,我们要做的就是将0x804d1d0内存的值修改为我们的cookie值,显然该地址和栈区相距很远,使用覆盖的方式无法完成任务,但是我们可以通过在栈区放入指令,实现对制定内存地址内容修改的目的。
首先,编写汇编代码。
|
mov 0x804a1e0, %eax # 将cookie放入%eax mov %eax, 0x804a1d0 # 将%eax放入global_value push $0x8048ee4 # 将bang的其实地址压栈 ret # 返回 |
通过下列指令编译我们写的汇编代码,再执行反汇编获得机器码。(该过程也可以自行计算,这样操作更简洁)
gcc -m32 -c 2.s # 2.s是上述机器代码
objdump -d 2.o # 反汇编获取机器码
这样,我们就可以得到汇编代码对应的机器码如下。
我们接下来看一下栈帧的情况。
高地址 |
本函数参数(cookie) |
返回地址 |
保存的ebp |
局部变量(buffer) |
调用其他函数参数建立区域 |
低地址 |
此时我们要做的事如下:
① 将上述机器码放到指定位置(我们直接放到buffer首地址)。
② 把getbuf返回地址修改为上述机器码首地址,达到函数跳转的目的;
问题转化为寻找buffer的首地址,使用gdb在0x8048ba6处设置断点,观察%eax的值为0x55587478,该地址即为字符串的起始地址。
于是可以得到我们的字节码,如下所示。
运行结果如下,通过测试。
4、 level 3
实验要求:将getbuf()的返回值修改为我们的cookie,并返回到调用者test()中。
首先我们需要理清test函数的执行流程,才能搞清楚我们的任务。程序先调用test函数,然后在test函数中调用getbuf后返回test函数(0x8048e92位置),同时修改getbuf返回值为cookie。同时应该考虑到,如果仅仅此刻返回,那么%ebp的值很可能已经被覆盖或者改变,我们必须要确保getbuf在返回之前%ebp的值已经调整为test原来的%ebp,test原来的%ebp使用gdb很容易可以看到为0x555874b0,那么明确我们的任务如下:
① 将getbuf的返回值修改为cookie;
② 返回test地址为0x8048e92的地方;
③ 修改%ebp的返回值为0x555874b0;
④ 返回指定的内存区域。
任务已经明确,第一个任务需要修改%eax寄存器的内容,只能通过插入机器码的方式完成;第二个任务执行必须在第一个任务之后,那么插入机器码将是更为行之有效的方法。第三个任务的完成使用插入机器码或者在程序从getbuf跳转到我们的机器码之前均可完成。任务四在上面几道题中已经反复讲述。下面将提供这两种不同的解题。
解题1:任务3在getbuf返回之前完成。
编写机器码如下:
|
mov 0x804a1e0, %eax # 任务1 push $0x8048e92 # 任务2 ret |
通过下列指令编译我们写的汇编代码,再执行反汇编获得机器码。(该过程也可以自行计算,这样操作更简洁)
gcc -m32 -c 3-1.s # 3-1.s是上述机器代码
objdump –d 3-1.o # 反汇编获取机器码
这样,我们就可以得到汇编代码对应的机器码如下。
接下来,就是组织我们的字节码,我们还是先来看一下栈帧的结构。我们的字节码需要从buffer首地址处开始,直到覆盖到返回地址,中间同时覆盖了%ebp,我们只需要将%ebp的位置修改为0x555874b0即可,同样需要注意到小端机的问题。
高地址 |
本函数参数(cookie) |
返回地址 |
保存的ebp |
局部变量(buffer) |
调用其他函数参数建立区域 |
低地址 |
字节码如下:
注:此处解释一下为什么程序代码的字节码不需要像返回地址和%ebp那样倒着写。I386所有指令的第一个字节隐含着指令的长度,在读取指令的时候也是每次只读取一个字节,然后依次分析该字节的内容,而不是像返回地址一样一次读取4个字节的长度,因此程序代码不需要像返回地址和%ebp那样倒着写。
按照前面题目的操作将该字节码写入栈区,可以成功完成实验。
解题2:任务3在代码中完成。
编写机器码如下:
|
mov 0x804a1e0, %eax # 任务1 mov $0x555874b0, %ebp # 任务3 push $0x8048e92 # 任务 2 ret |
通过下列指令编译我们写的汇编代码,再执行反汇编获得机器码。(该过程也可以自行计算,这样操作更简洁)
gcc -m32 -c3.s # 3.s是上述机器代码
objdump –d3.o # 反汇编获取机器码
这样,我们就可以得到汇编代码对应的机器码如下。
然后组织我们的字节码如下:
按照前面题目的操作将该字节码写入栈区,可以成功完成实验。
5、 level 4
实验要求:用bufbomb的-n参数进入Level 4模式,此时程序不会调用getbuf()而是其升级版getbufn()。getbufn()的调用者会使用alloca库函数随机分配栈空间,然后连续调用getbufn()五次。我们的任务是保证getbufn()每次都返回我们的cookie而不是1。
本题乍一看和level 3极其相似,重点在于该测试函数会连续调用getbufn()五次,而我们的字节码需要连续五次满足题目需求,每次调用getbufn,题目会通过alloc随机分配栈空间,那就意味着每次的buffer起始地址可能是不一样的,从上面的题目我们知道,我们设定的getbuf返回地址都是buffer起始地址,而这个地址此刻又是动态且不固定的,那么我们将难以找到一个固定的静态地址入口。但是我们又只能通过内存覆盖的方式改变函数返回地址,这个地址又必须是一个静态地址。这两者之间看似出现了冲突。
但是,有一个东西是不变的,testn的栈区是不变的,5次调用getbufn的栈空间位置也是不变的,变化的只是buffer的起始地址,而且从下面的汇编代码中可以发现,getbufn的栈空间大小为280个字节,而字符串buffer则需要占用264个字节。
现在明确我们的任务如下:
① 将getbufn的返回值修改为cookie;
② 返回testn中getbufn调用后的下一条语句
③ 恢复%ebp寄存器的内容,原来的被我们覆盖;
④ 返回指定的内存区域。
第一个任务和第二个任务之前已经完成过,此处不再赘述。第三个任务与之前略有不同,之前题目中test的%ebp可以通过覆盖正确的%ebp完成,但是此时我们无法准确推算%ebp的位置,因此只能通过代码的方式完成。
编写汇编代码如下:
|
mov 0x804a1e0, %eax # 将cookie赋给eax作为getbuf返回值 lea 0x18(%esp), %ebp # 将ebp寄存器的内容恢复(原来被我们覆盖) push $0x8048e22 # testn中getbufn调用后的下一条语句 ret # 返回 |
通过下列指令编译我们写的汇编代码,再执行反汇编获得机器码。(该过程也可以自行计算,这样操作更简洁)
gcc -m32 -c4.s # 4.s是上述机器代码
objdump –d4.o # 反汇编获取机器码
这样,我们就可以得到汇编代码对应的机器码如下。
解决了操作的机器码,我们还需要解决一个问题. 因为我们要覆盖掉原来的数据,我把这个函数的栈帧情况画一下:
高地址 | |
Getbufn返回地址 | |
存放ebp | |
栈内容区(280字节) | 未知区域(大小不定) |
Buffer区(214字节) | |
调用其他函数参数建立区域(0字节) | |
低地址 | |
帧指针申请了264的空间存放buffern,然后是4字节覆盖ebp内容,然后是4位getbufn返回地址(我们应该调整为buffern的首地址呢,这样就可以执行我们上面反汇编出来的机器码了).最后我们要解决的问题就是怎么才能拿到buf的首地址?
虽然buffern首地址可能会发生改变,但是其变化也是有范围的,即buffern首地址最高为0x55587358(%ebp – 0x108),最低为0x55587348(%ebp –0x118),我们完全可以直接将字节码放在最高开始位置(字符串的存放从低地址开始,向高地址方向增长),可以保证5次返回都不会轮空。因此我们取函数返回地址为0x55587358,这样无论什么情况下,程序都能执行到此处。
这样组织我们的字节码如下:
将上述字节码转换成字符串,运行程序,可以通过测试。
事实上,这道题有些虎头蛇尾,解题过程中,只需要把我们的汇编代码放在字符串48字节以后的位置,返回地址直接使用下图中使用gdb观察到的地址都可以完成任务。