Skip to content

Bomb Lab

Posted on:2021.09.05

TOC

Open TOC

Bomb Lab

CSAPP Lab 下载对应的 tar 包并解压。解压之后有三个文件,README 没啥用,bomb.c 中只给出了 main 函数,并且对我们进行了一番嘲讽 🤣:

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

所以最重要的就是 bomb 这个可执行文件。

我们的工具有两个,一个是 objdump,另一个是 gdb

首先使用 objdump 得到汇编代码:

$ touch assembly
$ objdump -d bomb > assembly

为了阅读方便,我们将结果保存在文件中。

汇编代码非常长,我们在其中找到 main 函数的部分:

0000000000400da0 <main>:
400da0: 53 push %rbx
400da1: 83 ff 01 cmp $0x1,%edi
400da4: 75 10 jne 400db6 <main+0x16>
400da6: 48 8b 05 9b 29 20 00 mov 0x20299b(%rip),%rax # 603748 <stdin@@GLIBC_2.2.5>
400dad: 48 89 05 b4 29 20 00 mov %rax,0x2029b4(%rip) # 603768 <infile>
400db4: eb 63 jmp 400e19 <main+0x79>
400db6: 48 89 f3 mov %rsi,%rbx
400db9: 83 ff 02 cmp $0x2,%edi
400dbc: 75 3a jne 400df8 <main+0x58>
400dbe: 48 8b 7e 08 mov 0x8(%rsi),%rdi
400dc2: be b4 22 40 00 mov $0x4022b4,%esi
400dc7: e8 44 fe ff ff call 400c10 <fopen@plt>
400dcc: 48 89 05 95 29 20 00 mov %rax,0x202995(%rip) # 603768 <infile>
400dd3: 48 85 c0 test %rax,%rax
400dd6: 75 41 jne 400e19 <main+0x79>
400dd8: 48 8b 4b 08 mov 0x8(%rbx),%rcx
400ddc: 48 8b 13 mov (%rbx),%rdx
400ddf: be b6 22 40 00 mov $0x4022b6,%esi
400de4: bf 01 00 00 00 mov $0x1,%edi
400de9: e8 12 fe ff ff call 400c00 <__printf_chk@plt>
400dee: bf 08 00 00 00 mov $0x8,%edi
400df3: e8 28 fe ff ff call 400c20 <exit@plt>
400df8: 48 8b 16 mov (%rsi),%rdx
400dfb: be d3 22 40 00 mov $0x4022d3,%esi
400e00: bf 01 00 00 00 mov $0x1,%edi
400e05: b8 00 00 00 00 mov $0x0,%eax
400e0a: e8 f1 fd ff ff call 400c00 <__printf_chk@plt>
400e0f: bf 08 00 00 00 mov $0x8,%edi
400e14: e8 07 fe ff ff call 400c20 <exit@plt>
400e19: e8 84 05 00 00 call 4013a2 <initialize_bomb>
400e1e: bf 38 23 40 00 mov $0x402338,%edi
400e23: e8 e8 fc ff ff call 400b10 <puts@plt>
400e28: bf 78 23 40 00 mov $0x402378,%edi
400e2d: e8 de fc ff ff call 400b10 <puts@plt>
400e32: e8 67 06 00 00 call 40149e <read_line>
400e37: 48 89 c7 mov %rax,%rdi
400e3a: e8 a1 00 00 00 call 400ee0 <phase_1>
400e3f: e8 80 07 00 00 call 4015c4 <phase_defused>
400e44: bf a8 23 40 00 mov $0x4023a8,%edi
400e49: e8 c2 fc ff ff call 400b10 <puts@plt>
400e4e: e8 4b 06 00 00 call 40149e <read_line>
400e53: 48 89 c7 mov %rax,%rdi
400e56: e8 a1 00 00 00 call 400efc <phase_2>
400e5b: e8 64 07 00 00 call 4015c4 <phase_defused>
400e60: bf ed 22 40 00 mov $0x4022ed,%edi
400e65: e8 a6 fc ff ff call 400b10 <puts@plt>
400e6a: e8 2f 06 00 00 call 40149e <read_line>
400e6f: 48 89 c7 mov %rax,%rdi
400e72: e8 cc 00 00 00 call 400f43 <phase_3>
400e77: e8 48 07 00 00 call 4015c4 <phase_defused>
400e7c: bf 0b 23 40 00 mov $0x40230b,%edi
400e81: e8 8a fc ff ff call 400b10 <puts@plt>
400e86: e8 13 06 00 00 call 40149e <read_line>
400e8b: 48 89 c7 mov %rax,%rdi
400e8e: e8 79 01 00 00 call 40100c <phase_4>
400e93: e8 2c 07 00 00 call 4015c4 <phase_defused>
400e98: bf d8 23 40 00 mov $0x4023d8,%edi
400e9d: e8 6e fc ff ff call 400b10 <puts@plt>
400ea2: e8 f7 05 00 00 call 40149e <read_line>
400ea7: 48 89 c7 mov %rax,%rdi
400eaa: e8 b3 01 00 00 call 401062 <phase_5>
400eaf: e8 10 07 00 00 call 4015c4 <phase_defused>
400eb4: bf 1a 23 40 00 mov $0x40231a,%edi
400eb9: e8 52 fc ff ff call 400b10 <puts@plt>
400ebe: e8 db 05 00 00 call 40149e <read_line>
400ec3: 48 89 c7 mov %rax,%rdi
400ec6: e8 29 02 00 00 call 4010f4 <phase_6>
400ecb: e8 f4 06 00 00 call 4015c4 <phase_defused>
400ed0: b8 00 00 00 00 mov $0x0,%eax
400ed5: 5b pop %rbx
400ed6: c3 ret
400ed7: 90 nop
400ed8: 90 nop
400ed9: 90 nop
400eda: 90 nop
400edb: 90 nop
400edc: 90 nop
400edd: 90 nop
400ede: 90 nop
400edf: 90 nop

下面分阶段讲解。

phase_1

阶段一从地址 0x400e32 开始,这里调用了 read_line 函数从输入中(可以是命令行里的输入,也可以是文件里的输入,详见 bomb.c 里的注释)读取一行。

现在的问题是读取的内容存放在了哪里?

