“金山杯2007逆向分析挑战赛”第一阶段第二题

简介:   注:题目来自于以下链接地址:http://www.pediy.com/kssd/   目录:第13篇 论坛活动 \ 金山杯2007逆向分析挑战赛 \ 第一阶段 \ 第二题 \ 题目 \ [第一阶段 第二题]     题目描述:     己知是一个 PE 格式 EXE 文件,其三个(section)区块的数据文件依次如下:(详见附件)   _text,_rdata,_data   1. 将 _text, _rdata, _data 合并成一个 EXE 文件,重建一个 PE 头,一些关键参数,如 EntryPoint,ImportTable 的 RVA,请自己分析文件获得。

  注:题目来自于以下链接地址:http://www.pediy.com/kssd/

  目录:第13篇 论坛活动 \ 金山杯2007逆向分析挑战赛 \ 第一阶段 \ 第二题 \ 题目 \ [第一阶段 第二题]

 

  题目描述:

 

  己知是一个 PE 格式 EXE 文件,其三个(section)区块的数据文件依次如下:(详见附件)
 
  _text,_rdata,_data

  1. 将 _text, _rdata, _data 合并成一个 EXE 文件,重建一个 PE 头,一些关键参数,如 EntryPoint,ImportTable 的 RVA,请自己分析文件获得。合并成功后,程序即可运行。
  2. 请在第1步获得的EXE文件基础上,增加菜单。具体见图:

  要插入的菜单

 

  3. 执行菜单 Help / About 弹出如下图所示的 MessageBox 窗口:

  点击菜单弹出的MessageBox

 

  题目分析和解答:

 

  (一)拼接可执行文件:

 

  首先下载题目的附件,附件中已经有三个文件,分别是 PE 文件的三个 section,可以看到三个 section 文件已经按照 0x1000 大小对齐。这样我们只需要把这三个文件依次连接在一起,接在一个正确的 PE 文件头后面就可以了。

 

  可以先用 VC (我采用 VS2005)创建一个 Windows 窗口程序(它将提供一些主要样本,所以称这个程序为样本程序),把程序写的尽可能和题目中的程序类似,然后编译,即首先得到了一个 PE 文件头的原型,再次基础上进行修改,也就是根据题目给出的 section,适当调整 PE 文件头中的需要修改的字段。

 

  在本题求解过程中,我严重依赖于我从前写的一个展示 PE 文件格式的应用程序,此程序最近经过我的调整和改进,它的优点是由于此程序基于扩展 TreeView 控件,因此帮助快速理解 PE 文件头的结构,其效果见以下截图:

 

  

  

  关于此程序的更多信息,请参见我的博客文章:《[VC6] 图像文件格式数据查看

 

  BmpFileView 的可执行文件的下载链接(不敢说它是最好的,但作为帮助学习PE文件格式的辅助工具而强烈推荐):

  http://files.cnblogs.com/hoodlum1980/BmpFileView_V2_Bin.zip

 

  观察题目给出的三个 section 文件,可以给出这三个 section 的基本信息如下:

 SectionName   VirtualAddress   RawDataSize   VirtualSize 
.text 1000h 6000h 5B73h
.rdata 7000h 1000h 0C6Eh
.data 8000h 3000h 4000h
.rsrc B000h    

  

  其中,.rsrc 是需要在稍后插入的资源 section,将在稍后讲解。

  这里需要特别注意的是,.data 的虚拟内存尺寸,必须要比文件尺寸(RawDataSize)更大一些,关于这一点我还暂时不能给出详细的解释,有待于在将来做进一步研究。如果把 .data 的 VirtualSize 设置为和 RawDataSize 一样大(3000h),则程序无法运行,会弹出一个消息框提示这不是一个有效的 Win32 程序。所以这一步我也是反复尝试是否是其他字段的问题,纠结了半天才发现原来问题卡在这个地方。

 

  对于 PE 文件头的 IMAGE_OPTINAL_HEADER.CheckSum,Windows 看起来完全忽略这个字段的值,所以这个字段可以不用管。

 

  明确了以上问题,现在可以把这三个 section 和文件头链接成一个新的 PE 文件了,把样本程序 pediy02.exe 和三个 section 文件放在同一个目录下,通过一个辅助的 Console 项目(pediy02_helper 项目)来完成这些工作,生成的新的 PE 文件名为 pediy02_new.exe,使用的辅助函数如下(为了简单明了起见,代码中并没有插入繁琐的检测性代码,例如申请的缓冲区大小,已经根据需要,在编码时被静态的确定了):

 

  Code 1.1 将三个 Section 拼接成 PE 文件的 C++ 代码:

 

