BombLab需要的前置知识为反汇编和阅读汇编指令的能力,使用的主要工具有二:
Objdump --- 反汇编二进制文件
gdb --- 调试程序
First of all
先用objdump反汇编bomb文件:
objdump -d bomb > bomb.s
汇总常用的gdb命令:
gdb命令 | 描述 |
---|---|
layout asm | 显示汇编代码窗口 |
layout regs | 显示寄存器窗口 |
b *address | 在指定地址上打断点 |
x/s | 打印ASCII字符串 |
x/nfu addr | 以f格式打印从addr开始的n个长度单元为u的内存值,常用x/nxb |
display /ni $pc | 显示当程序停止时,将要执行的n条汇编指令 |
si/ni | 执行一行汇编指令,区别是若存在函数调用,si进入函数内部,ni直接执行 |
接下来就可以开始手撕汇编了。
phase1
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
可以看到调用<strings_not_equal>函数后执行了test指令,这条指令的结果直接影响了bomb是否爆炸,成功拆解phase1的条件为eax=0
。看名字虽然能猜到这一函数的作用,但还是需要进一步分析<strings_not_equal>的功能以及对栈与寄存器的影响。
000000000040131b <string_length>:
40131b: 80 3f 00 cmpb $0x0,(%rdi)
40131e: 74 12 je 401332 <string_length+0x17>
401320: 48 89 fa mov %rdi,%rdx
401323: 48 83 c2 01 add $0x1,%rdx
401327: 89 d0 mov %edx,%eax
401329: 29 f8 sub %edi,%eax
40132b: 80 3a 00 cmpb $0x0,(%rdx)
40132e: 75 f3 jne 401323 <string_length+0x8>
401330: f3 c3 repz retq
401332: b8 00 00 00 00 mov $0x0,%eax
401337: c3 retq ;计算从(%rdi)开始的字符串长度
0000000000401338 <strings_not_equal>:
401338: 41 54 push %r12
40133a: 55 push %rbp
40133b: 53 push %rbx
40133c: 48 89 fb mov %rdi,%rbx
40133f: 48 89 f5 mov %rsi,%rbp
401342: e8 d4 ff ff ff callq 40131b <string_length>
401347: 41 89 c4 mov %eax,%r12d ;计算(%rdi)开始的字符串长度并保存到%12d
40134a: 48 89 ef mov %rbp,%rdi
40134d: e8 c9 ff ff ff callq 40131b <string_length>
401352: ba 01 00 00 00 mov $0x1,%edx
401357: 41 39 c4 cmp %eax,%r12d ;比较(%rdi)和(%rsi)开始的字符串长度大小
40135a: 75 3f jne 40139b <strings_not_equal+0x63>
40135c: 0f b6 03 movzbl (%rbx),%eax
40135f: 84 c0 test %al,%al ;若字符串大小都为0,则返回0
401361: 74 25 je 401388 <strings_not_equal+0x50>
401363: 3a 45 00 cmp 0x0(%rbp),%al ;比较两个字符串中第一个字符
401366: 74 0a je 401372 <strings_not_equal+0x3a>
401368: eb 25 jmp 40138f <strings_not_equal+0x57>
40136a: 3a 45 00 cmp 0x0(%rbp),%al ;比较两个字符串中下一个字符,不相等则返回1
40136d: 0f 1f 00 nopl (%rax)
401370: 75 24 jne 401396 <strings_not_equal+0x5e>
401372: 48 83 c3 01 add $0x1,%rbx
401376: 48 83 c5 01 add $0x1,%rbp
40137a: 0f b6 03 movzbl (%rbx),%eax
40137d: 84 c0 test %al,%al ;若遍历完字符串则返回0
40137f: 75 e9 jne 40136a <strings_not_equal+0x32>
401381: ba 00 00 00 00 mov $0x0,%edx
401386: eb 13 jmp 40139b <strings_not_equal+0x63>
401388: ba 00 00 00 00 mov $0x0,%edx
40138d: eb 0c jmp 40139b <strings_not_equal+0x63>
40138f: ba 01 00 00 00 mov $0x1,%edx
401394: eb 05 jmp 40139b <strings_not_equal+0x63>
401396: ba 01 00 00 00 mov $0x1,%edx
40139b: 89 d0 mov %edx,%eax
40139d: 5b pop %rbx
40139e: 5d pop %rbp
40139f: 41 5c pop %r12
4013a1: c3 retq
分析<strings_not_equal>函数可知,这一函数比较了(%rdi)和(%rsi)开始的字符串,相等返回0,否则返回1。其中这一函数调用了<string_length>,它的作用为计算从(%rdi)开始的字符串长度。
回到phase1的汇编代码,可以看到(%rsi)为指定的0x402400
,(%rdi)的值在main函数中由stdin输入给出(main函数中<read_line>使用了glibc相关的函数调用,且bomb.c也给出了源码,因此这里不再做分析)。所以phase1的答案为0x402400
开始的字符串,用 gdb的x/s
指令输出,结果为:
Border relations with Canada have never been better.
phase2
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) ;比较(%rsp)和1,不相等则bomb!
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) ;若(%rbx)不等于-0x4(%rbx)的两倍,则bomb!
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 ;%rbx+=4
400f29: 48 39 eb cmp %rbp,%rbx ;比较%rbx和上界%rbp,不相等则继续遍历
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
先不看<read_six_numbers>的函数调用(虽然看名字也知道作用),分析一下phase_2的汇编代码,可以看到该函数对(%rsp)的开始到0x18(%rsp)的长度为4byte的共6个数据做了比较,且要求每个数据都应当为前一个的2倍。初始的(%rsp)被赋值为1,所以6个数依次为:
1 2 4 8 16 32
<read_six_numbers>可能是为(%rsp)开始的地址填入6个int型的数据,而对应的汇编代码涉及到系统调用。所以在400f0a
处打上断点,程序中断时查看(%rsp)开始的数据来验证我们的猜想。
可以看到,(%rsp)开始6个int型数据正是我们所输入的6个数,输入之前推断出的6个数就能完成对phase2的defuse。
phase3
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
先看<sscanf>函数调用,首先scanf函数接受的参数为一个格式字符串和对应个数的数据指针,调用<sscanf>前赋了一个与地址很相似的值给%rsi并且清零了%rax,调用后将返回值与1作比较,若小于等于1则引爆bomb。<sscanf>函数的返回值为成功读入数据的个数,因此这里至少需要输入2个以上的数据。而格式字符串还不为我们所知,猜测%rsi中存放的地址与格式字符串相关,尝试用x/s
输出这一位置的字符串:
结果验证了我们的猜想,但输入数据存放的位置还未确定。接下来phase3对0x8(%rsp)和7作了比较,且一开始使用了
lea 0xc(%rsp),%rcx
和lea 0x8(%rsp),%rdx
存放了这两个地址,很有可能是存放我们输入数据的位置。照之前的过程验证一下:继续向下分析,0x8(%rsp)若大于7就会引爆bomb。
jmpq *0x402470(,%rax,8)
指令作了间接跳转,这是switch语句的特征。虽然汇编指令中没有跳转表可供参考,但可以通过逐步调试查看之后跳转的指令。第一个输入为0-7闭区间任意值共8种情况,第二个输入为跳转后%eax被赋予的值。这里选择了0作为第一个输入,跳转后%eax的值为0xcf
,因此其中合法的一组输入为:
0 207
phase4
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
phase4的开始部分和phase3很接近,甚至使用了同一个格式字符串,由phase3可知我们读入的两个int型数据存放在了0x8(%rsp)和0x8(%rsp)的位置。紧接着将%rdx赋值为0xe,%rsi赋值为0x0后调用了函数func4。
0000000000400fce <func4>: ;%rdx=0xe,%rsi=0x0,%rdi=0x8(%rsp)
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的逻辑,我们的目的是让%eax为0从而返回后的test指令能够将ZF置为0。可以看到cmp %edi,%ecx
前的指令都与我们的输入无关,打上断点让gdb调试到这里,%ecx的值为7,且%ecx<=%edi
时跳转到mov $0x0,%eax
,这正是我们所想要的。接着执行下面的指令,若满足%ecx>=%edi
则直接跳转出func4,且%eax也为0。为了同时满足这两个指令的跳转条件,只需将%rdi置为7即可,而%rdi被赋值为我们输入的第一个数据,因此第一个输入可置为7。
func4结束后继续看phase4中的指令,接下来的逻辑就很容易了,只需要比较第二个参数与0的大小,不相等则引爆bomb,相等则成功defuse。因此phase4的一组合法输入为:
7 0
phase5
0000000000401062 <phase_5>:
401062: 53 push %rbx
401063: 48 83 ec 20 sub $0x20,%rsp
401067: 48 89 fb mov %rdi,%rbx ;%rbx=%rdi
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>
401089: eb 47 jmp 4010d2 <phase_5+0x70>
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 ;所有1byte字符只保留最后四位
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>
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> ;与0x40245e开始的字符串比较
4010c2: 85 c0 test %eax,%eax
4010c4: 74 13 je 4010d9 <phase_5+0x77>
4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb>
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
4010d0: eb 07 jmp 4010d9 <phase_5+0x77>
4010d2: b8 00 00 00 00 mov $0x0,%eax
4010d7: eb b2 jmp 40108b <phase_5+0x29>
4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax
4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
4010e5: 00 00
4010e7: 74 05 je 4010ee <phase_5+0x8c>
4010e9: e8 42 fa ff ff callq 400b30 <__stack_chk_fail@plt>
4010ee: 48 83 c4 20 add $0x20,%rsp
4010f2: 5b pop %rbx
4010f3: c3 retq
可以看到0x18(%rsp)开始的8个byte用于作栈保护,这8个byte在执行过程中不能改动,否则会调用<__stack_chk_fail@plt>函数产生报错,最多能改动的内存为(%rsp)开始到0x18(%rsp)的共24的字节。接下来调用了<string_length>函数,若返回值不等于6则引爆bomb,所以我们的输入应当为一个长度为6的字符串。
继续向下分析,我们可以看到输入的6个1byte长的字符只保留了最后4bit,将剩下的4bit数与0x4024b0
相加得到新的地址,把映射得到的新字符串存放在0x10(%rsp)中。然后将新字符串与0x40245e
开始的字符串作比较,若不相等则引爆bomb。我们打印出这两个位置的字符串:
可以看到
0x4024b0
开始的16个字符虽然没有遍历整个字母表,但也足以表示我们需要的字符串'flyers'。我们先计算出6个符合要求的4bit串,然后查询ASCII码的字符表就能得到需要输入的字符串:
1001 1111 1110 0101 0110 0111
查询字母表后得到的一组输入为:
9?>%&'
phase6
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
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>
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>
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: 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
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
蚌埠住了人肉逆不下去,直接上ida:
__int64 __fastcall phase_6(__int64 a1)
{
int *v1; // r13
int v2; // er12
int v3; // ebx
char *v4; // rax
unsigned __int64 i; // rsi
_QWORD *v6; // rdx
int v7; // eax
int v8; // ecx
__int64 v9; // rbx
char *v10; // rax
__int64 j; // rcx
__int64 v12; // rdx
int v13; // ebp
__int64 result; // rax
int v15[6]; // [rsp+0h] [rbp-78h] BYREF
char v16; // [rsp+18h] [rbp-60h] BYREF
__int64 v17; // [rsp+20h] [rbp-58h]
char v18; // [rsp+28h] [rbp-50h] BYREF
char v19[40]; // [rsp+50h] [rbp-28h] BYREF
v1 = v15;
read_six_numbers(a1, v15);
v2 = 0;
while ( 1 )
{
if ( (unsigned int)(*v1 - 1) > 5 )
explode_bomb(a1, v15);
if ( ++v2 == 6 )
break;
v3 = v2;
do
{
if ( *v1 == v15[v3] )
explode_bomb(a1, v15);
++v3;
}
while ( v3 <= 5 );
++v1;
}
v4 = (char *)v15;
do
{
*(_DWORD *)v4 = 7 - *(_DWORD *)v4;
v4 += 4;
}
while ( v4 != &v16 );
for ( i = 0LL; i != 24; i += 4LL )
{
v8 = v15[i / 4];
if ( v8 <= 1 )
{
v6 = &node1;
}
else
{
v7 = 1;
v6 = &node1;
do
{
v6 = (_QWORD *)v6[1];
++v7;
}
while ( v7 != v8 );
}
*(__int64 *)((char *)&v17 + 2 * i) = (__int64)v6;
}
v9 = v17;
v10 = &v18;
for ( j = v17; ; j = v12 )
{
v12 = *(_QWORD *)v10;
*(_QWORD *)(j + 8) = *(_QWORD *)v10;
v10 += 8;
if ( v10 == v19 )
break;
}
*(_QWORD *)(v12 + 8) = 0LL;
v13 = 5;
do
{
result = **(unsigned int **)(v9 + 8);
if ( *(_DWORD *)v9 < (int)result )
explode_bomb(a1, v19);
v9 = *(_QWORD *)(v9 + 8);
--v13;
}
while ( v13 );
return result;
}
输入的6个数据存放在(%rsp)开始的4*6=24个字节中,即v15[6]中,v16的地址为%rsp+0x18,v17的地址为%rsp+0x20,v18的地址为%rsp+0x28。
接下来从头到尾捋一捋逻辑,首先输入的6个数据都应该小于7且互不相等,否则在第一个循环中会引爆bomb。然后对v15[6]中的六个数据做了x=7-x
的操作。关键的在下一个循环体中,v17开始的48个byte被赋了与v15[6]存放数据相关的值。为了理解这一循环体中的逻辑,我们打印从&node1开始的部分内存:
可以看到每个node i开始的第8个byte存放了node i+1的地址,node6的这一位置为0,这和我们熟知的单链表结构相似。再回到函数的逻辑,可以发现该循环体根据v15[6]的大小将v17开始的内存每一项赋值为单链表中node的地址,v15[6]中的数越大单链表遍历的越深。
下一个循环体的分析是最困难的,这个循环体将v17开始的6个node指针按次序拼接成一个单链表。结束后将尾结点的指针域置为null,所以我们整个过程实际上是将v17开始的结点按照输入给定的次序重新排列成单链表。
最后一个循环体中result为下一个结点的地址,只要当前结点的数据域值小于result中的数据就会引爆bomb,由此可以推断我们需要将1-6的整数根据单链表中数据域的数值大小重新排列:
3 4 5 6 1 2
经过x=7-x
处理后为:
4 3 2 1 6 5
secret_phase
年后再补。
phase_6的汇编代码后紧接着就是secret _phase,但我们还不清楚secret_phase的入口位置,全局查找后得知,每个phase后调用的phase_defused函数中调用了secret_phase,ida中phase_defused函数如下:
unsigned __int64 phase_defused()
{
char v1; // [rsp+8h] [rbp-70h] BYREF
char v2; // [rsp+Ch] [rbp-6Ch] BYREF
char v3[88]; // [rsp+10h] [rbp-68h] BYREF
unsigned __int64 v4; // [rsp+68h] [rbp-10h]
v4 = __readfsqword(0x28u);
if ( num_input_strings == 6 )
{
if ( (unsigned int)__isoc99_sscanf(&unk_603870, "%d %d %s", &v1, &v2, v3) == 3
&& !(unsigned int)strings_not_equal(v3, "DrEvil") )
{
puts("Curses, you've found the secret phase!");
puts("But finding it and solving it are quite different...");
secret_phase();
}
puts("Congratulations! You've defused the bomb!");
}
return __readfsqword(0x28u) ^ v4;
}
这里的num_input_strings变量在read_line函数中被增加,而read_line函数在每个phase执行前被调用作为输入,这点在bomb.c文件中也有体现,read_line函数如下:
const char *read_line()
{
int v0; // edx
const char *v1; // rsi
unsigned __int64 v2; // kr08_8
int v3; // eax
__int64 v4; // rax
if ( !skip() )
{
if ( infile == (FILE *)stdin )
{
puts("Error: Premature EOF on stdin");
exit(8);
}
if ( getenv("GRADE_BOMB") )
exit(0);
infile = (FILE *)stdin;
if ( !skip() )
{
puts("Error: Premature EOF on stdin");
exit(0);
}
}
v0 = num_input_strings;
v1 = (const char *)(80LL * num_input_strings + 6305664);
v2 = strlen(v1) + 1;
if ( (int)v2 - 1 > 78 )
{
puts("Error: Input line too long");
v3 = num_input_strings++;
v4 = 10LL * v3;
input_strings[v4] = 0x636E7572742A2A2ALL;
qword_603788[v4] = 0x2A2A2A64657461LL;
explode_bomb();
}
*((_BYTE *)&input_strings[10 * num_input_strings - 1] + (int)v2 + 6) = 0;
num_input_strings = v0 + 1;
return v1;
}
可以看到若输入符合给定的规范,num_input_strings变量就会+1,也就是输入的字符串总数,所以phase_defused的逻辑为,在6个phase执行结束后,检查0x603870
地址开始的三个参数是否与格式串"%d %d %s"匹配,且v3的值为DrEvil时进入secret_phase。我们在这个验证逻辑处打上断点并打印0x603870
开始的字符串:
这和我们phase_4中的输入一致,因此phase_4的输入后添加字符串DrEvil才能进入secret_phase。secret_phase汇编代码如下:
0000000000401242 <secret_phase>:
401242: 53 push %rbx
401243: e8 56 02 00 00 callq 40149e <read_line>
401248: ba 0a 00 00 00 mov $0xa,%edx
40124d: be 00 00 00 00 mov $0x0,%esi
401252: 48 89 c7 mov %rax,%rdi
401255: e8 76 f9 ff ff callq 400bd0 <strtol@plt>
40125a: 48 89 c3 mov %rax,%rbx
40125d: 8d 40 ff lea -0x1(%rax),%eax
401260: 3d e8 03 00 00 cmp $0x3e8,%eax
401265: 76 05 jbe 40126c <secret_phase+0x2a>
401267: e8 ce 01 00 00 callq 40143a <explode_bomb>
40126c: 89 de mov %ebx,%esi
40126e: bf f0 30 60 00 mov $0x6030f0,%edi
401273: e8 8c ff ff ff callq 401204 <fun7>
401278: 83 f8 02 cmp $0x2,%eax
40127b: 74 05 je 401282 <secret_phase+0x40>
40127d: e8 b8 01 00 00 callq 40143a <explode_bomb>
401282: bf 38 24 40 00 mov $0x402438,%edi
401287: e8 84 f8 ff ff callq 400b10 <puts@plt>
40128c: e8 33 03 00 00 callq 4015c4 <phase_defused>
401291: 5b pop %rbx
401292: c3 retq
401293: 90 nop
401294: 90 nop
401295: 90 nop
401296: 90 nop
401297: 90 nop
401298: 90 nop
401299: 90 nop
40129a: 90 nop
40129b: 90 nop
40129c: 90 nop
40129d: 90 nop
40129e: 90 nop
40129f: 90 nop
read_line函数调用后进入函数strtol,这个glibc库函数将字符串转化为长整型数据,三个参数依次为%rax,0,10
,其中寄存器%rax保存着read_line得到的字符串的地址。接着对转化后产生的整数上界作了限制,然后调用函数fun7:
__int64 __fastcall fun7(__int64 a1, __int64 a2)
{
__int64 result; // rax
if ( !a1 )
return 0xFFFFFFFFLL;
if ( *(_DWORD *)a1 > (int)a2 )
return 2 * (unsigned int)fun7(*(_QWORD *)(a1 + 8), a2);
result = 0LL;
if ( *(_DWORD *)a1 != (_DWORD)a2 )
return 2 * (unsigned int)fun7(*(_QWORD *)(a1 + 16), a2) + 1;
return result;
}
变量a1为secret_phase中%rdi存放的值0x6030f0
,a2则为我们输入的字符串转化的整数。乍一看这一段代码只是作递归调用,但我们先将0x6030f0
开始的64个Q_WORD打印出来:
每个地址的8byte和16byte偏移量的位置存放的值是内存地址,32个byte组成一个结点,存放着一个数据和两个结点地址,其结构为(盗了别人的图):
也就是二叉树的结构,所以fun7函数的逻辑可概括为:
- 如果结点地址为空,返回0xFFFFFFFF
- 如果数据值大于a2,返回2 * fun7(左结点,a2)
- 如果数据值小于a2,返回2 * fun7(右结点,a2) + 1
- 如果数据值等于a2,返回0
而我们secret_phase中最后需要的返回值为2,这个偶数应当由2 * (2 * 0 + 1)构造出来,拆解开就是左结点一次,右结点一次,最后相等,也就是0x16所存在的结点,换算成十进制即22。所以我们最终的input为:
Border relations with Canada have never been better.
1 2 4 8 16 32
0 207
7 0 DrEvil
9?>%&'
4 3 2 1 6 5
22
给一张bomb拆除截图:
Last
DrEvil还给我们留下了sig_handler以让我们一键通关,sig_handler函数逻辑如下:
void __noreturn sig_handler()
{
puts("So you think you can stop the bomb with ctrl-c, do you?");
sleep(3u);
__printf_chk(1LL, "Well...");
fflush(stdout);
sleep(1u);
puts("OK. :-)");
exit(16);
}
运行bomb后直接ctrl_c,也可以看到sig_handler的作用:
那sig_handler是如何生效的呢,全局查找sig_handler后在initialize_bomb函数中发现了蛛丝马迹:
__sighandler_t initialize_bomb()
{
return signal(2, (__sighandler_t)sig_handler);
}
这里使用了signal库函数将编号为2的信号与sig_handler绑定在一起,当触发这个信号时就调用sig_handler。查阅Linux信号列表后得知ctrl_c的编号为2:
SIGINT
产生方式: 键盘Ctrl+C
产生结果: 只对当前前台进程,和他的所在的进程组的每个进程都发送SIGINT信号,之后这些进程会执行信号处理程序再终止.
kill -l
命令可查看Linux支持的信号列表:
SIGINT即为触发sig_handler的信号。
Conclusion
本来想用markdown语法完成二叉树的绘制,但简书对markdown的作图支持不是很友善,之后可能会熟悉之后选择线上工具绘图。但总之,BombLab堂堂结束!