我起初认为读取的内容在寄存器中,在地址 0x400e37 设置断点(break * 0x400e37)发现,无论输入是什么,在调用了 read_line 地址 0x400e37 函数后,相关的寄存器(info registersrax 的值总是 0x603780,于是猜测读取的内容以字符串的形式存放在地址 0x603780 处(这很关键,不然会误入歧途 🤣)。

vgalaxy@vgalaxy-VirtualBox:~/Lab/bomb$ gdb bomb
GNU gdb (Ubuntu 10.1-2ubuntu2) 10.1.90.20210411-git
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from bomb...
(gdb) break * 0x400e37
Breakpoint 1 at 0x400e37: file bomb.c, line 74.
(gdb) run
Starting program: /home/vgalaxy/Lab/bomb/bomb
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
test
Breakpoint 1, main (argc=<optimized out>, argv=<optimized out>) at bomb.c:74
74 phase_1(input); /* Run the phase */
(gdb) info registers
rax 0x603780 6305664
rbx 0x402210 4203024
rcx 0x4 4
rdx 0x1 1
rsi 0x603780 6305664
rdi 0x603786 6305670
rbp 0x0 0x0
rsp 0x7fffffffded0 0x7fffffffded0
r8 0x603780 6305664
r9 0x6046b0 6309552
r10 0xfffffffffffffb7f -1153
r11 0x7ffff7dfe8a0 140737352034464
r12 0x400c90 4197520
r13 0x0 0
r14 0x0 0
r15 0x0 0
rip 0x400e37 0x400e37 <main+151>
eflags 0x202 [ IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0

地址 400e37 这一行将寄存器 rax 的值赋给 rdi,并调用 phase_1 函数:

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

地址 0x400ee4 这一行将 0x402400 赋给寄存器 esi,寄存器 rsi 的高四字节置 0。接着便调用了 strings_not_equal 函数:

0000000000401338 <strings_not_equal>:
401338: 41 54 push %r12
40133a: 55 push %rbp
40133b: 53 push %rbx
40133c: 48 89 fb mov %rdi,%rbx
40133f: 48 89 f5 mov %rsi,%rbp
401342: e8 d4 ff ff ff call 40131b <string_length>
401347: 41 89 c4 mov %eax,%r12d
40134a: 48 89 ef mov %rbp,%rdi
40134d: e8 c9 ff ff ff call 40131b <string_length>
401352: ba 01 00 00 00 mov $0x1,%edx
401357: 41 39 c4 cmp %eax,%r12d
40135a: 75 3f jne 40139b <strings_not_equal+0x63>
40135c: 0f b6 03 movzbl (%rbx),%eax
40135f: 84 c0 test %al,%al
401361: 74 25 je 401388 <strings_not_equal+0x50>
401363: 3a 45 00 cmp 0x0(%rbp),%al
401366: 74 0a je 401372 <strings_not_equal+0x3a>
401368: eb 25 jmp 40138f <strings_not_equal+0x57>
40136a: 3a 45 00 cmp 0x0(%rbp),%al
40136d: 0f 1f 00 nopl (%rax)
401370: 75 24 jne 401396 <strings_not_equal+0x5e>
401372: 48 83 c3 01 add $0x1,%rbx
401376: 48 83 c5 01 add $0x1,%rbp
40137a: 0f b6 03 movzbl (%rbx),%eax
40137d: 84 c0 test %al,%al
40137f: 75 e9 jne 40136a <strings_not_equal+0x32>
401381: ba 00 00 00 00 mov $0x0,%edx
401386: eb 13 jmp 40139b <strings_not_equal+0x63>
401388: ba 00 00 00 00 mov $0x0,%edx
40138d: eb 0c jmp 40139b <strings_not_equal+0x63>
40138f: ba 01 00 00 00 mov $0x1,%edx
401394: eb 05 jmp 40139b <strings_not_equal+0x63>
401396: ba 01 00 00 00 mov $0x1,%edx
40139b: 89 d0 mov %edx,%eax
40139d: 5b pop %rbx
40139e: 5d pop %rbp
40139f: 41 5c pop %r12
4013a1: c3 ret

观察从地址 0x40133c 到地址 0x40135a 的部分,通过调用 string_length 函数依次计算寄存器 rdirsi 所指向地址的字符串的长度(观察 string_length: 0x40131b(%rdi) 可知)。若长度不同,则返回 1,注意 phase_1: 0x400eee 检测返回值是否为 0,若不为 0,则引爆炸弹。可以推断,若两个字符串相同,那么返回值就是 0,炸弹就不会被引爆。

(╯‵□′)╯炸弹!•••*~●

于是,我们只要得到寄存器 rsi 中的 0x402400 所指向的字符串即可,我们使用下面的指令:

(gdb) x/s 0x402400
0x402400: "Border relations with Canada have never been better."

将结果保存在 input 文件中即可:

Terminal window
$ touch input
$ echo "Border relations with Canada have never been better." > input

来测试一下:

Terminal window
$ ./bomb input
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?

( ^_^ ) 不错嘛。

注:在走弯路的过程中 🤣,探索了 read_line 函数究竟是如何将字符串存放在地址 0x603780 处的。在命令行输入的场景下,read_line 函数调用了 skip 函数,skip 函数调用了**fgets 函数**和 blank_line 函数。

给几个观察寄存器的断点:

  • skip: 0x401421,此时寄存器 rsi 会有从字符串第二个字符开始的 ASCII 码,若字符串为 123456,则编码为 0x35343332,这里只有四个字符,推测是按四个字符读取

  • blank_line: 0x4013e1,此时寄存器 rbp 会有从字符串第一个字符的 ASCII 码,在上面的例子中,就是 0x31,这里推测是预读第一个字符,若遇到空行则继续读取

  • read_line: 0x40154b,此时寄存器 rcx 给出了读取的字符串的长度加 2 的结果,在 read_line 函数的后面,将寄存器 rcx 的值减去了 2,并以此为依据,在 read_line: 0x4015ab 处给字符串的最后加上了 0,此时寄存器 rax 的值为 0,尚不清楚其作用 🤣

    4015ab: c6 84 01 80 37 60 00 movb $0x0,0x603780(%rcx,%rax,1)

phase_2

阶段二从 main: 0x400e4e 开始,同样调用 read_line 函数从输入中读取一行,读取的内容以字符串的形式存放在地址 0x6037d0 处。

接下来 main 函数调用 phase_2 函数:

0000000000400efc <phase_2>:
400efc: 55 push %rbp
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp
400f02: 48 89 e6 mov %rsp,%rsi
400f05: e8 52 05 00 00 call 40145c <read_six_numbers>

注意 phase_2: 400f02 处,这里将寄存器 rsp 的值赋给寄存器 rsi,通过断点调试可知此时寄存器 rsi 的值为 0x7fffffffde80。接着调用 read_six_numbers 函数:

000000000040145c <read_six_numbers>:
40145c: 48 83 ec 18 sub $0x18,%rsp
401460: 48 89 f2 mov %rsi,%rdx
401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx
401467: 48 8d 46 14 lea 0x14(%rsi),%rax
40146b: 48 89 44 24 08 mov %rax,0x8(%rsp)
401470: 48 8d 46 10 lea 0x10(%rsi),%rax
401474: 48 89 04 24 mov %rax,(%rsp)
401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9
40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8
401480: be c3 25 40 00 mov $0x4025c3,%esi
401485: b8 00 00 00 00 mov $0x0,%eax
40148a: e8 61 f7 ff ff call 400bf0 <__isoc99_sscanf@plt>
40148f: 83 f8 05 cmp $0x5,%eax
401492: 7f 05 jg 401499 <read_six_numbers+0x3d>
401494: e8 a1 ff ff ff call 40143a <explode_bomb>
401499: 48 83 c4 18 add $0x18,%rsp
40149d: c3 ret

先看前四行,刚进入 read_six_numbers 函数,寄存器 rsp 的值就从 0x7fffffffde80 变为了 0x7fffffffde78(函数调用所致)。到 read_six_numbers: 0x40146b 时,相关寄存器的值如下:

read_six_numbers: 0x40146b0x7fffffffde94 赋给地址 0x7fffffffde68 处,接着寄存器 rax 的值变为 0x7fffffffde90,并将 0x7fffffffde90 赋给地址 0x7fffffffde60 处。使用 gdb 可以观察:

(gdb) stepi
0x0000000000401478 in read_six_numbers ()
(gdb) x/2g 0x7fffffffde60
0x7fffffffde60: 0x00007fffffffde90 0x00007fffffffde94

其实看到这里是一头雾水 🤣

到了 read_six_numbers: 0x40148aread_six_numbers 函数调用了 __isoc99_sscanf 函数,然后对其返回值进行测试,若 ≤ 5,则引爆炸弹。通过查阅sscanf可知,其返回值是成功赋值的接收参数的数量。根据函数名,我们可以推测输入的格式6 个以空格分割的数字。尝试输入 1 2 3 4 5 6,并在 gdb 中一通乱搞🤣,发现从地址 0x7fffffffde80 开始的 6 个字节存储了我们输入的数字。

(gdb) stepi
0x0000000000401499 in read_six_numbers ()
(gdb) x/3g 0x7fffffffde80
0x7fffffffde80: 0x0000000200000001 0x0000000400000003
0x7fffffffde90: 0x0000000600000005

不太理解为什么是以这种形式存储 🤣

下面就容易了,从 read_six_numbers 函数返回到 phase_2 函数后:

400f0a: 83 3c 24 01 cmpl $0x1,(%rsp)
400f0e: 74 20 je 400f30 <phase_2+0x34>
400f10: e8 25 05 00 00 call 40143a <explode_bomb>
400f15: eb 19 jmp 400f30 <phase_2+0x34>
400f17: 8b 43 fc mov -0x4(%rbx),%eax
400f1a: 01 c0 add %eax,%eax
400f1c: 39 03 cmp %eax,(%rbx)
400f1e: 74 05 je 400f25 <phase_2+0x29>
400f20: e8 15 05 00 00 call 40143a <explode_bomb>
400f25: 48 83 c3 04 add $0x4,%rbx
400f29: 48 39 eb cmp %rbp,%rbx
400f2c: 75 e9 jne 400f17 <phase_2+0x1b>
400f2e: eb 0c jmp 400f3c <phase_2+0x40>
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp
400f3a: eb db jmp 400f17 <phase_2+0x1b>
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 ret

phase_2: 0x400f0a 测试寄存器 rsp 中对应的地址中存储的值是否为 1,我们知道此时寄存器 rsp 中存储的值为 0x7fffffffde80,也就是说这里测试输入的第一个数字是否为 1。若为 1,我们跳转到 phase_2: 0x400f30,将寄存器 rbx 设置为 0x7fffffffde84,寄存器 rbp 设置为 0x7fffffffde98(终止条件)。接着我们跳转到 phase_2: 0x400f17,将地址 0x7fffffffde80 中的值赋给寄存器 eax,并乘 2,与地址 0x7fffffffde84 中的值比较,若相等,则跳过炸弹引爆,并将 rbx4,判断是否到了 0x7fffffffde98。若没有,则继续跳转到 phase_2: 0x400f17

也就是说,这里是一个循环。为了不引爆炸弹,我们输入的 6 个数字,后一个数字应该是前一个数字的 2

于是,我们将结果保存在文件中:

Terminal window
$ echo "1 2 4 8 16 32" >> input

来测试一下:

Terminal window
$ ./bomb input
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!

没有问题。

phase_3

阶段三从 main: 0x400e6a 开始,同样调用 read_line 函数从输入中读取一行,读取的内容以字符串的形式存放在地址 0x603820 处。

接下来 main 函数调用 phase_3 函数:

0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
400f51: be cf 25 40 00 mov $0x4025cf,%esi
400f56: b8 00 00 00 00 mov $0x0,%eax
400f5b: e8 90 fc ff ff call 400bf0 <__isoc99_sscanf@plt>

断点调试可知,在 phase_3: 0x400f47 处,寄存器 rsp 的值为 0x7fffffffde90,在调用 __isoc99_sscanf 函数前,寄存器 rcx 的值为 0x7fffffffde9c,寄存器 rdx 的值为 0x7fffffffde98。联系 phase_2 推测,这便是我们输入的 2 个数字(phase_3: 0x400f60 处检测参数数量是否 > 1)存放的地址。

400f60: 83 f8 01 cmp $0x1,%eax
400f63: 7f 05 jg 400f6a <phase_3+0x27>
400f65: e8 d0 04 00 00 call 40143a <explode_bomb>
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp)
400f6f: 77 3c ja 400fad <phase_3+0x6a>
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax
400f75: ff 24 c5 70 24 40 00 jmp *0x402470(,%rax,8)
400f7c: b8 cf 00 00 00 mov $0xcf,%eax
400f81: eb 3b jmp 400fbe <phase_3+0x7b>
400f83: b8 c3 02 00 00 mov $0x2c3,%eax
400f88: eb 34 jmp 400fbe <phase_3+0x7b>
400f8a: b8 00 01 00 00 mov $0x100,%eax
400f8f: eb 2d jmp 400fbe <phase_3+0x7b>
400f91: b8 85 01 00 00 mov $0x185,%eax
400f96: eb 26 jmp 400fbe <phase_3+0x7b>
400f98: b8 ce 00 00 00 mov $0xce,%eax
400f9d: eb 1f jmp 400fbe <phase_3+0x7b>
400f9f: b8 aa 02 00 00 mov $0x2aa,%eax
400fa4: eb 18 jmp 400fbe <phase_3+0x7b>
400fa6: b8 47 01 00 00 mov $0x147,%eax
400fab: eb 11 jmp 400fbe <phase_3+0x7b>
400fad: e8 88 04 00 00 call 40143a <explode_bomb>
400fb2: b8 00 00 00 00 mov $0x0,%eax
400fb7: eb 05 jmp 400fbe <phase_3+0x7b>
400fb9: b8 37 01 00 00 mov $0x137,%eax
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax
400fc2: 74 05 je 400fc9 <phase_3+0x86>
400fc4: e8 71 04 00 00 call 40143a <explode_bomb>
400fc9: 48 83 c4 18 add $0x18,%rsp
400fcd: c3 ret

phase_3: 0x400f6a 处检测第一个数字是否 ≤ 7,并将该数字赋给寄存器 eax

下面关键的地方是 phase_3: 0x400f75

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

这里根据寄存器 rax 中的值进行无条件跳转,我们使用 gdb 观察地址 0x402470 周围的值:

(gdb) nexti
0x0000000000400f75 in phase_3 ()
(gdb) x/8g 0x402470
0x402470: 0x0000000000400f7c 0x0000000000400fb9
0x402480: 0x0000000000400f83 0x0000000000400f8a
0x402490: 0x0000000000400f91 0x0000000000400f98
0x4024a0: 0x0000000000400f9f 0x0000000000400fa6

注意到这里的每一条地址都恰好对应了一条 mov 语句和一条 jmp 语句,其跳转的目标便是 phase_3: 0x400fbe

400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax

也就是说,我们要让第二个数字和 mov 语句赋给寄存器 eax 的值相同,于是我们有如下 8 种可能的输入(注意将十六进制转换为十进制):

我们任选(其实是我第一次通过的那个 🤣)一个保存在文件中:

Terminal window
$ echo "1 311" >> input

测试一下:

Terminal window
$ ./bomb input
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
Halfway there!

一半了 ~(≧▽≦)/~ 啦啦啦。

phase_4

阶段三从 main: 0x400e86 开始,同样调用 read_line 函数从输入中读取一行,读取的内容以字符串的形式存放在地址 0x603870 处。

接下来 main 函数调用 phase_4 函数:

000000000040100c <phase_4>:
40100c: 48 83 ec 18 sub $0x18,%rsp
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
40101a: be cf 25 40 00 mov $0x4025cf,%esi
40101f: b8 00 00 00 00 mov $0x0,%eax
401024: e8 c7 fb ff ff call 400bf0 <__isoc99_sscanf@plt>
401029: 83 f8 02 cmp $0x2,%eax
40102c: 75 07 jne 401035 <phase_4+0x29>
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp)
401033: 76 05 jbe 40103a <phase_4+0x2e>
401035: e8 00 04 00 00 call 40143a <explode_bomb>
40103a: ba 0e 00 00 00 mov $0xe,%edx
40103f: be 00 00 00 00 mov $0x0,%esi
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi
401048: e8 81 ff ff ff call 400fce <func4>
40104d: 85 c0 test %eax,%eax
40104f: 75 07 jne 401058 <phase_4+0x4c>
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
401056: 74 05 je 40105d <phase_4+0x51>
401058: e8 dd 03 00 00 call 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3 ret

