Bootstrap

HNU-计算机系统(CSAPP)实验三 BombLab

前言

BombLab来自《深入理解计算机系统》(CSAPP)一书的第三章“程序的机器级表示”的配套实验,该实验的目的是通过反汇编可执行程序bomb,来反推出程序执行内容,进而能够正确破解“密码”,拆除“炸弹”。

BombLab文件目录如下:

├── bomb
├── bomb.c
├── bomb-quiet
└── README

具体的bomb压缩包会由助教发给大家,每个人独一份,确保不重样。其他外校的uu也可以去官网搜索下载,享受拆炸弹的乐趣(bushi) 

一、实验题目及要求

实验的目标是拆除尽可能多的炸弹(包含隐藏炸弹)。不同的炸弹考察的是汇编语言的不同方面。

①阶段1:字符串比较

②阶段2:循环语句

③阶段3:条件/分支/跳转表

④阶段4:递归

⑤阶段5:指针

⑥阶段6:链表

二、准备工作

一点小建议:看汇编代码需要花费大量时间,没必要在Ubuntu终端里面看,可以找一个高级一点的代码编辑器(如vscode)里面看,还能边读边做注释...

1.打开bomb.c文件

/***************************************************************************
 * Dr. Evil's Insidious Bomb, Version 1.1
 * Copyright 2011, Dr. Evil Incorporated. All rights reserved.
 *
 * LICENSE:
 *
 * Dr. Evil Incorporated (the PERPETRATOR) hereby grants you (the
 * VICTIM) explicit permission to use this bomb (the BOMB).  This is a
 * time limited license, which expires on the death of the VICTIM.
 * The PERPETRATOR takes no responsibility for damage, frustration,
 * insanity, bug-eyes, carpal-tunnel syndrome, loss of sleep, or other
 * harm to the VICTIM.  Unless the PERPETRATOR wants to take credit,
 * that is.  The VICTIM may not distribute this bomb source code to
 * any enemies of the PERPETRATOR.  No VICTIM may debug,
 * reverse-engineer, run "strings" on, decompile, decrypt, or use any
 * other technique to gain knowledge of and defuse the BOMB.  BOMB
 * proof clothing may not be worn when handling this program.  The
 * PERPETRATOR will not apologize for the PERPETRATOR's poor sense of
 * humor.  This license is null and void where the BOMB is prohibited
 * by law.
 ***************************************************************************/

#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;
}

观察到main函数依次调用了phase_1()~phase_6()这六个函数,但函数的具体代码无法查看。可以知道从命令行输入的内容必须通过phase函数的层层测试,否则炸弹就会爆炸。

2.反汇编可执行文件bomb

方法一:

objdump -d bomb > bomb.asm

这个命令用于从二进制文件(通常是一个可执行文件或库)中提取汇编代码,并将输出重定向到bomb.asm文件中,在逆向工程和调试汇编语言时常用。

但是因为方法一反汇编出的数据量过大,因此查找某个函数时不是很方便,因此推荐第二种方法。

方法二:

gdb -q bomb

首先启动GDB调试, -q表示“安静模式”,在这个模式下,GDB启动时不会打印介绍信息和版本信息,直接进入命令提示符,使得调试过程更加简洁。

(gdb) disassemble phase_x

disassemble命令用于反汇编指定的函数或内存地址范围,将其机器代码转换为汇编语言指令。 

3.执行bomb可执行文件 

./bomb

程序运行后,随便输入某个数字或字符串,炸弹爆炸,提示密码错误。下面就需要我们一步一步探索了。

PS:

./bomb answer.txt

可以在执行文件指令后面加上一个文本文件,表示在当前目录下执行名为 bomb 的程序,并将 answer.txt 作为参数传递给该程序。answer.txt可以存入阶段性的答案,避免了每次引爆炸弹后都要重复输入以前的密码。

三、拆解炸弹

(1)phase_1

1.汇编代码

查看phase_1反汇编代码:

查看string_not_equal反汇编代码:

2.汇编分析
0x08048b53 <+3>:	movl   $0x804a1e4,0x4(%esp)

观察到程序传了一个神秘的立即数0x804a1e4进去,而且后面的strings_not_equal函数疑似涉及字符串的比较,所以尝试把这个地址的内容转换为/s类型查看一下:

x/s 0x804a1e4

 指令表示,在地址 0x804a1e4 处开始,以字符串的形式显示内存内容,直到遇到空字符或达到默认的字符串长度限制。

得到一个字符串:Brownie, you are doing a heck of a job.

3.测试运行

(2)phase_2

1.汇编代码

查看phase_2反汇编代码:

查看read_six_numbers反汇编代码:

2.汇编分析

分析如下:

0x08048b74 <+0>:	push   %ebx //将 %ebx 寄存器的值压入栈中。
0x08048b75 <+1>:	sub    $0x38,%esp //调整栈指针 %esp,使其向下移动 0x38(56)个字节,为局部变量和函数调用准备空间。
0x08048b78 <+4>:	lea    0x18(%esp),%eax //计算 %esp + 0x18 的地址,并将结果存储在 %eax 中
0x08048b7c <+8>:	mov    %eax,0x4(%esp) //将 %eax 的值(即上面计算的地址)存储在栈上,偏移量为 0x4(4)个字节。
0x08048b80 <+12>:	mov    0x40(%esp),%eax //从栈上偏移 0x40(64)个字节的位置读取一个值到 %eax 寄存器中。
0x08048b84 <+16>:	mov    %eax,(%esp) //将 %eax 的值存储在栈顶,准备作为 call 指令的参数。

0x08048b87 <+19>:	call   0x804924b <read_six_numbers> //调用函数 read_six_numbers(其地址在 0x804924b)
0x08048b8c <+24>:	cmpl   $0x0,0x18(%esp) //比较栈上偏移 0x18(24)个字节位置的值与 0x0。0x18(%esp)为数组的第一个值
0x08048b91 <+29>:	jns    0x8048b98 <phase_2+36> //如果上面的比较结果是非负的(即读取的值不是负数),则跳转到地址 0x8048b98。
0x08048b93 <+31>:	call   0x8049116 <explode_bomb> //如果上面的条件不满足(即读取了负数),则调用 explode_bomb 函数
0x08048b98 <+36>:	mov    $0x1,%ebx //将立即数 0x1(1)存储在 %ebx 寄存器中
//循环开始
0x08048b9d <+41>:	mov    %ebx,%eax
0x08048b9f <+43>:	add    0x14(%esp,%ebx,4),%eax //依次向数组后面读取数据
0x08048ba3 <+47>:	cmp    %eax,0x18(%esp,%ebx,4) //相减判断是否为0,由此判断是否相等
0x08048ba7 <+51>:	je     0x8048bae <phase_2+58> //如果上面的比较结果是相等的,则跳转到地址 0x8048bae
0x08048ba9 <+53>:	call   0x8049116 <explode_bomb> // 在循环中,比较栈上的某个值与另一个值,并可能调用 explode_bomb。
0x08048bae <+58>:	add    $0x1,%ebx //将 %ebx 的值增加 0x1
0x08048bb1 <+61>:	cmp    $0x6,%ebx //%ebx与6作比较,即只循环5次
0x08048bb4 <+64>:	jne    0x8048b9d <phase_2+41> //检查 %ebx 是否等于 0x6。如果不是,则跳转回循环的开始。
//循环结束
0x08048bb6 <+66>:	add    $0x38,%esp //调整栈指针 %esp,使其返回到之前的状态,即撤销之前 sub 
0x08048bb9 <+69>:	pop    %ebx //从栈中弹出一个值并存储在 %ebx 寄存器中,这是恢复之前保存的 %ebx 值
0x08048bba <+70>:	ret    //返回调用者,结束 phase_2 函数

