PE里里外外

PE里里外外

2013/11/15 13:00

从PE可执行文件规范说起

PE,Portable Executable是Windows平台的可迁移程序文件,它是DOS平台向Win32平台迁移过程产生的文件格式规范。程序在执行时,会由操作系统调入内存,然后将CPU控制转交给程序。而程序它是数据代码构造而成的。因此装入内存时,系统需要知道它的代码起点在哪?这也就是入口点Entry Point的概念。当然,文件结构远不是入口定义这么简单,在Visual C++ 5.0的头文件夹内,有一个WINNT.H。其中有一段是这样定义的:

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;

上面这段C语言结构体就是MS-DOS 2.0 .EXE兼容文件头结构定义,这个结构占用64个字节,即0x40处开始另一个数据结构。使用Hex编辑工具或记事本直接打开一个程序,会看到“MZ”(0x5A4D) 这样的打头字符。它就是一个幻数 Magic number,这种字符是文件内指示一种格式的标记,因而得名。这时“得名”是指幻数的因功能而名,而不是指MZ因此功能而得名,事实上MZ是为了纪念这种文件头的发明者Mark Zbikowski而来的!DOS系统在装入程序时,就会读取这个标记,从而识别这是一个可执行文件。然后再读取其它细节内容,如堆栈指针的初始值定义在0x10偏移位置的e_sp。checksum字段是整个文件的求和较验值,填充后,应该使用文件所有WORD总和为0。真正的DOS EXE程序头是不包含e_res[4]及后续字段的,估计这个保留字段的4个字节就是为了防止DOS程序越界而设置的。

PE头分解

看到最后一个字体即偏移0x36的e_lfanew,如注解而言,它是一个指向新的可执行程序头地址的指针。这时为什么提到另外的可执行程序头呢?这是因为,上面这段结构体定义是嵌入PE程序文件头的一个部分,是为了兼容DOS平台,即向后兼容要求而设置的。在更久远的年代,DOS使用的是COM程序,确切地讲,COM是一种不安全的程序文件。它只有代码和数据,没头也没重定位功能,代码要使用的数据地址也是固定在代码的,而且文件第一个字节开始就是二进行机器代码,系统一装入就执行了。在新的Windows平台上,微软改掉这个陋习,给程序加了头定义形成了DOS MZ程序。这种程序具有了重定位relocation功能,这样代码或数据可以装入任意的内地址,透过重定位就可以得到正确的地址了。同时重定位的应用,使得程序文件可以拥有多个代码片段数据片段。这种格式应用十分广泛,在32位平台下也十分常见。但是到x64系统后就不能使用了。Win 3.x下又出现 New Executable 16位的NE格式.exe、.dll文件,这就是一种分段可执行文件。向Win 9x过渡中,又出现了Windows 98专有的LE格式的文件 Linear Executable 线性可执行文件,专用于VxD文件。另一文件,Win32窗口程序的使用,是PE规范产生的直接原因,因为这种图形界面程序无法在DOS平台下运行,但在Windows和DOS兼有的年代,使用带有兼容DOS MZ程序的PE格式规范就成了不二之选。这样万一在DOS下打开Win32程序时就不会让系统挂掉了。目前最新的PE文档中展示经典的格式结构如下:

PE FORMAT COFF Format

例如任意截取一个程序的起始部分的Hex数据表,注意高亮处的关联:

