SkyWT / 博客 / CSAPP Bomblab 题解

CSAPP Bomblab 题解

2023 年 4 月 21 日 06:32
共 2 条评论


文章目录

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

做的时候就感觉到,不愧是 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][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:

  • d2 为 2、3 或 4;
  • func4(7, d2) = 33 * d2 = d1。

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

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 要满足:

  • 任意 ai 不大于 6;
  • 任意 ai、aj 两两不相等。

注意这里 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 函数主体部分一层层模拟:

  • fun7(n1, y) = 3 意味着 fun7(*(n1+16), y) = 1;得到的 n1' = 555555559170
  • fun7(n1', y) = 1 意味着 fun7(*(n1'+16), y) = 0;得到的 n1'' = 0x5555555591f0
  • fun7(n1'', y) = 0 意味着 *n1'' = y 或者 fun7(*(n1''+8), y) = 0
    • 使 y 为 0x6b 即可,不用继续搜索了。(当然如果继续搜索可能找到其他解)

所以,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

暂无评论


发表新的评论

所有评论都将经过博主审核。请勿填写无意义邮箱或发表无关评论、广告等,否则会被视为垃圾评论。

提交评论即表明你同意本网站使用 Cookie,并允许本站在后台记录你的邮箱、IP 地址等必要信息。这些信息不会被透露给其他用户。(提交一次评论后,本提示将不再展示)