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

news2024/12/30 2:41:38

前言

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.测试运行

四、实验报告

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1842896.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

四川赤橙宏海商务信息咨询有限公司引领抖音电商潮流

在当今数字化浪潮下&#xff0c;电商行业蓬勃发展&#xff0c;抖音电商作为新兴力量&#xff0c;正以其独特的魅力吸引着越来越多的商家和消费者。四川赤橙宏海商务信息咨询有限公司&#xff0c;作为抖音电商服务领域的佼佼者&#xff0c;凭借其专业的团队和丰富的经验&#xf…

番外篇 | 基于YOLOv5-RCS的明火烟雾检测 | 源于RCS-YOLO

前言:Hello大家好,我是小哥谈。RCS-YOLO是一种目标检测算法,它是基于YOLOv3算法的改进版本。通过查看RCS-YOLO的整体架构可知,其中包括RCS-OSA模块。RCS-OSA模块在模型中用于堆叠RCS模块,以确保特征的复用并加强不同层之间的信息流动。本文就给大家详细介绍如何将RCS-YOLO…

新疆旅游创新产品迎活力 伊吾胡杨文旅品牌发布

2024年,随着政策的引领、经济的形态、社会需求的多样化以及新媒体流量的赋能,我国旅游业的主基调将从“快速复苏”转向“理性繁荣”,文旅产业正呈现出前所未有的活力和潜力。6月14日,“千年敦煌万年胡杨”敦煌至伊吾踩线采风行暨伊吾胡杨产品发布大会圆满落幕。会上,景区、旅行…

一名女DBA的感谢信,到底发生了什么?

昨日我们收到这样一通来电 “早上九点刚上班便收到业务投诉电话&#xff0c;系统卡顿&#xff0c;接口失败率大增&#xff0c;怀疑数据库问题。打开运维平台发现是国产库&#xff0c;生无可恋&#xff0c;第一次生产环境遇到国产库性能问题&#xff0c;没什么排查经验&#xf…

什么是Amazon Relational Database Service(Amazon RDS)及实践体验

目录 前言亚马逊云服务免费体验中心三种优惠类型 Amazon RDS什么是Amazon RDS为什么选择 Amazon RDS&#xff1f;Amazon RDS 的优势关键功能详情工作原理Amazon RDSAmazon RDS CustomAmazon RDS on Amazon Outposts 实践创建并连接到 MySQL 数据库实例一、创建 EC2 实例二、创建…

大型Web应用的模块化与组织实践:Flask Blueprints深入解析

目录 一、引言 二、Flask Blueprints概述 三、Flask Blueprints的使用 创建Blueprint对象 定义路由和视图函数 注册Blueprint 使用Blueprints组织代码 四、案例分析 创建模块目录结构 创建Blueprint对象 注册Blueprint 五、代码示例与最佳实践 1. 代码示例 …

成为AIGC人才,是职场人当下的必修课?

随着科技的飞速进步&#xff0c;人工智能和机器学习技术正逐渐渗透到我们生活的每一个角落&#xff0c;其中&#xff0c;人工智能生成内容&#xff08;AIGC&#xff09;更是以其独特的魅力和广泛的应用前景&#xff0c;成为当下科技领域的热门话题。在这样的背景下&#xff0c;…

数字乡村:绘就乡村振兴的智慧新画卷

在乡村振兴战略的宏伟蓝图下&#xff0c;“数字乡村”作为新时代农村现代化的重要抓手&#xff0c;正悄然改变着中国乡村的面貌。本文旨在深度剖析数字乡村建设的核心价值、关键技术、成功案例以及未来展望&#xff0c;为乡村振兴战略提供前瞻性的思考与启示。 数字乡村的核心价…

java.io.eofexception:ssl peer shut down incorrectly

可能是因为 1)https设置 2&#xff09;超时设置 FeignConfig.java package zwf.service;import java.io.IOException; import java.io.InputStream; import java.security.KeyStore;import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory;import org.apac…

用电子表单替代纸质表格,签到报名、出入登记更轻松

用纸质表格收集信息时&#xff0c;常常会出现数据丢失、不易统计等问题。我们可以搭建电子表单来代替线下纸质表格&#xff0c;进行信息收集、记录数据。 这些数据会保存在账号下&#xff0c;可以导出Excel或PDF进行存档&#xff1b;也可以根据企业要求自定义PDF导出格式。 并…

