二进制炸弹-山东大学计组实验
计算机系统实验花了两周终于搞完了,趁考试周的闲暇写写思路-
MIPS32位架构
phase1
作为实验的入门题,题目设计者给我们展示了一下如何拆这个炸弹,首先插一个断点(b *xxxx)
``
单步调试 si,利用x 或r i命令 查看寄存器内的值,发现 $a0存放的是输入的字符串
发现应与
a0的值
至此我们得到phase1的答案 Let’s begin now!
phase2
第二关考察的是一个简单的循环语句,disas查看phase2代码发现这是一个按逆序依次取学号最后一位的阶乘
我们查看phase_2处代码,所列代码为取学号的过程,学号首地址-32660(gp)+12个字节也就是学号的最后一位,放到
a0相乘,将结果放入$a0,(注意mflo)然后与下一个输入的数作比较。
0x00400e3c <+128>: li v1,12
0x00400e40 <+132>: lw v0,24(s8)
0x00400e44 <+136>: nop
0x00400e48 <+140>: subu v0,v1,v0
0x00400e4c <+144>: lw v1,-32660(gp)
0x00400e50 <+148>: sll v0,v0,0x2
0x00400e54 <+152>: addu v0,v1,v0
0x00400e58 <+156>: lw v0,0(v0)
0x00400e5c <+160>: nop
0x00400e60 <+164>: mult a0,v0
0x00400e64 <+168>: mflo a0
0x00400e68 <+172>: lw v0,24(s8)
0x00400e6c <+176>: nop
就这么循环六次
phase3
第三关就是一个简单的switch结构,我们可以看到这一关的炸弹极多,进入scanf函数发现输入格式为"%d %c %d ",所以要输入两个数和一个字符。
0x00400f5c <+136>: sll v1,v0,0x2
0x00400f60 <+140>: lui v0,0x40
0x00400f64 <+144>: addiu v0,v0,10124
0x00400f68 <+148>: addu v0,v1,v0
0x00400f6c <+152>: lw v0,0(v0)
0x00400f70 <+156>: nop
0x00400f74 <+160>: jr v0
我们发现,程序是以输入的第一个数为基址进行switch跳转的,并且第一个数在[0,8)之间,我们以第一个数为4为例,第三个数要和学号最后一位相乘结果等于该炸弹处给的值,所以要根据学号最后一位还进行选关,最后一位为零的同学只能选择第三的炸弹,也就是第一个数字只能是2.第二个字符也是取决于所选的炸弹的,以输入的第二个数字是4为例,
0x00401088 <+436>: li v0,111
0x0040108c <+440>: sb v0,32(s8)
0x00401090 <+444>: lw v0,-32660(gp)
0x00401094 <+448>: nop
0x00401098 <+452>: lw v1,44(v0)
0x0040109c <+456>: lw v0,36(s8)
0x004010a0 <+460>: nop
0x004010a4 <+464>: mult v1,v0
0x004010a8 <+468>: mflo v1
0x004010ac <+472>: li v0,228
0x004010b0 <+476>: beq v1,v0,0x4011dc <phase_3+776>
0x004010b4 <+480>: nop
0x004010b8 <+484>: jal 0x4021f0 <explode_bomb>
0x004010bc <+488>: nop
0x004010c0 <+492>: lw gp,24(s8)
0x004010c4 <+496>: b 0x4011f8 <phase_3+804>
这里又出现了-32660(gp),不过这里是加44后取的是学号最后一位放到
v0相乘与一个立即数作比较,这里我们就发现,由于
v1都是整数,所以选择分支即输入第一个数要结合自己的学号综合分析。
接着往下看,
0x004011f8 <+804>: lb v0,40(s8)
0x004011fc <+808>: lb v1,32(s8)
0x00401200 <+812>: nop
0x00401204 <+816>: beq v1,v0,0x401218 <phase_3+836>
0x00401208 <+820>: nop
我们在之前的代码可以看到,第一行就是向32(s8)内存入一个字符,它的ASCII码为111,也就是’o’,40(s8)为输入的第二个字符的地址,所以答案已经很明显了。
出题人在这里也留下了一个坑,就是不是所有的分支都能通过。
phase4
这一关是一个函数递归模型,phase3中有一个判定学号最后一位奇偶的分支,
0x00401340 <+132>: lw v0,-32660(gp) 0x00401344 <+136>: nop 0x00401348 <+140>: lw v0,44(v0) 0x0040134c <+144>: nop 0x00401350 <+148>: andi v0,v0,0x1 0x00401354 <+152>:c andi v0,v0,0xff 0x00401358 <+156>: beqz v0,0x40139c <phase_4+224>
我们可以看出,若$v0的值为0,也就是学号最后一位为偶数,跳转至偶数段,我们以偶数段为例分析
0x0040139c <+224>: lw v0,24(s8)
0x004013a0 <+228>: nop
0x004013a4 <+232>: move a0,v0
0x004013a8 <+236>: jal 0x401230 <func4>
0x004013ac <+240>: nop
0x004013b0 <+244>: lw gp,16(s8)
0x004013b4 <+248>: move v1,v0
0x004013b8 <+252>: li v0,13
0x004013bc <+256>: beq v1,v0,0x4013d0 <phase_4+276>
0x004013c0 <+260>: nop
24(s8)为我们输入的数,进入func4函数,我们得到的返回值要为13,这就很明确了,让我们来查看func4的内容
(gdb) disas func4
Dump of assembler code for function func4:
0x00401230 <+0>: addiu sp,sp,-40
0x00401234 <+4>: sw ra,36(sp)
0x00401238 <+8>: sw s8,32(sp)
0x0040123c <+12>: sw s0,28(sp)
0x00401240 <+16>: move s8,sp
0x00401244 <+20>: sw a0,40(s8)
0x00401248 <+24>: lw v0,40(s8)
0x0040124c <+28>: nop
0x00401250 <+32>: slti v0,v0,2
0x00401254 <+36>: bnez v0,0x40129c <func4+108>
0x00401258 <+40>: nop
0x0040125c <+44>: lw v0,40(s8)
0x00401260 <+48>: nop
0x00401264 <+52>: addiu v0,v0,-1
0x00401268 <+56>: move a0,v0
0x0040126c <+60>: jal 0x401230 <func4>
0x00401270 <+64>: nop
0x00401274 <+68>: move s0,v0
0x00401278 <+72>: lw v0,40(s8)
0x0040127c <+76>: nop
0x00401280 <+80>: addiu v0,v0,-2
0x00401284 <+84>: move a0,v0
0x00401288 <+88>: jal 0x401230 <func4>
0x0040128c <+92>: nop
0x00401290 <+96>: addu v0,s0,v0
0x00401294 <+100>: b 0x4012a0 <func4+112>
0x00401298 <+104>: nop
0x0040129c <+108>: li v0,1
0x004012a0 <+112>: move sp,s8
0x004012a4 <+116>: lw ra,36(sp)
0x004012a8 <+120>: lw s8,32(sp)
0x004012ac <+124>: lw s0,28(sp)
0x004012b0 <+128>: addiu sp,sp,40
0x004012b4 <+132>: jr ra
0x004012b8 <+136>: nop
不难看出这是一个斐波那契数列的递归函数,这里给出c++代码
int func4(int a)
{
if(a<2)return 1;
return func(a-1)+func(a-2);
}
若要返回值为13,则输入应为6。这里回过头来看为奇数的情况,这里要求返回值为8,哦,破案了。phase_4就是偶数输入6,奇数输入5.
phase5
这一关是一个字符串转换算法,也不算算法. 先上炸弹段代码
0x004014cc <+228>: addiu a1,v0,10160
0x004014d0 <+232>: jal 0x401cf8 <strings_not_equal>
0x004014d4 <+236>: nop
0x004014d8 <+240>: beqz v0,0x4014e8 <phase_5+256>
这里和phase_1很类似,我按照相同的办法很轻松得知目标为"giants"但这次比较的不是我们输入的字符,接着来读程序,我们发现一个循环,在这个循环里依次取我们输入的字符,并且依次向$a0所在地址依次放字符,读到这些我们就能发现,这个作比较字符和我们输入的字符有某种对应关系,
0x00401440 <+88>: lb v1,0(v1)
0x00401444 <+92>: nop
0x00401448 <+96>: andi v1,v1,0xff
0x0040144c <+100>: andi v1,v1,0xf
0x00401450 <+104>: sll v0,v0,0x2
0x00401454 <+108>: addiu a0,s8,24
0x00401458 <+112>: addu v0,a0,v0
0x0040145c <+116>: sw v1,12(v0)
0x00401460 <+120>: lw a0,24(s8)
0x00401464 <+124>: lw v0,24(s8)
0x00401468 <+128>: nop
0x0040146c <+132>: sll v0,v0,0x2
0x00401470 <+136>: addiu v1,s8,24
0x00401474 <+140>: addu v0,v1,v0
0x00401478 <+144>: lw v1,12(v0)
0x0040147c <+148>: lui v0,0x41
0x00401480 <+152>: addiu v0,v0,12524
=> 0x00401484 <+156>: addu v0,v1,v0
0x00401488 <+160>: lb v1,0(v0)
我们可以看出<+96><+100>处的代码将输入的字符取后四位并存放在<+116>12(v0)处,并再<+156>以v1作为偏移量取值,想知道$v0基址内的具体内容,利用x/16c命令得到具体内容
(gdb) si
0x00401484 in phase_5 ()
(gdb) x/16c $v0
0x4130ec <array.3607>: 105 'i' 115 's' 114 'r' 118 'v' 101 'e' 97 'a' 119 'w' 104 'h'
0x4130f4 <array.3607+8>: 111 'o' 98 'b' 112 'p' 110 'n' 117 'u' 116 't' 102 'f' 103 'g'
这里符合我之前的猜想,我们得到了密码数组,c[16]={‘i’,s,r,v,e,a,w,h,o,b,p,n,u,t,f,g};我们利用这个密码可以推出输入字符的后四位大小依次为15 0 5 11 13 1.取ASCII码表中可以找到很多组答案,其中一组为opekma
哦~,答案貌似又出来了
phase6
链表
0x00401500 <+0>: addiu sp,sp,-96
0x00401504 <+4>: sw ra,92(sp)
0x00401508 <+8>: sw s8,88(sp)
0x0040150c <+12>: move s8,sp
函数的头就是那么吓人,整整开辟了96的栈空间,但是我们仍要带着“一切长代码都是纸老虎”的观点去啃它。
这个phase里面有很多的循环,先来看第一个循环,这里循环里还套着一个循环,
0x0040156c <+108>: slti v0,v0,7
0x00401570 <+112>: beqz v0,0x40159c <phase_6+156>
0x00401594 <+148>: bgtz v0,0x4015a8 <phase_6+168>
内循环:
0x004015c0 <+192>: lw v0,28(s8)
0x004015c4 <+196>: nop
0x004015c8 <+200>: sll v0,v0,0x2
0x004015cc <+204>: addiu v1,s8,24
0x004015d0 <+208>: addu v0,v1,v0
0x004015d4 <+212>: lw v1,12(v0)
0x004015d8 <+216>: lw v0,24(s8)
0x004015dc <+220>: nop
0x004015e0 <+224>: sll v0,v0,0x2
0x004015e4 <+228>: addiu a0,s8,24
0x004015e8 <+232>: addu v0,a0,v0
0x004015ec <+236>: lw v0,12(v0)
0x004015f0 <+240>: nop
0x004015f4 <+244>: bne v1,v0,0x401608 <phase_6+264>
0x004015f8 <+248>: nop
0x004015fc <+252>: jal 0x4021f0 <explode_bomb>
0x00401600 <+256>: nop
0x00401604 <+260>: lw gp,16(s8)
0x00401608 <+264>: lw v0,24(s8)
0x0040160c <+268>: nop
0x00401610 <+272>: addiu v0,v0,1
0x00401614 <+276>: sw v0,24(s8)
0x00401618 <+280>: lw v0,24(s8)
外循环判定的是输入的数要在(0,7)之间,而内循环判定的是当前的数与之后输入的数都不相同,所以第一个循环的意思就是输入的数只能是1 2 3 4 5 6这六个数的全排列。
再来看第二个循环,在这里出现了一个特殊的数据结构node,出题人给我们提示这是一个链表了
0x004016b0 <+432>: lw v1,12(v0)
0x004016b4 <+436>: lw v0,24(s8)
=> 0x004016b8 <+440>: nop
0x004016bc <+444>: slt v0,v0,v1
0x004016c0 <+448>: bnez v0,0x401678 <phase_6+376>
v0为节点号,对应输入的六个数也为123456,初始状态也是按照123456的顺序排列的,将六个节点遍历得到的第一个不小于$v1的数,也就是相等的节点,存入36(v0)
下一个循环只有五次,因为连接六个节点只需要五次
0x00401728 <+552>: lw v0,28(s8)
0x0040172c <+556>: nop
0x00401730 <+560>: sll v0,v0,0x2
0x00401734 <+564>: addiu v1,s8,24
0x00401738 <+568>: addu v0,v1,v0
0x0040173c <+572>: lw v1,36(v0)
0x00401740 <+576>: lw v0,32(s8)
0x00401744 <+580>: nop
0x00401748 <+584>: sw v1,8(v0)
0x00401750 <+592>: nop
0x00401754 <+596>: sll v0,v0,0x2
0x00401758 <+600>: addiu v1,s8,24
0x0040175c <+604>: addu v0,v1,v0
0x00401760 <+608>: lw v0,36(v0)
0x00401764 <+612>: nop
0x00401768 <+616>: sw v0,32(s8)
此为链接过程,取出两节点地址
v0,然后将
v0的next也就是8(
v0的节点,以便下一次链接这样就完成了一次连接过程。重复五次就得到了完整的链表。
再来看下一个循环,
0x004017b4 <+692>: lw v0,-32660(gp)
0x004017b8 <+696>: nop
0x004017bc <+700>: lw v0,44(v0)
0x004017c0 <+704>: nop
0x004017c4 <+708>: andi v0,v0,0x1
0x004017c8 <+712>: andi v0,v0,0xff
0x004017cc <+716>: beqz v0,0x401818 <phase_6+792>
其中又出现了和phase_4中一样的代码,也是根据奇偶进行分支,我们这次以奇数为例,
0x004017d4 <+724>: lw v0,32(s8)
0x004017d8 <+728>: nop
0x004017dc <+732>: lw v1,0(v0)
0x004017e0 <+736>: lw v0,32(s8)
0x004017e4 <+740>: nop
0x004017e8 <+744>: lw v0,8(v0)
0x004017ec <+748>: nop
0x004017f0 <+752>: lw v0,0(v0)
0x004017f4 <+756>: nop
0x004017f8 <+760>: slt v0,v1,v0
0x004017fc <+764>: beqz v0,0x401854 <phase_6+852>
......
0x00401854 <+852>: lw v0,32(s8)
0x00401858 <+856>: nop
0x0040185c <+860>: lw v0,8(v0)
0x00401860 <+864>: nop
0x00401864 <+868>: sw v0,32(s8)
取出链表首节点 v0,要让 v0才能跳过炸弹,之后更新23(s8)的值为下一个节点,这个循环的意思就是判断链表是不是非递增的,偶数情况是判断是否为非递减。现在目标很明确了,只需要知道各个节点里面存的element是多少就行了,在循环中分别查看得
node | 值 |
---|---|
node1 | 0xfd |
node2 | 0x2d5 |
node3 | 0x12d |
node4 | 0x3e5 |
node5 | 0xd4 |
node6 | 0x1b0 |
所以偶数答案为513624,奇数为426315
secret_phase
要想进入sercet需要花上好会儿功夫,可以看到main函数下,每个phase后面都有phase_defused,我们进入phase_defused查看,发现 jal 401990<secret_phase>
0x00402284 <+32>: lw v1,12864(v0)
0x00402288 <+36>: li v0,6
0x0040228c <+40>: bne v1,v0,0x402374 <phase_defused+272>
其中$v1为闯过的关数,就是说只有在完成第六关才会真正进入,接着往下看又发现ssanf函数,其输入格式为” %d %s“,要想获得这个格式的字符串只能在phase_4处构造,因为那里已经有了一个%d.现在只需要找到这个%s就可以进入serect了。函数又一次进入了<string_not_equal>,和phase_1类似,得到字符串应为"austinpowers",在第四关输入顺利进入secret.
现在进入serect,其sscanf格式为%d,只需要输入一个整数,
0x004019f4 <+100>: addiu v0,v0,-1
0x004019f8 <+104>: sltiu v0,v0,1001
0x004019fc <+108>: bnez v0,0x401a10 <secret_phase+128>
0x00401a00 <+112>: nop
0x00401a04 <+116>: jal 0x4021f0 <explode_bomb>
...
0x00401a18 <+136>: lw a1,24(s8)
0x00401a1c <+140>: jal 0x4018a4 <fun7>
0x00401a20 <+144>: nop
0x00401a24 <+148>: lw gp,16(s8)
0x00401a28 <+152>: move v1,v0
0x00401a2c <+156>: li v0,7
0x00401a30 <+160>: beq v1,v0,0x401a44 <secret_phase+180>
并且该整数要小于1002,后来将该整数传入fun7,得到的返回值为7,这里就将问题引到了fun7,我们进入fun7,这是类似二叉树搜索的过程,给出c++实现代码
node currentNode =root;
int v0;
void fun7(int a)
{
if(a==0){v0=-1;return ;}
else if(a<currentNode.element)//左
{
currentNode=currentNode.leftChild;
fun7(a);
v0*=2;
}
else if(a>currentNode.element)//右
{
currentNode=currentNode.leftChild;
fun7(a);
v0=v0*2+1;
}
else {v0=0;return;}
}
我们可以逆向推出v0的得到过程,因为7为奇数,只能来自右,倒推v0=3,仍为奇数,再倒推v0=1,再倒推v0=0;所以a应该是比较了三次右子树才得到了相等的element。按照这样思路,只需要看三层右子树就能得到答案;
(gdb) i r $a0 //根节点
a0: 0x413184
(gdb) x 0x413184+8 //右子树
0x41318c <n1+8>: 0x0041316c
(gdb) x 0x0041316c+8 //右子树
0x413174 <n22+8>: 0x0041313c
(gdb) x 0x0041313c+8 //右子树
0x413144 <n34+8>: 0x0041308c
(gdb) x 0x0041308c //查看element
0x41308c <n48>: 0x000003e9 //0x3e9=1001
输入10001,就可通过,至此拆完所以炸弹
总结
刚拿到这个实验的时候觉得这是不可能完成的任务,那么多炸弹,汇编代码晦涩难懂,看着实验没有任何头绪,网上的相关资料也很少,每周也只有一次进实验室上机的机会,还是一个小组一个机器。ddl是第一生产力果然不错,后来在宁dalao的帮助下在自己电脑上搭好了环境(再次感谢宁哥),硬着头皮往上怼,熬了三四个晚上,终于在验收前一周肝完了,现在回想起来还有几分欣慰自豪。