前面的部分我们已经很熟悉了,这里要求读取 2 个数字,存放在地址 0x7fffffffde98 处。

寄存器 rsp 的值为 0x7fffffffde90

phase_4: 0x40103a 开始,将寄存器 edx 赋值为 e,寄存器 esi 赋值为 0,寄存器 edi 赋值为输入的第一个数字。然后调用 func4 函数,并检测其返回值是否为 0,若不为 0,则引爆炸弹。同时检测第二个数字是否为 0。于是下面的重点便是 func4 函数。

0000000000400fce <func4>:
400fce: 48 83 ec 08 sub $0x8,%rsp
400fd2: 89 d0 mov %edx,%eax
400fd4: 29 f0 sub %esi,%eax
400fd6: 89 c1 mov %eax,%ecx
400fd8: c1 e9 1f shr $0x1f,%ecx
400fdb: 01 c8 add %ecx,%eax
400fdd: d1 f8 sar %eax
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx
400fe2: 39 f9 cmp %edi,%ecx
400fe4: 7e 0c jle 400ff2 <func4+0x24>
400fe6: 8d 51 ff lea -0x1(%rcx),%edx
400fe9: e8 e0 ff ff ff call 400fce <func4>
400fee: 01 c0 add %eax,%eax
400ff0: eb 15 jmp 401007 <func4+0x39>
400ff2: b8 00 00 00 00 mov $0x0,%eax
400ff7: 39 f9 cmp %edi,%ecx
400ff9: 7d 0c jge 401007 <func4+0x39>
400ffb: 8d 71 01 lea 0x1(%rcx),%esi
400ffe: e8 cb ff ff ff call 400fce <func4>
401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401007: 48 83 c4 08 add $0x8,%rsp
40100b: c3 ret

