秦万强PE文件学习笔记

秦万强PE文件学习笔记2019年8月~ 9月感谢网络上各类免费资源,由于引用资源太多,无法一一标注,故不在进行标注,LOG不在去除。如果侵权,请及时联系,我第一时间进行删除。取之免费,发之免费,故此套资料全部免费,如果侵权,请及时和我联系,必马上删除。特别感谢鱼C工作室!

一、PE文件是什么?PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,事实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是任何扩展名。那Windows是怎么区分可执行文件和非可执行文件的呢?我们调用LoadLibrary传递了一个文件名,系统是如何判断这个文件是一个合法的动态库呢?这就涉及到PE文件结构了。PE文件的结构一般来说如下图所示:从起始位置开始依次是DOS头,NT头,节表以及具体的节。

二、DOS头DOS头是用来兼容MS-DOS操作系统的,目的是当这个文件在MS-DOS上运行时提示一段文字,大部分情况下是:This program cannot be run in DOS mode.还有一个目的,就是指明NT头在文件中的位置。
typedef struct_IMAGE_DOS_  {  // DOS .EXE header// offset: 0HWORD  e_magic;                // Magic number// offset: 2HWORD   e_cblp;                 // Bytes on last page of file// offset: 4HWORD  e_cp;                   // Pages infile// offset: 6HWORD   e_crlc;                 // Relocations// offset: 8HWORD  e_cparhdr;             // Size of header in paragraphs// offset: AHWORD  e_minalloc;            // Minimumextra paragraphs needed// offset: CHWORD  e_maxalloc;            // Maximumextra paragraphs needed// offset: EHWORD  e_ss;                   // Initial(relative) SS value// offset: 10HWORD  e_sp;                   // Initial SPvalue// offset: 12HWORD   e_csum;                 // Checksum// offset: 14hWORD  e_ip;                   // Initial IPvalue// offset: 16HWORD  e_cs;                   // Initial(relative) CS value// offset: 18HWORD  e_lfarlc;              // File address of relocation table// offset: 1AHWORD  e_ovno;                // Overlaynumber// offset: 1CHWORD  e_res[4];              // Reservedwords// offset: 24HWORD  e_oemid;               // OEMidentifier (for e_oeminfo)// offset: 26HWORD  e_oeminfo;             // OEMinformation; e_oemid specific// offset: 28HWORD  e_res2[10];            // Reservedwords// offset: 3CHLONG   e_lfanew;              // File address of new exe header  }IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
批:WORD 为一个16bit 的无符号数1、需要关注的两个域:1.1、e_magic一个WORD类型,值是一个常数0x4D5A,用文本编辑器查看该值位‘MZ’,可执行文件必须都是'MZ'开头。
批:4DH 为M的ASCLL码的16进制, 5AH为Z的ASCLL码的16进制。1.2、e_lfanew为32位可执行文件扩展的域,用来表示DOS头之后的NT头相对文件起始地址的偏移。

三、NT头NT头包含windows PE文件的主要信息,其中包括一个‘PE’字样的签名,PE文件头(IMAGE_FILE_HEADER)和PE可选头(IMAGE_OPTIONAL_HEADER32)。typedef struct _IMAGE_NT_HEADERS{// offset: 0HDWORD Signature;// offset: 4HIMAGE_FILE_HEADER FileHeader;// offset: 18HIMAGE_OPTIONAL_HEADER32 OptionalHeader;} IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;1、需要关注的域1.1、Signature类似于DOS头中的e_magic,其高16位是0,低16是0x4550,用字符表示是’PE’。
批:45H 为P的ASCLL码的16进制, 50H为E的ASCLL码16进制。1.2、IMAGE_FILE_HEADERIMAGE_FILE_HEADER是PE文件头,C语言的定义是这样的:
typedefstruct _IMAGE_FILE_HEADER {+4H  WORD    Machine;+6H  WORD    NumberOfSections;+8H  DWORD   TimeDateStamp;+CH  DWORD   PointerToSymbolTable;+10HDWORD  NumberOfSymbols;+14HWORD   SizeOfOptionalHeader;+16HWORD   Characteristics;}IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

每个域的具体含义:Machine
该文件的运行平台,是x86、x64还是I64等等,可以是下面值里的某一个。#defineIMAGE_FILE_MACHINE_UNKNOWN      0#defineIMAGE_FILE_MACHINE_I386         0x014c  // Intel 386.#defineIMAGE_FILE_MACHINE_R3000        0x0162  // MIPS little-endian, 0x160big-endian#defineIMAGE_FILE_MACHINE_R4000        0x0166  // MIPS little-endian#defineIMAGE_FILE_MACHINE_R10000       0x0168  // MIPS little-endian#defineIMAGE_FILE_MACHINE_WCEMIPSV2   0x0169  // MIPS little-endian WCE v2#defineIMAGE_FILE_MACHINE_ALPHA         0x0184  // Alpha_AXP#defineIMAGE_FILE_MACHINE_SH3          0x01a2  // SH3 little-endian#defineIMAGE_FILE_MACHINE_SH3DSP       0x01a3#defineIMAGE_FILE_MACHINE_SH3E        0x01a4  // SH3E little-endian#defineIMAGE_FILE_MACHINE_SH4         0x01a6  // SH4 little-endian#defineIMAGE_FILE_MACHINE_SH5         0x01a8  // SH5#defineIMAGE_FILE_MACHINE_ARM         0x01c0  // ARM Little-Endian#defineIMAGE_FILE_MACHINE_THUMB        0x01c2#defineIMAGE_FILE_MACHINE_AM33         0x01d3#defineIMAGE_FILE_MACHINE_POWERPC    0x01F0  // IBM PowerPC Little-Endian#defineIMAGE_FILE_MACHINE_POWERPCFP   0x01f1#defineIMAGE_FILE_MACHINE_IA64        0x0200  // Intel 64#defineIMAGE_FILE_MACHINE_MIPS16      0x0266  // MIPS#defineIMAGE_FILE_MACHINE_ALPHA64     0x0284 //ALPHA64#defineIMAGE_FILE_MACHINE_MIPSFPU     0x0366  // MIPS#defineIMAGE_FILE_MACHINE_MIPSFPU16  0x0466  // MIPS#defineIMAGE_FILE_MACHINE_AXP64       IMAGE_FILE_MACHINE_ALPHA64#defineIMAGE_FILE_MACHINE_TRICORE    0x0520  // Infineon#defineIMAGE_FILE_MACHINE_CEF          0x0CEF#defineIMAGE_FILE_MACHINE_EBC          0x0EBC  // EFI Byte Code#defineIMAGE_FILE_MACHINE_AMD64       0x8664  // AMD64 (K8)#defineIMAGE_FILE_MACHINE_M32R        0x9041  // M32R little-endian#defineIMAGE_FILE_MACHINE_CEE          0xC0EE
NumberOfSections该PE文件中有多少个节,也就是节表中的项数。TimeDateStampPE文件的创建时间,一般有连接器填写。表明文件是何时被创建的。这个值是自1970年1月1日以来用格林威治时间(GMT)计算的秒数,这个值是比文件系统(FILESYSTEM)的日期时间更加精确的指示器。PointerToSymbolTableCOFF文件符号表在文件中的偏移,主要指向调式信息NumberOfSymbols符号表的数量。SizeOfOptionalHeader紧随其后的可选头的大小,对于32位系统,通常为0X00E0H,64位系统为0X00F0H。Characteristics可执行文件的属性,可以是下面这些值按位相或,定义在winnt.h头文件中。

1.3、IMAGE_OPTIONAL_HEADER32IMAGE_OPTIONAL_HEADER32是PE可选头,别看他名字叫可选头,其实一点都不能少,它在不同的平台下是不一样的,例如32位下是IMAGE_OPTIONAL_HEADER32,而在64位下是IMAGE_OPTIONAL_HEADER64。为了简单起见,我们只看32位。
typedef struct _IMAGE_OPTIONAL_HEADER {// 必选部分+18H  WORD    Magic;+1AH  BYTE    MajorLinkerVersion;+1BH  BYTE    MinorLinkerVersion;+1CH DWORD   SizeOfCode;+20H  DWORD   SizeOfInitializedData;+24H  DWORD   SizeOfUninitializedData;+28H  DWORD   AddressOfEntryPoint;+2CH  DWORD   BaseOfCode;+30H  DWORD   BaseOfData;// 可选部分+34H  DWORD   ImageBase;+38H  DWORD   SectionAlignment;+3CH  DWORD   FileAlignment;+40H  WORD    MajorOperatingSystemVersion;+42H  WORD    MinorOperatingSystemVersion;+44H  WORD    MajorImageVersion;+46H  WORD    MinorImageVersion;+48H  WORD    MajorSubsystemVersion;+4AH  WORD    MinorSubsystemVersion;+4CH  DWORD   Win32VersionValue;+50H  DWORD   SizeOfImage;+54H  DWORD   SizeOfHeaders;+58H  DWORD   CheckSum;+5CH  WORD    Subsystem;+5EH  WORD    DllCharacteristics;+60H  DWORD   SizeOfStackReserve;+64H  DWORD   SizeOfStackCommit;+68H  DWORD   SizeOfHeapReserve;+6CH  DWORD   SizeOfHeapCommit;+70H  DWORD   LoaderFlags;+74H  DWORD   NumberOfRvaAndSizes;+78H IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];} IMAGE_OPTIONAL_HEADER32,*PIMAGE_OPTIONAL_HEADER32;
Magic表示可选头的类型。
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC    0x10b  // 32PE可选头#defineIMAGE_NT_OPTIONAL_HDR64_MAGIC     0x20b  // 64PE可选头#defineIMAGE_ROM_OPTIONAL_HDR_MAGIC      0x107  
MajorLinkerVersion链接器的版本号MinorLinkerVersion链接器的版本号SizeOfCode代码段的长度,如果有多个代码段,则是代码段长度的总和。SizeOfInitializedData初始化的数据长度。SizeOfUninitializedData未初始化的数据长度。AddressOfEntryPoint程序入口的RVA,对于exe可以理解为WinMain的RVA。对于DLL可以理解为DllMain的RVA,对于驱动程序,可以理解为DriverEntry的RVA。当然,实际上入口点并非是WinMain,DllMain和DriverEntry,在这些函数之前还有一系列初始化要完成。BaseOfCode代码段起始地址的RVA。BaseOfData数据段起始地址的RVA。可选字段部分ImageBase映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。链接器产生可执行文件的时候对应这个地址来生成机器码,所以当文件被装入这个地址时不需要进行重定位操作,装入的速度最快。当文件被装载到其他地址时,进行重定位操作,会慢一点。对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被其他模块占据,所以EXE总是能够按照这个地址装入。这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被其他的DLL使用,所以DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 字段中,DLL 文件对应的 IMAGE_FILE_RELOCS_STRIPPED 位总是为0,而EXE文件的这个标志位总是为1,即DLL中不删除重定位信息,EXE文件中删除重定位信息。
批:#defineIMAGE_FILE_RELOCS_STRIPPED 0x0001
//Relocation info stripped from file.(从文件中删除重定位信息。)在链接的时候,可以通过对link.exe指定/base:address选项来自定义优先装入地址,如果不指定这个选项的话,一般EXE文件的默认优先装入地址被定为00400000h,而DLL文件的默认优先装入地址被定为10000000h。SectionAlignment节对齐,PE中的节被加载到内存时会按照这个域指定的值来对齐,比如这个值是0x1000,那么每个节的起始地址的低12位都为0。FileAlignment节在文件中按此值对齐,SectionAlignment必须大于或等于FileAlignment。MajorOperatingSystemVersion所需操作系统的版本号,随着操作系统版本越来越多,这个好像不是那么重要了。MinorOperatingSystemVersion所需操作系统的版本号,随着操作系统版本越来越多,这个好像不是那么重要了。MajorImageVersion映象的版本号,这个是开发者自己指定的,由连接器填写。MinorImageVersion映象的版本号,这个是开发者自己指定的,由连接器填写。MajorSubsystemVersion所需子系统版本号。MinorSubsystemVersion所需子系统版本号。Win32VersionValue保留,必须为0。SizeOfImage映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。SizeOfHeaders所有文件头(包括节表)的大小,这个值是以FileAlignment对齐的。CheckSum映象文件的校验和。Subsystem
运行该PE文件所需的子系统,可以是下面定义中的某一个:

DllCharacteristicsDLL的文件属性,只对DLL文件有效,可以是下面定义中某些的组合:
#defineIMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE        0x0040     // DLL can move.#defineIMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY   0x0080     // Code Integrity Image#defineIMAGE_DLLCHARACTERISTICS_NX_COMPAT            0x0100     // Image is NX compatible#defineIMAGE_DLLCHARACTERISTICS_NO_ISOLATION        0x0200     // Image understands isolation and doesn't want it#define IMAGE_DLLCHARACTERISTICS_NO_SEH               0x0400     // Image does not use SEH.  No SE handler may reside in this image#defineIMAGE_DLLCHARACTERISTICS_NO_BIND              0x0800     // Do not bind this image.//                                           0x1000     // Reserved.#defineIMAGE_DLLCHARACTERISTICS_WDM_DRIVER           0x2000    //Driver uses WDM model//                                           0x4000     // Reserved.#defineIMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE     0x8000
SizeOfStackReserve运行时为每个线程栈保留内存的大小。SizeOfStackCommit运行时每个线程栈初始占用内存大小。SizeOfHeapReserve运行时为进程堆保留内存大小。SizeOfHeapCommit运行时进程堆初始占用内存大小。LoaderFlags保留,必须为0。NumberOfRvaAndSizes数据目录的项数,即下面这个数组的项数。DataDirectory数据目录,这是一个数组,数组的项定义如下:typedefstruct _IMAGE_DATA_DIRECTORY {    DWORD   VirtualAddress;    DWORD   Size;} IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;VirtualAddress是一个RVA,Size:是一个大小。这两个数有什么用呢?一个是地址,一个是大小,可以看出这个数据目录项定义的是一个区域。那他定义的是什么东西的区域呢?前面说了,DataDirectory是个数组,数组中的每一项对应一个特定的数据结构,包括导入表,导出表等等,根据不同的索引取出来的是不同的结构,头文件里定义各个项表示哪个结构,如下面的代码所示:

批:15未使用,共16项四、节(块)表(Section Table)节表是PE文件后续节的描述,Windows根据节表的描述加载每个节。PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结构的最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中IMAGE_SECTION_HEADER结构数量等于节的数量加一。节表总是被存放在紧接在PE文件头的地方。节表中 IMAGE_SECTION_HEADER结构的总数总是由PE文件头IMAGE_NT_HEADERS(注:即本资料中的NT) 结构中的FileHeader.NumberOfSections 字段来指定的。typedef struct_IMAGE_SECTION_HEADER { BYTE  Name[IMAGE_SIZEOF_SHORT_NAME]; union {   DWORD PhysicalAddress;   DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORDPointerToRelocations; DWORDPointerToLinenumbers; WORD  NumberOfRelocations; WORD  NumberOfLinenumbers; DWORD Characteristics;} IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;
1、需要关注的域Name区块名。这是一个由8个ASCII码组成,用来定义区块的名称的数组。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.”实际上是不是必须的。值得我们注意的是,如果区块名达到8 个字节,后面就没有0字符了。前边带有一个“$” 的区块名字会从连接器那里得到特殊的待遇,前边带有“$”的相同名字的区块在载入时候将会被合并,在合并之后的区块中,他们是按照“$”后边的字符的字母顺序进行合并的。每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data”或者说将包含数据的区块命名为“.Code”都是合法的。当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。VirtualSize对表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。VirtualAddress该区块装载到内存中的RVA地址。这个地址是按照内存页来对齐的,因此它的数值总是SectionAlignment的值的整数倍。PointerToRawData指出节在磁盘文件中所处的位置。这个数值是从文件头开始算起的偏移量。SizeOfRawData该区块在磁盘中所占的大小,这个数值等于VirtualSize字段的值按照FileAlignment的值对齐以后的大小。Characteristics该区块的属性。该字段是按位来指出区块的属性(如代码/数据/可读/可写等)的标志。
批:更多可查MSDN2、装载过程依靠PointerToRawDataSizeOfRawDataVirtualAddressVirtualSize4个字段的值,装载器就可以从PE文件中找出某个节(从PointerToRawData偏移开始的SizeOfRawData字节)的数据,并将它映射到内存中去(映射到从模块基地址偏移VirtualAddress的地方,并占用以VirtualSize的值按照页的尺寸对齐后的空间大小)。五、节(块)每个节实际上是一个容器,可以包含代码、数据等等,每个节可以有独立的内存权限,比如代码节默认有读/执行权限,节的名字和数量可以自己定义。通常,区块中的数据在逻辑上是关联的。PE 文件一般至少都会有两个区块:一个是代码块,另一个是数据块。每一个区块都需要有一个截然不同的名字,这个名字主要是用来表达区块的用途。例如有一个区块叫.rdata,表明他是一个只读区块。注意:区块在映像中是按起始地址(RVA)来排列的,而不是按字母表顺序。另外,使用区块名字只是人们为了认识和编程的方便,而对操作系统来说这些是无关紧要的。微软给这些区块取了个有特色的名字,但这不是必须的。当编程从PE 文件中读取需要的内容时,如输入表、输出表,不能以区块名字作为参考,正确的方法是按照数据目录表中的字段来进行定位。1、块的偏移地址块起始地址在磁盘中是按照IMAGE_OPTIONAL_HEADER32中的FileAlignment字段的值进行对齐的,而当被加载到内存中时是按照同一结构中的SectionAlignment字段的值设置对齐的,两者的值可能不同。所以一个块表被装载到内存后相对于文件头的偏移地址和磁盘中的偏移地址可能是不同的。2、区块名称以及意义我们在Visual C++中也可以自己命名我们的区块,用#pragma 来声明,告诉编译器插入数据到一个区块内,格式如下:        #pragma data_msg( "FC_data" )以上语句告诉编译器将数据都放进一个叫“FC_data”的区块内,而不是默认的.data 区块。区块一般是从OBJ 文件开始,被编译器放置的。链接器的工作就是合并左右OBJ 和库中需要的块,使其成为一个最终合适的区块。链接器会遵循一套相当完整的规则,它会判断哪些区块将被合并以及如何被合并。

   3、合并区块链接器的一个有趣特征就是能够合并区块。如果两个区块有相似、一致性的属性,那么它们在链接的时候能被合并成一个单一的区块。这取决于是否开启编译器的 /merge 开关。事实上合并区块有一个好处就是可以节省磁盘的内存空间……注意:我们不应该将.rsrc、.reloc、.pdata 合并到**的区块里。4、区块的对齐值之前我们简单了解过区块是要对齐的,无论是在内存中存放还是在磁盘中存放,但他们一般的对齐值是不同的。PE 文件头里边的FileAligment 定义了磁盘区块的对齐值。每一个区块从对齐值的倍数的偏移位置开始存放。而区块的实际代码或数据的大小不一定刚好是这么多,所以在多余的地方一般以00h 来填充,这就是区块间的间隙。例如,在PE文件中,一个典型的对齐值是200h,这样,每个区块都将从200h 的倍数的文件偏移位置开始,假设第一个区块在400h 处,长度为90h,那么从文件400h 到490h 为这一区块的内容,而由于文件的对齐值是200h,所以为了使这一区块的长度为FileAlignment 的整数倍,490h 到 600h 这一个区间都会被00h 填充,这段空间称为区块间隙,下一个区块的开始地址为600h 。PE 文件头里边的SectionAligment 定义了内存中区块的对齐值。PE 文件被映射到内存中时,区块总是至少从一个页边界开始。一般在X86 系列的CPU 中,页是按4KB(1000h)来排列的;在IA-64 上,是按8KB(2000h)来排列的。所以在X86 系统中,PE文件区块的内存对齐值一般等于 1000h,每个区块按1000h 的倍数在内存中存放。5、RVA 和文件偏移的转换RVA 是相对虚拟地址(RelativeVirtual Address)的缩写,顾名思义,它是一个“相对地址”。PE 文件中的各种数据结构中涉及地址的字段大部分都是以 RVA 表示的。更为准确的说,RVA 是当PE 文件被装载到内存中后,某个数据位置相对于文件头的偏移量。举个例子,如果 Windows 装载器将一个PE 文件装入到 00400000h 处的内存中,而某个区块中的某个数据被装入 0040**xh 处,那么这个数据的 RVA 就是(0040**xh - 00400000h )= **xh,反过来说,将 RVA 的值加上文件被装载的基地址,就可以找到数据在内存中的实际地址。6、换算 RVA 和文件偏移当处理PE 文件时候,任何的 RVA 必须经过到文件偏移的换算,才能用来定位并访问文件中的数据,但换算却无法用一个简单的公式来完成,事实上,唯一可用的方法就是最土最笨的方法:步骤一:循环扫描区块表得出每个区块在内存中的起始 RVA(根据IMAGE_SECTION_HEADER 中的VirtualAddress 字段),并根据区块的大小(根据IMAGE_SECTION_HEADER 中的SizeOfRawData 字段)算出区块的结束 RVA(两者相加即可),最后判断目标 RVA 是否落在该区块内。步骤二:通过步骤一定位了目标 RVA 处于具体的某个区块中后,那么用目标 RVA 减去该区块的起始 RVA ,这样就能得到目标 RVA 相对于起始地址的偏移量 RVA2.步骤三:在区块表中获取该区块在文件中所处的偏移地址(根据IMAGE_SECTION_HEADER 中的PointerToRawData 字段), 将这个偏移值加上步骤二得到的 RVA2 值,就得到了真正的文件偏移地址。六、PE文件加载过程在执行一个PE文件的时候,Windows并不在一开始就将整个文件读入内存的,而是采用与内存映射文件类似的机制。也就是说,Windows 装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系。当且仅当真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系。

` 当一个PE文件被加载到内存中以后,我们称之为“映象”(image),一般来说,PE文件在硬盘上和在内存里是不完全一样的,被加载到内存以后其占用的虚拟地址空间要比在硬盘上占用的空间大一些,这是因为各个节在硬盘上是连续的,而在内存中是按页对齐的,所以加载到内存以后节之间会出现一些“空洞”。

Windows 装载器在装载DOS部分,PE文件头部分和节表(节表也称为区块表,块表)部分是不进行任何特殊处理的,而在装载节(节也称为区块)的时候则会自动按节(区块)的属性的不同做不同的处理。

因为存在这种对齐,所以在PE结构内部,表示某个位置的地址采用了两种方式,针对在硬盘上存储文件中的地址,称为原始存储地址或物理地址表示距离文件头的偏移;另外一种是针对加载到内存以后映象中的地址,称为相对虚拟地址(RVA,表示相对内存映象头的偏移。然而CPU的某些指令是需要使用绝对地址的,比如取全局变量的地址,传递函数的地址编译以后的汇编指令中肯定需要用到绝对地址而不是相对映象头的偏移,因此PE文件会建议操作系统将其加载到某个内存地址(这个叫基地址),编译器便根据这个地址求出代码中一些全局变量和函数的地址,并将这些地址用到对应的指令中。例如在IDA里看上去是这个样子:

这种表示方式叫做虚拟地址(VA
也许有人要问,既然有VA这么简单的表示方式为什么还要有前面的RVA呢?因为虽然PE文件为自己指定加载的基地址,但是windows有茫茫多的DLL,而且每个软件也有自己的DLL,如果指定的地址已经被别的DLL占了怎么办?如果PE文件无法加载到预期的地址,那么系统会帮他重新选择一个合适的基地址将他加载到此处,这时原有的VA就全部失效了,NT头保存了PE文件加载所需的信息,在不知道PE会加载到哪个基地址之前,VA是无效的,所以在PE文件头中大部分是使用RVA来表示地址的,而在代码中是用VA表示全局变量和函数地址的。那又有人要问了,既然加载基址变了以后VA都失效了,那存在于代码中的那些VA怎么办呢?答案是:重定位。系统有自己的办法修正这些值,到后续重定位表的文章中会详细描述。既然有重定位,为什么NT头不能依靠重定位采用VA表示地址呢(十万个为什么)?因为不是所有的PE都有重定位,早期的EXE就是没有重定位的。
七、导(输)入表输入表是PE(Portable Executable File Format)文件结构中不可或缺的部分,输入表也被称之为“导入表”。可执行文件使用来自于其他动态链接库(DLL)的代码或数据时,称为输入。输入表就相当于 EXE文件与 DLL文件沟通的钥匙,形象的可以比喻成两个城市之间交流的高速公路,所有的导入函数信息都会写入输入表中,在PE 文件映射到内存后,Windows 将相应的 DLL文件装入,EXE 文件通过“输入表”找到相应的 DLL 中的导入函数,从而完成程序的正常运行,这一动态连接的过程都是由“输入表”参与的。1、输入函数输入函数,表示被程序调用但是它的代码不在程序代码中的,而在DLL中的函数。对于这些函数,磁盘上的可执行文件只是保留相关的函数信息,如函数名,DLL文件名等。在程序运行前,程序是没有保存这些函数在内存中的地址。当程序运行起来时,windows加载器会把相关的DLL装入内存,并且将输入函数的指令与函数真在内存中正的地址联系起来。输入表(导入表)就是用来保存这些函数的信息的。2、DataDirectory在IMAGE_OPTIONAL_HEADER 中的 DataDirectory数组保存了输入表的RVA跟大小。通过RVA可以在OD中加载程序通过ImageBase+RVA 找到输入表,或者通过RVA计算出文件偏移地址,查看磁盘中的可执行文件,通过文件偏移地址找到输入表。在 PE文件头的 IMAGE_OPTIONAL_HEADER 结构中的DataDirectory(数据目录表) 的第二个成员就是指向输入表的。而输入表是以一个IMAGE_IMPORT_DESCRIPTOR(简称IID) 的数组开始。每个被 PE文件链接进来的 DLL文件都分别对应一个 IID数组结构。在这个 IID数组中,并没有指出有多少个项(就是没有明确指明有多少个链接文件),但它最后是以一个全为NULL(0) 的 IID 作为结束的标志。3、IMAGE_IMPORT_DESCRIPTOR输入表是以一个IMAGE_IMPORT_DESCRIPTOR(IID)数组 开始的,每一个被PE文件隐式的链接进来的DLL都有一个IID,IID数组的最后一个单元用NULL表示。typedef struct_IMAGE_IMPORT_DESCRIPTOR {     _ANONYMOUS_UNION union{              //00h        DWORDCharacteristics;          DWORDOriginalFirstThunk;      } DUMMYUNIONNAME;     DWORD TimeDateStamp;                  //04h   DWORDForwarderChain;                 //08h   DWORD Name;                             //0Ch   DWORD FirstThunk;                      //10h}IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;
OriginalFirstThunk指向一个IMAGE_THUNK_DATA数组叫做输入名称表Import Name Table(INT),用来保存函数。TimeDateStamp该字段可以忽略。如果那里有绑定的话它包含时间/数据戳(time/data stamp)。如果它是0,就没有绑定在被导入的DLL中发生。在最近,它被设置为0xFFFFFFFF以表示绑定发生。ForwarderChain一般情况下我们也可以忽略该字段。在老版的绑定中,它引用API的第一个forwarder chain(传递器链表)。它可被设置为0xFFFFFFFF以代表没有forwarder。NameDLL名字的指针, 指向一个用NULL作为结束符的ASCII字符串的一个RVA,该字符串是该导入DLL文件的名称,如:KERNEL32.DLLFirstThunk它也指向IMAGE_THUNK_DATA数组叫做输入地址表Import Address Table(IAT)。IMAGE_THUNK_DATA结构typedef struct_IMAGE_THUNK_DATA32 {     union {          DWORDForwarderString;          DWORDFunction;          DWORDOrdinal;          DWORDAddressOfData;     } u1;}IMAGE_THUNK_DATA32,*PIMAGE_THUNK_DATA32;

当IMAGE_THUNK_DATA 的值最高位为1时,表示函数是以序号方式输入,这时低31为被当作函数序号。当最高位是0时,表示函数是以字符串类型的函数名方式输入的,这时,IMAGE_THUNK_DATA 的值为指向IMAGE_IMPORT_BY_NAME 的结构的RVA。typedef struct_IMAGE_IMPORT_BY_NAME {     WORD Hint;     BYTE Name[1];  
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;

Hint表示这个函数在其所驻留DLL的输出表的序号,不是必须的。
Name表示 函数名,是一个ASCII字符串以0结尾,大小不固定。4、需要两个 IMAGE_THUNK_DATA 数组的原因
当程序加载时,IAT 会被PE加载器重写,PE加载器先搜索INT,PE加载器迭代搜索INT数组中的每个指针,找出 INT所指向的IMAGE_IMPORT_BY_NAME结构中的函数在内存中的真正的地址,并把它替代原来IAT中的值。当完成后,INT就没有用了,程序只需要IAT就可以正常运行了。
可执行程序在磁盘中的时:

当程序被加载后时:

八、导(输)出表导出表是用来描述模块中的导出函数的结构,如果一个模块导出了函数,那么这个函数会被记录在导出表中,这样通过GetProcAddress函数就能动态获取到函数的地址。函数导出的方式有两种,一种是按名字导出,一种是按序号导出。这两种导出方式在导出表中的描述方式也不相同。模块的导出函数可以通过Dependency walker工具来查看:

上图中红框位置显示的就是模块的导出函数,有时候显示的导出函数名字中有一些符号,像 ??0CP2PDownloadUIInterface@@QAE@ABV0@@Z,这种是导出了C++的函数名,编译器将名字进行了修饰。1、导出表的定义typedef struct _IMAGE_EXPORT_DIRECTORY {    DWORD  Characteristics;    DWORD  TimeDateStamp;    WORD   MajorVersion;    WORD   MinorVersion;    DWORD  Name;    DWORD  Base;    DWORD  NumberOfFunctions;    DWORD  NumberOfNames;    DWORD  AddressOfFunctions;     // RVAfrom base of image    DWORD  AddressOfNames;          // RVA frombase of image    DWORD  AddressOfNameOrdinals;  // RVAfrom base of image}IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
结构还算比较简单,具体每一项的含义如下:Characteristics现在没有用到,一般为0。TimeDateStamp导出表生成的时间戳,由连接器生成。MajorVersion,MinorVersion看名字是版本,实际貌似没有用,都是0。Name模块的名字。Base序号的基数,按序号导出函数的序号值从Base开始递增。NumberOfFunctions所有导出函数的数量。NumberOfNames按名字导出函数的数量。AddressOfFunctions一个RVA,指向一个DWORD数组,数组中的每一项是一个导出函数的RVA,顺序与导出序号相同。AddressOfNames一个RVA,依然指向一个DWORD数组,数组中的每一项仍然是一个RVA,指向一个表示函数名字。AddressOfNameOrdinals一个RVA,还是指向一个WORD数组,数组中的每一项与AddressOfNames中的每一项对应,表示该名字的函数在AddressOfFunctions中的序号。
基本各项间的图解

123

在上图中,AddressOfNames指向一个数组,数组里保存着一组RVA,每个RVA指向一个字符串,这个字符串即导出的函数名,与这个函数名对应的是AddressOfNameOrdinals中的对应项。获取导出函数地址时,先在AddressOfNames中找到对应的名字,比如Func2,他在AddressOfNames中是第二项,然后从AddressOfNameOrdinals中取出第二项的值,这里是2,表示函数入口保存在AddressOfFunctions这个数组中下标为2的项里,即第三项,取出其中的值,加上模块基地址便是导出函数的地址。如果函数是以序号导出的,那么查找的时候直接用序号减去Base,得到的值就是函数在AddressOfFunctions中的下标。2、从序号查找函数入口地址Windows 装载器的工作步骤如下:1.定位到PE 文件头2.从PE 文件头中的 IMAGE_OPTIONAL_HEADER32 结构中取出数据目录表,并从第一个数据目录中得到导出表的RVA3.从导出表的 Base 字段得到起始序号4.将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引5.检测索引值是否大于导出表的 NumberOfFunctions 字段的值,如果大于后者的话,说明输入的序号是无效的6.用这个索引值在 AddressOfFunctions 字段指向的导出函数入口地址表中取出相应的项目,这就是函数入口地址的RVA 值,当函数被装入内存的时候,这个RVA 值加上模块实际装入的基地址,就得到了函数真正的入口地址3、从函数名称查找入口地址Windows 装载器的工作步骤如下:1.最初的步骤是一样的,那就是首先得到导出表的地址2.从导出表的 NumberOfNames 字段得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环3.从 AddressOfNames 字段指向得到的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数4.如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在 AddressOfNamesOrdinals 指向的数组中以同样的索引值取出数组项的值,我们这里假设这个值是x5.最后,以 x 值作为索引值,在 AddressOfFunctions 字段指向的函数入口地址表中获取的 RVA 就是函数的入口地址九、重定位1、重定位定义
重定位就是把程序的逻辑地址空间变换成内存中的实际物理地址空间的过程,也就是说在装入时对目标程序中指令和数据的修改过程。他是实现多道程序在内存中同时运行的基础。它保存在Data Directory数组中。2、重定位的作用
这里给出几句代码,来说明重定位的作用

从这几句代码中不难发现,这几句代码的目的地址都是用直接寻址的方式来表示的,在代码的十六进制中,可以看到都是直接使用地址来表示的,当文件运行时,系统会分配一块内存给文件,如果分配的内存与文件镜像基址相同,这些代码则可以正常执行,但是当分配到的内存与文件镜像基址不同的时候,这些指令就会出现错误了,这个时候系统将会获取文件的重定位信息,并把错误指令修正.比如PUSH 403010这一句是在某个基址为400000的文件中的,当文件运行的时候,系统分配到的内存为500000,那么PUSH 403010这一指令如果是在重定位中,系统就会在加载时把它修复为PUSH 503010,这样就保证了分配到的内存与基址不同的时候也能正常运行。3、重定位结构struct_IMAGE_BASE_RELOCATION{    DWORD  VirtualAddress;  //重定位数据开始的RVA地址    DWORD  SizeOfBlock;     //重定位块的长度    WORD    TypeOffset;     //重定位项位数组}IMAGE_BASE_RELOCATION;
VirtualAddress是这一组重定位数据的开始RVA地址.各重定位项的地址加上这个值才是该重定位项完整的RVA地址.SizeOfBlock是重定位结构的大小TypeOffset是一个数组.数组每项大小为两个字节,共16位.它又分为高4位和低12位,高4位代表重定位类型;低12位是重定位地址,它与VirtualAddress相加即是指向PE映像中需要修改的地址数据的指针.4、深入分析重定位结构
我们继续深入解析一下重定位结构IMAGE_BASE_RELOCATION,我们就用DLL来分析.假设,已经知道其重定位的偏移量为4000,大小为10,现在我们把它载入到winhex中,跳到重定位,可以看到

十、资源1、简介程序内部和外部的界面等元素的二进制数据统称为资源,程序把它们放在一个特定的表中,符合数据和程序分离的设计原则。资源包括加速键(Accelerator)、位图(Bitmap)、光标(Cursor)、对话框(Dialog Box)、图标(Icon)、菜单(Menu)、串表(String Table)、工具栏(Toolbar)和版本信息(Version Information)等。资源有很多种类型,每种类型的资源中可能存在多个资源项,这些资源项用不同的ID或者名称来区分。但是要将这么多种类型的不同ID 的资源有序地组织起来是一件非常痛苦的事情,因此,我们采取类似于磁盘目录结构的方式保存。

PE文件中的资源是按照资源类型 -> 资源ID -> 资源代码页的3层树型目录结构来组织资源的,通过层层索引才能够进入相应的子目录找到正确的资源。每一层都是以IMAGE_RESOURCE_DIRECTORY结构为头部的,并且后面跟着一个IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组。其中IMAGE_RESOURCE_DIRECTORY负责指出后面数组中的成员个数,IMAGE_RESOURCE_DIRECTORY_ENTRY数组成员分别指向下一层目录结构。2、位置PE文件头可选映像头中数据目录表的第3成员IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE]指向映像调试信息,它保存在PE文件中,通常在".rsrc"区段。3、资源目录结构

4、IMAGE_RESOURCE_DIRECTORY我们来看下 IMAGE_RESOURCE_DIRECTORY 这个结构,该结构长度为16 字节,共有 6 个字段,定义如下:typedef struct _IMAGE_RESOURCE_DIRECTORY {    DWORD  Characteristics;       //属性,一般为0    DWORD  TimeDateStamp;          //资源的产生时刻,一般为0    WORD   MajorVersion;           //主版本号,一般为0    WORD   MinorVersion;           //次版本号,一般为0    WORD   NumberOfNamedEntries; //以名称(字符串)命名的资源数量    WORD   NumberOfIdEntries;    //ID(整型数字)命名的资源数量} IMAGE_RESOURCE_DIRECTORY,*PIMAGE_RESOURCE_DIRECTORY;
其实在这里边我们唯一要注意的就是 NameberOfNamedEntries 和 NumberOfIdEntries,它们说明了本目录中目录项的数量。两者加起来就是本目录中的目录项总和。也就是后边跟着的IMAGE_RESOURCE_DIRECTORY_ENTRY 数目。5、IMAGE_RESOURCE_DIRECTORY_ENTRY(资源目录入口的结构)IMAGE_RESOURCE_DIRECTORY_ENTRY 紧跟在资源目录结构后,此结构长度为 8 个字节,包含 2 个字段。该结构定义如下:typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {    union {        struct {            DWORD NameOffset:31;            DWORD NameIsString:1;        };        DWORD  Name;        WORD   Id;    };    union {        DWORD  OffsetToData;        struct {            DWORD   OffsetToDirectory:31;            DWORD   DataIsDirectory:1;        };    };} IMAGE_RESOURCE_DIRECTORY_ENTRY,*PIMAGE_RESOURCE_DIRECTORY_ENTRY;
根目录(资源目录头)下面存放的是这个结构体,这个结构体是一个联合体,所以会有不同的解释1.首先,联合体是8个字节大小.2.其中第一个DWORD大小,看高位,如果高位是1,那么低31位是指向新的目录项名称的结构体IMAGE_RESOURCE_DIR_STRING_U,当结构用于第一层目录时,定义的是资源类型;当结构定义于第二层目录时,定义的是资源的名称;当结构用于第三层目录时,定义的是代码页编号。
批:NameOffset相对地址指向的是IMAGE_RESOURCE_DIR_STRING_U结构体,该结构体定义如下:typedef  struct_IMAGE_RESOURCE_DIR_STRING_U {
    WORD    Length;           //字符串的长度
    WCHAR   NameString[ 1 ];  //UNICODE字符串,由于字符串是不定长的。由Length制定长度
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;3.如果高位为0,则是ID号,这个ID号说的是 资源ID类型,比如3类型指的就是ICON第二个DWORD,也是RVA偏移,如果高位为1,那么代表它还是一个目录,也就是指向了一个新的根目录了,这是个不断递归的过程,如果为0,则指向文件偏移结构体了。
OffsetToData,指针;当最高位为1时,表示低位数据指向下一层目录;当最高位为0时,表示没有下一层目录,低位数据指向一个IMAGE_RESOURCE_DATA_ENTRY结构。
注意,当Name和OffsetToData当做指针使用时,其值并不是RVA,而是表示相对于资源区块起始位置的偏移值。

6、_IMAGE_RESOURCE_DATA_ENTRY(资源数据结构体)文件偏移结构体(应该是资源数据结构体),共4个字段,16个字节typedef struct _IMAGE_RESOURCE_DATA_ENTRY {    DWORD  OffsetToData;  //资源数据的RVA    DWORD  Size;            //资源数据的长度    DWORD  CodePage;      //代码页, 一般为0    DWORD  Reserved;      //保留字段} IMAGE_RESOURCE_DATA_ENTRY,*PIMAGE_RESOURCE_DATA_ENTRY;结构中的OffsetOfData 指向资源数据的指针,其为 RVA 值。附录:图1:PE文件的基本结构如图示

THE END
喜欢就支持以下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称