写在前面

阅读汇编代码容易出现找不到重点的问题,建议首先看

  • 函数调用处上下文
  • 跳转处上下文

经常使用 gdb 可以事半功倍,实在找不着北的话,在当前理解有困难处设置断点并运行或 单步运行,查看运行过程当中变化的值。

  1. 一号弹
  2. 二号弹
  3. 三号弹
  4. 四号弹
  5. 五号弹
  6. 六号弹
  7. 隐藏弹

准备

  1. 自带的 bomb.c 文件,阅读其源码可以找到炸弹埋藏的线索;
  2. objdumb -d domb >> domb.s 从二进制可执行文件反编译出汇编代码文件(符号已经 进行过重定位),这是接下来拆弹的主要突破口;
  3. objdumb -t domb >> symbols.txt 获得二进制可执行文件中的符号表,包括所有函数名 和全局变量名,以及他们的对应虚拟地址;
  4. strings domb >> strings.txt 获得二进制可执行文件中的所有可打印字符串;
  5. gdb 常用命令参考表 一页 A4 纸的内容,包含了本次拆弹所要用的 gdb 命令;
  6. Emacs gdb-mode,有了它,debug 过程更友好了。

拆弹

Let’s dance!

阅读 bomb.c 源代码,理解它的主要运行逻辑,主要是三个步骤:

  1. 试图读取参数,如果有参数的话必须只有一个参数,且为文件路径,这样才合法,不然 报错;如果没有参数,后续从标准输入中读取数据;
  2. 埋炸弹,对应源码中的 initialize_bomb 函数;
  3. 6个炸弹需要拆除,如果输入是文件的话,一一匹配文件中的数据;如果输入是标准输入 的话,命令行重复六次输入过程。一旦数据错误,炸弹就引爆,程序退出。

一号弹

  1. bomb.c 源码逻辑中:

    1
    2
    3
    
    input = read_line();             /* Get input                   */
    phase_1(input);                  /* Run the phase               */
    phase_defused();                 /* Drat!  They figured it out! */
  2. 可知 input 是我们的输入,phase_1 函数将此输入作为参数,如果输入正确则炸弹拆除, 函数 phase_1 就是一号炸弹。

  3. 查看 bomb.s 的汇编代码,main 与上述源码逻辑对应的汇编代码是

    1
    2
    3
    4
    
    400e32: e8 67 06 00 00          callq  40149e <read_line>
    400e37: 48 89 c7                mov    %rax,%rdi
    400e3a: e8 a1 00 00 00          callq  400ee0 <phase_1>
    400e3f: e8 80 07 00 00          callq  4015c4 <phase_defused>

    read_line 函数顾名思义,就是将返回结果放置在 %rax 寄存器中(%rax 一般都会用来 保存返回结果);将 %eas 内容送到 %rdi 寄存器中(%rdi 寄存器一般都用来保存被调 用函数的参数);调用 phase_1 函数,再调用 phase_defused 函数。

  4. 在汇编代码中查找 phase_1 函数的定义

    1
    2
    3
    4
    5
    6
    7
    8
    
    400ee0: 48 83 ec 08             sub    $0x8,%rsp
    400ee4: be 00 24 40 00          mov    $0x402400,%esi
    400ee9: e8 4a 04 00 00          callq  401338 <strings_not_equal>
    400eee: 85 c0                   test   %eax,%eax
    400ef0: 74 05                   je     400ef7 <phase_1+0x17>
    400ef2: e8 43 05 00 00          callq  40143a <explode_bomb>
    400ef7: 48 83 c4 08             add    $0x8,%rsp
    400efb: c3                      retq

    可以看到里面调用了 explode_bomb 函数,可见 phase_1 函数就是炸弹;在爆炸前调用 了 strings_not_equal 函数,并用 test 指令对两个操作数作相与操作,如果结果是0 则将 ZF 标识位置1,否则置0;je 指令检查 ZF 标志位,如果是1则跳转到指定位置, 即跳过爆炸;看来 strings_not_equal 函数比较了两个字符串,如果相同则返回0,不 同返回1,而这两个参数地址一个已经在 %rdi 中,就是我们的输入,另外一个存放在 %esi 中,其值是虚拟内存地址 0x402400。

  5. 在 emacs 的 gdb-mode 中打开可执行文件 bomb,输入 x/s 0x402400 可输出此字符串

    1
    
    "Border relations with Canada have never been better."