首先注意到这是一个递归函数,在 func4 函数内部有两次调用:

我们的目标是让 func4 函数返回 0,为此我们不能走到 func4: 0x400ffe,因为该调用语句的后面的 func4: 0x401003 让寄存器 eax 的值发生了变化(注意到 func4: 0x400ff2 将寄存器 eax 的值赋值为 0,这里只是直觉,并不是严谨的解释 🤣)。

我们还发现这里跳转的条件中使用了寄存器 edi 的值,而寄存器 edi 的值是我们输入的第一个数字。若我们输入的第一个数字是 1(还是直觉 🤣),从 func4: 0x400ffe 执行到 func4: 0x400fe4,相关寄存器的值如下:

此时,满足寄存器 ecx 的值 寄存器 edi 的值,我们将寄存器 edx 赋值为 rcx-1,即 6,递归调用 func4 函数:

func4: 0x400ffe 执行到 func4: 0x400fe4,相关寄存器的值如下:

此时,满足寄存器 ecx 的值 寄存器 edi 的值,我们将寄存器 edx 赋值为 rcx-1,即 2,递归调用 func4 函数:

func4: 0x400ffe 执行到 func4: 0x400fe4,相关寄存器的值如下:

此时,满足寄存器 ecx 的值 寄存器 edi 的值,我们跳转到 func4: 0x400ff2,将寄存器 eax 赋值为 0。我们发现,此时也满足寄存器 ecx 的值 寄存器 edi 的值。于是我们成功的跳过了第二个递归调用,递归函数开始返回。

返回后的语句在 func4: 0x400fee,由于这条语句只是让寄存器 eax 的值乘 2,然而寄存器 eax 的值为 0,所以最终的返回值还是 0

看样子我们碰运气猜出了答案 🤣,实际上可以考虑输入的第一个数字是 2,我们会在两次递归调用 func4: 0x400fe9(△) 后递归调用 func4: 0x400ffe(☆),并继续调用 func4: 0x400fe9(□)。在 (□) 返回后,寄存器 eax 的值为 0,在 (☆) 返回后寄存器 eax 的值为 1,两次 (△) 返回让寄存器 eax 的值变为原来的 4 倍,总之肯定不是 0 了。

对于更严谨的说明,可以写出等价的 C 语言程序(但是我不会 🤣)

于是我们将答案保存在文件中:

Terminal window
$ echo "1 0" >> input

测试一下:

Terminal window
$ ./bomb input
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
Halfway there!
So you got that one. Try this one.

注:

  • 后来发现第一个数字还可以是 3 或者 7,规律也许是小于 e 并且有效位均为 1
  • func4 函数感觉有点像二分?

phase_5

阶段五从 main: 0x400ea2 开始,同样调用 read_line 函数从输入中读取一行,读取的内容以字符串的形式存放在地址 0x6038c0 处。

接下来 main 函数调用 phase_5 函数:

0000000000401062 <phase_5>:
401062: 53 push %rbx
401063: 48 83 ec 20 sub $0x20,%rsp
401067: 48 89 fb mov %rdi,%rbx
40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
401071: 00 00
401073: 48 89 44 24 18 mov %rax,0x18(%rsp)
401078: 31 c0 xor %eax,%eax
40107a: e8 9c 02 00 00 call 40131b <string_length>

