目录
实验目的
实验原理
实验准备
实验过程
一、phase_1
二、phase_2
三、phase_3
四、phase_4
五、phase_5
六、phase_6
七、secret_phase
理解汇编语言,学会使用调试器。
二进制炸弹是作为一个目标代码文件提供给学生们的程序,运行时,它提示用户输入6个不同的字符串。如果其中任何一个不正确,炸弹就会“爆炸”:打印出一条错误信息。学生通过反汇编和逆向工程来确定是哪六个字符串,从而解除他们各自炸弹的雷管。
找到自己对应的bomb文件并解压,得到可执行文件bomb和C语言文件bomb.c,通过VMtools里的共享文件夹传输到虚拟机里的ubantu系统里,就可以在ubantu系统里进行调试和运行了。
使用objdump -d bomb > bomblab.txt指令将汇编代码输出到文本文件里方便查看,得到汇编代码后就可以进行拆弹了。
phase_1的汇编代码如下:
08048b90 :
8048b90: 83 ec 1c sub $0x1c,%esp //初始化栈
8048b93: c7 44 24 04 44 a1 04 movl $0x804a144,0x4(%esp)
8048b9a: 08
8048b9b: 8b 44 24 20 mov 0x20(%esp),%eax
8048b9f: 89 04 24 mov %eax,(%esp) //参数准备
8048ba2: e8 63 04 00 00 call 804900a //调用函数
8048ba7: 85 c0 test %eax,%eax //检查结果
8048ba9: 74 05 je 8048bb0
8048bab: e8 65 05 00 00 call 8049115
8048bb0: 83 c4 1c add $0x1c,%esp
8048bb3: c3 ret
可以看到在我们输入并且调用phase_1函数后,phase_1函数便开始准备参数并调用strings_not_equal函数,该函数的功能为判断两个字符串是否相等,相等则返回0,不相等则返回1;后面的test指令将两个操作数进行按位与,并根据结果设置各个标志位,test %eax,%eax就是判断eax是否为0,如果为0则标志位ZF会被设置为1,下面的je指令就会执行,从而跳过炸弹,就是说只有我们输入的字符串与程序保存的字符串相等才能不触发炸弹,至此我们可以判断:传进去的参数0x804a144即为题给字符串的首地址,下面我们调试程序来验证这一猜想:
可以看到,该位置确实存放着一个特殊的字符串,下面验证该字符串是否为phase_1的钥匙:
验证成功,该字符串确实是phase_1的钥匙,第一关通过。
phase_2的汇编代码如下:
08048bb4 :
8048bb4: 53 push %ebx
8048bb5: 83 ec 38 sub $0x38,%esp
8048bb8: 8d 44 24 18 lea 0x18(%esp),%eax
8048bbc: 89 44 24 04 mov %eax,0x4(%esp)
8048bc0: 8b 44 24 40 mov 0x40(%esp),%eax
8048bc4: 89 04 24 mov %eax,(%esp)
8048bc7: e8 70 05 00 00 call 804913c //读入六个数
8048bcc: 83 7c 24 18 00 cmpl $0x0,0x18(%esp) //与0相比
8048bd1: 79 22 jns 8048bf5 //小于0则跳转
8048bd3: e8 3d 05 00 00 call 8049115
8048bd8: eb 1b jmp 8048bf5
8048bda: 89 d8 mov %ebx,%eax
8048bdc: 03 44 9c 14 add 0x14(%esp,%ebx,4),%eax
8048be0: 39 44 9c 18 cmp %eax,0x18(%esp,%ebx,4)
8048be4: 74 05 je 8048beb
8048be6: e8 2a 05 00 00 call 8049115
8048beb: 83 c3 01 add $0x1,%ebx
8048bee: 83 fb 06 cmp $0x6,%ebx
8048bf1: 75 e7 jne 8048bda
8048bf3: eb 07 jmp 8048bfc
8048bf5: bb 01 00 00 00 mov $0x1,%ebx
8048bfa: eb de jmp 8048bda
8048bfc: 83 c4 38 add $0x38,%esp
8048bff: 5b pop %ebx
8048c00: c3 ret
根据汇编我们可以看到phase_2在开头就调用了read_six_numbers来读入六个数,起初我找到了read_six_numbers的汇编代码,但发现其中还有许多其他的调用,很难能全部看懂,其实根据函数名就可以猜出来该函数的作用就是读入六个数,所以说并不用在意他的实现细节,同时这也体现了规范命名的重要性,如果read_six_numbers是别的乱取的名字那就另当别论了。
在读入六个数之后,程序将$0和0x18(%esp)进行了对比,并设置了jns指令,意思是将0x18(%esp)减去0,如果结果的符号位不为1则跳转,而0x18(%esp)这个位置储存的是我们输入的第一个数,这就要求我们输入的第一个数必须大于等于0。
接下来跳转到8048bf5,这是一个循环5次的循环,该循环的目的是检验0x14(%esp,%ebx,4)+%ebx和0x18(%esp,%ebx,4)是否相等,不相等则引爆炸弹,0x14(%esp,%ebx,4)和0x18(%esp,%ebx,4)在数组中是相邻的元素,而ebx储存的是循环次数,有了这些信息,我们就可以复现这个循环:
for(int i = 1; i < 6; i++){
if(arr[i] + i != arr[i+1])
bomb();
}
从这里可以看出,题目的要求是让我们输入一个满足特定关系的数组,这个关系就包含在上面的循环中,假设输入的的第一个数是1,则第二个数为1+1=2,第三个数为2+2=4,第四个数为4+3=7,第五个数为7+4=11,第六个数为11+5=16,可以得出一个钥匙为:1 2 4 7 11 16,下面验证该钥匙是否正确:
验证结果正确,前面提到过,输入的第一个数只要求大于等于0,所以本题应该不止一个答案,0 1 3 6 10 15应该也是一个正确的key,下面进行验证:
验证结果正确,可以得到本题的通解: x+0 x+1 x+3 x+6 x+10 x+15,x为输入的第一个数,x>=0,当然,如果x大到超出表示范围也是不可以的。
第二关通过。
phase_3的汇编代码如下:
08048c01 :
8048c01: 83 ec 2c sub $0x2c,%esp
8048c04: 8d 44 24 1c lea 0x1c(%esp),%eax
8048c08: 89 44 24 0c mov %eax,0xc(%esp)
8048c0c: 8d 44 24 18 lea 0x18(%esp),%eax
8048c10: 89 44 24 08 mov %eax,0x8(%esp)
8048c14: c7 44 24 04 0f a3 04 movl $0x804a30f,0x4(%esp)
8048c1b: 08
8048c1c: 8b 44 24 30 mov 0x30(%esp),%eax
8048c20: 89 04 24 mov %eax,(%esp)
8048c23: e8 38 fc ff ff call 8048860 <__isoc99_sscanf@plt>
8048c28: 83 f8 01 cmp $0x1,%eax //参数不止一个
8048c2b: 7f 05 jg 8048c32
8048c2d: e8 e3 04 00 00 call 8049115
8048c32: 83 7c 24 18 07 cmpl $0x7,0x18(%esp)
8048c37: 77 3c ja 8048c75 //大于7跳转
8048c39: 8b 44 24 18 mov 0x18(%esp),%eax
8048c3d: ff 24 85 a0 a1 04 08 jmp *0x804a1a0(,%eax,4) //跳转表
8048c44: b8 9e 01 00 00 mov $0x19e,%eax //0
8048c49: eb 3b jmp 8048c86
8048c4b: b8 f6 01 00 00 mov $0x1f6,%eaxc //2
8048c50: eb 34 jmp 8048c86
8048c52: b8 de 00 00 00 mov $0xde,%eax //3
8048c57: eb 2d jmp 8048c86
8048c59: b8 4d 00 00 00 mov $0x4d,%eax //4
8048c5e: eb 26 jmp 8048c86
8048c60: b8 e0 03 00 00 mov $0x3e0,%eax //5
8048c65: eb 1f jmp 8048c86
8048c67: b8 48 01 00 00 mov $0x148,%eax //6
8048c6c: eb 18 jmp 8048c86
8048c6e: b8 8b 03 00 00 mov $0x38b,%eax //7
8048c73: eb 11 jmp 8048c86
8048c75: e8 9b 04 00 00 call 8049115
8048c7a: b8 00 00 00 00 mov $0x0,%eax
8048c7f: eb 05 jmp 8048c86
8048c81: b8 d6 00 00 00 mov $0xd6,%eax //1
8048c86: 3b 44 24 1c cmp 0x1c(%esp),%eax
8048c8a: 74 05 je 8048c91
8048c8c: e8 84 04 00 00 call 8049115
8048c91: 83 c4 2c add $0x2c,%esp
8048c94: c3 ret
从输入后的cmp $0x1,%eax可以判断本题输入的参数不止一个,如果只有一个便会直接引爆炸弹,而下面将第一个数与7相比,并且在大于7的时候跳转到炸弹,说明输入的第一个数必须小于等于7。再下面出现了一个jmp *0x804a1a0(,%eax,4)指令,次数的eax里的值经过上一个指令的处理变成了我们输入的第一个数,意思是将我们输入的第一个数作为偏移量,到相应的位置去取地址并跳转,此处的0x804a1a0其实是跳转表的首地址,下面我们调试查看跳转表的内容:
可以看到该跳转表里储存着许多指令地址,当输入的第一个数为0时,对应的将要跳转到的地址是:0x8048c44,该指令将$0x19e放入寄存器eax,后面的操作是将我们输入的第二个数与eax储存的数进行对比,不相等则引爆炸弹,至此可以分析出题目要求:输入一个数,并且找出它在switch语句里对应的第二个数,当第一个数为0时,第二个数应该为0x19e,即十进制的414,下面验证该key是否正确:
验证正确,根据题意可知本题不止一个答案,只需输入的两个数在switch语句中对应即可,第一个数为1时,对应跳转地址为0x8048c81,该地址处的数为0xd6,即十进制的214,下面验证该key是否正确:
验证正确,本题中其他答案对应的数已在汇编代码的注释中标明,第三关通过。
phase_4的汇编代码如下:
08048cf6 :
8048cf6: 83 ec 2c sub $0x2c,%esp
8048cf9: 8d 44 24 1c lea 0x1c(%esp),%eax
8048cfd: 89 44 24 0c mov %eax,0xc(%esp)
8048d01: 8d 44 24 18 lea 0x18(%esp),%eax
8048d05: 89 44 24 08 mov %eax,0x8(%esp)
8048d09: c7 44 24 04 0f a3 04 movl $0x804a30f,0x4(%esp)
8048d10: 08
8048d11: 8b 44 24 30 mov 0x30(%esp),%eax
8048d15: 89 04 24 mov %eax,(%esp)
8048d18: e8 43 fb ff ff call 8048860 <__isoc99_sscanf@plt>
8048d1d: 83 f8 02 cmp $0x2,%eax //两个参数
8048d20: 75 07 jne 8048d29
8048d22: 83 7c 24 18 0e cmpl $0xe,0x18(%esp)//对比第一个输入的数
8048d27: 76 05 jbe 8048d2e
8048d29: e8 e7 03 00 00 call 8049115
8048d2e: c7 44 24 08 0e 00 00 movl $0xe,0x8(%esp)//参数3
8048d35: 00
8048d36: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)//参数2
8048d3d: 00
8048d3e: 8b 44 24 18 mov 0x18(%esp),%eax//参数1
8048d42: 89 04 24 mov %eax,(%esp)
8048d45: e8 4b ff ff ff call 8048c95
8048d4a: 83 f8 07 cmp $0x7,%eax //对比返回值
8048d4d: 75 07 jne 8048d56
8048d4f: 83 7c 24 1c 07 cmpl $0x7,0x1c(%esp)//对比第二个数
8048d54: 74 05 je 8048d5b
8048d56: e8 ba 03 00 00 call 8049115
8048d5b: 83 c4 2c add $0x2c,%esp
8048d5e: c3 ret
phase_4在输入后检查了参数数量,要求输入两个参数,并且要求第一个数小于等于0xe,即十进制的14,否则便会引爆炸弹。接下来便是准备参数,调用func4,从汇编中可以看出,func4的第一个参数为我们输入的第一个数,第二个参数为0x0,即十进制的0,第三个参数为0xe,即十进制的14。再往下可以发现程序将func4的返回值与7进行了对比,且将输入的第二个数与7进行了对比,如果有一个不相等则引爆炸弹,所以输入的第二个数可以确定是7,输入的第一个数x要使func4(x,0,14)的返回值为7。
func4的汇编代码如下:
08048c95 :
8048c95: 56 push %esi
8048c96: 53 push %ebx
8048c97: 83 ec 14 sub $0x14,%esp
8048c9a: 8b 54 24 20 mov 0x20(%esp),%edx //x
8048c9e: 8b 44 24 24 mov 0x24(%esp),%eax //y
8048ca2: 8b 5c 24 28 mov 0x28(%esp),%ebx //z
8048ca6: 89 d9 mov %ebx,%ecx //ecx:z
8048ca8: 29 c1 sub %eax,%ecx //ecx:z-y
8048caa: 89 ce mov %ecx,%esi //esi:z-y
8048cac: c1 ee 1f shr $0x1f,%esi //右移31位
8048caf: 01 f1 add %esi,%ecx //ecx:z-y+sign
8048cb1: d1 f9 sar %ecx //ecx:(z-y)/2
8048cb3: 01 c1 add %eax,%ecx //ecx:y+(z-y)/2
8048cb5: 39 d1 cmp %edx,%ecx //对比x与%ecx
8048cb7: 7e 17 jle 8048cd0
8048cb9: 83 e9 01 sub $0x1,%ecx
8048cbc: 89 4c 24 08 mov %ecx,0x8(%esp)
8048cc0: 89 44 24 04 mov %eax,0x4(%esp)
8048cc4: 89 14 24 mov %edx,(%esp) //准备参数
8048cc7: e8 c9 ff ff ff call 8048c95
8048ccc: 01 c0 add %eax,%eax //返回值*2
8048cce: eb 20 jmp 8048cf0
8048cd0: b8 00 00 00 00 mov $0x0,%eax
8048cd5: 39 d1 cmp %edx,%ecx
8048cd7: 7d 17 jge 8048cf0
8048cd9: 89 5c 24 08 mov %ebx,0x8(%esp)
8048cdd: 83 c1 01 add $0x1,%ecx
8048ce0: 89 4c 24 04 mov %ecx,0x4(%esp)
8048ce4: 89 14 24 mov %edx,(%esp) //准备参数
8048ce7: e8 a9 ff ff ff call 8048c95
8048cec: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax //返回值*2+1
8048cf0: 83 c4 14 add $0x14,%esp
8048cf3: 5b pop %ebx
8048cf4: 5e pop %esi
8048cf5: c3 ret
如汇编代码所示,将三个参数分别记为x,y,z,程序首先对参数进行了一系列算术操作得到(z-y),然后又将其右移了31位,这个右移的目的是获得它的符号位,然后将该符号位sign与(z-y)相加再右移一位,不难猜出这个右移的目的是做除法操作除以2,但这个加上符号位又是干什么呢?起初我没有反应过来,后来想到在datalab中有一个类似的题目:用右移实现除法,这才想起来加上符号位是为了进行修正:右移向负无穷大取整,而除法是向0取整,加上符号位就可以对负数除法进行修正,让其向0取整。说了这么多,这其实就是一个除法操作,得到(z-y)/2,然后进行一个加法操作得到y+(z-y)/2。
再往下可以发现func4是一个递归函数当x
int cal(int y,int z){
return y + (z-y)/2;
}
func4(int x, int y, int z){
if(cal(y, z) < x)
return 2 * func4(x, cal(y, z)+1, z) + 1;
else if(cal(y, z) > x)
return 2 * func4(x, y, cal(y, z)-1);
else
return 0;
}
当然,源代码中并没有封装cal函数,该函数只是我为了方便观察而自己写的,它并不影响func4的功能,现在的任务就是func(x, 0, 14) = 7,需要求出x的值。
func4(x, 0, 14) = 2 * func4(x, 7+1, 14) + 1, func(x, 8, 14)需等于3
func4(x, 8, 14) = 2 * func4(x, 11+1, 14) + 1, func(x, 12, 14)需等于1
func4(x, 12 ,14) = 2 * func4(x, 13+1, 14) + 1, func4(x, 14, 14)需等于0
x = 14 + (14-14)/2 = 14
至此,推出第一个数为14,第二个数为7,下面验证该key是否正确:
验证正确,解出结果的时候我比较疑惑为什么第二个数没有做任何操作,而只是简单的判断输入的是否是7,可能是我的这个func4相对较难?我看参考思路里面的func4是关于斐波那契数列的,所以我猜想我的也有可能是与一些知名的数学猜想或问题有关,也正是有了这个想法,当我把自己的func4函数原型写出来时,我都不太自信,因为这个函数很奇怪,我只能看出它最终是要达到让x等于y,z的中位数的效果,至于返回值有什么特殊的数学意义我还不太清楚,也有可能这就是一个有名的数学问题而我不了解罢了。
第四关通过。
phase_5的汇编代码如下:
08048d5f :
8048d5f: 83 ec 2c sub $0x2c,%esp
8048d62: 8d 44 24 1c lea 0x1c(%esp),%eax
8048d66: 89 44 24 0c mov %eax,0xc(%esp)
8048d6a: 8d 44 24 18 lea 0x18(%esp),%eax
8048d6e: 89 44 24 08 mov %eax,0x8(%esp)
8048d72: c7 44 24 04 0f a3 04 movl $0x804a30f,0x4(%esp)
8048d79: 08
8048d7a: 8b 44 24 30 mov 0x30(%esp),%eax
8048d7e: 89 04 24 mov %eax,(%esp)
8048d81: e8 da fa ff ff call 8048860 <__isoc99_sscanf@plt>
8048d86: 83 f8 01 cmp $0x1,%eax //参数不止一个
8048d89: 7f 05 jg 8048d90
8048d8b: e8 85 03 00 00 call 8049115
8048d90: 8b 44 24 18 mov 0x18(%esp),%eax
8048d94: 83 e0 0f and $0xf,%eax //保留第一个字节
8048d97: 89 44 24 18 mov %eax,0x18(%esp)
8048d9b: 83 f8 0f cmp $0xf,%eax //检查是否等于15
8048d9e: 74 2a je 8048dca
8048da0: b9 00 00 00 00 mov $0x0,%ecx
8048da5: ba 00 00 00 00 mov $0x0,%edx //edx中为循环次数
8048daa: 83 c2 01 add $0x1,%edx
8048dad: 8b 04 85 c0 a1 04 08 mov 0x804a1c0(,%eax,4),%eax
8048db4: 01 c1 add %eax,%ecx //ecx保存每次eax值的累加
8048db6: 83 f8 0f cmp $0xf,%eax //循环条件
8048db9: 75 ef jne 8048daa
8048dbb: 89 44 24 18 mov %eax,0x18(%esp)
8048dbf: 83 fa 0f cmp $0xf,%edx //判断循环次数
8048dc2: 75 06 jne 8048dca
8048dc4: 3b 4c 24 1c cmp 0x1c(%esp),%ecx
8048dc8: 74 05 je 8048dcf
8048dca: e8 46 03 00 00 call 8049115
8048dcf: 83 c4 2c add $0x2c,%esp
8048dd2: c3 ret
首先,从输入过后的cmp语句可以知道本题的参数不止一个,后面进行了一个AND操作保留了第一个数的第一个字节,还对第一个数进行了检验,如果等于15会直接引爆炸弹。然后就进入了一个15次的循环,其实说是15次的循环不够妥当,该循环的退出条件是eax的值为15,退出后会检查循环次数,只有循环次数为15次才不会引爆炸弹,所以15次是在正确情况下才会循环15次。
观察循环体可知每次循环使用地址0x804a1c0(,%eax,4),%eax寻值,将寻到的值保存在eax中,直至eax的值为15,而每次循环会将eax的值累加到ecx中,最后会将输入的第二个数与ecx中的数比较,相等才不引爆炸弹,而第一个参数在循环开始之前就被放到eax中,作为第一次寻值时的地址偏移量,所以现在的目标就是通过eax最终的值为15这个信息反推,推出这15次循环中eax的值后,结果也就浮出水面了。由于寻址的基址为0x804a1c0,我们不妨通过调试将这个地址的信息打印出来,方便后续求解:
15保存在0x804a1c0 + 24,上一个eax的值是24/4 = 6
6 保存在0x804a1c0 + 56,上一个eax的值是56/4 = 14
14保存在0x804a1c0 + 8,上一个eax的值8/4 = 2
2 保存在0x804a1c0 + 4,上一个eax的值4/4 = 1
1 保存在0x804a1c0 + 40,上一个eax的值40/4 = 10
10保存在0x804a1c0 + 0,上一个eax的值0/4 = 0
0 保存在0x804a1c0 + 32,上一个eax的值32/4 = 8
8 保存在0x804a1c0 + 16,上一个eax的值16/4 = 4
4 保存在0x804a1c0 + 36,上一个eax的值36/4 = 9
9 保存在0x804a1c0 + 52,上一个eax的值52/4 = 13
13保存在0x804a1c0 + 44,上一个eax的值44/4 = 11
11保存在0x804a1c0 + 28,上一个eax的值28/4 = 7
7 保存在0x804a1c0 + 12,上一个eax的值12/4 = 3
3 保存在0x804a1c0 + 48,上一个eax的值48/4 = 12
至此可以得出第一个数为12,第二个数为eax的所有值的累加:0+1+2+…+15-5=115
现在验证该key是否正确:
这个key是错误的,为什么呢?前面已经提到过:而第一个参数在循环开始之前就被放到eax中,作为第一次寻值时的地址偏移量,所以说12应该是第一个数经过寻值后得到的数,也就是说要再往前反推一个数:12 保存在0x804a1c0 + 20,上一个eax的值20/4 = 5,
这个5才是真正的第一个数,下面验证这个新的key:
验证正确。不难发现,最后得到的第一个数就是5,由于只用作寻值,所以并没有加到ecx中,在解这一题时,找到反推的这种思路并没有花很多时间,反倒是一些小错误浪费了许多时间,比如上面提到的直接将12作为第一个数,还有就是把13当成0xc,所以说还是要细心一点。另外,phase_5中前面只保留第一个数的第一个字节的操作是多余的,但是在参考思路中的那个例子理解这一操作却很重要,我猜测这是在自动分配关卡时将这些操作保留了下来,不过对解题并无影响。
第五关通过。
phase_6的汇编代码如下:
Part1:
8048dd3: 56 push %esi
8048dd4: 53 push %ebx
8048dd5: 83 ec 44 sub $0x44,%esp
8048dd8: 8d 44 24 10 lea 0x10(%esp),%eax
8048ddc: 89 44 24 04 mov %eax,0x4(%esp)
8048de0: 8b 44 24 50 mov 0x50(%esp),%eax
8048de4: 89 04 24 mov %eax,(%esp)
8048de7: e8 50 03 00 00 call 804913c
8048dec: be 00 00 00 00 mov $0x0,%esi
8048df1: 8b 44 b4 10 mov 0x10(%esp,%esi,4),%eax
8048df5: 83 e8 01 sub $0x1,%eax
8048df8: 83 f8 05 cmp $0x5,%eax //输入的数需小于等于6
8048dfb: 76 05 jbe 8048e02
8048dfd: e8 13 03 00 00 call 8049115
8048e02: 83 c6 01 add $0x1,%esi
8048e05: 83 fe 06 cmp $0x6,%esi
8048e08: 75 07 jne 8048e11
8048e0a: bb 00 00 00 00 mov $0x0,%ebx
8048e0f: eb 38 jmp 8048e49
8048e11: 89 f3 mov %esi,%ebx
8048e13: 8b 44 9c 10 mov 0x10(%esp,%ebx,4),%eax
8048e17: 39 44 b4 0c cmp %eax,0xc(%esp,%esi,4)//查重
8048e1b: 75 05 jne 8048e22
8048e1d: e8 f3 02 00 00 call 8049115
8048e22: 83 c3 01 add $0x1,%ebx
8048e25: 83 fb 05 cmp $0x5,%ebx
8048e28: 7e e9 jle 8048e13
8048e2a: eb c5 jmp 8048df1
Part2:
8048e2c: 8b 52 08 mov 0x8(%edx),%edx //找到对应地址
8048e2f: 83 c0 01 add $0x1,%eax //找到对应地址
8048e32: 39 c8 cmp %ecx,%eax
8048e34: 75 f6 jne 8048e2c
8048e36: eb 05 jmp 8048e3d
8048e38: ba 3c c1 04 08 mov $0x804c13c,%edx
8048e3d: 89 54 b4 28 mov %edx,0x28(%esp,%esi,4) //储存地址
8048e41: 83 c3 01 add $0x1,%ebx
8048e44: 83 fb 06 cmp $0x6,%ebx
8048e47: 74 17 je 8048e60
8048e49: 89 de mov %ebx,%esi
8048e4b: 8b 4c 9c 10 mov 0x10(%esp,%ebx,4),%ecx //加载输入
8048e4f: 83 f9 01 cmp $0x1,%ecx
8048e52: 7e e4 jle 8048e38
8048e54: b8 01 00 00 00 mov $0x1,%eax
8048e59: ba 3c c1 04 08 mov $0x804c13c,%edx //链表首地址
8048e5e: eb cc jmp 8048e2c
Part3:
8048e60: 8b 5c 24 28 mov 0x28(%esp),%ebx//加载地址
8048e64: 8d 44 24 2c lea 0x2c(%esp),%eax//加载地址
8048e68: 8d 74 24 40 lea 0x40(%esp),%esi
8048e6c: 89 d9 mov %ebx,%ecx
8048e6e: 8b 10 mov (%eax),%edx
8048e70: 89 51 08 mov %edx,0x8(%ecx)
8048e73: 83 c0 04 add $0x4,%eax
8048e76: 39 f0 cmp %esi,%eax
8048e78: 74 04 je 8048e7e
8048e7a: 89 d1 mov %edx,%ecx
8048e7c: eb f0 jmp 8048e6e
8048e7e: c7 42 08 00 00 00 00 movl $0x0,0x8(%edx)
8048e85: be 05 00 00 00 mov $0x5,%esi
8048e8a: 8b 43 08 mov 0x8(%ebx),%eax
8048e8d: 8b 00 mov (%eax),%eax
8048e8f: 39 03 cmp %eax,(%ebx) //对比值
8048e91: 7e 05 jle 8048e98
8048e93: e8 7d 02 00 00 call 8049115
8048e98: 8b 5b 08 mov 0x8(%ebx),%ebx
8048e9b: 83 ee 01 sub $0x1,%esi
8048e9e: 75 ea jne 8048e8a
8048ea0: 83 c4 44 add $0x44,%esp
8048ea3: 5b pop %ebx
8048ea4: 5e pop %esi
8048ea5: c3
当我第一眼看到phase_6的代码时,我就注意到了密密麻麻的jump指令,可以说是没有调用,全是循环。可以看见前面还是调用了read_six_numbers,所以本关还是输入六个数字,其实phase_6可以从0x8048e2a处和0x8048e5e处分为三部分,第一部分为一个两重循环,目的是检测输入的各个数是否相等,而前面也有个cmpl语句,要求输入的数小于等于6。
第二部分为处理输入部分,可以看到第二部分有一个特殊的立即数:$0x804c13c,由格式不难猜出这是一个地址,下面用gdb调试查看该地址储存的内容:
可以看出这个数据结构是一个链表,第一个字节储存数据,第二个字节储存节点编号,而第三个字节存储指针,再接着看第二部分,0x10(%esp)处为输入数组的首地址,这样每次加载一个输入,令eax的初值为1,edx的初值为链表首地址,如果eax的值和加载的输入不相等则eax自增,并且edx+8,这样做的目的是使eax的值等于加载的输入时,edx中储存的正好是对应节点的地址,比如加载输入为3的话,最终edx中的值应该是节点3的首地址,而该地址最终会存入0x28(%esp,%esi,4),用于第三部分的检验。 现在第二部分的作用已经明了:将我们输入的数字对应的链表节点的首地址存入地址:0x28(%esp)。结合前面输入六个数小于等于6的要求我们不难猜出,本题是要输入特定的链表顺序。
第三部分仍然是一个循环,用于检验结果的正确,根据第二部分加载的地址顺序,如果前一个节点的值小于等于后一个节点的值则跳过炸弹,否则引爆炸弹,到这里题目的意思就十分明确了:输入链表值由大到小的顺序。根据上面的图片可以退出当前的链表内容:
节点 |
1 |
2 |
3 |
4 |
5 |
6 |
值 |
505 |
461 |
105 |
923 |
240 |
847 |
根据顺序,key应该是:3 5 2 1 6 4,下面检验该key是否正确:
验证正确。其实在做第六关时,我真的没什么头绪,因为这一大段汇编代码用到的跳转指令和相关变量实在是太多了,在看的时候我经常搞不清要往哪里跳转、寄存器储存的变量是什么,看了半天只知道这是一个链表数据结构,并且后面有一个类似排序的语句,然后我一想,排序?这一题不是正好输入六个数吗,会不会是链表由小到大的顺序?然后就将key输入了进去,结果真的通关了,再后来用gdb慢慢调试才慢慢看懂这一大段汇编。其实这一大段分成三部分的话还是没那么难理解的,但是初看谁又知道要分成三部分呢,而且第一部分感觉有点迷惑性,因为它好像对正确答案没什么影响。
第六关通过。
由于隐藏关是需要自己触发的,所以这一关实际上有两个任务:一是触发隐藏关,而是找到隐藏关的key。为了知道如何触发隐藏关,在导出的bomblab.txt文件中搜索secret,找到了调用secret_phase的函数phase_defused:
08049286 :
8049286: 81 ec 8c 00 00 00 sub $0x8c,%esp
804928c: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8049292: 89 44 24 7c mov %eax,0x7c(%esp)
8049296: 31 c0 xor %eax,%eax
8049298: 83 3d c8 c3 04 08 06 cmpl $0x6,0x804c3c8
804929f: 75 72 jne 8049313
80492a1: 8d 44 24 2c lea 0x2c(%esp),%eax
80492a5: 89 44 24 10 mov %eax,0x10(%esp)
80492a9: 8d 44 24 28 lea 0x28(%esp),%eax
80492ad: 89 44 24 0c mov %eax,0xc(%esp)
80492b1: 8d 44 24 24 lea 0x24(%esp),%eax
80492b5: 89 44 24 08 mov %eax,0x8(%esp)
80492b9: c7 44 24 04 69 a3 04 movl $0x804a369,0x4(%esp)
80492c0: 08
80492c1: c7 04 24 d0 c4 04 08 movl $0x804c4d0,(%esp)
80492c8: e8 93 f5 ff ff call 8048860 <__isoc99_sscanf@plt>
80492cd: 83 f8 03 cmp $0x3,%eax //三个参数触发隐藏关
80492d0: 75 35 jne 8049307
80492d2: c7 44 24 04 72 a3 04 movl $0x804a372,0x4(%esp)
80492d9: 08
80492da: 8d 44 24 2c lea 0x2c(%esp),%eax
80492de: 89 04 24 mov %eax,(%esp)
80492e1: e8 24 fd ff ff call 804900a
80492e6: 85 c0 test %eax,%eax
80492e8: 75 1d jne 8049307
80492ea: c7 04 24 38 a2 04 08 movl $0x804a238,(%esp)
80492f1: e8 fa f4 ff ff call 80487f0
80492f6: c7 04 24 60 a2 04 08 movl $0x804a260,(%esp)
80492fd: e8 ee f4 ff ff call 80487f0
8049302: e8 f0 fb ff ff call 8048ef7
8049307: c7 04 24 98 a2 04 08 movl $0x804a298,(%esp)
804930e: e8 dd f4 ff ff call 80487f0
8049313: 8b 44 24 7c mov 0x7c(%esp),%eax
8049317: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
804931e: 74 05 je 8049325
8049320: e8 9b f4 ff ff call 80487c0 <__stack_chk_fail@plt>
8049325: 81 c4 8c 00 00 00 add $0x8c,%esp
804932b: c3 ret
804932c: 66 90 xchg %ax,%ax
804932e: 66 90 xchg %ax,%ax
由于phase_defused每一关都会调用,所以有一些对隐藏关没用的信息,我们只需关注调用secret_phase的那一部分即可,可以调用语句之前的一些语句有很多地址格式的立即数,并且有字符串相等函数的调用,我们可以用gdb调试看看这些地址储存的字符串,也许会对解题有帮助:
可以看到有三个特殊的字符串,前两个很明显是找到隐藏关之后的引导词,而第三个字符串很可能是通往隐藏关的钥匙,可是这个钥匙应该往哪放呢?
再往上研究引出调用secret_phase的部分,又可以发现两个可疑的立即数,同样用gdb查看该地址是否有有用的字符串:
到这一步可以说是豁然开朗了,14和7不正是我们在第四关输入的key吗,而上面的“%d %d %s”也提示我们第四关可以输入一个字符串,而后面的cmp $0x3,%eax更验证了这一点:如果第四关输入了三个参数则触发隐藏关,否则直接跳转至0x8049307,这里也有一个立即数,查看内容为:
这正是不触发隐藏关时的结束语,下面我们验证上面的结论:
验证成功,触发隐藏关。
隐藏关的汇编代码如下:
08048ef7 :
8048ef7: 53 push %ebx
8048ef8: 83 ec 18 sub $0x18,%esp
8048efb: e8 8c 02 00 00 call 804918c
8048f00: c7 44 24 08 0a 00 00 movl $0xa,0x8(%esp)
8048f07: 00
8048f08: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
8048f0f: 00
8048f10: 89 04 24 mov %eax,(%esp)
8048f13: e8 b8 f9 ff ff call 80488d0
8048f18: 89 c3 mov %eax,%ebx
8048f1a: 8d 40 ff lea -0x1(%eax),%eax
8048f1d: 3d e8 03 00 00 cmp $0x3e8,%eax //输入的数小于等于1001
8048f22: 76 05 jbe 8048f29
8048f24: e8 ec 01 00 00 call 8049115
8048f29: 89 5c 24 04 mov %ebx,0x4(%esp) //参数2
8048f2d: c7 04 24 88 c0 04 08 movl $0x804c088,(%esp)//参数1
8048f34: e8 6d ff ff ff call 8048ea6
8048f39: 83 f8 07 cmp $0x7,%eax //检查返回值
8048f3c: 74 05 je 8048f43
8048f3e: e8 d2 01 00 00 call 8049115
8048f43: c7 04 24 70 a1 04 08 movl $0x804a170,(%esp)
8048f4a: e8 a1 f8 ff ff call 80487f0
8048f4f: e8 32 03 00 00 call 8049286
8048f54: 83 c4 18 add $0x18,%esp
8048f57: 5b pop %ebx
8048f58: c3 ret
8048f59: 66 90 xchg %ax,%ax
8048f5b: 66 90 xchg %ax,%ax
8048f5d: 66 90 xchg %ax,%ax
8048f5f: 90
在前面可以看到调用的strtol函数,该函数将输入的字符串转化成数字,再往下有cmp指令,要求输入的数字小于等于1001,再接下来便是准备参数的环节,准备调用fun7函数,可以看到在调用完fun7函数后,对fun7函数的返回值进行了检查,只有返回值等于7时才不会引爆炸弹,因此本题的关键在函数fun7上,只要fun7的返回值等于7即可。
fun7的汇编代码如下:
08048ea6 :
8048ea6: 53 push %ebx
8048ea7: 83 ec 18 sub $0x18,%esp
8048eaa: 8b 54 24 20 mov 0x20(%esp),%edx
8048eae: 8b 4c 24 24 mov 0x24(%esp),%ecx
8048eb2: 85 d2 test %edx,%edx
8048eb4: 74 37 je 8048eed
8048eb6: 8b 1a mov (%edx),%ebx
8048eb8: 39 cb cmp %ecx,%ebx
8048eba: 7e 13 jle 8048ecf
8048ebc: 89 4c 24 04 mov %ecx,0x4(%esp)
8048ec0: 8b 42 04 mov 0x4(%edx),%eax
8048ec3: 89 04 24 mov %eax,(%esp)
8048ec6: e8 db ff ff ff call 8048ea6
8048ecb: 01 c0 add %eax,%eax
8048ecd: eb 23 jmp 8048ef2
8048ecf: b8 00 00 00 00 mov $0x0,%eax
8048ed4: 39 cb cmp %ecx,%ebx
8048ed6: 74 1a je 8048ef2
8048ed8: 89 4c 24 04 mov %ecx,0x4(%esp)
8048edc: 8b 42 08 mov 0x8(%edx),%eax
8048edf: 89 04 24 mov %eax,(%esp)
8048ee2: e8 bf ff ff ff call 8048ea6
8048ee7: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax
8048eeb: eb 05 jmp 8048ef2
8048eed: b8 ff ff ff ff mov $0xffffffff,%eax
8048ef2: 83 c4 18 add $0x18,%esp
8048ef5: 5b pop %ebx
8048ef6: c3 ret
不难看出fun7是一个递归函数,secret_phase在调用fun7时传入的两个初始参数是0x804c088和我们输入的那个数,下面用gdb调试查看参数1储存的内容:
可以看出该地址是一种特殊数据结构的首地址,该数据结构第一个字节存放数据,第二和第三个字节存放两个指针,到这里已经可以猜出这个数据结构是什么了,就是二叉树,并且通过变量的命名也能验证这一结论,n1应该是根节点,n21是第二层第一个节点,以此类推…
接下来看汇编的内容就比较好理解了,edx储存的是当前节点的首地址,edx+8储存的是它的右子节点的首地址,edx+4储存的是它的左子节点的首地址,如果当前节点数据域等于我们输入的数则返回0,大于则返回2*fun7(左子节点首地址,y),小于则返回2*fun7(右子节点首地址,y)+1,下面给出函数原型:
int fun7(int *btree, int y){
if(btree->data == y)
return 0;
else if(btree->data < y)
return 2 * fun7(btree->rchild, y) + 1;
else
return 2 * fun7(btree->lchild, y);
}
y的值即为我们要输入的内容,接下来反推y的值:
fun7(0x804c088, y) = 7 = 2 * fun7(0x804c0a0, y) + 1,fun7(0x804c0a0, y)应等于3
fun7(0x804c0a0, y) = 3 = 2 * fun7(0x804c0d0, y) + 1,fun7(0x804c0d0, y)应等于1
fun7(0x804c0d0, y) = 7 = 2 * fun7(0x804c130, y) + 1,fun7(0x804c130, y)应等于0
注意此处传递参数不是简单的将地址加8或加4,而是要拿加8或加4处储存的地址作为参数。0x804c130处的节点储存的数据为0x3e9,即十进制的1001,其实fun7(0x804c130, y)也可以等于2 * fun7(*(0x804c134), y)),让fun7(*(0x804c134), y))等于0,但是由于0x804c130处的节点无子节点,所以此路不通,这一切都是题目设计好的,最后,得出secret_phase的key为1001,下面验证该key:
验证成功,隐藏关通过。
至此,全部七关已全部通过。
本次实验的参考答案为:
And they have no disregard for human life.
1 2 4 7 11 16
0 414
14 7 DrEvil
5 115
3 5 2 1 6 4
1001
其中关卡2、3的答案不唯一,其他答案已在解题过程中指出。