首页
社区
课程
招聘
[原创]Flare-On 8th Challenge 9复现
发表于: 2022-1-21 11:05 10541

[原创]Flare-On 8th Challenge 9复现

erfze 活跃值
12
2022-1-21 11:05
10541

这道题目可以好好看下官方WriteUp,其前半部分是Core Architecture Concepts——将题目中所用到的各种核心技术都进行了讲解,后半部分Challenge Walkthrough则是以题目作为前半部分技术的例子,对细节进行了阐述。这篇文章将侧重技术而并非解题。

笔者使用的VS版本如下:

进入main函数之前的函数调用栈如下:

上图所示的栈回溯并不完整,完整调用关系如下:

重点在于_initterm_e_initterm两个函数(摘自How to explicitly call CRT startup functions):

__xi_a__xi_z__xc_a__xc_z含义如下(摘自How can I schedule some code to run after all '_atexit()' functions are completed):

微软在其文档中给出这样一段代码:

笔者编译时配置属性如下:

禁用优化:

可以看到生成之后在__xc_a__xc_z中间有一_dynamic_initializer_for__gi__指针:

题目中使用该技术进行VEH Hook,具体细节笔者会放到VEH Hooking一节中描述。关于该技术的利用,趋势科技在其博客中列出一表格如下:

文末会给出具体链接,感兴趣的读者可以进一步阅读。

VEH——Vectored Exception Handling,向量化异常处理。关于VEH与SEH知识讲解可以看VEH和SEH,这里不再赘述。

Vectored Exception Handling, Hooking Via Forced Exception这篇文章通过页的PAGE_GUARD属性触发VEH:

由于设置了pExceptionInfo->ContextRecord->EFlags |= 0x100,每执行一条指令就会重新回到LeoHandler。题目中__scrt_common_main_seh函数在调用_initterm时,会依次执行__xc_a__xc_z之间每个函数:

我们先来看sub_402130sub_402150两个函数:

sub_4054B0根据传递参数获取指定API地址,其具体过程暂不作展开。sub_402130通过给该函数传递值获取AddVectoredExceptionHandler地址,之后由sub_402150进行调用。跟进sub_406AD0查看其功能:

进入异常处理的一种情况:

sub_406AD0先是传递ContextRecord中ECX与EDX值给sub_4054B0获取API地址,之后将该地址写入ContextRecord—>EAX中,最后更改ContextRecord->Eip + 3处指令以及EIP值:

另一种触发异常类似于下面形式:

上文提到sub_4054B0函数会根据传递参数获取指定API地址,其接受两个参数——参数1用来指定DLL,参数2用来指定API。其获取DLL名称是通过搜索由sub_401100函数建立起来的映射,该函数首先会解密DLL名称:

字符串第一个字符解密算法不同于其他字符,之后会为每个DLL名称赋予一个Key值。最终建立起来的映射如下:

其计算导出函数名称Hash值算法如下:

官方WriteUp在这里给了一个计算Hash值的脚本:

通过题目中算法计算输入DLL中每个导出函数名称的Hash值并保存至C Header文件,之后于File—>Load File—>Parse C Header File选择C Header文件,便可在Local Types窗口看到对应DLL的枚举类型:

分析时遇到Hash值直接选择对应enum即可:

wmsuper师傅在Flare-ON 8th 之第九题evil一文中采用的方法是获取需要修改的地址,对应DLL的Key及Hash值,之后再去对应DLL导出表中寻找匹配函数进行输出,最后通过得到的输出结果修复原题目对应地址处指令。

笔者在处理时采用了一个折衷的方案,之前在复现Challenge 7时使用了一个IDA Plugin——Shellcode Hashes,虽然其覆盖算法很多,但对于题目中这种特定算法没有在脚本当中实现,笔者于make_sc_hash_db.py中添加了该算法:

之后生成数据库文件,使用插件时选择新生成数据库再应用对应算法即可:

关于该工具更多信息可阅读Using Precalculated String Hashes when Reverse Engineering Shellcodemake_sc_hash_db.py脚本对DLL目录中每个DLL的所有导出函数应用脚本内所有Hash算法计算Hash值写入到数据库文件中:

官方WriteUp上给出了一个Nop题目中Anti-Disassembly指令的脚本:

再配合上nop-hidder脚本:

如此一来,使用IDA进行静态分析便会轻松很多。

关于Windows调试原理可以参阅:

Anti-Debug:

sub_4023D0函数首先会PatchDbgBreakPointDbgUiRemoteBreakin两个函数:

通过Vmware使用的I/O端口0x5658进行检测:

如果是运行在Vmware中则直接退出:

之后的检测方法是来自Another VMWare Detection一文:

执行SELECT * FROM Win32_PnPEntity以检测是否位于VirtualBox中(下面代码出自al-khaser):

IsDebuggerPresent

之后依次是CheckRemoteDebuggerPresent,NtGlobalFlag,SeDebugPrivilege,HardwareBreakpoints及GetTickCount相关Anti-Debug,具体实现可以参阅al-khaserAnti-Debug:Timing,这里不再一一赘述。可以通过ScyllaHide进行Anti-Anti-Debug。

题目会校验传递参数个数:

之后sub_403A70会调用sub_410EE0函数(根据官方WriteUp,这里应该是使用了PolyHook2)HookCryptImportKey

解密字符串:

一共解密出4个字符串:"L0ve", "s3cret", "5Ex", "g0d"。笔者在这里没能动调,而是根据IDA静态分析结果计算而来,由于卡在此处故查到Flare-On 8 – Task 9一文,阅读过后发现其使用方法和工具都极高效,便不再重写这些步骤,感兴趣的读者可以跟着这篇文章学习一下。

 
 
 
 
 
 
 
typedef void (__cdecl *_PVFV)();
 
void _initterm(const _PVFV *ppfn, const _PVFV *end)
{
    do
    {
        if (_PVFV pfn = *++ppfn)
        {
            pfn();
        }
    } while (ppfn < end);
}
 
typedef int  (__cdecl *_PIFV)();
 
int _initterm_e(const _PIFV *ppfn, const _PIFV *end)
{
    do
    {
        if (_PIFV pfn = *++ppfn)
        {
            if (int err = pfn()) return err;
        }
    } while (ppfn < end);
 
    return 0;
}
typedef void (__cdecl *_PVFV)();
 
void _initterm(const _PVFV *ppfn, const _PVFV *end)
{
    do
    {
        if (_PVFV pfn = *++ppfn)
        {
            pfn();
        }
    } while (ppfn < end);
}
 
typedef int  (__cdecl *_PIFV)();
 
int _initterm_e(const _PIFV *ppfn, const _PIFV *end)
{
    do
    {
        if (_PIFV pfn = *++ppfn)
        {
            if (int err = pfn()) return err;
        }
    } while (ppfn < end);
 
    return 0;
}
extern _CRTALLOC(".CRT$XIA") _PIFV __xi_a[];
extern _CRTALLOC(".CRT$XIZ") _PIFV __xi_z[];    /* C initializers */
extern _CRTALLOC(".CRT$XCA") _PVFV __xc_a[];
extern _CRTALLOC(".CRT$XCZ") _PVFV __xc_z[];    /* C++ initializers */
extern _CRTALLOC(".CRT$XPA") _PVFV __xp_a[];
extern _CRTALLOC(".CRT$XPZ") _PVFV __xp_z[];    /* C pre-terminators */
extern _CRTALLOC(".CRT$XTA") _PVFV __xt_a[];
extern _CRTALLOC(".CRT$XTZ") _PVFV __xt_z[];    /* C terminators */
extern _CRTALLOC(".CRT$XIA") _PIFV __xi_a[];
extern _CRTALLOC(".CRT$XIZ") _PIFV __xi_z[];    /* C initializers */
extern _CRTALLOC(".CRT$XCA") _PVFV __xc_a[];
extern _CRTALLOC(".CRT$XCZ") _PVFV __xc_z[];    /* C++ initializers */
extern _CRTALLOC(".CRT$XPA") _PVFV __xp_a[];
extern _CRTALLOC(".CRT$XPZ") _PVFV __xp_z[];    /* C pre-terminators */
extern _CRTALLOC(".CRT$XTA") _PVFV __xt_a[];
extern _CRTALLOC(".CRT$XTZ") _PVFV __xt_z[];    /* C terminators */
int func(void)
{
    return 3;
}
 
