死去的炸弹又来攻击我!!😅😅😅

CS: APP 是本科时候的必选课,研究生阶段选修了这门课,相当于重新再学一遍。本科学这门课时正值疫情,在加里敦(家里蹲)大学上了一个学期,当时太迷茫了,啥也不会,整个课上得懵懵懂懂。估计那时的炸弹也是看着网上的教程东抄抄西补补勉强弄出来的。

现在研究生了,该补的地方还是得补。不管有多忙,我决定沉下心来重看《CSAPP》,炸弹客实验也打算亲自做做。最终还是做出来了😎,耗时 2 天。难度怎么说——还算适中吧,如果你仔细看过课本就不难。

目前基本的 6 层炸弹已经拆除,还有一层隐藏层有时间再研究。由于每个人收到的炸弹是不同的(你的老师会说根据你的学号选择相应的炸弹),所以文章仅提供基本思路供参考。

《深入理解计算机系统》:Computer Systems. A Programmer’s Perspective

CSAPP LAB 实验

实验介绍

二进制炸弹包含若干个阶段,每个阶段需要输入特定的字符串,所有输入正确则炸弹被排除,否则……

我们的任务是找出这些字符串字符串记录到文件中,可输入命令验证 ./bomb solution.txt 用换行区别不同阶段的字符串。

下载压缩包 bombs,压缩包包含多个 bomb 代码包,根据学号领取自己的代码包。

查看 bomb.c 可知程序利用 phase_* 函数(* 为 1~6) 检查输入字符串是否合法,不合法就引爆炸弹。我们的任务就是逆向出每个 phase 的检查规则,构造出合法字符串。当然,bomb.c 没有给出 phase_* 的源码

逆向方法:

  • GDB
  • 直接反汇编

课本参考:

  • CSAPP 第三章:GDB 的使用
  • CSAPP 第三章:过程

链接参考(喜报:全是 English):

汇编语法看课本就行,GDB 命令看本文后续章节就行。以上链接你可以一个都不用点开。

GDB 工具的使用

1
2
apt install gdb # 安装 GDB
gdb --version # 检查是否安装成功

GDB 不需要了解过多,想干什么直接查就行。现用现学。

你可能用到的命令只有这些:

  • run:运行 GDB
  • continue:继续运行,可简写为 c
  • break <addr>:在指定地址处设置断点。break 可简写为 b
  • print $eax:打印某个寄存器的值。print 可简写为 p
  • p /x ($esp-0x8):打印某个寄存器的值的运算结果
  • print (char *) <addr> :将直接打印该地址 addr 下的字符串
  • print /x * <addr> :以十六进制的形式打印指定地址 addr 的值
  • print ((int*) <addr>)[<index>] :打印指定位置 addr 数组下标为 index 的元素
  • print ((int*) <addr>)[<index>]@<num> :打印指定位置数组的元素,起始坐标为 index,输出 num 个元素

截一张课本的图供更多参考:

image.png

开始拆弹!

拆弹准备

在 Linux 环境下解压 bomb 压缩文件。

1
tar -xvf bomb7.tar

得到如下文件:

  • bomb:二进制可执行文件,任务目标文件
  • bomb.cbomb 的源文件,辅助理解 bomb 代码

首先使用 objdump 反汇编工具对 bomb 进行反汇编,输出的汇编文件名为 dump.s

1
objdump -d bomb > dump.s

bomb.cbomb 的部分源文件,包含主函数 main。通读 main 函数以及注释,我们知道:

  1. 运行 bomb 时如果不带参数,我们将在标准输入中输入拆弹的字符串。
  2. 运行 bomb 时如果携带一个参数,则先从指定的文件中读取字符串
  3. 运行 bomb 时如果携带多于一个参数,则输出提示:bomb [<input_file>]
  4. 一共有 6 层炸弹,每一层通过输入一行字符串以进行拆弹
  5. 炸弹爆炸后不会有任何人受到伤害
  6. 可能存在隐藏关卡

此外,我们可以在炸弹运行时 Ctrl+C 中止拆弹,邪恶博士😈(Dr. Evil)最终还是会放我们走的:

1
2
^CSo you think you can stop the bomb with ctrl-c, do you?
Well...OK. :-)

新建文件 solution.txt,之后我们可以直接在里面写字符串答案(每行按序写上各层答案),从而不用手动输入字符串。

bomb.c 看完后,它的作用基本结束了。后面我们的时间将会花在人工解读 dump.s 文件以及使用 gdb 调试工具的过程中。

dump.s 文件阅读要点:

  1. 不需要看懂所有汇编代码(实际上也不可能),只需看懂关键函数部分的代码。
  2. 关键函数:各层炸弹函数 <phase_X>,中间穿插的函数 <funcX>。为了便于自己的理解,有时候你也可以看一些工具类的函数,比如 <read_six_numbers> <strings_not_equal> 等,但这些函数看名字就知道其作用。
  3. 当运行到 <explode_bomb> 时炸弹爆炸,当该层函数安全结束返回到 main 运行 <phase_defused> 时该层炸弹安全破解。
  4. 多注释和分行便于阅读

在破解某一层炸弹是,我们可以先随便输几个数,通过 GDB 工具推测我们的输入存放在栈中的哪个位置,这样也能方便推理。

Phase 1 - 寻找字符串

