这应该是我写过最长的实验报告了……

做的时候就感觉到,不愧是 CMU 的镇系神课,游戏化的关卡设计和埋藏的彩蛋都给人做下去的动力。所以虽然有点难(富有挑战性),但是很有意思。

实验环境

我个人进行本实验的环境是 Kali GNU/Linux Rolling x86_64。

HNU 课程组的老师给每个同学都生成了不同的 bomb,所以以下只是我的 bomb 的答案与解析。

/***************************************************************************
 * 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.
 ***************************************************************************/

phase_1:字符串

使用 objdump 反编译后,审计这段 phase_1 的汇编代码:

00000000000015e7 <phase_1>:
    15e7:       f3 0f 1e fa             endbr64
    15eb:       48 83 ec 08             sub    $0x8,%rsp
    15ef:       48 8d 35 56 1b 00 00    lea    0x1b56(%rip),%rsi        # 314c <_IO_stdin_used+0x14c>
    15f6:       e8 bf 05 00 00          call   1bba <strings_not_equal>
    15fb:       85 c0                   test   %eax,%eax
    15fd:       75 05                   jne    1604 <phase_1+0x1d>
    15ff:       48 83 c4 08             add    $0x8,%rsp
    1603:       c3                      ret
    1604:       e8 c5 06 00 00          call   1cce <explode_bomb>
    1609:       eb f4                   jmp    15ff <phase_1+0x18>

可以看到调用了 strings_not_equal 函数,通过函数名可以猜测这个函数是用来比较两个字符串是否相等的。同时可以看到之后的代码,根据其结果 eax 寄存器的值进行了跳转,离开了 phase_1 函数。
那么,比较的两个对象(也就是传入 strings_not_equal 的两个字符串参数),一个是我们之前输入的字符串,一个是 bomb1 的答案。再看 15ef 这一行的 lea 语句,算出了一个地址放在 rsi 寄存器中,作为 strings_not_equal 的参数之一,显然这是 bomb1 的答案字符串存放的地址。

那么在运行的时候,只要看看 rsi 存放的地址对应的内存存放的字符串,就能得到答案了。
使用 gdb 运行,在 phase_1 打断点,然后不断 ni 直到 15f6 这一行,然后:

(gdb) i r rsi
rsi            0x55555555714c      93824992244044
(gdb) x/4bs $rsi
0x55555555714c: "Crikey! I have lost my mojo!"
0x555555557169: "%d %c %d"
0x555555557172: ""
0x555555557173: ""

得到答案:Crikey! I have lost my mojo!

事实上,rdi 存储函数的第一个参数,rsi 存储第二个,rdx 则是第三个,rcx 是第四个。如果我们查看此时 rdi 存储的地址处的内存存放的字符串,就能看见我们输入的内容:

(gdb) i r rdi
rdi            0x555555559700      93824992253696
(gdb) x/4bs $rdi
0x555555559700 <input_strings>: "test input"
0x55555555970b <input_strings+11>:      ""
0x55555555970c <input_strings+12>:      ""
0x55555555970d <input_strings+13>:      ""

phase_2:循环

000000000000160b <phase_2>:
    160b:    f3 0f 1e fa              endbr64
    160f:    55                       push   %rbp
    1610:    53                       push   %rbx
    1611:    48 83 ec 28              sub    $0x28,%rsp
    1615:    64 48 8b 04 25 28 00     mov    %fs:0x28,%rax
    161c:    00 00 
    161e:    48 89 44 24 18           mov    %rax,0x18(%rsp)
    1623:    31 c0                    xor    %eax,%eax
    1625:    48 89 e6                 mov    %rsp,%rsi
    1628:    e8 cd 06 00 00           call   1cfa <read_six_numbers>
    162d:    83 3c 24 00              cmpl   $0x0,(%rsp)
    1631:    78 0a                    js     163d <phase_2+0x32>
    1633:    48 89 e5                 mov    %rsp,%rbp
    1636:    bb 01 00 00 00           mov    $0x1,%ebx
    163b:    eb 13                    jmp    1650 <phase_2+0x45>
    163d:    e8 8c 06 00 00           call   1cce <explode_bomb>
    1642:    eb ef                    jmp    1633 <phase_2+0x28>
    1644:    83 c3 01                 add    $0x1,%ebx
    1647:    48 83 c5 04              add    $0x4,%rbp
    164b:    83 fb 06                 cmp    $0x6,%ebx
    164e:    74 11                    je     1661 <phase_2+0x56>
    1650:    89 d8                    mov    %ebx,%eax
    1652:    03 45 00                 add    0x0(%rbp),%eax
    1655:    39 45 04                 cmp    %eax,0x4(%rbp)
    1658:    74 ea                    je     1644 <phase_2+0x39>
    165a:    e8 6f 06 00 00           call   1cce <explode_bomb>
    165f:    eb e3                    jmp    1644 <phase_2+0x39>
    1661:    48 8b 44 24 18           mov    0x18(%rsp),%rax
    1666:    64 48 2b 04 25 28 00     sub    %fs:0x28,%rax
    166d:    00 00 
    166f:    75 07                    jne    1678 <phase_2+0x6d>
    1671:    48 83 c4 28              add    $0x28,%rsp
    1675:    5b                       pop    %rbx
    1676:    5d                       pop    %rbp
    1677:    c3                       ret
    1678:    e8 d3 fb ff ff           call   1250 <__stack_chk_fail@plt>

可以猜到 read_six_numbers 这个函数用于读取 6 个数字。根据使用 gdb 时的调用追踪情况,它调用了 sscanf 等函数从输入的字符串中读取了数字。
这些数字依次放在 rsp 到 rsp + 0x18 的位置,使用 x/6wd 就可以看见(这里我们依次输入的是 5、6、7、8、9、10):

(gdb) x/16wd $rsp
0x7fffffffe080: 5       6       7       8
0x7fffffffe090: 9       10      867141120       645910443
0x7fffffffe0a0: 0       0       -7720   32767
0x7fffffffe0b0: 1       0       1431655691      21845

汇编代码的可读性很低,因为它是从机器的角度编写的,而现在我们要从人的角度来审视。所以可以对汇编代码进行改写,先将每一步的含义用可以看懂的人话写出来,然后将其中发现的条件判断和循环抽出来,我们就得到了类似高级语言的伪代码(从 162d 开始):

if (a[0] < 0) boom();

rbp = rsp;
ebx = 0x1;
eax = ebx = 0x1;
eax += a[0] (= a[0] + 1);
if (eax != a[1]) boom();

// ebx is 0x1 now
for (;;){
    ebx++;
    rbp += 0x4;
    if (ebx == 0x06) break;
    eax = ebx;
    eax += a[rbp] (eax = a[rbp] + ebx);
    if (eax != a[rbp+4]) bomb();
}

rax = (rsp + 0x18); // = a[5]

没错,这里 jump 来 jump 去的代码实现了循环。

根据以上代码的分析,我们输入的六个数(从 0 到 5 编号)必须满足这样的关系:a[0]+1=a[1]a[1]+2=a[2]a[2]+3=a[3],……

一个可能的答案是:

1 2 4 7 11 16

这段代码中出现了几次形如 sub %fs:0x28,%rax 的代码,这些代码是干什么用的呢?

    1615:    64 48 8b 04 25 28 00     mov    %fs:0x28,%rax
    ...
    1666:    64 48 2b 04 25 28 00     sub    %fs:0x28,%rax
    166d:    00 00 
    166f:    75 07                    jne    1678 <phase_2+0x6d>
    ...
    1678:    e8 d3 fb ff ff           call   1250 <__stack_chk_fail@plt>

根据最后一行的调用可以猜到,这是某种栈保护的机制。事实上 %fs:0x28 这个位置存的是一个随机数(gdb 的时候查看 rax 就能看到一个乱七八糟的数),在主体部分代码运行完之后再次检查这个值是否改变,以此判断是否遭受了缓冲区溢出。没错,这就是传说中的 Canary 机制

phase_3:跳转表

000000000000167d <phase_3>:
    167d:    f3 0f 1e fa              endbr64
    1681:    48 83 ec 28              sub    $0x28,%rsp
    1685:    64 48 8b 04 25 28 00     mov    %fs:0x28,%rax
    168c:    00 00 
    168e:    48 89 44 24 18           mov    %rax,0x18(%rsp)
    1693:    31 c0                    xor    %eax,%eax
    1695:    48 8d 4c 24 0f           lea    0xf(%rsp),%rcx
    169a:    48 8d 54 24 10           lea    0x10(%rsp),%rdx
    169f:    4c 8d 44 24 14           lea    0x14(%rsp),%r8
    16a4:    48 8d 35 be 1a 00 00     lea    0x1abe(%rip),%rsi        # 3169 <_IO_stdin_used+0x169>
    16ab:    e8 50 fc ff ff           call   1300 <__isoc99_sscanf@plt>
    ...

首先看 16a4 开始的这两行,调用了 sscanf,而显然这个 rsi 存放的是其参数。
查看 rsi 的地址存放的内存,发现是 %d %c %d。看来,读入的是一个 int、一个 char、一个 int。以下方便起见,计为 d1、c、d2。

重新运行这个程序到 phase_3,输入符合要求的读入数据(例如 12 k 34,事实上这个输入会爆炸),查看栈,可以看到我们输入的第一个数字被放在 rsp + 16 位置,第二个数字放在 rsp + 20 位置,而这个 char(k 是 0x6b)则放在 rsp+15 位置。

(gdb) x/8wd $rsp
0x7fffffffe090: 1       0       1431657851      1795183957
0x7fffffffe0a0: 12      34      771968000       926201343
(gdb) x/32bx $rsp
0x7fffffffe090: 0x01    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffe098: 0x7b    0x5d    0x55    0x55    0x55    0x55    0x00    0x6b
0x7fffffffe0a0: 0x0c    0x00    0x00    0x00    0x22    0x00    0x00    0x00
0x7fffffffe0a8: 0x00    0x4c    0x03    0x2e    0xff    0xb5    0x34    0x37

接下来注意看这几行代码:

000000000000167d <phase_3>:
    ...
    16c0:    8b 44 24 10              mov    0x10(%rsp),%eax
    16c4:    48 8d 15 b5 1a 00 00     lea    0x1ab5(%rip),%rdx        # 3180 <_IO_stdin_used+0x180>
    16cb:    48 63 04 82              movslq (%rdx,%rax,4),%rax
    16cf:    48 01 d0                 add    %rdx,%rax
    16d2:    3e ff e0                 notrack jmp *%rax
    ...

显然从 3180 开始是一个跳转表,跳转的依据是 d1(所以要判断 d1 不大于 7!)。gdb 运行时 3180 位置分配的是 0x555555557180 这个地址(rdx 寄存器存放的地址)。查看这个跳转表:

(gdb) x/16wx 0x555555557180
0x555555557180: 0xffffe55c      0xffffe57e      0xffffe5a0      0xffffe5c2
0x555555557190: 0xffffe5e1      0xffffe5fc      0xffffe617      0xffffe632
0x5555555571a0 <array.0>:       0x0000000a      0x00000002      0x0000000e      0x00000007
0x5555555571b0 <array.0+16>:    0x00000008      0x0000000c      0x0000000f      0x0000000b

rdx 这个基址运行时是 0x555555557180,在 objdump 中是 3180,它加上跳转表某一项的内容就是跳转的实际目标地址。
将所有表项加上 3180 并截断,可以得到我们的跳转表:

0: 16dc
1: 16fe
2: 1720
3: 1742
4: 1761
5: 177c
6: 1797
7: 17b2

接下来的代码就都是由跳转的方式进入的。我们将每个入口标出来,每块分别进行分析:

000000000000167d <phase_3>:
    ...
0   16dc:    b8 62 00 00 00           mov    $0x62,%eax // al = 'b'
    16e1:    81 7c 24 14 70 02 00     cmpl   $0x270,0x14(%rsp) // 0x270 = 624 == d2
    16e8:    00 
    16e9:    0f 84 e8 00 00 00        je     17d7 <phase_3+0x15a>
    16ef:    e8 da 05 00 00           call   1cce <explode_bomb>
    16f4:    b8 62 00 00 00           mov    $0x62,%eax
    16f9:    e9 d9 00 00 00           jmp    17d7 <phase_3+0x15a>
1   16fe:    b8 78 00 00 00           mov    $0x78,%eax // al = 'x'
    1703:    81 7c 24 14 2d 03 00     cmpl   $0x32d,0x14(%rsp) // 0x32d = 813 == d2
    170a:    00 
    170b:    0f 84 c6 00 00 00        je     17d7 <phase_3+0x15a>
    1711:    e8 b8 05 00 00           call   1cce <explode_bomb>
    1716:    b8 78 00 00 00           mov    $0x78,%eax
    171b:    e9 b7 00 00 00           jmp    17d7 <phase_3+0x15a>
2   1720:    b8 75 00 00 00           mov    $0x75,%eax // al = 'u'
    1725:    81 7c 24 14 8c 00 00     cmpl   $0x8c,0x14(%rsp) // 0x8c = 140 == d2
    172c:    00 
    172d:    0f 84 a4 00 00 00        je     17d7 <phase_3+0x15a>
    1733:    e8 96 05 00 00           call   1cce <explode_bomb>
    1738:    b8 75 00 00 00           mov    $0x75,%eax
    173d:    e9 95 00 00 00           jmp    17d7 <phase_3+0x15a>
3   1742:    b8 70 00 00 00           mov    $0x70,%eax // al = 'p'
    1747:    81 7c 24 14 09 01 00     cmpl   $0x109,0x14(%rsp) // 0x109 = 265 == d2
    174e:    00 
    174f:    0f 84 82 00 00 00        je     17d7 <phase_3+0x15a>
    1755:    e8 74 05 00 00           call   1cce <explode_bomb>
    175a:    b8 70 00 00 00           mov    $0x70,%eax
    175f:    eb 76                    jmp    17d7 <phase_3+0x15a>
4   1761:    b8 71 00 00 00           mov    $0x71,%eax // al = 'q'
    1766:    81 7c 24 14 5f 03 00     cmpl   $0x35f,0x14(%rsp) // 0x35f = 863 == d2
    176d:    00 
    176e:    74 67                    je     17d7 <phase_3+0x15a>
    1770:    e8 59 05 00 00           call   1cce <explode_bomb>
    1775:    b8 71 00 00 00           mov    $0x71,%eax
    177a:    eb 5b                    jmp    17d7 <phase_3+0x15a>
5   177c:    b8 6f 00 00 00           mov    $0x6f,%eax // al = `o`
    1781:    81 7c 24 14 e5 03 00     cmpl   $0x3e5,0x14(%rsp) // 0x3e5 = 997 == d2
    1788:    00 
    1789:    74 4c                    je     17d7 <phase_3+0x15a>
    178b:    e8 3e 05 00 00           call   1cce <explode_bomb>
    1790:    b8 6f 00 00 00           mov    $0x6f,%eax
    1795:    eb 40                    jmp    17d7 <phase_3+0x15a>
6   1797:    b8 71 00 00 00           mov    $0x71,%eax // al = `q`
    179c:    81 7c 24 14 e0 00 00     cmpl   $0xe0,0x14(%rsp) // 0xe0 = 224 == d2
    17a3:    00 
    17a4:    74 31                    je     17d7 <phase_3+0x15a>
    17a6:    e8 23 05 00 00           call   1cce <explode_bomb>
    17ab:    b8 71 00 00 00           mov    $0x71,%eax
    17b0:    eb 25                    jmp    17d7 <phase_3+0x15a>
7   17b2:    b8 64 00 00 00           mov    $0x64,%eax // al = `d`
    17b7:    81 7c 24 14 48 03 00     cmpl   $0x348,0x14(%rsp)  // 0x348 = 840 == d2
    17be:    00 
    17bf:    74 16                    je     17d7 <phase_3+0x15a>
    17c1:    e8 08 05 00 00           call   1cce <explode_bomb>
    17c6:    b8 64 00 00 00           mov    $0x64,%eax
    17cb:    eb 0a                    jmp    17d7 <phase_3+0x15a>
    17cd:    e8 fc 04 00 00           call   1cce <explode_bomb>
    17d2:    b8 6d 00 00 00           mov    $0x6d,%eax
x   17d7:    38 44 24 0f              cmp    %al,0xf(%rsp) // rax == c
    17db:    75 15                    jne    17f2 <phase_3+0x175>
    17dd:    48 8b 44 24 18           mov    0x18(%rsp),%rax
    17e2:    64 48 2b 04 25 28 00     sub    %fs:0x28,%rax
    17e9:    00 00 
    17eb:    75 0c                    jne    17f9 <phase_3+0x17c>
    17ed:    48 83 c4 28              add    $0x28,%rsp
    17f1:    c3                       ret
    17f2:    e8 d7 04 00 00           call   1cce <explode_bomb>
    17f7:    eb e4                    jmp    17dd <phase_3+0x160>
    17f9:    e8 52 fa ff ff           call   1250 <__stack_chk_fail@plt>

可以看到,每个块都一开始将 eax 赋值(其实只动了 al),然后每一块对于 d2 有不同的要求,满足要求后跳转到最后的 17d7。
进入 17d7 后再判断 al 是否和 c 相等,如果是则可以成功 return 了。

所以正确答案有七个:

0 b 624
1 x 813
2 u 140
3 p 265
4 q 863
5 o 997
6 q 224
7 d 840

phase_4:递归

0000000000001839 <phase_4>:
    1839:    f3 0f 1e fa              endbr64
    183d:    48 83 ec 18              sub    $0x18,%rsp
    1841:    64 48 8b 04 25 28 00     mov    %fs:0x28,%rax
    1848:    00 00 
    184a:    48 89 44 24 08           mov    %rax,0x8(%rsp)
    184f:    31 c0                    xor    %eax,%eax
    1851:    48 89 e1                 mov    %rsp,%rcx
    1854:    48 8d 54 24 04           lea    0x4(%rsp),%rdx
    1859:    48 8d 35 b7 1a 00 00     lea    0x1ab7(%rip),%rsi        # 3317 <array.0+0x177>
    1860:    e8 9b fa ff ff           call   1300 <__isoc99_sscanf@plt>
    1865:    83 f8 02                 cmp    $0x2,%eax
    1868:    75 0b                    jne    1875 <phase_4+0x3c>
    ...

看到这个函数的开头依然是和 phase_3 类似用 sscanf 处理数据,则同样查看 rsi 存的地址位置的字符串,发现是 %d %d。说明该程序读入两个 int。
接下来又是典型的判断,如果读到的 int 不等于两个则爆炸。

此时查看栈,和上一题类似,读入的 d1 和 d2 依此放在 (rsp + 4)、(rsp)。

接下来的代码是这样的:

0000000000001839 <phase_4>:
    ...
    186a:    8b 04 24                 mov    (%rsp),%eax // eax = d2
    186d:    83 e8 02                 sub    $0x2,%eax // eax = d2-2
    1870:    83 f8 02                 cmp    $0x2,%eax // eax <> 2?
    1873:    76 05                    jbe    187a <phase_4+0x41> // if eax <= 2 (d2 <= 4) jump
    1875:    e8 54 04 00 00           call   1cce <explode_bomb> // else boom!
    ...

上面这段从 186a 到 1875 的部分将 d2 减去 2 后与 2 进行无符号比较,实际上实现的是判断 d2 是否属于 $[2, 4]$,如果不在此范围则爆炸。

0000000000001839 <phase_4>:
    ...
    187a:    8b 34 24                 mov    (%rsp),%esi // esi = d2 (argument 2)
    187d:    bf 07 00 00 00           mov    $0x7,%edi // edi = 7 (argument 1)
    1882:    e8 77 ff ff ff           call   17fe <func4> 
    1887:    39 44 24 04              cmp    %eax,0x4(%rsp)
    188b:    75 15                    jne    18a2 <phase_4+0x69>
    188d:    48 8b 44 24 08           mov    0x8(%rsp),%rax
    1892:    64 48 2b 04 25 28 00     sub    %fs:0x28,%rax
    1899:    00 00 
    189b:    75 0c                    jne    18a9 <phase_4+0x70>
    189d:    48 83 c4 18              add    $0x18,%rsp
    18a1:    c3                       ret
    18a2:    e8 27 04 00 00           call   1cce <explode_bomb>
    18a7:    eb e4                    jmp    188d <phase_4+0x54>
    18a9:    e8 a2 f9 ff ff           call   1250 <__stack_chk_fail@plt>
    ...

上面这一部分调用了 func4 函数(代码如下),将其返回值(eax)和 d1 进行比较,如果相等就正常结束(拆除炸弹)。

调用 func4 函数时,其第一个参数 edi 是 7,第二个参数 esi 是 d2。我们查看 func4 函数:

00000000000017fe <func4>:
    17fe:    f3 0f 1e fa              endbr64
    1802:    b8 00 00 00 00           mov    $0x0,%eax
    1807:    85 ff                    test   %edi,%edi // if edi <= 0
    1809:    7e 2d                    jle    1838 <func4+0x3a> // return 0
    180b:    41 54                    push   %r12
    180d:    55                       push   %rbp
    180e:    53                       push   %rbx
    180f:    89 fb                    mov    %edi,%ebx // ebx = a1 = 7
    1811:    89 f5                    mov    %esi,%ebp // save esi
    1813:    89 f0                    mov    %esi,%eax // eax = a2 = d2
    1815:    83 ff 01                 cmp    $0x1,%edi // edi == 1?
    1818:    74 19                    je     1833 <func4+0x35> // return d2
    181a:    8d 7f ff                 lea    -0x1(%rdi),%edi // edi -= 1
    181d:    e8 dc ff ff ff           call   17fe <func4>
    1822:    44 8d 24 28              lea    (%rax,%rbp,1),%r12d // r12d = rax + esi
    1826:    8d 7b fe                 lea    -0x2(%rbx),%edi // edi = rbx - 2
    1829:    89 ee                    mov    %ebp,%esi // restore esi
    182b:    e8 ce ff ff ff           call   17fe <func4>
    1830:    44 01 e0                 add    %r12d,%eax // eax += r12d = 2eax + esi
    1833:    5b                       pop    %rbx
    1834:    5d                       pop    %rbp
    1835:    41 5c                    pop    %r12
    1837:    c3                       ret
    1838:    c3                       ret

对 r12、rbp、rbx 压栈,是因为这三个寄存器是 callee-saved。
注意到 1811 行把 esi 复制到了 ebp,1829 行又将 ebp 放回了 esi。不难猜测,这是在调用 func4 前后保存 esi,esi 是 caller-saved。

接下来就是递归调用 func4 函数。这一部分如果手动模拟非常复杂(并且每个 func4 里最多调用了两次 func4)。我们可以先通过将汇编代码转换为逻辑伪代码的方式,尝试一下「复原」C 代码(差不多就是人肉 IDA)。

auto func4(int x, int y){ // x:edi; y:esi; z:edx; w:r12
    if (x <= 0) return 0;
    int z = x;
    if (x == 1) return y;
    x--;
    int w = func4(x, y) + y;
    x = z - 2;
    return func4(x, y) + w;
}

// simplify futher: 

auto func4(int x, int y){
    if (x <= 0) return 0;
    if (x == 1) return y;
    return func4(x-2, y) + func4(x-1, y) + y;
}

复原出这个代码,func4 的函数逻辑就很清晰了。可以递推计算出 func4(7, y) = 33y

别忘了我们最初的目标,我们是要输入这样的 d1 和 d2:

所以可能的答案有如下几种:

66 2
99 3
132 4

phase_5:简单链表

直接看 phase_5 函数,依然先看读入数据的部分:

00000000000018ae <phase_5>:
    18ae:       f3 0f 1e fa             endbr64
    18b2:       48 83 ec 18             sub    $0x18,%rsp
    18b6:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
    18bd:       00 00 
    18bf:       48 89 44 24 08          mov    %rax,0x8(%rsp)
    18c4:       31 c0                   xor    %eax,%eax
    18c6:       48 8d 4c 24 04          lea    0x4(%rsp),%rcx
    18cb:       48 89 e2                mov    %rsp,%rdx
    18ce:       48 8d 35 42 1a 00 00    lea    0x1a42(%rip),%rsi        # 3317 <array.0+0x177>
    18d5:       e8 26 fa ff ff          call   1300 <__isoc99_sscanf@plt>
    18da:       83 f8 01                cmp    $0x1,%eax
    18dd:       7e 5a                   jle    1939 <phase_5+0x8b>
    ...

phase_5 函数的开头还是和前面一样用到了 sscanf,读取 rsi 处的内存可以看到格式字符串是 %d %d,也就是读入两个 int。接下来一样判断如果读入的数字不足 2 个就爆炸。
读入的两个数字计为 d1、d2,查看栈可以得知它们分别存在 (rsp) 和 (rsp + 4)。

接下来:

00000000000018ae <phase_5>:
    ...
    18df:       8b 04 24                mov    (%rsp),%eax // eax = d1
    18e2:       83 e0 0f                and    $0xf,%eax // eax = d1 & 0xf
    18e5:       89 04 24                mov    %eax,(%rsp) // d1 &= 0xf
    18e8:       83 f8 0f                cmp    $0xf,%eax
    18eb:       74 32                   je     191f <phase_5+0x71>
    ...

这一小段代码将 d1 取出来 and 上一个 0xf 再放回去,再判断其是否等于 0xf,如果等于就爆炸。所以,d1 & 0xf 不能等于 15。
此时 d1 & 0xf 存放在 eax 里。继续看下面的代码:

00000000000018ae <phase_5>:
    ...
    18ed:       b9 00 00 00 00          mov    $0x0,%ecx
    18f2:       ba 00 00 00 00          mov    $0x0,%edx
    18f7:       48 8d 35 a2 18 00 00    lea    0x18a2(%rip),%rsi        # 31a0 <array.0>
    18fe:       83 c2 01                add    $0x1,%edx
    1901:       48 98                   cltq
    1903:       8b 04 86                mov    (%rsi,%rax,4),%eax
    1906:       01 c1                   add    %eax,%ecx
    1908:       83 f8 0f                cmp    $0xf,%eax
    190b:       75 f1                   jne    18fe <phase_5+0x50>

    190d:       c7 04 24 0f 00 00 00    movl   $0xf,(%rsp)
    1914:       83 fa 0f                cmp    $0xf,%edx
    1917:       75 06                   jne    191f <phase_5+0x71>
    1919:       39 4c 24 04             cmp    %ecx,0x4(%rsp)
    191d:       74 05                   je     1924 <phase_5+0x76>
    191f:       e8 aa 03 00 00          call   1cce <explode_bomb>
    1924:       48 8b 44 24 08          mov    0x8(%rsp),%rax
    1929:       64 48 2b 04 25 28 00    sub    %fs:0x28,%rax
    1930:       00 00 
    1932:       75 0c                   jne    1940 <phase_5+0x92>
    1934:       48 83 c4 18             add    $0x18,%rsp
    1938:       c3                      ret
    1939:       e8 90 03 00 00          call   1cce <explode_bomb>
    193e:       eb 9f                   jmp    18df <phase_5+0x31>
    1940:       e8 0b f9 ff ff          call   1250 <__stack_chk_fail@plt>

似乎 rsi 开始存放的是一个常量数组(有点类似之前的跳转表)。打印出来是这样的:

(gdb) x/32wx 0x5555555571a0
0x5555555571a0 <array.0>:       0x0000000a      0x00000002      0x0000000e      0x00000007
0x5555555571b0 <array.0+16>:    0x00000008      0x0000000c      0x0000000f      0x0000000b
0x5555555571c0 <array.0+32>:    0x00000000      0x00000004      0x00000001      0x0000000d
0x5555555571d0 <array.0+48>:    0x00000003      0x00000009      0x00000006      0x00000005
0x5555555571e0: 0x21776f57      0x756f5920      0x20657627      0x75666564
0x5555555571f0: 0x20646573      0x20656874      0x72636573      0x73207465
0x555555557200: 0x65676174      0x00000021      0x79206f53      0x7420756f
0x555555557210: 0x6b6e6968      0x756f7920      0x6e616320      0x6f747320

把这个表 key-value 列出来,内容是这样的:

0: 10
1: 2
2: 14
3: 7
4: 8
5: 12
6: 15
7: 11
8: 0
9: 4
10: 1
11: 13
12: 3
13: 9
14: 6
15: 5

上面的代码 190b 行跳转回了 18fe,说明这又是一个类似循环的东西。我们还是尝试将 C 代码复原出来:

// x:ecx; y:edx; t:eax;
x = y = 0;
t = d1 & 0xf;

do {
    y++;
    t = a[t];
    x += t;
} while (t != 0xf);

d1 = 0xf;
if (y != 0xf) boom();
if (x != d2) boom();
ret = (rsp+8);
return;

不难发现这个表项内容是 0 到 15 的一个排列,其实这是一个单向链表!
而 while 循环停下的条件是 t 为 0xf,也就是出口为 15。其中累计跳转次数(y)必须是 15,并且累积的 x 必须最终和 d2 相等。

经过反向的查找(倒着走 15 步),可以得知链表的入口为 5。
走 15 步,只有 (15, 5) 这条边没有经过,可算得总和是 115。所以这一关的答案是:

5 115

phase_6:二重循环与链表

看这题的函数汇编代码非常长,直接一起阅读令人望而生畏。所以,将这个函数的代码分成几块(部分)来阅读是一个好方法。
代码之间有很多 jump 的跳转关系,如何将代码分块呢?首先当然是逻辑上凭感觉分块,其次分块之后尽量让函数在每块之内跳来跳去,每块只有一个入口和出口。这样就可以说将函数分为了几个子函数,只有子函数内部存在循环等复杂结构。

依然首先看看读入数据的部分:

0000000000001945 <phase_6>:
    1945:    f3 0f 1e fa              endbr64
    1949:    41 56                    push   %r14
    194b:    41 55                    push   %r13
    194d:    41 54                    push   %r12
    194f:    55                       push   %rbp
    1950:    53                       push   %rbx
    1951:    48 83 ec 60              sub    $0x60,%rsp
    1955:    64 48 8b 04 25 28 00     mov    %fs:0x28,%rax
    195c:    00 00 
    195e:    48 89 44 24 58           mov    %rax,0x58(%rsp)
    1963:    31 c0                    xor    %eax,%eax
    1965:    49 89 e5                 mov    %rsp,%r13
    1968:    4c 89 ee                 mov    %r13,%rsi
    196b:    e8 8a 03 00 00           call   1cfa <read_six_numbers>
    ...

这一部分是开头工作,没什么特别重要的。一上来就保存了 r14、r13、r12、rbp、rbx 一大堆寄存器(说明在下面这些寄存器都用到了),然后开了挺大的一个栈空间。
接下来依然是 Canary 机制,不同之处在于后面 rax 也要被用到,所以将 Canary 存进 rax 又将 rax 暂存了起来,然后将 eax 清零了。

接下来,将 rsp 存放到 rsi 作为 read_six_numbers 的参数,这个参数并不是标示传入的字符串,而是标示读到的数据存储的位置。读取六个数字之后,它们会被依次存放在栈中 rsp、rsp + 4、rsp + 8……的位置。

0000000000001945 <phase_6>:
    ...
    1970:    41 be 01 00 00 00        mov    $0x1,%r14d
    1976:    49 89 e4                 mov    %rsp,%r12
    1979:    eb 28                    jmp    19a3 <phase_6+0x5e>--------------.
    197b:    e8 4e 03 00 00           call   1cce <explode_bomb>              |
    1980:    eb 30                    jmp    19b2 <phase_6+0x6d>              |
        :                                                                     |
    1982:    48 83 c3 01              add    $0x1,%rbx <--------------.       |
    1986:    83 fb 05                 cmp    $0x5,%ebx                |       |
    1989:    7f 10                    jg     199b <phase_6+0x56>--.   |       |
    198b:    41 8b 04 9c              mov    (%r12,%rbx,4),%eax <-+---+---.   |
    198f:    39 45 00                 cmp    %eax,0x0(%rbp)       |   |   |   |
    1992:    75 ee                    jne    1982 <phase_6+0x3d>--+---`   |   |
    1994:    e8 35 03 00 00           call   1cce <explode_bomb>  |       |   |
    1999:    eb e7                    jmp    1982 <phase_6+0x3d>  |       |   |
        :                                                         |       |   |
    199b:    49 83 c6 01              add    $0x1,%r14 <----------`       |   |
    199f:    49 83 c5 04              add    $0x4,%r13                    |   |
        :                                                                 |   |
    19a3:    4c 89 ed                 mov    %r13,%rbp <------------------+---`
    19a6:    41 8b 45 00              mov    0x0(%r13),%eax               |
    19aa:    83 e8 01                 sub    $0x1,%eax                    |
    19ad:    83 f8 05                 cmp    $0x5,%eax                    |
    19b0:    77 c9                    ja     197b <phase_6+0x36>--->boom  |
    19b2:    41 83 fe 05              cmp    $0x5,%r14d                   |
    19b6:    7f 05                    jg     19bd <phase_6+0x78>---.      |
    19b8:    4c 89 f3                 mov    %r14,%rbx             |      |
    19bb:    eb ce                    jmp    198b <phase_6+0x46>---+------`
    ...

