Angelo Lee's Blog
This is my kingdom .If i don't fight for it ,who will ?

PE 的意思就是 Portable Executable(可移植的执行体)。PE文件结构的总体层次分布图:
 
 --------------
|DOS MZ Header |
|--------------|
|DOS Stub      |
|--------------|
|PE Header     |
|--------------|
|Section Table |
|--------------|
|Section 1     |
|--------------| 
|Section 2     |
|--------------|
|Section ...   |
|--------------|
|Section n     |
 --------------
 
一、PE文件格式的概要

1.1、DOS MZ Header:
 所有 PE文件(甚至32位的 DLLs)必须以一个简单的 DOS MZ Header 开始。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随 MZ Header 之后的 DOS Stub。

1.2、DOS Stub:
 DOS Stub(存根)实际上是个有效的 MS-DOS .EXE 或者.COM 程序(如果文件格式不对会报错),在不支持 PE文件格式的操作系统中,它将通过简单调用中断21h服务9来显示字符串"This program cannot run in DOS mode"或者根据程序员自己的意图实现完整的 DOS 代码。它的大小一般不能确定。利用链接器(linker)的 /STUB:filename 选项可以替换这个程序。

1.3、PE Header:
 紧接着 DOS Stub 的是 PE Header。PE Header 是PE相关结构 IMAGE_NT_HEADERS 的简称,其中包含了许多PE装载器用到的重要域。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从 DOS MZ Header (IMAGE_DOS_HEADER)中找到 PE Header 的起始偏移量。因而跳过了DOS Stub 直接定位到真正的文件头PE Header。

1.4、Section Table:
 PE Header 接下来的数组结构 Section Table (节表)。如果PE文件里有5个节,那么此 Section Table 结构数组内就有5个成员,每个成员包含对应节的属性、文件偏移量、虚拟偏移量等。

1.5、Sections:
 PE文件的真正内容被划分成块,称之为Section(节)。每个标准节的名字均以圆点开头。Sections 是以其起始位址来排列,而不是以其字母次序来排列。下面是常见的节名及作用:
 
节名   作用
.arch  最初的构建信息(Alpha Architecture Information)
.bss   未经初始化的数据
.CRT   C运行期只读数据
.data   已经初始化的数据
.debug   调试信息
.didata  延迟输入文件名表
.edata  导出文件名表
.idata  导入文件名表
.pdata      异常信息(Exception Information)
.rdata  只读的初始化数据
.reloc  重定位表信息
.rsrc  资源
.text   .exe或.dll文件的可执行代码
.tls  线程的本地存储器
.xdata  异常处理表
 
 节的划分是基于各组数据的共同属性,而不是逻辑概念。每节是一块拥有共同属性的数据,比如代码/数据、读/写等。如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中。节名称仅仅是个区别不同节的符号而已,类似"data", "code"的命名只为了便于识别,惟有节的属性设置决定了节的特性和功能。

1.6、装载一PE文件的主要步骤:

1.当PE文件被执行,PE装载器检查 DOS MZ Header 里的 PE Header 偏移量。如果找到,则跳转到 PE Header。 
2.PE装载器检查 PE Header 的有效性。如果有效,就跳转到PE Header的尾部。 
3.紧跟 PE Header 的是节表。PE装载器读取其中的节信息,并采用文件映射方法将这些节映射到内存,同时付上节表里指定的节属性。 
4.PE文件映射入内存后,PE装载器将处理PE文件中类似 Import Table(导入表)逻辑部分。


二、DOS MZ Header 和 PE Header

2.1、DOS MZ Header 定义成结构 IMAGE_DOS_HEADER(64字节) 。结构定义如下:

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE Header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of Header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe Header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
  
IMAGE_DOS_HEADER 结构的e_lfanew成员就是指向 PE Header 的 RVA。e_magic 包含字符串"MZ"。

2.2、PE Header 实际就是一个 IMAGE_NT_HEADERS 结构。定义如下:

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;

 IMAGE_NT_HEADERS 结构成员含义:

1.Signature:一DWORD 类型,值为50h, 45h, 00h, 00h(PE\0\0)。如果IMAGE_NT_HEADERS的Signature域值等于"PE\0\0",那么就是有效的PE文件。Microsoft定义了常量IMAGE_NT_SIGNATURE供我们使用,定义如下:

#define IMAGE_DOS_SIGNATURE                 0x5A4D      // MZ
#define IMAGE_OS2_SIGNATURE                 0x454E      // NE
#define IMAGE_OS2_SIGNATURE_LE              0x454C      // LE
#define IMAGE_VXD_SIGNATURE                 0x454C      // LE
#define IMAGE_NT_SIGNATURE                  0x00004550  // PE00

2.FileHeader:该结构域包含了关于PE文件物理分布的信息,比如节数目、文件执行机器等。

3.OptionalHeader:该结构域包含了关于PE文件逻辑分布的信息,虽然域名有"可选"字样,但实际上本结构总是存在的。

2.3、检验PE文件的有效性步骤总结如下:

1.首先检验文件头部第一个字的值是否等于 IMAGE_DOS_SIGNATURE,是则 DOS MZ Header 有效。 
2.一旦证明文件的 DOS Header 有效后,就可用e_lfanew来定位 PE Header 了。 
3.比较 PE Header 的第一个字的值是否等于 IMAGE_NT_HEADER。如果前后两个值都匹配,那我们就认为该文件是一个有效的PE文件。

 下面将通过一个VC++ 6.0的例子来检验PE文件的有效性:

 我们首先调用打开文件通用对话框(GetOpenFileName),选择打开一个文件并映射到内存(CreateFile,CreateFileMapping、MapViewOfFile等),获得目标文件大小(m_buffer = new unsigned char[m_size];)。然后获取目标文件的头2个字节(((unsigned short*)m_buffer)[0];),看是否为"MZ"。如果相同,获得目标文件PE header的位置(((unsigned int*)(2*m_buffer + 0x3c));), 与0x00004550(PE)比较。由此验证PE有效性。

三、File Header(文件头)

 File Header(IMAGE_FILE_HEADER)包含在PE Header(IMAGE_NT_HEADERS)里面,其结构定义:

typedef struct _IMAGE_FILE_HEADER {
    WORD Machine;
    WORD NumberOfSections;
    DWORD TimeDateStamp;
    DWORD PointerToSymbolTable;
    DWORD NumberOfSymbols;
    WORD SizeOfOptionalHeader;
    WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

 IMAGE_FILE_HEADER 结构成员含义:

1.Machine:该文件运行所要求的CPU。对于Intel平台,该值是IMAGE_FILE_MACHINE_I386 (14Ch)。我们尝试了LUEVELSMEYER的pe.txt声明的14Dh和14Eh,但Windows不能正确执行。

一些CPU识别码的定义:

Intel I386    0x14C
Intel i860    0x14D
MIPS R300    0x162
MIPS R400    0x166
DEC Alpha AXP   0x184
Power PC    0x1F0(little endian)
Motorola 68000   0x268
PA RISC    0x290(Precision Architecture)

#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.
#define IMAGE_FILE_MACHINE_R3000             0x0162  // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000             0x0166  // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000            0x0168  // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2         0x0169  // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA             0x0184  // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3               0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB             0x01c2
#define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64

2.NumberOfSections:文件的节数目。如果我们要在文件中增加或删除一个节,就需要修改这个值。

3.TimeDateStamp:文件创建日期和时间。其格式是自从1969年12 月31 日4:00 P.M. 之后的总秒数。据我计算,0xFFFFFFFFh是136.19251950152207001522070015221 年。

4.PointerToSymbolTable:COFF 符号表格的偏移位置。此域只对COFF 除错信息有用。

5.NumberOfSymbols:COFF 符号表格中的符号个数。

6.SizeOfOptionalHeade:指示紧随本结构之后的 Optional Header(IMAGE_OPTIONAL_HEADER)结构大小,必须为有效值。

7.Chracteristics:关于本文件信息的标记。一些比较重要的性质如下:

0x0001 文件中没有重定位(relocation)
0x0002 文件是一个可执行程序exe(也就是說不是OBJ 或LIB)
0x2000 文件是dll,不是exe。

 一般情况下,如果要遍历节表就得使用 NumberOfSections,其它的几个域作用不大。
 

四、Optional Header

4.1、RVA 及其相关概念:

 RAV 代表相对虚拟地址。RVA是虚拟空间中到参考点的一段距离。RVA就是类似文件偏移量的东西。当然它是相对虚拟空间里的一个地址,而不是文件头部。举例说明,如果PE文件装入虚拟地址(VA)空间的400000h处,且进程从虚址401000h开始执行,我们可以说进程执行起始地址在RVA 1000h。每个RVA都是相对于模块的起始VA的。虛址(VA)0x401000h - 基址(BA)0x400000h = RVA 0x1464h。基址(Base Address)用来描述被映射到内存中的exe或者dll的起始位置。

 为什么PE文件格式要用到RVA呢? 这是为了减少PE装载器的负担。因为每个模块都有可能被重载到任何虚拟地址空间,如果让PE装载器修正每个重定位项,这肯定是个梦魇。相反,如果所有重定位项都使用RVA,那么PE装载器就不必操心那些东西了: 它只要将整个模块重定位到新的起始VA。这就象相对路径和绝对路径的概念: RVA类似相对路径,VA就象绝对路径。

 在PE文件中大多数地址多是RVAs 而 RVAs只有当PE文件被PE装载器装入内存后才有意义。如果直接将文件映射到内存而不是通过PE装载器载入,则不能直接使用那些RVAs。必须先将那些RVAs转换成文件偏移量。

4.2、Optional Header 结构是 IMAGE_NT_HEADERS 中的最后成员。包含了PE文件的逻辑分布信息。该结构共有31个域,一些是很关键,另一些不太常用。其结构定义:

typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;
    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;     DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;

 IMAGE_OPTIONAL_HEADER 结构成员含义:
 
1.Magic:用来定义 image 的状态

0x0107(IMAGE_ROM_OPTIONAL_HDR_MAGIC):一个 ROM image
0x010B(IMAGE_NT_OPTIONAL_HDR_MAGIC): 一个正常的(一般的)EXE image。大部份PE 文件都含此值。

2.MajorLinkerVersion、MinorLinkerVersion:产生此PE文件的链接器的版本。以十进制而非十六进制表示。例如2.23 版。

3.SizeOfCode:所有code section 的总和大小。大部分程序只有一个 code section,所以此域通常就是 .text section 的大小。 
4.SizeOfInitializedData:所有包含初始化内容的 sections(但不包括 code section)的总和大小。似乎不包括 initialized data sections 在内。

5.SizeOfUninitializedData:所有需要PE装载器将内存地址空间赋予它但是却不占用硬盘空间的所有 sections 的大小总和。这些 sections 在程序启动时并不需要特别内容,所以导致 Uninitialized Data 这种叫法。为初始化的内容通常放在 .bss section 中。

6.AddressOfEntryPoint:这是PE文件开始执行的位置。这是一个RVA,通常会落在 .text section.此域适用于 exe 或 dll。

7.BaseOfCode:一个RVA,表示程序中的 code section 从何开始。code section 通常在 data section 之前,在PE 表头之后。微软链接器所产生的exes 中,此值通常为0x1000。Borland 的TLINK32则通常指定此值为0x10000。因为预设情况下TLINK时以64k为对齐粒度的,而MS用的是4k。

8.BaseOfData:一个RVA,表示程序中的 data section 从何开始。data section 一般位于code section 和 PE 表头之后。
 
9.ImageBase:PE文件的优先装载地址(Base Address)。比如,如果该值是400000h,PE装载器将尝试把文件装到虚拟地址空间的400000h处。字眼"优先"表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。

10.SectionAlignment:内存中节对齐的粒度。例如,如果该值是4096 (1000h),那么每节的起始地址必须是4096的倍数。若第一节从401000h开始且大小是10个字节,则下一节必定从402000h开始,即使401000h和402000h之间还有很多空间没被使用。

11.FileAlignment:文件中节对齐的粒度。例如,如果该值是(200h),,那么每节的起始地址必须是512的倍数。若第一节从文件偏移量200h开始且大小是10个字节,则下一节必定位于偏移量400h,即使偏移量512和1024之间还有很多空间没被使用或定义。预设值就是0x200h。

12.MajorOperatingSystemVersion/MinorOperatingSystemVersion:使用此可执行程序的操作系统的最小版本。WIN32程序的这两个域通常指定为1.0。

13.MajorSubsystemVersion/MinorSubsystemVersion:WIN32子系统版本。若PE文件是专门为WIN32设计的,该子系统版本必定是4.0否则对话框不会有3维立体感。

14.MajorImageVersion/MinorImageVersion:使用者自定义的域,允许你拥有不同版本的exe或dll。可以利用链接器的 /VERSION 选项设定其值。例如:LINK /VERSION:2.0 myobj.obj。

15.Reserved1:似乎总是0。

16.SizeOfImage:内存中整个PE映像体的尺寸。它是所有头和节经过节对齐处理后的大小。也就是从image base 开始,直到最后一个 section为止。最后一个section 的尾端必需是SectionAlignment 的倍数。
  
17.SizeOfHeaders:所有头 + 节表的大小,也就等于文件尺寸减去文件中所有节的尺寸。可以以此值作为PE文件第一节的文件偏移量。

18.CheckSum:此程序的一个CRC 校验和。PE中此域通常被忽略并被设为0。然而,所有的driver DLLs、所有在开机时载入的DLLs、以及server DLLs 都必须有一个合法的 CheckSum。其演算法可以在IMAGEHLP.DLL中获得。IMAGEHLP.DLL 的代码可以在WIN32 SDK中找到。

19.Subsystem:用来识别PE文件属于哪个子系统。对于大多数Win32程序,只有两类值: Windows GUI 和 Windows CUI (控制台)。WINNT.h中定义如下:

#define IMAGE_SUBSYSTEM_UNKNOWN          0  Unknown subsystem.
#define IMAGE_SUBSYSTEM_NATIVE           1  不需要子系統(例如驱动程序)
#define IMAGE_SUBSYSTEM_WINDOWS_GUI      2  在Windows GUI 子系统中运行
#define IMAGE_SUBSYSTEM_WINDOWS_CUI      3  在Windows 字符模式子系统中运行(也就是console 应用程序)
#define IMAGE_SUBSYSTEM_OS2_CUI          5  在OS/2 字符模式子系统中运行(也就是OS/2 1.x 应用程序)
#define IMAGE_SUBSYSTEM_POSIX_CUI        7  在Posix 字符模式子系统中运行
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS   8  一个Win9x驱动
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI   9  在Win CE 子系统中运行

20.DllCharacteristics:一组标志位,用来指出dll的初始化函数(例如 DllMain)在什么环境下被调用。这个值总是0,但是操作系统会在四种情况发生式调用dll的初始化函数。此值的四个值的意义如下:

0x0001:当DLL被载入一个进程的地址空间时
0x0002:当一个线程结束时
0x0004:但一个线程开始时
0x0008:当DLL退出时
0x2000:一个WDM驱动

21.SizeOfStackReserve:线程初始堆栈的保留大小。然而并不是所有的这些内存都被系统指定。此值预设为0x100000(1MB)。如果你的程序中调用CreateThread 并指定其堆栈大小为0,获得的线程就有一个与此值相同大小的堆栈。

22.SizeOfStackCommit:一开始就被指定给执行线程初始堆栈的内存数量。微软的链接器预设此值为0x1000(一个page),Borland 的TLINK32把它设为0x2000(两个page)。

23.SizeOfHeapReserve:保留给最初的进程堆(process heap)的虚拟内存数量。这个堆的句柄可以利用GetProcessHeap 获得。并不是所有的这些内存都被指定。

24.SizeOfHeapCommit:一开始就被指定给进程堆(process heap)的内存数量。此值预设为0x1000个字节(位元组)。

25.LoaderFlags:Debug用。可能作用:
a.在开始这个进程之前引发一个中断?
b.在进程被载入之后引发一个除错器执行?

26.NumberOfRvaAndSizes:在DataDirectory(下一个域)数组的成员结构个数。目前的工具总是把此值设为16。

27.DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]:一个IMAGE_DATA_DIRECTORY 结构数组。每个结构给出一个重要数据结构的RVA。数组的第一个元素代表 Exported Function Table(如果有的话)的地址和大小,第二个元素代表Imported Function Table 的地址和大小,依此类推。下面是其顺序的完整列表:

// Directory Entries
#define IMAGE_DIRECTORY_ENTRY_EXPORT   0  // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT   1  // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE   2  // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION  3  // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY   4  // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC  5  // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG   6  // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT  7  // Description String
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR  8  // Machine Value (MIPS GP)
#define IMAGE_DIRECTORY_ENTRY_TLS    9  // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG  10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT  11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT    12 // Import Address Table

96/112   8  Export Table Export Table address and size. 
104/120  8  Import Table Import Table address and size 
112/128  8  Resource Table Resource Table address and size. 
120/136  8  Exception Table Exception Table address and size. 
128/144  8  Certificate Table Attribute Certificate Table address and size. 
136/152  8  Base Relocation Table Base Relocation Table address and size. 
144/160  8  Debug Debug data starting address and size. 
152/168  8  Architecture Architecture-specific data address and size. 
160/176  8  Global Ptr Relative virtual address of the value to be stored in the global pointer register. Size member of this structure must be set to 0. 
168/184  8  TLS Table Thread Local Storage (TLS) Table address and size. 
176/192  8  Load Config Table Load Configuration Table address and size. 
184/200  8  Bound Import Bound Import Table address and size. 
192/208  8  IAT Import Address Table address and size. 
200/216  8  Delay Import Descriptor Address and size of the Delay Import Descriptor. 
208/224  8  COM+ Runtime Header COM+ Runtime Header address and size 
216/232  8  Reserved

五、Section Table(节表)

 节表是紧挨着 PE Header 的一结构数组。该数组成员的数目由 File Header (IMAGE_FILE_HEADER) 结构中 NumberOfSections 域的域值来决定。节表成员结构又命名为 IMAGE_SECTION_HEADER(四十字节)。其结构定义:

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
        DWORD   PhysicalAddress;
        DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

 IMAGE_SECTION_HEADER 结构成员含义:

1.IMAGE_SIZEOF_SHORT_NAME:不超过8字节的节名。节名仅是个标记,我们选择任何名字甚至空着也行,不能用null结束。命名不是一个ASCIIZ字符串,所以不用null结尾。

2.PhysicalAddress:指定文件地址。

3.VirtualSize:这个域的意义与程序类型有关。如果是EXE,代表当节被装入内存之后的大小总和,这是在它们被调整为最接近文件对齐粒度的倍数之前的大小。稍后的SizeOfRawData则是调整后的大小。对于OBJ文档,这个域没有意义。

4.VirtualAddress:本节的RVA(相对虚拟地址)。PE装载器将节映射至内存时会读取本值,因此如果域值是1000h,而PE文件装在地址400000h处,那么本节就被载到401000h。微软把第一个Section 的此域值设为0x1000h。对于OBJ文档,此域没意义,总为0。
 
5.SizeOfRawData:经过文件对齐处理后节尺寸,PE装载器提取本域值了解需映射入内存的节字节数。 假设一个文件的文件对齐尺寸是0x200,如果前面的 VirtualSize 域指示本节长度是0x388字节,则本域值为0x400,表示本节是0x400字节长。在obj中,这个与表示有编译器或 組譯器 指定的真正的section 大小。

