首页 > 其他分享 >CSAPP Lab-2 BOMBLAB

CSAPP Lab-2 BOMBLAB

时间:2024-04-24 13:22:40浏览次数:32  
标签:CSAPP 48 00 BOMBLAB mov Lab eax 83 rsp

第二个 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

通关啦!

标签:CSAPP,48,00,BOMBLAB,mov,Lab,eax,83,rsp
From: https://www.cnblogs.com/hankeke303/p/18155074/csapp-bomblab

相关文章

  • 伯克利大学 CS61B Lab配置教程
    基本过程:首先将伯克利大学的代码框架下载到自己的电脑,然后我们直接在框架里修改就行将自己的代码上传到github上,然后使用伯克利大学的Gradescope评测自己写的代码下载代码在自己电脑桌面新建一个文件夹,这里我命名为:cs61b,打开gitbash,使用cd进入我们新创建的文件夹,注意路径......
  • 读《我和Labview》5条件结构和顺序结构
    5条件结构和顺序结构条件结构布尔类型条件选择结构其它数据类型的条件选择是否要设置默认分支?合理设置悬着条件隧道避免把控件放在条件结构内选择函数顺序结构程序执行顺序创建顺序结构层叠式顺序结构平铺式顺序结构无形胜有形的最高境界6用户自定义控件7控件的局......
  • Linux服务器中Docker部署的GitLab镜像访问出现500错误
    一背景这几天发现在Linux服务器中Docker部署的GitLab镜像访问出现500错误,在重启服务器后大概10分钟再次出现该情况,后面登录服务器一步步排查最终解决问题,现在将解决问题的过程做一个总结。二过程分析首先第一步就是看看我们Docker目录下文件占用的情况,因为我们的Linux服务......
  • 如何将Docker中GitLab数据备份到宿主Linux上
    一宿主机准备存放备份文件的目录建议以年月日进行命名使用putty.exe或者PowerShell登录远程服务器cdshare(如果没有当前目录请创建该共享目录)mkdir20220930(在共享目录下创建备份文件夹)二进入Docker容器内部备份数据1.执行命令sudodockerexec-itgitlab/bin/......
  • feign调用接口报错No qualifying bean of type '***HttpMessageConverters' available
    在整合springcloudgeateway时,调用了feign接口,报错Noqualifyingbeanoftype'org.springframework.boot.autoconfigure.http.HttpMessageConverters'available报错信息feign.codec.EncodeException:Noqualifyingbeanoftype'org.springframework.boot.autocon......
  • MIT6.S081 - Lab2: system calls
    Lab2:systemcalls预备知识执行一次系统调用的流程:USERMODEstep1:系统调用声明user/user.h:系统调用函数(如intfork(void))step2:ecall进入内核态user/usys.S(该文件由user/usys.pl生成,后续添加函数可以在这里添加):执行如下命令.globalforkfork:lia7,SYS_f......
  • 读《我和Labview》3.5-3.6路径和数据平化
    3.5路径3.5.1路径数据3.5.2相对路径3.5.3路径常量3.5.4路径与其他数据类型的转换3.6数据平化3.6.1数据平化至字符串3.6.2数据平化至XML3.6.3数据平化至JSON4图形化显示数据5条件结构和顺序结构6用户自定义控件7控件的局部变量和属性8按自己的喜好设置编程环境......
  • 基于PSO优化的CNN-LSTM-Attention的时间序列回归预测matlab仿真
    1.算法运行效果图预览PSO优化前:      PSO优化后:   2.算法运行软件版本MATLAB2022A  3.算法理论概述       时间序列回归预测是数据分析的重要领域,旨在根据历史数据预测未来时刻的数值。近年来,深度学习模型如卷积神经网络(ConvolutionalNe......
  • MIT 6.5830 simpleDB Lab2
    Exercise1需要完成的是:src/java/simpledb/execution/Predicate.javasrc/java/simpledb/execution/JoinPredicate.javasrc/java/simpledb/execution/Filter.javasrc/java/simpledb/execution/Join.java这里主要实现两个功能:Filter:通过Predicate过滤一部分满足条件的Tupl......
  • docker下安装gitlab配置以及备份
    安装dockerrun--detach--publish443:443--publish9980:80--publish9922:22--namegitlab--restartalways--volume/srv/gitlab/config:/etc/gitlab--volume/srv/gitlab/logs:/var/log/gitlab--volume/srv/gitlab/data:/var/opt/gitlab--shm-siz......