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

前言

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

BombLab文件目录如下:

cpp 复制代码
├── bomb
├── bomb.c
├── bomb-quiet
└── README

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

一、实验题目及要求

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

①阶段1:字符串比较

②阶段2:循环语句

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

④阶段4:递归

⑤阶段5:指针

⑥阶段6:链表

二、准备工作

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

1.打开bomb.c文件

cpp 复制代码
/***************************************************************************
 * 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

方法一:

cpp 复制代码
objdump -d bomb > bomb.asm

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

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

方法二:

bash 复制代码
gdb -q bomb

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

bash 复制代码
(gdb) disassemble phase_x

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

3.执行bomb可执行文件

bash 复制代码
./bomb

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

PS:

bash 复制代码
./bomb answer.txt

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

三、拆解炸弹

(1)phase_1

1.汇编代码

查看phase_1反汇编代码:

查看string_not_equal反汇编代码:

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

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

bash 复制代码
x/s 0x804a1e4

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

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

3.测试运行

(2)phase_2

1.汇编代码

查看phase_2反汇编代码:

查看read_six_numbers反汇编代码:

2.汇编分析

分析如下:

cpp 复制代码
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 |

伪代码如下:

cpp 复制代码
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.汇编分析
cpp 复制代码
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就是爆炸。

伪代码如下:

cpp 复制代码
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 |
| 4 | 0 |
| 5 | -659 |

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

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

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

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

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

|------|------|
| 第一个数 | 第二个数 |
| 6 | 0 |
| 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()函数

分析如下:

cpp 复制代码
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:

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

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

cpp 复制代码
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:

cpp 复制代码
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函数结束后的返回值:

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

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

进一步观察,

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

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

func4函数()

分析如下:

cpp 复制代码
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函数建立了栈帧:

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

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

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

cpp 复制代码
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语言代码如下(这次不是伪代码了):

cpp 复制代码
#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;
}

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

|------|------|
| 第一个数 | 第二个数 |
| 12 | 3 |
| 13 | 3 |

3.测试运行

(5)phase_5

1.汇编代码

查看phase_5反汇编代码:

2.汇编分析
cpp 复制代码
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到寄存器中

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

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

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

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

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

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

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

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

逆向工程求解:

|-------------|---------------|
| 循环次数 | mov后的%eax寄存器值 |
| 15 | 15 |
| 14 | 6 |
| 13 | 14 |
| 12 | 2 |
| 11 | 1 |
| 10 | 10 |
| 9 | 0 |
| 8 | 8 |
| 7 | 4 |
| 6 | 9 |
| 5 | 13 |
| 4 | 11 |
| 3 | 7 |
| 2 | 3 |
| 1 | 12 |
| 0(输入的第一个数字) | 5 |

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

cpp 复制代码
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

伪代码如下:

cpp 复制代码
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.汇编分析
cpp 复制代码
//准备工作
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这个熟悉的函数:

cpp 复制代码
0x08048ddd <+20>:	call   0x804924b <read_six_numbers> //输入6个数字

所以输入的数字个数为6

cpp 复制代码
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],否则会发生爆炸

cpp 复制代码
    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调试查看:

cpp 复制代码
x/32bx 0x804c13c

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

cpp 复制代码
p/x *0x804c13c@32

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

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

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

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

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

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

③第三阶段

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

④第四阶段

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

cpp 复制代码
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

伪代码:

cpp 复制代码
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.汇编分析

分析如下:

cpp 复制代码
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.汇编分析
cpp 复制代码
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,我们来查看一下:


一句话,用伪代码总结功能:

cpp 复制代码
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
  1. 再向上经过一层 % eax = %eax*2 +1 得到 3 return 3
    首先来到二叉树的首地址 0x804c088 先是从底层 0 开始,对应的数据:24

    再右节点+8

    继续找右节点+8

    最终为 0x6b,转为 10 进制为 107
3.测试运行

四、实验报告

相关推荐
洛水水4 小时前
【力扣100题】18.随机链表的复制
算法·leetcode·链表
南宫萧幕4 小时前
规则基 EMS 仿真实战:SOC 区间划分与 Simulink 闭环建模全解
算法·matlab·控制
多加点辣也没关系5 小时前
数据结构与算法|第二十三章:高级数据结构
数据结构·算法
庞轩px5 小时前
第七篇:Spring扩展点——如何优雅地介入Bean的创建流程
java·后端·spring·bean·aware·扩展点
代钦塔拉6 小时前
Qt4 vs Qt5 带参数信号槽的连接方式详解
开发语言·数据库·qt
tongluowan0077 小时前
一个请求在Spring MVC 中是怎么流转的
java·spring·mvc
hoiii1877 小时前
孤立森林 (Isolation Forest) 快速异常检测系统
算法
夜郎king7 小时前
Spring AI 对接大模型开发易错点总结与实战解决办法
java·人工智能·spring
InfinteJustice7 小时前
踩坑分享C 语言文件操作全攻略:从基础读写到随机访问与缓冲区原理
c语言·开发语言·microsoft
码云数智-大飞7 小时前
滥用Lombok的@EqualsAndHashCode导致线上事故复盘
开发语言