关于学习资源和视频等问题可以参考第一次写的Data Lab 开头中提及的相关问题。
这次的实验是Bomb Lab,也就是拆弹实验,听起来就很刺激~
顾名思义,那就是拆错了就会BOOM,然后扣分。按照CMU官方PDF的说法,那就是每炸一次扣半分,总共可以炸满20分,但是对于我们这些Self-Study的同学就没这么多要求了hhh
准备
对应课程
这次的Bomb Lab作业,如果是自学,在B站课程 中,应该大致需要完成P5~P9的学习,大致理解汇编语言的语法等。
P.S. 关于汇编语法,主要有Intel和AT&T两类。本质上并没有太大区别,主要是源操作数和目的操作数的顺序恰好相反。就像教授所讲的,就像开车换方向舵的感觉一样,如果只熟悉一种,一开始感觉会有点怪怪的。
之前在本专业学习“微机原理”8086/8088汇编语言时,我也是主要接触了Intel语法,由于课本和讲课都使用了AT&T的语法,那我也就强行换一下“舵”呗,小问题咯。
课程文件
相关的作业还是在CMU的官网上,相同位置:
Lab Assignments
在Bomb Lab一栏里面,我们可以查看相关文件,例如:
下载后并解压的文件如图所示:
这次的文件目录也是非常的简洁。
使用环境
和上期同样,我们还是使用了Arch Linux环境,配合GDB等程序调试工具:
使用建议
文件说明
文件夹里提供了一个可执行文件和一个bomb.c
文件。
打开bomb.c
文件,我们看到了一个main
函数的相关代码:
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 ; }
里面大致描述了程序的相关功能,但是很清晰的看到phase_1
到phase_6
等方法的代码并没有给出,其他的c
文件也没有给出,但是给出了最终生成的可执行文件bomb
。
P.S. 换句话说,这里降低了难度,给出了main
函数的相关代码,好让我们更加专注于各个题目的具体实现,不用去研究长长的main
函数到底做了啥。
总而言之,这里总共有6
道题,每道题会调用phase_1
到phase_6
的相关方法,并传入标准输入内容,如果我们输入的内容符合程序的要求,就会通过这一题,也就是“拆弹成功”,反之炸弹就会爆炸,界面如下:
而至于如何避免爆炸,那自然就需要我们对这个可执行文件进行反编译处理,通过阅读汇编程序来推测程序的相关意图。
交互方式
由于我们这类的菜鸡可能经常做错题目害得炸弹爆炸,而每次炸完就得从头运行程序,从头输入。这样的简单重复劳动造成了大量的emotional damage,而且一不小心操作手滑还可能导致半路爆炸。
因此,这里还贴心的为我们准备了文件输入的交互方式,我们可以事先将每一道题目的答案存在文件里,通过读取文件的方式输入,省事又简单:
题目
objdump
是GCC工具中的反汇编工具。首先我们使用objdump
工具对bomb
可执行文件进行反汇编,并将它写入到文件中:
1 [root@archlinux ~] objdump -d ./bomb > bomb-disassemble
我们看到main
函数如下:
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 000000000400da0 <main>: 400da0: 53 push %rbx 400da1: 83 ff 01 cmp $0x1,%edi 400da4: 75 10 jne 400db6 <main+0x16> 400da6: 48 8b 05 9b 29 20 00 mov 0x20299b(%rip),%rax # 603748 <stdin@@GLIBC_2.2.5> 400dad: 48 89 05 b4 29 20 00 mov %rax,0x2029b4(%rip) # 603768 <infile> 400db4: eb 63 jmp 400e19 <main+0x79> 400db6: 48 89 f3 mov %rsi,%rbx 400db9: 83 ff 02 cmp $0x2,%edi 400dbc: 75 3a jne 400df8 <main+0x58> 400dbe: 48 8b 7e 08 mov 0x8(%rsi),%rdi 400dc2: be b4 22 40 00 mov $0x4022b4,%esi 400dc7: e8 44 fe ff ff call 400c10 <fopen@plt> 400dcc: 48 89 05 95 29 20 00 mov %rax,0x202995(%rip) # 603768 <infile> 400dd3: 48 85 c0 test %rax,%rax 400dd6: 75 41 jne 400e19 <main+0x79> 400dd8: 48 8b 4b 08 mov 0x8(%rbx),%rcx 400ddc: 48 8b 13 mov (%rbx),%rdx 400ddf: be b6 22 40 00 mov $0x4022b6,%esi 400de4: bf 01 00 00 00 mov $0x1,%edi 400de9: e8 12 fe ff ff call 400c00 <__printf_chk@plt> 400dee: bf 08 00 00 00 mov $0x8,%edi 400df3: e8 28 fe ff ff call 400c20 <exit@plt> 400df8: 48 8b 16 mov (%rsi),%rdx 400dfb: be d3 22 40 00 mov $0x4022d3,%esi 400e00: bf 01 00 00 00 mov $0x1,%edi 400e05: b8 00 00 00 00 mov $0x0,%eax 400e0a: e8 f1 fd ff ff call 400c00 <__printf_chk@plt> 400e0f: bf 08 00 00 00 mov $0x8,%edi 400e14: e8 07 fe ff ff call 400c20 <exit@plt> 400e19: e8 84 05 00 00 call 4013a2 <initialize_bomb> 400e1e: bf 38 23 40 00 mov $0x402338,%edi 400e23: e8 e8 fc ff ff call 400b10 <puts@plt> 400e28: bf 78 23 40 00 mov $0x402378,%edi 400e2d: e8 de fc ff ff call 400b10 <puts@plt> 400e32: e8 67 06 00 00 call 40149e <read_line> 400e37: 48 89 c7 mov %rax,%rdi 400e3a: e8 a1 00 00 00 call 400ee0 <phase_1> 400e3f: e8 80 07 00 00 call 4015c4 <phase_defused> 400e44: bf a8 23 40 00 mov $0x4023a8,%edi 400e49: e8 c2 fc ff ff call 400b10 <puts@plt> 400e4e: e8 4b 06 00 00 call 40149e <read_line> 400e53: 48 89 c7 mov %rax,%rdi 400e56: e8 a1 00 00 00 call 400efc <phase_2> 400e5b: e8 64 07 00 00 call 4015c4 <phase_defused> 400e60: bf ed 22 40 00 mov $0x4022ed,%edi 400e65: e8 a6 fc ff ff call 400b10 <puts@plt> 400e6a: e8 2f 06 00 00 call 40149e <read_line> 400e6f: 48 89 c7 mov %rax,%rdi 400e72: e8 cc 00 00 00 call 400f43 <phase_3> 400e77: e8 48 07 00 00 call 4015c4 <phase_defused> 400e7c: bf 0b 23 40 00 mov $0x40230b,%edi 400e81: e8 8a fc ff ff call 400b10 <puts@plt> 400e86: e8 13 06 00 00 call 40149e <read_line> 400e8b: 48 89 c7 mov %rax,%rdi 400e8e: e8 79 01 00 00 call 40100c <phase_4> 400e93: e8 2c 07 00 00 call 4015c4 <phase_defused> 400e98: bf d8 23 40 00 mov $0x4023d8,%edi 400e9d: e8 6e fc ff ff call 400b10 <puts@plt> 400ea2: e8 f7 05 00 00 call 40149e <read_line> 400ea7: 48 89 c7 mov %rax,%rdi 400eaa: e8 b3 01 00 00 call 401062 <phase_5> 400eaf: e8 10 07 00 00 call 4015c4 <phase_defused> 400eb4: bf 1a 23 40 00 mov $0x40231a,%edi 400eb9: e8 52 fc ff ff call 400b10 <puts@plt> 400ebe: e8 db 05 00 00 call 40149e <read_line> 400ec3: 48 89 c7 mov %rax,%rdi 400ec6: e8 29 02 00 00 call 4010f4 <phase_6> 400ecb: e8 f4 06 00 00 call 4015c4 <phase_defused> 400ed0: b8 00 00 00 00 mov $0x0,%eax 400ed5: 5b pop %rbx 400ed6: c3 ret 400ed7: 90 nop 400ed8: 90 nop 400ed9: 90 nop 400eda: 90 nop 400edb: 90 nop 400edc: 90 nop 400edd: 90 nop 400ede: 90 nop 400edf: 90 nop
每次调用read_line
时,程序都将标准输入的字符串存入内存,并将内存首地址存储到rdi
中:
1 2 call 40149e <read_line> ;调用read_line mov %rax,%rdi ;将输入字符串首地址存入rdi
Phase1
1 2 3 4 5 6 input = read_line(); phase_1(input); phase_defused(); printf ("Phase 1 defused. How about the next one?\n" );
第一题中,我们读入了输入的字符串,将参数传入了phase_1
中,我们在刚才的反编译结果中找到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 ;将0x402400送入esi 400ee9: e8 4a 04 00 00 call 401338 <strings_not_equal> ;调用strings_not_equal 400eee: 85 c0 test %eax,%eax ;查看返回值eax是否为0 400ef0: 74 05 je 400ef7 <phase_1+0x17> ;是0跳转0x400ef7 400ef2: e8 43 05 00 00 call 40143a <explode_bomb> ;否则触发炸弹,调用explode_bomb,爆炸 400ef7: 48 83 c4 08 add $0x8,%rsp 400efb: c3 ret
至于strings_not_equal
是做什么的,大致根据名字猜测是比较两个字符串的功能,恰好调用时rdi
中存储的是标准输入中的字符串首地址,大致猜测第二个参数存储在rsi
中,也就是刚送入的0x402400
,作为另一个需要比较的字符串的首地址。如果相同就正常跳转执行,否则就会调用explode_bomb
,炸弹爆炸。这也给了我们提示,在GDB调试中可以提前给explode_bomb
打上断点,在爆炸前可以有效的截停程序(每日一个偷鸡小妙招)。
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 29 30 31 32 33 34 35 36 37 38 39 0000000000401338 <strings_not_equal>: 401338: 41 54 push %r12 40133a: 55 push %rbp 40133b: 53 push %rbx 40133c: 48 89 fb mov %rdi,%rbx ;字符串1首地址传至rbx 40133f: 48 89 f5 mov %rsi,%rbp ;字符串2首地址传至rbp 401342: e8 d4 ff ff ff call 40131b <string_length> ;调用string_length查询字符串1长度 401347: 41 89 c4 mov %eax,%r12d ;字符串1长度返回值传至r12d 40134a: 48 89 ef mov %rbp,%rdi 40134d: e8 c9 ff ff ff call 40131b <string_length> ;调用string_length查询字符串2长度 401352: ba 01 00 00 00 mov $0x1,%edx ;edx置0x1 401357: 41 39 c4 cmp %eax,%r12d ;比较字符串1和字符串2长度 40135a: 75 3f jne 40139b <strings_not_equal+0x63> ;不同则跳转0x40139b 40135c: 0f b6 03 movzbl (%rbx),%eax ;将字符串1首字符字节拷贝到eax 40135f: 84 c0 test %al,%al ;检查是否是中止\n符号 401361: 74 25 je 401388 <strings_not_equal+0x50> ;是则跳转0x401388表示相等,否则继续比较 401363: 3a 45 00 cmp 0x0(%rbp),%al ;比较字符串2和字符串1字符 401366: 74 0a je 401372 <strings_not_equal+0x3a> ;相等跳转0x401372 401368: eb 25 jmp 40138f <strings_not_equal+0x57> ;无条件跳转0x40138f 40136a: 3a 45 00 cmp 0x0(%rbp),%al ;比较字符串2和字符串1字符 40136d: 0f 1f 00 nopl (%rax) 401370: 75 24 jne 401396 <strings_not_equal+0x5e> ;不同则跳转0x401396,跳出循环 401372: 48 83 c3 01 add $0x1,%rbx ;字符串1指针++ 401376: 48 83 c5 01 add $0x1,%rbp ;字符串2指针++ 40137a: 0f b6 03 movzbl (%rbx),%eax ;将字符串1字符字节拷贝到eax 40137d: 84 c0 test %al,%al ;检查是否是中止\n符号 40137f: 75 e9 jne 40136a <strings_not_equal+0x32> ;否,则跳转0x40136a,循环 401381: ba 00 00 00 00 mov $0x0,%edx ;循环正常结束,edx置0,准备返回 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 ret
大致分析了一下确定,首先这个方法比较了两个字符串的长度,如果长度相同再进行逐字节的循环比较,直到读取到\0
而停止。
写作C语言大致是如下形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void phase_1 (char * input) { int val = strings_not_equal(input, 0x402400 ); if (val != 0 ) explode_bomb(); return ; } int strings_not_equal (char * str1, char * str2) { if (strlen (str1) != strlen (str2)) return 1 ; while (*str1 != '\0' ){ if (*str1 != *str2){ return 1 ; } str1++; str2++; } return 0 ; }
那么这个在内存中和输入比较的字符串又是多少呢?我们在phase_1
的反编译程序中看见它的地址是0x402400
,因此我们使用GDB获取在0x402400
地址开始的字符串:
也就是Border relations with Canada have never been better.
第一题的答案也就是这个了。
Phase2
1 2 3 4 5 6 input = read_line(); phase_2(input); phase_defused(); printf ("That's number 2. Keep going!\n" );
首先我们看看phase_2
的反编译代码:
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> ;调用read_six_numbers,读取6个数字? 400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) ;判断第一个数字是否是1 400f0e: 74 20 je 400f30 <phase_2+0x34> ;是,跳转0x400f30 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 ;rbx上一个数字赋值给eax 400f1a: 01 c0 add %eax,%eax ;上一个数字x2 400f1c: 39 03 cmp %eax,(%rbx) ;上一个数字x2后和rbx地址上的数字比较 400f1e: 74 05 je 400f25 <phase_2+0x29> ;如果是两倍关系,跳转0x400f25 400f20: e8 15 05 00 00 call 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> ;不相等,还没有移动到边界,跳转0x400f17,继续循环 400f2e: eb 0c jmp 400f3c <phase_2+0x40> ;否则说明循环到头了,跳转0x400f3c 400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx ;第二个数地址&a[1]赋值给rbx 400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp ;边界地址&a[6]赋给rbp 400f3a: eb db jmp 400f17 <phase_2+0x1b> ;无条件跳转400f17 400f3c: 48 83 c4 28 add $0x28,%rsp 400f40: 5b pop %rbx 400f41: 5d pop %rbp 400f42: c3 ret
如果有兴趣也可以看看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> ;调用sscanf 40148f: 83 f8 05 cmp $0x5,%eax ;成功读取的数字数量和5比较 401492: 7f 05 jg 401499 <read_six_numbers+0x3d> ;超过5个,成功读取,跳转0x401499 401494: e8 a1 ff ff ff call 40143a <explode_bomb> ;否则引爆炸弹 401499: 48 83 c4 18 add $0x18,%rsp 40149d: c3 ret
在read_six_numbers
中,可以看到调用了sscanf
方法,其大致参数如下:
1 int sscanf (const char *str, const char *format, ...)
其中str
为要解析的字符串,format
为读取指定格式的数据的字符串,接下来的若干参数都是需要读入的需要保存的数据的地址,返回值为成功转换的参数的个数。例如:
1 2 3 int a, b, c, d;char str[100 ] = "1 2 3 4" ;sscanf (str, "%d %d %d %d" , &a, &b, &c, &d);
在read_six_numbers
的汇编代码中,传入了两个参数,第一个参数在rdi
中,是标准输入字符串的首地址,而rsi
中传入了phase_2
的栈顶地址,猜测是数组的首地址。调用sscanf
时rdi
为第一个参数同上就是我们刚从标准输入获得的字符串的首地址,也是就是str
,rsi
寄存器内存储的应该是第二个参数,也就是format
的首地址0x4025c3
,我们使用GDB将format
打印出来:
清晰的可以看到是"%d %d %d %d %d %d"
,确实是读入6
个整型数字。那存储的数字放置在哪里呢?如果sscanf
需要读入6
个整数,那6
个数字的地址带上2
个字符串,总共就会产生8
个参数。
寄存器
描述
rdi
传递第一个参数
rsi
传递第二个参数
rdx
传递第三个参数或者第二个返回值
rcx
传递第四个参数
r8
传递第五个参数
r9
传递第六个参数
我们知道x86-64 规定只有6
个寄存器来存参数,所以还需要使用栈上的空间来存储其他参数。
通过大致阅读上述的代码,我们不难发现汇编程序首先以当时的phase_2
的栈顶为基准(我们这里将其记为p
),随后使用lea
,将参数rdx
定为p
,将参数rcx
定为p + 0x4
,将参数r8
定为p + 0x8
,将参数r9
定为p + 0xc
,表示各个数字存储的地址,很明显的也可以看出各地址间有4
个字节的差距,刚好是一个整数的大小。剩下的两个参数,则存储在了read_six_numbers
的栈帧内部,我们记read_six_numbers
的栈顶指针是sp
,则在sp
位置存储了p + 0x10
,在sp + 0x8
的位置存储了p + 0x14
,至此为止所有的参数都齐了。画图可以绘作如下:
综上,phase_2
的功能总结就是,传入标准输入字符串,调用read_six_numbers
,在内部调用sscanf
读取字符串中6
个整数,并写入到phase_2
的栈空间中,六个数的首地址从p
到p + 0x14
连续摆放,结束后如果随后sscanf
读出的整数数量不足6
个,则引爆炸弹。随后分析a[0]
是否是1
,不是则引爆炸弹,接下来遍历剩下个元素,判断每个元素是否是上一个元素的两倍,如果不是就引爆炸弹,如果是就继续,然后炸弹被解除。
整理成代码的话应该可以写作如下形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void phase_2 (char * input) { int * p, p_end; int a[6 ]; read_six_numbers(input, a); if (a[0 ] != 0 ) explode_bomb(); p = &a[1 ]; p_end = &a[6 ]; do { if (*(p - 1 ) * 2 != *p) explode_bomb(); p++; } while (p != p_end); return ; } void read_six_numbers (char * input, int * a) { int num = sscanf (input, "%d %d %d %d %d %d" , &a[0 ], &a[1 ], &a[2 ], &a[3 ], &a[4 ], &a[5 ]); if (num <= 5 ) explode_bomb(); return ; }
因此我们的输入应当是六个数:1 2 4 8 16 32
Phase3
1 2 3 4 5 6 input = read_line(); phase_3(input); phase_defused(); printf ("Halfway there!\n" );
首先我们看看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 ;rcx赋&a[1] 400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx ;rdx赋&a[0] 400f51: be cf 25 40 00 mov $0x4025cf,%esi ;esi赋"%d %d" 400f56: b8 00 00 00 00 mov $0x0,%eax 400f5b: e8 90 fc ff ff call 400bf0 <__isoc99_sscanf@plt> ;调用sscanf读取两数字 400f60: 83 f8 01 cmp $0x1,%eax ;读取数量和1比较 400f63: 7f 05 jg 400f6a <phase_3+0x27> ;读满两个就跳转0x400f6a继续执行 400f65: e8 d0 04 00 00 call 40143a <explode_bomb> ;否则引爆炸弹 400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp) ;比较a[0]和7 400f6f: 77 3c ja 400fad <phase_3+0x6a> ;如果a[0](无符号数格式)大于7,就跳转0x400fad,引爆炸弹 400f71: 8b 44 24 08 mov 0x8(%rsp),%eax ;eax赋a[0] 400f75: ff 24 c5 70 24 40 00 jmp *0x402470(,%rax,8) ;case语句,根据rax的值跳转到不同的分枝 400f7c: b8 cf 00 00 00 mov $0xcf,%eax ;如果a[0]取0跳转至此,eax赋0xcf 400f81: eb 3b jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe 400f83: b8 c3 02 00 00 mov $0x2c3,%eax ;如果a[0]取2跳转至此,eax赋0x2c3 400f88: eb 34 jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe 400f8a: b8 00 01 00 00 mov $0x100,%eax ;如果a[0]取3跳转至此,eax赋0x100 400f8f: eb 2d jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe 400f91: b8 85 01 00 00 mov $0x185,%eax ;如果a[0]取4跳转至此,eax赋0x185 400f96: eb 26 jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe 400f98: b8 ce 00 00 00 mov $0xce,%eax ;如果a[0]取5跳转至此,eax赋值0xce 400f9d: eb 1f jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe 400f9f: b8 aa 02 00 00 mov $0x2aa,%eax ;如果a[0]取6跳转至此,eax取0x2aa 400fa4: eb 18 jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe 400fa6: b8 47 01 00 00 mov $0x147,%eax ;如果a[0]取7跳转至此,eax取0x147 400fab: eb 11 jmp 400fbe <phase_3+0x7b> ;break,跳转至0x400fbe 400fad: e8 88 04 00 00 call 40143a <explode_bomb> ;炸弹爆炸 400fb2: b8 00 00 00 00 mov $0x0,%eax ;炸弹爆炸后,eax赋0 400fb7: eb 05 jmp 400fbe <phase_3+0x7b> ;跳转0x400fbe 400fb9: b8 37 01 00 00 mov $0x137,%eax ;如果a[0]取1跳转至此,eax赋0x137 400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax ;eax和a[1]比较 400fc2: 74 05 je 400fc9 <phase_3+0x86> ;如果相等,跳转0x400fc9 400fc4: e8 71 04 00 00 call 40143a <explode_bomb> ;否则爆炸 400fc9: 48 83 c4 18 add $0x18,%rsp 400fcd: c3 ret
头部的部分和phase_2
其实相当的类似,也是调用sscanf
方法,先不管别的,我们先把format
字符串给它爬出来,根据它的首地址存放在rsi
中是0x4025cf
,我们根据GDB调试有:
也就是说输入格式是"%d %d"
,考虑到传入rdi
第一参数str
是标准输入字符串首地址,rdx
第三参数是rsp + 0x8
,rcx
第四参数是rsp + 0xc
,大致猜测调用sscanf
中包含以下参数:
1 sscanf (input, "%d %d" , &a[0 ], &a[1 ]);
a[0]
地址是栈顶地址rsp
向栈底8
字节位置,a[1]
地址紧随其后,是栈顶地址rsp
向栈底12
字节位置。接下来的代码同样熟悉,如果读取数字小于两个,就引爆炸弹。
接下来是一个很典型的case
语句结构,首先判断了a[0]
作为无符号数是否大于7
,换句话说也就是如果a[0]
作为有符号数如果落在区间[0, 7]
外,这个知识点在视频中也有提及,具体在视频 57:16位置。
接下来使用jmp *0x402470(,%rax,8)
,根据rax
产生的偏移量跳转到了索引表结构上。我们通过GDB将索引表的各个分支代表的数字进行输出:
依据这张表我们绘制出索引表:
a[0]取值
跳转地址
0
0x400f7c
1
0x400fb9
2
0x400f83
3
0x400f8a
4
0x400f91
5
0x400f98
6
0x400f9f
7
0x400fa6
在不同的分枝内部,都对eax
进行了不同的赋值,跳出循环后,又对a[1]
和eax
进行了比较,如果不一致就炸弹爆炸,否则拆弹成功。
写成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 void phase_3 (char * intput) { int a[2 ]; int num = sscanf (input, "%d %d" , &a[0 ], &a[1 ]); if (num <= 1 ) explode_bomb(); switch (a[0 ]){ case 0 : a[0 ] = 0xcf ; break ; case 1 : a[0 ] = 0x137 ; break ; case 2 : a[0 ] = 0x2c3 ; break ; case 3 : a[0 ] = 0x100 ; break ; case 4 : a[0 ] = 0x185 ; break ; case 5 : a[0 ] = 0xce ; break ; case 6 : a[0 ] = 0x2aa ; break ; case 7 : a[0 ] = 0x147 ; break ; default : explode_bomb(); a[0 ] = 0 ; break ; } if (a[0 ] != a[1 ]) explode_bomb(); return ; }
因此,我们应当输入两组整数,以空格间隔开,而且答案并不唯一,a[0]
在[0, 7]
区间内均可,但是a[1]
则遵循case
语句中的数值需要对号入座,以下数对均可:
a[0]
a[1]
0
207
1
311
2
707
3
256
4
389
5
206
6
682
7
327
Phase4
1 2 3 4 5 input = read_line(); phase_4(input); phase_defused(); printf ("So you got that one. Try this one.\n" );
我们观察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 ;rcx赋&a[1] 401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx ;rcx赋&a[0] 40101a: be cf 25 40 00 mov $0x4025cf,%esi ;esi赋"%d %d" 40101f: b8 00 00 00 00 mov $0x0,%eax 401024: e8 c7 fb ff ff call 400bf0 <__isoc99_sscanf@plt> 401029: 83 f8 02 cmp $0x2,%eax ;检查填充的参数个数是否是2 40102c: 75 07 jne 401035 <phase_4+0x29> ;不相等则跳转0x401035,引爆炸弹 40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp) ;比较14和a[0] 401033: 76 05 jbe 40103a <phase_4+0x2e> ;如果a[0](转无符号数)<=14,跳转0x40103a 401035: e8 00 04 00 00 call 40143a <explode_bomb> ;否则触发爆炸 40103a: ba 0e 00 00 00 mov $0xe,%edx ;edx赋14 40103f: be 00 00 00 00 mov $0x0,%esi ;esi赋0 401044: 8b 7c 24 08 mov 0x8(%rsp),%edi ;edi赋a[0] 401048: e8 81 ff ff ff call 400fce <func4> ;调用func4 40104d: 85 c0 test %eax,%eax ;检查返回值是否是0 40104f: 75 07 jne 401058 <phase_4+0x4c> ;则不等,跳转0x401058,触发爆炸 401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) ;比较0和a[1] 401056: 74 05 je 40105d <phase_4+0x51> ;相等则跳转0x40105d 401058: e8 dd 03 00 00 call 40143a <explode_bomb> ;否则触发爆炸 40105d: 48 83 c4 18 add $0x18,%rsp 401061: c3 ret
接下来,我们来看看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 ;给eax赋值edx 400fd4: 29 f0 sub %esi,%eax ;eax -= esi 400fd6: 89 c1 mov %eax,%ecx ;给ecx赋值eax 400fd8: c1 e9 1f shr $0x1f,%ecx ;ecx逻辑右移31位,相当于取符号位 400fdb: 01 c8 add %ecx,%eax ;eax += ecx 400fdd: d1 f8 sar %eax ;算术右移指令 400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx ;ecx = rax + rsi 400fe2: 39 f9 cmp %edi,%ecx ;比较ecx和edi 400fe4: 7e 0c jle 400ff2 <func4+0x24> ;如果小于等于就跳转0x400ff2 400fe6: 8d 51 ff lea -0x1(%rcx),%edx ;否则edx = rcx - 1 400fe9: e8 e0 ff ff ff call 400fce <func4> ;递归调用func4 400fee: 01 c0 add %eax,%eax ;eax *= 2 400ff0: eb 15 jmp 401007 <func4+0x39> ;无条件跳转0x401007 400ff2: b8 00 00 00 00 mov $0x0,%eax ;eax置0 400ff7: 39 f9 cmp %edi,%ecx ;比较ecx和edi 400ff9: 7d 0c jge 401007 <func4+0x39> ;如果大于或者等于就跳转0x401007 400ffb: 8d 71 01 lea 0x1(%rcx),%esi ;esi = rcx + 1 400ffe: e8 cb ff ff ff call 400fce <func4> ;递归调用func4 401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax ;eax = rax + rax + 1 401007: 48 83 c4 08 add $0x8,%rsp 40100b: c3 ret
我们对phase_4
和func4
对程序稍加分析,即可看出func4
中使用了rdi
,rsi
和rdx
寄存器内部传入的数据,也就是func4
传入了三个参数,并通过eax
传出了一个参数。
前面的若干句指令其实可以翻译作如下伪代码,实际上其基本功能就是计算a / 2
:
1 2 3 4 5 6 7 8 int func4 (int x, int y, int z) { int a = z - y; int b = a >>> 31 ; a += b; a >>= 1 ; }
正常的算术逻辑右移,其实更加实际来说是向下取整,而非向零取整,3
通过算术右移能得到1
,但是-3
只能得到-2
。为此如果是负数,我们需要在算术右移前先对原数添加偏置常量1
。在这里程序通过逻辑右移31
位的方式获取到了符号位,在算数右移前先加在原数上,很巧妙的解决了问题。因此这段代码实现的结果也就是:
1 2 3 4 5 int func4 (int x, int y, int z) { int a = (z - y) / 2 ; }
接下来的代码我们直接逐行转成C语言:
1 2 3 4 5 6 7 8 9 10 11 int func4 (int x, int y, int z) { int a = y + (z - y) / 2 ; if (a <= x){ if (a >= x) return 0 ; y = a + 1 ; return 2 * func4(x, y, z) + 1 ; } else { z = a - 1 ; return 2 * func4(x, y, z); } }
有点像二分噢,稍稍优化一下写法:
1 2 3 4 5 6 7 8 9 int func4 (int val, int lo, int hi) { int mid = lo + (hi - lo) / 2 ; if (mid == val) return 0 ; if (mid < val){ return 2 * func4(val, mid + 1 , hi) + 1 ; } else { return 2 * func4(val, lo, mid - 1 ); } }
那我们接下来再回头观察phase_4
是如何实现的,在调用func4
时传入的参数分别为a[0]
,0
,14
。而我们作为保证必须使得返回值为0
否则会触发爆炸,同时第二个输入参数a[1]
还必须为0
,大致的C语言实现如下:
1 2 3 4 5 6 7 8 void phase_4 (char * input) { int a[2 ]; int val = sscanf (input, "%d %d" , &a[0 ], &a[1 ]); if (val != 2 || a[0 ] < 0 || a[0 ] > 14 ) explode_bomb(); val = func4(a[0 ], 0 , 14 ); if (val != 0 || a[1 ] != 0 ) explode_bomb(); return ; }
我们对a[0]
在区间[0, 14]
内部进行枚举,满足条件的有:0
,1
,3
,7
以上四个数任意一个配上a[1] = 0
即为结果。
Phase5
1 2 3 4 5 input = read_line(); phase_5(input); phase_defused(); printf ("Good work! On to the next...\n" );
废话不说,先看phase_5
的反编译结果:
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 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 ;从此处获取8字节的金丝雀值 401071: 00 00 401073: 48 89 44 24 18 mov %rax,0x18(%rsp) ;将金丝雀值放置在缓冲区旁边 401078: 31 c0 xor %eax,%eax ;eax置0 40107a: e8 9c 02 00 00 call 40131b <string_length> ;计算标准输入字符串长度 40107f: 83 f8 06 cmp $0x6,%eax ;比较长度是否是6 401082: 74 4e je 4010d2 <phase_5+0x70> ;是,则跳转0x4010d2 401084: e8 b1 03 00 00 call 40143a <explode_bomb> ;否则引爆炸弹 401089: eb 47 jmp 4010d2 <phase_5+0x70> ;引爆后跳转0x4010d2 40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx ;将rbx+rax位置一字节拷贝到ecx 40108f: 88 0c 24 mov %cl,(%rsp) ;将rax低1字节写入到栈顶 401092: 48 8b 14 24 mov (%rsp),%rdx ;再写入rdx 401096: 83 e2 0f and $0xf,%edx ;edx&=15,取低四位 401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx ;取出0x4020b0+rdx位置的1字节给edx 4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1) ;将rdx低1字节写入rsp+rax+16位置 4010a4: 48 83 c0 01 add $0x1,%rax ;rax += 1 4010a8: 48 83 f8 06 cmp $0x6,%rax ;rax和6比较 4010ac: 75 dd jne 40108b <phase_5+0x29> ;不等则跳转0x40108b,开始循环,如果满6则跳出 4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp) ;rsp+22位置写入两字节的0 4010b3: be 5e 24 40 00 mov $0x40245e,%esi ;给esi赋值0x40245e 4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi ;给rdi赋值rsp+16 4010bd: e8 76 02 00 00 call 401338 <strings_not_equal> ;比较esi和rsi为首地址的两字符串 4010c2: 85 c0 test %eax,%eax ;结果是否是0,即是否相同 4010c4: 74 13 je 4010d9 <phase_5+0x77> ;是则跳转0x4010d9 4010c6: e8 6f 03 00 00 call 40143a <explode_bomb> ;否则引爆炸弹 4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) 4010d0: eb 07 jmp 4010d9 <phase_5+0x77> ;跳转0x4010d9 4010d2: b8 00 00 00 00 mov $0x0,%eax ;eax置0 4010d7: eb b2 jmp 40108b <phase_5+0x29> ;跳转0x40108b,进入循环 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> ;相等,说明栈空间未被破坏,跳转0x4010ee 4010e9: e8 42 fa ff ff call 400b30 <__stack_chk_fail@plt> ;不等,说明被破坏 4010ee: 48 83 c4 20 add $0x20,%rsp 4010f2: 5b pop %rbx 4010f3: c3 ret
看到fs
寄存器,还有__stack_chk_fail
等命令,应该是对栈内存进行了破坏检查,而在fs:0x28
位置正好存储了对应的“金丝雀值”,结束时进行检测,如果发生了变化,则认为栈空间遭到了破坏。详情见视频 53:14位置。
phase_5
首先检查了标准输入字符串是否长度为6
,不满足直接爆炸。接下来,进入一个6
次的循环,程序使用了一个循环依次读入了标准输入input
的6
个字节,将取出的字节和15
取与,作为偏移量取出0x4024b0
为首地址的一个字符串中的一个字符,并将其填入到一块rsp + 16
为地址开头的空间中,在后续观察中,我们发现rsp + 16
指向的应当也是一块字符数组空间,循环结束后,我们还在数组空间末尾rsp + 22
位置补上了一个'\0'
。在最后还将这段字符串和0x40245e
地址开始的字符串进行比较,如果不一致就爆炸,否则拆弹成功。
转换成C语言大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 void phase_5 (char * input) { char a[7 ]; int i, val; if (strlen (input) != 6 ) explode_bomb(); for (i = 0 ; i < 6 ; i++){ a[i] = 0x4024b0 [input[i] & 15 ]; } a[6 ] = '\0' ; val = strings_not_equal(a, 0x4025e ); if (val != 0 ) explode_bomb(); return ; }
我们通过GDB获取到0x4024b0
地址和0x40245e
地址的字符串:
字符串分别为:"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
和"flyers"
栈内存空间具体可以画作如下:
根据以上关系我们建立映射:
Val
Val
input[0]&15
9
input[1]&15
15
input[2]&15
14
input[3]&15
5
input[4]&15
6
input[5]&15
7
换句话说本题答案也不唯一,输入字符串的各个字符的ASCII
码和15
取与只要在对应范围即可。
例如)/.%&'
就是一个可能的答案。
Phase6
1 2 3 4 5 input = read_line(); phase_6(input); phase_defused();
先看phase_6
的实现:
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 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 ;暂存栈顶地址到r13 401103: 48 89 e6 mov %rsp,%rsi 401106: e8 51 03 00 00 call 40145c <read_six_numbers> ;读取6个数字,存储在栈帧连续空间中 40110b: 49 89 e6 mov %rsp,%r14 ;暂存栈顶地址到r14 40110e: 41 bc 00 00 00 00 mov $0x0,%r12d ;r12置0 401114: 4c 89 ed mov %r13,%rbp ;将r13中存储地址写入rbp 401117: 41 8b 45 00 mov 0x0(%r13),%eax ;eax置指针4字节数据 40111b: 83 e8 01 sub $0x1,%eax ;eax -= 1 40111e: 83 f8 05 cmp $0x5,%eax ;比较eax和5 401121: 76 05 jbe 401128 <phase_6+0x34> ;(unsigned int)eax<=5则跳转0x401128 401123: e8 12 03 00 00 call 40143a <explode_bomb> ;否则引爆炸弹 401128: 41 83 c4 01 add $0x1,%r12d ;r12++ 40112c: 41 83 fc 06 cmp $0x6,%r12d ;r12是否满6? 401130: 74 21 je 401153 <phase_6+0x5f> ;满了跳出外侧大循环,跳转0x401153 401132: 44 89 e3 mov %r12d,%ebx ;给ebx赋值r12 401135: 48 63 c3 movslq %ebx,%rax ;给rax赋值ebx 401138: 8b 04 84 mov (%rsp,%rax,4),%eax ;eax = *(rsp + rax * 4),eax赋值第rax个元素 40113b: 39 45 00 cmp %eax,0x0(%rbp) ;比较当前位置元素和第rbx个元素 40113e: 75 05 jne 401145 <phase_6+0x51> ;不等则跳转0x401145 401140: e8 f5 02 00 00 call 40143a <explode_bomb> ;否则引爆炸弹 401145: 83 c3 01 add $0x1,%ebx ;ebx++ 401148: 83 fb 05 cmp $0x5,%ebx ;比较ebx和5 40114b: 7e e8 jle 401135 <phase_6+0x41> ;若ebx<=5,则跳转0x401135,开始内侧循环 40114d: 49 83 c5 04 add $0x4,%r13 ;r13 += 4 401151: eb c1 jmp 401114 <phase_6+0x20> ;跳转0x401114,开始外侧循环 401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi ;rsi指向数组末尾边界地址 401158: 4c 89 f0 mov %r14,%rax ;rax存储数组首地址 40115b: b9 07 00 00 00 mov $0x7,%ecx ;给ecx赋7 401160: 89 ca mov %ecx,%edx ;给edx赋7 401162: 2b 10 sub (%rax),%edx ;edx = 7 - *(rax) 401164: 89 10 mov %edx,(%rax) ;回写数组中,视作*(rax) = 7 - *(rax) 401166: 48 83 c0 04 add $0x4,%rax ;rax后移到下一个整数,地址+4 40116a: 48 39 f0 cmp %rsi,%rax ;判断rax是否已经指向了数组边界 40116d: 75 f1 jne 401160 <phase_6+0x6c> ;还没有,则继续循环,跳转0x401160 40116f: be 00 00 00 00 mov $0x0,%esi ;esi置0 401174: eb 21 jmp 401197 <phase_6+0xa3> ;跳转0x401197 401176: 48 8b 52 08 mov 0x8(%rdx),%rdx ;rdx=rdx->next 40117a: 83 c0 01 add $0x1,%eax ;eax++ 40117d: 39 c8 cmp %ecx,%eax ;比较eax和ecx 40117f: 75 f5 jne 401176 <phase_6+0x82> ;不相等则跳转0x401176 401181: eb 05 jmp 401188 <phase_6+0x94> ;否则跳转0x401188 401183: ba d0 32 60 00 mov $0x6032d0,%edx 401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2) ;将rdx写入到rsp + 2 * rsi + 32位置 40118d: 48 83 c6 04 add $0x4,%rsi ;rsi += 4 401191: 48 83 fe 18 cmp $0x18,%rsi ;比较rsi是否达到24 401195: 74 14 je 4011ab <phase_6+0xb7> ;达到则跳出循环,跳转0x4011ab 401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx ;ecx = *(rsp + rsi) 40119a: 83 f9 01 cmp $0x1,%ecx ;所指元素和1比较 40119d: 7e e4 jle 401183 <phase_6+0x8f> ;如果小于等于1则跳转0x401183 40119f: b8 01 00 00 00 mov $0x1,%eax ;eax赋值1 4011a4: ba d0 32 60 00 mov $0x6032d0,%edx ;edx赋值0x6032d0 4011a9: eb cb jmp 401176 <phase_6+0x82> ;跳转0x401176 4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx ;rbx指向address数组首元素 4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax ;rax指向下一元素 4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi ;rsi指向address数组结尾边缘 4011ba: 48 89 d9 mov %rbx,%rcx ;rcx赋值rbx 4011bd: 48 8b 10 mov (%rax),%rdx ;rdx赋值下一元素节点的代表的节点的地址 4011c0: 48 89 51 08 mov %rdx,0x8(%rcx) ;将前一节点的next修改为下一元素节点的地址 4011c4: 48 83 c0 08 add $0x8,%rax ;rax += 8,位移到下一格 4011c8: 48 39 f0 cmp %rsi,%rax ;检查rax指针是否到达边界 4011cb: 74 05 je 4011d2 <phase_6+0xde> ;如果到达,跳出循环,转0x4011d2 4011cd: 48 89 d1 mov %rdx,%rcx ;否则,将rcx也往后一格 4011d0: eb eb jmp 4011bd <phase_6+0xc9> ;进行循环,转0x4011bd 4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx) ;将数组最后一个元素代表的节点next置null 4011d9: 00 4011da: bd 05 00 00 00 mov $0x5,%ebp ;ebp置5 4011df: 48 8b 43 08 mov 0x8(%rbx),%rax ;rax = rbx -> next 4011e3: 8b 00 mov (%rax),%eax ;eax = rax -> val 4011e5: 39 03 cmp %eax,(%rbx) ;比较当前元素节点值rbx.val和下一元素节点值rax.val 4011e7: 7d 05 jge 4011ee <phase_6+0xfa> ;如果大于等于,转0x4011ee 4011e9: e8 4c 02 00 00 call 40143a <explode_bomb> ;否则爆炸 4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx ;rbx = rbx -> next 4011f2: 83 ed 01 sub $0x1,%ebp ;ebp-- 4011f5: 75 e8 jne 4011df <phase_6+0xeb> ;如果不为0,继续循环,转0x4011df 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 ret
前面部分其实也是相当的眼熟,功能和phase_2
头部代码类似,也是使用sscanf
读取6
个整数,并写入到phase_6
的栈顶数组位置。read_six_numbers
内部的实现已经看过了就不细说了。
后面的代码场面一度非常混乱,从0x401114
到0x401151
观察,大致能发现两层嵌套的循环代码。
对着瞎写了一点,大致代码可能是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void phase_6 (char * input) { int a[6 ]; int * t = a; int i, j; read_six_numbers(input, a); i = 0 ; while (true ){ if ((unsigned int )(*t - 1 ) > 5 ) explode_bomb(); if (++i == 6 ) break ; j = i; while (true ){ if (*(a + j) == *t) explode_bomb(); if (++j > 5 ) break ; } t++; } }
稍加整理:
1 2 3 4 5 6 7 8 9 10 11 12 void phase_6 (char * input) { int a[6 ]; int i, j; read_six_numbers(input, a); for (i = 0 ; i <= 5 ; i++){ if ((unsigned int )(a[i] - 1 ) > 5 ) explode_bomb(); for (j = i + 1 ; j <= 5 ; j++){ if (a[i] == a[j]) explode_bomb(); } } }
大致理解就是,我们读入6
个数字,而这几个数字必须要满足减一后转无符号数比5
小,换句话说就是原数必须落在[1, 6]
的范围内,不满足条件就会触发爆炸。随后,程序对数字进行了检测,内部的数字两两间不能相同。稍微一想,那就是1
~6
里面每个数字各占一个。
我们接下来看0x401153
到0x40116d
构成的循环,其实就是遍历每个数字,然后用7
减掉原数再填回去。简单的写作C那就是:
1 2 3 4 5 6 7 void phase_6 (char * input) { for (i = 0 ; i < 6 ; i++){ a[i] = 7 - a[i]; } }
注意到整体变换过的数组内部的元素其实范围还是落在1
~6
的区间内部的,还是互不相同。
接下来我们重点观察0x40116f
到0x4011a9
部分。看到mov 0x8(%rdx),%rdx
这样的代码,猜测可能在这块地址存在链表,地址存放在结构体向后偏移8
字节的位置,且链表的首节点地址应该是0x6032d0
。
我们使用GDB查看0x6032d0
位置的链表结构:
我们看到结构体分为三个部分,第一个部分是存储的值,第二部分是节点的序号,第三部分是next
指针的地址。总共存在6
个节点。
大致操作大致可以描述为遍历数组内部的数字,然后根据取出的数字决定向后移动到next
的次数。例如如果取出的数字是1
,那就不进入循环,直接取首节点;如果数字是2
,那就进入循环,取出第2
个节点,以此类推…
随后,我们将节点的地址依次存入rsp + 0x20
起始的一块内存区域中,8
字节间隔开。整理成C伪代码那就是:
1 2 3 4 5 6 7 8 void phase_6 (char * input) { long address[6 ]; for (i = 0 ; i <= 5 ; i++){ address[i] = &node_{a[i]}; } }
接下来,在0x4011ab
到0x4011d2
一段,程序按照数组的顺序,重新将各个节点相连,最后将最后一个元素代表的节点的next
置null
。写作C为:
1 2 3 4 5 6 7 8 void phase_6 (char * input) { for (i = 0 ; i < 5 ; i++){ (node*)address[i] -> next = address[i + 1 ]; } (node*)address[5 ] -> next = nullptr; }
最后一段代码就是从数组的头部节点开始向后依次便利检查,是否前一节点的值大于等于后一节点的值,不满足就会触发炸弹,检查完毕即通过。
1 2 3 4 5 6 7 void phase_6 (char * input) { for (int i = 0 ; i < 5 ; i++){ if ((node*)address[i] -> val < (node*)address[i + 1 ] -> val) explode_bomb(); } return ; }
根据这点信息,我们重排后的链表结果应该是:
node3(924) -> node4(691) -> node5(477) -> node6(443) -> node1(322) -> node2(168)
因此数组a
应该是{3, 4, 5, 6, 1, 2}
倒推到7 - a[i]
变换前应该是{4, 3, 2, 1, 6, 5}
所以答案是4 3 2 1 6 5
最后我们拆除了所有的炸弹,哭死,难度真不是盖的: