Lab2-Bomblab
一.实验题目及目的
1.实验题目 程序运行在linux环境中。程序运行中有6个关卡(6个phase),每个phase需要用户在终端上输入特定的字符或者数字才能通关,否则将会引爆炸弹。需要通过分析汇编代码,使用gdb调试等方式找到正确的字符。
2.实验目的 熟悉汇编代码的分析方法,c程序的机器级表达以及控制,过程等相关知识,熟悉gdb调试的方法及过程。
二.准备工作
1.打开bomb.c文件
发现main函数依次调用了phase_1~phase_6六个函数,但函数的具体代码被隐藏。可以知道从命令行输入的内容必须和phase函数里面的一样,否则炸弹爆炸。
2.反汇编可执行文件bomb objdump -d bomb > bomb.txt 这样就可以再bomb.txt文件里面看到整个文件(主要是phase函数)的汇编代码了
三.拆除炸弹
phase_1
下面是phase_1的具体汇编代码及分析
08048b60 <phase_1>:
8048b60: 83 ec 1c sub $0x1c,%esp
8048b63: c7 44 24 04 84 a1 04 movl $0x804a184,0x4(%esp) #
8048b6a: 08
8048b6b: 8b 44 24 20 mov 0x20(%esp),%eax
8048b6f: 89 04 24 mov %eax,(%esp)
8048b72: e8 3d 04 00 00 call 8048fb4 <strings_not_equal>
8048b77: 85 c0 test %eax,%eax #按位相与,如果eax为0,zf=1,否则zf=0
8048b79: 74 05 je 8048b80 <phase_1+0x20> #zf=1,即eax=0时,跳转到8048b80
8048b7b: e8 46 05 00 00 call 80490c6 <explode_bomb> #eax=1,炸弹爆炸
8048b80: 83 c4 1c add $0x1c,%esp
8048b83: c3 ret
其中调用了8048fb4< string_not_equal >这个函数,其具体的汇编代码和分析如下:
08048fb4 <strings_not_equal>: #第一题,判断字符串是否相等
8048fb4: 83 ec 10 sub $0x10,%esp
8048fb7: 89 5c 24 04 mov %ebx,0x4(%esp)
8048fbb: 89 74 24 08 mov %esi,0x8(%esp)
8048fbf: 89 7c 24 0c mov %edi,0xc(%esp)
8048fc3: 8b 5c 24 14 mov 0x14(%esp),%ebx #第一个字符串的指针
8048fc7: 8b 74 24 18 mov 0x18(%esp),%esi #第二个字符串的指针
8048fcb: 89 1c 24 mov %ebx,(%esp)
8048fce: e8 c8 ff ff ff call 8048f9b <string_length> #计算第一个字符串的长度
8048fd3: 89 c7 mov %eax,%edi #把结果保存在edi中
8048fd5: 89 34 24 mov %esi,(%esp)
8048fd8: e8 be ff ff ff call 8048f9b <string_length> #计算第二个字符串的长度,结果保存在eax中
8048fdd: ba 01 00 00 00 mov $0x1,%edx
8048fe2: 39 c7 cmp %eax,%edi #比较两个字符串的长度
8048fe4: 75 33 jne 8049019 <strings_not_equal+0x65> #不相等跳转,相等继续执行
8048fe6: 0f b6 03 movzbl (%ebx),%eax #第一个字符串的第一个字符?
8048fe9: b2 00 mov $0x0,%dl
8048feb: 84 c0 test %al,%al #eax的低8位是否为0
8048fed: 74 2a je 8049019 <strings_not_equal+0x65> #为0跳转
8048fef: b2 01 mov $0x1,%dl
8048ff1: 3a 06 cmp (%esi),%al #比较两个字符串的第一个字符?
8048ff3: 75 24 jne 8049019 <strings_not_equal+0x65> #不相等直接结束
8048ff5: b8 00 00 00 00 mov $0x0,%eax #相等eax置0
8048ffa: eb 08 jmp 8049004 <strings_not_equal+0x50>
8048ffc: 83 c0 01 add $0x1,%eax #循环开始
8048fff: 3a 14 06 cmp (%esi,%eax,1),%dl #比较两个字符?dl是8位,一个字符也是八位
8049002: 75 10 jne 8049014 <strings_not_equal+0x60> #不相等直接eax=1
8049004: 0f b6 54 03 01 movzbl 0x1(%ebx,%eax,1),%edx #edx更新,大概是起到遍历字符的作用
8049009: 84 d2 test %dl,%dl #判断字符串是否比较完毕
804900b: 75 ef jne 8048ffc <strings_not_equal+0x48> #如果dl为0,循环结束
804900d: ba 00 00 00 00 mov $0x0,%edx #eax置0
8049012: eb 05 jmp 8049019 <strings_not_equal+0x65>
8049014: ba 01 00 00 00 mov $0x1,%edx
8049019: 89 d0 mov %edx,%eax #eax=1(两个字符串的长度不相等/字符串不同)
804901b: 8b 5c 24 04 mov 0x4(%esp),%ebx
804901f: 8b 74 24 08 mov 0x8(%esp),%esi
8049023: 8b 7c 24 0c mov 0xc(%esp),%edi
8049027: 83 c4 10 add $0x10,%esp
804902a: c3 ret
这个函数主要实现的功能是判断两个字符串是否相等,判断的具体方法为先判断两个字符串的长度是否一致,如果不一致,将zf置1,炸弹爆炸。如果长度相等,则循环比较每一个字符是否相等,若出现不相等的情况炸弹也会直接爆炸。比较的两个字符串其一是输入的字符串,另外是程序中原先设置好的。我们注意到两个字符串的指针分别是0x8(%esp)和0x4(%esp),而phrse_1函数中有这样一条指令:
8048b63: c7 44 24 04 84 a1 04 movl $0x804a184,0x4(%esp)
用gdb查看0x804a184地址中的内容(x/s 0x804a184),得到以下字符串:
When I get angry, Mr. Bigglesworth gets upset.
用gdb输入这个字符串进行验证,结果正确,第一关通过。
phase_2
下面是phase_2的具体汇编代码及分析
08048b84 <phase_2>:
8048b84: 53 push %ebx
8048b85: 83 ec 38 sub $0x38,%esp
8048b88: 8d 44 24 18 lea 0x18(%esp),%eax
8048b8c: 89 44 24 04 mov %eax,0x4(%esp)
8048b90: 8b 44 24 40 mov 0x40(%esp),%eax
8048b94: 89 04 24 mov %eax,(%esp)
8048b97: e8 5f 06 00 00 call 80491fb <read_six_numbers> #输入应为6个数字
8048b9c: 83 7c 24 18 00 cmpl $0x0,0x18(%esp) #0x18>0时,跳转
8048ba1: 79 05 jns 8048ba8 <phase_2+0x24>
8048ba3: e8 1e 05 00 00 call 80490c6 <explode_bomb> #否则爆炸
8048ba8: bb 01 00 00 00 mov $0x1,%ebx
8048bad: 89 d8 mov %ebx,%eax #循环开始
8048baf: 03 44 9c 14 add 0x14(%esp,%ebx,4),%eax #地址对应的值加ebx
8048bb3: 39 44 9c 18 cmp %eax,0x18(%esp,%ebx,4) #比较 x+1
8048bb7: 74 05 je 8048bbe <phase_2+0x3a> #相等跳转
8048bb9: e8 08 05 00 00 call 80490c6 <explode_bomb> #两数不相等爆炸
8048bbe: 83 c3 01 add $0x1,%ebx #ebx++
8048bc1: 83 fb 06 cmp $0x6,%ebx
8048bc4: 75 e7 jne 8048bad <phase_2+0x29> #ebx!=6时进入循环
8048bc6: 83 c4 38 add $0x38,%esp
8048bc9: 5b pop %ebx
8048bca: c3 ret
根据代码可以猜测这一关需要输入的是六个数字,分析read_six_number这个函数发现其功能就是读取六个数字,所以剩下的有用的代码其实只有很小一段。我们接着来分析剩下的代码:
8048b9c: 83 7c 24 18 00 cmpl $0x0,0x18(%esp) #0x18>0时,跳转
8048ba1: 79 05 jns 8048ba8 <phase_2+0x24>
8048ba3: e8 1e 05 00 00 call 80490c6 <explode_bomb>
如果输入的第一个数大于0,才会继续执行,否则将直接引爆炸弹。
接下来的代码是这一关的核心代码:
8048bad: 89 d8 mov %ebx,%eax #循环开始
8048baf: 03 44 9c 14 add 0x14(%esp,%ebx,4),%eax #地址对应的值加ebx
8048bb3: 39 44 9c 18 cmp %eax,0x18(%esp,%ebx,4) #比较 x+1
8048bb7: 74 05 je 8048bbe <phase_2+0x3a> #相等跳转
8048bb9: e8 08 05 00 00 call 80490c6 <explode_bomb> #两数不相等爆炸
8048bbe: 83 c3 01 add $0x1,%ebx #ebx++
8048bc1: 83 fb 06 cmp $0x6,%ebx
8048bc4: 75 e7 jne 8048bad <phase_2+0x29> #ebx!=6时进入循环
寄存器eax将加上输入的值,如果eax+ebx不等于下一个输入的值,炸弹将直接爆炸,而ebx会在循环中每次加一。换而言之,我们输入的6个数字第二个数字应为第一个数字加一,第三个数字应为第二个数字加二,以此类推。因此,我们可以得出最终的答案有如下形式:
x x+1 x+1+2 x+1+2+3 x+1+2+3+4 x+1+2+3+4+5 最终我们试得答案可以为:1 2 4 7 11 16
我们接着测试了另外一组满足上面形式的数字:11 12 14 17 21 26
这组数字同样能够通过测试,因此,我们认为,凡是满足上述形式且第一个数字>0的一组(六个) 数字,均为正确答案。
phrse_3
下面是phase_3的具体汇编代码及分析
08048bcb <phase_3>:
8048bcb: 83 ec 2c sub $0x2c,%esp
8048bce: 8d 44 24 1c lea 0x1c(%esp),%eax
8048bd2: 89 44 24 0c mov %eax,0xc(%esp)
8048bd6: 8d 44 24 18 lea 0x18(%esp),%eax
8048bda: 89 44 24 08 mov %eax,0x8(%esp)
8048bde: c7 44 24 04 a3 a3 04 movl $0x804a3a3,0x4(%esp) #两个整数
8048be5: 08
8048be6: 8b 44 24 30 mov 0x30(%esp),%eax
8048bea: 89 04 24 mov %eax,(%esp)
8048bed: e8 7e fc ff ff call 8048870 <__isoc99_sscanf@plt>
8048bf2: 83 f8 01 cmp $0x1,%eax #
8048bf5: 7f 05 jg 8048bfc <phase_3+0x31> #eax大于1跳转
8048bf7: e8 ca 04 00 00 call 80490c6 <explode_bomb> #否则爆炸---输入的数的个数应大于1
8048bfc: 83 7c 24 18 07 cmpl $0x7,0x18(%esp) #0x18(%esp)为输入的第一个数
8048c01: 77 3c ja 8048c3f <phase_3+0x74> #无符号大于跳转,爆炸
8048c03: 8b 44 24 18 mov 0x18(%esp),%eax
8048c07: ff 24 85 e0 a1 04 08 jmp *0x804a1e0(,%eax,4) #这个地址里的内容就是下一行的地址?跳转表
8048c0e: b8 ea 01 00 00 mov $0x1ea,%eax #答案零:0 490
8048c13: eb 3b jmp 8048c50 <phase_3+0x85>
8048c15: b8 f9 00 00 00 mov $0xf9,%eax #答案二:2 249
8048c1a: eb 34 jmp 8048c50 <phase_3+0x85>
8048c1c: b8 e2 00 00 00 mov $0xe2,%eax #答案三:3 226
8048c21: eb 2d jmp 8048c50 <phase_3+0x85>
8048c23: b8 58 03 00 00 mov $0x358,%eax #答案四:4 856
8048c28: eb 26 jmp 8048c50 <phase_3+0x85>
8048c2a: b8 ef 00 00 00 mov $0xef,%eax #答案五:5 239
8048c2f: eb 1f jmp 8048c50 <phase_3+0x85>
8048c31: b8 97 02 00 00 mov $0x297,%eax #答案六:6 663
8048c36: eb 18 jmp 8048c50 <phase_3+0x85>
8048c38: b8 22 02 00 00 mov $0x222,%eax #答案七:7 546
8048c3d: eb 11 jmp 8048c50 <phase_3+0x85>
8048c3f: e8 82 04 00 00 call 80490c6 <explode_bomb>
8048c44: b8 00 00 00 00 mov $0x0,%eax #答案八? default???
8048c49: eb 05 jmp 8048c50 <phase_3+0x85>
8048c4b: b8 9f 02 00 00 mov $0x29f,%eax #答案一:1 671
8048c50: 3b 44 24 1c cmp 0x1c(%esp),%eax #eax和esp+28比较
8048c54: 74 05 je 8048c5b <phase_3+0x90> #相等跳转
8048c56: e8 6b 04 00 00 call 80490c6 <explode_bomb> #不相等爆炸
8048c5b: 83 c4 2c add $0x2c,%esp
8048c5e: c3 ret
当第一眼看到这一关的代码时,最先吸引我的就是下面这行代码:
movl $0x804a3a3,0x4(%esp)
它直接将一个地址中的内容传入esp+4的地址。在第一关中我们就是查看一个可疑的地址从而拿到了答案,这里我们同样在gdb中查看地址中的内容(x/s 0x804a3a3)
得到的结果为:%d %d
可以猜测,要求我们输入的为两个整数,接下来的代码调用了这样一个函数isoc99_sscanf@plt。其返回值需要大于1才不会引发爆炸,故推测isoc99_sscanf@plt函数的功能就是判断输入的数字个数是否符合要求。
8048bfc: 83 7c 24 18 07 cmpl $0x7,0x18(%esp)
8048c01: 77 3c ja 8048c3f <phase_3+0x74>
8048c03: 8b 44 24 18 mov 0x18(%esp),%eax
这里esp+0x18为输入的第一个数,第一个数如果大于7将爆炸,用的jump指令为ja,为无符号数所使用,故输入的第一个数应小于等于7并且大于等于0,取值范围即[0,7],之后将第一个数移到寄存器eax。
接下来又有一个地址暴露在外,我们查看其内容得到:
(gdb) x/23xw 0x804a1e0
0x804a1e0: 0x08048c0e 0x08048c4b 0x08048c15 0x08048c1c
0x804a1f0: 0x08048c23 0x08048c2a 0x08048c31 0x08048c38
这是一个跳转表,其中存储的内容十分眼熟,它们对应接下来的代码的地址:
8048c07: ff 24 85 e0 a1 04 08 jmp *0x804a1e0(,%eax,4)
8048c0e: b8 ea 01 00 00 mov $0x1ea,%eax #答案零:0 490
8048c13: eb 3b jmp 8048c50 <phase_3+0x85>
8048c15: b8 f9 00 00 00 mov $0xf9,%eax #答案二:2 249
8048c1a: eb 34 jmp 8048c50 <phase_3+0x85>
8048c1c: b8 e2 00 00 00 mov $0xe2,%eax #答案三:3 226
8048c21: eb 2d jmp 8048c50 <phase_3+0x85>
8048c23: b8 58 03 00 00 mov $0x358,%eax #答案四:4 856
8048c28: eb 26 jmp 8048c50 <phase_3+0x85>
8048c2a: b8 ef 00 00 00 mov $0xef,%eax #答案五:5 239
8048c2f: eb 1f jmp 8048c50 <phase_3+0x85>
8048c31: b8 97 02 00 00 mov $0x297,%eax #答案六:6 663
8048c36: eb 18 jmp 8048c50 <phase_3+0x85>
8048c38: b8 22 02 00 00 mov $0x222,%eax #答案七:7 546
8048c3d: eb 11 jmp 8048c50 <phase_3+0x85>
8048c3f: e8 82 04 00 00 call 80490c6 <explode_bomb>
8048c44: b8 00 00 00 00 mov $0x0,%eax #答案八? default???
8048c49: eb 05 jmp 8048c50 <phase_3+0x85>
8048c4b: b8 9f 02 00 00 mov $0x29f,%eax #答案一:1 671
我们先跳过这一部分,分析最后的几行代码:
8048c50: 3b 44 24 1c cmp 0x1c(%esp),%eax #eax和esp+28比较
8048c54: 74 05 je 8048c5b <phase_3+0x90> #相等跳转
8048c56: e8 6b 04 00 00 call 80490c6 <explode_bomb>
esp+0x1c为输入的第二个数的值,这里将eax与输入的第二个数的值相比,如果不相等就爆炸。而eax在上面的代码中存储的是一些固定的值,例如在地址0x8048c0e中,eax的值就是0x1ea。于是我们再回过去看那一大块代码,执行的操作基本都是eax赋值,eax赋什么值则取决于我们我们输入的第一个数字,这里的逻辑很像一个switch语句。
我们将gdb查看出来的内容整理一下,可以得到以下的组合:
0 490 1 671 2 249 3 226
4 856 5 239 6 663 7 546
经过测试,以上8组数字均能过关。
phrse_4
下面是phase_4的具体汇编代码及分析
08048ccc <phase_4>:
8048ccc: 83 ec 2c sub $0x2c,%esp
8048ccf: 8d 44 24 1c lea 0x1c(%esp),%eax
8048cd3: 89 44 24 0c mov %eax,0xc(%esp)
8048cd7: 8d 44 24 18 lea 0x18(%esp),%eax
8048cdb: 89 44 24 08 mov %eax,0x8(%esp)
8048cdf: c7 44 24 04 a3 a3 04 movl $0x804a3a3,0x4(%esp) #两个整数
8048ce6: 08
8048ce7: 8b 44 24 30 mov 0x30(%esp),%eax
8048ceb: 89 04 24 mov %eax,(%esp)
8048cee: e8 7d fb ff ff call 8048870 <__isoc99_sscanf@plt> #判断输入的个数
8048cf3: 83 f8 02 cmp $0x2,%eax
8048cf6: 75 0d jne 8048d05 <phase_4+0x39> #不等于二直接爆炸(输入的数字只能是两个)
8048cf8: 8b 44 24 18 mov 0x18(%esp),%eax #0x18为输入的第一个数
8048cfc: 85 c0 test %eax,%eax
8048cfe: 78 05 js 8048d05 <phase_4+0x39> #eax为负数直接爆炸
8048d00: 83 f8 0e cmp $0xe,%eax #和14比较
8048d03: 7e 05 jle 8048d0a <phase_4+0x3e> #小于等于就跳转
8048d05: e8 bc 03 00 00 call 80490c6 <explode_bomb> #否则爆炸(第一个数的范围:[0-14])
8048d0a: c7 44 24 08 0e 00 00 movl $0xe,0x8(%esp) #参数-12
8048d11: 00
8048d12: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp) #参数-0
8048d19: 00
8048d1a: 8b 44 24 18 mov 0x18(%esp),%eax
8048d1e: 89 04 24 mov %eax,(%esp) #参数-输入
8048d21: e8 39 ff ff ff call 8048c5f <func4>
8048d26: 83 f8 05 cmp $0x5,%eax
8048d29: 75 07 jne 8048d32 <phase_4+0x66> #eax不等于5爆炸
8048d2b: 83 7c 24 1c 05 cmpl $0x5,0x1c(%esp)
8048d30: 74 05 je 8048d37 <phase_4+0x6b> #输入的第二个数必须是5
8048d32: e8 8f 03 00 00 call 80490c6 <explode_bomb>
8048d37: 83 c4 2c add $0x2c,%esp
8048d3a: c3 ret
根据前面三关的经验,我们可以认为输入的第一个数在 0x1c(%esp)中,第二个数在0x18(%esp)中,在gdb中查看地址中的内容(x/s 0x804a3a3),得到的结果为:%d %d。
很好地验证了我们的经验。这一题的输入为两个整数。
这一题同样调用了 isoc_ssanf @plt 这个函数,随后的代码的功能:检查输入的数字的个数是否为2,否则直接爆炸。
接下来一段代码是对输入的第一个数进行限制(esp+0x18为输入的第一个数):
8048cf8: 8b 44 24 18 mov 0x18(%esp),%eax #0x18为输入的第一个数
8048cfc: 85 c0 test %eax,%eax
8048cfe: 78 05 js 8048d05 <phase_4+0x39> #eax为负数直接爆炸
8048d00: 83 f8 0e cmp $0xe,%eax #和14比较
8048d03: 7e 05 jle 8048d0a <phase_4+0x3e> #小于等于就跳转
8048d05: e8 bc 03 00 00 call 80490c6 <explode_bomb>
输入如果小于0,则直接爆炸,同样地,eax如果大于14,也会直接爆炸。
因此,我们可以得出,第一个输入的值的范围为[0,14]。
8048d0a: c7 44 24 08 0e 00 00 movl $0xe,0x8(%esp) #参数-12
8048d11: 00
8048d12: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp) #参数-0
8048d19: 00
8048d1a: 8b 44 24 18 mov 0x18(%esp),%eax
8048d1e: 89 04 24 mov %eax,(%esp) #参数-输入
8048d21: e8 39 ff ff ff call 8048c5f <func4>
8048d26: 83 f8 05 cmp $0x5,%eax
8048d29: 75 07 jne 8048d32 <phase_4+0x66>
这一段代码调用了func4这个函数,同时传入的3个参数,分别是:第一个输入,0,14。
最后的返回值eax如果不等于5,直接爆炸。
8048d2b: 83 7c 24 1c 05 cmpl $0x5,0x1c(%esp)
8048d30: 74 05 je 8048d37 <phase_4+0x6b> #输入的第二个数必须是5
8048d32: e8 8f 03 00 00 call 80490c6 <explode_bomb>
这里判断输入的第二个数(esp+0x1c),如果不是5则直接爆炸,因此第二个输入只能为5。
这里我们完全可以不用管func4中发生了什么,范围已经缩小到足够小了。
第一个数:[0,14],第二个数:5。
最终试出来答案只有一个:
10 5
现在回看func4这个函数,其原代码的结构应该是这样的:
int func4(int x,int y,int z)
{
int res=0;
int mid=(y+z)/2;
if(mid>x)
return func4(x,y,mid-1)*2;
else if(mid<x)
return func4(x,mid+1,z)*2+1;
else
return res;
}
我们已经知道返回值是5,且第一个输入是10。
即res=5,第一个参数为10,接下来我们开始验证。
参数 | mid值 | |
---|---|---|
第一次 | (10,0,14) | 7 |
第二次 | (10,8,14) | 11 |
第三次 | (10,8,10) | 9 |
第四次 | (10,10,10) | 10 |
以上是递归中func4函数的参数和局部变量mid值的变化,调用的顺序是从上到下的。
因此,我们计算实际的结果应该从下往上。
第四次返回的res值为0;
第三次返回的res值为2*0+1=1;
第二次返回的res值为1*2=2;
第一次(最终)返回的res值为2*2+1=5;
人工验证结果正确。
接下来我们让电脑帮我们验证一下,编写如下的c++代码:
#include <iostream>
using namespace std;
int func4(int x,int y,int z)
{
int res=0;
int mid=(y+z)/2;
if(mid>x)
return func4(x,y,mid-1)*2;
else if(mid<x)
return func4(x,mid+1,z)*2+1;
else
return res;
}
int main()
{
for(int i=0;i<=14;i++)
cout<<func4(i,0,14)<<" ";
return 0;
}
输出结果为:
可以看到0-14的范围中只有x=10有输出为5,其他均不符合条件。
因此,我们可以断定原代码大概就是如此。
(这里不再给出原函数的具体推测过程了,我这是一点一点猜出来的,不好从何说起)
phrse_5
下面是phase_5的具体汇编代码及分析
08048d3b <phase_5>:
8048d3b: 53 push %ebx
8048d3c: 83 ec 18 sub $0x18,%esp
8048d3f: 8b 5c 24 20 mov 0x20(%esp),%ebx
8048d43: 89 1c 24 mov %ebx,(%esp)
8048d46: e8 50 02 00 00 call 8048f9b <string_length>
8048d4b: 83 f8 06 cmp $0x6,%eax
8048d4e: 74 05 je 8048d55 <phase_5+0x1a> #字符串长度等于6
8048d50: e8 71 03 00 00 call 80490c6 <explode_bomb>
8048d55: ba 00 00 00 00 mov $0x0,%edx
8048d5a: b8 00 00 00 00 mov $0x0,%eax
8048d5f: 0f be 0c 03 movsbl (%ebx,%eax,1),%ecx #循环开始
8048d63: 83 e1 0f and $0xf,%ecx #ecx=0000 0000 0000 0000 0000 ?
8048d66: 03 14 8d 00 a2 04 08 add 0x804a200(,%ecx,4),%edx
8048d6d: 83 c0 01 add $0x1,%eax
8048d70: 83 f8 06 cmp $0x6,%eax
8048d73: 75 ea jne 8048d5f <phase_5+0x24> #eax不为6进入循环
8048d75: 83 fa 33 cmp $0x33,%edx
8048d78: 74 05 je 8048d7f <phase_5+0x44> #edx不为51就爆炸
8048d7a: e8 47 03 00 00 call 80490c6 <explode_bomb>
8048d7f: 83 c4 18 add $0x18,%esp
8048d82: 5b pop %ebx
8048d83: c3 ret
同样,根据经验,我们可以推断输入应该被保存在esp+0x20,并且输入的应该是一个字符串,因为后面调用了string_length这个函数,其返回值,也就是输入的字符串的长度如果不等于6,则炸弹爆炸。也就是说,我们的输入应该是一个长度为6的字符串。
接下来的代码将edx和eax两个寄存器置零。
下面的代码是一个循环:
8048d5f: 0f be 0c 03 movsbl (%ebx,%eax,1),%ecx #循环开始
8048d63: 83 e1 0f and $0xf,%ecx #ecx=0000 0000 0000 0000 0000 ?
8048d66: 03 14 8d 00 a2 04 08 add 0x804a200(,%ecx,4),%edx
8048d6d: 83 c0 01 add $0x1,%eax
8048d70: 83 f8 06 cmp $0x6,%eax
8048d73: 75 ea jne 8048d5f <phase_5+0x24>
其中将ebx+eax的值传入ecx,然后拿到ecx的低4位,之后将0x804a200+4*ecx的值加到edx中。接着eax+1,eax和6作比较,如果不相等就重新回到这段代码的开始处。我们注意到,在这段代码运行之前,edx和eax的值都被置零了。那么这段代码应该被循环运行了6次,edx也加上了6个数字的值。
8048d75: 83 fa 33 cmp $0x33,%edx
8048d78: 74 05 je 8048d7f <phase_5+0x44> #edx不为51就爆炸
8048d7a: e8 47 03 00 00 call 80490c6 <explode_bomb>
这最后一部分代码中,将edx的值与51相比较,如果不相等则炸弹爆炸。那么我们可以知道,上面那段循环的代码中,edx为6个数的和,和必须为51。剩下我们要解决的就是怎么把edx加到51了。
我们注意到这样一个地址:0x804a200
8048d66: 03 14 8d 00 a2 04 08 add 0x804a200(,%ecx,4),%edx
直接在gdb中查看(x/128bx 0x804a200)得到如下结果:
0x804a200 <array.2999>: 0x02 0x00 0x00 0x00 0x0a 0x00 0x00 0x00
0x804a208 <array.2999+8>: 0x06 0x00 0x00 0x00 0x01 0x00 0x00 0x00
0x804a210 <array.2999+16>: 0x0c 0x00 0x00 0x00 0x10 0x00 0x00 0x00
0x804a218 <array.2999+24>: 0x09 0x00 0x00 0x00 0x03 0x00 0x00 0x00
0x804a220 <array.2999+32>: 0x04 0x00 0x00 0x00 0x07 0x00 0x00 0x00
0x804a228 <array.2999+40>: 0x0e 0x00 0x00 0x00 0x05 0x00 0x00 0x00
0x804a230 <array.2999+48>: 0x0b 0x00 0x00 0x00 0x08 0x00 0x00 0x00
0x804a238 <array.2999+56>: 0x0f 0x00 0x00 0x00 0x0d 0x00 0x00 0x00
因此我们知道,edx加的数值只能是上面所列出来的,并且ecx的值对应一个数值,将上面的内容整理一下并和ecx的值整合起来,可以得到如下表(个人感觉像个数组):
下标 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
数值 2 10 6 1 12 16 9 3 4 7 14 5 11 8 15 13
我们的目标是让edx等于51,这里我们发现下标456789对应的数值加起来为:
12+16+9+3+4+7=51,这可能就是我们所找的解。
接下来,我们要让ecx的低4位等于4 5 6 7 8 9。ecx的值为ebx+eax对应的内容,其中ebx的值为我们的输入。那么ecx的值其实就是我们输入的6个字符,即:我们输入的6个字符的低四位应该分别是4 5 6 7 8 9,接下来我们去查ascii码表有如下发现:
二进制 | 十进制 | 十六进制 | 字符 |
---|---|---|---|
00110100 | 52 | 34 | 4 |
00110101 | 53 | 35 | 5 |
00110110 | 54 | 36 | 6 |
00110111 | 55 | 37 | 7 |
00111000 | 56 | 38 | 8 |
00111001 | 57 | 39 | 9 |
我们发现字符456789正好满足条件,在gdb中验证通过:
此外,这里还测试了其他满足条件的字符串,例如DEFGHI和defghi,这两个答案也能通过这一关,因此认为,凡是ascii码的后四位依次为4 5 6 7 8 9的字符串均符合条件。
DEFGHI和defghi的ascii码表如下:
二进制 | 十进制 | 十六进制 | 字符 |
---|---|---|---|
01000100 | 68 | 44 | D |
01000101 | 69 | 45 | E |
01000110 | 70 | 46 | F |
01000111 | 71 | 47 | G |
01001000 | 72 | 48 | H |
01001001 | 73 | 49 | I |
01100100 | 100 | 64 | d |
01100101 | 101 | 65 | e |
01100110 | 102 | 66 | f |
01100111 | 103 | 67 | g |
01101000 | 104 | 68 | h |
01101001 | 105 | 69 | i |
phase_6
这里由于代码太长,就不把整个代码放出来了。
我将整个代码分成了5个部分,每个部分之间换行相间隔,我们一个部分一个部分地来看。
第一部分
8048d84: 56 push %esi
8048d85: 53 push %ebx
8048d86: 83 ec 44 sub $0x44,%esp
8048d89: 8d 44 24 10 lea 0x10(%esp),%eax
8048d8d: 89 44 24 04 mov %eax,0x4(%esp)
8048d91: 8b 44 24 50 mov 0x50(%esp),%eax
8048d95: 89 04 24 mov %eax,(%esp)
8048d98: e8 5e 04 00 00 call 80491fb <read_six_numbers> #输入6个数字
第一个部分为输入,输入为6个数字,esp+0x10就是我们的输入,这里不多作解释了。
第二部分
8048d9d: be 00 00 00 00 mov $0x0,%esi
8048da2: 8b 44 b4 10 mov 0x10(%esp,%esi,4),%eax #第二个循环从这里开始
8048da6: 83 e8 01 sub $0x1,%eax
8048da9: 83 f8 05 cmp $0x5,%eax
8048dac: 76 05 jbe 8048db3 <phase_6+0x2f> #eax小于等于5跳转
8048dae: e8 13 03 00 00 call 80490c6 <explode_bomb>
8048db3: 83 c6 01 add $0x1,%esi #esi+1
8048db6: 83 fe 06 cmp $0x6,%esi
8048db9: 74 33 je 8048dee <phase_6+0x6a> #esi=6跳转,此时完成所有遍历
8048dbb: 89 f3 mov %esi,%ebx #!
8048dbd: 8b 44 9c 10 mov 0x10(%esp,%ebx,4),%eax #输入的第ebx(2-6)个数字?
8048dc1: 39 44 b4 0c cmp %eax,0xc(%esp,%esi,4) #和第1个数字比较?
8048dc5: 75 05 jne 8048dcc <phase_6+0x48>
8048dc7: e8 fa 02 00 00 call 80490c6 <explode_bomb> #相等爆炸
8048dcc: 83 c3 01 add $0x1,%ebx #不相等继续遍历
8048dcf: 83 fb 05 cmp $0x5,%ebx
8048dd2: 7e e9 jle 8048dbd <phase_6+0x39>
8048dd4: eb cc jmp 8048da2 <phase_6+0x1e> #又他妈一个循环
#6个数字两层循环,像冒泡那样比较两个数字是否相等,这里相当于约束了6个数字不能两两相等
第二个部分中包含一个双层循环。
这里先将esi赋0,这里eax中的内容为输入的(第一个)数字,因为后面eax将不断自减一并和5比较,如果大于5就会直接爆炸。
8048da9: 83 f8 05 cmp $0x5,%eax
8048dac: 76 05 jbe 8048db3 <phase_6+0x2f> #eax小于等于5跳转
8048dae: e8 13 03 00 00 call 80490c6 <explode_bomb>
这里是无符号数比较,说明输入为无符号数,即大于等于0。
由于num[i]-1<=5,所以num[i]<=6。
之后esi自加一,并不断与6比较,如果相等就跳转,分析完这一部分后我们会知道这里是跳出循环。接下来比较esp+0x10+4esi和esp+0xc+4esi,即比较两个相邻的输入的数字,如果相等就直接爆炸,不相等就继续遍历,一个内循环完成后就会切换到下一个数字继续遍历,具体的过程很想冒泡排序中的遍历方法。
总结一下就是:这一段代码限定了我们输入的6个数字。
1.这6个数字两两不能相等。
2.这六个数字都是在[0,6]这个区间内。
(嫌麻烦的话分析到这里就可以了,组合共7!种,虽然有点多,但应该还是能试,应该?)
第三部分
8048dd6: 8b 52 08 mov 0x8(%edx),%edx #esi=6,eax=0,ebx=6
8048dd9: 83 c0 01 add $0x1,%eax
8048ddc: 39 c8 cmp %ecx,%eax
8048dde: 75 f6 jne 8048dd6 <phase_6+0x52> #令eax=ecx
8048de0: 89 54 b4 28 mov %edx,0x28(%esp,%esi,4)
8048de4: 83 c3 01 add $0x1,%ebx
8048de7: 83 fb 06 cmp $0x6,%ebx
8048dea: 75 07 jne 8048df3 <phase_6+0x6f> #ebx不等于6跳转8048df3
8048dec: eb 1c jmp 8048e0a <phase_6+0x86>
8048dee: bb 00 00 00 00 mov $0x0,%ebx #跳转点
8048df3: 89 de mov %ebx,%esi #esi=ebx
8048df5: 8b 4c 9c 10 mov 0x10(%esp,%ebx,4),%ecx #ecx=第一个数字
8048df9: b8 01 00 00 00 mov $0x1,%eax
8048dfe: ba 3c c1 04 08 mov $0x804c13c,%edx
8048e03: 83 f9 01 cmp $0x1,%ecx
8048e06: 7f ce jg 8048dd6 <phase_6+0x52> #第ebx个数字>1跳转
8048e08: eb d6 jmp 8048de0 <phase_6+0x5c> #
这段代码中有一个可以查看的地址,用gdb查看(x/32a 0x804c13c)得到:
0x804c13c <node1>: 0x3db 0x1 0x804c148 <node2> 0x2fc
0x804c14c <node2+4>: 0x2 0x804c154 <node3> 0xb8 0x3
0x804c15c <node3+8>: 0x804c160 <node4> 0x3ba 0x4 0x804c16c <node5>
0x804c16c <node5>: 0xdd 0x5 0x804c178 <node6> 0x2c6
0x804c17c <node6+4>: 0x6 0x0 0x99 0x0
0x804c18c: 0x0 0x0 0x0 0x0
0x804c19c: 0x0 0x804a3b9 0x0 0x0
0x804c1ac <host_table+12>: 0x0 0x0 0x0 0x0
我的第一感觉就是这些玩意很像链表,数据以3个为一组,第一个数据推测为存储的值,即我们的输入值,第二个数据逐个加一,推测为节点的编号,第三个数据则为下一个节点的地址。我们访问下一个节点的地址,例如node1中的0x804c148,得到以下内容:
(gdb) x/32a 0x804c148
0x804c148 <node2>: 0x2fc 0x2 0x804c154 <node3> 0xb8
0x804c158 <node3+4>: 0x3 0x804c160 <node4> 0x3ba 0x4
0x804c168 <node4+8>: 0x804c16c <node5> 0xdd 0x5 0x804c178 <node6>
0x804c178 <node6>: 0x2c6 0x6 0x0
这证明了我们的猜想是正确的。那第三部分代码具体做了什么?
这部分代码用循环的方式根据输入数将链表中对应的第输入数个结点的地址复制到 esp+0x28 开始的栈中。
第四部分
8048e0a: 8b 5c 24 28 mov 0x28(%esp),%ebx #ebx等于6跳到这来,第一个输入的值对应的节点
8048e0e: 8b 44 24 2c mov 0x2c(%esp),%eax #eax,第二个输入的值对应的节点
8048e12: 89 43 08 mov %eax,0x8(%ebx)
8048e15: 8b 54 24 30 mov 0x30(%esp),%edx
8048e19: 89 50 08 mov %edx,0x8(%eax)
8048e1c: 8b 44 24 34 mov 0x34(%esp),%eax
8048e20: 89 42 08 mov %eax,0x8(%edx)
8048e23: 8b 54 24 38 mov 0x38(%esp),%edx
8048e27: 89 50 08 mov %edx,0x8(%eax)
8048e2a: 8b 44 24 3c mov 0x3c(%esp),%eax
8048e2e: 89 42 08 mov %eax,0x8(%edx)
8048e31: c7 40 08 00 00 00 00 movl $0x0,0x8(%eax)
8048e38: be 05 00 00 00 mov $0x5,%esi
#大概是构建链表?
这部分代码全是mov指令,猜测是在构架链表,不作细致分析。
第五部分
8048e3d: 8b 43 08 mov 0x8(%ebx),%eax
8048e40: 8b 10 mov (%eax),%edx
8048e42: 39 13 cmp %edx,(%ebx)
8048e44: 7e 05 jle 8048e4b <phase_6+0xc7> #ebx小于等于edx跳转
8048e46: e8 7b 02 00 00 call 80490c6 <explode_bomb> #否则爆炸
8048e4b: 8b 5b 08 mov 0x8(%ebx),%ebx
8048e4e: 83 ee 01 sub $0x1,%esi
8048e51: 75 ea jne 8048e3d <phase_6+0xb9>
8048e53: 83 c4 44 add $0x44,%esp
8048e56: 5b pop %ebx
8048e57: 5e pop %esi
8048e58: c3 ret
这里将ebx+0x8移到edx中,将ebx+0x8与ebx比较,如果ebx小于等于ebx+0x8,则跳转,否则爆炸。这里猜测ebx是链表的第一个值,ebx+8是链表的第二个值,对应的也是我们输入的第一个和第二个数字。之后就是将计数的esi减一,并将第二个和第三个输入移到ebx和ebx+0x8的位置上继续比较,直到esi=1(进入这部分代码时esi的值为5),即比较5次后跳出循环,代码结束。
总结一下,这部分代码要求我们的输入是递增的。最后我们回到第三部分得到的链表中:
0x804c13c <node1>: 0x3db 0x1 0x804c148 <node2> 0x2fc
0x804c14c <node2+4>: 0x2 0x804c154 <node3> 0xb8 0x3
0x804c15c <node3+8>: 0x804c160 <node4> 0x3ba 0x4 0x804c16c <node5>
0x804c16c <node5>: 0xdd 0x5 0x804c178 <node6> 0x2c6
0x804c17c <node6+4>: 0x6
整理一下可以得到:
编号 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
值 | 0x3db | 0x2fc | 0xb8 | 0x3ba | 0xdd | 0x2c6 |
将其按从小到大排序后得到的编号顺序为:
3 5 6 2 4 1
在gdb中验证此结果,通过:
secret_phase
我们在汇编代码中发现这样一句注释:
并且,我们在.c代码中可以看到:
/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */
由此,我们可以断定这个炸弹之中是存在隐藏关卡的。
我们来找找汇编代码中哪些地方调用了这个secret_parse函数,在txt文件中ctrl+f搜索secret_parse,发现phase_defused调用了这个函数,继续搜索phase_defused,发现在main函数中,之前的所有关卡调用完毕后都调用了这个函数。
下面是phase_defused的具体汇编代码,我们分成三个部分来分析:
part1
0804924b <phase_defused>:
804924b: 81 ec 8c 00 00 00 sub $0x8c,%esp
8049251: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8049257: 89 44 24 7c mov %eax,0x7c(%esp)
804925b: 31 c0 xor %eax,%eax
804925d: 83 3d cc c3 04 08 06 cmpl $0x6,0x804c3cc
8049264: 75 72 jne 80492d8 <phase_defused+0x8d>
这里0x804c3cc下的内容如果不等于6,直接跳转到最下方,则secret_phase无法进入。于是我们猜测,这里是需要我们完成前6关才能进入隐藏关卡。
接着我们在gdb的前6关中都各设置一个断点,依次查看这个地址下的内容,发现在第i关中的断点下查看这个地址,这个地址的值就是i。因此,我们的猜测应该是基本正确的:进入secret_phase的则先决条件是:完成phase 1 - 6,0x804c3cc代表的是关卡的完成数。
part_2
8049266: 8d 44 24 2c lea 0x2c(%esp),%eax
804926a: 89 44 24 10 mov %eax,0x10(%esp)
804926e: 8d 44 24 28 lea 0x28(%esp),%eax
8049272: 89 44 24 0c mov %eax,0xc(%esp)
8049276: 8d 44 24 24 lea 0x24(%esp),%eax
804927a: 89 44 24 08 mov %eax,0x8(%esp)
804927e: c7 44 24 04 a9 a3 04 movl $0x804a3a9,0x4(%esp)
8049285: 08
8049286: c7 04 24 d0 c4 04 08 movl $0x804c4d0,(%esp)
804928d: e8 de f5 ff ff call 8048870 <__isoc99_sscanf@plt>
8049292: 83 f8 03 cmp $0x3,%eax #判断返回值%eax是否等于3
8049295: 75 35 jne 80492cc <phase_defused+0x81>
根据经验,esp+0x2c,esp+0x28,esp+0x24这三个地址下保存的是我们的三个输入,接着问查看地址0x804a3a9下的内容,得到以下信息:
(gdb) x/s 0x804a3a9
0x804a3a9: "%d %d %s"
这说明我们进入隐藏关的输入应该是两个整数加一个字符串。我们接着查看下一个地址:
(gdb) print (char*) 0x804c4d0
$1 = 0x804c4d0 <input_strings+240> "10 5"
这个输出是我们第四关的答案,因此我们猜测,进入隐藏关是需要我们在第四关中输入两个整数和一个字符串,接下来我们要做的就是找到这个字符串。
接着代码调用了isoc99_sscanf@plt这个函数,如果返回值不等于3也会跳转到最下方。
part_3
8049297: c7 44 24 04 b2 a3 04 movl $0x804a3b2,0x4(%esp)
804929e: 08
804929f: 8d 44 24 2c lea 0x2c(%esp),%eax
80492a3: 89 04 24 mov %eax,(%esp)
80492a6: e8 09 fd ff ff call 8048fb4 <strings_not_equal>
80492ab: 85 c0 test %eax,%eax
80492ad: 75 1d jne 80492cc <phase_defused+0x81>
80492af: c7 04 24 78 a2 04 08 movl $0x804a278,(%esp)
80492b6: e8 45 f5 ff ff call 8048800 <puts@plt>
80492bb: c7 04 24 a0 a2 04 08 movl $0x804a2a0,(%esp)
80492c2: e8 39 f5 ff ff call 8048800 <puts@plt>
80492c7: e8 de fb ff ff call 8048eaa <secret_phase>
80492cc: c7 04 24 d8 a2 04 08 movl $0x804a2d8,(%esp)
80492d3: e8 28 f5 ff ff call 8048800 <puts@plt>
80492d8: 8b 44 24 7c mov 0x7c(%esp),%eax
80492dc: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
80492e3: 74 05 je 80492ea <phase_defused+0x9f>
80492e5: e8 e6 f4 ff ff call 80487d0 <__stack_chk_fail@plt>
80492ea: 81 c4 8c 00 00 00 add $0x8c,%esp
80492f0: c3 ret
80492f1: 90 nop
80492f2: 90 nop
80492f3: 90 nop
80492f4: 90 nop
80492f5: 90 nop
80492f6: 90 nop
80492f7: 90 nop
80492f8: 90 nop
80492f9: 90 nop
80492fa: 90 nop
80492fb: 90 nop
80492fc: 90 nop
80492fd: 90 nop
80492fe: 90 nop
80492ff: 90 nop
我们直接查看裸露在外的地址,得到以下信息:
(gdb) x/s 0x804a3b2
0x804a3b2: "DrEvil"
一个字符串,我们要找的字符串很可能就是它。代码接着调用了字符串比较函数,判断输入的字符串和esp中存的字符串是否相等,若不相等,则跳转到最下方,正好错过进入隐藏关;若相等,就可以进入隐藏关了。
我们来验证一下,在第四关输入10 5 DrEvi,其他不变,出现了一些新的对话:
这证明我们已经成功进入隐藏关卡了,我们接下来回到中secret_phase来分析。
以下是secret_phase的具体汇编代码及分析:
08048eaa <secret_phase>:
8048eaa: 53 push %ebx
8048eab: 83 ec 18 sub $0x18,%esp
8048eae: e8 3a 02 00 00 call 80490ed <read_line> #读取字符串?
8048eb3: c7 44 24 08 0a 00 00 movl $0xa,0x8(%esp)
8048eba: 00
8048ebb: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
8048ec2: 00
8048ec3: 89 04 24 mov %eax,(%esp)
8048ec6: e8 15 fa ff ff call 80488e0 <strtol@plt> #将字符串转换为整型num,存储在eax中
8048ecb: 89 c3 mov %eax,%ebx
8048ecd: 8d 40 ff lea -0x1(%eax),%eax
8048ed0: 3d e8 03 00 00 cmp $0x3e8,%eax
8048ed5: 76 05 jbe 8048edc <secret_phase+0x32> #num-1>1000爆炸,所以输入的数字必须小于等于1001
8048ed7: e8 ea 01 00 00 call 80490c6 <explode_bomb>
8048edc: 89 5c 24 04 mov %ebx,0x4(%esp) #参数-输入
8048ee0: c7 04 24 88 c0 04 08 movl $0x804c088,(%esp) #参数-36
8048ee7: e8 6d ff ff ff call 8048e59 <fun7>
8048eec: 83 f8 03 cmp $0x3,%eax #将fun7的返回值%eax与3比较
8048eef: 74 05 je 8048ef6 <secret_phase+0x4c> #如果相等就跳转,否则爆炸
8048ef1: e8 d0 01 00 00 call 80490c6 <explode_bomb> #即:返回值必须等于3
8048ef6: c7 04 24 b4 a1 04 08 movl $0x804a1b4,(%esp) #"Wow! You've defused the secret stage!"
8048efd: e8 fe f8 ff ff call 8048800 <puts@plt>
8048f02: e8 44 03 00 00 call 804924b <phase_defused>
8048f07: 83 c4 18 add $0x18,%esp
8048f0a: 5b pop %ebx
8048f0b: c3 ret
8048f0c: 90 nop
8048f0d: 90 nop
8048f0e: 90 nop
8048f0f: 90 nop
这里先调用read_line函数,读取字符串。然后调用strtol函数,将字符串转换为整型num,并存储在eax中,之后比较num-1>1000(0x3e8),如果成立则会爆炸,所以输入的数字必须小于等于1001。接着把num存在esp+4中,把地址0x804c088中的值存在esp中。我们查看一下这个地址中的内容为:0x00000024。然后代码调用了func7这个函数,那么我们输入的字符串的整型值和36(0x00000024)均为func7的参数。
如果我们多查看一下地址0x804c088中的内容,会有如下发现:
(gdb) x/150 0x804c088
0x804c088 <n1>: 0x00000024 0x0804c094 0x0804c0a0 0x00000008
0x804c098 <n21+4>: 0x0804c0c4 0x0804c0ac 0x00000032 0x0804c0b8
0x804c0a8 <n22+8>: 0x0804c0d0 0x00000016 0x0804c118 0x0804c100
0x804c0b8 <n33>: 0x0000002d 0x0804c0dc 0x0804c124 0x00000006
0x804c0c8 <n31+4>: 0x0804c0e8 0x0804c10c 0x0000006b 0x0804c0f4
0x804c0d8 <n34+8>: 0x0804c130 0x00000028 0x00000000 0x00000000
0x804c0e8 <n41>: 0x00000001 0x00000000 0x00000000 0x00000063
0x804c0f8 <n47+4>: 0x00000000 0x00000000 0x00000023 0x00000000
0x804c108 <n44+8>: 0x00000000 0x00000007 0x00000000 0x00000000
0x804c118 <n43>: 0x00000014 0x00000000 0x00000000 0x0000002f
0x804c128 <n46+4>: 0x00000000 0x00000000 0x000003e9 0x00000000
0x804c138 <n48+8>: 0x00000000 0x000003db 0x00000001 0x0804c148
0x804c148 <node2>: 0x000002fc 0x00000002 0x0804c154 0x000000b8
0x804c158 <node3+4>: 0x00000003 0x0804c160 0x000003ba 0x00000004
0x804c168 <node4+8>: 0x0804c16c 0x000000dd 0x00000005 0x0804c178
0x804c178 <node6>: 0x000002c6 0x00000006 0x00000000 0x00000099
这很好,但我们先不管他,接着看代码。func7这个函数的返回值保存在eax中,返回值不等于3直接爆炸。好了,我们最后查看一下裸露在外的地址0x804a1b4,得到以下内容:
"Wow! You've defused the secret stage!"
很明显,这是成功拆完炸弹后的对话。总结一下,我们要输入一个字符串,这个字符串被转换成整型之后和36一起作为参数传入func7中,如果返回值是3,则成功。
那么我们接着来看func7中的内容,以下是func7的具体汇编代码及分析:
08048e59 <fun7>:
8048e59: 53 push %ebx
8048e5a: 83 ec 18 sub $0x18,%esp
8048e5d: 8b 54 24 20 mov 0x20(%esp),%edx #设为x
8048e61: 8b 4c 24 24 mov 0x24(%esp),%ecx #设为y
8048e65: 85 d2 test %edx,%edx
8048e67: 74 37 je 8048ea0 <fun7+0x47> #edx为0,eax返回0xffffffff
8048e69: 8b 1a mov (%edx),%ebx
8048e6b: 39 cb cmp %ecx,%ebx #两个数比较
8048e6d: 7e 13 jle 8048e82 <fun7+0x29> #小于等于跳转
8048e6f: 89 4c 24 04 mov %ecx,0x4(%esp)
8048e73: 8b 42 04 mov 0x4(%edx),%eax
8048e76: 89 04 24 mov %eax,(%esp)
8048e79: e8 db ff ff ff call 8048e59 <fun7> #大于调用递归
8048e7e: 01 c0 add %eax,%eax #eax=eax*2
8048e80: eb 23 jmp 8048ea5 <fun7+0x4c>
8048e82: b8 00 00 00 00 mov $0x0,%eax #eax=0
8048e87: 39 cb cmp %ecx,%ebx #两个数比较
8048e89: 74 1a je 8048ea5 <fun7+0x4c> #相等跳转
8048e8b: 89 4c 24 04 mov %ecx,0x4(%esp)
8048e8f: 8b 42 08 mov 0x8(%edx),%eax
8048e92: 89 04 24 mov %eax,(%esp)
8048e95: e8 bf ff ff ff call 8048e59 <fun7> #小于调用递归
8048e9a: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax #eax=eax*2+1
8048e9e: eb 05 jmp 8048ea5 <fun7+0x4c>
8048ea0: b8 ff ff ff ff mov $0xffffffff,%eax
8048ea5: 83 c4 18 add $0x18,%esp #和第四题的逻辑很像
8048ea8: 5b pop %ebx
8048ea9: c3 ret
可以看到,这个函数的逻辑和第四题中func4的逻辑简直是一模一样,那么我们直接来猜,怎么才能使返回值为3。
其实不难猜出:
最底层:return 0
0*2+1=1
1*2+1=3
这样就得到了返回值。好,接下来我们回过去来看那个地址下的一大串东西
(gdb) x/150 0x804c088
0x804c088 <n1>: 0x00000024 0x0804c094 0x0804c0a0 0x00000008
0x804c098 <n21+4>: 0x0804c0c4 0x0804c0ac 0x00000032 0x0804c0b8
0x804c0a8 <n22+8>: 0x0804c0d0 0x00000016 0x0804c118 0x0804c100
0x804c0b8 <n33>: 0x0000002d 0x0804c0dc 0x0804c124 0x00000006
0x804c0c8 <n31+4>: 0x0804c0e8 0x0804c10c 0x0000006b 0x0804c0f4
0x804c0d8 <n34+8>: 0x0804c130 0x00000028 0x00000000 0x00000000
0x804c0e8 <n41>: 0x00000001 0x00000000 0x00000000 0x00000063
0x804c0f8 <n47+4>: 0x00000000 0x00000000 0x00000023 0x00000000
0x804c108 <n44+8>: 0x00000000 0x00000007 0x00000000 0x00000000
0x804c118 <n43>: 0x00000014 0x00000000 0x00000000 0x0000002f
0x804c128 <n46+4>: 0x00000000 0x00000000 0x000003e9 0x00000000
0x804c138 <n48+8>: 0x00000000 0x000003db 0x00000001 0x0804c148
0x804c148 <node2>: 0x000002fc 0x00000002 0x0804c154 0x000000b8
0x804c158 <node3+4>: 0x00000003 0x0804c160 0x000003ba 0x00000004
0x804c168 <node4+8>: 0x0804c16c 0x000000dd 0x00000005 0x0804c178
0x804c178 <node6>: 0x000002c6 0x00000006 0x00000000 0x00000099
这些数据以3个为一组,第一个为值,第二,第三个都是地址,这种结构很像二叉树。我们直接大胆假设这就是二叉树,那么0x00000024就是树的头节点的值,要使返回值是3,需要两次2*x+1,也就是两次的向右查找,也就是顺序遍历的第七个节点。我们找到第七组数据,它存储的值是0x0000006b,也就是107,我们来验证一下:
验证通过,我们的假设是正确的。
最后,我们来具体分析一下怎么得到107。
首先来到二叉树的首地址0x804c088对应的数据:0x00000024(36),因为36需要小于x,才能得到 eax = eax * 2+1,那么指针值应该为edx+8(加载右结点),指针值为0x0804c0a0,查看得到值为0x00000032(50)。 来到0x00000032对应的位置,我们想要数据eax = eax*2 + 1,则50需要小于等于x,那么指针值应该为0x0804c0a0+48(加载右结点),指针值为0x0804c0d0,查看得到的值为0x0000006b(107),最后我们得到了数据107,当我们输入107的时候,因为其对应的两个指针所处位置对应头部数据的值相等,所以eax=0。
所以107为答案。
四.实验总结
个人感觉前三关都挺简单,第四关和第六关最难,隐藏关由于有了第四关的经验减少了不少难度,这些关卡分别考察了汇编代码中的字符串存储,循环,跳转表,递归,字符的ascii码表示,链表,二叉树。最后,这p实验是真的耗时间。