CSAPP lab2 哈哈~ 不愧是“美国进口的六级*”!爽歪歪的“升级打怪” 我把实验材料都上传到下面这个link了,0分下载(良心啊~) http://download.csdn.net/detail/u011368821/7892649 再一个实验指导说明供大家下载: http://download.csdn.net/detail/u011
哈哈~ 不愧是“美国进口的六级*”!爽歪歪的“升级打怪”
我把实验材料都上传到下面这个link了,0分下载(良心啊~)
http://download.csdn.net/detail/u011368821/7892649
再一个实验指导说明供大家下载:
http://download.csdn.net/detail/u011368821/7892677
0000000000400ef0: 400ef0: 48 83 ec 08 sub $0x8,%rsp 400ef4: be 18 1b 40 00 mov $0x401b18,%esi 400ef9: e8 10 04 00 00 callq 40130e 400efe: 85 c0 test %eax,%eax 400f00: 74 05 je 400f07 400f02: e8 dc 07 00 00 callq 4016e3 400f07: 48 83 c4 08 add $0x8,%rsp 400f0b: c3 retq
注意到这句指令.
400ef4:be 18 1b 40 00 mov $0x401b18,%esi
ESI/EDI 分别叫做"源/目标索引寄存器"(source/destination
index),因为在很多字符串操作指令中
就是要培养的对寄存器很敏感的能力.上面mov $0x401b18,%esi之后立马callq string_not_equal.这是在bomb爆炸之前.我们看看string_not_equal 里面能找到什么线索
000000000040130e: 40130e: 41 54 push %r12 401310: 55 push %rbp 401311: 53 push %rbx 401312: 48 89 fb mov %rdi,%rbx 401315: 48 89 f5 mov %rsi,%rbp 401318: e8 d4 ff ff ff callq 4012f1 40131d: 41 89 c4 mov %eax,%r12d 401320: 48 89 ef mov %rbp,%rdi 401323: e8 c9 ff ff ff callq 4012f1 401328: ba 01 00 00 00 mov $0x1,%edx 40132d: 41 39 c4 cmp %eax,%r12d 401330: 75 3e jne 401370 ... ... 401369: eb 05 jmp 401370 40136b: ba 01 00 00 00 mov $0x1,%edx 401370: 89 d0 mov %edx,%eax 401372: 5b pop %rbx 401373: 5d pop %rbp 401374: 41 5c pop %r12 401376: c3 retq
我们可以看到这里有连个string_length. 注意到之前的%rdi 和%rsi 分别被mov到%rbx和%rbp中
%rdi记录的是第一个参数的地址,于是当第一次调用直接第一个参数(指针)指向的字符串,
经过
mov %rsi %rbp
mov %rbp %rdi
这样第二次传入string_length的参数就变成了string_not_equal的第二个参数(指针),
string_length 对这两个不同指针指向的字符串进行计算,返回它们的长度值,如果不一致,返回1,如果一致返回0(关于返回值的情况这里的汇编不足以解释,还要分析所有的string_not_equal反汇编才会知道)
phase_1中
callq 40130e
test %eax,%eax
je 如果上一个test指令的结果是0就跳转,否者不跳转
这里如果返回值是0就跳转到了
add $0x8,%rsp
retq
正确返回弹栈了。
上面唧唧歪歪说了这么一对铺垫就一个目的,推理出
400ef4:be 18 1b 40 00 mov $0x401b18,%esi涉及的这个0x401b18是不寻常的,它指向一个字符串,而我们输入的字符串必须和这个指针指向的字符串完全相同才能不触发bomb否者触发BOMB
那就看看0x401b18这里究竟都藏了什么!
gdb debug去
Science isn't about why, it's about why not?
输入0x401b18 指向的字符串即可过了phase 1 : -)
main函数部分截选
400e30: e8 bb 00 00 00 callq 400ef0我们可以看到搞定了phase_1这里又要进入pahse_2鸟~400e35: e8 f2 09 00 00 callq 40182c 400e3a: bf 08 1a 40 00 mov $0x401a08,%edi 400e3f: e8 ac fc ff ff callq 400af0 400e44: e8 fe 08 00 00 callq 401747 400e49: 48 89 c7 mov %rax,%rdi 400e4c: e8 bb 00 00 00 callq 400f0c
mov $0x401a08,%edi
0x401a08这个地址装着是一个普通的提示字符串不必在意(测试过了)
看看pahse_2
0000000000400f0c: 400f0c: 55 push %rbp 400f0d: 53 push %rbx 400f0e: 48 83 ec 28 sub $0x28,%rsp 400f12: 48 89 e6 mov %rsp,%rsi 400f15: e8 eb 07 00 00 callq 401705 400f1a: 83 3c 24 01 cmpl $0x1,(%rsp) 400f1e: 74 25 je 400f45 400f20: e8 be 07 00 00 callq 4016e3 400f25: eb 1e jmp 400f45 400f27: 83 c3 01 add $0x1,%ebx 400f2a: 89 d8 mov %ebx,%eax 400f2c: 0f af 45 fc imul -0x4(%rbp),%eax 400f30: 39 45 00 cmp %eax,0x0(%rbp) 400f33: 74 05 je 400f3a 400f35: e8 a9 07 00 00 callq 4016e3 400f3a: 48 83 c5 04 add $0x4,%rbp 400f3e: 83 fb 06 cmp $0x6,%ebx 400f41: 75 e4 jne 400f27 400f43: eb 0c jmp 400f51 400f45: 48 8d 6c 24 04 lea 0x4(%rsp),%rbp 400f4a: bb 01 00 00 00 mov $0x1,%ebx 400f4f: eb d6 jmp 400f27 400f51: 48 83 c4 28 add $0x28,%rsp 400f55: 5b pop %rbx 400f56: 5d pop %rbp 400f57: c3 retq
核心语句
imul -0x4(%rbp),%eax
cmp %eax,0x0(%rbp) //如果比较结果是0,就跳转,否则BOMB
je 400f3a
callq 4016e3
callq 401705
cmpl $0x1,(%rsp)
je 400f45
callq 4016e3
首先比较第一个输入的数(%rsp)和1是否相等,如果是则跳转到400f45标记的位置,否者callq explode_bomb,爆炸
那么可以确定,第一个输入的数字必须是1,接着我们跳转到 400f45
400f45: 48 8d 6c 24 04 lea 0x4(%rsp),%rbp 400f4a: bb 01 00 00 00 mov $0x1,%ebx 400f4f: eb d6 jmp 400f27这里lea把rsp寄存器指向的指针+4,然后赋值给rbp,并把%edx寄存器初始化为1
最后跳转到400f27的位置
400f27: 83 c3 01 add $0x1,%ebx 400f2a: 89 d8 mov %ebx,%eax 400f2c: 0f af 45 fc imul -0x4(%rbp),%eax 400f30: 39 45 00 cmp %eax,0x0(%rbp) 400f33: 74 05 je 400f3a此时add 把ebx 增 1,然后赋值给%eax,即数值2.400f35: e8 a9 07 00 00 callq 4016e3 400f3a: 48 83 c5 04 add $0x4,%rbp 400f3e: 83 fb 06 cmp $0x6,%ebx 400f41: 75 e4 jne 400f27
把-0x4(%rbp)即第一个输入的数字,imul 乘以%eax的值,然后赋值给%eax,即 %eax = 1*2
cmp把此时的%eax 和当前输入0x0(%rbp),此时为第二个输入数字,比较。如果相同就跳转到400f3a的位置,否者callq explode_bomb爆炸. 由此我们可以知道,第二个输入的数字必须是2.
如果第二个数字输入的是2,接着add 把%rbp +4, 接着cmp 比较%ebx(此时为2)和0x6比较,不相等就跳转到400f27,
于是我们又回到了400f27的位置,有点汇编经验的人一看就可以发现,这里其实是一个循环. %ebx每次+1, 循环6次。
400f2c: 0f af 45 fc imul -0x4(%rbp),%eax 400f30: 39 45 00 cmp %eax,0x0(%rbp)每次都把%eax和前一个输入数字相乘,赋值给%eax,并和当前数字0x0(%rbp)比较。
这样一步步按照汇编语句构造的循环,执行下去,可以发现要想不bomb,输入应该是
1 2 6 24 120 720
OK~ 搞定,phase_2。
当我们搞定phase_2之后,phase_3来了~
400e4c: e8 bb 00 00 00 callq 400f0c400e51: e8 d6 09 00 00 callq 40182c 400e56: bf 67 19 40 00 mov $0x401967,%edi 400e5b: e8 90 fc ff ff callq 400af0 400e60: e8 e2 08 00 00 callq 401747 400e65: 48 89 c7 mov %rax,%rdi 400e68: e8 eb 00 00 00 callq 400f58 400e6d: e8 ba 09 00 00 callq 40182c
phase_3:
0000000000400f58: 400f58: 48 83 ec 18 sub $0x18,%rsp 400f5c: 4c 8d 44 24 08 lea 0x8(%rsp),%r8 //把%rsp指向的地址+0x8 赋值给%rcx ,以下的lea操作同理 400f61: 48 8d 4c 24 07 lea 0x7(%rsp),%rcx 400f66: 48 8d 54 24 0c lea 0xc(%rsp),%rdx 400f6b: be 6e 1b 40 00 mov $0x401b6e,%esi //把0x401b6赋值给%esi ,注意esi是字符串相关寄存器,我们看看这个地址里面是什么 400f70: b8 00 00 00 00 mov $0x0,%eax 400f75: e8 86 fc ff ff callq 400c00 <__isoc99_sscanf@plt> 400f7a: 83 f8 02 cmp $0x2,%eax 400f7d: 7f 05 jg 400f84 400f7f: e8 5f 07 00 00 callq 4016e3 400f84: 83 7c 24 0c 07 cmpl $0x7,0xc(%rsp) //比较0xc(%rsp)处的值和7比较,如果大于7,ja跳转,接着bomb,于是我们知道0xc(%rsp)处的值要在(0~7) 400f89: 0f 87 fc 00 00 00 ja 40108b 400f8f: 8b 44 24 0c mov 0xc(%rsp),%eax 400f93: ff 24 c5 80 1b 40 00 jmpq *0x401b80(,%rax,8) 400f9a: b8 6a 00 00 00 mov $0x6a,%eax //把0x6a赋值给%eax,这里很重要,每一个witch分支的这里都不一样,而且第二个输入参数和这个有关系 400f9f: 81 7c 24 08 40 02 00 cmpl $0x240,0x8(%rsp) //把0x8(%rsp)的值和0x240比较,如果不相等,bomb,相等就正常跳转到 401095 400fa6: 00 400fa7: 0f 84 e8 00 00 00 je 401095 400fad: e8 31 07 00 00 callq 4016e3 400fb2: b8 6a 00 00 00 mov $0x6a,%eax 400fb7: e9 d9 00 00 00 jmpq 401095 400fbc: b8 66 00 00 00 mov $0x66,%eax 400fc1: 81 7c 24 08 bc 03 00 cmpl $0x3bc,0x8(%rsp) 400fc8: 00 400fc9: 0f 84 c6 00 00 00 je 401095 400fcf: e8 0f 07 00 00 callq 4016e3 400fd4: b8 66 00 00 00 mov $0x66,%eax 400fd9: e9 b7 00 00 00 jmpq 401095 400fde: b8 6a 00 00 00 mov $0x6a,%eax 400fe3: 81 7c 24 08 2a 02 00 cmpl $0x22a,0x8(%rsp) 400fea: 00 400feb: 0f 84 a4 00 00 00 je 401095 400ff1: e8 ed 06 00 00 callq 4016e3 400ff6: b8 6a 00 00 00 mov $0x6a,%eax 400ffb: e9 95 00 00 00 jmpq 401095 401000: b8 76 00 00 00 mov $0x76,%eax 401005: 81 7c 24 08 c9 00 00 cmpl $0xc9,0x8(%rsp) 40100c: 00 40100d: 0f 84 82 00 00 00 je 401095 401013: e8 cb 06 00 00 callq 4016e3 401018: b8 76 00 00 00 mov $0x76,%eax 40101d: eb 76 jmp 401095 40101f: b8 62 00 00 00 mov $0x62,%eax 401024: 81 7c 24 08 07 01 00 cmpl $0x107,0x8(%rsp) 40102b: 00 40102c: 74 67 je 401095 40102e: e8 b0 06 00 00 callq 4016e3 401033: b8 62 00 00 00 mov $0x62,%eax 401038: eb 5b jmp 401095 40103a: b8 69 00 00 00 mov $0x69,%eax 40103f: 81 7c 24 08 3b 03 00 cmpl $0x33b,0x8(%rsp) 401046: 00 401047: 74 4c je 401095 401049: e8 95 06 00 00 callq 4016e3 40104e: b8 69 00 00 00 mov $0x69,%eax 401053: eb 40 jmp 401095 401055: b8 71 00 00 00 mov $0x71,%eax 40105a: 81 7c 24 08 c6 00 00 cmpl $0xc6,0x8(%rsp) 401061: 00 401062: 74 31 je 401095 401064: e8 7a 06 00 00 callq 4016e3 401069: b8 71 00 00 00 mov $0x71,%eax 40106e: eb 25 jmp 401095 401070: b8 77 00 00 00 mov $0x77,%eax 401075: 81 7c 24 08 74 01 00 cmpl $0x174,0x8(%rsp) 40107c: 00 40107d: 74 16 je 401095 40107f: e8 5f 06 00 00 callq 4016e3 401084: b8 77 00 00 00 mov $0x77,%eax 401089: eb 0a jmp 401095 40108b: e8 53 06 00 00 callq 4016e3 401090: b8 68 00 00 00 mov $0x68,%eax 401095: 3a 44 24 07 cmp 0x7(%rsp),%al //把0x7(%rsp)和 %al(%eax低字节)比较,相等就跳转,否者,bomb 401099: 74 05 je 4010a0 40109b: e8 43 06 00 00 callq 4016e3 4010a0: 48 83 c4 18 add $0x18,%rsp 4010a4: c3 retq
注意到开头这里
mov $0x401b6e,%esi
把0x401b6赋值给%esi ,注意esi是字符串相关寄存器,我们看看这个地址里面是什么
我们发现是一个格式化输入说明字符串
于是我们就知道我们应该输入什么类型的数据了
400f75: callq 400c00 <__isoc99_sscanf@plt>
400f7a: cmp $0x2,%eax
400f7d: jg 400f84
400f7f: callq 4016e3
400f84: cmpl $0x7,0xc(%rsp)
根据前面的格式化字符串我们可以注意到
输入三个数据即正常跳转
这里又有个跳转语句
400f93: ff 24 c5 80 1b 40 00 jmpq *0x401b80(,%rax,8) 400f9a: b8 6a 00 00 00 mov $0x6a,%eax我们看看 *0x401b80处是什么东东
是一个数,0x400f9a,这个是指令mov $0x6a,%eax 的代码段标记地址
如果%rax是0,就直接跳转到0x400f9a 否则0x400f9a + %rax * 8
这里是典型的witch 语句的汇编形式...如果不明白就去看CSAPP第三章
上面给出的phase_3汇编部分注释我已经写的很清楚了~
400f5c: 4c 8d 44 24 08 lea 0x8(%rsp),%r8 //把%rsp指向的地址+0x8 赋值给%rcx ,以下的lea操作同理 400f61: 48 8d 4c 24 07 lea 0x7(%rsp),%rcx 400f66: 48 8d 54 24 0c lea 0xc(%rsp),%rdx这里可以推知phase_3一开始就保存了0x8(%rsp),0x7(%rsp),0xc(%rsp)三处的值,这三处肯定要被接下来的程序重写利用.
而分析偏移量0x8 0x7 0xc,以及格式化字符串参数入栈顺序可知,0xc(%rsp)储存第一个参数,0x7(%rsp)储存第二个参数,0x8(%rsp)储存第三个参数.
提示: printf("%d %d",a,b); 参数是b先入栈,然后是a
综合以上分析,输入
0 j 576
即可过关
这里我偷一下懒第一个选择了0
实际上第一个数据可以是0~7内的任意数字,这也会导致跳转至不同的分支,而影响第二个第三个参数的输入情况。
以0 作为第一个输入参数的例子,
400f9a: b8 6a 00 00 00 mov $0x6a,%eax
401095: 3a 44 24 07 cmp 0x7(%rsp),%al以上两个指令将被执行,我们可以知道0x7(%rsp)第二个输入的参数应当为0x6a对应的ascii码字符,于是这里应当输入 j
第三个参数的确定是在
400f9f: 81 7c 24 08 40 02 00 cmpl $0x240,0x8(%rsp) //把0x8(%rsp)的值和0x240比较,如果不相等,bomb,相等就正常跳转到 401095 400fa6: 00 400fa7: 0f 84 e8 00 00 00 je 401095于是第三个参数必须为0x240,否则bomb,这里是%d的读入,于是转化为相应的十进制数为576.
OK, Phase_3 搞定 halfway there!
注意到phase_4会调用到func4
00000000004010a5: 4010a5: 53 push %rbx 4010a6: 89 fb mov %edi,%ebx 4010a8: b8 01 00 00 00 mov $0x1,%eax 4010ad: 83 ff 01 cmp $0x1,%edi 4010b0: 7e 0b jle 4010bd 4010b2: 8d 7f ff lea -0x1(%rdi),%edi 4010b5: e8 eb ff ff ff callq 4010a5 4010ba: 0f af c3 imul %ebx,%eax 4010bd: 5b pop %rbx 4010be: c3 retq 00000000004010bf : 4010bf: 48 83 ec 18 sub $0x18,%rsp 4010c3: 48 8d 54 24 0c lea 0xc(%rsp),%rdx 4010c8: be 74 1b 40 00 mov $0x401b74,%esi 4010cd: b8 00 00 00 00 mov $0x0,%eax 4010d2: e8 29 fb ff ff callq 400c00 <__isoc99_sscanf@plt> 4010d7: 83 f8 01 cmp $0x1,%eax 4010da: 75 07 jne 4010e3 4010dc: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) 4010e1: 7f 05 jg 4010e8 4010e3: e8 fb 05 00 00 callq 4016e3 4010e8: 8b 7c 24 0c mov 0xc(%rsp),%edi 4010ec: e8 b4 ff ff ff callq 4010a5 4010f1: 3d 00 5f 37 00 cmp $0x375f00,%eax 4010f6: 74 05 je 4010fd 4010f8: e8 e6 05 00 00 callq 4016e3 4010fd: 48 83 c4 18 add $0x18,%rsp 401101: c3 retq
主题就是phase_4要求根据格式化字符串“%d”输入一个数字,这个数字传入func做递归乘法,相当于传入参数的阶乘.
最后
4010f1: 3d 00 5f 37 00 cmp $0x375f00,%eax返回值和0x375f00比较,如果不相等就bomb,于是我们必须找到阶乘等于0x375f00的数字.
的方法嘛.写个阶乘的C 函数去测试,什么输入参数的阶乘是0x375f00
#includeint main() { int ans = 1; int tmp = 1; for(tmp = 1;ans != 0x375f00;tmp++) { ans *= tmp; } printf("%d \n",tmp-1); return 0; }
0000000000401102: 401102: 53 push %rbx 401103: 48 89 fb mov %rdi,%rbx 401106: e8 e6 01 00 00 callq 4012f1 40110b: 83 f8 06 cmp $0x6,%eax //输入字符串的长度必须是6,否则bomb 40110e: 74 05 je 401115 401110: e8 ce 05 00 00 callq 4016e3 401115: b8 00 00 00 00 mov $0x0,%eax // 把%eax %edx 寄存器赋值为0 40111a: ba 00 00 00 00 mov $0x0,%edx 40111f: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx //%rbx + %rax*1 赋值给%ecx 401123: 83 e1 0f and $0xf,%ecx //取%ecx低4位赋值给%ecx 401126: 03 14 8d c0 1b 40 00 add 0x401bc0(,%rcx,4),%edx 40112d: 48 83 c0 01 add $0x1,%rax 401131: 48 83 f8 06 cmp $0x6,%rax 401135: 75 e8 jne 40111f 401137: 83 fa 3e cmp $0x3e,%edx 40113a: 74 05 je 401141 40113c: e8 a2 05 00 00 callq 4016e3 401141: 5b pop %rbx 401142: c3 retq
值得注意的是
401126: 03 14 8d c0 1b 40 00 add 0x401bc0(,%rcx,4),%edx这里又有个很特殊的地址0x401bc0
我尝试打印这里的数据,怀疑这里很可能是个数组
对于phase_5,概括性的说,就是输入6个ascii字符,然后这6个字符的低4位对应这个数组的index,然后对应数字累加要等于0x3e,即62
这里我的想法是62 = 2+2+10+16+16+16
对应的偏移量是001555
我们查看ascii码表可以发现,恰巧数字字符00155符合要求,于是输入00155
最后一关了!
0000000000401143: 401143: 48 89 f8 mov %rdi,%rax 401146: 4c 8b 47 08 mov 0x8(%rdi),%r8 40114a: 48 c7 47 08 00 00 00 movq $0x0,0x8(%rdi) 401151: 00 401152: 4d 85 c0 test %r8,%r8 401155: 75 48 jne 40119f 401157: f3 c3 repz retq 401159: 48 89 d1 mov %rdx,%rcx 40115c: 48 8b 51 08 mov 0x8(%rcx),%rdx 401160: 48 85 d2 test %rdx,%rdx 401163: 74 09 je 40116e 401165: 39 32 cmp %esi,(%rdx) 401167: 7f f0 jg 401159 401169: 48 89 d6 mov %rdx,%rsi 40116c: eb 03 jmp 401171 40116e: 48 89 d6 mov %rdx,%rsi 401171: 48 39 ce cmp %rcx,%rsi 401174: 74 06 je 40117c 401176: 4c 89 41 08 mov %r8,0x8(%rcx) 40117a: eb 13 jmp 40118f 40117c: 4c 89 c0 mov %r8,%rax 40117f: eb 0e jmp 40118f 401181: 48 89 c2 mov %rax,%rdx 401184: 4c 89 c0 mov %r8,%rax 401187: eb 06 jmp 40118f 401189: 48 89 c2 mov %rax,%rdx 40118c: 4c 89 c0 mov %r8,%rax 40118f: 49 8b 48 08 mov 0x8(%r8),%rcx 401193: 49 89 50 08 mov %rdx,0x8(%r8) 401197: 48 85 c9 test %rcx,%rcx 40119a: 74 14 je 4011b0 40119c: 49 89 c8 mov %rcx,%r8 40119f: 48 85 c0 test %rax,%rax 4011a2: 74 dd je 401181 4011a4: 41 8b 30 mov (%r8),%esi 4011a7: 39 30 cmp %esi,(%rax) 4011a9: 7e de jle 401189 4011ab: 48 89 c1 mov %rax,%rcx 4011ae: eb ac jmp 40115c 4011b0: f3 c3 repz retq 00000000004011b2 : 4011b2: 48 83 ec 08 sub $0x8,%rsp 4011b6: ba 0a 00 00 00 mov $0xa,%edx 4011bb: be 00 00 00 00 mov $0x0,%esi 4011c0: e8 1b fa ff ff callq 400be0 4011c5: 89 05 55 21 20 00 mov %eax,0x202155(%rip) # 603320 4011cb: bf 20 33 60 00 mov $0x603320,%edi 4011d0: e8 6e ff ff ff callq 401143 4011d5: 48 8b 40 08 mov 0x8(%rax),%rax 4011d9: 8b 0d 41 21 20 00 mov 0x202141(%rip),%ecx # 603320 4011df: 39 08 cmp %ecx,(%rax) 4011e1: 74 05 je 4011e8 4011e3: e8 fb 04 00 00 callq 4016e3 4011e8: 48 83 c4 08 add $0x8,%rsp 4011ec: c3 retq
这步不要陷入到func6了,逐步的去gdb stepi nexti 调试就是了. 具体的调试方法不讲了
值得一提的就是这里
4011c5: 89 05 55 21 20 00 mov %eax,0x202155(%rip) # 603320这里0x202155(%rip) 0x202141(%rip)都是指向同一个地址,这个地址储存着输入数据4011d9: 8b 0d 41 21 20 00 mov 0x202141(%rip),%ecx # 603320
cmp %ecx,(%rax)最后这里比较的时候,直接gdb查看%rax指向的地址存的什么东东,
打印出来
826!于是我们就应该输入826
其实应该还有个彩蛋——secrete phase
我目前~没心思看了,过两天继续更新
*****************************************************************************************************************************
参考资料——向所有前辈致敬, sharing,step together! open source!
逆向工程——二进制*(CSAPP Project)
http://www.cnblogs.com/remlostime/archive/2011/05/21/2052708.html
http://blog.csdn.net/u013648407/article/details/24845129
CSAPP:二进制*实验 lycos的六度空间
http://blog.csdn.net/caoxu1987728/article/details/6056947
最后感谢
呵呵,国内的某些大学学着点别人的风度.
***************************************************************************************************************************