CSAPP-bomblab
CSAPP-bomblab
实验目的
理解汇编语言,学习使用gdb调试器,推导出正确的字符串用于跳过explode_bomb,破解bomb.c生成的炸弹。
实验环境和工具
ubuntu 12.04.5 (32位) ;
gdb 7.4 ;
实验内容及操作步骤
阅读bomb.c
- C文件开头的注释如下:
1 |
|
- 翻译如下:
Evil Incorporated博士(PERPETRATOR)特此授予您(VICTIM)使用该炸弹(BOMB)的明确许可。 这是一个有时间限制的许可证,在VICTIM死亡时到期。 PERPETRATOR对损坏,沮丧,精神错乱,虫眼,腕管综合症,睡眠不足或对VICTIM造成的其他伤害不承担任何责任。 除非PERPETRATOR想要获得信誉,否则就是这样。 VICTIM不得将此炸弹源代码分发给PERPETRATOR的任何敌人。 VICTIM不得调试,反向工程,在其上运行“字符串”,反编译,解密或使用任何其他技术来了解和缓解BOMB。 处理此程序时,可能不能穿防弹衣。 PERPETRATOR不会因PERPETRATOR的幽默感而道歉。 在法律禁止BOMB的情况下,此许可无效。
- bomb.c代码如下:(英文注释做了适当翻译)
1 |
|
- 总之,有两种方法闯关:
./bomb
,然后在终端输入./bomb xxx.txt
,xxx.txt
中按行保存用于过关的字符串
分析汇编代码,找到通关密码
1. phase_1
-
phase_1的汇编代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17Dump of assembler code for function phase_1:
0x08048b50 <+0>: sub $0x1c,%esp
;栈指针esp减去0x1c,给当前帧开辟大小为0x1c的空间
0x08048b53 <+3>: movl $0x804a1a4,0x4(%esp)
;将0x804a1a4(原字符串在内存中的地址)赋给M[0x4+esp]。
;(从后面调用函数<strings_not_equal>推测其为字符串,通过查看<strings_not_equal>的汇编代码进行验证)
0x08048b5b <+11>: mov 0x20(%esp),%eax ;将M[0x20+%esp](输入的字符串)赋值给eax
0x08048b5f <+15>: mov %eax,(%esp)
;eax的值赋给M[esp],与M[0x4+esp]共同作为<strings_not_equal>的参数
0x08048b62 <+18>: call 0x8048fd4 <strings_not_equal>
;比较两字符串是否相等,若相等,则返回值%eax=0
0x08048b67 <+23>: test %eax,%eax ;如果eax=0,则zf=1
0x08048b69 <+25>: je 0x8048b70 <phase_1+32> ;zf=1时跳转至*(phase_1+32),即下一关
0x08048b6b <+27>: call 0x80490e6 <explode_bomb> ;否则爆炸
0x08048b70 <+32>: add $0x1c,%esp
0x08048b73 <+35>: ret
End of assembler dump.-
其中,
<strings_not_equal>
的汇编代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47·#执行call指令:esp=esp-4
Dump of assembler code for function strings_not_equal:
0x08048fd4 <+0>: sub $0x10,%esp ;esp=esp-10
0x08048fd7 <+3>: mov %ebx,0x4(%esp)
0x08048fdb <+7>: mov %esi,0x8(%esp)
0x08048fdf <+11>: mov %edi,0xc(%esp)
0x08048fe3 <+15>: mov 0x14(%esp),%ebx
;此时M[0x14+esp]为输入的字符串*s1,存入ebx
0x08048fe7 <+19>: mov 0x18(%esp),%esi
;此时M[0x18+esp]为原字符串*s0,存入esi
0x08048feb <+23>: mov %ebx,(%esp)
0x08048fee <+26>: call 0x8048fbb <string_length>
;eax返回值为ebx中字符串的长度s1.length
0x08048ff3 <+31>: mov %eax,%edi ;存入edi
0x08048ff5 <+33>: mov %esi,(%esp)
0x08048ff8 <+36>: call 0x8048fbb <string_length>
;eax返回值为esi中字符串的长度s0.length
0x08048ffd <+41>: mov $0x1,%edx
0x08049002 <+46>: cmp %eax,%edi ;比较s1.length和s0.length
0x08049004 <+48>: jne 0x8049039 <strings_not_equal+101>;不相等,则跳转->return 1
0x08049006 <+50>: movzbl (%ebx),%eax ;s1首地址存入eax
0x08049009 <+53>: mov $0x0,%dl ;edx=0
0x0804900b <+55>: test %al,%al
0x0804900d <+57>: je 0x8049039 <strings_not_equal+101>;if(*s1==0)->return 0
;(输入空字符)
0x0804900f <+59>: mov $0x1,%dl ;edx=1
0x08049011 <+61>: cmp (%esi),%al
0x08049013 <+63>: jne 0x8049039 <strings_not_equal+101>;if(*s1!=*s0)->return 1
0x08049015 <+65>: mov $0x0,%eax ;eax=0(字符串从*s+1开始的偏移量)
0x0804901a <+70>: jmp 0x8049024 <strings_not_equal+80>
0x0804901c <+72>: add $0x1,%eax
0x0804901f <+75>: cmp (%esi,%eax,1),%dl ;if(*s0+(x++)+1!=*s1+x+1)
0x08049022 <+78>: jne 0x8049034 <strings_not_equal+96> ;return 1
0x08049024 <+80>: movzbl 0x1(%ebx,%eax,1),%edx
;edx存入ebx+(eax存储的偏移量)+1,即*s1+x+1
0x08049029 <+85>: test %dl,%dl
0x0804902b <+87>: jne 0x804901c <strings_not_equal+72> ;if(*s1+x+1!=0) 执行循环
0x0804902d <+89>: mov $0x0,%edx
0x08049032 <+94>: jmp 0x8049039 <strings_not_equal+101>;否则完成比较,字符串相等,return 0
0x08049034 <+96>: mov $0x1,%edx
0x08049039 <+101>: mov %edx,%eax ;if(edx==1) return 1;else return 0
0x0804903b <+103>: mov 0x4(%esp),%ebx
0x0804903f <+107>: mov 0x8(%esp),%esi
0x08049043 <+111>: mov 0xc(%esp),%edi
0x08049047 <+115>: add $0x10,%esp
0x0804904a <+118>: ret
End of assembler dump.-
<string_length>
的汇编代码如下:1
2
3
4
5
6
7
8
9
10Dump of assembler code for function string_length:
0x08048fbb <+0>: mov 0x4(%esp),%edx ;edx=[0x4+esp] --字符串首地址
0x08048fbf <+4>: mov $0x0,%eax ;eax=0 --计数
0x08048fc4 <+9>: cmpb $0x0,(%edx)
0x08048fc7 <+12>: je 0x8048fd2 <string_length+23> ;edx=0则退出
0x08048fc9 <+14>: add $0x1,%eax ;eax++ --偏移量++
0x08048fcc <+17>: cmpb $0x0,(%edx,%eax,1) ;edx+eax!=0,即当前字符不为空
0x08048fd0 <+21>: jne 0x8048fc9 <string_length+14> ;则继续循环
0x08048fd2 <+23>: repz ret ;返回eax则为字符串长度
End of assembler dump.
-
-
-
所以我们要使输入的字符串和
0x804a1a4
存储的字符串相等。 -
查看0x804a1a4存储的字符串:
- 运行
./bomb
,输入该字符串:Crikey! I have lost my mojo!
,即可得到通过第一关:
2. phase_2
- phase_2的汇编代码(调用函数<read_six_numbers>前的部分)
1 |
|
-
<read_six_numbers>
的汇编代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26#调用函数 esp=esp-4
Dump of assembler code for function read_six_numbers:
0x0804921b <+0>: sub $0x2c,%esp ;栈指针esp减去0x2c
0x0804921e <+3>: mov 0x34(%esp),%eax ;eax=esp+0x34 [原esp+0x4]
0x08049222 <+7>: lea 0x14(%eax),%edx
0x08049225 <+10>: mov %edx,0x1c(%esp) ;读入第六个数
0x08049229 <+14>: lea 0x10(%eax),%edx
0x0804922c <+17>: mov %edx,0x18(%esp) ;读入第五个数
0x08049230 <+21>: lea 0xc(%eax),%edx
0x08049233 <+24>: mov %edx,0x14(%esp) ;读入第四个数
0x08049237 <+28>: lea 0x8(%eax),%edx
0x0804923a <+31>: mov %edx,0x10(%esp) ;读入第三个数
0x0804923e <+35>: lea 0x4(%eax),%edx
0x08049241 <+38>: mov %edx,0xc(%esp) ;读入第二个数
0x08049245 <+42>: mov %eax,0x8(%esp) ;读入第一个数
0x08049249 <+46>: movl $0x804a3bf,0x4(%esp)
0x08049251 <+54>: mov 0x30(%esp),%eax ;eax=esp+0x30
0x08049255 <+58>: mov %eax,(%esp) ;M[esp]=eax=输入
0x08049258 <+61>: call 0x8048870 <__isoc99_sscanf@plt>
;函数__isoc99_sscanf返回输入参数的个数
0x0804925d <+66>: cmp $0x5,%eax
0x08049260 <+69>: jg 0x8049267 <read_six_numbers+76> ;输入参数数量大于5则安全退出该函数
0x08049262 <+71>: call 0x80490e6 <explode_bomb>
0x08049267 <+76>: add $0x2c,%esp
0x0804926a <+79>: ret
End of assembler dump.-
函数
<__isoc99_sscanf@plt>
的汇编代码:1
2
3
4
5Dump of assembler code for function __isoc99_sscanf@plt:
0x08048870 <+0>: jmp *0x804c040
0x08048876 <+6>: push $0x80
0x0804887b <+11>: jmp 0x8048760
End of assembler dump.查看
__isoc99_sscanf
调用的第二个参数,可知可以成功输入6个参数的形式
-
-
phase_2的汇编代码(调用函数<read_six_numbers>后的部分)
1 |
|
- 通过上方汇编代码的<+62>、<+64>可知,该函数会执行循环,在每次循环中ebx所存地址(初始地址为0x1c+esp)的值会先加0x4,直到等于0x30+esp。故循环的执行次数为5次。
0x1c+esp
的初始值为M[0x18+esp]=1
的两倍为2,如此循环。可以推知我们输入6个数存储地址分别为上方的(0x18+esp)
、(0x1c+esp)
、(0x20+esp)
、(0x24+esp)
、(0x28+esp)
、(0x2c+esp)
,是以1为首项,2为公比的等比数列。故通关密码为:1 2 4 8 16 32
,结果如下:
3. phase_3
-
phase_3的汇编代码(调用
<__isoc99_sscanf@plt>
前的部分)1
2
3
4
5
6
7
8
9
10Dump of assembler code for function phase_3:
0x08048bbc <+0>: sub $0x2c,%esp ;栈指针esp减去0x2c
0x08048bbf <+3>: lea 0x1c(%esp),%eax ;eax=(0x1c+esp)
0x08048bc3 <+7>: mov %eax,0xc(%esp) ;M[0xc+esp]=(0x1c+esp)
0x08048bc7 <+11>: lea 0x18(%esp),%eax ;eax=(0x18+esp)
0x08048bcb <+15>: mov %eax,0x8(%esp) ;M[0x8+esp]=(0x18+esp)
0x08048bcf <+19>: movl $0x804a3cb,0x4(%esp) ;M[0x4+esp]=0x804a3cb
0x08048bd7 <+27>: mov 0x30(%esp),%eax ;eax=M[0x30+esp]
0x08048bdb <+31>: mov %eax,(%esp) ;M[esp]=eax
0x08048bde <+34>: call 0x8048870 <__isoc99_sscanf@plt>- 这里也调用了函数
<__isoc99_sscanf@plt>
。查看其第二个参数所在地址$0x804a3cb
的内容:
- 可知输入应该为两个整型数据,对应的保存地址分别为
0x18+esp
,0x1c+esp
。
- 这里也调用了函数
-
phase_3的汇编代码(剩余部分):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
440x08048be3 <+39>: cmp $0x1,%eax
;`<__isoc99_sscanf@plt>的返回值为输入参数的个数,保存在eax中
0x08048be6 <+42>: jg 0x8048bed <phase_3+49> ;参数个数需大于1,否则爆炸
0x08048be8 <+44>: call 0x80490e6 <explode_bomb>
0x08048bed <+49>: cmpl $0x7,0x18(%esp)
0x08048bf2 <+54>: ja 0x8048c5a <phase_3+158>
;第一个整数需要无符号小于等于7,即范围为0-7否则跳转->爆炸
0x08048bf4 <+56>: mov 0x18(%esp),%eax
0x08048bf8 <+60>: jmp *0x804a1e0(,%eax,4)
;无条件跳转至地址(M[0x804a1e0+4*eax]),即根据第一个整数为跳转的偏移量
;根据跳转表,以eax=1为例子如下:
0x08048bff <+67>: mov $0x0,%eax ;eax=0
0x08048c04 <+72>: jmp 0x8048c0b <phase_3+79> ;跳转至<phase_3+79>
0x08048c06 <+74>: mov $0x240,%eax ;(被跳过)
0x08048c0b <+79>: sub $0x28b,%eax ;eax=eax-0x28b=-0x28b
0x08048c10 <+84>: jmp 0x8048c17 <phase_3+91> ;跳转至<phase_3+91>
0x08048c12 <+86>: mov $0x0,%eax ;(被跳过)
0x08048c17 <+91>: add $0x35a,%eax ;eax=eax+0x35a=0xcf
0x08048c1c <+96>: jmp 0x8048c23 <phase_3+103> ;跳转至<phase_3+103>
0x08048c1e <+98>: mov $0x0,%eax ;(被跳过)
0x08048c23 <+103>: sub $0xc5,%eax ;eax=eax-0xc5=0xa
0x08048c28 <+108>: jmp 0x8048c2f <phase_3+115> ;跳转至<phase_3+115>
0x08048c2a <+110>: mov $0x0,%eax ;(被跳过)
0x08048c2f <+115>: add $0xc5,%eax ;eax=eax+0xc5=0xcf
0x08048c34 <+120>: jmp 0x8048c3b <phase_3+127> ;跳转至<phase_3+127>
0x08048c36 <+122>: mov $0x0,%eax ;(被跳过)
0x08048c3b <+127>: sub $0xc5,%eax ;eax=eax-0xc5=0xa
0x08048c40 <+132>: jmp 0x8048c47 <phase_3+139> ;跳转至<phase_3+139>
0x08048c42 <+134>: mov $0x0,%eax ;(被跳过)
0x08048c47 <+139>: add $0xc5,%eax ;eax=eax+0xc5=0xcf
0x08048c4c <+144>: jmp 0x8048c53 <phase_3+151> ;跳转至<phase_3+151>
0x08048c4e <+146>: mov $0x0,%eax ;(被跳过)
0x08048c53 <+151>: sub $0xc5,%eax ;eax=eax-0xc5=0xa
0x08048c58 <+156>: jmp 0x8048c64 <phase_3+168> ;(需要此步)跳转至<phase_3+168>,否则爆炸
0x08048c5a <+158>: call 0x80490e6 <explode_bomb>
0x08048c5f <+163>: mov $0x0,%eax
0x08048c64 <+168>: cmpl $0x5,0x18(%esp)
0x08048c69 <+173>: jg 0x8048c71 <phase_3+181> ;M[0x18+esp]>5则爆炸
0x08048c6b <+175>: cmp 0x1c(%esp),%eax
0x08048c6f <+179>: je 0x8048c76 <phase_3+186> ;eax=M[0X1c+esp]时跳转过关,否则爆炸
0x08048c71 <+181>: call 0x80490e6 <explode_bomb>
0x08048c76 <+186>: add $0x2c,%esp
0x08048c79 <+189>: ret
End of assembler dump.-
从该汇编代码的
<+156>
到结束可知,我们需要经过跳转到达0x08048c58 <+156>
,且输入的第一个数的值需要,输入的第二个数需要等于eax。 -
我们通过x指令,参照
jmp *0x804a1e0(,%eax,4)
的跳转表如下: -
上述汇编代码的注释输入的第一个数
M[0x18+esp]=1
为例,先跳转至<phase_3+67>
:得到最终
M[0x1c+esp]
需要等于0xa,即十进制的10。故我们可以输入1 10
过关:-
同样可推知输入0—7对应的第二个数组合如下:
0 586
1 10
2 661
3 -197
4 0
5 -197
6 0
7 197
-
同样输入0~5中的其他数以及经过运算得到的相对应的第二个数,也能过关。
-
-
4. phase_4
-
phase_4的汇编代码(调用
<__isoc99_sscanf@plt>
前的部分)1
2
3
4
5
6
7
8
9
10Dump of assembler code for function phase_4:
0x08048cd7 <+0>: sub $0x2c,%esp ;栈指针esp减去0x2c
0x08048cda <+3>: lea 0x18(%esp),%eax ;eax=(0x18+esp)
0x08048cde <+7>: mov %eax,0xc(%esp) ;M[0xc+esp]=eax=(0x18+esp)
0x08048ce2 <+11>: lea 0x1c(%esp),%eax ;eax=(0x1c+esp)
0x08048ce6 <+15>: mov %eax,0x8(%esp) ;M[0x8+esp]=eax=(0x1c+esp)
0x08048cea <+19>: movl $0x804a3cb,0x4(%esp) ;M[0x4+esp]=0x804a3cb
0x08048cf2 <+27>: mov 0x30(%esp),%eax ;eax=M[0x30+esp]
0x08048cf6 <+31>: mov %eax,(%esp) ;M[esp]=eax=M[0x30+esp]
0x08048cf9 <+34>: call 0x8048870 <__isoc99_sscanf@plt>-
和phase_3相似,调用函数
<__isoc99_sscanf@plt>
将输入以地址0x804a3cb
存储的形式保存:可见输入为两个整型数据,分别保存在地址
0x1c+esp
和0x18+esp
中。(要注意0x1c+esp
是第三个参数,0x18+esp
是第四个参数,表示输入数据存储的次序,和phase_3正好相反!)。函数返回值为输入数据的个数。
-
-
phase_4的汇编代码(剩余部分)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
190x08048cfe <+39>: cmp $0x2,%eax ;比较<__isoc99_sscanf@plt>的返回值和2
0x08048d01 <+42>: jne 0x8048d11 <phase_4+58> ;不相等则爆炸
0x08048d03 <+44>: mov 0x18(%esp),%eax ;eax=M[0x18+esp] 即输入的第二个整数
0x08048d07 <+48>: cmp $0x1,%eax ;比较eax和1
0x08048d0a <+51>: jle 0x8048d11 <phase_4+58> ;eax<=1则爆炸,故第二个整数要大于1
0x08048d0c <+53>: cmp $0x4,%eax ;比较eax和4
0x08048d0f <+56>: jle 0x8048d16 <phase_4+63> ;eax<=4则不会爆炸,故第二个整数小于等于4
0x08048d11 <+58>: call 0x80490e6 <explode_bomb>
0x08048d16 <+63>: mov 0x18(%esp),%eax ;eax=M[0x18+esp],还是第二个整数
0x08048d1a <+67>: mov %eax,0x4(%esp) ;M[0x4+esp]=eax=M[0x18+esp]
0x08048d1e <+71>: movl $0x9,(%esp) ;M[esp]=9
0x08048d25 <+78>: call 0x8048c7a <func4> ;调用函数<func4>
0x08048d2a <+83>: cmp 0x1c(%esp),%eax
;比较<func4>的返回值和M[esp+0x1c](即输入的第一个整数)的大小
0x08048d2e <+87>: je 0x8048d35 <phase_4+94> ;相等则过关
0x08048d30 <+89>: call 0x80490e6 <explode_bomb>
0x08048d35 <+94>: add $0x2c,%esp
0x08048d38 <+97>: ret
End of assembler dump.-
故输入的第二个整数(大于1且小于等于4)会进入函数
<func4>
产生返回值,输入的第一个整数应该和该返回值相等 -
其中,
<func4>
的汇编代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34;call调用函数,esp=esp-4。观察整个代码,有两个参数,设为x和y(初值为9和输入的第一个整数)
(gdb) disass func4
Dump of assembler code for function func4:
0x08048c7a <+0>: sub $0x1c,%esp ;esp=esp-0x1c,开辟0x1c的栈帧空间
0x08048c7d <+3>: mov %ebx,0x10(%esp)
0x08048c81 <+7>: mov %esi,0x14(%esp)
0x08048c85 <+11>: mov %edi,0x18(%esp) ;以上三行保存被调用寄存器的原值
0x08048c89 <+15>: mov 0x20(%esp),%esi ;esi=M[0x20+esp]=x
0x08048c8d <+19>: mov 0x24(%esp),%ebx ;ebx=M[0x24+esp]=y
0x08048c91 <+23>: test %esi,%esi
0x08048c93 <+25>: jle 0x8048cc0 <func4+70> ;ZF=1或SF!=OF时跳转
;test %esi,%esi后,若esi=0,则ZF=1;或esi为负数,SF!=OF ->return 0
0x08048c95 <+27>: cmp $0x1,%esi
0x08048c98 <+30>: je 0x8048cc5 <func4+75> ;x=1时跳转至<func4+75> ->return y
0x08048c9a <+32>: mov %ebx,0x4(%esp) ;M[0x4+esp]=ebx=y
0x08048c9e <+36>: lea -0x1(%esi),%eax ;eax=(esi-1)=(x-1)
0x08048ca1 <+39>: mov %eax,(%esp) ;M[esp]=eax=(esi-1)=(x-1)
0x08048ca4 <+42>: call 0x8048c7a <func4> ;递归调用func4(x-1,y)
0x08048ca9 <+47>: lea (%eax,%ebx,1),%edi ;edi=(eax+ebx)=func4(x-1,y)+y;
0x08048cac <+50>: mov %ebx,0x4(%esp) ;M[0x4+esp]=ebx=y
0x08048cb0 <+54>: sub $0x2,%esi ;esi=(esi-2)=(x-2)
0x08048cb3 <+57>: mov %esi,(%esp) ;M[esp]=esi=(x-2)
0x08048cb6 <+60>: call 0x8048c7a <func4> ;递归调用func4(x-2,y)
0x08048cbb <+65>: lea (%edi,%eax,1),%ebx ;ebx=(edi+eax)=func4(x-1,y)+y+func4(x-2,y)
0x08048cbe <+68>: jmp 0x8048cc5 <func4+75>
;跳转至<func4+75> ->return func4(x-1,y)+y+func4(x-2,y)
0x08048cc0 <+70>: mov $0x0,%ebx
0x08048cc5 <+75>: mov %ebx,%eax ;eax=ebx,即为返回值
0x08048cc7 <+77>: mov 0x10(%esp),%ebx
0x08048ccb <+81>: mov 0x14(%esp),%esi
0x08048ccf <+85>: mov 0x18(%esp),%edi ;以上三行恢复被调用寄存器的值
0x08048cd3 <+89>: add $0x1c,%esp
0x08048cd6 <+92>: ret
End of assembler dump.- 可以得到
func4
函数和主函数的C++代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21int func4(int x,int y)
{
if(x<=0)
return 0;
else if(x==1)
return y;
else
return func4(x-1,y)+y+func4(x-2,y);
}
int main()
{
cin>>a>>b;
if(b<=1||b>4)
explode_bomb();
else
{
if(func4(9,b)!=a)
explode_bomb();
}
return 0;
}- 另写代码输出func4得到的第二个整数(只能为2、3、4)对应的第一个整数:
- 可以得到
-
输入这3组整数都能过关。
-
5. phase_5
-
phase_5的汇编代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25Dump of assembler code for function phase_5:
0x08048d39 <+0>: push %ebx ;保存ebx的原值
0x08048d3a <+1>: sub $0x18,%esp ;esp=esp-0x18,预留0x18的栈帧空间
0x08048d3d <+4>: mov 0x20(%esp),%ebx ;ebx=M[0x20+esp],为输入的字符串
0x08048d41 <+8>: mov %ebx,(%esp) ;M[esp]=ebx
0x08048d44 <+11>: call 0x8048fbb <string_length>
;调用函数<string_length>(phase_1中出现过),返回值为输入字符串的长度
0x08048d49 <+16>: cmp $0x6,%eax
0x08048d4c <+19>: je 0x8048d53 <phase_5+26> ;若eax=6则跳转
0x08048d4e <+21>: call 0x80490e6 <explode_bomb> ;否则爆炸
0x08048d53 <+26>: mov $0x0,%edx ;edx=0
0x08048d58 <+31>: mov $0x0,%eax ;eax=0
0x08048d5d <+36>: movsbl (%ebx,%eax,1),%ecx ;ecx=M(ebx+eax),高位用符号位补齐
0x08048d61 <+40>: and $0xf,%ecx ;保留ecx的低四位(字符的ASCII码对16取余)
0x08048d64 <+43>: add 0x804a200(,%ecx,4),%edx ;edx=edx+M[0x804a200+4*ecx]
0x08048d6b <+50>: add $0x1,%eax ;eax=eax+1
0x08048d6e <+53>: cmp $0x6,%eax
0x08048d71 <+56>: jne 0x8048d5d <phase_5+36> ;eax不等于6则跳转至<phase_5+36>,执行循环
0x08048d73 <+58>: cmp $0x2b,%edx
0x08048d76 <+61>: je 0x8048d7d <phase_5+68> ;若edx=0x2b 则过关
0x08048d78 <+63>: call 0x80490e6 <explode_bomb>
0x08048d7d <+68>: add $0x18,%esp
0x08048d80 <+71>: pop %ebx
0x08048d81 <+72>: ret
End of assembler dump.-
可见输入的字符串长度为6。在循环中,依次取字符串的1个字符,ASCII码对16取余,保存于ecx中。每次edx加上M[0x804a200+4*ecx],相当于0x804a200为数组,ecx为数组的索引。
-
循环执行次数为6次。结束循环后,edx的值需为0x2b(即43)。
-
查看首地址为0x804a200的数组保存的数字,数组长度为16,如下:
- 需要在其中取6个数,和为43。可选择:13、10、12、4、3、1。(不唯一)
- 对应索引分别为:15、1、4、8、7、3,分别对应字符ASCII码的余数。
-
对应字符可以为:O、A、D、H、G、C。(答案不唯一)
-
-
-
输入OADHGC,过关:
6. phase_6
-
phase_6的汇编代码(分为多个部分)如下:
-
第一部分:
1 |
|
-
在读入6个数后,根据汇编代码可以推得对应的C++代码(数组为a[]):
1
2
3
4
5
6
7
8
9
10for(int i=0;i<=5;i++)
{
if(a[i]-1>5||a[i]-1<0)
explode_bomb();
else
for(int j=i+1;j<=5;j++)
if(a[j]==a[i])
explode_bomb();
}
//结束后跳转至<phase_6+82>- 这个双重循环确保数组a[]中的6个整数不相等且范围为[1,6]。
-
第二部分:
1
2
3
4
5
6
7
8
90x08048dd4 <+82>: lea 0x10(%esp),%eax ;eax=(0x10+esp)
0x08048dd8 <+86>: lea 0x28(%esp),%ebx ;ebx=(0x28+esp)
0x08048ddc <+90>: mov $0x7,%ecx ;ecx=7
0x08048de1 <+95>: mov %ecx,%edx ;edx=ecx=7
0x08048de3 <+97>: sub (%eax),%edx ;edx=edx-M[eax]=7-M[eax]
0x08048de5 <+99>: mov %edx,(%eax) ;M[eax]=edx
0x08048de7 <+101>: add $0x4,%eax ;eax=eax+4
0x08048dea <+104>: cmp %ebx,%eax
0x08048dec <+106>: jne 0x8048de1 <phase_6+95> ;若eax!=ebx,则回跳至<phase_6+95>-
这部分汇编代码写成C++代码如下:
1
2
3
4for(int i=0;a[i]!=a[6];i++)
{
a[i]=7-a[i];
}
-
-
第三部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
180x08048dee <+108>: mov $0x0,%ebx ;ebx=0;
0x08048df3 <+113>: jmp 0x8048e0b <phase_6+137> ;跳转至<phase_6+137>
0x08048df5 <+115>: mov 0x8(%edx),%edx ;edx=M[0x8+edx]
0x08048df8 <+118>: add $0x1,%eax ;eax=eax+1
0x08048dfb <+121>: cmp %ecx,%eax
0x08048dfd <+123>: jne 0x8048df5 <phase_6+115> ;若eax!=ecx,回跳至<phase_6+115>
0x08048dff <+125>: mov %edx,0x28(%esp,%esi,4) ;M[0x28+esp+4*esi]=edx
0x08048e03 <+129>: add $0x1,%ebx ;ebx=ebx+1
0x08048e06 <+132>: cmp $0x6,%ebx
0x08048e09 <+135>: je 0x8048e22 <phase_6+160> ;若ebx=6,跳转至<phase_6+160>(跳出循环)
0x08048e0b <+137>: mov %ebx,%esi ;esi=ebx
0x08048e0d <+139>: mov 0x10(%esp,%ebx,4),%ecx ;ecx=M[0x10+esp+4*ebx]=a[ebx]
0x08048e11 <+143>: mov $0x1,%eax ;eax=1;
0x08048e16 <+148>: mov $0x804c13c,%edx ;edx=0x804c13c
;以上4行相当于初始化寄存器
0x08048e1b <+153>: cmp $0x1,%ecx
0x08048e1e <+156>: jg 0x8048df5 <phase_6+115> ;若a[ebx]>1,跳转至<phase_6+115>
0x08048e20 <+158>: jmp 0x8048dff <phase_6+125> ;否则跳转至<phase_6+125>-
以
edx=0x804c13c
和edx=M[0x8+edx]
为突破口,先查看0x804c13c
存放的东西:- 由node联想到结点类(结构体),且地址
0x8+edx
存储的为指向下一结点的地址。
- 由node联想到结点类(结构体),且地址
-
依次查看结构体的信息如下,共有6个结构体:
- 可以观察到结构体有一个值(val)、编号(id)、下一节点的地址(*next)构成,形成链表结构。
-
可推成C++代码帮助理解,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26struct node
{
int val;
int id;
node *next;
};
node *n[6];
ebx=0;
do
{
esi=ebx;
ecx=a[ebx];
eax=1;
edx=0x804c13c //头结点的首地址
if(a[ebx]>1) //数组a[]的范围为[1,6],只有a[ebx]等于1时不执行if内的循环。
{
do
{
edx=M[0x8+edx]; //下一结点地址
eax++;
}while(eax!=ecx); //直到eax=a[ebx]时跳出循环,故循环次数为a[ebx]的值
}
node[esi]=edx; //故第i次循环是为了让原先的第a[i-1]个结点变为node[i-1]。 (i=ebx+1,esi=ebx)
//其中a[i-1]=(7-输入的第i个值)
ebx++;
}while(ebx<6) //共执行循环6次- 故根据数组a[]的值,给6个node节点重新排列:让原先的第a[i-1]个结点变为node[i-1]。其中a[i-1]=(7-输入的第i个值)。
-
-
第四部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
290x08048e22 <+160>: mov 0x28(%esp),%ebx ;ebx=M[0x28+esp] --node[0]的地址
0x08048e26 <+164>: mov 0x2c(%esp),%eax ;eax=M[0x2c+esp] --node[1]的地址
0x08048e2a <+168>: mov %eax,0x8(%ebx) ;M[ebx+0x8]=eax --node[0].next=node[1]
;以下至<+196>都为连接结点、形成链表的操作
0x08048e2d <+171>: mov 0x30(%esp),%edx
0x08048e31 <+175>: mov %edx,0x8(%eax)
0x08048e34 <+178>: mov 0x34(%esp),%eax
0x08048e38 <+182>: mov %eax,0x8(%edx)
0x08048e3b <+185>: mov 0x38(%esp),%edx
0x08048e3f <+189>: mov %edx,0x8(%eax)
0x08048e42 <+192>: mov 0x3c(%esp),%eax
0x08048e46 <+196>: mov %eax,0x8(%edx)
0x08048e49 <+199>: movl $0x0,0x8(%eax) ;M[0x8+eax]=0,node[5].next=0(空)
0x08048e50 <+206>: mov $0x5,%esi ;esi=5
0x08048e55 <+211>: mov 0x8(%ebx),%eax ;eax=M[0x8+ebx] --下一节点的地址
0x08048e58 <+214>: mov (%eax),%edx ;edx=M[eax] --下一结点的值
0x08048e5a <+216>: cmp %edx,(%ebx)
0x08048e5c <+218>: jge 0x8048e63 <phase_6+225>
;若M[ebx]>=edx,即某结点的值不小于其后一个结点的值,则不会爆炸
0x08048e5e <+220>: call 0x80490e6 <explode_bomb> ;否则爆炸
0x08048e63 <+225>: mov 0x8(%ebx),%ebx ;ebx=M[ebx+0x8] --下一个结点的地址
0x08048e66 <+228>: sub $0x1,%esi ;esi=esi-1
0x08048e69 <+231>: jne 0x8048e55 <phase_6+211> ;当esi!=0时,回跳至<phase_6+211>,执行循环
;故执行<+211>到<+231>的次数为6。
0x08048e6b <+233>: add $0x44,%esp ;恢复栈帧,过关。
0x08048e6e <+236>: pop %ebx
0x08048e6f <+237>: pop %esi
0x08048e70 <+238>: ret
End of assembler dump.-
故phase_6要求输入6个数,根据这6个数,对带值(val)的6个结点进行重新排序,使得结点的值降序排列。
-
根据之前查看的结点信息:
可以列表如下:
原结点编号 1 2 3 4 5 6 原值 0xdf 0x252 0x376 0xea 0x2fb 0xf3 目标编号 6 3 1 5 2 4 -
让原先的第a[i]个结点变为node[i],故数组
a[6]={3,5,2,6,4,1}
-
故输入
(7-a[i])
为4 2 5 1 3 6
,过关截图如下:
-
7. phase_defused (含secret_phase)
-
看似结束了。但回到
bomb.c
,在生成炸弹后,和输入有关的函数中,我们还有两个函数还没查看汇编代码,分别为read_line()
、
phase_defused()
。何况邪恶博士Dr. Evil还留下了这样一句话,好似幸灾乐祸:1
2
3/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */
//哇,他们明白了! 但是,是不是……缺少了什么? 也许他们忽略了什么? 哇哈哈哈哈哈! -
先看了看
read_line()
,就是对一行输入进行花里胡哨的处理后保存在指定地址,没有什么特别之处。 -
而
phase_defused()
的汇编代码如下:- 第一部分:
1
2
3
4
5
6
7Dump of assembler code for function phase_defused:
0x0804926b <+0>: sub $0x8c,%esp
0x08049271 <+6>: mov %gs:0x14,%eax ;取gs:0x14处的值
0x08049277 <+12>: mov %eax,0x7c(%esp) ;置于M[0x7c+esp]
0x0804927b <+16>: xor %eax,%eax ;ZF=1
0x0804927d <+18>: cmpl $0x6,0x804c3cc ;比较M[0x804c3cc]和6的值
0x08049284 <+25>: jne 0x80492f8 <phase_defused+141> ;不相等则跳过secret_phase,故我们要让其相等-
猜测6和我们的关卡数量有关。故在进入每个关卡的函数的位置设断点,观察
0x804c3cc
的变化情况:-
进入关卡1前,
M[0x804c3cc]=0
: -
进入关卡1后,
M[0x804c3cc]=1
: -
进入关卡2后,
M[0x804c3cc]=2
: -
进入关卡3后,
M[0x804c3cc]=3
: -
进入关卡4后,
M[0x804c3cc]=4
: -
进入关卡5后,
M[0x804c3cc]=5
: -
进入关卡6后,
M[0x804c3cc]=6
:
-
-
显然只有在第6关之后,
M[0x804c3cc]=6
,才可能进入secret_phase。 -
第二部分:
1
2
3
4
5
6
7
8
9
10
110x08049286 <+27>: lea 0x2c(%esp),%eax ;eax=(0x2c+esp)
0x0804928a <+31>: mov %eax,0x10(%esp) ;M[0x10+esp]=eax=(0x2c+esp)
0x0804928e <+35>: lea 0x28(%esp),%eax ;eax=(0x28+esp)
0x08049292 <+39>: mov %eax,0xc(%esp) ;M[0xc+esp]=eax=(0x28+esp)
0x08049296 <+43>: lea 0x24(%esp),%eax ;eax=(0x24+esp)
0x0804929a <+47>: mov %eax,0x8(%esp) ;M[0x8+esp]=eax=(0x24+esp)
0x0804929e <+51>: movl $0x804a3d1,0x4(%esp) ;M[0x4+esp]=eax=0x804a3d1
0x080492a6 <+59>: movl $0x804c4d0,(%esp) ;M[esp]=0x804c4d0--从该位置读入
0x080492ad <+66>: call 0x8048870 <__isoc99_sscanf@plt>
0x080492b2 <+71>: cmp $0x3,%eax ;输入参数个数应等于3
0x080492b5 <+74>: jne 0x80492ec <phase_defused+129> ;否则不会到达scret_phase-
调用
<__isoc99_sscanf@plt>
从0x804c4d0读入参数。根据函数的参数可知,共读入3个参数分别保存在(0x24+esp)
、(0x28+esp)
、(0x2c+esp)
。输入参数的格式为地址0x804a3d1
所示:
-
-
第三部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
270x080492b7 <+76>: movl $0x804a3da,0x4(%esp) ;M[0x4+esp]=0x804a3da
0x080492bf <+84>: lea 0x2c(%esp),%eax ;eax=(0x2c+esp)
0x080492c3 <+88>: mov %eax,(%esp) ;M[esp]=eax=(0x2c+esp)
0x080492c6 <+91>: call 0x8048fd4 <strings_not_equal>
;0x804a3da存储的字符串和(0x2c+esp)中的字符串的字符串进行比较。若相等则eax=0。
;0x804a3da存储的字符串为 "DrEvil"
;(0x2c+esp)存储的为<__isoc99_sscanf@plt>读入的第三个参数%s对应的字符串。
0x080492cb <+96>: test %eax,%eax
0x080492cd <+98>: jne 0x80492ec <phase_defused+129> ;如果eax!=0,则会跳过secret_phase
0x080492cf <+100>: movl $0x804a2a0,(%esp) ;M[esp]=0x804a2a0
0x080492d6 <+107>: call 0x8048800 <puts@plt>
;查看0x804a2a0存储的值,得输出的字符串:"Curses, you've found the secret phase!"”
0x080492db <+112>: movl $0x804a2c8,(%esp) ;;M[esp]=0x804a2c8
0x080492e2 <+119>: call 0x8048800 <puts@plt>
;查看0x804a2c8存储的值,得输出的字符串:"But finding it and solving it are quite different..."
0x080492e7 <+124>: call 0x8048ec2 <secret_phase> ;secret_phase:顾名思义,秘密关卡!
0x080492ec <+129>: movl $0x804a300,(%esp)
0x080492f3 <+136>: call 0x8048800 <puts@plt>
0x080492f8 <+141>: mov 0x7c(%esp),%eax ;取出原%gs:0x14的值
0x080492fc <+145>: xor %gs:0x14,%eax ;与现值异或
0x08049303 <+152>: je 0x804930a <phase_defused+159>
;异或结果为0,栈保护者canary未被非法篡改,安全退出程序
0x08049305 <+154>: call 0x80487d0 <__stack_chk_fail@plt>
;否则ZF=0,调用__stack_chk_fail,退出程序
0x0804930a <+159>: add $0x8c,%esp
0x08049310 <+165>: ret
End of assembler dump.-
此时需要找到地址0x804c4d0存储的是什么,哪来的
%d %d %s
。 -
故在进入每个关卡的函数的位置设断点,观察地址
0x804c4d0
存储值的变化,进行调试:-
进入前三关,皆无变化:
-
进入第4关,变化来了:
-
故
0x804c4d0
存储的是第4关的输入。
-
-
我们在第4关的输入后增加字符串"DrEvil" ,即可打开秘密关卡secret_phase:
-
secret_phase
-
汇编代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30Dump of assembler code for function secret_phase:
0x08048ec2 <+0>: push %ebx ;ebx入栈
0x08048ec3 <+1>: sub $0x18,%esp ;esp=esp-0x18,开辟栈帧空间
0x08048ec6 <+4>: call 0x804910d <read_line> ;读取一行,保存在eax中
0x08048ecb <+9>: movl $0xa,0x8(%esp) ;M[0x8+esp]=0xa=10 --strtol的第3个参数
0x08048ed3 <+17>: movl $0x0,0x4(%esp) ;M[0x4+esp]=0 --strtol的第2个参数
0x08048edb <+25>: mov %eax,(%esp) ;M[esp]=eax --strtol的第1个参数
0x08048ede <+28>: call 0x80488e0 <strtol@plt>
;strtol将eax存储的字符串转化为10进制整数(使结果没有""),结束符为空字符。
;返回值10进制整数保存在eax
0x08048ee3 <+33>: mov %eax,%ebx ;ebx=eax
0x08048ee5 <+35>: lea -0x1(%eax),%eax ;eax=eax-1;
0x08048ee8 <+38>: cmp $0x3e8,%eax ;比较eax和0x3e8
0x08048eed <+43>: jbe 0x8048ef4 <secret_phase+50> ;若eax<=0x3e8,则不爆炸
0x08048eef <+45>: call 0x80490e6 <explode_bomb>
0x08048ef4 <+50>: mov %ebx,0x4(%esp) ;M[0x4+esp]=ebx
0x08048ef8 <+54>: movl $0x804c088,(%esp) ;M[esp]=0x804c088
0x08048eff <+61>: call 0x8048e71 <fun7>
;调用函数fun7(0x804c088,ebx),返回值保存在eax
0x08048f04 <+66>: cmp $0x4,%eax
0x08048f07 <+69>: je 0x8048f0e <secret_phase+76> ;若eax=4,则不爆炸且过关
0x08048f09 <+71>: call 0x80490e6 <explode_bomb>
0x08048f0e <+76>: movl $0x804a240,(%esp) ;M[esp]=0x804a240
0x08048f15 <+83>: call 0x8048800 <puts@plt>
;输出:"Wow! You've defused the secret stage!"
0x08048f1a <+88>: call 0x804926b <phase_defused> ;退出秘密关卡
0x08048f1f <+93>: add $0x18,%esp
0x08048f22 <+96>: pop %ebx
0x08048f23 <+97>: ret
End of assembler dump.-
其中,
fun7
的汇编代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30Dump of assembler code for function fun7:
0x08048e71 <+0>: push %ebx ;ebx入栈
0x08048e72 <+1>: sub $0x18,%esp ;esp=esp-0x18,开辟栈帧空间
0x08048e75 <+4>: mov 0x20(%esp),%edx ;edx=M[0x20+esp] --第一个参数
0x08048e79 <+8>: mov 0x24(%esp),%ecx ;ecx=M[0x24+esp] --第二个参数
0x08048e7d <+12>: test %edx,%edx
0x08048e7f <+14>: je 0x8048eb8 <fun7+71> ;如果edx=0 跳转->return 0xffffffff
0x08048e81 <+16>: mov (%edx),%ebx ;ebx=M[edx]
0x08048e83 <+18>: cmp %ecx,%ebx
0x08048e85 <+20>: jle 0x8048e9a <fun7+41> ;如果ebx<=ecx(有符号数),跳转至<fun7+41>
0x08048e87 <+22>: mov %ecx,0x4(%esp) ;M[0x4+esp]=ecx
0x08048e8b <+26>: mov 0x4(%edx),%eax ;eax=M[0x4+edx]
0x08048e8e <+29>: mov %eax,(%esp) ;M[esp]=eax=M[0x4+edx]
0x08048e91 <+32>: call 0x8048e71 <fun7> ;递归调用函数fun7(M[0x4+edx],ecx)
0x08048e96 <+37>: add %eax,%eax ;eax=2*fun7(M[0x4+edx],ecx)
0x08048e98 <+39>: jmp 0x8048ebd <fun7+76> ;跳转-> return 2*fun7(M[0x4+edx],ecx);
0x08048e9a <+41>: mov $0x0,%eax ;eax=0
0x08048e9f <+46>: cmp %ecx,%ebx
0x08048ea1 <+48>: je 0x8048ebd <fun7+76> ;如果ebx=ecx,跳转-> return 0;
0x08048ea3 <+50>: mov %ecx,0x4(%esp) ;M[0x4+esp]=ecx
0x08048ea7 <+54>: mov 0x8(%edx),%eax ;eax=M[0x8+edx]
0x08048eaa <+57>: mov %eax,(%esp) ;M[esp]=eax
0x08048ead <+60>: call 0x8048e71 <fun7> ;递归调用函数fun7(M[0x8+edx],ecx)
0x08048eb2 <+65>: lea 0x1(%eax,%eax,1),%eax;return eax=2*fun7(M[0x8+edx],ecx)+1
0x08048eb6 <+69>: jmp 0x8048ebd <fun7+76>
0x08048eb8 <+71>: mov $0xffffffff,%eax
0x08048ebd <+76>: add $0x18,%esp
0x08048ec0 <+79>: pop %ebx
0x08048ec1 <+80>: ret
End of assembler dump.-
显然fun7是个含2个参数的递归函数,C++代码可如下表示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16#include<iostream>
using namespace std;
int fun7(int *p,int m)
{
if(p==0)
return -1;
else
{
if(*p<m)
return 2*fun7(*(p+2),m)+1;
else if(*p==m)
return 0
else
return 2*fun7(*(p+1),m);
}
}-
这个递归的返回值为0时结束递归,对于每个返回值,根据与m的大小关系,有两个递归分支,分别是和。
-
fun7最终的返回值为4,故
0->1->2->4
为递归返回的路径,则递归调用时地址的增量为0x4,0x4,0x8。 -
故
m=7
,使得在最后一层递归返回0。整个过程如下:*p=24>m,return 2*fun7(*(p+1),m)
*p=8>m,return 2*fun7(*(p+1),m)
*p=6<m,return 2*fun7(*(p+1),m)+1
*p=7=m,return 0
-
-
-
-
输入7,fun7得到返回值4,通关:
实验结果
实验心得
-
这次实验的各个关卡对汇编语言实现的功能各有侧重:
phase_1:字符串在内存地址中的存放、访问,字符串长度的获取与字符串的比较。
phase_2:利用循环得到存储等比数列的数组。
phase_3:实现switch语句,需要查看跳转表。
phase_4:递归函数的实现。
phase_5:数组及其索引。
phase_6:链表及其重组。
secret_phase:通过含指针参数的递归函数实现地址跳转。
通过学习、理解汇编代码在该实验中实现的各种功能,我对汇编代码的各种指令,如比较、跳转、调用指令等有了更深的理解。当遇到难以直接明白其功能的地方,我会尝试把汇编语言转换成自己编写的C++代码帮助理解,从而对汇编的功能实现有了更全面的认识。
-
在实验过程中难免需要对汇编代码进行调试,查看某个寄存器或在某个地址内存的值及其变化。所以gdb工具的使用是不可或缺的。正是通过完成这次实验,我对gdb工具的使用愈发熟练。
-
在进行函数调用时,栈帧的变化格外重要。通过这次实验对汇编代码的调试,思考在何处保存函数的参数、何处保存临时变量等,我对栈帧的结构有了更深刻的认识。
-
进行实验,细心和耐心也是很重要的品质。有时候会因为不够细心而耽误时间,如phase_4中我看反了两个输入参数的位置,但好在能够及时发现并改正。在遭遇比较复杂的结构,如phase_6中重构链表的双重循环,则需要耐心分析汇编语句。有了细心和耐心的加持,才能更好地闯关一个个关卡,收获知识,提升技能。
本博客所有文章除特别声明外,均为博客作者本人编写整理,转载请联系作者!