asm 代码阅读提示:左侧为程序地址与指令字节码,右侧为反汇编后得到的汇编代码。注释使用 ; 以及 #

1
2
3
4
5
6
7
8
9
10
11
12
13
14
08048b80 <phase_1>:
8048b80: 55 push %ebp
8048b81: 89 e5 mov %esp,%ebp
8048b83: 83 ec 08 sub $0x8,%esp # 栈指针 -8
8048b86: c7 44 24 04 d8 99 04 movl $0x80499d8,0x4(%esp) # 栈+4 取 0x80499d8 存储的字符串
8048b8d: 08
8048b8e: 8b 45 08 mov 0x8(%ebp),%eax # 原先的栈指针 + 8 给 eax
8048b91: 89 04 24 mov %eax,(%esp) # eax 放到 栈当前指向的位置
8048b94: e8 86 05 00 00 call 804911f <strings_not_equal> # 返回 0 表示字符串相等
8048b99: 85 c0 test %eax,%eax # test 执行的是 AND 操作。 eax为0才成功
8048b9b: 74 05 je 8048ba2 <phase_1+0x22> # 破解成功 je 是判断标志位ZF是否为1
8048b9d: e8 44 0b 00 00 call 80496e6 <explode_bomb> # 破解失败
8048ba2: c9 leave
8048ba3: c3 ret

使用 GDB 调试前记得打断点,比如:b *0x8048b94

1
2
(gdb) p (char*)  0x80499d8
$2 = 0x80499d8 "I am not part of the problem. I am a Republican."

解析及要点:

  • 本层通过判断输入的字符串是否与指定字符串相等。
  • 此层只需使用 GDB 指令找出地址 $0x80499d8 存储的字符串,该字符串就是本层答案。

点评与提示:

  • 拆弹入门。
  • 像遇到这种存在「魔数」的指令要多留心,常使用 GDB 打印该位置的内容,有可能存储关键字符串或数组。
  • %eax 多用于存储函数返回结果
  • test %eax,%eaxtest 相当于 and 指令,用于测试 %eax 是否为 0。je 指令会判断 ZF 标志位,当 ZF 为 1 时说明 %eax 为 0,程序执行跳转。

Phase 2 - 有序数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
08048ba4 <phase_2>:
8048ba4: 55 push %ebp
8048ba5: 89 e5 mov %esp,%ebp
8048ba7: 83 ec 28 sub $0x28,%esp # 栈指针 - 0x28 = 40 ;栈分配 40 字节
8048baa: 8d 45 e4 lea -0x1c(%ebp),%eax # 原先栈 - 0x1c = 28
8048bad: 89 44 24 04 mov %eax,0x4(%esp) # 栈+4 <- 原先栈-28
8048bb1: 8b 45 08 mov 0x8(%ebp),%eax
8048bb4: 89 04 24 mov %eax,(%esp) # 栈 <- 原先栈+8
8048bb7: e8 d0 04 00 00 call 804908c <read_six_numbers>
8048bbc: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%ebp) # i = 1
8048bc3: eb 1e jmp 8048be3 <phase_2+0x3f>

# 循环开始(比较前后两个数是否相差5)
8048bc5: 8b 45 fc mov -0x4(%ebp),%eax # 取 i
8048bc8: 8b 54 85 e4 mov -0x1c(%ebp,%eax,4),%edx # arr[i]
8048bcc: 8b 45 fc mov -0x4(%ebp),%eax
8048bcf: 48 dec %eax # 取 i-1
8048bd0: 8b 44 85 e4 mov -0x1c(%ebp,%eax,4),%eax # arr[i-1]
8048bd4: 83 c0 05 add $0x5,%eax
8048bd7: 39 c2 cmp %eax,%edx
8048bd9: 74 05 je 8048be0 <phase_2+0x3c> # arr[i-1]+5 == arr[i] 必须相等
8048bdb: e8 06 0b 00 00 call 80496e6 <explode_bomb>
8048be0: ff 45 fc incl -0x4(%ebp) # i+=1

8048be3: 83 7d fc 05 cmpl $0x5,-0x4(%ebp)
8048be7: 7e dc jle 8048bc5 <phase_2+0x21> # i <= 5 继续循环(i从1开始)
8048be9: c9 leave
8048bea: c3 ret

断点打到 <read_six_numbers> 之后,比如 0x8048bbc。我们随便输入 5 个数字,比如 1 6 11 16 21 26,通过 GDB 可以判断其存储位置:

1
2
3
4
5
6
7
# 下面展示了输入的 6 个数字的存储位置(示例数据)
(gdb) p *(int*) 0xffffcc6c $6 = 1
(gdb) p *(int*) 0xffffcc70 $8 = 6
(gdb) p *(int*) 0xffffcc74 $9 = 11
(gdb) p *(int*) 0xffffcc78 $10 = 16
(gdb) p *(int*) 0xffffcc7c $11 = 21
(gdb) p *(int*) 0xffffcc80 $12 = 26

为啥会知道查 6 个地址。我们根据这个原则就行:汇编中用到哪里,GDB 就查哪里。

这里的汇编代码中存在一个典型的 for 循环结构,使用了「跳转到中间」策略(具体请看教材)。for 循环的通用形式:

1
2
3
4
5
6
7
8
9
for(init-expr;test-expr;update-expr)
body-statement;

// 行为与下面的while循环代码一样
init-expr;
while(test-expr){
body-statement
update-expr;
}

