0%

CSAPP——BombLabⅡ

紧接上篇文章,这篇文章将分析CSAPP配套课程ICS(Introduction to Computer Science)中,Bomb Lab的第二部分——Phase 3和Phase 4。

Phase 3

有了上次的经验,现在直接查看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
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>
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)
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>
400fc9: 48 83 c4 18 add $0x18,%rsp
400fcd: c3 retq a

先观察调用sscanf函数前的指令。看起来套路与read_six_numbers函数的相同,只用关心匹配的格式。查看地址0x4025cf处的字符串:

1
2
(gdb) x /s 0x4025cf
0x4025cf: "%d %d"

这样前九行指令的作用就清晰了:从输入的字符串中获得两个int型整数。接下来看看用这两个数干什么:

1
2
3
4
5
6
···
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp)
400f6f: 77 3c ja 400fad <phase_3+0x6a>
···
400fad: e8 88 04 00 00 callq 40143a <explode_bomb>
···

第一行比较第一个整数,使用的是无符号数的跳转指令,如果该数大于7(负数被当作无符号数时也大于7),则炸弹爆炸。推断出第一个数小于等于7,大于等于0。继续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  400f71:	8b 44 24 08          	mov    0x8(%rsp),%eax
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8)
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
···

这是一个典型的switch语句的跳转表实现(实际上我没有一眼认出):在这里,第一个整数决定了访问跳转表的哪一个内存地址,进而决定了跳转到哪一条指令。找到跳转表的首地址0x402470,查看后面18个字节的内容:

1
2
3
4
5
6
(gdb) x /18x 0x402470
0x402470: 0x00400f7c 0x00000000 0x00400fb9 0x00000000
0x402480: 0x00400f83 0x00000000 0x00400f8a 0x00000000
0x402490: 0x00400f91 0x00000000 0x00400f98 0x00000000
0x4024a0: 0x00400f9f 0x00000000 0x00400fa6 0x00000000
0x4024b0 <array.3449>: 0x7564616d 0x73726569

这样就建立起了第一个整数数值(0~7)与指令间的映射关系。这些指令做的事情都一致:向eax寄存器中传送常量值。

有趣的是,当第一个整数为1时,程序会跳转到接近switch语句结束的位置,这样做减少了一个跳转指令。(相对于其它整数值)

最后来看末尾的几条指令:

1
2
3
4
5
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>
400fc9: 48 83 c4 18 add $0x18,%rsp
400fcd: c3 retq a

第二个整数终于出现。意思很简单,如果eax寄存器中的值与第二个整数不相同,炸弹爆炸。

结合上面的全部分析可以发现,输入的第一个整数与第二个整数之间要有一一对应关系,而这个对应关系到switch语句中找任意一个即可(记得把十六进制转换为十进制)。输入后通过:

1
2
3
That's number 2.  Keep going!
1 311
Halfway there!

Phase 4

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

0C~35地址的指令作用与phase_3前面部分的相似,从输入中解析出两个整数,但第一个整数不能大于14。

然后调用func4函数,第一个整数的值、0及14被传入。

最后如果返回值为等于0,且第二个整数为0,该阶段通过。剩下的任务只有看看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的函数声明是这样的:

1
int func4(int i, int j, int k);

逐步分析(这里就不像之前这么啰嗦了)后得出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int fake_func4(int i, int j, int k)
{
int ret = 0;
int x = (k - j) >> 1; // 算数位移
int y = x + j;
if (i > y)
{
j = y + 1;
ret = fake_func4(i, j, k) * 2 + 1;
}
else if (i < y)
{
k = y - 1;
ret = fake_func4(i, j, k) * 2;
}
return ret;
}

看上去有点乱,实际上我们只要让第一次进入函数时i==y就行,不用管哪些花里胡哨的递归。

有趣的一个地方是,下面的逻辑右移操作会将ecx寄存器清零。

1
shr    $0x1f,%ecx

经过简单的数学运算得出,第一个整数应该为7。(我这里忽略了递归,也许有其他可能?)输入,通过:

1
2
3
Halfway there!
7 0
So you got that one. Try this one.

总结

  1. 有了之前的经验,这两个phase就不是很难了。
  2. 这次的分析过程中调试很少。
  3. 静下心慢慢看是最重要的。
  4. 下面是开了-O优化的fake_func4函数的部分汇编代码,不知道对不对劲,也不知道怎么测试(内联汇编?)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fake_func4:
.LFB0:
.cfi_startproc
endbr64
subq $8, %rsp
.cfi_def_cfa_offset 16
movl %edx, %ecx
subl %esi, %ecx
sarl %ecx
addl %esi, %ecx
cmpl %edi, %ecx
jl .L6
movl $0, %eax
jg .L7
.L1:
addq $8, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 8
ret
.L6:
.cfi_restore_state
leal 1(%rcx), %esi
call fake_func4
leal 1(%rax,%rax), %eax
jmp .L1
.L7:
leal -1(%rcx), %edx
call fake_func4
addl %eax, %eax
jmp .L1
.cfi_endproc