计算机系统实验-Bomblab(含隐藏关卡)
这个实验一共有七个题目,其中前三题难度较低,从第四题开始难度就逐渐提升,因为每个人的炸弹都不一样,但是基本的解题方法是一样的,在涉及到递归和循环的部分时会比较绕,部分汇编代码也比较难看懂,建议在看不下去的时候先看看总结,可以知道代码的作用是什么,这样会比较好推。同时由于注释较多,我在上传时没有认真校对可以看到文件夹中生成了汇编文件bomb.asm,接下来在汇编文件中可以找到对于炸弹的代码部分,逐一
目录
前言和准备工作
这个实验一共有七个题目,其中前三题难度较低,从第四题开始难度就逐渐提升,因为每个人的炸弹都不一样,但是基本的解题方法是一样的,在涉及到递归和循环的部分时会比较绕,部分汇编代码也比较难看懂,建议在看不下去的时候先看看总结
,可以知道代码的作用是什么,这样会比较好推。同时由于注释较多,我在上传时没有认真校对,如果有问题请根据实际情况判断。
在做题之前,需要提前生成bomb.c的汇编文件,在文件目录下输入以下指令:
objdump -d bomb > bomb.asm
可以看到文件夹中生成了汇编文件bomb.asm,接下来在汇编文件中可以找到对于炸弹的代码部分,逐一分析每个炸弹。
一、Phase_1
代码分析
查看phase_1的汇编代码:
08048b60 <phase_1>:
8048b60: 83 ec 1c sub $0x1c,%esp //开辟新栈,分配28个字节的空间
8048b63: c7 44 24 04 e4 a1 04 movl $0x804a1e4,0x4(%esp) //将0x804a1e4地址存的值存入esp+4的地址
8048b6a: 08
8048b6b: 8b 44 24 20 mov 0x20(%esp),%eax //将esp+32处的值存入寄存器eax
8048b6f: 89 04 24 mov %eax,(%esp) //将eax处的值存入esp处
8048b72: e8 8d 04 00 00 call 8049004 <strings_not_equal> //调用8049004处函数,判断字符串是否相等
8048b77: 85 c0 test %eax,%eax //判断eax的值是否为0
8048b79: 74 05 je 8048b80 <phase_1+0x20> //若为0则跳转至8048b80处,进入下一关
8048b7b: e8 96 05 00 00 call 8049116 <explode_bomb> //不为0则引爆
8048b80: 83 c4 1c add $0x1c,%esp //回收栈指针
8048b83: c3 ret //返回
此题较简单,可以看出炸弹密码就存放在0x804a1e4处,我们只需使用gdb查看改地址处的内容即可:
因此,第一个炸弹的字符串密码为:Brownie, you are doing a heck of a job.
结果验证
二、Phase_2
代码分析
08048b84 <phase_2>:
8048b84: 56 push %esi //将esi处数据压入栈
8048b85: 53 push %ebx //将ebx处数据压入栈
8048b86: 83 ec 34 sub $0x34,%esp //开辟新栈,分配52个字节的空间
8048b89: 8d 44 24 18 lea 0x18(%esp),%eax
8048b8d: 89 44 24 04 mov %eax,0x4(%esp) //esp+24处值存入esp+4
8048b91: 8b 44 24 40 mov 0x40(%esp),%eax
8048b95: 89 04 24 mov %eax,(%esp) //esp+64值存入esp 24 28 32 36 40 44 48
8048b98: e8 ae 06 00 00 call 804924b <read_six_numbers> //调用804924b处的读取六个数字的函数
8048b9d: 83 7c 24 18 01 cmpl $0x1,0x18(%esp) //判断esp+24处的值是否等于1(输入的第一个数)
8048ba2: 74 05 je 8048ba9 <phase_2+0x25> //若相等跳转至8048ba9处
8048ba4: e8 6d 05 00 00 call 8049116 <explode_bomb> //不相等则引爆
8048ba9: 8d 5c 24 1c lea 0x1c(%esp),%ebx //esp+28处地址存入ebx
8048bad: 8d 74 24 30 lea 0x30(%esp),%esi //esp+48处地址存入esi
8048bb1: 8b 43 fc mov -0x4(%ebx),%eax //ebx-4地址处的值(第一个数)存入eax
8048bb4: 01 c0 add %eax,%eax //eax*2
8048bb6: 39 03 cmp %eax,(%ebx) //判断eax处的值与ebx地址处的值是否相等(第二个数
8048bb8: 74 05 je 8048bbf <phase_2+0x3b> //相等则跳转至8048bbf处
8048bba: e8 57 05 00 00 call 8049116 <explode_bomb> //不相等则引爆炸弹
8048bbf: 83 c3 04 add $0x4,%ebx //ebx处的值加4,指向下一个数
8048bc2: 39 f3 cmp %esi,%ebx //比较esi和ebx内的值是否相等
8048bc4: 75 eb jne 8048bb1 <phase_2+0x2d> //不是最后一个就继续循环
8048bc6: 83 c4 34 add $0x34,%esp //相等则esp处的值+52
8048bc9: 5b pop %ebx //弹出ebx内的值
8048bca: 5e pop %esi //弹出esi内的值
8048bcb: c3 ret //返回
这题稍微有一点点难度,涉及到循环
,由代码可以得到以下结论:
- 拆弹密码为6个数字,由cmpl行可知第一个数字为1;
- esp+28至esp+48存放着我们输入的后五个数字,此后在这段范围进行循环判断;
- 接着将ebx-4即esp+24地址处的值乘2后判断与ebx地址处的值是否相等,若相等则ebx往后移一个单元,再判断是否已经判断到最后一个数字,若没到则继续循环;
- 可猜想6个数字分别为1、2、4、8、16、32
结果验证
显然,猜想正确,密码为1 2 4 8 16 32
三、Phase_3
代码分析
08048bcc <phase_3>:
8048bcc: 83 ec 2c sub $0x2c,%esp
8048bcf: 8d 44 24 1c lea 0x1c(%esp),%eax
8048bd3: 89 44 24 0c mov %eax,0xc(%esp) //参数2存入esp+12地址处
8048bd7: 8d 44 24 18 lea 0x18(%esp),%eax
8048bdb: 89 44 24 08 mov %eax,0x8(%esp) //参数1存入esp+8地址处
8048bdf: c7 44 24 04 03 a4 04 movl $0x804a403,0x4(%esp) //对之后输入的格式进行测试
8048be6: 08
8048be7: 8b 44 24 30 mov 0x30(%esp),%eax
8048beb: 89 04 24 mov %eax,(%esp) //esp+48处的地址存入esp地址处
8048bee: e8 7d fc ff ff call 8048870 <__isoc99_sscanf@plt> //调用sscanf函数,eax为输入数据的个数
8048bf3: 83 f8 01 cmp $0x1,%eax
8048bf6: 7f 05 jg 8048bfd <phase_3+0x31> //若输入数据的个数大于1则跳转至cmpl处
8048bf8: e8 19 05 00 00 call 8049116 <explode_bomb>
8048bfd: 83 7c 24 18 07 cmpl $0x7,0x18(%esp)
8048c02: 77 66 ja 8048c6a <phase_3+0x9e> //若输入的第一个参数的值大于7则引爆炸弹
8048c04: 8b 44 24 18 mov 0x18(%esp),%eax //否则将第一个参数存入eax
8048c08: ff 24 85 40 a2 04 08 jmp *0x804a240(,%eax,4) //跳转至4*%eax+0x804a240地址处
- 分析可知,输入的第一个无符号参数范围为[0,7],即存在8个分支,转表地址为0x804a240,可使用gdb查看跳转表内容:
补充:sscanf函数:用于从一个字符串中读进与指定格式相符的数据,通过 gdb 调试可看出 sscanf 检查的格式:由此判断输入为两个数字
- 根据跳转表内容找到各分支对应汇编代码:
//case 1:
8048c0f: b8 00 00 00 00 mov $0x0,%eax //eax存入0
8048c14: eb 05 jmp 8048c1b <phase_3+0x4f> //跳转至8048c1b处
//case 0:
8048c16: b8 74 02 00 00 mov $0x274,%eax //eax存入0x274
8048c1b: 2d a3 02 00 00 sub $0x2a3,%eax //eax处的值减去0x2a3
8048c20: eb 05 jmp 8048c27 <phase_3+0x5b> //跳转至8048c27处
//case2:
8048c22: b8 00 00 00 00 mov $0x0,%eax //eax存入0
8048c27: 05 9e 01 00 00 add $0x19e,%eax //eax处的值加上0x19e
8048c2c: eb 05 jmp 8048c33 <phase_3+0x67> //跳转至8048c33处
//case 3:
8048c2e: b8 00 00 00 00 mov $0x0,%eax //eax存入0
8048c33: 2d 19 02 00 00 sub $0x219,%eax //eax处的值减去0x219
8048c38: eb 05 jmp 8048c3f <phase_3+0x73> //跳转至8048c3f处
//case 4:
8048c3a: b8 00 00 00 00 mov $0x0,%eax //eax存入0
8048c3f: 05 19 02 00 00 add $0x219,%eax //eax处的值加上0x219
8048c44: eb 05 jmp 8048c4b <phase_3+0x7f> //跳转至8048c4b处
//case 5:
8048c46: b8 00 00 00 00 mov $0x0,%eax //eax存入0
8048c4b: 2d 19 02 00 00 sub $0x219,%eax //eax处的值减去0x219
8048c50: eb 05 jmp 8048c57 <phase_3+0x8b> //跳转至8048c57处
//case 6:
8048c52: b8 00 00 00 00 mov $0x0,%eax //eax存入0
8048c57: 05 19 02 00 00 add $0x219,%eax //eax处的值加上0x219
8048c5c: eb 05 jmp 8048c63 <phase_3+0x97> //跳转至8048c63处
//case 7:
8048c5e: b8 00 00 00 00 mov $0x0,%eax //eax存入0
8048c63: 2d 19 02 00 00 sub $0x219,%eax //eax处的值减去0x219
8048c68: eb 0a jmp 8048c74 <phase_3+0xa8> //跳转至8048c74处
8048c6a: e8 a7 04 00 00 call 8049116 <explode_bomb>
8048c6f: b8 00 00 00 00 mov $0x0,%eax //eax存入0
8048c74: 83 7c 24 18 05 cmpl $0x5,0x18(%esp) //比较参数1与5
8048c79: 7f 06 jg 8048c81 <phase_3+0xb5> //若参数1大于5则引爆
8048c7b: 3b 44 24 1c cmp 0x1c(%esp),%eax //否则再比较计算结果与参数2
8048c7f: 74 05 je 8048c86 <phase_3+0xba> //若相等则跳至下一关
8048c81: e8 90 04 00 00 call 8049116 <explode_bomb>
8048c86: 83 c4 2c add $0x2c,%esp
8048c89: c3 ret
- 最后,参数1大于5时会直接引爆,因此可能存在的密码有6种,通过分析计算,参数1为0-5时,eax对应的计算结果为-170、-798、-123、-537、0、-537
经过验证,答案确实有六组:0 -170、1 -798、2 -123、3 -537、4 0、5 -537
四、Phase_4
此题涉及到递归
,需要我们通过一个函数的反汇编代码反推出该该函数,有一定的难度。
代码分析
1.输入为两个数
08048cf3 <phase_4>:
8048cf3: 83 ec 2c sub $0x2c,%esp
8048cf6: 8d 44 24 1c lea 0x1c(%esp),%eax
8048cfa: 89 44 24 0c mov %eax,0xc(%esp)
8048cfe: 8d 44 24 18 lea 0x18(%esp),%eax
8048d02: 89 44 24 08 mov %eax,0x8(%esp)
8048d06: c7 44 24 04 03 a4 04 movl $0x804a403,0x4(%esp)
8048d0d: 08
8048d0e: 8b 44 24 30 mov 0x30(%esp),%eax
8048d12: 89 04 24 mov %eax,(%esp)
8048d15: e8 56 fb ff ff call 8048870 <__isoc99_sscanf@plt> //以上代码和phase_3相同
8048d1a: 83 f8 02 cmp $0x2,%eax //将输入数据个数与2比较
8048d1d: 75 0d jne 8048d2c <phase_4+0x39> //不等于2则引爆
- 参数1的范围为[0,14],参数2为13,参数1为使func函数返回值为13的值
8048d1f: 8b 44 24 18 mov 0x18(%esp),%eax
8048d23: 85 c0 test %eax,%eax //判断参数1和0
8048d25: 78 05 js 8048d2c <phase_4+0x39> //小于0则引爆
8048d27: 83 f8 0e cmp $0xe,%eax //否则与14比较
8048d2a: 7e 05 jle 8048d31 <phase_4+0x3e> //小于等于14则跳转调用func4
8048d2c: e8 e5 03 00 00 call 8049116 <explode_bomb> //否则引爆
8048d31: c7 44 24 08 0e 00 00 movl $0xe,0x8(%esp) //esp+8存入14
8048d38: 00
8048d39: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp) //esp+4存入0
8048d40: 00
8048d41: 8b 44 24 18 mov 0x18(%esp),%eax
8048d45: 89 04 24 mov %eax,(%esp) //esp存入参数1
8048d48: e8 3d ff ff ff call 8048c8a <func4> //调用func4函数
8048d4d: 83 f8 0d cmp $0xd,%eax //返回值与13比较
8048d50: 75 07 jne 8048d59 <phase_4+0x66> //不等于13则引爆
8048d52: 83 7c 24 1c 0d cmpl $0xd,0x1c(%esp) //否则参数2与13比较
8048d57: 74 05 je 8048d5e <phase_4+0x6b> //等于13则进入下一关
8048d59: e8 b8 03 00 00 call 8049116 <explode_bomb>
8048d5e: 83 c4 2c add $0x2c,%esp
8048d61: c3 ret
分析func4函数
08048c8a <func4>:
8048c8a: 83 ec 1c sub $0x1c,%esp
8048c8d: 89 5c 24 14 mov %ebx,0x14(%esp)
8048c91: 89 74 24 18 mov %esi,0x18(%esp)
8048c95: 8b 44 24 20 mov 0x20(%esp),%eax //eax:num1
8048c99: 8b 54 24 24 mov 0x24(%esp),%edx //edx:num2
8048c9d: 8b 74 24 28 mov 0x28(%esp),%esi //esi:num3
8048ca1: 89 f1 mov %esi,%ecx //ecx:num3
8048ca3: 29 d1 sub %edx,%ecx //ecx:num3-num2
8048ca5: 89 cb mov %ecx,%ebx //ebx:num3-num2
8048ca7: c1 eb 1f shr $0x1f,%ebx //逻辑右移31位
8048caa: 01 d9 add %ebx,%ecx //加上原来的值num3-num2。结果记为a
8048cac: d1 f9 sar %ecx //a算数右移1位,即除以2。
8048cae: 8d 1c 11 lea (%ecx,%edx,1),%ebx //ebx:a+num2->b
8048cb1: 39 c3 cmp %eax,%ebx //b与num1相比
8048cb3: 7e 17 jle 8048ccc <func4+0x42> //7小于等于参数1则跳转
8048cb5: 8d 4b ff lea -0x1(%ebx),%ecx //ecx:b-1
8048cb8: 89 4c 24 08 mov %ecx,0x8(%esp) //b-1
8048cbc: 89 54 24 04 mov %edx,0x4(%esp) //0
8048cc0: 89 04 24 mov %eax,(%esp) //num1
8048cc3: e8 c2 ff ff ff call 8048c8a <func4> //递归调用func4(num1,num2,b-1)
8048cc8: 01 c3 add %eax,%ebx //b+返回值
8048cca: eb 19 jmp 8048ce5 <func4+0x5b> //return
8048ccc: 39 c3 cmp %eax,%ebx //b和num1
8048cce: 7d 15 jge 8048ce5 <func4+0x5b> //b=num1
8048cd0: 89 74 24 08 mov %esi,0x8(%esp) //num3
8048cd4: 8d 53 01 lea 0x1(%ebx),%edx //edx:b+1
8048cd7: 89 54 24 04 mov %edx,0x4(%esp) //b+1
8048cdb: 89 04 24 mov %eax,(%esp) //num1
8048cde: e8 a7 ff ff ff call 8048c8a <func4> //递归调用func4(num1,b+1,num3)
8048ce3: 01 c3 add %eax,%ebx //b+返回值
8048ce5: 89 d8 mov %ebx,%eax //结果放到eax,return
8048ce7: 8b 5c 24 14 mov 0x14(%esp),%ebx
8048ceb: 8b 74 24 18 mov 0x18(%esp),%esi
8048cef: 83 c4 1c add $0x1c,%esp
8048cf2: c3 ret
调用函数前传参,因此func函数有三个参数:参数1、0、14分别存放在esp、esp+4、esp+8的位置,根据汇编代码写出func4函数的c代码,并运行func,输出能使func4的返回值为13的参数1:
int func4(int num1,int num2,int num3)
{
int a=(((num3-num2)>>31)+(num3-num2))/2;
int b=num2+a;
int result=0;
if(b>num1)
result=func4(num1,num2,b-1);
if(b<num1)
result=func4(num1,b+1,num3);return result+b;
}
int main()
{
int num1;
int result;
for(num1=0;num1<=14;num1++)
{
result=func4(num1,0,14);
if(result==13)
printf("%d ",num1);
}
printf("\n");
return 0;
}
运行以上代码可得符合条件的参数1只有2,则拆弹密码为2 13
五、Phase_5
代码分析
08048d62 <phase_5>:
8048d62: 83 ec 2c sub $0x2c,%esp
8048d65: 8d 44 24 1c lea 0x1c(%esp),%eax
8048d69: 89 44 24 0c mov %eax,0xc(%esp)
8048d6d: 8d 44 24 18 lea 0x18(%esp),%eax
8048d71: 89 44 24 08 mov %eax,0x8(%esp)
8048d75: c7 44 24 04 03 a4 04 movl $0x804a403,0x4(%esp)
8048d7c: 08
8048d7d: 8b 44 24 30 mov 0x30(%esp),%eax
8048d81: 89 04 24 mov %eax,(%esp)
8048d84: e8 e7 fa ff ff call 8048870 <__isoc99_sscanf@plt> //以上代码和phase_3/4相同
8048d89: 83 f8 01 cmp $0x1,%eax //输入的个数与1比较
8048d8c: 7f 05 jg 8048d93 <phase_5+0x31>
8048d8e: e8 83 03 00 00 call 8049116 <explode_bomb> //如果小于等于1则引爆
8048d93: 8b 44 24 18 mov 0x18(%esp),%eax //参数1存入eax
8048d97: 83 e0 0f and $0xf,%eax
8048d9a: 89 44 24 18 mov %eax,0x18(%esp)
8048d9e: 83 f8 0f cmp $0xf,%eax //参数1低四位与15相比
8048da1: 74 2a je 8048dcd <phase_5+0x6b> //低四位全为1则引爆
8048da3: b9 00 00 00 00 mov $0x0,%ecx
8048da8: ba 00 00 00 00 mov $0x0,%edx //ecx,edx清零
8048dad: 83 c2 01 add $0x1,%edx //edx自加1
8048db0: 8b 04 85 60 a2 04 08 mov 0x804a260(,%eax,4),%eax //4*eax+0x804a260地址处的值
8048db7: 01 c1 add %eax,%ecx //ecx+eax存入ecx
8048db9: 83 f8 0f cmp $0xf,%eax //比较该数组元素与15
8048dbc: 75 ef jne 8048dad <phase_5+0x4b> //如果不等于则edx循环+1
8048dbe: 89 44 24 18 mov %eax,0x18(%esp) //等于15则存入
8048dc2: 83 fa 0f cmp $0xf,%edx //比较15和edx的值
8048dc5: 75 06 jne 8048dcd <phase_5+0x6b> //不等于15则引爆
8048dc7: 3b 4c 24 1c cmp 0x1c(%esp),%ecx //比较参数2和ecx的值
8048dcb: 74 05 je 8048dd2 <phase_5+0x70> //相等则进入下一关
8048dcd: e8 44 03 00 00 call 8049116 <explode_bomb>
8048dd2: 83 c4 2c add $0x2c,%esp
8048dd5: c3 ret
很显然此题也有两个输入,分析可得以下结论:
- 参数1的低四位必须小于15,保留低四位之后进入循环,由倒数第三行可知循环要执行15次,并且第15次取出的元素一定为15;
- 0x804a260通过查看内容之后发现是为某一数组的首地址,每次循环取出一数组元素并与15比较,等于15时保存当前下标,ecx存放着数组元素的累加和,参数2与该累加和相等;
查看数组元素:(16个内存单元,每个单元四个字节,以十进制形式)
倒推15次取出的元素:
取的次数 | eax(下标) | 当前a[eax] |
---|---|---|
15 | 6 | 15 |
14 | 14 | 6 |
13 | 2 | 14 |
12 | 1 | 2 |
11 | 10 | 1 |
10 | 0 | 10 |
9 | 8 | 0 |
8 | 4 | 8 |
7 | 9 | 4 |
6 | 13 | 9 |
5 | 11 | 13 |
4 | 7 | 11 |
3 | 3 | 7 |
2 | 12 | 3 |
1 | 5 | 12 |
分析可知第一次输入的eax=5即参数1为5,将数组各元素累加得参数2为115,即拆弹密码为5 115
六、Phase_6
此题涉及到链表和嵌套循环
,难度较大,并且汇编代码很长,建议分成多个部分
代码分析
//链表+嵌套循环
08048dd6 <phase_6>:
8048dd6: 56 push %esi
8048dd7: 53 push %ebx
8048dd8: 83 ec 44 sub $0x44,%esp
8048ddb: 8d 44 24 10 lea 0x10(%esp),%eax
8048ddf: 89 44 24 04 mov %eax,0x4(%esp)
8048de3: 8b 44 24 50 mov 0x50(%esp),%eax
8048de7: 89 04 24 mov %eax,(%esp)
8048dea: e8 5c 04 00 00 call 804924b <read_six_numbers>
//以上代码与phase_2相同,不详细解释
//判断输入的6个数字是否都1<=num<=6,6个数字都判断完之后跳到e40
8048def: be 00 00 00 00 mov $0x0,%esi
8048df4: 8b 44 b4 10 mov 0x10(%esp,%esi,4),%eax //num[i]
8048df8: 83 e8 01 sub $0x1,%eax //num[i]-1
8048dfb: 83 f8 05 cmp $0x5,%eax
8048dfe: 76 05 jbe 8048e05 <phase_6+0x2f> //num[i]<=6
8048e00: e8 11 03 00 00 call 8049116 <explode_bomb> //num[i]-1大于5则引爆
num[0]为无符号数,且0≤num[0]-1≤5,因此1≤num[0]≤6;即若输入的第一个数字大于等于1小于等于6则跳转至以下:
8048e05: 83 c6 01 add $0x1,%esi //esi为数组下标i
8048e08: 83 fe 06 cmp $0x6,%esi //i+1与6比较
8048e0b: 74 33 je 8048e40 <phase_6+0x6a> //等于6则跳出
8048e0d: 89 f3 mov %esi,%ebx //不等于6则i+1存入ebx
8048e0f: 8b 44 9c 10 mov 0x10(%esp,%ebx,4),%eax //num[i+1](j)
8048e13: 39 44 b4 0c cmp %eax,0xc(%esp,%esi,4) //与num[i]比较
8048e17: 75 05 jne 8048e1e <phase_6+0x48>
8048e19: e8 f8 02 00 00 call 8049116 <explode_bomb> //相等则引爆
8048e1e: 83 c3 01 add $0x1,%ebx //j++
8048e21: 83 fb 05 cmp $0x5,%ebx //i+2与5比较
8048e24: 7e e9 jle 8048e0f <phase_6+0x39>
8048e26: eb cc jmp 8048df4 <phase_6+0x1e> //大于5时返回判断
//------------------------------
若i+2大于5则返回判断1≤num[i]≤6
若i+2≤5,则返回判断num[i+2]与num[i]是否相等
因此可以初步判断以上的代码为双重循环,作用是判断输入的6个数字是否都小于等于6且各不相同,伪代码如下:
当判断完之后,在接下来的内容中看到一行地址,查看地址内容发现是链表,查看链表发现有6个节点,六个数按照编号1-6
分别为:964、85、77、595、72、870
接下来按照输入的六个数字的顺序将六个节点的地址存入一个数组,具体步骤见注释:
//num[i]>1 <-------------------------
8048e28: 8b 52 08 mov 0x8(%edx),%edx //0x804c13c+8 //从第二个节点开始找
8048e2b: 83 c0 01 add $0x1,%eax //j++ <-------- //下一个节点的编号
8048e2e: 39 c8 cmp %ecx,%eax //num[i]与j比较 | //找到该编号的节点
8048e30: 75 f6 jne 8048e28 <phase_6+0x52> //node[0]!=j------
//num[i]=1
8048e32: 89 54 b4 28 mov %edx,0x28(%esp,%esi,4) //将该节点的地址记录在esp+40开始的数组
8048e36: 83 c3 01 add $0x1,%ebx //i++继续找下一个编号的节点
8048e39: 83 fb 06 cmp $0x6,%ebx
8048e3c: 75 07 jne 8048e45 <phase_6+0x6f> //ebx!=6
8048e3e: eb 1c jmp 8048e5c <phase_6+0x86> //存完了6个地址
//将六个节点的地址按照输入数字的顺序存入数组
8048e40: bb 00 00 00 00 mov $0x0,%ebx //ebx置0
8048e45: 89 de mov %ebx,%esi //i
8048e47: 8b 4c 9c 10 mov 0x10(%esp,%ebx,4),%ecx //num[i]
8048e4b: b8 01 00 00 00 mov $0x1,%eax //j=1
8048e50: ba 3c c1 04 08 mov $0x804c13c,%edx //头节点地址
8048e55: 83 f9 01 cmp $0x1,%ecx
8048e58: 7f ce jg 8048e28 <phase_6+0x52> //num[i]大于1跳到e28------
8048e5a: eb d6 jmp 8048e32 <phase_6+0x5c> //num[i]=1就直接存头节点的地址
链表的六个节点首地址存完之后再将链表重新链接,最后判断排完序的链表是否按从大到小的顺序重新排列(注意,也有可能是从小到大),若不是,则引爆,因此可推出输入的六个数字应该是原链表中从大到小的节点的编号
:1 6 4 2 3 5
//完成链表的重新排序链接
8048e5c: 8b 5c 24 28 mov 0x28(%esp),%ebx //ebx=1.
8048e60: 8b 44 24 2c mov 0x2c(%esp),%eax //eax=2.
8048e64: 89 43 08 mov %eax,0x8(%ebx) //第一个节点指向第二个节点
8048e67: 8b 54 24 30 mov 0x30(%esp),%edx //edx=3.
8048e6b: 89 50 08 mov %edx,0x8(%eax) //第二个节点指向第三个节点
8048e6e: 8b 44 24 34 mov 0x34(%esp),%eax //eax=4.
8048e72: 89 42 08 mov %eax,0x8(%edx) //第三个节点指向第四个节点
8048e75: 8b 54 24 38 mov 0x38(%esp),%edx //edx=5.
8048e79: 89 50 08 mov %edx,0x8(%eax) //第四个节点指向第五个节点
8048e7c: 8b 44 24 3c mov 0x3c(%esp),%eax //eax=6.
8048e80: 89 42 08 mov %eax,0x8(%edx) //第五个节点指向第六个节点
8048e83: c7 40 08 00 00 00 00 movl $0x0,0x8(%eax) //第六个节点NULL
8048e8a: be 05 00 00 00 mov $0x5,%esi //i=5
//判断新链表是否按降序排列
8048e8f: 8b 43 08 mov 0x8(%ebx),%eax //eax=第二个节点
8048e92: 8b 10 mov (%eax),%edx //后一个节点的值
8048e94: 39 13 cmp %edx,(%ebx) //比较当前节点与后一个节点的值
8048e96: 7d 05 jge 8048e9d <phase_6+0xc7> //非严格递增
8048e98: e8 79 02 00 00 call 8049116 <explode_bomb>//小于则引爆
//后一个节点大于等于前一个节点
8048e9d: 8b 5b 08 mov 0x8(%ebx),%ebx
8048ea0: 83 ee 01 sub $0x1,%esi //i--
8048ea3: 75 ea jne 8048e8f <phase_6+0xb9>//当前节点是第六个时应该为0
8048ea5: 83 c4 44 add $0x44,%esp
8048ea8: 5b pop %ebx
8048ea9: 5e pop %esi
8048eaa: c3 ret
结果验证
综上,已完成六个炸弹的拆除
七、隐藏关卡
触发条件
- 观察bomb.c
发现每个phase的密码之后都调用了phase_defused函数,且最后有两行话提示我们有什么东西漏掉了
/* Hmm... Six phases must be more secure than one phase! */
input = read_line(); /* Get input */
phase_1(input); /* Run the phase */
phase_defused(); /* Drat! They figured it out!
* Let me know how they did it. */
printf("Phase 1 defused. How about the next one?\n");
/* The second phase is harder. No one will ever figure out
* how to defuse this... */
input = read_line();
phase_2(input);
phase_defused();
printf("That's number 2. Keep going!\n");
/* I guess this is too easy so far. Some more complex code will
* confuse people. */
input = read_line();
phase_3(input);
phase_defused();
printf("Halfway there!\n");
/* Oh yeah? Well, how good is your math? Try on this saucy problem! */
input = read_line();
phase_4(input);
phase_defused();
printf("So you got that one. Try this one.\n");
/* Round and 'round in memory we go, where we stop, the bomb blows! */
input = read_line();
phase_5(input);
phase_defused();
printf("Good work! On to the next...\n");
/* This phase will never be used, since no one will get past the
* earlier ones. But just in case, make this one extra hard. */
input = read_line();
phase_6(input);
phase_defused();
/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */
- 观察phase_defused()
在bomb.asm中找到phase_defused()函数的部分,通过观察phase_defused代码,发现在调用secret_phase函数之前,将地址0x804a409的值放入了esp+4中,接着将输入的值 (0x804c4d0)放入esp,并判断输入的个数是否为3,如果不为3就会跳过secret_phase函数的调用
80492ce: c7 44 24 04 09 a4 04 movl $0x804a409,0x4(%esp) //格式
80492d5: 08
80492d6: c7 04 24 d0 c4 04 08 movl $0x804c4d0,(%esp) //应该输入3个
80492dd: e8 8e f5 ff ff call 8048870 <__isoc99_sscanf@plt>
80492e2: 83 f8 03 cmp $0x3,%eax
80492e5: 75 35 jne 804931c <phase_defused+0x81>
80492e7: c7 44 24 04 12 a4 04 movl $0x804a412,0x4(%esp)
地址0x804a409的值:
接着将0x804a412地址里的值放入esp+4,再把刚才输入的第三个字符串放入esp,判断这两个字符串是否相等,若相等则调用secret_phase函数
80492ef: 8d 44 24 2c lea 0x2c(%esp),%eax
80492f3: 89 04 24 mov %eax,(%esp)
80492f6: e8 09 fd ff ff call 8049004 <strings_not_equal>
80492fb: 85 c0 test %eax,%eax
80492fd: 75 1d jne 804931c <phase_defused+0x81>
因此需要查看0x804a412的内容,与输入的第三个字符串相等即可触发隐藏关卡;
在第六个关卡结束后打上断点,我是在bomb.c文件的115行打上断点,查看地址0x804c4d0发现两个数字为2 13,即第四关密码,此时再查看0x804a412内容,发现字符串为“DrEvil”
因此触发条件即0x804a412处应有3个输入,为在第四关密码后加上 DrEvil
这是我们就会发现拆完六个炸弹之后还没结束,我们可以继续输入第七关的密码。
代码分析
难点主要是fun7函数的分析
08048efc <secret_phase>:
8048efc: 53 push %ebx
8048efd: 83 ec 18 sub $0x18,%esp
8048f00: e8 38 02 00 00 call 804913d <read_line> //调用函数:输入一行为字符串
8048f05: c7 44 24 08 0a 00 00 movl $0xa,0x8(%esp)
8048f0c: 00
8048f0d: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
8048f14: 00
8048f15: 89 04 24 mov %eax,(%esp) //输入的值传入esp
8048f18: e8 c3 f9 ff ff call 80488e0 <strtol@plt> //调用函数strtol
8048f1d: 89 c3 mov %eax,%ebx //返回值传到ebx
8048f1f: 8d 40 ff lea -0x1(%eax),%eax //eax-1
8048f22: 3d e8 03 00 00 cmp $0x3e8,%eax //比较eax与1000
8048f27: 76 05 jbe 8048f2e <secret_phase+0x32> //小于等于1000
8048f29: e8 e8 01 00 00 call 8049116 <explode_bomb> //大于1000则引爆
8048f2e: 89 5c 24 04 mov %ebx,0x4(%esp) //将strtol返回值传入esp+4
8048f32: c7 04 24 88 c0 04 08 movl $0x804c088,(%esp) //地址内容放入esp
8048f39: e8 6d ff ff ff call 8048eab <fun7> //调用函数fun7
8048f3e: 83 f8 01 cmp $0x1,%eax //返回值与1比较
8048f41: 74 05 je 8048f48 <secret_phase+0x4c>
8048f43: e8 ce 01 00 00 call 8049116 <explode_bomb> //返回值不等于1则引爆
8048f48: c7 04 24 0c a2 04 08 movl $0x804a20c,(%esp) //地址内容放入esp
8048f4f: e8 ac f8 ff ff call 8048800 <puts@plt>
8048f54: e8 42 03 00 00 call 804929b <phase_defused> //隐藏关卡破解成功
8048f59: 83 c4 18 add $0x18,%esp
8048f5c: 5b pop %ebx
8048f5d: c3 ret
8048f5e: 90 nop
8048f5f: 90 nop
strtol的作用是将输入的字符串转为整数,并将该整数作为参数b传入递归函数fun7;第一个参数为地址0x804c088,第二个参数为输入的数,下面我们主要分析fun7:
若* a<b则递归调用fun7(a+8,b),返回2×fun7(a+8,b);若*a==b则返回0;
08048eab <fun7>:
8048eab: 53 push %ebx
8048eac: 83 ec 18 sub $0x18,%esp
8048eaf: 8b 54 24 20 mov 0x20(%esp),%edx //参数a(一个地址)
8048eb3: 8b 4c 24 24 mov 0x24(%esp),%ecx //参数b
8048eb7: 85 d2 test %edx,%edx //判断参数1是否为0
8048eb9: 74 37 je 8048ef2 <fun7+0x47>
8048ebb: 8b 1a mov (%edx),%ebx //不为0则将*a存入ebx
8048ebd: 39 cb cmp %ecx,%ebx //比较*a与b中的值
8048ebf: 7e 13 jle 8048ed4 <fun7+0x29> //*a小于等于b
若*a>b则返回2×fun7(a+4,b);
8048ec1: 89 4c 24 04 mov %ecx,0x4(%esp) //若*a>b则
8048ec5: 8b 42 04 mov 0x4(%edx),%eax //参数2仍为b
8048ec8: 89 04 24 mov %eax,(%esp) //参数1为a+4
8048ecb: e8 db ff ff ff call 8048eab <fun7> //递归调用fun7
8048ed0: 01 c0 add %eax,%eax //返回值*2
8048ed2: eb 23 jmp 8048ef7 <fun7+0x4c>
8048ed4: b8 00 00 00 00 mov $0x0,%eax
8048ed9: 39 cb cmp %ecx,%ebx //比较*a与b
8048edb: 74 1a je 8048ef7 <fun7+0x4c> //*a与b相等则结束
8048edd: 89 4c 24 04 mov %ecx,0x4(%esp) //*a<b,参数2为b
8048ee1: 8b 42 08 mov 0x8(%edx),%eax
8048ee4: 89 04 24 mov %eax,(%esp) //参数1为a+8
8048ee7: e8 bf ff ff ff call 8048eab <fun7> //调用fun7
8048eec: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax //返回值*2+1
8048ef0: eb 05 jmp 8048ef7 <fun7+0x4c>
8048ef2: b8 ff ff ff ff mov $0xffffffff,%eax //根节点地址为0则返回-1
8048ef7: 83 c4 18 add $0x18,%esp
8048efa: 5b pop %ebx
8048efb: c3 ret
综上分析可知该递归函数为二叉树结构,根节点地址为0x804c088,如果当前节点的数大于b则调用递归传入左子节点,若当前节点的数小于b则递归传入右子节点,若相等则返回0,此外若根节点为NULL则直接返回-1,由以上分析可以写出改递归函数的伪代码,如下:
上面分析secret_phase的代码时发现最后要求调用fun7之后返回值为1,即fun7(0x804c088,b)=1,
只有该返回值为2×0+1时才成立,因此进入else if部分,fun7(0x804c088+8,b)=0,只有当* a=b时返回值才为0,因此b=* (0x804c090),查看地址发现:(其中n1表示根节点,n21表示根节点的左子节点,n22表示根节点的右子节点)
b=*(0x804c090)=*0x804c0a0=0x32=50,则密码为50
结果验证
总结
本实验一共七个题目,考察了多个知识点
- 其中第一题较简单,考察了
查看地址内容
与一些常见指令的作用,为后面的题打下基础; - 第二题主要考察
循环
,同时要了解sscanf的使用,找到循环结束的条件以及循环内部代码的功能即可得出答案; - 第三题考察了
switch-case语句
,要找到跳转表的地址
并查看内容,找到每个分支对应的汇编代码,并分析出有效的第一个密码范围、个数,再简单根据汇编代码计算出密码的第二个数; - 第四题难度提升,不难看出此题调用了
递归函数
。很容易得知要使函数的返回值为13,第一个突破点是分析出该递归函数的参数个数,而难点是分析递归函数要做什么,实际上结合隐藏关卡可以总结得出对于递归函数只需要对着汇编代码一行行写出其对应的伪代码即可,最后再反过来验证,其实结果很明朗; - 第五题考察了
循环+数组
;突破点是进行了15次循环且最后一次取到的数组元素为15,且每次取出的元素会作为下一次取的数组下标,反推即可; - 个人认为第六题最难最绕,因为有
嵌套循环+链表
,且不止一个嵌套,第一个难点是分析出第一个双重for循环的作用,第二个难点是要知道此题涉及到链表,第三是要分析出接下来的嵌套循环是将链表重新排列,尤其难在循环在各种寄存器和地址之间mov; - 隐藏关卡第一个难点在触发,主要是
递归考察了二叉树
,也有点绕,第二个难点在于如何根据地址内容还原出二叉树,第三个难点在于递归函数的还原,实际上就是找到某个节点使最深层递归返回0,该节点的值就正好为密码。

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)