该部分汇编代码结构翻译为 C 语言的大致结构如下:

1
2
3
4
5
6
7
8
9
	init-expr;
goto test;
loop:
body-statement;
update-expr;
test:
t=test-expr;
if(t)
goto loop;

解析及要点:

  • 本层需输入 6 个数字,每个数字需构成升序的等差数列(公差为 5)

点评与提示:

  • 识别 for 循环
  • 知道输入的 6 个数字的存储位置

Phase 3 - 跳转表

相信你通过了前面汇编的洗礼后,汇编代码的阅读能力提高了不少。本层开始就不会啰嗦太多哦,看注释即可。

1
2
3
4
5
6
7
8
9
10
11
12
08048beb <phase_3>:
8048beb: 55 push %ebp
8048bec: 89 e5 mov %esp,%ebp
8048bee: 83 ec 38 sub $0x38,%esp
8048bf1: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp)
8048bf8: 8d 45 f0 lea -0x10(%ebp),%eax
8048bfb: 89 44 24 10 mov %eax,0x10(%esp)
8048bff: 8d 45 ef lea -0x11(%ebp),%eax
8048c02: 89 44 24 0c mov %eax,0xc(%esp)
8048c06: 8d 45 f4 lea -0xc(%ebp),%eax
8048c09: 89 44 24 08 mov %eax,0x8(%esp)
8048c0d: c7 44 24 04 09 9a 04 movl $0x8049a09,0x4(%esp)

查看魔数信息:

1
2
(gdb) p (char*) 0x8049a09
$13 = 0x8049a09 "%d %c %d"

这个字符串是不是很眼熟,这可能提示我们输入 3 个数据:1 个数字、1 个字符以及 1 个数字。我们可以像上一层那样随便输入 3 个数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
8048c14:	08 
8048c15: 8b 45 08 mov 0x8(%ebp),%eax
8048c18: 89 04 24 mov %eax,(%esp)
8048c1b: e8 48 fc ff ff call 8048868 <sscanf@plt>
8048c20: 89 45 f8 mov %eax,-0x8(%ebp) # 返回输入的个数
8048c23: 83 7d f8 02 cmpl $0x2,-0x8(%ebp)
8048c27: 7f 05 jg 8048c2e <phase_3+0x43> # 输入(长度)必须大于2
8048c29: e8 b8 0a 00 00 call 80496e6 <explode_bomb>
8048c2e: 8b 45 f4 mov -0xc(%ebp),%eax # -0xc(%ebp)携带输入的首地址
8048c31: 89 45 dc mov %eax,-0x24(%ebp) # 经过GDB调试,-0x24(%ebp) 表示第一个输入的数字
8048c34: 83 7d dc 07 cmpl $0x7,-0x24(%ebp) # 输入的数字必须小于等于7
8048c38: 0f 87 c0 00 00 00 ja 8048cfe <phase_3+0x113> # 无符号大于7 - 则爆炸
8048c3e: 8b 55 dc mov -0x24(%ebp),%edx # %edx = 第一个输入的数字
8048c41: 8b 04 95 14 9a 04 08 mov 0x8049a14(,%edx,4),%eax
8048c48: ff e0 jmp *%eax # 例如第一个输入的数字是6 则 eax=0x8048cd4

汇编中用到哪里,GDB 就查哪里。-0x24(%ebp) 指向我们第一个输入的数字。

jmp *%eax 这种语法很眼熟,像是课本中提到的 switch 语句的汇编。我们可以尝试找出其跳转表:

1
2
(gdb) print /x ((int*) 0x8049a14)[0]@8
$25 = {0x8048c4a, 0x8048c66, 0x8048c82, 0x8048c97, 0x8048cac, 0x8048cbf, 0x8048cd4, 0x8048ce9}

我们就随便选一个,比如 6。接下来我们看 6 的代码就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
 # 0
8048c4a: c6 45 ff 67 movb $0x67,-0x1(%ebp)
8048c4e: 8b 45 f0 mov -0x10(%ebp),%eax
8048c51: 3d c8 00 00 00 cmp $0xc8,%eax
8048c56: 0f 84 ab 00 00 00 je 8048d07 <phase_3+0x11c>
8048c5c: e8 85 0a 00 00 call 80496e6 <explode_bomb>
8048c61: e9 a1 00 00 00 jmp 8048d07 <phase_3+0x11c>

# 1
8048c66: c6 45 ff 6c movb $0x6c,-0x1(%ebp)
8048c6a: 8b 45 f0 mov -0x10(%ebp),%eax
8048c6d: 3d c7 03 00 00 cmp $0x3c7,%eax
8048c72: 0f 84 8f 00 00 00 je 8048d07 <phase_3+0x11c>
8048c78: e8 69 0a 00 00 call 80496e6 <explode_bomb>
8048c7d: e9 85 00 00 00 jmp 8048d07 <phase_3+0x11c>

# 2
8048c82: c6 45 ff 69 movb $0x69,-0x1(%ebp)
8048c86: 8b 45 f0 mov -0x10(%ebp),%eax
8048c89: 3d 1e 02 00 00 cmp $0x21e,%eax
8048c8e: 74 77 je 8048d07 <phase_3+0x11c>
8048c90: e8 51 0a 00 00 call 80496e6 <explode_bomb>
8048c95: eb 70 jmp 8048d07 <phase_3+0x11c>

