计算机系统实验:BombLab

一、实验题目:Bomb Lab

二、实验目的:

  1. 了解并熟悉底层汇编语言的特点,能够看懂汇编语言
  2. 能够通过汇编语言,联系起高级语言,推测出每一行汇编代码的意义
  3. 用汇编相关知识拆除六颗炸弹以及发现隐藏炸弹并进行拆除

三、实验环境:Ubuntu12.04环境,gdb-peda

四、实验内容及操作步骤:

  1. 题目审阅

打开文件夹,出现如下文件:

[()(https://gitee.com/dominique-yiu/csdn/raw/master/image-20210509132755227.png)]

其中bomb是可调式文件,bomb.c文件是“源文件”,README是一个文档。bomb无法直接打开以及README没有内容,因此首先查看bomb.c文件中是否有有用信息。C程序内容如下:

#include <stdio.h>
#include <stdlib.h>
#include "support.h"
#include "phases.h"

/* 
 * Note to self: Remember to erase this file so my victims will have no
 * idea what is going on, and so they will all blow up in a
 * spectaculary fiendish explosion. -- Dr. Evil 
 */

FILE *infile;

int main(int argc, char *argv[])
{
    char *input;

    /* Note to self: remember to port this bomb to Windows and put a 
     * fantastic GUI on it. */

    /* When run with no arguments, the bomb reads its input lines 
     * from standard input. */
    if (argc == 1) {  
	infile = stdin;
    } 

    /* When run with one argument <file>, the bomb reads from <file> 
     * until EOF, and then switches to standard input. Thus, as you 
     * defuse each phase, you can add its defusing string to <file> and
     * avoid having to retype it. */
    else if (argc == 2) {
	if (!(infile = fopen(argv[1], "r"))) {
	    printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]);
	    exit(8);
	}
    }

    /* You can't call the bomb with more than 1 command line argument. */
    else {
	printf("Usage: %s [<input_file>]\n", argv[0]);
	exit(8);
    }

    /* Do all sorts of secret stuff that makes the bomb harder to defuse. */
    initialize_bomb();

    printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
    printf("which to blow yourself up. Have a nice day!\n");

    /* Hmm...  Six phases must be more secure than one phase! */
    input = read_line();             /* Get input                   */
    phase_1(input);                  /* Run the phase               */
    phase_defused();                 /* Drat!  They figured it out!
				      * Let me know how they did it. */
    printf("Phase 1 defused. How about the next one?\n");

    /* The second phase is harder.  No one will ever figure out
     * how to defuse this... */
    input = read_line();
    phase_2(input);
    phase_defused();
    printf("That's number 2.  Keep going!\n");

    /* I guess this is too easy so far.  Some more complex code will
     * confuse people. */
    input = read_line();
    phase_3(input);
    phase_defused();
    printf("Halfway there!\n");

    /* Oh yeah?  Well, how good is your math?  Try on this saucy problem! */
    input = read_line();
    phase_4(input);
    phase_defused();
    printf("So you got that one.  Try this one.\n");
    
    /* Round and 'round in memory we go, where we stop, the bomb blows! */
    input = read_line();
    phase_5(input);
    phase_defused();
    printf("Good work!  On to the next...\n");

    /* This phase will never be used, since no one will get past the
     * earlier ones.  But just in case, make this one extra hard. */
    input = read_line();
    phase_6(input);
    phase_defused();

    /* Wow, they got it!  But isn't something... missing?  Perhaps
     * something they overlooked?  Mua ha ha ha ha! */
    
    return 0;
}

通过观察C程序可以清晰的看出该C程序的结构,每个炸弹小关的结构都是四行:read_line进行输入、phase_i进入第i个炸弹关卡、phase_defused函数以及通关后的祝贺字符串。因此,这说明在每一关卡都得输入一些数值,而你的输入会决定炸弹是否会爆炸。

但遗憾的是这个C程序是不完整的,它并没有每个调用函数的源程序,因此为了探究这个实验必须研究它的汇编代码。

  1. 环境准备

由于接下来的汇编代码会非常多,而为了有好的研究体验,可以首先配置好一个插件。在本实验中,我们使用了gdb-peda插件,gdb-peda具有更加友好的用户页面,使得调试更加有效,并且gdb-peda能够实时跟踪查看寄存器、反汇编语句以及栈帧之中的部分内容,并且在进行函数跳转时,提供了可能进行传递的参数,使得gdb调试更加可视化。

  • 首先安装git:
sudo apt-get install git
  • 安装gdb-peda插件:
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
  1. 实验任务

3.1. phase_1

反汇编,phase_1汇编代码如下:

下面对其进行解释与说明(我的输入:Brownie, you are doing a heck of a job.):

Dump of assembler code for function phase_1:
   0x08048b50 <+0>:	sub    esp,0x1c
       //	新栈帧的初始化
   0x08048b53 <+3>:	mov    DWORD PTR [esp+0x4],0x804a184
       //	函数调用前参数的准备,这是第一个参数的地址
   0x08048b5b <+11>:	mov    eax,DWORD PTR [esp+0x20]
       //	第二个参数首先放在eax
   0x08048b5f <+15>:	mov    DWORD PTR [esp],eax
       //	把存放在eax的参数给到栈顶
   0x08048b62 <+18>:	call   0x8048fa4 <strings_not_equal>
       //	调用函数strings_not_equal,由字面意思可知是判断两个字符串是否一致的函数
   0x08048b67 <+23>:	test   eax,eax
       //	判断eax的符号,以及给一些标志位赋值
   0x08048b69 <+25>:	je     0x8048b70 <phase_1+32>
       //	判断标志位ZF,即eax是否为0
   0x08048b6b <+27>:	call   0x80490b6 <explode_bomb>
       //	如果不为0,那么就调用explode_bomb函数引爆炸药
   0x08048b70 <+32>:	add    esp,0x1c
       //	exp恢复
   0x08048b73 <+35>:	ret    
       //	返回到call的下一条语句
End of assembler dump.

为了验证自己对strings_not_equal函数功能的猜测,将该函数汇编中重要关键的部分拿出来进行研究:

Dump of assembler code for function strings_not_equal:
   0x08048fbb <+23>:	mov    DWORD PTR [esp],ebx
   0x08048fbe <+26>:	call   0x8048f8b <string_length>
   0x08048fc3 <+31>:	mov    edi,eax
   0x08048fc5 <+33>:	mov    DWORD PTR [esp],esi
   0x08048fc8 <+36>:	call   0x8048f8b <string_length>
   0x08048fcd <+41>:	mov    edx,0x1
   0x08048fd2 <+46>:	cmp    edi,eax
   0x08048fd4 <+48>:	jne    0x8049009 <strings_not_equal+101>
      //	以上是先比较两个字符串的长度,如果不同的话直接输出1
   0x08048fd6 <+50>:	movzx  eax,BYTE PTR [ebx]
   0x08048fd9 <+53>:	mov    dl,0x0
   0x08048fdb <+55>:	test   al,al
   0x08048fdd <+57>:	je     0x8049009 <strings_not_equal+101>
       //	判断是否为空字符串
   ......
   ......
   ......
       //	接下来就是对字符串的每一个字符进行对比
End of assembler dump.

通过上面的汇编分析,可以验证我们猜想是正确的,如果两个字符串不相同返回值为1,反之为0。于是,我们就要重点关注在调用strings_not_equal函数前进行传递的参数它们到底是什么?于是我们查看得到如下结果(由于我已知答案,就直接输入正确答案了):

在这里插入图片描述

通过gdb-peda的帮助,我们一下子就知道了,两个参数的地址和内容是什么。为了验证自己推断的是否和peda提供的一致,因此通过以上的汇编代码,我们直到第一个参数的地址是0x8040a184,另外一个的地址放在%esp当中。因此首先x/s 0x8040a184查看得到一个参数,这个显然是本来提供的参数,那么另外一个就是自己输入的参数了。在汇编代码中可以看出这个地址被传递到栈顶了,因此首先查看%esp得到输入参数的地址,然后用相同的方式查看得到<input_strings>的字样,那么这就是我们的输入了。

在这里插入图片描述

于是我们可以合理的推测,我们只需要输入和地址0x8040a184下内容相同的字符串就可以通过第一关,因此有以下的场景:

在这里插入图片描述

3.2. phase_2

对于第二关,依然是先查看它的汇编代码,如下:

在这里插入图片描述

下面对每一行汇编代码进行解释和说明(我的输入:0 1 3 6 10 15):

Dump of assembler code for function phase_2:
=> 0x08048b74 <+0>:	push   ebx
       //	保留旧的ebx
   0x08048b75 <+1>:	sub    esp,0x38
       //	给栈帧开辟0x38的空间
   0x08048b78 <+4>:	lea    eax,[esp+0x18]
       //	第一个参数的传递,先存放在eax
   0x08048b7c <+8>:	mov    DWORD PTR [esp+0x4],eax
       //	参数放在esp+0x4

在这里插入图片描述

可以看到传递的地址存放的值是0,接着下一步看看传递第二个参数:

   0x08048b80 <+12>:	mov    eax,DWORD PTR [esp+0x40]
   0x08048b84 <+16>:	mov    DWORD PTR [esp],eax
       //	这两行是在传递另外一个参数,这个参数就是我们输入的字符串

在这里插入图片描述

在执行完这两步之后,可以发现eax存放的就是输入的字符串,exp存放的地址的内容是输入字符串的地址,接着进行剩下汇编语句的分析:

   0x08048b87 <+19>:	call   0x80491eb <read_six_numbers>
       //	调用read_six_numbers函数,猜测读入六个数字
   0x08048b8c <+24>:	cmp    DWORD PTR [esp+0x18],0x0
   0x08048b91 <+29>:	jns    0x8048b98 <phase_2+36>
       //	以上两行是判断符号,我们有知道esp+0x18存放的是第一个数字的值,在这里是0
   0x08048b93 <+31>:	call   0x80490b6 <explode_bomb>
       //	如果是负数,那么就引爆炸药
   0x08048b98 <+36>:	mov    ebx,0x1
       //	初始化ebx为1
   0x08048b9d <+41>:	mov    eax,ebx
       //	传递给eax
   0x08048b9f <+43>:	add    eax,DWORD PTR [esp+ebx*4+0x14]
       //	eax = eax + 第i个数
   0x08048ba3 <+47>:	cmp    DWORD PTR [esp+ebx*4+0x18],eax
       //	把刚刚计算的结果和第i+1个数进行比较
   0x08048ba7 <+51>:	je     0x8048bae <phase_2+58>
       //	如果相等不爆炸
   0x08048ba9 <+53>:	call   0x80490b6 <explode_bomb>
   0x08048bae <+58>:	add    ebx,0x1
       //	ebx = exb + 1
   0x08048bb1 <+61>:	cmp    ebx,0x6
       //	由于只输入了六个数,所以不大于六
   0x08048bb4 <+64>:	jne    0x8048b9d <phase_2+41>
       //	当ebx<6的时候继续相同的以上操作
   0x08048bb6 <+66>:	add    esp,0x38
       //	还原esp
   0x08048bb9 <+69>:	pop    ebx
       //	还原ebx
   0x08048bba <+70>:	ret 
       //	返回call的下一条语句
End of assembler dump.

通过上述的分析,我们得知对于输入有以下的限制条件:

  1. 输入是六个数字
  2. 六个数字中第一个数字必须要为非负数
  3. 对于第i+1个数字,它必须是第i个数字与i的和

结合上述的限制条件,我们可以得到以下符合条件的输入例子:

0 1 3 6 10 15;
1 2 4 7 11 16;
2 3 5 8 12 17……;
100 101 103 106 110 115…… ;
n n+1 n+3 n+6 n+10 n+15; 其中n是任何非负整数

于是我们输入的0 1 3 6 10 15自然能够得到以下画面:

在这里插入图片描述

3.3. phase_3

按照之前的步骤,首先查看phase_3的汇编代码:

在这里插入图片描述

phase_3的汇编代码的长度一下子就上来了,下面就一段一段进行分析:

首先是栈帧的初始化以及调用scanf函数前的参数准备:

首先看一看寄存器eax的值,它保存着输入的字符串的地址(我的输入:2 50):

在这里插入图片描述

然后看看第一部分每一行的意义:

=> 0x08048bbb <+0>:	sub    esp,0x2c
       //	为栈帧开辟0x2c的空间
   0x08048bbe <+3>:	lea    eax,[esp+0x1c]
       //	通关观察寄存器,得知传给eax的是存放ebx值的地址
   0x08048bc2 <+7>:	mov    DWORD PTR [esp+0xc],eax
       //	第一个参数的准备
   0x08048bc6 <+11>:	lea    eax,[esp+0x18]
       //	EAX: 0xbffff278 --> 0xa ('\n')
   0x08048bca <+15>:	mov    DWORD PTR [esp+0x8],eax
       //	第二个参数的准备
   0x08048bce <+19>:	mov    DWORD PTR [esp+0x4],0x804a3a3
       //	0x804a3a3:	 "%d %d",可以看出应该输入两个整数
   0x08048bd6 <+27>:	mov    eax,DWORD PTR [esp+0x30]
       //	EAX: 0x804c480 ("2 50")
   0x08048bda <+31>:	mov    DWORD PTR [esp],eax
       //	第三个参数的准备,这个参数时我们输入的字符串
   0x08048bdd <+34>:	call   0x8048870 <__isoc99_sscanf@plt>
       //	调用scanf函数进行输入

通过peda得到以下信息,表示可能的参数(这非常有帮助):

在这里插入图片描述

继续分析之后的汇编代码:

   0x08048be2 <+39>:	cmp    eax,0x1
       //	这里的eax保存的应该是读入整数的个数,因为可能会出现只读到一个整数的情况
   0x08048be5 <+42>:	jg     0x8048bec <phase_3+49>
       //	输入的整数为2是正常,否者引爆bomb!
   0x08048be7 <+44>:	call   0x80490b6 <explode_bomb>
   0x08048bec <+49>:	cmp    DWORD PTR [esp+0x18],0x7
       //	esp+0x18猜测是我们输入的第一个整数,将它和7比较
   0x08048bf1 <+54>:	ja     0x8048c2f <phase_3+116>
       //	如果大于7Bomb!
   0x08048bf3 <+56>:	mov    eax,DWORD PTR [esp+0x18]
       //	把输入的第一个整数存储在eax
   0x08048bf7 <+60>:	jmp    DWORD PTR [eax*4+0x804a1e0]
       //	这里可以意识到,这是利用switch产生的一个跳转表来实现各种不同情况的输出

以上猜测的验证:esp+0x18往后的八个字节:

在这里插入图片描述

不难发现就是我们输入的两个整数2和50;接下来就是对跳转表不同情况下的输出的每条汇编的说明:

   0x08048bfe <+67>:	mov    eax,0x1bc
   0x08048c03 <+72>:	jmp    0x8048c40 <phase_3+133>
       //	以上是当第一个输入的数是1,那么把0x1bc->eax
   0x08048c05 <+74>:	mov    eax,0x32
   0x08048c0a <+79>:	jmp    0x8048c40 <phase_3+133>
       //	以上是当第一个输入的数是2,那么把0x32->eax
   0x08048c0c <+81>:	mov    eax,0x29f
   0x08048c11 <+86>:	jmp    0x8048c40 <phase_3+133>
       //	第一个数数3
   0x08048c13 <+88>:	mov    eax,0x29d
   0x08048c18 <+93>:	jmp    0x8048c40 <phase_3+133>
       //	第一个数数4
   0x08048c1a <+95>:	mov    eax,0x30b
   0x08048c1f <+100>:	jmp    0x8048c40 <phase_3+133>
       //	第一个数数5
   0x08048c21 <+102>:	mov    eax,0x11f
   0x08048c26 <+107>:	jmp    0x8048c40 <phase_3+133>
       //	第一个数数6
   0x08048c28 <+109>:	mov    eax,0x1a2
   0x08048c2d <+114>:	jmp    0x8048c40 <phase_3+133>
       //	第一个数数7
   0x08048c2f <+116>:	call   0x80490b6 <explode_bomb>
   0x08048c34 <+121>:	mov    eax,0x0
   0x08048c39 <+126>:	jmp    0x8048c40 <phase_3+133>
       //	default
   0x08048c3b <+128>:	mov    eax,0x25b
   0x08048c40 <+133>:	cmp    eax,DWORD PTR [esp+0x1c]
       //	把输入的第二个数与eax进行比较
   0x08048c44 <+137>:	je     0x8048c4b <phase_3+144>
       //	如果不相等Bomb!
   0x08048c46 <+139>:	call   0x80490b6 <explode_bomb>
   0x08048c4b <+144>:	add    esp,0x2c
       //	还原esp
   0x08048c4e <+147>:	ret 
       //	返回call的下一条语句

通过上述的分析,它是一个switch 的分支语句,用的是跳转表。结合一些限制条件因此有以下的集中情况情况,并且它们就是正确的通关输入:1 444,2 50,3 671,4 669…… 于是有以下通关提示:

在这里插入图片描述

3.4. phase_4

同样的,首先看看phase_4的汇编代码的情况:

在这里插入图片描述

这个汇编相比上一个而言少了一些,那么先看看func4的汇编代码:

在这里插入图片描述

那么现在加起来比第三题的汇编还长了,不过幸运的是func4函数的结构非常清晰,它不断的调用自己,可以推断出这就是一个递归函数的汇编代码,所以为了了解递归汇编代码的规律,我们用下面的简单例子进行引入:

在这里插入图片描述

因此我们把递归总结为以下四步:

  1. 传递参数
  2. 调用自身函数
  3. 对函数进行数学运算,一般是线性变化
  4. 返回

由上面的步骤,我们可以写出上面例子表示成代码为:return 2*fun4(parameter1,parameter2,parameter3),由此我们分析我的func4:

   0x08048c4f <+0>:	sub    esp,0x1c							%开辟栈空间
   0x08048c52 <+3>:	mov    DWORD PTR [esp+0x10],ebx			
   0x08048c56 <+7>:	mov    DWORD PTR [esp+0x14],esi
   0x08048c5a <+11>:	mov    DWORD PTR [esp+0x18],edi		%edi初值是0,eax初值是外部传入
   0x08048c5e <+15>:	mov    esi,DWORD PTR [esp+0x20]		%传入第一个参数
   0x08048c62 <+19>:	mov    ebx,DWORD PTR [esp+0x24]		%传入第二个参数
   0x08048c66 <+23>:	test   esi,esi						
   %如果esi非正数则跳转,return eax = ebx = 0
   0x08048c68 <+25>:	jle    0x8048c95 <func4+70>
   0x08048c6a <+27>:	cmp    esi,0x1
   %如果参数esi等于1跳转,return eax = ebx
   0x08048c6d <+30>:	je     0x8048c9a <func4+75>

    0x08048c6f <+32>:	mov    DWORD PTR [esp+0x4],ebx
    %传入参数ebx,这个值没有改变
    0x08048c73 <+36>:	lea    eax,[esi-0x1]
    0x08048c76 <+39>:	mov    DWORD PTR [esp],eax
    %传入参数eax = esi - 1,调用函数func4(s-1,b)
    0x08048c79 <+42>:	call   0x8048c4f <func4>

    %以上数传递参数以及函数自身调用

    0x08048c7e <+47>:	lea    edi,[eax+ebx*1]

    %d = func4(s-1,b) + b,有些数学运算不像前面的例子那么简单,通常可能多次调用,这里就是

    以下是第二次调用,老套路先传递参数

    0x08048c81 <+50>:	mov    DWORD PTR [esp+0x4],ebx
    %传入参数ebx,这个值没有改变
    0x08048c85 <+54>:	sub    esi,0x2
    0x08048c88 <+57>:	mov    DWORD PTR [esp],esi
    %传入参数esi - 2,调用函数func(s-2,b)
    0x08048c8b <+60>:	call   0x8048c4f <func4>

    %第二次调用函数自身

    0x08048c90 <+65>:	lea    ebx,[edi+eax*1]

    %这里的计算其实就是在计算返回值eax,b = d + func4(s-2,b) = func4(s-1,b) + b + func4(s-2,b)

    %最后一句是跳转到递归返回return的位置,所以就是直接return计算结果(如果func4+75没有其他对eax进行操作的时候)

    0x08048c93 <+68>:	jmp    0x8048c9a <func4+75>

由以上的分析我们可以写出func4函数的C++程序代码,如下:

#include <iostream>
using namespace std;

int func4(int s, int b){
	if(s<=0)
		return 0;
	if(s==1)
		return b;
	return func4(s-1,b)+b+func4(s-2,b);
}

int main(){
	cout<<func4(5,3)<<endl;
    //	输出的结果为36
	return 0;
}

然后再回到phase_4进行分析(我输入的是36 3 DrEvil,为什么要输入DrEvil,先卖一个关子):

Dump of assembler code for function phase_4:
=> 0x08048cac <+0>:	sub    esp,0x2c
   0x08048caf <+3>:	lea    eax,[esp+0x18]
   0x08048cb3 <+7>:	mov    DWORD PTR [esp+0xc],eax
   0x08048cb7 <+11>:	lea    eax,[esp+0x1c]
   0x08048cbb <+15>:	mov    DWORD PTR [esp+0x8],eax
   0x08048cbf <+19>:	mov    DWORD PTR [esp+0x4],0x804a3a3
   0x08048cc7 <+27>:	mov    eax,DWORD PTR [esp+0x30]
   0x08048ccb <+31>:	mov    DWORD PTR [esp],eax
   0x08048cce <+34>:	call   0x8048870 <__isoc99_sscanf@plt>

上面的汇编代码的操作和前一关有异曲同工 之妙,实现的功能都是输入两个整数,那就不在赘述了,我们通过peda记入一下可能的参数:

在这里插入图片描述

以下的汇编代码就是对输入的判断和调用func4前的参数的准备:

   0x08048cd3 <+39>:	cmp    eax,0x2
       //	对输入的整数的个数的判断,如果不是2,Bomb!
   0x08048cd6 <+42>:	jne    0x8048ce6 <phase_4+58>
   0x08048cd8 <+44>:	mov    eax,DWORD PTR [esp+0x18]
       //	通过观察寄存器eax的值,发现传进来的是3,就是我们驶入的第二个数字
   0x08048cdc <+48>:	cmp    eax,0x1
       //	这个数必须大于1,不然Bomb!
   0x08048cdf <+51>:	jle    0x8048ce6 <phase_4+58>
   0x08048ce1 <+53>:	cmp    eax,0x4
       //	这个数必须要小于等于4,也就是第二个数为(1,4],可以为2,3,4
   0x08048ce4 <+56>:	jle    0x8048ceb <phase_4+63>
   0x08048ce6 <+58>:	call   0x80490b6 <explode_bomb>
   0x08048ceb <+63>:	mov    eax,DWORD PTR [esp+0x18]
   0x08048cef <+67>:	mov    DWORD PTR [esp+0x4],eax
       //	准备第一个参数3
   0x08048cf3 <+71>:	mov    DWORD PTR [esp],0x5
       //	准备第二个参数5,这个数是既定的,不能改变
   0x08048cfa <+78>:	call   0x8048c4f <func4>
       //	调用递归函数func4

当我们运行完上面的递归过程会把结果放入寄存器eax,然后接着分析下面的汇编:

   0x08048cff <+83>:	cmp    eax,DWORD PTR [esp+0x1c]
       //	把输出的结果和esp+0x1c上的值进行比较,这个地址存放的其实就是我们输入的第一个整数36,换句话说就是第一个数你必须输入递归正确计算得到的结果(第三个输入先忽略)
   0x08048d03 <+87>:	je     0x8048d0a <phase_4+94>
   0x08048d05 <+89>:	call   0x80490b6 <explode_bomb>
   0x08048d0a <+94>:	add    esp,0x2c
       //	恢复esp
   0x08048d0d <+97>:	ret    
       //	返回call下一条
End of assembler dump.

由以上的分析,我们得知第一个整数应该是输入第二个整数通过递归计算得到的结果,而我们已知第二个整数输入的值只能是2,3,4,同时我们又有递归程序,所以把这三个值输入分别可以得到对应的递归输出24,36,48,所以我们输入可以是以下三种情况:24 2, 36 3, 48 4。因此,我们可以得到以下的反馈画面:

在这里插入图片描述

3.5. phase_5

依然先看它的汇编代码:

在这里插入图片描述

显然这次的函数没有调用函数,且汇编代码的行数也不多,但说实话难度不低,看一下逐句分析(我的输入:513489):

Dump of assembler code for function phase_5:
=> 0x08048d0e <+0>:	push   ebx								//保存旧的ebp
   0x08048d0f <+1>:	sub    esp,0x18							//开闭0x18的栈帧空间
   0x08048d12 <+4>:	mov    ebx,DWORD PTR [esp+0x20]			//把输入字符串的地址传入ebx
   0x08048d16 <+8>:	mov    DWORD PTR [esp],ebx				//输入地址放入栈顶
   0x08048d19 <+11>:	call   0x8048f8b <string_length>	//计算输入的字符串的长度
   0x08048d1e <+16>:	cmp    eax,0x6						//输入的字符串的长度要等于6
   0x08048d21 <+19>:	je     0x8048d28 <phase_5+26>
   0x08048d23 <+21>:	call   0x80490b6 <explode_bomb>		//如果不等于6,Bomb!
   0x08048d28 <+26>:	mov    edx,0x0
   0x08048d2d <+31>:	mov    eax,0x0						//这两步是初始化eax和edx进行置零
   0x08048d32 <+36>:	movsx  ecx,BYTE PTR [ebx+eax*1]		//写入第i个字符符号扩展到ecx,如果是第一个字符,那么这里是'5',可以看看ecx有:ECX: 0x35 ('5')
   0x08048d36 <+40>:	and    ecx,0xf						//取出最低一个字节,这里是5
   0x08048d39 <+43>:	add    edx,DWORD PTR [ecx*4+0x804a200]
       //	这里是重点,需要查看0x804a200地址之后一大串保存的数值,把这个偏移地址保存的值加到edx
   0x08048d40 <+50>:	add    eax,0x1						//eax自加,即偏移量两个取址的偏移量分别+1,+4	(phase_5+36和phase_5+43这两个步骤)
   0x08048d43 <+53>:	cmp    eax,0x6						//只有六个字符,不能多访问了
   0x08048d46 <+56>:	jne    0x8048d32 <phase_5+36>
   0x08048d48 <+58>:	cmp    edx,0x32						//在计算六次加法后保存在edx的结果和50进行比较
   0x08048d4b <+61>:	je     0x8048d52 <phase_5+68>		//如果结果不为50,Bomb!
   0x08048d4d <+63>:	call   0x80490b6 <explode_bomb>
   0x08048d52 <+68>:	add    esp,0x18						//还原esp
   0x08048d55 <+71>:	pop    ebx							//还原ebx
   0x08048d56 <+72>:	ret    								//返回call的下一条
End of assembler dump.

由上述的详细分析,直到该关卡的目的就是要你在一个数组中选取六个数,使这六个数加起来的和等于50,所以第一步就是查看0x804a200地址之后存放的数值,但是为了简单起见同时考虑到这个问题有非常多的解,就考虑<phase_5+40>取出来的数在0~9之间,同时考虑到[0,9]数字的十六进制表示,所以输入的六个字符均用数字代替,以下是[0,9]的ASCII十六进制表示:

123456789
0x310x320x330x340x350x360x370x380x39

通过查看内存得到地址连续内存下的9个数值为:10,6,1,12,16,9,3,4,7

通过回溯法求解出从中可重复抽取六个数,从而凑出50的C++程序如下:

#include <iostream>
#include <vector> 
using namespace std;

vector <vector <int> > vec;


int arr[9] = {10,6,1,12,16,9,3,4,7}; 

void backtrace(int depth, vector <int> V){
	if(depth == 6){
		int result = 0;
		for(int i=0;i<6;i++)
			result += V[i];
		if(result == 50){
			vec.push_back(V);
		}
		return;
	}
	for(int i=0;i<9;i++){
		V.push_back(arr[i]);
		backtrace(depth+1,V);
		V.pop_back();
	}
}

int main(){
	vector <int> V;
	backtrace(0,V);
	for(int i=0;i<vec.size();i++){
		for(int j=0;j<vec[i].size();j++){
			cout<<vec[i][j]<<" ";
		}
		cout<<endl;
	}
	return 0;
}
/*
	result:
10 9 3 3 9 16
10 9 3 4 12 12
10 9 3 7 12 9
10 9 3 7 9 12
10 9 4 10 10 7
10 9 4 10 1 16
10 9 4 10 16 1
10 9 4 10 7 10
10 9 4 6 12 9......
*/

我们从其中随便抽取一种情况:(16,10,1,12,4,7),对应的序号形成的字符串为“513489”,输入该字符串可以得到以下的反馈:

在这里插入图片描述

3.6. phase_6

phase_6的汇编有点长,我们直接对它进行逐句分析(我的输入:4 5 3 1 2 6):

Dump of assembler code for function phase_6:
=> 0x08048d57 <+0>:	push   esi
   0x08048d58 <+1>:	push   ebx									//保存esi和ebx
   0x08048d59 <+2>:	sub    esp,0x44								//开辟栈空间
   0x08048d5c <+5>:	lea    eax,[esp+0x10]						
   0x08048d60 <+9>:	mov    DWORD PTR [esp+0x4],eax				//第一个参数
   0x08048d64 <+13>:	mov    eax,DWORD PTR [esp+0x50]
   0x08048d68 <+17>:	mov    DWORD PTR [esp],eax				
       //第二个参数,EAX: 0x804c570 ("4 5 3 1 2 6")
   0x08048d6b <+20>:	call   0x80491eb <read_six_numbers>		//输入六个整数

以上部分是数据输入的部分,接着往下看:

友情提示:由于存在双层循环,为了简化,我就令外循环为i(代表esi),内循环为j(代表ebx),为了理解第一个双层循环,写出以下循环代码:

int i,j;
for(i = 0; i < 6; ){
    
	if(a[i] - 1 > 5){
        explode_bomb();
        return ;
    }
    i = i + 1;
    for(j = i; j <= 5; j++)
        if(a[j] == a[i - 1]){
            explode_bomb();
            return ;
        }
}
        
   0x08048d70 <+25>:	mov    esi,0x0							//初始化esi,i = 0
   0x08048d75 <+30>:	mov    eax,DWORD PTR [esp+esi*4+0x10]	//将输入的第i个数
   0x08048d79 <+34>:	sub    eax,0x1							//eax = eax - 1
   0x08048d7c <+37>:	cmp    eax,0x5							//eax和5比较
   0x08048d7f <+40>:	jbe    0x8048d86 <phase_6+47>			//意思就是输入的数必须要<=6
   0x08048d81 <+42>:	call   0x80490b6 <explode_bomb>
   0x08048d86 <+47>:	add    esi,0x1							//i = i + 1
   0x08048d89 <+50>:	cmp    esi,0x6							//外循环终止条件i<6
   0x08048d8c <+53>:	je     0x8048da9 <phase_6+82>			//外循环截止	
   0x08048d8e <+55>:	mov    ebx,esi							//j = i,内循环的"i"
   0x08048d90 <+57>:	mov    eax,DWORD PTR [esp+ebx*4+0x10]	//eax存放第j个数
   0x08048d94 <+61>:	cmp    DWORD PTR [esp+esi*4+0xc],eax	
       //加入输入的六个数在a[6]中,外循环为i(代表esi),内循环为j(代表ebx),这条语句的意思就是比较a[j]和a[i - 1]的大小,同时j的初始值为i
   0x08048d98 <+65>:	jne    0x8048d9f <phase_6+72>			//代表着输入不能有两两相同
   0x08048d9a <+67>:	call   0x80490b6 <explode_bomb>
   0x08048d9f <+72>:	add    ebx,0x1							//j = j + 1
   0x08048da2 <+75>:	cmp    ebx,0x5							//j <= 5就执行内循环
   0x08048da5 <+78>:	jle    0x8048d90 <phase_6+57>			
   0x08048da7 <+80>:	jmp    0x8048d75 <phase_6+30>			//否则外循环

以上循环结束时,得到的结论为:A、B、C、D、E、F的取值范围均为<=6,且两两间互不相等。这就是以上程序对你的输入作出的限制条件。

   0x08048da9 <+82>:	lea    eax,[esp+0x10]
   0x08048dad <+86>:	lea    ebx,[esp+0x28]					
       //分别传递的是开始存放数值的地址和开始存放地址的地址
   0x08048db1 <+90>:	mov    ecx,0x7
   0x08048db6 <+95>:	mov    edx,ecx
       //初始化ecx和edx为7
   0x08048db8 <+97>:	sub    edx,DWORD PTR [eax]
   0x08048dba <+99>:	mov    DWORD PTR [eax],edx
       //以上两句的意思是:如果用a[6]保存六个输入的数字,那么a[i] = 7 - a[i]
   0x08048dbc <+101>:	add    eax,0x4					//地址的偏移量+4
   0x08048dbf <+104>:	cmp    eax,ebx					//循环终止条件,这个循环执行六次
   0x08048dc1 <+106>:	jne    0x8048db6 <phase_6+95>

这个循环的效果如下,假如a[6] = {1,2,3,4,5,6},那么执行之后的效果变为a[6] = {6,5,4,3,2,1},C程序表示如下:

for(int i = 0; i < 6; i++)
	a[i] = 7 - a[i];

接着分析以下汇编:

注意:这里也是一个双层循环,所以我这里也用i代表ebx为外循环,j代表eax为内循环

另外地址0x804c13c连续空间储存的值为:

在这里插入图片描述

于是我们可以联想到“链表”,如果想到这个层面那么之后的都简单了

