首页
论坛
课程
招聘
[原创]国庆PE总复习(1-7)合集
2010-10-1 17:44 137818

[原创]国庆PE总复习(1-7)合集

2010-10-1 17:44
137818
本来打算国庆去上海观看世博会最后精彩去的,因为有些事要做,所以就没去了,还是在家呆着吧,给大家重温一下PE文件的知识,国庆七天每天都会更新,我会从最基础的PE理论再结合编程给大家详细讲解,参考书籍主要两本:看雪《加密与解密》第三版,老罗《Windows下32位汇编程序设计》第二版,如果大家看了我的PE文件详解能给大家一点点小的帮助,我想国庆的努力也没白费,呵呵~~其实论坛上,这样的文章也讲了不少,这里给大家复习复习,熟能生巧嘛!!如果您对PE文件已经很熟悉了,就可以直接跳过了总复习一,后面的会更加精彩,呵呵,免得浪费你保贵的时间~~
      PE格式是Windows下最常用的可执行文件格式,在DOS时代COM文件是最早的也是结构最简单的可执行文件,COM文件中仅包含可执行代码,没有附带任何“支持性”数据,所以,第一句执行指令必须安排在文件头部:再就是没有重定位的信息,这样代码中不能有跨段操作数据的指令,造成代码和数据,甚至包括堆栈只能限制在同一个64KB的段中,由于这个原因,DOS系统中又定义了一种可执行文件---EXE文件,EXE文件在代码的前面加了一个文件头,文件头中包括各种说明数据,如文件入口,堆栈位置,重定位表等,操作系统根据文件头的信息将代码部分装入内存,根据重定位表修正代码,最后在设置好堆栈后从文件头中指定的入口开始执行。
    当Windows3.X出现的时候,可执行文件中出现了32位代码,程序运行时转到保护模式之前需要在实模式下做一些初始化,这样实模式的16位代码必须和32位代码一起放在可执行文件中,旧的DOS可执行文件格式无法满足需要,所以Windows3.X执行文件使用新的LE格式的可执行文件(Linear executable/线性可执行文件),Window9x中的VxD程序也是使用LE格式,因为这些驱动程序中也同时包括16位和32位代码。
    而在Windows 9x,Windows NT,Windows 2000下,纯32位可执行文件都使用微软设计的一种新格式——PE格式(Portable Executable File Format/可移值的执行体)。
        在学习的同时建议用Stud_PE工具配合!!!
PE文件的框架结构如下图所示:
       
PE基本概念:
        PE文件使用的是一个平面地址空间,所有代码和数据都被合并在一起,组成一个很大的结构。文件的内容被分割为不同的区块(Section),区块中包含代码或数据,各个区块按页边界来对齐,区块没有大小限制,是一个连续结构,每个块都有它自己在内存中的一套属性,比如:这个块是否包含代码,是否只读或可读/写等。
        认识PE文件不是作为单一内存映射文件被装入内存。Windows加载器(又称PE装载器)遍历PE文件并决定文件的哪一部分被映射,这种映射方式是将文件较高的偏移位置映射到较高的内存地址中。当磁盘文件一旦被装入内存中,磁盘上的数据结构布局和内存中的数据结构布局是一致的。这样如果在磁盘的数据结构中寻找一些内容,那么几乎都能在被装入到内存中找到相同的信息。但数据之间的相对位置可能改变,其某项的偏移地址可能区别于原始的偏移地置,不管怎样,所有表现出来的信息都允许从磁盘文件偏移到内存偏移的转换,如图所示:
       
从图中可以看出映射方式为:低位到低位,高位到高位的方式
这里一定要强调一点:RVA(虚拟地址),FileOffset(文件偏移地址),VA(虚拟地址),
ImageBase(基地址),入口点(Entry Point)
RVA(Relative Virtual Address)的缩写,相对虚拟地址,这是一个“相对”地址,PE文件的各种数据结构中涉及地址的字段大部分都是以RVA表示的。
FileOffset(文件偏移地址)
FileOffset是当PE文件储存在磁盘上时,各数据的地址称做为文件偏移地址(File Offset)。文件偏移地址从PE文件的第一个字节开始计数,起始值为0
VA(虚拟地址)
VA程序访问存储器所使用的逻辑地址称为虚拟地址(Virtual Address),又称为内存偏移地址
ImageBase(基地址)
ImageBase文件执行时将被映射到指定内存地址中,这个初始内存地址称为基地址(ImageBase)。这个值是由PE文件决定的,按照默认设置。Visual C++建立的EXE文件的基地址为00400000h,DLL文件基地址是10000000h,但可以改变这个地址,有很多种方法,以后在慢慢给大家介绍,书上说的是在链接程序的/BASE选项中改为你的程序的入口函数名称就行了。
Entry Point(入口点)
Entry Point是指PE文件执行时的入口点(Entry Point)。也就是说,程序在执行时的第一行代码的地址应该就是这个值。大家用LoadPE工具就可以查看PE文件的区段表!
汇编中虚拟地址(VRA)与文件偏移地址(FileOffset)的相互转换:
+---------+---------+---------+---------+---------+---------+---------+--------------------+
| 段名称    虚拟地址   虚拟大小   物理地址   物理大小    标志   
|+---------+---------+---------+---------+---------+---------+----------+--------------------+
|   Name      VOffset     VSize     ROffset     RSize       Flags
|+---------+---------+---------+---------+---------+---------+-----------+--------------------+
||   .text    00001000    00000092   00000400   00000200   60000020
||  .rdata   00002000    000000F6   00000600   00000200   40000040
||   .data    00003000    0000018E   00000800   00000200   C0000040
||   .rsrc    00004000    000003A0   00000A00   00000400   C0000040
|+---------+---------+---------+---------+---------+---------+-----------+----------------------+
将RVA转换为File Offset,给大家一个非常经典的公式:
设:VK为相对虚拟地址RVA与文件偏移地址File Offset的差值
VA=ImageBase+RVA
File Offset = RVA ---VK
File Offset = VA---ImageBase---VK
大家如果不想自己去算,也可以用LoadPE中的转换工具来计算,如图所示:
       
