Bomb Lab

简介

这是HNU计算机系统课程的第三次实验,实验要求是给一个用C语言编写的可执行文件bomb,你可以看到它主函数的C语言代码,除此之外,一概不知,实验分为六个阶段,每个阶段需要输入一串字符,以此来破译炸弹的密码,如果六次输入的密码都是正确的,则炸弹拆除,否则炸弹爆炸(退出并打印爆炸信息)。为了防止学生互相借鉴,HNU采用了最传统的防作弊方式——让每个学生做不同的题目。这次实验好行并没有什么评分标准,所以可以不断测试自己的答案。

C语言源代码

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/***************************************************************************
* 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.
*
* 邪恶博士公司(PerPETRATOR)在此授予您(受害者)明确的许可来使用这个炸弹(the BOMB)。
* 这是一个有时间限制的许可,在受害者死亡后失效。
* 对于受害者的损害、挫折、精神错乱、虫眼、腕管综合症、失眠或其他伤害,PerPETRATOR不承担任何责任。
* 除非 "保护者 "想邀功,也就是说。受害者不得将此炸弹的源代码分发给PERPETRATOR的任何敌人。
* 受害者不得调试、逆向工程、运行 "字符串"、反编译、解密或使用任何其他技术来获得对炸弹的了解和拆除炸弹。
* 在处理本程序时,不得穿戴防弹衣。
* PERPETRATOR不会为PERPETRATOR的不良幽默感道歉。
* 在法律禁止使用BOMB的地方,本许可是无效的。
***************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include "support.h"
#include "phases.h"

/*
* Note to self: Remember to erase this file so my victims will have no
* idea what is going on, and so they will all blow up in a
* spectaculary fiendish explosion. -- Dr. Evil
* 自我注意: 记得删除这个文件,这样我的受害者就不会知道发生了什么事,
* 而且他们都会在一场可怕的魔鬼爆炸中被炸死。-- 邪恶博士
*/

FILE *infile;

int main(int argc, char *argv[])
{
char *input;

/* Note to self: remember to port this bomb to Windows and put a
* fantastic GUI on it.
请注意:记得把这个炸弹移植到Windows上,并给它装上一个奇妙的GUI。
*/

/* When run with no arguments, the bomb reads its input lines
* from standard input.
当运行时没有参数,炸弹从标准输入中读取其输入行。
*/
if (argc == 1) {
infile = stdin;
}

/* When run with one argument <file>, the bomb reads from <file>
* until EOF, and then switches to standard input. Thus, as you
* defuse each phase, you can add its defusing string to <file> and
* avoid having to retype it.
* 当运行时有一个参数<file>,炸弹从<file>读取,直到EOF,然后切换到标准输入。
* 因此,当你拆除每个阶段的炸弹时,你可以将其拆除的字符串添加到<file>中,避免重新输入。
* */
else if (argc == 2) {
if (!(infile = fopen(argv[1], "r"))) {
printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]);
exit(8);
}
}

/* You can't call the bomb with more than 1 command line argument.
你不能用1个以上的命令行参数调用炸弹。
*/
else {
printf("Usage: %s [<input_file>]\n", argv[0]);
exit(8);
}

/* Do all sorts of secret stuff that makes the bomb harder to defuse.
做各种秘密的事情,使炸弹更难拆除。*/
initialize_bomb();

printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
printf("which to blow yourself up. Have a nice day!\n");

/* Hmm... Six phases must be more secure than one phase!
嗯... 六个阶段一定比一个阶段更安全!!*/
input = read_line(); /* Get input 获取输入 */
phase_1(input); /* Run the phase 运行阶段 */
phase_defused(); /* Drat! They figured it out!糟了! 他们发现了!
* Let me know how they did it. 让我知道他们是如何做到的。*/
printf("Phase 1 defused. How about the next one?\n");

/* The second phase is harder. No one will ever figure out
第二阶段更难。 没有人能够弄清楚
* how to defuse this...
如何化解这...*/
input = read_line();
phase_2(input);
phase_defused();
printf("That's number 2. Keep going!\n");

/* I guess this is too easy so far. Some more complex code will
* confuse people.
我想到目前为止这太容易了。 一些更复杂的代码会使人们感到困惑
*/
input = read_line();
phase_3(input);
phase_defused();
printf("Halfway there!\n");

/* Oh yeah? Well, how good is your math? Try on this saucy problem!
哦,是吗? 那么,你的数学水平如何? 试试这个俏皮的问题吧!
*/
input = read_line();
phase_4(input);
phase_defused();
printf("So you got that one. Try this one.\n");

/* Round and 'round in memory we go, where we stop, the bomb blows!
我们在记忆中绕来绕去,我们在哪里停下来,炸弹就在哪里爆炸!
*/
input = read_line();
phase_5(input);
phase_defused();
printf("Good work! On to the next...\n");

/* This phase will never be used, since no one will get past the
* earlier ones. But just in case, make this one extra hard.
这个阶段将永远不会被使用,因为没有人会通过前面的阶段。
但为了以防万一,要把这个阶段变得特别难。
*/
input = read_line();
phase_6(input);
phase_defused();

/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha!
哇,他们得到了它! 但是不是有什么东西......不见了? 也许他们忽略了什么? Mua ha ha ha!
*/

return 0;
}

实验环境

本实验使用 WSL2 以及 Ubuntu 20.4 完成,编译器为 VS code。

准备工作

由于实验所给的文件中只有一个bomb.c 和一个可执行文件,而bomb.c 并不能直接编译,因此我们只能通过给出的可执行文件来进行分析。将可执行文件通过反汇编得到完整程序的汇编代码,操作如下:

建议创建一个文件,并将反汇编出的代码输入到该文件中,方便后续查看。

1
linux> objdump -d > objdump.txt

实验解析

phase_1

密钥:The moon unit will be divided into two divisions.

反汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
08048b60 <phase_1>:
8048b60: 83 ec 1c sub $0x1c,%esp
8048b63: c7 44 24 04 c4 a1 04 movl $0x804a1c4,0x4(%esp)
8048b6a: 08
8048b6b: 8b 44 24 20 mov 0x20(%esp),%eax
8048b6f: 89 04 24 mov %eax,(%esp)
8048b72: e8 6d 04 00 00 call 8048fe4 <strings_not_equal>
8048b77: 85 c0 test %eax,%eax
8048b79: 74 05 je 8048b80 <phase_1+0x20>
8048b7b: e8 76 05 00 00 call 80490f6 <explode_bomb>
8048b80: 83 c4 1c add $0x1c,%esp
8048b83: c3 ret

分析如下:

结合源代码可以得出 0x20(%esp) 对应的是 bomb.c 中 read_line读入的字符串的首地址。

阅读phase_1的第 2 到第 6行 ,观察到数据 $0x804a1c40x20(%esp) 均被作为参数传入栈帧中,结合第 7 行的调用 strings_not_equal 函数,可以推测, $0x804a1c4 处存储的也是一个字符串的首地址,并且要与之前读入的字符串进行比较。

查看 strings_not_equal 函数的汇编代码,得知该函数是用于比较两个字符串是否相等的,若相等则返回0,不相等则返回1,返回值均保存在 %eax 中。

继续查看下面的汇编代码,其中第 8 到第 10 行可以得出:当 %eax 为 0 时,跳转到 <phase_1+0x20> 处(即跳过炸弹爆炸);当 %eax 为 1 时,不会发生跳转,此时炸弹爆炸。

因此我们可以得出结论,当我们输入的字符串与 $0x804a1c4 处存储的字符串相等时,就能通过phase_1。

通过 gdb 调试查看 $0x804a1c4 处存储的字符串:

image-20230417093216100

通过 gdb 中的 print 命令,打印出来phase_1的通关密钥。

phase_2

密钥:1 2 4 8 16 32

反汇编代码如下:

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
08048b84 <phase_2>:
8048b84: 56 push %esi
8048b85: 53 push %ebx
8048b86: 83 ec 34 sub $0x34,%esp
8048b89: 8d 44 24 18 lea 0x18(%esp),%eax
8048b8d: 89 44 24 04 mov %eax,0x4(%esp)
8048b91: 8b 44 24 40 mov 0x40(%esp),%eax
8048b95: 89 04 24 mov %eax,(%esp)
8048b98: e8 8e 06 00 00 call 804922b <read_six_numbers>
8048b9d: 83 7c 24 18 01 cmpl $0x1,0x18(%esp) //0x18(%esp)为第一个数字,且必须为1
8048ba2: 74 05 je 8048ba9 <phase_2+0x25>
8048ba4: e8 4d 05 00 00 call 80490f6 <explode_bomb>
8048ba9: 8d 5c 24 1c lea 0x1c(%esp),%ebx
8048bad: 8d 74 24 30 lea 0x30(%esp),%esi
8048bb1: 8b 43 fc mov -0x4(%ebx),%eax
8048bb4: 01 c0 add %eax,%eax //上一个数字的两倍
8048bb6: 39 03 cmp %eax,(%ebx) //第二个数字是第一个数字的两倍,2
8048bb8: 74 05 je 8048bbf <phase_2+0x3b>
8048bba: e8 37 05 00 00 call 80490f6 <explode_bomb>
8048bbf: 83 c3 04 add $0x4,%ebx
8048bc2: 39 f3 cmp %esi,%ebx
8048bc4: 75 eb jne 8048bb1 <phase_2+0x2d> //如果当前这个数的下一个数是第6个输入的值,跳出循环
8048bc6: 83 c4 34 add $0x34,%esp
8048bc9: 5b pop %ebx
8048bca: 5e pop %esi
8048bcb: c3 ret

可以看到phase_2中调用了一个交 read_six_numbers 的函数,反汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0804922b <read_six_numbers>:
804922b: 83 ec 2c sub $0x2c,%esp
804922e: 8b 44 24 34 mov 0x34(%esp),%eax
8049232: 8d 50 14 lea 0x14(%eax),%edx
8049235: 89 54 24 1c mov %edx,0x1c(%esp)
8049239: 8d 50 10 lea 0x10(%eax),%edx
804923c: 89 54 24 18 mov %edx,0x18(%esp)
8049240: 8d 50 0c lea 0xc(%eax),%edx
8049243: 89 54 24 14 mov %edx,0x14(%esp)
8049247: 8d 50 08 lea 0x8(%eax),%edx
804924a: 89 54 24 10 mov %edx,0x10(%esp)
804924e: 8d 50 04 lea 0x4(%eax),%edx
8049251: 89 54 24 0c mov %edx,0xc(%esp)
8049255: 89 44 24 08 mov %eax,0x8(%esp)
8049259: c7 44 24 04 d7 a3 04 movl $0x804a3d7,0x4(%esp)
8049260: 08
8049261: 8b 44 24 30 mov 0x30(%esp),%eax
8049265: 89 04 24 mov %eax,(%esp)
8049268: e8 03 f6 ff ff call 8048870 <__isoc99_sscanf@plt>
804926d: 83 f8 05 cmp $0x5,%eax
8049270: 7f 05 jg 8049277 <read_six_numbers+0x4c>
8049272: e8 7f fe ff ff call 80490f6 <explode_bomb>
8049277: 83 c4 2c add $0x2c,%esp
804927a: c3 ret

从函数的名字我们也不难猜出该函数的作用,读取 6 个数字。注意到 8049259 这一行:

1
8049259:	c7 44 24 04 d7 a3 04 	movl   $0x804a3d7,0x4(%esp)

这里在 0x4(%esp) 里存了一个地址,输出一下看看是什么。

image-20230417094147134

想来这个就是输入函数 sscanf 的格式了,果不其然是输入6个数字。

已知 read_six_numbers 函数的作用,我们再次回到 phase_2的汇编代码中。第 10 行的 cmpl $0x1,0x18(%esp)表明第一个数字必须为 1,否则炸弹直接爆炸。

由第 13 行到第 22 行,我们可以看出,每次将下一个数字的地址给到寄存器 %ebx,将当前数字给到寄存器 %eax ,再将 %eax 中的值做乘 2 处理,最后将 %ebx 地址处的值与 %eax 中的值作比较,相等则继续循环,不等则炸弹爆炸。循环结束的条件是 6 个数字全部比较结束。

因此输入的 6 个数字应该为:1 2 4 8 16 32

phase_3

密钥:0 -418 或 1 -906 或 2 -536 或 3 -639 或 4 0 或 5 -639

反汇编代码如下:

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
08048bcc <phase_3>:
8048bcc: 83 ec 2c sub $0x2c,%esp
8048bcf: 8d 44 24 1c lea 0x1c(%esp),%eax
8048bd3: 89 44 24 0c mov %eax,0xc(%esp)
8048bd7: 8d 44 24 18 lea 0x18(%esp),%eax
8048bdb: 89 44 24 08 mov %eax,0x8(%esp)
8048bdf: c7 44 24 04 e3 a3 04 movl $0x804a3e3,0x4(%esp) //该处地址内容为 "%d %d",说明调用下一步的函数要输入两个整形
8048be6: 08
8048be7: 8b 44 24 30 mov 0x30(%esp),%eax
8048beb: 89 04 24 mov %eax,(%esp)
8048bee: e8 7d fc ff ff call 8048870 <__isoc99_sscanf@plt> //返回值为输入参数的个数,小于2则炸弹爆炸
8048bf3: 83 f8 01 cmp $0x1,%eax
8048bf6: 7f 05 jg 8048bfd <phase_3+0x31>
8048bf8: e8 f9 04 00 00 call 80490f6 <explode_bomb>
8048bfd: 83 7c 24 18 07 cmpl $0x7,0x18(%esp)
8048c02: 77 64 ja 8048c68 <phase_3+0x9c> //如果第一个参数超过 7 ,则爆炸
8048c04: 8b 44 24 18 mov 0x18(%esp),%eax
8048c08: ff 24 85 20 a2 04 08 jmp *0x804a220(,%eax,4)
8048c0f: b8 00 00 00 00 mov $0x0,%eax
8048c14: eb 05 jmp 8048c1b <phase_3+0x4f>
8048c16: b8 e8 01 00 00 mov $0x1e8,%eax
8048c1b: 2d 72 01 00 00 sub $0x172,%eax 减172
8048c20: eb 05 jmp 8048c27 <phase_3+0x5b>
8048c22: b8 00 00 00 00 mov $0x0,%eax
8048c27: 83 c0 67 add $0x67,%eax 加67
8048c2a: eb 05 jmp 8048c31 <phase_3+0x65>
8048c2c: b8 00 00 00 00 mov $0x0,%eax
8048c31: 2d 7f 02 00 00 sub $0x27f,%eax 减27f
8048c36: eb 05 jmp 8048c3d <phase_3+0x71>
8048c38: b8 00 00 00 00 mov $0x0,%eax
8048c3d: 05 7f 02 00 00 add $0x27f,%eax 加27f
8048c42: eb 05 jmp 8048c49 <phase_3+0x7d>
8048c44: b8 00 00 00 00 mov $0x0,%eax
8048c49: 2d 7f 02 00 00 sub $0x27f,%eax 减27f
8048c4e: eb 05 jmp 8048c55 <phase_3+0x89>
8048c50: b8 00 00 00 00 mov $0x0,%eax
8048c55: 05 7f 02 00 00 add $0x27f,%eax 加27f
8048c5a: eb 05 jmp 8048c61 <phase_3+0x95>
8048c5c: b8 00 00 00 00 mov $0x0,%eax
8048c61: 2d 7f 02 00 00 sub $0x27f,%eax 减27f
8048c66: eb 0a jmp 8048c72 <phase_3+0xa6>
8048c68: e8 89 04 00 00 call 80490f6 <explode_bomb>
8048c6d: b8 00 00 00 00 mov $0x0,%eax
8048c72: 83 7c 24 18 05 cmpl $0x5,0x18(%esp) //第一个参数的取值范围为 【0, 5】
8048c77: 7f 06 jg 8048c7f <phase_3+0xb3>
8048c79: 3b 44 24 1c cmp 0x1c(%esp),%eax //第二个参数与 eax作比较,
8048c7d: 74 05 je 8048c84 <phase_3+0xb8> //相等结束,否则爆炸
8048c7f: e8 72 04 00 00 call 80490f6 <explode_bomb>
8048c84: 83 c4 2c add $0x2c,%esp
8048c87: c3 ret

分析:

第 3 ~ 11 行是为scanf函数配置参数以及执行scanf函数的过程,用 print 查看0x804a3e3 中的内容,发现是 "%d %d" ,说明 scanf 读取的是两个整型数字,即密钥是由两个整数组成的。从第 12 行可以看出,当输入的参数个数小于 2 时,炸弹就会爆炸。

第15 ~ 16行,0x18(%esp)中存储的是输入的第一个数,ja 指令意味着无符号指令的大小比较,可以推断出第一个数的取值范围是[0,7]。

第 18 行代码是一个变址寻址的地址跳转,跳转的地址为*(0x804a200+(%eax)*4) ,使用 gdb 输出 0x804a200 地址处的 8 个值(因为输入的第一个参数为0~7,合理推测此处最多只有 8 个值)

1
2
3
(gdb) x/8wx 0x804a220
0x804a220: 0x08048c16 0x08048c0f 0x08048c22 0x08048c2c
0x804a230: 0x08048c38 0x08048c44 0x08048c50 0x08048c5c

