CSAPP Lab-2 BOMBLAB

第二个 Lab 就比较有趣了。

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

准备

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

bomb.c 第 72 至第 77 行

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

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

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 函数的指令:

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 处的字符串作比较。结合下面的指令可以发现,如果返回值为 \(0\)(猜测是比较成功),那么就会直接返回。

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

Border relations with Canada have never been better.

于是,第一个炸弹拆除!

image-20230902205014721

Phase 2

继续来看 phase_2 中的指令:

0000000000400efc <phase_2>:
  400efc:	55                   	push   %rbp
  400efd:	53                   	push   %rbx
  400efe:	48 83 ec 28          	sub    $0x28,%rsp
  400f02:	48 89 e6             	mov    %rsp,%rsi
  400f05:	e8 52 05 00 00       	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 这个名字一看就是要我们输入六个整数,不过来看看程序,具体是怎么操作的吧!

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 函数的小数组的前 \(6\) 位中。为了方便起见,我们后面用 \(s[i]\) 表示这六个数中第 \(i\) 个数。

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

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

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

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

1 2 4 8 16 32

第二个炸弹,拆除!

image-20230902212215935

Phase 3

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

0000000000400f43 <phase_3>:
  400f43:	48 83 ec 18          	sub    $0x18,%rsp
  400f47:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  400f4c:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  400f51:	be cf 25 40 00       	mov    $0x4025cf,%esi
  400f56:	b8 00 00 00 00       	mov    $0x0,%eax
  400f5b:	e8 90 fc ff ff       	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 的返回值,如果返回值小于等于 \(1\),那么说明输入有误,直接爆炸!

我们继续看!

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)

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

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

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

image-20230902214922431

结合汇编代码:

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 准备返回。

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

0 207

image-20230902215605716

Phase 4

下一弹!

000000000040100c <phase_4>:
  40100c:	48 83 ec 18          	sub    $0x18,%rsp
  401010:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  401015:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  40101a:	be cf 25 40 00       	mov    $0x4025cf,%esi
  40101f:	b8 00 00 00 00       	mov    $0x0,%eax
  401024:	e8 c7 fb ff ff       	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(也就是 \(14\)),如果成立就跳转 40103a,没有跳转就会爆炸。

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>

下面,程序以第一个数的值、\(0\)\(14\) 依次作为三个参数调用了函数 func4

0000000000400fce <func4>:
  400fce:	48 83 ec 08          	sub    $0x8,%rsp
  400fd2:	89 d0                	mov    %edx,%eax
  400fd4:	29 f0                	sub    %esi,%eax
  400fd6:	89 c1                	mov    %eax,%ecx
  400fd8:	c1 e9 1f             	shr    $0x1f,%ecx
  400fdb:	01 c8                	add    %ecx,%eax
  400fdd:	d1 f8                	sar    %eax
  400fdf:	8d 0c 30             	lea    (%rax,%rsi,1),%ecx
  400fe2:	39 f9                	cmp    %edi,%ecx
  400fe4:	7e 0c                	jle    400ff2 <func4+0x24>
  400fe6:	8d 51 ff             	lea    -0x1(%rcx),%edx
  400fe9:	e8 e0 ff ff ff       	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, R\) 作为左右端点,这段代码大概就是先构造出 \(R - L\),然后求出 \((R - L) / 2\) (这里是整除,向 \(0\) 舍入)的值,得到 \(M = L + (R - L) / 2\) 作为中点。(值得注意的是,代码中使用了右移 \(31\) 位得到符号位,然后在执行除以 \(2\) 之前给被除数加上了符号位的值,从而确保了为正时下去整,为负时上取整)

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

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

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   

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

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

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

7 0

image-20230902234541525

Phase 5

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

我们先来看第一段:

0000000000401062 <phase_5>:
  401062:	53                   	push   %rbx
  401063:	48 83 ec 20          	sub    $0x20,%rsp
  401067:	48 89 fb             	mov    %rdi,%rbx
  40106a:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
  401071:	00 00 
  401073:	48 89 44 24 18       	mov    %rax,0x18(%rsp)
  401078:	31 c0                	xor    %eax,%eax
  40107a:	e8 9c 02 00 00       	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,因此这里得到了我们输入字符串的长度。下一句中将这个长度和 \(6\) 作比较,如果相等会跳转到 4010d2,否则继续执行就会发生爆炸。

继续看 4010d2 这边的一段:

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

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

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 是一个循环变量,下面用 \(i\) 来代表这个寄存器的值。%rbx 寄存器在之前的 401067 中被修改为了 input 的地址,因此第一句话可以看作是将 char 类型的 input[i] 零扩展到了 int 类型的变量中,用 %ecx 存储。下一句话中,%cl%ecx 的一个字节版本,于是这两句话可以看做是将 input[i] 存储进了栈中。

下面两句话,从栈中取出了 input[i] 的值,然后和 0xf = 1111&,也就是取出了后 \(4\) 位。注意到数字字符 '0' - '9' 的 ASCII 码是从 0x30 - 0x39,而大小写字母的前 \(15\) 个字符也分别是 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 对应的地址记作 \(t\),那么这句话就是将这个字符存进了 t[i] 中。

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

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

接着看:

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>

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

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

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

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

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。当然,甚至不一定要是字母,任意的满足末四位与之对应的字符串都是可以的。

于是我提交的答案就是:

ionefg

image-20230903115515555

Phase 6

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

首先来看第一段指令。

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,这个函数我们已经分析过了,是从我们的输入里读取六个整数,存进了紧贴着栈指针的六个 \(4\) 字节空间中,我们将这些数记作 \(a[i]\)

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

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 加上 \(4\),猜测这个变量可能是 \(a\) 数组的一个指针,我们记这个指针为 \(p\)。从 401130 这一句跳转到 401153 跳出了循环,猜测这里是循环的唯一退出条件。因此前两句话中每次将 %r12d 加一并和 \(6\) 比较应该就是循环的测试条件,因此 %r12d 应该就是循环的数组下标 id,我们记这个下标为 \(i\)

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

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

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

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

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)\(24\) 个字节,因此 %rsi 就是数组结束的下一位,可以用来判断循环枚举结束。结合下一句话以及循环的最后三句指令可以看出,%rax 就是这个循环中用来枚举数据的指针(记不记得 %r14 被存储了一份栈指针的副本),每次循环结束会 \(+4\) 然后和 %rsi 比较,相等了就会退出循环。我们记 %rax 中的指针为 \(p\)

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

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 是一个循环变量,所以这里记作 \(i\)

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

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

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

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

// 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]\) 个项目的地址存储进 \(b[i]\) 中。

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

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]\) 的值存储进了 %rbx,第二条指令和第三条指令分别存储了 &b[1]&b[6] 的地址。第四条指令将 %rbx 中的 \(b[0]\) 拷贝了一份给 %rcx,我们记存储进 %rcx 的指针为 \(p\)。结合循环体的内容可以猜测,%rax 是这次的循环变量,它从 &b[1] 枚举到 &b[5],我们记它为 \(q\)

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

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

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

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 一定是一个循环变量,从 \(5\) 枚举到 \(1\)。初始时,%rbx 仍然存储着 b[0] 的值,我们记这个寄存器的内容为 \(p\)。每次循环,将 p->next 的值存储进 %rax,从而将 p->next->val 存储进 %eax(因为 0x8(%rbx) 存储的是 next 成员,很容易猜测到 (%rbx) 存储的是 val 成员)。如果 p->val < p->next->val 炸弹就会爆炸。否则会把 p->next 赋值给 \(p\),进入下一次循环。

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

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

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

image-20230903153834148

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

4 3 2 1 6 5

最后

image-20230903154142292

通关啦!

posted @ 2024-04-24 13:15  hankeke303  阅读(23)  评论(0编辑  收藏  举报