本文是CS:APP学习笔记的一部分:

相关的代码都放在了GitHub下了:RayZhang13/CSAPP-Labs

关于学习资源和视频等问题可以参考第一次写的Data Lab开头中提及的相关问题。

这次的实验是Bomb Lab,也就是拆弹实验,听起来就很刺激~

顾名思义,那就是拆错了就会BOOM,然后扣分。按照CMU官方PDF的说法,那就是每炸一次扣半分,总共可以炸满20分,但是对于我们这些Self-Study的同学就没这么多要求了hhh

准备

对应课程

这次的Bomb Lab作业,如果是自学,在B站课程中,应该大致需要完成P5~P9的学习,大致理解汇编语言的语法等。

P.S. 关于汇编语法,主要有Intel和AT&T两类。本质上并没有太大区别,主要是源操作数和目的操作数的顺序恰好相反。就像教授所讲的,就像开车换方向舵的感觉一样,如果只熟悉一种,一开始感觉会有点怪怪的。

之前在本专业学习“微机原理”8086/8088汇编语言时,我也是主要接触了Intel语法,由于课本和讲课都使用了AT&T的语法,那我也就强行换一下“舵”呗,小问题咯。

课程文件

相关的作业还是在CMU的官网上,相同位置:

Lab Assignments

在Bomb Lab一栏里面,我们可以查看相关文件,例如:

下载后并解压的文件如图所示:

这次的文件目录也是非常的简洁。

使用环境

和上期同样,我们还是使用了Arch Linux环境,配合GDB等程序调试工具:

使用建议

文件说明

文件夹里提供了一个可执行文件和一个bomb.c文件。

打开bomb.c文件,我们看到了一个main函数的相关代码:

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

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

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

return 0;
}

里面大致描述了程序的相关功能,但是很清晰的看到phase_1phase_6等方法的代码并没有给出,其他的c文件也没有给出,但是给出了最终生成的可执行文件bomb

P.S. 换句话说,这里降低了难度,给出了main函数的相关代码,好让我们更加专注于各个题目的具体实现,不用去研究长长的main函数到底做了啥。

总而言之,这里总共有6道题,每道题会调用phase_1phase_6的相关方法,并传入标准输入内容,如果我们输入的内容符合程序的要求,就会通过这一题,也就是“拆弹成功”,反之炸弹就会爆炸,界面如下:

第一题瞎输了一个'a',触发了爆炸

而至于如何避免爆炸,那自然就需要我们对这个可执行文件进行反编译处理,通过阅读汇编程序来推测程序的相关意图。

交互方式

由于我们这类的菜鸡可能经常做错题目害得炸弹爆炸,而每次炸完就得从头运行程序,从头输入。这样的简单重复劳动造成了大量的emotional damage,而且一不小心操作手滑还可能导致半路爆炸。

因此,这里还贴心的为我们准备了文件输入的交互方式,我们可以事先将每一道题目的答案存在文件里,通过读取文件的方式输入,省事又简单:

通过key.txt文件输入

题目

objdump是GCC工具中的反汇编工具。首先我们使用objdump工具对bomb可执行文件进行反汇编,并将它写入到文件中:

1
[root@archlinux ~] objdump -d ./bomb > bomb-disassemble

我们看到main函数如下:

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
000000000400da0 <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

每次调用read_line时,程序都将标准输入的字符串存入内存,并将内存首地址存储到rdi中:

1
2
call   40149e <read_line>	;调用read_line
mov %rax,%rdi ;将输入字符串首地址存入rdi

Phase1