phase_5: 0x40106a 之后,寄存器 rax 为一个随机的值(联系下面的 phase_5: 0x4010e9,猜测是为了进行栈破坏检测,这里的随机值便是哨兵值,亦或金丝雀值,详见 CSAPP §3.10.4),并将其存入地址 0x7fffffffdea8 处(寄存器 rsp 的值为 0x7fffffffde90)。接着 phase_5 调用 string_length 函数检测寄存器 rdi 中对应地址的字符串的长度(也就是我们输入的字符串长度)是否为 6

40107f: 83 f8 06 cmp $0x6,%eax
401082: 74 4e je 4010d2 <phase_5+0x70>
401084: e8 b1 03 00 00 call 40143a <explode_bomb>
401089: eb 47 jmp 4010d2 <phase_5+0x70>
40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx
40108f: 88 0c 24 mov %cl,(%rsp)
401092: 48 8b 14 24 mov (%rsp),%rdx
401096: 83 e2 0f and $0xf,%edx
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1)
4010a4: 48 83 c0 01 add $0x1,%rax
4010a8: 48 83 f8 06 cmp $0x6,%rax
4010ac: 75 dd jne 40108b <phase_5+0x29>
4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp)
4010b3: be 5e 24 40 00 mov $0x40245e,%esi
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
4010bd: e8 76 02 00 00 call 401338 <strings_not_equal>
4010c2: 85 c0 test %eax,%eax
4010c4: 74 13 je 4010d9 <phase_5+0x77>
4010c6: e8 6f 03 00 00 call 40143a <explode_bomb>
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
4010d0: eb 07 jmp 4010d9 <phase_5+0x77>
4010d2: b8 00 00 00 00 mov $0x0,%eax
4010d7: eb b2 jmp 40108b <phase_5+0x29>
4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax
4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
4010e5: 00 00
4010e7: 74 05 je 4010ee <phase_5+0x8c>
4010e9: e8 42 fa ff ff call 400b30 <__stack_chk_fail@plt>
4010ee: 48 83 c4 20 add $0x20,%rsp
4010f2: 5b pop %rbx
4010f3: c3 ret

若为 6,则跳转到 phase_5: 0x4010d2,置寄存器 rax0,再跳转到 phase_5: 0x40108b

phase_5: 0x40108bphase_5: 0x4010ac 是一个循环,**主要工作是依次取我们输入的字符的低 4 位,作为一个字节数组的索引,取出索引的字节,存入从地址 0x7fffffffdea0 开始的一块内存空间。**这个字节数组从 0x4024b0 开始,我们来看看:

(gdb) x/2g 0x4024b0
0x4024b0 <array.3449>: 0x737265697564616d 0x6c796276746f666e

换一种显示方式 🤣:

(gdb) x/16b 0x4024b0
0x4024b0 <array.3449>: 0x6d 0x61 0x64 0x75 0x69 0x65 0x72 0x73
0x4024b8 <array.3449+8>: 0x6e 0x66 0x6f 0x74 0x76 0x62 0x79 0x6c

举一个例子,若我们输入的第一个字符为 1,对应的 ASCII 码为 0x31phase_5: 0x40108b 取得我们第一个字符(注意 phase_5: 0x4401067 将地址赋值给了寄存器 rbx),并符号扩展为一个双字,存入寄存器 ecx 中。在 phase_5: 0x401099 之前,寄存器 edx 的值从 0x31 变为 1(取输入字符的低 4 位,防止索引越界),同时地址 0x7fffffffde90 处的值为 0x31(不知道有什么用 🤣)。

执行完 phase_5: 0x401099 后,有 array[%rdx]→ %edx,对照字节数组可知,此时寄存器 edx 的值为 0x61,我们将其存入地址 0x7fffffffdea0 处,此时附近内存空间的情况如下:

(gdb) x/3g 0x7fffffffde90
0x7fffffffde90: 0x0000000000000031 0x0000000000401431
0x7fffffffdea0: 0x00007fffffffdf61

这里地址 0x7fffffffdea0 原来的内容是 0x00007fffffffdfb8

将我们输入的 6 个字符作为索引,得到 6 个字节以后。我们在这 6 个字节之后加上一个 0(可见 phase_5: 0x4010ae),形成一个字符串,并与地址 0x40245e 的字符串进行比较,我们来看看:

(gdb) x/s 0x40245e
0x40245e: "flyers"
(gdb) x/6bx 0x40245e
0x40245e: 0x66 0x6c 0x79 0x65 0x72 0x73

注:gdbx 的一些参数

  • 数字:显示几个内存单元
  • 输出格式
    • /s 字符串
    • /x 十六进制
    • /d 十进制
  • 以多少字节为一个内存单元
    • /b 一个字节
    • /w 四个字节
    • /g 八个字节

若省略(或直接回车 🤣),则默认为上一次的配置(或命令)

与字节数组对应,可知索引依次为 9 15 14 5 6 7,我们可以使用 i o n e f g 得到这组索引(不唯一)。

可对照 man ascii 编码

我们将答案保存在文件中,注意这里没有空格,上面加了空格是为了能看清楚:

Terminal window
$ echo "ionefg" >> input

测试一下:

Terminal window
$ ./bomb input
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
Halfway there!
So you got that one. Try this one.
Good work! On to the next...

ヽ(✿゚▽゚)ノ

phase_6

阶段六从 main: 0x400ebe 开始,同样调用 read_line 函数从输入中读取一行,读取的内容以字符串的形式存放在地址 0x603910 处。接下来 main 函数调用 phase_6 函数:

00000000004010f4 <phase_6>:
4010f4: 41 56 push %r14
4010f6: 41 55 push %r13
4010f8: 41 54 push %r12
4010fa: 55 push %rbp
4010fb: 53 push %rbx
4010fc: 48 83 ec 50 sub $0x50,%rsp
401100: 49 89 e5 mov %rsp,%r13
401103: 48 89 e6 mov %rsp,%rsi
401106: e8 51 03 00 00 call 40145c <read_six_numbers>

这里类似阶段二,调用了 read_six_numbers 函数,可以分析得到这 6 个数字存储在以 0x7fffffffde30 开始的地址空间中(此时寄存器 rsp 的值便是 0x7fffffffde30)。

phase_6 函数的主体由五个循环构成。下面我们来依次分析:

40110b: 49 89 e6 mov %rsp,%r14
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d
401114: 4c 89 ed mov %r13,%rbp
401117: 41 8b 45 00 mov 0x0(%r13),%eax
40111b: 83 e8 01 sub $0x1,%eax
40111e: 83 f8 05 cmp $0x5,%eax
401121: 76 05 jbe 401128 <phase_6+0x34>
401123: e8 12 03 00 00 call 40143a <explode_bomb>
401128: 41 83 c4 01 add $0x1,%r12d
40112c: 41 83 fc 06 cmp $0x6,%r12d
401130: 74 21 je 401153 <phase_6+0x5f>
401132: 44 89 e3 mov %r12d,%ebx
401135: 48 63 c3 movslq %ebx,%rax
401138: 8b 04 84 mov (%rsp,%rax,4),%eax
40113b: 39 45 00 cmp %eax,0x0(%rbp)
40113e: 75 05 jne 401145 <phase_6+0x51>
401140: e8 f5 02 00 00 call 40143a <explode_bomb>
401145: 83 c3 01 add $0x1,%ebx
401148: 83 fb 05 cmp $0x5,%ebx
40114b: 7e e8 jle 401135 <phase_6+0x41>
40114d: 49 83 c5 04 add $0x4,%r13
401151: eb c1 jmp 401114 <phase_6+0x20>

寄存器 r13 中存放着数字的地址,初始为 0x7fffffffde30,即第 1 个数字的地址。寄存器 r12d 是计数器,初始为 1,代表检测第 1 个数字,若等于 6,说明所有的数字都通过了检测(第 6 个数字 ≤ 6,不必再测试是否与其他数字相同,因为之前已经测试过了,详见下面的分析),那么就跳出循环来到 phase_6: 0x401153

在循环的内部还有一个循环,是检测当前的数字和之后的数字是否都不同,即从 phase_6: 0x401132phase_6: 0x40114b 的部分,不再赘述。

401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi
401158: 4c 89 f0 mov %r14,%rax
40115b: b9 07 00 00 00 mov $0x7,%ecx
401160: 89 ca mov %ecx,%edx
401162: 2b 10 sub (%rax),%edx
401164: 89 10 mov %edx,(%rax)
401166: 48 83 c0 04 add $0x4,%rax
40116a: 48 39 f0 cmp %rsi,%rax
40116d: 75 f1 jne 401160 <phase_6+0x6c>

phase_6: 0x401153 将寄存器 rsi 的值设置为 0x7fffffffde48,用于循环是否终止的测试,寄存器 r14 存放着数字的起始地址,即 0x7fffffffde30,并赋值给了寄存器 rax。其余部分不再赘述。

40116f: be 00 00 00 00 mov $0x0,%esi
401174: eb 21 jmp 401197 <phase_6+0xa3>
401176: 48 8b 52 08 mov 0x8(%rdx),%rdx
40117a: 83 c0 01 add $0x1,%eax
40117d: 39 c8 cmp %ecx,%eax
40117f: 75 f5 jne 401176 <phase_6+0x82>
401181: eb 05 jmp 401188 <phase_6+0x94>
401183: ba d0 32 60 00 mov $0x6032d0,%edx
401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2)
40118d: 48 83 c6 04 add $0x4,%rsi
401191: 48 83 fe 18 cmp $0x18,%rsi
401195: 74 14 je 4011ab <phase_6+0xb7>
401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx
40119a: 83 f9 01 cmp $0x1,%ecx
40119d: 7e e4 jle 401183 <phase_6+0x8f>
40119f: b8 01 00 00 00 mov $0x1,%eax
4011a4: ba d0 32 60 00 mov $0x6032d0,%edx
4011a9: eb cb jmp 401176 <phase_6+0x82>

这个部分就有意思了,我们发现链表存储在以 0x6032d0 开始的一块内存空间中,来看一看:

(gdb) x/24w 0x6032d0
0x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x006032f0 0x00000000
0x6032f0 <node3>: 0x0000039c 0x00000003 0x00603300 0x00000000
0x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x00000000
0x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x00000000
0x603320 <node6>: 0x000001bb 0x00000006 0x00000000 0x00000000

注意上面的内存空间内容的第三列,除了尾节点,其余节点的这个部分都存储了下一个节点的地址,所以我们判断这是一个链表。

我们回到循环本身,在 phase_6: 0x40116f,寄存器 esi 被置为 0,其作用相当于索引。刚开始,我们取出第一个数字(实际上是 7-第一个数字 )到寄存器 ecx 中,记为 x。若 x!=1,置寄存器 eax1,寄存器 edx0x6032d0,并通过一个内循环得到第 x 个节点的地址(在寄存器 rdx 中),并存放在 0x7fffffffde50 中;否则,直接置寄存器 rdx 为第一个节点的地址,并存放在 0x7fffffffde50 中。

对所有的数字进行相同操作后,我们可以从地址 0x7fffffffde50 观察到 6 个节点的地址:

(gdb) x/10g 0x7fffffffde30
0x7fffffffde30: 0x0000000400000003 0x0000000600000005
0x7fffffffde40: 0x0000000200000001 0x0000000000000000
0x7fffffffde50: 0x00000000006032f0 0x0000000000603300
0x7fffffffde60: 0x0000000000603310 0x0000000000603320
0x7fffffffde70: 0x00000000006032d0 0x00000000006032e0

这里假设输入的 6 个数字为 4 3 2 1 6 5,用 7 减去后便是 3 4 5 6 1 2,可以观察到地址的对应

4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx
4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax
4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi
4011ba: 48 89 d9 mov %rbx,%rcx
4011bd: 48 8b 10 mov (%rax),%rdx
4011c0: 48 89 51 08 mov %rdx,0x8(%rcx)
4011c4: 48 83 c0 08 add $0x8,%rax
4011c8: 48 39 f0 cmp %rsi,%rax
4011cb: 74 05 je 4011d2 <phase_6+0xde>
4011cd: 48 89 d1 mov %rdx,%rcx
4011d0: eb eb jmp 4011bd <phase_6+0xc9>
4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx)
4011d9: 00

存储的第一个节点的地址赋给寄存器 rbx,寄存器 rbx 再赋给寄存器 rcx,置寄存器 rax存储第二个节点的地址(这里少了一个**“的”**,你细品 🤣),置寄存器 rsi0x7fffffffde80,用于循环是否终止的测试。

phase_6: 0x4011c0,我们将存储的第二个节点的地址赋给 0x8(%rcx),也就是存储的第一个节点的下一个节点。以上面为例,也就是节点 0x6032f0 的下一个节点改为 0x603300(虽然原来就是如此 🤣)。接着将寄存器 rax 加上 8(即存储的第三个节点的地址),判断循环是否终止,此时未终止,我们将存储的第二个节点的地址赋给寄存器 rcx,然后跳转到 phase_6: 0x4011bd,将存储的第二个节点的下一个节点改为存储的第三个节点的地址……以此类推,最后在 phase_6: 0x4011d2 将链表的尾节点的下一个节点设置为 0

综上可知,修改后链表的结构是 node3 → node4 → node5 → node6 → node1 → node2