可以看出,0x804a220之后存储的 8 个值都是要跳转的地址。跳转后就会执行各种加或减的操作。一系列操作后,第44 ~ 45行代码又将第一个参数的取值范围缩小至[0,5]。第46 ~ 47行代码则是将操作后得到的数与我们输入的第二个参数相比较,相等则函数结束,不相等则炸弹爆炸。

因此要得出所有的答案,只需要在第18行跳转完成后计算出加减操作得到的最后的值,就是我们要输入的第二个数。

phase_4

密钥:11 18

反汇编代码如下:

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
08048cf1 <phase_4>:
8048cf1: 83 ec 2c sub $0x2c,%esp
8048cf4: 8d 44 24 1c lea 0x1c(%esp),%eax
8048cf8: 89 44 24 0c mov %eax,0xc(%esp)
8048cfc: 8d 44 24 18 lea 0x18(%esp),%eax
8048d00: 89 44 24 08 mov %eax,0x8(%esp)
8048d04: c7 44 24 04 e3 a3 04 movl $0x804a3e3,0x4(%esp)
8048d0b: 08
8048d0c: 8b 44 24 30 mov 0x30(%esp),%eax
8048d10: 89 04 24 mov %eax,(%esp)
8048d13: e8 58 fb ff ff call 8048870 <__isoc99_sscanf@plt>
8048d18: 83 f8 02 cmp $0x2,%eax
8048d1b: 75 0d jne 8048d2a <phase_4+0x39> //输入参数不等于2时,炸弹爆炸
8048d1d: 8b 44 24 18 mov 0x18(%esp),%eax //第一个参数
8048d21: 85 c0 test %eax,%eax //
8048d23: 78 05 js 8048d2a <phase_4+0x39> //第一个参数为负数时,炸弹爆炸
8048d25: 83 f8 0e cmp $0xe,%eax
8048d28: 7e 05 jle 8048d2f <phase_4+0x3e> //eax(第一个参数)应该小于等于 14,否则爆炸
8048d2a: e8 c7 03 00 00 call 80490f6 <explode_bomb>
8048d2f: c7 44 24 08 0e 00 00 movl $0xe,0x8(%esp) 调用函数func,第三个参数是 14,第二个参数是 0,第一个参数为输入的第一个值
8048d36: 00
8048d37: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
8048d3e: 00
8048d3f: 8b 44 24 18 mov 0x18(%esp),%eax
8048d43: 89 04 24 mov %eax,(%esp)
8048d46: e8 3d ff ff ff call 8048c88 <func4>
8048d4b: 83 f8 12 cmp $0x12,%eax //比较返回值与18的大小,返回值不等于18,则炸弹爆炸
8048d4e: 75 07 jne 8048d57 <phase_4+0x66>
8048d50: 83 7c 24 1c 12 cmpl $0x12,0x1c(%esp) //比较第二个输入值与18的大小,不等于则炸弹爆炸
8048d55: 74 05 je 8048d5c <phase_4+0x6b>
8048d57: e8 9a 03 00 00 call 80490f6 <explode_bomb>
8048d5c: 83 c4 2c add $0x2c,%esp
8048d5f: c3 ret

有第 7 行的 0x804a3e3地址处所存的内容以及第 12 行将 scanf 函数的返回值与 2 作比较,可以得出输入的是两个参数,且参数类型为整形。

image-20230422153859139

由第 14 ~ 18 行分析可得,输入的第一个参数的取值范围应该是 [0, 14]。第 20 ~ 26 行,主函数调用 func4 函数,并将(x, 0, 14)作为函数的三个参数传入(x 表示我们输入的第一个参数)。以下是 func4 函数的反汇编代码:

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
08048c88 <func4>:A
8048c88: 83 ec 1c sub $0x1c,%esp
8048c8b: 89 5c 24 14 mov %ebx,0x14(%esp)
8048c8f: 89 74 24 18 mov %esi,0x18(%esp)
8048c93: 8b 44 24 20 mov 0x20(%esp),%eax //a = x
8048c97: 8b 54 24 24 mov 0x24(%esp),%edx //b = 0
8048c9b: 8b 74 24 28 mov 0x28(%esp),%esi //c = 14
8048c9f: 89 f1 mov %esi,%ecx
8048ca1: 29 d1 sub %edx,%ecx //d = c - b
8048ca3: 89 cb mov %ecx,%ebx
8048ca5: c1 eb 1f shr $0x1f,%ebx //d进行逻辑右移31位,取出其符号位,e = d>>31
8048ca8: 01 d9 add %ebx,%ecx //d = d + e
8048caa: d1 f9 sar %ecx //对d进行符号位拓展
8048cac: 8d 1c 11 lea (%ecx,%edx,1),%ebx //f = (b + d)
8048caf: 39 c3 cmp %eax,%ebx //比较f与a的大小
8048cb1: 7e 17 jle 8048cca <func4+0x42> //如果 f <= a 则发生跳转
8048cb3: 8d 4b ff lea -0x1(%ebx),%ecx //f - 1 -> ecx
8048cb6: 89 4c 24 08 mov %ecx,0x8(%esp) //f-1 为第三个参数
8048cba: 89 54 24 04 mov %edx,0x4(%esp) //b 为第二个参数
8048cbe: 89 04 24 mov %eax,(%esp) //a 为第一个参数
8048cc1: e8 c2 ff ff ff call 8048c88 <func4>
8048cc6: 01 c3 add %eax,%ebx //eax保存func4的返回值
8048cc8: eb 19 jmp 8048ce3 <func4+0x5b>
8048cca: 39 c3 cmp %eax,%ebx
8048ccc: 7d 15 jge 8048ce3 <func4+0x5b> //如果 f >= a 发生跳转
8048cce: 89 74 24 08 mov %esi,0x8(%esp)
8048cd2: 8d 53 01 lea 0x1(%ebx),%edx
8048cd5: 89 54 24 04 mov %edx,0x4(%esp)
8048cd9: 89 04 24 mov %eax,(%esp)
8048cdc: e8 a7 ff ff ff call 8048c88 <func4>
8048ce1: 01 c3 add %eax,%ebx
8048ce3: 89 d8 mov %ebx,%eax
8048ce5: 8b 5c 24 14 mov 0x14(%esp),%ebx
8048ce9: 8b 74 24 18 mov 0x18(%esp),%esi
8048ced: 83 c4 1c add $0x1c,%esp
8048cf0: c3 ret

这个函数有点绕,大概是做一个二分,具体的内容写在注释里,自己可以用笔模拟下这个函数在干嘛,可以推断出C语言代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int func(int a, int b, int c){
int d = c - b;
int e = (d >> 31) & 1;
d = (d + e) >> 1;
int f = b + d;
if(a < f)
{
int g = func4(a, b, f-1);
return g + f;
}
else if(a > f)
{
int g = func4(a , f+1, c);
return g + f;
}
else
return a;
}

func4 函数中返回后,第 27 行代码将函数的返回值与 0x12 作比较,不相等时炸弹直接爆炸,相等则继续下面的判断。第 29 行代码显示将输入的第二个值与 0x12 作比较,当不相等时发生爆炸,相等则安全退出函数。由推断出的 func4 的C语言代码可尝试在输入[0, 14]时,函数的不同返回值。只有当输入值为 11 时,返回值才是18,即与 0x12 相等。由此便可推出密钥为 11 18

phase_5

密钥:bcejno 或 cdeijn 或 cejlmn(只列出了极少的情况,全部可能应该有几万种可能)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
08048d60 <phase_5>:
8048d60: 53 push %ebx
8048d61: 83 ec 18 sub $0x18,%esp
8048d64: 8b 5c 24 20 mov 0x20(%esp),%ebx
8048d68: 89 1c 24 mov %ebx,(%esp)
8048d6b: e8 5b 02 00 00 call 8048fcb <string_length>//获取输入字符串的长度
8048d70: 83 f8 06 cmp $0x6,%eax
8048d73: 74 05 je 8048d7a <phase_5+0x1a> //输入字符串的长度不等于 6 ,炸弹爆炸
8048d75: e8 7c 03 00 00 call 80490f6 <explode_bomb>
8048d7a: ba 00 00 00 00 mov $0x0,%edx
8048d7f: b8 00 00 00 00 mov $0x0,%eax
8048d84: 0f be 0c 03 movsbl (%ebx,%eax,1),%ecx
8048d88: 83 e1 0f and $0xf,%ecx
8048d8b: 03 14 8d 40 a2 04 08 add 0x804a240(,%ecx,4),%edx
8048d92: 83 c0 01 add $0x1,%eax
8048d95: 83 f8 06 cmp $0x6,%eax
8048d98: 75 ea jne 8048d84 <phase_5+0x24>
8048d9a: 83 fa 41 cmp $0x41,%edx
8048d9d: 74 05 je 8048da4 <phase_5+0x44>
8048d9f: e8 52 03 00 00 call 80490f6 <explode_bomb>
8048da4: 83 c4 18 add $0x18,%esp
8048da7: 5b pop %ebx
8048da8: c3 ret

分析前 8 行代码可知,输入值为一个字符串,且长度一定要是 6,否则炸弹直接爆炸。接下来就是一个循环结构,遍历输入的字符串,并将当前遍历的字节与 oxf 相与得到一个值,得到的值将作为首地址在 0x804a240 处的整数数组的下标,并将访问的值进行累加,若和为 0x41 ,则该字符串就是密钥,否则炸弹爆炸。下面是该函数的C语言代码:

1
2
3
4
5
6
7
8
9
10
char s[6] = 输入的字符串
int a[16] = 首地址在0x804a220处的数组,由于和0xf相与,故下标最大值为15,数组长度为16
int sum = 0; //累加和
for(int i = 0; i < 6; ++i)
{
int pos = (int)s[i] & 0xf;
sum += a[pos];
}
if(sum != 0x41)
explode_bomb();

查看 0x804a240 地址处的长度为 16 的整数数组的全部值:

image-20230409222538168

每个整数值对应的字母大小写如下:

1
2
3
4
0x804a240 <array.2999>: 		2-p\P       10-a\q\A\Q      6-b\r\B\Q       1-c\s\C\S
0x804a250 <array.2999+16>: 12-d\t\D\T 16-E\U\e\u 9-F\V\f\v 3-g\w\G\W
0x804a260 <array.2999+32>: 4-H\X\h\x 7-I\Y\i\y 14-j\z\J\Z 5-k\K
0x804a270 <array.2999+48>: 11-L\l 8-M\m 15-N\n 13-o\O

全部不重复的情况如下(没有任何参考价值,尽量不要尝试算出所有的情况):

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

一共有117种可能

phase_6

密钥:4 2 3 6 5 1 或 4 2 6 3 5 1

反汇编代码如下:

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
08048da9 <phase_6>:
8048da9: 56 push %esi
8048daa: 53 push %ebx
8048dab: 83 ec 44 sub $0x44,%esp
8048dae: 8d 44 24 10 lea 0x10(%esp),%eax
8048db2: 89 44 24 04 mov %eax,0x4(%esp)
8048db6: 8b 44 24 50 mov 0x50(%esp),%eax
8048dba: 89 04 24 mov %eax,(%esp)
8048dbd: e8 69 04 00 00 call 804922b <read_six_numbers>
8048dc2: be 00 00 00 00 mov $0x0,%esi /*
8048dc7: 8b 44 b4 10 mov 0x10(%esp,%esi,4),%eax
8048dcb: 83 e8 01 sub $0x1,%eax
8048dce: 83 f8 05 cmp $0x5,%eax
8048dd1: 76 05 jbe 8048dd8 <phase_6+0x2f>
8048dd3: e8 1e 03 00 00 call 80490f6 <explode_bomb>
8048dd8: 83 c6 01 add $0x1,%esi
8048ddb: 83 fe 06 cmp $0x6,%esi
8048dde: 74 33 je 8048e13 <phase_6+0x6a>
8048de0: 89 f3 mov %esi,%ebx
8048de2: 8b 44 9c 10 mov 0x10(%esp,%ebx,4),%eax 6个数字都要小于等于6,并且大于等于1
8048de6: 39 44 b4 0c cmp %eax,0xc(%esp,%esi,4) 且不能两两相等
8048dea: 75 05 jne 8048df1 <phase_6+0x48> 否则炸弹爆炸
8048dec: e8 05 03 00 00 call 80490f6 <explode_bomb>
8048df1: 83 c3 01 add $0x1,%ebx
8048df4: 83 fb 05 cmp $0x5,%ebx
8048df7: 7e e9 jle 8048de2 <phase_6+0x39>
8048df9: eb cc jmp 8048dc7 <phase_6+0x1e> */
8048dfb: 8b 52 08 mov 0x8(%edx),%edx
8048dfe: 83 c0 01 add $0x1,%eax
8048e01: 39 c8 cmp %ecx,%eax //使ecx的值与输入值相等
8048e03: 75 f6 jne 8048dfb <phase_6+0x52>
8048e05: 89 54 b4 28 mov %edx,0x28(%esp,%esi,4)
8048e09: 83 c3 01 add $0x1,%ebx
8048e0c: 83 fb 06 cmp $0x6,%ebx
8048e0f: 75 07 jne 8048e18 <phase_6+0x6f>
8048e11: eb 1c jmp 8048e2f <phase_6+0x86>
8048e13: bb 00 00 00 00 mov $0x0,%ebx
8048e18: 89 de mov %ebx,%esi
8048e1a: 8b 4c 9c 10 mov 0x10(%esp,%ebx,4),%ecx
8048e1e: b8 01 00 00 00 mov $0x1,%eax
8048e23: ba 3c c1 04 08 mov $0x804c13c,%edx
8048e28: 83 f9 01 cmp $0x1,%ecx //输入的数字大于 1 发生跳转
8048e2b: 7f ce jg 8048dfb <phase_6+0x52>
8048e2d: eb d6 jmp 8048e05 <phase_6+0x5c>
8048e2f: 8b 5c 24 28 mov 0x28(%esp),%ebx j0 -> ebx
8048e33: 8b 44 24 2c mov 0x2c(%esp),%eax j1 -> eax
8048e37: 89 43 08 mov %eax,0x8(%ebx) j1 -> ebx+8
8048e3a: 8b 54 24 30 mov 0x30(%esp),%edx j2 -> edx
8048e3e: 89 50 08 mov %edx,0x8(%eax) j2 -> eax+8
8048e41: 8b 44 24 34 mov 0x34(%esp),%eax j3 -> eax
8048e45: 89 42 08 mov %eax,0x8(%edx) j3 -> edx+8
8048e48: 8b 54 24 38 mov 0x38(%esp),%edx j4 -> edx
8048e4c: 89 50 08 mov %edx,0x8(%eax) j4 -> eax+8
8048e4f: 8b 44 24 3c mov 0x3c(%esp),%eax j5 -> eax
8048e53: 89 42 08 mov %eax,0x8(%edx) j5 -> edx+8
8048e56: c7 40 08 00 00 00 00 movl $0x0,0x8(%eax) 0 -> eax+8
8048e5d: be 05 00 00 00 mov $0x5,%esi esi = 5
8048e62: 8b 43 08 mov 0x8(%ebx),%eax eax = j1
8048e65: 8b 10 mov (%eax),%edx
8048e67: 39 13 cmp %edx,(%ebx) j1与j0比较
8048e69: 7d 05 jge 8048e70 <phase_6+0xc7>
8048e6b: e8 86 02 00 00 call 80490f6 <explode_bomb>
8048e70: 8b 5b 08 mov 0x8(%ebx),%ebx
8048e73: 83 ee 01 sub $0x1,%esi
8048e76: 75 ea jne 8048e62 <phase_6+0xb9>
8048e78: 83 c4 44 add $0x44,%esp
8048e7b: 5b pop %ebx
8048e7c: 5e pop %esi
8048e7d: c3 ret

由第 9 行代码可以得出我们一共输入 6 个整型数字。分析后面的代码可得,输入的 6 个值的取值范围为[1, 6],且不能两两相等。第 28 ~ 44 行是一个循环结构,用于比较当前数字与后一个数字的大小,前一个数一定不能小于后一个数,否则炸弹直接爆炸。其中要进行比较的数组为以 0x804c13c 为首地址的数组。

但是在输出该地址的内容后,发现这并不是一个数组,而是一个结构体,每个结构体中有3个元素,一个记录数值、一个记录node编号,一个存放下一个node的地址(可以把node理解为链表),共占12个字节。

存储node的方式是:按照读入的6个数(假定为a[i]),将编号为a[i]的node存储到第i个位置。然后从栈帧中的存储的第一个node开始,每个node与其后一个node中的数值进行比较,若后大于前,则会引爆炸弹。以下是打印出的 6 个node的信息。

1
2
3
4
5
6
0x804c13c <node1>:    99      1       134529352       
0x804c148 <node2>: 476 2 134529364
0x804c154 <node3>: 208 3 134529376
0x804c160 <node4>: 740 4 134529388
0x804c16c <node5>: 135 5 134529400
0x804c178 <node6>: 208 6

由此可以的出密钥应该是 4 2 3 6 5 1 或 4 2 6 3 5 1(因为6 与 3中的值是相等的,所以可以调换顺序)。

答案

至此,我们以及得出了六个关卡的正确密钥,如下:

1
2
3
4
5
6
The moon unit will be divided into two divisions.
1 2 4 8 16 32
4 0
11 18
bcejno
4 2 3 6 5 1

输入以上六个密钥,便可拆除炸弹。

但是,不知道你有没有注意到在该程序的源代码中的结尾处有一行注释,如下:

1
2
3
4
/* Wow, they got it!  But isn't something... missing?  Perhaps
* something they overlooked? Mua ha ha ha ha!
哇,他们得到了它! 但是不是有什么东西......不见了? 也许他们忽略了什么? Mua ha ha ha!
*/

这就十分可疑,再结合源代码中每个关卡在输出正确答案提示之前都调用了同一个函数——phase_defused();,下面的这个部分就是探寻隐藏关卡的部分。

隐藏关卡

密钥: 107

phase_defused函数的反汇编代码如下:

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
0804927b <phase_defused>:
804927b: 81 ec 8c 00 00 00 sub $0x8c,%esp
8049281: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8049287: 89 44 24 7c mov %eax,0x7c(%esp)
804928b: 31 c0 xor %eax,%eax
804928d: 83 3d cc c3 04 08 06 cmpl $0x6,0x804c3cc //该地址处存储的是通过的个数,如果没有全部通过,则不会进入隐藏关卡
8049294: 75 72 jne 8049308 <phase_defused+0x8d>
8049296: 8d 44 24 2c lea 0x2c(%esp),%eax
804929a: 89 44 24 10 mov %eax,0x10(%esp) //第一个参数
804929e: 8d 44 24 28 lea 0x28(%esp),%eax
80492a2: 89 44 24 0c mov %eax,0xc(%esp) //第二个参数
80492a6: 8d 44 24 24 lea 0x24(%esp),%eax
80492aa: 89 44 24 08 mov %eax,0x8(%esp) //第三个参数
80492ae: c7 44 24 04 e9 a3 04 movl $0x804a3e9,0x4(%esp) //0x804a3e9 "%d %d %s",输入参数为两个整形个一个字符串,只有第三关和第四关符合条件,测试后应该是在第四关输入特殊字符串,进入隐藏关卡
80492b5: 08
80492b6: c7 04 24 d0 c4 04 08 movl $0x804c4d0,(%esp)
80492bd: e8 ae f5 ff ff call 8048870 <__isoc99_sscanf@plt>
80492c2: 83 f8 03 cmp $0x3,%eax //输入参数一定要是3个,否则退出函数
80492c5: 75 35 jne 80492fc <phase_defused+0x81>
80492c7: c7 44 24 04 f2 a3 04 movl $0x804a3f2,0x4(%esp) //0x804a3f2: "DrEvil"
80492ce: 08
80492cf: 8d 44 24 2c lea 0x2c(%esp),%eax
80492d3: 89 04 24 mov %eax,(%esp)
80492d6: e8 09 fd ff ff call 8048fe4 <strings_not_equal>//调用比较字符串的函数,说明该地址处的字符串有关键作用
80492db: 85 c0 test %eax,%eax //eax为0表示两个字符串相等,否则不相等
80492dd: 75 1d jne 80492fc <phase_defused+0x81>
80492df: c7 04 24 b8 a2 04 08 movl $0x804a2b8,(%esp) //0x804a2b8: "Curses, you've found the secret phase!"
80492e6: e8 15 f5 ff ff call 8048800 <puts@plt> //输出字符串
80492eb: c7 04 24 e0 a2 04 08 movl $0x804a2e0,(%esp) //0x804a2e0: "But finding it and solving it are quite different..."
80492f2: e8 09 f5 ff ff call 8048800 <puts@plt> //输出字符串
80492f7: e8 d3 fb ff ff call 8048ecf <secret_phase> //进入隐藏关卡
80492fc: c7 04 24 18 a3 04 08 movl $0x804a318,(%esp) //0x804a318: "Congratulations! You've defused the bomb!"
8049303: e8 f8 f4 ff ff call 8048800 <puts@plt>//输出字符串
8049308: 8b 44 24 7c mov 0x7c(%esp),%eax
804930c: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8049313: 74 05 je 804931a <phase_defused+0x9f>
8049315: e8 b6 f4 ff ff call 80487d0 <__stack_chk_fail@plt>
804931a: 81 c4 8c 00 00 00 add $0x8c,%esp
8049320: c3 ret
8049321: 90 nop
8049322: 90 nop
8049323: 90 nop
8049324: 90 nop
8049325: 90 nop
8049326: 90 nop
8049327: 90 nop
8049328: 90 nop
8049329: 90 nop
804932a: 90 nop
804932b: 90 nop
804932c: 90 nop
804932d: 90 nop
804932e: 90 nop
804932f: 90 nop

分析

在找到 phase_defused 函数的反汇编之后,不难找到几个有助于我们分析关键部分,如下:

  • 第 6 行将地址 0x804c3cc 处的值与 6 作比较;
  • 第 8 ~ 18 行调用 __isoc99_sscanf@plt 函数,其中第 14 和第 18 行为分析的关键点;
  • 第 20 行将 0x804a3f2 地址处的值传给寄存器;
  • 第 24 行调用比较字符串函数;
  • 第 27 ~ 30 行输出两次地址中的值;
  • 第 31 行调用名为 secret_phase 的函数;
  • 第 32 ~ 33 行输出地址 0x804a318 中的值。