还是试试人肉 IDA 大法。对于复杂的循环,我们可以一律视为无限循环,将判断条件写为其中的 if break。
需要注意,从 1982 行开始的这个循环入口是 198b 行,所以 C 代码中循环的代码块逻辑上是从 198b 行开始的。按照这样写下来,会发现复原出来的代码比汇编代码看起来逻辑清晰很多(比如 1982 行不知所云的突然 add)。
按照汇编代码的流程走,如果遇到了重复执行,就可以感觉到进入了下一轮循环。

在人肉 IDA 的时候,我们可以先对着汇编代码写出一个初步的伪代码,将循环分支等结构表示出来,保留寄存器当作变量使用;第二步再将其抽象成真正的 C 代码,将变量等都进行一定的抽象,最后就可以得到一个类似我们自己写的代码的源码。

// 之前 r13 存放的是 rsp
r14 = 1;
r12 = rsp;

for (;;){
    rbp = r13;
    eax = (r13);
    eax--;
    if eax > 5 boom();
    if r14 > 5 break;
    rbx = r14;
    for (;;){
        eax = (r12 + 4*rbx);
        if eax == (rbp) boom();
        rbx++;
        if ebx > 5 break;
    }
    r14++;
    r13 += 4;
}

// 进一步简化
// i:r14; j:rbx;
// a:输入的六个数字组成的数组
// r13 = rbp
i = 1;

for (;;){
    if (*r13 > 6) boom();
    if (i > 5) break;
    j = i;
    for (;;){
        if (a[j] == *r13) boom(); // 其实这里 *r13 就是 a[i-1]
        j++;
        if (j > 5) break;
    }
    i++;
    r13++; // 4 Byte 指针的 ++ 相当于 +4
}

简化到这一步,这个双层循环的代码逻辑就很清晰了。我们输入的六个数字 a1 到 a5 要满足:

注意这里 i、j 从 1 开始,这意味着我们不管 a0。
终于解决了魔鬼双重循环,继续看下一段汇编:

0000000000001945 <phase_6>:
    ...
    19bd:    be 00 00 00 00           mov    $0x0,%esi
    19c2:    8b 0c b4                 mov    (%rsp,%rsi,4),%ecx <-------------.
    19c5:    b8 01 00 00 00           mov    $0x1,%eax                        |
    19ca:    48 8d 15 3f 38 00 00     lea    0x383f(%rip),%rdx        # 5210 <|node1>
    19d1:    83 f9 01                 cmp    $0x1,%ecx                        |
    19d4:    7e 0b                    jle    19e1 <phase_6+0x9c>----------.   |
    19d6:    48 8b 52 08              mov    0x8(%rdx),%rdx <---------.   |   |
    19da:    83 c0 01                 add    $0x1,%eax                |   |   |
    19dd:    39 c8                    cmp    %ecx,%eax                |   |   |
    19df:    75 f5                    jne    19d6 <phase_6+0x91>------`   |   |
    19e1:    48 89 54 f4 20           mov    %rdx,0x20(%rsp,%rsi,8) <-----`   |
    19e6:    48 83 c6 01              add    $0x1,%rsi                        |
    19ea:    48 83 fe 06              cmp    $0x6,%rsi                        |
    19ee:    75 d2                    jne    19c2 <phase_6+0x7d>--------------`
    ...

肯定还有循环,还是人肉 IDA 一下吧:

esi = 0;
for (;;){
    ecx = a[rsi];
    eax = 1;
    rdx = 5210;
    if (ecx > 1){
        for (;;){
            rdx = (rdx + 8);
            eax++;
            if (eax == ecx) break;
        }
    }
    (rsp + 0x20 + 8*rsi) = rdx;
    rsi++;
    if (rsi == 6) break;
}

注意这段代码里的 rdx = (rdx + 8)。这里可以看出 rdx 本身存的是一个内存地址,它指向 8 Byte 内存(的起始位置)。紧跟这段内存之后的,是 8 Byte 的 next 指针,指向下一个 rdx。没错,这就是一个链表。
我们可以想象成这样一个结构:每个指针指向的是一个结构体(的内存起始位置),每个结构体中,第一个元素是 8 Byte 的某个东西,第二个元素是 next,一个 8 Byte 的指针。

这一步 lea 0x383f(%rip),%rdx 得到的地址是什么呢?objdump 反汇编出来的注释显示的是「node1」。我们直接用 gdb 运行,然后查看这一部分的内容:

(gdb) x/16gx 0x555555559210
0x555555559210 <node1>: 0x0000000100000336      0x0000555555559220
0x555555559220 <node2>: 0x000000020000013e      0x0000555555559230
0x555555559230 <node3>: 0x0000000300000217      0x0000555555559240
0x555555559240 <node4>: 0x000000040000031d      0x0000555555559250
0x555555559250 <node5>: 0x00000005000001a8      0x0000555555559110
0x555555559260 <host_table>:    0x0000555555557371      0x0000000000000000
0x555555559270 <host_table+16>: 0x0000000000000000      0x0000000000000000
0x555555559280 <host_table+32>: 0x0000000000000000      0x0000000000000000

看来我们的猜测没错!这部分内存里静态地存了五个 node,每个 node 是 16 字节,前 8 字节存了值,后 8 字节存了 next 指针。
node5 指向的 0x0000555555559110 地址其实是 node6:

(gdb) x/4gx 0x555555559110
0x555555559110 <node6>: 0x00000006000000bc      0x0000000000000000
0x555555559120 <bomb_id>:       0x00000000000001e3      0x0000000000000000

既然有了指针和结构体的抽象,我们可以进一步简化这个代码:

// 进一步简化:
// i:rsi
i = 0;
for (;;){
    p = node1;
    if (a[i] > 1){
        j = 1;
        for (;;){
            p = p->next;
            j++;
            if (j == a[i]) break;
        }
    }
    (rsp + 0x20 + 8*i) = p;
    i++;
    if (i == 6) break;
}

这时候代码逻辑就非常简单了。对于每个 ai,从 node1 开始走 ai-1 次(所以任何 ai 不呢大于 6),停在的 node 指针存入栈上 rsp + 0x20 开始的位置。

接下来,可以看到一堆看起来很整齐的 mov 操作:

0000000000001945 <phase_6>:
    ...
    19f0:    48 8b 5c 24 20           mov    0x20(%rsp),%rbx
    19f5:    48 8b 44 24 28           mov    0x28(%rsp),%rax
    19fa:    48 89 43 08              mov    %rax,0x8(%rbx)
    19fe:    48 8b 54 24 30           mov    0x30(%rsp),%rdx
    1a03:    48 89 50 08              mov    %rdx,0x8(%rax)
    1a07:    48 8b 44 24 38           mov    0x38(%rsp),%rax
    1a0c:    48 89 42 08              mov    %rax,0x8(%rdx)
    1a10:    48 8b 54 24 40           mov    0x40(%rsp),%rdx
    1a15:    48 89 50 08              mov    %rdx,0x8(%rax)
    1a19:    48 8b 44 24 48           mov    0x48(%rsp),%rax
    1a1e:    48 89 42 08              mov    %rax,0x8(%rdx)
    1a22:    48 c7 40 08 00 00 00     movq   $0x0,0x8(%rax)
    1a29:    00 
    ...

上面这一段代码,把之前链表的关系重写了。
记从 rsp + 0x20 开始,每 8 个字节存的指针依次计为 ptr0、ptr1、ptr2……经过这段代码的处理,我们有 *(ptr0 + 8) = ptr1,*(ptr1 + 8) = ptr2……
也就是说,将存入栈的指针顺序覆盖了之前 node 的边关系,重写了 node 的 next 指针,使其按照栈中节点排列的顺序相连。

接下来,我们终于来到了这个 phase 的核心判断:

0000000000001945 <phase_6>:
    ...
    1a2a:    bd 05 00 00 00           mov    $0x5,%ebp
    1a2f:    eb 09                    jmp    1a3a <phase_6+0xf5>---.
    1a31:    48 8b 5b 08              mov    0x8(%rbx),%rbx <------+---.
    1a35:    83 ed 01                 sub    $0x1,%ebp             |   |
    1a38:    74 11                    je     1a4b <phase_6+0x106>--+---+---.
    1a3a:    48 8b 43 08              mov    0x8(%rbx),%rax  <-----`   |   |
    1a3e:    8b 00                    mov    (%rax),%eax               |   |
    1a40:    39 03                    cmp    %eax,(%rbx)               |   |
    1a42:    7d ed                    jge    1a31 <phase_6+0xec>-------`   |
    1a44:    e8 85 02 00 00           call   1cce <explode_bomb>           |
    1a49:    eb e6                    jmp    1a31 <phase_6+0xec>           |
    ...

和之前一样的人肉 IDA 思路,将其改写成 C 代码,大概是这样的:

// 在之前,rbx 被置为 ptr0,即 (rsp + 0x20)
ebp = 5;
for (;;) {
    rax = rbx->next;
    eax = *rax;
    if (*rbx < eax) boom();
    rbx = rbx->next;
    ebp--;
    if (ebp == 0) break;
}

// 进一步抽象:
cnt = 5;
for (;;) {
    if (p->value < (p->next->value)) boom();
    p = p->next;
    cnt--;
    if (cnt == 0) break;
}

这一部分就是真正的核心判断部分。
注意,p->value 这个值指的是指针开始的 8 Byte 内存(因为存放的寄存器是 eax),取出之后又放入 4 Byte 的 eax(高位 0 填充)。
简化成这样逻辑就简单了,不就是遍历这个链表嘛,正好 6 次经过 6 个节点,遍历时依次经过的六个值必须满足单调递减,否则 boom。
实际上这题是要我们对 node 从大到小重新排序,使其值单调减。回顾之前查看 node 时看到的值:

node1: 0x336
node2: 0x13e
node3: 0x217
node4: 0x31d
node5: 0x1a8
node6: 0x0bc

从大到小排序,满足要求的排列是:1 4 3 5 2 6。这就是本题的答案。

接下来,这个函数就接近尾声了:

0000000000001945 <phase_6>:
    ...
    1a4b:    48 8b 44 24 58           mov    0x58(%rsp),%rax
    1a50:    64 48 2b 04 25 28 00     sub    %fs:0x28,%rax
    1a57:    00 00 
    1a59:    75 0d                    jne    1a68 <phase_6+0x123>
    1a5b:    48 83 c4 60              add    $0x60,%rsp
    1a5f:    5b                       pop    %rbx
    1a60:    5d                       pop    %rbp
    1a61:    41 5c                    pop    %r12
    1a63:    41 5d                    pop    %r13
    1a65:    41 5e                    pop    %r14
    1a67:    c3                       ret
    1a68:    e8 e3 f7 ff ff           call   1250 <__stack_chk_fail@plt>
    ...

这一部分就是一些收尾工作。将之前 195e 行暂存起来的 rax 恢复,然后是 Canary 机制的判断,然后收栈、恢复寄存器、返回。

这题所有 node 的值都已经确定了,所以 1 4 3 5 2 6 应该是唯一的答案。

进入 secret_phase

在看反编译出来的代码的过程中,发现除了 phase_1 到 phase_6 以外,还有一个 secret_phase,算是作者隐藏的小彩蛋吧。其实 bomb.c 的最后也有暗示:

    /* Wow, they got it!  But isn't something... missing?  Perhaps
     * something they overlooked?  Mua ha ha ha ha! */
    
    return 0;

然而在寻常地完成六个 phase 的时候,并不会进入 secret_phase。

在反编译出来的代码中搜索,发现只有 phase_defused 函数调用了 secret_phase。审计这个函数代码:

0000000000001e77 <phase_defused>:
    1e77:    f3 0f 1e fa              endbr64
    1e7b:    48 83 ec 78              sub    $0x78,%rsp
    1e7f:    64 48 8b 04 25 28 00     mov    %fs:0x28,%rax
    1e86:    00 00 
    1e88:    48 89 44 24 68           mov    %rax,0x68(%rsp)
    1e8d:    31 c0                    xor    %eax,%eax
    1e8f:    83 3d 5a 38 00 00 06     cmpl   $0x6,0x385a(%rip)        # 56f0 <num_input_strings>
    1e96:    74 15                    je     1ead <phase_defused+0x36>
    1e98:    48 8b 44 24 68           mov    0x68(%rsp),%rax
    1e9d:    64 48 2b 04 25 28 00     sub    %fs:0x28,%rax
    1ea4:    00 00 
    1ea6:    75 73                    jne    1f1b <phase_defused+0xa4>
    1ea8:    48 83 c4 78              add    $0x78,%rsp
    1eac:    c3                       ret
    ...

上面这段代码,最关键的是:判断如果 (rip + 0x385a) 也就是 (56f0) 内存中存放的值等于 6 则跳转进下面的代码,否则函数正常返回,不会进入之后的代码。进入 secret_phase 的入口其实在后面的代码里。
记住这个地址:56f0。搜索反编译出来的汇编代码全文,可以找到 read_line 函数里有涉及写入 (56f0) 的代码:

0000000000001d3f <read_line>:
    1d3f:    f3 0f 1e fa              endbr64
    1d43:    55                       push   %rbp
    1d44:    53                       push   %rbx
    1d45:    48 83 ec 08              sub    $0x8,%rsp
    1d49:    b8 00 00 00 00           mov    $0x0,%eax
    1d4e:    e8 29 ff ff ff           call   1c7c <skip>
    1d53:    48 85 c0                 test   %rax,%rax
    1d56:    74 5d                    je     1db5 <read_line+0x76>
    1d58:    8b 2d 92 39 00 00        mov    0x3992(%rip),%ebp        # 56f0 <num_input_strings>
    1d5e:    48 63 c5                 movslq %ebp,%rax
    1d61:    48 8d 1c 80              lea    (%rax,%rax,4),%rbx
    1d65:    48 c1 e3 04              shl    $0x4,%rbx
    1d69:    48 8d 05 90 39 00 00     lea    0x3990(%rip),%rax        # 5700 <input_strings>
    1d70:    48 01 c3                 add    %rax,%rbx
    1d73:    48 89 df                 mov    %rbx,%rdi
    1d76:    e8 c5 f4 ff ff           call   1240 <strlen@plt>
    1d7b:    83 f8 4e                 cmp    $0x4e,%eax
    1d7e:    0f 8f a9 00 00 00        jg     1e2d <read_line+0xee>
    1d84:    83 e8 01                 sub    $0x1,%eax
    1d87:    48 98                    cltq
    1d89:    48 63 d5                 movslq %ebp,%rdx
    1d8c:    48 8d 0c 92              lea    (%rdx,%rdx,4),%rcx
    1d90:    48 c1 e1 04              shl    $0x4,%rcx
    1d94:    48 8d 15 65 39 00 00     lea    0x3965(%rip),%rdx        # 5700 <input_strings>
    1d9b:    48 01 ca                 add    %rcx,%rdx
    1d9e:    c6 04 02 00              movb   $0x0,(%rdx,%rax,1)
    1da2:    83 c5 01                 add    $0x1,%ebp
    1da5:    89 2d 45 39 00 00        mov    %ebp,0x3945(%rip)        # 56f0 <num_input_strings>
    1dab:    48 89 d8                 mov    %rbx,%rax
    1dae:    48 83 c4 08              add    $0x8,%rsp
    1db2:    5b                       pop    %rbx
    1db3:    5d                       pop    %rbp
    1db4:    c3                       ret

    1db5:    48 8b 05 b4 38 00 00     mov    0x38b4(%rip),%rax        # 5670 <stdin@GLIBC_2.2.5>
    1dbc:    48 39 05 cd 38 00 00     cmp    %rax,0x38cd(%rip)        # 5690 <infile>
    1dc3:    74 1b                    je     1de0 <read_line+0xa1>
    1dc5:    48 8d 3d 6f 15 00 00     lea    0x156f(%rip),%rdi        # 333b <array.0+0x19b>
    1dcc:    e8 1f f4 ff ff           call   11f0 <getenv@plt>
    1dd1:    48 85 c0                 test   %rax,%rax
    1dd4:    74 20                    je     1df6 <read_line+0xb7>
    1dd6:    bf 00 00 00 00           mov    $0x0,%edi
    1ddb:    e8 50 f5 ff ff           call   1330 <exit@plt>
    1de0:    48 8d 3d 36 15 00 00     lea    0x1536(%rip),%rdi        # 331d <array.0+0x17d>
    1de7:    e8 34 f4 ff ff           call   1220 <puts@plt>
    1dec:    bf 08 00 00 00           mov    $0x8,%edi
    1df1:    e8 3a f5 ff ff           call   1330 <exit@plt>
    1df6:    48 8b 05 73 38 00 00     mov    0x3873(%rip),%rax        # 5670 <stdin@GLIBC_2.2.5>
    1dfd:    48 89 05 8c 38 00 00     mov    %rax,0x388c(%rip)        # 5690 <infile>
    1e04:    b8 00 00 00 00           mov    $0x0,%eax
    1e09:    e8 6e fe ff ff           call   1c7c <skip>
    1e0e:    48 85 c0                 test   %rax,%rax
    1e11:    0f 85 41 ff ff ff        jne    1d58 <read_line+0x19>
    1e17:    48 8d 3d ff 14 00 00     lea    0x14ff(%rip),%rdi        # 331d <array.0+0x17d>
    1e1e:    e8 fd f3 ff ff           call   1220 <puts@plt>
    1e23:    bf 00 00 00 00           mov    $0x0,%edi
    1e28:    e8 03 f5 ff ff           call   1330 <exit@plt>
    1e2d:    48 8d 3d 12 15 00 00     lea    0x1512(%rip),%rdi        # 3346 <array.0+0x1a6>
    1e34:    e8 e7 f3 ff ff           call   1220 <puts@plt>
    1e39:    8b 05 b1 38 00 00        mov    0x38b1(%rip),%eax        # 56f0 <num_input_strings>
    1e3f:    8d 50 01                 lea    0x1(%rax),%edx
    1e42:    89 15 a8 38 00 00        mov    %edx,0x38a8(%rip)        # 56f0 <num_input_strings>
    1e48:    48 98                    cltq
    1e4a:    48 6b c0 50              imul   $0x50,%rax,%rax
    1e4e:    48 8d 15 ab 38 00 00     lea    0x38ab(%rip),%rdx        # 5700 <input_strings>
    1e55:    48 be 2a 2a 2a 74 72     movabs $0x636e7572742a2a2a,%rsi
    1e5c:    75 6e 63 
    1e5f:    48 bf 61 74 65 64 2a     movabs $0x2a2a2a64657461,%rdi
    1e66:    2a 2a 00 
    1e69:    48 89 34 02              mov    %rsi,(%rdx,%rax,1)
    1e6d:    48 89 7c 02 08           mov    %rdi,0x8(%rdx,%rax,1)
    1e72:    e8 57 fe ff ff           call   1cce <explode_bomb>

先粗略地看一看上面的这段代码,发现它主要分为两个部分,如果调用 skip 函数返回 0 就进入第二个部分(1db5 开始的行),否则继续第一个部分。第一个部分最后 ret,第二个部分最后 explode_bomb。我们可以先不管它做了什么,只需要看哪里修改了 56f0。

上面这段代码主要修改 56f0 这个位置的有两处:

0000000000001d3f <read_line>:
    ...
    1d58:    8b 2d 92 39 00 00        mov    0x3992(%rip),%ebp        # 56f0 <num_input_strings>
    ...
    1da2:    83 c5 01                 add    $0x1,%ebp
    1da5:    89 2d 45 39 00 00        mov    %ebp,0x3945(%rip)        # 56f0 <num_input_strings>
    ...
    ...
    1e39:    8b 05 b1 38 00 00        mov    0x38b1(%rip),%eax        # 56f0 <num_input_strings>
    1e3f:    8d 50 01                 lea    0x1(%rax),%edx
    1e42:    89 15 a8 38 00 00        mov    %edx,0x38a8(%rip)        # 56f0 <num_input_strings>
    ...

不管是哪一处,做的事情都是将 56f0 中存的数加一。也就是说,每调用一次 read_line 函数,这个位置记录的数字会加一。所以只有拆完最后一个 phase,这个地方会变成 6,能够进入下一部分。

继续看下面的代码:

0000000000001e77 <phase_defused>:
    ...
    1ead:    48 8d 4c 24 0c           lea    0xc(%rsp),%rcx
    1eb2:    48 8d 54 24 08           lea    0x8(%rsp),%rdx
    1eb7:    4c 8d 44 24 10           lea    0x10(%rsp),%r8
    1ebc:    48 8d 35 9e 14 00 00     lea    0x149e(%rip),%rsi        # 3361 <array.0+0x1c1>
    1ec3:    48 8d 3d 26 39 00 00     lea    0x3926(%rip),%rdi        # 57f0 <input_strings+0xf0>
    1eca:    e8 31 f4 ff ff           call   1300 <__isoc99_sscanf@plt>
    1ecf:    83 f8 03                 cmp    $0x3,%eax
    1ed2:    74 0e                    je     1ee2 <phase_defused+0x6b>
    1ed4:    48 8d 3d c5 13 00 00     lea    0x13c5(%rip),%rdi        # 32a0 <array.0+0x100>
    1edb:    e8 40 f3 ff ff           call   1220 <puts@plt>
    1ee0:    eb b6                    jmp    1e98 <phase_defused+0x21>

上面这段里,传入 sscanf 的两个参数 rsi 和 rdi,分别是 phase_4 输入的字符串,和 %d %d %s。也就是说,这里除了读入 phase_4 输入的两个数字之外,还读入后面的一个字符串!

(gdb) x/4bs $rdi
0x5555555597f0 <input_strings+240>:     "66 2"
0x5555555597f5 <input_strings+245>:     ""
0x5555555597f6 <input_strings+246>:     ""
0x5555555597f7 <input_strings+247>:     ""
(gdb) x/4bs $rsi
0x555555557361: "%d %d %s"
0x55555555736a: "DrEvil"
0x555555557371: "liu-virtual-machine"
0x555555557385: ""

如果读出来的个数是 3 个(读到了字符串)则跳转后面 1ee2 执行,否则就 puts 客套话。查看 1ed4 行的 32a0 存的字符串,是 Congratulations! You've defused the bomb!

看来在 phase_4 的输入里,除了两个数字,还应该输入一个字符串,才能进入下面的代码。

0000000000001e77 <phase_defused>:
    ...
    1ee2:    48 8d 7c 24 10           lea    0x10(%rsp),%rdi
    1ee7:    48 8d 35 7c 14 00 00     lea    0x147c(%rip),%rsi        # 336a <array.0+0x1ca>
    1eee:    e8 c7 fc ff ff           call   1bba <strings_not_equal>
    1ef3:    85 c0                    test   %eax,%eax
    1ef5:    75 dd                    jne    1ed4 <phase_defused+0x5d>
    1ef7:    48 8d 3d 42 13 00 00     lea    0x1342(%rip),%rdi        # 3240 <array.0+0xa0>
    1efe:    e8 1d f3 ff ff           call   1220 <puts@plt>
    1f03:    48 8d 3d 5e 13 00 00     lea    0x135e(%rip),%rdi        # 3268 <array.0+0xc8>
    1f0a:    e8 11 f3 ff ff           call   1220 <puts@plt>
    1f0f:    b8 00 00 00 00           mov    $0x0,%eax
    1f14:    e8 95 fb ff ff           call   1aae <secret_phase>
    1f19:    eb b9                    jmp    1ed4 <phase_defused+0x5d>
    1f1b:    e8 30 f3 ff ff           call   1250 <__stack_chk_fail@plt>

又见到了熟悉的 strings_not_equal 函数。我们试试输入 phase_4 的时候加上一个字符串 test,在这个函数之前,看看 rdi、rsi 两个参数是啥:

(gdb) x/4bs $rdi
0x7fffffffe050: "test"
0x7fffffffe055: "U"
0x7fffffffe057: ""
0x7fffffffe058: "@\222UUUU"
(gdb) x/4bs $rsi
0x55555555736a: "DrEvil"
0x555555557371: "liu-virtual-machine"
0x555555557385: ""
0x555555557386: ""

显然,要求输入的字符串是 DrEvilstrings_not_equal 函数 eax 返回 0,才会进入下面的部分,否则就回到上面输出客套话和正常退出的代码。
经过重重困难,我们终于走到了 call secret_phase

secret_phase:递归与链表

我们先来看 secret_phase 里调用的 fun7 函数。这个函数接受两个参数 rdi 和 rsi。

0000000000001a6d <fun7>:
    1a6d:    f3 0f 1e fa              endbr64
    1a71:    48 85 ff                 test   %rdi,%rdi
    1a74:    74 32                    je     1aa8 <fun7+0x3b>-------------.
    1a76:    48 83 ec 08              sub    $0x8,%rsp                    |
    1a7a:    8b 17                    mov    (%rdi),%edx                  |
    1a7c:    39 f2                    cmp    %esi,%edx                    |
    1a7e:    7f 0c                    jg     1a8c <fun7+0x1f>---.         |
    1a80:    b8 00 00 00 00           mov    $0x0,%eax          |         |
    1a85:    75 12                    jne    1a99 <fun7+0x2c>---+---.     |
    1a87:    48 83 c4 08              add    $0x8,%rsp <--------+---+-----+----.
    1a8b:    c3                       ret                       |   |     |    |
    1a8c:    48 8b 7f 08              mov    0x8(%rdi),%rdi <---`   |     |    |
    1a90:    e8 d8 ff ff ff           call   1a6d <fun7>            |     |    |
    1a95:    01 c0                    add    %eax,%eax              |     |    |
    1a97:    eb ee                    jmp    1a87 <fun7+0x1a>-------+-----+----+
    1a99:    48 8b 7f 10              mov    0x10(%rdi),%rdi <------`     |    |
    1a9d:    e8 cb ff ff ff           call   1a6d <fun7>                  |    |
    1aa2:    8d 44 00 01              lea    0x1(%rax,%rax,1),%eax        |    |
    1aa6:    eb df                    jmp    1a87 <fun7+0x1a>-------------+----+
    1aa8:    b8 ff ff ff ff           mov    $0xffffffff,%eax <-----------`
    1aad:    c3                       ret

