准备工作
在终端中输入objdump -d bomb > bomb.asm,将bomb反汇编并保存到文件bomb.asm中,相较于在gdb中输入disassemble命令更便于阅读。
080489e4 <main>:
80489e4: 55 push %ebp
80489e5: 89 e5 mov %esp,%ebp
80489e7: 53 push %ebx
80489e8: 83 e4 f0 and $0xfffffff0,%esp
80489eb: 83 ec 10 sub $0x10,%esp
80489ee: 8b 45 08 mov 0x8(%ebp),%eax
80489f1: 8b 5d 0c mov 0xc(%ebp),%ebx
80489f4: 83 f8 01 cmp $0x1,%eax
80489f7: 75 0c jne 8048a05 <main+0x21>
80489f9: a1 a4 c3 04 08 mov 0x804c3a4,%eax
80489fe: a3 d0 c3 04 08 mov %eax,0x804c3d0
8048a03: eb 74 jmp 8048a79 <main+0x95>
8048a05: 83 f8 02 cmp $0x2,%eax
8048a08: 75 49 jne 8048a53 <main+0x6f>
8048a0a: c7 44 24 04 68 a0 04 movl $0x804a068,0x4(%esp)
8048a11: 08
8048a12: 8b 43 04 mov 0x4(%ebx),%eax
8048a15: 89 04 24 mov %eax,(%esp)
8048a18: e8 63 fe ff ff call 8048880 <fopen@plt>
8048a1d: a3 d0 c3 04 08 mov %eax,0x804c3d0
8048a22: 85 c0 test %eax,%eax
8048a24: 75 53 jne 8048a79 <main+0x95>
8048a26: 8b 43 04 mov 0x4(%ebx),%eax
8048a29: 89 44 24 0c mov %eax,0xc(%esp)
8048a2d: 8b 03 mov (%ebx),%eax
8048a2f: 89 44 24 08 mov %eax,0x8(%esp)
8048a33: c7 44 24 04 6a a0 04 movl $0x804a06a,0x4(%esp)
8048a3a: 08
8048a3b: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048a42: e8 59 fe ff ff call 80488a0 <__printf_chk@plt>
8048a47: c7 04 24 08 00 00 00 movl $0x8,(%esp)
8048a4e: e8 ed fd ff ff call 8048840 <exit@plt>
8048a53: 8b 03 mov (%ebx),%eax
8048a55: 89 44 24 08 mov %eax,0x8(%esp)
8048a59: c7 44 24 04 87 a0 04 movl $0x804a087,0x4(%esp)
8048a60: 08
8048a61: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048a68: e8 33 fe ff ff call 80488a0 <__printf_chk@plt>
8048a6d: c7 04 24 08 00 00 00 movl $0x8,(%esp)
8048a74: e8 c7 fd ff ff call 8048840 <exit@plt>
8048a79: e8 bd 05 00 00 call 804903b <initialize_bomb>
8048a7e: c7 04 24 ec a0 04 08 movl $0x804a0ec,(%esp)
8048a85: e8 76 fd ff ff call 8048800 <puts@plt>
8048a8a: c7 04 24 28 a1 04 08 movl $0x804a128,(%esp)
8048a91: e8 6a fd ff ff call 8048800 <puts@plt>
8048a96: e8 62 06 00 00 call 80490fd <read_line>
8048a9b: 89 04 24 mov %eax,(%esp)
8048a9e: e8 ad 00 00 00 call 8048b50 <phase_1>
8048aa3: e8 b3 07 00 00 call 804925b <phase_defused>
8048aa8: c7 04 24 54 a1 04 08 movl $0x804a154,(%esp)
8048aaf: e8 4c fd ff ff call 8048800 <puts@plt>
8048ab4: e8 44 06 00 00 call 80490fd <read_line>
8048ab9: 89 04 24 mov %eax,(%esp)
8048abc: e8 b3 00 00 00 call 8048b74 <phase_2>
8048ac1: e8 95 07 00 00 call 804925b <phase_defused>
8048ac6: c7 04 24 a1 a0 04 08 movl $0x804a0a1,(%esp)
8048acd: e8 2e fd ff ff call 8048800 <puts@plt>
8048ad2: e8 26 06 00 00 call 80490fd <read_line>
8048ad7: 89 04 24 mov %eax,(%esp)
8048ada: e8 e5 00 00 00 call 8048bc4 <phase_3>
8048adf: e8 77 07 00 00 call 804925b <phase_defused>
8048ae4: c7 04 24 bf a0 04 08 movl $0x804a0bf,(%esp)
8048aeb: e8 10 fd ff ff call 8048800 <puts@plt>
8048af0: e8 08 06 00 00 call 80490fd <read_line>
8048af5: 89 04 24 mov %eax,(%esp)
8048af8: e8 e2 01 00 00 call 8048cdf <phase_4>
8048afd: e8 59 07 00 00 call 804925b <phase_defused>
8048b02: c7 04 24 80 a1 04 08 movl $0x804a180,(%esp)
8048b09: e8 f2 fc ff ff call 8048800 <puts@plt>
8048b0e: e8 ea 05 00 00 call 80490fd <read_line>
8048b13: 89 04 24 mov %eax,(%esp)
8048b16: e8 26 02 00 00 call 8048d41 <phase_5>
8048b1b: e8 3b 07 00 00 call 804925b <phase_defused>
8048b20: c7 04 24 ce a0 04 08 movl $0x804a0ce,(%esp)
8048b27: e8 d4 fc ff ff call 8048800 <puts@plt>
8048b2c: e8 cc 05 00 00 call 80490fd <read_line>
8048b31: 89 04 24 mov %eax,(%esp)
8048b34: e8 51 02 00 00 call 8048d8a <phase_6>
8048b39: e8 1d 07 00 00 call 804925b <phase_defused>
8048b3e: b8 00 00 00 00 mov $0x0,%eax
8048b43: 8b 5d fc mov -0x4(%ebp),%ebx
8048b46: c9 leave
8048b47: c3 ret
8048b48: 90 nop
8048b49: 90 nop
8048b4a: 90 nop
8048b4b: 90 nop
8048b4c: 90 nop
8048b4d: 90 nop
8048b4e: 90 nop
8048b4f: 90 nop
首先分析main函数,观察到main函数中,read_line函数的意义是读取一行字符串。phase_1~phase_6,read_line之后都会将返回值%eax传给(%esp),也就是read_line读取的字符串的首地址作为接下来执行phase_n的第一个参数。
Phase_1
密钥:I can see Russia from my house!
08048b50 <phase_1>:
8048b50: 83 ec 1c sub $0x1c,%esp
8048b53: c7 44 24 04 a4 a1 04 movl $0x804a1a4,0x4(%esp)
8048b5a: 08
8048b5b: 8b 44 24 20 mov 0x20(%esp),%eax
8048b5f: 89 04 24 mov %eax,(%esp)
8048b62: e8 5d 04 00 00 call 8048fc4 <strings_not_equal>
8048b67: 85 c0 test %eax,%eax
8048b69: 74 05 je 8048b70 <phase_1+0x20>
8048b6b: e8 66 05 00 00 call 80490d6 <explode_bomb>
8048b70: 83 c4 1c add $0x1c,%esp
8048b73: c3 ret
分析
阅读phase_1的前5行代码,观察到数据$0x804a1a4和0x20(%esp)对应的read_line读入的字符串首地址作为参数被传入栈帧中,结合第6行的strings_not_equal函数,可以推测,$0x804a1a4处存储的也是一个字符串的首地址,并且将要与此前读入的字符串进行比较。阅读strings_not_equal的汇编代码,得知该函数比较两个字符串是否相等,若相等则返回0,保存在%eax中。再看第7、8、9行,可知若%eax为0,则跳过第9行至<phase_1+0x20>处,否则执行第9行。第9行的指令进入explode_bomb函数,执行此行命令将会引爆炸弹,所以必须令程序在第8行时进行跳转。结合以上信息,得知,我们必须令输入的字符串与$0x804a1a4处的字符串相等,才能通过phase_1。
通过gdb中的print命令,打印出$0x804a1a4处的字符串,出现了如图所示的字符串“I can see Russia from my house!”,这便是phase_1的通关密钥。运行bomb,输入该字符串,成功通过phase_1。
Phase_2
密钥:0 1 1 2 3 5
08048b74 <phase_2>:
8048b74: 56 push %esi
8048b75: 53 push %ebx
8048b76: 83 ec 34 sub $0x34,%esp
8048b79: 8d 44 24 18 lea 0x18(%esp),%eax
8048b7d: 89 44 24 04 mov %eax,0x4(%esp)
8048b81: 8b 44 24 40 mov 0x40(%esp),%eax
8048b85: 89 04 24 mov %eax,(%esp)
8048b88: e8 7e 06 00 00 call 804920b <read_six_numbers>
8048b8d: 83 7c 24 18 00 cmpl $0x0,0x18(%esp)
8048b92: 75 07 jne 8048b9b <phase_2+0x27>
8048b94: 83 7c 24 1c 01 cmpl $0x1,0x1c(%esp)
8048b99: 74 05 je 8048ba0 <phase_2+0x2c>
8048b9b: e8 36 05 00 00 call 80490d6 <explode_bomb>
8048ba0: 8d 5c 24 20 lea 0x20(%esp),%ebx
8048ba4: 8d 74 24 30 lea 0x30(%esp),%esi
8048ba8: 8b 43 f8 mov -0x8(%ebx),%eax
8048bab: 03 43 fc add -0x4(%ebx),%eax
8048bae: 39 03 cmp %eax,(%ebx)
8048bb0: 74 05 je 8048bb7 <phase_2+0x43>
8048bb2: e8 1f 05 00 00 call 80490d6 <explode_bomb>
8048bb7: 83 c3 04 add $0x4,%ebx
8048bba: 39 f3 cmp %esi,%ebx
8048bbc: 75 ea jne 8048ba8 <phase_2+0x34>
8048bbe: 83 c4 34 add $0x34,%esp
8048bc1: 5b pop %ebx
8048bc2: 5e pop %esi
8048bc3: c3 ret
分析
观察到phase_2调用了read_six_numbers这个函数,根据phase_1的经验,phase_n函数内调用的函数很可能就是突破口。第4~7行配置read_six_numbers的参数,其中根据main函数可知0x40(%esp)中的内容为输入的字符串的首地址,结合阅读read_six_numbers函数的代码,得知该函数读取6个整型数字,并将读取的数字从左到右依次保存至0x18(%esp)~0x2c(%esp)中。若读取的数字不足6个,将会引爆炸弹。然而,并非随意输入6个数字即可。通过第9~13行得知,0x18(%esp)中的内容,即第一个数字必须等于0,而0x1c(%esp)中的内容,即第二个数字必须等于1,否则都将进入explode_bomb函数导致炸弹引爆。而15~24行是一个循环结构,翻译成伪代码为
pos = %edx; end_pos = %esi;
a[] = 读取的数字
while(pos != end_pos)
{
if(a[pos] + a[pos+1] != a[pos+2])
explode_bomb();
pos++;
}
于是,可以得知,要输入的6个数字,就是斐波那契数列的前六个数字:0 1 1 2 3 5。
Phase_3
密钥:0 392 或 1 -213 或 2 -47 或 3 -878 或 4 0 或5 -878
08048bc4 <phase_3>:
8048bc4: 83 ec 2c sub $0x2c,%esp
8048bc7: 8d 44 24 1c lea 0x1c(%esp),%eax
8048bcb: 89 44 24 0c mov %eax,0xc(%esp)
8048bcf: 8d 44 24 18 lea 0x18(%esp),%eax
8048bd3: 89 44 24 08 mov %eax,0x8(%esp)
8048bd7: c7 44 24 04 c3 a3 04 movl $0x804a3c3,0x4(%esp)
8048bde: 08
8048bdf: 8b 44 24 30 mov 0x30(%esp),%eax
8048be3: 89 04 24 mov %eax,(%esp)
8048be6: e8 85 fc ff ff call 8048870 <__isoc99_sscanf@plt>
8048beb: 83 f8 01 cmp $0x1,%eax
8048bee: 7f 05 jg 8048bf5 <phase_3+0x31>
8048bf0: e8 e1 04 00 00 call 80490d6 <explode_bomb>
8048bf5: 83 7c 24 18 07 cmpl $0x7,0x18(%esp)
8048bfa: 77 66 ja 8048c62 <phase_3+0x9e>
8048bfc: 8b 44 24 18 mov 0x18(%esp),%eax
8048c00: ff 24 85 00 a2 04 08 jmp *0x804a200(,%eax,4)
8048c07: b8 00 00 00 00 mov $0x0,%eax
8048c0c: eb 05 jmp 8048c13 <phase_3+0x4f>
8048c0e: b8 5d 02 00 00 mov $0x25d,%eax
8048c13: 2d a6 00 00 00 sub $0xa6,%eax
8048c18: eb 05 jmp 8048c1f <phase_3+0x5b>
8048c1a: b8 00 00 00 00 mov $0x0,%eax
8048c1f: 05 3f 03 00 00 add $0x33f,%eax
8048c24: eb 05 jmp 8048c2b <phase_3+0x67>
8048c26: b8 00 00 00 00 mov $0x0,%eax
8048c2b: 2d 6e 03 00 00 sub $0x36e,%eax
8048c30: eb 05 jmp 8048c37 <phase_3+0x73>
8048c32: b8 00 00 00 00 mov $0x0,%eax
8048c37: 05 6e 03 00 00 add $0x36e,%eax
8048c3c: eb 05 jmp 8048c43 <phase_3+0x7f>
8048c3e: b8 00 00 00 00 mov $0x0,%eax
8048c43: 2d 6e 03 00 00 sub $0x36e,%eax
8048c48: eb 05 jmp 8048c4f <phase_3+0x8b>
8048c4a: b8 00 00 00 00 mov $0x0,%eax
8048c4f: 05 6e 03 00 00 add $0x36e,%eax
8048c54: eb 05 jmp 8048c5b <phase_3+0x97>
8048c56: b8 00 00 00 00 mov $0x0,%eax
8048c5b: 2d 6e 03 00 00 sub $0x36e,%eax
8048c60: eb 0a jmp 8048c6c <phase_3+0xa8>
8048c62: e8 6f 04 00 00 call 80490d6 <explode_bomb>
8048c67: b8 00 00 00 00 mov $0x0,%eax
8048c6c: 83 7c 24 18 05 cmpl $0x5,0x18(%esp)
8048c71: 7f 06 jg 8048c79 <phase_3+0xb5>
8048c73: 3b 44 24 1c cmp 0x1c(%esp),%eax
8048c77: 74 05 je 8048c7e <phase_3+0xba>
8048c79: e8 58 04 00 00 call 80490d6 <explode_bomb>
8048c7e: 83 c4 2c add $0x2c,%esp
8048c81: c3 ret
分析
第2~9行是为scanf函数配置参数以及执行scanf函数的过程,用print查看$0x804a3c3中的内容,发现是“%d %d”,说明scanf读取两个整型数,即密钥由两个整型数构成。第13~14行,0x18(%esp)中是读取的第一个数,ja 指令意味着无符号数的大小比较,即第一个数的取值范围在[0, 7]中。第15~16行是变址寻址的地址跳转,%eax中是我们输入的第一个数,跳转的地址是*(0x804a200+(%eax)*4)。这里以输入的第一个数是0为例,跳转的地址为*0x804a200,查看该地址中的内容,发现是0x8048c0e,于是跳转至该处的指令上。指令不停执行,直到倒数第6~7行,我们发现,该处指令对第一个数的范围做了新的限制,即将第一个数的范围缩小到[0, 5]。倒数第5行,将%eax与输入的第二个数进行比较,若不相等则引爆炸弹。%eax是根据此前的操作得来的,这时利用print命令查看%eax的内容,发现为392,即密钥为0 392。但由于第一个数可取0,1,2,3,4,5,故依次测试其余5个数作为第一个数,可得到不同第一个数对应的第二个数。所有可能的结果为0 392 或 1 -213 或 2 -47 或 3 -878 或 4 0 或5 -878。
Phase_4
密钥:40 2 或 60 3 或 80 4
08048c82 <func4>:
8048c82: 83 ec 1c sub $0x1c,%esp
8048c85: 89 5c 24 10 mov %ebx,0x10(%esp)
8048c89: 89 74 24 14 mov %esi,0x14(%esp)
8048c8d: 89 7c 24 18 mov %edi,0x18(%esp)
8048c91: 8b 74 24 20 mov 0x20(%esp),%esi
8048c95: 8b 5c 24 24 mov 0x24(%esp),%ebx
8048c99: 85 f6 test %esi,%esi
8048c9b: 7e 2b jle 8048cc8 <func4+0x46>
8048c9d: 83 fe 01 cmp $0x1,%esi
8048ca0: 74 2b je 8048ccd <func4+0x4b>
8048ca2: 89 5c 24 04 mov %ebx,0x4(%esp)
8048ca6: 8d 46 ff lea -0x1(%esi),%eax
8048ca9: 89 04 24 mov %eax,(%esp)
8048cac: e8 d1 ff ff ff call 8048c82 <func4>
8048cb1: 8d 3c 18 lea (%eax,%ebx,1),%edi
8048cb4: 89 5c 24 04 mov %ebx,0x4(%esp)
8048cb8: 83 ee 02 sub $0x2,%esi
8048cbb: 89 34 24 mov %esi,(%esp)
8048cbe: e8 bf ff ff ff call 8048c82 <func4>
8048cc3: 8d 1c 07 lea (%edi,%eax,1),%ebx
8048cc6: eb 05 jmp 8048ccd <func4+0x4b>
8048cc8: bb 00 00 00 00 mov $0x0,%ebx
8048ccd: 89 d8 mov %ebx,%eax
8048ccf: 8b 5c 24 10 mov 0x10(%esp),%ebx
8048cd3: 8b 74 24 14 mov 0x14(%esp),%esi
8048cd7: 8b 7c 24 18 mov 0x18(%esp),%edi
8048cdb: 83 c4 1c add $0x1c,%esp
8048cde: c3 ret
08048cdf <phase_4>:
8048cdf: 83 ec 2c sub $0x2c,%esp
8048ce2: 8d 44 24 18 lea 0x18(%esp),%eax
8048ce6: 89 44 24 0c mov %eax,0xc(%esp)
8048cea: 8d 44 24 1c lea 0x1c(%esp),%eax
8048cee: 89 44 24 08 mov %eax,0x8(%esp)
8048cf2: c7 44 24 04 c3 a3 04 movl $0x804a3c3,0x4(%esp)
8048cf9: 08
8048cfa: 8b 44 24 30 mov 0x30(%esp),%eax
8048cfe: 89 04 24 mov %eax,(%esp)
8048d01: e8 6a fb ff ff call 8048870 <__isoc99_sscanf@plt>
8048d06: 83 f8 02 cmp $0x2,%eax
8048d09: 75 0e jne 8048d19 <phase_4+0x3a>
8048d0b: 8b 44 24 18 mov 0x18(%esp),%eax
8048d0f: 83 f8 01 cmp $0x1,%eax
8048d12: 7e 05 jle 8048d19 <phase_4+0x3a>
8048d14: 83 f8 04 cmp $0x4,%eax
8048d17: 7e 05 jle 8048d1e <phase_4+0x3f>
8048d19: e8 b8 03 00 00 call 80490d6 <explode_bomb>
8048d1e: 8b 44 24 18 mov 0x18(%esp),%eax
8048d22: 89 44 24 04 mov %eax,0x4(%esp)
8048d26: c7 04 24 06 00 00 00 movl $0x6,(%esp)
8048d2d: e8 50 ff ff ff call 8048c82 <func4>
8048d32: 3b 44 24 1c cmp 0x1c(%esp),%eax
8048d36: 74 05 je 8048d3d <phase_4+0x5e>
8048d38: e8 99 03 00 00 call 80490d6 <explode_bomb>
8048d3d: 83 c4 2c add $0x2c,%esp
8048d40: c3 ret
分析
第2~9行是为scanf函数配置参数以及执行scanf函数的过程,用print查看$0x804a3c3中的内容,发现是“%d %d”,说明scanf读取两个整型数,即密钥由两个整型数构成,这也可以通过第10~11行,scanf的返回值不为2时则引爆炸弹看出。第12~16行,0x18(%esp)中的内容是输入的第二个数(与phase_3不同是因为写入的位置调换了),可以看出第二个数的取值范围为(1, 4]。第18~20行,为func4函数配置参数。接下来我们进入func4函数,func4是一个递归函数,为了便于解释,在此将func4函数翻译成c语言。func4函数翻译成c语言为
int func4(int k, int n)
{
if(k <= 0) return 0;
if(k == 1) return n;
return func4(k-1, n) + func4(k-2, n) + n;
}
由于第二个数的取值范围为(1, 4],故第二个数可取2,3,4,这里以2为例。代码中传入的参数为k = 6, n = 2(即以2为例的第二个数),经过计算得到func4(6, 2)的结果为40。接着观察到第21~22行,需要比较func4的结果是否与0x1c(%esp),即输入的第一个数相等,若不相等则引爆炸弹。于是,便可得知第一个数即是func(6, 第二个数)的值,当第二个数为2时,第一个数为40,故密钥为40 2。当第二个数取3/4时,还可以生成密钥60 3和80 4。
Phase_5
密钥:`adejn 或 abdejl 或 abejmn 或 adefin 或 adefjm 或 adekln 或 adijln 或 aefjkn 或 aeflmn 或 aegjln 或 bdefln 或 beijln 或 cdejln 或 defgjn 或 defijl 或 dehjmn 或 deijkn 或 deilmn 或 dfjlmn 或 efhjln 或 efijmn 或 ejklmn 或 更多
08048d41 <phase_5>:
8048d41: 53 push %ebx
8048d42: 83 ec 18 sub $0x18,%esp
8048d45: 8b 5c 24 20 mov 0x20(%esp),%ebx
8048d49: 89 1c 24 mov %ebx,(%esp)
8048d4c: e8 5a 02 00 00 call 8048fab <string_length>
8048d51: 83 f8 06 cmp $0x6,%eax
8048d54: 74 05 je 8048d5b <phase_5+0x1a>
8048d56: e8 7b 03 00 00 call 80490d6 <explode_bomb>
8048d5b: ba 00 00 00 00 mov $0x0,%edx
8048d60: b8 00 00 00 00 mov $0x0,%eax
8048d65: 0f be 0c 03 movsbl (%ebx,%eax,1),%ecx
8048d69: 83 e1 0f and $0xf,%ecx
8048d6c: 03 14 8d 20 a2 04 08 add 0x804a220(,%ecx,4),%edx
8048d73: 83 c0 01 add $0x1,%eax
8048d76: 83 f8 06 cmp $0x6,%eax
8048d79: 75 ea jne 8048d65 <phase_5+0x24>
8048d7b: 83 fa 45 cmp $0x45,%edx
8048d7e: 74 05 je 8048d85 <phase_5+0x44>
8048d80: e8 51 03 00 00 call 80490d6 <explode_bomb>
8048d85: 83 c4 18 add $0x18,%esp
8048d88: 5b pop %ebx
8048d89: c3 ret
分析
由第3~6行可知,程序将输入的字符串作为参数执行了string_length函数,若输入的字符串长度不为6,则引爆炸弹。接下来是一个循环结构,遍历输入的字符串,将当前遍历的字节与0xf相与得到一个数,这个数将作为首地址在0x804a220处的整型数组的下标,并将访问到的整型数组中的数进行累加,若和为0x45,则该字符串即是密钥。为了便于解释,将汇编代码翻译成c代码如下:
char s[6] = 输入的字符串
int a[16] = 首地址在0x804a220处的数组,由于和0xf相与,故下标最大值为15,数组长度为16
int sum = 0; //累加和
for(int i = 0; i < 6; ++i)
{
int pos = (int)s[i] & 0xf;
sum += a[pos];
}
if(sum != 0x45)
explode_bomb();
于是,我们要找到密钥,就要先找到总和为0x45的6个数的下标。为了方便找到这样的6个数的集合,可以编写程序,得到这16个数中,共有22组这样的6个数的集合。这22组数在数组中的下标如下:
0 1 4 5 10 14
1 2 4 5 10 12
1 2 5 10 13 14
1 4 5 6 9 14
1 4 5 6 10 13
1 4 5 11 12 14
1 4 9 10 12 14
1 5 6 10 11 14
1 5 6 12 13 14
1 5 7 10 12 14
2 4 5 6 12 14
2 5 9 10 12 14
3 4 5 10 12 14
4 5 6 7 10 14
4 5 6 9 10 12
4 5 8 10 13 14
4 5 9 10 11 14
4 5 9 12 13 14
4 6 10 12 13 14
5 6 8 10 12 14
5 6 9 10 13 14
5 10 11 12 13 14
那么,现在的问题变成了如何构造满足条件的字符串了。因为下标的范围为[0, 15],ASCII码为0~15的字符都是控制字符,是不可打印的字符,所以不能够直接输入[0, 15]对应的字符组成的字符串。考虑到输出英文字母比较直观,且字母’a’和’A’的ASCII码分别为1100001和1000001,后4位是0001,故可以将下标加上’a’-1或’A’-1,将得到的字符组成字符串即为密钥。如下标为5时,其二进制为0101,加上’a’-1为1100101,和0xf(1111)相与后仍为0101。
通过这种方法,更换下标的加数或调换下标的顺序,可以产生大量的密钥。
Phase_6
密钥:5 3 2 4 6 1
08048d8a <phase_6>:
8048d8a: 56 push %esi
8048d8b: 53 push %ebx
8048d8c: 83 ec 44 sub $0x44,%esp
8048d8f: 8d 44 24 10 lea 0x10(%esp),%eax
8048d93: 89 44 24 04 mov %eax,0x4(%esp)
8048d97: 8b 44 24 50 mov 0x50(%esp),%eax
8048d9b: 89 04 24 mov %eax,(%esp)
8048d9e: e8 68 04 00 00 call 804920b <read_six_numbers>
8048da3: be 00 00 00 00 mov $0x0,%esi
8048da8: 8b 44 b4 10 mov 0x10(%esp,%esi,4),%eax
8048dac: 83 e8 01 sub $0x1,%eax
8048daf: 83 f8 05 cmp $0x5,%eax
8048db2: 76 05 jbe 8048db9 <phase_6+0x2f>
8048db4: e8 1d 03 00 00 call 80490d6 <explode_bomb>
8048db9: 83 c6 01 add $0x1,%esi
8048dbc: 83 fe 06 cmp $0x6,%esi
8048dbf: 74 33 je 8048df4 <phase_6+0x6a>
8048dc1: 89 f3 mov %esi,%ebx
8048dc3: 8b 44 9c 10 mov 0x10(%esp,%ebx,4),%eax
8048dc7: 39 44 b4 0c cmp %eax,0xc(%esp,%esi,4)
8048dcb: 75 05 jne 8048dd2 <phase_6+0x48>
8048dcd: e8 04 03 00 00 call 80490d6 <explode_bomb>
8048dd2: 83 c3 01 add $0x1,%ebx
8048dd5: 83 fb 05 cmp $0x5,%ebx
8048dd8: 7e e9 jle 8048dc3 <phase_6+0x39>
8048dda: eb cc jmp 8048da8 <phase_6+0x1e>
8048ddc: 8b 52 08 mov 0x8(%edx),%edx
8048ddf: 83 c0 01 add $0x1,%eax
8048de2: 39 c8 cmp %ecx,%eax
8048de4: 75 f6 jne 8048ddc <phase_6+0x52>
8048de6: 89 54 b4 28 mov %edx,0x28(%esp,%esi,4)
8048dea: 83 c3 01 add $0x1,%ebx
8048ded: 83 fb 06 cmp $0x6,%ebx
8048df0: 75 07 jne 8048df9 <phase_6+0x6f>
8048df2: eb 1c jmp 8048e10 <phase_6+0x86>
8048df4: bb 00 00 00 00 mov $0x0,%ebx
8048df9: 89 de mov %ebx,%esi
8048dfb: 8b 4c 9c 10 mov 0x10(%esp,%ebx,4),%ecx
8048dff: b8 01 00 00 00 mov $0x1,%eax
8048e04: ba 3c c1 04 08 mov $0x804c13c,%edx
8048e09: 83 f9 01 cmp $0x1,%ecx
8048e0c: 7f ce jg 8048ddc <phase_6+0x52>
8048e0e: eb d6 jmp 8048de6 <phase_6+0x5c>
8048e10: 8b 5c 24 28 mov 0x28(%esp),%ebx
8048e14: 8b 44 24 2c mov 0x2c(%esp),%eax
8048e18: 89 43 08 mov %eax,0x8(%ebx)
8048e1b: 8b 54 24 30 mov 0x30(%esp),%edx
8048e1f: 89 50 08 mov %edx,0x8(%eax)
8048e22: 8b 44 24 34 mov 0x34(%esp),%eax
8048e26: 89 42 08 mov %eax,0x8(%edx)
8048e29: 8b 54 24 38 mov 0x38(%esp),%edx
8048e2d: 89 50 08 mov %edx,0x8(%eax)
8048e30: 8b 44 24 3c mov 0x3c(%esp),%eax
8048e34: 89 42 08 mov %eax,0x8(%edx)
8048e37: c7 40 08 00 00 00 00 movl $0x0,0x8(%eax)
8048e3e: be 05 00 00 00 mov $0x5,%esi
8048e43: 8b 43 08 mov 0x8(%ebx),%eax
8048e46: 8b 10 mov (%eax),%edx
8048e48: 39 13 cmp %edx,(%ebx)
8048e4a: 7d 05 jge 8048e51 <phase_6+0xc7>
8048e4c: e8 85 02 00 00 call 80490d6 <explode_bomb>
8048e51: 8b 5b 08 mov 0x8(%ebx),%ebx
8048e54: 83 ee 01 sub $0x1,%esi
8048e57: 75 ea jne 8048e43 <phase_6+0xb9>
8048e59: 83 c4 44 add $0x44,%esp
8048e5c: 5b pop %ebx
8048e5d: 5e pop %esi
8048e5e: c3 ret
分析
第2~5行读取6个整型数,由6~11行可知,读入的6个数的范围都必须在区间[1, 6]中。第12~22行的作用是判断读入的6个数是否两两互不相同,若否则会引爆炸弹。接下来的部分是将存储在0x804c13c处的内容存储到栈帧中,其中0x804c13c处存储的数据类型是node,node是一个由两个int(一个记录数值,一个记录node编号)、一个node*构成的结构体(实际上,node是一个链表),共占12个字节。存储node的方式是:按照读入的6个数(假定为a[i]),将编号为a[i]的node存储到第i个位置。然后从栈帧中的存储的第一个node开始,每个node与其后一个node中的数值进行比较,若后大于前,则会引爆炸弹。
综合该函数的行为,可以发现,函数要求将6个node在栈帧中根据数值从大到小排列,而输入的6个数,即是从大到小排列的node的编号。
以下是6个node的信息:
(gdb) x 0x804c13c
0x804c13c <node1>: 0x00000036
0x804c140 <node1+4>: 0x00000001
0x804c144 <node1+8>: 0x0804c148
0x804c148 <node2>: 0x000000c1
0x804c14c <node2+4>: 0x00000002
0x804c150 <node2+8>: 0x0804c154
0x804c154 <node3>: 0x00000273
0x804c158 <node3+4>: 0x00000003
0x804c15c <node3+8>: 0x0804c160
0x804c160 <node4>: 0x00000089
0x804c164 <node4+4>: 0x00000004
0x804c168 <node4+8>: 0x0804c16c
0x804c16c <node5>: 0x000003d7
0x804c170 <node5+4>: 0x00000005
0x804c174 <node5+8>: 0x0804c178
0x804c178 <node6>: 0x0000006c
0x804c17c <node6+4>: 0x00000006
0x804c180 <node6+8>: 0x00000000
可见,6个node根据数值从大到小排列的编号为5 3 2 4 6 1,即为密钥。
Secret_phase
密钥:107
0804925b <phase_defused>:
804925b: 81 ec 8c 00 00 00 sub $0x8c,%esp
8049261: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8049267: 89 44 24 7c mov %eax,0x7c(%esp)
804926b: 31 c0 xor %eax,%eax
804926d: 83 3d cc c3 04 08 06 cmpl $0x6,0x804c3cc
8049274: 75 72 jne 80492e8 <phase_defused+0x8d>
8049276: 8d 44 24 2c lea 0x2c(%esp),%eax
804927a: 89 44 24 10 mov %eax,0x10(%esp)
804927e: 8d 44 24 28 lea 0x28(%esp),%eax
8049282: 89 44 24 0c mov %eax,0xc(%esp)
8049286: 8d 44 24 24 lea 0x24(%esp),%eax
804928a: 89 44 24 08 mov %eax,0x8(%esp)
804928e: c7 44 24 04 c9 a3 04 movl $0x804a3c9,0x4(%esp)
8049295: 08
8049296: c7 04 24 d0 c4 04 08 movl $0x804c4d0,(%esp)
804929d: e8 ce f5 ff ff call 8048870 <__isoc99_sscanf@plt>
80492a2: 83 f8 03 cmp $0x3,%eax
80492a5: 75 35 jne 80492dc <phase_defused+0x81>
80492a7: c7 44 24 04 d2 a3 04 movl $0x804a3d2,0x4(%esp)
80492ae: 08
80492af: 8d 44 24 2c lea 0x2c(%esp),%eax
80492b3: 89 04 24 mov %eax,(%esp)
80492b6: e8 09 fd ff ff call 8048fc4 <strings_not_equal>
80492bb: 85 c0 test %eax,%eax
80492bd: 75 1d jne 80492dc <phase_defused+0x81>
80492bf: c7 04 24 98 a2 04 08 movl $0x804a298,(%esp)
80492c6: e8 35 f5 ff ff call 8048800 <puts@plt>
80492cb: c7 04 24 c0 a2 04 08 movl $0x804a2c0,(%esp)
80492d2: e8 29 f5 ff ff call 8048800 <puts@plt>
80492d7: e8 d4 fb ff ff call 8048eb0 <secret_phase>
80492dc: c7 04 24 f8 a2 04 08 movl $0x804a2f8,(%esp)
80492e3: e8 18 f5 ff ff call 8048800 <puts@plt>
80492e8: 8b 44 24 7c mov 0x7c(%esp),%eax
80492ec: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
80492f3: 74 05 je 80492fa <phase_defused+0x9f>
80492f5: e8 d6 f4 ff ff call 80487d0 <__stack_chk_fail@plt>
80492fa: 81 c4 8c 00 00 00 add $0x8c,%esp
8049300: c3 ret
8049301: 90 nop
8049302: 90 nop
8049303: 90 nop
8049304: 90 nop
8049305: 90 nop
8049306: 90 nop
8049307: 90 nop
8049308: 90 nop
8049309: 90 nop
804930a: 90 nop
804930b: 90 nop
804930c: 90 nop
804930d: 90 nop
804930e: 90 nop
804930f: 90 nop
08048e5f <fun7>:
8048e5f: 53 push %ebx
8048e60: 83 ec 18 sub $0x18,%esp
8048e63: 8b 54 24 20 mov 0x20(%esp),%edx
8048e67: 8b 4c 24 24 mov 0x24(%esp),%ecx
8048e6b: 85 d2 test %edx,%edx
8048e6d: 74 37 je 8048ea6 <fun7+0x47>
8048e6f: 8b 1a mov (%edx),%ebx
8048e71: 39 cb cmp %ecx,%ebx
8048e73: 7e 13 jle 8048e88 <fun7+0x29>
8048e75: 89 4c 24 04 mov %ecx,0x4(%esp)
8048e79: 8b 42 04 mov 0x4(%edx),%eax
8048e7c: 89 04 24 mov %eax,(%esp)
8048e7f: e8 db ff ff ff call 8048e5f <fun7>
8048e84: 01 c0 add %eax,%eax
8048e86: eb 23 jmp 8048eab <fun7+0x4c>
8048e88: b8 00 00 00 00 mov $0x0,%eax
8048e8d: 39 cb cmp %ecx,%ebx
8048e8f: 74 1a je 8048eab <fun7+0x4c>
8048e91: 89 4c 24 04 mov %ecx,0x4(%esp)
8048e95: 8b 42 08 mov 0x8(%edx),%eax
8048e98: 89 04 24 mov %eax,(%esp)
8048e9b: e8 bf ff ff ff call 8048e5f <fun7>
8048ea0: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax
8048ea4: eb 05 jmp 8048eab <fun7+0x4c>
8048ea6: b8 ff ff ff ff mov $0xffffffff,%eax
8048eab: 83 c4 18 add $0x18,%esp
8048eae: 5b pop %ebx
8048eaf: c3 ret
08048eb0 <secret_phase>:
8048eb0: 53 push %ebx
8048eb1: 83 ec 18 sub $0x18,%esp
8048eb4: e8 44 02 00 00 call 80490fd <read_line>
8048eb9: c7 44 24 08 0a 00 00 movl $0xa,0x8(%esp)
8048ec0: 00
8048ec1: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
8048ec8: 00
8048ec9: 89 04 24 mov %eax,(%esp)
8048ecc: e8 0f fa ff ff call 80488e0 <strtol@plt>
8048ed1: 89 c3 mov %eax,%ebx
8048ed3: 8d 40 ff lea -0x1(%eax),%eax
8048ed6: 3d e8 03 00 00 cmp $0x3e8,%eax
8048edb: 76 05 jbe 8048ee2 <secret_phase+0x32>
8048edd: e8 f4 01 00 00 call 80490d6 <explode_bomb>
8048ee2: 89 5c 24 04 mov %ebx,0x4(%esp)
8048ee6: c7 04 24 88 c0 04 08 movl $0x804c088,(%esp)
8048eed: e8 6d ff ff ff call 8048e5f <fun7>
8048ef2: 83 f8 03 cmp $0x3,%eax
8048ef5: 74 05 je 8048efc <secret_phase+0x4c>
8048ef7: e8 da 01 00 00 call 80490d6 <explode_bomb>
8048efc: c7 04 24 c4 a1 04 08 movl $0x804a1c4,(%esp)
8048f03: e8 f8 f8 ff ff call 8048800 <puts@plt>
8048f08: e8 4e 03 00 00 call 804925b <phase_defused>
8048f0d: 83 c4 18 add $0x18,%esp
8048f10: 5b pop %ebx
8048f11: c3 ret
8048f12: 90 nop
8048f13: 90 nop
8048f14: 90 nop
8048f15: 90 nop
8048f16: 90 nop
8048f17: 90 nop
8048f18: 90 nop
8048f19: 90 nop
8048f1a: 90 nop
8048f1b: 90 nop
8048f1c: 90 nop
8048f1d: 90 nop
8048f1e: 90 nop
8048f1f: 90 nop
分析
在阅读汇编代码时,发现phase_6之下还有一个secret_phase,直接输入6个密钥会发现直接退出了运行,而不会进入这个秘密关卡,于是我启用搜索,看看是什么函数调用了这个secret_phase。
在代码中搜索secret_phase,会发现phase_defused函数调用了secret_phase,而这个phase_defused函数是每个phase通过之后会执行的函数,于是我联想到是否需要在某个phase输入特定的信息才会进入这个secret_phase。阅读phase_defused的汇编代码,会发现出现了几个内存的地址,查看这些地址的内容,会发现这的确是secret_phase的入口。
其中,DrEvil出现在0x804a3d2中,而phase_defused调用了strings_not_equal函数来比较DrEvil和一个应该是我们输入的字符串的地方,这说明,我们开启secret_phase需要输入的信息是DrEvil!
80492a7: c7 44 24 04 d2 a3 04 movl $0x 804a3d2,0x4(%esp)
80492ae: 08
80492af: 8d 44 24 2c lea 0x2c(%esp),%eax
80492b3: 89 04 24 mov %eax,(%esp)
80492b6: e8 09 fd ff ff call 8048fc4 <strings_not_equal>
80492bb: 85 c0 test %eax,%eax
80492bd: 75 1d jne 80492dc <phase_defused+0x81>
那么这个DrEvil该什么时候输入呢?
804929d: e8 ce f5 ff ff call 8048870 <__isoc99_sscanf@plt>
80492a2: 83 f8 03 cmp $0x3,%eax
80492a5: 75 35 jne 80492dc <phase_defused+0x81>
通过以上这3行可以看到,包括DrEvil在内,输入的参数的数量需要是3,且分别为%d %d %s否则将会跳过secret_phase的调用。可以发现,只有phase_4的密钥加上DrEvil满足这个条件。经测试,在phase_4的密钥后加上DrEvil后,在phase_6通过后,果然出现了secret_phase。
进入secret_phase,函数先读取我们输入的密钥,然后调用了c语言的strtol函数,这个函数将字符串转为长整型数,其中参数0xa意味着将其转为十进制。接下来函数会比较经转换后的数自减1后与0x3e8(十进制为1000)的无符号数大小,这意味着我们输入的数必须在[1, 1001]中。接着,函数会调用一个递归函数func7,参数为0x804c088和我们输入的数,func7的伪代码如下
int func7(int* k, int n)
{
if(k <= 0) return -1;
int kn = *k;
if(kn == n) return 0;
else if(kn < n) return 2*func7(k+2, n) + 1;
else return 2*func7(k+1, n);
}
又由以下两个指令知,func7(0x804c088, 我们输入的数)的返回值必须为3,否则将引爆炸弹。
8048ef2: 83 f8 03 cmp $0x3,%eax
8048ef5: 74 05 je 8048efc <secret_phase+0x4c>
观察内存中地址为0x804c088的内容,是一棵二叉树:第一个地址存储数据,第二、三个地址存储下子结点的地址。于是我们只要分析func7的行为,并根据0x804c088中的内容,即可确定要输入的数。
首先,函数要求func7最终返回3。要构造一个3,根据func7的特性,可以这样构造:2*(2*0+1)+1,这意味着,最后一次要返回0,倒数第二次(或第二次)要返回2*func7(k+2, n) + 1,倒数第三次(或第一次)要返回2*func7(k+2, n) + 1。或者说,要返回的值是二叉树查找时比较的次数。要满足这样的关系,假如我们把第1~3次比较的数设为k1、k2、k3,那么,我们输入的数n要满足的条件是n < k1且n < k2且n == k3。要找到k3,查找0x804c088处的内存即可,经查找,为0x6b,转换为十进制为107,即为密钥。
(gdb) x/x 0x804c088
0x804c088 <n1>: 0x00000024
0x804c08c <n1+4>: 0x0804c094
0x804c090 <n1+8>: 0x0804c0a0
0x804c094 <n21>: 0x00000008
0x804c098 <n21+4>: 0x0804c0c4
0x804c09c <n21+8>: 0x0804c0ac
0x804c0a0 <n22>: 0x00000032
0x804c0a4 <n22+4>: 0x0804c0b8
0x804c0a8 <n22+8>: 0x0804c0d0
0x804c0ac <n32>: 0x00000016
0x804c0b0 <n32+4>: 0x0804c118
0x804c0b4 <n32+8>: 0x0804c100
0x804c0b8 <n33>: 0x0000002d
0x804c0bc <n33+4>: 0x0804c0dc
0x804c0c0 <n33+8>: 0x0804c124
0x804c0c4 <n31>: 0x00000006
0x804c0c8 <n31+4>: 0x0804c0e8
0x804c0cc <n31+8>: 0x0804c10c
0x804c0d0 <n34>: 0x0000006b
0x804c0d4 <n34+4>: 0x0804c0f4
0x804c0d8 <n34+8>: 0x0804c130
0x804c0dc <n45>: 0x00000028
0x804c0e0 <n45+4>: 0x00000000
0x804c0e4 <n45+8>: 0x00000000
0x804c0e8 <n41>: 0x00000001
0x804c0ec <n41+4>: 0x00000000
0x804c0f0 <n41+8>: 0x00000000
0x804c0f4 <n47>: 0x00000063
0x804c0f8 <n47+4>: 0x00000000
0x804c0fc <n47+8>: 0x00000000
0x804c100 <n44>: 0x00000023
0x804c104 <n44+4>: 0x00000000
0x804c108 <n44+8>: 0x00000000
0x804c10c <n42>: 0x00000007
0x804c110 <n42+4>: 0x00000000
0x804c114 <n42+8>: 0x00000000
0x804c118 <n43>: 0x00000014
0x804c11c <n43+4>: 0x00000000
0x804c120 <n43+8>: 0x00000000
0x804c124 <n46>: 0x0000002f
0x804c128 <n46+4>: 0x00000000
0x804c12c <n46+8>: 0x00000000
0x804c130 <n48>: 0x000003e9
0x804c134 <n48+4>: 0x00000000
0x804c138 <n48+8>: 0x00000000