1
2
3
4
5
6
/* 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");

第一题中,我们读入了输入的字符串,将参数传入了phase_1中,我们在刚才的反编译结果中找到phase_1

1
2
3
4
5
6
7
8
9
0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi ;将0x402400送入esi
400ee9: e8 4a 04 00 00 call 401338 <strings_not_equal> ;调用strings_not_equal
400eee: 85 c0 test %eax,%eax ;查看返回值eax是否为0
400ef0: 74 05 je 400ef7 <phase_1+0x17> ;是0跳转0x400ef7
400ef2: e8 43 05 00 00 call 40143a <explode_bomb> ;否则触发炸弹,调用explode_bomb,爆炸
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 ret

至于strings_not_equal是做什么的,大致根据名字猜测是比较两个字符串的功能,恰好调用时rdi中存储的是标准输入中的字符串首地址,大致猜测第二个参数存储在rsi中,也就是刚送入的0x402400,作为另一个需要比较的字符串的首地址。如果相同就正常跳转执行,否则就会调用explode_bomb,炸弹爆炸。这也给了我们提示,在GDB调试中可以提前给explode_bomb打上断点,在爆炸前可以有效的截停程序(每日一个偷鸡小妙招)。

strings_not_equal的反编译结果如下:

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
0000000000401338 <strings_not_equal>:
401338: 41 54 push %r12
40133a: 55 push %rbp
40133b: 53 push %rbx
40133c: 48 89 fb mov %rdi,%rbx ;字符串1首地址传至rbx
40133f: 48 89 f5 mov %rsi,%rbp ;字符串2首地址传至rbp
401342: e8 d4 ff ff ff call 40131b <string_length> ;调用string_length查询字符串1长度
401347: 41 89 c4 mov %eax,%r12d ;字符串1长度返回值传至r12d
40134a: 48 89 ef mov %rbp,%rdi
40134d: e8 c9 ff ff ff call 40131b <string_length> ;调用string_length查询字符串2长度
401352: ba 01 00 00 00 mov $0x1,%edx ;edx置0x1
401357: 41 39 c4 cmp %eax,%r12d ;比较字符串1和字符串2长度
40135a: 75 3f jne 40139b <strings_not_equal+0x63> ;不同则跳转0x40139b
40135c: 0f b6 03 movzbl (%rbx),%eax ;将字符串1首字符字节拷贝到eax
40135f: 84 c0 test %al,%al ;检查是否是中止\n符号
401361: 74 25 je 401388 <strings_not_equal+0x50> ;是则跳转0x401388表示相等,否则继续比较
401363: 3a 45 00 cmp 0x0(%rbp),%al ;比较字符串2和字符串1字符
401366: 74 0a je 401372 <strings_not_equal+0x3a> ;相等跳转0x401372
401368: eb 25 jmp 40138f <strings_not_equal+0x57> ;无条件跳转0x40138f
40136a: 3a 45 00 cmp 0x0(%rbp),%al ;比较字符串2和字符串1字符
40136d: 0f 1f 00 nopl (%rax)
401370: 75 24 jne 401396 <strings_not_equal+0x5e> ;不同则跳转0x401396,跳出循环
401372: 48 83 c3 01 add $0x1,%rbx ;字符串1指针++
401376: 48 83 c5 01 add $0x1,%rbp ;字符串2指针++
40137a: 0f b6 03 movzbl (%rbx),%eax ;将字符串1字符字节拷贝到eax
40137d: 84 c0 test %al,%al ;检查是否是中止\n符号
40137f: 75 e9 jne 40136a <strings_not_equal+0x32> ;否,则跳转0x40136a,循环
401381: ba 00 00 00 00 mov $0x0,%edx ;循环正常结束,edx置0,准备返回
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

大致分析了一下确定,首先这个方法比较了两个字符串的长度,如果长度相同再进行逐字节的循环比较,直到读取到\0而停止。

写作C语言大致是如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void phase_1(char* input){
int val = strings_not_equal(input, 0x402400);
if(val != 0) explode_bomb();
return;
}
int strings_not_equal(char* str1, char* str2){
if(strlen(str1) != strlen(str2)) return 1;
while(*str1 != '\0'){
if(*str1 != *str2){
return 1;
}
str1++;
str2++;
}
return 0;
}

那么这个在内存中和输入比较的字符串又是多少呢?我们在phase_1的反编译程序中看见它的地址是0x402400,因此我们使用GDB获取在0x402400地址开始的字符串:

也就是Border relations with Canada have never been better.

第一题的答案也就是这个了。

Phase2

1
2
3
4
5
6
/* 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");

首先我们看看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
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> ;调用read_six_numbers,读取6个数字?
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) ;判断第一个数字是否是1
400f0e: 74 20 je 400f30 <phase_2+0x34> ;是,跳转0x400f30
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 ;rbx上一个数字赋值给eax
400f1a: 01 c0 add %eax,%eax ;上一个数字x2
400f1c: 39 03 cmp %eax,(%rbx) ;上一个数字x2后和rbx地址上的数字比较
400f1e: 74 05 je 400f25 <phase_2+0x29> ;如果是两倍关系,跳转0x400f25
400f20: e8 15 05 00 00 call 40143a <explode_bomb> ;否则炸弹爆炸
400f25: 48 83 c3 04 add $0x4,%rbx ;rbx+=4,移动到下一个数字的地址
400f29: 48 39 eb cmp %rbp,%rbx ;比较rbx和rbp
400f2c: 75 e9 jne 400f17 <phase_2+0x1b> ;不相等,还没有移动到边界,跳转0x400f17,继续循环
400f2e: eb 0c jmp 400f3c <phase_2+0x40> ;否则说明循环到头了,跳转0x400f3c
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx ;第二个数地址&a[1]赋值给rbx
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp ;边界地址&a[6]赋给rbp
400f3a: eb db jmp 400f17 <phase_2+0x1b> ;无条件跳转400f17
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 ret

如果有兴趣也可以看看read_six_numbers是怎么实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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> ;调用sscanf
40148f: 83 f8 05 cmp $0x5,%eax ;成功读取的数字数量和5比较
401492: 7f 05 jg 401499 <read_six_numbers+0x3d> ;超过5个,成功读取,跳转0x401499
401494: e8 a1 ff ff ff call 40143a <explode_bomb> ;否则引爆炸弹
401499: 48 83 c4 18 add $0x18,%rsp
40149d: c3 ret

read_six_numbers中,可以看到调用了sscanf方法,其大致参数如下:

1
int sscanf(const char *str, const char *format, ...)

其中str为要解析的字符串,format为读取指定格式的数据的字符串,接下来的若干参数都是需要读入的需要保存的数据的地址,返回值为成功转换的参数的个数。例如:

1
2
3
int a, b, c, d;
char str[100] = "1 2 3 4";
sscanf(str, "%d %d %d %d", &a, &b, &c, &d); //通过格式将1, 2, 3, 4写入a, b, c, d所在地址空间,返回值为4

read_six_numbers的汇编代码中,传入了两个参数,第一个参数在rdi中,是标准输入字符串的首地址,而rsi中传入了phase_2的栈顶地址,猜测是数组的首地址。调用sscanfrdi为第一个参数同上就是我们刚从标准输入获得的字符串的首地址,也是就是strrsi寄存器内存储的应该是第二个参数,也就是format的首地址0x4025c3,我们使用GDB将format打印出来:

清晰的可以看到是"%d %d %d %d %d %d",确实是读入6个整型数字。那存储的数字放置在哪里呢?如果sscanf需要读入6个整数,那6个数字的地址带上2个字符串,总共就会产生8个参数。

寄存器 描述
rdi 传递第一个参数
rsi 传递第二个参数
rdx 传递第三个参数或者第二个返回值
rcx 传递第四个参数
r8 传递第五个参数
r9 传递第六个参数

我们知道x86-64 规定只有6个寄存器来存参数,所以还需要使用栈上的空间来存储其他参数。

通过大致阅读上述的代码,我们不难发现汇编程序首先以当时的phase_2的栈顶为基准(我们这里将其记为p),随后使用lea,将参数rdx定为p ,将参数rcx定为p + 0x4,将参数r8定为p + 0x8,将参数r9定为p + 0xc,表示各个数字存储的地址,很明显的也可以看出各地址间有4个字节的差距,刚好是一个整数的大小。剩下的两个参数,则存储在了read_six_numbers的栈帧内部,我们记read_six_numbers的栈顶指针是sp,则在sp位置存储了p + 0x10,在sp + 0x8的位置存储了p + 0x14,至此为止所有的参数都齐了。画图可以绘作如下:

综上,phase_2的功能总结就是,传入标准输入字符串,调用read_six_numbers,在内部调用sscanf读取字符串中6个整数,并写入到phase_2的栈空间中,六个数的首地址从pp + 0x14连续摆放,结束后如果随后sscanf读出的整数数量不足6个,则引爆炸弹。随后分析a[0]是否是1,不是则引爆炸弹,接下来遍历剩下个元素,判断每个元素是否是上一个元素的两倍,如果不是就引爆炸弹,如果是就继续,然后炸弹被解除。

整理成代码的话应该可以写作如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void phase_2(char* input){
int* p, p_end;
int a[6];
read_six_numbers(input, a);
if(a[0] != 0) explode_bomb();
p = &a[1];
p_end = &a[6];
do{
if(*(p - 1) * 2 != *p) explode_bomb();
p++;
} while(p != p_end);
return;
}
void read_six_numbers(char* input, int* a){
int num = sscanf(input, "%d %d %d %d %d %d", &a[0], &a[1], &a[2], &a[3], &a[4], &a[5]);
if(num <= 5) explode_bomb();
return;
}

因此我们的输入应当是六个数:1 2 4 8 16 32

Phase3

1
2
3
4
5
6
/* 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");

首先我们看看phase_3的反编译代码:

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
0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx ;rcx赋&a[1]
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx ;rdx赋&a[0]
400f51: be cf 25 40 00 mov $0x4025cf,%esi ;esi赋"%d %d"
400f56: b8 00 00 00 00 mov $0x0,%eax
400f5b: e8 90 fc ff ff call 400bf0 <__isoc99_sscanf@plt> ;调用sscanf读取两数字
400f60: 83 f8 01 cmp $0x1,%eax ;读取数量和1比较
400f63: 7f 05 jg 400f6a <phase_3+0x27> ;读满两个就跳转0x400f6a继续执行
400f65: e8 d0 04 00 00 call 40143a <explode_bomb> ;否则引爆炸弹
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp) ;比较a[0]和7
400f6f: 77 3c ja 400fad <phase_3+0x6a> ;如果a[0](无符号数格式)大于7,就跳转0x400fad,引爆炸弹
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax ;eax赋a[0]
400f75: ff 24 c5 70 24 40 00 jmp *0x402470(,%rax,8) ;case语句,根据rax的值跳转到不同的分枝
400f7c: b8 cf 00 00 00 mov $0xcf,%eax ;如果a[0]取0跳转至此,eax赋0xcf
400f81: eb 3b jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe
400f83: b8 c3 02 00 00 mov $0x2c3,%eax ;如果a[0]取2跳转至此,eax赋0x2c3
400f88: eb 34 jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe
400f8a: b8 00 01 00 00 mov $0x100,%eax ;如果a[0]取3跳转至此,eax赋0x100
400f8f: eb 2d jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe
400f91: b8 85 01 00 00 mov $0x185,%eax ;如果a[0]取4跳转至此,eax赋0x185
400f96: eb 26 jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe
400f98: b8 ce 00 00 00 mov $0xce,%eax ;如果a[0]取5跳转至此,eax赋值0xce
400f9d: eb 1f jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe
400f9f: b8 aa 02 00 00 mov $0x2aa,%eax ;如果a[0]取6跳转至此,eax取0x2aa
400fa4: eb 18 jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe
400fa6: b8 47 01 00 00 mov $0x147,%eax ;如果a[0]取7跳转至此,eax取0x147
400fab: eb 11 jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe
400fad: e8 88 04 00 00 call 40143a <explode_bomb> ;炸弹爆炸
400fb2: b8 00 00 00 00 mov $0x0,%eax ;炸弹爆炸后,eax赋0
400fb7: eb 05 jmp 400fbe <phase_3+0x7b> ;跳转0x400fbe
400fb9: b8 37 01 00 00 mov $0x137,%eax ;如果a[0]取1跳转至此,eax赋0x137
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax ;eax和a[1]比较
400fc2: 74 05 je 400fc9 <phase_3+0x86> ;如果相等,跳转0x400fc9
400fc4: e8 71 04 00 00 call 40143a <explode_bomb> ;否则爆炸
400fc9: 48 83 c4 18 add $0x18,%rsp
400fcd: c3 ret

头部的部分和phase_2其实相当的类似,也是调用sscanf方法,先不管别的,我们先把format字符串给它爬出来,根据它的首地址存放在rsi中是0x4025cf,我们根据GDB调试有:

也就是说输入格式是"%d %d",考虑到传入rdi第一参数str是标准输入字符串首地址,rdx第三参数是rsp + 0x8rcx第四参数是rsp + 0xc,大致猜测调用sscanf中包含以下参数:

1
sscanf(input, "%d %d", &a[0], &a[1]);

a[0]地址是栈顶地址rsp向栈底8字节位置,a[1]地址紧随其后,是栈顶地址rsp向栈底12字节位置。接下来的代码同样熟悉,如果读取数字小于两个,就引爆炸弹。

接下来是一个很典型的case语句结构,首先判断了a[0]作为无符号数是否大于7,换句话说也就是如果a[0]作为有符号数如果落在区间[0, 7]外,这个知识点在视频中也有提及,具体在视频 57:16位置。

接下来使用jmp *0x402470(,%rax,8) ,根据rax产生的偏移量跳转到了索引表结构上。我们通过GDB将索引表的各个分支代表的数字进行输出:

依据这张表我们绘制出索引表:

a[0]取值 跳转地址
0 0x400f7c
1 0x400fb9
2 0x400f83
3 0x400f8a
4 0x400f91
5 0x400f98
6 0x400f9f
7 0x400fa6

在不同的分枝内部,都对eax进行了不同的赋值,跳出循环后,又对a[1]eax进行了比较,如果不一致就炸弹爆炸,否则拆弹成功。

写成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
void phase_3(char* intput){
int a[2];
int num = sscanf(input, "%d %d", &a[0], &a[1]);
if(num <= 1) explode_bomb();
switch(a[0]){
case 0:
a[0] = 0xcf;
break;
case 1:
a[0] = 0x137;
break;
case 2:
a[0] = 0x2c3;
break;
case 3:
a[0] = 0x100;
break;
case 4:
a[0] = 0x185;
break;
case 5:
a[0] = 0xce;
break;
case 6:
a[0] = 0x2aa;
break;
case 7:
a[0] = 0x147;
break;
default:
explode_bomb();
a[0] = 0;
break;
}
if(a[0] != a[1]) explode_bomb();
return;
}

因此,我们应当输入两组整数,以空格间隔开,而且答案并不唯一,a[0][0, 7]区间内均可,但是a[1]则遵循case语句中的数值需要对号入座,以下数对均可:

a[0] a[1]
0 207
1 311
2 707
3 256
4 389
5 206
6 682
7 327

Phase4

1
2
3
4
5
/* 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");

我们观察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
000000000040100c <phase_4>:
40100c: 48 83 ec 18 sub $0x18,%rsp
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx ;rcx赋&a[1]
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx ;rcx赋&a[0]
40101a: be cf 25 40 00 mov $0x4025cf,%esi ;esi赋"%d %d"
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 ;检查填充的参数个数是否是2
40102c: 75 07 jne 401035 <phase_4+0x29> ;不相等则跳转0x401035,引爆炸弹
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp) ;比较14和a[0]
401033: 76 05 jbe 40103a <phase_4+0x2e> ;如果a[0](转无符号数)<=14,跳转0x40103a
401035: e8 00 04 00 00 call 40143a <explode_bomb> ;否则触发爆炸
40103a: ba 0e 00 00 00 mov $0xe,%edx ;edx赋14
40103f: be 00 00 00 00 mov $0x0,%esi ;esi赋0
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi ;edi赋a[0]
401048: e8 81 ff ff ff call 400fce <func4> ;调用func4
40104d: 85 c0 test %eax,%eax ;检查返回值是否是0
40104f: 75 07 jne 401058 <phase_4+0x4c> ;则不等,跳转0x401058,触发爆炸
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) ;比较0和a[1]
401056: 74 05 je 40105d <phase_4+0x51> ;相等则跳转0x40105d
401058: e8 dd 03 00 00 call 40143a <explode_bomb> ;否则触发爆炸
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3 ret

接下来,我们来看看func4方法内部是如何工作的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0000000000400fce <func4>:
400fce: 48 83 ec 08 sub $0x8,%rsp
400fd2: 89 d0 mov %edx,%eax ;给eax赋值edx
400fd4: 29 f0 sub %esi,%eax ;eax -= esi
400fd6: 89 c1 mov %eax,%ecx ;给ecx赋值eax
400fd8: c1 e9 1f shr $0x1f,%ecx ;ecx逻辑右移31位,相当于取符号位
400fdb: 01 c8 add %ecx,%eax ;eax += ecx
400fdd: d1 f8 sar %eax ;算术右移指令
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx ;ecx = rax + rsi
400fe2: 39 f9 cmp %edi,%ecx ;比较ecx和edi
400fe4: 7e 0c jle 400ff2 <func4+0x24> ;如果小于等于就跳转0x400ff2
400fe6: 8d 51 ff lea -0x1(%rcx),%edx ;否则edx = rcx - 1
400fe9: e8 e0 ff ff ff call 400fce <func4> ;递归调用func4
400fee: 01 c0 add %eax,%eax ;eax *= 2
400ff0: eb 15 jmp 401007 <func4+0x39> ;无条件跳转0x401007
400ff2: b8 00 00 00 00 mov $0x0,%eax ;eax置0
400ff7: 39 f9 cmp %edi,%ecx ;比较ecx和edi
400ff9: 7d 0c jge 401007 <func4+0x39> ;如果大于或者等于就跳转0x401007
400ffb: 8d 71 01 lea 0x1(%rcx),%esi ;esi = rcx + 1
400ffe: e8 cb ff ff ff call 400fce <func4> ;递归调用func4
401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax ;eax = rax + rax + 1
401007: 48 83 c4 08 add $0x8,%rsp
40100b: c3 ret

我们对phase_4func4对程序稍加分析,即可看出func4中使用了rdirsirdx寄存器内部传入的数据,也就是func4传入了三个参数,并通过eax传出了一个参数。

前面的若干句指令其实可以翻译作如下伪代码,实际上其基本功能就是计算a / 2

1
2
3
4
5
6
7
8
//x->rdi, y->rsi, z->rdx
int func4(int x, int y, int z){
int a = z - y;
int b = a >>> 31;
a += b;
a >>= 1;
//........
}

正常的算术逻辑右移,其实更加实际来说是向下取整,而非向零取整,3通过算术右移能得到1,但是-3只能得到-2。为此如果是负数,我们需要在算术右移前先对原数添加偏置常量1。在这里程序通过逻辑右移31位的方式获取到了符号位,在算数右移前先加在原数上,很巧妙的解决了问题。因此这段代码实现的结果也就是:

1
2
3
4
5
//x->rdi, y->rsi, z->rdx
int func4(int x, int y, int z){
int a = (z - y) / 2;
//......
}

接下来的代码我们直接逐行转成C语言:

1
2
3
4
5
6
7
8
9
10
11
int func4(int x, int y, int z){
int a = y + (z - y) / 2;
if(a <= x){
if(a >= x) return 0;
y = a + 1;
return 2 * func4(x, y, z) + 1;
} else {
z = a - 1;
return 2 * func4(x, y, z);
}
}

有点像二分噢,稍稍优化一下写法:

1
2
3
4
5
6
7
8
9
int func4(int val, int lo, int hi){
int mid = lo + (hi - lo) / 2;
if(mid == val) return 0;
if(mid < val){
return 2 * func4(val, mid + 1, hi) + 1;
} else {
return 2 * func4(val, lo, mid - 1);
}
}

那我们接下来再回头观察phase_4是如何实现的,在调用func4时传入的参数分别为a[0]014。而我们作为保证必须使得返回值为0否则会触发爆炸,同时第二个输入参数a[1]还必须为0,大致的C语言实现如下:

1
2
3
4
5
6
7
8
void phase_4(char* input){
int a[2];
int val = sscanf(input, "%d %d", &a[0], &a[1]);
if(val != 2 || a[0] < 0 || a[0] > 14) explode_bomb();
val = func4(a[0], 0, 14);
if(val != 0 || a[1] != 0) explode_bomb();
return;
}

我们对a[0]在区间[0, 14]内部进行枚举,满足条件的有:0137

以上四个数任意一个配上a[1] = 0即为结果。

Phase5

1
2
3
4
5
/* 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");

废话不说,先看phase_5的反编译结果:

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
0000000000401062 <phase_5>:
401062: 53 push %rbx
401063: 48 83 ec 20 sub $0x20,%rsp
401067: 48 89 fb mov %rdi,%rbx ;给rbx赋rdi
40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax ;从此处获取8字节的金丝雀值
401071: 00 00
401073: 48 89 44 24 18 mov %rax,0x18(%rsp) ;将金丝雀值放置在缓冲区旁边
401078: 31 c0 xor %eax,%eax ;eax置0
40107a: e8 9c 02 00 00 call 40131b <string_length> ;计算标准输入字符串长度
40107f: 83 f8 06 cmp $0x6,%eax ;比较长度是否是6
401082: 74 4e je 4010d2 <phase_5+0x70> ;是,则跳转0x4010d2
401084: e8 b1 03 00 00 call 40143a <explode_bomb> ;否则引爆炸弹
401089: eb 47 jmp 4010d2 <phase_5+0x70> ;引爆后跳转0x4010d2
40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx ;将rbx+rax位置一字节拷贝到ecx
40108f: 88 0c 24 mov %cl,(%rsp) ;将rax低1字节写入到栈顶
401092: 48 8b 14 24 mov (%rsp),%rdx ;再写入rdx
401096: 83 e2 0f and $0xf,%edx ;edx&=15,取低四位
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx ;取出0x4020b0+rdx位置的1字节给edx
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1) ;将rdx低1字节写入rsp+rax+16位置
4010a4: 48 83 c0 01 add $0x1,%rax ;rax += 1
4010a8: 48 83 f8 06 cmp $0x6,%rax ;rax和6比较
4010ac: 75 dd jne 40108b <phase_5+0x29> ;不等则跳转0x40108b,开始循环,如果满6则跳出
4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp) ;rsp+22位置写入两字节的0
4010b3: be 5e 24 40 00 mov $0x40245e,%esi ;给esi赋值0x40245e
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi ;给rdi赋值rsp+16
4010bd: e8 76 02 00 00 call 401338 <strings_not_equal> ;比较esi和rsi为首地址的两字符串
4010c2: 85 c0 test %eax,%eax ;结果是否是0,即是否相同
4010c4: 74 13 je 4010d9 <phase_5+0x77> ;是则跳转0x4010d9
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> ;跳转0x4010d9
4010d2: b8 00 00 00 00 mov $0x0,%eax ;eax置0
4010d7: eb b2 jmp 40108b <phase_5+0x29> ;跳转0x40108b,进入循环
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> ;相等,说明栈空间未被破坏,跳转0x4010ee
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

看到fs寄存器,还有__stack_chk_fail等命令,应该是对栈内存进行了破坏检查,而在fs:0x28位置正好存储了对应的“金丝雀值”,结束时进行检测,如果发生了变化,则认为栈空间遭到了破坏。详情见视频 53:14位置。

phase_5首先检查了标准输入字符串是否长度为6,不满足直接爆炸。接下来,进入一个6次的循环,程序使用了一个循环依次读入了标准输入input6个字节,将取出的字节和15取与,作为偏移量取出0x4024b0为首地址的一个字符串中的一个字符,并将其填入到一块rsp + 16为地址开头的空间中,在后续观察中,我们发现rsp + 16指向的应当也是一块字符数组空间,循环结束后,我们还在数组空间末尾rsp + 22位置补上了一个'\0'。在最后还将这段字符串和0x40245e地址开始的字符串进行比较,如果不一致就爆炸,否则拆弹成功。

转换成C语言大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
void phase_5(char* input){
char a[7];
int i, val;
if(strlen(input) != 6) explode_bomb();
for(i = 0; i < 6; i++){
a[i] = 0x4024b0[input[i] & 15];
}
a[6] = '\0';
val = strings_not_equal(a, 0x4025e);
if(val != 0) explode_bomb();
return;
}

我们通过GDB获取到0x4024b0地址和0x40245e地址的字符串:

字符串分别为:"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?""flyers"

栈内存空间具体可以画作如下:

根据以上关系我们建立映射:

Val Val
input[0]&15 9
input[1]&15 15
input[2]&15 14
input[3]&15 5
input[4]&15 6
input[5]&15 7

换句话说本题答案也不唯一,输入字符串的各个字符的ASCII码和15取与只要在对应范围即可。

例如)/.%&'就是一个可能的答案。

Phase6

1
2
3
4
5
/* 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();

先看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
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 ;暂存栈顶地址到r13
401103: 48 89 e6 mov %rsp,%rsi
401106: e8 51 03 00 00 call 40145c <read_six_numbers> ;读取6个数字,存储在栈帧连续空间中
40110b: 49 89 e6 mov %rsp,%r14 ;暂存栈顶地址到r14
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d ;r12置0
401114: 4c 89 ed mov %r13,%rbp ;将r13中存储地址写入rbp
401117: 41 8b 45 00 mov 0x0(%r13),%eax ;eax置指针4字节数据
40111b: 83 e8 01 sub $0x1,%eax ;eax -= 1
40111e: 83 f8 05 cmp $0x5,%eax ;比较eax和5
401121: 76 05 jbe 401128 <phase_6+0x34> ;(unsigned int)eax<=5则跳转0x401128
401123: e8 12 03 00 00 call 40143a <explode_bomb> ;否则引爆炸弹
401128: 41 83 c4 01 add $0x1,%r12d ;r12++
40112c: 41 83 fc 06 cmp $0x6,%r12d ;r12是否满6?
401130: 74 21 je 401153 <phase_6+0x5f> ;满了跳出外侧大循环,跳转0x401153
401132: 44 89 e3 mov %r12d,%ebx ;给ebx赋值r12
401135: 48 63 c3 movslq %ebx,%rax ;给rax赋值ebx
401138: 8b 04 84 mov (%rsp,%rax,4),%eax ;eax = *(rsp + rax * 4),eax赋值第rax个元素
40113b: 39 45 00 cmp %eax,0x0(%rbp) ;比较当前位置元素和第rbx个元素
40113e: 75 05 jne 401145 <phase_6+0x51> ;不等则跳转0x401145
401140: e8 f5 02 00 00 call 40143a <explode_bomb> ;否则引爆炸弹
401145: 83 c3 01 add $0x1,%ebx ;ebx++
401148: 83 fb 05 cmp $0x5,%ebx ;比较ebx和5
40114b: 7e e8 jle 401135 <phase_6+0x41> ;若ebx<=5,则跳转0x401135,开始内侧循环
40114d: 49 83 c5 04 add $0x4,%r13 ;r13 += 4
401151: eb c1 jmp 401114 <phase_6+0x20> ;跳转0x401114,开始外侧循环
401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi ;rsi指向数组末尾边界地址
401158: 4c 89 f0 mov %r14,%rax ;rax存储数组首地址
40115b: b9 07 00 00 00 mov $0x7,%ecx ;给ecx赋7
401160: 89 ca mov %ecx,%edx ;给edx赋7
401162: 2b 10 sub (%rax),%edx ;edx = 7 - *(rax)
401164: 89 10 mov %edx,(%rax) ;回写数组中,视作*(rax) = 7 - *(rax)
401166: 48 83 c0 04 add $0x4,%rax ;rax后移到下一个整数,地址+4
40116a: 48 39 f0 cmp %rsi,%rax ;判断rax是否已经指向了数组边界
40116d: 75 f1 jne 401160 <phase_6+0x6c> ;还没有,则继续循环,跳转0x401160
40116f: be 00 00 00 00 mov $0x0,%esi ;esi置0
401174: eb 21 jmp 401197 <phase_6+0xa3> ;跳转0x401197
401176: 48 8b 52 08 mov 0x8(%rdx),%rdx ;rdx=rdx->next
40117a: 83 c0 01 add $0x1,%eax ;eax++
40117d: 39 c8 cmp %ecx,%eax ;比较eax和ecx
40117f: 75 f5 jne 401176 <phase_6+0x82> ;不相等则跳转0x401176
401181: eb 05 jmp 401188 <phase_6+0x94> ;否则跳转0x401188
401183: ba d0 32 60 00 mov $0x6032d0,%edx
401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2) ;将rdx写入到rsp + 2 * rsi + 32位置
40118d: 48 83 c6 04 add $0x4,%rsi ;rsi += 4
401191: 48 83 fe 18 cmp $0x18,%rsi ;比较rsi是否达到24
401195: 74 14 je 4011ab <phase_6+0xb7> ;达到则跳出循环,跳转0x4011ab
401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx ;ecx = *(rsp + rsi)
40119a: 83 f9 01 cmp $0x1,%ecx ;所指元素和1比较
40119d: 7e e4 jle 401183 <phase_6+0x8f> ;如果小于等于1则跳转0x401183
40119f: b8 01 00 00 00 mov $0x1,%eax ;eax赋值1
4011a4: ba d0 32 60 00 mov $0x6032d0,%edx ;edx赋值0x6032d0
4011a9: eb cb jmp 401176 <phase_6+0x82> ;跳转0x401176
4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx ;rbx指向address数组首元素
4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax ;rax指向下一元素
4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi ;rsi指向address数组结尾边缘
4011ba: 48 89 d9 mov %rbx,%rcx ;rcx赋值rbx
4011bd: 48 8b 10 mov (%rax),%rdx ;rdx赋值下一元素节点的代表的节点的地址
4011c0: 48 89 51 08 mov %rdx,0x8(%rcx) ;将前一节点的next修改为下一元素节点的地址
4011c4: 48 83 c0 08 add $0x8,%rax ;rax += 8,位移到下一格
4011c8: 48 39 f0 cmp %rsi,%rax ;检查rax指针是否到达边界
4011cb: 74 05 je 4011d2 <phase_6+0xde> ;如果到达,跳出循环,转0x4011d2
4011cd: 48 89 d1 mov %rdx,%rcx ;否则,将rcx也往后一格
4011d0: eb eb jmp 4011bd <phase_6+0xc9> ;进行循环,转0x4011bd
4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx) ;将数组最后一个元素代表的节点next置null
4011d9: 00
4011da: bd 05 00 00 00 mov $0x5,%ebp ;ebp置5
4011df: 48 8b 43 08 mov 0x8(%rbx),%rax ;rax = rbx -> next
4011e3: 8b 00 mov (%rax),%eax ;eax = rax -> val
4011e5: 39 03 cmp %eax,(%rbx) ;比较当前元素节点值rbx.val和下一元素节点值rax.val
4011e7: 7d 05 jge 4011ee <phase_6+0xfa> ;如果大于等于,转0x4011ee
4011e9: e8 4c 02 00 00 call 40143a <explode_bomb> ;否则爆炸
4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx ;rbx = rbx -> next
4011f2: 83 ed 01 sub $0x1,%ebp ;ebp--
4011f5: 75 e8 jne 4011df <phase_6+0xeb> ;如果不为0,继续循环,转0x4011df
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

前面部分其实也是相当的眼熟,功能和phase_2头部代码类似,也是使用sscanf读取6个整数,并写入到phase_6的栈顶数组位置。read_six_numbers内部的实现已经看过了就不细说了。

后面的代码场面一度非常混乱,从0x4011140x401151观察,大致能发现两层嵌套的循环代码。

对着瞎写了一点,大致代码可能是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//a->rsp, t->r13, i->r12, j->rbx
void phase_6(char* input){
int a[6];
int* t = a;
int i, j;
read_six_numbers(input, a);
i = 0;
while(true){
if((unsigned int)(*t - 1) > 5) explode_bomb();
if(++i == 6) break;
j = i;
while(true){
if(*(a + j) == *t) explode_bomb();
if(++j > 5) break;
}
t++;
}
//.....
}

稍加整理:

1
2
3
4
5
6
7
8
9
10
11
12
void phase_6(char* input){
int a[6];
int i, j;
read_six_numbers(input, a);
for(i = 0; i <= 5; i++){
if((unsigned int)(a[i] - 1) > 5) explode_bomb();
for(j = i + 1; j <= 5; j++){
if(a[i] == a[j]) explode_bomb();
}
}
//.......
}

大致理解就是,我们读入6个数字,而这几个数字必须要满足减一后转无符号数比5小,换句话说就是原数必须落在[1, 6]的范围内,不满足条件就会触发爆炸。随后,程序对数字进行了检测,内部的数字两两间不能相同。稍微一想,那就是1~6里面每个数字各占一个。

我们接下来看0x4011530x40116d构成的循环,其实就是遍历每个数字,然后用7减掉原数再填回去。简单的写作C那就是:

1
2
3
4
5
6
7
void phase_6(char* input){
//........
for(i = 0; i < 6; i++){
a[i] = 7 - a[i];
}
//........
}

注意到整体变换过的数组内部的元素其实范围还是落在1~6的区间内部的,还是互不相同。

接下来我们重点观察0x40116f0x4011a9部分。看到mov 0x8(%rdx),%rdx这样的代码,猜测可能在这块地址存在链表,地址存放在结构体向后偏移8字节的位置,且链表的首节点地址应该是0x6032d0

我们使用GDB查看0x6032d0位置的链表结构:

我们看到结构体分为三个部分,第一个部分是存储的值,第二部分是节点的序号,第三部分是next指针的地址。总共存在6个节点。

大致操作大致可以描述为遍历数组内部的数字,然后根据取出的数字决定向后移动到next的次数。例如如果取出的数字是1,那就不进入循环,直接取首节点;如果数字是2,那就进入循环,取出第2个节点,以此类推…

随后,我们将节点的地址依次存入rsp + 0x20起始的一块内存区域中,8字节间隔开。整理成C伪代码那就是:

1
2
3
4
5
6
7
8
void phase_6(char* input){
long address[6];
//.......
for(i = 0; i <= 5; i++){
address[i] = &node_{a[i]};
}
//.....
}

接下来,在0x4011ab0x4011d2一段,程序按照数组的顺序,重新将各个节点相连,最后将最后一个元素代表的节点的nextnull。写作C为:

1
2
3
4
5
6
7
8
void phase_6(char* input){
//.....
for(i = 0; i < 5; i++){
(node*)address[i] -> next = address[i + 1];
}
(node*)address[5] -> next = nullptr;
//.....
}

最后一段代码就是从数组的头部节点开始向后依次便利检查,是否前一节点的值大于等于后一节点的值,不满足就会触发炸弹,检查完毕即通过。

1
2
3
4
5
6
7
void phase_6(char* input){
//.....
for(int i = 0; i < 5; i++){
if((node*)address[i] -> val < (node*)address[i + 1] -> val) explode_bomb();
}
return;
}

根据这点信息,我们重排后的链表结果应该是:

node3(924) -> node4(691) -> node5(477) -> node6(443) -> node1(322) -> node2(168)

因此数组a应该是{3, 4, 5, 6, 1, 2}

倒推到7 - a[i]变换前应该是{4, 3, 2, 1, 6, 5}

所以答案是4 3 2 1 6 5

最后我们拆除了所有的炸弹,哭死,难度真不是盖的: