我们解完六个关卡后似乎程序已经运行完成了,说明隐藏关卡还需要一定的条件才能触发,那么我们首先就要先去找到触发隐藏关卡的条件。我们看到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,其实可以理解为节点为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