com.lowagie:itext:jar:2.1.7.js9 was not found

1 在 https://jaspersoft.jfrog.io/ui/native/third-party-ce-artifacts/com/lowagie/itext/2.1.7.js9/下载com/lowagie/itext/2.1.7.js9/itext-2.1.7.js9.jar的包&#xff0c; 2 在本地maven仓库com.lowagie.itext.2.1.7的目录下&#xff0c;将itext-2.1.7.js9.jar复制更名为…

摄像头劫持——保护自己免受窥探

今天为您带来当今科技界的最新趋势及探索方法。本周&#xff0c;我们将为您提供五个防止黑客在您不知情的情况下访问您的网络摄像头的建议。 网络摄像头 一、摄像头劫持 你是否曾经怀疑过&#xff0c;即使你没有主动使用网络摄像头&#xff0c;也可能有人正在通过它窥视你&am…

苹果入局AI手机 iOS 18将应用AI功能

当三星、华为等国内外手机厂商都在卷着造AI手机时&#xff0c;智能手机大佬苹果那边确一直没什么动静。直到今年5月&#xff0c;距离苹果 WWDC24 全球开发者大会还有十多天时&#xff0c;长期关注苹果的博社记者Mark Gurman放料&#xff0c;iOS 18系统中将会应用一些AI功能。 从…

Java按照GBK拼音顺序对汉字排序

最近遇到一个需求&#xff0c;需要对一个列表数据的特定字段&#xff0c;比如姓名&#xff0c;初看应该不难&#xff0c;但是有什么比较快速便捷的方法&#xff0c;因为是Java的项目&#xff0c;所以想到比较火的工具框架Hutool&#xff0c;在Hutool官网 找到一个比较器类Pinyi…

【启明智显产品分享】Model3工业级HMI芯片详解系列专题(三):安全、稳定、高防护

芯片作为电子设备的核心部件&#xff0c;&#xff0c;根据不同的应用领域被分为不同等级。工业级芯片适用于工业自动化、控制系统和仪器仪表等领域&#xff0c;对芯片的安全、稳定、防护能力等等有着较高的要求。这些芯片往往需要具备更宽的工业温度范围&#xff0c;能够在更恶…

第4章 工程经济评价指标 作业

第4章 工程经济评价指标 作业 一单选题&#xff08;共27题&#xff0c;100分&#xff09; (单选题)利息备付率是指( )与应付利息费用的比值。 A. 息税前利润 B. 利润总额 C. 净利润 D. 营业收入 正确答案: A:息税前利润; (单选题)当净现值( )0时,该项目不可行。 A. < B. …

摊位纠纷演变肢体冲突,倒赔了500:残疾夫妇与摊主谁之过?

在一个小商贩密集的街区&#xff0c;一起由摊位纠纷引发的肢体冲突事件在当地社区和网络上引起了热议。涉事双方为一名摊主和一对残疾夫妇&#xff0c;他们的争执源自对一个摊位的使用权。本是口头上的争吵&#xff0c;却由于双方情绪激动&#xff0c;迅速升级为肢体冲突&#…

windy开发笔记

https://www.windy.com/ 基于windyApi 开发自己的气象项目 文档: widnyAPI 主要属性https://api.windy.com/map-forecast/docswidny 参数 以及数据https://api.windy.com/map-forecast/examples/parameters 属性值 https://api.windy.com/point-forecast/docs#parameterssto…

英国牛津大学基因组学方向博士后职位

英国牛津大学基因组学方向博士后职位 牛津大学&#xff08;University of Oxford&#xff09;&#xff0c;简称“牛津”&#xff08;Oxford&#xff09;&#xff0c;位于英国牛津&#xff0c;是一所公立研究型大学&#xff0c;采用传统学院制。是罗素大学集团成员&#xff0c;被…

[Shell编程学习路线]——for循环应用技巧 语法和案例

&#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f6e0;️Shell编程专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年6月20日16点21分 &#x1f004;️文章质量&#xff1a;96分 目录 ————前言———— for 循环语句 基本结构 图示原理…