void WriteToFile(FILE *fp, void* pBuf, DWORD nSize);

int CreateNewPe()
{
    //PIMAGE_IMPORT_DESCRIPTOR pImportTable = NULL;
    PIMAGE_DOS_HEADER pDosHdr = NULL;
    PIMAGE_NT_HEADERS pNtHdrs = NULL;
    PIMAGE_SECTION_HEADER pSectionHdr = NULL;

    FILE *fp1, *fp2, *fp3;
    TCHAR szPath[MAX_PATH];
    LPCTSTR szNames[3] = 
    {
        _T("_text"), _T("_rdata"), _T("_data")
    };

    _stprintf_s(szPath, _T("%s\\pediy02.exe"), THE_DIR);
    _tfopen_s(&fp1, szPath, _T("rb"));
    _stprintf_s(szPath, _T("%s\\pediy02_new.exe"), THE_DIR);
    _tfopen_s(&fp2, szPath, _T("wb"));

    //读取文件头部
    void* buf = malloc(0xD000);
    fread(buf, 1, 0x1000, fp1);

    pDosHdr = (PIMAGE_DOS_HEADER)buf;
    pNtHdrs = (PIMAGE_NT_HEADERS)((DWORD)buf + pDosHdr->e_lfanew);
    pSectionHdr = (PIMAGE_SECTION_HEADER)((DWORD)pNtHdrs + sizeof(IMAGE_NT_HEADERS));

    /*
         ----------------------------------------------
        | section | addr  | RawDataSize  | VirtualSize |
        |---------+-------+--------------+-------------|
        | .text   | 1000h | 6000h        | 5B73h       |
        | .rdata  | 7000h | 1000h        | 0C6Eh       |
        | .data   | 8000h | 3000h        | 4000h       |
        | .rsrc   | B000h | 1000h        | 1000h       |
         ----------------------------------------------
    */

    pNtHdrs->FileHeader.NumberOfSections = 4;
    pNtHdrs->OptionalHeader.BaseOfCode = 0x1000;
    pNtHdrs->OptionalHeader.BaseOfData = 0x8000; //+1000h 的 .rsrc
    pNtHdrs->OptionalHeader.SizeOfCode = 0x6000;
    pNtHdrs->OptionalHeader.SizeOfImage = 0xD000;
    pNtHdrs->OptionalHeader.SizeOfInitializedData = 0x5000;
    pNtHdrs->OptionalHeader.SizeOfUninitializedData = 0;
    pNtHdrs->OptionalHeader.AddressOfEntryPoint = 0x1527; //入口点

    //IMAGE_DIRECTORY_ENTRY_IMPORT 需要进一步调整, kernel32.dll, gdi32.dll, user32.dll 加上一个结尾
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress = 0x7618;
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size = 
        sizeof(IMAGE_IMPORT_DESCRIPTOR) * (3 + 1);

    //资源表
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress = 0xC000;
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].Size = 0x011C;
    
    // IMAGE_DIRECTORY_ENTRY_DEBUG 6
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress = 0;
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].Size = 0;

    // IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].VirtualAddress = 0;
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].Size = 0;

    //IMAGE_DIRECTORY_ENTRY_IAT 12; (import address table), IMAGE_IMPORT_DESCRIPTOR.FirstTrunk 中的最小值
    //IAT 地址需要在修改后找,需要进一步调整
    //IAT 的地址通常就是 .rdata 的起始地址
    //Size 是 FirstTrunk 中的最大地址 - IAT 起始地址) + 8;
    //(其中 +4 是最后一个元素占用的空间,再 +4 是一个NULL元素,表示结尾)
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress = 0x7000;
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].Size = 0x012C;


    //section_headers
    //.text
    pSectionHdr[0].VirtualAddress = 0x1000;
    pSectionHdr[0].SizeOfRawData = 0x6000;
    pSectionHdr[0].PointerToRawData = 0x1000;
    pSectionHdr[0].Misc.VirtualSize = 0x5B73;
    //.rdata
    pSectionHdr[1].VirtualAddress = 0x7000;
    pSectionHdr[1].SizeOfRawData = 0x1000;
    pSectionHdr[1].PointerToRawData = 0x7000;
    pSectionHdr[1].Misc.VirtualSize = 0x1000;
    //.data
    //.data 的虚拟内存大小(VirtualSize)必须比文件中更大,否则无法启动,现在我也不知道为什么
    pSectionHdr[2].VirtualAddress = 0x8000;
    pSectionHdr[2].SizeOfRawData = 0x3000;
    pSectionHdr[2].PointerToRawData = 0x8000;
    pSectionHdr[2].Misc.VirtualSize = 0x4000; //【重要!】必须比 SizeofRawData 大一些

    //.rsrc (resource) 因为.data 比文件中大,所以.rsrc 相应的要像高地址移动
    pSectionHdr[3].VirtualAddress = 0xC000;
    pSectionHdr[3].SizeOfRawData = 0x1000;
    pSectionHdr[3].PointerToRawData = 0xB000; //文件中的地址还是紧靠.data
    pSectionHdr[3].Misc.VirtualSize = 0x011C; //从范本文件中得到该值

    fwrite(buf, 1, 0x1000, fp2);
    fflush(fp2);

    int i;
    DWORD dwFileSize;
    for(i = 0; i < 3; i++)
    {
        _stprintf_s(szPath, _T("%s\\%s"), THE_DIR, szNames[i]);
        _tfopen_s(&fp3, szPath, _T("rb"));
        fseek(fp3, 0, SEEK_END);
        dwFileSize = ftell(fp3);
        fseek(fp3, 0, SEEK_SET);
        fread(buf, 1, dwFileSize, fp3);
        fclose(fp3);
        
        WriteToFile(fp2, buf, dwFileSize);
    }

    //从已有的范本复制 .rsrc 节
    fseek(fp1, 0xB000, SEEK_SET);
    fread(buf, 1, 0x1000, fp1);
    WriteToFile(fp2, buf, 0x1000);

    fclose(fp1);
    fclose(fp2);

    free(buf);
    return 0;
}
//写入文件,以 1KB 为单位 void WriteToFile(FILE *fp, void* pBuf, DWORD nSize) { //以1KB为基本单位,逐次写入 char* pos = (char*)pBuf; size_t BytesToWrite; while(nSize > 0) { BytesToWrite = min(nSize, 0x400); fwrite(pos, 1, BytesToWrite, fp); fflush(fp); nSize -= BytesToWrite; pos += BytesToWrite; } }

 

  上面的函数已经是最终版本的函数,它已经完成了以下工作:

 

  (1)确定 AddressOfEntryPoint 的地址。

  (2)确定 DataDirectory[1]: ImportTable (导入表)的地址和尺寸。

  (3)确定 DataDirectory[12]: Import Address Table (绑定导入函数地址表)的地址和尺寸。

  (4)从样本程序 pediy02.exe 中插入资源 (.rsrc) section,并确定 DataDirectory[2]: resource Table (资源表)的地址和尺寸。

 

  当然很显然上面的工作并不是一步到位完成的,下面简要介绍上面的工作是如何完成的:

 

  (1)确定入口点地址:

 

  该工作相对简单容易,先把 EntryPoint 设置为 .text (代码段)的起始地址:0x1000,然后生成文件后,加载到 IDA 中分析代码段的内容,就可以很容易的找到以下函数的地址(以下地址为 VA,即加上了 ImageBase 后的地址):

 

  0x00401527: __tmainCRTStartup,是 PE 文件的实际入口点。

  0x004011EC: WinMain,高级语言编程时的程序入口点。

  0x004012D5: WndProc, 当前的窗口过程(稍后将会被子类化)

  0x004059C4: sub_4059C4,基本等价于 MessageBoxA,很重要,称它为 ___crtMessageBoxA

 

  现在只要知道,在文件头中把入口地址设置到 __tmainCRTStartup 函数即可,文件头要求的是 RVA,因此在代码中设置入口点:

 

  IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint = 0x1527;

 

  这样入口点地址就确定好了。

 

  (2)确定 DataDirectory [1] 导入表的地址和大小:

 

  这一步也相对比较简单,导入表位于 .rdata 中(位于中部)。在此之前,必须了解导入表的结构,导入表是一个由多个 IMAGE_IMPORT_DESCRIPTOR 元素组成的数组,以 NULL 元素(内容全部是 0 )标识结尾(IMAGE_IMPORT_DESCRIPTOR 的数据结构定义参见 winnt.h)。每个元素由 5 个 DWORD 组成,其中倒数第二个 DWORD 是 Name 字段(字符串指针),它的值是一个 RVA(即相对于 ImageBase 的偏移),指向了 Dll 名字(ASCII)字符串(该字符串同样位于 .rdata 中)。

  导入表的示意结构如下图所示(图中展示的是两个 Thunk 数组并行情况,因此 FirstThunk 也是字符串指针的大多数情况,图中的字符串虽然位于整齐的矩形格子之内,这只是为了图形外观,应该强调的是这些字符串的长度是不固定的,长度有长有短,所以它们在空间中的分布是参差不齐的):

 

  

  

  上图表示了 pediy2_new.exe 的实际导入表,共导入了 3 个 DLL,每个导入 DLL 是导入表中的一个元素,在这个数组中的每个元素大小为 20 Bytes,如果引用了 3 个 DLL,则这个数组一共为 (3 + 1) * 20 = 80 Bytes (最后有一个 null terminator element)。下面是单个元素 descriptor 大小:

 

  sizeof ( IMAGE_IMPORT_DESCRIPTOR ) = sizeof ( DWORD ) * 5 = 20 Bytes;

 

  每个元素的 OriginalFirstTrunk 和 FirstTrunk 是两个指针,指向了两个 并行的指针数组,通常情况下(即没有在链接时事先绑定)这两个数组的内容是相同的(即两个数组的所有元素的值相同),在静态 PE 文件中,都指向相同的长度不固定的函数名称字符串(或者是被导入函数的 Ordinal)。

 

  补充说明:在没有经过事先绑定时,OriginalFirstTrunkFirstTrunk 指向的数组内容在加载之前都指向 .rdata 中的一些长度不固定的 Ascii 编码的字符串,在加载时 FirstTrunk 指向的数组被系统绑定成映射到本进程的 DLL 的实际函数地址(因此该数组称为 IAT),所以这些元素称为 Trunk (意味着其身份的可变性,这些元素在加载后其身份发生了变化),因为指向的是数组头部,所以称之为 First(IMAGE_IMPORT_DESCRIPTOR.(Original)FirstTrunk 表示某个 DLL 被本模块导入的首个函数的 Trunk 的位置,后面还有更多的函数 Trunk,以 NULL 表征结束)。OriginalFirstTrunk 在加载后保持不变(所以称为 Original),所以相当于存储着导入函数名称的一份副本。在模块被加载后,可以通过 OriginalFirstTrunk 数组了解到该模块导入了哪些函数(名称),通过 FirstTrunk 数组的内容可了解到导入函数的运行时虚拟地址。导入函数的实际地址是在加载时绑定的(无法在编译时确定),编译器可能为每个 dll 函数调用生成一个很小的函数体,称为 j_XXX, 该函数体负责 jmp 到 FirstTrunk 数组中的元素给出的运行时函数地址,也可以直接调用 IAT 元素内容指向的 VA 地址。

 

  虽然应用程序可以通过序号导入函数,并具有极高效率,但是这样会导致看不到导入函数的名字,对程序和系统的维护造成障碍。所以除非成本太高(例如 MFC 类库的导出函数过多,且面向对象的 C++ 函数名称也很长,所以 MFC 类库的函数以 Ordinal 方式被导入),按名称导入是普遍做法,显然按名称导入,需要线性搜索模块的导出函数表,这就会消耗一定的加载时间成本。为了提高程序加载时效率,应用程序可以通过 “事先 Rebase” (将程序需要导入的模块自身建议的 ImageBase 进行精心调整,从而避免在加载时重定向) 和 “事先绑定” 提高程序在客户运行环境的加载速度,系统通过时间戳判定绑定信息是否有效,如果时间戳不一致,或者发生重定向,系统则必须再次进行加载时绑定。

  

  OriginalFirstTrunkFirstTrunk 指向的这两个指针数组位于 .rdata 的不同位置,其中 FirstTrunk 指向的数组位于 .rdata 的起始位置(稍后可以看到这就是 IAT),OriginalFirstTrunk 指向的数组位于稍微靠后的位置。两个 Trunk 在 PE 文件中的值都指向相同的 IMAGE_IMPORT_BY_NAME (由 Hint 和 函数名称字符串 组成的数据结构)。IAT 所在的页面将在加载时被临时设定为可写,绑定之后再恢复为只读。有关这部分的细节请参考我的博客文章:《读取PE文件的导入表》

 

  关于导入表和 IAT 的在内存空间中的位置布局,请参考本文的补充讨论(2)。

 

  了解了导入表结构,就可以很快找到导入表的位置了,首先在 .rdata 中查找 DLL 名称字符串,可以找到如下的字符串:

  FA: 0x000077AC: "KERNEL32.dll";  (这里使用的是文件地址 FA,或者说是 RVA)

  

  找到附近指向该位置的指针,即在附近的文件内容中搜索 "AC 77 00 00" 片段,可以找到文件地址:

  FA: 0x00007624: AC 77 00 00

 

  这里就是一个 IMAGE_IMPORT_DESCRIPTOR 元素,把该地址减去 3 个 DWORD 值,即得到该元素的起始地址为 0x00007618。由于导入表元素内容非常有特点,很容易就可以判断导入表的两端边界,因此可以很快确定导入表的起始地址(RVA)和 Size 如下:

 

  IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER.DataDirectory[2].VirtualAddress = 0x7618;

  IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER.DataDirectory[2].Size = sizeof ( IMAGE_IMPORT_DESCRIPTOR ) * 4;

 

  (3)确定 DataDirectory [12],IAT的地址和大小:

 

  IAT 的地址比较简单,它就是所有 DLL 的 FirstTrunk 字段的最小值,通常就是 .rdata 的起始位置(那些常量字符串位于 IAT 和 ImportTable 的后面),也就是 0x7000 (可以看到这里是从 Gdi32.dll 的导入的第一个函数 DeleteObject)。

 

  要计算 IAT 的大小,需要遍历导入表,找到导入的所有 Dll 的 FirstTrunk 的最后一个元素的位置,同时还要考虑到结尾还需要一个 NULL 指针作为结束标志,所以:

  

  IAT.Size = max ( 所有 DLL 的 FirstTrunk 数组元素所在的地址(RVA) ) - IAT.VirtualAddress (RVA) + 8 。

  

  有关如何遍历导入表的更多内容,请参考我的博客文章(在此就不再详细叙述了):《读取PE文件的导入表》。

 

  本题目中所有的 Trunk 的最大地址(RVA)是 0x7124(从 USER32.dll 导入的 DispatchMessageA),可得:

 

  DataDirectory[12].VirualAddress = 0x7000; // RVA (Relative to ImageBase )

  DataDirectory[12].Size = 0x012C;

 

  经过以上修改,可以通过 CreateNewPe 函数,生成一个可以执行的 PE 文件了。题目的前半部分要求此时完成。接下来考虑后半部分要求,为程序添加菜单和相关的命令处理函数。

 

  (二)添加菜单 和 处理函数。

 

  (1)添加 .rsrc section (菜单资源)

 

  添加资源,同样通过在样本程序中实现。在样本程序中,添加题目要求一样的资源(只保留菜单,删除所有其他种类资源,这样可以使 .rsrc 最小,仅占用 1000h 大小),然后可以从样本程序中拷贝 .rsrc 段,追加到我们已经得到的 PE 文件的尾部。同时调整 PE 文件头中的相关字段。

 

  注意:由于 .data 节在加载到虚拟内存中时被扩大了 1000h,所以位于最后的 .rsrc 的文件地址(FA)和虚拟地址(VA)将会偏差 1000h。即:

 

  VA = FA + 1000h;

 

  众所周知,窗口的菜单通常是在注册窗口类时指定的。因此为了添加菜单,在 IDA 中观察 WinMain 函数的代码:

 

  Code 2.1 由 .text 提供的 WinMain 函数的汇编代码:

 

