二进制拿炸弹实验报告完整版
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
课程名称:计算机系统原理实验
实验课时:32课时
实验项目名称:BombLab二进制炸弹
实验材料:可执行文件bomb、源代码bomb.c、说明README
实验环境:Linux操作系统(安装虚拟机和Ubuntu)
实验工具:调试工具gdb
实验内容:程序运行中有6道关卡(phases),每道关卡需要用户在终端上输入特定的字符串。
输入正确则炸弹解除,错误则炸弹引爆。
实验方法:
1.将程序反汇编成汇编语言。
2.结合C语言文件找到每个关卡的入口函数。
3.然后分析汇编代码,找到每个phase中引导程序跳转到explode_bomb程序的地方。
4.分析其成功跳转的条件——以此为突破口寻找应该输入的字符串!
【实验分析】
预先准备
首先查看整个bomb.c的代码,发现整个炸弹组是由6个小炸弹(函数)组成的。
整个main函数比较简单,函数间变量几乎没有影响。
因此,只需要依次解除6个小炸弹即可。
实验材料只有一个可执行文件,先把他用objdump -d反汇编得到汇编文件。
打开文件发现对应6个phase,对应的汇编文件十分长,因此我们直接步入phase部分。
一、phase_1:
1.利用gdb调试,gdb bomb
2.disassemble phaes_1
经分析,答案极有可能存放在0x804a15c处3.可看汇编代码如上图所示,以下为栈帧的计算过程
4.我们看到如上图所示,调用了一个<strings_not_equal>函数,
根据名字判断是判断两个字符串是否相等。
我们找到这个函数来验证我们的猜想是否正确:
5.在gdb下调试,设立断点,并检验
二、phase_2:
1.disassemble phase_2
可看到phase_2的汇编代码以及理解,利用一个循环,不断计算出这个六位密码。
2.栈帧的计算步骤
a[0]=0 a[1]=1 a[2]=a[0]+a[1]=1 a[3]=a[2]+a[1]=2 a[4]=3
a[5]=5
即答案猜想为 0 1 1 2 3 5,下面进行检验
3.gdb下进行调试检验
总结
通过这次『拆弹』的历练,我对数据在内存中以及汇编的表示方法有了更加深刻的认识,做得过程可能有时候会摸不着头脑,这个时候告诉自己冷静,相信自己。
三、phase_3:
6.利用gdb调试,gdb bomb
7.disassemble phaes_3
通过汇编代码画出栈的情况可看汇编代码如上图所示,以下为栈帧的计算过程
可以看到调用函数sscanf,那么sscanf 函数是做什么用的的呢?查阅资料如下:
function
<cstdio> sscanf
int sscanf ( const char * s, const char * format, ...); Read formatted data from string
Reads data from s and stores them according to parameter format into the locations given by the additional arguments, as if scanf was used, but reading from s instead of the standard input (stdin).
The additional arguments should point to already allocated objects of the type specified by their corresponding format specifier within the format string.
Return Value
On success, the function returns the number of items in the argument list successfully filled. This count can match the expected number of items or be less (even zero) in the case of a matching failure.
In the case of an input failure before any data could be successfully interpreted, EOF is returned.
成功时, 该函数返回已成功填充的参数列表中的个数。
在匹配的失败情况下, 此计数可以与预期的参数个数匹配, 或小于 (甚至为零)。
在成功解释任何数据之前输入失败的情况下, 将返回 EOF
※即这个函数的返回值为函数接收到的有效参数的个数
结合有疑问的语句
0x08048eb5 <+20>: movl $0x804a23e,0x4(%esp)
%eax,(%esp)
查看该处将地址0x804a23e传给了0x4(%esp),0x8(%edp)传给了(%esp)
经过分析,是让我们输入两个十进制整数
加上对sscanf函数的理解并结合上面两句,将函数
__isoc99_sscanf@plt的返回值与1比较,即输入的数据数目应该大于一个,那就应该是两个数据了,并且输入的第一个数还要比7小。
当看到汇编代码后半部分时,发现当输入的第一个数大于5时,炸弹依旧会爆炸,因此判断第一个数的取值范围为 0-5.
接下来看汇编代码:
此句话有着至关重要的位置,因为它是典型的switch语句,调试*0x804a1a0处,我们看见了其调用的初步步骤,接下来开始实验检查。
设置断点,追踪验证
断点1:0x08048ed2 <+49>: cmpl $0x7,-0xc(%ebp)
断点2:0x08048f53 <+178>: cmp -0x10(%ebp),%eax
第三关的初始输入值我们先输入 0 9999 进行下一步操作
可以看到,输入第一个之后,系统会生成一个数与输入的第二个数作比较,即和eax作比较,不相同时炸弹就会bomb!!!
由此可知,0 147 是本关卡的一个正确答案,剩下的结果均可以照此方法得出
本周收获:一些指令的使用更加轻松一些,结合老师讲的栈的内容做了一些深入复习,同时在计算机系统课上又得到了巩固,效果很好。
四、phase_4:
8.利用gdb调试,gdb bomb
9.disassemble phaes_4
经过分析,是让我们输入两个十进制整数,和第三关一样,查看
0x804a23e处的值,结合语句cmp $0x2,%eax ,jne 0x8048e66 <phase_4+56>,加上对sscanf函数的理解,将函数
__isoc99_sscanf@plt的返回值与2比较可知,输入的参数个数必须为两个,不然就会引发爆炸.
这里将第一个参数传给eax,然后执行js指令则当eax为负数时会引爆炸弹,然后将eax与14作比较当它大于14时会引爆炸弹,压栈,并调用func4函数。
func4函数内部:
经过检验得到函数func4的C语言代码:
int func4(int b,int a,int a1) {
int x,y;
y = b;
y = y -a;
x = y;
x = (unsigned)x>>31;
y = x + y;
y = y/2;
y+=a;
if((y-a1)>0)
{
y = y-1;
func4(y,a,a1);
return 2*a;
}
else
{
a = 0;
if((y-a1)<0)
{
y = y+1;
func4(b,y,a1);
return 2*a+1;
}
}
return 0;
}
phase_4最后将 14 .0 ,和第一个我们输入的参数传进func4函数,比较func4的返回值,其要求返回值只能为1,否则就会爆炸。
并且第二个参数也只能为1.
经过编译运行,第一个参数值为8、9、11.
即其答案为:
8 1;
9 1;
11 1。
五、phase_5:
1.利用gdb调试,gdb bomb
2.disassemble phaes_4
可以看到,栈进行初始化时,使用了被调用者保存寄存器,并且如前两关一样,查了寄存器的使用如下:
程序寄存器是唯一一个被所有过程共享的资源。
虽然在给定时刻只能有一个过程是活动的,我们必须保证当一个过程(调用者)调用另一个(被调用者)时,被调用者不会覆盖某个调用者稍后会使用的寄存器的值。
为此,IA32采用了一组统一的寄存器使用惯例,所有的过程都必须遵守,包括程序库中的过程。
根据惯例,寄存器eax,edx,ecx被划分为调用者保存(caller save)寄存器。
当过程P调用Q时,Q可以覆盖这些寄存器,而不会破坏P 所需要的数据。
另外,寄存器ebx,esi和edi被划分为被调用者保存(callee save)寄存器。
这意味着Q必须在覆盖它们之前,将这些寄存器的值保存到栈中,并在返回前恢复它们,因为P(或某个更高层次的过程)可能会在今后的计算中需要这些值。
此外,根据这里描述的惯例,必须保存寄存器ebp,esp。
过程P在调用Q之前计算y,但是它必须保证y的值在Q返回后是可用的。
有两种方式可以做到这一点:
1:它可以在调用Q之前,将y的值存放在自己的栈帧中;当Q返回时,它可以从栈中取出y的值。
2:它可以将y的值保存在被调用者保存寄存器中。
如果Q,或任何其他Q调用的程序,想使用这个寄存器,它必须将这个寄存器的值保存在栈帧中,并在返回前恢复该值。
因此,当Q返回到P时,y的值会在被调用者保存寄存器中,或者是因为寄存器根本就没有改变,或者是因为它被保存并恢复了。
这里还是要输入两个数,eax要大于1,不然炸弹就爆炸,那么返回值肯定是2。
如果eax此时等于1111,炸弹爆炸,即原输入参数1与0xf作位与不能为1111。
所以参数1的低4位不能都为1.经过分析,参数1的范围为 0-14。
可以看到这个地址里存放着一串数字。
接着看,把ebx(也就是0x804a1c0)加4倍eax的地址处的值,传
给eax,如果这次的
eax低4位不是1111,就回到<phase_5+80>继续循环。
这里当eax不
等15时就会一直循环并且每次循环edx会加1,ecx会不断加上eax
的值。
而上面那一串数字中也有15这个数字。
将完成循环的ecx和参数2进行比较,相等就顺利结束,可知,参数
2的值需要通过循环来计算。
10 2 14 7 8 12 15 11 0 4 1
13 3 9 6 5
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
故得到:
15-6 6-14 14-2 2-1 1-10 10-0 0-8 8-4 4-9 9-13 13-11 11-7 7-3 3-12
12-5 ,即有
5 12 3 7 11 13 9 4 8 0 10 1 2 14
6 15
第二个输入的参数与ecx的值进行了比较,我们知道ecx是每次eax
的值累加得到所以可以计算得出第二个参数是115
得到一组密码为5 115,其他解类推。
六、phase_6:
1.利用gdb调试,gdb bomb
2.disassemble phaes_4
接下来把eax-1与5比,如果小于等于就继续,大于就爆炸,所以eax的值小于等于6,又因为jbe是无符号操作,所以eax的值的范围确定为1、2、3、4、5、6
这段表示,将输入的第二个数的地址给ebx,将地址ebx中的内容与第一个输入作比较,相等时就会爆炸。
将ebp-76的值加1,即ebp-76为2;如果ebp-76的值小于等于5,跳到65行,大于5跳到35行。
ebp-48及其之前的5个数即为要输入的数,<phase_6+93>之前就是对这6个数做操作,我们现在对ebp-48定义为参数1,ebp-44是参数2, ebp-40是参数3,ebp-36是参数4, ebp-32是参数5, ebp-28是参数6。
并且它们的取值范围为1-6,相邻两个数不能相等,差值不能是4的倍数。
检查地址0x804c0c4中的内容。
可以看到,这里存放的是一个链表。
有关链表操作时,有一个跳转条件是0x08048d15 <+140>: cmp $0x1,%ecx,而ecx就是某一个参数,这些参数的值是1~6中的。
在
做这一步的分析时,就需要我们假设输入的这个数列是某个顺序才能继续计算下去了,假设原数列为2,3,5,6,1,4,则有参数1为节点2的值,参数2是节点3的值,循环的任务是把原来的数列中值为i的数,换成链表中对应节点i的值。
对于我假定的这个数列,完成这个循环之后的结果就是:
(Node2.data,Node3.data,Node5.data,Node6.data,Node1.data,Node4.data)
判断上一步生成的六个结点的值的大小应该是由大到小排列的,不然就爆炸。
链表中的六个值分别是:
1a7 6c 155 187 3bd 255 要按照从大到小排序,为:3bd 255 1a7 187 155 6c 故输入的数字应该是5 6 1 4 3 2。
感想:
第六关感觉比较难,用了很长时间,向班级同学问,他们给我做了解答,目前只是做了大概,在他们的讲解下理解了,自己还要多练习,深刻理解汇编代码。
实验材料只有一个可执行文件,先把他用objdump -d反汇编得到汇编文件。
打开文件发现对应6个phase,对应的汇编文件十分长,因此我们直接步入phase部分。
前六关已经解决完成,还有最后一个秘密关卡,接下来解决秘密关卡。
七、phase_defused:
phase_defused的前半部分很简单,我们可以得知,必须要先解开前六关之后才能进入秘密关卡。
前:
0x0804901d <+9>: mov %gs:0x14,%eax
0x08049023 <+15>: mov %eax,-0xc(%ebp)
后:
0x080490bb <+167>: mov -0xc(%ebp),%eax
0x080490be <+170>: xor %gs:0x14,%eax
这段汇编代码前后分别出现了如上代码,第一次见到他们,因此我在网上找到了它们的作用:
其为Linux栈溢出漏洞的利用,考查Linux Canary绕过技术及ROP(Return-Oriented-Programming)攻击负载的构造。
0x01 Linux Canary介绍
首先了解一下Linux的Canary保护机制。
Canary是Linux众多安全保护机制中的一种,主要用于防护栈溢出攻击。
我们知道,在32位系统上,对于栈溢出漏洞,攻击者通常是通过溢出栈缓冲区,覆盖栈上保存的函数返回地址来达到劫持程序执行流的目的:
针对此种攻击情况,如果在函数返回之前,我们能够判断ret地址是否被改写,若被改写则终止程序的执行,便可以有效地应对攻击。
如何做到呢?一个很自然的想法是在刚进入函数时,在栈上放置一个标志,在函数结束时,判断该标志是否被改变,如果被改变,则表示有攻击行为发生。
Linux Canary保护机制便是如此,如下:
攻击者如果要通过栈溢出覆盖ret,则必先覆盖Canary。
如果我们能判断Canary前后是否一致,便能够判断是否有攻击行为发生。
说明:上述图例仅用于说明,实际上canary并不一定是与栈上保存的BP地址相邻的。
0x02 Linux Canary实现
Linux程序的Canary保护是通过gcc编译选项来控制的,gcc与canary相关的参数及其意义分别为:
-fstack-protector:启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
-fstack-protector-all:启用堆栈保护,为所有函数插入保护代码。
-fno-stack-protector:禁用堆栈保护,为默认选项。
在函数开始时,会取gs:0x14处的值,并放在%ebp-0xc的地方(mov %gs:0x14,%eax, mov %eax,-0xc(%ebp)),在程序结束时,会将该值取出,并与gs:0x14的值进行抑或(mov -0xc(%ebp),%eax,xor %gs:0x14,%eax),如果抑或的结果为0,说明canary未被修改,程序会正常结束,反之如果抑或结果不为0,说明canary已经被非法修改,存在攻击行为,此时程序流程会走到__stack_chk_fail,从而终止程序。
movl $0x804a200,0x4(%esp)这里读取为%D %D %S 中输入的%s
为DrEvil就能开启隐藏关卡,而且这个字符串应该附在某个答案为两个数字的炸弹后面。
之前破解炸弹输入为两个参数的有3、4、5关,分别在他们后面加上DrEvil
在第四关后加上DrEvil后成功找到隐藏关。
八、disassemble secret_phase:
0x08048c22 <+7>: call 0x8049206 <read_line>.
0x08048c3a <+31>: call 0x80488b0 <strtol@plt>
开始就看到其调用了两个函数。
从而可以推断出此关卡应该是一个字符串作为参数
第二为 strtol()函数。
strtol()函数的参数原型为long int strtol(const char *nptr,char **endptr,int base) ,作用是将一个字符串转换成一个长整数,又因为参数 0xa 被入栈,所以推测这个函数是将输入的字符串转换成十进制长整数赋给$eax作为返回值. ,且jbe 8048c50<secret_phase+53> 表明eax<=3e8由这两句知输入的十进制数要小于等于1001。
可以看到接下来调用函数fun7
fun7:
因为递归调用结束后:*a1==a2,
0x08048c60 <+69>: cmp $0x5,%eax
0x08048c63 <+72>: je 0x8048c6a <secret_phase+79>
0x08048c65 <+74>: call 0x80490d1 <explode_bomb>
最外层返回值为5。
那么反递归可得:
(eax)*2+1=5-->eax=2 进行下面的循环有*a1<a2 a1+=8;(eax)*2==2-->eax=1 进行上面的循环有*a1>a2 a1+=4;(eax)*2+1=1-->eax=0 进行下面的循环有*a1<a2 a1+=8;*a1=a2时跳出;
之前已经判断出a1=36,接下来分别查看:
地址0x804c0dc,这里存储的数字就是我们所要输入的0x2f,也就是十进制的47。
2f大于24走下面循环 2f小于32 走上面循环 2f大于2d走下面循环 2f=2f回退递归。