开始之前
开始紧张刺激的拆炸弹之旅之前,先了解一下以下几点:
1)如何使用gdb调试
在解压后的炸弹目录下输入命令:
gdb bomb
2)如何查看反汇编代码
方法一:进入gdb调试后,可以使用disassemble命令+函数名,可实时查看该函数的反汇编代码。
方法二:在终端输入:
objdump -d bomb>bomb.out
会在当前目录下生成一个bomb.out文件,可用vim查看,文件内容就是整个bomb的反汇编代码。
3)小技巧
1.爆炸的原理是在输入的答案经过判定不正确后调用了一个函数名为explode_bomb的爆炸函数。
进入gdb调试后,输入:
b explode_bomb
即在爆炸函数打断点,这样在爆炸前程序执行就会中断。
2.遇见传入一个地址值的mov语句,就用p或者x命令查看一下这个地址上的内容,要么直接就是答案要么是要输入的答案格式要么就是和答案有关的内容。
3.通常看到调用的一些函数,可以直接看函数名,然后结合c语言的函数推测函数的作用和返回值。
phase_1
<main>部分代码:
8048a1f: e8 f2 07 00 00 call 8049216 <read_line>
//读取一行输入的内容(字符串形式),返回首地址
8048a24: 89 04 24 mov %eax,(%esp)//输入内容的首地址压栈
8048a27: e8 b4 00 00 00 call 8048ae0 <phase_1>
08048ae0 <phase_1>:
8048ae0: 55 push %ebp
8048ae1: 89 e5 mov %esp,%ebp
8048ae3: 83 ec 18 sub $0x18,%esp
(此后会省略以上重复的内容)
8048ae6: c7 44 24 04 50 a1 04 movl $0x804a150,0x4(%esp)
8048aed: 08
8048aee: 8b 45 08 mov 0x8(%ebp),%eax
8048af1: 89 04 24 mov %eax,(%esp)
8048af4: e8 69 04 00 00 call 8048f62 <strings_not_equal>
//调用字符串比较函数strings_not_equal
8048af9: 85 c0 test %eax,%eax
//根据eax中返回值是否为0设置标志位
8048afb: 74 05 je 8048b02 <phase_1+0x22>
//若返回值为零则跳转,否则执行下一句调用爆炸函数
8048afd: e8 83 06 00 00 call 8049185 <explode_bomb>
8048b02: c9 leave
8048b03: c3 ret
即比较了地址0x804a150上的内容和输入的内容是否一致,直接扫描内存即可。
phase_2
08048b04 <phase_2>:
8048b04: 55 push %ebp
8048b05: 89 e5 mov %esp,%ebp
8048b07: 53 push %ebx
8048b08: 83 ec 34 sub $0x34,%esp
8048b0b: 8d 45 e0 lea -0x20(%ebp),%eax
8048b0e: 89 44 24 04 mov %eax,0x4(%esp)
8048b12: 8b 45 08 mov 0x8(%ebp),%eax
8048b15: 89 04 24 mov %eax,(%esp)
8048b18: e8 aa 06 00 00 call 80491c7 <read_six_numbers>
由此可知read_six_number的参数除输入的内容外,还有一个ebp-0x20,结合后面代码以及考虑到栈中还有很多空间开辟后未使用,可推出读取的六个数字以类似数组形式存储在了ebp-0x20开始的连续空间中(具体可看read_six_number的代码)。
8048b1d: 83 7d e0 00 cmpl $0x0,-0x20(%ebp)
8048b21: 79 22 jns 8048b45 <phase_2+0x41>
8048b23: e8 5d 06 00 00 call 8049185 <explode_bomb>
8048b28: eb 1b jmp 8048b45 <phase_2+0x41>
/*循环体*/
8048b2a: 89 d8 mov %ebx,%eax //ebx=eax
8048b2c: 03 44 5d dc add -0x24(%ebp,%ebx,2),%eax
//eax+=*(ebp+2*ebx-0x24)
8048b30: 39 44 5d e0 cmp %eax,-0x20(%ebp,%ebx,2)
//比较eax 和 *(ebp+2*ebx-0x20)
8048b34: 74 05 je 8048b3b <phase_2+0x37>
8048b36: e8 4a 06 00 00 call 8049185 <explode_bomb>
8048b3b: 83 c3 02 add $0x2,%ebx //ebx+=2
8048b3e: 83 fb 0c cmp $0xc,%ebx //比较ebx和0xc
8048b41: 75 e7 jne 8048b2a <phase_2+0x26>
//若ebx不等于12,继续循环
/*循环初始化*/
8048b43: eb 07 jmp 8048b4c <phase_2+0x48>
8048b45: bb 02 00 00 00 mov $0x2,%ebx //初始化ebx=2
/*循环结束*/
8048b4a: eb de jmp 8048b2a <phase_2+0x26>
8048b4c: 83 c4 34 add $0x34,%esp
8048b4f: 5b pop %ebx
8048b50: 5d pop %ebp
8048b51: c3 ret
上述注释结合起来就是:
for ebx != 12
eax = ebx
eax += *(ebp + 2 * ebx - 0x24)
if eax == *(ebp + 2 * ebx - 0x20) then
ebx += 2
else explode
end for
转化为C语言代码:
for(ebx = 2; ebx != 12) {
eax = ebx;
eax += a[ebx/2 - 1];
if(eax == a[ebx/2])
ebx += 2;
else explode();
}
即输入的六个数为首项大于等于0且符合a[i]-a[i-1]=2*i(i>0)的二阶等差数列。
phase_3
8048a5b: e8 b6 07 00 00 call 8049216 <read_line>
8048a60: 89 04 24 mov %eax,(%esp)
8048a63: e8 ea 00 00 00 call 8048b52 <phase_3>
08048b52 <phase_3>:
8048b52: 55 push %ebp
8048b53: 89 e5 mov %esp,%ebp
8048b55: 83 ec 28 sub $0x28,%esp
8048b58: 8d 45 f0 lea -0x10(%ebp),%eax
8048b5b: 89 44 24 0c mov %eax,0xc(%esp)
8048b5f: 8d 45 f4 lea -0xc(%ebp),%eax
8048b62: 89 44 24 08 mov %eax,0x8(%esp)
8048b66: c7 44 24 04 b1 a3 04 movl $0x804a3b1,0x4(%esp)
8048b6d: 08
8048b6e: 8b 45 08 mov 0x8(%ebp),%eax
8048b71: 89 04 24 mov %eax,(%esp)
8048b74: e8 67 fc ff ff call 80487e0 <__isoc99_sscanf@plt>
__isoc99_sscanf@plt有四个参数,扫描可得有一个参数内容为”%d %d”,然后联想C语言sscanf函数的作用,可知要输入两个整数,这两个整数会存在ebp-0xc和ebp-0x10位置上(注意压栈和参数顺序的关系)。
8048b79: 83 f8 01 cmp $0x1,%eax
8048b7c: 7f 05 jg 8048b83 <phase_3+0x31>
//__isoc99_sscanf@plt读出的参数个数应大于1
8048b7e: e8 02 06 00 00 call 8049185 <explode_bomb>
8048b83: 83 7d f4 07 cmpl $0x7,-0xc(%ebp)
8048b87: 77 5b ja 8048be4 <phase_3+0x92>
//第一个数不大于7且大于等于0
8048b89: 8b 45 f4 mov -0xc(%ebp),%eax
8048b8c: ff 24 85 b0 a1 04 08 jmp *0x804a1b0(,%eax,4)
//根据第一个数选择跳转的地址(注意*号和没有*号的区别)
8048b93: b8 00 00 00 00 mov $0x0,%eax
8048b98: eb 05 jmp 8048b9f <phase_3+0x4d>
8048b9a: b8 9e 03 00 00 mov $0x39e,%eax
8048b9f: 2d 5e 03 00 00 sub $0x35e,%eax
8048ba4: eb 05 jmp 8048bab <phase_3+0x59>
8048ba6: b8 00 00 00 00 mov $0x0,%eax
8048bab: 05 d3 02 00 00 add $0x2d3,%eax
8048bb0: eb 05 jmp 8048bb7 <phase_3+0x65>
8048bb2: b8 00 00 00 00 mov $0x0,%eax
8048bb7: 83 e8 4e sub $0x4e,%eax
8048bba: eb 05 jmp 8048bc1 <phase_3+0x6f>
8048bbc: b8 00 00 00 00 mov $0x0,%eax
8048bc1: 83 c0 4e add $0x4e,%eax
8048bc4: eb 05 jmp 8048bcb <phase_3+0x79>
8048bc6: b8 00 00 00 00 mov $0x0,%eax
8048bcb: 83 e8 4e sub $0x4e,%eax
8048bce: eb 05 jmp 8048bd5 <phase_3+0x83>
8048bd0: b8 00 00 00 00 mov $0x0,%eax
8048bd5: 83 c0 4e add $0x4e,%eax
8048bd8: eb 05 jmp 8048bdf <phase_3+0x8d>
8048bda: b8 00 00 00 00 mov $0x0,%eax
8048bdf: 83 e8 4e sub $0x4e,%eax
8048be2: eb 0a jmp 8048bee <phase_3+0x9c>
8048be4: e8 9c 05 00 00 call 8049185 <explode_bomb>
8048be9: b8 00 00 00 00 mov $0x0,%eax
8048bee: 83 7d f4 05 cmpl $0x5,-0xc(%ebp)
8048bf2: 7f 05 jg 8048bf9 <phase_3+0xa7>
//第一个数不大于5
8048bf4: 3b 45 f0 cmp -0x10(%ebp),%eax
8048bf7: 74 05 je 8048bfe <phase_3+0xac>
//第二个数要等于eax
8048bf9: e8 87 05 00 00 call 8049185 <explode_bomb>
8048bfe: c9 leave
8048bff: 90 nop
8048c00: c3 ret
即要输入的第一个数需要大于等于0且小于等于5的数,然后根据第一个数会选择一个跳转目的地址(比如第一个数为0,则跳转的地址就是0x804a1b0上的内容),然后会给eax一个值,之后顺序执行,会对eax做一系列的加减法,最后eax的值要和第二个数相等。
phase_4
08048c5f <phase_4>:
8048c5f: 55 push %ebp
8048c60: 89 e5 mov %esp,%ebp
8048c62: 83 ec 28 sub $0x28,%esp
8048c65: 8d 45 f0 lea -0x10(%ebp),%eax
8048c68: 89 44 24 0c mov %eax,0xc(%esp)
8048c6c: 8d 45 f4 lea -0xc(%ebp),%eax
8048c6f: 89 44 24 08 mov %eax,0x8(%esp)
8048c73: c7 44 24 04 b1 a3 04 movl $0x804a3b1,0x4(%esp)
8048c7a: 08
8048c7b: 8b 45 08 mov 0x8(%ebp),%eax
8048c7e: 89 04 24 mov %eax,(%esp)
8048c81: e8 5a fb ff ff call 80487e0 <__isoc99_sscanf@plt>
可以看到目前为止都和phase3的代码一致,唯一差别为压栈的地址值不同。同样扫描这个地址得到内容为:“%d %d”,因此这里同样是要输入两个整数。
8048c86: 83 f8 02 cmp $0x2,%eax
8048c89: 75 06 jne 8048c91 <phase_4+0x32>
//获取到的参数个数应等于2
8048c8b: 83 7d f4 0e cmpl $0xe,-0xc(%ebp)
8048c8f: 76 05 jbe 8048c96 <phase_4+0x37>
//第一个数应小于等于0xe
8048c91: e8 ef 04 00 00 call 8049185 <explode_bomb>
8048c96: c7 44 24 08 0e 00 00 movl $0xe,0x8(%esp)
8048c9d: 00 //0xe压栈
8048c9e: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
8048ca5: 00 //0x0压栈
8048ca6: 8b 45 f4 mov -0xc(%ebp),%eax
8048ca9: 89 04 24 mov %eax,(%esp)
//第一个数压栈
8048cac: e8 50 ff ff ff call 8048c01 <func4>//调用func4
8048cb1: 83 f8 07 cmp $0x7,%eax
8048cb4: 75 06 jne 8048cbc <phase_4+0x5d>
//返回值应当为7
8048cb6: 83 7d f0 07 cmpl $0x7,-0x10(%ebp)
8048cba: 74 05 je 8048cc1 <phase_4+0x62>
//第二个数应当为7
8048cbc: e8 c4 04 00 00 call 8049185 <explode_bomb>
8048cc1: c9 leave
8048cc2: c3 ret
根据以上部分代码可知:第一个数应当在[0,14]的区间内,第二个数为7,同时还需要满足func4(x,0,14)的返回值为7(设第一个数为x)。因此接下来分析func4的作用,根据返回值反向推出第一个数即可。
08048c01 <func4>:
8048c01: 55 push %ebp
8048c02: 89 e5 mov %esp,%ebp
8048c04: 56 push %esi
8048c05: 53 push %ebx
8048c06: 83 ec 10 sub $0x10,%esp
8048c09: 8b 55 08 mov 0x8(%ebp),%edx //第一个参数赋值给edx
8048c0c: 8b 45 0c mov 0xc(%ebp),%eax//第二个参数赋值给eax
8048c0f: 8b 75 10 mov 0x10(%ebp),%esi//第三个参数赋值给esi
8048c12: 89 f1 mov %esi,%ecx //ecx = esi
8048c14: 29 c1 sub %eax,%ecx //ecx -= eax
8048c16: 89 cb mov %ecx,%ebx //ebx = ecx
8048c18: c1 eb 1f shr $0x1f,%ebx //ebx逻辑右移31位
8048c1b: 01 d9 add %ebx,%ecx //ecx +=ebx
8048c1d: d1 f9 sar %ecx //ecx算术右移1位
8048c1f: 8d 1c 01 lea (%ecx,%eax,1),%ebx//ebx = ecx + eax
8048c22: 39 d3 cmp %edx,%ebx //比较edx和ebx
8048c24: 7e 17 jle 8048c3d <func4+0x3c>
/*ebx > edx*/
8048c26: 8d 4b ff lea -0x1(%ebx),%ecx //ecx = ebx - 1
8048c29: 89 4c 24 08 mov %ecx,0x8(%esp) //ecx压栈
8048c2d: 89 44 24 04 mov %eax,0x4(%esp) //eax压栈
8048c31: 89 14 24 mov %edx,(%esp) //edx压栈
8048c34: e8 c8 ff ff ff call 8048c01 <func4>//调用func4
8048c39: 01 d8 add %ebx,%eax //eax += ebx
8048c3b: eb 1b jmp 8048c58 <func4+0x57>
/*ebx <= edx*/
8048c3d: 89 d8 mov %ebx,%eax //eax = ebx
8048c3f: 39 d3 cmp %edx,%ebx //比较edx和ebx
8048c41: 7d 15 jge 8048c58 <func4+0x57>
/*ebx < edx*/
8048c43: 89 74 24 08 mov %esi,0x8(%esp) //esi压栈
8048c47: 8d 43 01 lea 0x1(%ebx),%eax //eax = ebx + 1
8048c4a: 89 44 24 04 mov %eax,0x4(%esp) //eax压栈
8048c4e: 89 14 24 mov %edx,(%esp) //edx压栈
8048c51: e8 ab ff ff ff call 8048c01 <func4>//调用func4
8048c56: 01 d8 add %ebx,%eax //eax += ebx
/*恢复调用现场*/
8048c58: 83 c4 10 add $0x10,%esp //释放栈空间
8048c5b: 5b pop %ebx
8048c5c: 5e pop %esi
8048c5d: 5d pop %ebp
8048c5e: c3 ret
上述注释内容整合起来就是:
edx = a
eax = b
esi = c
ecx = esi
ecx -= eax
ebx =ecx
ebx >> 0x1f //逻辑右移
ecx += ebx
ecx >> 1 //算术右移
ebx = ecx + eax
if ebx <= edx then
eax = ebx
if ebx >= edx then
return
else return func4(edx,ebx + 1,esi) + ebx
end if
else return func4(edx,eax,ecx-1) + ebx
end if
其中a、b、c依次表示func4的三个参数。
由于数字的范围可以确定是[0,14]的子集,则逻辑右移31位的结果一定为0,算术右移1位可以等效于/2操作。可以列表手动运行,得到函数返回前各个寄存器的内容如下:
register | val |
---|---|
edx | a |
eax | b |
esi | c |
ecx | [c-b+0]/2 |
ebx | [c-b+0]/2+b |
则func4转化为C语言后如下:
int func4(a,b,c){
if((c-b)/2+b < a)
return func4(a,(c-b)/2+b+1,c) + (c-b)/2+b;
else if((c-b)/2+b = a)
return a;
else if((c-b)/2+b > a)
return func4(a,b,(c-b)/2-1) + (c-b)/2+b;
}
实质上func4就是一个递归实现的二分搜索过程,每一次划分都会将最终的返回值加上作为划分标志的数(比如[8,14]的中间数为11,则返回值会加11),最终查找到a后,返回值再加a。
二分搜索树结构如下:
7
3 11
1 5 9 13
0 2 4 6 8 10 12 14
各自对应的返回值为:
7
10 18
11 15 27 31
11 13 9 21 35 37 43 45
即第一个数就是7
phase_5
08048cc3 <phase_5>:
8048cc3: 55 push %ebp
8048cc4: 89 e5 mov %esp,%ebp
8048cc6: 53 push %ebx
8048cc7: 83 ec 24 sub $0x24,%esp
8048cca: 8b 5d 08 mov 0x8(%ebp),%ebx
//将输入的内容的首地址传递给ebx
8048ccd: 89 1c 24 mov %ebx,(%esp)
8048cd0: e8 6b 02 00 00 call 8048f40 <string_length>
//获取输入的字符串长度
8048cd5: 83 f8 06 cmp $0x6,%eax
8048cd8: 74 48 je 8048d22 <phase_5+0x5f>
//长度应为6
8048cda: e8 a6 04 00 00 call 8049185 <explode_bomb>
8048cdf: 90 nop
8048ce0: eb 40 jmp 8048d22 <phase_5+0x5f>
//跳转至循环初始化部分
易知这里是要输入长度为6的字符串,同时注意ebx中存储了输入的字符串的首地址,在下面的代码分析中会用到。
/*循环体*/
8048ce2: 0f b6 14 03 movzbl (%ebx,%eax,1),%edx
//将第eax个字符存入edx
8048ce6: 83 e2 0f and $0xf,%edx //取edx低4位
8048ce9: 0f b6 92 d0 a1 04 08 movzbl 0x804a1d0(%edx),%edx
//将0x804a1d0地址上的第edx个元素存入edx
8048cf0: 88 54 05 f1 mov %dl,-0xf(%ebp,%eax,1)
//将edx低8位传递到从ebp-0xf位置开始的第eax个空间
//ebp-0xf可以看成是另一个数组的首地址
8048cf4: 83 c0 01 add $0x1,%eax
8048cf7: 83 f8 06 cmp $0x6,%eax
8048cfa: 75 e6 jne 8048ce2 <phase_5+0x1f>
//eax自增,当eax不等于6的时候,继续循环
//即循环6次,eax取值为0~6
/*检验答案*/
8048cfc: c6 45 f7 00 movb $0x0,-0x9(%ebp)
//添加终止符
8048d00: c7 44 24 04 a6 a1 04 movl $0x804a1a6,0x4(%esp)
8048d07: 08 //将0x804a1a6压栈
8048d08: 8d 45 f1 lea -0xf(%ebp),%eax
8048d0b: 89 04 24 mov %eax,(%esp)
//将ebp-0xf压栈(意义见循环体)
8048d0e: e8 4f 02 00 00 call 8048f62 <strings_not_equal>
8048d13: 85 c0 test %eax,%eax
8048d15: 74 12 je 8048d29 <phase_5+0x66>
//0x804a1a6和ebp-0xf位置上的内容应相等
8048d17: e8 69 04 00 00 call 8049185 <explode_bomb>
8048d1c: 8d 74 26 00 lea 0x0(%esi,%eiz,1),%esi
8048d20: eb 07 jmp 8048d29 <phase_5+0x66>
/*循环初始化*/
8048d22: b8 00 00 00 00 mov $0x0,%eax //初始化eax为0
8048d27: eb b9 jmp 8048ce2 <phase_5+0x1f>
/*函数结束*/
8048d29: 83 c4 24 add $0x24,%esp
8048d2c: 5b pop %ebx
8048d2d: 5d pop %ebp
8048d2e: c3 ret
循环体的作用就是依次对字符串的每个字符进行两次变换,其中一次变换需要用到数组A(首地址为0x804a1d0),然后存入另一个数组B(首地址为ebp-0xf)对应的位置,要求是数组B中的内容要和指定数组C(首地址0x804a1a6)的内容相同。简单来说就是有一个函数关系:C = f(input) = A[input & 0xf] & 0xff,已知C和f,求input。
PS:ascll码中会有多个字符低4位相同,因此答案不唯一
phase_6
本关循环较多,跳转多,汇编代码看起来比较绕,但还是能清晰分为几块(所以C语言转换就不写了ㄟ( ▔, ▔ )ㄏ,看注释再结合几个图应该很好理解了)。
整个过程整理如下:
扫描代码中唯一出现的地址,结果如下:
可以看到每个node都有三个部分:一个值(val)、编号(id)、还有一个地址(next)
即本关的数据结构为链表,有几点看注释前需要先注意:
1)仔细看可以发现链表原本是按id顺序排好的。
2)假设ebp为某个结点地址,则0x8(%ebp)等效于ebp→next
8048ab5: e8 5c 07 00 00 call 8049216 <read_line>
8048aba: 89 04 24 mov %eax,(%esp)
8048abd: e8 6d 02 00 00 call 8048d2f <phase_6>
08048d2f <phase_6>:
8048d2f: 55 push %ebp
8048d30: 89 e5 mov %esp,%ebp
8048d32: 56 push %esi
8048d33: 53 push %ebx
8048d34: 83 ec 40 sub $0x40,%esp
8048d37: 8d 45 e0 lea -0x20(%ebp),%eax
8048d3a: 89 44 24 04 mov %eax,0x4(%esp)
8048d3e: 8b 45 08 mov 0x8(%ebp),%eax
8048d41: 89 04 24 mov %eax,(%esp)
8048d44: e8 7e 04 00 00 call 80491c7 <read_six_numbers>
//以上部分同第二关类似
/*初步检测输入是否符合要求*/
/*外层循环,检测输入数字大小范围*/
8048d49: be 00 00 00 00 mov $0x0,%esi //初始化esi=0
8048d4e: 8b 44 b5 e0 mov -0x20(%ebp,%esi,4),%eax
//取数组的第esi个元素给eax
8048d52: 83 e8 01 sub $0x1,%eax //eax-=1
8048d55: 83 f8 05 cmp $0x5,%eax
8048d58: 76 05 jbe 8048d5f <phase_6+0x30>
//eax的值应在[0,5]的区间
//即输入的每个数都应在[1,6]的区间
8048d5a: e8 26 04 00 00 call 8049185 <explode_bomb>
8048d5f: 83 c6 01 add $0x1,%esi //esi加1
8048d62: 83 fe 06 cmp $0x6,%esi
8048d65: 75 07 jne 8048d6e <phase_6+0x3f>
//若esi不等于6,继续循环
//esi==6
8048d67: bb 00 00 00 00 mov $0x0,%ebx //初始化ebx=0
8048d6c: eb 38 jmp 8048da6 <phase_6+0x77>
//结束循环
//esi!=6,内层循环,检测数字是否各不相同
8048d6e: 89 f3 mov %esi,%ebx //初始化ebx=esi
8048d70: 8b 44 9d e0 mov -0x20(%ebp,%ebx,4),%eax
//取数组的第ebx个元素给eax
8048d74: 39 44 b5 dc cmp %eax,-0x24(%ebp,%esi,4)
8048d78: 75 05 jne 8048d7f <phase_6+0x50>
//第esi个元素应当和第ebx个元素值不相等
8048d7a: e8 06 04 00 00 call 8049185 <explode_bomb>
8048d7f: 83 c3 01 add $0x1,%ebx //ebx自增
8048d82: 83 fb 05 cmp $0x5,%ebx
8048d85: 7e e9 jle 8048d70 <phase_6+0x41>
8048d87: eb c5 jmp 8048d4e <phase_6+0x1f>
//若ebx大于5,则结束内层循环
此时栈的结构如下:
/*结点按输入的id顺序重新将地址排序并记录*/
8048d89: 8b 52 08 mov 0x8(%edx),%edx
//edx=edx->next
8048d8c: 83 c0 01 add $0x1,%eax //eax加一
//PS:由于链表初始按id顺序排列,eax自增等效于下一个结点的id
8048d8f: 39 c8 cmp %ecx,%eax
8048d91: 75 f6 jne 8048d89 <phase_6+0x5a>
//遍历链表直到ecx=eax即找到特定id的结点
8048d93: eb 05 jmp 8048d9a <phase_6+0x6b>
//跳过下一句
//id=1
8048d95: ba 54 c1 04 08 mov $0x804c154,%edx
//edx=0x804c154
8048d9a: 89 54 b5 c8 mov %edx,-0x38(%ebp,%esi,4)
//将edx(即id为ecx结点的地址)记录在ebp-0x38开始的第esi个空间
8048d9e: 83 c3 01 add $0x1,%ebx //ebx自增
8048da1: 83 fb 06 cmp $0x6,%ebx
8048da4: 74 17 je 8048dbd <phase_6+0x8e>
//若ebx等于6则结束循环
//获取下一个id
8048da6: 89 de mov %ebx,%esi //esi=ebx
8048da8: 8b 4c 9d e0 mov -0x20(%ebp,%ebx,4),%ecx
//将输入的数组第ebx个元素给ecx
8048dac: 83 f9 01 cmp $0x1,%ecx
8048daf: 7e e4 jle 8048d95 <phase_6+0x66>
//根据结点id选择要执行的代码
//id>1
8048db1: b8 01 00 00 00 mov $0x1,%eax //eax=1
8048db6: ba 54 c1 04 08 mov $0x804c154,%edx
//edx=0x804c154
8048dbb: eb cc jmp 8048d89 <phase_6+0x5a>
/*重建链表*/
//循环初始化
8048dbd: 8b 5d c8 mov -0x38(%ebp),%ebx
//将第一个结点地址给ebx
8048dc0: 8d 45 cc lea -0x34(%ebp),%eax
//eax=ebp-0x34即指向第二个结点的地址
8048dc3: 8d 75 e0 lea -0x20(%ebp),%esi
//esi=ebp-0x20即指针数组末尾
8048dc6: 89 d9 mov %ebx,%ecx
//ecx=ebx
//循环体
8048dc8: 8b 10 mov (%eax),%edx
//取下一个结点地址给edx
8048dca: 89 51 08 mov %edx,0x8(%ecx)
//ecx->next=edx
8048dcd: 83 c0 04 add $0x4,%eax
//eax+=4即指向下一个指针
8048dd0: 39 f0 cmp %esi,%eax
8048dd2: 74 04 je 8048dd8 <phase_6+0xa9>
//若eax=esi,结束循环
8048dd4: 89 d1 mov %edx,%ecx //ecx=edx
8048dd6: eb f0 jmp 8048dc8 <phase_6+0x99>
/*检测链表的val成员是否降序排列*/
//循环初始化
8048dd8: c7 42 08 00 00 00 00 movl $0x0,0x8(%edx)
8048ddf: be 05 00 00 00 mov $0x5,%esi
//循环体
8048de4: 8b 43 08 mov 0x8(%ebx),%eax
//将ebx->next给eax
8048de7: 8b 00 mov (%eax),%eax
//取*eax即下个结点的val给eax
8048de9: 39 03 cmp %eax,(%ebx)
8048deb: 7d 05 jge 8048df2 <phase_6+0xc3>
//ebx->val的值应当大于等于下个结点的val
8048ded: e8 93 03 00 00 call 8049185 <explode_bomb>
8048df2: 8b 5b 08 mov 0x8(%ebx),%ebx
//ebx=ebx->next
8048df5: 83 ee 01 sub $0x1,%esi //esi自减
8048df8: 75 ea jne 8048de4 <phase_6+0xb5>
//esi为0时结束循环
/*函数结束*/
8048dfa: 83 c4 40 add $0x40,%esp
8048dfd: 5b pop %ebx
8048dfe: 5e pop %esi
8048dff: 5d pop %ebp
8048e00: c3 ret
总体来说读起来都知道是什么意思,就是跳转太多,逻辑上需要很多整理。
Secret_phase
每一关都是对应的一个phase函数,隐藏关对应的就是secret_phase函数。在bomb.out中查找函数名,可以看到在phase_defused中调用了secret_phase函数。
804936c: 8d 45 a8 lea -0x58(%ebp),%eax
804936f: 89 44 24 10 mov %eax,0x10(%esp)
//将ebp-0x58压栈
8049373: 8d 45 a0 lea -0x60(%ebp),%eax
8049376: 89 44 24 0c mov %eax,0xc(%esp)
//将ebp-0x60压栈
804937a: 8d 45 a4 lea -0x5c(%ebp),%eax
804937d: 89 44 24 08 mov %eax,0x8(%esp)
//将ebp-0x5c压栈
8049381: c7 44 24 04 0b a4 04 movl $0x804a40b,0x4(%esp)
8049388: 08 //将0x804a40b压栈
8049389: c7 04 24 f0 c8 04 08 movl $0x804c8f0,(%esp)
//将0x804c8f0压栈
8049390: e8 4b f4 ff ff call 80487e0 <__isoc99_sscanf@plt>
//调用__isoc99_sscanf@plt
8049395: 83 f8 03 cmp $0x3,%eax
8049398: 75 34 jne 80493ce <phase_defused+0x80>
//若输入的个数不为3,则跳过secret_phase
804939a: c7 44 24 04 14 a4 04 movl $0x804a414,0x4(%esp)
80493a1: 08
//将0x804a414压栈
80493a2: 8d 45 a8 lea -0x58(%ebp),%eax
80493a5: 89 04 24 mov %eax,(%esp)
//ebp-0x58压栈
80493a8: e8 b5 fb ff ff call 8048f62 <strings_not_equal>
//比较字符串
80493ad: 85 c0 test %eax,%eax
80493af: 75 1d jne 80493ce <phase_defused+0x80>
//若比较结果不一样则跳过secret_phase
扫描以上出现的地址值,根据前几关的经验易知进入隐藏关需要输入一个正确的字符串。
同时还扫描出格式为”%d %d %s”。在前几关中,输入格式为”%d %d”的仅有第三、四两关,只需再在原有输入后添加正确的字符串即可在phase6之后进入隐藏关(具体是哪一关的原理待研究)。
首先看secret_phase代码
08048e54 <secret_phase>:
8048e54: 55 push %ebp
8048e55: 89 e5 mov %esp,%ebp
8048e57: 53 push %ebx
8048e58: 83 ec 14 sub $0x14,%esp
8048e5b: e8 b6 03 00 00 call 8049216 <read_line>
8048e60: c7 44 24 08 0a 00 00 movl $0xa,0x8(%esp)
8048e67: 00 //将0xa压栈
8048e68: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
8048e6f: 00 //将0x0压栈
8048e70: 89 04 24 mov %eax,(%esp)
8048e73: e8 c8 f9 ff ff call 8048840 <strtol@plt>
//调用strtol@plt(首地址,0,10)
//函数作用自行百度
8048e78: 89 c3 mov %eax,%ebx
8048e7a: 8d 40 ff lea -0x1(%eax),%eax
8048e7d: 3d e8 03 00 00 cmp $0x3e8,%eax
8048e82: 76 05 jbe 8048e89 <secret_phase+0x35>
//eax的值要小于等于0x3e8
8048e84: e8 fc 02 00 00 call 8049185 <explode_bomb>
8048e89: 89 5c 24 04 mov %ebx,0x4(%esp)
8048e8d: c7 04 24 a0 c0 04 08 movl $0x804c0a0,(%esp)
//将0x804c0a0压栈
8048e94: e8 68 ff ff ff call 8048e01 <fun7>//调用fun7
8048e99: 83 f8 04 cmp $0x4,%eax
8048e9c: 74 05 je 8048ea3 <secret_phase+0x4f>
//返回值要为4
8048e9e: e8 e2 02 00 00 call 8049185 <explode_bomb>
8048ea3: c7 04 24 80 a1 04 08 movl $0x804a180,(%esp)
8048eaa: e8 d1 f8 ff ff call 8048780 <puts@plt>
8048eaf: e8 9a 04 00 00 call 804934e <phase_defused>
//输出提示信息并解除炸弹
8048eb4: 83 c4 14 add $0x14,%esp
8048eb7: 5b pop %ebx
8048eb8: 5d pop %ebp
8048eb9: c3 ret
8048eba: 66 90 xchg %ax,%ax
8048ebc: 66 90 xchg %ax,%ax
8048ebe: 66 90 xchg %ax,%ax
分析得本关的答案为一个整数,结合第四关易知需要使fun7返回值为4,即fun7(0x804c0a0,输入的值)=4。
08048e01 <fun7>:
8048e01: 55 push %ebp
8048e02: 89 e5 mov %esp,%ebp
8048e04: 53 push %ebx
8048e05: 83 ec 14 sub $0x14,%esp
8048e08: 8b 55 08 mov 0x8(%ebp),%edx
//第一个参数赋值给edx
8048e0b: 8b 4d 0c mov 0xc(%ebp),%ecx
//第二个参数赋值给ecx
8048e0e: 85 d2 test %edx,%edx
8048e10: 74 37 je 8048e49 <fun7+0x48>
//若edx为0,则跳转返回-1
/*edx!=0*/
8048e12: 8b 1a mov (%edx),%ebx
//将*edx赋值给ebx
8048e14: 39 cb cmp %ecx,%ebx //比较ecx和ebx
8048e16: 7e 13 jle 8048e2b <fun7+0x2a>
/*ebx>ecx*/
8048e18: 89 4c 24 04 mov %ecx,0x4(%esp) //ecx压栈
8048e1c: 8b 42 04 mov 0x4(%edx),%eax
//将*(edx+0x4)压栈
8048e1f: 89 04 24 mov %eax,(%esp) //eax压栈
8048e22: e8 da ff ff ff call 8048e01 <fun7>//递归调用
8048e27: 01 c0 add %eax,%eax
//设置返回值为2*fun7
8048e29: eb 23 jmp 8048e4e <fun7+0x4d>
/*ebx<=ecx*/
8048e2b: b8 00 00 00 00 mov $0x0,%eax //设置返回值为0
8048e30: 39 cb cmp %ecx,%ebx //比较ecx和ebx
8048e32: 74 1a je 8048e4e <fun7+0x4d>
//若ecx==ebx,则返回0
8048e34: 89 4c 24 04 mov %ecx,0x4(%esp) //ecx压栈
8048e38: 8b 42 08 mov 0x8(%edx),%eax
//将*(edx+0x8)压栈
8048e3b: 89 04 24 mov %eax,(%esp) //eax压栈
8048e3e: e8 be ff ff ff call 8048e01 <fun7>//递归调用
8048e43: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax
//设置返回值为2*fun7+1
8048e47: eb 05 jmp 8048e4e <fun7+0x4d>
/*edx==0*/
8048e49: b8 ff ff ff ff mov $0xffffffff,%eax
//设置返回值为-1
/*恢复调用现场*/
8048e4e: 83 c4 14 add $0x14,%esp
8048e51: 5b pop %ebx
8048e52: 5d pop %ebp
8048e53: c3 ret
结合第4关和第6关来看,这里的代码应该都比较好理解。这里作为参数的地址扫描结果如下
可以看到这里的结构体也有三个成员:一个数值和两个地址值。分析数据可以发现每个地址值都是另外一个结点的地址且互不相同,且最终都可追踪到一个没有后续结点的结点,由此推出这是一个二叉树结构。
将fun7转化为C语言:
int fun7(addr, x){
if (addr == 0)
return -1;
//比较结点的值和x的大小
else {
if (addr->val > x)//若大于,访问左孩子
return 2 * fun7(addr->leftchild, x);//返回的为偶数
else if (addr->val == x)//若相等,则返回0
return 0;
else //若小于,访问右孩子
return 2 * fun7(addr->rightchild, x) + 1;//返回的为奇数
}
}
根据返回值的奇偶性以及二叉树高度为4的条件可以反推出遍历二叉树的过程。比如这里返回值要为4,则返回值的递归计算式一定为2 x (2 x (2 x 0 + 1)),比较结点的值和x的结果一定依次是:大于、大于、小于、等于。遍历方式为左、左、右。
待补充