深入剖析PE文件

来源:互联网 发布:qq抽奖软件 编辑:程序博客网 时间:2024/05/22 16:45

本文有一些过于文字化,适合已对PE有一些了解的童鞋。
如果是初学者,博主推荐:有兴趣的童鞋可以看看这个链接,用实例解释了PE文件的结构,更清晰明了。
http://www.mouseos.com/assembly/06.html。
微笑
一、 基本结构.

 PE 文件的基本结构.(注意: DOS MZ Header 和部分 PE header 的大小是不变的; DOS stub 部分的大小是可变的.)
一个 PE 文件至少需要两个 Section, 一个是存放代码,一个存放数据. NT 上的 PE 文件基本上有 9 个预定义的 Section .分别是: .text, .bss, .rdata, .data, .rsrc, .edata, .idata, .pdata, 和.debug .一些 PE 文件中只需要其中的一部分 Section. 以下是通常的分类:
l 执行代码 Section , 通常命名为: .text (MS) or CODE (Borland)
l 数据 Section, 通常命名为: .data, .rdata, 或.bss(MS) 或DATA(Borland).
l 资源 Section, 通常命名为: .edata
l 输入数据 Section, 通常命名为: .idata
l 调试信息 Section ,通常命名为: .debug
这些只是命名方式,便于识别.通常与系统并无直接关系.通常,一个 PE 文件在磁盘上的映像跟内存中的基本一致.但并不是完全的拷贝. Windows 加载器会决定加载哪些部分,哪些部分不需要加载.而且由于磁盘对齐与内存对齐的不一致,加载到内存的 PE 文件与磁盘上的 PE 文件各个部分的分布都会有差异.
当一个 PE 文件被加载到内存后,便是主人翁们常说的模块 (Module), 其起始地址就是所谓的 HModule.

二、 DOS 头结构.
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;

所有的 PE 文件都是以一个 64 字节的 DOS 头开始.这个 DOS 头只是为了兼容早期的 DOS 操作系统.这里不做详细讲解.只需要了解一下其中几个有用的数据.

1. e_magic :是 MS-DOS 文件头的签名,它的值是:0x5A4D,这个签名在 WinNT.h 中定义为:
                             
#define IMAGE_DOS_SIGNATURE                 0x5A4D      // MZ 表示 MS-DOS 文件头

2. e_lfanew :一个双字数据,为PE 头的离文件头部的偏移量. Windows 加载器通过它可以跳过 DOS Stub 部分直接找到 PE 头.
3. DOS 头后跟一个 DOS Stub 数据,是链接器链接执行文件的时候加入的部分数据,一般是" This program must be run under Microsoft Windows" .这个可以通过修改链接器的设置来修改成自己定义的数据.

三、 PE 头结构.
PE头的数据结构被定义为 IMAGE_NT_HEADERS,它在 WinNT.h 中定义为两个版本,分别是:IMAGE_NT_HEADERS64 (运行于win64位系统上)和 IMAGE_NT_HEADERS32 (运行于win32位系统上)
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

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;

PE头文件包含三部分:

1. Signature : PE 头的标识.双字结构.为50h, 45h, 00h, 00h. 即" PE ".

2. FileHeader : 20 bytes的数据.包含了文件的物理层信息及文件属性.

   IMAGE_FILE_HEADER比较重要的域:

(1) Machine 可以用来判断目标平台,如值为 0x8664 是代表 AMD64(即x64 平台)它也适合 Intel64 平台。
在 WinNT.h 文件里定义了一系列的 Machine 值,如下所示。
#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.
#define IMAGE_FILE_MACHINE_AMD64             0x8664  // AMD64 (K8)
(2) SizeOfOptionalHeader 指出 IMAGE_OPTIONAL_HEADER 结构的大小。
(3) NumberOfSections :定义 PE 文件 Section 的个数.
(4) Characteristics :主要用来标识当前的 PE 文件是执行文件还是 DLL .其各位都有具体的含义.

#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved externel references).
#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_LARGE_ADDRESS_AWARE       0x0020  // App can handle >2gb addresses
#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 .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // If Image is on Net, copy and run from the swap file.
#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.


以上定义的含义是:
IMAGE_FILE_RELOCS_STRIPPED     文件中不存在重定位信息

IMAGE_FILE_EXECUTABLE_IMAGE    文件是可执行的
IMAGE_FILE_LINE_NUMS_STRIPPED  不存在行信息
IMAGE_FILE_LOCAL_SYMS_STRIPPED 不存在符号信息
IMAGE_FILE_BYTES_REVERSED_LO   小尾方式??
IMAGE_FILE_32BIT_MACHINE       只在32位平台运行
IMAGE_FILE_DEBUG_STRIPPED      不包含调试信息
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 不能从可移动盘运行
IMAGE_FILE_NET_RUN_FROM_SWAP  不能从网络运行
IMAGE_FILE_SYSTEM              系统文件.不能直接运行
IMAGE_FILE_DLL                 DLL文件
IMAGE_FILE_UP_SYSTEM_ONLY      文件不能在多处理器上运行
IMAGE_FILE_BYTES_REVERSED_HI   大尾方式

3. OptionalHeader :总共 224 个字节.最后 128 个字节为数据目录 (Data Directory) .
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_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

以下是字段的说明:

(l)在 IMAGE_OPTIONAL_HEADER 里,第 1 个域 magic 用来识别文件头是 32 位还是 64 位

      这个 magic 的值在 WinNT.h 的定义如下:
       #define IMAGE_NT_OPTIONAL_HDR32_MAGIC      0x10b
       #define IMAGE_NT_OPTIONAL_HDR64_MAGIC      0x20b

(1)AddressOfEntryPoint :程序入口点地址.但加载器要运行加载的 PE 文件时要执行的第一个指令的地址.它是一个 RVA (相对虚拟地址)地址.一些对 PE 文件插入代码的程序就是修改此处的地址为要运行的代码,然后再跳转回此处原来的地址.(2) ImageBase : PE 文件被加载到内存的期望的基地址.对于 EXE 文件,通常加载后的地址就期望的地址.但是 DLL 却可能是其他的.因为如果这个地址被占,系统就会重新分配一块新的内存,同时会修改此处加载后的地址. EXE 文件通常是 400000h.
(3) SectionAlignment : 每一个 Section 的内存对齐粒度.比如:此值为 4096(1000h) ,那么每一个 Section 的起始地址都应该是 4096(1000h) 的整数倍.如果第一个 Section 的地址是 401000h ,大小为 100 个字节.那么下一个 Section 的起始地址为 402000h. .两个 Section 之间的空间大部分是空的,未用的.
(4) FileAlignment : 每一个 Section 的磁盘对齐粒度.比如,此值为 512(200h) ,那么每一个 Section 在文件内的偏移位置都是 512(200h) 的整数倍.与SectionAlignment 同理.
(5) SizeOfImage : PE 文件在内存空间整个映像的大小.包含所有的头及按 SectinAlignment 对齐的所有的 Section .
(6) SizeOfHeaders : 所有的头加上 Section 表的大小.也就是文件大小减去文件中所有 Section 的大小.可以用这个值获取 PE 文件中第一 Section 的位置.
(7) DataDiretory : 16 个IMAGE_DATA_DIRECTORY 结构的数组.每一个成员都对应一个重要的数据结构,比如输入表,输出表等.
有两个地方需要注意:
A. 如果 PE header 里的最后两个字段被赋予一个伪造的值的话,比如:
n LoaderFlags = ABDBFFFFh ( 其默认值为 0)
n NumberOfRvaAndSizes = DFFDEEEEh ( 其默认值为 10h)
一些调试工具或反编译工具会认为这个 PE 文件是损坏的.有的会直接执行,如果是病毒的话,就会被直接感染;有的则会重启工具.所以最好在查看调试一个 PE 文件前,先看一下这里的取值是否被人赋予一个伪造的很大的值.如果是的话,先修改成默认的值.
l 有人可能注意到在一些 PE 文件 (MS 的链接器链接的 PE 文件 ) 的DOS Stub 部分跟 PE header 部分之间存在一部分垃圾数据.标识为其倒数第二非 0 的双字节是一个 "Rich " .这部分数据包含了一些加密数据,来标识编译这个 PE 文件的组件.可用来检举某些病毒程序所编译的程序来自哪台机器.
四、 数据目录结构 (Data Directory) .
DataDirectory 是OptionalHeader 的最后 128 个字节,也是 IMAGE_NT_HEADERS 的最后一部分数据.它由 16 个IMAGE_DATA_DIRECTORY 结构组成的数组构成. IMAGE_DATA_DIRECTORY 的结构如下:
每一个 IMAGE_DATA_DIRECTORY 都是对应一个 PE 文件重要的数据结构.他们分别如下:
VirtualAddress 指的是对应数据结构的 RVA 地址; iSize 指的是对应数据结构的大小 ( 字节单位 ) .一个 PE 文件一般只包含其中的一部分,也就是其中一部分数据结构是有数据的;另一部分则都是 0 .比如, EXE 文件一般都存在 IMAGE_DIRECTORY_ENTRY_IMPORT( 输入表 ), 而不存在 IMAGE_DIRECTORY_ENTRY_EXPORT( 输出表 ) .而DLL 则两者都包含.
五、 Section 表.
Section 表紧跟在 PE header 后面.由IMAGE_SECTION_HEADER 数据结构组成的数组.每一个包含了对应 Section 在PE 文件中的属性和偏移位置.

这里不是所有的成员都是有用的.
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;
#define IMAGE_SIZEOF_SECTION_HEADER          40

l Name 1 : 块名,这是一个 8 位ASCII 码名,用来定义块名.多数块名以一个 "." 开始(如.text) ,尽管许多 PE 文档都认为这个 "." 实际上并不是必须的.值得注意的是,如果块名超过 8 位,则最后的 NULL 不存在.带有一个 "$" 的区块名字会从链接器那里得到特殊的对待,前面带 "$" 的相同名字的区块被合并,在合并后的区块中它们是按 "$" 后面的字符字母顺序进行合并的.
l Misc.VirtualSize : 指出实际的、被使用的区块大小.如果 VirtualSize 大于 SizeOfRawData, 那么 SizeOfRawData 来自于可执行文件初始化数据的大小,与VirtualSize 相差的字节用 0 填充.这个字段在 OBJ 文件中设为 0 .
l VirtualAddress : 该块装载到内存中的 RVA .这个地址是按照内存页对齐的,它的数值总是 SectionAlignment 的整数倍.在MS 工具中,第一块的默认 RVA 为1000H. 在OBJ 中,该字段没意义.如果该值为 1000H, PE 文件被加载到 400000H ,那么该 Section 的起始地址为 401000H .
l SizeOfRawData : 该块在磁盘文件中所占的大小.在可执行文件中,这个值必须是 PE 头部指定的文件对齐大小的倍数.如果是 0, 则说明区块中的数据是未初始化的.该块在磁盘文件中所占的大小 , 这个数值等于 VirtualSize 字段的值按照 FileAlignment 的值对齐以后的大小.例如, FileAlignment 的大小为 1000H, 如果 VirtualSize 中的块长度为 2911, 则SizeOfRawData 为3000H}
l PointerToRawData : 该块在磁盘文件中的偏移.对于可执行文件,这个值必须是 PE 头部指定的文件对齐大小的倍数.
l PointerToRelocations : 这部分在 EXE 文件中无意义.在OBJ 文件中,表示本块重定位信息的偏移量.在OBJ 文件中如果不是零,则会指向一个 IMAGE_RELOCATION 的数据结构.
l NumberOfRelocations : 由PointerToRelocations 指向的重定位的数目.
l NumberOfLinenumbers : 由NumberOfRelocations 指向的行号的数目,只在 COFF 样式的行号被指定时使用.
l Characteristics : 块属性,该字段是一组指出块属性(如代码 / 数据 / 可读 / 可写等)的标志.多个标志值通过 OR 操作形成 Characteristics 的值.这些标志很多都可以通过链接器 /SECTION 选项设置.

数据位在 Windows.inc 中的预定义
为1时的含义
IMAGE_SCN_CNT_CODE (00000020H)
节中包含代码
IMAGE_SCN_CNT_INITIALIZED_DATA (00000040H)
节中包含已初始化数据
IMAGE_SCN_CNT_UNINITIALIZED_DATA (00000080H)
节中包含未初始化数据
IMAGE_SCN_MEM_DISCARDABLE (02000000H)
节中的数据在进程开始后将被丢弃
IMAGE_SCN_MEM_NOT_CACHED (04000000H)
节中的数据不会经过缓存
IMAGE_SCN_MEM_NOT_PAGED (08000000H)
节中的数据不会被交换到磁盘
IMAGE_SCN_MEM_SHARED (10000000H)
节中的数据将被不同的进程所共享
IMAGE_SCN_MEM_EXECUTE (20000000H)
映射到内存后的页面包含可执行属性
IMAGE_SCN_MEM_READ (40000000H)
映射到内存后的页面包含可读属性
IMAGE_SCN_MEM_WRITE (80000000H)
映射到内存后的页面包含可写属性
六、 PE 文件各个 Section.
PE 文件的 Sections 部分包含了文件的内容.包括代码,数据,资源和其他可执行信息.每一个 Section 由一个头部和一个数据部分组成.所有的头部都存放在紧跟 PE header 后的 Section 表内.
1. 执行代码.
在NT Windows 系统内,所有的 PE 文件的代码段都存放在一个 Section 内,通常命名为 .text(MS) 或CODE(Borland) .这一段包含了早先提起的 AddressOfEntryPoint 多指地址的指令及输入表中的 jump thunk table .
2. 数据.
l .bss 段存放未初始化的数据,包括函数内或源模块内声明的静态变量.
l .rdata 段存放只读数据,比如常字符串,手机跟踪器,常量,调试指示信息.
l .data 段存放其他所有的数据(除了自动化变量,其存放在栈中).比如程序的全局变量.
3. 资源.
.rsrc 段包含了一个模块的资源信息.以资源树的结构存放数据.需要用工具来查看.
4. 输出数据.
.edata 段包含了 PE 文件的输出目录 (Export Directory) .
5. 输入数据.
.idata 包含了 PE 文件的输入目录和输入地址表.
6. 调试信息.
调试信息存放在 .debug 段. PE 文件也支持单独的调试文件. Debug 段包含调试信息,但是调试目录却存放在 .rdata 内.
7. 线程局部存储. (TLS)
Windows 支持每一个进程包含多个线程.每一个线程有其私有的存储空间(TLS)去存放线程自身的数据.链接器都会为进程创建一个 .tls 段来存放 TLS 模板.当进程创建一个线程时,系统就会按照这个模板创建一个线程私有的局部存储空间.
8. 基重定位.
当加载器加载 PE 文件到内存的时候,有时候不一定是其预期的基地址.那么就需要调整内部指令的相对地址.所有需要调整的地址都存放在 .reloc 段内.
七、 输出 Section.
这个 Section 跟DLL 关系比较密切. DLL 一般定义两种函数,内部使用的,和输出到外部给其他调用程序使用的.输出到外部的函数就存储在这个 Section 内.
DLL 输出函数分两种方式,通过名称和通过序号输出.当其他程序需要调用 DLL 的时候,调用 GetProcAddress ,通过设置需要调用的函数名称或函数序号可以调用 DLL 内部输出的函数.
那么 GetProcAddress 是怎么获取 DLL 中真正的输出函数地址呢?以下是详细的解说.
PE 头的数据目录 (DATA DIRECTORY) 数组的第一个成员对应的(通过其中的 RVA 地址可获得)数据结构是 IMAGE_EXPORT_DIRECTORY( 这里称为输出目录 ) .
成员
大小
描述
Characteristics
DWORD
未定义,总是 0
TimeDateStamp
DWORD
输出表的创建时间.与IMAGE_NT_HEADER.FileHeader.TimeDateStamp 有相同的定义
MajorVersion
WORD
输出表的主版本号.未使用,为0MinorVersion
DWORD
输出表的次版本号.未使用,为0nName
DWORD
指向一个 ASCII 字符串的 RVA ,这个字符串是与这些输出函数关联的 DLL 的名称(比如, Kernel32.dll ).这个值必须定义,因为如果 DLL 文件的名称如果被修改,加载器将使用这里的名称.
nBase
DWORD
这个字段包含用于这个可执行文件输出表的起始序数值(基数).正常情况下为 1 ,但不是一定是.当通过序数来查询一个输出函数时,这个值会被从序数里减去.(比如,如果 nBase = 1, 被查询的函数的序数是 3, 那么这个函数在序号表的索引是 3 -1 = 2 ).
NumberOfFunctions
DWORD
输出地址表 (EAT) 的条目数.其中一些条目可能是 0 ,意味着这个序数值没有代码和数据输出.
NumberOfNames
DWORD
输出名称表 (ENT) 的条目数.这个值总是大于或等于 NumberOfFunctions .小于的情况发生在符号只通过序数来输出时.另外,当被赋值的序数里有数字间隔时也会有小于的情况.这个值也是输出序数表的长度.
AddressOfFunctions
DWORD
输出地址表 (EAT) 的RVA .输出地址表本身是一个 RVA 数组,数组中的每一个非零的 RVA 都对应一个被输出的符号.
AddressOfNames
DWORD
输出名称表 (ENT) 的RVA .输出名称表本身是一个 RVA 数组.数组中的每一个非零的 RVA 都向一个 ASCII 字符串.每一个字符串都对应一个通过名称输出的符号.这个表是排序.这允许加栽器在查询一个被输出的符号时可用二进制查找方式.名称的排序是二进制的,而不是按字母.
AddressOfNameOrdinals
DWORD
输出序数表( EOT )的RVA .这个表将 ENT 中的数组索引映射到相应的输出地址条目.
实际上, IMAGE_EXPORT_DIRECTORY 结构指向三个数组和一个 ASCII 字符串表.其中重要的是输出地址表 (EAT, 即AddressOfFunctions 指向的表 ), 输出函数地址指针( RVA )构成了这个表.而ENT 和EOT 则是可以一起合作来获取 EAT 里对应的地址数据.下图演示了这个过程.
这个被加载的 DLL 的名称是 F00.DLL .总共输出了四个函数,其RVA 地址分别为 0x400042 、 0x400156 、 0x401256 和0x400520 .一个外部调用程序需要调用其中一个名为 "Bar" 的函数,那么它先在输出名称表 (ENT) 里查找名称为 Bar 的函数,找到后,根据其在输出序号表 (EOT) 中对应的索引号,获取其中的数值为 EAT 中的索引值,这里是 4, 然后从 EAT 中根据索引 4 获取其真正的 RVA 地址 0x400520 .以下是几个注意点:
l 输出序号表( EOT )的存在就是为了是 EAT 跟ENT 之间产生关联.每一个 ENT 内的成员(函数名)有且只有一个 EAT 内的成员(函数地址)对应.但是一个 EAT 内的成员并不是只有一个 ENT 内的成员对应.比如,有的函数存在别名的话,就会出现多个 ENT 内的成员都对应一个 EAT 内的成员.
l 如果已经获得一个函数的序号值,那么就可以直接到 EAT 内获得其 RVA 地址,而不需要经过 ENT 和EOT 进行查找.但是这样的按序号输出的 DLL 不易于维护.
l 通常情况下, EAT 的个数 (NumberOfFunctions) 必须小于或等于 ENT 的个数 (NumberOfNames) .只有在一个函数按序号输出时(其在 ENT 和EOT 表里没有对应的数据), ENT 的数量才有可能少于 EAT 的数量.比如,总共有 70 个函数输出,但是在 ENT 表里只有 40 个,这就意味着剩余的 30 个函数是靠序号输出的.那么主人翁们如何知道哪些是直接靠序号输出的呢?只有通过排除法来获得.把存在在 EOT 表里的序号从 EAT 里排除出去,剩下的就是靠序号输出的函数.
l 当通过一个序号值来获取 EAT 内的函数 RVA 时,需要把这个序号值减去 nBase 的值来获取在 EAT 表里真正的索引位置.而通过名称查找则不需要这么做.
l 输出转向.某些时候,你从一个 DLL 中调用的一个函数可能位于另一个 DLL 中.这就叫输出转向.比如, Kernel32.dll 中的 HeapAlloc 就是转到调用 NTDLL.dll 中的 RtlAllocHeap .这种转向是在链接的时候,在.DEF 文件中定义一个特殊的指令来实现的.那么当一个函数被转向后,在其所在 EAT 表里对应的数据便不是其地址,而是一个指向表明被转向的 DLL 和函数的 ASCII 字符串的地址指针.
上图就是 Kernel32.dll 的输出函数表,其中 HeapAlloc 的RVA 值0x00009048 就是一个指向 "NTDLL.RtlAllocHeap" 的指针.
八、输入 Section.
输入Section通常位于.idata段内.它包含了所有程序需要用到的来自其他DLL的函数的信息.Windows加载器负责加载所有程序用到的DLL到进程空间.然后为进程找到所有其需要用到的函数的地址.下面描述这个过程:
PE头的数据目录(DATA DIRECTORY)数组的第二个成员对应的(通过其中的RVA地址可获得)数据结构是输入表.输入表是一个 IMAGE_IMPORT_DESCRIPTOR数据结构的数组.没有字段表明这个数组的个数,只是它的最后一个成员的数据都为0.每一个数组成员都对应 一个DLL.
描述
指向输入名称表(INT)的RVA.INT是由IMAGE_THUNK_DATA数据结构构成的数组.数组中的每一个成员定义了一个输入函数的信息,数组最后以一个内容为0的IMAGE_THUNK_DATA结束.
当执行文件不与被输入的DLL进行绑定时,这个字段为0.当以旧的方式绑定时,这个字段包括时间/日期.当以新的样式绑定时,这个字段为-1.
这是第一个被转向的API的索引.老样式绑定的定义.
指向被输入DLL的ASCII字符串的RVA.
指向输入地址表(IAT)的RVA.IAT也是一个IMAGE_THUNK_DATA数据结构的数组.
由上表可知,输入表主要是通过IMAGE_THUNK_DATA这个数据结构导入函数.下面是IMAGE_THUNK_DATA的描述:
这是一个DWORD联合体数据结构.其实这里对输入表有意义的字段只有两个,Ordinal和AddressOfData.当这个DWORD数据的最高位为1的时候,代表函数以序号的方式导入,Ordinal的低31位就是输入函数在其DLL内的 导出序号.当这个DWORD的数据最高位为0的时候,代表函数以字符串方式导入.AddressOfData就是一个指向用来导入函数名称的 IMAGE_IMPORT_BY_NAME的数据结构的RVA.(这里用来判断最高位的值0x8000000,预定义值为 IMAGE_ORDINAL_FLAG32.)
l Hint字段也表示函数的序号,主要是用来便与加载器快速查找在导入的DLL的函数导出表