(gdb) x/24w 0x6032d0
0x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x00000000 0x00000000
0x6032f0 <node3>: 0x0000039c 0x00000003 0x00603300 0x00000000
0x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x00000000
0x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x00000000
0x603320 <node6>: 0x000001bb 0x00000006 0x006032d0 0x00000000
4011da: bd 05 00 00 00 mov $0x5,%ebp
4011df: 48 8b 43 08 mov 0x8(%rbx),%rax
4011e3: 8b 00 mov (%rax),%eax
4011e5: 39 03 cmp %eax,(%rbx)
4011e7: 7d 05 jge 4011ee <phase_6+0xfa>
4011e9: e8 4c 02 00 00 call 40143a <explode_bomb>
4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx
4011f2: 83 ed 01 sub $0x1,%ebp
4011f5: 75 e8 jne 4011df <phase_6+0xeb>
4011f7: 48 83 c4 50 add $0x50,%rsp
4011fb: 5b pop %rbx
4011fc: 5d pop %rbp
4011fd: 41 5c pop %r12
4011ff: 41 5d pop %r13
401201: 41 5e pop %r14
401203: c3 ret

置寄存器 ebp5,用于循环是否终止的测试。注意寄存器 rbx 中是存储的第一个节点的地址,也就是链表的头节点,我们将目前链表的第二个节点存储的数字(寄存器 eax 中)与链表的头节点存储的数字进行比较,若 头节点存储的数字≥第二个节点存储的数字,则继续循环。由此我们推测,我们修改后的链表,元素应该是降序排列的。再次观察原链表:

(gdb) x/24w 0x6032d0
0x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x006032f0 0x00000000
0x6032f0 <node3>: 0x0000039c 0x00000003 0x00603300 0x00000000
0x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x00000000
0x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x00000000
0x603320 <node6>: 0x000001bb 0x00000006 0x00000000 0x00000000

我们应该让链表的结构变为 node3 → node4 → node5 → node6 → node1 → node2,也就是我们上面的例子 🤣。

我们将答案保存在文件中:

Terminal window
$ echo "4 3 2 1 6 5" >> input

测试一下:

Terminal window
$ ./bomb input
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
Halfway there!
So you got that one. Try this one.
Good work! On to the next...
Congratulations! You've defused the bomb!

O(∩_∩)O 哈!

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_defused: 0x401630 处好像可以调用 secret_phase 函数:

00000000004015c4 <phase_defused>:
4015c4: 48 83 ec 78 sub $0x78,%rsp
4015c8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
4015cf: 00 00
4015d1: 48 89 44 24 68 mov %rax,0x68(%rsp)
4015d6: 31 c0 xor %eax,%eax
4015d8: 83 3d 81 21 20 00 06 cmpl $0x6,0x202181(%rip) # 603760 <num_input_strings>
4015df: 75 5e jne 40163f <phase_defused+0x7b>
4015e1: 4c 8d 44 24 10 lea 0x10(%rsp),%r8
4015e6: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
4015eb: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
4015f0: be 19 26 40 00 mov $0x402619,%esi
4015f5: bf 70 38 60 00 mov $0x603870,%edi
4015fa: e8 f1 f5 ff ff call 400bf0 <__isoc99_sscanf@plt>
4015ff: 83 f8 03 cmp $0x3,%eax
401602: 75 31 jne 401635 <phase_defused+0x71>
401604: be 22 26 40 00 mov $0x402622,%esi
401609: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
40160e: e8 25 fd ff ff call 401338 <strings_not_equal>
401613: 85 c0 test %eax,%eax
401615: 75 1e jne 401635 <phase_defused+0x71>
401617: bf f8 24 40 00 mov $0x4024f8,%edi
40161c: e8 ef f4 ff ff call 400b10 <puts@plt>
401621: bf 20 25 40 00 mov $0x402520,%edi
401626: e8 e5 f4 ff ff call 400b10 <puts@plt>
40162b: b8 00 00 00 00 mov $0x0,%eax
401630: e8 0d fc ff ff call 401242 <secret_phase>
401635: bf 58 25 40 00 mov $0x402558,%edi
40163a: e8 d1 f4 ff ff call 400b10 <puts@plt>
40163f: 48 8b 44 24 68 mov 0x68(%rsp),%rax
401644: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
40164b: 00 00
40164d: 74 05 je 401654 <phase_defused+0x90>
40164f: e8 dc f4 ff ff call 400b30 <__stack_chk_fail@plt>
401654: 48 83 c4 78 add $0x78,%rsp
401658: c3 ret
401659: 90 nop
40165a: 90 nop
40165b: 90 nop
40165c: 90 nop
40165d: 90 nop
40165e: 90 nop
40165f: 90 nop

现在我们研究如何走到 phase_defused: 0x401630。首先在 phase_defused: 0x4015d8 对地址 0x603760 的值进行了检测,若不为 6 则跳过调用 secret_phase。通过枚举法 🤣,我们很容易得到,在 main: 0x400ecb 处调用 phase_defused 时,地址 0x603760 的值为 6(所以地址 0x603760 的值为阶段数),也就是说隐藏阶段在阶段六之后。

然后,在 phase_defused: 0x4015ff 处对 __isoc99_sscanf 函数的返回值进行了检测。通过前后对比,可知 sscanf 将地址 0x603870 处的 3 个以空格分割的字符传入了以地址 0x7fffffffde38 开头的内存空间中,前两个解释为数字,最后的一个解释为字符串:

(gdb) x/s 0x402619
0x402619: "%d %d %s"

然而我们发现地址 0x402622 处存放的是我们在阶段四输入的内容(只有 2 个数字):

(gdb) x/w 0x603870
0x603870 <input_strings+240>: 0x00302031

为此我们需要在阶段四的答案后面加上一个字符串,根据下面调用了 strings_not_equal 函数可知,这个字符串应该和地址 0x402520 存放的字符串相同:

(gdb) x/s 0x402622
0x402622: "DrEvil"

点题了,点题了 🤣

为此我们修改 input 文件,使其变为:

Terminal window
$ cat input
Border relations with Canada have never been better.
1 2 4 8 16 32
1 311
1 0 DrEvil
ionefg
4 3 2 1 6 5

注意 phase_4: 0x401029sscanf 的返回值进行了检测,由于对应的模式中只有两个数字,所以再加上一个字符串并不影响检测:

(gdb) x/s 0x4025cf
0x4025cf: "%d %d"

然后我们就能够进入隐藏阶段了:

Curses, you've found the secret phase!
But finding it and solving it are quite different...

DrEvil 继续嘲讽 🤣

来看一看隐藏阶段的反汇编代码:

0000000000401242 <secret_phase>:
401242: 53 push %rbx
401243: e8 56 02 00 00 call 40149e <read_line>
401248: ba 0a 00 00 00 mov $0xa,%edx
40124d: be 00 00 00 00 mov $0x0,%esi
401252: 48 89 c7 mov %rax,%rdi
401255: e8 76 f9 ff ff call 400bd0 <strtol@plt>
40125a: 48 89 c3 mov %rax,%rbx
40125d: 8d 40 ff lea -0x1(%rax),%eax
401260: 3d e8 03 00 00 cmp $0x3e8,%eax
401265: 76 05 jbe 40126c <secret_phase+0x2a>
401267: e8 ce 01 00 00 call 40143a <explode_bomb>
40126c: 89 de mov %ebx,%esi
40126e: bf f0 30 60 00 mov $0x6030f0,%edi
401273: e8 8c ff ff ff call 401204 <fun7>
401278: 83 f8 02 cmp $0x2,%eax
40127b: 74 05 je 401282 <secret_phase+0x40>
40127d: e8 b8 01 00 00 call 40143a <explode_bomb>
401282: bf 38 24 40 00 mov $0x402438,%edi
401287: e8 84 f8 ff ff call 400b10 <puts@plt>
40128c: e8 33 03 00 00 call 4015c4 <phase_defused>
401291: 5b pop %rbx
401292: c3 ret
401293: 90 nop
401294: 90 nop
401295: 90 nop
401296: 90 nop
401297: 90 nop
401298: 90 nop
401299: 90 nop
40129a: 90 nop
40129b: 90 nop
40129c: 90 nop
40129d: 90 nop
40129e: 90 nop
40129f: 90 nop

