PE结构

学习Win  PE结构笔记

 

 

 

 

 

windows PE 文件是指windows系统中的一种文件格式,它包括 .exe, .dll, .sys, .ocx, .cpl, .scr, .drv, .efi, .fon ....                

在了解PE文件结构以前,大家应该先明白下面几个名词的概念:

1 虚拟地址(VA

windows中每一个进程加载到内存中,都会有一个的4GB大小的虚拟内存地址空间,在这片虚拟的空间中,任何一个位置的地址,都称之为虚拟地址(VA)。

2 相对虚拟地址(RVA

每一个PE文件加载到内存中,都会有一个加载基址,相对虚拟地址(RVA)就是相对于加载基址的偏移量。

公式:虚拟地址(VA) = 加载基址(ImageBase)+ 相对虚拟地址(RVA);

PE文件会有一个默认的加载基址,是扩展头的第十个数据成员ImageBase(映像基址)。当这个位置被其他PE文件占用,就会加载到别地方。

3 文件偏移(Offset

文件偏移是用于指定文件中的某个位置(未加载到内存中,在磁盘文件中的位置),是指在磁盘中相对于文件起始的偏移量。

4 对齐的概

PE中的SectionAligment 字段与FileAligment字段描述了此映像文件的内存对齐大小与文件对齐大小。

根据上面两个字段,系统再给某一个区段上的数据块分配空间时,都会根据其对齐大小的整数倍来分配空间。

文件对齐和内存对齐的概念,举例来说一个体积为0x1150的区段,如果它的内存对齐大小为0x1000(4KB),文件对齐大小为0x200(512B),

那么这个区段在内存中实际占的大小就是0x2000,其中0x1150是有意义的,剩余的用零填充。在文件中实际占用的大小是0x1200,其中0x1150,是有意义的,剩下的用零填充。

下面是比较常见的节表的名称和作用,在分析的过程中如果遇到不在下表中的一些节表名称,这种现象说明这个文件是十分可疑的。

节名

描述

.text

代码段,里面的数据全都是代码

.data

可读写的数据段,存放全局变量或静态变量

.rdata

只读数据区

 .idata

导入数据区,存放导入表信息

.edata

导出数据区,导出表信息

.rsrc

资源区段,存放程序用到的所有资源,如图表,菜单等

.bss

未初始化数据区

.crt

用于支持C++运行时库所添加的数据

.tls

存储线程局部变量

.reloc

包含重定位信息

.sdata

包含相对于可被全局指针定位的可读写数据

.srdata

包含相对于可被全局指针定位的只读数据

.pdata

包含异常表

.debug$S

包含OBJ文件中的Codeview格式符号

.debug$T

包含OBJ文件中的Codeview格式类型的符号

.debug$P

包含使用预编译头时的一些信息

.drectve

包含编译时的一些链接命令

.didat

包含延迟装入的数据

PE文件由以下几个结构顺序构成:

DOS头部                                                        //大小为64(0x40)

DOS Stub                                                       //为兼容DOS程序而设立.

NT头部                                                           //存储PE文件的全部属性,初始化信息

区段头表(也叫节表,因为区段也可以叫做节)               //对于PE主体文件属性的分段描述,个数不定.

各个区段(节)(代码,数据,重定位等等)                   //PE文件的主体,分段存储着可执行的代码,各种数据,资源等( .rdata .reloc .text .rsrc ...)

接下来,分别介绍各个部分,每一个部分,都有一些常用到的重要成员,把这些常用的成员位置记住,基本就算熟悉了PE文件结构。

DOS:

typedef struct _IMAGE_DOS_HEADER {     

    WORD   e_magic;                                   // [0x00] Magic number(重要,是否为PE文件的第一个标志)

    WORD   e_cblp;                                     // [0x02]Bytes on last page of file

    WORD   e_cp;                                        // [0x04]Pages in file

    WORD   e_crlc;                                      // [0x06]Relocations

    WORD   e_cparhdr;                                // [0x08]Size of header in paragraphs

    WORD   e_minalloc;                               //[0x0A] Minimum extra paragraphs needed

    WORD   e_maxalloc;                              //[0x0C] Maximum extra paragraphs needed

    WORD   e_ss;                                       // [0x0E]Initial (relative) SS value

    WORD   e_sp;                                       // [0x10]Initial SP value

    WORD   e_csum;                                   // [0x12]Checksum

    WORD   e_ip;                                        //[0x14] Initial IP value

    WORD   e_cs;                                        //[0x16] Initial (relative) CS value

    WORD   e_lfarlc;                                   // [0x18]File address of relocation table

    WORD   e_ovno;                                    // [0x1A]Overlay number

    WORD   e_res[4];                                  // [0x1C]Reserved words

    WORD   e_oemid;                                  // [0x24]OEM identifier (for e_oeminfo)

    WORD   e_oeminfo;                                //[0x26] OEM information; e_oemid specific

    WORD   e_res2[10];                               // [0x28]Reserved words

    LONG   e_lfanew;                                   // [0x3C]File address of new exe header(有用,PE解析时用它找到PE头的位置

  } IMAGE_DOS_HEADER, * PIMAGE_DOS_HEADER;

补充:

1 PE 文件的第一个结构便是这个结构体.

2 重要的是第一个和最后一个,也就是e_magic与e_lfanew这两个数据成员。

3 e_magic,翻译为魔数,其实它就是一个标记,DOS头标志位,其值恒为4D5A,在系统中用宏定义为:IMAGE_DOS_SIGNATURE

4 e_lfanew,它表示NT头部在文件中的偏移.

5 其余大多已经没有什么用了,其内容通常为0居多.

DOS Stub:

在DOS头部与NT头部之间有一部分区域,存储着一些被dos头使用的数据.包括一些提示字符串等等,这部分的大小不太确定,所以,NT头的具体位置要由DOS头的最后一个成员e_lfanwe确定.

NT头部:

typedef struct _IMAGE_NT_HEADERS {

    DWORD Signature;                                                  //标记(重要,判断是否为PE文件的第二个标志

    IMAGE_FILE_HEADER FileHeader;                              //文件头(重要,存储着PE文件的基本信息

    IMAGE_OPTIONAL_HEADER32 OptionalHeader;           //扩展头(重要,存储着关于PE文件时加载的信息

} IMAGE_NT_HEADERS32, * PIMAGE_NT_HEADERS32;

补充:

1 NT头由一个简单的标记,一个不太复杂的文件头和一个比较复杂的扩展头组成.

2 如果是PE文件,标记的值恒为0x00004550, ASCII为PE00,,在系统中用宏定义为:IMAGE_NT_SIGNATURE

3 另外两个成员是两个结构体,里面存储的信息非常有用,可以说解析这两个结构是PE解析真正的开始。

文件头:

NT头的第二个成员,是一个结构体,可以称为文件头,存储着一些关于这个PE文件的信息.

typedef struct _IMAGE_FILE_HEADER {

    WORD      Machine;                         1  (文件的运行平台)

    WORD      NumberOfSections;      2  (区段的数量

    DWORD   TimeDateStamp;               3  (文件创建时间)

    DWORD   PointerToSymbolTable;      4  (符号表偏移,用于调试)

    DWORD   NumberOfSymbols;            5   (符号个数,用于调试)

    WORD    SizeOfOptionalHeader;     6   (扩展头的大小

    WORD    Characteristics;                 7   PE文件的一些属性

} IMAGE_FILE_HEADER, * PIMAGE_FILE_HEADER;

补充:

1 关于Machine: 这个文件可以运行在哪一个CPU平台.常见的  0x014c 代表 CPU 型号为Intel 386, 0x0200  代表 CPU 型号为Intel 64.

2 NumberOfSections:区段的个数,也就是PE文件的主体被分成了多少个部分,一般有代码,只读数据,数据,重定位等区段(节)。

3 TimeDateStamp:表明文件是何时被创建的,但是这个数据是一个非常大的32位的数值,具体解析这个数据可以使用函数

struct tm *gmtime( 

   const time_t *timer 

);

将TimeDateStamp的地址强制转换后作为参数,之后使用一个tm结构体接收就可以得到具体时间了.

4 PointerToSymbolTable:指向符号表,用于调试.

5 SizeOfOptionalHeader:扩展头的大小,32位系统中一般是224(0x00E0),在64位系统中一般是240(0x00F0),扩展头大小其实是不太确定的,因为不排除人为的修改.

6 Characteristics:PE文件属性值,可以用来判断文件类型。有几个需要知道:Dll一般是0x0210,EXE一般是0x010F.win32 SDK 中的winNT.h定义一组宏,来表示不同的文件属性.

ValueMeaning
IMAGE_FILE_RELOCS_STRIPPED
0x0001

Relocation information was stripped from the file. The file must be loaded at its preferred base address.         If the base address is not available, the loader reports an error.

IMAGE_FILE_EXECUTABLE_IMAGE
0x0002

The file is executable (there are no unresolved external references).

IMAGE_FILE_LINE_NUMS_STRIPPED
0x0004

COFF line numbers were stripped from the file.

IMAGE_FILE_LOCAL_SYMS_STRIPPED
0x0008

COFF symbol table entries were stripped from file.

IMAGE_FILE_AGGRESIVE_WS_TRIM
0x0010

Aggressively trim the working set. This value is obsolete.

IMAGE_FILE_LARGE_ADDRESS_AWARE
0x0020

The application can handle addresses larger than 2 GB.

IMAGE_FILE_BYTES_REVERSED_LO
0x0080

The bytes of the word are reversed. This flag is obsolete.

IMAGE_FILE_32BIT_MACHINE
0x0100

The computer supports 32-bit words.

IMAGE_FILE_DEBUG_STRIPPED
0x0200

Debugging information was removed and stored separately in another file.

IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP
0x0400

If the image is on removable media, copy it to and run it from the swap file.

IMAGE_FILE_NET_RUN_FROM_SWAP
0x0800

If the image is on the network, copy it to and run it from the swap file.

IMAGE_FILE_SYSTEM
0x1000

The image is a system file.

IMAGE_FILE_DLL
0x2000

The image is a DLL file. While it is an executable file, it cannot be run directly.

IMAGE_FILE_UP_SYSTEM_ONLY
0x4000

The file should be run only on a uniprocessor computer.

IMAGE_FILE_BYTES_REVERSED_HI
0x8000

The bytes of the word are reversed. This flag is obsolete.

扩展头

typedef struct _IMAGE_OPTIONAL_HEADER {

    //

    // Standard fields.

    //

    WORD    Magic;                                 1    文件类型标识,32位一般是0x010B,64位的PE文件一般是0x020B,还有0x0170,代表ROM镜像。

    BYTE    MajorLinkerVersion;                    2    连接器主版本

    BYTE    MinorLinkerVersion;                    3    连接器次版本

    DWORD   SizeOfCode;                            4   (重要)指所有代码区段(节)的总大

    DWORD   SizeOfInitializedData;                 5    已初始化数据的总大小

    DWORD   SizeOfUninitializedData;               6    未初始化数据的总大小,在磁盘中不占用空间,在加载进内存之后,会预留这么大的空间。一般存储在.bss区段中。

    DWORD   AddressOfEntryPoint;                   7   (重要)程序开始执行的相对虚拟地址(RVA),也叫OEPOrginal Entry Point ,源入口点

    DWORD   BaseOfCode;                            8   (重要)起始代码的相对虚拟地址(RVA),一般这个值为0x00001000.

    DWORD   BaseOfData;                            9    起始数据的相对虚拟地址(RVA)

    //

    // NT additional fields.

    //

    DWORD   ImageBase;                             10  (重要)默认加载基址(如果没有加载到这个地址,会发生重定位.)

    DWORD   SectionAlignment;                      11  (重要)块对齐数,就是在映射到内存中的区段(节)对齐,这个数必须大于文件对齐数,一般是0x1000

    DWORD   FileAlignment;                         12  (重要)文件对齐数,就是在硬盘中的文件的区段(节)对齐,一般是0x200

    WORD    MajorOperatingSystemVersion;           13   主操作系统版本号

    WORD    MinorOperatingSystemVersion;           14   次操作系统版本号

    WORD    MajorImageVersion;                     15   主映像版本

    WORD    MinorImageVersion;                     16   次映像版本

    WORD    MajorSubsystemVersion;                 17   主子系统版本

    WORD    MinorSubsystemVersion;                 18   次子系统版本

    DWORD   Win32VersionValue;                     19   保留值,一般是0

    DWORD   SizeOfImage;                           20  (重要)要把文件加载进内存,所需要的内存大小,注意是进行了块对齐之

    DWORD   SizeOfHeaders;                         21   所有头部大小,Dos头、PE头、区段表的尺寸之和

    DWORD   CheckSum;                              22   校验和(一般无用)对于驱动和一些系统dll来说需要校验(使用IMAGEHLP.DLL中的CheckSumMappedFile API)

    WORD    Subsystem;                             23  (重要)子系统

    WORD    DllCharacteristics ;                   24  (重要)指示Dll特征的标志,DllMain()函数何时被调用,默认为0.

    DWORD   SizeOfStackReserve;                    25   初始化时栈的大小

    DWORD   SizeOfStackCommit;                     26   初始化时实际提交的栈的大小

    DWORD   SizeOfHeapReserve;                     27   初始化时保留的堆的大小

    DWORD   SizeOfHeapCommit;                      28   初始化时实际提交的堆的大小

    DWORD   LoaderFlags;                           29   与调试相关  

    DWORD   NumberOfRvaAndSizes;                   30   数据目录的个数,也就是下面那个数组中元素的个数。

    IMAGE_DATA_DIRECTORY DataDirectory[ IMAGE_NUMBEROF_DIRECTORY_ENTRIES];   31  (非常重要)数据目录

} IMAGE_OPTIONAL_HEADER32, * PIMAGE_OPTIONAL_HEADER32;

说明:

1 一共有31个成员,重要的大概有10个。另外有一个极度重要的数据目录。

2 扩展头是属于NT头部的第三部分,紧随文件头结构之后,存储着加载文件时的一些初始化信息,32位系统中扩展头大小一般是224(0x00E0),在64位系统中一般是240(0x00F0)。

3  IMAGE_NUMBEROF_DIRECTORY_ENTRIES  这是个宏定义,值是0x10,表示一般情况下有16个数据目录.

这个是数据目录的定义

typedef struct _IMAGE_DATA_DIRECTORY {

    DWORD   VirtualAddress;     // 数据的相对虚拟地址(RVA

    DWORD   Size;               // 数据的大

} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

它包含了这段数据存储的相对虚拟地址(RVA)以及这段数据的大小,所以数据目录表只是一个引导,只是帮助我们找到这段数据,这里列出数据目录的宏与名称。

IMAGE_DIRECTORY_ENTRY_EXPORT           0

导出表(IMAGE_EXPORT_DIRECTORY结构)。

IMAGE_DIRECTORY_ENTRY_IMPORT           1

导入表(IMAGE_IMPORT_DESCRIPTOR结构数组)。

IMAGE_DIRECTORY_ENTRY_RESOURCE       2

资源表(IMAGE_RESOURCE_DIRECTORY结构)。

IMAGE_DIRECTORY_ENTRY_EXCEPTION      3

异常处理程序表(IMAGE_RUNTIME_FUNCTION_ENTRY结构数组)

IMAGE_DIRECTORY_ENTRY_SECURITY        4

安全目录表,一般情况用于保存数字签名或安全证书。

IMAGE_DIRECTORY_ENTRY_BASERELOC      5 

基址重定位信息。

IMAGE_DIRECTORY_ENTRY_DEBUG              6

索引调试信息

IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7

版权 

IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8

全局指针目录。用在64位平台

IMAGE_DIRECTORY_ENTRY_TLS                  9

指向线程局部存储(Thread Local Storage)初始化节。

IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG  10

载入配置

IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT  11

绑定输入目录

IMAGE_DIRECTORY_ENTRY_IAT                  12

导入地址表

IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13

延迟载入描述

IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR  14 

COM信息

最后还有一个全零的保留目录,也就是数组长度是16.这是一般的情况之下,文件是可以拥有更多数据目录的。这个时候,扩展头的大小也不是E0了。

至此,NT头的内容就结束了,之后是区段头表.

区段头表:

再回忆PE文件结构:1 DOS头   2 DOS头用的数据   3 NT头(包括文件头与扩展头) 4  区段(节)头表   5 各个区段(节).

区段是不需要我们直接解析的地方,也就说,区段头表是我们直接探索的最后位置了。

区段头表存储着PE文件主体的一些属性,区段头表是由若干个结构体依次排列组成,每一个结构体代表着PE文件主体中一段数据的属性,也就是每一个区段头都对应着PE文件主体的一段数据,这段数据叫做区段或者节,区段头规定了区段(节)的属性。

区段头结构

typedef struct _IMAGE_SECTION_HEADER {

    BYTE    Name[ IMAGE_SIZEOF_SHORT_NAME];                   1         //区段的名字,如.text  .reloc  .rdata

    union {

            DWORD   PhysicalAddress;

            DWORD   VirtualSize;                              2 (重要)//这个区段在虚拟内存中会使用的总大小,没有经过对齐

    } Misc;                                                   

    DWORD   VirtualAddress;                                   3 (重要)//这个区段起始的相对虚拟地址(RVA

    DWORD   SizeOfRawData;                                    4 (重要)//区段在文件中的大小,这个值进行了文件对齐

    DWORD   PointerToRawData;                                 5 (重要)//区段的文件偏移 RVA Offset 时用

    DWORD   PointerToRelocations;                             6         //区段的重定位信息的文件偏移。在OBJ文件中才有用

    DWORD   PointerToLinenumbers;                             7         //COFF行号信息的文件偏移.

    WORD    NumberOfRelocations;                              8         //PointerToRelocations域指向的重定位信息的数目。在OBJ文件中才有用

    WORD    NumberOers;                                       9         //PointerToLinenumbers域指向的行号信息的数目。只有当生成COFF行号信息时才使用。

    DWORD   Characteristics;                                  10 (重要)//区段的属性,这个在文件被载入的时候意义比较重大.

} IMAGE_SECTION_HEADER, * PIMAGE_SECTION_HEADER;

补充:

1 区段头表是由多个这个结构体构成,以一个全是0的结构体结尾

2 关于区段名字的规矩是这样的:

.text                     一般是代码段,这个是非常重要的

.data段                   一般是数据段

.bss段                    表示未初始化的数据,比如static变量,可能是在进入一个函数的时候才被初始化的

.rdata段                  表示只读的数据,比如字符串,

.textbss段                和代码有关,不是很清楚做什么用的

.idata和edata             存储导入表和导出表的信息。

.rsrc段                   存储资源的区段(节)

.relcoc段                 存储重定位信息的区段(节)

3 Characteristics:       区段(节)的属性。属性的具体值,参考MSDN.

4 这个结构的大小是40(0x28)。

5 虽然区段头表就在NT头的后面,系统提供了一个宏来方便的找到它的位置:IMAGE_FIRST_SECTION( pNTHeader ),参数是NT头的指针。

6 相对虚拟地址(RVA)与文件偏移(Offectset)的转换

这是解析数据目录表的基础

这里提供转换的方法:我们把需要转换的RVA,在下面的公式中定义成 RVA(转)

RVA(转),一定是在某个区段中,通过循环遍历上面的结构体中成员变量,如果RVA(转)符合大于 VirtualAddress(区段起始的相对虚拟地址RVA

小于 (VirtualAddress + Misc.VirtualSize).就是在这个区段中,这样得到了这个区段的起始RVA和区段起始Offset.通过下面的公式就可以得到转换后的文件偏移了

公式:Offect  =  RVA(转) -  RVA(区段起始)+Offect(区段起始

导出表

概述:

1)导出是指这个PE文件所导出的供其他PE文件使用的函数,变量,或者类的行为。

2)每一个导出的函数(变量,类),都有一个唯一的序号与之对应,有的情况下,会没有函数名(变量,类),但是会有函数(变量,类)地址和序号。可以通过序号调用这样的函数。

3)由上一条可知,导出表包括   1 函数(变量,类)地址   2 序号   3 函数(变量,类)名

4)导出表,是根据数据目录表的第一个数组元素中的相对虚拟地址,再通过刚刚说过的相对虚拟地址转换文件偏移的方式,可以方便的找到它。

下面是导出表的数据结构

typedef struct _IMAGE_EXPORT_DIRECTORY {

    DWORD    Characteristics;                  1        保留值,恒为0

    DWORD   TimeDateStamp;                     2        时间,和文件头中的时间一样的

    WORD      MajorVersion;                    3        主版本号

    WORD      MinorVersion;                    4        次版本号

    DWORD   Name;                              5(重要)本PE文件的名字,也就是谁导出的这些函数(变量,类

    DWORD   Base;                              6(重要)序号基

    DWORD   NumberOfFunctions;                 7(重要)函数数

    DWORD   NumberOfNames;                     8(重要)函数名称数

    DWORD   AddressOfFunctions;                9(重要)函数地址表的相对虚拟地址RVA  

    DWORD   AddressOfNames;                   10(重要)函数名称表的相对虚拟地址RVA

    DWORD   AddressOfNameOrdinals;            11(重要)序号表的相对虚拟地址RVA   

} IMAGE_EXPORT_DIRECTORY, * PIMAGE_EXPORT_DIRECTORY;

补充:

1) 导出表应该被安排在.edata中,不过这个段一般都会合并到.rdata中。

2) 有一个序号基数,通过序号表得到的序号再加上这个序号基数才是真正的函数序号。

3) 最后三个成员,通过这三个相对虚拟地址,转换为文件偏移后,可以方便的找到函数地址表,函数名称表,序号表。

4) 函数名称表,存储的是函数名称的相对虚拟地址,再一次的转换为文件偏移后才能使用,这一点在解析的时候要注意。

5) 导出的函数地址表,序号表,函数名表的关系:

     5.1序号不是按顺序排列的 

     5.2序号与函数名是一一对应的,一一对应的意思是,两个表中相同位置的元素相对应。这也说明了结构体中为什么没有序号的数量,因为序号的数量和函数名数量是一样的。

     5.3序号可能不是连续的,比如可能会没有1 ,有2,没有5,有6,但这并不代表缺失的这个序号没有所对应的函数地址

     5.4函数个数会比函数名个数多,多出来的这些函数,可能是用序号导出的函数,也可能是一个无效函数,地址填充0

     5.5序号表元素的值,对应着函数地表的位置,那一个位置中的函数地址,是这个序号所对应的函数名的函数地址。由此,三个表联系起来。这条很重要。

     5.6函数地址表中元素,有地址值,但是序号表中没有序号与之对应,说明这是一个序号导出函数,没有函数名,他的序号就是它自己在函数地址表中的位置。(当然这个位置加上序号基数才是它真正的序号)

     5.7函数地址表中元素,填充为0,说明这是一个无效的函数,也不会有序号和函数名与之对应。

下面这个图指出了一个比较混乱的导出表结构,通过这个混乱的结构能更好的理解导出表。

                       

可以看出,序号的基数为X,导出表中的函数地址表中的第一个函数,函数名表和序号表中没有相应的值和它对应的,它是由序号导出的。

导入表

概述

1)导入表是根据数据目录表的第二个元素找到的。找出的方法与找导出表相同。

2)导入是这个PE文件在运行时,需要别的PE文件给予的支持。导入表存储的是从其他PE文件导入过来的函数名,序号。在加载到内存之后,会存储这些函数的地址。

3)由于一个PE文件可能会需要多个PE文件的支持,所以导入表结构一般有多个,就是说导入表其实是一个结构体数组,以一个全零元素为结尾,每一个数组的元素,代表一个PE文件的导入信息。

下面是导入表的数据结构:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {

    union {

        DWORD   Characteristics;                 

        DWORD   OriginalFirstThunk;          1 (重要)指向一个结构体数组的相对虚拟地址(RVA,结构体数组叫输入名称表(INTImport Name Table

    } DUMMYUNIONNAME; 

    DWORD   TimeDateStamp;                   2  时间标志

    DWORD   ForwarderChain;                  3  转发机制用到

    DWORD   Name;                            4(重要)导入的PE文件的名字的相对虚拟地址RVA

    DWORD   FirstThunk;                      5 (重要) 指向一个结构体数组的相对虚拟地址(RVA),结构体数组叫做输入地址表(IATImport Address Table

} IMAGE_IMPORT_DESCRIPTOR, * PIMAGE_IMPORT_DESCRIPTOR;

typedef struct _IMAGE_THUNK_DATA32 {

    union {

        DWORD ForwarderString;  //转发用到

        DWORD Function;             //导入函数的地址,在加载到内存后,这里才起作用 

        DWORD Ordinal;              //假如是序号导入的,用到这里

        DWORD AddressOfData;   //假如是函数名导入的,用到这里,它指向一个PIMAGE_IMPORT_BY_NAME结构体

    } u1;

} IMAGE_THUNK_DATA32;

1 在磁盘文件中,起作用的只有后两个成员

2 这个结构占据4个字节,假如最高位为1,那么序号导入起作用,只需输出一个序号,假如最高位为0,那么是最后一个成员其作用,指向一个PIMAGE_IMPORT_BY_NAME,判断最高位是否为1可以使用系统提供的宏IMAGE_SNAP_BY_ORDINAL32(),参数就是这个结构体。

typedef struct _IMAGE_IMPORT_BY_NAME {

    WORD    Hint;

    CHAR   Name[1];

} IMAGE_IMPORT_BY_NAME, * PIMAGE_IMPORT_BY_NAME;

这个结构包含了序号和函数名。

下面这张图表示导入表的双桥结构:
 

1 OriginalFirstThunk 与  FirstThunk 指向的是相同类型的结构体IMAGE_THUNK_DATA32

2 在磁盘文件中OriginalFirstThunk与FirstThunk中的数据是相同的,可以将输入名称表(INT)看成是输入地址表(IAT)的一个备份。在加载到内存中之后,输入地址表会由PE加载器把相应PE文件的函数地址覆盖到这里来。这时,输入地址表才是真正的输入地址表。

  

重定位:

1)什么是重定位?

重定位就是你本来这个程序理论上要占据这个地址,但是由于某种原因,这个地址现在不能让你霸占,你必须转移到别的地址,这就需要基址重定位。由于每个进程都有自己独立的虚拟地址空间,既然都是自己的,怎么会被占据呢?其实对于EXE应用程序来说,是这样的。但是动态链接库就不一样了,动态链接库都是寄居在别的应用程序的空间的,所以出现要载入的基地址被应用程序占据了也是很正常的,这时它就不得不进行重定位了。

2)重定位表是根据数据目录表的第6个元素。

3)重定位表也是一个结构体数组,以全零元素结尾,每一个数组元素描述了4KB大小的区域的重定位信息。

以下是重定位表的数据结构

typedef struct _IMAGE_BASE_RELOCATION {

    DWORD   VirtualAddress;               1(重要)需要重定位内存页的起始位置(RVA

    DWORD   SizeOfBlock;                   2 (重要)这个结构体(算上TypeOffset)的大

//  WORD     TypeOffset[1];               3 (重要)一个特殊的数据,存放的是相对于第一个元素描述的位置的偏

} IMAGE_BASE_RELOCATION;

说明:

1 这是一个特殊的结构,第三个成员并不真正是这个结构体的成员,他紧随在结构体之后,是一个不定多长的数组,第一个成员描述的是某个区段中第一个需要重定位开始。

2 那我们如何知道这个区域中有多少个需要重定位的位置呢?就要根据第二个成员,它的大小是这个结构体大小与后面的TypeOffset数组的总大小。可以推出,重定位个数等于总大小减去结构体大小,再除以2。

公式描述:需重定位个数   n = (SizeOfBlock-8)/ 2;

3 第三个成员的高4位,描述的是一个属性。低12位描述的才是一个偏移。如下图所示:

 

4 当基址重定位发生的时候,用第一个成员是虚拟基址(VA),依次加上偏移(第三个成员的后12位),就能得到存储全局变量地址的相对虚拟地址,也就找到了这个地方,再根据第三个成员高四位描述的属性,对其进行重定位操作。

资源表:

概述:

1 Windows 将程序的各种界面定义为资源,包括加速键(Accelerator)、位图(Bitmap)、光标(Cursor)、对话框(Dialog Box)、图标(Icon)、菜单(Menu)、串表(String Table)、工具栏(Toolbar)和版本信息(Version Information)等。

2 资源表有三层结构。每一层都以一个IMAGE_RESOURCE_DIRECTORY开头,之后跟数个IMAGE_RESOURCE_DIRECTORY_ENTRY结构,可以说每一层由一个IMAGE_RESOURCE_DIRECTORY结构与一个IMAGE_RESOURCE_DIRECTORY_ENTRY结构体数组组成,这个结构体数组元素的个数由之前的结构给出,所以要注意:这个结构体数组不是以一个全零元素为结尾了,在解析的时候要注意。

3 资源表是根据数据目录表的第3个元素找到的,它找到的是资源结构的第一层。

4 理解三层目录结构,第一层告诉你有几种资源。每种资源叫什么,第二层告诉你这一种资源有多少个,每个资源叫什么,第三层告诉你一个具体资源在文件的什么位置,注意区分多少种资源和多少个资源。

如图所示: 

 

下面分别查看两个结构体

typedef struct _IMAGE_RESOURCE_DIRECTORY {

    DWORD    Characteristics;                  1属性,一般填0

    DWORD    TimeDateStamp;                2时间,一般填0

    WORD      MajorVersion;                    3 主版本号

    WORD      MinorVersion;                    4 次版本号

    WORD      NumberOfNamedEntries;    5 (重要)用字符串作为资源标识的条目个数

    WORD      NumberOfIdEntries;           6 (重要) 用数字ID作为资源标识的条目个数

//  IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];

} IMAGE_RESOURCE_DIRECTORY, * PIMAGE_RESOURCE_DIRECTORY;

说明

其实在这里边我们唯一要注意的就是 NameberOfNamedEntries 和 NumberOfIdEntries,它们说明了本目录中目录项的数量。两者加起来就是本目录中的目录项总和。

也就是后边跟着的IMAGE_RESOURCE_DIRECTORY_ENTRY 数目。

下面是IMAGE_RESOURCE_DIRECTORY_ENTRY结构。

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY STRUCT

{

    DWORD      Name;             1//目录项的名称字符串指针或ID

    DWORD      OffsetOfData;  2//目录项指针

}IMAGE_RESOURCE_DIRECTORY_ENTRY

1 .Name 字段是联合体,当最高位为 0  的时候,表示字段的值作为 ID 使用;而最高位为 1 的时候,字段的低位作为指针使用(资源名称字符串是使用 UNICODE编码),但是这个指针不是直接指向字符串哦,而是指向一个 IMAGE_RESOURCE_DIR_STRING_U 结构的。当结构用于第一层目录时,定义的是资源类型;当结构定义于第二层目录时,定义的是资源的名称;当结构用于第三层目录时,定义的是代码页编号。

该结构定义如下:

typedef struct _IMAGE_RESOURCE_DIR_STRING_U STRUCT

{

   DWORD   Length ; 字符串的长度

   DWORD   NameString    ; UNICODE字符串,由于字符串是不定长的。由Length 制定长度

}IMAGE_RESOURCE_DIR_STRING_U

2. OffsetOfData 字段是一个指针,当最高位为 1 时,低位数据指向下一层目录块的其实地址;当最高位为 0 时,指针指向 IMAGE_RESOURCE_DATA_ENTRY 结构。

注意:将 Name 和 OffsetOfData 用做指针时需要注意,该指针是从资源区块开始的地方算起的偏移量(即根目录的起始位置的偏移量),不是我们习惯的 RVA 哦。

3下面讲一下常规情况下的三层结构

3.1第一层:

 通过数据目录表的第3个元素找到这里,首先遇到的是那个IMAGE_RESOURCE_DIRECTORY结构,最后两个成员的和会告诉你后面有多少种需要解析的资源

 如果这种资源是已知的,那么这种资源属于按序号作为资源标识,Name元素最高位为0,这个时候整个四个字节代表着已知资源的类型, 这个表能说明不同的数字,代表的资源类型。

 

如果这种资源是未知的,那么这种资源属于字符串作为资源标识,这时联合体的最高位为1, OffseToData指明了一个结构体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;

第二个成员是这种资源类型的名字。第一个成员是这个名字的长度。需要注意的是这个名字不是以0结尾的。有个长度给你,解析的时候不要越界。

当OffseToData最高位为 1 时,指针指向 IMAGE_RESOURCE_DATA_ENTRY 结构。这个地方就是第二层。

3.2第二层 

通过第一层找到这里,首先遇到的是那个IMAGE_RESOURCE_DIRECTORY结构,最后两个成员的和会告诉你后面有多少个需要解析的资源

第一个成员,通过上一层的理解,其实这一层也很好理解了。当Name成员最高位是0的时候,说明这个资源的标识是一个数字,其实一些对话框,控件的ID值就是这个数字。如果Name成员最高位是1,说明这个资源的标识是字符串,

第二个成员的OffsetToData成员最高位为1,说明这个联合体表示的地方是一个目录,会带你去寻找这个资源的具体在地方,这个地方就是第三层。

3.3第三层

第一个联合体已经不是标识的意思了。整个四个字节代表的是这个资源是什么语言的。

第二个联合体的OffsetToData成员会为0,说明这个联合体表示的地方是一个数据,它指出了资源具体的位置。由OffsetToData会得到一个结构体IMAGE_RESOURCE_DATA_ENTRY 

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {

    DWORD   OffsetToData;        //(重要)资源的偏移,注意这是一个相对虚拟地址(RVA

    DWORD   Size;                     //  ( 重要)资源的大

    DWORD   CodePage;             //  ( 重要)资源页属

    DWORD   Reserved;              //  保留,一般是0

} IMAGE_RESOURCE_DATA_ENTRY, * PIMAGE_RESOURCE_DATA_ENTRY;

比较重要第一个成员和第二个成员,一个是资源的偏移,另一个是资源的大小。

3.4 以上为常规情况下,不常规的情况下,可能第二层的时候,OffsetToData成员就为0了,直接指出了资源的位置。可能是因为不需要知道这个资源是什么语言类型。

3.5 但是最后得到的结构体中数据的偏移OffsetToData是一个相对虚拟地址(RVA),找到它需要转换文件偏移。

声明:以上资料图片参考(黑客免杀攻防 任晓珲  Windows PE 权威指南 戚利   鱼C工作室 小甲鱼)

 
原文地址:https://www.cnblogs.com/hekkoav/p/4424091.html