还有就是对这个结点的理解,这个结点包括三个信息,比如第一个结点{0x6d,0x01,0x0804c148},比如p是指向该结点的地址那么* p就是该结点的数值* (p+4)指的就是结点序号,* (p+8)就是他下一个结点的指针,指向下一个结点

   0x08048dc3 <+108>:	mov    ebx,0x0								//初始化i为0
   0x08048dc8 <+113>:	jmp    0x8048de0 <phase_6+137>				//无条件跳转
   0x08048dca <+115>:	mov    edx,DWORD PTR [edx+0x8]				
       //这个地址储存的是地址,并且是下一个结点的地址
   0x08048dcd <+118>:	add    eax,0x1								//j = j + 1
   0x08048dd0 <+121>:	cmp    eax,ecx								//j和a[i]进行比较
   0x08048dd2 <+123>:	jne    0x8048dca <phase_6+115>				
       //不相等跳转,这个跳转的意义就是按照你输入的数值进行排序,什么意思呢?举个例子(秩和比法的味道):
       /*
       		你的输入:4 1 3 2(它就代表秩,就理解为顺序即可)
       		原始数据:23 11 26 8
       		然后用你的输入对原始数据进行排序得到新序列:8 23 36 11
            第四个(8)排到了第一,第一个(23)排到了第二......
            
            到这里结合语境就是:把第四个地址放在第一个,第一个地址放在第二个......
       */
   0x08048dd4 <+125>:	mov    DWORD PTR [esp+esi*4+0x28],edx
       //把这个地址写入内存
   0x08048dd8 <+129>:	add    ebx,0x1								//i = i + 1
   0x08048ddb <+132>:	cmp    ebx,0x6								//i < 6
   0x08048dde <+135>:	je     0x8048df7 <phase_6+160>
   0x08048de0 <+137>:	mov    esi,ebx								//esi = i
   0x08048de2 <+139>:	mov    ecx,DWORD PTR [esp+ebx*4+0x10]		//ecx = a[i]
   0x08048de6 <+143>:	mov    eax,0x1								//j = 1
   0x08048deb <+148>:	mov    edx,0x804c13c						
       //主要是要理解它之后的18个4字节空间下储存的数值的意义
   0x08048df0 <+153>:	cmp    ecx,0x1								//a[i]和1进行比较
   0x08048df3 <+156>:	jg     0x8048dca <phase_6+115>				//大于1
   0x08048df5 <+158>:	jmp    0x8048dd4 <phase_6+125>				//反之

以上的汇编代码的作用用一句话说就是,把这些节点进行排序,比如我输入的4、1、3、5、6、2。操作以后的结果就是节点4放在了ad1位置、节点1放在了ad2位置、节点3放在了ad3位置、节点5放在了ad4位置、节点6放在了ad5位置、节点2放在了ad6位置

   0x08048df7 <+160>:	mov    ebx,DWORD PTR [esp+0x28]
   0x08048dfb <+164>:	mov    eax,DWORD PTR [esp+0x2c]
   0x08048dff <+168>:	mov    DWORD PTR [ebx+0x8],eax
   0x08048e02 <+171>:	mov    edx,DWORD PTR [esp+0x30]
   0x08048e06 <+175>:	mov    DWORD PTR [eax+0x8],edx
   0x08048e09 <+178>:	mov    eax,DWORD PTR [esp+0x34]
   0x08048e0d <+182>:	mov    DWORD PTR [edx+0x8],eax
   0x08048e10 <+185>:	mov    edx,DWORD PTR [esp+0x38]
   0x08048e14 <+189>:	mov    DWORD PTR [eax+0x8],edx
   0x08048e17 <+192>:	mov    eax,DWORD PTR [esp+0x3c]
   0x08048e1b <+196>:	mov    DWORD PTR [edx+0x8],eax
   0x08048e1e <+199>:	mov    DWORD PTR [eax+0x8],0x0
       /*
       		以上的关系自己画个变化过程图,要比我用文字写出来好理解多了
       		ebx、eax分别指向两个节点,(ebx+8)和(eax+8)就是它们下一个节点的指针
       		所以以上的目的就是形成一个链表,这个链表的顺序是依照你输入的数据
       */
   0x08048e25 <+206>:	mov    esi,0x5						//初始化esi,我们假设它为i,i = 5
   0x08048e2a <+211>:	mov    eax,DWORD PTR [ebx+0x8]		//下一个结点的地址放入eax
   0x08048e2d <+214>:	mov    edx,DWORD PTR [eax]			//下一个结点地址的值放入edx
   0x08048e2f <+216>:	cmp    DWORD PTR [ebx],edx			
       //前三句的意思就是前一个结点的数值要大于有一个结点的数值,换句话说就是这个链表数值要递减
   0x08048e31 <+218>:	jge    0x8048e38 <phase_6+225>
   0x08048e33 <+220>:	call   0x80490b6 <explode_bomb>
   0x08048e38 <+225>:	mov    ebx,DWORD PTR [ebx+0x8]		//更新ebx,变为下一个结点的地址
   0x08048e3b <+228>:	sub    esi,0x1						//i > 0
   0x08048e3e <+231>:	jne    0x8048e2a <phase_6+211>
       //以下四句是恢复原来的状态
   0x08048e40 <+233>:	add    esp,0x44
   0x08048e43 <+236>:	pop    ebx
   0x08048e44 <+237>:	pop    esi
   0x08048e45 <+238>:	ret    
End of assembler dump.

依照我的输入4 5 3 1 2 6(经过(7-a[i])变化之后得到:3 2 4 6 5 1),我们首先看看最初的链表的构造:

在这里插入图片描述

它显然不能满足递减的性质,但是针对上面六个结点,我们可以给出一个秩,让它经过我们的秩排序后称为一个有序递减的序列,显然如果这个秩为{3 2 4 6 5 1},那么就会形成一个有序递减的序列

然后看看经过排序后新形成的链表的构造:

在这里插入图片描述

经过这个变化之后达到了递增效果,于是得到反馈:

在这里插入图片描述

为什么出现secret_phase呢?回到前面第四题我们多输入了一个字符串DrEvil,那又为什么,看下面的分析

3.7. secret_phase

我们解完六个关卡后似乎程序已经运行完成了,说明隐藏关卡还需要一定的条件才能触发,那么我们首先就要先去找到触发隐藏关卡的条件。我们看到bomb.c文件,每段phase函数运行完成以后又会运行一个phase_defused()函数,这个函数我们在上述拆炸弹过程中都没有用到,自然它的嫌疑就很大,故我们先看看这个函数的具体内容:

Dump of assembler code for function phase_defused:
=> 0x0804923b <+0>:	sub    esp,0x8c									//开辟栈空间
   0x08049241 <+6>:	mov    eax,gs:0x14								
   //储存金丝雀值,进行栈保护
   0x08049247 <+12>:	mov    DWORD PTR [esp+0x7c],eax				
   //把金丝雀值放到0x7c(%esp)
   0x0804924b <+16>:	xor    eax,eax								//将%eax进行置零
   0x0804924d <+18>:	cmp    DWORD PTR ds:0x804c3cc,0x6
   //将0x804c3cc地址中的值与6进行比较,可以猜测这个地址储存的值是你通过的关的数量
   0x08049254 <+25>:	jne    0x80492c8 <phase_defused+141>		//如果不等于跳转到+141
   0x08049256 <+27>:	lea    eax,[esp+0x2c]						//参数的地址
   0x0804925a <+31>:	mov    DWORD PTR [esp+0x10],eax				//把参数地址保存
   0x0804925e <+35>:	lea    eax,[esp+0x28]						//第二个参数地址
   0x08049262 <+39>:	mov    DWORD PTR [esp+0xc],eax				//第二个参数地址保存
   0x08049266 <+43>:	lea    eax,[esp+0x24]						//第三个
   0x0804926a <+47>:	mov    DWORD PTR [esp+0x8],eax				//第三个
   //这我不自主的看看存放的值0x00000024	0x00000003	0xbffff264,这是这三个参数的值,前面两个数十进制分别是36 3,这里联想到前面一关也输入了这个,为了验证猜想可以看看地址
   0x0804926e <+51>:	mov    DWORD PTR [esp+0x4],0x804a3a9		
   //一个字符串地址,"%d %d %s"
   0x08049276 <+59>:	mov    DWORD PTR [esp],0x804c4d0
   //0x804c4d0 <input_strings+240>:	 "",这里对上述的假设验证,我们查看发现0x0804c4d0其实是字符串"36 3"的地址,也就是第四关输入的地址,那么我们只需要在后面添加一个输入即可,输入什么呢?接着看
   0x0804927d <+66>:	call   0x8048870 <__isoc99_sscanf@plt>
   //进行输入了
   0x08049282 <+71>:	cmp    eax,0x3
   //参数的个数与3进行比较
   0x08049285 <+74>:	jne    0x80492bc <phase_defused+129>
   //如果不等于跳转+129
   0x08049287 <+76>:	mov    DWORD PTR [esp+0x4],0x804a3b2
   //0x804a3b2:	 "DrEvil"
   0x0804928f <+84>:	lea    eax,[esp+0x2c]
   //调用函数前的参数的准备,进行传参,猜测应该是你输入的字符串,所以应该输入"DrEvil"
   //那么我们在第四关的时候就输入36 3 DrEvil,这时候完成其他六关会出现提示哦!
   0x08049293 <+88>:	mov    DWORD PTR [esp],eax
   //参数的地址拿到栈顶
   0x08049296 <+91>:	call   0x8048fa4 <strings_not_equal>
   //调用函数进行比较
   0x0804929b <+96>:	test   eax,eax
   //判断结果eax存放值的符号
   0x0804929d <+98>:	jne    0x80492bc <phase_defused+129>
   //如果不相等就跳转到+129,否则执行执行下方的secret_phase
   0x0804929f <+100>:	mov    DWORD PTR [esp],0x804a278
   //0x804a278:	 "Curses, you've found the secret phase!"
   0x080492a6 <+107>:	call   0x8048800 <puts@plt>
   0x080492ab <+112>:	mov    DWORD PTR [esp],0x804a2a0
   //0x804a2a0:	 "But finding it and solving it are quite different..."
   0x080492b2 <+119>:	call   0x8048800 <puts@plt>
   //打印以上两句话
   0x080492b7 <+124>:	call   0x8048e97 <secret_phase>
   //调用隐藏炸弹
   0x080492bc <+129>:	mov    DWORD PTR [esp],0x804a2d8
   //以第一个炸弹为例,这时候这个代表的是"Congratulations! You've defused the bomb!"
   0x080492c3 <+136>:	call   0x8048800 <puts@plt>
   //把上面的字符串进行输出
   0x080492c8 <+141>:	mov    eax,DWORD PTR [esp+0x7c]				//取出程序运行完之后的金丝雀值
   0x080492cc <+145>:	xor    eax,DWORD PTR gs:0x14				//对金丝雀值进行对比,检查是否发生异常
   0x080492d3 <+152>:	je     0x80492da <phase_defused+159>		//如果结果为0,表示正常
   0x080492d5 <+154>:	call   0x80487d0 <__stack_chk_fail@plt>		//否者报错
   0x080492da <+159>:	add    esp,0x8c
   0x080492e0 <+165>:	ret    
End of assembler dump.

我们反汇编看一看secret_phase的反汇编代码:

Dump of assembler code for function secret_phase:
=> 0x08048e97 <+0>:	push   ebx
   		//保存旧的ebp地址
   0x08048e98 <+1>:	sub    esp,0x18
       //开辟栈空间
   0x08048e9b <+4>:	call   0x80490dd <read_line>
       //读入一行数据,假设我们输入"100"
   0x08048ea0 <+9>:	mov    DWORD PTR [esp+0x8],0xa
       //输入参数10
   0x08048ea8 <+17>:	mov    DWORD PTR [esp+0x4],0x0
       //输入参数0
   0x08048eb0 <+25>:	mov    DWORD PTR [esp],eax
       //把eax中的值保存在栈顶,显然这个eax是我们输入字符串的地址
   0x08048eb3 <+28>:	call   0x80488e0 <strtol@plt>
       //经过这个函数eax保存的是0x64显然是100的16进制,那么推测上面函数是将一个字符串的数转化为整数
   0x08048eb8 <+33>:	mov    ebx,eax
       //用ebx保存100
   0x08048eba <+35>:	lea    eax,[eax-0x1]
       //eax-1
   0x08048ebd <+38>:	cmp    eax,0x3e8
       //eax即99和1000比较(后面观察到这个是二叉树的最大值-1)
   0x08048ec2 <+43>:	jbe    0x8048ec9 <secret_phase+50>
       //如果小于等于跳转+50,不会爆炸哦
   0x08048ec4 <+45>:	call   0x80490b6 <explode_bomb>
   0x08048ec9 <+50>:	mov    DWORD PTR [esp+0x4],ebx
       //把100移到esp+4,传参
   0x08048ecd <+54>:	mov    DWORD PTR [esp],0x804c088
       //gdb-peda$ x/s 0x804c088 0x804c088 <n1>:	 "$"    
       //gdb-peda$ x/xw 0x804c088 0x804c088 <n1>:	0x00000024
   0x08048ed4 <+61>:	call   0x8048e46 <fun7>
       //调用函数fun7
   0x08048ed9 <+66>:	cmp    eax,0x2
       //当返回值等于2时,bingo!
   0x08048edc <+69>:	je     0x8048ee3 <secret_phase+76>
   0x08048ede <+71>:	call   0x80490b6 <explode_bomb>
   0x08048ee3 <+76>:	mov    DWORD PTR [esp],0x804a1ac
   0x08048eea <+83>:	call   0x8048800 <puts@plt>
       //输出"Wow! You've defused the secret stage!"
   0x08048eef <+88>:	call   0x804923b <phase_defused>
   0x08048ef4 <+93>:	add    esp,0x18
       //esp恢复调用前的状态
   0x08048ef7 <+96>:	pop    ebx
       //恢复ebp
   0x08048ef8 <+97>:	ret    
       //跳转到调用函数时call的下一条语句
