首页
社区
课程
招聘
VMP源码分析:反调试与绕过方法
发表于: 2024-6-22 14:05 10408

VMP源码分析:反调试与绕过方法

2024-6-22 14:05
10408

1.vmp反调试相关源码部分

1.1 如何检索反调试源码

我们都知道,当vmp检测到被调试,会有如下弹框
图片描述
通过这条报错信息,不难在源码中找到
图片描述
然后通过它的消息传递机制,不难找到

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
void LoaderMessage(MessageType type, const void *param1 = NULL, const void *param2 = NULL)
{
    const VMP_CHAR *message;
    bool need_format = false;
    switch (type) {
    case mtDebuggerFound:
        message = reinterpret_cast<const VMP_CHAR *>(FACE_DEBUGGER_FOUND);
        break;
    case mtVirtualMachineFound:
        message = reinterpret_cast<const VMP_CHAR *>(FACE_VIRTUAL_MACHINE_FOUND);
        break;
    case mtFileCorrupted:
        message = reinterpret_cast<const VMP_CHAR *>(FACE_FILE_CORRUPTED);
        break;
    case mtUnregisteredVersion:
        message = reinterpret_cast<const VMP_CHAR *>(FACE_UNREGISTERED_VERSION);
        break;
    case mtInitializationError:
        message = reinterpret_cast<const VMP_CHAR *>(FACE_INITIALIZATION_ERROR);
        need_format = true;
        break;
    case mtProcNotFound:
        message = reinterpret_cast<const VMP_CHAR *>(FACE_PROC_NOT_FOUND);
        need_format = true;
        break;
    case mtOrdinalNotFound:
        message = reinterpret_cast<const VMP_CHAR *>(FACE_ORDINAL_NOT_FOUND);
        need_format = true;
        break;
    default:
        return;
    }

然后查找 mtDebuggerFound 的引用即可检索到各处反调试相关源码,也就是此文将要详细说的,至于其他部分的检测,感兴趣的童鞋可以自行研究。

1.2 源码阅读:反调试手段

1.2.1 系统版本号的判断

1
2
3
4
5
6
7
if (!os_build_number) {
    if (data.options() & LOADER_OPTION_CHECK_DEBUGGER) {
        LoaderMessage(mtDebuggerFound);
        return LOADER_ERROR;
    }
    tmp_loader_data->set_is_debugger_detected(true);
}

那么这个 os_build_number 怎么获取的呢
图片描述
简单说,一共两种获取方式,1. 从 peb 里直接去取得;2.从 ntdll.dll 的头部获取文件版本号从而确定系统版本。
可能有的童鞋会问了,系统版本号拿来判断反调试是不是有点什么大病,其实不是,私以为,这边判断系统版本号纯纯的只是为了方便取 syscall 所使用的系统调用号。
如下,vmp 应该是把全量的发行版系统都是硬编码了:
图片描述
当系统版本号不在 vmp 适配过的范围(比如测试版 windows),他则会去 map 一份新的 ntdll ,然后从中找他要的NT函数的系统调用号,至于系统调用号是什么,这里就不赘述了。

1.2.2 peb->BeingDebugged 标记

1
2
3
4
if (peb->BeingDebugged) {
    LoaderMessage(mtDebuggerFound);
    return LOADER_ERROR;
}

会心一笑,peb里的这个位就不用过多解释了

1.2.3 ProcessDebugPort

1
2
3
4
if (NT_SUCCESS(reinterpret_cast<tNtQueryInformationProcess *>(syscall | sc_query_information_process)(process, ProcessDebugPort, &debug_object, sizeof(debug_object), NULL)) && debug_object != 0) {
    LoaderMessage(mtDebuggerFound);
    return LOADER_ERROR;
}

查询 ProcessDebugPort,如果查到了,自然是被调试了,也是很常见的反调

1.2.4 ProcessDebugObjectHandle

1
2
3
4
5
if (NT_SUCCESS(reinterpret_cast<tNtQueryInformationProcess *>(syscall | sc_query_information_process)(process, ProcessDebugObjectHandle, &debug_object, sizeof(debug_object), reinterpret_cast<PULONG>(&debug_object)))
    || debug_object == 0) {
    LoaderMessage(mtDebuggerFound);
    return LOADER_ERROR;
}

查询 ProcessDebugObjectHandle, 如果 存在调试对象句柄,那也是被调试了,也属于常见反调

1.2.5 SystemKernelDebuggerInformation

1
2
3
4
5
6
SYSTEM_KERNEL_DEBUGGER_INFORMATION info;
NTSTATUS status = nt_query_system_information(SystemKernelDebuggerInformation, &info, sizeof(info), NULL);
if (NT_SUCCESS(status) && info.DebuggerEnabled && !info.DebuggerNotPresent) {
    LoaderMessage(mtDebuggerFound);
    return LOADER_ERROR;
}

针对内核调试器的监测,也属常见

1.2.6 针对驱动模块名的匹配

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
SYSTEM_MODULE_INFORMATION *buffer = NULL;
ULONG buffer_size = 0;
status = nt_query_system_information(SystemModuleInformation, &buffer, 0, &buffer_size);
if (buffer_size) {
    buffer = reinterpret_cast<SYSTEM_MODULE_INFORMATION *>(LoaderAlloc(buffer_size * 2));
    if (buffer) {
        status = nt_query_system_information(SystemModuleInformation, buffer, buffer_size * 2, NULL);
        if (NT_SUCCESS(status)) {
            for (size_t i = 0; i < buffer->Count && !is_found; i++) {
                SYSTEM_MODULE_ENTRY *module_entry = &buffer->Module[i];
                for (size_t j = 0; j < 5 ; j++) {
                    const char *module_name;
                    switch (j) {
                    case 0:
                        module_name = reinterpret_cast<const char *>(FACE_SICE_NAME);
                        break;
                    case 1:
                        module_name = reinterpret_cast<const char *>(FACE_SIWVID_NAME);
                        break;
                    case 2:
                        module_name = reinterpret_cast<const char *>(FACE_NTICE_NAME);
                        break;
                    case 3:
                        module_name = reinterpret_cast<const char *>(FACE_ICEEXT_NAME);
                        break;
                    case 4:
                        module_name = reinterpret_cast<const char *>(FACE_SYSER_NAME);
                        break;
                    }
                    if (Loader_stricmp(module_name, module_entry->Name + module_entry->PathLength, true) == 0) {
                        is_found = true;
                        break;
                    }
                }
            }
        }
        LoaderFree(buffer);
    }
}

这也是针对了一些常见的内核级调试器的检测,他们的驱动名

1.2.7 线程隐藏

1
2
if (sc_set_information_thread)
    reinterpret_cast<tNtSetInformationThread *>(syscall | sc_set_information_thread)(thread, ThreadHideFromDebugger, NULL, 0);

对调试器隐藏了当前线程

1.2.8 函数头 0xCC断点检测

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
tNtOpenFile *open_file = reinterpret_cast<tNtOpenFile *>(LoaderGetProcAddress(ntdll, reinterpret_cast<const char *>(FACE_NT_OPEN_FILE_NAME), true));
tNtCreateSection *create_section = reinterpret_cast<tNtCreateSection *>(LoaderGetProcAddress(ntdll, reinterpret_cast<const char *>(FACE_NT_CREATE_SECTION_NAME), true));
tNtMapViewOfSection *map_view_of_section = reinterpret_cast<tNtMapViewOfSection *>(LoaderGetProcAddress(ntdll, reinterpret_cast<const char *>(FACE_NT_MAP_VIEW_OF_SECTION), true));
tNtUnmapViewOfSection *unmap_view_of_section = reinterpret_cast<tNtUnmapViewOfSection *>(LoaderGetProcAddress(ntdll, reinterpret_cast<const char *>(FACE_NT_UNMAP_VIEW_OF_SECTION), true));
tNtClose *close = reinterpret_cast<tNtClose *>(LoaderGetProcAddress(ntdll, reinterpret_cast<const char *>(FACE_NT_CLOSE), true));
 
if (!create_section || !open_file || !map_view_of_section || !unmap_view_of_section || !close) {
    LoaderMessage(mtInitializationError, INTERNAL_GPA_ERROR);
    return LOADER_ERROR;
}
 
// check breakpoint
uint8_t *ckeck_list[] = { reinterpret_cast<uint8_t*>(create_section),
                                    reinterpret_cast<uint8_t*>(open_file),
                                    reinterpret_cast<uint8_t*>(map_view_of_section),
                                    reinterpret_cast<uint8_t*>(unmap_view_of_section),
                                    reinterpret_cast<uint8_t*>(close) };
for (i = 0; i < _countof(ckeck_list); i++) {
                if (*ckeck_list[i] == 0xcc) {
                    if (data.options() & LOADER_OPTION_CHECK_DEBUGGER) {
                        LoaderMessage(mtDebuggerFound);
                        return LOADER_ERROR;
                    }
                    tmp_loader_data->set_is_debugger_detected(true);
                }
}
1
2
3
4
5
6
7
if (*reinterpret_cast<uint8_t*>(virtual_protect) == 0xcc) {
    if (data.options() & LOADER_OPTION_CHECK_DEBUGGER) {
        LoaderMessage(mtDebuggerFound);
        return LOADER_ERROR;
    }
    tmp_loader_data->set_is_debugger_detected(true);
}

检测自己要调用的函数有没有被下0xCC断点

1.2.9 内存断点检测

1
2
3
4
5
6
7
if (old_protect & PAGE_GUARD) {
    if (data.options() & LOADER_OPTION_CHECK_DEBUGGER) {
        LoaderMessage(mtDebuggerFound);
        return LOADER_ERROR;
    }
    tmp_loader_data->set_is_debugger_detected(true);
}

1.2.10 假句柄

1
2
3
4
5
6
7
8
9
10
11
12
tCloseHandle *close_handle = reinterpret_cast<tCloseHandle *>(LoaderGetProcAddress(kernel32, reinterpret_cast<const char *>(FACE_CLOSE_HANDLE_NAME), true));
if (close_handle) {
    __try {
        if (close_handle(HANDLE(INT_PTR(0xDEADC0DE)))) {
            LoaderMessage(mtDebuggerFound);
            return LOADER_ERROR;
        }
    } __except(EXCEPTION_EXECUTE_HANDLER) {
        LoaderMessage(mtDebuggerFound);
        return LOADER_ERROR;
    }
}

通过关闭无效句柄来判断是否成功,如果成功则中了陷阱

1.2.11 TrapFlag 与 硬件断点检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__try {
    __writeeflags(__readeflags() | 0x100);
     val = __rdtsc();
     __nop();
    LoaderMessage(mtDebuggerFound);
    return LOADER_ERROR;
} __except(ctx = (GetExceptionInformation())->ContextRecord,
    drx = (ctx->ContextFlags & CONTEXT_DEBUG_REGISTERS) ? ctx->Dr0 | ctx->Dr1 | ctx->Dr2 | ctx->Dr3 : 0,
    EXCEPTION_EXECUTE_HANDLER) {
    if (drx) {
        LoaderMessage(mtDebuggerFound);
        return LOADER_ERROR;
    }
}

可还行,两个检测写在一起了,通过设置flags的TrapFlag触发异常,然后在异常处理里检查硬件断点寄存器是否设置
至此,反调弹框部分基本看完了

1.3 源码部分总结

  • 用了10+种反调手段,基本都属于常见范畴
  • 有检测普通调试器的,有检测内核调试器的
  • 一些查询api使用的syscall,难以从 r3 直接突破

2. vmp 反调试的 bypass (纯r3)

2.1 几句废话

vmp的反调试基本是一些常见的反调试手段,
其中比较棘手的是一些NT函数的调用,他使用了SYSCALL,通过自实现的系统调用规避了我们从 r3 hook 然后绕过的可能。
通过网上一顿检索,确实看到了不少从 r0 来过 vmp 反调的插件/工具/源码。
但是!难道!我们就只能上驱动了么? 它是r3却把我们逼到了r0,有没有纯纯的三环方法还能绕过他的呢?
答案当然是,当然存在(狗头),不然我也就不写这个分享了。

2.2 bypass 关键点

通过不死心的源码阅读,终于让我看到了这块代码
图片描述
也就是关键的这一句

1
LoaderGetProcAddress(ntdll, reinterpret_cast<const char *>(FACE_WINE_GET_VERSION_NAME), true)

此时,小伙伴就会问了,VMP在搞啥?
这其实是 vmp 在给 wine 环境做兼容,如果发现 ntdll.dll 的导出表存在 wine_get_version 函数,则会关闭使用系统调用的特性!!!
关闭系统调用以后,那还不是随便我们hook?
所以理论上只要给 ntdll.dll 的导出表做点手脚即可
理论可行,开始动手

2.3 bypasss 代码

首先,重复造轮子的事不要做,针对vmp的那些常见的反调,已经有很多大佬写好插件并开源了,
我在这里拿这个x64dbg官方的插件做例子 https://github.com/x64dbg/ScyllaHide
找到x64dbg插件的代码 ScyllaHide\InjectorCLI\ApplyHooking.cpp
在其中插入一段新的代码

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
void AddWineFunctionName(HANDLE hProcess)
{
    BYTE* remote_ntdll = (BYTE*)GetModuleBaseRemote(hProcess, L"ntdll.dll");
    // check input
    if (!remote_ntdll)
        return;
 
    SIZE_T readed = 0;
    // check module's header
    IMAGE_DOS_HEADER dos_header;
    ReadProcessMemory(hProcess, remote_ntdll, &dos_header, sizeof(IMAGE_DOS_HEADER), &readed);
    if (dos_header.e_magic != IMAGE_DOS_SIGNATURE)
        return;
 
    // check NT header
    IMAGE_NT_HEADERS pe_header;
    ReadProcessMemory(hProcess, (BYTE*)remote_ntdll + dos_header.e_lfanew, &pe_header, sizeof(IMAGE_NT_HEADERS), &readed);
    if (pe_header.Signature != IMAGE_NT_SIGNATURE)
        return;
 
    // get the export directory
    DWORD export_adress = pe_header.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    if (!export_adress)
        return;
 
    DWORD export_size = pe_header.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
 
    BYTE* new_export_table = (BYTE*)VirtualAllocEx(hProcess, remote_ntdll + 0x1000000, export_size + 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
 
    IMAGE_EXPORT_DIRECTORY export_directory;
    ReadProcessMemory(hProcess, remote_ntdll + export_adress, &export_directory, sizeof(IMAGE_EXPORT_DIRECTORY), &readed);
 
    BYTE* tmp_table = (BYTE*)malloc(export_size + 0x1000);
    if (tmp_table == nullptr)return;
 
    //copy functions table
    BYTE* new_functions_table = new_export_table;
    ReadProcessMemory(hProcess, remote_ntdll + export_directory.AddressOfFunctions, tmp_table, export_directory.NumberOfFunctions * sizeof(DWORD), &readed);
    WriteProcessMemory(hProcess, new_functions_table, tmp_table, export_directory.NumberOfFunctions * sizeof(DWORD), &readed);
    g_log.LogInfo(L"[VMPBypass] new_functions_table: %p", new_functions_table);
 
    //copy ordinal table
    BYTE* new_ordinal_table = new_functions_table + export_directory.NumberOfFunctions * sizeof(DWORD) + 0x100;
    ReadProcessMemory(hProcess, remote_ntdll + export_directory.AddressOfNameOrdinals, tmp_table, export_directory.NumberOfNames * sizeof(WORD), &readed);
    WriteProcessMemory(hProcess, new_ordinal_table, tmp_table, export_directory.NumberOfNames * sizeof(WORD), &readed);
    g_log.LogInfo(L"[VMPBypass] new_ordinal_table: %p", new_ordinal_table);
     
    //copy name table
    BYTE* new_name_table = new_ordinal_table + export_directory.NumberOfNames * sizeof(WORD) + 0x100;
    ReadProcessMemory(hProcess, remote_ntdll + export_directory.AddressOfNames, tmp_table, export_directory.NumberOfNames * sizeof(DWORD), &readed);
    WriteProcessMemory(hProcess, new_name_table, tmp_table, export_directory.NumberOfNames * sizeof(DWORD), &readed);
    g_log.LogInfo(L"[VMPBypass] new_name_table: %p", new_name_table);
 
    free(tmp_table);
    tmp_table = nullptr;
 
    //setup new name  & name offset
    BYTE* wine_func_addr = new_name_table + export_directory.NumberOfNames * sizeof(DWORD) + 0x100;
    WriteProcessMemory(hProcess, wine_func_addr, "wine_get_version\x00", 17, &readed);
    DWORD wine_func_offset = (DWORD)(wine_func_addr - remote_ntdll);
    WriteProcessMemory(hProcess, new_name_table + export_directory.NumberOfNames * sizeof(DWORD), &wine_func_offset, 4, &readed);
 
    //set fake ordinal
    WORD last_ordinal = export_directory.NumberOfNames;
    WriteProcessMemory(hProcess, new_ordinal_table + export_directory.NumberOfNames * sizeof(WORD), &last_ordinal, 2, &readed);
 
    //set fake function offset
    BYTE* query_information_process = reinterpret_cast<BYTE*>(GetProcAddress(hNtdll, "NtCurrentProcess"));
    DWORD function_offset = (DWORD)(query_information_process - remote_ntdll);
    WriteProcessMemory(hProcess, new_functions_table + export_directory.NumberOfFunctions * sizeof(DWORD), &function_offset, 4, &readed);
 
    //setup new directory
    export_directory.NumberOfNames++;
    export_directory.NumberOfFunctions++;
 
    DWORD name_table_offset = (DWORD)(new_name_table - remote_ntdll);
    export_directory.AddressOfNames = name_table_offset;
 
    DWORD function_talble_offset = (DWORD)(new_functions_table - remote_ntdll);
    export_directory.AddressOfFunctions = function_talble_offset;
 
    DWORD ordinal_table_offset = (DWORD)(new_ordinal_table - remote_ntdll);
    export_directory.AddressOfNameOrdinals = ordinal_table_offset;
 
    //// change the offset of header data
    DWORD old_prot;
    VirtualProtectEx(hProcess, remote_ntdll + export_adress, sizeof(IMAGE_EXPORT_DIRECTORY), PAGE_EXECUTE_READWRITE, &old_prot);
    WriteProcessMemory(hProcess, remote_ntdll + export_adress, &export_directory, sizeof(IMAGE_EXPORT_DIRECTORY), &readed);
    VirtualProtectEx(hProcess, remote_ntdll + export_adress, sizeof(IMAGE_EXPORT_DIRECTORY), old_prot, &old_prot);
}

通过这段代码,复制了一份ntdll.dll的导出表,并在里边添加了wine_get_version的导出项,实际调用其实是调用的 NtCurrentProcess。
然后,调用这个函数
图片描述
然后编译产物,放到x64dbg插件目录
图片描述
至此,vmp的反调保护已经被我们从纯纯的 r3 bypass了(狗头)
图片描述

3. 结语

加个导出表还是个比较简单的操作,vmp 想要兼容 wine 环境,但是又只进行了很简单的校验,而且源码又泄露了,这才给了我们可乘之机。
此外 vmp 源码种还有很多别的地方值得学习,这点下次有机会再说。
ps: 我本次实验用的vmp版本是 最我能找到的最高版本的 3.8.4


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2024-6-22 14:19 被JoJoRun编辑 ,原因: 勘误
收藏
免费 21
支持
分享
打赏 + 1.00雪花
打赏次数 1 雪花 + 1.00
 
赞赏  pxhb   +1.00 2024/06/23 感谢分享~
最新回复 (10)
雪    币: 58
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
感谢分享!
2024-6-22 16:39
0
雪    币: 8576
活跃值: (5030)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
3
感谢分享! 
2024-6-22 22:45
0
雪    币: 3180
活跃值: (3546)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
赞,感谢分享。
2024-6-23 09:17
0
雪    币: 2223
活跃值: (90)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
大佬由兴趣转Android VMP方向研究嘛 ,高薪稳定安全
2024-7-2 17:47
0
雪    币: 0
活跃值: (211)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
_rf
6
```
NTSTATUS NTAPI HookedNtMapViewOfSection(HANDLE SectionHandle, HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize, SECTION_INHERIT InheritDisposition, ULONG AllocationType, ULONG Win32Protect)
{
    NTSTATUS status = HookDllData.dNtMapViewOfSection(SectionHandle, ProcessHandle, BaseAddress, ZeroBits, CommitSize, SectionOffset, ViewSize, InheritDisposition, AllocationType, Win32Protect);

    if (NT_SUCCESS(status) && ProcessHandle == NtCurrentProcess && hNtdllSection != INVALID_HANDLE_VALUE && SectionHandle == hNtdllSection)
    {
        hNtdllSection = INVALID_HANDLE_VALUE;
        ApplyNtdllVersionPatch(ProcessHandle, *BaseAddress);

        // Prevent syscall numbers from being extracted from API code.
        PVOID hRealNtdll;
        UNICODE_STRING usNtdll;
        RtlInitUnicodeString(&usNtdll, L"ntdll.dll");
        if (NT_SUCCESS(LdrGetDllHandle(NULL, NULL, &usNtdll, &hRealNtdll)))
        {
            DestroyMappedNtApi("NtSetInformationProcess", hRealNtdll, *BaseAddress); // If VMProtect can syscall this, it will unset the instrumentation callback.
            DestroyMappedNtApi("NtQueryInformationProcess", hRealNtdll, *BaseAddress);
            DestroyMappedNtApi("NtSetInformationThread", hRealNtdll, *BaseAddress);
            DestroyMappedNtApi("NtQueryInformationThread", hRealNtdll, *BaseAddress);
            DestroyMappedNtApi("NtQuerySystemInformation", hRealNtdll, *BaseAddress);
            DestroyMappedNtApi("NtQueryVirtualMemory", hRealNtdll, *BaseAddress);
        }
    }

    return status;
}
```

看ScyllaHide源码也处理了, 
大佬知道为啥没生效吗
2024-7-4 17:53
0
雪    币: 0
活跃值: (2344)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
兄弟,分享下编译好的插件,谢谢。
2024-7-5 00:22
0
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
为什么我编译完运行后找不到你说的那几个文件呢
2024-7-7 19:06
0
雪    币: 7103
活跃值: (3750)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
VMP勾选了内存保护后在x64dbg打不开软件,也不会提示发现调试器就是一直异常,没有选内存保护的话可以打开不会发现调试器
2024-7-7 23:31
0
雪    币: 923
活跃值: (1633)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
10
必须关闭内存保护,的确可以过调试
2024-7-8 08:27
0
雪    币: 923
活跃值: (1633)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
11
说错了 怎么取消评论 这边64位的确可以
2024-7-8 08:35
0
游客
登录 | 注册 方可回帖
返回
//