CS:APP 二进制炸弹拆解详解

如果下面官话太多,那只是因为这本来是我的一份实验报告搬过来的。。。

一、实验名称

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)。

四、实验过程与结果

4.0 预备阶段:

​ 使用反汇编工具 o b j d u m p objdump objdump b o m b bomb bomb的汇编代码搞出来,方便日后破解。

sysu@debian:~/bomb41$ objdump -d ./bomb > disbomb.txt
4.1 第一关:
 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?

至此第一关通过,拆炸弹真简单

4.2 第二关:

​ 第二关的汇编代码开始变得复杂起来,但仔细阅读后发现其大致可以划分为几小段,下面就一段一段的分析:

 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:=ebp0x24,然后和 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:=ebp0x24,所以这关读入的六个数字存放在以 e b p − 0 x 24 ebp-0x24 ebp0x24开头的地址中。

读完六个数字后,紧接着的汇编代码为:

 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 

简化后的流程图为:
CS:APP 二进制炸弹拆解详解_第1张图片
进一步转化为c++语言:

//假设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[i1]+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}即可.

4.3 第三关:

第三关开始就变得很长了.

先看输入:

 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),ebp0x14,ebp0x15,ebp0x10).

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)) ((eax4+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),管他多少个,能过就行。

4.4 第四关:

​ 老规矩,先看读入:

 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,ebp0x14,ebp0x10),而在 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+%edi1,所以相当于是 % e a x : = % e a x + % e d i ∗ 1 \%eax:=\%eax+\%edi*1 %eax:=%eax+%edi1,一个较为复杂的运算竟然就在这一步之内就完成了,服气!

剩下的汇编代码都比较容易懂,转化为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(a1,b)+func4(a2,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)=bfunc4(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(a1,1)+1+func4(a2,1)+1=f(a1)+f(a2),,,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)=bfunc4(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=211=20.

好,这关也名正言顺地通过了。

4.5 第五关:

​ 突然第五关的长度就比前几关短了好多,看来难度也简单很多。

​ 照样先看读入:

 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 

其流程图为:
CS:APP 二进制炸弹拆解详解_第2张图片

经过前一关的熟练后易知这是一个循环,代码为:

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".

这一关果然很简单。

4.6 第六关:

​ 第六关突然就又变得很长。。。

​ 还是先看输入:

 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 

​ 紧接着的代码还比较长,但根据跳转的不同划分成以上若干小段后,应该很容易看出这其实是一个双层循环。

​ 严谨一点,其流程图为:CS:APP 二进制炸弹拆解详解_第3张图片

若还是感觉很混乱的话,下图就一目了然了:CS:APP 二进制炸弹拆解详解_第4张图片

​ 所以其伪代码为:

////假设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]=7a[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的链表:
CS:APP 二进制炸弹拆解详解_第5张图片

都叫node了这还不是链表?说出来我自己都不信.

那么接下来的代码应该就没有什么难点了,先上流程图:
CS:APP 二进制炸弹拆解详解_第6张图片

然后是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]} node7a[i](记得是 7 − a [ i ] 7-a[i] 7a[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 隐藏关:

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,%ebp0x64,%ebp0x60,%ebp0x5c).

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 %ebp0x64,%ebp0x60,%ebp0x5c分别存放了两个数字和一个字符串。

 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"

它 变 了 ! ! !

看来还真是第四关的答案,但这个程序什么时候存过我读入的数啊。。。

仔细看 g d b gdb gdb中的内容:在这里插入图片描述

发现 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 0x804c8f0240=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+80i为首地址的内存里(每关被限制存最多 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,%ebp0x64,%ebp0x60,%ebp0x5c)

​ 因为 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=804,所以$ 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说:CS:APP 二进制炸弹拆解详解_第7张图片

。。。行叭。。。算你狠。。。

强烈建议调整隐藏关可以放在任意关卡后面激活!

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<%eax0x10x3e8     1%eax0x3e9,即 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个…是有点多

根据其指向的关系,可以画出一个二叉树:CS:APP 二进制炸弹拆解详解_第8张图片

而这刚好是一颗二叉查找树,即每个节点的数据是有序的!

那么再回看一下 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=rootnodexfa(nodex)nodexfa(nodex)    
对应的值为:CS:APP 二进制炸弹拆解详解_第9张图片

而最后我们要得到 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++代码

你可能感兴趣的:(CS:APP 二进制炸弹拆解详解)