6.PointerToRawData:这是本节基于文件的偏移量,PE装载器通过本域值找到节数据在文件中的位置。 如果你自己以内存映射的方式应设了一个PE程序(而不是由操作系统的装载器载入),那么你就必须根据此值找到本节的信息,而不是根据VirtualAddress 中的RVA值。

7.PointerToRelocations:在OBJs中,这是以程序开始为基准的偏移量,用来指向section 的重定位信息。每个OBJ section 的重定位信息紧跟在section 信息之后。在EXEs中,这个域(一记下一个域)没有意义,总是为0。但链接器产生一个EXE,它会决定大部分的待修正纪录(fixups),只剩下基址的重定位地址以及 imported 函数的重定位地址,留待载入时在解决。两份相同的信息放在 base relocation section 和imported function section 之中,所以EXEs 不需要在每一个 section 之后又有重定位信息。

8.PointerToLinenumbers:行号表的偏移量(以程序开始为基准)。行号表与源代码行号和其被映射到内存中的位置有关。在EXE文件中,行号信息被放在程序的最尾端。如果没有COFF行好,设为0。

9.NumberOfRelocations:重定位表格(由PointerToRelocations 指向)中的重定位项目的个数。此域只用于OBJ中。EXE中为0。

10.NumberOfLinenumbers:行号表格(由PointerToLinenumbers 指向)中的行号个数。

11.Characteristics:包含标记以指示节属性,比如节是否含有可执行代码、初始化数据、未初始数据,是否可写、可读等。 下面是一些标记:

IMAGE_SCN_TYPE_REG      Reserved. 
IMAGE_SCN_TYPE_DSECT     Reserved. 
IMAGE_SCN_TYPE_NOLOAD    Reserved. 
IMAGE_SCN_TYPE_GROUP     Reserved. 
IMAGE_SCN_TYPE_NO_PAD     Reserved. 
IMAGE_SCN_TYPE_COPY     Reserved. 
IMAGE_SCN_CNT_CODE      Section contains executable code. 
IMAGE_SCN_CNT_INITIALIZED_DATA   Section contains initialized data. 
IMAGE_SCN_CNT_UNINITIALIZED_DATA  Section contains uninitialized data. 
IMAGE_SCN_LNK_OTHER     Reserved. 
IMAGE_SCN_LNK_INFO      Reserved. 
IMAGE_SCN_TYPE_OVER     Reserved. 
IMAGE_SCN_LNK_COMDAT     Section contains COMDAT data.  
IMAGE_SCN_MEM_FARDATA     Reserved. 
IMAGE_SCN_MEM_PURGEABLE    Reserved. 
IMAGE_SCN_MEM_16BIT     Reserved. 
IMAGE_SCN_MEM_LOCKED     Reserved. 
IMAGE_SCN_MEM_PRELOAD     Reserved. 
IMAGE_SCN_ALIGN_1BYTES     Align data on a 1-byte boundary.  
IMAGE_SCN_ALIGN_2BYTES     Align data on a 2-byte boundary.  
IMAGE_SCN_ALIGN_4BYTES     Align data on a 4-byte boundary.  
IMAGE_SCN_ALIGN_8BYTES     Align data on a 8-byte boundary.  
IMAGE_SCN_ALIGN_16BYTES    Align data on a 16-byte boundary.  
IMAGE_SCN_ALIGN_32BYTES    Align data on a 32-byte boundary.  
IMAGE_SCN_ALIGN_64BYTES    Align data on a 64-byte boundary.  
IMAGE_SCN_LNK_NRELOC_OVFL    Section contains extended relocations. 
IMAGE_SCN_MEM_DISCARDABLE    Section can be discarded as needed. 
IMAGE_SCN_MEM_NOT_CACHED    Section cannot be cached. 
IMAGE_SCN_MEM_NOT_PAGED    Section cannot be paged. 
IMAGE_SCN_MEM_SHARED     Section can be shared in memory. 
IMAGE_SCN_MEM_EXECUTE     Section can be executed as code. 
IMAGE_SCN_MEM_READ      Section can be read. 
IMAGE_SCN_MEM_WRITE     Section can be written to.


 遍历节表的步骤:

1.PE文件有效性校验。
2.定位到 PE Header 的起始地址。 
3.从 file Header 的 NumberOfSections域获取节数。 
4.通过两种方法定位节表: ImageBase+SizeOfHeaders 或者 PE header的起始地址+ PE header结构大小。 (节表紧随 PE Header)。如果不是使用文件映射的方法,可以用SetFilePointer 直接将文件指针定位到节表。节表的文件偏移量存放在 SizeOfHeaders域里(SizeOfHeaders 是 IMAGE_OPTIONAL_HEADER 的结构成员) 。
5.处理每个 IMAGE_SECTION_HEADER 结构。


六、Import Table(导入表)

6.1、导入函数:

 一个导入函数是被某模块调用的但又不在调用者模块中的函数,因而命名为"import(导入)"。导入函数实际位于一个或者更多的DLL里。调用者模块里只保留一些函数信息,包括函数名及其驻留的DLL名。

    PE 程序被载入到内存之前,存放在 PE 文件的 .data 中的内容是给装载器用来决定函数位置并修补它们以便完成image 用的。而在被载入之后,.idata内含有的是指向 EXE/DLL 的导入函数的指针。

6.2、Data Directory:

 Data Directory 是一个 IMAGE_DATA_DIRECTORY 结构数组,共有16个成员。Data Directory 包含了PE文件中各重要数据结构的位置和尺寸信息。 每个成员包含了一个重要数据结构的信息。

 Data Directory 的每个成员都是 IMAGE_DATA_DIRECTORY 结构类型的,其定义如下所示:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

 IMAGE_DATA_DIRECTORY 结构成员含义:
 
1.VirtualAddress: 实际上是数据结构的相对虚拟地址(RVA)。比如,如果该结构是关于Import Symbols的,该域就包含指向IMAGE_IMPORT_DESCRIPTOR 数组的RVA。

2.Size: 含有VirtualAddress所指向数据结构的字节数。

6.3、找寻PE文件中重要数据结构的一般方法:

1、从 DOS Header 定位到 PE Header。
2、从 Optional Header 读取 Data Directory 的地址。 
3、IMAGE_DATA_DIRECTORY 结构尺寸乘上找寻结构的索引号:比如您要找寻Import Symbols的位置信息,必须用IMAGE_DATA_DIRECTORY 结构尺寸(8 bytes)乘上1(Import Symbols 在 Data Diectory 中的索引号)。 
4、将上面的结果加上 Data Diectory 地址,我们就得到包含所查询数据结构信息的 IMAGE_DATA_DIRECTORY 结构项。

6.4、导入表:

 Data Directory 数组第一项的 VirtualAddress 包含导入表地址。导入表实际上是一个 IMAGE_IMPORT_DESCRIPTOR 结构数组。每个结构包含PE文件导入函数的一个相关DLL的信息。该数组以一个全0的成员结尾。

 IMAGE_IMPORT_DESCRIPTOR结构组成:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    };
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

 IMAGE_IMPORT_DESCRIPTOR 结构成员含义:
 
1.结构第一项是一个union子结构。事实上,这个union子结构只是给 OriginalFirstThunk 增添了个别名,您也可以称其为"Characteristics"。该成员项含有指向一个 IMAGE_THUNK_DATA 结构数组的RVA。

2.TimeDateStamp:程序生成的时刻。此域通常为0。微软的 BIND 程序可以将此 IMAGE_IMPORT_DESCRIPTOR 所对应的dll的生成时刻写到这里来。

3.ForwarderChain:此域涉及到 forwarding(转交),意味着一个dll 函数在调用另一个 dll。例如,在 WINNT 中,Kernel32.dll 将它的某些输出函数转交给 NTDLL.dll。应用程序可能以为它调用 Kernel32.dll,而事实上它调用的事NTDLL.dll。这个域中含有一个索引,指向 FirstThunk 数组。被这个索引所指定的函数就是一个转交函数。

3.Name:含有指向DLL名字的RVA,即指向DLL名字的指针,也是一个ASCII字符串。

4.FirstThunk:与 OriginalFirstThunk 非常相似,它也包含指向一个 IMAGE_THUNK_DATA 结构数组的RVA(当然这是另外一个IMAGE_THUNK_DATA 结构数组)。

 IMAGE_IMPORT_DESCRIPTOR 数组中,最重要的部分是 imported DLL 的名称以及两个 IMAGE_THUNK_DATA 数组。每个 IMAGE_THUNK_DATA 对应一个导入函数。在exe中,两个数组(分别由 Characteristics 和 FirstThunk 域指向)平行存在,并且都以 NULL 位结束符。

 为什么需要两个平行数组?第一个数组(由 Characteristics 指向)从不被修改,有时它被称为 hint-name table。第二个数组(由 FirstThunk 指向)则被装载器改写。装载器一一检查每一个 IMAGE_THUNK_DATA 并且找出它所记录的函数的地址,然后把地址写入 IMAGE_THUNK_DATA 这个 DWORD 之中。由于这个 IMAGE_THUNK_DATA 数组内容已经被装载器改写为输入函数的地址,所以它又被叫做 Import Address Table(IAT)。IAT 是一个可写区域。API Hook 就利用到这一特性。PE装载器载入PE后,FirstThunk 指向的 IMAGE_THUNK_DATA 被改写,而 Characteristics 所指向的 IMAGE_THUNK_DATA 没有被改写。所以若还反过头来查找导入函数名,PE装载器还能够根据 Characteristics 所指向的 IMAGE_THUNK_DATA 找寻到函数名。 
 
