说明 给定一个可执行目标文件bomb,共有6个阶段,需要用gdb等程序分析反汇编,以获取每个阶段对应的密码。若密码输错则炸弹爆炸。其中一个炸弹可以从这里 获取(若提示无法安全下载可以复把链接复制到浏览器试试)。如果想要答案可以直接翻到最后一部分。
准备工作 实验环境:Ubuntu 20.04
获取反汇编 1 objdump -d bomb > asm.txt
就可以把反汇编以文件形式存到当前文件夹下。
炸弹程序框架 和bomb同时下发了一个bomb.c文件,为炸弹程序框架:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 #include <stdio.h> #include <stdlib.h> #include "support.h" #include "phases.h" FILE *infile; int main (int argc, char *argv[]) { char *input; if (argc == 1 ) { infile = stdin ; } else if (argc == 2 ) { if (!(infile = fopen(argv[1 ], "r" ))) { printf ("%s: Error: Couldn't open %s\n" , argv[0 ], argv[1 ]); exit (8 ); } } else { printf ("Usage: %s [<input_file>]\n" , argv[0 ]); exit (8 ); } initialize_bomb(); printf ("Welcome to my fiendish little bomb. You have 6 phases with\n" ); printf ("which to blow yourself up. Have a nice day!\n" ); input = read_line(); phase_1(input); phase_defused(); printf ("Phase 1 defused. How about the next one?\n" ); input = read_line(); phase_2(input); phase_defused(); printf ("That's number 2. Keep going!\n" ); input = read_line(); phase_3(input); phase_defused(); printf ("Halfway there!\n" ); input = read_line(); phase_4(input); phase_defused(); printf ("So you got that one. Try this one.\n" ); input = read_line(); phase_5(input); phase_defused(); printf ("Good work! On to the next...\n" ); input = read_line(); phase_6(input); phase_defused(); return 0 ; }
可以看到程序在每个阶段之前调用read_line函数读取一行,然后进入各个phase。
输入方式 程序可以直接从键盘获取输入;或者以文件给出头一部分输入,随后用键盘输入,用以避免破解后面阶段时对前面阶段答案的重复输入。以文件给出输入时,需要在bomb的目录下新建文本文件文件(如ans.txt),输入若干个阶段的答案,在运行时传入另一个参数:
或者在gdb内:
注:当运行时出现permission denied, 右键文件->Properties(属性)->Permissions(许可)->把execute的Allow executing file as program属性勾上即可。
要用到的GDB指令 disas 用于反汇编某一函数(有了objdump反汇编出的就可以不用了)。
break break指令用于设置断点。可以设置于反汇编的某地址处:
或者设置于某一函数开始处偏移一定位置处:
print 用于打印某处内存/寄存器的值,一般读内存加上类型强制转换:
1 2 3 4 5 6 7 8 9 10 11 (注意不是%rax而是$rax) print $rax 以16进制输出 print /x $rax 即0x8(%rsp),以四字节整型解释,不加强制转换会被视为void* print *(int*)($rsp+8) 以字符串方式解释0x403150处的数据 print (char*)0x403150
x 以一定格式输出字符串的值。 格式: x /nfu <addr> 其中: n为要显示的单元数; f为显示格式,本实验中有用的有d:十进制,x:十六进制 u为单元大小,b/h/w/g分别表示1/2/4/8字节。
1 2 以十六进制打印0x4050f0开始的80个8字节值 x /80xg 0x4050f0
阶段一 考察点:字符串比较;函数传参 反汇编如下(汇编中前面不空两格的是注释):
1 2 3 4 5 6 7 8 9 10 00000000004013ec <phase_1>: 4013ec: 48 83 ec 08 sub $0x8,%rsp 4013f0: be 50 31 40 00 mov $0x403150,%esi 4013f5: e8 b7 04 00 00 callq 4018b1 <strings_not_equal> 4013fa: 85 c0 test %eax,%eax 4013fc: 75 05 jne 401403 <phase_1+0x17> 4013fe: 48 83 c4 08 add $0x8,%rsp 401402: c3 retq 401403: e8 8b 05 00 00 callq 401993 <explode_bomb> 401408: eb f4 jmp 4013fe <phase_1+0x12>
可以看到,在程序调用strings_not_equal之前,将地址0x403150传入参数。猜测内存该处存放了答案字符串。用gdb设置断点,在此处查看该地址处的字符串:
即为phase_1的答案**I was trying to give Tina Fey more material.**。至于strings_not_equal函数具体如何实现,在这一阶段并不是重点,只要功能和函数名对上就不用费时间去考察了。
阶段二 考察点:循环 反汇编:
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 000000000040140a <phase_2>: let ebx = i 40140a: 53 push %rbx 40140b: 48 83 ec 20 sub $0x20,%rsp 40140f: 48 89 e6 mov %rsp,%rsi 401412: e8 a0 05 00 00 callq 4019b7 <read_six_numbers> 401417: 83 3c 24 01 cmpl $0x1,(%rsp) 40141b: 75 07 jne 401424 <phase_2+0x1a> 40141d: bb 01 00 00 00 mov $0x1,%ebx 401422: eb 0f jmp 401433 <phase_2+0x29> 401424: e8 6a 05 00 00 callq 401993 <explode_bomb> 401429: eb f2 jmp 40141d <phase_2+0x13> 40142b: e8 63 05 00 00 callq 401993 <explode_bomb> 401430: 83 c3 01 add $0x1,%ebx 401433: 83 fb 05 cmp $0x5,%ebx 401436: 7f 14 jg 40144c <phase_2+0x42> 401438: 48 63 d3 movslq %ebx,%rdx 40143b: 8d 43 ff lea -0x1(%rbx),%eax 40143e: 48 98 cltq 401440: 8b 04 84 mov (%rsp,%rax,4),%eax 401443: 01 c0 add %eax,%eax 401445: 39 04 94 cmp %eax,(%rsp,%rdx,4) 401448: 74 e6 je 401430 <phase_2+0x26> 40144a: eb df jmp 40142b <phase_2+0x21> 40144c: 48 83 c4 20 add $0x20,%rsp 401450: 5b pop %rbx 401451: c3 retq
在401412地址处调用read_six_numbers,推测要输入六个参数。随后比较(%rsp)与1,若(%rsp)!=1,则引爆炸弹,于是可知第一个参数(存放于栈指针处)为1.接下来程序初始化%ebx为1,并且在%ebx>5时跳转到40144c,通过phase_2.因此只需让程序顺利通过该循环即可。 设%ebx代表循环变量i,那么下面的循环相当于如下代码(省去了一些中间过程):
1 2 3 4 5 6 7 8 9 arg[0 ] = 1 ; int i;for (i=1 ;i<=5 ;i++) { int prev_arg = arg[i-1 ]; int now_arg = arg[i]; if (prev_arg * 2 != now_arg) explode_bomb(); } return ;
该程序检查6个参数是否为首项为1,公比为2的等比数列。因此答案即为等比数列前6项1 2 4 8 16 32 。
阶段三 考察点:switch(跳转表)
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 0000000000401452 <phase_3>: 401452: 48 83 ec 18 sub $0x18,%rsp 401456: 4c 8d 44 24 08 lea 0x8(%rsp),%r8 40145b: 48 8d 4c 24 07 lea 0x7(%rsp),%rcx 401460: 48 8d 54 24 0c lea 0xc(%rsp),%rdx 401465: be a6 31 40 00 mov $0x4031a6,%esi 40146a: b8 00 00 00 00 mov $0x0,%eax 40146f: e8 9c fc ff ff callq 401110 <__isoc99_sscanf@plt> 401474: 83 f8 02 cmp $0x2,%eax 401477: 7e 16 jle 40148f <phase_3+0x3d> 401479: 8b 44 24 0c mov 0xc(%rsp),%eax 40147d: 83 f8 07 cmp $0x7,%eax 401480: 0f 87 0d 01 00 00 ja 401593 <phase_3+0x141> 401486: 89 c0 mov %eax,%eax 401488: ff 24 c5 c0 31 40 00 jmpq *0x4031c0(,%rax,8) 40148f: e8 ff 04 00 00 callq 401993 <explode_bomb> 401494: eb e3 jmp 401479 <phase_3+0x27> 401496: 81 7c 24 08 b2 01 00 cmpl $0x1b2,0x8(%rsp) 40149d: 00 40149e: 75 0a jne 4014aa <phase_3+0x58> 4014a0: b8 65 00 00 00 mov $0x65,%eax 4014a5: e9 f3 00 00 00 jmpq 40159d <phase_3+0x14b> 4014aa: e8 e4 04 00 00 callq 401993 <explode_bomb> 4014af: b8 65 00 00 00 mov $0x65,%eax 4014b4: e9 e4 00 00 00 jmpq 40159d <phase_3+0x14b> 4014b9: 81 7c 24 08 46 02 00 cmpl $0x246,0x8(%rsp) 4014c0: 00 4014c1: 75 0a jne 4014cd <phase_3+0x7b> 4014c3: b8 74 00 00 00 mov $0x74,%eax 4014c8: e9 d0 00 00 00 jmpq 40159d <phase_3+0x14b> 4014cd: e8 c1 04 00 00 callq 401993 <explode_bomb> 4014d2: b8 74 00 00 00 mov $0x74,%eax 4014d7: e9 c1 00 00 00 jmpq 40159d <phase_3+0x14b> 4014dc: 81 7c 24 08 81 00 00 cmpl $0x81,0x8(%rsp) 4014e3: 00 4014e4: 75 0a jne 4014f0 <phase_3+0x9e> 4014e6: b8 6c 00 00 00 mov $0x6c,%eax 4014eb: e9 ad 00 00 00 jmpq 40159d <phase_3+0x14b> 4014f0: e8 9e 04 00 00 callq 401993 <explode_bomb> 4014f5: b8 6c 00 00 00 mov $0x6c,%eax 4014fa: e9 9e 00 00 00 jmpq 40159d <phase_3+0x14b> 4014ff: 81 7c 24 08 7f 02 00 cmpl $0x27f,0x8(%rsp) 401506: 00 401507: 75 0a jne 401513 <phase_3+0xc1> 401509: b8 68 00 00 00 mov $0x68,%eax 40150e: e9 8a 00 00 00 jmpq 40159d <phase_3+0x14b> 401513: e8 7b 04 00 00 callq 401993 <explode_bomb> 401518: b8 68 00 00 00 mov $0x68,%eax 40151d: eb 7e jmp 40159d <phase_3+0x14b> 40151f: 81 7c 24 08 12 01 00 cmpl $0x112,0x8(%rsp) 401526: 00 401527: 75 07 jne 401530 <phase_3+0xde> 401529: b8 6e 00 00 00 mov $0x6e,%eax 40152e: eb 6d jmp 40159d <phase_3+0x14b> 401530: e8 5e 04 00 00 callq 401993 <explode_bomb> 401535: b8 6e 00 00 00 mov $0x6e,%eax 40153a: eb 61 jmp 40159d <phase_3+0x14b> 40153c: 81 7c 24 08 74 03 00 cmpl $0x374,0x8(%rsp) 401543: 00 401544: 75 07 jne 40154d <phase_3+0xfb> 401546: b8 76 00 00 00 mov $0x76,%eax 40154b: eb 50 jmp 40159d <phase_3+0x14b> 40154d: e8 41 04 00 00 callq 401993 <explode_bomb> 401552: b8 76 00 00 00 mov $0x76,%eax 401557: eb 44 jmp 40159d <phase_3+0x14b> 401559: 81 7c 24 08 b9 03 00 cmpl $0x3b9,0x8(%rsp) 401560: 00 401561: 75 07 jne 40156a <phase_3+0x118> 401563: b8 75 00 00 00 mov $0x75,%eax 401568: eb 33 jmp 40159d <phase_3+0x14b> 40156a: e8 24 04 00 00 callq 401993 <explode_bomb> 40156f: b8 75 00 00 00 mov $0x75,%eax 401574: eb 27 jmp 40159d <phase_3+0x14b> 401576: 81 7c 24 08 af 00 00 cmpl $0xaf,0x8(%rsp) 40157d: 00 40157e: 75 07 jne 401587 <phase_3+0x135> 401580: b8 64 00 00 00 mov $0x64,%eax 401585: eb 16 jmp 40159d <phase_3+0x14b> 401587: e8 07 04 00 00 callq 401993 <explode_bomb> 40158c: b8 64 00 00 00 mov $0x64,%eax 401591: eb 0a jmp 40159d <phase_3+0x14b> 401593: e8 fb 03 00 00 callq 401993 <explode_bomb> 401598: b8 6c 00 00 00 mov $0x6c,%eax 40159d: 38 44 24 07 cmp %al,0x7(%rsp) 4015a1: 75 05 jne 4015a8 <phase_3+0x156> 4015a3: 48 83 c4 18 add $0x18,%rsp 4015a7: c3 retq 4015a8: e8 e6 03 00 00 callq 401993 <explode_bomb> 4015ad: eb f4 jmp 4015a3 <phase_3+0x151>
地址40146f调用了sscanf函数:获取格式化的字符串中的参数。接下来检查sscanf的返回值%eax:若%eax<=2,则引爆炸弹。 检查sscanf的格式串(存在0x4031a6):
第一个参数和第三个参数是int,第二个是char。从0x401456~0x401460传入的参数可以知道,三个参数分别存在0x7(%rsp),0x8(%rsp),0xc(%rsp)处。有了参数的输入格式,可以开始分析整个程序了。 在地址0x401480附近,可以得到如下结论:第一个参数一定要<=7,否则炸弹直接爆炸。紧接着为一条switch跳转语句:
跳转表位于0x4031c0处。而switch的对象是%rax(之前把第一个参数挪到%rax),而第一个参数<=7,所以猜测表项共有8项,即0~7.因此用gdb的x指令看0x4031c0处开始的八个八字节:
就获取了转移表的表项。选取一个最靠近函数末尾的进行分析:即跳转表偏移为7*8字节处的0x401576,当第一个参数为7时,跳转到0x401576.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 401576: 81 7c 24 08 af 00 00 cmpl $0xaf,0x8(%rsp) 40157d: 00 40157e: 75 07 jne 401587 <phase_3+0x135> 401580: b8 64 00 00 00 mov $0x64,%eax 401585: eb 16 jmp 40159d <phase_3+0x14b> 401587: e8 07 04 00 00 callq 401993 <explode_bomb> 40158c: b8 64 00 00 00 mov $0x64,%eax 401591: eb 0a jmp 40159d <phase_3+0x14b> 401593: e8 fb 03 00 00 callq 401993 <explode_bomb> 401598: b8 6c 00 00 00 mov $0x6c,%eax 40159d: 38 44 24 07 cmp %al,0x7(%rsp) 4015a1: 75 05 jne 4015a8 <phase_3+0x156> 4015a3: 48 83 c4 18 add $0x18,%rsp 4015a7: c3 retq 4015a8: e8 e6 03 00 00 callq 401993 <explode_bomb> 4015ad: eb f4 jmp 4015a3 <phase_3+0x151>
该部分的逻辑较简单。比较0xaf与0x8(%rsp);%al与0x7(%rsp).%al即%eax的低位字节,0x64代表ASCII中的’d’。因此只需使0x7(%rsp)和0x8(%rsp)取到’d’和0xaf(175)即可。因此第一个参数为7 ,第二个参数为’d ’,第三个参数为175 .
阶段四 考察点:递归
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 00000000004015ec <phase_4>: 4015ec: 48 83 ec 18 sub $0x18,%rsp 4015f0: 48 8d 4c 24 08 lea 0x8(%rsp),%rcx 4015f5: 48 8d 54 24 0c lea 0xc(%rsp),%rdx 4015fa: be 4f 33 40 00 mov $0x40334f,%esi 4015ff: b8 00 00 00 00 mov $0x0,%eax 401604: e8 07 fb ff ff callq 401110 <__isoc99_sscanf@plt> 401609: 83 f8 02 cmp $0x2,%eax 40160c: 75 0d jne 40161b <phase_4+0x2f> 40160e: 8b 44 24 0c mov 0xc(%rsp),%eax 401612: 85 c0 test %eax,%eax 401614: 78 05 js 40161b <phase_4+0x2f> 401616: 83 f8 0e cmp $0xe,%eax 401619: 7e 05 jle 401620 <phase_4+0x34> 40161b: e8 73 03 00 00 callq 401993 <explode_bomb> 401620: ba 0e 00 00 00 mov $0xe,%edx 401625: be 00 00 00 00 mov $0x0,%esi 40162a: 8b 7c 24 0c mov 0xc(%rsp),%edi 40162e: e8 7c ff ff ff callq 4015af <func4> 401633: 83 f8 04 cmp $0x4,%eax 401636: 75 07 jne 40163f <phase_4+0x53> 401638: 83 7c 24 08 04 cmpl $0x4,0x8(%rsp) 40163d: 74 05 je 401644 <phase_4+0x58> 40163f: e8 4f 03 00 00 callq 401993 <explode_bomb> 401644: 48 83 c4 18 add $0x18,%rsp 401648: c3 retq
0x401604处调用sscanf;紧接着比较%eax与2,仅当%eax==2时,炸弹才不会爆炸。因此输入参数为两个。接下来的语句限制arg[0](第一个参数,0xc(%rsp))必须大于等于0(由js指令看出);之后(0x401620~0x40162a)分别把arg[0],0,0xe移动到func4的三个参数上。即调用func4(arg[0], 0, 15)。0x401633处比较返回值%eax是否等于4,因此,需要分析当arg[0]为多少时,func4的值为4.于是对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 x in %edi, y in %esi, z in %edx 00000000004015af <func4>: 4015af: 48 83 ec 08 sub $0x8,%rsp 4015b3: 89 d1 mov %edx,%ecx 4015b5: 29 f1 sub %esi,%ecx int ret = ((((z - y) >> 31) + (z - y)) >> 1) + y; 4015b7: 89 c8 mov %ecx,%eax 4015b9: c1 e8 1f shr $0x1f,%eax 4015bc: 01 c8 add %ecx,%eax 4015be: d1 f8 sar %eax 4015c0: 01 f0 add %esi,%eax 4015c2: 39 f8 cmp %edi,%eax 4015c4: 7f 0c jg 4015d2 <func4+0x23> 4015c6: 7c 16 jl 4015de <func4+0x2f> if(ret == 0) return 0; 4015c8: b8 00 00 00 00 mov $0x0,%eax 4015cd: 48 83 c4 08 add $0x8,%rsp 4015d1: c3 retq if(ret > 0) return 2 * func4(x, y, ret - 1); 4015d2: 8d 50 ff lea -0x1(%rax),%edx 4015d5: e8 d5 ff ff ff callq 4015af <func4> 4015da: 01 c0 add %eax,%eax 4015dc: eb ef jmp 4015cd <func4+0x1e> if(ret < 0) return 2 * func4(x, ret + 1, z); 4015de: 8d 70 01 lea 0x1(%rax),%esi 4015e1: e8 c9 ff ff ff callq 4015af <func4> 4015e6: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax 4015ea: eb e1 jmp 4015cd <func4+0x1e>
在函数开始前标注了参数所在寄存器。在0x4015c2处比较%edi与%eax的值,%edi为传入的第一个参数x;%eax经过前几行的一系列操作,其值为449行注释的表达式。接下来分别于459,465,472行标注了比较的三种情况。可以将func4近似为如下的C语言递归函数:
1 2 3 4 5 6 int func4 (int x, int l, int r) { int mid = (((r - l) >> 31 ) + (r - l))/ 2 + l; if (mid == x) return 0 ; else if (mid > x) return 2 * func(x, l,mid - 1 ); else return 2 * func(x, mid + 1 , r) + 1 ; }
用程序进行检验,很容易发现当arg[0]=2时,func4的值为4.
接下来回到phase_4的末尾部分:只要0x8(%rsp)(第二个参数)==4即可。因此答案为2 4 .
阶段五 考察点:数组(指针)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 0000000000401649 <phase_5>: 401649: 53 push %rbx 40164a: 48 89 fb mov %rdi,%rbx 40164d: e8 4b 02 00 00 callq 40189d <string_length> 401652: 83 f8 06 cmp $0x6,%eax 401655: 75 25 jne 40167c <phase_5+0x33> 401657: b9 00 00 00 00 mov $0x0,%ecx 40165c: b8 00 00 00 00 mov $0x0,%eax 401661: 83 f8 05 cmp $0x5,%eax 401664: 7f 1d jg 401683 <phase_5+0x3a> 401666: 48 63 d0 movslq %eax,%rdx 401669: 0f b6 14 13 movzbl (%rbx,%rdx,1),%edx 40166d: 83 e2 0f and $0xf,%edx 401670: 03 0c 95 00 32 40 00 add 0x403200(,%rdx,4),%ecx 401677: 83 c0 01 add $0x1,%eax 40167a: eb e5 jmp 401661 <phase_5+0x18> 40167c: e8 12 03 00 00 callq 401993 <explode_bomb> 401681: eb d4 jmp 401657 <phase_5+0xe> 401683: 83 f9 31 cmp $0x31,%ecx 401686: 75 02 jne 40168a <phase_5+0x41> 401688: 5b pop %rbx 401689: c3 retq 40168a: e8 04 03 00 00 callq 401993 <explode_bomb> 40168f: eb f7 jmp 401688 <phase_5+0x3f>
注意到0x40164d处调用了string_length函数,在0x401652处将返回值与6比较,推测输入为一个长度为6的字符串。接下来初始化%ecx=0,%eax=0,在%eax>5时,跳转至地址0x401683.因此下面是一个循环变量i(%eax)从0到5共6次的循环.接下来,0x401669对(%rbx,%rdx,1)做零扩展传送到%edx,将其按位与0xf(即取低四位)。先检验一下该内存地址是什么: 这次检测中输入了字符串”string”,而用char*方式解释%rbx开始的位置正好就是输入的字符串”string”.因此,%rbx就是输入的字符串的首地址,而%rdx=%eax是循环变量i。因此循环每次取arg[i],即输入字符串的第i位字符;并将其按位与0xf后的值存入%edx中。 继续看0x401670处:
1 401670: 03 0c 95 00 32 40 00 add 0x403200(,%rdx,4),%ecx
发现它把0x403200偏移4*%edx处的四字节值累加到%ecx.因此可以推测出0x403200处有一个含16个int的数组(因为%edx是按位与0xf得来的,共有16种可能)。用x指令查看该数组: 该数组即
1 2 int A[] = {2 , 10 , 6 , 1 , 12 , 16 , 9 , 3 , 4 , 7 , 14 , 5 , 11 , 8 , 15 , 13 };
查看phase_5出循环后的代码:其比较%ecx是否等于0x31=49.
1 401683: 83 f9 31 cmp $0x31,%ecx
经过上述分析,可以将过程转化为如下C代码:
1 2 3 4 5 6 7 8 9 10 11 void phase_5 () { char input[10 ]; scanf ("%s" , input); if (strlen (input) != 6 ) explode_bomb(); int i, sum = 0 ; for (i = 0 ; i < 6 ; i++) { int index = input[i] & 0xf ; sum += A[index]; } if (sum != 49 ) explode_bomb(); }
因此,关键就在于选择输入字符串的值,使得6次累加的值为49.可以在A数组中找到7+7+7+7+7+14=49,即5个A[9]和1个A[10],接下来只要找到char型的ch1,ch2使得(ch1&0xf == 9) && (ch2&0xf == 10)即可。选择ch1=’i’,ch2=’j’即满足要求。因此只要输入5个i和1个j 即可。(顺序可以调换)
阶段六 考察点:结构体,链表 phase_6的反汇编有100多行,分为多个步骤,接下来分步骤说明。
1 2 3 4 5 6 7 0000000000401691 <phase_6>: 401691: 41 54 push %r12 401693: 55 push %rbp 401694: 53 push %rbx 401695: 48 83 ec 50 sub $0x50,%rsp 401699: 48 8d 74 24 30 lea 0x30(%rsp),%rsi 40169e: e8 14 03 00 00 callq 4019b7 <read_six_numbers>
首先与前面的阶段一样,读入6个数字;由传入的参数(0x401699)可以知道,第一个读入的数字存在0x30(%rsp)处。
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 let rbp = i, ebx = j 4016a3: bd 00 00 00 00 mov $0x0,%ebp 4016a8: eb 29 jmp 4016d3 <phase_6+0x42> 4016aa: e8 e4 02 00 00 callq 401993 <explode_bomb> 4016af: eb 36 jmp 4016e7 <phase_6+0x56> 4016b1: e8 dd 02 00 00 callq 401993 <explode_bomb> 4016b6: 83 c3 01 add $0x1,%ebx 4016b9: 83 fb 05 cmp $0x5,%ebx 4016bc: 7f 12 jg 4016d0 <phase_6+0x3f> 4016be: 48 63 c5 movslq %ebp,%rax 4016c1: 48 63 d3 movslq %ebx,%rdx edi = arg[j] 4016c4: 8b 7c 94 30 mov 0x30(%rsp,%rdx,4),%edi compare arg[i] and arg[j] 4016c8: 39 7c 84 30 cmp %edi,0x30(%rsp,%rax,4) arg[i] != arg[j],jump to bomb_explode 4016cc: 75 e8 jne 4016b6 <phase_6+0x25> 4016ce: eb e1 jmp 4016b1 <phase_6+0x20> 4016d0: 44 89 e5 mov %r12d,%ebp 4016d3: 83 fd 05 cmp $0x5,%ebp 4016d6: 7f 18 jg 4016f0 <phase_6+0x5f> 4016d8: 48 63 c5 movslq %ebp,%rax 4016db: 8b 44 84 30 mov 0x30(%rsp,%rax,4),%eax 4016df: 83 e8 01 sub $0x1,%eax 4016e2: 83 f8 05 cmp $0x5,%eax 4016e5: 77 c3 ja 4016aa <phase_6+0x19> 4016e7: 44 8d 65 01 lea 0x1(%rbp),%r12d 4016eb: 44 89 e3 mov %r12d,%ebx 4016ee: eb c9 jmp 4016b9 <phase_6+0x28>
接下来(经过分析)是一个二重循环。首先初始化%ebp=0,设其为i;随后jmp到0x4016d3,当i>5时,结束这一部分。否则,把%ebp作符号扩展传送到%rax,把0x30(%rsp,%rax,4)即arg[i](rsp+0x30+4*%rax)传送到%eax.然后比较%eax-1与5(即%eax与6),若%eax>6,炸弹爆炸。因此推测出arg[i]<=6(i=0,1,…,5) 在0x4016e7,0x4016eb两行把%rbp+1即i+1赋给%ebx,跳转到0x4016b9.紧接着1比较%ebx与5,当%ebx>5时跳转(正因如此确定这是一个二重循环)。设%ebx为j,接下来的几行比较arg[j]与arg[i],当arg[i] == arg[j]时,炸弹爆炸。因此这一部分可以转化为如下的C代码:
1 2 3 4 5 6 7 8 int i, j;for (i = 0 ; i < 6 ; i++) { for (j = i + 1 ; j < 6 ; j++) { if (arg[i] == arg[j]) { explode_bomb(); } } }
从这一段C代码就可以看出这一部分是确保对任意的i != j,arg[i] != arg[j].再结合之前的arg[i]<=6,可以得出,输入应当是一个1~6的排列。
继续下一段:
1 2 3 4 5 6 7 8 9 10 let eax = i 4016f0: b8 00 00 00 00 mov $0x0,%eax 4016f5: eb 13 jmp 40170a <phase_6+0x79> 4016f7: 48 63 c8 movslq %eax,%rcx 4016fa: ba 07 00 00 00 mov $0x7,%edx 4016ff: 2b 54 8c 30 sub 0x30(%rsp,%rcx,4),%edx 401703: 89 54 8c 30 mov %edx,0x30(%rsp,%rcx,4) 401707: 83 c0 01 add $0x1,%eax 40170a: 83 f8 05 cmp $0x5,%eax 40170d: 7e e8 jle 4016f7 <phase_6+0x66>
开始初始化%eax=0,立即跳转到0x40170a与5进行比较.由经验,%eax一般为第一层循环变量,设其为i;在i<=5时,跳转到0x4016f7,接下来的0x4016fa~0x401703将7-arg[i]赋值给arg[i],即对参数进行了一次变换,但六个参数还是一个1~6的排列。
下一段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let esi = i, eax = j, edx = (pointer*)ptr, rcx = k 40170f: be 00 00 00 00 mov $0x0,%esi 401714: eb 17 jmp 40172d <phase_6+0x9c> 401716: 48 8b 52 08 mov 0x8(%rdx),%rdx 40171a: 83 c0 01 add $0x1,%eax 40171d: 48 63 ce movslq %esi,%rcx 401720: 39 44 8c 30 cmp %eax,0x30(%rsp,%rcx,4) 401724: 7f f0 jg 401716 <phase_6+0x85> 401726: 48 89 14 cc mov %rdx,(%rsp,%rcx,8) 40172a: 83 c6 01 add $0x1,%esi 40172d: 83 fe 05 cmp $0x5,%esi 401730: 7f 0c jg 40173e <phase_6+0xad> loop initialize: 401732: b8 01 00 00 00 mov $0x1,%eax 401737: ba d0 52 40 00 mov $0x4052d0,%edx 40173c: eb df jmp 40171d <phase_6+0x8c>
本段初始化%esi为0,随后jmp到0x40172d与5比较。由经验设%esi=i,当i<=5时,把%eax赋值1,%edx赋值0x4052d0.因此推测,%eax是一个int型临时变量j;%edx是一个指针型临时变量,但不确定其类型,记为pointer *ptr. 做完这些初始化后jmp到0x40171d,把i赋给%rcx(记为k,这些对应关系在这段汇编的开始标注);比较arg[k]与j,若arg[k]>j,跳到0x401716,做
1 401716: 48 8b 52 08 mov 0x8(%rdx),%rdx
这样一条操作,随后j++。图中指令形式很类似于链表,即跳转到下一个结点:ptr=ptr->next.把自己指向的位置偏移8处赋给自身,说明链表节点的next指针在首地址偏移8处。因此节点数据大小应为8字节。推测链表结构声明如下:
1 2 3 4 struct node { long val; node *next; };
继续分析:程序做完上面的操作后又会落到判断arg[k]和j的大小关系处,可以把这里转化为一个while循环。当跳出while循环后,
1 2 401726: 48 89 14 cc mov %rdx,(%rsp,%rcx,8) 40172a: 83 c6 01 add $0x1,%esi
把%rdx(ptr)赋值给rsp偏移为8*%rcx(8*i)处,因此这部分汇编可转换为如下C语言(其中的rsp[k]可视为一个指针数组:pointer *rsp[6]; ): 1 2 3 4 5 6 7 8 9 10 11 12 int i, j, k, ptr;for (i = 0 ; i <= 5 ;i++) { k = i; j = 1 ; ptr = 0x4052d0 ; while (arg[k] > j) { ptr = ptr->next; j++; } rsp[k] = ptr; }
可以看到,对于每个arg[i],ptr初始化都在0x4052d0处,每次j自增,ptr都会指向自己的下一个元素。最后,把ptr最终指向的值赋给rsp寄存器偏移k的位置。注意到在单次循环内k没有改变,因此可以把k和i等同,从而消除k:
1 2 3 4 5 6 7 8 9 10 int i, j, ptr;for (i = 0 ; i <= 5 ;i++) { j = 1 ; ptr = 0x4052d0 ; while (arg[i] > j) { ptr = ptr->next; j++; } rsp[i] = ptr; }
为了验证猜想,我们需要查看运行时0x4052d0的内存。经过上面的假设,可以得到节点大小为16字节,应该共有6个这样的节点。因此,运行时用x指令查看: 查看的结果很明显:结构体名为node1~node6,每个节点的第二个数据首尾相接(如,0x4052d0的第二个数据段为0x4052e0,正好是下一块内存的地址),第一个数据应当就是数据域。 验证了上面的猜想后,就可以解释这段代码的行为了:rsp[i]指向从0x4052d0开始的第arg[i]个节点(更准确地说,rsp[i]保存该节点的首地址)。如arg[0]=5,那么j应该向后跳4次,ptr指向第5个节点(0x405310)。由于arg[i]是一个排列,因此rsp[i]指向的内容也应该不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let rbx = ptr1, rcx = ptr2, eax = i, rdx = j, ebp = k The arg[i]-th node's next node is the arg[i+1]-th node 40173e: 48 8b 1c 24 mov (%rsp),%rbx 401742: 48 89 d9 mov %rbx,%rcx 401745: b8 01 00 00 00 mov $0x1,%eax 40174a: eb 11 jmp 40175d <phase_6+0xcc> 40174c: 48 63 d0 movslq %eax,%rdx 40174f: 48 8b 14 d4 mov (%rsp,%rdx,8),%rdx 401753: 48 89 51 08 mov %rdx,0x8(%rcx) 401757: 83 c0 01 add $0x1,%eax 40175a: 48 89 d1 mov %rdx,%rcx 40175d: 83 f8 05 cmp $0x5,%eax 401760: 7e ea jle 40174c <phase_6+0xbb> 401762: 48 c7 41 08 00 00 00 movq $0x0,0x8(%rcx) 401769: 00
下一部分:把rsp[0]移动到%rbx,设%rbx为node *ptr1;又将%rbx传送给%rcx,设%rcx为node *ptr2.%eax初始化为1,设为i。对于i<=5的情况,程序跳转到0x40174c:把%rdx赋值为rsp[i](在0x40174f),再把0x8(%rcx)赋值%rdx,即ptr2->next = rsp[i]; 0x40175a处的mov指令把%rcx赋值%rdx=rsp[i].出循环后(0x401762)给ptr2->next赋值0,即NULL.结合起来即
1 2 3 4 5 6 node *ptr2 = rsp[0 ]; for (int i = 1 ; i <= 5 ; i++) { ptr2->next = rsp[i]; ptr2 = ptr2->next; } ptr->next = NULL ;
因此,这段代码的本质即对链表进行了一次重排序,规则为:第arg[i]个节点的下一个节点为第arg[i+1]个节点。如:arg[]={3,2,1,6,5,4},那么第3个节点为头,随后连接第2个节点,以此类推。(这里的第i个节点指的是内存的排列顺序:0x4052d0为第一个,0x4052e0为第二个)
接下来就是phase_6的最后一部分:对重排序的链表判断是否符合要求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let rbx = ptr1, rcx = ptr2, eax = i, rdx = j, ebp = k 40176a: bd 00 00 00 00 mov $0x0,%ebp 40176f: eb 07 jmp 401778 <phase_6+0xe7> 401771: 48 8b 5b 08 mov 0x8(%rbx),%rbx 401775: 83 c5 01 add $0x1,%ebp 401778: 83 fd 04 cmp $0x4,%ebp 40177b: 7f 11 jg 40178e <phase_6+0xfd> 40177d: 48 8b 43 08 mov 0x8(%rbx),%rax 401781: 8b 00 mov (%rax),%eax 401783: 39 03 cmp %eax,(%rbx) 401785: 7d ea jge 401771 <phase_6+0xe0> 401787: e8 07 02 00 00 callq 401993 <explode_bomb> 40178c: eb e3 jmp 401771 <phase_6+0xe0> 40178e: 48 83 c4 50 add $0x50,%rsp 401792: 5b pop %rbx 401793: 5d pop %rbp 401794: 41 5c pop %r12 401796: c3 retq
把%ebp初始化为0,随即跳到0x401778.将其与4比较。若%ebp>4,就成功到达phase_6的结尾,因此只需度过这个循环即可。0x40177d把ptr1->next赋值给%rax,然后把(%rax)即ptr1->next->val(由于结构体偏移为0的地方为其数据段)赋值给%eax,比较%eax=ptr1->next->val与(%rbx)=ptr1->val.这里比较的逻辑很明显:比较ptr1指向的节点的数据是否比ptr1->next的大。还有一点需要注意:由于cmp指令其中一个参数是%eax,因此cmp应补为cmpl,按int型进行比较,而非long型。
那么答案的构造方法就清楚了:只需在重排序步骤中,使得重排序后的链表的数据递减。之前内存中各节点的数据从左到右分别为(按int型解释,用gdb print指令)258,355,577,973,465,330. 那么表头应该为973,随后是577,465,355,330,258. 按重排序的规则,很容易构造出符合的arg[]={4,3,5,2,6,1}.(第四个节点后面是第三个,然后是第二个,…)但是在前面程序又做了一步处理:arg[i] = 7 - arg[i].因此,需要把这个arg数组还原(用7减)。因此最初输入的arg就应该是{3,4,2,5,1,6}. 在漫长的分析后,终于得出六个参数依次为:3 4 2 5 1 6 .
隐藏阶段考察点:递归,结构体,二叉查找树 在破解phase_6的时候就可以发现下面还有fun7和secret_phase函数。搜索secret_phase的调用,发现其调用在phase_defused中:
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 0000000000401b24 <phase_defused>: 401b24: 83 3d 41 3c 00 00 06 cmpl $0x6,0x3c41(%rip) # 40576c <num_input_strings> 401b2b: 74 01 je 401b2e <phase_defused+0xa> 401b2d: c3 retq 401b2e: 48 83 ec 68 sub $0x68,%rsp 401b32: 4c 8d 44 24 10 lea 0x10(%rsp),%r8 401b37: 48 8d 4c 24 08 lea 0x8(%rsp),%rcx 401b3c: 48 8d 54 24 0c lea 0xc(%rsp),%rdx 401b41: be 99 33 40 00 mov $0x403399,%esi 401b46: bf 70 58 40 00 mov $0x405870,%edi 401b4b: b8 00 00 00 00 mov $0x0,%eax 401b50: e8 bb f5 ff ff callq 401110 <__isoc99_sscanf@plt> 401b55: 83 f8 03 cmp $0x3,%eax 401b58: 74 0f je 401b69 <phase_defused+0x45> 401b5a: bf d8 32 40 00 mov $0x4032d8,%edi 401b5f: e8 fc f4 ff ff callq 401060 <puts@plt> 401b64: 48 83 c4 68 add $0x68,%rsp 401b68: c3 retq 401b69: be a2 33 40 00 mov $0x4033a2,%esi 401b6e: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi 401b73: e8 39 fd ff ff callq 4018b1 <strings_not_equal> 401b78: 85 c0 test %eax,%eax 401b7a: 75 de jne 401b5a <phase_defused+0x36> 401b7c: bf 78 32 40 00 mov $0x403278,%edi 401b81: e8 da f4 ff ff callq 401060 <puts@plt> 401b86: bf a0 32 40 00 mov $0x4032a0,%edi 401b8b: e8 d0 f4 ff ff callq 401060 <puts@plt> 401b90: b8 00 00 00 00 mov $0x0,%eax call secret_phase! 401b95: e8 3a fc ff ff callq 4017d4 <secret_phase> 401b9a: eb be jmp 401b5a <phase_defused+0x36>
phase_defused是一个在每次通过阶段后都调用的一个函数。为了使控制到达secret_phase,需要在0x401b2b处通过检验0x3c41(%rip)==6.注意到后面标记40576c ,推测它是0x3c41(%rip)处的一个int型。(事实上0x3c41+%rip=0x3c41+0x401b2b=0x40576c,考虑到%rip应该指向下一条指令) 经过多次检验,num_input_strings是输入字符串数(由于字符串不对就会立即爆炸,故也是通过阶段数)。那么隐藏阶段就是在第六阶段通过后进行判断的。在0x401b2e处设置断点,通过六阶段后控制也的确转移到这里。再看0x401b55处的判断:cmp $0x3,%eax,判断之前的一个sscanf的读入参数的个数是否为3.既然此处没有读入(完成六阶段后程序直接结束),那么这个sscanf就应该读的是之前的字符串。可以看到第一个参数是%edi=0x405870。在之前读入字符串的函数(read_line)后设置断点,可以发现0x405870是第四次读入的答案首地址。表面上第四阶段读入两个数字(2,4),但隐藏阶段需要在这里额外输入另一个参数触发。 查看sscanf的格式串(存于0x403399),可以直到第三个参数应为字符串。注意到后面调用了一次strings_not_equal,用一阶段的方法可以得到这个字符串为”DrEvil”.于是在第四阶段后面额外附加上这个字符串,进入了隐藏阶段。 接下来分析secret_phase:下面为其反汇编:
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 00000000004017d4 <secret_phase>: 4017d4: 53 push %rbx 4017d5: e8 1c 02 00 00 callq 4019f6 <read_line> 4017da: 48 89 c7 mov %rax,%rdi 4017dd: e8 5e f9 ff ff callq 401140 <atoi@plt> 4017e2: 89 c3 mov %eax,%ebx 4017e4: 8d 40 ff lea -0x1(%rax),%eax 4017e7: 3d e8 03 00 00 cmp $0x3e8,%eax if(val > 1001) explode_bomb(); 4017ec: 77 22 ja 401810 <secret_phase+0x3c> 4017ee: 89 de mov %ebx,%esi 4017f0: bf f0 50 40 00 mov $0x4050f0,%edi tmp = fun7(0x4050f0, val) 4017f5: e8 9d ff ff ff callq 401797 <fun7> 4017fa: 83 f8 03 cmp $0x3,%eax if(tmp != 3) explode_bomb(); 4017fd: 75 18 jne 401817 <secret_phase+0x43> 4017ff: bf 80 31 40 00 mov $0x403180,%edi 401804: e8 57 f8 ff ff callq 401060 <puts@plt> 401809: e8 16 03 00 00 callq 401b24 <phase_defused> 40180e: 5b pop %rbx 40180f: c3 retq 401810: e8 7e 01 00 00 callq 401993 <explode_bomb> 401815: eb d7 jmp 4017ee <secret_phase+0x1a> 401817: e8 77 01 00 00 callq 401993 <explode_bomb> 40181c: eb e1 jmp 4017ff <secret_phase+0x2b>
可以看到0x4017dd调用了atoi ,那么输入为一个字符串,并将其转为数字。0x4017e2~0x404017ec判断了该数字应<=1001.随后调用fun7(0x4050f0, %edi) (%edi就是输入的值),将其返回值与0x3比较。因此还需分析0x4050f0处的内存和fun7的行为,使得fun7返回3.先看0x4050f0处的内存: 可以看到和阶段6类似,每个结构体大小为32字节,每个数据占8字节;推测第一个为数据域,而第二个第三个显然为地址形式,推测为指针。第四个应为对齐要求,所有都为0.对于每个结构体有两个指针,很容易想到它是一棵二叉树。
可以推测节点定义如下:
1 2 3 4 5 struct node { long val; node *lson; node *rson; };
继续检查内存,发现这棵二叉树共有15个节点,正好符合4层满二叉树的节点个数。按照内存画出树的结构: (只画出数据,因为地址意义不大) 可以看到它正好符合二叉查找树的结构:左儿子<根<右儿子。那么现在只需要弄清楚fun7的行为即可。fun7的反汇编:
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 let rdi = ptr, esi = val 0000000000401797 <fun7>: if(ptr == NULL) return -1; 401797: 48 85 ff test %rdi,%rdi 40179a: 74 32 je 4017ce <fun7+0x37> 40179c: 48 83 ec 08 sub $0x8,%rsp compare val and ptr->val 4017a0: 8b 07 mov (%rdi),%eax 4017a2: 39 f0 cmp %esi,%eax 4017a4: 7f 0c jg 4017b2 <fun7+0x1b> 4017a6: 75 17 jne 4017bf <fun7+0x28> drop to here(when val == ptr->val): 4017a8: b8 00 00 00 00 mov $0x0,%eax 4017ad: 48 83 c4 08 add $0x8,%rsp 4017b1: c3 retq if(val < ptr->val) return 2 * fun7(ptr->lson, val); 4017b2: 48 8b 7f 08 mov 0x8(%rdi),%rdi 4017b6: e8 dc ff ff ff callq 401797 <fun7> 4017bb: 01 c0 add %eax,%eax 4017bd: eb ee jmp 4017ad <fun7+0x16> if(val > ptr->val) return 2 * fun7(ptr->rson, val); 4017bf: 48 8b 7f 10 mov 0x10(%rdi),%rdi 4017c3: e8 cf ff ff ff callq 401797 <fun7> 4017c8: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax 4017cc: eb df jmp 4017ad <fun7+0x16> 4017ce: b8 ff ff ff ff mov $0xffffffff,%eax 4017d3: c3 retq
fun7的行为比较清晰。如注释标记,先检查第一个参数%rdi(称为ptr)是否为0(即NULL),若为NULL,返回0xffffffff即-1;然后把(%rdi),即ptr->val传送给%eax,将其与第二个参数%esi(称为val)比较,这与二叉查找树的查找过程非常相似。随后分出三种情况:大于/等于/小于。 $(1)$对于val==ptr->val的情况,直接下落到0x4017a8,返回0; $(2)$对于valval的情况,返回2*fun7(0x8(%rdi),%esi),由节点的定义可知就是2*func(ptr->lson,val); $(3)$对于val>ptr->val的情况,返回 2*fun7(0x10(%rdi),%esi)+1=2*fun7(ptr->rson,val)+1. 可以写出C代码:
1 2 3 4 5 6 int fun7 (node *ptr, int val) { if (ptr == NULL ) return -1 ; if (ptr->val == val) return 0 ; if (val < ptr->val) return 2 * fun7(ptr->lson, val); if (val > ptr->val) return 2 * fun7(ptr->rson, val); }
接下来只需要寻找输入val,使得fun7(root,val)返回3即可。3可以通过 $v_1=20+1=1;$ $v_2=2 v_1+1=3$ 得到。从二叉查找树的根root向下找,由于都是$2v+1$的形式,因此需要走两次右儿子,对照二叉树图可得该值为0x6b=107.也就是说,若输入107,在0x6b处函数返回0,然后在0x32处返回2*0+1=1,最后在根处返回21+1=3.因此,输入值应当为*107 .
答案 以下为七个阶段(包含隐藏阶段)的ans.txt内容(答案可能不唯一,如第三阶段和第五阶段):
1 2 3 4 5 6 7 I was trying to give Tina Fey more material. 1 2 4 8 16 32 7 d 175 2 4 DrEvil iiiiij 3 4 2 5 1 6 107