# 3
8048c97: c6 45 ff 6e movb $0x6e,-0x1(%ebp)
8048c9b: 8b 45 f0 mov -0x10(%ebp),%eax
8048c9e: 3d 6a 03 00 00 cmp $0x36a,%eax
8048ca3: 74 62 je 8048d07 <phase_3+0x11c>
8048ca5: e8 3c 0a 00 00 call 80496e6 <explode_bomb>
8048caa: eb 5b jmp 8048d07 <phase_3+0x11c>
8048cac: c6 45 ff 6f movb $0x6f,-0x1(%ebp)

# 4
8048cb0: 8b 45 f0 mov -0x10(%ebp),%eax
8048cb3: 83 f8 3f cmp $0x3f,%eax
8048cb6: 74 4f je 8048d07 <phase_3+0x11c>
8048cb8: e8 29 0a 00 00 call 80496e6 <explode_bomb>
8048cbd: eb 48 jmp 8048d07 <phase_3+0x11c>

# 5
8048cbf: c6 45 ff 69 movb $0x69,-0x1(%ebp)
8048cc3: 8b 45 f0 mov -0x10(%ebp),%eax
8048cc6: 3d 8f 00 00 00 cmp $0x8f,%eax
8048ccb: 74 3a je 8048d07 <phase_3+0x11c>
8048ccd: e8 14 0a 00 00 call 80496e6 <explode_bomb>
8048cd2: eb 33 jmp 8048d07 <phase_3+0x11c>

# 6
8048cd4: c6 45 ff 77 movb $0x77,-0x1(%ebp) # 77 对应字符 w 为后文埋下伏笔
8048cd8: 8b 45 f0 mov -0x10(%ebp),%eax # ebp - 0x10 是第三个输入的数字
; (gdb) p /x *0xffffcc78
; $31 = 0x1b1
8048cdb: 3d b1 01 00 00 cmp $0x1b1,%eax # $0x1b1 = 433 第三个数字和 0x1b1 比较
8048ce0: 74 25 je 8048d07 <phase_3+0x11c>
8048ce2: e8 ff 09 00 00 call 80496e6 <explode_bomb>
8048ce7: eb 1e jmp 8048d07 <phase_3+0x11c>

# 7
8048ce9: c6 45 ff 76 movb $0x76,-0x1(%ebp)
8048ced: 8b 45 f0 mov -0x10(%ebp),%eax
8048cf0: 3d 9f 03 00 00 cmp $0x39f,%eax
8048cf5: 74 10 je 8048d07 <phase_3+0x11c>
8048cf7: e8 ea 09 00 00 call 80496e6 <explode_bomb>

8048cfc: eb 09 jmp 8048d07 <phase_3+0x11c>
8048cfe: c6 45 ff 65 movb $0x65,-0x1(%ebp) # 爆炸
8048d02: e8 df 09 00 00 call 80496e6 <explode_bomb>

汇编中用到哪里,GDB 就查哪里。-0x10(%ebp) 指向我们第三个输入的数字。

1
2
3
4
5
6
7
8
9
# 返回
8048d07: 0f b6 45 ef movzbl -0x11(%ebp),%eax # 零扩展传送到eax
; (gdb) p *(char*) 0xffffcc77
; $37 = 119 'w'
8048d0b: 38 45 ff cmp %al,-0x1(%ebp) # al是rax的低字节 上文的伏笔
8048d0e: 74 05 je 8048d15 <phase_3+0x12a> # 等于才成功,否则爆炸
8048d10: e8 d1 09 00 00 call 80496e6 <explode_bomb>
8048d15: c9 leave
8048d16: c3 ret

汇编中用到哪里,GDB 就查哪里。-0x11(%ebp) 指向我们第二个输入的字符。

解析及要点:

  • 本层需输入 3 个数据。根据跳转表内容输入符合要求的数字。比如:6 w 433

点评与提示:

  • 识别 switch
  • 善用 GDB

Phase 4 - 递归函数

第四层使用到了一个函数,我们先解读这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
08048d17 <func4>:
8048d17: 55 push %ebp
8048d18: 89 e5 mov %esp,%ebp
8048d1a: 53 push %ebx
8048d1b: 83 ec 08 sub $0x8,%esp
8048d1e: 83 7d 08 01 cmpl $0x1,0x8(%ebp)
8048d22: 7f 09 jg 8048d2d <func4+0x16> # if x>1
8048d24: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp) # else
8048d2b: eb 21 jmp 8048d4e <func4+0x37>

8048d2d: 8b 45 08 mov 0x8(%ebp),%eax # if x>1
8048d30: 48 dec %eax
8048d31: 89 04 24 mov %eax,(%esp) # x-1 作为参数递归
8048d34: e8 de ff ff ff call 8048d17 <func4>
8048d39: 89 c3 mov %eax,%ebx # f(x-1) 的结果暂存
8048d3b: 8b 45 08 mov 0x8(%ebp),%eax
8048d3e: 83 e8 02 sub $0x2,%eax
8048d41: 89 04 24 mov %eax,(%esp) # x-2 作为参数递归
8048d44: e8 ce ff ff ff call 8048d17 <func4>
8048d49: 01 c3 add %eax,%ebx # f(x-1) 的结果+f(x-1)
8048d4b: 89 5d f8 mov %ebx,-0x8(%ebp) # 作为返回结果

