第二个 Lab 就比较有趣了。

这一个 Lab 的任务是,我们得到了一个 bomb 炸弹程序,这个炸弹程序有 66 个 phase,每个 phase 都会读取我们的输入,判断我们的输入是否符合要求,如果正确这个 phase 的炸弹就会被拆除,否则炸弹就会爆炸。我们需要借助各种工具,对程序进行反汇编等等,获得能够通过程序判断的输入。

准备

首先我们通过阅读下发的 bomb.c,得知我们的输入是通过一个 read_line 函数输入,字符串地址被拷贝给 input 变量,这个变量作为参数传递给 phase_x 函数进行判断(如果不正确会在这个函数中调用变量让炸弹爆炸),成功后运行 phase_defused 进行拆除,然后输出成功通知。

bomb.c 第 72 至第 77 行

因此,我们应该找到 phase_x 函数,通过这个函数的内容来寻找正确的答案。

我们通过运行下面的指令,获得反汇编的程序,以及程序的字符串表。因为内存的最低地址是从 0x400000 开始的,因此在查阅字符串表时需要将查阅地址减去 0x400000

1
2
objdump -d bomb > bomb.s
strings -t x bomb > strings.txt

下图是整数寄存器的示意图,可能会对我们有所帮助。如函数的六个参数依次为 %rdi, %rsi, %rdx, %rcx, %r8, %r9,而被调用者保存的寄存器为 %rbx, %rbp, %r12~%r15,我们可以认为在函数调用后这些寄存器的值不变。

Integer registers

Phase 1

我们查看 bomb.sphase_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
400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal>
400eee: 85 c0 test %eax,%eax
400ef0: 74 05 je 400ef7 <phase_1+0x17>
400ef2: e8 43 05 00 00 callq 40143a <explode_bomb>
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 retq

注意,我们是在 main 函数中通过 phase_1(input) 调用的,因此 input 字符串的地址已经在寄存器 %rdi 中了。

400ee4 指令中,我们将 $0x402400 移动到了 %esi 寄存器。在下一个指令中,我们调用了 strings_not_equal 函数,从字面意思不难判断这是用于判断字符串相等的函数。而寄存器 %rsi 是函数第二个参数的寄存器,因此我们可以明白这里是将 input 参数和 $0x402400 处的字符串作比较。结合下面的指令可以发现,如果返回值为 00(猜测是比较成功),那么就会直接返回。

因此,我们只需要找到 $0x402400 处的字符串即可。通过查找 strings.txt 的字符串表,并且将地址减去 0x400000 得到答案:

1
Border relations with Canada have never been better.

于是,第一个炸弹拆除!

image-20230902205014721

Phase 2

继续来看 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 callq 40145c <read_six_numbers>
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp)
400f0e: 74 20 je 400f30 <phase_2+0x34>
400f10: e8 25 05 00 00 callq 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 callq 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 retq

一开始程序就将栈指针减去了 0x28,然后将地址作为第二个参数调用了函数 read_six_numbers,显然这里是开了一个小数组。

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 callq 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 callq 40143a <explode_bomb>
401499: 48 83 c4 18 add $0x18,%rsp
40149d: c3 retq

很容易看到调用了 sscanf 函数,这个函数的作用是从字符串中读取变量。注意到 phase_2read_six_numbers 都没有修改寄存器 %rdi,也就是说我们的 input 一直是第一个参数,这里也会是 sscanf 的第一个参数,也就是要从 input 里读取变量。那么我们只要确定剩余几个参数的值和顺序,就能知道是怎么读取数据的了!

401480 指令将地址 $0x4025c3 赋值给了 %esi 作为 sscanf 第二个参数,也就是 sscanf 的格式化字符串,查阅字符串表得到格式化字符串的内容是:%d %d %d %d %d %d,不出所料是读取了六个 int 类型的整数。

