本篇博客将会展示 CSAPP 之 BombLab 的拆弹过程,粉碎 Dr.Evil 的邪恶阴谋。Dr.Evil 的替身,杀手皇后,总共设置了 6 个炸弹,每个炸弹对应一串字符串,如果字符串错误,炸弹就会被引爆??,如下图所示:

字符串的长度未知,所以暴力破解是不可取的,也就是说这个实验就是要逼着拆弹小分队将 bomb 可执行文件反汇编,根据汇编代码推测出每个炸弹对应的字符串。在终端输入 objdump -d bomb > bomb.asm ,就可以将汇编代码写入 bomb.asm 文件中,方便后续分析。同时,Dr.Evil 还提供了 bomb.c 源文件,通过它可以看到炸弹程序的主要结构,代码如下:
/*************************************************************************** * Dr. Evil's Insidious Bomb, Version 1.1 * Copyright 2011, Dr. Evil Incorporated. All rights reserved. * * LICENSE: * * Dr. Evil Incorporated (the PERPETRATOR) hereby grants you (the * VICTIM) explicit permission to use this bomb (the BOMB). This is a * time limited license, which expires on the death of the VICTIM. * The PERPETRATOR takes no responsibility for damage, frustration, * insanity, bug-eyes, carpal-tunnel syndrome, loss of sleep, or other * harm to the VICTIM. Unless the PERPETRATOR wants to take credit, * that is. The VICTIM may not distribute this bomb source code to * any enemies of the PERPETRATOR. No VICTIM may debug, * reverse-engineer, run "strings" on, decompile, decrypt, or use any * other technique to gain knowledge of and defuse the BOMB. BOMB * proof clothing may not be worn when handling this program. The * PERPETRATOR will not apologize for the PERPETRATOR's poor sense of * humor. This license is null and void where the BOMB is prohibited * by law. ***************************************************************************/#include <stdio.h>#include <stdlib.h>#include "support.h"#include "phases.h"/* * Note to self: Remember to erase this file so my victims will have no * idea what is going on, and so they will all blow up in a * spectaculary fiendish explosion. -- Dr. Evil */FILE *infile;int main(int argc, char *argv[]){ char *input; /* Note to self: remember to port this bomb to Windows and put a * fantastic GUI on it. */ /* When run with no arguments, the bomb reads its input lines * from standard input. */ if (argc == 1) { infile = stdin; } /* When run with one argument <file>, the bomb reads from <file> * until EOF, and then switches to standard input. Thus, as you * defuse each phase, you can add its defusing string to <file> and * avoid having to retype it. */ else if (argc == 2) { if (!(infile = fopen(argv[1], "r"))) { printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]); exit(8); } } /* You can't call the bomb with more than 1 command line argument. */ else { printf("Usage: %s [<input_file>]\n", argv[0]); exit(8); } /* Do all sorts of secret stuff that makes the bomb harder to defuse. */ initialize_bomb(); printf("Welcome to my fiendish little bomb. You have 6 phases with\n"); printf("which to blow yourself up. Have a nice day!\n"); /* 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! */ return 0;}不过我们也可以使用下述指令在终端查看 bomb.c:
$ gdb bomb(gdb) l可以看到六个炸弹分别对应着 phase_1() 到 phase_6() 函数,有了这些信息就可以着手分析汇编代码了。
在汇编代码中搜索 phase_1,可以看到 phase_1 共出现三次,左侧是 phase_1() 被调用,右侧是 phase_1() 的代码:

可以看到 phase_1 中把 $0x402400 写到了寄存器 %rsi 中,接着调用了 strings_not_equal 函数,相当于 strings_not_equal(%rdi, 0x402400),如果字符串不相等即 %rax 为 1 时,第一炸弹就会被引爆。容易猜到 0x402400 应该是正确字符串的起始地址,而 %rdi 中存的应该是拆弹小分队输入的字符串的起始地址。可以使用 GDB 来验证一下这个猜想:

先在 phase_1 的第三行 callq 401338 <strings_not_equal> 处打上断点,然后运行程序并输入 zhiyiYo,使用 display /x $rdi 查看 %rdi 的值 0x603780,最后使用 x/8c 0x603780 查看从该地址开始的8个字符,发现和我们输入的一样(多了结束符),验证了上述猜想。
由于我们还不知道真实字符串的长度,只知道它的起始地址是 0x402400,所以可以试下长一点的,只要看到 \000 就能说明字符串结束了。
(gdb) x/64c 0x4024000x402400: 66 'B' 111 'o' 114 'r' 100 'd' 101 'e' 114 'r' 32 ' ' 114 'r'0x402408: 101 'e' 108 'l' 97 'a' 116 't' 105 'i' 111 'o' 110 'n' 115 's'0x402410: 32 ' ' 119 'w' 105 'i' 116 't' 104 'h' 32 ' ' 67 'C' 97 'a'0x402418: 110 'n' 97 'a' 100 'd' 97 'a' 32 ' ' 104 'h' 97 'a' 118 'v'0x402420: 101 'e' 32 ' ' 110 'n' 101 'e' 118 'v' 101 'e' 114 'r' 32 ' '0x402428: 98 'b' 101 'e' 101 'e' 110 'n' 32 ' ' 98 'b' 101 'e' 116 't'0x402430: 116 't' 101 'e' 114 'r' 46 '.' 0 '\000' 0 '\000' 0 '\000' 0 '\000'0x402438: 87 'W' 111 'o' 119 'w' 33 '!' 32 ' ' 89 'Y' 111 'o' 117 'u'将上述字符连接得到 Border relations with Canada have never been better.,这个就是第一炸弹的正确答案。

phase_2 的汇编代码如下所示:
0000000000400efc <phase_2>: 400efc: 55 push %rbp 400efd: 53 push %rbx 400efe: 48 83 ec 28 sub $0x28,%rsp 400f02: 48 89 e6 mov %rsp,%rsi 400f05: e8 52 05 00 00 callq 40145c <read_six_numbers> 400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) 400f0e: 74 20 je 400f30 <phase_2+0x34> 400f10: e8 25 05 00 00 callq 40143a <explode_bomb> 400f15: eb 19 jmp 400f30 <phase_2+0x34> 400f17: 8b 43 fc mov -0x4(%rbx),%eax 400f1a: 01 c0 add %eax,%eax 400f1c: 39 03 cmp %eax,(%rbx) 400f1e: 74 05 je 400f25 <phase_2+0x29> 400f20: e8 15 05 00 00 callq 40143a <explode_bomb> 400f25: 48 83 c3 04 add $0x4,%rbx 400f29: 48 39 eb cmp %rbp,%rbx 400f2c: 75 e9 jne 400f17 <phase_2+0x1b> 400f2e: eb 0c jmp 400f3c <phase_2+0x40> 400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx 400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp 400f3a: eb db jmp 400f17 <phase_2+0x1b> 400f3c: 48 83 c4 28 add $0x28,%rsp 400f40: 5b pop %rbx 400f41: 5d pop %rbp 400f42: c3 retq可以看到 phase_2 显示进行了被调用者保护,然后将 %rsp 栈指针减小 28 以腾出空间并将 %rsp 的值赋给 %rsi。接着调用了 read_six_numbers 函数,从名字可以看出这个函数应该用来读入 6 个数字,具体代码为:
000000000040145c <read_six_numbers>: 40145c: 48 83 ec 18 sub $0x18,%rsp 401460: 48 89 f2 mov %rsi,%rdx 401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx 401467: 48 8d 46 14 lea 0x14(%rsi),%rax 40146b: 48 89 44 24 08 mov %rax,0x8(%rsp) 401470: 48 8d 46 10 lea 0x10(%rsi),%rax 401474: 48 89 04 24 mov %rax,(%rsp) 401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9 40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8 401480: be c3 25 40 00 mov $0x4025c3,%esi 401485: b8 00 00 00 00 mov $0x0,%eax 40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt> 40148f: 83 f8 05 cmp $0x5,%eax 401492: 7f 05 jg 401499 <read_six_numbers+0x3d> 401494: e8 a1 ff ff ff callq 40143a <explode_bomb> 401499: 48 83 c4 18 add $0x18,%rsp 40149d: c3 retq从 read_six_numbers 可以看出,6个数字的地址分别为 %rsp、%rsp+0x4、%rsp+0x8、%rsp+0xc、%rsp+0x10 和%rsp+0x14。调用完 read_six_numbers 之后,phase_2 又将 (%rsp) 和 0x1 进行比较,如果不相等就引爆第二炸弹。接下来的代码可以翻译为:
rbx = rsp + 0x4;rbp = rsp + 0x18;while (rbx != rbp) { rax = 2 * M[rbx - 0x4]; if (rax != M[rbx]) { explode_bomb(); } rbx += 0x4;}说明读入的 6 个数字应该是一个等比数列,且 a[n] = 2*a[n-1],由于 (%rsp) 为 1,后续的几个数字就是 2、4、8、16 和 32。测试一下发现第二炸弹确实被成功拆除了:

phase_3 的汇编代码如下所示:
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可以看到 phase_3 先读入了一个数字,接着跳转到 0x400f6a, 将 (%rsp + 0x8) 和 0x7 进行比较,如果比它大就引爆炸弹,其中 (%rsp + 0x8) 的值就是我们输入的第一个数字。可以运行 GDB 来验证一下这个猜想,将断点打在 0x400f6a 处,对第三炸弹输入 8 9,结果如下图所示:

如果输入的第一个数字小于等于 7,就暂时不会引爆炸弹。接着运行了 jmpq *0x402470(,%rax,8) 指令,这个指令的作用就是让程序跳转到 M[0x402470 + %rax * 8] 处,此处 %rax 就是输入的第一个数字,由于每个地址 64 位为 8 个字节,所以将 %rax 乘上了 8。下图显示了跳转之前 %rax 的值和跳转表的 8 个地址:

可以看到输入的第一个数字 0~7 分别对应着:0x400f7c、0x400fb9、0x400f83、0x400f8a、0x400f91、0x400f98、0x400f9f 和 0x400fa6。在 phase_3 中,上述地址都是一条对 %rax 进行赋值的指令,赋的值的十进制为 207、311、707、256、389、206、682 和 327。接着将 (%rsp + 0xc) 和 %rax 进行比较,如果不相等就引爆炸弹,否则退出 phase_3,解除炸弹。也就是说第三炸弹的前 2 个数字对应着 8 种正确答案(后面的字符串就无所谓了),当前两个数字为 1 311 时运行结果如下图所示:

phase_4 的汇编代码如下:
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可以看到,phase_4 读入了两个数字(并且只允许输入两个),然后判断输入的第一个数字是否小于等于 14,大于 14 就会引爆炸弹。接着调用了 func4(输入的第一个数字, 0, 14) 。从 func4() 返回后判断 %eax 是否为 0,不为 0 就引爆炸弹,如果 (%rsp + 0xc) 也就是输入的第二个数字不为 0 也会引爆炸弹。为了顺利拆除第四炸弹,有必要分析一下 func4 的执行流程。
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 retqfunc4 的操作可以使用如下的代码来描述:
do { rsp -= 0x8; rax = rdx; rax -= rsi; // 接下来的三行代码感觉没啥大用,因为正数逻辑右移 31 位之后一定为 0 rcx = rax; rcx >>= 31 // 此处为逻辑右移 rax += rcx; rax >>= 1; // rax = rax / 2 rcx = rax + rsi; if (rcx <= rdi) { rax = 0; // 其实就是两个相等 if (rcx >= rdi) { rsp += 0x8; continue; } rsi = rcx + 1; // 下面这条语句应该在 rsp -= 0x8 后执行 // M[rsp] = 0x401003; } else { rdx = rcx - 1; // 下面这条语句应该在 rsp -= 0x8 后执行 // M[rsp] = 0x400fee; } } while (rsp != 0x40104d)有上述伪代码可以看到,要想返回的时候 %rax 为 0,就一定不能走到第 23 行,不然执行完 retq 语句后会将 M[%rsp] 的值也就是 0x401003 送到 %rip,程序跳到 lea 0x1(%rcx),%esi 继续执行,之后 %rax 不可能变成 0。在第 23 行没被执行的情况下,%rsi 的值一直都是 0,%rcx 的值可以为下面几种:
%rdi = 7 时可以直接返回 phase_4 并解除炸弹%rdi = 3 时会跳到 add %eax,%eax 1 次才返回 phase_4 并解除炸弹%rdi = 1 时会跳到 add %eax,%eax 2 次才返回 phase_4 并解除炸弹%rdi = 0 时会跳到 add %eax,%eax 3 次才返回 phase_4 并解除炸弹来测试一下最后一种情况,因为它最复杂:
(gdb) b *0x40100bBreakpoint 1 at 0x40100b(gdb) rStarting program: /home/zhiyiyo/Documents/CSAPP/bomblab/bomb Welcome to my fiendish little bomb. You have 6 phases withwhich to blow yourself up. Have a nice day!Border relations with Canada have never been better.Phase 1 defused. How about the next one?1 2 4 8 16 32That's number 2. Keep going!0 207Halfway there!0 0Breakpoint 1, 0x000000000040100b in func4 ()(gdb) disassembleDump of assembler code for function func4: 0x0000000000400fce <+0>: sub $0x8,%rsp 0x0000000000400fd2 <+4>: mov %edx,%eax 0x0000000000400fd4 <+6>: sub %esi,%eax 0x0000000000400fd6 <+8>: mov %eax,%ecx 0x0000000000400fd8 <+10>: shr $0x1f,%ecx 0x0000000000400fdb <+13>: add %ecx,%eax 0x0000000000400fdd <+15>: sar %eax 0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx 0x0000000000400fe2 <+20>: cmp %edi,%ecx 0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36> 0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx 0x0000000000400fe9 <+27>: callq 0x400fce <func4> 0x0000000000400fee <+32>: add %eax,%eax 0x0000000000400ff0 <+34>: jmp 0x401007 <func4+57> 0x0000000000400ff2 <+36>: mov $0x0,%eax 0x0000000000400ff7 <+41>: cmp %edi,%ecx 0x0000000000400ff9 <+43>: jge 0x401007 <func4+57> 0x0000000000400ffb <+45>: lea 0x1(%rcx),%esi 0x0000000000400ffe <+48>: callq 0x400fce <func4> 0x0000000000401003 <+53>: lea 0x1(%rax,%rax,1),%eax 0x0000000000401007 <+57>: add $0x8,%rsp=> 0x000000000040100b <+61>: retq End of assembler dump.(gdb) si0x0000000000400fee in func4 ()(gdb) disassembleDump of assembler code for function func4: 0x0000000000400fce <+0>: sub $0x8,%rsp 0x0000000000400fd2 <+4>: mov %edx,%eax 0x0000000000400fd4 <+6>: sub %esi,%eax 0x0000000000400fd6 <+8>: mov %eax,%ecx 0x0000000000400fd8 <+10>: shr $0x1f,%ecx 0x0000000000400fdb <+13>: add %ecx,%eax 0x0000000000400fdd <+15>: sar %eax 0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx 0x0000000000400fe2 <+20>: cmp %edi,%ecx 0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36> 0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx 0x0000000000400fe9 <+27>: callq 0x400fce <func4>=> 0x0000000000400fee <+32>: add %eax,%eax 0x0000000000400ff0 <+34>: jmp 0x401007 <func4+57> 0x0000000000400ff2 <+36>: mov $0x0,%eax 0x0000000000400ff7 <+41>: cmp %edi,%ecx 0x0000000000400ff9 <+43>: jge 0x401007 <func4+57> 0x0000000000400ffb <+45>: lea 0x1(%rcx),%esi 0x0000000000400ffe <+48>: callq 0x400fce <func4> 0x0000000000401003 <+53>: lea 0x1(%rax,%rax,1),%eax 0x0000000000401007 <+57>: add $0x8,%rsp 0x000000000040100b <+61>: retq End of assembler dump.(gdb) cContinuing.Breakpoint 1, 0x000000000040100b in func4 ()(gdb) cContinuing.Breakpoint 1, 0x000000000040100b in func4 ()(gdb) cContinuing.Breakpoint 1, 0x000000000040100b in func4 ()(gdb) cContinuing.So you got that one. Try this one.可以看到当输入的第一数字为 0 时,确实跳转到 add %eax,%eax 才能返回,最终第四炸弹被成功拆除。
phase_5 的汇编代码如下所示:
0000000000401062 <phase_5>: 401062: 53 push %rbx 401063: 48 83 ec 20 sub $0x20,%rsp 401067: 48 89 fb mov %rdi,%rbx 40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 401071: 00 00 401073: 48 89 44 24 18 mov %rax,0x18(%rsp) 401078: 31 c0 xor %eax,%eax 40107a: e8 9c 02 00 00 callq 40131b <string_length> 40107f: 83 f8 06 cmp $0x6,%eax 401082: 74 4e je 4010d2 <phase_5+0x70> 401084: e8 b1 03 00 00 callq 40143a <explode_bomb> 401089: eb 47 jmp 4010d2 <phase_5+0x70> 40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx 40108f: 88 0c 24 mov %cl,(%rsp) 401092: 48 8b 14 24 mov (%rsp),%rdx 401096: 83 e2 0f and $0xf,%edx 401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx 4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1) 4010a4: 48 83 c0 01 add $0x1,%rax 4010a8: 48 83 f8 06 cmp $0x6,%rax 4010ac: 75 dd jne 40108b <phase_5+0x29> 4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp) 4010b3: be 5e 24 40 00 mov $0x40245e,%esi 4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi 4010bd: e8 76 02 00 00 callq 401338 <strings_not_equal> 4010c2: 85 c0 test %eax,%eax 4010c4: 74 13 je 4010d9 <phase_5+0x77> 4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb> 4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) 4010d0: eb 07 jmp 4010d9 <phase_5+0x77> 4010d2: b8 00 00 00 00 mov $0x0,%eax 4010d7: eb b2 jmp 40108b <phase_5+0x29> 4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax 4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 4010e5: 00 00 4010e7: 74 05 je 4010ee <phase_5+0x8c> 4010e9: e8 42 fa ff ff callq 400b30 <__stack_chk_fail@plt> 4010ee: 48 83 c4 20 add $0x20,%rsp 4010f2: 5b pop %rbx 4010f3: c3 retq可以看到 phase_5 开启了金丝雀保护机制,用于防范缓冲区溢出可能造成的安全问题。接着判断输入的字符串的长度,如果不为 6 就引爆炸弹。接下来的代码可以翻译为:
rbx = rdi; // %rdi 保存了输入的字符串的起始地址for (rax = 0; rax < 6; rax++) { rcx = M[rbx + rax]; M[rsp] = cl; // %rcx 低八位,将值限制在 0~255 之间 rdx = M[rsp]; rdx &= 0xF; // 将 %rdx 的值限制在 0~15 之间 rdx = M[rdx + 0x4024b0]; M[rsp + rax + 0x10] = dl; // %rdx 的低八位}M[rsp + 0x16] = 0;rsi = 0x40245e;rdi = rsp + 0x10;if (strings_not_equal(rdi, rsi)) { explode_bomb()}根据第一炸弹的套路,%rsi 中存的应该是正确字符串的起始地址,由于字符串的长度为 6,所以结果如下图所示:

可以看到字符串是 flyers,当然第五炸弹可能没有第一炸弹那么简单,直接将 flyers 输到终端就企图解除炸弹只会无功而返,毕竟在 strings_not_equal 之前还有一段干了脏活的代码。再来认真分析一下 for 循环里面都发生了什么。
由于 %rdx 保存了输入的字符串的起始地址,所以 M[rbx + rax] 取出了字符串的一个字符,只留下字符的低 8 位并赋值给 %rdx。%rdx 更进一步,只留下了低四位(取值范围 0~15)。之后 rdx = M[rdx + 0x4024b0] 以 0x4024b0 为基地址,加上 0~15 的偏移量,从内存中取出了一个字符并赋给 %rdx。最后把 %rdx 的低 8 位赋值给 M[rsp + rax + 0x10],由于循环进行了 6 次,所以 strings_not_equal 是将 %rsp + 0x10 到 %rsp + 0x15 组成的字符串和 flyers 进行比较。
下图显示了 0x4024b0 开始的 16 个( rdx &= 0xF )字符,里面出现了 flyers 所需的 6 个字母:

只要让给 0x4024b0 加上 9 就能取到 f,所以字符串的第一个字符应该是 9。而为了取到 l,我们应该给 0x4024b0 加上 15,但是这里对应的字符就不该是 15 的十六进制 F,而应该是低四位全为 1 的某个字符,字符 ? 的的二进制值为 0b11111,满足低四位全 1 的条件,所以字符串的第二位取 ?。根据这个原理可以分别确定出后面几个字符为 >567,最终结果是 9?>567。