00000000 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 MZ..............
00000010 5C 8F C2 75 2B 4F E4 40 40 00 00 00 00 00 00 00 \..u+O.@@.......
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030 00 00 00 00 00 00 00 00 00 00 00 00 B0 00 00 00 ................
00000040 0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68 ........!..L.!Th
00000050 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F is program canno
00000060 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20 t be run in DOS
00000070 6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00 mode....$.......
00000080 C4 61 6E 53 80 00 00 00 80 00 00 00 80 00 00 00 .anS............
00000090 80 00 00 00 29 00 00 00 52 69 63 68 80 00 00 00 ....)...Rich....
000000A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000B0 50 45 00 00 4C 01 05 00 94 91 7D 52 00 00 00 00 PE..L.....}R....
000000C0 00 00 00 00 E0 00 0A 01 0B 01 05 0A 00 CE 01 00 ................
000000D0 00 B2 00 00 00 00 00 00 00 85 00 00 00 10 00 00 ................
000000E0 00 E0 01 00 00 00 40 00 00 10 00 00 00 02 00 00 ......@.........
000000F0 04 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 ................

如数据所示,DOS程序在PE文件中只是起兼容的作用,正常情况下,它也只是用来显示“This program cannot be run in DOS mode.”别无它用。想起98年刚接触电脑的时候,老熟悉了这句话。但是指向另一个程序头的指针e_lfanew即很重要,它就是指向PE头这个真正程序开始的地址。而兼容DOS的代码又称为DOS Stub,在链接时可以通过设置/STUP:filename来指定自定义的DOS程序代码,这样可以用来实现WIN32+DOS双平台运行的程序。

负责载入可执行程序的系统功能暂且称为加载器,它把磁盘中的文件读入内存,并按程序定义映射到进程的地址空间,它会处理各个部分出现在内存的不同位置。磁盘文件一旦被装入内存中,数据偏移地址就与原始的偏移地址有所不同,但装载器映射处理就是要保证这个过程中程序的功能不受到损伤。装入内存后,PE文件就称为模块 Module。映射文件的起始地址称为模块句柄 hModule,这就是文件映像基址 ImageBase,通过模块句柄可以访问内存中的其他数据。装载器主要处理步骤如下:

  1. 当PE文件被执行时,首先为创建进程分配一个4GB的虚拟地址空间,然后把程序所占用的磁盘空间作为虚拟内存映射到这个4GB的虚拟地址空间中,这里已经在使用虚拟内存了。映射到虚拟地址空间的地址一般是0x400000,因此此时文件所在的磁盘空间已经映射为虚拟内存,所以按程序的角度看,就像真的已经在内容了,装载器只要简单地处理一下要运行的代码就可以了。加载器会检查导入地址表IAT来判断这个模块是否依赖于其它DLL。如果它依赖的DLL还未被加载进那个进程,加载器也将它们映射进内存。这个过程递归进行,直到所有依赖的模块都被映射进内存。
  2. 初始化DLL,初始化例程是在所有模块都被映射进内存之后才被调用的,DLL被映射进内存的顺序并不需要与它们被初始化的顺序一样。装载器在内核中创建进程对象和主线程对象以及其他内容。
  3. 装载器执行PE文件首部所指定地址处的代码,开始执行应用程序主线程。

当程序执行后,会按需求从磁盘读取数据交换到物理内存中,操作系统会根据需要和内存占用情况交换一页或多页。当然,这种交换是双向的,即存在于物理内存中的一部分当前没有被使用的页,也可能被交换到磁盘中。这里会使用相对虚拟地址RVA (Relative virutal address) 和虚拟地址 VA,这些地址是系统在管理的映射地址,并非真实的物理内存地址。

根据前面PECOFF档案图例指示,PE头紧接着DOS程序的MZ头存储。在VC中对应的数据结构中,PE头包括签名Signature、COFF文件头FileHeader和可选文件头 OptionalHeader 这三部分信息,组合在一起是一个叫IMAGE_NT_HEADERS的结构体。因此PE头也可以称之为NT头。写文章时,我使用的是Visual C++ 5.0,请看到过有IMAGE_NT_HEADERS32、IMAGE_NT_HEADERS64的自动略过。

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

其中Signature对应的数据是0x00004550("PE\0\0"),它可以通过前面的MZ头来定位,这里对应前面Hex数据表的0xB0偏移处。至于COFF文件头则是重要的组成,它的结构模型在前面的图列已经展示过,COFF就是通用对象文件结构 Common Object File Format,此处特别注意“对象”这个用词,它是指Image和Object之类的文件,因此得名。其中映像Image文件指的就是PE这类可执行程序文件,这是因为PE文件内的所有数据代码都要映射到系统内的程序独立内存空间,它有4GB寻址空间。即使你的内存条只有1GB,Windows也会虚拟出这么多内存给程序使用,即虚拟内存映射空间。在C++代码文件编译链接过程中,先会被编译成为一个和文件同名的OBJ文件,然后链接器会将这些所有OBJ文件链接成可执行PE格式文件。这里的OBJ文件就是COFF指的目标Object文件。所以映像文件Image和目标文件Object结合才是COFF的完整概念。

COFF文件头

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;
  • Machine字段指示当前机器的各类,对就前面Hex数据表位置偏移为0xB4,根据WINNT.H中的定义,对于Intel 386或其后续处理器及兼容处理器应该取值为0x014C,在Hex数据表中看到的是"4C 01"序列,因为数值百WORD双字节的,使用小头Little endian存储规则,即低8位先存储,高8位后存储。这种从低到高的存储规则是Intel系列CUP的默认规则,而IBM的Power PC则相反。更新的还有0x014d、0x014e分别指示指示Intel 80486 处理器以上、Intel Pentium 处理器以上,完整的列表见最新的PECOFF文档。这个字段是判断程序能否运行的先决条件,如果在一个Intel平台来执行为Alpha CUP开发的程序时,可想而知,微软也只能告诉你:它不是有效的Win32程序!
    #define IMAGE_FILE_MACHINE_UNKNOWN           0
    #define IMAGE_FILE_MACHINE_I386              0x14c   // Intel 386.
    #define IMAGE_FILE_MACHINE_R3000             0x162   // MIPS little-endian, 0x160 big-endian
    #define IMAGE_FILE_MACHINE_R4000             0x166   // MIPS little-endian
    #define IMAGE_FILE_MACHINE_R10000            0x168   // MIPS little-endian
    #define IMAGE_FILE_MACHINE_ALPHA             0x184   // Alpha_AXP
    #define IMAGE_FILE_MACHINE_POWERPC           0x1F0   // IBM PowerPC Little-Endian
    
  • NumberOfSections字段非常重要,它表示节表数量,节表 section table 是PE文件的基本组织结构,比较常见有代码节.text用煤来存放编译后代码,数据节.data用来存放程序使用的数据,还有存储资源用的节,用户自定义的节等等。这些节的数据将紧接着可选头后面存储,接下来将接触它。
  • TimeDateStamp字段存储了程序编译时间,计时从1970.1.1 00:00:00开始的秒数。
  • SizeOfOptionalHeader字段指示文件包含可以选头的字节数。

这里跳过可选头部分,先来看看COFF文件头最后一个字段,功能特性 Characteristics。它是一个标志组合位,可以由以下的位定义组合而成。英文部分就不翻译了,网络可以很方便找到翻译,关键要理解系统装入映像的过程。

#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. fixup externel ref).
#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004  // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008  // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010  // Agressively trim working set
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED            0x0200  // Debugging info stripped from file in a .DBG
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // Copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // Copy and run from the swap, if on NET.
#define IMAGE_FILE_SYSTEM                    0x1000  // System File.
#define IMAGE_FILE_DLL                       0x2000  // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // Bytes of machine word are reversed.

0x0001只在映像文件中使用,它指示映像已经剥去重定位信息,要求加载到首选基址,如果首选基址不可用则,则报错。在系统中SYS和EXE文件,Characteristics字段常取值为0x010E或者0x010F,DLL文件一般为0x210E,它不使用0x0001。因为DLL文件要求良好的兼容性,不能有太多要求。假如DLL可以指定装入地址时,程序装入了两个指定同样的首选地址的DLL,那么系统如何是好?对DLL文件肯定要设置IMAGE_FILE_DLL,所以DLL文件的后缀名改了,系统还是可以识别的。这也就是EXE和DLL的区别,除此以外EXE文件与DLL文件的区别完全是语义上的。

可选头

可选头紧接NT头存储,并在NT头的SizeOfOptionalHeader字段留下字节数信息。虽然它的名字是“可选头部”,但是请确信:这个头部对于PE程序并非可选,而是必需的,它只对编译时的目标文件可选。可选头部包含了很多关于可执行映像的重要信息。例如,初始的堆栈大小、程序入口点的位置、首选基地址、操作系统版本、段对齐的信息等。可选头对应IMAGE_OPTIONAL_HEADER的结构体,由224个字节组成。x64位系统中,还有一个64位的版本IMAGE_OPTIONAL_HEADER64。它由标准部分和Windows平台部分组成,如下:

typedef struct _IMAGE_OPTIONAL_HEADER {

    /* Standard fields. */

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    /* NT additional fields. */

    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文件而言,具有以下功能:

  • Magic幻数字段指示是PE32还是使用64位地址的PE32+格式,对应值为0x10b,0x20b。
  • SizeOfCode指代码大小,如果有多个代码节,则表示总和。
  • SizeOfInitializedData和SizeOfUninitializedData则表示需要初始化和不需要初始化的数据大小。
  • AddressOfEntryPoint是指入口地址,即是入口点,是PE程序代码开始执行的地址,又称为OEP(Original Entry Point),一些黑客程序也是通过修改OEP值来获得对目标程序的控制权从而实施攻击。它的地址是相对映像基址的,装入后会根据基址来重新定位。对于驱动,它指向初始化函数。
  • BaseOfCode代码基址,相对于代码节开始基址。
  • BaseOfData则只在PE32格式中使用,PE32+的64位寻址格式已经不再使用它。

在这里,可选头有一个很重点字段,即最后的数据目录DataDirectory,这个字段也只对映像文件有效。

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

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

数据目录由一个16位的虚拟地址和一个16位Size分别表示一个数据数据表的相对虚拟地址和大小。这里预定义为16个数据目录,但具体数量还要通过可选头的NumberOfRvaAndSizes字段来确认。而每个数据目录用途是确定的,其中打头的两个分别就是导出目录和导入目录,他们记录了程序中导出供其它程序使用的方法,这常用在DLL中使用。导入目录则记录了程序需要用到的API函数,装入程序时会将导入目录内所涉及的DLL一起装入:

#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

节表头

如前面所言,COFF文件头中在NumberOfSections字段指定了节表的数量,节的数据紧接可选头存储。节表的第一行就是一个节表头部,每个节表的地址是不确定的,要通过计算每节数据大小和节表头的大小来确认。表头定义如下:

#define IMAGE_SIZEOF_SHORT_NAME              8

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;

这些节表头存储了每个节表的基本信息,但是重要的还是每个节表存储的数据体!这些是重点内容,由于节表类型较多,数据结构也比较复杂,下面针对不同的节表进行分解。相信看到这里,脑子也胀得差不远了,看个经典PE结构图整理一下思路了。

PE文件框架结构

导出表

导出节表 Export section table,默认又命名为.edata节表,又可以称为导出数据节表,简称导出表,名称存储在Name字段中。导入表则命名为idata,此名称可以使用记事本打开PE文件直接查看到。导出表的作为就是将映像文件内部的程序接口向其它映像公开,使得其它映像通过导入这些已经导出的符号,这些符号可以是某些数据,又可以是函数。导出表中使用了5种基本数据结构,首先来了解最主要的导出目录表Export directory table:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    PDWORD  *AddressOfFunctions;
    PDWORD  *AddressOfNames;
    PWORD   *AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
  • Name 字段是一个指向映像文件名的指针,RVA地址,因为有它,当导出符号所在的映像文件名被修改后,还可以通过它来识别原始文件名。
  • Base 序号基数,这个序号作为导出地址表中的条目序号的基数,通常为1。其它映像在导入符号时,指定导出地址表中条目的序号就可以找到它,这个表是导出表使用的基本数据结构之一,后面要谈到。
  • NumberOfFunctions ,在PECOFF文档中也称为Address Table Entries,它指的是导出地址表中条目的数量,这个表是导出表中使用的基本数据结构之一,在 AddressOfFunctions 字段就是一个指向它的指针。表中包含导出的函数入口或数据地址等。条目的序号加上Base字段的基数形成一个序号标识,导入时可以根据这个序号找到指定的导出条目,即按序号导入。如果条目指定的地址不是导出表之内的话,那么就是一个相对虚拟地址Export RVA导出,被导出的符号加载到内存时,将按相对于映像基址来映射。否则就当作一个向前虚拟地址 Forwarder RVA来处理。此时,条目将指向一个以null结束的字符串,这个字符串必需作为可选头的数据目录字段中的条目存储。字符串可以使用全字符格式如mydll.expfun,或使用指定序号格式,如mydll.#2。
  • NumberOfNames 是指导出名称表 Export name table中的条目数量,也同样是指导出序数表 Export ordinal table中条目数量。
  • AddressOfNames 是一个指针,指向导出名称表 Export Name Table,这个表是真正存储导出符号名称的地方,也是导出表中使用的第5种数据结构。正如前面所言,只有名称指针表中存在指向它的名称才会导出,这些字符串是以null结束的ASCII字符串。这些导出名字就是其它映像文件导入时使用的名称,这些有导出名称的导出符号,结合名称指针表和导出序数表相结合一起才可以正确导出。
  • AddressOfNameOrdinals 这个字段指向一个导出序数表 Export Ordinal Table,这又是导出表的一个基本数据结构。它是一个存储16位元素的数组,这些条目存储一个序号,这序号加上Base这个字段的基数就形成了其他映像导入时使用的索引值,正如前面所言,使用这个索引来导入就是按序号导入

对比PECOFF文档,在Visual C++ 5.0中的头定义中,并没有发现名称指针表 name pointer table,这个表是一个存放32位指针元素的数组,这些指针都按其指向的地址以词法排序过的,以便二进制搜索。名称只有存在名称指针指向它时才会被定义导出。

导入表

导入表又命名为.idata节表,当其它映像文件通过名称导入符号,通过动态链接,这些符号就可以由其它程序调用了。通俗地理解就是在程序中声明要使用某个DLL链接库的函数,这些声明信息经过编译后就是导入表了。加载器加载程序执行时,检测到导入表有函数请求,就会主动装入所要求的DLL文件,并按其内部的导出表找到所需要的函数。一个典型的导入表如下所示:

导入目录表 Import Directory Table
...
空目录 Null Directory Entry
DLL文件A查询表 DLL1 Import Lookup Table
...
Null
DLL文件B查询表 DLL2 Import Lookup Table
...
Null
....
名字提示表 Hint‐Name Table

导入表中首先以一个导入目录表开始,它包含用来定位DLL链接库入口的信息。它包含了一个存放目录条目的数组,每一个条目对应一个被引用到的DLL映像。最后还有一个空的条目,即填充null数据的目录条目作为结束。如下罗列的几个和导入表密切相关的结构体定义,而导入目录表的每个数据条目结构就是一个导入描述结构体IMAGE_IMPORT_DESCRIPTOR,这里简称为导入描述体

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    BYTE    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
typedef struct _IMAGE_THUNK_DATA {
    union {
        PBYTE  ForwarderString;
        PDWORD Function;
        DWORD Ordinal;
        PIMAGE_IMPORT_BY_NAME AddressOfData;
    } u1;
} IMAGE_THUNK_DATA;
typedef IMAGE_THUNK_DATA * PIMAGE_THUNK_DATA;
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;                // 0 for terminating null import descriptor
        PIMAGE_THUNK_DATA OriginalFirstThunk;   // RVA to original unbound IAT
    };
    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;
    PIMAGE_THUNK_DATA FirstThunk;           // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
// New format import descriptors pointed to by DataDirectory[ IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT ]

typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
    DWORD   TimeDateStamp;
    WORD    OffsetModuleName;
    WORD    NumberOfModuleForwarderRefs;
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR,  *PIMAGE_BOUND_IMPORT_DESCRIPTOR;

先来了解一下导入表中要使用的两个最基本的数据结构,IMAGE_IMPORT_BY_NAME 和 IMAGE_THUNK_DATA。前者对应了导入表图例中的 名称提示表,在导入表中只需要一个就够了,它在第一个字段存放导出名称指针的索引值,第二个字段存储需要导入的符号名称,是以null结束的ASCII字符串,因此是变长的,且区分大小写。加载器在导入这些符号时,会先按给定的索引值查找目标符号,如果没有找到就按给出的符号名称查找导出表中对应的导出名称,再取得索引,将目标符号导入,即按序数导入;后者则对应导入查询表的数据条目,它是一个联合体,这个联合体或作为 Ordinal 存储导出符号的序数,或作为 AddressOfData 存放一个指向名称提示表的指针。

导入查询表也称为导入地址表 IAT Import Address Table,在文件绑定之前,两者是等价的。加载一个被绑定的可执行文件时,加载器可以跳过查找每个导入函数并覆盖IAT这一步。因为IAT中已经是正确的地址了。在绑定期间,导入地址表的条目会被PE32的32‐bit或者PE32+的64‐bit符号地址覆盖,这个地址是实地址,尽管在加载器中常称为虚拟地址。