secret_phase: 0x401243 从输入中读取一行,读取的内容存放在地址 0x603960 中(寄存器 rax 的值)。secret_phase: 0x401255 调用了 strtol 函数,将字符串转换为对应的数字,保存在寄存器 rax 中。rax → rbxeax-=1,然后检测 eax 是否 ≤ 1000(即 0x3e8,后面二叉树中元素的最大值为 1001)。

然后,ebx → esi0x6030f0 → edi,并调用 fun7 函数,测试其返回值是否为 2

我们先看看地址 0x6030f0 里是什么:

(gdb) x/120w 0x6030f0
0x6030f0 <n1>: 0x00000024 0x00000000 0x00603110 0x00000000
0x603100 <n1+16>: 0x00603130 0x00000000 0x00000000 0x00000000
0x603110 <n21>: 0x00000008 0x00000000 0x00603190 0x00000000
0x603120 <n21+16>: 0x00603150 0x00000000 0x00000000 0x00000000
0x603130 <n22>: 0x00000032 0x00000000 0x00603170 0x00000000
0x603140 <n22+16>: 0x006031b0 0x00000000 0x00000000 0x00000000
0x603150 <n32>: 0x00000016 0x00000000 0x00603270 0x00000000
0x603160 <n32+16>: 0x00603230 0x00000000 0x00000000 0x00000000
0x603170 <n33>: 0x0000002d 0x00000000 0x006031d0 0x00000000
0x603180 <n33+16>: 0x00603290 0x00000000 0x00000000 0x00000000
0x603190 <n31>: 0x00000006 0x00000000 0x006031f0 0x00000000
0x6031a0 <n31+16>: 0x00603250 0x00000000 0x00000000 0x00000000
0x6031b0 <n34>: 0x0000006b 0x00000000 0x00603210 0x00000000
0x6031c0 <n34+16>: 0x006032b0 0x00000000 0x00000000 0x00000000
0x6031d0 <n45>: 0x00000028 0x00000000 0x00000000 0x00000000
0x6031e0 <n45+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x6031f0 <n41>: 0x00000001 0x00000000 0x00000000 0x00000000
0x603200 <n41+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603210 <n47>: 0x00000063 0x00000000 0x00000000 0x00000000
0x603220 <n47+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603230 <n44>: 0x00000023 0x00000000 0x00000000 0x00000000
0x603240 <n44+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603250 <n42>: 0x00000007 0x00000000 0x00000000 0x00000000
0x603260 <n42+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603270 <n43>: 0x00000014 0x00000000 0x00000000 0x00000000
0x603280 <n43+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603290 <n46>: 0x0000002f 0x00000000 0x00000000 0x00000000
0x6032a0 <n46+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x6032b0 <n48>: 0x000003e9 0x00000000 0x00000000 0x00000000
0x6032c0 <n48+16>: 0x00000000 0x00000000 0x00000000 0x00000000

排版好丑 🤣

这是一个二叉树!大概的结构长这样:

graph TD; n1-->n21 n1-->n22 n21-->n31 n21-->n32 n22-->n33 n22-->n34 n31-->n41 n31-->n42 n32-->n43 n32-->n44 n33-->n45 n33-->n46 n34-->n47 n34-->n48

注:观察节点对应的值,可知是一棵二叉查找树

再看看 fun7 函数长啥样:

0000000000401204 <fun7>:
401204: 48 83 ec 08 sub $0x8,%rsp
401208: 48 85 ff test %rdi,%rdi
40120b: 74 2b je 401238 <fun7+0x34>
40120d: 8b 17 mov (%rdi),%edx
40120f: 39 f2 cmp %esi,%edx
401211: 7e 0d jle 401220 <fun7+0x1c>
401213: 48 8b 7f 08 mov 0x8(%rdi),%rdi
401217: e8 e8 ff ff ff call 401204 <fun7>
40121c: 01 c0 add %eax,%eax
40121e: eb 1d jmp 40123d <fun7+0x39>
401220: b8 00 00 00 00 mov $0x0,%eax
401225: 39 f2 cmp %esi,%edx
401227: 74 14 je 40123d <fun7+0x39>
401229: 48 8b 7f 10 mov 0x10(%rdi),%rdi
40122d: e8 d2 ff ff ff call 401204 <fun7>
401232: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401236: eb 05 jmp 40123d <fun7+0x39>
401238: b8 ff ff ff ff mov $0xffffffff,%eax
40123d: 48 83 c4 08 add $0x8,%rsp
401241: c3 ret

fun7: 0x401208 测试当前节点是否为空。若为空,置寄存器 eax-1,那必然没救了 🤣,因为修改寄存器 eax 的两条语句:

都没有办法让 -1 在几步之内变为 2,所以我们不能走到空节点。

观察 fun7: 0x401211 可知,若当前节点存储的值 我们之前输入的值,则走到右子树(fun7: 0x401229),反之走到左子树(fun7: 0x401213)。另外,递归调用右子树前,还会置寄存器 eax0,并比较当前节点存储的值是否等于我们之前输入的值,若相等,则结束递归调用。

综合分析可知,我们输入的值应该等于二叉树中某一个节点的对应的值。并且,在递归返回的过程中,应该在第一次从右子树返回到父节点(1 → eax)后,有且仅有一次从左子树返回到父节点 (eax+eax → eax)。

于是我们输入的值可以是节点 n43 或者节点 n32 对应的值,即 2022(将十六进制转换为十进制)。

为此我们修改 input 文件,使其变为:

Terminal window
$ cat input
Border relations with Canada have never been better.
1 2 4 8 16 32
1 311
1 0 DrEvil
ionefg
4 3 2 1 6 5
20

测试一下,注意这里隐藏阶段的成功提示在阶段六之前:

Terminal window
vgalaxy@vgalaxy-VirtualBox:~/Lab/bomb$ ./bomb input
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
Halfway there!
So you got that one. Try this one.
Good work! On to the next...
Curses, you've found the secret phase!
But finding it and solving it are quite different...
Wow! You've defused the secret stage!
Congratulations! You've defused the bomb!
vgalaxy@vgalaxy-VirtualBox:~/Lab/bomb$ date
2021 09 06 星期一 22:14:16 CST

完结撒花 🤗