吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1299|回复: 5
上一主题 下一主题
收起左侧

[CTF] 2025腾讯游戏安全技术竞赛决赛题解

[复制链接]
跳转到指定楼层
楼主
xia0ji233 发表于 2025-4-14 10:31 回帖奖励

题目描述

(1)在intel CPU/64位Windows10系统上运行sys,成功加载驱动(0.5分)

(2)能在双机环境运行驱动并调试(1分)

(3)优化驱动中的耗时算法,并给出demo能快速计算得出正确的key(1分)

(4)分析并给出flag的计算执行流程(1.5分),能准确说明其串联逻辑(0.5分)

(5)正确解出flag(1分)

(6)该题目使用了一种外挂常用的隐藏手段,请给出多种检测方法,要求demo程序能在题目驱动运行的环境下进行精确检测,方法越多分数越高(3分)

(7)文档编写,详细描述解题过程,详述提供的解题程序的演示方法。做到清晰易懂,操作可以复现结果;编码工整风格优雅、注释详尽(1.5分)

加载驱动

驱动带反调,且目测有 VMP 壳,于是选择 dump+Fix,由于驱动带反调,会蓝屏,于是 hook 蓝屏代码,选择该时机去 dump 内存,找到 Entry。

随后跟进,遇到一些立即数的赋值,且有函数加密,直接选择模拟执行。

import idaapi
import idc
from unicorn import *
from unicorn.x86_const import *
import ida_name
import mmap
import sys
import idautils
import struct
base_addr = idaapi.get_imagebase()
fix_function_start=0xFFFFF806FF8D9F0C
fix_function_end=0xFFFFF806FF8DA05F
PAGE_SIZE=0x1000
RSP=0xdead0000
RBP=0xdead0000
map_addr=idaapi.get_imagebase()
offset=base_addr-map_addr
def hook_mem_unmapped(uc, access, address, size, value, user_data):
    aligned_addr = address&0xFFFFFFFFFFFFF000 
    try:
        uc.mem_map(aligned_addr, PAGE_SIZE)
        data=idaapi.get_bytes(aligned_addr,PAGE_SIZE)
        uc.mem_write(aligned_addr,data)
        return True  # 表示错误已处理,继续执行
    except Exception as e:
        print(f"[-] 动态映射内存页失败: {e}")
        return False  

instr_count = 0
cnt=0
def hook_code(uc, address, size, user_data):
    global instr_count,cnt
    instr_count += 1
    rax=uc.reg_read(UC_X86_REG_RAX)
    rcx=uc.reg_read(UC_X86_REG_RCX)
    rdx=uc.reg_read(UC_X86_REG_RDX)
    r8=uc.reg_read(UC_X86_REG_R8)
    r9=uc.reg_read(UC_X86_REG_R9)
    r10=uc.reg_read(UC_X86_REG_R10)
    rbp=uc.reg_read(UC_X86_REG_RBP)
    rsp=uc.reg_read(UC_X86_REG_RSP)
    rip=uc.reg_read(UC_X86_REG_RIP)
    if rip==0xFFFFF806FF8DA05C:
        st=b''
        offset=0xE0
        while True:
            if uc.mem_read(rsp+offset,2)==b'\x00\x00':
                break
            st+=uc.mem_read(rsp+offset,1)
            offset+=2
        print(st)
        #print(uc.mem_read(rsp+offset))

mu = Uc(UC_ARCH_X86, UC_MODE_64)
mu.reg_write(UC_X86_REG_RIP, fix_function_start)  # 设置执行起始地址
mu.reg_write(UC_X86_REG_R13, 0xFF)
mu.reg_write(UC_X86_REG_RSP, RSP)
mu.reg_write(UC_X86_REG_RBP, RBP)
mu.mem_map(RSP-PAGE_SIZE,PAGE_SIZE*2)
mu.hook_add(UC_HOOK_MEM_FETCH_UNMAPPED, hook_mem_unmapped)
mu.hook_add(UC_HOOK_MEM_READ_UNMAPPED, hook_mem_unmapped)
mu.hook_add(UC_HOOK_MEM_WRITE_UNMAPPED, hook_mem_unmapped)
mu.hook_add(UC_HOOK_CODE, hook_code)
instr_count=0
print(hex(mu.reg_read(UC_X86_REG_RIP)))
try:
    mu.emu_start(fix_function_start,fix_function_end)