End of assembler dump.

fun7函数:显然地,这又是一个递归函数

Dump of assembler code for function fun7:
=> 0x08048e46 <+0>:	push   ebx
   		//放入ebx
   0x08048e47 <+1>:	sub    esp,0x18
       //为栈帧开辟空间
   0x08048e4a <+4>:	mov    edx,DWORD PTR [esp+0x20]
       //读入形式参数edx = 0x804c088
   0x08048e4e <+8>:	mov    ecx,DWORD PTR [esp+0x24]
       //读入形式参数ecx = 0x64
       
       //这里推测输入的两个参数一个数地址一个是数值,又结合递归联想到他可能和树结构有关系,假设函数		   //为fun7(root, val)
       
   0x08048e52 <+12>:	test   edx,edx
       //判断edx的符号
   0x08048e54 <+14>:	je     0x8048e8d <fun7+71>
       //等于0,跳转至+71,(一定不能等于0),其实可以理解为节点为NULL
   0x08048e56 <+16>:	mov    ebx,DWORD PTR [edx]
       //ebx = 0x00000024,节点的数值
   0x08048e58 <+18>:	cmp    ebx,ecx
       //把节点的数值和输入的形参数值比较
   0x08048e5a <+20>:	jle    0x8048e6f <fun7+41>
       //如果ebx<=ecx。跳转至+41
       //如果ebx里面的值更大
   0x08048e5c <+22>:	mov    DWORD PTR [esp+0x4],ecx
       //把ecx保存在esp+0x4
   0x08048e60 <+26>:	mov    eax,DWORD PTR [edx+0x4]
       //传递参数,这里传入的是左节点,edx +0x4是左节点 +0x8是右节点
   0x08048e63 <+29>:	mov    DWORD PTR [esp],eax
       //eax放到栈顶,方便调用
   0x08048e66 <+32>:	call   0x8048e46 <fun7>
       //调用函数fun7
   0x08048e6b <+37>:	add    eax,eax
       //eax = 2*eax,return 2*fun7(val1,val2),val1 = root->left,val2 = val
   0x08048e6d <+39>:	jmp    0x8048e92 <fun7+76>
       //跳出递归
   0x08048e6f <+41>:	mov    eax,0x0
       //将eax置0
   0x08048e74 <+46>:	cmp    ebx,ecx
   0x08048e76 <+48>:	je     0x8048e92 <fun7+76>
       //如果ebx(0x24) == ecx(0x64),(显然不等于),跳转至+76,就是直接返回了,return 0
   0x08048e78 <+50>:	mov    DWORD PTR [esp+0x4],ecx
   0x08048e7c <+54>:	mov    eax,DWORD PTR [edx+0x8]
       //为什么加8,摆明了是指向其右节点
   0x08048e7f <+57>:	mov    DWORD PTR [esp],eax
       //以上三步就是参数的传递
   0x08048e82 <+60>:	call   0x8048e46 <fun7>
   0x08048e87 <+65>:	lea    eax,[eax+eax*1+0x1]
       //递归的最后一步结果的线性组合,return 2*fun7(val1,val2)+1,val1 = root->right,val2 = val
   0x08048e8b <+69>:	jmp    0x8048e92 <fun7+76>
       //结束递归
   0x08048e8d <+71>:	mov    eax,0xffffffff
       //将0xffffffff传入eax
   0x08048e92 <+76>:	add    esp,0x18
   0x08048e95 <+79>:	pop    ebx
   0x08048e96 <+80>:	ret  
       //恢复调用前的状态
End of assembler dump.

我们写出递归函数:

int fun7(Node *root, int val){
    if(root == NULL)
        return -1;
    if(root->value == val)
         return 0;       
    else if(root->value > val)
    	return 2*fun7(root->left,val);
    else if(root->value < val)
        return 2*fun7(root->right,val)+1;
}

可以通过以下指令查看一个结点的信息:

p/x *0x804c088@3

通过这个可以查看一个节点,其中0x804c088是一个结点的地址指针,可以自行修改。

gdb-peda$ p/x *0x804c088@3
$5 = {0x24, 0x804c094, 0x804c0a0}
gdb-peda$ p/x *0x804c088@3
$6 = {0x24, 0x804c094, 0x804c0a0}
gdb-peda$ p/x *0x804c094@3
$8 = {0x8, 0x804c0c4, 0x804c0ac}
gdb-peda$ p/x *0x804c0c4@3
$9 = {0x6, 0x804c0e8, 0x804c10c}
gdb-peda$ p/x *0x804c0ac@3
$10 = {0x16, 0x804c118, 0x804c100}
gdb-peda$ p/x *0x804c0e8@3
$11 = {0x1, 0x0, 0x0}
gdb-peda$ p/x *0x804c10c@3
$12 = {0x7, 0x0, 0x0}
gdb-peda$ p/x *0x804c118@3
$13 = {0x14, 0x0, 0x0}
gdb-peda$ p/x *0x804c100@3
$14 = {0x23, 0x0, 0x0}
gdb-peda$ p/x *0x804c0ac@3
$15 = {0x16, 0x804c118, 0x804c100}
gdb-peda$ p/x *0x804c0a0@3
$16 = {0x32, 0x804c0b8, 0x804c0d0}
gdb-peda$ p/x *0x804c0b8@3
$17 = {0x2d, 0x804c0dc, 0x804c124}
gdb-peda$ p/x *0x804c0d0@3
$18 = {0x6b, 0x804c0f4, 0x804c130}
gdb-peda$ p/x *0x804c0dc@3
$19 = {0x28, 0x0, 0x0}
gdb-peda$ p/x *0x804c124@3
$20 = {0x2f, 0x0, 0x0}
gdb-peda$ p/x *0x804c0f4@3
$21 = {0x63, 0x0, 0x0}
gdb-peda$ p/x *0x804c130@3
$22 = {0x3e9, 0x0, 0x0}

通过上述的结点的信息,可以大致画出以下的树结构图(16进制):

​ 24

​ 8 23

​ 6 16 2d 6b

​ 1 7 14 23 28 2f 63 3e9

限制条件:

   0x08048ebd <+38>:	cmp    eax,0x3e8
       //eax即99和1000比较(后面观察到这个是二叉树的最大值-1)
   0x08048ec2 <+43>:	jbe    0x8048ec9 <secret_phase+50>

在每一个节点都有两种选择,然后利用递归的性质我们可以得到以下的展开图:

在这里插入图片描述

那么显然有:

  1. 根->左->右 :0x16
  2. 根->左->右->左 :0x14

当我们输入22 or 20的时候我们就会得到成功通关的提示!

在这里插入图片描述

  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

call me Patrick

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值