.text:004011EC ; int __stdcall WinMain(int,int,int,int nCmdShow)
.text:004011EC WinMain         proc near               ; CODE XREF: start+C9
目录
相关文章
|
1月前
|
机器学习/深度学习 人工智能 自然语言处理
前谷歌科学家离职后创业一年,发文自述算力是训练大模型的难点
【2月更文挑战第20天】前谷歌科学家离职后创业一年,发文自述算力是训练大模型的难点
28 2
前谷歌科学家离职后创业一年,发文自述算力是训练大模型的难点
|
9月前
|
安全 Java Go
阿里比赛AliCrackme逆向分析
阿里比赛AliCrackme逆向分析
185 0
|
存储 JSON 人工智能
送给大模型的「高考」卷:442人联名论文给大模型提出204个任务,谷歌领衔
送给大模型的「高考」卷:442人联名论文给大模型提出204个任务,谷歌领衔
136 0
送给大模型的「高考」卷:442人联名论文给大模型提出204个任务,谷歌领衔
|
机器学习/深度学习 边缘计算 人工智能
液冷技术再下一城 阿里云三篇论文入选DesignCon 2022
阿里云三篇液冷技术论文入选DesignCon 2022~
液冷技术再下一城 阿里云三篇论文入选DesignCon 2022
|
区块链
《《技术的乌托邦还是商业的潘多拉魔盒?》比特币研究报告》电子版地址
《技术的乌托邦还是商业的潘多拉魔盒?》比特币研究报告
《《技术的乌托邦还是商业的潘多拉魔盒?》比特币研究报告》电子版地址
|
机器学习/深度学习 编解码 监控
大淘宝技术斩获NTIRE 2023视频质量评价比赛冠军(内含夺冠方案)
近日,CVPR NTIRE 2023 Quality Assessment of Video Enhancement Challenge比赛结果公布,来自大淘宝音视频技术团队的同学组成「TB-VQA」队伍,从37支队伍中脱颖而出,拿下该比赛(唯一赛道)冠军。此次夺冠是团队继MSU 2020和2021世界编码器比赛、CVPR NTIRE 2022压缩视频超分与增强比赛夺魁后,再次在音视频核心技术的权威比赛中折桂。
127 0
|
机器学习/深度学习 数据采集 人工智能
AI十级「找茬」选手,非这个书生莫属,节后开源!(1)
AI十级「找茬」选手,非这个书生莫属,节后开源!
116 0
|
运维 监控 安全
评测5款国内外免费远控,谁是最好用第一名?
远程控制应用不少人都有了解使用过,尤其是会常用电脑进行工作的群体,比如程序员、设计师、运维、文员等岗位。在隔离居家远程办公时,通过家里的手机、平板或电脑跨系统、跨设备操控公司所用的办公电脑,就能及时处理工作内容,不会因缺少资料素材而影响到项目进度。像我个人在家办公就常习惯用平板,连上鼠标,利用远程控制软件操纵公司的电脑,很方便~
583 0
2021年度训练联盟热身训练赛第一场——Weird Flecks, But OK(最小圆覆盖)
2021年度训练联盟热身训练赛第一场——Weird Flecks, But OK(最小圆覆盖)
78 0
2021年度训练联盟热身训练赛第三场——C,G,I
2021年度训练联盟热身训练赛第三场——C,G,I
73 0