6.4、IMAGE_THUNK_DATA:

 IMAGE_THUNK_DATA是一个DWORD类型的集合。通常我们将其解释为指向一个 IMAGE_IMPORT_BY_NAME 结构的指针。注意 IMAGE_THUNK_DATA 包含了指向一个 IMAGE_IMPORT_BY_NAME 结构的指针,而不是结构本身。

 IMAGE_THUNK_DATA 结构定义:
 
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        PBYTE   ForwarderString;
        PDWORD  Function;
        DWORD  Ordinal;
        PIMAGE_IMPORT_BY_NAME  AddressOfData;
    } u1;
} IMAGE_THUNK_DATA32;

 IMAGE_THUNK_DATA 实在PE被载入之后才被决定的。WIN32装载器使用 IMAGE_THUNK_DATA 的初始内容(可能是函数名称也可能是函数序号)来寻找输入函数的位置。然后装载器就以获得的地址改写 IMAGE_THUNK_DATA 的内容。

6.5、IMAGE_IMPORT_BY_NAME:

 IMAGE_IMPORT_BY_NAME 结构定义:
 
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    BYTE    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

1.Hint:指示本函数在其所驻留DLL的导出表中的索引号。该域被PE装载器用来在DLL的导出表里快速查询函数。该值不是必须的,一些连接器将此值设为0。

2.Name:含有导入函数的函数名。函数名是一个ASCII字符串。注意这里虽然将Name的大小定义成字节,其实它是可变尺寸域,只不过我们没有更好方法来表示结构中的可变尺寸域。这个结构被提供用于查阅描述名字的结构。

 有些情况下一些函数仅由序数导出,也就是说不能用函数名来调用它们,只能用它们的位置来调用。此时,调用者模块中就不存在该函数的 IMAGE_IMPORT_BY_NAME 结构。不同的,对应该函数的 IMAGE_THUNK_DATA 值的低位字指示函数序数,而最高二进位 (MSB)设为1。例如,如果一个函数只由序数导出且其序数是1234h,那么对应该函数的 IMAGE_THUNK_DATA 值是80001234h。Microsoft提供了一个方便的常量来测试dword值的MSB位,就是 IMAGE_ORDINAL_FLAG32,其值为80000000h。

6.6、列出某个PE文件的所有导入函数步骤:

1、校验文件是否是有效的PE。 
2、从 DOS Header 定位到 PE Header。 
3、获取位于 OptionalHeader 数据目录地址。 
4、转至数据目录的第二个成员提取其VirtualAddress值。 
5、利用上值定位第一个 IMAGE_IMPORT_DESCRIPTOR 结构。 
6、检查 OriginalFirstThunk值。若不为0,顺着 OriginalFirstThunk 里的RVA值转入那个RVA数组。若 OriginalFirstThunk 为0,就改用FirstThunk值。有些连接器生成PE文件时会置OriginalFirstThunk值为0,这应该算是个bug。不过为了安全起见,我们还是检查 OriginalFirstThunk值先。 
7、对于每个数组元素,我们比对元素值是否等于IMAGE_ORDINAL_FLAG32。如果该元素值的最高二进位为1,那么函数是由序数导入的,可以从该值的低字节提取序数。 
8、如果元素值的最高二进位为0,就可将该值作为RVA转入 IMAGE_IMPORT_BY_NAME 数组,跳过 Hint 就是函数名字了。 
9、再跳至下一个数组元素提取函数名一直到数组底部(它以null结尾)。现在我们已遍历完一个DLL的导入函数,接下去处理下一个DLL。 
10、即跳转到下一个 IMAGE_IMPORT_DESCRIPTOR 并处理之,如此这般循环直到数组见底。(IMAGE_IMPORT_DESCRIPTOR 数组以一个全0域元素结尾)。

6.7、Bound Import:

 当PE装载器装入PE文件时,检查导入表并将相关DLLs映射到进程地址空间。然后象我们这样遍历IMAGE_THUNK_DATA 数组并用导入函数的真实地址替换IMAGE_THUNK_DATAs 值。这一步需要很多时间。如果程序员能事先正确预测函数地址,PE装载器就不用每次装入PE文件时都去修正IMAGE_THUNK_DATAs 值了。Bound import就是这种思想的产物。
 
 Microsoft 出品的类似Visual Studio的编译器多提供了bind.exe这样的工具,由它检查PE文件的导入表并用导入函数的真实地址替换IMAGE_THUNK_DATA 值。当文件装入时,PE装载器必定检查地址的有效性,如果DLL版本不同于PE文件存放的相关信息,或则DLLs需要重定位,那么装载器认为原先计算的地址是无效的,它必定遍历OriginalFirstThunk指向的数组以获取导入函数新地址。

七、Export Table(导出表)

 当PE装载器执行一个程序,它将相关DLLs都装入该进程的地址空间。然后根据主程序的导入函数信息,查找相关DLLs中的真实函数地址来修正主程序。PE装载器搜寻的是DLLs中的导出函数。PE 程序把它的导出函数相关信息放在.edata 中。

 DLL/EXE要导出一个函数给其他DLL/EXE使用,有两种实现方法: 通过函数名导出或者仅仅通过序数导出。比如某个DLL要导出名为"GetSysConfig"的函数,如果它以函数名导出,那么其他DLLs/EXEs若要调用这个函数,必须通过函数名,就是GetSysConfig。另外一个办法就是通过序数导出。序数是唯一指定DLL中某个函数的16位数字,在所指向的DLL里是独一无二的。例如在上例中,DLL可以选择通过序数导出,假设是16,那么其他DLLs/EXEs若要调用这个函数必须以该值作为GetProcAddress调用参数。这就是所谓的仅仅靠序数导出。