导入描述体中,第一个字段是一个DWORD长度的联合体,它作为 OriginalFirstThunk 时,表示为一个RVA指针,指向了另一个结构体 IMAGE_THUNK_DATA ,这个结构体就包含了要导入符号的名称或序数。TimeDateStamp字段存储时间戳,除非DLL已经绑定,这时设置为DLL的时戳,否则就设置为0。ForwarderChan 导向链,指向第一个导向链引用的索引号。Name字段是字符串指针,指向一个DLL内导出名称ASCII字符串的相对虚拟地址。

至此,PE的知识点已经基本把握,其它内容可以参考PECOFF文档或其它相关材料。虽然PE是微软自家的产品,但是MS的资料也不是很完整的,不知道是不是资料管理人员有不满啊。

参考资料

  1. Wiki COM文件参考:http://en.wikipedia.org/wiki/COM_file
  2. DOS EXE Format:http://www.delorie.com/djgpp/doc/exe/
  3. Wiki Relocation Table参考:http://en.wikipedia.org/wiki/Relocation_table
  4. Wiki EXE MZ文件参考:http://en.wikipedia.org/wiki/DOS_executable
  5. Wiki New Executable文件参考:http://en.wikipedia.org/wiki/New_Executable
  6. Wiki Linear Executable文件参考:http://en.wikipedia.org/wiki/Linear_Executable
  7. PE文件框架结构的图片组织:http://t.cn/8DFHliv
  8. Windows加载器与模块初始化:http://www.cnblogs.com/dubingsky/archive/2009/06/25/1510940.html
  9. Matt Pietrek Peering Inside the PE:http://msdn.microsoft.com/zh-cn/magazine/ms809762%28en-us%29.aspx
  10. Matt Pietrek An In-Depth Look into the Win32 Portable Executable File Format :http://msdn.microsoft.com/zh-cn/magazine/cc301805.aspx
  11. Matt Pietrek An In-Depth Look into the Win32 Portable Executable File Format part2 :http://msdn.microsoft.com/zh-cn/magazine/cc301808.aspx
  12. Microsoft PE and COFF Specification:http://t.cn/zOndtVV
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值