int gi = func();
 
int main()
{
    return gi;
}
int func(void)
{
    return 3;
}
 
int gi = func();
 
int main()
{
    return gi;
}
 
 
 
 
 
 
 
 
 
 
bool LeoHook::Hook(uintptr_t original_fun, uintptr_t hooked_fun)
{
    LeoHook::og_fun = original_fun;
    LeoHook::hk_fun = hooked_fun;
 
    //We cannot hook two functions in the same page, because we will cause an infinite callback
    if (AreInSamePage((const uint8_t*)og_fun, (const uint8_t*)hk_fun))
        return false;
 
    //Register the Custom Exception Handler
    VEH_Handle = AddVectoredExceptionHandler(true, (PVECTORED_EXCEPTION_HANDLER)LeoHandler);
 
    //Toggle PAGE_GUARD flag on the page
    if(VEH_Handle && VirtualProtect((LPVOID)og_fun, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &oldProtection))
        return true;
 
    return false;
}
 
LONG WINAPI LeoHook::LeoHandler(EXCEPTION_POINTERS *pExceptionInfo)
{
    if (pExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) //We will catch PAGE_GUARD Violation
    {
        if (pExceptionInfo->ContextRecord->XIP == (uintptr_t)og_fun) //Make sure we are at the address we want within the page
        {
            pExceptionInfo->ContextRecord->XIP = (uintptr_t)hk_fun; //Modify EIP/RIP to where we want to jump to instead of the original function
        }
 
        pExceptionInfo->ContextRecord->EFlags |= 0x100; //Will trigger an STATUS_SINGLE_STEP exception right after the next instruction get executed. In short, we come right back into this exception handler 1 instruction later
        return EXCEPTION_CONTINUE_EXECUTION; //Continue to next instruction
    }
 
    if (pExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) //We will also catch STATUS_SINGLE_STEP, meaning we just had a PAGE_GUARD violation
    {
        DWORD dwOld;
        VirtualProtect((LPVOID)og_fun, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &dwOld); //Reapply the PAGE_GUARD flag because everytime it is triggered, it get removes
 
        return EXCEPTION_CONTINUE_EXECUTION; //Continue the next instruction
    }
 
    return EXCEPTION_CONTINUE_SEARCH; //Keep going down the exception handling list to find the right handler IF it is not PAGE_GUARD nor SINGLE_STEP
}
bool LeoHook::Hook(uintptr_t original_fun, uintptr_t hooked_fun)
{
    LeoHook::og_fun = original_fun;
    LeoHook::hk_fun = hooked_fun;
 
    //We cannot hook two functions in the same page, because we will cause an infinite callback
    if (AreInSamePage((const uint8_t*)og_fun, (const uint8_t*)hk_fun))
        return false;
 
    //Register the Custom Exception Handler
    VEH_Handle = AddVectoredExceptionHandler(true, (PVECTORED_EXCEPTION_HANDLER)LeoHandler);
 
    //Toggle PAGE_GUARD flag on the page
    if(VEH_Handle && VirtualProtect((LPVOID)og_fun, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &oldProtection))
        return true;
 
    return false;
}
 
LONG WINAPI LeoHook::LeoHandler(EXCEPTION_POINTERS *pExceptionInfo)
{
    if (pExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) //We will catch PAGE_GUARD Violation
    {
        if (pExceptionInfo->ContextRecord->XIP == (uintptr_t)og_fun) //Make sure we are at the address we want within the page
        {
            pExceptionInfo->ContextRecord->XIP = (uintptr_t)hk_fun; //Modify EIP/RIP to where we want to jump to instead of the original function
        }
 
        pExceptionInfo->ContextRecord->EFlags |= 0x100; //Will trigger an STATUS_SINGLE_STEP exception right after the next instruction get executed. In short, we come right back into this exception handler 1 instruction later
        return EXCEPTION_CONTINUE_EXECUTION; //Continue to next instruction
    }
 
    if (pExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) //We will also catch STATUS_SINGLE_STEP, meaning we just had a PAGE_GUARD violation
    {
        DWORD dwOld;
        VirtualProtect((LPVOID)og_fun, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &dwOld); //Reapply the PAGE_GUARD flag because everytime it is triggered, it get removes
 
        return EXCEPTION_CONTINUE_EXECUTION; //Continue the next instruction
    }
 
    return EXCEPTION_CONTINUE_SEARCH; //Keep going down the exception handling list to find the right handler IF it is not PAGE_GUARD nor SINGLE_STEP
}
 
 
sub_402130 proc near
mov     edx, 542F881Eh
mov     ecx, 246132h
call    GetProcAddr
mov     dword_6D16E4, eax
retn
sub_402130 endp
 
