一、实验题目:Bomb Lab
二、实验目的:
- 了解并熟悉底层汇编语言的特点,能够看懂汇编语言
- 能够通过汇编语言,联系起高级语言,推测出每一行汇编代码的意义
- 用汇编相关知识拆除六颗炸弹以及发现隐藏炸弹并进行拆除
三、实验环境:Ubuntu12.04环境,gdb-peda
四、实验内容及操作步骤:
打开文件夹,出现如下文件:
其中bomb是可调式文件,bomb.c文件是“源文件”,README是一个文档。bomb无法直接打开以及README没有内容,因此首先查看bomb.c文件中是否有有用信息。C程序内容如下:
#include <stdio.h>
#include <stdlib.h>
#include "support.h"
#include "phases.h"
/*
* Note to self: Remember to erase this file so my victims will have no
* idea what is going on, and so they will all blow up in a
* spectaculary fiendish explosion. -- Dr. Evil
*/
FILE *infile;
int main(int argc, char *argv[])
{
char *input;
/* Note to self: remember to port this bomb to Windows and put a
* fantastic GUI on it. */
/* When run with no arguments, the bomb reads its input lines
* from standard input. */
if (argc == 1) {
infile = stdin;
}
/* When run with one argument <file>, the bomb reads from <file>
* until EOF, and then switches to standard input. Thus, as you
* defuse each phase, you can add its defusing string to <file> and
* avoid having to retype it. */
else if (argc == 2) {
if (!(infile = fopen(argv[1], "r"))) {
printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]);
exit(8);
}
}
/* You can't call the bomb with more than 1 command line argument. */
else {
printf("Usage: %s [<input_file>]\n", argv[0]);
exit(8);
}
/* Do all sorts of secret stuff that makes the bomb harder to defuse. */
initialize_bomb();
printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
printf("which to blow yourself up. Have a nice day!\n");
/* Hmm... Six phases must be more secure than one phase! */
input = read_line(); /* Get input */
phase_1(input); /* Run the phase */
phase_defused(); /* Drat! They figured it out!
* Let me know how they did it. */
printf("Phase 1 defused. How about the next one?\n");
/* The second phase is harder. No one will ever figure out
* how to defuse this... */
input = read_line();
phase_2(input);
phase_defused();
printf("That's number 2. Keep going!\n");
/* I guess this is too easy so far. Some more complex code will
* confuse people. */
input = read_line();
phase_3(input);
phase_defused();
printf("Halfway there!\n");
/* Oh yeah? Well, how good is your math? Try on this saucy problem! */
input = read_line();
phase_4(input);
phase_defused();
printf("So you got that one. Try this one.\n");
/* Round and 'round in memory we go, where we stop, the bomb blows! */
input = read_line();
phase_5(input);
phase_defused();
printf("Good work! On to the next...\n");
/* This phase will never be used, since no one will get past the
* earlier ones. But just in case, make this one extra hard. */
input = read_line();
phase_6(input);
phase_defused();
/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */
return 0;
}
通过观察C程序可以清晰的看出该C程序的结构,每个炸弹小关的结构都是四行:read_line进行输入、phase_i进入第i个炸弹关卡、phase_defused函数以及通关后的祝贺字符串。因此,这说明在每一关卡都得输入一些数值,而你的输入会决定炸弹是否会爆炸。
但遗憾的是这个C程序是不完整的,它并没有每个调用函数的源程序,因此为了探究这个实验必须研究它的汇编代码。
由于接下来的汇编代码会非常多,而为了有好的研究体验,可以首先配置好一个插件。在本实验中,我们使用了gdb-peda插件,gdb-peda具有更加友好的用户页面,使得调试更加有效,并且gdb-peda能够实时跟踪查看寄存器、反汇编语句以及栈帧之中的部分内容,并且在进行函数跳转时,提供了可能进行传递的参数,使得gdb调试更加可视化。
- 首先安装git:
sudo apt-get install git
- 安装gdb-peda插件:
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
- 实验任务
3.1. phase_1
反汇编,phase_1汇编代码如下:
下面对其进行解释与说明(我的输入:Brownie, you are doing a heck of a job.):
Dump of assembler code for function phase_1:
0x08048b50 <+0>: sub esp,0x1c
// 新栈帧的初始化
0x08048b53 <+3>: mov DWORD PTR [esp+0x4],0x804a184
// 函数调用前参数的准备,这是第一个参数的地址
0x08048b5b <+11>: mov eax,DWORD PTR [esp+0x20]
// 第二个参数首先放在eax
0x08048b5f <+15>: mov DWORD PTR [esp],eax
// 把存放在eax的参数给到栈顶
0x08048b62 <+18>: call 0x8048fa4 <strings_not_equal>
// 调用函数strings_not_equal,由字面意思可知是判断两个字符串是否一致的函数
0x08048b67 <+23>: test eax,eax
// 判断eax的符号,以及给一些标志位赋值
0x08048b69 <+25>: je 0x8048b70 <phase_1+32>
// 判断标志位ZF,即eax是否为0
0x08048b6b <+27>: call 0x80490b6 <explode_bomb>
// 如果不为0,那么就调用explode_bomb函数引爆炸药
0x08048b70 <+32>: add esp,0x1c
// exp恢复
0x08048b73 <+35>: ret
// 返回到call的下一条语句
End of assembler dump.
为了验证自己对strings_not_equal函数功能的猜测,将该函数汇编中重要关键的部分拿出来进行研究:
Dump of assembler code for function strings_not_equal:
0x08048fbb <+23>: mov DWORD PTR [esp],ebx
0x08048fbe <+26>: call 0x8048f8b <string_length>
0x08048fc3 <+31>: mov edi,eax
0x08048fc5 <+33>: mov DWORD PTR [esp],esi
0x08048fc8 <+36>: call 0x8048f8b <string_length>
0x08048fcd <+41>: mov edx,0x1
0x08048fd2 <+46>: cmp edi,eax
0x08048fd4 <+48>: jne 0x8049009 <strings_not_equal+101>
// 以上是先比较两个字符串的长度,如果不同的话直接输出1
0x08048fd6 <+50>: movzx eax,BYTE PTR [ebx]
0x08048fd9 <+53>: mov dl,0x0
0x08048fdb <+55>: test al,al
0x08048fdd <+57>: je 0x8049009 <strings_not_equal+101>
// 判断是否为空字符串
......
......
......
// 接下来就是对字符串的每一个字符进行对比
End of assembler dump.
通过上面的汇编分析,可以验证我们猜想是正确的,如果两个字符串不相同返回值为1,反之为0。于是,我们就要重点关注在调用strings_not_equal函数前进行传递的参数它们到底是什么?于是我们查看得到如下结果(由于我已知答案,就直接输入正确答案了):
通过gdb-peda的帮助,我们一下子就知道了,两个参数的地址和内容是什么。为了验证自己推断的是否和peda提供的一致,因此通过以上的汇编代码,我们直到第一个参数的地址是0x8040a184,另外一个的地址放在%esp当中。因此首先x/s 0x8040a184查看得到一个参数,这个显然是本来提供的参数,那么另外一个就是自己输入的参数了。在汇编代码中可以看出这个地址被传递到栈顶了,因此首先查看%esp得到输入参数的地址,然后用相同的方式查看得到<input_strings>的字样,那么这就是我们的输入了。
于是我们可以合理的推测,我们只需要输入和地址0x8040a184下内容相同的字符串就可以通过第一关,因此有以下的场景:
3.2. phase_2
对于第二关,依然是先查看它的汇编代码,如下:
下面对每一行汇编代码进行解释和说明(我的输入:0 1 3 6 10 15):
Dump of assembler code for function phase_2:
=> 0x08048b74 <+0>: push ebx
// 保留旧的ebx
0x08048b75 <+1>: sub esp,0x38
// 给栈帧开辟0x38的空间
0x08048b78 <+4>: lea eax,[esp+0x18]
// 第一个参数的传递,先存放在eax
0x08048b7c <+8>: mov DWORD PTR [esp+0x4],eax
// 参数放在esp+0x4
可以看到传递的地址存放的值是0,接着下一步看看传递第二个参数:
0x08048b80 <+12>: mov eax,DWORD PTR [esp+0x40]
0x08048b84 <+16>: mov DWORD PTR [esp],eax
// 这两行是在传递另外一个参数,这个参数就是我们输入的字符串
在执行完这两步之后,可以发现eax存放的就是输入的字符串,exp存放的地址的内容是输入字符串的地址,接着进行剩下汇编语句的分析:
0x08048b87 <+19>: call 0x80491eb <read_six_numbers>
// 调用read_six_numbers函数,猜测读入六个数字
0x08048b8c <+24>: cmp DWORD PTR [esp+0x18],0x0
0x08048b91 <+29>: jns 0x8048b98 <phase_2+36>
// 以上两行是判断符号,我们有知道esp+0x18存放的是第一个数字的值,在这里是0
0x08048b93 <+31>: call 0x80490b6 <explode_bomb>
// 如果是负数,那么就引爆炸药
0x08048b98 <+36>: mov ebx,0x1
// 初始化ebx为1
0x08048b9d <+41>: mov eax,ebx
// 传递给eax
0x08048b9f <+43>: add eax,DWORD PTR [esp+ebx*4+0x14]
// eax = eax + 第i个数
0x08048ba3 <+47>: cmp DWORD PTR [esp+ebx*4+0x18],eax
// 把刚刚计算的结果和第i+1个数进行比较
0x08048ba7 <+51>: je 0x8048bae <phase_2+58>
// 如果相等不爆炸
0x08048ba9 <+53>: call 0x80490b6 <explode_bomb>
0x08048bae <+58>: add ebx,0x1
// ebx = exb + 1
0x08048bb1 <+61>: cmp ebx,0x6
// 由于只输入了六个数,所以不大于六
0x08048bb4 <+64>: jne 0x8048b9d <phase_2+41>
// 当ebx<6的时候继续相同的以上操作
0x08048bb6 <+66>: add esp,0x38
// 还原esp
0x08048bb9 <+69>: pop ebx
// 还原ebx
0x08048bba <+70>: ret
// 返回call的下一条语句
End of assembler dump.
通过上述的分析,我们得知对于输入有以下的限制条件:
- 输入是六个数字
- 六个数字中第一个数字必须要为非负数
- 对于第i+1个数字,它必须是第i个数字与i的和
结合上述的限制条件,我们可以得到以下符合条件的输入例子:
0 1 3 6 10 15; |
---|
1 2 4 7 11 16; |
2 3 5 8 12 17……; |
100 101 103 106 110 115…… ; |
n n+1 n+3 n+6 n+10 n+15; 其中n是任何非负整数 |
于是我们输入的0 1 3 6 10 15自然能够得到以下画面:
3.3. phase_3
按照之前的步骤,首先查看phase_3的汇编代码:
phase_3的汇编代码的长度一下子就上来了,下面就一段一段进行分析:
首先是栈帧的初始化以及调用scanf函数前的参数准备:
首先看一看寄存器eax的值,它保存着输入的字符串的地址(我的输入:2 50):
然后看看第一部分每一行的意义:
=> 0x08048bbb <+0>: sub esp,0x2c
// 为栈帧开辟0x2c的空间
0x08048bbe <+3>: lea eax,[esp+0x1c]
// 通关观察寄存器,得知传给eax的是存放ebx值的地址
0x08048bc2 <+7>: mov DWORD PTR [esp+0xc],eax
// 第一个参数的准备
0x08048bc6 <+11>: lea eax,[esp+0x18]
// EAX: 0xbffff278 --> 0xa ('\n')
0x08048bca <+15>: mov DWORD PTR [esp+0x8],eax
// 第二个参数的准备
0x08048bce <+19>: mov DWORD PTR [esp+0x4],0x804a3a3
// 0x804a3a3: "%d %d",可以看出应该输入两个整数
0x08048bd6 <+27>: mov eax,DWORD PTR [esp+0x30]
// EAX: 0x804c480 ("2 50")
0x08048bda <+31>: mov DWORD PTR [esp],eax
// 第三个参数的准备,这个参数时我们输入的字符串
0x08048bdd <+34>: call 0x8048870 <__isoc99_sscanf@plt>
// 调用scanf函数进行输入
通过peda得到以下信息,表示可能的参数(这非常有帮助):
继续分析之后的汇编代码:
0x08048be2 <+39>: cmp eax,0x1
// 这里的eax保存的应该是读入整数的个数,因为可能会出现只读到一个整数的情况
0x08048be5 <+42>: jg 0x8048bec <phase_3+49>
// 输入的整数为2是正常,否者引爆bomb!
0x08048be7 <+44>: call 0x80490b6 <explode_bomb>
0x08048bec <+49>: cmp DWORD PTR [esp+0x18],0x7
// esp+0x18猜测是我们输入的第一个整数,将它和7比较
0x08048bf1 <+54>: ja 0x8048c2f <phase_3+116>
// 如果大于7Bomb!
0x08048bf3 <+56>: mov eax,DWORD PTR [esp+0x18]
// 把输入的第一个整数存储在eax
0x08048bf7 <+60>: jmp DWORD PTR [eax*4+0x804a1e0]
// 这里可以意识到,这是利用switch产生的一个跳转表来实现各种不同情况的输出
以上猜测的验证:esp+0x18往后的八个字节:
不难发现就是我们输入的两个整数2和50;接下来就是对跳转表不同情况下的输出的每条汇编的说明:
0x08048bfe <+67>: mov eax,0x1bc
0x08048c03 <+72>: jmp 0x8048c40 <phase_3+133>
// 以上是当第一个输入的数是1,那么把0x1bc->eax
0x08048c05 <+74>: mov eax,0x32
0x08048c0a <+79>: jmp 0x8048c40 <phase_3+133>
// 以上是当第一个输入的数是2,那么把0x32->eax
0x08048c0c <+81>: mov eax,0x29f
0x08048c11 <+86>: jmp 0x8048c40 <phase_3+133>
// 第一个数数3
0x08048c13 <+88>: mov eax,0x29d
0x08048c18 <+93>: jmp 0x8048c40 <phase_3+133>
// 第一个数数4
0x08048c1a <+95>: mov eax,0x30b
0x08048c1f <+100>: jmp 0x8048c40 <phase_3+133>
// 第一个数数5
0x08048c21 <+102>: mov eax,0x11f
0x08048c26 <+107>: jmp 0x8048c40 <phase_3+133>
// 第一个数数6
0x08048c28 <+109>: mov eax,0x1a2
0x08048c2d <+114>: jmp 0x8048c40 <phase_3+133>
// 第一个数数7
0x08048c2f <+116>: call 0x80490b6 <explode_bomb>
0x08048c34 <+121>: mov eax,0x0
0x08048c39 <+126>: jmp 0x8048c40 <phase_3+133>
// default
0x08048c3b <+128>: mov eax,0x25b
0x08048c40 <+133>: cmp eax,DWORD PTR [esp+0x1c]
// 把输入的第二个数与eax进行比较
0x08048c44 <+137>: je 0x8048c4b <phase_3+144>
// 如果不相等Bomb!
0x08048c46 <+139>: call 0x80490b6 <explode_bomb>
0x08048c4b <+144>: add esp,0x2c
// 还原esp
0x08048c4e <+147>: ret
// 返回call的下一条语句
通过上述的分析,它是一个switch 的分支语句,用的是跳转表。结合一些限制条件因此有以下的集中情况情况,并且它们就是正确的通关输入:1 444,2 50,3 671,4 669…… 于是有以下通关提示:
3.4. phase_4
同样的,首先看看phase_4的汇编代码的情况:
这个汇编相比上一个而言少了一些,那么先看看func4的汇编代码:
那么现在加起来比第三题的汇编还长了,不过幸运的是func4函数的结构非常清晰,它不断的调用自己,可以推断出这就是一个递归函数的汇编代码,所以为了了解递归汇编代码的规律,我们用下面的简单例子进行引入:
因此我们把递归总结为以下四步:
- 传递参数
- 调用自身函数
- 对函数进行数学运算,一般是线性变化
- 返回
由上面的步骤,我们可以写出上面例子表示成代码为:return 2*fun4(parameter1,parameter2,parameter3),由此我们分析我的func4:
0x08048c4f <+0>: sub esp,0x1c %开辟栈空间
0x08048c52 <+3>: mov DWORD PTR [esp+0x10],ebx
0x08048c56 <+7>: mov DWORD PTR [esp+0x14],esi
0x08048c5a <+11>: mov DWORD PTR [esp+0x18],edi %edi初值是0,eax初值是外部传入
0x08048c5e <+15>: mov esi,DWORD PTR [esp+0x20] %传入第一个参数
0x08048c62 <+19>: mov ebx,DWORD PTR [esp+0x24] %传入第二个参数
0x08048c66 <+23>: test esi,esi
%如果esi非正数则跳转,return eax = ebx = 0
0x08048c68 <+25>: jle 0x8048c95 <func4+70>
0x08048c6a <+27>: cmp esi,0x1
%如果参数esi等于1跳转,return eax = ebx
0x08048c6d <+30>: je 0x8048c9a <func4+75>
0x08048c6f <+32>: mov DWORD PTR [esp+0x4],ebx
%传入参数ebx,这个值没有改变
0x08048c73 <+36>: lea eax,[esi-0x1]
0x08048c76 <+39>: mov DWORD PTR [esp],eax
%传入参数eax = esi - 1,调用函数func4(s-1,b)
0x08048c79 <+42>: call 0x8048c4f <func4>
%以上数传递参数以及函数自身调用
0x08048c7e <+47>: lea edi,[eax+ebx*1]
%d = func4(s-1,b) + b,有些数学运算不像前面的例子那么简单,通常可能多次调用,这里就是
以下是第二次调用,老套路先传递参数
0x08048c81 <+50>: mov DWORD PTR [esp+0x4],ebx
%传入参数ebx,这个值没有改变
0x08048c85 <+54>: sub esi,0x2
0x08048c88 <+57>: mov DWORD PTR [esp],esi
%传入参数esi - 2,调用函数func(s-2,b)
0x08048c8b <+60>: call 0x8048c4f <func4>
%第二次调用函数自身
0x08048c90 <+65>: lea ebx,[edi+eax*1]
%这里的计算其实就是在计算返回值eax,b = d + func4(s-2,b) = func4(s-1,b) + b + func4(s-2,b)
%最后一句是跳转到递归返回return的位置,所以就是直接return计算结果(如果func4+75没有其他对eax进行操作的时候)
0x08048c93 <+68>: jmp 0x8048c9a <func4+75>
由以上的分析我们可以写出func4函数的C++程序代码,如下:
#include <iostream>
using namespace std;
int func4(int s, int b){
if(s<=0)
return 0;
if(s==1)
return b;
return func4(s-1,b)+b+func4(s-2,b);
}
int main(){
cout<<func4(5,3)<<endl;
// 输出的结果为36
return 0;
}
然后再回到phase_4进行分析(我输入的是36 3 DrEvil,为什么要输入DrEvil,先卖一个关子):
Dump of assembler code for function phase_4:
=> 0x08048cac <+0>: sub esp,0x2c
0x08048caf <+3>: lea eax,[esp+0x18]
0x08048cb3 <+7>: mov DWORD PTR [esp+0xc],eax
0x08048cb7 <+11>: lea eax,[esp+0x1c]
0x08048cbb <+15>: mov DWORD PTR [esp+0x8],eax
0x08048cbf <+19>: mov DWORD PTR [esp+0x4],0x804a3a3
0x08048cc7 <+27>: mov eax,DWORD PTR [esp+0x30]
0x08048ccb <+31>: mov DWORD PTR [esp],eax
0x08048cce <+34>: call 0x8048870 <__isoc99_sscanf@plt>
上面的汇编代码的操作和前一关有异曲同工 之妙,实现的功能都是输入两个整数,那就不在赘述了,我们通过peda记入一下可能的参数:
以下的汇编代码就是对输入的判断和调用func4前的参数的准备:
0x08048cd3 <+39>: cmp eax,0x2
// 对输入的整数的个数的判断,如果不是2,Bomb!
0x08048cd6 <+42>: jne 0x8048ce6 <phase_4+58>
0x08048cd8 <+44>: mov eax,DWORD PTR [esp+0x18]
// 通过观察寄存器eax的值,发现传进来的是3,就是我们驶入的第二个数字
0x08048cdc <+48>: cmp eax,0x1
// 这个数必须大于1,不然Bomb!
0x08048cdf <+51>: jle 0x8048ce6 <phase_4+58>
0x08048ce1 <+53>: cmp eax,0x4
// 这个数必须要小于等于4,也就是第二个数为(1,4],可以为2,3,4
0x08048ce4 <+56>: jle 0x8048ceb <phase_4+63>
0x08048ce6 <+58>: call 0x80490b6 <explode_bomb>
0x08048ceb <+63>: mov eax,DWORD PTR [esp+0x18]
0x08048cef <+67>: mov DWORD PTR [esp+0x4],eax
// 准备第一个参数3
0x08048cf3 <+71>: mov DWORD PTR [esp],0x5
// 准备第二个参数5,这个数是既定的,不能改变
0x08048cfa <+78>: call 0x8048c4f <func4>
// 调用递归函数func4
当我们运行完上面的递归过程会把结果放入寄存器eax,然后接着分析下面的汇编:
0x08048cff <+83>: cmp eax,DWORD PTR [esp+0x1c]
// 把输出的结果和esp+0x1c上的值进行比较,这个地址存放的其实就是我们输入的第一个整数36,换句话说就是第一个数你必须输入递归正确计算得到的结果(第三个输入先忽略)
0x08048d03 <+87>: je 0x8048d0a <phase_4+94>
0x08048d05 <+89>: call 0x80490b6 <explode_bomb>
0x08048d0a <+94>: add esp,0x2c
// 恢复esp
0x08048d0d <+97>: ret
// 返回call下一条
End of assembler dump.
由以上的分析,我们得知第一个整数应该是输入第二个整数通过递归计算得到的结果,而我们已知第二个整数输入的值只能是2,3,4,同时我们又有递归程序,所以把这三个值输入分别可以得到对应的递归输出24,36,48,所以我们输入可以是以下三种情况:24 2, 36 3, 48 4。因此,我们可以得到以下的反馈画面:
3.5. phase_5
依然先看它的汇编代码:
显然这次的函数没有调用函数,且汇编代码的行数也不多,但说实话难度不低,看一下逐句分析(我的输入:513489):
Dump of assembler code for function phase_5:
=> 0x08048d0e <+0>: push ebx //保存旧的ebp
0x08048d0f <+1>: sub esp,0x18 //开闭0x18的栈帧空间
0x08048d12 <+4>: mov ebx,DWORD PTR [esp+0x20] //把输入字符串的地址传入ebx
0x08048d16 <+8>: mov DWORD PTR [esp],ebx //输入地址放入栈顶
0x08048d19 <+11>: call 0x8048f8b <string_length> //计算输入的字符串的长度
0x08048d1e <+16>: cmp eax,0x6 //输入的字符串的长度要等于6
0x08048d21 <+19>: je 0x8048d28 <phase_5+26>
0x08048d23 <+21>: call 0x80490b6 <explode_bomb> //如果不等于6,Bomb!
0x08048d28 <+26>: mov edx,0x0
0x08048d2d <+31>: mov eax,0x0 //这两步是初始化eax和edx进行置零
0x08048d32 <+36>: movsx ecx,BYTE PTR [ebx+eax*1] //写入第i个字符符号扩展到ecx,如果是第一个字符,那么这里是'5',可以看看ecx有:ECX: 0x35 ('5')
0x08048d36 <+40>: and ecx,0xf //取出最低一个字节,这里是5
0x08048d39 <+43>: add edx,DWORD PTR [ecx*4+0x804a200]
// 这里是重点,需要查看0x804a200地址之后一大串保存的数值,把这个偏移地址保存的值加到edx
0x08048d40 <+50>: add eax,0x1 //eax自加,即偏移量两个取址的偏移量分别+1,+4 (phase_5+36和phase_5+43这两个步骤)
0x08048d43 <+53>: cmp eax,0x6 //只有六个字符,不能多访问了
0x08048d46 <+56>: jne 0x8048d32 <phase_5+36>
0x08048d48 <+58>: cmp edx,0x32 //在计算六次加法后保存在edx的结果和50进行比较
0x08048d4b <+61>: je 0x8048d52 <phase_5+68> //如果结果不为50,Bomb!
0x08048d4d <+63>: call 0x80490b6 <explode_bomb>
0x08048d52 <+68>: add esp,0x18 //还原esp
0x08048d55 <+71>: pop ebx //还原ebx
0x08048d56 <+72>: ret //返回call的下一条
End of assembler dump.
由上述的详细分析,直到该关卡的目的就是要你在一个数组中选取六个数,使这六个数加起来的和等于50,所以第一步就是查看0x804a200地址之后存放的数值,但是为了简单起见同时考虑到这个问题有非常多的解,就考虑<phase_5+40>取出来的数在0~9之间,同时考虑到[0,9]数字的十六进制表示,所以输入的六个字符均用数字代替,以下是[0,9]的ASCII十六进制表示:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|
0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 |
通过查看内存得到地址连续内存下的9个数值为:10,6,1,12,16,9,3,4,7
通过回溯法求解出从中可重复抽取六个数,从而凑出50的C++程序如下:
#include <iostream>
#include <vector>
using namespace std;
vector <vector <int> > vec;
int arr[9] = {10,6,1,12,16,9,3,4,7};
void backtrace(int depth, vector <int> V){
if(depth == 6){
int result = 0;
for(int i=0;i<6;i++)
result += V[i];
if(result == 50){
vec.push_back(V);
}
return;
}
for(int i=0;i<9;i++){
V.push_back(arr[i]);
backtrace(depth+1,V);
V.pop_back();
}
}
int main(){
vector <int> V;
backtrace(0,V);
for(int i=0;i<vec.size();i++){
for(int j=0;j<vec[i].size();j++){
cout<<vec[i][j]<<" ";
}
cout<<endl;
}
return 0;
}
/*
result:
10 9 3 3 9 16
10 9 3 4 12 12
10 9 3 7 12 9
10 9 3 7 9 12
10 9 4 10 10 7
10 9 4 10 1 16
10 9 4 10 16 1
10 9 4 10 7 10
10 9 4 6 12 9......
*/
我们从其中随便抽取一种情况:(16,10,1,12,4,7),对应的序号形成的字符串为“513489”,输入该字符串可以得到以下的反馈:
3.6. phase_6
phase_6的汇编有点长,我们直接对它进行逐句分析(我的输入:4 5 3 1 2 6):
Dump of assembler code for function phase_6:
=> 0x08048d57 <+0>: push esi
0x08048d58 <+1>: push ebx //保存esi和ebx
0x08048d59 <+2>: sub esp,0x44 //开辟栈空间
0x08048d5c <+5>: lea eax,[esp+0x10]
0x08048d60 <+9>: mov DWORD PTR [esp+0x4],eax //第一个参数
0x08048d64 <+13>: mov eax,DWORD PTR [esp+0x50]
0x08048d68 <+17>: mov DWORD PTR [esp],eax
//第二个参数,EAX: 0x804c570 ("4 5 3 1 2 6")
0x08048d6b <+20>: call 0x80491eb <read_six_numbers> //输入六个整数
以上部分是数据输入的部分,接着往下看:
友情提示:由于存在双层循环,为了简化,我就令外循环为i(代表esi),内循环为j(代表ebx),为了理解第一个双层循环,写出以下循环代码:
int i,j;
for(i = 0; i < 6; ){
if(a[i] - 1 > 5){
explode_bomb();
return ;
}
i = i + 1;
for(j = i; j <= 5; j++)
if(a[j] == a[i - 1]){
explode_bomb();
return ;
}
}
0x08048d70 <+25>: mov esi,0x0 //初始化esi,i = 0
0x08048d75 <+30>: mov eax,DWORD PTR [esp+esi*4+0x10] //将输入的第i个数
0x08048d79 <+34>: sub eax,0x1 //eax = eax - 1
0x08048d7c <+37>: cmp eax,0x5 //eax和5比较
0x08048d7f <+40>: jbe 0x8048d86 <phase_6+47> //意思就是输入的数必须要<=6
0x08048d81 <+42>: call 0x80490b6 <explode_bomb>
0x08048d86 <+47>: add esi,0x1 //i = i + 1
0x08048d89 <+50>: cmp esi,0x6 //外循环终止条件i<6
0x08048d8c <+53>: je 0x8048da9 <phase_6+82> //外循环截止
0x08048d8e <+55>: mov ebx,esi //j = i,内循环的"i"
0x08048d90 <+57>: mov eax,DWORD PTR [esp+ebx*4+0x10] //eax存放第j个数
0x08048d94 <+61>: cmp DWORD PTR [esp+esi*4+0xc],eax
//加入输入的六个数在a[6]中,外循环为i(代表esi),内循环为j(代表ebx),这条语句的意思就是比较a[j]和a[i - 1]的大小,同时j的初始值为i
0x08048d98 <+65>: jne 0x8048d9f <phase_6+72> //代表着输入不能有两两相同
0x08048d9a <+67>: call 0x80490b6 <explode_bomb>
0x08048d9f <+72>: add ebx,0x1 //j = j + 1
0x08048da2 <+75>: cmp ebx,0x5 //j <= 5就执行内循环
0x08048da5 <+78>: jle 0x8048d90 <phase_6+57>
0x08048da7 <+80>: jmp 0x8048d75 <phase_6+30> //否则外循环
以上循环结束时,得到的结论为:A、B、C、D、E、F的取值范围均为<=6,且两两间互不相等。这就是以上程序对你的输入作出的限制条件。
0x08048da9 <+82>: lea eax,[esp+0x10]
0x08048dad <+86>: lea ebx,[esp+0x28]
//分别传递的是开始存放数值的地址和开始存放地址的地址
0x08048db1 <+90>: mov ecx,0x7
0x08048db6 <+95>: mov edx,ecx
//初始化ecx和edx为7
0x08048db8 <+97>: sub edx,DWORD PTR [eax]
0x08048dba <+99>: mov DWORD PTR [eax],edx
//以上两句的意思是:如果用a[6]保存六个输入的数字,那么a[i] = 7 - a[i]
0x08048dbc <+101>: add eax,0x4 //地址的偏移量+4
0x08048dbf <+104>: cmp eax,ebx //循环终止条件,这个循环执行六次
0x08048dc1 <+106>: jne 0x8048db6 <phase_6+95>
这个循环的效果如下,假如a[6] = {1,2,3,4,5,6},那么执行之后的效果变为a[6] = {6,5,4,3,2,1},C程序表示如下:
for(int i = 0; i < 6; i++)
a[i] = 7 - a[i];
接着分析以下汇编:
注意:这里也是一个双层循环,所以我这里也用i代表ebx为外循环,j代表eax为内循环
另外地址0x804c13c连续空间储存的值为:
于是我们可以联想到“链表”,如果想到这个层面那么之后的都简单了
还有就是对这个结点的理解,这个结点包括三个信息,比如第一个结点{0x6d,0x01,0x0804c148},比如p是指向该结点的地址那么* p就是该结点的数值* (p+4)指的就是结点序号,* (p+8)就是他下一个结点的指针,指向下一个结点
0x08048dc3 <+108>: mov ebx,0x0 //初始化i为0
0x08048dc8 <+113>: jmp 0x8048de0 <phase_6+137> //无条件跳转
0x08048dca <+115>: mov edx,DWORD PTR [edx+0x8]
//这个地址储存的是地址,并且是下一个结点的地址
0x08048dcd <+118>: add eax,0x1 //j = j + 1
0x08048dd0 <+121>: cmp eax,ecx //j和a[i]进行比较
0x08048dd2 <+123>: jne 0x8048dca <phase_6+115>
//不相等跳转,这个跳转的意义就是按照你输入的数值进行排序,什么意思呢?举个例子(秩和比法的味道):
/*
你的输入:4 1 3 2(它就代表秩,就理解为顺序即可)
原始数据:23 11 26 8
然后用你的输入对原始数据进行排序得到新序列:8 23 36 11
第四个(8)排到了第一,第一个(23)排到了第二......
到这里结合语境就是:把第四个地址放在第一个,第一个地址放在第二个......
*/
0x08048dd4 <+125>: mov DWORD PTR [esp+esi*4+0x28],edx
//把这个地址写入内存
0x08048dd8 <+129>: add ebx,0x1 //i = i + 1
0x08048ddb <+132>: cmp ebx,0x6 //i < 6
0x08048dde <+135>: je 0x8048df7 <phase_6+160>
0x08048de0 <+137>: mov esi,ebx //esi = i
0x08048de2 <+139>: mov ecx,DWORD PTR [esp+ebx*4+0x10] //ecx = a[i]
0x08048de6 <+143>: mov eax,0x1 //j = 1
0x08048deb <+148>: mov edx,0x804c13c
//主要是要理解它之后的18个4字节空间下储存的数值的意义
0x08048df0 <+153>: cmp ecx,0x1 //a[i]和1进行比较
0x08048df3 <+156>: jg 0x8048dca <phase_6+115> //大于1
0x08048df5 <+158>: jmp 0x8048dd4 <phase_6+125> //反之
以上的汇编代码的作用用一句话说就是,把这些节点进行排序,比如我输入的4、1、3、5、6、2。操作以后的结果就是节点4放在了ad1位置、节点1放在了ad2位置、节点3放在了ad3位置、节点5放在了ad4位置、节点6放在了ad5位置、节点2放在了ad6位置
0x08048df7 <+160>: mov ebx,DWORD PTR [esp+0x28]
0x08048dfb <+164>: mov eax,DWORD PTR [esp+0x2c]
0x08048dff <+168>: mov DWORD PTR [ebx+0x8],eax
0x08048e02 <+171>: mov edx,DWORD PTR [esp+0x30]
0x08048e06 <+175>: mov DWORD PTR [eax+0x8],edx
0x08048e09 <+178>: mov eax,DWORD PTR [esp+0x34]
0x08048e0d <+182>: mov DWORD PTR [edx+0x8],eax
0x08048e10 <+185>: mov edx,DWORD PTR [esp+0x38]
0x08048e14 <+189>: mov DWORD PTR [eax+0x8],edx
0x08048e17 <+192>: mov eax,DWORD PTR [esp+0x3c]
0x08048e1b <+196>: mov DWORD PTR [edx+0x8],eax
0x08048e1e <+199>: mov DWORD PTR [eax+0x8],0x0
/*
以上的关系自己画个变化过程图,要比我用文字写出来好理解多了
ebx、eax分别指向两个节点,(ebx+8)和(eax+8)就是它们下一个节点的指针
所以以上的目的就是形成一个链表,这个链表的顺序是依照你输入的数据
*/
0x08048e25 <+206>: mov esi,0x5 //初始化esi,我们假设它为i,i = 5
0x08048e2a <+211>: mov eax,DWORD PTR [ebx+0x8] //下一个结点的地址放入eax
0x08048e2d <+214>: mov edx,DWORD PTR [eax] //下一个结点地址的值放入edx
0x08048e2f <+216>: cmp DWORD PTR [ebx],edx
//前三句的意思就是前一个结点的数值要大于有一个结点的数值,换句话说就是这个链表数值要递减
0x08048e31 <+218>: jge 0x8048e38 <phase_6+225>
0x08048e33 <+220>: call 0x80490b6 <explode_bomb>
0x08048e38 <+225>: mov ebx,DWORD PTR [ebx+0x8] //更新ebx,变为下一个结点的地址
0x08048e3b <+228>: sub esi,0x1 //i > 0
0x08048e3e <+231>: jne 0x8048e2a <phase_6+211>
//以下四句是恢复原来的状态
0x08048e40 <+233>: add esp,0x44
0x08048e43 <+236>: pop ebx
0x08048e44 <+237>: pop esi
0x08048e45 <+238>: ret
End of assembler dump.
依照我的输入4 5 3 1 2 6(经过(7-a[i])变化之后得到:3 2 4 6 5 1),我们首先看看最初的链表的构造:
它显然不能满足递减的性质,但是针对上面六个结点,我们可以给出一个秩,让它经过我们的秩排序后称为一个有序递减的序列,显然如果这个秩为{3 2 4 6 5 1},那么就会形成一个有序递减的序列
然后看看经过排序后新形成的链表的构造:
经过这个变化之后达到了递增效果,于是得到反馈:
为什么出现secret_phase呢?回到前面第四题我们多输入了一个字符串DrEvil,那又为什么,看下面的分析
3.7. secret_phase
我们解完六个关卡后似乎程序已经运行完成了,说明隐藏关卡还需要一定的条件才能触发,那么我们首先就要先去找到触发隐藏关卡的条件。我们看到bomb.c文件,每段phase函数运行完成以后又会运行一个phase_defused()函数,这个函数我们在上述拆炸弹过程中都没有用到,自然它的嫌疑就很大,故我们先看看这个函数的具体内容:
Dump of assembler code for function phase_defused:
=> 0x0804923b <+0>: sub esp,0x8c //开辟栈空间
0x08049241 <+6>: mov eax,gs:0x14
//储存金丝雀值,进行栈保护
0x08049247 <+12>: mov DWORD PTR [esp+0x7c],eax
//把金丝雀值放到0x7c(%esp)
0x0804924b <+16>: xor eax,eax //将%eax进行置零
0x0804924d <+18>: cmp DWORD PTR ds:0x804c3cc,0x6
//将0x804c3cc地址中的值与6进行比较,可以猜测这个地址储存的值是你通过的关的数量
0x08049254 <+25>: jne 0x80492c8 <phase_defused+141> //如果不等于跳转到+141
0x08049256 <+27>: lea eax,[esp+0x2c] //参数的地址
0x0804925a <+31>: mov DWORD PTR [esp+0x10],eax //把参数地址保存
0x0804925e <+35>: lea eax,[esp+0x28] //第二个参数地址
0x08049262 <+39>: mov DWORD PTR [esp+0xc],eax //第二个参数地址保存
0x08049266 <+43>: lea eax,[esp+0x24] //第三个
0x0804926a <+47>: mov DWORD PTR [esp+0x8],eax //第三个
//这我不自主的看看存放的值0x00000024 0x00000003 0xbffff264,这是这三个参数的值,前面两个数十进制分别是36 3,这里联想到前面一关也输入了这个,为了验证猜想可以看看地址
0x0804926e <+51>: mov DWORD PTR [esp+0x4],0x804a3a9
//一个字符串地址,"%d %d %s"
0x08049276 <+59>: mov DWORD PTR [esp],0x804c4d0
//0x804c4d0 <input_strings+240>: "",这里对上述的假设验证,我们查看发现0x0804c4d0其实是字符串"36 3"的地址,也就是第四关输入的地址,那么我们只需要在后面添加一个输入即可,输入什么呢?接着看
0x0804927d <+66>: call 0x8048870 <__isoc99_sscanf@plt>
//进行输入了
0x08049282 <+71>: cmp eax,0x3
//参数的个数与3进行比较
0x08049285 <+74>: jne 0x80492bc <phase_defused+129>
//如果不等于跳转+129
0x08049287 <+76>: mov DWORD PTR [esp+0x4],0x804a3b2
//0x804a3b2: "DrEvil"
0x0804928f <+84>: lea eax,[esp+0x2c]
//调用函数前的参数的准备,进行传参,猜测应该是你输入的字符串,所以应该输入"DrEvil"
//那么我们在第四关的时候就输入36 3 DrEvil,这时候完成其他六关会出现提示哦!
0x08049293 <+88>: mov DWORD PTR [esp],eax
//参数的地址拿到栈顶
0x08049296 <+91>: call 0x8048fa4 <strings_not_equal>
//调用函数进行比较
0x0804929b <+96>: test eax,eax
//判断结果eax存放值的符号
0x0804929d <+98>: jne 0x80492bc <phase_defused+129>
//如果不相等就跳转到+129,否则执行执行下方的secret_phase
0x0804929f <+100>: mov DWORD PTR [esp],0x804a278
//0x804a278: "Curses, you've found the secret phase!"
0x080492a6 <+107>: call 0x8048800 <puts@plt>
0x080492ab <+112>: mov DWORD PTR [esp],0x804a2a0
//0x804a2a0: "But finding it and solving it are quite different..."
0x080492b2 <+119>: call 0x8048800 <puts@plt>
//打印以上两句话
0x080492b7 <+124>: call 0x8048e97 <secret_phase>
//调用隐藏炸弹
0x080492bc <+129>: mov DWORD PTR [esp],0x804a2d8
//以第一个炸弹为例,这时候这个代表的是"Congratulations! You've defused the bomb!"
0x080492c3 <+136>: call 0x8048800 <puts@plt>
//把上面的字符串进行输出
0x080492c8 <+141>: mov eax,DWORD PTR [esp+0x7c] //取出程序运行完之后的金丝雀值
0x080492cc <+145>: xor eax,DWORD PTR gs:0x14 //对金丝雀值进行对比,检查是否发生异常
0x080492d3 <+152>: je 0x80492da <phase_defused+159> //如果结果为0,表示正常
0x080492d5 <+154>: call 0x80487d0 <__stack_chk_fail@plt> //否者报错
0x080492da <+159>: add esp,0x8c
0x080492e0 <+165>: ret
End of assembler dump.
我们反汇编看一看secret_phase的反汇编代码:
Dump of assembler code for function secret_phase:
=> 0x08048e97 <+0>: push ebx
//保存旧的ebp地址
0x08048e98 <+1>: sub esp,0x18
//开辟栈空间
0x08048e9b <+4>: call 0x80490dd <read_line>
//读入一行数据,假设我们输入"100"
0x08048ea0 <+9>: mov DWORD PTR [esp+0x8],0xa
//输入参数10
0x08048ea8 <+17>: mov DWORD PTR [esp+0x4],0x0
//输入参数0
0x08048eb0 <+25>: mov DWORD PTR [esp],eax
//把eax中的值保存在栈顶,显然这个eax是我们输入字符串的地址
0x08048eb3 <+28>: call 0x80488e0 <strtol@plt>
//经过这个函数eax保存的是0x64显然是100的16进制,那么推测上面函数是将一个字符串的数转化为整数
0x08048eb8 <+33>: mov ebx,eax
//用ebx保存100
0x08048eba <+35>: lea eax,[eax-0x1]
//eax-1
0x08048ebd <+38>: cmp eax,0x3e8
//eax即99和1000比较(后面观察到这个是二叉树的最大值-1)
0x08048ec2 <+43>: jbe 0x8048ec9 <secret_phase+50>
//如果小于等于跳转+50,不会爆炸哦
0x08048ec4 <+45>: call 0x80490b6 <explode_bomb>
0x08048ec9 <+50>: mov DWORD PTR [esp+0x4],ebx
//把100移到esp+4,传参
0x08048ecd <+54>: mov DWORD PTR [esp],0x804c088
//gdb-peda$ x/s 0x804c088 0x804c088 <n1>: "$"
//gdb-peda$ x/xw 0x804c088 0x804c088 <n1>: 0x00000024
0x08048ed4 <+61>: call 0x8048e46 <fun7>
//调用函数fun7
0x08048ed9 <+66>: cmp eax,0x2
//当返回值等于2时,bingo!
0x08048edc <+69>: je 0x8048ee3 <secret_phase+76>
0x08048ede <+71>: call 0x80490b6 <explode_bomb>
0x08048ee3 <+76>: mov DWORD PTR [esp],0x804a1ac
0x08048eea <+83>: call 0x8048800 <puts@plt>
//输出"Wow! You've defused the secret stage!"
0x08048eef <+88>: call 0x804923b <phase_defused>
0x08048ef4 <+93>: add esp,0x18
//esp恢复调用前的状态
0x08048ef7 <+96>: pop ebx
//恢复ebp
0x08048ef8 <+97>: ret
//跳转到调用函数时call的下一条语句
End of assembler dump.
fun7函数:显然地,这又是一个递归函数
Dump of assembler code for function fun7:
=> 0x08048e46 <+0>: push ebx
//放入ebx
0x08048e47 <+1>: sub esp,0x18
//为栈帧开辟空间
0x08048e4a <+4>: mov edx,DWORD PTR [esp+0x20]
//读入形式参数edx = 0x804c088
0x08048e4e <+8>: mov ecx,DWORD PTR [esp+0x24]
//读入形式参数ecx = 0x64
//这里推测输入的两个参数一个数地址一个是数值,又结合递归联想到他可能和树结构有关系,假设函数 //为fun7(root, val)
0x08048e52 <+12>: test edx,edx
//判断edx的符号
0x08048e54 <+14>: je 0x8048e8d <fun7+71>
//等于0,跳转至+71,(一定不能等于0),其实可以理解为节点为NULL
0x08048e56 <+16>: mov ebx,DWORD PTR [edx]
//ebx = 0x00000024,节点的数值
0x08048e58 <+18>: cmp ebx,ecx
//把节点的数值和输入的形参数值比较
0x08048e5a <+20>: jle 0x8048e6f <fun7+41>
//如果ebx<=ecx。跳转至+41
//如果ebx里面的值更大
0x08048e5c <+22>: mov DWORD PTR [esp+0x4],ecx
//把ecx保存在esp+0x4
0x08048e60 <+26>: mov eax,DWORD PTR [edx+0x4]
//传递参数,这里传入的是左节点,edx +0x4是左节点 +0x8是右节点
0x08048e63 <+29>: mov DWORD PTR [esp],eax
//eax放到栈顶,方便调用
0x08048e66 <+32>: call 0x8048e46 <fun7>
//调用函数fun7
0x08048e6b <+37>: add eax,eax
//eax = 2*eax,return 2*fun7(val1,val2),val1 = root->left,val2 = val
0x08048e6d <+39>: jmp 0x8048e92 <fun7+76>
//跳出递归
0x08048e6f <+41>: mov eax,0x0
//将eax置0
0x08048e74 <+46>: cmp ebx,ecx
0x08048e76 <+48>: je 0x8048e92 <fun7+76>
//如果ebx(0x24) == ecx(0x64),(显然不等于),跳转至+76,就是直接返回了,return 0
0x08048e78 <+50>: mov DWORD PTR [esp+0x4],ecx
0x08048e7c <+54>: mov eax,DWORD PTR [edx+0x8]
//为什么加8,摆明了是指向其右节点
0x08048e7f <+57>: mov DWORD PTR [esp],eax
//以上三步就是参数的传递
0x08048e82 <+60>: call 0x8048e46 <fun7>
0x08048e87 <+65>: lea eax,[eax+eax*1+0x1]
//递归的最后一步结果的线性组合,return 2*fun7(val1,val2)+1,val1 = root->right,val2 = val
0x08048e8b <+69>: jmp 0x8048e92 <fun7+76>
//结束递归
0x08048e8d <+71>: mov eax,0xffffffff
//将0xffffffff传入eax
0x08048e92 <+76>: add esp,0x18
0x08048e95 <+79>: pop ebx
0x08048e96 <+80>: ret
//恢复调用前的状态
End of assembler dump.
我们写出递归函数:
int fun7(Node *root, int val){
if(root == NULL)
return -1;
if(root->value == val)
return 0;
else if(root->value > val)
return 2*fun7(root->left,val);
else if(root->value < val)
return 2*fun7(root->right,val)+1;
}
可以通过以下指令查看一个结点的信息:
p/x *0x804c088@3
通过这个可以查看一个节点,其中0x804c088是一个结点的地址指针,可以自行修改。
gdb-peda$ p/x *0x804c088@3
$5 = {0x24, 0x804c094, 0x804c0a0}
gdb-peda$ p/x *0x804c088@3
$6 = {0x24, 0x804c094, 0x804c0a0}
gdb-peda$ p/x *0x804c094@3
$8 = {0x8, 0x804c0c4, 0x804c0ac}
gdb-peda$ p/x *0x804c0c4@3
$9 = {0x6, 0x804c0e8, 0x804c10c}
gdb-peda$ p/x *0x804c0ac@3
$10 = {0x16, 0x804c118, 0x804c100}
gdb-peda$ p/x *0x804c0e8@3
$11 = {0x1, 0x0, 0x0}
gdb-peda$ p/x *0x804c10c@3
$12 = {0x7, 0x0, 0x0}
gdb-peda$ p/x *0x804c118@3
$13 = {0x14, 0x0, 0x0}
gdb-peda$ p/x *0x804c100@3
$14 = {0x23, 0x0, 0x0}
gdb-peda$ p/x *0x804c0ac@3
$15 = {0x16, 0x804c118, 0x804c100}
gdb-peda$ p/x *0x804c0a0@3
$16 = {0x32, 0x804c0b8, 0x804c0d0}
gdb-peda$ p/x *0x804c0b8@3
$17 = {0x2d, 0x804c0dc, 0x804c124}
gdb-peda$ p/x *0x804c0d0@3
$18 = {0x6b, 0x804c0f4, 0x804c130}
gdb-peda$ p/x *0x804c0dc@3
$19 = {0x28, 0x0, 0x0}
gdb-peda$ p/x *0x804c124@3
$20 = {0x2f, 0x0, 0x0}
gdb-peda$ p/x *0x804c0f4@3
$21 = {0x63, 0x0, 0x0}
gdb-peda$ p/x *0x804c130@3
$22 = {0x3e9, 0x0, 0x0}
通过上述的结点的信息,可以大致画出以下的树结构图(16进制):
24
8 23
6 16 2d 6b
1 7 14 23 28 2f 63 3e9
限制条件:
0x08048ebd <+38>: cmp eax,0x3e8
//eax即99和1000比较(后面观察到这个是二叉树的最大值-1)
0x08048ec2 <+43>: jbe 0x8048ec9 <secret_phase+50>
在每一个节点都有两种选择,然后利用递归的性质我们可以得到以下的展开图:
那么显然有:
- 根->左->右 :0x16
- 根->左->右->左 :0x14
当我们输入22 or 20的时候我们就会得到成功通关的提示!