7.1 导出表是数据目录的第一个成员,又可称为 IMAGE_EXPORT_DIRECTORY。结构定义:
 
 typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

 IMAGE_EXPORT_DIRECTORY 结构成员含义:

1.Characteristics:此域没有用途,总是为0。

2.TimeDateStamp:程序被生成的时刻。

3.MajorVersion/MinorVersion:无实际用途,0。

4.Name:一个 RVA 值,指向一个 ASCIIZ 字串(dll 名称,如MYDLL.dll)。模块的真实名称。本域是必须的,因为文件名可能会改变。这种情况下,PE装载器将使用这个内部名字。

3.Base:基数,加上序数就是函数地址数组的索引值了。

4.NumberOfFunctions:模块导出的函数/符号总数。

5.NumberOfNames:通过名字导出的函数/符号数目。该值不是模块导出的函数/符号总数,这是由上面的NumberOfFunctions给出。本域可以为0,表示模块可能仅仅通过序数导出。如果模块根本不导出任何函数/符号,那么数据目录中导出表的RVA为0。

6.AddressOfFunctions:模块中有一个指向所有函数/符号的RVAs数组,本域就是指向该RVAs数组的RVA。简言之,模块中所有函数的RVAs都保存在一个数组里,本域就指向这个数组的首地址。

7.AddressOfNames:类似上个域,模块中有一个指向所有函数名的RVAs数组,本域就是指向该RVAs数组的RVA。

9.AddressOfNameOrdinals:RVA,指向包含上述 AddressOfNames数组中相关函数之序数的16位数组。

 导出表的设计是为了方便PE装载器工作。
 
 首先,模块必须保存所有导出函数的地址以供PE装载器查询。模块将这些信息保存在AddressOfFunctions域指向的数组中,而数组元素数目存放在NumberOfFunctions域中。 因此,如果模块导出40个函数,则AddressOfFunctions指向的数组必定有40个元素,而NumberOfFunctions值为40。

 现在如果有一些函数是通过名字导出的,那么模块必定也在文件中保留了这些信息。这些名字的RVAs存放在一数组中以供PE装载器查询。该数组由AddressOfNames指向,NumberOfNames包含名字数目。考虑一下PE装载器的工作机制,它知道函数名,并想以此获取这些函数的地址。至今为止,模块已有两个模块: 名字数组和地址数组,但两者之间还没有联系的纽带。因此我们还需要一些联系函数名及其地址的东东。PE参考指出使用到地址数组的索引作为联接,因此PE装载器在名字数组中找到匹配名字的同时,它也获取了指向地址表中对应元素的索引。而这些索引保存在由AddressOfNameOrdinals域指向的另一个数组(最后一个)中。由于该数组是起了联系名字和地址的作用,所以其元素数目必定和名字数组相同,比如,每个名字有且仅有一个相关地址,反过来则不一定: 每个地址可以有好几个名字来对应。因此我们给同一个地址取"别名"。为了起到连接作用,名字数组和索引数组必须并行地成对使用,譬如,索引数组的第一个元素必定含有第一个名字的索引,以此类推。

7.2 如果我们有了导出函数名并想以此获取地址,可以这么做:

1、定位到PE Header。 
2、从数据目录读取导出表的虚拟地址。 
3、定位导出表获取名字数目(NumberOfNames)。 
4、并行遍历AddressOfNames和AddressOfNameOrdinals指向的数组匹配名字。如果在AddressOfNames 指向的数组中找到匹配名字,从AddressOfNameOrdinals 指向的数组中提取索引值。例如,若发现匹配名字的RVA存放在AddressOfNames 数组的第77个元素,那就提取AddressOfNameOrdinals数组的第77个元素作为索引值。如果遍历完NumberOfNames 个元素,说明当前模块没有所要的名字。 
5、从AddressOfNameOrdinals 数组提取的数值作为AddressOfFunctions 数组的索引。也就是说,如果值是5,就必须读取AddressOfFunctions 数组的第5个元素,此值就是所要函数的RVA。

7.3 假设我们只有函数的序数,那么怎样获取函数地址呢,可以这么做:

1、定位到PE Header。 
2、从数据目录读取导出表的虚拟地址。 
3、定位导出表获取nBase值。 
4、减掉nBase值得到指向AddressOfFunctions 数组的索引。 
5、将该值与NumberOfFunctions作比较,大于等于后者则序数无效。 
6、通过上面的索引就可以获取AddressOfFunctions 数组中的RVA了。

posted on 2012-11-16 12:35  Angelo Lee  阅读(296)  评论(1编辑  收藏  举报