sub_402150 proc near
push    offset sub_406AD0
push    1
call    dword_6D16E4
retn
sub_402150 endp
sub_402130 proc near
mov     edx, 542F881Eh
mov     ecx, 246132h
call    GetProcAddr
mov     dword_6D16E4, eax
retn
sub_402130 endp
 
sub_402150 proc near
push    offset sub_406AD0
push    1
call    dword_6D16E4
retn
sub_402150 endp
 
 
 
 
 
 
 
 
 
 
0x176684        ntdll.dll
0x246132        kernel32.dll
0x052325        ws2_32.dll
0x234324        user32.dll
0x523422        advapi32.dll
0x43493856    gdi32.dll
0x4258672    ole32.dll
0x7468951    oleaut32.dll
0x176684        ntdll.dll
0x246132        kernel32.dll
0x052325        ws2_32.dll
0x234324        user32.dll
0x523422        advapi32.dll
0x43493856    gdi32.dll
0x4258672    ole32.dll
0x7468951    oleaut32.dll
 
 
import pefile
import sys
import os
M32 = 0xffffffff
def main():
     if len(sys.argv) != 3:
        print("usage: generate_hashes <in.dll> <out.hashes>")
        sys.exit(0)
 
     d = [pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]]
     pe = pefile.PE(sys.argv[1], fast_load=True)
     pe.parse_data_directories(directories=d)
 
     names = [ e.name for e in pe.DIRECTORY_ENTRY_EXPORT.symbols]
     names = [i for i in names if i]
 
     with open(sys.argv[2], 'wb') as f:
        f.write(b"enum %s_hash\n"% os.path.split(sys.argv[1])[-1].split('.')[0].encode())
        f.write(b"{\n")
        for name in sorted(names):
            h = 0x40
            for x in name:
                h = x - 0x45523f21 * h & M32
            f.write((b" %s = 0x%x,\n"%(name, h)))
        f.write(b"};")
if __name__ == '__main__':
    main()
import pefile
import sys
import os
M32 = 0xffffffff
def main():
     if len(sys.argv) != 3:
        print("usage: generate_hashes <in.dll> <out.hashes>")
        sys.exit(0)
 
     d = [pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]]
     pe = pefile.PE(sys.argv[1], fast_load=True)
     pe.parse_data_directories(directories=d)
 
     names = [ e.name for e in pe.DIRECTORY_ENTRY_EXPORT.symbols]
     names = [i for i in names if i]
 
     with open(sys.argv[2], 'wb') as f:
        f.write(b"enum %s_hash\n"% os.path.split(sys.argv[1])[-1].split('.')[0].encode())
        f.write(b"{\n")
        for name in sorted(names):
            h = 0x40
            for x in name:
                h = x - 0x45523f21 * h & M32
            f.write((b" %s = 0x%x,\n"%(name, h)))
        f.write(b"};")
if __name__ == '__main__':
    main()
 
 
 
 
 
def imul45523f21hSub(inString,fName):
    if inString is None:
        return 0
    val = 0x40
    for i in inString:
        val = val * 0x45523f21
        val = i -val
        val = val & 0xFFFFFFFF
    val = val & 0xFFFFFFFF
    return val
 