except UcError as e:
    print(e)
    pass

最后得到一个注册表字符串 \\Machine\\System\\CurrentControlSet\\Services\\ACEDriver\\2025ACECTF

正常直接加载驱动会返回 31 错误,猜测判定了注册表的某些东西,继续往下模拟可得一个字符串 Key。

模拟执行可以尽量挑不依赖外部函数,且立即数比较多的片段,这样可以省略计算的过程。

下面也可以模拟,但是根据题目描述也能猜个大概,有一个 Key,有一个 Flag。再结合该函数的定义和调用

不难得到 Key 应该是一个 __int64 的值,Flag 是一个字符串,保存到全局变量当中,创建对应的注册表项,成功加载驱动。

调试驱动

前面说过,有反调试,观察导入表遍历了 NtQuerySystemInformation,于是想到可能是检测到了 kdcom.dll 模块(因为之前有游戏做过类似的检测),那么直接 hook 把 kdcom.dll 改名。

因为保护了 IAT,因此不能使用常规的 IAT hook,还是选择使用 inline hook

NTSTATUS gh_NtQuerySystemInformation(...)
{
    unhook();
    auto ret = ((NtQuerySystemInformation_t)(TargetFunction))(...);
    if (SystemInformationLength&& SystemInformationClass== SystemModuleInformation) {
        PSYSTEM_MODULE_INFORMATION pModInfo = (PSYSTEM_MODULE_INFORMATION)SystemInformation;
        for (int i = 0; i < pModInfo->ModulesCount; i++) {
            PSYSTEM_MODULE_INFORMATION_ENTRY pEntry = &pModInfo->Modules[i];
            if (strcmp(pEntry->Name + pEntry->NameOffset, "kdcom.dll")) {
                (pEntry->Name + pEntry->NameOffset)[0] = 'x';
            }
        }
    }
    rehook();
    return ret;
}

绕过之后加载驱动不会蓝屏,但是会出现另一个错误。

随后查看 DbgView 发现似乎是 vmp 自带的,手上有 3.8 版本,尝试编译放进去加载,果然如此,一摸一样的错误代码。

Key算法分析

这个可以通过字符串定位,也可以由上面注册表继续往后分析得到。

当输入的 Key0 时,尝试使用算法生成。通过分析该函数,结合一些一些字符串可知,该算法自己实现了一个双端队列(deque),但是实际使用的时候是把它当成栈来用了,实现了一个深度优先搜索算法。

第一步恢复 deque 结构体,第一个 8 字节是一个指向自身的指针,但是似乎没有用过,正常来说应该是虚表。双向队列会有全队列大小(队列最多容纳的元素个数),头指针还有尾指针,而通常情况下,后两者可以使用头指针 + 有效元素个数来实现,因此最后得到以下定义:

struct deque
{
    void *vtable;
    data **map;
    __int64 MAX_SIZE;
    __int64 begin_idx;
    __int64 size;
};
struct data
{
    int x1;
    int y1;
    _QWORD data2;
    _QWORD data3;
    int x4;
    int y4;
};

应用到 IDA 之后,配合注释,算法一目了然。

深入阅读它实现 deque 的源码其实可以明白,第一,它的 MAX_SIZE 一定是 2 的整数幂,并且它是环形队列。第二,在取模的时候更加高效(即 &(MAX_SIZE-1))。

循环开头压入了 (44,22) 元素。

每次循环开始,取得尾部的元素,判断 x1 是否为 0,或者说 x==y,如果是则删除该元素。

否则尝试先往左走(即 x-1)并立刻将往左走的点压入栈中重新循环,经典的 DFS。

往左走之后会将当前点标记为已经往左走过,这里 x4 的值有以下三种情况:

  • 0:还没走过。
  • 1:已经往左走过。
  • 2:已经往左走过,且已经往左下走过。

x4==2 时,该点也会被删除,并将,结合图中的注释大概也能看懂这个算法了,这里画了一个图更好理解

