zoukankan      html  css  js  c++  java
  • PE文件格式综述

    前言:本文由《加密与解密第三版》和《WindowsPE权威指南》总结而成,将两者综合,作为一个较全面的PE结构总览。作为逆向的“内功”有必要深入学习。

    数据目录项的具体细节还在整理ing。

    正文:

    PE总结

    概述:

    微软的可执行文件格式,也就是PEPortable Executable)格式。从某种意义上讲,可执行文件的格式是操作系统本身执行机制的反映。PE文件衍生于早期建立在VAX/VMS上的COFF文件格式(Common Object File Format)。所谓的COFF格式,它是一种流行的对象文件格式,这种格式不只用于目标文件,还包括库文件、可执行文件等(并不单纯的指编译器产生的目标文件,注意区别)。另外,EXEDLL文件之间区别完全是语义上的,它们使用完全相同的PE格式。唯一的区别就是用一个字段标识出这个文件是EXE还是DLL格式(后面会讲到)。

    1PE基本概念

    PE文件头记录了PE文件中的所有数据的组织方式,它类似于一本书的目录,通过目录我们可以快速定位到某个具体的章节。通过PE文件头部分对某些数据结构的描述,我么可以定位到那些不在文件头部的信息,比如导入表数据、导出表数据、资源表数据等。PE文件使用的是一个平面地址空间,所有代码和数据都被合并在一起,组成一个很大的结构。文件的内容被分割为不同的区块(Section,又称区段、节等),区块中包含代码和数据,各个区块按页边界来对齐,区块没有大小限制,是一个连续结构。每个块都有它自己在内存中的一套属性,比如:这个块是否包含代码、是否只读或可读\写等。

     

    1.1、地址

    PE中涉及的地址有四类,它们分别是:

    内存虚拟地址(VA

    相对虚拟地址(RVA

    文件偏移地址(FOA

    特殊地址

    拓展:

    32CPU的寻址能力位4GB232个字节)。操作系统和CPU的内存管理单元共同作用,为用户提供了虚拟内存的管理机制,即分页机制。

    分页机制原理:

    操作系统假设一个进程独立拥有4GB内存,按照某个固定大小(例如4KB)将这4GB空间分成N1M)个页。在某一时刻,所有这些页只有一部分和物理内存是对应的(所以这种机制允许物理内存比4GB小)。没有物理内存对应的页面被标记为脏的页面,一般存储在一个名为“交换文件”的磁盘文件中。当系统需要读取未在内存中的数据时,这部分数据会将内存中不经常读写的页交换出内存,而把要读取的、为于交换文件中的页换进内存。通过这种存取机制可以让一个进程拥有比实际内存大得多的内存。利用这种机制管理的内存称为虚拟内存。

     

    1、虚拟内存地址

    PE文件通过Windows加载器被装入内存后,其相关的动态链接库(DLL)也会被加载,我们把这些同时加载到进程地址空间的文件称作模块(Module)。映射文件的起始地址被称为模块句柄(hModule),可以通过模块句柄访问内存中其他的数据结构。这个初始内存地址也称为基地址(ImageBase)。

    内存中的模块代表着进程从这个可执行文件中所需要的代码、数据、资源、输入表、输出表及其他有用的数据结构所使用的内存都放在一个连续的内存块中,编程人员只要知道装载程序文件映像到内存后的基地址即可。基地址的指是由PE文件本身设定的。按照默认设置,VC++建立的EXE文件基地址是00400000hDLL文件的基地址是10000000h

    2、相对虚拟内存地址

    每一个模块在被加载时都会有一个基地址,也就是预先告诉操作系统:它会占用4GB空间的哪个部分(即从哪里开始存储该模块)。不同模块的基地址一般是不同的,如果两个模块的基地址相同,就由操作系统来决定这两个模块在虚拟空间中具体位置。

    相对虚拟地址(Reverse Virtual AddressRVA)是对于基地址的偏移,即RVA是虚拟内存用来定位某个特定位置的地址,该地址的值是这个特定位置距离某个模块基地址的便宜量,所以说RVA是针对某个模块而存在的。

    3、文件偏移地址

    文件偏移地址(File Offset AddressFOA)和内存无关,它是指当PE文件储存在硬盘上时,某个数据的位置距离文件头的偏移量。文件偏移地址从PE文件的第一个字节开始计数,起始值为0

    4、特殊地址

    PE结构中还有一种特殊地址,其计算方法并不是从头文件算起,也不是从内存某个模块的基地址算起,而是从某个特定的位置算起。这种地址在PE结构中很少见,如在资源表里就出现过这样的地址。

    1.2、指针

    PE数据结构中指针的定义:如果数据结构中某个字段存储的值为一个地址,那么这个字段就是一个指针。

    1.3、数据目录

    PE中有一个数据结构称为数据目录,其中记录了所有可能的数据类型。这些数据类型中,目前已经定义的有15种,包括导出表、导入表、资源表、异常表、属性证书表、重定位表、调试数据、ArchitectureGlobal Ptr、线程局部存储、加载配置表、绑定输入表、IAT、延迟导入表和CLR运行时头部。

    1.4、节

    由于提倡程序与数据的独立性,因此,程序中的代码和数据通常是分开存放的。为了保证程序执行的安全,保障内核的稳定,Windows操作系统通常对不同用途的数据设置不同的访问权限。Windows操作系统在加载可执行程序时,会为这些具有相同属性的数据分别分配标记有不同属性的页面(注意,不同属性的数据可能会被放到同一个页面中),以确保程序运行时的安全。

    节(section,又叫区段、区块)就是存放不同类型数据(代码、数据、资源等)的地方,不同的节具有不同的访问权限。节是PE文件中存放代码或数据的基本单元。一个节中的所有数据必须被加载到连续的内存空间中。

    从操作系统加载角度来看,节是相同属性数据的组合。与数据目录不同的是,尽管有些数据类型不同分别属于不同的数据目录,但由于其访问属性相同,便被归类到同一个节中。这个节最终可能会占用一个或多个页面;但无论有多少个,所有相关页面均会被赋予相同的页属性。这些属性包括只读、只写、可读、可写等。

    1.5、对齐

    PE中规定了三类对齐:数据在内存中的对齐、数据在文件中的对齐、资源文件中资源数据的对齐。

    1、内存对齐

    由于Windows操作系统对内存属性的设置以页为单位,所以通常情况下,节在内存中的对齐单位必须至少是一个页大小。对32位的Windows XP系统来说,这个值是4KB1000h),而对于64位操作系统来说,这个值就是8KB2000h)。

    2、文件对齐

    为了提高磁盘利用率,通常情况下,定义的节在文件中对齐单位要远小于内存对齐的单位;通常以一个物理扇区的大小作为对齐粒度的值,即512字节,十六进制表示为200h。因此数据段、代码段等起始地址都是200h的倍数。(如果内存对齐被定义为小于操作系统页的大小,则文件对齐和内存对齐的值必须一致!)

    3、资源数据对齐

    资源文件中,资源字节码部分一般要求以双字(4字节)对齐。

    1.6Unicode字符串

    Unicode是用全16位表示一个字符,ASCII码使用7位表示一个字符。Unicode字符串中的每个字符均为双字节,所以又称为宽字符串。

    Unicode128个字符码(0x0000~0x007F)同ASCII码具有同样的字节值(占用字节不同),剩下128Unicode字符(0x0080~0x00FF)是对ASCII码的扩展。使用一个字节来表示字符串中的字符,称为ANSI字符串。PE格式中涉及的字符串部分均采用ANSI字符串。然而,在资源表中,对菜单名、对话框标题等的描述则全部使用Unicode字符串。所以,在读取这些资源的字符串时,首先需要使用一些API函数实现从宽字符集到窄字符集的转换。(注意:Unicode字符串不想ANSI字符串保证用“\0”结束!)

     

    2PE文件结构

    32位系统下的PE文件结构被划分为5个部分,包括:

    DOS MZ头、DOS StubPE头、节表和节内容。

    节表和节内容两部分其实就是图3-5中所示的PE数据区。DOS MZ头的大小就是64个字节(DOS Stub大小不确定,但以某大小对齐),PE头的大小是456个字节(由于数据目录项不一定是16个,所以准确地说,PE头也是一个不能确定大小的结构(如何确定下面会讲到))。节表的大小之所以不固定,是因为每个PE中节的数量是不固定的。每个节的描述信息则是个固定值,共40个字节,节表是由不确定数量的节描述信息组成的,其大小等于节的数量*40。节内容的大小也是不可确定的。

     

    2.1MS_DOS头部

    每个PE文件是以一个DOS程序开始的,有了它,一旦程序在DOS就能识别出这是有效的执行体,随后运行DOS头的MZ header之后的DOS stub(指令字节码)。PE文件的第一个字节起始于一个传统的MS-DOS头部,被称作IMAGE_DOS_HEADER。结构如下所示:

    IMAGE_DOS_HEADER_STRUCT{

    +0h e_magic WORD ? DOS可执行文件标记“MZ

    +2h e_cblp WORD ? ;最后(部分)页中的字节数

    +4h e_cp WORD ? ;文件中的全部和部分页数

    +6h e_crlc WORD ? ;重定位表中的指针数

    +8h e_cparhdr WORD ? ;头部尺寸,以段落为单位

    +0Ah e_minalloc WORD ? ;所需最小附加段

    +0Ch e_maxalloc WORD ? ;所需最大附加段

    +0Eh e_ss WORD ? ;初始的SS值(相对偏移量,DOS代码)

    +10h e_sp WORD ? ;初始的SS值(DOS代码)

    +12h e_csum WORD ? ;补码校验值

    +14h e_ip WORD ? ;初始的IP值(DOS代码)

    +16h e_cs WORD ? ;初始的CS值(DOS代码)

    +18h e_lfarlc WORD ? ;重定位表的字节偏移量

    +1Ah e_ovno WORD ? ;覆盖号

    +1Ch e_res WORD 4 ;保留字

    +24h e_oemid WORD ? OEM标识符(相对e_oeminfo

    +26h e_oeminfo WORD ? OEM信息

    +28h e_res2 WORD 10 ;保留字

    +3Ch e_lfanew DWORD ? PE头相对于文件的偏移地址

    }IMage_DOS_HEADER_ENDS

    其中两个字段比较重要,分别是e_magice_Ifanewe_magic字段(一个字大小)需要被设置为5A4Dh,这个值ASCII为“MZ”(MS-DOS最初创始人之一的缩写)。E——Ifanew字段是真正PE文件头的相对偏移,其指出真正PE头的文件偏移地址,它占用4个字节,位于文件开始偏移的3Ch字节中。该字段的值是一个相对偏移量,绝对定位时需要加上DOS MZ头的基地址:
    PE_start=DOS MZ基地址+IMAGE_DOS_HEADER.e_Ifanew

     

    2.2PE文件头

    32位系统下,最重要的部分就是PE头和PE数据区。紧跟着DOS stub的是PE文件头(PE Header)。PE HeaderPE相关结构NT映像头(IMAGE_NT_HEADERS)的简称,其中包含许多PE装载器用到的重要字段。IMAGE_NT_HEADER是由三个字段组成(左边数字为到PE文件头的偏移量):

    IMAGE_NT_HEADERS_STRUCT{

    +0h Signature DWORD ? PE文件标识

    +4h FileHeader IMAGE_FILE_HEADER <> PE标准头

    +18h OptionalHeader IMAGE_OPTIONAL_HEADER32 <> PE扩展头

    }IMAGE_NT_HEADERS ENDS

    Signature字段被设置为00004550hASCII码字符为“PE00”。该标识位于指针IMAGE_DOS_HEADER.e_Ifanew指向的位置。

     

    2.2.1、标准PEIMAGE_FILE_HEADER

    标准PEIMAGE_FILE_HEADER紧跟在PE头标识后,即位于IMAGE_DOS_HEADER.e_Ifanew+4的位置。由此开始的20个字节为数据结构标准PEIMAGE_FILE_HEADER的内容。该结构在微软的官方文档中被称为标准通用对象文件格式(Common Object File FormatCOFF)头。它记录了PE文件的基本信息,如该PE文件运行的平台、PE文件类型(是EXE文件还是DLL文件)、文件中存在的节的总数,详细定义如下:

    IMAGE_FILE_HEADER STRUCT{

    +4h Machine WORD ? ;运行平台

    +6h NumberOfSections WORD ? PE中节的数量

    +8h TimeDateStamp DWORD ? ;文件创建日期和时间

    +Ch PointerToSymbolTable DWORD ? ;指向符号表(用于调试)

    +10h NumberOfSymbols DWORD ? ;符号表中符号数量(用于调试)

    +14h SizeOfOptionalHeader WORD ? ;可选头结构的长度

    +16h Characteristics WORD ? ;文件属性

    }IMAGE_FILE_HEADER ENDS

    (1)Machine:可执行文件的目标CPU类型,即PE文件的执行平台类型。

    (2)NumberOfSections:节(Section)的数目,节表紧跟在IMAGE_NT_HEADERS后面。

    (3)TimeDateStamp:表明文件是何时创建的。这个值使用格林威治时间计算。

    (4)PointerToSymbolTableCOFF符号表的文件偏移地址。如果没有符号表存在,将此值设为0

    (5)NumberOfSymbols:如果有COFF符号表,它代表其中符号的数目。

    (6)SizeOfOptionalHeader:紧跟着IMAGE_FILE_HEADER后面的数据大小。在PE文件中,这个数据结构叫IMAGE_OPTIONAL_HEADER,其中大小依赖于是32位还是64位文件。对于32PE文件,这个域通常是00E0h;对于64PE32+文件,这个域是00F0h。(这些是要求的最小值,较大的值也可能会出现)

    (7)Characteristics:文件属性。一般定义于winnt.h内的IMAGE_FILE_XXX值。普通的EXE文件这个字段一般为010fhDLL文件这个字段一般为0210h

     

    2.2.2、拓展PEIMAGE_OPTIONAL_HEADER32

    可选映像头(IMAGE_OPTIONAL_HEADER)是一个可选的结构,但实际上它却定义了更多PE的数据,结构如下:

    IMAGE_OPTIONAL_HEADER32 STRUCT{

    +18h Magic WORD ? ;标志字

    +1Ah MajorLinkerVersion BYTE ? ;链接器的主版本号

    +1Bh MinorLinkerVersion BYTE ? ;链接器的次版本号

    +1Ch SizeOfCode DWORD ? ;所有含代码的节(以字节计算)的总大小

    +20h SizeOfInitializeData DWORD ? ;所有含已初始化数据的节的大小

    +24h SizeOfUninitializeData DWORD ? ;所有含未初始化数据的节的大小

    +28h AddressOfEntryPoint DWORD ? ;程序执行入口RVA

    +2Ch BaseOfCode DWORD ? ;代码节的起始RVA

    +30h BaseOfData DWORD ? ;数据节的起始RVA

    +34h ImageBase DWORD ? ;程序的建议装载地址(基地址)

    +38h SectionAlignment DWORD ? ;内存中节的对齐粒度

    +3Ch FileAlignment DWORD ? ;文件中节的对齐粒度

    +40h MajorOperatingSystemVersion WORD ? ;操作系统主版本号

    +42h MinorOperatingSystemVersion WORD ? ;操作系统次版本号

    +44h MajorImageVersion WORD ? ;该PE主版本号

    +46h MinorImageVersion WORD ? ;该PE次版本号

    +48h MajorSubsystemVersion WORD ? ;所需子系统主版本号

    +4Ah MinorSubsystemVersion WORD ? ;所需子系统次版本号

    +4Ch Win32VersionValue DWORD ? ;未用,设置为0

    +50h SizeOfImage DWORD ? ;内存中的整个PE映像尺寸(对齐)

    +54h SizeOfHeaders DWORD ? ;所有头+节表的大小(对齐)

    +58h CheckSum DWORD ? ;校验和

    +5Ch Subsystem WORD ? ;文件的子系统

    +5Eh DllCharacteristics WORD ? ;Dll文件特性

    +60h SizeOfStackReserve DWORD ? ;初始化时的栈大小

    +64h SizeOfStackCommit DWORD ? ;初始化时实际提交的栈大小

    +68h SizeOfHeapReserve DWORD ? ;初始化是保留的堆大小

    +6Ch SizeOfHeapCommit DWORD ? ;初始化时实际提交的堆大小

    +70h LoaderFlags DWORD ? ;与调试有关

    +74h NumberOfRvaAndSizes DWORD ? ;下面的数据目录结构的项目数量

    +78h DataDirecotry IMAGE_DATA_DIRECTORY 16 ;数据目录

    }IMAGE_OPTIONAL_HEADER32 ENDS

     

    2.2.3、数据目录项IMAGE_DATA_DIRECTORY

    IMAGE_OPTIONAL_HEADER32(拓展PE头)结构的最后一个字段DataDirectory。该字段定义了PE文件中出现的所有不同类型的数据的目录信息。该结构只有两个字段,定义如下:

    IMAGE_DATA_DIRECTORY STRUCT{

    +0h VirtualAddress DWORD ? ;数据的起始RVA

    +4h isize DWORD ? ;数据块的长度

    }IMAGE_DATA_DIRECTORY ENDS

    总的数据目录一共由16个相同的IAMGE_DATA_DIRECTORY结构连续排列组成。这16个元组的数组每一项均代表PE中的某个类型的数据。

    0  Export table address and size 导出表

    1  Import table address and size 导入表

    2  Resource table address and size 资源表

    3  Exception table address and size 异常表

    4  Certificate table address and size 属性证书数据

    5  Base relocation table address and size 基址重定位表

    6  Debugging information starting address and size 调试信息

    7  Architecture-specific data 预留为0

    8  Global pointer register relative virtual address 指向全局指针寄存器的值

    9  Thread local storage table address and size 线程局部存储

    10  Load configuration table address and size 加载配置表

    11  Bound import table address and size 绑定导入表

    12  Import address table address and size 导入函数地址表

    13  Delay import descriptor address and size 延迟导入表

    14  CLR Runtime Header address and size CLR运行时头部数据

    15  Reserved 系统保留

    0】导出数据所在的节通常被命名为.edata,它包含一些可被其他EXE程序访问的符的相关信息,比如导出函数和资源等。

    1】导入数据所在的节通常被命名为.idata,它包含了PE映像中所有导入的符号。

    2】资源数据所在的节通常被命名为.rsrc。该节是一个多层二叉排序树,该树的节点指向PE中各种类型的资源,如图标、对话框、菜单等。树的深度可达231层,但是PE中常用的只有3层:类型层、名称层、语言代码层。

    3】异常表数据所在的节通常被命名为.pdata。该节是由用于异常处理的函数表项组成的数组。可选文件头中的ExceptionTable(异常表)字段指向它。在将它们放进最终映像文件之前,这些表项必须按函数地址进行排序,并且这些函数表项的描述必须符合特定的目标平台。该部分数据主要用于基于表的异常处理,适用于除x86之外的所有类型的CPU

    4】属性证书数据作用类似PE文件的校验和或者MD5码,通过这种属性证书的方式可以校验一个PE文件是或否被非法修改过。为PE文件添加属性证书表可以使该PE与属性证书相关联。属性证书表是由一组连续的按八进制字(从任意字节边界开始的16个连续字节)边界对齐的属性证书表项组成,每个属性证书表项指向WIN_CERTIFICATE结构。此结构可以在WinTrust.H文件中找到,该结构的详细定义如下:(该数据不作为映像的一部分被映射到内,因此DataDirectory.Certificate_VirtualAddress字段是文件偏移,而不是RVA

    WIN_CERTIFICATE STRUCT{

    +0h dwLength DWORD ? ;证书长度

    +4h wRevision WORD ? ;证书版本号

    +6h wCertificateType WORD ? ;证书类型

    +8h bCerticicate byte ? ;证书内容

    }WIN_CERTIFICATE ENDS

    5】基址重定位信息所处的节通常被命名为.reloc,基址重定位表包含了映像中所有需要重定位的内容。它被划分成许多块,每一块表示一个4KB页面范围内的基址重定位信息,必须从32位边界开始。

    6】调试数据所处的节通常被命名为.debug,它指向IMAGE_DEBUG_DIRECTORY结构数组。其中的每个元素都描述了PE中的一些调试信息。

    7】预留,必须为0

    8Global Ptr数据描述的是被存储在全局指针寄存器中的一个值。

    9】线程本地存储数据所处的节,通常命名为.tls。线程本地存储(TLS)是Windows支持的一种特殊存储类别,其中数据对象不是栈对象,而是对应于运行相应代码的单个线程。因此,每个线程都可以为使用TLS定义的变量来维护一个不同于其他线程的值。

    当创建线程时,PE加载器通过将线程环境块(TEB)的地址放入FS寄存器来传递线程的TLS数组地址,距TEB开头0x2C的位置处有一个指针指向该TLS数组。

    10】加载配置信息用于包含保留的SEH技术。它提供了一个安全的结构化异常处理程序列表,操作系统在进行异常处理时要用到这些异常处理程序。

    11】绑定导入数据的存在主要是为了优化导入信息,提高PE的加载效率。当PE文件被加载到内存时,加载器会先检查导入表,然后把需要加载的DLL载入到地址空间中。加载器还有一向比较重要的工作是根据导入信息的描述使用动态链接库里输入函数的实际地址取替换IAT表的内容。简单来讲,绑定是指由程序员或链接器代替Windows PE加载器完成了一部分对导入表的处理工作。

    12IAT是导入地址表的英文缩写。准确的讲,它是导入表的一部分,这个双字数组里定义了所有导入函数的VA,程序可以直接通过跳转指令跳转到该VA处执行。

    13】延迟导入表也和动态链接库调用有关,这种数据的存在是为了给“应用程序直到首次调用某个DLL中的函数或数据时才加载这个DLL(即延迟加载)”这种行为提供一种统一的访问机制。

    14CLR数据所处的节通常被命名为.cormeta,该信息是.NET框架的一个重要组成部分,所有基于.NET框架开发的程序,其初始化部分都是通过访问这部分定义而实现的。PE加载时将通过该结构加载代码托管机制需要的所有动态链接库文件,并完成与CLR有关的一些基本操作。

    15】系统预留,未定义。

     

    2.3、节表项IMAGE_SECTION_HEADER

    PEIMAGE_NT_HEADERS后紧跟这节表。它由许多个节表项(IMAGE_SECTION_HEADER)组成,每个节表项记录了PE中与某个特定节有关的信息,如节的属性、节的大小、在文件和内存中的起始位置等。节表中节的数量由字段IMAGE_FILE_HEADER.NumberOfSection来定义。详细数据结构定义如下:

    IMAGE_SECTION_HEADER STRUCT{

    +0h Name1 Dbyte 8 8个字节的块名(不遵循“\0”结尾,长度截断)

    +8h union Misc

    PhysicalAddress DWORD ? ;

    VirtualSize DWORD ? ;节区尺寸(未对齐)

    Ends

    +Ch VirtualAddress DWORD ? ;节区的RVA

    +10h SizeOfRawData DWORD ? ;在文件中对齐后的尺寸

    +14h PointerToRawData DWORD ? ;在文件中的偏移

    +18h PointerToRelocations DWORD ? ;在OBJ文件中使用(指向重定位表)

    +1Ch PointerToLinenumbers DWORD ? ;行号表的位置(供调试用)

    +20h NumberOfRelocations WORD ? ;在OBJ文件中使用(重定位表个数)

    +22h NumberOfLinenmbers WORD ? ;行号表中行号的数量

    +24h Characteristics DWORD ? ;节的属性

    }IMAGE_SECTION_HEADER ENDS

     

    IMAGE_SECTION_HEADER.Characteristics数据位定义

    5  IMAGE_SCN_CNT_CODE00000020h 节中包含代码

    6  IMAGE_SCN_CNT_INITIALIZED_DATA00000040h 节中包含已初始化数据

    7  IMAGE_SCN_CNT_UNINITIALIZED_DATA00000080h节中包含未初始化数据

    8  IMAGE_SCN_LNK_OTHER00000100h 保留供将来使用

    25  IMAGE_SCN_MEM_DISCARDABLE02000000h 节中数据在进程开始后丢弃,如.reloc

    26  IMAGE_SCN_MEM_NOT_CACHED04000000h 节中数据不会经过缓存

    27  IMAGE_SCN_MEM_NOT_PAGED08000000h 节中数据不会交换到磁盘

    28  IMAGE_SCN_MEM_SHARED10000000h 表示节中数据被不同进程共享

    29  IMAGE_SCN_MEM_EXECUTE20000000h 映射到内存后的页面可执行

    30  IMAGE_SCN_MEM_READ40000000h 映射到内存后的页面可读

    31 IMAGE_SCN_MEM_WRITE80000000h 映射到内存后的页面可写

  • 相关阅读:
    request和response概念用法
    servlet知识点
    Nginx的安装和配置文件详细说明
    Tomcat优化
    Tomcat安装和常见问题
    WEB服务器和tomcat介绍
    WEB技术相关入门知识点
    前期绑定和后期绑定
    1-4选择题
    1-3选择题
  • 原文地址:https://www.cnblogs.com/xingzherufeng/p/8330782.html
Copyright © 2011-2022 走看看