显然,由每移位4个字节读取一个值,可以看出输入的为6个数同在一个数组中。

地址
0x18(%esp)x
0x1c(%esp)x+1
0x20(%esp)x+1+2
0x24(%esp)x+1+2+3
0x28(%esp)x+1+2+3+4
0x2c(%esp)x+1+2+3+4+5

伪代码如下:

void phase_2(int arr[6])
{
	if(a[0]<=0)
	{
		return;//bomb!
	}
	for(int i=1;i<6;i++)
	{
		if(a[i-1]+i!=a[i])
		{
			return;//bomb!
		}
	}
	cout<<"Congratulation!";
}
3.测试运行 

(3)phase_3

1.汇编代码

查看phase_3反汇编代码:

2.汇编分析
0x08048bbb <+0>:	sub    $0x2c,%esp //为本地变量或临时存储空间腾出空间
0x08048bbe <+3>:	lea    0x1c(%esp),%eax //输入的第二个数在寄存器0x1c(%esp)中
0x08048bc2 <+7>:	mov    %eax,0xc(%esp)
0x08048bc6 <+11>:	lea    0x18(%esp),%eax //输入的第一个数在寄存器0x18(%esp)中
0x08048bca <+15>:	mov    %eax,0x8(%esp)
0x08048bce <+19>:	movl   $0x804a403,0x4(%esp)
0x08048bd6 <+27>:	mov    0x30(%esp),%eax
0x08048bda <+31>:	mov    %eax,(%esp)
0x08048bdd <+34>:	call   0x8048870 <__isoc99_sscanf@plt> //调用函数 __isoc99_sscanf,该函数用于格式化输入
0x08048be2 <+39>:	cmp    $0x1,%eax //__isoc99_sscanf@plt的返回值 %eax 要大于1,即输入两个及以上的数
0x08048be5 <+42>:	jg     0x8048bec <phase_3+49> //如果寄存器 eax 中的值大于 0x1,则跳转到地址 0x8048bec,继续执行
0x08048be7 <+44>:	call   0x8049116 <explode_bomb>
0x08048bec <+49>:	cmpl   $0x7,0x18(%esp) //跳转表中有8个分支,即0~7
0x08048bf1 <+54>:	ja     0x8048c59 <phase_3+158> //若输入不是0~7则爆炸
0x08048bf3 <+56>:	mov    0x18(%esp),%eax
0x08048bf7 <+60>:	jmp    *0x804a240(,%eax,4) //跳转到地址为寄存器 eax 中值的四倍再加上偏移 0x804a240 的位置处执行
//.L1
0x08048bfe <+67>:	mov    $0x0,%eax
0x08048c03 <+72>:	jmp    0x8048c0a <phase_3+79>
//.L0
0x08048c05 <+74>:	mov    $0xb3,%eax //179
0x08048c0a <+79>:	sub    $0x292,%eax //179-658=-479 
0x08048c0f <+84>:	jmp    0x8048c16 <phase_3+91>
//.L2
0x08048c11 <+86>:	mov    $0x0,%eax
0x08048c16 <+91>:	add    $0x1f3,%eax //-479+499=20
0x08048c1b <+96>:	jmp    0x8048c22 <phase_3+103>
//.L3
0x08048c1d <+98>:	mov    $0x0,%eax
0x08048c22 <+103>:	sub    $0x293,%eax //20-659=-639
0x08048c27 <+108>:	jmp    0x8048c2e <phase_3+115>
//.L4
0x08048c29 <+110>:	mov    $0x0,%eax
0x08048c2e <+115>:	add    $0x293,%eax //-639+659=20
0x08048c33 <+120>:	jmp    0x8048c3a <phase_3+127>
//.L5
0x08048c35 <+122>:	mov    $0x0,%eax
0x08048c3a <+127>:	sub    $0x293,%eax //20-659=-639
0x08048c3f <+132>:	jmp    0x8048c46 <phase_3+139>
//.L6
0x08048c41 <+134>:	mov    $0x0,%eax
0x08048c46 <+139>:	add    $0x293,%eax //-639+659=20
0x08048c4b <+144>:	jmp    0x8048c52 <phase_3+151>
//.L7
0x08048c4d <+146>:	mov    $0x0,%eax
0x08048c52 <+151>:	sub    $0x293,%eax //20-659=-639
0x08048c57 <+156>:	jmp    0x8048c63 <phase_3+168>
0x08048c59 <+158>:	call   0x8049116 <explode_bomb>
0x08048c5e <+163>:	mov    $0x0,%eax
0x08048c63 <+168>:	cmpl   $0x5,0x18(%esp) //输入的第一个数要≤5
0x08048c68 <+173>:	jg     0x8048c70 <phase_3+181>
0x08048c6a <+175>:	cmp    0x1c(%esp),%eax  比较栈指针偏移 0x1c(28) 处的值与寄存器 eax 中的值
0x08048c6e <+179>:	je     0x8048c75 <phase_3+186> //如果相等,则跳转到地址 0x8048c75,继续执行
0x08048c70 <+181>:	call   0x8049116 <explode_bomb>
0x08048c75 <+186>:	add    $0x2c,%esp  //清理栈空间
0x08048c78 <+189>:	ret    //返回

//跳转表如下:
0x804a240:	0x08048c05	0x08048bfe	0x08048c11	0x08048c1d
0x804a250:	0x08048c29	0x08048c35	0x08048c41	0x08048c4d

观察汇编代码前几行,注意到代码输入了一个神秘的立即数$0x804a403到寄存器中

0x08048bce <+19>:	movl   $0x804a403,0x4(%esp)

gdb查看一下: 

可以确定,输入为两个整数。

而且,两个输入数字的放置位置为前四行lea指令的第一个操作数的位置。

继续分析,可以看到汇编代码中有大量的jmp命令,可以猜测这是一个跳转表,又看到这个间接跳转符号*

0x08048bec <+49>:	cmpl   $0x7,0x18(%esp) //跳转表中有8个分支,即0~7
0x08048bf1 <+54>:	ja     0x8048c59 <phase_3+158> //若输入不是0~7则爆炸
0x08048bf3 <+56>:	mov    0x18(%esp),%eax
0x08048bf7 <+60>:	jmp    *0x804a240(,%eax,4) //跳转到地址为寄存器 eax 中值的四倍再加上偏移 0x804a240 的位置处执行

可以确定输入的第一个数范围为0~7(因为ja表示无符号大于,所以负数和大于7的数都跳转到bomb中了) 

gdb查看跳转表地址: 


跟着跳转表一路往下走,可以发现,这些case里面没有break语句,switch里面的default就是爆炸。

伪代码如下:

void phase_3(int a, int b)
{
    int temp;
    if(a == 0)
    {
        temp = 179;
    }
    else temp = 0;
    switch(a){
        case 1:
        case 0:
            temp -= 0x292;
        case 2:
            temp += 0x1f3;
        case 3:
            temp -= 0x293;
        case 4:
            temp += 0x293;
        case 5:
            temp -= 0x293;
        case 6:
            temp += 0x293;
        case 7:
            temp -= 0x293;
        default:
            return;//bomb!
    }
    if(a>5)
    {
        return;//bomb!
    }
    if(b!=temp)
    {
        return;//bomb!
    }
    cout<<"Congratulation!";
}

所以第三个炸弹的解系为:

第一个数第二个数

0

-639
1-818
2-160
3-659
40
5-659

又因为存在下面的代码,最后又对输入的第一个数进行一次判断:

0x08048c63 <+168>:	cmpl   $0x5,0x18(%esp) //输入的第一个数要≤5
0x08048c68 <+173>:	jg     0x8048c70 <phase_3+181>
...

0x08048c70 <+181>:	call   0x8049116 <explode_bomb>

所以输入的第一个数要小于等于5,于是有两组解被过河拆桥掉了。 

被过河拆桥的两组解系为:

第一个数第二个数
60
7-659
3.测试运行

 这里有一个小bug,因为在汇编代码中,下面指令:

0x08048bdd <+34>:	call   0x8048870 <__isoc99_sscanf@plt> //调用函数 __isoc99_sscanf,该函数用于格式化输入
0x08048be2 <+39>:	cmp    $0x1,%eax //__isoc99_sscanf@plt的返回值 %eax 要大于1,即输入两个及以上的数
0x08048be5 <+42>:	jg     0x8048bec <phase_3+49> //如果寄存器 eax 中的值大于 0x1,则跳转到地址 0x8048bec,继续执行

只判断了输入是否大于1,但是没有默认要求输入是两条指令,所以如果我们输入:

0 -639 1 1 1 1 

也会提示正确! (所以可以增加一个再判断小于等于2的命令,但源程序没有给出这样的操作,我也不是很会修改汇编代码,这里就顺带提一嘴hhh)

(4)phase_4

1.汇编代码

查看phase_4反汇编代码:

查看func4反汇编代码: 

 

2.汇编分析
phase_4()函数

分析如下:

0x08048ce6 <+0>:	sub    $0x2c,%esp
0x08048ce9 <+3>:	lea    0x1c(%esp),%eax //输入的第二个数在寄存器0x1c(%esp)中
0x08048ced <+7>:	mov    %eax,0xc(%esp)
0x08048cf1 <+11>:	lea    0x18(%esp),%eax //输入的第一个数在寄存器0x18(%esp)中
0x08048cf5 <+15>:	mov    %eax,0x8(%esp)
0x08048cf9 <+19>:	movl   $0x804a403,0x4(%esp) //输入为两个int型整数
0x08048d01 <+27>:	mov    0x30(%esp),%eax
0x08048d05 <+31>:	mov    %eax,(%esp)
0x08048d08 <+34>:	call   0x8048870 <__isoc99_sscanf@plt> //调用函数 __isoc99_sscanf,该函数用于格式化输入
0x08048d0d <+39>:	cmp    $0x2,%eax
0x08048d10 <+42>:	jne    0x8048d1f <phase_4+57> //若输入数量不为2,直接跳转到0x8048d1f,引爆炸弹
0x08048d12 <+44>:	mov    0x18(%esp),%eax //把输入的第一个数放入寄存器 %eax 中
0x08048d16 <+48>:	test   %eax,%eax //对寄存器 %eax 执行逻辑与操作,并设置相应的标志位
0x08048d18 <+50>:	js     0x8048d1f <phase_4+57> //若输入的第一个数为负数,则跳转到0x8048d1f,引爆炸弹。所以输入的第一个数要≥0
0x08048d1a <+52>:	cmp    $0xe,%eax
0x08048d1d <+55>:	jle    0x8048d24 <phase_4+62> //若第一个数小于或等于 0xe(14),则继续执行。否则爆炸。至此,第一个数范围为[0,14]
0x08048d1f <+57>:	call   0x8049116 <explode_bomb>
0x08048d24 <+62>:	movl   $0xe,0x8(%esp) //将立即数 0xe(14) 存储到esp+0x8的地址中
0x08048d2c <+70>:	movl   $0x0,0x4(%esp) //将立即数 0x0(0) 存储到esp+0x4的地址中
0x08048d34 <+78>:	mov    0x18(%esp),%eax
0x08048d38 <+82>:	mov    %eax,(%esp) //将 %eax(输入的第一个数)的值存储到栈顶,即 %esp 指向的地址
//调用func4函数
0x08048d3b <+85>:	call   0x8048c79 <func4>
//func4函数调用结束
0x08048d40 <+90>:	cmp    $0x3,%eax //比较寄存器eax的值与立即数0x3
0x08048d43 <+93>:	jne    0x8048d4c <phase_4+102> //%eax 中的值不为3就爆炸
0x08048d45 <+95>:	cmpl   $0x3,0x1c(%esp)
0x08048d4a <+100>:	je     0x8048d51 <phase_4+107> //输入的第二个数等于3则跳转到0x8048d51继续执行,否则爆炸
0x08048d4c <+102>:	call   0x8049116 <explode_bomb>
0x08048d51 <+107>:	add    $0x2c,%esp  //清理栈空间
0x08048d54 <+110>:	ret    

观察phase_1函数汇编代码前几行,又一次观察到代码输入了一个神秘的立即数$0x804a403到寄存器中(同炸弹三)

可以确定,输入为两个整数。

而且,两个输入数字的放置位置为前四行lea指令的第一个操作数的位置,即0x1c(%esp)和0x18(%esp)

下面代码再次佐证了,输入参数数量为2:

0x08048d0d <+39>:	cmp    $0x2,%eax
0x08048d10 <+42>:	jne    0x8048d1f <phase_4+57> //若输入数量不为2,直接跳转到0x8048d1f,引爆炸弹

由下面的代码可知,输入的第一个参数num1范围应为[0,14],否则引爆炸弹:

0x08048d16 <+48>:	test   %eax,%eax //对寄存器 %eax 执行逻辑与操作,并设置相应的标志位
0x08048d18 <+50>:	js     0x8048d1f <phase_4+57> //若输入的第一个数为负数,则跳转到0x8048d1f,引爆炸弹。所以输入的第一个数要≥0
0x08048d1a <+52>:	cmp    $0xe,%eax
0x08048d1d <+55>:	jle    0x8048d24 <phase_4+62> //若第一个数小于或等于 0xe(14),则继续执行。否则爆炸。至此,第一个数范围为[0,14]

通过下面指令,可以得知,<func4>函数传入了三个参数0x8(%esp)、0x4(%esp)、(%esp),值分别为14、0、输入的第一个数num1:

0x08048d24 <+62>:	movl   $0xe,0x8(%esp) //将立即数 0xe(14) 存储到esp+0x8的地址中
0x08048d2c <+70>:	movl   $0x0,0x4(%esp) //将立即数 0x0(0) 存储到esp+0x4的地址中
0x08048d34 <+78>:	mov    0x18(%esp),%eax
0x08048d38 <+82>:	mov    %eax,(%esp) //将 %eax(输入的第一个数)的值存储到栈顶,即 %esp 指向的地址

先不着急看func函数,先看一下func函数结束后的返回值:

0x08048d40 <+90>:	cmp    $0x3,%eax //比较寄存器eax的值与立即数0x3
0x08048d43 <+93>:	jne    0x8048d4c <phase_4+102> //%eax 中的值不为3就爆炸

 由上述代码可知,返回值保存在%eax寄存器中,且返回值应该为3,若不为3则爆炸

进一步观察,

0x08048d45 <+95>:	cmpl   $0x3,0x1c(%esp)
0x08048d4a <+100>:	je     0x8048d51 <phase_4+107> //输入的第二个数等于3则跳转到0x8048d51继续执行,否则爆炸

由此判断输入的第二个数字值一定为3,否则还是爆炸。

func4函数()

分析如下:

0x08048c79 <+0>:	sub    $0x1c,%esp //将栈指针esp减去0x1c(28),为局部变量和可能的函数调用预留空间。
0x08048c7c <+3>:	mov    %ebx,0x14(%esp) //保存上一层递归函数中 %esi 寄存器的值
0x08048c80 <+7>:	mov    %esi,0x18(%esp) //保存上一层递归函数中 %esi 寄存器的值
0x08048c84 <+11>:	mov    0x20(%esp),%edx //将esp+0x20(即esp向上偏移32个字节)的地址中的值加载到寄存器edx中,即输入的第一个数num1
0x08048c88 <+15>:	mov    0x24(%esp),%eax //将esp+0x24(即esp向上偏移36个字节)的地址中的值加载到寄存器eax中,即0
0x08048c8c <+19>:	mov    0x28(%esp),%ebx //将esp+0x28(即esp向上偏移40个字节)的地址中的值加载到寄存器ebx中,即14
0x08048c90 <+23>:	mov    %ebx,%ecx //将寄存器 ebx 的值复制到寄存器 ecx 中,即 0x28(%esp) 存储到 ecx 中
0x08048c92 <+25>:	sub    %eax,%ecx //从寄存器 ecx 中减去寄存器eax的值,即 0x28(%esp)-0x24(%esp)
0x08048c94 <+27>:	mov    %ecx,%esi //将寄存器ecx的值复制到寄存器esi中,即 0x28(%esp)-0x24(%esp)
0x08048c96 <+29>:	shr    $0x1f,%esi //将寄存器esi的逻辑右移31位,通常用于实现符号扩展
0x08048c99 <+32>:	add    %esi,%ecx //将寄存器esi的值加到寄存器ecx上,即 0x28(%esp)-0x24(%esp)+自己的最高位
0x08048c9b <+34>:	sar    %ecx //对寄存器ecx进行算术右移1位,保持符号位不变
0x08048c9d <+36>:	add    %eax,%ecx //再次将寄存器eax的值加到寄存器ecx上,即 0x24(%esp) 值加上 ecx
0x08048c9f <+38>:	cmp    %edx,%ecx //比较寄存器edx(0x20(%esp))和ecx的值
0x08048ca1 <+40>:	jle    0x8048cba <func4+65> //如果ecx小于或等于edx,则跳转到地址0x8048cba

0x08048ca3 <+42>:	sub    $0x1,%ecx //从寄存器ecx中减去1
0x08048ca6 <+45>:	mov    %ecx,0x8(%esp) //保存参数,便于递归调用
0x08048caa <+49>:	mov    %eax,0x4(%esp) //保存参数,便于递归调用
0x08048cae <+53>:	mov    %edx,(%esp) //保存参数,便于递归调用
0x08048cb1 <+56>:	call   0x8048c79 <func4> //递归调用func4函数
0x08048cb6 <+61>:	add    %eax,%eax
0x08048cb8 <+63>:	jmp    0x8048cda <func4+97>

0x08048cba <+65>:	mov    $0x0,%eax
0x08048cbf <+70>:	cmp    %edx,%ecx
0x08048cc1 <+72>:	jge    0x8048cda <func4+97> //如果ecx大于或等于edx,则跳转到地址0x8048cda

0x08048cc3 <+74>:	mov    %ebx,0x8(%esp) //保存参数,便于递归调用
0x08048cc7 <+78>:	add    $0x1,%ecx
0x08048cca <+81>:	mov    %ecx,0x4(%esp) //保存参数,便于递归调用
0x08048cce <+85>:	mov    %edx,(%esp) //保存参数,便于递归调用
0x08048cd1 <+88>:	call   0x8048c79 <func4>
0x08048cd6 <+93>:	lea    0x1(%eax,%eax,1),%eax
0x08048cda <+97>:	mov    0x14(%esp),%ebx //恢复上一层递归函数中 %ebx 寄存器的值
0x08048cde <+101>:	mov    0x18(%esp),%esi//恢复上一层递归函数中 %esi 寄存器的值
0x08048ce2 <+105>:	add    $0x1c,%esp
0x08048ce5 <+108>:	ret    

 首先,第一行代码为func函数建立了栈帧:

0x08048c79 <+0>:	sub    $0x1c,%esp //将栈指针esp减去0x1c(28),为局部变量和可能的函数调用预留空间。

但是这里需要注意,%esp距离调用前的esp位置并不是相差0x1c,而是要加上4,因为上面还保存了函数的返回地址。 

下面的代码载入了传入的三个参数:

0x08048c84 <+11>:	mov    0x20(%esp),%edx //将esp+0x20(即esp向上偏移32个字节)的地址中的值加载到寄存器edx中,即输入的第一个数num1
0x08048c88 <+15>:	mov    0x24(%esp),%eax //将esp+0x24(即esp向上偏移36个字节)的地址中的值加载到寄存器eax中,即0
0x08048c8c <+19>:	mov    0x28(%esp),%ebx //将esp+0x28(即esp向上偏移40个字节)的地址中的值加载到寄存器ebx中,即14

之前%esp保存返回地址减去了0x4(即4),建立栈帧又减去了0x1c(即28),所以需要mov 0x20(%esp)、0x24(%esp)、0x28(%esp),才能恰好读取到原先phase_4函数中传入的(%esp)、0x4(%esp)、0x8(%esp)三个参数。

C语言代码如下(这次不是伪代码了):

#include<bits/stdc++.h>
using namespace std;
int func4(int a, int b, int c)
{
	int edx, eax, ebx, ecx, esi;
	edx = a;
	eax = b;
	ebx = c;
	ecx = ebx - eax;
	esi = 0;
	ecx += esi;
	ecx /= 2;
	ecx += eax;
	if(ecx > edx)
	{
		ecx--;
		eax = func4(edx, eax, ecx);
		eax *= 2;
	}
	else
	{
		eax = 0;
		if(ecx < edx)
		{
			ecx++;
			eax = func4(edx, ecx, ebx);
			eax = 2 * eax + 1;
		}
	}
	return eax;
}
int main()
{
	for(int i = 0;i <= 14;i++)
	{
		int ans = func4(i, 0, 14);
		if(ans == 3)
		{
			cout<<"第一个参数为:"<<i<<",第二个参数为:"<<3<<endl;
		}
	}
	return 0;
}

 所以第四个炸弹的解系为:

第一个数第二个数
123
133
3.测试运行

(5)phase_5

1.汇编代码

查看phase_5反汇编代码:

2.汇编分析
0x08048d55 <+0>:	sub    $0x2c,%esp
0x08048d58 <+3>:	lea    0x1c(%esp),%eax //输入的第二个数在寄存器0x1c(%esp)中
0x08048d5c <+7>:	mov    %eax,0xc(%esp)
0x08048d60 <+11>:	lea    0x18(%esp),%eax //输入的第一个数在寄存器0x18(%esp)中
0x08048d64 <+15>:	mov    %eax,0x8(%esp)
0x08048d68 <+19>:	movl   $0x804a403,0x4(%esp) //输入为两个int型整数
0x08048d70 <+27>:	mov    0x30(%esp),%eax
0x08048d74 <+31>:	mov    %eax,(%esp)
0x08048d77 <+34>:	call   0x8048870 <__isoc99_sscanf@plt> //调用函数 __isoc99_sscanf,该函数用于格式化输入
0x08048d7c <+39>:	cmp    $0x1,%eax
0x08048d7f <+42>:	jg     0x8048d86 <phase_5+49> //若输入数量大于1,则跳过炸弹
0x08048d81 <+44>:	call   0x8049116 <explode_bomb>
0x08048d86 <+49>:	mov    0x18(%esp),%eax //将输入的第一个数存入寄存器 %eax 中
0x08048d8a <+53>:	and    $0xf,%eax //将 %eax 寄存器的值与0xf进行按位与操作,保留低4位
0x08048d8d <+56>:	mov    %eax,0x18(%esp)
0x08048d91 <+60>:	cmp    $0xf,%eax
0x08048d94 <+63>:	je     0x8048dc0 <phase_5+107> //若 %eax 值等于0xf,则炸弹爆炸
//准备工作,寄存器初始化
0x08048d96 <+65>:	mov    $0x0,%ecx
0x08048d9b <+70>:	mov    $0x0,%edx
//循环开始
0x08048da0 <+75>:	add    $0x1,%edx
0x08048da3 <+78>:	mov    0x804a260(,%eax,4),%eax //根据%eax的值作为索引,从地址0x804a260开始,以4字节为单位进行偏移,并将偏移后的地址中的值加载到eax寄存器中
0x08048daa <+85>:	add    %eax,%ecx //此时 %ecx 值为0x804a260(,%eax,4)
0x08048dac <+87>:	cmp    $0xf,%eax
0x08048daf <+90>:	jne    0x8048da0 <phase_5+75> //如果 %eax 不等于0xf,则跳转到地址0x8048da0,即回到循环的开始处
//循环结束
0x08048db1 <+92>:	mov    %eax,0x18(%esp) //这里 %eax 值已经为0xf了
0x08048db5 <+96>:	cmp    $0xf,%edx
0x08048db8 <+99>:	jne    0x8048dc0 <phase_5+107> //如果 %edx 不等于0xf,则跳转到地址0x8048dc0,即爆炸。也就是说得循环15次
0x08048dba <+101>:	cmp    0x1c(%esp),%ecx //比较第二个数和寄存器 %ecx
0x08048dbe <+105>:	je     0x8048dc5 <phase_5+112> //如果两者相等,则跳转到地址0x8048dc5。否则爆炸
0x08048dc0 <+107>:	call   0x8049116 <explode_bomb>
0x08048dc5 <+112>:	add    $0x2c,%esp
0x08048dc8 <+115>:	ret    

观察phase_1函数汇编代码前几行,再一次观察到代码输入了这个神秘的立即数$0x804a403到寄存器中

可以确定,输入为两个整数。

0x08048d86 <+49>:	mov    0x18(%esp),%eax //将输入的第一个数存入寄存器 %eax 中
0x08048d8a <+53>:	and    $0xf,%eax //将 %eax 寄存器的值与0xf进行按位与操作,保留低4位

因为需要进行后续判断的只有%eax的值的后四位二进制数,所以可以推测,这题第一个数是一组解系而非一个解。 

0x08048da3 <+78>:	mov    0x804a260(,%eax,4),%eax //根据%eax的值作为索引,从地址0x804a260开始,以4字节为单位进行偏移,并将偏移后的地址中的值加载到eax寄存器中

注意到这里需要根据0x804a260这个地址和%eax寄存器进行索引,可以通过x/128bx 0x804a260命令查看0x804a260后面128字节的值:

这里我们需要寻找0x0f的位置。可以观察到,0x804a278地址处的值为5

由以下代码知,总计需要循环15次:

0x08048db8 <+99>:	jne    0x8048dc0 <phase_5+107> //如果 %edx 不等于0xf,则跳转到地址0x8048dc0,即爆炸。也就是说得循环15次

逆向工程求解:

循环次数mov后的%eax寄存器值
1515
146
1314
122
111
1010
90
88
74
69
513
411
37
23
112
0(输入的第一个数字)5

 逆向推理,直到得到输入的第一个数的后四位编码。最后得出结果为5,即0101。所以输入的第一个数为16n+5,n∈Z

0x08048dba <+101>:	cmp    0x1c(%esp),%ecx //比较第二个数和寄存器 %ecx
0x08048dbe <+105>:	je     0x8048dc5 <phase_5+112> //如果两者相等,则跳转到地址0x8048dc5。否则爆炸

由以上代码知,输入的第二个数值需要和%ecx相等,%ecx即15次循环中%eax之和,即15+6+14+2+1+10+0+8+4+9+13+11+7+3+12=115

伪代码如下:

void phase_5(int a, int b, int arr[60])
{
    a = a & 0xf;
    if(a == 0xf)
    {
        return;//bomb!
    }
    int i, j = 0, sum = 0;
    do{
        j ++;
        a = *(arr + 4 * a);
        sum += a;
    } while (a != 0xf);
    if(j != 0xf || b != sum)
    {
        return;//bomb!
    }
    cout<<"Congratulation!";
}
3.测试运行

(6)phase_6

1.汇编代码

查看phase_6反汇编代码:

2.汇编分析 
//准备工作
0x08048dc9 <+0>: 	push   %esi //将 %esi 寄存器的值压入栈中
0x08048dca <+1>:	push   %ebx //将 %ebx 寄存器的值压入栈中
0x08048dcb <+2>:	sub    $0x44,%esp //将栈指针%esp减去0x44(68),为局部变量预留空间
0x08048dce <+5>:	lea    0x10(%esp),%eax //计算%esp+0x10的地址,并放入%eax寄存器。即输入的首个数字
0x08048dd2 <+9>:	mov    %eax,0x4(%esp)
0x08048dd6 <+13>:	mov    0x50(%esp),%eax
0x08048dda <+17>:	mov    %eax,(%esp)
0x08048ddd <+20>:	call   0x804924b <read_six_numbers> //输入6个数字
0x08048de2 <+25>:	mov    $0x0,%esi // %esi = 0
//=========================================================================================================
//【第一阶段】
//两重for循环,保证了数组元素∈[1,6]且两两之间不重复
//第一重for循环
0x08048de7 <+30>:	mov    0x10(%esp,%esi,4),%eax // %eax = 16 + %esp + 4 * %esi
0x08048deb <+34>:	sub    $0x1,%eax // %eax --
0x08048dee <+37>:	cmp    $0x5,%eax
0x08048df1 <+40>:	jbe    0x8048df8 <phase_6+47> //如果 %eax 小于或等于5,则跳转到phase_6+47,否则爆炸。也就是说(16 + %esp + 4 * %esi)∈[1,6]
0x08048df3 <+42>:	call   0x8049116 <explode_bomb>
0x08048df8 <+47>:	add    $0x1,%esi // %esi ++
0x08048dfb <+50>:	cmp    $0x6,%esi 
0x08048dfe <+53>:	je     0x8048e33 <phase_6+106> //如果%esi等于6,则跳转到phase_6+106
0x08048e00 <+55>:	mov    %esi,%ebx
    //第二重for循环
    0x08048e02 <+57>:	mov    0x10(%esp,%ebx,4),%eax // %eax = 16 + %esp + 4 * %ebx
    0x08048e06 <+61>:	cmp    %eax,0xc(%esp,%esi,4) //比较(%esp + 4 * %esi + 12)和 %eax
    0x08048e0a <+65>:	jne    0x8048e11 <phase_6+72> //若不相等则跳转到phase_6+72,否则爆炸
    0x08048e0c <+67>:	call   0x8049116 <explode_bomb>
    0x08048e11 <+72>:	add    $0x1,%ebx // %ebx ++
    0x08048e14 <+75>:	cmp    $0x5,%ebx
    0x08048e17 <+78>:	jle    0x8048e02 <phase_6+57> //如果%ebx小于或等于5,则跳转到phase_6+57。
    0x08048e19 <+80>:	jmp    0x8048de7 <phase_6+30> //否则,跳转到phase_6+30
//=========================================================================================================
//【第二阶段】
//将链表各个节点的地址保存在新开辟出的空间内
0x08048e1b <+82>:	mov    0x8(%edx),%edx // %edx为链表下一个节点的地址 p->next->address
0x08048e1e <+85>:	add    $0x1,%eax  // %eax ++
0x08048e21 <+88>:	cmp    %ecx,%eax
0x08048e23 <+90>:	jne    0x8048e1b <phase_6+82> //如果它们不相等,则跳转到phase_6+82

0x08048e25 <+92>:	mov    %edx,0x28(%esp,%esi,4) //将链表各个节点的地址保存在0x28(%esp,%esi,4)这个新开辟的空间内
0x08048e29 <+96>:	add    $0x1,%ebx  // %ebx ++
0x08048e2c <+99>:	cmp    $0x6,%ebx
0x08048e2f <+102>:	jne    0x8048e38 <phase_6+111> //如果%ebx不等于6,则跳转到phase_6+111
0x08048e31 <+104>:	jmp    0x8048e4f <phase_6+134> //否则,跳转到phase_6+134,进入第三阶段
//第二阶段入口处:
0x08048e33 <+106>:	mov    $0x0,%ebx //将 %ebx 寄存器的值设置为0

0x08048e38 <+111>:	mov    %ebx,%esi
0x08048e3a <+113>:	mov    0x10(%esp,%ebx,4),%ecx // %ecx = 16 + %esp + 4 * %ebx
0x08048e3e <+117>:	mov    $0x1,%eax // %eax = 1
0x08048e43 <+122>:	mov    $0x804c13c,%edx // %edx 存储链表节点相关信息
0x08048e48 <+127>:	cmp    $0x1,%ecx
0x08048e4b <+130>:	jg     0x8048e1b <phase_6+82> //如果%ecx大于1,则跳转到phase_6+82
0x08048e4d <+132>:	jmp    0x8048e25 <phase_6+92> //否则,跳转到phase_6+92

//【第三阶段】
0x08048e4f <+134>:	mov    0x28(%esp),%ebx //保存第一个节点的地址
0x08048e53 <+138>:	mov    0x2c(%esp),%eax
0x08048e57 <+142>:	mov    %eax,0x8(%ebx) //保存第二个节点的地址
0x08048e5a <+145>:	mov    0x30(%esp),%edx
0x08048e5e <+149>:	mov    %edx,0x8(%eax) //保存第三个节点的地址
0x08048e61 <+152>:	mov    0x34(%esp),%eax
0x08048e65 <+156>:	mov    %eax,0x8(%edx) //保存第四个节点的地址
0x08048e68 <+159>:	mov    0x38(%esp),%edx
0x08048e6c <+163>:	mov    %edx,0x8(%eax) //保存第五个节点的地址
0x08048e6f <+166>:	mov    0x3c(%esp),%eax
0x08048e73 <+170>:	mov    %eax,0x8(%edx) //保存第六个节点的地址
0x08048e76 <+173>:	movl   $0x0,0x8(%eax)

//【第四阶段】
0x08048e7d <+180>:	mov    $0x5,%esi

0x08048e82 <+185>:	mov    0x8(%ebx),%eax //将第二个(后一个)节点地址存入 %eax 寄存器中
0x08048e85 <+188>:	mov    (%eax),%edx //将第二个(后一个)节点值存入 %edx 中
0x08048e87 <+190>:	cmp    %edx,(%ebx) //比较第二个(后一个)节点和第一个(前一个)节点值大小
0x08048e89 <+192>:	jle    0x8048e90 <phase_6+199> //(%ebx)小于等于%edx时,跳转到phase_6+199,否则爆炸。即升序排序
0x08048e8b <+194>:	call   0x8049116 <explode_bomb>
0x08048e90 <+199>:	mov    0x8(%ebx),%ebx
0x08048e93 <+202>:	sub    $0x1,%esi //计数器 %esi --
0x08048e96 <+205>:	jne    0x8048e82 <phase_6+185> //若 %esi 不等于1则跳转回phase_6+185

//收尾工作
0x08048e98 <+207>:	add    $0x44,%esp //清理栈上的局部变量

0x08048e9b <+210>:	pop    %ebx
0x08048e9c <+211>:	pop    %esi
0x08048e9d <+212>:	ret    
①第一阶段

先整体观察一遍,又看到了read_six_numbers这个熟悉的函数:

0x08048ddd <+20>:	call   0x804924b <read_six_numbers> //输入6个数字

所以输入的数字个数为6 

0x08048de7 <+30>:	mov    0x10(%esp,%esi,4),%eax // %eax = 16 + %esp + 4 * %esi
0x08048deb <+34>:	sub    $0x1,%eax // %eax --
0x08048dee <+37>:	cmp    $0x5,%eax
0x08048df1 <+40>:	jbe    0x8048df8 <phase_6+47> //如果 %eax 小于或等于5,则跳转到phase_6+47,否则爆炸。也就是说(16 + %esp + 4 * %esi)∈[1,6]

根据以上代码可以得知,输入的6个数字范围为[1,6],否则会发生爆炸

    0x08048e02 <+57>:	mov    0x10(%esp,%ebx,4),%eax // %eax = 16 + %esp + 4 * %ebx
    0x08048e06 <+61>:	cmp    %eax,0xc(%esp,%esi,4) //比较(%esp + 4 * %esi + 12)和 %eax
    0x08048e0a <+65>:	jne    0x8048e11 <phase_6+72> //若不相等则跳转到phase_6+72,否则爆炸

根据以上代码发现,程序使用了两重for循环,对输入的6个数字进行检查,每一个数字都不能与他后面的任何一个数字相等,即保证了6个数字互不相等 

②第二阶段

还是先观察代码,再一次看到代码片段中传入了一个立即数进去,gdb调试查看:

x/32bx 0x804c13c

观察到这个地址的数据结构为链表 ,进一步观察,输入以下指令,调试查看:

p/x *0x804c13c@32

发现他们在结构上以三个为一组,所以推测这个链表可能包含三部分内容,如下图所示:

0x08048e1b <+82>:	mov    0x8(%edx),%edx // %edx为链表下一个节点的地址 p->next->address

其中,这行代码表示进入链表的下一节点,即p = p -> next

因为输入的6个数字范围为1~6,0x804c13链表地址处的标号也为1~6,所以代码进行了一个匹配操作,确保每个数字都能和链表的节点一一对应

0x08048e25 <+92>:	mov    %edx,0x28(%esp,%esi,4) //将链表各个节点的地址保存在0x28(%esp,%esi,4)这个新开辟的空间内

第二阶段的关键操作在于以上代码,将每个节点的地址都保存在了一块新开辟的空间内 

③第三阶段

第三阶段保存了6个节点的地址,其中第一个节点和第二个节点的地址是最关键的,因为后续所有地址都能用节点1和节点2递推得到

④第四阶段

最后一个阶段的精髓在于排序

0x08048e82 <+185>:	mov    0x8(%ebx),%eax //将第二个(后一个)节点地址存入 %eax 寄存器中
0x08048e85 <+188>:	mov    (%eax),%edx //将第二个(后一个)节点值存入 %edx 中
0x08048e87 <+190>:	cmp    %edx,(%ebx) //比较第二个(后一个)节点和第一个(前一个)节点值大小
0x08048e89 <+192>:	jle    0x8048e90 <phase_6+199> //(%ebx)小于等于%edx时,跳转到phase_6+199,否则爆炸。即升序排序

以上代码保证了后一个节点的data值一定大于等于前一个节点的data值,这就保证了整个链表的data值呈现升序排列

根据链表各部分data值:

{0x276, 0x1, 0x804c148, 0x8d, 0x2, 0x804c154, 0xd7, 0x3, 0x804c160, 0x128, 0x4, 0x804c16c, 0x3b3, 0x5, 0x804c178, 0x231, 0x6, 0x0}

排序得到的结果为:2 3 4 6 1 5

伪代码:

typedef struct LNode{
    int data;
    int no;
    struct LNode *next;
}LNode, *LinkList;
void phase_6(int arr[6], LNode temp[6], LinkList L)
{
    //第一阶段
    //两重for循环,保证了数组元素∈[1,6]且两两之间不重复
    for(int i = 0; i < 6; i ++)
    {
        if(arr[i] > 6 || arr[i] < 1)
        {
            return;//bomb!
        }
        for(int j = i + 1; j < 6; j ++)
        {
            if(arr[i] == arr[j])
            {
                return;//bomb!
            }
        }
    }
    //第二阶段
    for(int i = 0; i < 6; i ++)
    {
        LNode *p = L;
        if(arr[i] > 1)
        {
            for(int j = 1; i <= arr[i]; j ++)
            {
                p = p -> next;
            }
            temp[i] = p;
        }
    }
    //第四阶段
    for(int i = 1; i < 6; i++)
    {
        if(temp[i] -> data < temp[i - 1] -> data)
        {
            return;//bomb!
        }
    }
    cout<<"Congratulation!";
}
3.测试运行 

 (7)phase_defused

“But isn’t something… missing? Perhaps something they overlooked? Mua ha ha ha ha! ”

但是是不是有什么东西… 缺失了? 也许是他们忽略了什么!*得意地怪笑

观察bomb.c文件,发现最后还遗留了一个phase_defused()函数和上面这句奇怪的话。

1.汇编代码

对phase_defused函数进行反汇编,

2.汇编分析 

分析如下:

0x0804929b <+0>:	sub    $0x8c,%esp //减少栈指针%esp的值,为即将进行的函数调用和局部变量预留空间
0x080492a1 <+6>:	mov    %gs:0x14,%eax //将%gs:0x14(一个特殊的段寄存器与偏移的组合,通常用于TLS(线程本地存储))的值移动到%eax寄存器
0x080492a7 <+12>:	mov    %eax,0x7c(%esp)
0x080492ab <+16>:	xor    %eax,%eax //将%eax寄存器的值清零,这是一种常见的初始化或清除寄存器的方法
0x080492ad <+18>:	cmpl   $0x6,0x804c3cc //比较内存地址0x804c3cc处的值与0x6,也就是检测是否拆完了前六个炸弹
0x080492b4 <+25>:	jne    0x8049328 <phase_defused+141> //如果上面的比较不相等,则跳转到地址0x8049328。
0x080492b6 <+27>:	lea    0x2c(%esp),%eax //第三个输入
0x080492ba <+31>:	mov    %eax,0x10(%esp)
0x080492be <+35>:	lea    0x28(%esp),%eax //第二个输入
0x080492c2 <+39>:	mov    %eax,0xc(%esp)
0x080492c6 <+43>:	lea    0x24(%esp),%eax //第一个输入
0x080492ca <+47>:	mov    %eax,0x8(%esp)
0x080492ce <+51>:	movl   $0x804a409,0x4(%esp) //查看这个地址中存放的内容,发现为"%d %d %s"
0x080492d6 <+59>:	movl   $0x804c4d0,(%esp) 查看这个地址中存放的内容,发现为"12 3"
0x080492dd <+66>:	call   0x8048870 <__isoc99_sscanf@plt> //调用__isoc99_sscanf函数,该函数用于从字符串中读取格式化的输入
0x080492e2 <+71>:	cmp    $0x3,%eax
0x080492e5 <+74>:	jne    0x804931c <phase_defused+129> //如果输入参数个数不等于3,则跳转到0x804931c
0x080492e7 <+76>:	movl   $0x804a412,0x4(%esp) //"DrEvil"
0x080492ef <+84>:	lea    0x2c(%esp),%eax
0x080492f3 <+88>:	mov    %eax,(%esp)
0x080492f6 <+91>:	call   0x8049004 <strings_not_equal>  //判断字符串是否等于"DrEvil"
0x080492fb <+96>:	test   %eax,%eax
0x080492fd <+98>:	jne    0x804931c <phase_defused+129> //若不等于0则跳转到0x804931c
0x080492ff <+100>:	movl   $0x804a2d8,(%esp) //"Curses, you've found the secret phase!
0x08049306 <+107>:	call   0x8048800 <puts@plt> //调用puts函数,用于输出字符串
0x0804930b <+112>:	movl   $0x804a300,(%esp) //"But finding it and solving it are quite different..."
0x08049312 <+119>:	call   0x8048800 <puts@plt> //调用puts函数,用于输出字符串
//问题的关键↓↓↓
0x08049317 <+124>:	call   0x8048eef <secret_phase>
//问题的关键↑↑↑

0x0804931c <+129>:	movl   $0x804a338,(%esp) //"Congratulations! You've defused the bomb!"
0x08049323 <+136>:	call   0x8048800 <puts@plt> //调用puts函数,用于输出字符串

//若没拆完6个则跳转到这里,直接返回
0x08049328 <+141>:	mov    0x7c(%esp),%eax
0x0804932c <+145>:	xor    %gs:0x14,%eax
0x08049333 <+152>:	je     0x804933a <phase_defused+159>
0x08049335 <+154>:	call   0x80487d0 <__stack_chk_fail@plt>
0x0804933a <+159>:	add    $0x8c,%esp
0x08049340 <+165>:	ret 

再次观察到了一个立即数0x804a409,查看一下内存地址:

看来我们需要输入两个整形数字、一个字符串才能进入。

我们再进入gdb,打断点查看内存地址0x804c4d0的值:

发现正是第四个函数中我们输入的值,所以增加的字符串应该跟在第四个函数后面。 

 

验证一下, 与函数4相对比,果然多了一个字符串输入。

这下真相大白了,后面跟的字符串是“DrEvil”。

3.测试运行

尝试输入一下:

果然,进入了隐藏关卡——secret_phase

(8)secret_phase

1.汇编代码

secret_phase函数反汇编代码如下:

2.汇编分析
0x08048eef <+0>:	push   %ebx //将 ebx 寄存器的值压入栈中,用于保存 ebx 的原始值  
0x08048ef0 <+1>:	sub    $0x18,%esp
0x08048ef3 <+4>:	call   0x804913d <read_line> //读取一行,意味着隐藏的密码放置在txt文件下一行即可
0x08048ef8 <+9>:	movl   $0xa,0x8(%esp) //将立即数 0xa(十进制中的 10)放入栈顶往下偏移 8 个字节的位置
0x08048f00 <+17>:	movl   $0x0,0x4(%esp) //将立即数 0x0(即 0)放入栈顶往下偏移 4 个字节的位置
0x08048f08 <+25>:	mov    %eax,(%esp) //将 eax 寄存器的值放入栈顶,即 read_line 函数的返回值
0x08048f0b <+28>:	call   0x80488e0 <strtol@plt> //调用 strtol 函数,将字符串转换为长整数
0x08048f10 <+33>:	mov    %eax,%ebx //将 eax 寄存器的值(转换后的长整数)移动到 ebx 寄存器 
0x08048f12 <+35>:	lea    -0x1(%eax),%eax //将 eax 寄存器的值减 1,结果再存回 eax 寄存器
0x08048f15 <+38>:	cmp    $0x3e8,%eax //比较 eax 寄存器的值和立即数 0x3e8(十进制中的 1000)
0x08048f1a <+43>:	jbe    0x8048f21 <secret_phase+50> //如果 eax <= 0x3e8,则跳转到地址 0x8048f21,否则引爆炸弹。意味着eax必须<=1000
0x08048f1c <+45>:	call   0x8049116 <explode_bomb>
//准备参数
0x08048f21 <+50>:	mov    %ebx,0x4(%esp) //将 ebx 寄存器的值放入栈顶往下偏移 4 个字节的位置,作为函数参数
0x08048f25 <+54>:	movl   $0x804c088,(%esp) //将立即数 0x804c088 放入栈顶,作为函数调用的地址参数
//调用fun7函数
0x08048f2c <+61>:	call   0x8048e9e <fun7>

0x08048f31 <+66>:	cmp    $0x3,%eax //比较 eax 寄存器的值和立即数 0x3(十进制中的 3)
0x08048f34 <+69>:	je     0x8048f3b <secret_phase+76> //eax等于3则跳转,否则爆炸。即函数返回值要等于3
0x08048f36 <+71>:	call   0x8049116 <explode_bomb>
0x08048f3b <+76>:	movl   $0x804a20c,(%esp)
0x08048f42 <+83>:	call   0x8048800 <puts@plt>
0x08048f47 <+88>:	call   0x804929b <phase_defused>
0x08048f4c <+93>:	add    $0x18,%esp
0x08048f4f <+96>:	pop    %ebx
0x08048f50 <+97>:	ret    



//<fun7>:
0x08048e9e <+0>:	push   %ebx //将 ebx 寄存器的值压入栈中,用于保存 ebx 的原始值  
0x08048e9f <+1>:	sub    $0x18,%esp //减少栈指针 esp 的值,为局部变量和函数调用预留空间
0x08048ea2 <+4>:	mov    0x20(%esp),%edx //第一个参数,放入 edx(地址),即二叉树的内存地址
0x08048ea6 <+8>:	mov    0x24(%esp),%ecx //第二个参数,放入 ecx(数据)
0x08048eaa <+12>:	test   %edx,%edx //判断 edx 是否为 0
0x08048eac <+14>:	je     0x8048ee5 <fun7+71> //如果 edx 为 0,则跳转到地址 0x8048ee5
0x08048eae <+16>:	mov    (%edx),%ebx //取节点中的值给 ebx
0x08048eb0 <+18>:	cmp    %ecx,%ebx //比较 ecx 和 ebx 的大小
0x08048eb2 <+20>:	jle    0x8048ec7 <fun7+41> //如果 ecx >= ebx,则跳转到地址 0x8048ec7,即访问右节点
//左子树:
0x08048eb4 <+22>:	mov    %ecx,0x4(%esp)
0x08048eb8 <+26>:	mov    0x4(%edx),%eax
0x08048ebb <+29>:	mov    %eax,(%esp)
0x08048ebe <+32>:	call   0x8048e9e <fun7>
0x08048ec3 <+37>:	add    %eax,%eax //将 eax 寄存器的值加到自己上(乘以2)
0x08048ec5 <+39>:	jmp    0x8048eea <fun7+76>
//右子树:
0x08048ec7 <+41>:	mov    $0x0,%eax
0x08048ecc <+46>:	cmp    %ecx,%ebx
0x08048ece <+48>:	je     0x8048eea <fun7+76>
0x08048ed0 <+50>:	mov    %ecx,0x4(%esp)
0x08048ed4 <+54>:	mov    0x8(%edx),%eax
0x08048ed7 <+57>:	mov    %eax,(%esp)
0x08048eda <+60>:	call   0x8048e9e <fun7>
0x08048edf <+65>:	lea    0x1(%eax,%eax,1),%eax //将 eax 寄存器的值乘以 2,再加 1,结果存回 eax 寄存器
0x08048ee3 <+69>:	jmp    0x8048eea <fun7+76>

0x08048ee5 <+71>:	mov    $0xffffffff,%eax
0x08048eea <+76>:	add    $0x18,%esp
0x08048eed <+79>:	pop    %ebx
0x08048eee <+80>:	ret    

查看地址0x804c088处的值: 

可以发现其 3 个一组,第一个存权值 key,第二个和第三个都存地址。没错!就是 二叉树
紧接着,它调用了 func7,我们来查看一下:

一句话,用伪代码总结功能:
int fun7(Node*addr, int x)
{
    if(addr == 0)
    {
        return -1;
    }
    int v = addr -> value;
    if(v == x)
    {
        return 0;
    }
    else if(v>x)
    {//访问左子节点
        return 2 * fun7(add -> left, x);
    }
    else
    {
        return 2 * func7(addr->right, x) + 1;
    }
}
其实就是不断寻找,直到 addr=0 或 val=x(我们输入的),并不断计算,
函数最后的返回值放在 eax 里。
我们回到 secret_phase 里, 发现,他只允许 func7 返回 3,其他值都会爆炸,那么我们的目的就变
为怎么让 func 返回 3, 那就只有右子树可以得到 2*(1)+1,
需要得到返回值 %eax=3 ,说明 递归顺序 为:
1. 最底层得到 0 return 0
2. 向上经过一层 %eax = %eax*2 + 1 得到 1 return 1
3. 再向上经过一层 % eax = %eax*2 +1 得到 3 return 3
首先来到二叉树的首地址 0x804c088 先是从底层 0 开始,对应的数据:24
再右节点+8
继续找右节点+8
最终为 0x6b,转为 10 进制为 107
3.测试运行

四、实验报告

;