二号弹

  1. bomb.c 源码逻辑结构不变,phase_2 就是二号炸弹。
  2. 汇编代码中调用 phase_2 函数部分除了函数名外与一号弹结构相同。
  3. 再来看 phase_2 函数的定义部分

    1
    2
    
    400f02: 48 89 e6                mov    %rsp,%rsi
    400f05: e8 52 05 00 00          callq  40145c <read_six_numbers>

    此时 read_line 函数的输入保存在 %rdi 寄存器中,而 %rsi 寄存器的内容保存的是 %rsp 寄存器即栈指针寄存器的内容; phase_2 定义中有两处调用 explode_bomb 函数,其条件转移的指令分别是

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    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>
    ...
    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>

    显然 read_six_numbers 的调用改变了调用者栈中的数据。 再来看 read_six_numbers 的部分定义,它竟然也调用了 explode_bomb 函数。

    1
    2
    3
    4
    5
    6
    
    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>
  4. read_six_numbers 功能分析

    1. 前半部分将定位其调用者函数传入的参数的地址,从左到右参数的地址分别是调用者 的 %rsp %rsp+4 %rsp+8 %rsp+12 %rsp+16 %rsp+20
    2. 然后的部分就是上面第3个代码示例的部分,0x4025c3保存的是字符串指针,其内容是 “%d %d %d %d %d %d”,可以推断将会以此格式化字符串对输入数据进行解析;
    3. 寄存器 %eax 放置了立即数0,在调用了 __isoc99_sscanf@plt 之后将会检查这个寄 存器的值,如果解析次数不到6次的话就会出发炸弹,所以我们输入的数据字符串应该 是以空格分隔的6个数字(其实超过6个也没关系,只会取前6个);
    4. 根据但见汇编代码文件的内容无法得出 __isoc99_sscanf@plt 函数的主要工作逻辑, 因为它又调用了 glibc 库函数,不过可以推断它的主要工作是扫描输入字符串,并 以前面的那个格式化字符串解析它,将里面的字符串数字一一转变为数字。
  5. phase_2 功能分析

    1. 在上面第2个代码示例中,再调用了 read_six_numbers 后,将会使用 cmpl 指令检 查栈中栈底变量存放的值的大小,这个值是由 read_six_numbers 生成并存放的;如 果这个值等于1的话将进行后续的逻辑运作,如果不等于,就爆炸;所以可知输入的 字符串中必须有数字1;
    2. 跳转到的 400f30 地址处,可以看到,接下来是一个循环,循环条件是 %rbx 寄存器 的值不等于 %rbp 寄存器的值;首先在 %rbx 中存放 %rsp+4 的值,后续按4递增; 将 %rsp 和 %rsp+4 虚拟地址处存放的值两两进行比较,后者必须是前者的2倍,检 查直到 %rsp+18 地址为止;如果不符合条件,则引爆炸弹。
  6. 结论:综上可以推断得知,拆除该炸弹需要输入:1 2 4 8 16 32