8048d4e: 8b 45 f8 mov -0x8(%ebp),%eax # 1 返回1
8048d51: 83 c4 08 add $0x8,%esp
8048d54: 5b pop %ebx
8048d55: 5d pop %ebp
8048d56: c3 ret

不难看出是个递归求斐波那契数列的函数:

1
2
3
4
5
6
7
8
func4(x){
if x>1:
return func4(x-1)+func4(x-2)
else:
return 1;
}
// 输入:0 1 2 3 4 5 ... 13
// 输出:1 1 2 3 5 8 ... 377

第四层的逻辑更加简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
08048d57 <phase_4>:
8048d57: 55 push %ebp
8048d58: 89 e5 mov %esp,%ebp
8048d5a: 83 ec 28 sub $0x28,%esp
8048d5d: 8d 45 f4 lea -0xc(%ebp),%eax
8048d60: 89 44 24 08 mov %eax,0x8(%esp)
8048d64: c7 44 24 04 34 9a 04 movl $0x8049a34,0x4(%esp) # 0x8049a34 -> "%d"
8048d6b: 08
8048d6c: 8b 45 08 mov 0x8(%ebp),%eax
8048d6f: 89 04 24 mov %eax,(%esp)
8048d72: e8 f1 fa ff ff call 8048868 <sscanf@plt>
8048d77: 89 45 fc mov %eax,-0x4(%ebp)
8048d7a: 83 7d fc 01 cmpl $0x1,-0x4(%ebp) # 只能输入一个数i,否则爆炸
8048d7e: 75 07 jne 8048d87 <phase_4+0x30> # 跳转到爆炸
8048d80: 8b 45 f4 mov -0xc(%ebp),%eax # -0xc(%ebp)存放我们第一个输入的数i
; (gdb) p *0xffffcc7c
; $42 = 13
8048d83: 85 c0 test %eax,%eax # -0xc(%ebp) 不能为零
8048d85: 7f 05 jg 8048d8c <phase_4+0x35> # 大于才成功,否则爆炸
8048d87: e8 5a 09 00 00 call 80496e6 <explode_bomb>
8048d8c: 8b 45 f4 mov -0xc(%ebp),%eax
8048d8f: 89 04 24 mov %eax,(%esp) # 将数字传参
8048d92: e8 80 ff ff ff call 8048d17 <func4> # i 进入函数func4

8048d97: 89 45 f8 mov %eax,-0x8(%ebp)
8048d9a: 81 7d f8 79 01 00 00 cmpl $0x179,-0x8(%ebp) # 结果必须为 0x179 = 377 (斐波那契数列第13项)
8048da1: 74 05 je 8048da8 <phase_4+0x51> # 等于才成功,否则爆炸
8048da3: e8 3e 09 00 00 call 80496e6 <explode_bomb>
8048da8: c9 leave
8048da9: c3 ret

解析及要点:

  • 本层需输入 1 个数据。使得函数的输出结果与给定数字相等。在这里为 13

点评与提示:

  • 会读递归函数

Phase 5 - 算数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
08048daa <phase_5>:
8048daa: 55 push %ebp
8048dab: 89 e5 mov %esp,%ebp
8048dad: 83 ec 18 sub $0x18,%esp
8048db0: 8b 45 08 mov 0x8(%ebp),%eax
8048db3: 89 04 24 mov %eax,(%esp)
8048db6: e8 3a 03 00 00 call 80490f5 <string_length>
8048dbb: 89 45 fc mov %eax,-0x4(%ebp)
8048dbe: 83 7d fc 06 cmpl $0x6,-0x4(%ebp)
8048dc2: 74 05 je 8048dc9 <phase_5+0x1f> # 输入的字符串长度必须为 6
8048dc4: e8 1d 09 00 00 call 80496e6 <explode_bomb>
8048dc9: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp) # result = 0
8048dd0: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp) # i = 0
8048dd7: eb 1c jmp 8048df5 <phase_5+0x4b>

我们看到后文老是出现 -0x8(%ebp)-0xc(%ebp)。又看到熟悉的循环结构,干脆分别定义变量 resulti 辅助我们进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 循环开始
8048dd9: 8b 45 f4 mov -0xc(%ebp),%eax
8048ddc: 03 45 08 add 0x8(%ebp),%eax # 0x804a9a0 + i 就是遍历输入的字符串

8048ddf: 0f b6 00 movzbl (%eax),%eax
8048de2: 0f be c0 movsbl %al,%eax
8048de5: 83 e0 0f and $0xf,%eax # 对每一个字符串取低位字节 得到 k
8048de8: 8b 04 85 c0 a5 04 08 mov 0x804a5c0(,%eax,4),%eax # 取 arr[k]

8048def: 01 45 f8 add %eax,-0x8(%ebp) # result += arr[k]
8048df2: ff 45 f4 incl -0xc(%ebp) # i++

8048df5: 83 7d f4 05 cmpl $0x5,-0xc(%ebp)
8048df9: 7e de jle 8048dd9 <phase_5+0x2f> # i <= 5 继续循环
8048dfb: 83 7d f8 24 cmpl $0x24,-0x8(%ebp) # result == 36
8048dff: 74 05 je 8048e06 <phase_5+0x5c> # 必须相等
8048e01: e8 e0 08 00 00 call 80496e6 <explode_bomb>
8048e06: c9 leave
8048e07: c3 ret