根据以上关键点,我们可以得出以下结论:

  • 第 6 行, 地址 0x804c3cc 中的值应该是当前通过的数目,只有通关数为 6 时才迈出了进入隐藏关卡的第一步;

  • 第 8 ~ 18 行,调用输入函数,且由第 18 行得知输入的参数的个数应该是 3 个;由第 14 行可知,三个参数的类型为前两个是整形,第三个是字符串类型。

    因为一共要输入两个整数和一个字符串,且每次在解决一个关卡后都会调用该函数。由此可得,前两个整数应该是某一个关卡的密钥。而在我们得出得答案中,只有第 3 关和第 4 关的密钥是两个整型数字,经过测试后,隐藏关卡的实际入口在第4关

    image-20230418194052316

  • 输出第 20 行地址处的值为:DrEvil

image-20230418194200376

  • 比较函数用于将第 20 行地址处的字符串与输入的第三个参数作比较,根据后面两行的代码可以分析出当输入值与设置值不相等时,会直接打印祝贺信息并退出该函数,只有当两个字符串相等时,我们操迈出了进入隐藏关卡的第二步。
  • 第 27 ~ 30 行,一共输出了两个地址存储的值,打印出来后发现并没有什么实质价值,只是成功输入字符串的隐藏值。

image-20230418195114104

  • 第 31 行,被调用的函数就是隐藏关卡,反汇编代码如下:
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
08048ecf <secret_phase>:
8048ecf: 53 push %ebx
8048ed0: 83 ec 18 sub $0x18,%esp
8048ed3: e8 45 02 00 00 call 804911d <read_line>
8048ed8: c7 44 24 08 0a 00 00 movl $0xa,0x8(%esp) //第一个参数10
8048edf: 00
8048ee0: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp) //第二个参数0
8048ee7: 00
8048ee8: 89 04 24 mov %eax,(%esp) //第三个参数为输入值
8048eeb: e8 f0 f9 ff ff call 80488e0 <strtol@plt>//转化为十进制数
8048ef0: 89 c3 mov %eax,%ebx
8048ef2: 8d 40 ff lea -0x1(%eax),%eax
8048ef5: 3d e8 03 00 00 cmp $0x3e8,%eax
8048efa: 76 05 jbe 8048f01 <secret_phase+0x32> //输入值-1 小于等于 0x3e8的话跳转,炸弹不爆炸
8048efc: e8 f5 01 00 00 call 80490f6 <explode_bomb>
8048f01: 89 5c 24 04 mov %ebx,0x4(%esp)
8048f05: c7 04 24 88 c0 04 08 movl $0x804c088,(%esp)
8048f0c: e8 6d ff ff ff call 8048e7e <fun7>
8048f11: 83 f8 03 cmp $0x3,%eax //函数fun7的返回值与 3 作比较,等于 3 炸弹不爆炸
8048f14: 74 05 je 8048f1b <secret_phase+0x4c>
8048f16: e8 db 01 00 00 call 80490f6 <explode_bomb>
8048f1b: c7 04 24 f8 a1 04 08 movl $0x804a1f8,(%esp) //0x804a1f8: "Wow! You've defused the secret stage!"
8048f22: e8 d9 f8 ff ff call 8048800 <puts@plt>//输出字符串
8048f27: e8 4f 03 00 00 call 804927b <phase_defused> //返回调用函数
8048f2c: 83 c4 18 add $0x18,%esp
8048f2f: 5b pop %ebx
8048f30: c3 ret
8048f31: 90 nop
8048f32: 90 nop
8048f33: 90 nop
8048f34: 90 nop
8048f35: 90 nop
8048f36: 90 nop
8048f37: 90 nop
8048f38: 90 nop
8048f39: 90 nop
8048f3a: 90 nop
8048f3b: 90 nop
8048f3c: 90 nop
8048f3d: 90 nop
8048f3e: 90 nop
8048f3f: 90 nop

进入secret_phase,函数先读取我们输入的密钥,然后调用了c语言的strtol函数,这个函数将字符串转为长整型数,其中参数0xa意味着将其转为十进制。接下来函数会比较经转换后的数自减1后与0x3e8(十进制为1000)的无符号数大小,这意味着我们输入的数必须在[1, 1001]中。接着,函数会调用一个递归函数func7,参数为0x804c088和我们输入的数,func7的伪代码如下

1
2
3
4
5
6
7
8
int func7(int* k, int n)
{
if(k <= 0) return -1;
int kn = *k;
if(kn == n) return 0;
else if(kn < n) return 2*func7(k+2, n) + 1;
else return 2*func7(k+1, n);
}

又由以下两个指令知,func7(, 我们输入的数)的返回值必须为3,否则将引爆炸弹。

1
2
8048ef2: 83 f8 03              cmp    $0x3,%eax
8048ef5: 74 05 je 8048efc <secret_phase+0x4c>

观察内存中地址为0x804c088的内容,是一棵二叉树:第一个地址存储数据,第二、三个地址存储下子结点的地址。于是我们只要分析func7的行为,并根据0x804c088中的内容,即可确定要输入的数。

首先,函数要求func7最终返回3。要构造一个3,根据func7的特性,可以这样构造$2\cdot(2\cdot(0+1))+1$ ,这意味着,最后一次要返回 0,倒数第二次(或第二次)要返回 $2 \cdot func7(k+2, n) + 1$,倒数第三次(或第一次)要返回 $2 \cdot func7(k+2, n) + 1$ 。或者说,要返回的值是二叉树查找时比较的次数。要满足这样的关系,假如我们把第1~3次比较的数设为k1、k2、k3,那么,我们输入的数n要满足的条件是n > k1且n > k2且n == k3。要找到k3,查找0x804c088处的内存即可,经查找,为0x6b,转换为十进制为107,即为密钥。

打印 0x804c088 地址处的值可得:

image-20230418201622434

所以,隐藏关卡的密钥为 107

下图为实验结果:

image-20230418205634637

总结

写到此处,我们的炸弹也就拆完了。在拆除炸弹之后,有关于x86-64的汇编指令以及gdb的一些调试方法我们都已经有所了解。总的来说,这个实验还是十分有趣的,通过剧情让我们了解汇编指令,值得自己独立研究一下,会有不小的收获。