根据寄存器使用规范,跟着的四个参数应该是使用的 %rdx, %rcx, %r8, %r9 四个寄存器,可以看到在对应的值依次是 %rsi, 0x4 + %rsi, 0x8 + %rsi, 0xc(%rsi)。应该还有两个参数无法用寄存器传递,那么应该是存放在栈中,找到 (%rsp), 0x8(%rsp) 对应的值,分别是 0x10 + %rsi, 0x14(%rsi)。也就是说,我们的 input 中六个数字依次被放进了 phase_2 函数的小数组的前 66 位中。为了方便起见,我们后面用 s[i]s[i] 表示这六个数中第 ii 个数。

让我们回到 phase_2 函数。在调用 read_six_numbers 结束后,将 (%rsp)s[0]s[0]) 和 11 做了比较,如果不相等就爆炸,所以显然第一个数应该是 11。然后第一个数正确的话,程序跳转到 400f30

接下来,程序将 s[1],s[6]s[1], s[6] 的地址分别存进了 %rbx, %rbp 中,然后跳转到了 400f17,这条指令中,-0x4(%rbx) 也就是 s[0]s[0] 的值被拷贝到了 %eax 并累加了其自身一次,得到的 2s[0]2s[0] 被用于和 s[1]s[1] 比较,如果不相等就爆炸,因此第二个数正确的值应该是 22

随后程序跳转到 400f25,将 %rbx 的值加上了 44(现在指向了 s[2]s[2]),然后与地址 s[6]s[6] 比较,如果不相等就跳转回 400f17。显然,这里出现了一个循环,%rbx 就是一个循环变量,从 s[1]s[1] 循环到 s[5]s[5]。因此很容易类推得后面的数字。

综上,这里的正确答案就是:

1
1 2 4 8 16 32

第二个炸弹,拆除!

image-20230902212215935

Phase 3

汇编代码太长了……我们慢一点看。

1
2
3
4
5
6
7
8
9
10
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 callq 400bf0 <__isoc99_sscanf@plt>
400f60: 83 f8 01 cmp $0x1,%eax
400f63: 7f 05 jg 400f6a <phase_3+0x27>
400f65: e8 d0 04 00 00 callq 40143a <explode_bomb>

哈,又是熟悉的 sscanf!可以看到,依然是用我们的 input 字符串作为输入,而第二个参数的格式化字符串是 %d %d,也就是要输入两个整数,存储地址是第三、四个参数:0x8(%rsp), 0xc(%rsp)

然后对比了 sscanf 的返回值,如果返回值小于等于 11,那么说明输入有误,直接爆炸!

我们继续看!

1
2
3
4
5
0000000000400f43 <phase_3>:
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 jmpq *0x402470(,%rax,8)

嗯,如果第一个数比 77 大,那么就跳转到 400fad。我们看一下这个地址对应的指令,发现是 callq 40143a <explode_bomb> 也就是直接爆炸。因此我们第一个参数必须要是 0077 之间的。然后程序会将第一个参数的值拷贝给 %eax,然后跳转到 (0x402470 + 8 * %rax) 处存储的地址。

这里真的卡了我一下,因为这种跳转到被存储进内存的地址的操作让我措手不及。没办法,只能上 gdb 了。

我们使用 gdb 查看当第一个数分别为 070\cdots 7 时,对应内存中的地址是多少:

image-20230902214922431

结合汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0000000000400f43 <phase_3>:
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 callq 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 callq 40143a <explode_bomb>

可以发现,不论第一个数是几,对应的指令都会将 %eax 赋值成某个数,然后跳转到 400fbe。在这个位置,程序会将 %eax 与输入的第二个数相比较,如果不相等就会直接爆炸。如果相等就会跳转到 400fc9 准备返回。

因此这个题目的答案其实很多,输入的第一个数不同,对应合法的第二个数也不同,最简单的答案就是:

1
0 207

image-20230902215605716

Phase 4

下一弹!

1
2
3
4
5
6
7
8
9
10
11
12
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 callq 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 callq 40143a <explode_bomb>

Phase 4 最开始的代码和 Phase 3 几乎一样,通过 sscanf 函数从我们的 input 中读入两个正整数,存进 0x8(%rsp)0xc(%rsp),如果数量不对就爆炸。

然后,程序判断第一个输入是否小于 0xe(也就是 1414),如果成立就跳转 40103a,没有跳转就会爆炸。

1
2
3
4
5
000000000040100c <phase_4>:
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 callq 400fce <func4>