pseudocode_imul45523f21hSub = '''acc := 0x40;
for c in input_string {
   acc := acc * 45523f21h:
   acc := c -acc;
}
'''
......
# The list of tuples of (supported hash name, hash size, pseudo_code)
HASH_TYPES = [
    ('ror7AddHash32',       32, pseudocode_ror7AddHash32),
    ('ror9AddHash32',       32, pseudocode_ror9AddHash32),
    ('ror11AddHash32',      32, pseudocode_ror11AddHash32),
    ('ror13AddHash32',      32, pseudocode_ror13AddHash32),
    ('ror13AddWithNullHash32'32, pseudocode_ror13AddWithNullHash32),
    ('ror13AddHash32AddDll',   32, pseudocode_ror13AddHash32AddDll),
    ('ror13AddHash32DllSimple',   32, pseudocode_ror13AddHash32DllSimple),
    ('ror13AddHash32Sub20h', 32, pseudocode_ror13AddHash32Sub20h),
    ('ror13AddHash32Sub1'32, pseudocode_ror13AddHash32),
    ('addRor4WithNullHash32',   32, pseudocode_addRor4WithNullHash32),
    ('addRor13Hash32',      32, pseudocode_addRor13Hash32),
    ('addRor13HashOncemore32',      32, pseudocode_addRor13HashOncemore32),
    ('rol3XorEax',          32, pseudocode_rol3XorEax),
    ('rol3XorHash32',       32, pseudocode_rol3XorHash32),
    ('rol5AddHash32',       32, pseudocode_rol5AddHash32),
    ('addRol5HashOncemore32',      32, pseudocode_addRol5HashOncemore32),
    ('rol7AddHash32',       32, pseudocode_rol7AddHash32),
    ('rol7AddXor2Hash32',       32, pseudocode_rol7AddXor2Hash32),
    ('rol7XorHash32',       32, pseudocode_rol7XorHash32),
    ('rol5XorHash32',       32, pseudocode_rol5XorHash32),
    ('rol8Xor0xB0D4D06Hash32',       32, pseudocode_rol8Xor0xB0D4D06Hash32),
    ('chAddRol8Hash32',     32, pseudocode_chAddRol8Hash32),
    ('rol9AddHash32',       32, pseudocode_rol9AddHash32),
    ('rol9XorHash32',       32, pseudocode_rol9XorHash32),
    ('xorRol9Hash32',       32, pseudocode_xorRol9Hash32),
    ('shl7Shr19XorHash32',     32, pseudocode_shl7Shr19XorHash32),
    ('shl7Shr19AddHash32',     32, pseudocode_shl7Shr19AddHash32),
    ('shl7SubHash32DoublePulser',     32, pseudocode_shl7SubHash32DoublePulser),
    ('sll1AddHash32',       32, pseudocode_sll1AddHash32),
    ('shr2Shl5XorHash32',   32, pseudocode_shr2Shl5XorHash32),
    ('xorShr8Hash32',       32, pseudocode_xorShr8Hash32),
    ('imul83hAdd',          32, pseudocode_imul83hAdd),
    ('imul21hAddHash32',          32, pseudocode_imul21hAddHash32),
    ('or21hXorRor11Hash32',          32, pseudocode_or21hXorRor11Hash32),
    ('or23hXorRor17Hash32',          32, pseudocode_or23hXorRor17Hash32),
    ('playWith0xe8677835Hash', 32, pseudocode_playWith0xe8677835Hash),
    ('poisonIvyHash',       32, pseudocode_poisonIvyHash),
    ('crc32',               32, 'Standard crc32'),
    ('crc32Xor0xca9d4d4e'32, 'crc32 ^ 0xCA9D4D4E'),
    ('crc32bzip2lower',           32, 'crc32 bzip2 and str lower'),
    ('mult21AddHash32',         32, pseudocode_hashMult21),
    ('add1505Shl5Hash32',       32, pseudocode_add1505Shl5Hash32),
    ('dualaccModFFF1Hash',      32, pseudocode_dualaccModFFF1Hash),
    ('hash_Carbanak',           32, pseudocode_hash_Carbanak),
    ('hash_ror13AddUpperDllnameHash32',32, pseudocode_hash_ror13AddUpperDllnameHash32),
    ('fnv1Xor67f', 32, pseudocode_fnv1Xor67f),
    ('adler32_666', 32, 'Adler32 with starting value 666'),
    ('shift0x82F63B78',           32, 'like crc32c'),
    ('contiApiHashing',       32, pseudocode_contiApiHashing),
    ('fnv1', 32, pseudocode_fnv1),
    ('imul45523f21hSub',32 , pseudocode_imul45523f21hSub)
]
def imul45523f21hSub(inString,fName):
    if inString is None:
        return 0
    val = 0x40
    for i in inString:
        val = val * 0x45523f21
        val = i -val
        val = val & 0xFFFFFFFF
    val = val & 0xFFFFFFFF
    return val
 