三号弹

  1. 在 phase_3 的汇编代码前面调用了 一号弹 中调用过的 __isoc99_sscanf@plt 函数, 它将扫描输入字符串中的数字表示并将数字赋值参数;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    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)

    由代码中的 cmp 和 jg 部分可知,此次需要输入的字符串中的数字应在两个以上;

    在第二行的 mov 指令中传送了一个内存地址,x/s 命令可知其值是 “%d %d”,所以本次 拆弹需要输入的字符串中应包含两个数字;

    扫描获得的两个数字先后存放在 0x8(%rsp) 0xc(%rsp) 中;

    由最后一个 cmpl 指令及其后的 ja 指令(跳转到炸弹爆炸),可以推算输入的第一个 数字不能大于7。

  2. 如果输入字符串被接受的话,接下来将会进行一次 基址变址比例因子偏移量的间接跳转

    1
    
    400f75: ff 24 c5 70 24 40 00    jmpq   *0x402470(,%rax,8)

    跳转到的地址是 (%rax * 8) + 0x402470 这个地址中存放的地址,%rax 中存放的是输入的第一个数字;

    如果 %rax 存放的是0;则将跳转到 0x402470 这个地址中存放的地址;通过 x/w 可以 得出这个跳转地址是 0x400f7c;

    简单起见,我们输入的第一个数确实是0,那么将跳转到:

    1
    2
    
    400f7c: b8 cf 00 00 00          mov    $0xcf,%eax
    400f81: eb 3b                   jmp    400fbe <phase_3+0x7b>

    mov 指令将立即数 0xcf 送入 %eax 寄存器中,这个值换成10进制就是207,然后跳转到虚拟地址 0x400fbe 处;

    1
    2
    3
    
    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>

    在跳转地址处,进行 cmp 指令,比较 %eax 和 0xc(%rsp) 各自存放的值,后面的值就 是我们输入的第二个数字,这个数字应该等于 207,否则引爆炸弹;

  3. 所以结果应该是 “0 207”,当然理论上还有7种组合,就不一一演算了。

四号弹

  1. 同样这次又调用了那个扫描函数,本次需要输入的字符串仍然需要两个数字(这一次必 须是两个,不多也不少),以空格分开;
  2. 后续一段比较和跳转代码:

    1
    2
    3
    
    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>

    表示扫描得到的第一个数值应当不大于十进制数14,不然炸弹引爆;

  3. 后续准备参数(%edx: 14, %esi: 0, %edi: 第一个输入数字)并调用 fun4 函数,调用 之后检查 func4 的返回值,如果最低位是1就引爆炸弹,func4 中两个关键跳转结构:

    1
    2
    3
    4
    5
    
    400fe2: 39 f9                   cmp    %edi,%ecx
    400fe4: 7e 0c                   jle    400ff2 <func4+0x24>
    ...
    400ff7: 39 f9                   cmp    %edi,%ecx
    400ff9: 7d 0c                   jge    401007 <func4+0x39>

    第一个关键跳转之前已经将 %edx 中的值经过转换存储在了 %ecx 中,值为7,如果7小 于等于第一个输入数字,就跳转,跳转到的地方首先将0存放在 %eax 中,然后进行上面 的第二个跳转逻辑;

    再次进行相同的比较,如果这次的结果是大于等于,就跳转,跳转的位置是函数的结束 阶段;

    既大于等于又小于等于,可知就是要求等于,所以在输入的第一个数是7的条件下两次跳 转都会实现,该函数的返回值就是0;

    func4 内部逻辑是个递归结构,假设存在这种情况:7 小于等于第一个传入数,但不大 于等于它,那么就会进入递归,而要让此递归终止,那么必然两次后续跳转条件都符合, 当下一级递归返回时,它的调用者对 %eax 的操作一定会将其中的值最后一位置为1,整 个 func4 结束之后 %eax 就为1,后续基于此的跳转就会引爆炸弹,所以 func4 函数的 调用过程中每次遇到第二个跳转条件都要实现跳转,也就是说,传入的第一个数字一定 是小于等于7的;

    而根据上一个跳转条件如果失败其前后对 %ecx 中值的处理,可以推得输入的第一个数 字必须是 0 1 3 7 中的一个,这是一个数列)。

  4. 在 phase_4 中调用 func4 之后获得的返回值是0的话,就不跳转到引爆炸弹的位置;然 后比较第二个输入参数和0是否相等,如果相等就跳转到该函数的结束阶段,表示要求第 二个输入数字必须是0;

  5. 最后结果就是 “7 0” (第一个数可以是 0 1 3 7 中的任一个)