还是人肉 IDA 出来:

if (rdi == 0) return -1;
edx = (rdi);
if (edx > esi){
    rdi = (rdi + 8);
    fun7;
    eax *= 2;
    return;
} else {
    eax = 0;
    if (edx == esi) return;
    rdi = (rdi + 10);
    fun7;
    eax = 2*rax + 1;
    return;
}

// 进一步抽象和简化
// x:rdi; y:rsi; ret:eax;
auto fun7(auto x, auto y){
    if (x == 0) return -1;
    if (*x == y) return 0;
    if (*x > y){
        x = *(x + 8); // 不同于 C 标准语法,此处 +8 指八个字节,下同理
        return 2 * fun7(x, y);
    } else {
        x = *(x + 16);
        return 2 * fun7(x, y) + 1;
    }
}

接下来看看 secret_phase 的主函数,这基本上是一个顺序执行的函数,没什么 jump。
开头和结尾,因为 main 里没有为它提供输入输出,这个函数里自己做了 read_lineputs

0000000000001aae <secret_phase>:
    1aae:    f3 0f 1e fa              endbr64
    1ab2:    53                       push   %rbx
    1ab3:    e8 87 02 00 00           call   1d3f <read_line>
    1ab8:    48 89 c7                 mov    %rax,%rdi
    1abb:    ba 0a 00 00 00           mov    $0xa,%edx
    1ac0:    be 00 00 00 00           mov    $0x0,%esi
    1ac5:    e8 16 f8 ff ff           call   12e0 <strtol@plt>
    1aca:    89 c3                    mov    %eax,%ebx
    1acc:    83 e8 01                 sub    $0x1,%eax
    1acf:    3d e8 03 00 00           cmp    $0x3e8,%eax
    1ad4:    77 26                    ja     1afc <secret_phase+0x4e>
    1ad6:    89 de                    mov    %ebx,%esi
    1ad8:    48 8d 3d 51 36 00 00     lea    0x3651(%rip),%rdi        # 5130 <n1>
    1adf:    e8 89 ff ff ff           call   1a6d <fun7>
    1ae4:    83 f8 03                 cmp    $0x3,%eax
    1ae7:    75 1a                    jne    1b03 <secret_phase+0x55>
    1ae9:    48 8d 3d f0 16 00 00     lea    0x16f0(%rip),%rdi        # 31e0 <array.0+0x40>
    1af0:    e8 2b f7 ff ff           call   1220 <puts@plt>
    1af5:    e8 7d 03 00 00           call   1e77 <phase_defused>
    1afa:    5b                       pop    %rbx
    1afb:    c3                       ret
    1afc:    e8 cd 01 00 00           call   1cce <explode_bomb>
    1b01:    eb d3                    jmp    1ad6 <secret_phase+0x28>
    1b03:    e8 c6 01 00 00           call   1cce <explode_bomb>
    1b08:    eb df                    jmp    1ae9 <secret_phase+0x3b>

