如果下面官话太多,那只是因为这本来是我的一份实验报告搬过来的。。。
X 86 X86 X86汇编基础—二进制炸弹
1. 1. 1.初步认识 X 86 X86 X86汇编语言;
2. 2. 2.掌握阅读程序反汇编代码的方法,了解程序在机器上运行的实质;
3. 3. 3.熟悉 L i n u x Linux Linux环境,掌握调试器 g d b gdb gdb和反汇编工具 o b j d u m p objdump objdump的使用。
1. 1. 1.装有 L i n u x Linux Linux系统/虚拟系统的电脑;
2. 2. 2. g d b , o b j d u m p gdb,objdump gdb,objdump等相关软件;
3. 3. 3.一个二进制炸弹(此为bomb41)。
使用反汇编工具 o b j d u m p objdump objdump将 b o m b bomb bomb的汇编代码搞出来,方便日后破解。
sysu@debian:~/bomb41$ objdump -d ./bomb > disbomb.txt
8048b60: 68 44 a2 04 08 push $0x804a244
8048b65: ff 75 08 pushl 0x8(%ebp)
8048b68: e8 f8 04 00 00 call 8049065
8048b70: 85 c0 test %eax,%eax //若eax为0,则ZF置1,否则置0
8048b72: 75 02 jne 8048b76 //若ZF=1则跳转
8048b74: c9 leave
8048b76: e8 1e 07 00 00 call 8049299
第一关的汇编代码相对比较短,关键部分如上所示,可以看出:
首先它先把地址$ 0 x 804 a 244 0x804a244 0x804a244和 0 x 8 ( % e b p ) 0x8(\%ebp) 0x8(%ebp)压栈,并且调用 s t r i n g s _ n o t _ e q u a l strings\_not\_equal strings_not_equal函数。
根据函数名字可以大(bu)胆(yong)猜(zheng)测(ming),该函数接受两个字符串参数,并返回其是否相等。
而在 X 86 X86 X86中,运算结果默认保存在累加寄存器,即eax中。
所以接下来它检测 e a x eax eax是否等于 0 0 0( t e s t test test和 j n e jne jne的组合操作),等于 0 0 0则安全退出,否则爆炸。
那么现在的问题则是$ 0 x 804 a 244 0x804a244 0x804a244和 0 x 8 ( % e b p ) 0x8(\%ebp) 0x8(%ebp)分别是什么:
e b p ebp ebp是栈底指针寄存器,查看之前的压栈记录可知:
8048a5a: e8 b4 08 00 00 call 8049313
8048a5f: 89 04 24 mov %eax,(%esp) //%eax即read_line的返回值
8048b5a: 55 push %ebp
8048b5b: 89 e5 mov %esp,%ebp //esp是栈顶指针寄存器
e b p ebp ebp+ 4 4 4为上次的栈底指针, e b p ebp ebp+ 8 8 8则是 r e a d _ l i n e read\_line read_line的返回内容。
而在 g d b gdb gdb中查看$ 0 x 804 a 244 0x804a244 0x804a244的内容,可知:
(gdb) x/s 0x804a244
0x804a244: "I was trying to give Tina Fey more material."
那么到这答案就呼之欲出了,程序先是调用了 r e a d _ l i n e read\_line read_line读取一整行,然后将该行字符串和$ 0 x 804 a 244 0x804a244 0x804a244中的内容进行比较,相等则安全通过。
所以第一关的答案应该是**“I was trying to give Tina Fey more material.”**(没有引号)
sysu@debian:~/bomb41$ ./bomb
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
I was trying to give Tina Fey more material.
Phase 1 defused. How about the next one?
至此第一关通过,拆炸弹真简单。
第二关的汇编代码开始变得复杂起来,但仔细阅读后发现其大致可以划分为几小段,下面就一段一段的分析:
8048b8f: 8d 45 dc lea -0x24(%ebp),%eax
8048b92: 50 push %eax
8048b93: ff 75 08 pushl 0x8(%ebp)
8048b96: e8 3e 07 00 00 call 80492d9
在这一段中, e a x : = e b p − 0 x 24 eax:=ebp-0x24 eax:=ebp−0x24,然后和 0 x 8 ( % e b p ) 0x8( \%ebp) 0x8(%ebp)一起被传进去了 r e a d _ s i x _ n u m b e r s read\_six\_numbers read_six_numbers函数中;
回看之前的压栈记录可知:
8048a78: e8 96 08 00 00 call 8049313
8048a7d: 89 04 24 mov %eax,(%esp)
8048b7d: 55 push %ebp
8048b7e: 89 e5 mov %esp,%ebp
此时 0 x 8 ( % e b p ) 0x8(\%ebp) 0x8(%ebp)就是第二关读取的字符串,而根据 r e a d _ s i x _ n u m b e r s read\_six\_numbers read_six_numbers的名字猜测,这关应该要读入六个数字。
所以接下来一个很明显的问题就是这六个数字放在了什么地方?
查看 r e a d _ s i x _ n u m b e r s read\_six\_numbers read_six_numbers的汇编代码:
080492d9 :
80492d9: 55 push %ebp
80492da: 89 e5 mov %esp,%ebp
80492dc: 83 ec 08 sub $0x8,%esp
80492df: 8b 45 0c mov 0xc(%ebp),%eax
//-------------------------------------------------------
80492e2: 8d 50 14 lea 0x14(%eax),%edx
80492e5: 52 push %edx
80492e6: 8d 50 10 lea 0x10(%eax),%edx
80492e9: 52 push %edx
80492ea: 8d 50 0c lea 0xc(%eax),%edx
80492ed: 52 push %edx
80492ee: 8d 50 08 lea 0x8(%eax),%edx
80492f1: 52 push %edx
80492f2: 8d 50 04 lea 0x4(%eax),%edx
80492f5: 52 push %edx
80492f6: 50 push %eax
80492f7: 68 e5 a4 04 08 push $0x804a4e5
80492fc: ff 75 08 pushl 0x8(%ebp)
80492ff: e8 0c f5 ff ff call 8048810 <__isoc99_sscanf@plt>
分割线以下的代码显然是
s s c a n f ( 0 x 8 ( % e b p ) , ( 0 x 804 a 4 e 5 ) , e a x , e a x + 0 x 4 , e a x + 0 x 8 , e a x + 0 x c , e a x + 0 x 10 , e a x + 0 x 14 ) sscanf(0x8(\%ebp),(0x804a4e5),eax,eax+0x4,eax+0x8,eax+0xc,eax+0x10,eax+0x14) sscanf(0x8(%ebp),(0x804a4e5),eax,eax+0x4,eax+0x8,eax+0xc,eax+0x10,eax+0x14).
0 x 8 ( % e b p ) 0x8(\%ebp) 0x8(%ebp)就是第二关读取的字符串,而在 g d b gdb gdb中查看$ 0 x 804 a 4 e 5 0x804a4e5 0x804a4e5的内容:
(gdb) x/s 0x804a4e5
0x804a4e5: "%d %d %d %d %d %d"
结合 e a x = 0 x c ( % e b p ) eax=0xc(\%ebp) eax=0xc(%ebp)可知, r e a d _ s i x _ n u m b e r s read\_six\_numbers read_six_numbers接受两个参数,分别为一个字符串 s t r str str和一个地址 p o i n t e r pointer pointer,并在 s t r str str中读取六个数放在以 p o i n t e r pointer pointer开头的连续六个地址中。
回到 p h a s e _ 2 phase\_2 phase_2的函数,由于传给 r e a d _ s i x _ n u m b e r s read\_six\_numbers read_six_numbers的地址为 e a x : = e b p − 0 x 24 eax:=ebp-0x24 eax:=ebp−0x24,所以这关读入的六个数字存放在以 e b p − 0 x 24 ebp-0x24 ebp−0x24开头的地址中。
读完六个数字后,紧接着的汇编代码为:
8048b9e: 83 7d dc 03 cmpl $0x3,-0x24(%ebp)
8048ba2: 75 07 jne 8048bab
8048ba4: bb 01 00 00 00 mov $0x1,%ebx
8048ba9: eb 0f jmp 8048bba //类似于循环的初始化
8048bab: e8 e9 06 00 00 call 8049299
8048bb0: eb f2 jmp 8048ba4
8048bb2: 83 c3 01 add $0x1,%ebx //循环的判断
8048bb5: 83 fb 06 cmp $0x6,%ebx
8048bb8: 74 1a je 8048bd4
8048bba: 8b 44 9d d8 mov -0x28(%ebp,%ebx,4),%eax
8048bbe: 89 45 d4 mov %eax,-0x2c(%ebp)
8048bc1: 83 c0 01 add $0x1,%eax
8048bc4: 0f af c3 imul %ebx,%eax
8048bc7: 39 44 9d dc cmp %eax,-0x24(%ebp,%ebx,4)
8048bcb: 74 e5 je 8048bb2 //循环的标志:会跳回到前面
8048bcd: e8 c7 06 00 00 call 8049299
8048bd2: eb de jmp 8048bb2
//假设ebp-0x24为int a[]的首地址
if (a[0]!=3) explode_bomb();
for (ebx:=1;ebx!=6;++ebx) {
eax:=a[ebx-1];
//mov %eax,-0x2c(%ebp)在这里仿佛没用~
eax+=1;
eax*=ebx;
if (eax==a[ebx]) continue;
explode_bomb();
}
到了这里就很容易看出:
a [ i ] = { 3 , i = 0 ( a [ i − 1 ] + 1 ) ⋅ i , i = 1 , 2 , 3 , 4 , 5 \begin{aligned} a[i]= \begin{cases} 3&,&i=0\\ (a[i-1]+1)\cdot i&,&i=1,2,3,4,5 \end{cases} \end{aligned} a[i]={3(a[i−1]+1)⋅i,,i=0i=1,2,3,4,5
即读入 a [ ] = { 3 , 4 , 10 , 33 , 136 , 685 } a[]=\{3,4,10,33,136,685\} a[]={3,4,10,33,136,685}即可.
第三关开始就变得很长了.
先看输入:
8048bfb: 8d 45 f0 lea -0x10(%ebp),%eax
8048bfe: 50 push %eax
8048bff: 8d 45 eb lea -0x15(%ebp),%eax
8048c02: 50 push %eax
8048c03: 8d 45 ec lea -0x14(%ebp),%eax
8048c06: 50 push %eax
8048c07: 68 9a a2 04 08 push $0x804a29a
8048c0c: ff 75 08 pushl 0x8(%ebp)
8048c0f: e8 fc fb ff ff call 8048810 <__isoc99_sscanf@plt>
由前几关的经验可知是 s s c a n f ( 0 x 8 ( % e b p ) , ( 0 x 804 a 29 a ) , e b p − 0 x 14 , e b p − 0 x 15 , e b p − 0 x 10 ) sscanf(0x8(\%ebp),(0x804a29a),ebp-0x14,ebp-0x15,ebp-0x10) sscanf(0x8(%ebp),(0x804a29a),ebp−0x14,ebp−0x15,ebp−0x10).
0 x 8 ( % e b p ) 0x8(\%ebp) 0x8(%ebp)就是第三关读取的字符串,而在 g d b gdb gdb中查看$ 0 x 804 a 29 a 0x804a29a 0x804a29a的内容:
(gdb) x/s 0x804a29a
0x804a29a: "%d %c %d"
即这关需要读入一个数字,一个字符以及一个数字。
接下来就是长达十几行的汇编代码,但实际上是有很多重复性的代码,截取有代表性的代码段:
8048c1c: 83 7d ec 07 cmpl $0x7,-0x14(%ebp) //无符号比较
8048c20: 0f 87 b4 00 00 00 ja 8048cda //explode_bomb
8048c26: 8b 45 ec mov -0x14(%ebp),%eax
8048c29: ff 24 85 c0 a2 04 08 jmp *0x804a2c0(,%eax,4)
8048c30: e8 64 06 00 00 call 8049299
//类似于以下形式的代码段重复了六次
8048c37: b8 63 00 00 00 mov $0x63,%eax //以下忽略...
8048c58: b8 61 00 00 00 mov $0x61,%eax //以下忽略...
8048c72: b8 6b 00 00 00 mov $0x6b,%eax //以下忽略...
8048c8c: b8 70 00 00 00 mov $0x70,%eax //以下忽略...
8048ca6: b8 73 00 00 00 mov $0x73,%eax //以下忽略...
8048cc0: b8 6d 00 00 00 mov $0x6d,%eax //以下忽略...
先是第一个数和 0 x 7 0x7 0x7比较大小(无符号比较),若大于直接爆炸,即第一个数取值范围是0,1,2,3,4,5,6,7.
紧接着这两句话:
8048c26: 8b 45 ec mov -0x14(%ebp),%eax
8048c29: ff 24 85 c0 a2 04 08 jmp *0x804a2c0(,%eax,4)
∗ 0 x 804 a 2 c 0 ( , % e a x , 4 ) *0x804a2c0(,\%eax,4) ∗0x804a2c0(,%eax,4)这种格式为寄存器间接寻址,即 ( ( e a x ∗ 4 + 0 x 804 a 2 c 0 ) ) ((eax*4+0x804a2c0)) ((eax∗4+0x804a2c0)).
在 g d b gdb gdb中查看 0 x 804 a 2 c 0 0x804a2c0 0x804a2c0地址指向的内容为:
(gdb) x/8xw 0x804a2c0
0x804a2c0: 0x08048c37 0x08048cda 0x08048c58 0x08048cda
0x804a2d0: 0x08048c72 0x08048c8c 0x08048ca6 0x08048cc0
而这8个值恰好对应上述重复代码段的首地址!
即该程序会根据读入第一个数的值分别跳转到不同的代码段,不难想到这其实对应c++中的 s w i t c h switch switch.
eax:=-0x14(%ebp);//读入的第一个数
switch(eax) {
case 0: ....
case 1: ....
case 2: ....
case 3: ....
case 4: ....
case 5: ....
case 6: ....
case 7: ....
default:
}
则这关应该会有多种通关的答案,这里以 c a s e 0 case\ 0 case 0作为分析:
//case 0:
8048c37: b8 63 00 00 00 mov $0x63,%eax
8048c3c: 81 7d f0 c3 00 00 00 cmpl $0xc3,-0x10(%ebp)
8048c43: 0f 84 9b 00 00 00 je 8048ce4 //jump out
8048c49: e8 4b 06 00 00 call 8049299
8048c4e: b8 63 00 00 00 mov $0x63,%eax //这个地方应该是代码冗余
8048c53: e9 8c 00 00 00 jmp 8048ce4
即 − 0 x 10 ( % e b p ) -0x10(\%ebp) −0x10(%ebp)(第三个数字)和 0 x c 3 0xc3 0xc3( 19 5 10 195_{10} 19510)比较,相等则跳出 s w i t c h switch switch,否则爆炸。
接着跳出 s w i t c h switch switch后还有一个判断:
8048ce4: 38 45 eb cmp %al,-0x15(%ebp)
8048ce7: 74 05 je 8048cee //leave
8048ce9: e8 ab 05 00 00 call 8049299
将 − 0 x 15 ( % e b p ) -0x15(\%ebp) −0x15(%ebp)(第二个字符)和 % a l \%al %al进行比较,相等则安全通过此关,否则爆炸。
而 % a l \%al %al是寄存器 % e a x \%eax %eax的 l o w low low半段,而在进入 c a s e 0 case\ 0 case 0时, % e a x : = 0 x 63 \%eax:=0x63 %eax:=0x63,
所以第二个字符应该要等于 0 x 63 0x63 0x63( A S C I I 码 ASCII码 ASCII码对应为字符 ′ c ′ 'c' ′c′).
至此 c a s e 0 case\ 0 case 0的通关答案为 0 c 193 0\ c\ 193 0 c 193.
其他 c a s e case case分析类似,而实际上 c a s e 1 case\ 1 case 1和 c a s e 3 case\ 3 case 3会跳转到 0 x 8048 c d a 0x8048cda 0x8048cda(没猜错的话应该对应 d e f a u l t default default):
8048cda: e8 ba 05 00 00 call 8049299
所以本关总共应有六种通关答案( 0 , 2 , 4 , 5 , 6 , 7 0,2,4,5,6,7 0,2,4,5,6,7),管他多少个,能过就行。
老规矩,先看读入:
8048d5d: 8d 45 ec lea -0x14(%ebp),%eax
8048d60: 50 push %eax
8048d61: 8d 45 f0 lea -0x10(%ebp),%eax
8048d64: 50 push %eax
8048d65: 68 f1 a4 04 08 push $0x804a4f1
8048d6a: ff 75 08 pushl 0x8(%ebp)
8048d6d: e8 9e fa ff ff call 8048810 <__isoc99_sscanf@plt>
即 s s c a n f ( 0 x 8 ( % e b p ) , 0 x 804 a 4 f 1 , e b p − 0 x 14 , e b p − 0 x 10 ) sscanf(0x8(\%ebp),0x804a4f1,ebp-0x14,ebp-0x10) sscanf(0x8(%ebp),0x804a4f1,ebp−0x14,ebp−0x10),而在 g d b gdb gdb中查看$ 0 x 804 a 4 f 1 0x804a4f1 0x804a4f1的内容:
(gdb) x/s 0x804a4f1
0x804a4f1: "%d %d"
所以这关应该需要读入两个数字。
8048d7a: 8b 45 ec mov -0x14(%ebp),%eax //eax:=读入的第二个数
8048d7d: 83 e8 02 sub $0x2,%eax //eax-=2
8048d80: 83 f8 02 cmp $0x2,%eax
8048d83: 76 05 jbe 8048d8a //jbe为无符号比较
8048d85: e8 0f 05 00 00 call 8049299 //如果eax>=2时爆炸
紧接着的判断则可以知道,第二个数的数字只能是 2 , 3 , 4 2,3,4 2,3,4(无符号比较,如果 e a x eax eax小于2则往下溢出为很大的数)
8048d8d: ff 75 ec pushl -0x14(%ebp)
8048d90: 6a 06 push $0x6
8048d92: e8 6a ff ff ff call 8048d01
8048d97: 83 c4 10 add $0x10,%esp
8048d9a: 39 45 f0 cmp %eax,-0x10(%ebp) //运算结果默认存放在eax中
8048d9d: 74 05 je 8048da4
8048d9f: e8 f5 04 00 00 call 8049299
8048db0: c9 leave
然后 p h a s e _ 4 phase\_4 phase_4调用 f u n c ( − 0 x 14 ( % e b p ) , 0 x 6 ) func(-0x14(\%ebp),0x6) func(−0x14(%ebp),0x6),结果和 − 0 x 10 ( % e b p ) -0x10(\%ebp) −0x10(%ebp)(第一个数)进行比较,相等则此关通过.
这里其实有个小 t r i c k trick trick,因为第二个数只有三种取值,可以分别试出其对应的第一个数的值:
(gdb) p $eax //第一个数为2
$1: 40
(gdb) p $eax //第一个数为3
$1: 60
(gdb) p $eax //第一个数为4
$1: 80
所以读入 ( 40 , 2 ) 、 ( 60 , 3 ) 、 ( 80 , 4 ) (40,2)、(60,3)、(80,4) (40,2)、(60,3)、(80,4)都可通过此关。
当然本着求知的态度还是看一下func4,其实觉得第一个数刚好都是第二个数的20倍很好奇。。。
以下是 f u n c 4 func4 func4的汇编代码:
08048d01 :
8048d0a: 8b 75 08 mov 0x8(%ebp),%esi //func4第一个参数
8048d0d: 8b 7d 0c mov 0xc(%ebp),%edi //func4第二个参数
8048d10: b8 00 00 00 00 mov $0x0,%eax
8048d15: 85 f6 test %esi,%esi //test等价于and,但只改变标志寄存器的值
8048d17: 7e 07 jle 8048d20 //判断esi是否为0
8048d19: 89 f8 mov %edi,%eax //eax:=edi
8048d1b: 83 fe 01 cmp $0x1,%esi
8048d1e: 75 08 jne 8048d28 //判断esi是否为1
...
8048d27: c3 ret //默认返回结果放在eax
8048d28: 83 ec 08 sub $0x8,%esp
8048d2b: 57 push %edi
8048d2c: 8d 46 ff lea -0x1(%esi),%eax //这步等价于eax:=esi-0x1
8048d2f: 50 push %eax
8048d30: e8 cc ff ff ff call 8048d01 //调用func4(eax,edi)
8048d35: 83 c4 08 add $0x8,%esp
8048d38: 8d 1c 38 lea (%eax,%edi,1),%ebx //这步等价于ebx:=eax+edi*1
8048d3b: 57 push %edi
8048d3c: 83 ee 02 sub $0x2,%esi //esi-=2
8048d3f: 56 push %esi
8048d40: e8 bc ff ff ff call 8048d01 //再次调用func4(esi,edi)
8048d45: 83 c4 10 add $0x10,%esp
8048d48: 01 d8 add %ebx,%eax //eax+=ebx
8048d4a: eb d4 jmp 8048d20 //return
这里有个操作很值得一提:
8048d38: 8d 1c 38 lea (%eax,%edi,1),%ebx
这其实是一步很狡猾的操作:取 ( % e a x , % e d i , 1 ) (\%eax,\%edi,1) (%eax,%edi,1)的地址给 % e a x \%eax %eax,而这个地址其实就是 % e a x + % e d i ∗ 1 \%eax+\%edi*1 %eax+%edi∗1,所以相当于是 % e a x : = % e a x + % e d i ∗ 1 \%eax:=\%eax+\%edi*1 %eax:=%eax+%edi∗1,一个较为复杂的运算竟然就在这一步之内就完成了,服气!
剩下的汇编代码都比较容易懂,转化为c++代码为:
int func4(esi,edi) {
if (esi==0) return 0;
if (esi==1) return edi;
return func4(esi-1,edi)+func4(esi-2,edi)+edi;
}
即:
f u n c 4 ( a , b ) = { 0 , a = 0 b , a = 1 f u n c 4 ( a − 1 , b ) + f u n c 4 ( a − 2 , b ) + b , o t h e r \begin{aligned} func4(a,b)= \begin{cases} 0&,&a=0\\ b&,&a=1\\ func4(a-1,b)+func4(a-2,b)+b&,&other \end{cases} \end{aligned} func4(a,b)=⎩⎪⎨⎪⎧0bfunc4(a−1,b)+func4(a−2,b)+b,,,a=0a=1other
这里就可以解答为什么第一个数总是第二个数的20倍了:
首先观察到 f u n c 4 ( a , b ) = b ⋅ f u n c 4 ( a , 1 ) func4(a,b)=b\cdot func4(a,1) func4(a,b)=b⋅func4(a,1),然后令 f ( a ) = f u n c 4 ( a , 1 ) + 1 f(a)=func4(a,1)+1 f(a)=func4(a,1)+1有:
f ( a ) = { 1 , a = 0 2 , a = 1 f u n c 4 ( a − 1 , 1 ) + 1 + f u n c 4 ( a − 2 , 1 ) + 1 = f ( a − 1 ) + f ( a − 2 ) , o t h e r \begin{aligned} f(a)= \begin{cases} 1&,&a=0\\ 2&,&a=1\\ func4(a-1,1)+1+func4(a-2,1)+1=f(a-1)+f(a-2)&,&other \end{cases} \end{aligned} f(a)=⎩⎪⎨⎪⎧12func4(a−1,1)+1+func4(a−2,1)+1=f(a−1)+f(a−2),,,a=0a=1other
即 f ( a ) 为 F i b o n a c c i ( a + 2 ) f(a)为Fibonacci(a+2) f(a)为Fibonacci(a+2)!所以显然有 f u n c 4 ( a , b ) = b ⋅ f u n c 4 ( a , 1 ) = b ⋅ [ F i b o n a c c i ( a + 2 ) − 1 ] func4(a,b)=b\cdot func4(a,1)=b\cdot[ Fibonacci(a+2)-1] func4(a,b)=b⋅func4(a,1)=b⋅[Fibonacci(a+2)−1]。
恰好a取6时, F i b o n a c c i ( a + 2 ) − 1 = F i b o n a c c i ( 8 ) − 1 = 21 − 1 = 20 Fibonacci(a+2)-1=Fibonacci(8)-1=21-1=20 Fibonacci(a+2)−1=Fibonacci(8)−1=21−1=20.
好,这关也名正言顺地通过了。
突然第五关的长度就比前几关短了好多,看来难度也简单很多。
照样先看读入:
8048dbe: 8b 5d 08 mov 0x8(%ebp),%ebx
8048dc1: 53 push %ebx
8048dc2: e8 7c 02 00 00 call 8049043
8048dc7: 83 c4 10 add $0x10,%esp
8048dca: 83 f8 06 cmp $0x6,%eax //读入字符串长度和6比较
8048dcd: 75 2d jne 8048dfc //explode_bomb
可以看出,这一关首先要读入一个长度为六的字符串。
紧接着一个代码段:
8048dcf: 89 d8 mov %ebx,%eax
8048dd1: 83 c3 06 add $0x6,%ebx
8048dd4: b9 00 00 00 00 mov $0x0,%ecx //循环的初始化
8048dd9: 0f b6 10 movzbl (%eax),%edx
8048ddc: 83 e2 0f and $0xf,%edx
8048ddf: 03 0c 95 e0 a2 04 08 add 0x804a2e0(,%edx,4),%ecx
8048de6: 83 c0 01 add $0x1,%eax
8048de9: 39 d8 cmp %ebx,%eax
8048deb: 75 ec jne 8048dd9 //往前面跳转,实现循环
8048ded: 83 f9 35 cmp $0x35,%ecx
8048df0: 74 05 je 8048df7
8048df2: e8 a2 04 00 00 call 8049299
经过前一关的熟练后易知这是一个循环,代码为:
eax:=ebx;
ebx+=6;
for (ecx=0;eax!=ebx;++eax) {
edx:=(eax)&0xf; //等价于取低四位
ecx+=(0x804a2e0+edx*4);
}
if (ecx!=0x35) explode_bomb();
在 g d b gdb gdb中打印 0 x 804 a 2 e 0 0x804a2e0 0x804a2e0可知,这是一个长度为16的数组:
(gdb) x/16w 0x804a2e0
0x804a2e0 : 2 10 6 1
0x804a2f0 : 12 16 9 3
0x804a300 : 4 7 14 5
0x804a310 : 11 8 15 13
综上可知,其读入长度为6的字符串,并将每个字符的低四位作为下标,把对应的数加起来后要等于 0 x 35 ( 5 3 10 ) 0x35(53_{10}) 0x35(5310).
此处我选的是 { 10 , 10 , 10 , 10 , 10 , 3 } \{10,10,10,10,10,3\} {10,10,10,10,10,3},对应的下标为 { 1 , 1 , 1 , 1 , 1 , 7 } \{1,1,1,1,1,7\} {1,1,1,1,1,7},即读入字符串"111117".
这一关果然很简单。
第六关突然就又变得很长。。。
还是先看输入:
8048e16: 8d 45 c4 lea -0x3c(%ebp),%eax
8048e19: 50 push %eax
8048e1a: ff 75 08 pushl 0x8(%ebp)
8048e1d: e8 b7 04 00 00 call 80492d9
这关同样调用了 r e a d _ s i x _ n u m b e r s read\_six\_numbers read_six_numbers这个函数,有了第二关的经验可知,这关需要输入六个数,并且这六个数存放在 e b p ebp ebp- 0 x 3 c 0x3c 0x3c开始的地址中。
8048e25: be 00 00 00 00 mov $0x0,%esi
8048e2a: 8b 44 b5 c4 mov -0x3c(%ebp,%esi,4),%eax
8048e2e: 83 e8 01 sub $0x1,%eax
8048e31: 83 f8 05 cmp $0x5,%eax //无符号比较
8048e34: 77 0c ja 8048e42 //<1||>6 bomb
8048e36: 83 c6 01 add $0x1,%esi //第一次跳转的地方
8048e39: 83 fe 06 cmp $0x6,%esi
8048e3c: 74 24 je 8048e62 //jump out
8048e3e: 89 f3 mov %esi,%ebx
8048e40: eb 0f jmp 8048e51
8048e42: e8 52 04 00 00 call 8049299
8048e47: eb ed jmp 8048e36 //第一次往前面跳转
8048e49: 83 c3 01 add $0x1,%ebx //第二次跳转的地方
8048e4c: 83 fb 05 cmp $0x5,%ebx
8048e4f: 7f d9 jg 8048e2a
8048e51: 8b 44 9d c4 mov -0x3c(%ebp,%ebx,4),%eax
8048e55: 39 44 b5 c0 cmp %eax,-0x40(%ebp,%esi,4)
8048e59: 75 ee jne 8048e49 //第二次往前面跳转
8048e5b: e8 39 04 00 00 call 8049299
8048e60: eb e7 jmp 8048e49
紧接着的代码还比较长,但根据跳转的不同划分成以上若干小段后,应该很容易看出这其实是一个双层循环。
所以其伪代码为:
////假设ebp-0x3c为int a[]的首地址
esi=0;
while (1) {
eax:=a[esi]-1;
if ((unsigned)%eax>0x5) explode_bomb();
++esi;
if (esi==0x6) break;
for (ebx:=esi;ebx<=0x5;++ebx) {
//-0x40(%ebp,%esi,4)=-0x3c(%ebp,%esi-1,4)
if (a[ebx]!=a[esi-1]) continue;
explode_bomb();
}
}
不难看出,这一段代码其实是用来判断读入的六个数的格式的:
1.每个数的取值范围是1,2,3,4,5,6;
**2.**六个数互不相同.
即读入的六个数为1-6的一个排列,所以为什么不直接判断是否1-6都出现过呢…
紧接着的一个汇编代码段还是一个循环,这个十分容易看懂,其作用如下c++代码所示:
//注意到-0x3c(%ebp)是int a[]首地址,那么-0x24(%ebp)=-0x3c-0x4*6(%ebp),即a+6.
eax:=&a[0],ebx:=&a[6],ecx:=0x7;
for (;eax!=ebx;eax+=0x4)
*eax=ecx-*eax;
即令 a [ i ] = 7 − a [ i ] a[i]=7-a[i] a[i]=7−a[i].
接下来的代码可能会让人有些困惑?最关键的是这个地方:
8048ea2: ba 54 c1 04 08 mov $0x804c154,%edx
//此处有一个循环
8048e81: 8b 52 08 mov 0x8(%edx),%edx
//此处循环结束
将 % e d x + 8 \%edx+8 %edx+8指向的值赋值给 % e d x \%edx %edx? 给人一种类型不同强行赋值的感觉?还一直重复这种操作?
如果 % e d x \%edx %edx是一个数值的话,那么取 % e d x \%edx %edx地址这个操作就毫无意义?
如果 % e d x \%edx %edx是一个地址的话,那么 ( % e d x + 8 ) (\%edx+8) (%edx+8)也是一个地址?仿佛没什么毛病.
排除掉所有的不可能,那么真相就只有一个: % e d x , ( % e d x + 8 ) , ( ( % e d x + 8 ) + 8 ) . . . \%edx,(\%edx+8),((\%edx+8)+8)... %edx,(%edx+8),((%edx+8)+8)...的确都是地址。
很容易联想到这是一个链表! % e d x \%edx %edx是存放数据的地址,而 % e d x + 8 \%edx+8 %edx+8则是存放下一个节点的地址。
在 g d b gdb gdb中查看$ 0 x 804 c 154 0x804c154 0x804c154的值后更加验证了这种说法,并发现这是一个长度为6的链表:
都叫node了这还不是链表?说出来我自己都不信.
然后是c++代码:
//假设-0x24(%ebp)为node* p[]的首地址。
for (ebx:=0;ebx!=6;++ebx) {
ecx=a[ebx];//还记得我们读入了六个数吗?
edx=$node1;
if (cx>1)
for (eax:=1;eax!=ecx;++eax)
edx:=edx->next;//下一个节点
p[ebx]=edx;
}
这部分的意思相当于按顺序把节点 n o d e a [ i ] node_{a[i]} nodea[i]放在一个新数组 p [ ] p[] p[]里。
然后下面又跟着一段,phase_6怎么这么鬼长…:
8048eae: 8b 5d dc mov -0x24(%ebp),%ebx
8048eb1: 8b 45 e0 mov -0x20(%ebp),%eax
8048eb4: 89 43 08 mov %eax,0x8(%ebx)
8048eb7: 8b 55 e4 mov -0x1c(%ebp),%edx
8048eba: 89 50 08 mov %edx,0x8(%eax)
8048ebd: 8b 45 e8 mov -0x18(%ebp),%eax
8048ec0: 89 42 08 mov %eax,0x8(%edx)
8048ec3: 8b 55 ec mov -0x14(%ebp),%edx
8048ec6: 89 50 08 mov %edx,0x8(%eax)
8048ec9: 8b 45 f0 mov -0x10(%ebp),%eax
8048ecc: 89 42 08 mov %eax,0x8(%edx)
8048ecf: c7 40 08 00 00 00 00 movl $0x0,0x8(%eax)
这部分其实等价于更新链表每个节点的下一个节点是谁,操作完之后长这样:
还剩最后一段代码,终于要结束了,是一个比较中规中矩的循环,直接贴上其c++代码:
//根据上面可知ebx指向链表的第一个节点
for (esi:=0x5;!esi;--esi) {
eax:=ebx->next;
if (ebx->data>=eax->data) continue;
explode_bomb();
}
即检查链表中的数据是否按降序排列。
综上, p h a s e _ 6 phase\_6 phase_6先读入六个数字,然后这六个数字分别对应的链表节点 n o d e 7 − a [ i ] node_{7-a[i]} node7−a[i](记得是 7 − a [ i ] 7-a[i] 7−a[i])得是降序.
按照刚才 g d b gdb gdb中显示的数据排好降序为: { 0 x 2 f b , 0 x 2 e e , 0 x 2 a 9 , 0 x 270 , 0 x 135 , 0 x a f } \{0x2fb,0x2ee,0x2a9,0x270,0x135,0xaf\} {0x2fb,0x2ee,0x2a9,0x270,0x135,0xaf}
对应的节点编号为: { n o d e 5 , n o d e 1 , n o d e 4 , n o d e 3 , n o d e 2 , n o d e 6 } \{node_5,node_1,node_4,node_3,node_2,node_6\} {node5,node1,node4,node3,node2,node6}
所以应该读入: { 2 , 6 , 3 , 4 , 5 , 1 } \{2,6,3,4,5,1\} {2,6,3,4,5,1}
至此第六关安全通过.
4.7.0 触发隐藏关:
据说炸弹还有一个隐藏关?
在汇编代码里搜索 p h a s e phase phase关键字,发现还真有一个 s e c r e t _ p h a s e secret\_phase secret_phase,继续看下去,发现 s e c r e t _ p h a s e secret\_phase secret_phase是在 p h a s e _ d e f u s e d phase\_defused phase_defused被调用的,那就研究一下 p h a s e _ d e f u s e d phase\_defused phase_defused吧~
首先上来的就是这段:
804943f: 83 3d ec c7 04 08 06 cmpl $0x6,0x804c7ec
8049446: 74 12 je 804945a
8049458: c9 leave
8049459: c3 ret
804945a: //以下省略
这里突然出现了一个变量 0 x 804 c 7 e c 0x804c7ec 0x804c7ec,它要等于6才能开始触发隐藏关,继续在代码中查找 0 x 804 c 7 e c 0x804c7ec 0x804c7ec,发现它在 r e a d _ l i n e read\_line read_line里面也出现过:
08049313 :
8049325: 8b 15 ec c7 04 08 mov 0x804c7ec,%edx
//....
8049365: 83 c2 01 add $0x1,%edx
8049368: 89 15 ec c7 04 08 mov %edx,0x804c7ec
看到这里恍然大悟,每读一行 0 x 804 c 7 e c 0x804c7ec 0x804c7ec就加一,应该是记录目前是第几关的变量,那它等于6才能开始触发隐藏关也很容易理解——毕竟要前六关都过了才能解隐藏关~~,前六关都过不了怎么过隐藏关?~~
继续看 p h a s e _ d e f u s e d phase\_defused phase_defused:
804945d: 8d 45 a4 lea -0x5c(%ebp),%eax
8049460: 50 push %eax
8049461: 8d 45 a0 lea -0x60(%ebp),%eax
8049464: 50 push %eax
8049465: 8d 45 9c lea -0x64(%ebp),%eax
8049468: 50 push %eax
8049469: 68 4b a5 04 08 push $0x804a54b
804946e: 68 f0 c8 04 08 push $0x804c8f0
8049473: e8 98 f3 ff ff call 8048810 <__isoc99_sscanf@plt>
804947b: 83 f8 03 cmp $0x3,%eax
804947e: 74 1e je 804949e
//此处会调用函数返回
804949e: //..下面会开始触发隐藏关
做了前六关,这一看就知道是$sscanf($0x804c8f0, 0 x 804 a 54 b , % e b p − 0 x 64 , % e b p − 0 x 60 , % e b p − 0 x 5 c ) 0x804a54b,\%ebp-0x64,\%ebp-0x60,\%ebp-0x5c) 0x804a54b,%ebp−0x64,%ebp−0x60,%ebp−0x5c).
在 g d b gdb gdb中分别查看$$0x804c8f0, 0 x 804 a 54 b 0x804a54b 0x804a54b得:
(gdb) x/s 0x804c8f0
0x804c8f0 : "40 2"
(gdb) x/s 0x804a54b
0x804a54b: "%d %d %s"
到了这里气氛突然变得十分尴尬,明明是要读入两个数字和一个字符串,但是输入里面却只有两个数字???
在代码中查找$ 0 x 804 c 8 f 0 0x804c8f0 0x804c8f0这个关键字,但也只在 p h a s e _ d e f u s e d phase\_defused phase_defused里面出现过一次,那我应该修改不了???
事情发展到这里仿佛陷入了停滞,而实际上我在这里也的确卡了一天左右~~,要知道前六关都是一气呵成半天搞定的23333.~~
我不甘心,毕竟都到了隐藏关,到这里放弃岂不是血亏?
抱着不服气的心态,我抱着边看边试的心态继续看下去(请先记住这里读入卡住了)。
根据上面的 s s c a n f sscanf sscanf可知, % e b p − 0 x 64 , % e b p − 0 x 60 , % e b p − 0 x 5 c \%ebp-0x64,\%ebp-0x60,\%ebp-0x5c %ebp−0x64,%ebp−0x60,%ebp−0x5c分别存放了两个数字和一个字符串。
80494a1: 68 54 a5 04 08 push $0x804a554
80494a6: 8d 45 a4 lea -0x5c(%ebp),%eax
80494a9: 50 push %eax
80494aa: e8 b6 fb ff ff call 8049065
80494af: 83 c4 10 add $0x10,%esp
80494b2: 85 c0 test %eax,%eax //和jne组合操作相当于判断%eax是否非零
80494b4: 75 ca jne 8049480 //return
//这里中间输出了一堆骚话...
80494cf: e8 8d fa ff ff call 8048f61
这里调用了第一关用过的 s t r i n g s _ n o t _ e q u a l strings\_not\_equal strings_not_equal,参数是 − 0 x 5 c ( % e b p ) ( 输 入 的 字 符 串 ) -0x5c(\%ebp)(输入的字符串) −0x5c(%ebp)(输入的字符串)和$ 0 x 804 a 554 0x804a554 0x804a554.
在 g d b gdb gdb中查看$ 0 x 804 a 554 0x804a554 0x804a554的内容可知:
(gdb) x/s 0x804a554
0x804a554: "SecretSYSU"
这里应该就是触发隐藏关的条件了。
道理我都懂,但这个字符串要怎么读入啊…,到底是哪里突然冒出来0x804c8f0?
到此隐藏关触发条件已经搞清楚了:在$ 0 x 804 c 8 f 0 0x804c8f0 0x804c8f0(虽然并不知道是在哪里冒出来的一个东西)读入两个数字和一个字符串,然后这个字符串得是 ‘ S e c r e t S Y S U ’ ‘SecretSYSU’ ‘SecretSYSU’才能触发,否则就无事发生。
哎慢着,是不是发现了一点蹊跷——读入的两个数字没用的吗?仔细看过一遍后,发现还真的没用过!
为什么要额外读入两个无用的数字呢?那为什么$ 0 x 804 c 8 f 0 0x804c8f0 0x804c8f0里只存有这两个没用过的数字,后面的字符串呢? ′ 40 2 ′ '40\ 2' ′40 2′有什么特别的含义吗?但有用的不应该是后面理论上要存在的字符串吗?这几个问题我曾一直百思不得其解.
终于有一天,我突然意识到 ′ 40 2 ′ '40\ 2' ′40 2′好像就是我解决第四关而读入的两个数!
我迫不及待地修改了第四关的答案(还记得第四关有三个答案分别是 ( 40 , 2 ) 、 ( 60 , 3 ) 、 ( 80 , 4 ) (40,2)、(60,3)、(80,4) (40,2)、(60,3)、(80,4)?),再次在 g d b gdb gdb中查看$ 0 x 804 c 8 f 0 0x804c8f0 0x804c8f0:
(gdb) x/s 0x804c8f0
0x804c8f0 : "60 3"
它 变 了 ! ! !
看来还真是第四关的答案,但这个程序什么时候存过我读入的数啊。。。
发现 0 x 804 c 8 f 0 0x804c8f0 0x804c8f0对应 i n p u t _ s t r i n g s + 240 input\_strings+240 input_strings+240这个地址,反推计算一下, i n p u t _ s t r i n g s input\_strings input_strings开始的地址应该是 0 x 804 c 8 f 0 − 240 = 0 x 804 c 800 0x804c8f0-240=0x804c800 0x804c8f0−240=0x804c800,在代码中查找 0 x 804 c 800 0x804c800 0x804c800这个关键字,还真给我找到了:
08049313 :
8049331: 81 c3 00 c8 04 08 add $0x804c800,%ebx
08049171 :
804918e: 05 00 c8 04 08 add $0x804c800,%eax
080491b9 :
80491dd: 81 c2 00 c8 04 08 add $0x804c800,%edx
除开 s e n d _ m s g send\_msg send_msg,即有两个函数 r e a d _ l i n e read\_line read_line和 s k i p skip skip两个函数涉及到这个 i n p u t _ s t r i n g s input\_strings input_strings,而实际上 r e a d _ l i n e read\_line read_line正是通过调用 s k i p skip skip来实现读取一行的(其实 r e a d _ l i n e read\_line read_line大有文章,由于通关不涉及这个函数,具体解读就不谈了),所以下面着重分析 s k i p skip skip:
08049171 :
804917b: ff 35 f0 c7 04 08 pushl 0x804c7f0 //这个下面解释
8049181: 6a 50 push $0x50
//---------------------------------------------------------------
8049183: a1 ec c7 04 08 mov 0x804c7ec,%eax //0x804c7ec是记录通到第几关的
8049188: 8d 04 80 lea (%eax,%eax,4),%eax
804918b: c1 e0 04 shl $0x4,%eax
804918e: 05 00 c8 04 08 add $0x804c800,%eax
//---------------------------------------------------------------
8049193: 50 push %eax
8049194: e8 b7 f5 ff ff call 8048750
除了分割线之间的代码外, s k i p skip skip本质上只是调用了 f g e t s ( % e a x , 0 x 50 , 0 x 804 c 7 f 0 ) fgets(\%eax,0x50,0x804c7f0) fgets(%eax,0x50,0x804c7f0).
在 g d b gdb gdb中查看 0 x 804 c 7 f 0 0x804c7f0 0x804c7f0:
(gdb) x/xw 0x804c7f0
0x804c7f0 : 0xb7fc65c0
而下发的 m a i n main main中也的确有 i n f i l e infile infile的定义(唯一一个在 m a i n main main函数定义的变量):
FILE *infile;
int main(int argc,char *argv[]) {
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);
}
}
}
由上可知, i n f i l e infile infile实际上是读入流,而 f g e t s fgets fgets的作用正是把读入流中的字符读到 % e a x \%eax %eax中,那 % e a x \%eax %eax是啥?
8049183: a1 ec c7 04 08 mov 0x804c7ec,%eax //0x804c7ec是记录通到第几关的
8049188: 8d 04 80 lea (%eax,%eax,4),%eax
804918b: c1 e0 04 shl $0x4,%eax
804918e: 05 00 c8 04 08 add $0x804c800,%eax
仔细品味一下,这其实是 % e a x : = 5 ⋅ ( 0 x 804 c 7 e c ) ⋅ 16 + 0 x 804 c 800 = 80 ⋅ ( 0 x 804 c 7 e c ) + 0 x 804 c 800 \%eax:=5\cdot(0x804c7ec)\cdot16+0x804c800=80\cdot(0x804c7ec)+0x804c800 %eax:=5⋅(0x804c7ec)⋅16+0x804c800=80⋅(0x804c7ec)+0x804c800;
而之前得出过结论, 0 x 804 c 7 e c 0x804c7ec 0x804c7ec是记录目前第几关的变量,而 0 x 804 c 800 0x804c800 0x804c800则是$input_strings $的首地址!
到这里就很清楚了,每一关读入的字符串,都被存放在 i n p u t _ s t r i n g s input\_strings input_strings里面,准确来说:
第 i i i关的字符串被存放在以 i n p u t _ s t r i n g s + 80 ⋅ i input\_strings+80\cdot i input_strings+80⋅i为首地址的内存里(每关被限制存最多 0 x 50 ( 8 0 10 ) 0x50(80_{10}) 0x50(8010)个字符).
至此,终于可以返回去解释怎么触发隐藏关了:
关键是这个语句: s s c a n f ( 0 x 804 c 8 f 0 , 0 x 804 a 54 b , % e b p − 0 x 64 , % e b p − 0 x 60 , % e b p − 0 x 5 c ) sscanf(0x804c8f0,0x804a54b,\%ebp-0x64,\%ebp-0x60,\%ebp-0x5c) sscanf(0x804c8f0,0x804a54b,%ebp−0x64,%ebp−0x60,%ebp−0x5c),
因为 0 x 804 c 8 f 0 = 0 x 804 c 800 + 240 0x804c8f0=0x804c800+240 0x804c8f0=0x804c800+240,而 240 = 80 ∗ 4 240=80*4 240=80∗4,所以$ 0 x 804 c 8 f 0 0x804c8f0 0x804c8f0其实就是第四关读取的字符串,
然后在这个字符串中以 ′ % d % d % s ′ '\%d\ \%d\ \%s' ′%d %d %s′的格式读入两个数字和一个字符串,因为前面两个数字是解开第四关的答案,所以在触发隐藏关时,只有后面的字符串是真正有用的,那么不难得出结论,只要在第四关的答案后面加上 ‘ S e c r e t S Y S U ’ ‘SecretSYSU’ ‘SecretSYSU’,便能触发隐藏关。
添加完 ‘ S e c r e t S Y S U ’ ‘SecretSYSU’ ‘SecretSYSU’后,再查看一下$ 0 x 804 c 8 f 0 0x804c8f0 0x804c8f0的内容:
(gdb) x/s 0x804c8f0
0x804c8f0 : "60 3 SecretSYSU"
的确万无一失,那当然也是进入了隐藏关啦~
这里附上触发隐藏关后的一堆骚话23333:
Curses, you've found the secret phase!
But finding it ans solving it are quite different...
后记:
后来我问了一下别人是怎么知道怎么触发隐藏关的,他指着一份 p d f pdf pdf说:
。。。行叭。。。算你狠。。。
强烈建议调整隐藏关可以放在任意关卡后面激活!
4.7.1 解决隐藏关:
好,~~经过别人早就知道的触发条件后,~~现在终于进入了隐藏关。
相比于第六关长不忍睹(没敲错,就是长)的代码,隐藏关显得格外简短:
8048f68: e8 a6 03 00 00 call 8049313
8048f70: 6a 0a push $0xa
8048f72: 6a 00 push $0x0
8048f74: 50 push %eax
8048f75: e8 06 f9 ff ff call 8048880 //string to long
r e a d _ l i n e read\_line read_line得到一个字符串并存放在 % e a x \%eax %eax指向的地址中,然后调用 s t r t o l strtol strtol,顾名思义, s t r t o l strtol strtol是把 s t r i n g string string转换成 l o n g long long的函数,所以本质上隐藏关读入了一个数字。
8048f7a: 89 c3 mov %eax,%ebx //备份
8048f7c: 8d 40 ff lea -0x1(%eax),%eax//骚操作,%eax=%eax-0x1
8048f82: 3d e8 03 00 00 cmp $0x3e8,%eax
8048f87: 77 35 ja 8048fbe //无符号比较
紧接着就是判断 % e a x \%eax %eax的范围, 0 < % e a x − 0 x 1 ≤ 0 x 3 e 8 ⇒ 1 ≤ % e a x ≤ 0 x 3 e 9 0<\%eax-0x1\leq0x3e8\ \ \ \Rightarrow\ \ 1\leq \%eax \leq0x3e9 0<%eax−0x1≤0x3e8 ⇒ 1≤%eax≤0x3e9,即 1 , 2 , 3...1001 1,2,3...1001 1,2,3...1001.
还剩最后一段汇编代码:
8048f8c: 53 push %ebx
8048f8d: 68 a0 c0 04 08 push $0x804c0a0
8048f92: e8 76 ff ff ff call 8048f0d
8048f9a: 83 f8 02 cmp $0x2,%eax
8048f9d: 74 05 je 8048fa4
8048f9f: e8 f5 02 00 00 call 8049299 //(eax)!=0x2则bomb
8048fa4: //输出一堆骚话后就安全退出了
看样子就是调用了 f u n 7 ( 0 x 804 c 0 a 0 , % e b x ) fun7(0x804c0a0,\%ebx) fun7(0x804c0a0,%ebx),然后返回 0 x 2 0x2 0x2就万事大吉了~
在 g d b gdb gdb中查看$ 0 x 804 c 0 a 0 0x804c0a0 0x804c0a0的内容:
(gdb) x/xw 0x804c0a0
0x804c0a0 : 0x00000024
即$ 0 x 804 c 0 a 0 0x804c0a0 0x804c0a0是变量 n 1 n1 n1的地址,不过这个名字起得还真令人发怵,莫非还有 n 2 , n 3 , n 4... n2,n3,n4... n2,n3,n4...?
先看 f u n 7 fun7 fun7,看上去格式和 f u n c 4 func4 func4有点类似,所以这里直接贴上其伪代码:
fun7(edx,ecx) {
//mov 0x8(%ebp),%edx
//mov 0xc(%ebp),%ecx
//又因为0x8(%ebp)是fun7第一个参数,0xc(%ebp)是fun7第二个参数,
//所以等价于传参是edx,ecx
if (edx==0)
return -1;//%eax:=$0xffffffff之后ret,等价于return $0xffffffff
ebx=(edx);
if (ebx>ecx)
return fun7(0x4(edx),ecx)*2;//原理同func4
if (ecx!=ebx)
return fun7(0x8(edx),ecx)*2+1;//原理同func4
return 0;
}
可以看到, f u n 7 fun7 fun7又是一个递归函数,而且还出现了类似于第六关那种 e d x : = 0 x 4 ( e d x ) / 0 x 8 ( e d x ) edx:=0x4(edx)/0x8(edx) edx:=0x4(edx)/0x8(edx)的操作.
有了第六关的经验, e d x edx edx是一个地址的事实就很容易理解了,最开始传参的时候 e d x : = 0 x 804 c 0 a 0 edx:=0x804c0a0 edx:=0x804c0a0( n 1 n1 n1的地址)更加证明了这一点。
那么在 g d b gdb gdb中查看所有的 0 x 4 ( e d x ) / 0 x 8 ( e d x ) 0x4(edx)/0x8(edx) 0x4(edx)/0x8(edx)得知:
(gdb) x/3xw 0x804c0a0
0x804c0a0 : 0x00000024 0x0804c0ac 0x0804c0b8
(gdb) x/3xw 0x804c0ac
0x804c0ac : 0x00000008 0x0804c0dc 0x0804c0c4
(gdb) x/3xw 0x804c0b8
0x804c0b8 : 0x00000032 0x0804c0d0 0x0804c0e8
(gdb) x/3xw 0x804c0dc
0x804c0dc : 0x00000006 0x0804c100 0x0804c124
(gdb) x/3xw 0x804c0c4
0x804c0c4 : 0x00000016 0x0804c130 0x0804c118
(gdb) x/3xw 0x804c0d0
0x804c0d0 : 0x0000002d 0x0804c0f4 0x0804c13c
(gdb) x/3xw 0x804c0e8
0x804c0e8 : 0x0000006b 0x0804c10c 0x0804c148
(gdb) x/3xw 0x804c100
0x804c100 : 0x00000001 0x00000000 0x00000000
(gdb) x/3xw 0x804c124
0x804c124 : 0x00000007 0x00000000 0x00000000
(gdb) x/3xw 0x804c130
0x804c130 : 0x00000014 0x00000000 0x00000000
(gdb) x/3xw 0x804c118
0x804c118 : 0x00000023 0x00000000 0x00000000
(gdb) x/3xw 0x804c0f4
0x804c0f4 : 0x00000028 0x00000000 0x00000000
(gdb) x/3xw 0x804c13c
0x804c13c : 0x0000002f 0x00000000 0x00000000
(gdb) x/3xw 0x804c10c
0x804c10c : 0x00000063 0x00000000 0x00000000
(gdb) x/3xw 0x804c148
0x804c148 : 0x000003e9 0x00000000 0x00000000
还真的有n2 n3 n4…还整整15个…是有点多
而这刚好是一颗二叉查找树,即每个节点的数据是有序的!
那么再回看一下 f u n 7 fun7 fun7的代码,易知这刚好就是边找边定位的过程,准确来说:
p o s i t i o n ( n o d e x ) = { 0 , n o d e x = r o o t p o s i t i o n ( f a ( n o d e x ) ) , n o d e x 是 f a ( n o d e x ) 的 左 儿 子 p o s i t i o n ( f a ( n o d e x ) ) ∣ 2 d e p ( n o d e x ) , n o d e x 是 f a ( n o d e x ) 的 右 儿 子 ∣ 为 按 位 或 \begin{aligned} position(node_x)= \begin{cases} 0&,&node_x=root\\ position(fa(node_x))&,&node_x是fa(node_x)的左儿子\\ position(fa(node_x))|2^{dep(node_x)}&,&node_x是fa(node_x)的右儿子\\ \end{cases} \end{aligned} \\ \ \ \\ \ \ \\ |为按位或 position(nodex)=⎩⎪⎨⎪⎧0position(fa(nodex))position(fa(nodex))∣2dep(nodex),,,nodex=rootnodex是fa(nodex)的左儿子nodex是fa(nodex)的右儿子 ∣为按位或
对应的值为:
而最后我们要得到 p o s i t i o n ( n o d e x ) = 2 position(node_x)=2 position(nodex)=2,所以输入对应节点的数据可以为 20 ( 0 x 14 ) , 22 ( 0 x 16 ) . 20(0x14),22(0x16). 20(0x14),22(0x16).
那么至此所有的关卡都已经解决了,完结撒花:
Wow! You've defused the secret phase!
Congratulation! You've defused the bomb!
Your instructor has been notified ans will verify your solution.
strings_not_equal:
好歹也是要看一下的,万一名字是用来唬人的呢?
int string_length(const char *s) {
if (!*s) return 0;
int i=0;
while (1) {
++i;
if (!s[i])
return i;
}
}
int strings_not_equal(const char *s1,const char *s2) {//这是直译的,代码丑也不关我的事-.-
int len1=string_length(s1),len2=string_length(s2);
if (len1!=len2)
return 1;
if (!*s1) return 0;
if (*s1!=*s2) return 1;
while(1) {
++s1,++s2;
if (!*s1) return 0;
if (*s1==*s2) continue;
return 1;
}
puts("out");
}
不过不得不说,这个判断字符串相不相等也写得太丑了吧…
第三关共有6种答案,分别是:
1 ) 0 c 195 1)\ 0\ c\ 195 1) 0 c 195
2 ) 2 a 600 2)\ 2\ a\ 600 2) 2 a 600
3 ) 4 k 360 3)\ 4\ k\ 360 3) 4 k 360
4 ) 5 p 445 4)\ 5\ p\ 445 4) 5 p 445
5 ) 6 s 1087 5)\ 6\ s\ 1087 5) 6 s 1087
6 ) 7 m 219 6)\ 7\ m\ 219 6) 7 m 219
%edx+4:
我们已经知道了 % e d x \%edx %edx为链表节点的数据,而 ( % e d x + 8 ) (\%edx+8) (%edx+8)的值为下一个节点的地址,那 % e d x + 4 \%edx+4 %edx+4是啥?
根据我的观察, n o d e n node_n noden的这个值刚好是 n n n,大胆猜测这就是为了存取原来节点编号而设置的变量。
所以其节点的声明可能为:
struct node {
int data,id;
node *next;
}
read_line:
r e a d _ l i n e read\_line read_line的确是一个很值得谈的函数,其中出现了一堆神奇的操作,比如:
8049343: f2 ae repnz scas %es:(%edi),%al
804941d: f3 a5 rep movsl %ds:(%esi),%es:(%edi)
这个大概是属于 X 86 X86 X86自带的循环语句,谁说X86没有循环语句的,解读如下:
repnz : repeat if not equal,即只要不相等就一直循环
scas : scan string,即扫描字符串
%es:(%edi) : 当前扫描的位置,每次扫描后,%edi自减一
%al : low of %eax,即%eax的低四位
所以等价于下面的c++代码:
while (%es:(%edi)!=%al) --%al;
在这里等价于,找到字符串结束符**’\0’**的位置.
没有六,原本六是实验心得。不用想了,实验心得是不可能放出来的
完整的汇编代码
完整的c++代码(高仿版炸弹):连 r e a d _ l i n e read\_line read_line等函数都实现了(除了异常检测那些部分),可以运行的哦~
第一关汇编,c++代码
第二关汇编,c++代码
第三关汇编,c++代码
第四关汇编,c++代码
第五关汇编,c++代码
第六关汇编,c++代码
隐藏关汇编,c++代码
其他相关函数的c++代码