PE文件结构及其加载机制

一、PE文件结构

PE即Portable Executable,是win32环境自身所带的执行体文件格式,其部分特性继承自Unix的COFF(Common Object File Format)文件格式。PE表示该文件格式是跨win32平台的,即使Windows运行在非Intel的CPU上,任何Win32平台的PE装载器也能识别和使用该文件格式的文件。

所有Win32执行体(除了VxD和16位的DLL)都使用PE文件格式,如EXE文件、DLL文件等,包括NT的内核模式驱动程序(Kernel Mode Driver)。

PE文件至少包含两个段,即数据段和代码段。Windows NT 的应用程序有9个预定义的段,分别为 .text 、.bss 、.rdata 、.data 、.pdata 和.debug 段,这些段并不是都是必须的,当然,也可以根据需要定义更多的段(比如一些加壳程序)。

在应用程序中最常出现的段有以下6种:

.执行代码段,通常  .text (Microsoft)或 CODE(Borland)命名;

.数据段,通常以 .data 、.rdata 或 .bss(Microsoft)、DATA(Borland)命名;

.资源段,通常以 .rsrc命名;

.导出表,通常以 .edata命名;

.导入表,通常以 .idata命名;

.调试信息段,通常以 .debug命名;

 

PE文件的结构在磁盘和内存中是基本一样的,但在装入内存中时又不是完全复制。Windows装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系,只有真正执行到某个内存页中的指令或访问某一页中的数据时,这个页才会被从磁盘提交到物理内存。但因为装载可执行文件时,有些数据在装入前会被预先处理(如需要重定位的代码),装入以后,数据之间的相对位置也可能发生改变。因此,一个节的偏移和大小在装入内存前后可能是完全不同的。

..

PE的基本结构就是这样了。

下面开始各个部分学习。

==================================================

(1)IMAGE_DOS_HEADER和Dos Stub

其实IMAGE_DOS_HEADER和Dos Stub没有什么重要的,只是IMAGE_DOS_HEADER中的第十九个成员指向IMAGE_NT_HEADERS的位置。

复制代码
复制代码
   typedef struct IMAGE_DOS_HEADER
  {
        WORD e_magic;
        WORD e_cblp;
        WORD e_cp;
        WORD e_crlc;
        WORD e_cparhdr;
        WORD e_minalloc;
        WORD e_maxalloc;
        WORD e_ss;
        WORD e_sp;
        WORD e_csum;
        WORD e_ip;
        WORD e_cs;
        WORD e_lfarlc;
        WORD e_ovno;
        WORD e_res[4];
        WORD e_oemid;
        WORD e_oeminfo;
        WORD e_res2[10];
        DWORD e_lfanew;             //指向IMAGE_NT_HEADERS的所在
  }IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; 
复制代码
复制代码

下面我们用一个具体的PE文件来看看。

这是64个字节(64Byte),下面是一些分析。

首先,只要是PE文件,那么开始两个字节就一定是4D 5A。

然后看最后四个字节,是E8000000,代表了地址是000000E8h,我们往下找找。

通过搜索,可以直接到达这个位置,一会我们再来看这个位置。

也就是说,00000040h到000000E8h之间的数据都是Dos Stub(称为dos残余程序)。

 

-----------------------------------------------------------------------------

(2)PE文件头

现在我们来看看IMAGE_NT_HEADERS的情况,看看e_lfanew指向的这里是什么含义,看看000000E8h(这个数值是不固定的,不同的PE程序的值可能不同,我们只要找到这个位置,读取它的值即可找到PE文件的头所在)指向的这里又是什么。

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

首先我们看到的,这是一个结构体,而且结构体中还有结构体。

我们先看看它的总大小和在c32中的情况

【1】其中红色的部分就是DWORD Signature,表示字符“PE\0\0”,十六进制为00004550h,这个值也是不会变化的。

【2】绿色线部分表示IMAGE_FILE_HEADER FileHeader,我们具体来看看这个结构体代表了什么含义。

 

复制代码
复制代码
typedef struct _IMAGE_FILE_HEADER {   
        WORD      Machine;                 //运行平台 
        WORD      NumberOfSections;        //块(section)数目      
        DWORD     TimeDateStamp;           //时间日期标记     
        DWORD     PointerToSymbolTable;    //COFF符号指针,这是程序调试信息    
        DWORD     NumberOfSymbols;         //符号数  
        WORD      SizeOfOptionalHeader;    //可选部首长度,是IMAGE_OPTIONAL_HEADER的长度    
        WORD      Characteristics;         //文件属性 
} 
复制代码
复制代码

第一个表示这个程序运行需要的平台,来看看我刚才这个程序的值

这里表示运行的平台是Intel的CPU,下面是一个列表代表各种对应的平台的值:

第二个表示块的数目

0004当然就是4个节了,一会我们可以看看是哪四个节

第三个表示时间,我们先看看

也就是4F91318Fh,用计算器算一下就是1334915471,这个值应该是秒,我们换算一下,结果大约是42年

这个是程序的创建日期,减去42年,大约就是1970年,这个日期就是从1970到文件最后修改时间之间的秒数?

这个我可不知道,有待研究。

第六个SizeOfOptionalHeader表示之后OptionalHeader的大小,我们先来看看

00E0就是十进制的224,也就是说OptionalHeader的大小是224字节。

第七个值Characteristics表示文件属性,它的每一个bit都代表了某种含义。

复制代码
复制代码
         Bit 0 :置1表示文件中没有重定向信息。每个段都有它们自己的重定向信息。
                 这个标志在可执行文件中没有使用,在可执行文件中是用一个叫做基址重定向目录表来表示重定向信息的,这将在下面介绍。
         Bit 1 :置1表示该文件是可执行文件(也就是说不是一个目标文件或库文件)。
         Bit 2 :置1表示没有行数信息;在可执行文件中没有使用。
         Bit 3 :置1表示没有局部符号信息;在可执行文件中没有使用。
         Bit 4 :
         Bit 7 
         Bit 8 :表示希望机器为32位机。这个值永远为1。
         Bit 9 :表示没有调试信息,在可执行文件中没有使用。
         Bit 10:置1表示该程序不能运行于可移动介质中(如软驱或CD-ROM)。在这    种情况下,OS必须把文件拷贝到交换文件中执行。
         Bit 11:置1表示程序不能在网上运行。在这种情况下,OS必须把文件拷贝到交换文件中执行。
         Bit 12:置1表示文件是一个系统文件例如驱动程序。在可执行文件中没有使用。
         Bit 13:置1表示文件是一个动态链接库(DLL)。
         Bit 14:表示文件被设计成不能运行于多处理器系统中。
         Bit 15:表示文件的字节顺序如果不是机器所期望的,那么在读出之前要进行
                 交换。在可执行文件中它们是不可信的(操作系统期望按正确的字节顺序执行程序)。
复制代码
复制代码

010Fh就是0000000100001111

具体的我们来看一张图:

【3】下面是OptionalHeader,占224个字节

我们看看 它的结构是怎样的

复制代码
复制代码
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;
复制代码
复制代码

 

同样的,我们先在msdn中查看下这个结构体

第一个值,表示文件的格式,它可能的值有

IMAGE_NT_OPTIONAL_HDR_MAGIC,这个值包括两个值,分别为IMAGE_NT_OPTIONAL_HDR32_MAGICIMAGE_NT_OPTIONAL_HDR64_MAGIC,分别表示是32位的应用程序和64位的应用程序,具体的十六进制值为0x10b和0x20b。

还有一个值是IMAGE_ROM_OPTIONAL_HDR_MAGIC,表示这是一个ROM文件,其值为0x107。

总结为一张图

对于这个文件,它的值是

表示32位的应用程序。

第二个值是MajorLinkerVersion,表示主版本号的链接器,这个值一般来说是不重要的。

对于这个文件,它的值为

第三个值是MinorLinkerVersion,表示次版本号的连接器,这个值一般来说是不重要的。

它的值

第四个值是SizeOfCode,表示The size of the code section, in bytes, or the sum of all such sections if there are multiple code sections.

代码段的总大小,单位为字节,如果是多个部分,则表示它们的总和。

94208字节,即92kb。

第五个值是SizeOfInitializedData,表示所有含已初始化数据的节的大小。

The size of the initialized data section, in bytes, or the sum of all such sections if there are multiple initialized data sections.

77824字节,76kb。

第六个值是SizeOfUninitializedData,表示未初始化数据的节的大小。

The size of the uninitialized data section, in bytes, or the sum of all such sections if there are multiple uninitialized data sections.

第七个值是AddressOfEntryPoint,表示程序的入口点。

A pointer to the entry point function, relative to the image base address. For executable files, this is the starting address. For device drivers, this is the address of the initialization function. The entry point function is optional for DLLs. When no entry point is present, this member is zero.