pseudocode_imul45523f21hSub = '''acc := 0x40;
for c in input_string {
   acc := acc * 45523f21h:
   acc := c -acc;
}
'''
......
# The list of tuples of (supported hash name, hash size, pseudo_code)
HASH_TYPES = [
    ('ror7AddHash32',       32, pseudocode_ror7AddHash32),
    ('ror9AddHash32',       32, pseudocode_ror9AddHash32),
    ('ror11AddHash32',      32, pseudocode_ror11AddHash32),
    ('ror13AddHash32',      32, pseudocode_ror13AddHash32),
    ('ror13AddWithNullHash32'32, pseudocode_ror13AddWithNullHash32),
    ('ror13AddHash32AddDll',   32, pseudocode_ror13AddHash32AddDll),
    ('ror13AddHash32DllSimple',   32, pseudocode_ror13AddHash32DllSimple),
    ('ror13AddHash32Sub20h', 32, pseudocode_ror13AddHash32Sub20h),
    ('ror13AddHash32Sub1'32, pseudocode_ror13AddHash32),
    ('addRor4WithNullHash32',   32, pseudocode_addRor4WithNullHash32),
    ('addRor13Hash32',      32, pseudocode_addRor13Hash32),
    ('addRor13HashOncemore32',      32, pseudocode_addRor13HashOncemore32),
    ('rol3XorEax',          32, pseudocode_rol3XorEax),
    ('rol3XorHash32',       32, pseudocode_rol3XorHash32),
    ('rol5AddHash32',       32, pseudocode_rol5AddHash32),
    ('addRol5HashOncemore32',      32, pseudocode_addRol5HashOncemore32),
    ('rol7AddHash32',       32, pseudocode_rol7AddHash32),
    ('rol7AddXor2Hash32',       32, pseudocode_rol7AddXor2Hash32),
    ('rol7XorHash32',       32, pseudocode_rol7XorHash32),
    ('rol5XorHash32',       32, pseudocode_rol5XorHash32),
    ('rol8Xor0xB0D4D06Hash32',       32, pseudocode_rol8Xor0xB0D4D06Hash32),
    ('chAddRol8Hash32',     32, pseudocode_chAddRol8Hash32),
    ('rol9AddHash32',       32, pseudocode_rol9AddHash32),
    ('rol9XorHash32',       32, pseudocode_rol9XorHash32),
    ('xorRol9Hash32',       32, pseudocode_xorRol9Hash32),
    ('shl7Shr19XorHash32',     32, pseudocode_shl7Shr19XorHash32),
    ('shl7Shr19AddHash32',     32, pseudocode_shl7Shr19AddHash32),
    ('shl7SubHash32DoublePulser',     32, pseudocode_shl7SubHash32DoublePulser),
    ('sll1AddHash32',       32, pseudocode_sll1AddHash32),
    ('shr2Shl5XorHash32',   32, pseudocode_shr2Shl5XorHash32),
    ('xorShr8Hash32',       32, pseudocode_xorShr8Hash32),
    ('imul83hAdd',          32, pseudocode_imul83hAdd),
    ('imul21hAddHash32',          32, pseudocode_imul21hAddHash32),
    ('or21hXorRor11Hash32',          32, pseudocode_or21hXorRor11Hash32),
    ('or23hXorRor17Hash32',          32, pseudocode_or23hXorRor17Hash32),
    ('playWith0xe8677835Hash', 32, pseudocode_playWith0xe8677835Hash),
    ('poisonIvyHash',       32, pseudocode_poisonIvyHash),

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2022-1-21 12:32 被erfze编辑 ,原因:
收藏
免费 3
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//