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