吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 5652|回复: 22
收起左侧

[调试逆向] PE文件笔记十三 合并节

  [复制链接]
lyl610abc 发表于 2021-4-6 20:28

继续PE系列笔记的更新

PE其它笔记索引可前往:
PE文件笔记一 PE介绍


前面在PE文件笔记十一 新增节学习了关于节的操作之新增节,接下来继续学习节的操作之合并节

合并节

为什么要合并节

在前面新增节中,要判断最后一个节表后面是否有空间用于新增节表,只有当最后一个节表后40个字节全为0时,才能进行新增节的操作;但当条件不满足时又想要新增节,该如何操作?

答案便是:合并节,合并节就是用一个节表描述多个节,这样省下来的节表空间就可以用于新增节了

于是合并节的目的便是:节省节表空间,这样就能实现新增节


合并节涉及的结构体成员

涉及的节表成员 含义
Name 节名称
VirtualAddress 节在内存中的偏移 (RVA)
Misc 节的实际大小
SizeOfRawData 节在文件中对齐后的尺寸
PointerToRawData 节区在文件中的偏移
Characteristics 节的属性
涉及的标准PE头成员 含义
NumberOfSections 节的个数

合并节的流程

  1. 修正内存对齐
  2. 修改第一个节的大小
  3. 修改第一个节的权限
  4. 修改节数量为1
  5. 清空后面的节(可选)

按流程合并节

修正内存对齐

关于修正内存对齐的内容在上一篇笔记 PE文件笔记十二 修正内存对齐中已经详细说明了

这次就以上一篇修正过内存对齐的结果:EverEdit_修正.exe直接进行合并节,在这里省略修正内存对齐的步骤;不了解如何修正内存对齐的可以回顾上一篇笔记

给出修正完内存对齐后 节的信息

image-20210406135858359


Name Misc SizeOfRawData PointerToRawData Characteristics
.text 0x19a000 0x19a000 0x400 0x60000020
.rdata 0x38000 0x38000 0x19a400 0x40000040
.data 0x4b000 0x4b000 0x1d2400 0xc0000040
.rsrc 0x63000 0x63000 0x21d400 0x40000040
.reloc 0x17000 0x17000 0x280400 0x42000040

修改第一个节的大小

修改第一个节的大小为后面 为所有节内存对齐后的大小的和

即修改第一个节的大小为 0x19a000+0x38000+0x4b000+0x63000+0x17000=0x297000

仍然使用PE工具:DIE进行修改

image-20210406191646661


修改后

image-20210406191729130


修改第一个节的权限

既然要用一个节表概括所有的节,那么该节表就必须具备先前所有节表的权限

也就是第一个节的权限 = 所有节的权限 相或

image-20210406192516941


得到新的权限为0xE2000060

使用PE工具:DIE修改

image-20210406192958994


修改后

image-20210406193139515


修改节数量为1

找到标准PE头中的NumberOfSections成员,将其修改为1

image-20210406193233571


修改后

image-20210406193332082


清空后面的节(可选)

其实做完上面一步就已经完成了合并节,但合并节是为了腾出节表空间,于是这里再将后面无用的节表清空掉

使用WinHex找到节表处

image-20210406193717767


选中要清空的部分,编辑→填充选块(快捷键 Ctrl+L)

image-20210406193921600


image-20210406193818020


清空后保存即可

image-20210406194003256


测试运行

程序仍然可以正常运行

image-20210406194149811


并且此时再用PE工具:DIE查看节的信息,也只有一个节了

image-20210406194230742


代码实现合并节

完整代码

// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
#include <math.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64  0x8664

//向文件中指定位置追加数据
//第一个参数为文件路径
//第二个参数为要追加的数据指针
//第三个参数为要追加的数据大小
//第四个参数为位置偏移
//第五个参数为hMap的指针
//第六个参数为pFile的指针
BOOL appendFile(LPCSTR filePath, PVOID writeData, DWORD sizeOfWriteData, DWORD offset, HANDLE* phMap, PVOID* ppFile) {
    HANDLE hFile = CreateFileA(filePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
    char newPath[100];
    strcpy(newPath, filePath);

    strcat(newPath, ".exe");
    HANDLE hFile2 = CreateFileA(newPath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, 0, 0);

    //WriteFile用于接收实际写入的大小的参数
    DWORD dwWritenSize = 0;

    //根据文件句柄创建映射
    HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
    //映射内容
    LPVOID pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);

    BYTE* content = (BYTE*)pFile;
    content += offset;

    //写入要插入数据前的数据
    DWORD size = SetFilePointer(hFile, NULL, NULL, FILE_END);
    BOOL bRet;
    bRet = WriteFile(hFile2, pFile, offset, &dwWritenSize, NULL);
    if (!bRet)return false;
    //写入要插入的数据
    SetFilePointer(hFile, NULL, NULL, FILE_END);
    bRet = WriteFile(hFile2, writeData, sizeOfWriteData, &dwWritenSize, NULL);
    if (!bRet)return false;
    //写入要插入数据后的数据
    SetFilePointer(hFile, NULL, NULL, FILE_END);
    bRet = WriteFile(hFile2, content, size - offset, &dwWritenSize, NULL);
    if (!bRet)return false;
    CloseHandle(hFile);
    CloseHandle(hMap);
    CloseHandle(*phMap);
    UnmapViewOfFile(pFile);
    UnmapViewOfFile(*ppFile);
    bRet = DeleteFileA(filePath);
    if (!bRet)return false;

    hFile = CreateFileA(filePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, 0, 0);
    //根据文件句柄创建映射
    hMap = CreateFileMappingA(hFile2, NULL, PAGE_READWRITE, 0, 0, 0);
    //映射内容
    pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
    SetFilePointer(hFile, NULL, NULL, FILE_BEGIN);
    bRet = WriteFile(hFile, pFile, sizeOfWriteData + size, &dwWritenSize, NULL);
    if (!bRet)return false;
    CloseHandle(hFile);
    CloseHandle(hFile2);
    CloseHandle(hMap);
    UnmapViewOfFile(pFile);
    bRet = DeleteFileA(newPath);
    if (!bRet)return false;
    hFile = CreateFileA(filePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, 0, 0);
    //根据文件句柄创建映射
    hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
    //映射内容
    *ppFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
    *phMap = hMap;
    CloseHandle(hFile);
    return true;
}

//根据pFile获取PE文件结构
void GetPeStruct32(LPVOID pFile, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
    dos = (_IMAGE_DOS_HEADER*)pFile;

    //创建指向PE文件头标志的指针
    DWORD* peId;
    //让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
    peId = (DWORD*)((UINT)dos + dos->e_lfanew);

    //创建指向可选PE头的第一个成员magic的指针
    WORD* magic;
    //让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
    magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));

    //根据magic判断为32位程序还是64位程序

        //让PE文件头指针指向其对应的地址
    nt = (_IMAGE_NT_HEADERS*)peId;
    //创建指向块表的指针
    _IMAGE_SECTION_HEADER* sectionHeader;
    //让块表的指针指向其对应的地址
    sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
    //计数,用来计算块表地址
    int cnt = 0;
    //比较 计数 和 块表的个数,即遍历所有块表
    while (cnt < nt->FileHeader.NumberOfSections) {
        //创建指向块表的指针
        _IMAGE_SECTION_HEADER* section;
        //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
        section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
        //将得到的块表指针存入数组
        sectionArr[cnt++] = section;

    }
}

