紧接上篇文章 ,这篇文章将分析CSAPP配套课程ICS(Introduction to Computer Science)中,Bomb Lab的第二部分——Phase 3和Phase 4。
Phase 3 有了上次的经验,现在直接查看phase_3
的反汇编代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 0000000000400f43 <phase_3>: 400f43: 48 83 ec 18 sub $0x18,%rsp 400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx 400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx 400f51: be cf 25 40 00 mov $0x4025cf,%esi 400f56: b8 00 00 00 00 mov $0x0,%eax 400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt> 400f60: 83 f8 01 cmp $0x1,%eax 400f63: 7f 05 jg 400f6a <phase_3+0x27> 400f65: e8 d0 04 00 00 callq 40143a <explode_bomb> 400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp) 400f6f: 77 3c ja 400fad <phase_3+0x6a> 400f71: 8b 44 24 08 mov 0x8(%rsp),%eax 400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8) 400f7c: b8 cf 00 00 00 mov $0xcf,%eax 400f81: eb 3b jmp 400fbe <phase_3+0x7b> 400f83: b8 c3 02 00 00 mov $0x2c3,%eax 400f88: eb 34 jmp 400fbe <phase_3+0x7b> 400f8a: b8 00 01 00 00 mov $0x100,%eax 400f8f: eb 2d jmp 400fbe <phase_3+0x7b> 400f91: b8 85 01 00 00 mov $0x185,%eax 400f96: eb 26 jmp 400fbe <phase_3+0x7b> 400f98: b8 ce 00 00 00 mov $0xce,%eax 400f9d: eb 1f jmp 400fbe <phase_3+0x7b> 400f9f: b8 aa 02 00 00 mov $0x2aa,%eax 400fa4: eb 18 jmp 400fbe <phase_3+0x7b> 400fa6: b8 47 01 00 00 mov $0x147,%eax 400fab: eb 11 jmp 400fbe <phase_3+0x7b> 400fad: e8 88 04 00 00 callq 40143a <explode_bomb> 400fb2: b8 00 00 00 00 mov $0x0,%eax 400fb7: eb 05 jmp 400fbe <phase_3+0x7b> 400fb9: b8 37 01 00 00 mov $0x137,%eax 400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax 400fc2: 74 05 je 400fc9 <phase_3+0x86> 400fc4: e8 71 04 00 00 callq 40143a <explode_bomb> 400fc9: 48 83 c4 18 add $0x18,%rsp 400fcd: c3 retq a
先观察调用sscanf
函数前的指令。看起来套路与read_six_numbers
函数的相同,只用关心匹配的格式。查看地址0x4025cf处的字符串:
1 2 (gdb) x /s 0x4025cf 0x4025cf: "%d %d"
这样前九行指令的作用就清晰了:从输入的字符串中获得两个int型整数。接下来看看用这两个数干什么:
1 2 3 4 5 6 ··· 400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp) 400f6f: 77 3c ja 400fad <phase_3+0x6a> ··· 400fad: e8 88 04 00 00 callq 40143a <explode_bomb> ···
第一行比较第一个整数,使用的是无符号数的跳转指令,如果该数大于7(负数被当作无符号数时也大于7),则炸弹爆炸。推断出第一个数小于等于7,大于等于0。继续:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 400f71: 8b 44 24 08 mov 0x8(%rsp),%eax 400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8) 400f7c: b8 cf 00 00 00 mov $0xcf,%eax 400f81: eb 3b jmp 400fbe <phase_3+0x7b> 400f83: b8 c3 02 00 00 mov $0x2c3,%eax 400f88: eb 34 jmp 400fbe <phase_3+0x7b> 400f8a: b8 00 01 00 00 mov $0x100,%eax 400f8f: eb 2d jmp 400fbe <phase_3+0x7b> 400f91: b8 85 01 00 00 mov $0x185,%eax 400f96: eb 26 jmp 400fbe <phase_3+0x7b> 400f98: b8 ce 00 00 00 mov $0xce,%eax 400f9d: eb 1f jmp 400fbe <phase_3+0x7b> 400f9f: b8 aa 02 00 00 mov $0x2aa,%eax 400fa4: eb 18 jmp 400fbe <phase_3+0x7b> 400fa6: b8 47 01 00 00 mov $0x147,%eax 400fab: eb 11 jmp 400fbe <phase_3+0x7b> 400fad: e8 88 04 00 00 callq 40143a <explode_bomb> 400fb2: b8 00 00 00 00 mov $0x0,%eax 400fb7: eb 05 jmp 400fbe <phase_3+0x7b> 400fb9: b8 37 01 00 00 mov $0x137,%eax 400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax ···
这是一个典型的switch
语句的跳转表 实现(实际上我没有一眼认出):在这里,第一个整数决定了访问跳转表的哪一个内存地址,进而决定了跳转到哪一条指令。找到跳转表的首地址0x402470,查看后面18个字节的内容:
1 2 3 4 5 6 (gdb) x /18x 0x402470 0x402470: 0x00400f7c 0x00000000 0x00400fb9 0x00000000 0x402480: 0x00400f83 0x00000000 0x00400f8a 0x00000000 0x402490: 0x00400f91 0x00000000 0x00400f98 0x00000000 0x4024a0: 0x00400f9f 0x00000000 0x00400fa6 0x00000000 0x4024b0 <array.3449>: 0x7564616d 0x73726569
这样就建立起了第一个整数数值(0~7)与指令间的映射关系。这些指令做的事情都一致:向eax寄存器中传送常量值。
有趣的是,当第一个整数为1时,程序会跳转到接近switch
语句结束的位置,这样做减少了一个跳转指令。(相对于其它整数值)
最后来看末尾的几条指令:
1 2 3 4 5 400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax 400fc2: 74 05 je 400fc9 <phase_3+0x86> 400fc4: e8 71 04 00 00 callq 40143a <explode_bomb> 400fc9: 48 83 c4 18 add $0x18,%rsp 400fcd: c3 retq a
第二个整数终于出现。意思很简单,如果eax寄存器中的值与第二个整数不相同,炸弹爆炸。
结合上面的全部分析可以发现,输入的第一个整数与第二个整数之间要有一一对应关系,而这个对应关系到switch
语句中找任意一个即可(记得把十六进制转换为十进制)。输入后通过:
1 2 3 That's number 2. Keep going! 1 311 Halfway there!
Phase 4 phase_4
函数的反汇编代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 000000000040100c <phase_4>: 40100c: 48 83 ec 18 sub $0x18,%rsp 401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx 401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx 40101a: be cf 25 40 00 mov $0x4025cf,%esi 40101f: b8 00 00 00 00 mov $0x0,%eax 401024: e8 c7 fb ff ff callq 400bf0 <__isoc99_sscanf@plt> 401029: 83 f8 02 cmp $0x2,%eax 40102c: 75 07 jne 401035 <phase_4+0x29> 40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp) 401033: 76 05 jbe 40103a <phase_4+0x2e> 401035: e8 00 04 00 00 callq 40143a <explode_bomb> 40103a: ba 0e 00 00 00 mov $0xe,%edx 40103f: be 00 00 00 00 mov $0x0,%esi 401044: 8b 7c 24 08 mov 0x8(%rsp),%edi 401048: e8 81 ff ff ff callq 400fce <func4> 40104d: 85 c0 test %eax,%eax 40104f: 75 07 jne 401058 <phase_4+0x4c> 401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) 401056: 74 05 je 40105d <phase_4+0x51> 401058: e8 dd 03 00 00 callq 40143a <explode_bomb> 40105d: 48 83 c4 18 add $0x18,%rsp 401061: c3 retq
0C~35地址的指令作用与phase_3
前面部分的相似,从输入中解析出两个整数,但第一个整数不能大于14。
然后调用func4
函数,第一个整数的值、0及14被传入。
最后如果返回值为等于0,且第二个整数为0,该阶段通过。剩下的任务只有看看func4
函数了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0000000000400fce <func4>: 400fce: 48 83 ec 08 sub $0x8,%rsp 400fd2: 89 d0 mov %edx,%eax 400fd4: 29 f0 sub %esi,%eax 400fd6: 89 c1 mov %eax,%ecx 400fd8: c1 e9 1f shr $0x1f,%ecx 400fdb: 01 c8 add %ecx,%eax 400fdd: d1 f8 sar %eax 400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx 400fe2: 39 f9 cmp %edi,%ecx 400fe4: 7e 0c jle 400ff2 <func4+0x24> 400fe6: 8d 51 ff lea -0x1(%rcx),%edx 400fe9: e8 e0 ff ff ff callq 400fce <func4> 400fee: 01 c0 add %eax,%eax 400ff0: eb 15 jmp 401007 <func4+0x39> 400ff2: b8 00 00 00 00 mov $0x0,%eax 400ff7: 39 f9 cmp %edi,%ecx 400ff9: 7d 0c jge 401007 <func4+0x39> 400ffb: 8d 71 01 lea 0x1(%rcx),%esi 400ffe: e8 cb ff ff ff callq 400fce <func4> 401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax 401007: 48 83 c4 08 add $0x8,%rsp 40100b: c3 retq
我们不妨假设func4
的函数声明是这样的:
1 int func4 (int i, int j, int k) ;
逐步分析(这里就不像之前这么啰嗦了)后得出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int fake_func4 (int i, int j, int k) { int ret = 0 ; int x = (k - j) >> 1 ; int y = x + j; if (i > y) { j = y + 1 ; ret = fake_func4(i, j, k) * 2 + 1 ; } else if (i < y) { k = y - 1 ; ret = fake_func4(i, j, k) * 2 ; } return ret; }
看上去有点乱,实际上我们只要让第一次进入函数时i==y
就行,不用管哪些花里胡哨的递归。
有趣的一个地方是,下面的逻辑右移操作会将ecx寄存器清零。
经过简单的数学运算得出,第一个整数应该为7。(我这里忽略了递归,也许有其他可能?)输入,通过:
1 2 3 Halfway there! 7 0 So you got that one. Try this one.
总结
有了之前的经验,这两个phase就不是很难了。
这次的分析过程中调试很少。
静下心慢慢看是最重要的。
下面是开了-O优化的fake_func4
函数的部分汇编代码,不知道对不对劲,也不知道怎么测试(内联汇编?)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 fake_func4: .LFB0: .cfi_startproc endbr64 subq $8, %rsp .cfi_def_cfa_offset 16 movl %edx, %ecx subl %esi, %ecx sarl %ecx addl %esi, %ecx cmpl %edi, %ecx jl .L6 movl $0, %eax jg .L7 .L1: addq $8, %rsp .cfi_remember_state .cfi_def_cfa_offset 8 ret .L6: .cfi_restore_state leal 1(%rcx), %esi call fake_func4 leal 1(%rax,%rax), %eax jmp .L1 .L7: leal -1(%rcx), %edx call fake_func4 addl %eax, %eax jmp .L1 .cfi_endproc