好了,上面基本上把基础知识介绍了一遍,下面我们要着手去研究PE的内部各个部分结构。
DOS文件头和DOS块
        PE文件中还包括一个标准的DOS可执行文件部分,如图17.1中左边的①所示,这看上去有些奇怪,但是这对于可执行文件的向下兼容性来说却是不可缺少的。
        但是这种方法也存在一个问题,假如一个PE格式的可执行文件在Windows中执行,那没有任何异常,因为Windows能够识别PE文件头并正确装入,但如果将PE文件放入DOS执行,那么DOS系统肯定无法识别PE文件头,假如PE文件的头部不包括一个DOS部分的话,那么按照前面介绍的规则,PE文件头的数据会被DOS系统作为代码装入并执行,这种操作几乎可以肯定会让系统立刻挂起。
        为了避免这种情况,PE文件的头部包括了一个标准的DOS MZ格式的可执行部分,这样万一在DOS下执行一个PE文件,系统可以将文件解释为DOS下的.exe可执行格式,并执行DOS部分的代码。
        一般来说,DOS部分的执行代码只是简单地显示一个“This program cannot be run in DOS mode.”就退出了,这段简单的代码是编译器自动生成的。
        PE文件中的DOS部分由MZ格式的文件头和可执行代码部分组成,可执行代码被称为“DOS块”(DOS stub)。MZ格式的文件头由IMAGE_DOS_HEADER结构定义:
        IMAGE_DOS_HEADER STRUCT
         e_magic WORD ? ;DOS可执行文件标记,为“MZ”
         e_cblp WORD ?
         e_cp WORD ?
         e_crlc WORD ?
         e_cparhdr WORD ?
         e_minalloc WORD ?
         e_maxalloc WORD ?
         e_ss WORD ? ;DOS代码的初始化堆栈段
         e_sp WORD ? ;DOS代码的初始化堆栈指针
         e_csum WORD ?
         e_ip WORD ? ;DOS代码的入口IP
         e_cs WORD ? ;DOS代码的入口CS
         e_lfarlc WORD ?
         e_ovno WORD ?
         e_res WORD  4 dup(?)
         e_oemid WORD ?
         e_oeminfo WORD ?
         e_res2 WORD 10 dup(?)
         e_lfanew DWORD ? ;指向PE文件头
        IMAGE_DOS_HEADER ENDS
DOS文件头的前面部分并不陌生,第一个字段e_magic被定义成字符“MZ”(在Windows.inc文件中已经预定义为IMAGE_DOS_SIGNATURE)作为识别标志,后面的一些字段指明了入口地址、堆栈位置和重定位表位置等。
        其中我们还要关心的是e_lfanew这个字段,e_lfanew字段是真正PE文件头的相对偏移(RVA),其指出真正PE头的文件偏移位置,它占用四个字节,位于文件开始偏移3Ch字节中。
        分析如图所示:
       
从图中我们可以看到e_lfanew的值为000000c0,也就是说000000c0处是我们的PE文件头的位置。
PE文件头
紧跟在DOS stub的是PE文件头,从DOS文件头的e_lfanew字段(文件偏移003ch)得到真正PE文件头位置后,现在来看看它的定义,PE文件头是由IMAGE_NT_HEADERS结构定义的:
IMAGE_NT_HEADERS STRUCT
Signature DWORD ? ;PE文件标识
FileHeader    IMAGE_FILE_HEADER    <>
OptionalHeader   IMAGE_OPTIONAL_HEADER32 <>
IMAGE_NT_HEADERS ENDS
PE文件头的第一个双字是一个标志,它被定义为00004550h,也就是字符“P”,“E”加上两个0,这也是“PE”这个称呼的由来,大部分的文件属性由标志后面的IMAGE_FILE_HEADER和IMAGE_OPTIONAL_HEADER32结构来定义,从名称看,似乎后面的这个PE文件表头结构是可选的(Optional),但实际上这个名称是名不符实的,因为它总是存在于每个PE文件中。
Signature字段:
在一个有效的PE文件里,Signature字段被设置为00004550h。ASCII码字符是"PE00",
#define IMAGE_NT_SIGNATURE定义了这个信息。
#define IMAGE_NT_SIGNATURE   0x00004550
"PE\0\0"字段是PE文件头的开始,DOS头部的e_lfanew字段正是指向"PE\0\0"。

IMAGE_FILE_HEADER结构
IMAGE_FILE_HEADER STRUCT
Machine WORD ? ;0004h - 运行平台
NumberOfSections WORD ? ;0006h - 文件的节数目
TimeDateStamp DWORD ? ;0008h - 文件创建日期和时间
PointerToSymbolTable DWORD ? ;000ch - 指向符号表(用于调试)
NumberOfSymbols DWORD ? ;0010h - 符号表中的符号数量(用于调试)
SizeOfOptionalHeader WORD ? ;0014h - IMAGE_OPTIONAL_HEADER32结构的长度
Characteristics WORD ? ;0016h - 文件属性
IMAGE_FILE_HEADER ENDS
Machine字段
用来指定文件的运行平台,常见的定义值见表17.1所示。Windows可以运行在Intel和SUN等几种不同的硬件平台上,不同平台指令的机器码是不同的,为不同平台编译的可执行文件显然无法通用。如果Windows检测到这个字段指定的适用平台与当前的硬件平台不兼容,它将拒绝装入这个文件。
NumberOfSections字段
指出文件中存在的节的数量(如图17.1中的④所示),同样,节表的数量(如图17.1中的③所示)也等于节的数量。
TimeDateStamp字段
编译器创建此文件的时间,它的数值是从1969年12月31日下午4:00开始到创建时间为止的总秒数。
PointerToSymbolTable和NumberOfSymbols字段
这两个字段并不重要,它们与调试用的符号表有关。
SizeOfOptionalHeader字段
紧接在当前结构下面的IMAGE_OPTIONAL_HEADER32结构的长度,这个值等于00e0h。
Characteristics字段
属性标志字段,它的不同数据位定义了不同的文件属性,具体内容如表17.2所示,这是一个很重要的字段,不同的定义将影响系统对文件的装入方式,比如,当位13为1时,表示这是一个DLL文件,那么系统将使用调用DLL入口函数的方式调用文件入口,否则的话,表示这是一个普通的可执行文件,系统直接跳到入口处执行。对于普通的可执行PE文件,这个字段的值一般是010fh,而对于DLL文件来说,这个字段的值一般是210eh。
如图所示:

IMAGE_OPTIONAL_HEADER32结构
定义IMAGE_OPTIONAL_HEADER32结构的本意在于让不同的开发者能够在PE文件头中使用自定义的数据,这就是结构名称中“Optional”一词的由来,但实际上IMAGE_FILE_HEADER结构不足以用来定义PE文件的属性,反而在这个“可选”的部分中有着更多的定义数据,对于读者来说,可以完全不必考虑这两个结构的区别在哪里,只要把它们当成是连在一起的“PE文件头结构”就可以了。
IMAGE_OPTIONAL_HEADER32 STRUCT
Magic WORD ? ;0018h 107h=ROM Image,10Bh=exe Image
MajorLinkerVersion BYTE ? ;001ah 链接器版本号
MinorLinkerVersion BYTE ? ;001bh
SizeOfCode DWORD ? ;001ch 所有含代码的节的总大小
SizeOfInitializedData DWORD? ;0020h所有含已初始化数据的节的总大小
SizeOfUninitializedData DWORD ? ;0024h 所有含未初始化数据的节的大小
AddressOfEntryPoint DWORD ? ;0028h 程序执行入口RVA
BaseOfCode DWORD ? ;002ch 代码的节的起始RVA
BaseOfData DWORD ? ;0030h 数据的节的起始RVA
ImageBase DWORD ? ;0034h 程序的建议装载地址
SectionAlignment DWORD ? ;0038h 内存中的节的对齐粒度
FileAlignment DWORD ? ;003ch 文件中的节的对齐粒度
MajorOperatingSystemVersion WORD ? ;0040h 操作系统主版本号
MinorOperatingSystemVersion WORD ? ;0042h 操作系统副版本号
MajorImageVersion WORD ? ;0044h可运行于操作系统的最小版本号
MinorImageVersion WORD ? ;0046h
MajorSubsystemVersion WORD ?;0048h 可运行于操作系统的最小子版本号
MinorSubsystemVersion WORD ? ;004ah
Win32VersionValue DWORD ? ;004ch 未用
SizeOfImage DWORD ? ;0050h 内存中整个PE映像尺寸
SizeOfHeaders DWORD ? ;0054h 所有头+节表的大小
CheckSum DWORD ? ;0058h
Subsystem WORD ? ;005ch 文件的子系统
DllCharacteristics WORD ? ;005eh
SizeOfStackReserve DWORD ? ;0060h 初始化时的堆栈大小
SizeOfStackCommit DWORD ? ;0064h 初始化时实际提交的堆栈大小
SizeOfHeapReserve DWORD ? ;0068h 初始化时保留的堆大小
SizeOfHeapCommit DWORD ? ;006ch 初始化时实际提交的堆大小
LoaderFlags DWORD ? ;0070h 未用
NumberOfRvaAndSizes DWORD ? ;0074h 下面的数据目录结构的数量
DataDirectory    IMAGE_DATA_DIRECTORY 16 dup(<>) ;0078h
IMAGE_OPTIONAL_HEADER32 ENDS
这个结构的字段比较多,我就不一一介绍了,具体请参考《加密与解密》第三版第十章PE文件格式,下面介绍几个比较重要的:
AddressOfEntryPoint字段

指出文件被执行时的入口地址,这是一个RVA地址(RVA的含义在下一节中详细介绍)。如果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要将这个入口地址指向附加的代码就可以了。

ImageBase字段
指出文件的优先装入地址。也就是说当文件被执行时,如果可能的话,Windows优先将文件装入到由ImageBase字段指定的地址中,只有指定的地址已经被其他模块使用时,文件才被装入到其他地址中。链接器产生可执行文件的时候对应这个地址来生成机器码,所以当文件被装入这个地址时不需要进行重定位操作,装入的速度最快,如果文件被装载到其他地址的话,将不得不进行重定位操作,这样就要慢一点。
对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被其他模块占据,所以EXE总是能够按照这个地址装入,这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被其他的DLL使用,所以DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的IMAGE_FILE_HEADER 结构的Characteristics字段中,DLL文件对应的IMAGE_FILE_RELOCS_STRIPPED位总是为0,而EXE文件的这个标志位总是为1。
在链接的时候,可以通过对link.exe指定/base:address选项来自定义优先装入地址,如果不指定这个选项的话,一般EXE文件的默认优先装入地址被定为00400000h,而DLL文件的默认优先装入地址被定为10000000h。

SectionAlignment字段和FileAlignment字段
SectionAlignment字段指定了节被装入内存后的对齐单位。也就是说,每个节被装入的地址必定是本字段指定数值的整数倍。而FileAlignment字段指定了节存储在磁盘文件中时的对齐单位。

DataDirectory字段
这个字段可以说是最重要的字段之一,它由16个相同的IMAGE_DATA_DIRECTORY结构组成,虽然PE文件中的数据是按照装入内存后的页属性归类而被放在不同的节中的,但是这些处于各个节中的数据按照用途可以被分为导出表、导入表、资源、重定位表等数据块,这16个IMAGE_DATA_DIRECTORY结构就是用来定义多种不同用途的数据块的(如表17.4所示)。IMAGE_DATA_DIRECTORY结构的定义很简单,它仅仅指出了某种数据块的位置和长度。
IMAGE_DATA_DIRECTORY STRUCT
VirtualAddress DWORD ? ;数据的起始RVA
Size DWORD ? ;数据块的长度
IMAGE_DATA_DIRECTORY ENDS
一共有十六个IMAGE_DATA_DIRECTORYENDS结构
IMAGE_DIRECTORY_ENTRY_EXPORT        导出表
IMAGE_DIRECTORY_ENTRY_IMPORT        导入表
IMAGE_DIRECTORY_ENTRY_RESOURCE     资源
IMAGE_DIRECTORY_ENTRY_EXCEPTION     异常(具体资料不详)
IMAGE_DIRECTORY_ENTRY_SECURITY      安全(具体资料不详)
IMAGE_DIRECTORY_ENTRY_BASERELOC    重定位表
IMAGE_DIRECTORY_ENTRY_DEBUG         调试信息
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 版权信息
IMAGE_DIRECTORY_ENTRY_GLOBALPTR    具体资料不详
IMAGE_DIRECTORY_ENTRY_TLS            Thread Local Storage
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG  具体资料不详
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 具体资料不详
IMAGE_DIRECTORY_ENTRY_IAT              导入函数地址表
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT  具体资料不详
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 具体资料不详
未使用保留
PE文件中定位输出表,输入表和资源等重要数据时,就是从IMAGE_DATA_DIRECTORY结构开始的。
如图所示:

由于数据结构太多,这里只给出部分,具体请参考《加密与解密》第三版

如图七所示,数据目录表位于138---1b07h之间,每个成员占8个字节,分别指向相关的结构,前面四个字节代表VirtualAddress(数据块的起始RVA),后面四个字节代表Size(数据块的长度),比如上面程序中没有输出表,有输入表,且在00000140h处,我们就可以得出此程序的输入表RVA为00002090,Size为0000003Ch
上面得到的结果和用LoadPE查看的结果一样,呵呵,如图所示:

上面将PE文件结构的文件头介绍了一下,下面我们开始重点讲解PE中的各个区块
区块表
紧跟在IMAGE_NT_HEADERS后面的是区块表,它是一个IMAGE_SECTION_HEADER结构数组。每个IMAGE_SECTION_HEADER结构包含了它所关联区块的信息,如位置,长度,属性;该数组的数目由IMAGE_NT_HEADERS.FileHeader.NumberOfSections指出。
IMAGE_SECTION_HEADERS结构
IMAGE_SECTION_HEADER STRUCT
  Name1 db IMAGE_SIZEOF_SHORT_NAME dup(?) ;8个字节的节区名称
  union Misc
  PhysicalAddress dd ?
  VirtualSize dd ? ;节区的尺寸
  ends
  VirtualAddress dd ? ;节区的RVA地址
  SizeOfRawData dd ? ;在文件中对齐后的尺寸
  PointerToRawData dd ? ;在文件中的偏移
  PointerToRelocations dd ? ;在OBJ文件中使用
  PointerToLinenumbers dd ? ;行号表的位置(供调试用)
  NumberOfRelocations dw ? ;在OBJ文件中使用
  NumberOfLinenumbers dw ? ;行号表中行号的数量
  Characteristics dd ? ;节的属性
IMAGE_SECTION_HEADER ENDS
Name1字段
这个字段的字段名原来应该是“Name”,但是这个名称和MASM中的关键字冲突,所以在定义的时候改为“Name1”,Name1字段定义了节的名称,字段的长度为8个字节。
PE文件中的节的名称是一个由ANSI字符组成的字符串,但并没有规定以0结束,如果节的名称字符串长度小于8个字节的话,后面以0补齐,但是字符串长度达到8个字节的话,后面就没有0字符了,所以在处理的时候要注意字符串的结束方式。
每个节的名称是惟一的,不能有同名的两个节,但是节的名称不代表任何含义,它仅仅是为了查看方便而设置的一个标记而已,可以选择任何名称甚至将它空着也可以,将包含代码的节命名为“DATA”或者将包含数据的节命名为“CODE”都是合法的。
各种编译器都以自己的方式对节进行命名,所以,在PE文件中可以看到各式各样的节名称,比如,在MASM32产生的可执行文件中,代码节被命名为“.text”;可读写的数据节被命名为“.data”;包含只读数据、导入表以及导出表的节被命名为“.rdata”;而资源节被命名为“.rsrc”等。但是在其他一些编译器中,导入表被单独放在“.idata”中;而代码节可能被命名为“.code”。
当从PE文件中读取需要的节时,不能以节的名称作为定位标准,正确的方法是按照IMAGE_OPTIONAL_HEADER32结构中的数据目录字段定位。

VirtualSize字段
代表节的大小,这是节的数据在没有进行对齐处理前的实际大小。

VirtualAddress字段
指出节被装载到内存中后的偏移地址,这是一个RVA地址。这个地址是按照内存页对齐的,它的数值总是SectionAlignment的值的整数倍。

PointerToRawData字段
指出节在磁盘文件中的所处的位置。这个数值是从文件头开始算起的偏移量。

SizeOfRawData字段
指出节在磁盘文件中所占的空间大小,这个数值等于VirtualSize字段的值按照FileAlignment的值对齐以后的大小。
依靠这4个字段的值,装载器就可以从PE文件中找出某个节(从PointerToRawData偏移开始的SizeOfRawData字节)的数据,并将它映射到内存中去(映射到从模块基地址开始偏移VirtualAddress的地方,并占用以VirtualSize的值按照页的尺寸对齐后的空间大小)。

Characteristics字段
这是节的属性标志字段,其中的不同数据位代表了不同的属性,具体的定义如表17.5所示,这些数据位组合起来描述了节的属性。
结合上面的讲解,对照下面的图示,看就会很清楚,如图所示:

下面给大家讲几个学习区块的重要概念:
区块对齐值:
区块的大小是要对齐的,有两种对齐值,一种用于磁盘文件内,另一种用于内存文件中。PE文件头指出了这两个值,它们可以不同。
PE文件头里FileAlignment定义了磁盘区块的对齐值,每一个区块从对齐值的倍数的偏移位置开始。而区块的实际代码或数据的大小不一定刚好就是这么多,所以不足的地方一般以00h来填充,这就是区块间的间隙。
PE文件头里SectionAlignment定义了内存中区块的对齐值。PE文件被映射到内存中,区块总是至少从一个页边界开始,也就是说,当一个PE文件映射到内存中,每个区块的第一个字节对应于某个内存页。