//修正节表的Misc和SizeOfRawData
//第一个参数为指向dos头的指针
//第二个参数为指向nt头的指针
//第三个参数为存储指向节指针的数组
//第四个参数为文件路径
//第五个参数为文件映射
//第六个参数为文件映射内容指针
//第七个参数为要修正的节表在数组中的下标
void sectionAlignment(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr, LPCSTR filePath, HANDLE* phMap,LPVOID* ppFile, int n) {

    //获得最后一个节的实际大小
    DWORD VirtualSize = sectionArr[n]->Misc.VirtualSize;
    //获得最后一个节的文件对齐后的大小
    DWORD SizeOfRawData = sectionArr[n]->SizeOfRawData;
    //计算上一个节内存对齐后的大小
    UINT SizeInMemory = (UINT)ceil((double)max(VirtualSize, SizeOfRawData) / (double)nt->OptionalHeader.SectionAlignment) * nt->OptionalHeader.SectionAlignment;
    printf("%X\n", SizeInMemory);
    //计算差值= 内存对齐后大小 - 文件对齐后大小
    UINT offset = SizeInMemory - sectionArr[n]->SizeOfRawData;
    printf("%X\n", offset);
    //根据节在文件中的偏移 + 文件对齐后的大小 得到节的末尾
    UINT end = sectionArr[n]->PointerToRawData + sectionArr[n]->SizeOfRawData;
    printf("end:%X\n", end);

    //申请要填充的空间
    INT* content = (INT*)malloc(offset);
    //初始化为0
    ZeroMemory(content, offset);
    //WriteFile用于接收实际写入的大小的参数
    DWORD dwWritenSize = 0;

    BOOL bRet=appendFile(filePath, (PVOID)content, offset, end,phMap,ppFile);
    GetPeStruct32(*ppFile, dos, nt, sectionArr);
    if (bRet) {
        //开始修正Misc和SizeOfRawData
        sectionArr[n]->Misc.VirtualSize = SizeInMemory;
        sectionArr[n]->SizeOfRawData = SizeInMemory;
        //修正后面受到影响的节的PointerOfRawData和VirtualAddress
        int i;
        while (n + 1 <= nt->FileHeader.NumberOfSections - 1) {
            n++;
            sectionArr[n]->PointerToRawData += offset;
        }
    }

}

//合并节
//第一个参数为指向dos头的指针
//第二个参数为指向nt头的指针
//第三个参数为存储指向节指针的数组
void combineSection(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
    //所有节内存对齐后的大小的和,这里要求已经修正过内存对齐,只有这样文件对齐大小才会等于内存对齐大小
    DWORD allSectionSize = 0;
    //所有节的权限,初始为第一个节的权限,和后面的每个节的权限进行或操作
    DWORD allSectionCharacteristics = sectionArr[0]->Characteristics;
    int i;
    for (i = 0; i < nt->FileHeader.NumberOfSections;i++) {
        allSectionSize += sectionArr[i]->SizeOfRawData;
        allSectionCharacteristics = allSectionCharacteristics | sectionArr[i]->Characteristics;
    }
    printf("allSectionSize:%X\n", allSectionSize);
    printf("allSectionCharacteristics:%X\n", allSectionCharacteristics);

    sectionArr[0]->Misc.VirtualSize = allSectionSize;
    sectionArr[0]->SizeOfRawData = allSectionSize;
    sectionArr[0]->Characteristics = allSectionCharacteristics;

    //清零后面的节
    for (i = 1; i < nt->FileHeader.NumberOfSections; i++) {
        ZeroMemory(sectionArr[i], sizeof(_IMAGE_SECTION_HEADER));
    }

    //节个数设置为1
    nt->FileHeader.NumberOfSections = 1;
}