五号弹

  1. 注意,phase_5 显式地将 %rdi 中存放的输入字符串的地址送给 %rbx 寄存器

    1
    2
    3
    
    400ea7: 48 89 c7                mov    %rax,%rdi
    ...
    401067: 48 89 fb                mov    %rdi,%rbx
  2. mov %fs:0x28,%rax %fs 这个段寄存器指向当前活动线程的TEB结构(线程结构),表 示将 fs 段上的偏移地址 0x28 上的数据传送给了 %rax 寄存器(其实和谜题解法无关)

  3. 第一次跳转:

    1
    2
    3
    4
    5
    
    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>

    将 %eax 中的内容清零;

    调用 string_length 函数计算输入字符串的长度;

    比较 string_length 的返回值和立即数6,如果相等则跳转,否则引爆炸弹;

    所以输入的字符串应该有6个字符。

  4. 如果字符串长度符合条件,跳转到 0x4010d2,将 %eax 的值置0,再跳转到 0x40108b;

    在此处执行 mobzbl 指令(将一个源操作数低1字节长度的值0扩展到32位并存放在目标 寄存器处),在这里就是取得字符串内第一个字符;而后将此字符(就是 %cl 内存放的 值,%cl 是 %ecx 的低8位寄存器)送到栈顶。

    1
    2
    
    40108b: 0f b6 0c 03             movzbl (%rbx,%rax,1),%ecx
    40108f: 88 0c 24                mov    %cl,(%rsp)
  5. 对第4步获得的字符进行处理,最后字符(其实是字符的ASCII码的低四位二进制表示)存放在 %edx 的低8位寄存器 %dl 中,%edx 寄存器中除了低8位其余全为0,接下来:

    1
    2
    3
    4
    5
    
    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>

    将 %rdx + 0x4024b0 地址处存放的值传送到 %edx 寄存器处;

    将 %edx 寄存器处的底8位数据传送到栈顶+10地址处,

    将 %rax 存放的值加1;

    比较 %rax 存放的值,如果不等于0则跳转到 0x40108b处,其实就是重新第4、5步的循环;

  6. 在循环完成后,将 %rsp + 10 地址处存放的地址和 0x40245e 作为参数传入 strings_not_equal,如果返回值是1的话,表示不相等,引爆炸弹,返回值是0的就跳转 到 phase_5 函数收尾阶段,炸弹排除。

  7. x/s 0x40245e 得到该地址处开始存放的字符串 “flyers”

    x/s 0x4024b0 得到该地址处开始存放的字符串 “maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?”

    输入的字符串应该包含6个数字,每个数字对应于第二条字符串的1个索引,索引从0开始, 6个索引相对的字符可以拼凑成”flyers”

  8. 可以推算,index的组合是9fe567,查找ASCII表,最后的结果是 “IONEFG”(不唯一)。

六号弹

  1. phase_6 首先调用了 read_six_numbers 函数,这个函数的功能是将输入字符串解析出6 个数字,详细逻辑在 二号弹 分析过;
  2. 然后对 %rsp 处的值进行判断,也就是输入字符串的第一个数字,这个数字是必须无符号数, 且必须小于等于6;
  3. 然后进入一个循环,依次比较第一个数字和后续数字是否相等、第二个数字和后续数字……, 如果遇到相等的情况则引爆炸弹,且所有数字都必须小于等于6且不能为0;

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    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>  # 数字不能大于6
    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>  # 一轮比较结束,继续下一轮
  4. 接下来又是一个循环,依次将7减去输入的数字,并将结果存储在原地址处:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    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>x
  5. 继续进入一个循化,

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    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  # 将 0x6032d0 加 8 获得的地址处存放的值
    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)  # 将 0x6032d0 处理(或者原值)后的值存放在由 %rsi 决定的地址处
    40118d: 48 83 c6 04             add    $0x4,%rsi  # 循环条件值加4
    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>  # 如果该值小于等于1,跳转
    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>  # 跳转到大于1的处理循环
    1. 此循环依次将由前面步骤获得的 %rsp %rsp+4 等地址处存放的值与1比较,如果相等, 则将 0x6032d0 这个地址存放在 %rsp + %rsi*2 + 0x20 地址处,%rsi 中存放的数 字用来判断是否终止循环,从0开始依次加4,等于24时终止此次循环;
    2. 如果不相等,则再与2比较,并将 0x6032d0 加 8,这又是一个循环:不相等就递增, 直到相等为止,此时将获得的 0x6032d0 递增后的值放入相应地址处;
  6. 又进入一次循环

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    4011ab: 48 8b 5c 24 20          mov    0x20(%rsp),%rbx  # 将第5步中获得的第一个值送入 %rbx 寄存器
    4011b0: 48 8d 44 24 28          lea    0x28(%rsp),%rax  # 将...第二个值的地址送入 %rax 寄存器
    4011b5: 48 8d 74 24 50          lea    0x50(%rsp),%rsi  # 将终止条件值送入 %rsi 寄存器
    4011ba: 48 89 d9                mov    %rbx,%rcx  # 将 %rbx 中的值送入 %rcx 寄存器
    4011bd: 48 8b 10                mov    (%rax),%rdx  # 将 %rax 中地址存储的值送入 %rdx 寄存器
    4011c0: 48 89 51 08             mov    %rdx,0x8(%rcx)  # 将 %rdx 中的值送到由 %rcx+8 得出的地址处
    4011c4: 48 83 c0 08             add    $0x8,%rax  # 第5步中获得值中的下一个
    4011c8: 48 39 f0                cmp    %rsi,%rax
    4011cb: 74 05                   je     4011d2 <phase_6+0xde> # 是否终止
    4011cd: 48 89 d1                mov    %rdx,%rcx  # 如果不终止,将 %rdx 的值送入 %rcx 中
    4011d0: eb eb                   jmp    4011bd <phase_6+0xc9> # 继续下一次循环

    逻辑看起来很复杂,其实抓住一个关键点就可以理解:第5步中获得的值中,按顺序总是 把下一个值放置于上一个值+8得到的地址中;其实就是在建立链表。

  7. 最后一个循环:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    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>

    循环交替比较链表元素中的大小,前面的值必须大于等于后面的。

  8. 根据前面的推演,可知输入的字符串中6个数字只能是 123456 的排列组合;第5步中提 及的 0x6032d0 中存放的是332,0x6032d8 处存放的是地址,0x6032e0 存放的是 168, 接下内依次是 924、691、477、443;根据第6步得出的结论即链表前一个元素必须比接 下来的大,其实就是以我们输入的数据为索引,根据原有的链表建立新的降序链表。那 么可以得出结果应该是 “4 3 2 1 6 5”