内存页的属性:
对于磁盘映射文件来说,所有的页都是按照磁盘映射文件函数指定的属性设置的,但是装载可执行文件时,与节对应的内存页的属性要按照节的属性来设置。所以在同一属性模块的内存页中,从不同映射过来的内存页的属性是不同的。

节的偏移地址:
节的起始地址在磁盘文件中要按照IMAGE_OPTIONAL_HEADER32结构的FileAlignment字段的值对齐,而被装载到内存中时是按照同一结构中的SectionAlignment字段的值来对齐,两者的值可能不同,所以一个节被装入内存后相对于文件头的偏移和在磁盘文件中的偏移量可能是不同的。

节的尺寸:
对节的尺寸的处理有两个方面:首先是由于磁盘映像和内存映像中节对齐单位不同而造成的长度扩展;其次是对包含未初始化数据的节的处理。
对于未初始化数据来说,没必要为它们在磁盘文件中预留空间,只要在可执行文件被装载到内存中后为它们分配空间就可以了,所以包含未初始化数据的节在磁盘文件中的长度被定义为0,但是装载到内存中的地址和大小是被明确指定的。

不进行映射的节:
有些节中包含的数据仅仅在装载的时候用到,当文件装载完毕时候,它们不会被递交到物理内存页。
今天就讲到这里吧,也有点累了,写文章很累的,用了三个多小时,不过没事,只要能给大家一点帮助,付出的就是值得的,明天咱们继续学习输入表,输出表,基址重定位,资源,我打算在两天之类,把PE资源的理论介绍完毕,后面几天我重点讲解PE方面的编程知识,这样理论与实践相结合,学起来会更清楚明白一点!!

格式经较乱,大家将就的看吧~~~不好意思了

[招聘] 欢迎你加入看雪团队!

上传的附件:
收藏
点赞1
打赏
分享
最新回复 (147)
雪    币: 142
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
xiejienet 活跃值 2010-10-1 18:42
2
0
不错,sf学习
雪    币: 200
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hsyxh 活跃值 2010-10-1 19:33
3
0
菜鸟学习了,谢谢楼主。
雪    币: 71
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mumaren 活跃值 2010-10-1 20:31
4
0
好东西,希望继续