从黑色格子出发,只能向左或者向左上(y轴往下的情况下)。红色格子不能继续走,价值为1,同样在 y 0层也有一行红色格子价值为 1,其余格子价值均为 x%5,最后应该是计算黑色格子到红色格子的所有不同的路径的价值之和。

优化可以使用记忆化搜索,或者直接使用动态规划,记忆化搜索简单无脑,三行搞定。

#include<stdio.h>
#include<string.h>
int v[50][50];
long long f[50][50];
long long dfs(int x, int y) {
    if (x == y || !y)return 1;
    if (f[x][y] != -1)return f[x][y];
    return f[x][y] = x%5+dfs(x-1,y)+dfs(x-1,y-1);
}

int main() {
    memset(f, -1, sizeof(f));
    printf("%lld\n", dfs(44,22));
}
//7039739125714

调试驱动(续)

结论

反调试检测:

  • 题目检测 kdcom.dll 模块,检测到则直接 'ACE' 蓝屏。
  • 题目有个线程一直在调用,KdDisableDebugger

绕过:

  • 对于第一个检测,把蓝屏函数直接返回即可。
  • 对于第二个检测,把该api直接返回即可。

分析过程

尝试 hook NtQuerySystemInformationKeBugCheckEx,找到蓝屏的函数在 0x74F0,于是考虑在 hook NtQuerySystemInformation 的某个节点,把该函数 hook 直接返回,不会蓝屏,但是调试器被剥离。

调试发现是调用了 KdDisableDebugger 函数。

同样也是直接返回,操作完成后,可以发现驱动已经可以正常运行,且调试器正常工作。

这里代码实现仅仅变动了 hook 的 NtQuerySystemInformation 函数,因为有 vmp 壳,所以在加载的时候去 hook 是不明智的,直接在调用 NtQuerySystemInformation 的某一刻过掉即可。

NTSTATUS gh_NtQuerySystemInformation(...)
{
    unhook();
    //...
    PVOID stack[MAX_BACKTRACE_DEPTH+1] = { 0 };
    RtlWalkFrameChain(stack, MAX_BACKTRACE_DEPTH,0);
    bool flag = 0;
    static int cnt = 0;
    for(int i = 0; i < MAX_BACKTRACE_DEPTH; i++) {
        if(stack[i]>= Hooks::Base && stack[i] <= (PVOID)((UINT64)Hooks::Base + Hooks::Size)) {
            flag = 1;
            break;
        }
    }
    if (flag) {
        char code = 0xC3;
        cnt += 1;
        if (cnt == 3) {
            DriverUtil::MDLWriteMemory((PVOID)((UINT64)Hooks::Base + 0x74f0), &code, 1);
            DriverUtil::MDLWriteMemory((PVOID)(KdDisableDebugger), &code, 1);
            return ret;
        }
    }
    rehook();
    return ret;
}

因为壳似乎有 API 防 hook 的检测,如果不及时下掉钩子则会加载失败,因此选择在第三次调用之后下掉钩子并做反调试的相关 hook。

Flag逻辑分析