1
2
3
(gdb) p  (char*) 0x804a9a0
$56 = 0x804a9a0 <input_strings+320> "@@BBAA" # 假设输入的字符串为 `@@BBAA`
# 0x804a5c0: {2, 10, 6, 1, 12, 16, 9, 3, 4, 7, 14, 5, 11, 8, 15, 13, ...}

0x804a9a0 是一个数组 arr 的地址,我们需要输入包含 6 个字符的字符串:

  • 每个字符的低 4 位作为数组 arr 的索引 i。
  • 本例中,需要使得 arr[0]+arr[1]+…+arr[5] 的总和为 36 即可通关。

例如:

  • 36 = 2+2+6+6+10+10
  • 各加数对应数组的序号为 0 0 2 2 1 1
  • ASCII 中,@:0x40 B:0x42 A:0x41,也就是对应序号 0, 2, 1
  • 答案即为 @@BBAA

解析及要点:

  • 本层需输入合适字符串指示数组的下标,使得下标对应数组数字之和等于指定值即可。
  • 知道输入字符串的存储位置

点评与提示:

  • 汇编的融会贯通

Phase 6 - 链表排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
08048e08 <phase_6>:
8048e08: 55 push %ebp
8048e09: 89 e5 mov %esp,%ebp
8048e0b: 83 ec 48 sub $0x48,%esp
8048e0e: c7 45 f0 3c a6 04 08 movl $0x804a63c,-0x10(%ebp) # $0x804a63c-> 0x329
8048e15: 8d 45 d8 lea -0x28(%ebp),%eax
8048e18: 89 44 24 04 mov %eax,0x4(%esp)
8048e1c: 8b 45 08 mov 0x8(%ebp),%eax
8048e1f: 89 04 24 mov %eax,(%esp)
8048e22: e8 65 02 00 00 call 804908c <read_six_numbers>
8048e27: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp) # i=0
8048e2e: eb 48 jmp 8048e78 <phase_6+0x70>

# 循环A ------- 开始 用于判断:每个元素不相等,且每个元素不为0
8048e30: 8b 45 f8 mov -0x8(%ebp),%eax
8048e33: 8b 44 85 d8 mov -0x28(%ebp,%eax,4),%eax # -0x28(%ebp) 的位置是输入数组的第一个元素
8048e37: 85 c0 test %eax,%eax
8048e39: 7e 0c jle 8048e47 <phase_6+0x3f> # arr[i] == 0 跳转爆炸
8048e3b: 8b 45 f8 mov -0x8(%ebp),%eax # arr[i] != 0
8048e3e: 8b 44 85 d8 mov -0x28(%ebp,%eax,4),%eax
8048e42: 83 f8 06 cmp $0x6,%eax
8048e45: 7e 05 jle 8048e4c <phase_6+0x44> # arr[i]<= 6 否则爆炸
8048e47: e8 9a 08 00 00 call 80496e6 <explode_bomb>

8048e4c: 8b 45 f8 mov -0x8(%ebp),%eax
8048e4f: 40 inc %eax # i++
8048e50: 89 45 fc mov %eax,-0x4(%ebp) # 存储ti = i+1
8048e53: eb 1a jmp 8048e6f <phase_6+0x67>

8048e55: 8b 45 f8 mov -0x8(%ebp),%eax
8048e58: 8b 54 85 d8 mov -0x28(%ebp,%eax,4),%edx # edx = arr[i]
8048e5c: 8b 45 fc mov -0x4(%ebp),%eax
8048e5f: 8b 44 85 d8 mov -0x28(%ebp,%eax,4),%eax # eax = arr[i+1]
8048e63: 39 c2 cmp %eax,%edx
8048e65: 75 05 jne 8048e6c <phase_6+0x64> # 必须 arr[i] != arr[i+1] 否则爆炸
8048e67: e8 7a 08 00 00 call 80496e6 <explode_bomb>
8048e6c: ff 45 fc incl -0x4(%ebp) # ti++

8048e6f: 83 7d fc 05 cmpl $0x5,-0x4(%ebp)
8048e73: 7e e0 jle 8048e55 <phase_6+0x4d> # ti<=5 则跳转
8048e75: ff 45 f8 incl -0x8(%ebp) # i+1==5 ,i++ , i=6 即将退出循环

8048e78: 83 7d f8 05 cmpl $0x5,-0x8(%ebp)
8048e7c: 7e b2 jle 8048e30 <phase_6+0x28> # i<=5 循环(遍历6个元素)
# 循环A ---- ------ 结束


8048e7e: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp) # j=0
8048e85: eb 34 jmp 8048ebb <phase_6+0xb3>


# 循环B 开始 ---- 将数组平移到内存中的特定的位置$0x804a63c 开始的位置,8位隔开
# 这是个链表
8048e87: 8b 45 f0 mov -0x10(%ebp),%eax # 是个地址$0x804a63c,提前存有 0x329 = 809
8048e8a: 89 45 f4 mov %eax,-0xc(%ebp) # t[1] 表示上一次的计算结果 = $0x804a63c
8048e8d: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%ebp) # tj = 1
8048e94: eb 0c jmp 8048ea2 <phase_6+0x9a>