phase_6 的汇编代码如下所示:
00000000004010f4 <phase_6>: 4010f4: 41 56 push %r14 4010f6: 41 55 push %r13 4010f8: 41 54 push %r12 4010fa: 55 push %rbp 4010fb: 53 push %rbx 4010fc: 48 83 ec 50 sub $0x50,%rsp 401100: 49 89 e5 mov %rsp,%r13 401103: 48 89 e6 mov %rsp,%rsi 401106: e8 51 03 00 00 callq 40145c <read_six_numbers> 40110b: 49 89 e6 mov %rsp,%r14 40110e: 41 bc 00 00 00 00 mov $0x0,%r12d 401114: 4c 89 ed mov %r13,%rbp 401117: 41 8b 45 00 mov 0x0(%r13),%eax 40111b: 83 e8 01 sub $0x1,%eax 40111e: 83 f8 05 cmp $0x5,%eax 401121: 76 05 jbe 401128 <phase_6+0x34> 401123: e8 12 03 00 00 callq 40143a <explode_bomb> 401128: 41 83 c4 01 add $0x1,%r12d 40112c: 41 83 fc 06 cmp $0x6,%r12d 401130: 74 21 je 401153 <phase_6+0x5f> 401132: 44 89 e3 mov %r12d,%ebx 401135: 48 63 c3 movslq %ebx,%rax 401138: 8b 04 84 mov (%rsp,%rax,4),%eax 40113b: 39 45 00 cmp %eax,0x0(%rbp) 40113e: 75 05 jne 401145 <phase_6+0x51> 401140: e8 f5 02 00 00 callq 40143a <explode_bomb> 401145: 83 c3 01 add $0x1,%ebx 401148: 83 fb 05 cmp $0x5,%ebx 40114b: 7e e8 jle 401135 <phase_6+0x41> 40114d: 49 83 c5 04 add $0x4,%r13 401151: eb c1 jmp 401114 <phase_6+0x20> 401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi 401158: 4c 89 f0 mov %r14,%rax 40115b: b9 07 00 00 00 mov $0x7,%ecx 401160: 89 ca mov %ecx,%edx 401162: 2b 10 sub (%rax),%edx 401164: 89 10 mov %edx,(%rax) 401166: 48 83 c0 04 add $0x4,%rax 40116a: 48 39 f0 cmp %rsi,%rax 40116d: 75 f1 jne 401160 <phase_6+0x6c> 40116f: be 00 00 00 00 mov $0x0,%esi 401174: eb 21 jmp 401197 <phase_6+0xa3> 401176: 48 8b 52 08 mov 0x8(%rdx),%rdx 40117a: 83 c0 01 add $0x1,%eax 40117d: 39 c8 cmp %ecx,%eax 40117f: 75 f5 jne 401176 <phase_6+0x82> 401181: eb 05 jmp 401188 <phase_6+0x94> 401183: ba d0 32 60 00 mov $0x6032d0,%edx 401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2) 40118d: 48 83 c6 04 add $0x4,%rsi 401191: 48 83 fe 18 cmp $0x18,%rsi 401195: 74 14 je 4011ab <phase_6+0xb7> 401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx 40119a: 83 f9 01 cmp $0x1,%ecx 40119d: 7e e4 jle 401183 <phase_6+0x8f> 40119f: b8 01 00 00 00 mov $0x1,%eax 4011a4: ba d0 32 60 00 mov $0x6032d0,%edx 4011a9: eb cb jmp 401176 <phase_6+0x82> 4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx 4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax 4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi 4011ba: 48 89 d9 mov %rbx,%rcx 4011bd: 48 8b 10 mov (%rax),%rdx 4011c0: 48 89 51 08 mov %rdx,0x8(%rcx) 4011c4: 48 83 c0 08 add $0x8,%rax 4011c8: 48 39 f0 cmp %rsi,%rax 4011cb: 74 05 je 4011d2 <phase_6+0xde> 4011cd: 48 89 d1 mov %rdx,%rcx 4011d0: eb eb jmp 4011bd <phase_6+0xc9> 4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx) 4011d9: 00 4011da: bd 05 00 00 00 mov $0x5,%ebp 4011df: 48 8b 43 08 mov 0x8(%rbx),%rax 4011e3: 8b 00 mov (%rax),%eax 4011e5: 39 03 cmp %eax,(%rbx) 4011e7: 7d 05 jge 4011ee <phase_6+0xfa> 4011e9: e8 4c 02 00 00 callq 40143a <explode_bomb> 4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx 4011f2: 83 ed 01 sub $0x1,%ebp 4011f5: 75 e8 jne 4011df <phase_6+0xeb> 4011f7: 48 83 c4 50 add $0x50,%rsp 4011fb: 5b pop %rbx 4011fc: 5d pop %rbp 4011fd: 41 5c pop %r12 4011ff: 41 5d pop %r13 401201: 41 5e pop %r14 401203: c3 retq这段代码体积有点大,不过拆弹小分队还是得忍一下。phase_6 首先读入了 6 个数字,接着从 0x40110e 到 0x401151 的代码可以翻译为:
r12d = 0;while (True) { rbp = r13; // 输入的数字的地址 rax = M[r13]; rax -= 1; // M[r13] 的取值范围在 1~6 之间 if (rax > 5 ) { explode_bomb(); } r12d += 1; if (r12d == 6) break; for (rbx = r12d; rbx <= 5; rbx++) { rax = rbx; if (rax == M[rsp]) { explode_bomb(); } } r13 += 4;}上述代码中 %r13 存储的是输入的数字的地址,如下图所示:

根据上述代码可以确定输入的数字应该在 1~6 之间,并且不能重复,不然就会引爆第六炸弹。从 0x401153 到 0x40116d 的代码可以翻译为:
rsi = rsp + 18;rax = r14; // 输入数字的开始地址do { rdx = 7 - M[rax]; M[rax] = rdx; rax += 4;} while (rsi != rax)此处 %r14 和 %r13 一样存的是输入数字的开始地址(不贴图了),而上述代码的作用就是用 7 减去每个输入的数字并作为新值。接下来的代码就有点混乱了,反复横跳,所以这里先看下最后一段代码 0x4011da 到 0x4011f5 的翻译:
for (rbp = 5; rbp > 1; rbp--) { rax = M[rbx + 8]; eax = M[rax]; // 只保留四个字节,高位填充 0 if (M[rbx] < eax) { explode_bomb(); } rbx = M[rbx + 8]; // rbx 滞后 rax}由此可见 %rbx 是一个二重指针,需要解引用两次才能拿到正确的值,这段代码用来确定一段内存是否已降序排列,如果不满足降序条件就会引爆第六炸弹。如果输入的 6 个数字为 6~1,在开始检查是否满足倒序条件之前暂停程序,可以看到内存中以 %rbx 为起始地址的 12 个 8 字节数,其中左侧一列的低 32 位用来比较大小,右侧一列用来作为 %rax 和下次 %rbx 存放的地址:

根据上述信息,再来审视没提及的那一部分汇编代码,其实就是根据用户输入的数字,改变右侧一栏的地址和 %rbx 的初始值。如果将 %rbx 的初始值设置为 0x6032f0,沿着左侧一栏向下,到底之后回到 0x6032d0,一路得到的序列就是递减的。由此可以得到输入的数字为 4 3 2 1 6 5(被 7 减过得到的)。结果如下:

通过这次实验,可以熟悉 GDB 调试工具的使用方法,同时也能看懂一些汇编代码了(不过有一说一,第六炸弹的代码真的就是又臭又长)。拆了这么久的炸弹,中间还失败过好多次,希望人质没事,以上~~