我们用PEiD来看看这个值

如果看过《C++反汇编与逆向分析技术揭秘》的童鞋就会知道,PEiD是如何工作的。

第八个值是BaseOfCode,表示代码段的起始RVA。

即RVA为00001000,如果加上ImageBase的话,就可以知道这个位置在内存中的位置为00401000,我们用OD打开这个程序看看

第九个值是BaseOfData,表示数据段的起始RVA。

同样,我们用OD来查看下,00018000+00400000=00418000

第十个值是ImageBase,表示程序的建议装载地址。

用PEiD查看

 

第十一个值SectionAlignment,表示节对齐粒度。这个值一定要大于或等于文件对齐粒度。
The alignment of sections loaded in memory, in bytes. This value must be greater than or equal to the FileAlignment member.
The default value is the page size for the system.

原来是4096B,也就是4kb了。

第十二个值是FileAlignment,表示文件对齐粒度。

The alignment of the raw data of sections in the image file, in bytes. The value should be a power of 2 between 512 and 64K (inclusive). The default is 512. If the SectionAlignment member is less than the system page size, this member must be the same as SectionAlignment.

这个值应该是512B的倍数。

第十三个值是MajorOperatingSystemVersion,所需操作系统的主版本号。

The major version number of the required operating system.

第十四个值是MinorOperatingSystemVersion,所需操作系统的副版本号。

The minor version number of the required operating system.

第十五个值是MajorImageVersion

The major version number of the image.

第十六个值是MinorImageVersion

The minor version number of the image.

第十七个值是MajorSubsystemVersion

The major version number of the subsystem.

第十八个值为MinorSubsystemVersion

The minor version number of the subsystem.

第十九个值为Win32VersionValue,保留值,且必须为零。

This member is reserved and must be 0.

 

 

 

第二十个值为SizeOfImage,4个字节,表示程序调入后占用内存大小(字节),等于所有段的长度之和。

The size of the image, in bytes, including all headers. Must be a multiple of SectionAlignment.

 

0x2B000=?

好吧,两三天了,终于弄明白这个值了,由于在实验过程中,为了防止意外,所以复制了一个副本在当前文件夹下,通过二进制的对比,发现这两个文件的SizeOfImage值是不一样的,所以走了弯路。

既然错了文件,那么我还是以这个文件为例吧,因为其他的部分都一样,所以就不修改其他的部分了。

0x25000+0x5188=0x2A188,再考虑内存对齐,我们试着用这个值除以对齐粒度0x1000,看是否能除尽。

结果是不能除尽,所以要求大一点,结果这个SizeOfImage就变成了0x2B000。

第二十一个值为SizeOfHeaders,占用4个字节,表示所有头加节表的大小。

The combined size of the following items, rounded to a multiple of the value specified in the FileAlignment member.

 

      • 4 byte signature
      • size of IMAGE_FILE_HEADER
      • size of optional header
      • size of all section headers

 

也就是0x1000了。

 

第二十二个值为CheckSum,占用四个字节。

The image file checksum. The following files are validated(验证) at load time: all drivers, any DLL loaded at boot time, and any DLL loaded into a critical (关键)system process.

 

 

第二十三个值为Subsystem,占用两个字节。表示文件运行所需的子系统。

 The subsystem required to run this image. The following values are defined.

 

ValueMeaning
IMAGE_SUBSYSTEM_UNKNOWN
0

Unknown subsystem.

IMAGE_SUBSYSTEM_NATIVE
1

No subsystem required (device drivers and native system processes).

IMAGE_SUBSYSTEM_WINDOWS_GUI
2

Windows graphical user interface (GUI) subsystem.

IMAGE_SUBSYSTEM_WINDOWS_CUI
3

Windows character-mode user interface (CUI) subsystem.

IMAGE_SUBSYSTEM_OS2_CUI
5

OS/2 CUI subsystem.

IMAGE_SUBSYSTEM_POSIX_CUI
7

POSIX CUI subsystem.

IMAGE_SUBSYSTEM_WINDOWS_CE_GUI
9

Windows CE system.

IMAGE_SUBSYSTEM_EFI_APPLICATION
10

Extensible Firmware Interface (EFI) application.

IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER
11

EFI driver with boot services.

IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER
12

EFI driver with run-time services.

IMAGE_SUBSYSTEM_EFI_ROM
13

EFI ROM image.

IMAGE_SUBSYSTEM_XBOX
14

Xbox system.

IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION
16

Boot application.

 

 

 

 

第二十四个值为DllCharacteristics,占用两个字节。表示dll文件的属性值。

The DLL characteristics of the image. The following values are defined.

 

ValueMeaning
0x0001

Reserved.(保留)

0x0002

Reserved.

0x0004

Reserved.

0x0008

Reserved.

IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE
0x0040

The DLL can be relocated at load time.(允许在载入的时候进行重定位)

IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY
0x0080

Code integrity checks are forced. If you set this flag and a section contains only uninitialized data, set the PointerToRawData member ofIMAGE_SECTION_HEADER for that section to zero; otherwise, the image will fail to load because the digital signature cannot be verified.

IMAGE_DLLCHARACTERISTICS_NX_COMPAT
0x0100

The image is compatible(兼容) with data execution prevention (DEP).

IMAGE_DLLCHARACTERISTICS_NO_ISOLATION
0x0200

The image is isolation(隔离) aware, but should not be isolated.

IMAGE_DLLCHARACTERISTICS_NO_SEH
0x0400

The image does not use structured exception handling (SEH). No handlers can be called in this image.

IMAGE_DLLCHARACTERISTICS_NO_BIND
0x0800

Do not bind the image.

0x1000

Reserved.

IMAGE_DLLCHARACTERISTICS_WDM_DRIVER
0x2000

A WDM driver.

0x4000

Reserved.

IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE
0x8000

The image is terminal server aware.

 

 

 

第二十五个值为SizeOfStackReserve,占用4个字节。表示初始化是的堆栈大小。

The number of bytes to reserve for the stack. Only the memory specified by the SizeOfStackCommit member is committed at load time; the rest is made available one page at a time until this reserve size is reached.

 

0x00100000=1MB

第二十六个值为SizeOfStackCommit,占用四个字节。表示初始化时实际提交的堆栈大小。

The number of bytes to commit for the stack.

 

0x1000字节=4kb

第二十七个值为SizeOfHeapReserve,占用四个字节。初始化时保留堆的大小。

The number of bytes to reserve for the local heap. Only the memory specified by the SizeOfHeapCommit member is committed at load time; the rest is made available one page at a time until this reserve size is reached.

 

第二十八个值为SizeOfHeapCommit,占用四个字节。初始化时实际提交的堆得大小。

The number of bytes to commit for the local heap.

 

第二十九个值为LoaderFlags,占用4个字节。未使用。

This member is obsolete. 

第三十个值为NumberOfRvaAndSizes,占用四个字节。表示下面个成员数据目录结构的数量。

 

这个值一般就直接是16.

 


下面是最后一个成员DataDirectory,占用128个字节,为一个IMAGE_DATA_DIRECTORY structure结构体数组(16个)。

A pointer to the first IMAGE_DATA_DIRECTORY structure in the data directory.

 

 

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

 

这个结构体有两个成员,一个成员占用4个字节,也就是8个字节。这个数组有16个数据,也就是16*8=128字节。

我们来看第一个。

IMAGE_DIRECTORY_ENTRY_EXPORT    导出表

这个程序没有导出函数,所以没有导出表。

第二个IMAGE_DIRECTORY_ENTRY_IMPORT  导入表

这个程序需要用到dll中的函数

 

我们用PEiD来查看下

 

结果是一样的。这个是RVA,表示偏移地址哦。

 

第三个IMAGE_DIRECTORY_ENTRY_RESOURCE   资源目录

从上面这张图也可以看出。RVA为00025000,大小为5188byte

 

第四个IMAGE_DIRECTORY_ENTRY_EXCEPTION  异常目录

 

未使用。

 


第五个 IMAGE_DIRECTORY_ENTRY_SECURITY  安全目录

 

 

第六个 IMAGE_DIRECTORY_ENTRY_BASERELOC   重定位表

 

 

第七个 IMAGE_DIRECTORY_ENTRY_DEBUG  调试信息

 

 

第八个 IMAGE_DIRECTORY_ENTRY_COPYRIGHT 版权信息

 

 

第九个  IMAGE_DIRECTORY_ENTRY_GLOBALPTR 

 

 

第十个 IMAGE_DIRECTORY_ENTRY_TLS  线程的本地存储器

 

 

第十一个 IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 载入配置目录

Load configuration table address and size

 

 

第十二个 IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT  绑定导入表地址和大小

Bound import table address and size

 

 

第十三个 IMAGE_DIRECTORY_ENTRY_IAT  导入函数地址表Import Address Table

Import address table address and size

 

用Exeinfo PE 查看

 

 

第十四个 IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT

Delay import descriptor address and size

 

 

 

第十五个 IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR

The CLR header address and size

 

 

第十六个 IMAGE_NUMBEROF_DIRECTORY_ENTRIES   保留值

 

 

到此,整个PE文件头结束了。

 

下面我们开始学习节表。

不知道还记不记得在前面哪个结构体中出现过节的数量?

 

嘿嘿,忘记了吧,我们翻开以前的记录,看看。

原来是

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

中的

复制代码
复制代码
typedef struct _IMAGE_FILE_HEADER {   
        WORD      Machine;                 //运行平台 
        WORD      NumberOfSections;        //块(section)数目      
        DWORD     TimeDateStamp;           //时间日期标记     
        DWORD     PointerToSymbolTable;    //COFF符号指针,这是程序调试信息    
        DWORD     NumberOfSymbols;         //符号数  
        WORD      SizeOfOptionalHeader;    //可选部首长度,是IMAGE_OPTIONAL_HEADER的长度    
        WORD      Characteristics;         //文件属性 
}
复制代码
复制代码

第二个成员就是了。

我们回去找找这个程序的这个值是多少。

原来是四个节啊,当然了,也可以说四个段。

果然是四个段。

好了,复习完需要的知识,我们就继续学习。

 

================================================

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_SHORT_NAME 8"    原来是个8

分类: PE
 
  二、导入表与IAT

(一)、导入表简介

在编程中常常用到“导入函数”(Import functions),导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL中,在调用者程序中只保留一些函数信息,包括函数名及其驻留的DLL名等。

于磁盘上的PE 文件来说,它无法得知这些输入函数在内存中的地址,只有当PE 文件被装入内存后,Windows 加载器才将相关DLL 装入,并将调用输入函数的指令和函数实际所处的地址联系起来。这就是“动态链接”的概念。动态链接是通过PE 文件中定义的“导入表”来完成的,导入表中保存的正是函数名和其驻留的DLL 名等。

1.调用导入函数的指令

程序被执行的时候是怎样使用导入函数的呢?我们来对一个简单的弹出一个MessageBox的程序反汇编一把,看看调用导入函数的指令都是什么样子的.

image

灰常简单的一个小程序,如图双击程序只显示一个对话窗口,然后就结束~试验用小程序,我们尽量的将内部的结构删减,调试起来才方便些。我们这次体验的目的就是想靠所学的知识,试图来找到MessageBox 在内存中的地址。
注:MessageBox 是来自于USER32.DLL 动态链接库里的一个函数,我们通过对PE 文件的静态反编译分析来观察hello.exe 这个试验品是如何定位和调用MessageBox 这个在USER32.dll的函数的。
(MessageBox 有两个版本,一个是MessageBoxA 还有一个是MessageBoxW 分别代表ASCII码形式和UNICODE~)

需要反汇编的两句源码如下:

invoke  MessageBox,NULL,offset szText,offset szCaption,MB_OK

invoke  ExitProcess,NULL

我们直接对其反汇编:

image

反汇编后,对MessageBox和ExitProcess函数的调用变成了对0040101A和00401020地址的调用,但是这两个地址显然是位于程序自身模块而不是DLL模块中,实际上,这是由编译器在程序所有代码的后面自动加上的Jmp dword ptr[xxxxxxxx]类型的指令,这个指令时一个间接寻址的跳转指令,xxxxxxxx地址中存放的才是真正的导入函数的地址。在这个例子中,00402000地址处存放的就是ExitProcess函数的地址。

那在没有装载到内存前,PE文件中的00402000地址处的内容又是什么呢?我们来分析一下它的节表。

image

由于建议装入地址是00400000h,所以00402000地址实际上处于RVA为2000h的地方,再看看各个节的虚拟地址,可以发现2000h开始的地方位于.rdata节内,而这个节的Raw_偏移为600h,也就是00402000h的内容实际上对应于PE文件中偏移600h处的数据。

我们再来看看文件0600h处的内容是什么:

image

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

是不是感觉有点不对?

如果我告诉你,当PE文件被装载的时候,Windows装载器会根据xxxxxxxx处的RVA得到函数名,再根据函数名在内存中找到函数地址,并且用函数地址将xxxxxxx处的内容替换成真正的函数地址,那么所有的疑惑就迎刃而解了。接下来看看如何获取导入表的位置。

2.获取导入表的位置

导入表的位置和大小可以从PE文件中IMAGE_OPTIONAL_HEADER32结构的数据目录字段中获取。从IMAGE_OPTIONAL_DIRECTORY结构的VirtualAddress字段得到的是导入表的RVA值,如果在内存中查找导入表,那么将RVA值加上PE文件装入的基址就是实际的地址,如果在PE文件中查找导入表,那么需要使用上一篇中讲的将RVA转换成文件偏移的方法进行转换。

 

(二)、导入表的结构

1.PE文件中的导入表

导入表由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成,结构的数量取决于程序要使用的DLL文件的数量,每一个结构对应一个DLL文件,在所有这些结构的最后,由一个内容全为0的IMAGE_IMPORT_DESCRIPTOR结构作为结束。

IMAGE_IMPORT_DESCRIPTOR结构的定义:

IMAGE_IMPORT_DESCRIPTOR STRUCT 
union 
    Characteristics              DWORD   ? 
    OriginalFirstThunk        DWORD   ? 
ends 
TimeDateStamp                     DWORD   ? 
ForwarderChain                     DWORD   ? 
Name1                                    DWORD   ? 
FirstThunk                            DWORD   ?
IMAGE_IMPORT_DESCRIPTOR ENDS

①Name1

它表示DLL 名称的相对虚地址(译注:相对一个用null作为结束符的ASCII字符串的一个RVA,该字符串是该导入DLL文件的名称,如:KERNEL32.DLL)。

②OriginalFirstThunk和FirstThunk

现在可以看成是相同的(现在),它们都指向一个包含一系列IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构定义了一个导入函数的信息,数组最后以一个内容为0的IMAGE_THUNK_DATA结构作为结束。

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

IMAGE_THUNK_DATA STRUC
union u1
ForwarderString       DWORD  ?        ; 指向一个转向者字符串的RVA
Function                      DWORD  ?        ; 被输入的函数的内存地址
Ordinal                       DWORD  ?        ; 被输入的API 的序数值
AddressOfData         DWORD  ?        ; 指向 IMAGE_IMPORT_BY_NAME
ends
IMAGE_THUNK_DATA ENDS

当 IMAGE_THUNK_DATA 值的最高位为 1时,表示函数以序号方式输入,这时候低 31位被看作一个函数序号。(读者可以用预定义值IMAGE_ORDINAL_FLAG32或80000000h来对最高位进行测试)
当 IMAGE_THUNK_DATA 值的最高位为 0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个 RVA,指向一IMAGE_IMPORT_BY_NAME 结构。

IMAGE_IMPORT_BY_NAME STRUCT
Hint        WORD    ? 
Name1      BYTE      ?
IMAGE_IMPORT_BY_NAME ENDS

结构中的 Hint 字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为 0,Name1字段定义了导入函数的名称字符串,这是一个以 0 为结尾的字符串。

我们来看一个例子:

image2.内存中的导入表

为什么需要两个一样的IMAGE_THUNK_DATA 数组呢?

当PE文件被装入内存的时候,其中一个数组的值将被改作他用,正如上面分析的,Windows装载器会将指令Jmp dword ptr[xxxxxxxx]指定的xxxxxxxx处的RVA替换成真正的函数地址,其实xxxxxxx地址正是FirstThunk字段指向的那个数组的一员。

实际上,当PE文件被装入内存后,内存中的映象就被Windows装载器修正成了下图的样子,其中由FirstThunk字段指向的那个数组中的每个双字都被替换成了真正的函数入口地址,之所以在PE文件中使用两份IMAGE_THUNK_DATA 数组的拷贝并修改其中的一份,是为了最后还可以留下一份拷贝用来反过来查询地址所对应的导入函数名。

3.导入地址表(IAT)

暂把上面FirstThunk指向的真正导入函数地址数组称为导入地址数组。在PE文件中,所有DLL对应的导入地址数组是被排列在一起的,全部这些数组的组合也被称为导入地址表(Import Address Table),导入表中第一个IMAGE_IMPORT_DESCRIPTOR结构的FirstThunk字段指向的就是IAT的起始地址。也可以通过数据目录表的第13项找到IAT数据块的位置和大小。

image

 
 
posted @ 2019-11-14 12:38  gd_沐辰  阅读(1851)  评论(0编辑  收藏  举报