前言
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 |
4 | 0 |
5 | -659 |
又因为存在下面的代码,最后又对输入的第一个数进行一次判断:
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()函数
分析如下:
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;
}
所以第四个炸弹的解系为:
第一个数 | 第二个数 |
12 | 3 |
13 | 3 |
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寄存器值 |
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
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处的值:
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;
}
}
1. 最底层得到 0 return 02. 向上经过一层 %eax = %eax*2 + 1 得到 1 return 13. 再向上经过一层 % eax = %eax*2 +1 得到 3 return 3