下面,程序以第一个数的值、001414 依次作为三个参数调用了函数 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
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 callq 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 callq 400fce <func4>
401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401007: 48 83 c4 08 add $0x8,%rsp
40100b: c3 retq

func4 这个函数……我能看懂它的含义,但是我并不是很能理解这个东西算出来的有什么实际意义……

很显然,看到函数中两处 callq 400fce <func4> 调用它自身,很容易明白这是一个递归函数。

400fd2400fdf 这一段的含义比较明显。假设 %rsi 作为一段区间的左端点,%rdx 作为右端点,那么这一段代码执行后 %ecx 的值就是这段区间的中点。具体解释一下,假设 L,RL, R 作为左右端点,这段代码大概就是先构造出 RLR - L,然后求出 (RL)/2(R - L) / 2 (这里是整除,向 00 舍入)的值,得到 M=L+(RL)/2M = L + (R - L) / 2 作为中点。(值得注意的是,代码中使用了右移 3131 位得到符号位,然后在执行除以 22 之前给被除数加上了符号位的值,从而确保了为正时下去整,为负时上取整)

然后,代码出现了分支。我们记 XX%edi。如果 X<MX < M,那么代码以 (X,L,M1)(X, L, M - 1) 作为参数递归调用 func4,将返回值 ×2\times 2 返回回去。如果 XMX \geq M,那么代码会进一步判断,如果 XMX \leq M(综合起来就是 X=MX = M)就直接返回 00,否则递归调用 func4(X,M+1,R)func4(X, M + 1, R),并将返回值 ×2+1\times 2 + 1

并不明白在计算什么,我们回到 phase_4 中。

1
2
3
4
5
6
7
8
000000000040100c <phase_4>:
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 callq 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3 retq

先判断返回值如果不为 00,那么直接跳转到 401058 爆炸,否则继续判断如果第二个输入为 00 那么直接返回,否则爆炸。很显然第二个参数一定要为 00,同时第一个参数在调用 func4 的返回值也要是 00

很显然如果第一个输入为 77,那么一定会返回 00(甚至不用递归)。但是这应该不是唯一答案。比如,第一个输入为 33 或者为 11 都是可能的,甚至可以为 00

我是用的是最直接的解答:

1
7 0

image-20230902234541525

Phase 5

到了 Phase 5 难度就增大了,涉及了一些控制逻辑了。

我们先来看第一段:

1
2
3
4
5
6
7
8
9
10
11
12
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 callq 40131b <string_length>
40107f: 83 f8 06 cmp $0x6,%eax
401082: 74 4e je 4010d2 <phase_5+0x70>
401084: e8 b1 03 00 00 callq 40143a <explode_bomb>

401062 是在执行 %rbx 寄存器的被调用者保存的义务,而后面的 401063401073 的语句看不太懂,还涉及到了 %fs 寄存器,隐隐约约记得书上提到过这个寄存器和段寻址有关,是用来存储金丝雀值以供栈破坏检测使用的,我们应该不用太关心。这里涉及到的唯一寄存器 %rax 也很快会被 string_length 调用的返回值覆盖。

string_length 的参数依然是 %rdi,也就是我们的 input,因此这里得到了我们输入字符串的长度。下一句中将这个长度和 66 作比较,如果相等会跳转到 4010d2,否则继续执行就会发生爆炸。

继续看 4010d2 这边的一段:

1
2
3
0000000000401062 <phase_5>:
4010d2: b8 00 00 00 00 mov $0x0,%eax
4010d7: eb b2 jmp 40108b <phase_5+0x29>

嗯,没有什么特殊的意思,将 %eax 寄存器清零,然后跳转到了 40108b

1
2
3
4
5
6
7
8
9
10
0000000000401062 <phase_5>:
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>

从最后一句来看,这一段似乎是一个循环啊。

注意到下面的 %rax 的值一直在被修改,所以我们这里先假设 %rax 是一个循环变量,下面用 ii 来代表这个寄存器的值。%rbx 寄存器在之前的 401067 中被修改为了 input 的地址,因此第一句话可以看作是将 char 类型的 input[i] 零扩展到了 int 类型的变量中,用 %ecx 存储。下一句话中,%cl%ecx 的一个字节版本,于是这两句话可以看做是将 input[i] 存储进了栈中。