隐藏弹

  1. 还没完!虽然说将前面得到的结果依次输入,bomb 程序就会提示拆弹完成正常退出,但 bomb.s 汇编代码文件中还有一个隐藏炸弹 secret_phase,这个炸弹在第六个炸弹被拆 除后且后续继续又输入时才会接受拆除,secret_phase 函数在每次拆弹成功后调用的 phase_defused 函数中被调用(可能):

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    4015d8: 83 3d 81 21 20 00 06    cmpl   $0x6,0x202181(%rip)        # 603760 <num_input_strings>
    4015df: 75 5e                   jne    40163f <phase_defused+0x7b>  # 不然就跳过隐藏炸弹
    ...
    4015f0: be 19 26 40 00          mov    $0x402619,%esi  # 格式化字符串 "%d %d %s"
    4015f5: bf 70 38 60 00          mov    $0x603870,%edi  # 输入的符合格式的字符串的地址
    4015fa: e8 f1 f5 ff ff          callq  400bf0 <__isoc99_sscanf@plt>
    4015ff: 83 f8 03                cmp    $0x3,%eax  # 输入的字符串应该包含3个元素
    401602: 75 31                   jne    401635 <phase_defused+0x71>  # 否则跳过隐藏炸弹
    401604: be 22 26 40 00          mov    $0x402622,%esi  # 字符串地址,值是 "DrEvil"
    401609: 48 8d 7c 24 10          lea    0x10(%rsp),%rdi # 输入的字符串栈中地址
    40160e: e8 25 fd ff ff          callq  401338 <strings_not_equal>
    401613: 85 c0                   test   %eax,%eax
    401615: 75 1e                   jne    401635 <phase_defused+0x71>  # 字符串比较一定要相等
    ...
    401630: e8 0d fc ff ff          callq  401242 <secret_phase>  # 调用 secret_phase 函数
    1. 可以看到,__isoc99_sscanf@plt 接受了一个格式化字符串和一个输入字符串,而这 个输入字符串是由两个数字和一个字符串组成的,这个字符串在哪里?注意,它并不 是在第六个字符串输入后重新输入一行,而是在原有的六行字符串中的一行;
    2. 使用 gdb 对可执行文件进行断点设置,断点设置在 phase_defused 函数的入口处, 按正确答案执行-输入-输出,到第六个字符串输入后,在 phase_defused 函数内部 单步进行到 0x4015f5 地址处,x/s 0x603870 得到 “7 0”;可知,这里是解析了第4 个输入字符串;
    3. 要想拆除隐藏炸弹,第4个字符串应该是 “7 0 DrEvil”
  2. secret_phase 函数逻辑:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    401243: e8 56 02 00 00          callq  40149e <read_line>  # 读取第7个输入
    401248: ba 0a 00 00 00          mov    $0xa,%edx  # 10进制 允许 strol 的字符串参数包括"1...9"
    40124d: be 00 00 00 00          mov    $0x0,%esi
    401252: 48 89 c7                mov    %rax,%rdi  # 准备参数
    401255: e8 76 f9 ff ff          callq  400bd0 <strtol@plt>  # 根据参数转换输入字符串为相应的数字
    40125a: 48 89 c3                mov    %rax,%rbx
    40125d: 8d 40 ff                lea    -0x1(%rax),%eax
    401260: 3d e8 03 00 00          cmp    $0x3e8,%eax  # 数字不能大于1001
    401265: 76 05                   jbe    40126c <secret_phase+0x2a>
    401267: e8 ce 01 00 00          callq  40143a <explode_bomb>  # 不然炸弹爆炸
    40126e: bf f0 30 60 00          mov    $0x6030f0,%edi
    401273: e8 8c ff ff ff          callq  401204 <fun7>  # 调用 fun7
    401278: 83 f8 02                cmp    $0x2,%ea
    40127b: 74 05                   je     401282 <secret_phase+0x40>  # fun7 的返回值必须等于2
    40127d: e8 b8 01 00 00          callq  40143a <explode_bomb>
  3. fun7 函数逻辑

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    ...
    40120d: 8b 17                   mov    (%rdi),%edx  # 获得 %rdi 存放的地址处存储的值
    40120f: 39 f2                   cmp    %esi,%edx  # 与输入数字比较
    401211: 7e 0d                   jle    401220 <fun7+0x1c>  # 如果小于等于输入数字则跳转
    401213: 48 8b 7f 08             mov    0x8(%rdi),%rdi  # 否则将 %rdi+8 地址处存放的值送给 %rdi
    401217: e8 e8 ff ff ff          callq  401204 <fun7>  # 递归
    40121c: 01 c0                   add    %eax,%eax  # 递归返回后将其返回值加倍
    40121e: eb 1d                   jmp    40123d <fun7+0x39>  # 跳转到返回处
    401220: b8 00 00 00 00          mov    $0x0,%eax  # 如果小于等于输入数字后的跳转处将0送 %eax
    401225: 39 f2                   cmp    %esi,%edx  # 重复比较
    401227: 74 14                   je     40123d <fun7+0x39>  # 如果等于则跳转到返回处
    401229: 48 8b 7f 10             mov    0x10(%rdi),%rdi  # 否则将 %rdi+8 地址处存放的值送给 %rdi
    40122d: e8 d2 ff ff ff          callq  401204 <fun7>  # 递归
    401232: 8d 44 00 01             lea    0x1(%rax,%rax,1),%eax  # 递归结束后将返回值加倍在加1
    401236: eb 05                   jmp    40123d <fun7+0x39>
    401238: b8 ff ff ff ff          mov    $0xffffffff,%eax
    ...

    按照第2步分析的 fun7 的返回值必须是2,那么 fun7 的递归调用轨迹只有一种可能

    1. 进入 fun7 函数,初始返回值为0
      1. 进入 401217 处递归,此递归结束后会将返回值加倍;
        1. 进入 40122d 处递归,递归结束后会将返回值加倍再加1,也就是将 %eax 中 的值置1;
          1. 此次调用中保持 %eax 为0,两个跳转条件都满足,不进入递归。
  4. gdb x/2w 查询 0x6030f0 地址处的值,以及接下来预判的函数调用轨迹将会使用的各个 增量地址处的值,可以确定要想使第3步中的轨迹实现,输入数字必须是22。