结论:

  • 读取 flag 之后,先做一次单表映射的替换,这里是由 VT 实现的,hook点在(+0x95DF
  • 用输入的 key 对输入的 flag 进行异或加密。
  • 开启 VT,使用 VT hook 过的 TEA 加密逻辑加密上一步得到的密文。
  • 用VT hook rdmsr,使得 rdmsr(0xE8)check 之前异或了一个密钥,这个密钥由输入的 flag 长度决定。

从后续的逻辑来看,生成的 key 就是 flag 做某种加密的密钥。

这里的 v10,经过动态调试,记录了最高有效位,例如我现在输入的 key=0x25312620c4fe,占用 6 字节,所以最高有效位为第五位(从零开始),如图所示

因此第一步就是实现一个简单的异或加密,根据密钥的长度而定。

紧随其后的是 TEA 加密,和初赛一样,每两个字符零扩展成 int 之后放入 TEA 加密。

乍一看这里居然用了 key 的地址进行运算,实则不需要被他吓到,这么玩确实会导致每次加密的结果不一样,但是不代表就不可逆(后来嘎嘎被打脸),逆了一下发现逆推到第一个式子的时候推不动了。

经过调试,发现是代码被 VT hook 了,联想到之前要求一定是 Intel CPU

可以看到单步执行得到的指令结果不符合预期,题目在此处开启 VT 环境。

+0x5150 处的函数实现 hook 的分发。

计算正确的 Flag

答案:flag: flag{ACE_C0n9raTs0nPA55TheZ02S9AmeScTf#}

由于分发函数过于庞大,且 VT 的hook是无痕的,因此考虑能否使用加密的弱点去实现 flag 的解密,由于 TEA 加密的输入是被零扩展的,因此实际 8 字节的分组只有 2 字节是有效的。

可以计算两字节的所有组合,获得它的密文结果,实施这个方法之前,需要确定,相同的密文,相同的 key,得到的一定是相同的输出,断 TEA 加密的 call,选中 RCX 的内存,改成全 0,得到 A9 59 CF AB EB 9D A3 0A,多个位置尝试发现得到的始终是这个结果,因此判定该方法可行。

这里方法就多起来了,第一可以把注册表写满 0000-FFFF,然后指定 Key0xFF,就可以 dump 得到一份表,或者可以直接写一个驱动去调用那个功能,这里我选择了后者。

typedef VOID (*TEAEnc)(unsigned int*, unsigned int*);
extern "C" NTSTATUS DriverEntry(
    _In_ PDRIVER_OBJECT  DriverObject,
    _In_ PUNICODE_STRING RegistryPath
) {
    DriverObject->DriverUnload = DriverUnload;

    TEAEnc teaEnc = (TEAEnc)0xFFFFF8032AC51560;
    unsigned int key[4] = { 0x00000089, 0x000000FE, 0x00000076, 0x000000A0 };
    unsigned int data[2] = { 0x00000000, 0x00000000 };
    teaEnc((unsigned int*)data, (unsigned int*)key);
    for(int i = 0; i < 8; i++) {
        DBG_PRINT("data[%d]=%02x\n", i,((unsigned char*)data)[i]);
    }
    return STATUS_SUCCESS;
}

观察 windbg 的输出,得到了正确的运行结果:

理论可行,那就直接 for 爆一遍,然后存到内存里面,最后 windbg 直接 dump 出来。

但是发现直接 dump 无法直接查找得到,经检查,原来是 rdmsrVT hook,做了一次异或加密,并且根据长度生成异或的密钥,很简单,直接把内存都置 0 就能直接拿到异或的密钥,并且密钥由输入的 flag 长度决定,34 长度的密钥为 03 39 49 26 2F F6 F8 4E

做完异或加密之后,就可以配合 dump 的密文查表。

用如下脚本查表:

target=b'\xc0\x6d\xda\x3f\xc9\x8d\x05\xff\x56\x62\x69\x55\x6c\xbb\xa4\x90\x57\xa0\xa6\x82\xfb\xa7\x8e\xe7\x69\x52\xf3\xc9\xf1\xd7\x1a\x88\xfd\x7b\xea\xa9\x91\x95\xe4\x02\xc5\xdd\xe7\xf6\x64\xcb\x0e\x88\xd9\xd2\x4e\x1e\x3b\xae\x27\x64\x2d\xfd\xca\x5c\x1c\x7b\xcc\xd0\xe7\x40\x6e\x5e\xef\xa9\x5d\x8a\xd3\x5d\x42\xfa\x72\x9a\x1b\x30\x2f\xdd\x72\xc2\xe5\xf3\x1c\x9b\xa4\xf0\x3b\x91\xe8\x3b\xc8\x4e\x1f\x8a\x40\x09\xf8\x6b\x7b\xb4\x8b\x42\x1d\x71\x81\x43\xac\xb9\x76\x42\x56\x05\x5f\xf1\xee\x5d\x7a\x2c\xfe\x48\x92\x7d\x5a\x41\x93\xd1\x4a\x47\x32\xb8\x98\xa1'
table=open("./enc.bin","rb").read()
for i in range(0,136,8):
    num = table.find(target[i:i+8])//8
    x=num//256
    y=num%256
    print(f"{x:02x} {y:02x}",end=" ")

得到结果

f6 59 ab d7 ea 18 94 ab d4 57 b1 d4 84 c6 f0 d4 53 4e 32 81 bc 86 c3 32 1b 5b f5 67 84 c3 05 96 c6 01

为了验证 TEA,选择在做完异或加密之后直接把以上密文贴到 check 的内存中,结果返回正确。

随后拿 Key 异或还是无法得到正确的结果,经查在 +0x95DF 处的指令,读取 flag 的时候存在 VT hook

可以发现内存实际是 A 但是读取结果为 0x24,存在类似的单表替换,而刚刚好,TEA 解密得到的信息异或 key 之后得到的值刚好是 0x24

结合初赛的 flag 格式,A 应该是正确的明文了。

这里就是动调大法,按了三个小时调试器,在取内存的地方下断点,然后每次给内存自增 1,观察 ax 寄存器是否符合预期,最终得到正确的结果

检测方法

题目明显使用 VT 技术实现对内存某些部分的无痕 hook,因此我们的做法就是去检测自身运行是否处于 VT 环境。

MSR检测

bool is_vt_enabled1()
{
    const unsigned int IA32_FEATURE_CONTROL = 0x3A;
    unsigned long long msr_val = __readmsr(IA32_FEATURE_CONTROL);
    return (msr_val & (1 << 2)) != 0; // VMX outside SMX enabled
}
if (is_vt_enabled1()) {
    DBG_PRINT("VT1 is enabled\n");
}
else {
    DBG_PRINT("VT1 is not enabled\n");
}

但是题目做了 hook,题目运行的时候,我们读取的 MSR 显示是没开启 VT 的。

如图所示,前者开启题目驱动,VT1 提示 not enable,卸载题目驱动后又能够检测到 VT 处于开启状态。

于是这里我想到,如果题目要 hook,那么在读取 MSR 的时候必然要运行很多额外代码,这里选择对比开启题目驱动和关闭题目驱动之间的时间差。

bool is_vt_enabled1()
{
    LARGE_INTEGER freq; // 获取频率
    LARGE_INTEGER start = KeQueryPerformanceCounter(&freq);
    const unsigned int IA32_FEATURE_CONTROL = 0x3A;
    unsigned long long msr_val;
    for (int i = 0; i < 100000; i++){
        msr_val = __readmsr(IA32_FEATURE_CONTROL);
    }
    LARGE_INTEGER end = KeQueryPerformanceCounter(NULL);
    LONGLONG delta = end.QuadPart - start.QuadPart;
    LONGLONG elapsed_us = (delta * 1000) / freq.QuadPart;
    DBG_PRINT("Elapsed time: %lld ms\n", elapsed_us); // 打印经过的时间
    return (msr_val & (1 << 2)) != 0; // VMX outside SMX enabled
}

运行结果:

可以发现,我测试了 100000 条 MSR 指令,正常虚拟机运行 132ms,而开启题目驱动之后来到了 2079ms,接近 20 倍的差距。那么第一种方法可以是找一条跟 MSR 指令差不多周期的指令,但是这个指令不会被 VT 影响,最后计算两者的偏差率,超过很多则时间判定不通过,当然它如果不 hook MSR,就可以选择直接判定对应的 VT 位是否处于开启状态,两者联合检测怎么都是可以检测到的。

这里我使用 inc 指令去比较。

start = KeQueryPerformanceCounter(NULL);
int a = 1;
int* k = &a;
for (int i = 0; i < 100000000; i++) {
    *k *= 3;
}
end = KeQueryPerformanceCounter(NULL);
delta = end.QuadPart - start.QuadPart;
LONGLONG elapsed_us2 = (delta * 1000) / freq.QuadPart;
DBG_PRINT("Elapsed time: %lld ms\n", elapsed_us2); // 打印经过的时间

这里使用指针来保证每次循环的结果均写入内存中。

结果也很完美,那么最终检测 VT 环境可以判断 elapsed_us2/elapsed_us1>5,如果是则说明处于 VT 环境中被 hook。

最终代码:

bool is_vt_enabled1()
{
    LARGE_INTEGER freq; // 获取频率
    LARGE_INTEGER start = KeQueryPerformanceCounter(&freq);
    const unsigned int IA32_FEATURE_CONTROL = 0x3A;
    unsigned long long msr_val;
    for (int i = 0; i < 100000; i++){
        msr_val = __readmsr(IA32_FEATURE_CONTROL);
    }
    LARGE_INTEGER end = KeQueryPerformanceCounter(NULL);
    LONGLONG delta = end.QuadPart - start.QuadPart;
    LONGLONG elapsed_us1 = (delta * 1000) / freq.QuadPart;
    DBG_PRINT("Elapsed time: %lld ms\n", elapsed_us1); // 打印经过的时间
    start = KeQueryPerformanceCounter(NULL);
    int a = 1;
    int* k = &a;
    for (int i = 0; i < 100000000; i++) {
        *k *= 3;
    }
    end = KeQueryPerformanceCounter(NULL);
    delta = end.QuadPart - start.QuadPart;
    LONGLONG elapsed_us2 = (delta * 1000) / freq.QuadPart;
    DBG_PRINT("Elapsed time: %lld ms\n", elapsed_us2); // 打印经过的时间
    return elapsed_us1 / elapsed_us2 > 5;
}

运行效果:

能够在开启题目驱动的情况下检测到。

CPUID检测

伪造虚假的 cpuid 参数,通常情况下真机会返回 0,而开启了 VT 则会接管返回正常的值。参考文章:https://secret.club/2020/04/13/how-anti-cheats-detect-system-emulation.html

bool check_invalid_leaf()
{
    constexpr unsigned int invalid_leaf = 0x04201337;
    constexpr unsigned int valid_leaf   = 0x40000000;

    _cpuid_buffer_t InvalidLeafResponse = {};
    _cpuid_buffer_t ValidLeafResponse   = {};

    __cpuid(reinterpret_cast<int32_t*>(&InvalidLeafResponse), invalid_leaf);
    __cpuid(reinterpret_cast<int32_t*>(&ValidLeafResponse), valid_leaf);

    if ((InvalidLeafResponse.EAX != ValidLeafResponse.EAX) ||
        (InvalidLeafResponse.EBX != ValidLeafResponse.EBX) ||
        (InvalidLeafResponse.ECX != ValidLeafResponse.ECX) ||
        (InvalidLeafResponse.EDX != ValidLeafResponse.EDX))
        return true;

    return false;
}

该代码可以运行在用户层,经测试,该代码在开启题目驱动的真机上返回为 true,未运行题目的真机返回为 false,虚拟机中则一律返回 true

目录说明

  • XSafe:包含驱动的源码,编译之后先加载该驱动,后加载题目给的驱动,配置正确的情况下可以双机调试且成功加载驱动(无法二次加载及卸载)。
  • XSafe.sys:XSafe编译的二进制文件。
  • XSafe2:包含爆破 TEA 加密爆破的源码,需要借助驱动 1 找到 TEA 函数的具体位置,并且根据输出的内存地址手动 dump 文件,得到 enc.bin。
  • XSafe2.sys:XSafe2编译的二进制文件
  • VT-Detection1:包含方法1所述的MSR检测源码,加载时可以判断当前是否处于题目的 VT hook 环境中,虚拟机和真机均有效。
  • VT-Detection1.sys:VT-Detection1编译的二进制文件
  • VT-Detection2:包含方法2所属的检测方法,只在真机有效。
  • VT-Detection2.exe:VT-Detection2编译的二进制文件
  • enc.bin:由 XSafe2.sys dump 分配的内存文件。
  • exp.py:TEA 加密的还原脚本

免费评分

参与人数 4吾爱币 +4 热心值 +4 收起 理由
chuling01 + 1 谢谢@Thanks!
flexlm_crk + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
xu206363 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
寞叶 + 2 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
星辰丿 发表于 2025-4-14 16:28
鸡神无敌了
3#
sheild 发表于 2025-4-16 14:34
4#
_默默_ 发表于 2025-4-17 10:40
5#
BananaLi 发表于 2025-4-22 18:19
虽然看不太懂,但是很厉害
6#
cjy2323 发表于 2025-4-23 07:23
谢谢分享好资源
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-4-25 01:16

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表