下面两句话,从栈中取出了 input[i] 的值,然后和 0xf = 1111&,也就是取出了后 44 位。注意到数字字符 '0' - '9' 的 ASCII 码是从 0x30 - 0x39,而大小写字母的前 1515 个字符也分别是 0x41 - 0x4f, 0x61 - 0x6f,我们可以先假设这六个字符都是数字或者字母,那么这两句话的效果将这些字符映射成了一些编号。

下一句从这些编号加上 0x4024b0 得到的地址中取一个字节零扩展存进 %edx,我们可以认为 0x4024b0 这是一个数组或者字符串吧,去查查字符串表,可以发现这个地址中的字符串是 maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?

下一句话将我们取出来的字符存进了地址 0x10(%rsp,%rax,1)。emm,假设 16 + %rsp 对应的地址记作 tt,那么这句话就是将这个字符存进了 t[i] 中。

下面两句就很简单了,将循环变量 ii 加一,然后判断如果不等于 66 了就继续循环。

于是这段循环的意义就是完成了一段映射,将输入的字符串转化成了另一个字符串。

接着看:

1
2
3
4
5
6
7
8
0000000000401062 <phase_5>:
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 callq 401338 <strings_not_equal>
4010c2: 85 c0 test %eax,%eax
4010c4: 74 13 je 4010d9 <phase_5+0x77>
4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb>

下面将将 00 赋值给了 0x16(%rsp),算下来这个位置应该就是我们之前的 tt 数组的第六位,也就是新字符串的末尾,这句话应该是在补充 '\0'

下面几句话很容易理解:将 $0x40245e 处的字符串作为第二个参数,0x10(%rsp) 处的 tt 字符串作为第一个参数,调用 strings_not_equal 判断两个字符串是否相等,如果不相等就跳转到 4010d9,否则继续执行就会发生爆炸。

4010d9 处的操作和开头第一段的操作很类似,应该依然是对金丝雀值和栈破坏检测的操作,我们不关心,没有什么影响。

所以我们的目标很明确了,让我们输入的字符串长度为 66,且经过映射后的字符串与 0x40245e 处字符串相同。那么 0x40245e 处的字符串查一下表就可以发现是 flyers

1
2
0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
m a d u i e r s n f o t v b y l

根据上表映射回原来的字符串,那么每个字符的编号应该依次是:9, 15, 14, 5, 6, 7。这显然不可能是数字,因此可以被映射为字母串 ionefg 或者大写的版本 IONEFG。当然,甚至不一定要是字母,任意的满足末四位与之对应的字符串都是可以的。

于是我提交的答案就是:

1
ionefg

image-20230903115515555

Phase 6

终于到最后一题啦(o)/

首先来看第一段指令。

1
2
3
4
5
6
7
8
9
10
11
12
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 callq 40145c <read_six_numbers>
40110b: 49 89 e6 mov %rsp,%r14
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d

开头就是五行 push,一看就知道是把被调用者保存的寄存器压进栈里。然后 4010fc 分配了一堆栈空间,应该有不止一个数组。接着有吧栈地址存进了 %r13,不知道是做什么的。

401103 下面两句话比较清晰,将栈地址作为第二的参数(input 仍然是第一个参数)调用 read_six_numbers,这个函数我们已经分析过了,是从我们的输入里读取六个整数,存进了紧贴着栈指针的六个 44 字节空间中,我们将这些数记作 a[i]a[i]

下面两句话将栈指针赋值给了 %r14,然后将 %r12d 清空了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
00000000004010f4 <phase_6>:
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 callq 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 callq 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>

从最后一句的跳转 401114,估摸着这一段应该是一个循环,因为后面的指令也没有在跳转回 401151 之前的了。看起来这个循环内部还嵌套了一个循环,我们先着重看一下外循环。