8048e96: 8b 45 f4 mov -0xc(%ebp),%eax # t[0]地址$0x804a63c
8048e99: 8b 40 08 mov 0x8(%eax),%eax # 0x8(%eax) =$0x804a644 -> $0x804a630
8048e9c: 89 45 f4 mov %eax,-0xc(%ebp) # 链表结点下移
8048e9f: ff 45 fc incl -0x4(%ebp) # tj++

8048ea2: 8b 45 f8 mov -0x8(%ebp),%eax
8048ea5: 8b 44 85 d8 mov -0x28(%ebp,%eax,4),%eax # arr[j]
8048ea9: 3b 45 fc cmp -0x4(%ebp),%eax # tj
8048eac: 7f e8 jg 8048e96 <phase_6+0x8e> # arr[j]>tj 大于

8048eae: 8b 55 f8 mov -0x8(%ebp),%edx # edx = j
8048eb1: 8b 45 f4 mov -0xc(%ebp),%eax # eax = t[0]
8048eb4: 89 44 95 c0 mov %eax,-0x40(%ebp,%edx,4) # 存储统计结果
8048eb8: ff 45 f8 incl -0x8(%ebp) # j++

8048ebb: 83 7d f8 05 cmpl $0x5,-0x8(%ebp)
8048ebf: 7e c6 jle 8048e87 <phase_6+0x7f> # j<=5 循环(遍历6个元素)
# 循环B ------------------------ 结束

8048ec1: 8b 45 c0 mov -0x40(%ebp),%eax # arr8[0]
8048ec4: 89 45 f0 mov %eax,-0x10(%ebp) #
8048ec7: 8b 45 f0 mov -0x10(%ebp),%eax
8048eca: 89 45 f4 mov %eax,-0xc(%ebp) # t[1] 存储上一次运算结果 = arr8[0]
8048ecd: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp) # k = 1
8048ed4: eb 19 jmp 8048eef <phase_6+0xe7>

# 循环C ------------------------- 开始 调整链表内的顺序
8048ed6: 8b 45 f8 mov -0x8(%ebp),%eax # k
8048ed9: 8b 54 85 c0 mov -0x40(%ebp,%eax,4),%edx # edx = arr8[k]
8048edd: 8b 45 f4 mov -0xc(%ebp),%eax # eax = $0x804a63c
8048ee0: 89 50 08 mov %edx,0x8(%eax) # $0x804a63c+8 = arr8[k]
8048ee3: 8b 45 f4 mov -0xc(%ebp),%eax
8048ee6: 8b 40 08 mov 0x8(%eax),%eax # $0x804a63c+8
8048ee9: 89 45 f4 mov %eax,-0xc(%ebp) # 基地址上移8
8048eec: ff 45 f8 incl -0x8(%ebp) # k++

8048eef: 83 7d f8 05 cmpl $0x5,-0x8(%ebp)
8048ef3: 7e e1 jle 8048ed6 <phase_6+0xce> # k<=5 循环(遍历5个元素,因为k从1开始的)
# 循环C -------------------------- 结束

8048ef5: 8b 45 f4 mov -0xc(%ebp),%eax
8048ef8: c7 40 08 00 00 00 00 movl $0x0,0x8(%eax) # 基址设置为0
8048eff: 8b 45 f0 mov -0x10(%ebp),%eax # 重新获取$0x804a63c
8048f02: 89 45 f4 mov %eax,-0xc(%ebp)
8048f05: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp) # l = 0
8048f0c: eb 22 jmp 8048f30 <phase_6+0x128>

# 循环 D -------- 开始:左边的数大于右边的数
8048f0e: 8b 45 f4 mov -0xc(%ebp),%eax # $0x804a63c
8048f11: 8b 10 mov (%eax),%edx
8048f13: 8b 45 f4 mov -0xc(%ebp),%eax
8048f16: 8b 40 08 mov 0x8(%eax),%eax
8048f19: 8b 00 mov (%eax),%eax
8048f1b: 39 c2 cmp %eax,%edx # 相隔 8 的两个基址
8048f1d: 7d 05 jge 8048f24 <phase_6+0x11c> #必须: 左基址 >= 右基址
8048f1f: e8 c2 07 00 00 call 80496e6 <explode_bomb>
8048f24: 8b 45 f4 mov -0xc(%ebp),%eax
8048f27: 8b 40 08 mov 0x8(%eax),%eax # 基址+8
8048f2a: 89 45 f4 mov %eax,-0xc(%ebp) # 基址更新
8048f2d: ff 45 f8 incl -0x8(%ebp) # l++

8048f30: 83 7d f8 04 cmpl $0x4,-0x8(%ebp) # l<=4 5 次
8048f34: 7e d8 jle 8048f0e <phase_6+0x106>
# 循环D结束

8048f36: c9 leave
8048f37: c3 ret

本例中出现的魔数 0x804a63c 为链表头结点地址,通过 GDB 我们可以得知链表结点地址以及存储的值:

1
2
3
4
5
6
7
; 0x804a63c 0x329
; 0x804a630 0x124
; 0x804a638 0x325
; 0x804a62c 0x178
; 0x804a620 0x207
; 0x804a614 0x92
; 0x804a608 0x00 NULL

输入的数组要求:

  1. 6 个数字互不相同,范围 1~6
  2. 第 n 个数字值 v,代表第 n 个结点排序后的位置为 v
  3. 结点排序后的结果为:结点代表的值降序排序