调用了 strtol 这个函数,这是标准库里的函数,定义如下(参考):

long int strtol (const char* str, char** endptr, int base);

将这段汇编代码改编成 C,因为没什么 jump,逻辑非常简单:

read_line();
rdi = rax;
edx = 0xa;
esi = 0x0;
strtol();
ebx = eax;
eax--;
if (eax > 0x3e8) boom();
esi = ebx;
rdi = 5130;
fun7();
if (eax != 0x3) boom();
puts();
phase_defused();

// 简化后
int y = strtol(read_line(), 0xa, 0x0);
if (y-1 > 0x3e8) boom();
ret = fun7(5130, y);
if (ret != 0x3) boom();

这里 strtol 函数的三个参数就分别是 rdi、esi、edx。也就是说,将读入的这一行前 10 个字符转为 int,作为 fun7 的第二个参数。这个数字不能大于 1001(0x3e9)。

接下来调用 fun7,它的第一个参数是内存中存放的一个部分 0x3651(%rip),%rdi,我们用 gdb 看看这里存着啥:

(gdb) x/32gx 0x555555559130
0x555555559130 <n1>:    0x0000000000000024    0x0000555555559150
0x555555559140 <n1+16>:    0x0000555555559170    0x0000000000000000
0x555555559150 <n21>:    0x0000000000000008    0x00005555555591d0
0x555555559160 <n21+16>:    0x0000555555559190    0x0000000000000000
0x555555559170 <n22>:    0x0000000000000032    0x00005555555591b0
0x555555559180 <n22+16>:    0x00005555555591f0    0x0000000000000000
0x555555559190 <n32>:    0x0000000000000016    0x00005555555590b0
0x5555555591a0 <n32+16>:    0x0000555555559070    0x0000000000000000
0x5555555591b0 <n33>:    0x000000000000002d    0x0000555555559010
0x5555555591c0 <n33+16>:    0x00005555555590d0    0x0000000000000000
0x5555555591d0 <n31>:    0x0000000000000006    0x0000555555559030
0x5555555591e0 <n31+16>:    0x0000555555559090    0x0000000000000000
0x5555555591f0 <n34>:    0x000000000000006b    0x0000555555559050
0x555555559200 <n34+16>:    0x00005555555590f0    0x0000000000000000
0x555555559210 <node1>:    0x0000000100000336    0x0000555555559240
0x555555559220 <node2>:    0x000000020000013e    0x0000555555559110

看起来可能还是链表。fun7 返回值必须是 3 才算炸弹拆除,我们可以试试看按照 fun7 函数主体部分一层层模拟:

所以,secret_phase 的答案是 107。

答案

一组不包含 secret_phase 的一组答案如下:

Crikey! I have lost my mojo!
1 2 4 7 11 16
0 b 624
66 2
5 115
1 4 3 5 2 6

包含 sccret_phase 的一组答案如下:

Crikey! I have lost my mojo!
1 2 4 7 11 16
0 b 624
66 2 DrEvil
5 115
1 4 3 5 2 6
107