3q
雪    币: 233
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
BeMaverick 活跃值 2010-10-1 21:38
5
0
非常的好啊 学习 谢谢  期待新作!!
雪    币: 331
活跃值: 活跃值 (43)
能力值: ( LV12,RANK:310 )
在线值:
发帖
回帖
粉丝
evilkis 活跃值 7 2010-10-1 21:40
6
0
正正 你的图片是用什么工具修改的
雪    币: 1776
活跃值: 活跃值 (194)
能力值: ( LV12,RANK:480 )
在线值:
发帖
回帖
粉丝
熊猫正正 活跃值 9 2010-10-1 23:19
7
0
图片就在网上下的,我觉得挺有意思的就下载下来了,正好,呵呵~~到网上多淘淘,总能找到自己喜欢的
雪    币: 1776
活跃值: 活跃值 (194)
能力值: ( LV12,RANK:480 )
在线值:
发帖
回帖
粉丝
熊猫正正 活跃值 9 2010-10-2 00:32
8
0
哦,不好意思,楼上的我理解错误了,我还以为你说的我看雪图象呢?呵呵,我文中的图片是用红晴蜓改的,很简单实用的!!!
雪    币: 41
活跃值: 活跃值 (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
sdnyzjzx 活跃值 2010-10-2 08:32
9
0
谢谢楼主耐心细致的讲解,楼主讲完这些后,我要把它整理一块,作为资料保存下来,以方便经常学习。
雪    币: 6437
活跃值: 活跃值 (195)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
傷遺忘 活跃值 2010-10-2 08:43
10
0
学习了..谢谢楼主分享.
雪    币: 331
活跃值: 活跃值 (43)
能力值: ( LV12,RANK:310 )
在线值:
发帖
回帖
粉丝
evilkis 活跃值 7 2010-10-2 10:20
11
0
谢谢正正
雪    币: 1115
活跃值: 活跃值 (15)
能力值: ( LV12,RANK:230 )
在线值:
发帖
回帖
粉丝
pencil 活跃值 5 2010-10-2 15:25
12
0
收藏了,谢谢正正
雪    币: 270
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
小天狼星 活跃值 2010-10-2 16:26
13
0
正正弟弟,,,
雪    币: 469
活跃值: 活跃值 (10)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
xianboabcd 活跃值 2010-10-2 20:29
14
0
跟着楼主一起复习!
雪    币: 41
活跃值: 活跃值 (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
sdnyzjzx 活跃值 2010-10-2 21:30
15
0
尽管第一部分还没看明白,期待楼主继续。。。
雪    币: 1776
活跃值: 活跃值 (194)
能力值: ( LV12,RANK:480 )
在线值:
发帖
回帖
粉丝
熊猫正正 活跃值 9 2010-10-3 05:00
16
0
上一篇文章主要给大家介绍了一下PE文件的各个结构的数据结构,以及了解PE结构中的几个重要概念,下面我重点给大家讲解区块表中的四个非常重要的区块:输入表,输出表,重定位表,资源,我会一个一个详解给大家讲解的,请跟随我的进度慢慢消化学习!!如果你对PE中的这四个区块都很了解,请跳过PE总复习二,谢谢~~以免浪费您保贵的时间!

输入表
可执行文件使用来自于其他DLL的代码或数据时,称为输入。当PE文件装载时,Windows加载器的工作之一就是定位所有被输入函数和数据,并且让正在被装载入的文件可以使用那些地址。这个过程通过PE文件的输入表(Import Table)来完成的,输入表中保存的是函数名和其驻留的DLL名等动态链接所需要的信息,输入表在软件外壳技术上的地位非常重要,我这里会重点讲解的!!!!

当应用程序调用一个DLL的代码和数据时,那它正在隐含链接到DLL,这个过程完全由Windows加载器完成,另外一种是运行期的显示链接,这意味着必须确定目标DLL已经被加载,然后寻找API地址,这几乎总是通过调用LoadLibrary和GetProcAddress来完成的。

当隐含地链接一个API 时,类似LoadLibrary和GetProcAddress的代码始终在执行,只不过这是Windows装载器自动完成的。装载器还保证PE文件所需要的任何附加的DLL都已载入。

在PE文件内,有一组数据结构,它们分别对应着每个被输入的DLL。每一个这样结构都给出了被输入的DLL的名称并指向一组函数指针。这组函数指针被称为输入地址表(Import Address Table)简称IAT,每一个被引入的API在IAT里都有它自己保留的位置,在那里它将被Windows加载器写入输入函数的地址,最后一点是特别重要的:一旦模块被装入,IAT中包含所要调用输入函数的地址。

把所有输入函数放在IAT中同一个地方是很有意义的,这样无论代码中多少次调用一个输入函数,都会通过IAT中的同一个函数指针来完成。

调用输入表函数方法:
高效:CALL DWORD PTR [00402010]
直接调用[00402010]中的函数,地址00402010h位于IAT里
低效:CALL 00401164
........................................
:00401164
        jmp dword ptr [00402010]
这种情况,CALL把控制权转到一个子程序,子程序中的JMP指令跳转到位于IAT中的00402010h。简单的说它使用5个字节的额外代码,并且由额外的JMP将花费更多的时间去执行。
为什么要使用这种低效的方法?因为编译器无法区别输入函数的调用与普通函数调用,对于每一个函数调用,编译器使用同样形式的CALL指令:CALL XXXXXXXX
XXXXXXXX是一个由链接器填充的实际的地址。注意指令不是从函数指针而是代码中实际地址而来的,为了因果平衡,链接器必须表示产生一块代码来取代XXXXXXXX,简单位的方法是像上面那样调用一个JMP Stub。
我们可以通过使用修饰来优化我们的低效调用方式,可以用修饰函数的_declspec(dllimport)来告诉编译器,这个函数来自另一个DLL中,这样编译器就会产生这样的指令:
CALL  DWORD PTR [XXXXXXXX]
而不是CALL XXXXXXXX,编译器将给函数加上_imp_前缀,然后直接送给链接器,这样可以直接把_imp_xxx送到IAT,就不需要JMP Stub了。
下面简单分析一个实例,看看是怎么回事?
程序被执行的时候是怎样使用导入函数的呢?先写个简单的Hello World程序反汇编一把,看看调用导入函数的指令都是什么样子的,需要反汇编的两句源代码如下(呵呵,这个代码我就不写了):
  invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
  invoke ExitProcess,NULL
当使用W32Dasm反汇编以后,这两句代码变成了以下的指令,如图所示:

反汇编后,对MessageBox和ExitProcess函数的调用变成了对0040101A和00401020地址的调用,但是这两个地址显然是位于程序自身模块而不是在DLL模块中的,实际上,这是由编译器在程序所有代码的后面自动加上的Jmp dword ptr [xxxxxxxx]类型的指令,这个指令是一个间接寻址的跳转指令,xxxxxxxx地址中存放的才是真正的导入函数的地址。在这个例子中,00402000地址处存放的就是ExitProcess函数的地址。
那么在没有装载到内存之前,PE文件中的00402000地址处的内容是什么呢?
用PEID工具查得结果如图所示


由于镜像基址为00400000h,所以00402000h地址实际上处于RVA为2000h的地方,再看看各个节的虚拟地址,可以发现2000h开始的地方位于.rdata节内,而这个节的Raw_偏移项目为600h,也就是说00402000h地址内容实际上对应PE文件偏移600h处的数据。
我们就看看文件0600h处的内容是什么?用UE打开程序,如图所示:

查看的结果是00002076h,这显然不会是内存中的ExitProcess函数的地址,慢着!将它作为RVA看会怎么样呢?查看节表可以发现RVA地址00002076h也处于.rdata节内,减去节的起始地址00002000h后得到这个RVA相对于节首的偏移是76h,也就是说它对应文件0676h开始的地方,接下来可以惊奇地发现,0676h再过去两个字节的内容正是函数名字符串“ExitProcess”!

这都有点搞糊涂了,Call ExitProcess指令被编译成了Call aaaaaaaa类型的指令,而aaaaaaaa处的指令是Jmp dword ptr [xxxxxxxx],而xxxxxxxx地址的地方只是一个似乎是指向函数名字符串的RVA地址,这一系列的指令显然是无法正确执行的!

但如果告诉你,当PE文件被装载的时候,Windows装载器会根据xxxxxxxx处的RVA得到函数名,再根据函数名在内存中找到函数地址,并且用函数地址将xxxxxxxx处的内容替换成真正的函数地址,那么所有的疑惑就迎刃而解了。
呵呵,这样讲解之后,你对输入表是否有的更新的认识呢?
怎样获取输入表呢?
导入表的位置和大小可以从PE文件头中IMAGE_OPTIONAL_HEADER32结构的数据目录字段中获取,对应的项目是DataDirectory字段的第2个IMAGE_DATA_DIRECTORY结构
从IMAGE_DATA_DIRECTORY结构的VirtualAddress字段得到的是导入表的RVA值,如果在内存中查找导入表,那么将RVA值加上PE文件装入的基址就是实际的地址;如果在PE文件中查找导入表,需要将RVA转换成File Offset。

导入表由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成,结构的数量取决于程序要使用的DLL文件的数量,每个结构对应一个DLL文件,例如,如果一个PE文件从10个不同的DLL文件中引入了函数,那么就存在10个IMAGE_IMPORT_DESCRIPTOR结构来描述这些DLL文件,在所有这些结构的最后,由一个内容全为0的IMAGE_IMPORT_DESCRIPTOR结构作为结束。
每个被PE文件隐式地链接进来的DLL都有一个IID。在这个数组中,没有字段指出该结构数组的项数,但它的最后一个单元是NULL,可以由此计算出该数组的项数。
IMAGE_IMPORT_DESCRIPTOR STRUCT
  union
  Characteristics dd ?
  OriginalFirstThunk dd ?
  ends
  TimeDateStamp dd ?
  ForwarderChain dd ?
  Name1 dd ?
  FirstThunk dd ?
IMAGE_IMPORT_DESCRIPTOR ENDS

结构中的Name1字段(使用Name1作为字段名同样是因为Name一词和MASM的关键字冲突)是一个RVA,它指向此结构所对应的DLL文件的名称,这个文件名是一个以NULL结尾的字符串。
OriginalFirstThunk字段和FirstThunk字段的含义现在可以看成是相同的(使用“现在”一词的含义马上会见分晓),它们都指向一个包含一系列IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构定义了一个导入函数的信息,数组的最后以一个内容为0的IMAGE_THUNK_DATA结构作为结束。

一个IMAGE_THUNK_DATA结构实际上就是一个双字,之所以把它定义成结构,是因为它在不同的时刻有不同的含义,结构的定义如下:

IMAGE_THUNK_DATA STRUCT
  union u1
  ForwarderString dd ?
  Function dd ?
  Ordinal dd ?
  AddressOfData dd ?
  ends
IMAGE_THUNK_DATA ENDS

一个IMAGE_THUNK_DATA结构如何用来指定一个导入函数呢?当双字(就是指结构!)的最高位为1时,表示函数是以序号的方式导入的,这时双字的低位就是函数的序号。读者可以用预定义值IMAGE_ORDINAL_FLAG32(或80000000h)来对最高位进行测试,当双字的最高位为0时,表示函数以字符串类型的函数名方式导入,这时双字的值是一个RVA,指向一个用来定义导入函数名称的IMAGE_IMPORT_BY_NAME结构,此结构的定义如下:

IMAGE_IMPORT_BY_NAME STRUCT
  Hint dw ?
  Name1 db ?
IMAGE_IMPORT_BY_NAME ENDS
结构中的Hint字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为0,Name1字段定义了导入函数的名称字符串,这是一个以0为结尾的字符串。
整个过程听起来很复杂,其实看下面的图示,可执行文件导入了Kernel32.dll中的ExitProcess,ReadFile,WriteFile和lstcmp函数的情况,其中,前面3个函数按照名称方式导入,最后lstrcmp函数按照序号导入,这四个函数分别是02f6h,0111h,002bh和0010h。

导入表中IMAGE_IMPORT_DESCRIPTOR结构的Name1字段指向字符串“Kernel32.dll”,表明当前要从Kernel32.dll文件中导入函数,OriginalFirstThunk和FirstThunk字段指向两个同样的IMAGE_THUNK_DATA数组,由于要导入的是4个函数,所以数组中包含4个有效项目并以最后一个内容为0的项目作为结束。

第4个函数lstrcmp函数是以序号导入的,与其对应的IMAGE_THUNK_DATA结构的最高位等于1,和函数的序号0010h组合起来的数值就是80000010h,其余的3个函数采用的是以函数名导入的方式,所以IMAGE_THUNK_DATA结构的数值是一个RVA,分别指向3个IMAGE_IMPORT_BY_NAME结构,每个IMAGE_IMPORT_BY_NAME结构的第一个字段是函数的序号,后面就是函数的字符串名称了,一切就是这么简单!

这里有个问题:为什么要用两个并行的指针数组指向IMAGE_IMPORT_BY_NAME结构呢?
答安:当PE文件被装入内存的时候,其中一个数组的值将被改作他用,还记得前面分析Hello World程序时提到的,Windows装载器会将指令JMP DWORD PTR [XXXXXXXX]指定的XXXXXXXX处的RVA替换成真正的函数地址,其实XXXXXXXX地址正是由FirstThunk字段指向的那个数组中的一员。
实际上,当PE文件被装载入内存后,内存中的映像就被Windows装载器修正成了如下图所示的样子,其中由FirstThunk字段指向的那个数组中的每个双字都被替换成了真正的函数入口地址,之所以在PE文件中使用两份IMAGE_THUNK_DATA数组的拷贝并修改其中的一份,是为了最后还可以留下一份拷贝用来反过来查询地址所对应的导入函数名。


输入表实例分析
这里我就用老罗书上的那个FirstWindows作为实例进行分析,因为看雪上那个PE.EXE被我的360删了,我也不知道,不管了,反正也要重新分析一遍,这样更加深记忆!!
数据目录表第二成员指向输入表,该指针具体位置在PE文件头的80h偏移处。该文件的PE文件头起始位置是C0h,输入表地址就是整个文件的C0h+80h=140h处,因此在140h处可以发现四字节指针90200000,倒过来是00002090,即输入表在内存中偏移量是为2090h的地方,当然,这个2090h是RVA值,需要将其转换为磁盘文件的绝对偏移量,才能够在十六进制编辑器中找到输入表。具体如下图所示:

大家可以通过计算来实现RVA到File Offset的转换,这里我了节省篇幅,我就直接用LoadPE来计算了,如图所示,我们要计算的RVA地址为00002090h,得到File Offset的地址为690h,如下图所示:


得到文件偏移地址之后,我们用UE打开FirstWindow程序,跳到偏移为690h处,这里就是输入表的内容,每个IID包含5个双字,用来描述一个引入的DLL文件,最后以NULL结束。如图所示:


将图中所列的输入表的IID数组整理到下面的表中。每个IID包含了一个DLL的描述信息,现在有两个IID,因此这里引入了两个DLL,第三个IID全为0,作为结束标志。


每个IID中的第四个字段是指向DLL名称的指针,这里第一个IID中的第四个字段是0E220000,翻转过来也就是RVA地址0000220Eh,用上面的方法转换得到File Offset为80Eh,还有下面那个IID中的第四个字段也是指向DLL名称的指,RVA地址为0000224Ch,转换得到File Offset为84Ch,这样我们就得到输入表中所使用的两个DLL的名称,所图所示:


由上图可知EXE文件中偏移量为80Eh处的字符是user32.dll,84Ch处的字符是kernel32.dll,所以此程序调用了两个DLL。
上面的表格转换成RVA地址,如下所示:

再查找USER32.dll中被调用的函数,在第一个IID中,查看第一个字段OrignalFirstThunk,它指向一个数据,这个数组的元素都是指针,分别指向引入函数名的ASCII字符串。有些程序的OriginalFirstThunk的值为0,所以这时就要看FirstThunk,它在程序运行时初始化。
这时我就分析一个IID,第二个IID留着读者自己去分析!
USER32.DLL这个IID结构中的OriginalFirstThunk的字段的值为000020DC,转化为File Offset为:6DC,所以在偏移6DCh处就是IMAGE_THUNK_DATA数组,它存储的是指向IMAGE_IMPORT_BY_NAME结构的地址,以一串00结束。
可得到如下表所示的IMAGE_THUNK_DATA的数组。

具体的位置如下图所示:

再来看看同一个IID结构中FirstThunk情况,USER32.dll所在IID的FirstThunk字段值是2010h,然后转换得到File Offset为610h,在偏移610h处就是IMAGE_THUNK_DATA数组,其数据与OrignalFirstThunk字段所指的完全一样,如下图所示:


通常一个完整的程序就这些,现在有15个IMAGE_THUNK_DATA,表示有15个函数调用,先选择一个分析一下:
4E210000翻转后为0000214E,然后转换为File Offset为74Eh,会发现在偏移74Eh处的字符串为DestroyWindow。你也许注意到了,计算出来的偏移量并不刚好指向函数名的ASCII字符串,而是前面还有两个字节的空缺,这是作为函数名(Hint)引用的,可以为0。
第一个IID指向的API函数表如下:

如上图是FirstWindow文件运行前第一个IID的结构示意图,在程序运行前,它的FirstThunk字段值是指向一个地址串,而且和OrignalFirstThunk字段值指向的INT是重复的,系统在程序初始化时根据OrignalFirstThunk的值找到函数名,调用GetProcAddress函数(或类似功能的系统代码)且根据函数名取得函数的入口地址,然后用函数入口地址取代FirstThunk指向的地址串中对应的值(IAT)。
其内部结构如下图所示,图片来源《加密与解密》第三版


下面利用《加密与解密》第三版上的实例dumped.exe讲解PE文件映射到内存的状态,找开映象文件,由于在内存中区块的对齐值与内存页相同,因此此时其文件偏移地址与相对虚拟地址(RVA)的值相等。输入表的RVA地址是2040h,具体见下图:


由于00002040处的值为8C200000,翻转为0000208C,再看208C处的IMAGE_THUNK_DATA和值为没有映射到内存中的是一样的,但是FirstThunk的值为2010h,,该处指向的输入表IAT,将这张表与没有映射之前的比较,可以发现完全不同了。
具体情况如下图所示:


内存中第一个IID结构的输入地址表(IAT)

表中各地址都是USER32.dl链连库的相关输出函数,反汇编USER32.dll,跳到77D216DDh地址处,显示代码如下:
Exproted fn():LoadIconA -Ord:01BCh
:77D216DD 8BC0 mov eax,eax
:77D216DF 55      push ebp
:77D216E0 8BEC  mov ebp,esp
:77D216E2 66F7450EFFFF test [ebp+0E],FFFF
:77D216E8 0F8529170200 jne 77D42E17
:77D216EE 5D     pop ebp
:77D216EF EBB6 jmp 77D216A7
:77D216F1 90     nop
:77D216F2 90     nop
不过我刚才用反汇编工具查的时候好像不是这个地址了,可能是版本不同吧,呵呵
原来,77D216DD指向的是USER32.dll中LoadIconA函数代码处,如下图反应了PE.EXE文件装载到内存里的结构示意图


程序装载进内存后,只与IAT交换信息,输入表的其他部分不需要了,例如:程序需要调用LoadIconA函数的指针是指向IAT的,而IAT已指向系统USER32.dll的LoadIconA函数代码里。调用LoadIconA函数的相关代码如下:
CALL 00401164
:00401164
        JMP DWORD PTR [00402010]   ;跳到77D216DD,此处是USER32.dll指向的LoadIconA
具体细节请参考《加密与解密》第三版~~

好了,今天就先讲到这里吧,不好意思,这么晚才传上来,今天出去和朋友玩了一天,到晚上十二点才回家,一直写到现在,本来想把输出表,资源,重定位也在今天讲了,可是我看了输入表内容太多,又非常重要,所以我还是把输入表作为单独来讲解,还有一个原因就可能是我太想睡觉了,晚上会花时间把后面三个部分也传上来,希望大家阅读了上面的文章会有一点小小的帮助,呵呵,晚安~~
上传的附件:
雪    币: 200
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
随风而去 活跃值 2010-10-3 06:59
17
0
楼主!答应七天的啊!不要爽约哈!!
雪    币: 1115
活跃值: 活跃值 (15)
能力值: ( LV12,RANK:230 )
在线值:
发帖
回帖
粉丝
pencil 活跃值 5 2010-10-3 07:44
18
0
真不错,国庆礼物啊,呵呵,谢谢楼主
雪    币: 564
活跃值: 活跃值 (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lixupeng 活跃值 2010-10-3 08:46
19
0
温故而知新收下了
雪    币: 7035
活跃值: 活跃值 (11766)
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
linhanshi 活跃值 2010-10-3 08:52
20
0
Thanks for share.

Программное обеспечение выпуска и Windows Crack Обучение
Нам-Dabei Guanyin Бодхисаттва Нам без митабха
雪    币: 7035
活跃值: 活跃值 (11766)
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
linhanshi 活跃值 2010-10-3 08:53
21
0
Thanks fro share.

Программное обеспечение выпуска и Windows Crack Обучение
Нам-Dabei Guanyin Бодхисаттва Нам без митабха
雪    币: 41
活跃值: 活跃值 (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
sdnyzjzx 活跃值 2010-10-3 09:30
22
0
楼主辛苦了,谢谢!
雪    币: 6437
活跃值: 活跃值 (195)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
傷遺忘 活跃值 2010-10-3 10:22
23
0
继续跟帖学习..

谢谢分享.
雪    币: 118
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
风声 活跃值 2010-10-3 10:26
24
0
Thank you,学习了,
雪    币: 207
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zlionking 活跃值 2010-10-3 14:14
25
0
3Q 再看看吧
游客
登录 | 注册 方可回帖
返回