CSAPP--BOMBLAB实验
本实验要求你使用课程所学知识拆除“binary bombs”,增强对程序的机器级表示、汇编语言、调试器和逆向工程等方面原理与技能的掌握。一个“binary bombs”(二进制炸弹,下文将简称为炸弹)是一个Linux可执行程序,包含了6个阶段(或层次、关卡)。炸弹运行的每个阶段要求你输入一个特定字符串,你的输入符合程序预期的输入,该阶段的炸弹就被拆除引信即解除了,否则炸弹“爆炸”打印输出 "BOO
目录
一、bomblab介绍
1.简介
本实验要求你使用课程所学知识拆除“binary bombs”,增强对程序的机器级表示、汇编语言、调试器和逆向工程等方面原理与技能的掌握。 一个“binary bombs”(二进制炸弹,下文将简称为炸弹)是一个Linux可执行程序,包含了6个阶段(或层次、关卡)。炸弹运行的每个阶段要求你输入一个特定字符串,你的输入符合程序预期的输入,该阶段的炸弹就被拆除引信即解除了,否则炸弹“爆炸”打印输出 "BOOM!!!"。实验的目标是拆除尽可能多的炸弹层次。 每个炸弹阶段考察了机器级程序语言的一个不同方面,难度逐级递增:
阶段1:字符串比较
阶段2:循环
阶段3:条件/分支
阶段4:递归调用和栈
阶段5:指针
阶段6:链表/指针/结构
另外还有一个隐藏阶段,只有当你在第4阶段的解后附加一特定字符串后才会出现。
为完成二进制炸弹拆除任务,你需要使用gdb调试器和objdump来反汇编炸弹的可执行文件并跟踪调试每一阶段的机器代码,从中理解每一汇编语言代码的行为或作用,进而设法推断拆除炸弹所需的目标字符串。比如在每一阶段的开始代码前和引爆炸弹的函数前设置断点。
实验语言:C;实验环境:Linux
2. 实验步骤
2.1. 第一步:获取bomb
在远程桌面的浏览器中打开 http://172.16.2.207:15213 (或打开桌面上的Bomblab Download Page),在二进制炸弹请求表格中输入你的学号和邮箱地址,点击Submit按钮。服务器会构造属于你的炸弹,并以tar文件的形式bombXXX.tar返回给你,其中XXX是一个你的bomb的唯一标识。
解压该tar文件(tar -xvf bombX.tar)得到一个目录./bombXXX,其中包含如下文件:
README:标识该bomb和所有者。
bomb:bomb的可执行程序。
bomb.c:bomb程序的main函数。
2.2. 第二步:拆除bomb
本实验的任务就是拆除炸弹。一定要在指定的虚拟机上完成作业,在其他的环境上运行有可能导致失败。
运行./bomb可执行程序需要0或1个命令行参数(详见bomb.c源文件中的main()函数)。如果运行时不指定参数,则该程序打印出欢迎信息后,期望你按行输入每一阶段用来拆除炸弹的字符串,根据你当前输入的字符串决定你是通过相应阶段还是炸弹爆炸导致任务失败。你也可将拆除每一阶段炸弹的字符串按行组织在一个文本文件中,然后作为运行程序时的唯一一个命令行参数传给程序,程序读入文件中的每一行直到遇到EOF,再转到从stdin等待输入。这样对于你已经拆除的炸弹,就不用每次都重新输入,只用放进文件里即可。
前四个阶段每个10分,第五和第六阶段更难一些,每个15分,满分70分。每输入错误一次,炸弹爆炸,会扣除0.5分(最多扣除20分)。所以你必须小心!要学会单步跟踪调试汇编代码以及学会设置断点。你还要学会如何检查寄存器和内存状态。很好的使用调试器是你在未来的职业生涯中赚到更多money的一项重要技能!
二、工具使用
Objdump:https://blog.csdn.net/qq_41683305/article/details/105375214
三、拆解分析
3.1 phase_1 考察字符串比较
按照顺序对汇编代码进行解读,可以发现
这四句代码先为变量存储开栈
将地址$402520写入%esi寄存器
随后调用strings_not_equal函数,
调用结束后检测返回值(%eax即返回值存放的寄存器)
所以strings_not_equal做了什么事,需要我们自行查看它的汇编代码:
后面还有很多,再此就不全部截取了,
老师曾经提醒过,bomblab中很多函数调用,是可以通过函数名来了解它们的作用的
在此就是将我们输入的字符串与地址$402520处存储的字符串进行比较
打开gdb,让我们看看这个位置存储的字符串是什么:
可见这就是我们要输入的字符串了
将结果输入:
这样就把第一个炸弹拆除了。
一些小建议是,手动输入答案容易出错,
最好还是把内容复制进一个专门用于存放答案的文件中。
然后用如上的gdb指令运行答案集文件。
3.2 phase_2 考察循环
进入第二关了,还是继续对汇编代码进行分析
常规的入栈操作,第三句mov将%rsp栈指针的状态保存在%rsi中
调用<read_six_numbers>函数
查看read_six_numbers函数的汇编代码,
对代码进行分析,
第一行汇编代码完成了开栈操作
空间为0x18 = 16 * 1 + 8 * 1 = 24 ,很容易联想到是6个int型数据的空间
下面的汇编操作就是将它们的地址存放到各个寄存器中
往下再看三行,
第一行是不是感觉在phase_1中见过呢
他将地址0x402815存到了%esi寄存器中,
后续将返回值寄存器设置为0
调用sscanf函数(C 库函数 – sscanf() | 菜鸟教程 (runoob.com))
可以先查看一下地址0x402815处到底存放了什么
结果如上,结合开栈时与六个int型数据大小相等的空间,
这时候知道我们应该输入6个int
调用之后,检查返回值,若返回值不满足条件则会引爆炸弹
Jg是大于则跳转,因此满足输入6个int数,便不会出发此处的爆炸
回到phase_2的汇编代码,对剩下的代码进行分析
在分析代码之前,需要了解调用约定
将r替换成e的寄存器实际上指的是同一个寄存器,只不过包含不同的范围r:64,e:32
调用完<read_six_numbers>之后,马上对输入的第一个元素进行判断
将其与0进行比较,如果不是0则引爆炸弹,这说明我们输入的6个数中,第一个数为0
对与接下来的代码分析,都在图中有所体现
在纸上根据汇编代码推演一下过程:
可以得知,初始值为0
%ebx与循环计数相关,随着每次循环自增1
在第n此循环中,
%eax经过一系列操作后,值为第n-1个输入数值+第n次循环的%ebx值
它将与我们第n个输入的数值进行相比
若相等则继续循环,反之引爆炸弹
由此可知,第一个数应为:0
第二个数应为:1
第三个数应为:3
第四个数应为:6
第五个数应为:10
第六个数应为:15
他们之间的关系就是
现在我们得到了phase_2的答案
输入看看是否是对的
3.3 phase_3 考察条件分支(switch)
首先把phase_3的汇编代码列出:
对代码进行分析:
Phase_3的开头这几行代码,处理的事情还是,
开栈,为调用sscanf做准备,检测sscanf的返回值,不合法则引爆炸弹
值得注意的是,根据调用约定
%rcx存放的是第四个参数
%rdx存放的是第三个参数
因此%rsp+8存放的是第二个参数,%rsp+12存放着第一个参数
我们将$0x402821地址处的值取出来看看
这时候与上面返回值检侧就对应上了,我们应当输入两个数
否则将检测为不合法,从而引爆炸弹
接着对程序进行分析
此行汇编代码将第一个参数与7进行了比较,
若无符号大于则跳转至400feb处
发现是个炸弹,因此得到结果,第一个参数应当小于等于7且大于等于0
取值范围是 0,1,2,3,4,5,6,7
接着往下看
代码将%rsp+12处的值赋给%eax
并且根据%rax的值进行地址跳转
跳转的地址为 0x402580+8*%rax
这个地址随着rax的值变化而变化,因此,和switch语句相关
在此我们先假设第一个参数我们传入的是0
因此这行汇编代码将会跳转到0x402580处
我们查看一下0x402580地址处的内容
发现输出的是也是地址,这些地址处于phase_3中汇编代码的地址值范围
由于我们假设输入的第一个数为0,因此地址应该为0x400f97
因此我们回到phase_3,看应该会如何跳转
从0x400f97往下的语句基本上就是对%eax进行简单的运算,
在此不赘述运算的过程了
输入为0时,%eax最终运算结果为-359
最终会将%eax与%rsp+8地址处的值(我们输入的第二个数)进行比较
若不相等,则会引爆炸弹,因此我们可以得知,第二个数即计算结果
因此,0 -359是本题的一个解
值得注意的是,对于第一个输入来说,有0到7总共八个取值
因此跳转地址对应也有八个选项
所以第一个数为其他值的时候,也能找到对应的第二个输入满足代码要求
故本题答案不唯一,具有多个解
将结果输入,得到反馈:
对于汇编代码的逐行分析如下,谨抛砖引玉,若有谬误请见谅
3.4 phase_4 考察递归调用和栈
首先把汇编代码放上来
接下来对代码进行分析
上述的代码依次进行了
- 开栈
- 将%rsp + 8的地址放入%rcx中,即输入的第二个参数地址(调用约定)
- 将%rsp + 12的地址放入%rdx中,即输入的第一个参数地址(调用约定)
- 为sscanf的调用做准备
- 判断返回值是否为2,若不是2则引爆炸弹(跳转到了如下语句)
我们将$0x402821地址处的值取出来看看
结合分析刚刚放上来的代码,我们可以确定phase_4的输入也是两个
继续对代码进行分析
这一段代码,先将%rsp + 12处的值与0xe,也就是14进行比较
若小于等于14则跳转,继续执行代码
否则引爆炸弹
因此可以得知,我们的第一个输入应当小于等于14
接下来的几句话是为了调用fun4做准备
首先将%edx的值设置为14,将%esi的值设置为0.
将%edi的值设置为%rsp+12(第一个输入的数)
根据调用约定,
%edi是调用fun4函数时传入的第一个参数,%esi是第二个,%edx是第三个
调用完函数fun4之后,将返回值寄存器里的值与4进行比较,
若返回值为4则继续执行,返回值非4则引爆炸弹
意思是执行完fun4之后,我们希望得到的返回值为4
将fun4的代码放上来进行分析:
通过汇编代码可以看出,在fun4中又调用了fun4
这是递归调用的形式
将代码转化为C语言风格:
int func4(int x,int y,int z)
{//x in %rdi,y in %rsi,z in %rdx,t in %rax,k in %ecx
int t=z-y;
int k=t>>31;
t=(t+k)>>1;
k=t+y;
if(k>x)
{
z=k-1;
func4(x,y,z);
t=2t;
return t;
}
else
{
t=0;
if(k<x)
{
y=k+1;
func4(x,y,z);
t=2*t+1;
return t;
}
else
{
return t;
}
}
}
这里的x对应的是%edi,y对应%esi,z对应%edx
我们需要的返回值为4
经过计算,有一个解为2
这样我们就把fun4的功能与如何获得返回值4的输入弄明白了
返回到phase_4的汇编代码继续往下,
调用fun4,返回结果为4之后,继续执行
将%rsp+8(我们输入的第二个数)与4进行比较
若相等则跳到程序结尾,phase_4执行结束,
若不相等则引爆炸弹,这说明我们输入的第二个数必须为4,也就是fun4的返回值
因此,得到本题的答案为 2 4
输入结果并执行程序,得到如下的信息:
对于phase_4汇编代码的逐行解读见下图:
3.5 phase_5 考察指针
先把代码放上来,phase_5的汇编代码明显比之前长了不少
接下来对汇编代码进行分析:
开头的几行代码还是老套路,开栈,做准备,调用sscanf,检查返回值
做一些简单说明即可,
0x402821地址我们已经在前面分析过两次了,它存储的内容是:
这表明我们会有2个输入
因此后续返回值检查就是在检查sscanf返回值是否大于1,说明小于两个输入是不合法的
继续分析接下来的代码
4010c8 将%rsp+12地址处的值(我们输入的第一个值)放入寄存器%eax中
4010cc 将0xf和%eax的值做与位运算(保留输入的低四位)
4010cf 将处理完的%eax值存入地址%rsp+12处
4010d6 将%eax值与0xf进行比较
4010d6 如果相等的话,跳转至401104处
发现这个地方是个炸弹,因此第一个输入值不能为15
接下来的结构是一个循环,
4010d8和4010dd是第一次进入循环之前做的初始化工作
4010e2进行%edx的自增,它是记录循环次数的寄存器
4010e5 进行符号扩展
4010e7 %eax = 0x4025c0 + 4*%rax,即根据%rax的值进行跳转,%rax即先前符号扩展了的%eax
不妨看看0x4025c0地址处是什么
是一个数组,这个地方就是根据%rax的值,在数组中获取一个值,赋值给%eax
(注意%rax和%eax代表的是同一个寄存器,因此第n个%eax的值,由第n-1个的值决定)
4010ee %ecx = %ecx + %eax
这里的%ecx在每次循环内都会加上本次循环的%eax值,相当于是对%eax的值进行求和
4010f0 将%eax的值和15进行比较
如果值等于15,则结束循环,如果不等于15,则会继续进行循环。
4010f9 将%edx与14进行比较
注意,此处的%edx是循环的计数器,这边在比较循环次数与14的关系
4010fc 不等于则跳转到0x401104
这个地方是个炸弹,因此,循环次数应该等于14
以上就是对于数组、循环和循环次数需求的分析,可以得到如下信息:
数组值如下
循环伪代码为:
由于前面的分析,我们知道,第一次进入循环的%eax值是我们输入的第一个数
而这个循环执行了14次才结束,结束时,%eax = 15
因此,我们需要根据这个结果进行逆推
因此,我们输入的第一个数应为12
继续分析代码,思考我们要输入的第二个数有何要求
4010fe 将%ecx的值和%rsp+8处的值进行比较
401102 相等则跳转到401109,此时退栈,phase_5结束
否则将会引爆炸弹
因此,我们得知第二个输入数应该是所有%eax值的和
将我们前面的推理过程中%eax的值进行求和
15 + 6 + 14 + 2 + 1 + 10 + 0 + 8 + 4 + 9 + 13 + 11 + 7 + 3 = 103
由循环的伪代码可知,12并不在求和范围内(一进来eax就由12变为了3)
因此本题的答案就是 12 103
输入答案,发现确实如此:
对于phase_5的逐行分析:
3.6 phase_6 考察链表/指针/结构
Phase_6的代码很长,在此直接放上我的逐行分析
发现phase_5的代码中,有不少循环,以及大的循环嵌套小的循环
大致将汇编代码按照循环语句的判断和跳转代码划分
先来看开头这段不包含循环结构的汇编代码
这段代码的作用是入栈操作,并调用read_six_numbers让读者输入6个整型
在read_six_numbers中,会判断输入内容是否合法,不合法将引爆炸弹
这一段代码的分析见图中注释
其作用为:遍历输入的6个数,确保他们都不大于6且不相等
并且可以得知的是,我们的输入在栈中存放的地址从%rsp+48开始
接着分析下一代码块
代码块的划分依据可以从一开始的phase_6汇编代码全览中获取
这一块代码的执行是从401194开始的,上一部分的代码运行完后,会跳转到401194处
在汇编代码中,有个值得关注的点
401181和4011a2将某一地址存到了我们的寄存器中,
我们可以访问一下0x6042f0这个地址,查看一下它是什么
输出结果如上,根据命名的提示,我们发现它应该是一个链表的结点
而跟在node1后面的3个信息,第一个应该是结点的值,第三个应该是下一结点地址
那么未知的就是第二个信息了,对于node1有0x1,姑且认为它是结点序号吧
这段代码使用两层循环,将我们从链表中取得的节点地址放入栈中
选取结点的顺序与我们输入的数字顺序有关,让我们接着分析,看看有什么约束输入的要求
这一块代码写了一个循环。经过循环,将六个结点又依次串联起来
这一块代码要求结点值按照从小到大排序
如果结点们没有按照节点值从小到大排序的话,将会引爆炸弹。
代码分析完毕,我们知道,
我们应当输入6个恰当的数,来将链表结点按从小到大的顺序进行排列
因此,应将链表的结点全部取出,经过值大小比对之后,决定我们的输入
根据前面我们寻找到的链表首节点地址依次查询链表结点信息
将他们的数值从小到大进行排序,
0x1e2 < 0x263 < 0x273 < 0x2ce < 0x35f < 0x367
因此,正确的答案应该为:5 6 3 2 4 1
将答案输入,返回如下结果:
3.7 secret_phase 考察二叉树
写完phase_6之后已经很崩溃了,更崩溃的是居然还有个secret_phase
大学生哪有不疯的
好在bomblab实验说明里直接告诉我们
Secret_phase需要在第4阶段的解后附加一特定字符串后才会出现
但是知道了也还是不会写,最终借助了一篇博客的方法,终于顺利完成了secret_phase
以下是链接:
BombLab phase-6 & secret_phase_等不到结尾的博客-CSDN博客
由于代码中并没有像调用phase_123456那样直接调用secret_phase方法
因此我们需要先找到secret_phase是在哪调用的,
找到在哪调用以后,我们再对secret_phase函数的汇编代码进行分析
发现在phase_defuse函数中调用了它
对phase_defuse函数的汇编代码进行分析
图片中已经能涵盖对phase_defuse函数的分析了,
在这里简单提几个值得注意的地方
这两行代码是对通关数的判断,如果没有通过6关,则不会出发secret_phase的调用
这两行代码将内存地址存入寄存器
我们查看地址所存放的内容,就可以将secret_phase的调用和phase_4的输入关联起来
这表示调用sscanf时要转化的输入有三个,两个数和一个字符串
而0x6048b0处存放的则是我们第四关中的输入
因此,我们知道了触发secret_phase的条件有:
通过六个关卡并在第四关输入后面添加一个字符串
要添加的字符串在下面这几行代码中给出了答案
这段代码为string_not_equal的调用做了准备
而string_not_equal函数的作用不用阅读代码也能猜到,是比较两个字符串是否相等的函数
受比较的字符串,一个是我们输入的字符串,另一个则是从0x402874地址处取出的字符串
因此,查看0x402874处的字符串内容,就可以知道如何出发secret_phase了
因此,在第四题的输入内容后面加上DrEvil
就能看到以下输出:
接下来就看如何解决secret_phase带来的问题
对secret_phase整段代码的分析如下图所示:
接下来进行代码的详细分析
这几行代码为strtol的调用做准备
Strtol函数内容可以在以下链接中了解:
将我们输入的数字与0x3e8 ,即1000进行比较
小于等于1000则跳转,继续运行,否则引爆炸弹
继续分析接下来的代码
由调用约定,%edi为第一个参数寄存器,%esi为第二个参数寄存器
因此,在调用fun7的时候
我们输入的值作为第二个参数传入
作为第一个参数传入的值存储于0x604110地址处
值得注意的是,这个值是十六进制值,所以并非十进制的24,而应该是十进制的36
(在这里犯了个错,想了好久才发现)
因此在这里调用fun7的时候,传入参数为36所在地址和我们输入的数值
调用结束之后,将对fun7的返回值进行比较
若等于3则继续执行,若不等于3则引爆炸弹
因此得到了对fun7返回值的限制,返回值必须为3
接下来分析fun7函数汇编代码
阅读代码,会发现,这段代码的内容为:
当输入的a,b相同时,返回0
当输入的a<b时, a变为a地址移动8字节后的值,返回2*f(a,b)+1
当输入的a>b时,a变为a地址移动16字节后的值,返回2*f(a,b)
用伪代码来描述这个函数的话:
Int fun7(node* addr,int input)
{
If(addr==null){return -1;}
If(*addr<=input)
{
If(*addr==input){return 0;}
Else{return 2*fun(addr+8,input)+1;}
}
Else{return 2*fun7(addr+4,input);}
}
因此,如果希望我们的返回值为3
那么应该是
3 = 2*1 + 1
下一层返回1,在这一层走了*addr<input的分支
1 = 2*0 + 1
下一层返回1,在这一层走了*addr<input的分支
0 = 0
下一层直接返回了0
根据地址关系画出二叉树
最终可以画出二叉树:
根据分析,两次比较结果都是*addr < input
并且在最后一次 *add = input
因此答案应该为0x6d,即十进制的107
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)