int main(int argc, char* argv[])
{
    //创建DOS对应的结构体指针
    _IMAGE_DOS_HEADER* dos;
    //读取文件,返回文件句柄
    HANDLE hFile = CreateFileA("C:\\Users\\lyl610abc\\Desktop\\EverEdit\\EverEdit_修正.exe", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
    //根据文件句柄创建映射
    HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
    //映射内容
    LPVOID pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
    //类型转换,用结构体的方式来读取
    dos = (_IMAGE_DOS_HEADER*)pFile;
    //输出dos->e_magic,以十六进制输出
    printf("dos->e_magic:%X\n", dos->e_magic);

    //创建指向PE文件头标志的指针
    DWORD* peId;
    //让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
    peId = (DWORD*)((UINT)dos + dos->e_lfanew);
    //输出PE文件头标志,其值应为4550,否则不是PE文件
    printf("peId:%X\n", *peId);

    //创建指向可选PE头的第一个成员magic的指针
    WORD* magic;
    //让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
    magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
    //输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
    printf("magic:%X\n", *magic);
    //根据magic判断为32位程序还是64位程序
    switch (*magic) {
    case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
    {
        printf("32位程序\n");
        //确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
        //创建指向PE文件头的指针
        _IMAGE_NT_HEADERS* nt;
        //让PE文件头指针指向其对应的地址
        nt = (_IMAGE_NT_HEADERS*)peId;
        printf("Machine:%X\n", nt->FileHeader.Machine);
        printf("Magic:%X\n", nt->OptionalHeader.Magic);
        //创建一个指针数组,该指针数组用来存储所有的节表指针
        //这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
        _IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

        //创建指向块表的指针
        _IMAGE_SECTION_HEADER* sectionHeader;
        //让块表的指针指向其对应的地址
        sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
        //计数,用来计算块表地址
        int cnt = 0;
        //比较 计数 和 块表的个数,即遍历所有块表
        while (cnt < nt->FileHeader.NumberOfSections) {
            //创建指向块表的指针
            _IMAGE_SECTION_HEADER* section;
            //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
            section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
            //将得到的块表指针存入数组
            sectionArr[cnt++] = section;
            //输出块表名称
            printf("%s\n", section->Name);
        }
        CloseHandle(hFile);

        int i;
        //sectionAlignment(dos, nt, sectionArr, "C:\\Users\\sixonezero\\Desktop\\EverEdit\\EverEdit.exe",hMap, pFile,2);

        for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
            sectionAlignment(dos, nt, sectionArr, "C:\\Users\\lyl610abc\\Desktop\\EverEdit\\EverEdit_修正.exe", &hMap, &pFile,i);
        }

        combineSection(dos, nt, sectionArr);

        break;
    }

    case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
    {
        printf("64位程序\n");
        //确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
        //创建指向PE文件头的指针
        _IMAGE_NT_HEADERS64* nt;
        nt = (_IMAGE_NT_HEADERS64*)peId;
        printf("Machine:%X\n", nt->FileHeader.Machine);
        printf("Magic:%X\n", nt->OptionalHeader.Magic);

        //创建一个指针数组,该指针数组用来存储所有的节表指针
        //这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
        _IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

        //创建指向块表的指针
        _IMAGE_SECTION_HEADER* sectionHeader;
        //让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
        sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
        //计数,用来计算块表地址
        int cnt = 0;
        //比较 计数 和 块表的个数,即遍历所有块表
        while (cnt < nt->FileHeader.NumberOfSections) {
            //创建指向块表的指针
            _IMAGE_SECTION_HEADER* section;
            //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
            section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
            //将得到的块表指针存入数组
            sectionArr[cnt++] = section;
            //输出块表名称
            printf("%s\n", section->Name);
        }

        break;
    }

    default:
    {
        printf("error!\n");
        break;
    }

    }
    return 0;
}

合并节代码

//合并节
//第一个参数为指向dos头的指针
//第二个参数为指向nt头的指针
//第三个参数为存储指向节指针的数组
void combineSection(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
    //所有节内存对齐后的大小的和,这里要求已经修正过内存对齐,只有这样文件对齐大小才会等于内存对齐大小
    DWORD allSectionSize = 0;
    //所有节的权限,初始为第一个节的权限,和后面的每个节的权限进行或操作
    DWORD allSectionCharacteristics = sectionArr[0]->Characteristics;
    int i;
    for (i = 0; i < nt->FileHeader.NumberOfSections;i++) {
        allSectionSize += sectionArr[i]->SizeOfRawData;
        allSectionCharacteristics = allSectionCharacteristics | sectionArr[i]->Characteristics;
    }
    printf("allSectionSize:%X\n", allSectionSize);
    printf("allSectionCharacteristics:%X\n", allSectionCharacteristics);

    sectionArr[0]->Misc.VirtualSize = allSectionSize;
    sectionArr[0]->SizeOfRawData = allSectionSize;
    sectionArr[0]->Characteristics = allSectionCharacteristics;

    //清零后面的节
    for (i = 1; i < nt->FileHeader.NumberOfSections; i++) {
        ZeroMemory(sectionArr[i], sizeof(_IMAGE_SECTION_HEADER));
    }

    //节个数设置为1
    nt->FileHeader.NumberOfSections = 1;
}

运行结果

image-20210406200009298


image-20210406194149811


说明

  • 合并节就是用一个节表来包括多个节表的信息
  • 可以看到合并节除了修正内存对齐,其余部分都十分简单
  • 合并节之后多出了的节表空间可以用来新增节

附件

附上本笔记中分析的EverEdit文件:点我下载

此次附件中添加了合并完节后的exe

image-20210406202251712


免费评分

参与人数 6吾爱币 +5 热心值 +6 收起 理由
aswcy815174418 + 1 + 1 对楼主代码优化:https://wwe.lanzous.com/iuh6yo72x7i
朱朱你堕落了 + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
bailemenmlbj + 1 + 1 谢谢@Thanks!
debug_cat + 1 来打call。啦啦啦啦啦
Service123 + 1 + 1 热心回复!
sam喵喵 + 1 谢谢@Thanks!

查看全部评分

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

aswcy815174418 发表于 2021-4-16 23:28
本帖最后由 aswcy815174418 于 2021-4-16 23:32 编辑

研究了楼主代码一两天,查了许多资料,修改了一些代码,看起来更简化一些,可能写法不是很规范
https://wwe.lanzouj.com/iuh6yo72x7i

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
lyl610abc + 1 + 1 用心讨论,共获提升!

查看全部评分

 楼主| lyl610abc 发表于 2021-4-6 21:09
sam喵喵 发表于 2021-4-6 20:58
大佬,请问你在哪里学到的这么多新技能

自学
首先要知道自己想要实现什么
然后要知道实现功能涉及的内容
接着要利用各种搜索引擎,包括但不限于百度、谷歌等检索自己所需要的
然后就是边查边写
拿这次的用C语言 appendFile,在指定位置插入数据为例
想要实现的功能:在指定位置插入数据为例
实现功能涉及内容:C语言 文件读写  插入数据
搜索关键词:C语言插入数据到指定位置
查完发现没有现成的代码,但有看到有用的信息:文件是连续存储的,无法直接插入
于是就想到要复制 然后插入数据 最后再复制剩下的
最后实现功能等等
很多人在不会的情况下,不知道如何检索有用的信息,这才是不会的根本
我个人觉得,学习不只是要学知识本身,更是要学习处理问题的方法
虽然我也只是个小菜鸡,但是莫名的大道理倒是能讲不少,你就随便看看就好

点评

这才是大佬,不限于当前,只要我想,就能冲。  发表于 2021-4-6 23:10
sam喵喵 发表于 2021-4-6 20:58
sam喵喵 发表于 2021-4-6 21:00
没见到书上提合并节这一神技
sam喵喵 发表于 2021-4-6 21:09
sam喵喵 发表于 2021-4-6 20:58
大佬,请问你在哪里学到的这么多新技能

找到了,滴水
sam喵喵 发表于 2021-4-6 21:14
lyl610abc 发表于 2021-4-6 21:09
自学
首先要知道自己想要实现什么
然后要知道实现功能涉及的内容

听君一席话,胜读十年书!
莫莫 发表于 2021-4-6 22:12
非常感谢
wangshouyin 发表于 2021-4-7 06:28
您好。希望以后代码段您可以用编辑功能里的代码段标记起来,不然看的有点乱。谢谢分享
Spa495 发表于 2021-4-7 08:39
学习了,大神
lifz888 发表于 2021-4-7 10:07
非常好 的实习资料,支持分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2024-11-5 20:32

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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