那么我们先猜测一下哪些可能是循环变量。从 40114d 来看,每次循环都会把 %r13 加上 44,猜测这个变量可能是 aa 数组的一个指针,我们记这个指针为 pp。从 401130 这一句跳转到 401153 跳出了循环,猜测这里是循环的唯一退出条件。因此前两句话中每次将 %r12d 加一并和 66 比较应该就是循环的测试条件,因此 %r12d 应该就是循环的数组下标 id,我们记这个下标为 ii

然后我们来看看每一句话的含义。第一条指令将 pp 指针拷贝到了 %rbp 中。下面五条指令实现了从当前 pp 指向的位置读取数值到 %eax,并将 %eax 减去 11,然后和 55 比较,如果小于等于 55 就跳转到 401128,如果没有跳转就会发生爆炸。从这里可以看出,我们输入的六个整数每个数都必须为正,且小于等于 66

再往下的三句话我们刚刚已经分析过了,就是跳出循环的语句。

下面是一个内循环。在循环之前,将当前的循环下标 ii 拷贝到了 %ebx 中。从内循环的末尾三句话可以看出,这个循环中 %ebx 就是循环变量,我们记为 jj。这个循环变量将会完成从 0055 的递增。内循环的前两条指令将 aa 数组第 jj 个数拷贝进了 %eax,接着判断 a[i]a[i]a[j]a[j] 是否相等,如果相等就会爆炸。**这意味着我们输入的 66 个整数必须互不相等。**接着就是内循环的循环条件了,我们已经分析过了。

综上,这一个嵌套循环告诉了我们两件事情,这里重复一遍:我们输入的六个整数每个数都必须为正,且小于等于 66,且互不相等。

1
2
3
4
5
6
7
8
9
10
00000000004010f4 <phase_6>:
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>

从最后一句话可以看出,40116040116d 这一段又是一个循环。

第一句指令将 0x18(%rsp) 的地址拷贝进 %rsi,注意到我们读入的六个整数的地址应该是 0x0(%rsp) - 0x17(%rsp)2424 个字节,因此 %rsi 就是数组结束的下一位,可以用来判断循环枚举结束。结合下一句话以及循环的最后三句指令可以看出,%rax 就是这个循环中用来枚举数据的指针(记不记得 %r14 被存储了一份栈指针的副本),每次循环结束会 +4+4 然后和 %rsi 比较,相等了就会退出循环。我们记 %rax 中的指针为 pp

第三句话将 77 写入了 %ecx,这个值在循环过程中一直没有改变。然后我们来看看循环的内容。每次循环将 77 减去 pp 指针处的值,然后写进 pp 指向的位置。也就是说,这个循环的意义是将数组的每个数用 77 减掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
00000000004010f4 <phase_6>:
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>

下面这一段应该还是一个大循环。我之所以认为这个循环应该划到这里结束,是因为上面这一段最多也就是跳转到 4011ab(最后一句的下一句指令),而 4011ab 之后的语句就没有跳转回 4011ab 之前的了。

并不能很快看出来这个代码在做什么,也不能很快猜测出循环的结构,没办法,只能一行一行看了。

首先将 %esi 清零,考虑到代码中经常有 (%rsp,%rsi,1)``0x20(%rsp,%rsi,2) 这样的引用,我们可以认为 %esi 是一个循环变量,所以这里记作 ii

接下来跳转到 401197,然后首先取出了 a[i]a[i] 存进 %ecx,与 11 比较,如果 a[i]1a[i] \leq 1 就跳转到 401183。这里很绕人,我看了很久都没有判断出来循环的结构,我们还是先假设没有跳转吧,那么继续看下面的语句。下面的指令将 11 写入了 %eax,然后将地址 0x6032d0 写入了 %edx,跳转到 401176。跳转后的 44 行应该是一个小循环(401176 - 40117f),很明显 %eax 是循环变量,%ecx(也就是 a[i]a[i])是枚举终点。这个循环是从 11 枚举到 a[i]1a[i] - 1 结束,每次将 0x8(%rdx) 存进 %rdx。这样的赋值方法很容易让我们联想到链表的结构,因此我们可以假设这里的 %rdx 其实是一个链表的指针。而这个操作实际上就是在取链表中的第 a[i]a[i] 个项目。

