Bomb-lab 总结

csapp 的实验。破解一个程序中的 6 + 1 个密码,做完之后就忘不掉 gdb 怎么用了。

你会不会忽然地出现
在街角的咖啡店

做这个 lab 的时候 gdb 没有装插件,用的 layout asm 和 layout regs。其实就很够用了,可以滚动翻看断点附近的代码,可视化一步一步跑然后看寄存器变化,非常不错。

Phase 1

上来先下断点到 phase_1 和 explode_bomb(防爆,如果真的当作业做的话爆一次扣 0.5 分)。随便输点东西,跳到 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
400ee9: e8 4a 04 00 00 call 401338 <strings_not_equal>
400eee: 85 c0 test %eax,%eax
400ef0: 74 05 je 400ef7 <phase_1+0x17>
400ef2: e8 43 05 00 00 call 40143a <explode_bomb>
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 ret

可以看到调用了一个 strings_not_equal,顺便一说没有把字符表剥掉真的立大功,真实环境下没有人会把这么重要的信息暴露给破解者。看到 call 了一下 strings_not_equal,这个函数具体怎么回事没仔细看,看名字就知道是判断是否相等的函数,调用规约规定 % rdi,% rsi 是前两个参数寄存器,% rdi 储存了输入字符串的地址,那就是和 0x402400 处的字符串做比较,翻看一下, x /s 0x402400,出现字符串:“Border relations with Canada have never been better.”,这就是这一关的密码。

Phase 2

强烈建议用 objdump 反汇编完整代码,这样一边开 gdb 动态调试,想到什么想法翻看静态的汇编代码,很方便。顺便一提,linux 下的调试器绝对不止 gdb,至于为什么不用 x64gdb 和 ida,大概是他们太强大了吧。

记得小时候玩的还是 ollydbg,那个只有 32 位版本,而且已经被扫进历史垃圾堆。

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>
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp)
400f0e: 74 20 je 400f30 <phase_2+0x34>
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
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 call 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 ret

上来在栈上开大,为了读入 6 个数字调用了 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>
40148f: 83 f8 05 cmp $0x5,%eax
401492: 7f 05 jg 401499 <read_six_numbers+0x3d>
401494: e8 a1 ff ff ff call 40143a <explode_bomb>
401499: 48 83 c4 18 add $0x18,%rsp
40149d: c3 ret

为了用 sscanf 函数,% rdi 是源字符串,% rsi 是格式化字符串(后面要考),% rdx-% r9 全是参数,也就是将要读取的 6 个数的地址。可以看到都是放到栈上的。如果读取数字 <=5 个就爆炸。另外,% al = 0 是指定向量寄存器个数,因为 sscanf 有变长参数

于是读入了 6 个 int 数字。cmpl $0x1,(% rsp) 要求栈顶元素是 1,即第一个数是 1。看出来是一个 while / for 循环,先跳到循环初始化位置,% rbx 放第二个数字地址做起始地址,% rdp 放第六个数字后的地址做结束地址。

循环关键的地方在:

1
2
3
400f17:	8b 43 fc             	mov    -0x4(%rbx),%eax
400f1a: 01 c0 add %eax,%eax
400f1c: 39 03 cmp %eax,(%rbx)

也就是每次拿出来 % rbx 的前一个元素,*2 和 % rbx 指向的元素比较,不相等就爆炸,也就是要是一个从 1 开始,2 为公比的等比数列。答案是 "1 2 4 8 16 32"

Phase 3

直接用了 sscanf。看到 % rsi 存储的地址是 "% d % d" 以及用了两个参数来看,是需要输入两个数字。

1
2
3
4
5
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 call 400bf0 <__isoc99_sscanf@plt>

% rax 做为返回值,返回实际读取参数个数。如果 < 2 爆炸。

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

这是判别第一个数字要 < 0x7,然后把第一个数字放到 rax 里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
400f75:	ff 24 c5 70 24 40 00 	jmp    *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 call 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

这是一个跳转表。从 0x402470 + rax * 8 处找出一个 8 字节地址进行跳转,类似 switch 功能。rax = 1 时,对应内存中地址是 0x400fb9,对应将 rax = 0x137 = 311。然后将 rax 和第二个数字对比,一样就过关。所以答案 "1 311"。当然不唯一。

Phase 4

待更