解析及要点:

  • 本层需输入 6 个数字对链表进行排序,使得排序结果满足目标要求
  • 必须识别出这是个链表
  • 逐个循环阅读

点评与提示:

  • 耐心耐心耐心

Secret Phase

拆除炸弹各层的提示语如下:

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

但这并不意味着炸弹已经拆除完成,注意到 bomb.c 文件中有邪恶博士😈在最后一层炸弹拆除时留下的注释:

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

反汇编得到的汇编代码中还存在一些隐藏内容:<func7> 以及 <secret_phase> 函数。

这部分就留给读者自己解决啦~~ 反正助教通过我的实验了,为了「完美主义」,我有时间在弄吧。

看起来不多呢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
08048f38 <fun7>:
8048f38: 55 push %ebp
8048f39: 89 e5 mov %esp,%ebp
8048f3b: 83 ec 0c sub $0xc,%esp
8048f3e: 83 7d 08 00 cmpl $0x0,0x8(%ebp)
8048f42: 75 09 jne 8048f4d <fun7+0x15>
8048f44: c7 45 fc ff ff ff ff movl $0xffffffff,-0x4(%ebp)
8048f4b: eb 54 jmp 8048fa1 <fun7+0x69>
8048f4d: 8b 45 08 mov 0x8(%ebp),%eax
8048f50: 8b 00 mov (%eax),%eax
8048f52: 3b 45 0c cmp 0xc(%ebp),%eax
8048f55: 7e 1c jle 8048f73 <fun7+0x3b>
8048f57: 8b 45 08 mov 0x8(%ebp),%eax
8048f5a: 8b 50 04 mov 0x4(%eax),%edx
8048f5d: 8b 45 0c mov 0xc(%ebp),%eax
8048f60: 89 44 24 04 mov %eax,0x4(%esp)
8048f64: 89 14 24 mov %edx,(%esp)
8048f67: e8 cc ff ff ff call 8048f38 <fun7>
8048f6c: 01 c0 add %eax,%eax
8048f6e: 89 45 fc mov %eax,-0x4(%ebp)
8048f71: eb 2e jmp 8048fa1 <fun7+0x69>
8048f73: 8b 45 08 mov 0x8(%ebp),%eax
8048f76: 8b 00 mov (%eax),%eax
8048f78: 3b 45 0c cmp 0xc(%ebp),%eax
8048f7b: 75 09 jne 8048f86 <fun7+0x4e>
8048f7d: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp)
8048f84: eb 1b jmp 8048fa1 <fun7+0x69>
8048f86: 8b 45 08 mov 0x8(%ebp),%eax
8048f89: 8b 50 08 mov 0x8(%eax),%edx
8048f8c: 8b 45 0c mov 0xc(%ebp),%eax
8048f8f: 89 44 24 04 mov %eax,0x4(%esp)
8048f93: 89 14 24 mov %edx,(%esp)
8048f96: e8 9d ff ff ff call 8048f38 <fun7>
8048f9b: 01 c0 add %eax,%eax
8048f9d: 40 inc %eax
8048f9e: 89 45 fc mov %eax,-0x4(%ebp)
8048fa1: 8b 45 fc mov -0x4(%ebp),%eax
8048fa4: c9 leave
8048fa5: c3 ret

08048fa6 <secret_phase>:
8048fa6: 55 push %ebp
8048fa7: 89 e5 mov %esp,%ebp
8048fa9: 83 ec 18 sub $0x18,%esp
8048fac: e8 a9 03 00 00 call 804935a <read_line>
8048fb1: 89 45 f4 mov %eax,-0xc(%ebp)
8048fb4: 8b 45 f4 mov -0xc(%ebp),%eax
8048fb7: 89 04 24 mov %eax,(%esp)
8048fba: e8 99 f8 ff ff call 8048858 <atoi@plt>
8048fbf: 89 45 f8 mov %eax,-0x8(%ebp)
8048fc2: 83 7d f8 00 cmpl $0x0,-0x8(%ebp)
8048fc6: 7e 09 jle 8048fd1 <secret_phase+0x2b>
8048fc8: 81 7d f8 e9 03 00 00 cmpl $0x3e9,-0x8(%ebp)
8048fcf: 7e 05 jle 8048fd6 <secret_phase+0x30>
8048fd1: e8 10 07 00 00 call 80496e6 <explode_bomb>
8048fd6: 8b 45 f8 mov -0x8(%ebp),%eax
8048fd9: 89 44 24 04 mov %eax,0x4(%esp)
8048fdd: c7 04 24 f0 a6 04 08 movl $0x804a6f0,(%esp)
8048fe4: e8 4f ff ff ff call 8048f38 <fun7>
8048fe9: 89 45 fc mov %eax,-0x4(%ebp)
8048fec: 83 7d fc 06 cmpl $0x6,-0x4(%ebp)
8048ff0: 74 05 je 8048ff7 <secret_phase+0x51>
8048ff2: e8 ef 06 00 00 call 80496e6 <explode_bomb>
8048ff7: c7 04 24 38 9a 04 08 movl $0x8049a38,(%esp)
8048ffe: e8 c5 f7 ff ff call 80487c8 <puts@plt>
8049003: e8 08 07 00 00 call 8049710 <phase_defused>
8049008: c9 leave
8049009: c3 ret
804900a: 90 nop
804900b: 90 nop

后续任务

  • 有时间再完善隐藏层部分的内容

本文参考