然后程序会直接跳转到 401188。这时候我们会过来看之前 a[i]1a[i] \leq 1 时应该跳转到的 401183。这一句话是直接将链表头指针赋值给了 %edx。那么这里的用意就很明显了——如果 a[i]=1a[i] = 1 那么就不用循环了,直接拿走头指针就行了。然后两个分支都会运行到 401188。这一个语句会把 %edx 储存的链表项地址存进 0x20(%rsp,%rsi,2)。这是一个新的数组,我们之前没有使用过这里的地址,结合后续两个语句很容易观察到这个数组一个元素的大小是 88,因此应该是指针数组,符合其存储地址的用意。我们把这个数组就做 b[i]b[i]

下面的三条语句就是循环变量改变和循环结束的判断了,很容易理解。

以下程序是我根据汇编指令改写的 C 语言程序,应该能很直观的体现运行的流程。这个部分的难点在于指令跳转混乱,而且指令的编排顺序和常规的代码书写顺序完全不同,因此很难直观地判断循环结构,必须得一条条指令逐步代入。

1
2
3
4
5
6
7
8
9
// int a[6]; T *b[6];
for (int i = 0; i < 6; ++i) {
T *p;
if (a[i] > 1) {
p = head;
for (int j = 1; j < a[i]; ++j) p = p->next;
} else p = head;
b[i] = p;
}

那么这段代码的作用就是将一个链表的第 a[i]a[i] 个项目的地址存储进 b[i]b[i] 中。

然后我们来看下一个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
00000000004010f4 <phase_6>:
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

相比于这一个部分这里可简单太多了!很容易看出来这是几条顺序指令和一个循环组成的。

前三条指令存储了几个地址。第一条指令将 b[0]b[0] 的值存储进了 %rbx,第二条指令和第三条指令分别存储了 &b[1]&b[6] 的地址。第四条指令将 %rbx 中的 b[0]b[0] 拷贝了一份给 %rcx,我们记存储进 %rcx 的指针为 pp。结合循环体的内容可以猜测,%rax 是这次的循环变量,它从 &b[1] 枚举到 &b[5],我们记它为 qq

每次循环中,qq 指向的内容会被拷贝给 %rdx,然后存储进 8 + %rcx 也就是 p->next。下面三行就是常规的判断循环结束了,如果 qq 指向了 b[6]b[6] 就跳出循环。如果没有跳出,那么就会把 qq 指向的内容赋值给 pp 指针——如此,每次循环中,qq 指向的内容永远是 pp 的下一位。

循环结束后,将最后的 %rdx 也就是 b[5]next 设置为了 NULL

这一段代码实现了b[i+1]b[i + 1] 变成 b[i]->next,也就是将这些链表的项目按照输入顺序重新连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
00000000004010f4 <phase_6>:
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 callq 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 retq

下面的代码就简单了。最后的一堆 addpop 可以不用管,这是处理栈指针和被调用者保存寄存器的指令。

主体部分还是一个循环,可以看出来 %ebp 一定是一个循环变量,从 55 枚举到 11。初始时,%rbx 仍然存储着 b[0] 的值,我们记这个寄存器的内容为 pp。每次循环,将 p->next 的值存储进 %rax,从而将 p->next->val 存储进 %eax(因为 0x8(%rbx) 存储的是 next 成员,很容易猜测到 (%rbx) 存储的是 val 成员)。如果 p->val < p->next->val 炸弹就会爆炸。否则会把 p->next 赋值给 pp,进入下一次循环。

啊,终于判断清楚程序的意图了!

所以,我们回顾一下我们的输入需要满足的条件:我们要输入 66 个正整数 aia_i,每个正整数必须要在 1-6 之间,且不能重复。然后每个正整数会作为减数被 77 减去得到 ai=7aia'_i = 7 - a_i。然后,程序从一个链表中取出第 aia'_i 项,要求取出的项随着 ii 增加要严格递减。

那么我们用 gdb 查看一下这个链表的内容!

image-20230903153834148

可以看出,我们的链表项目的值依次是 332, 168, 924, 691, 477, 443。从大到小排序后项目编号依次是 3, 4, 5, 6, 1, 2。对 77 求一个补可以得到:

1
4 3 2 1 6 5

最后

image-20230903154142292

通关啦!