PE文件格式粗浅认识

前言:最近开始学习PE文件格式,写点笔记做点记录,当然,下面这些只是我这个初学者的一些简单的认识。

1.PE文件格式图总览

在这里插入图片描述

2.PE文件格式介绍

PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有EXE、SCR、DLL、OCX、CPL、DRV、SYS、VXD、OBJ等,事实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是任何扩展名。
认识PE文件不是作为单一内存映射文件被装入内存是很重要的。Windows加载器(又称PE加载器)遍历PE文件并决定文件的哪一部分被映射,这种映射方式是将文件较高的偏移位置映射到较高的内存地址中。由于磁盘对齐与内存对齐的不一样,加载到内存的PE文件与磁盘上的PE文件各个部分的分布有差异。
在这里插入图片描述
先简单说明一下几个知识点:
1 从DOS头到节区头是PE头(NT头)部分,其下的节区合称为PE体。
2 文件中使用偏移(offset),内存中使用VA(VirtualAddress 虚拟地址)来表示位置。
3 文件加载到内存中时,情况就会发生变化。文件的内容一般可以分为代码(.text)、数据(.data)、资源(.rsrc)节,分别保存。
4 PE头与各节区的尾部存在一个区域,称为NULL填充。文件/内存中节区的起始位置应该在各文件/内存最小单位的倍数位置上,空白区域将用NULL填充。
5 VA&RVA,VA是进程虚拟内存的绝对地址,RVA指的是从某个基准位置开始的相对地址。计算公式如下:
RVA+ImageBase=VA
PE头内部信息大多数是以RVA形式存在,因为PE文件加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他的PE文件,这个时候必须通过重定位来将其加载到其他空白的位置。

总括一下:
PE就是在windows下最常用的可执行文件格式,在PE文件中代码,已经初始化的数据,资源和重定位信息等数据被按照属性分类放在不同的section(简称节)中,每个节的属性和位置等信息用一个IMAGE_SECTION_HEADER结构来描述,所有的IMAGE_SECTION_HEADER结构组成一个节表(Section table),节表数据在PE文件中被放在所有节数据的前面,由于数据是按照属性在节中放置的,不同用途但是属性相同的数据(如导入表,导出表等吧。。。可能被放在同一个节中),所以PE结构还用一系列的数据目录结构IMAGE_DATA_DIRECTORY来分别来指明这些数据的位置。
数据目录表和其他描述文件属性的数据合在一起称为PE头文件,PE头文件被放置在节和节表的前面,
为了与DOS系统的文件格式相容又加上了包括IMAGE_DOS_HEADER结构和DOS Stub的DOS部分

3.PE结构分析

在这里插入图片描述
正如上图所示,pe文件由DOS部首,PE头,节区表,节区,调试信息组成
接下来我们用notepad.exe与PE文件的各个部分对比进行演示,稍显不那么枯燥和抽象。

3.1 DOS头(40字节)

PE文件都是以一个64字节的DOS头开始。这个DOS头只是为了兼容早期的DOS操作系统。总共占40个字节,结构如下:

typedef struct IMAGE_DOS_HEADER{
      WORD e_magic;			//DOS头的标识,为4Dh和5Ah。分别为字母MZ
      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;

看到上面结构体众多的成员直接浇灭学习的热情,但是我们只需要关心两个重要成员,e_magic和e_lfanew。前者为DOS头的标识,后者指向NT头,也就是PE头。
在这里插入图片描述

DOS头后跟一个DOS Stub数据,也就是DOS存根,一般是“This program cannot run in DOS mode”(这个可以通过修改链接器的设置来修改成自己定义的数据)。
里面是16位汇编代码,用debug可以看(-u:Unassemble),64位系统看不了。
在这里插入图片描述

3.2 PE头(248字节:4+20+224)

紧跟着DOS stub的时PE头文件(PE Header)。PE Header是PE相关结构NT映像头(IMAGE_NT_HEADER)的简称,其中包含许多PE装载器用到的重要字段。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从IMAGE_DOS_HEADER结构中的e_lfanew字段里找到PE Header的起始偏移量,加上基址得到PE文件头的指针。
PE头的数据结构被定义为IMAGE_NT_HEADERS。包含三部分,其结构如下:

typedef struct IMAGE_NT_HEADERS{
      DWORD Signature;							//PE头标识 50450000
      IMAGE_FILE_HEADER FileHeader;				//文件头
      IMAGE_OPTIONAL_HEADER32 OptionalHeader;	//可选头
}IMAGE_NT_HEADERS,*PIMAGE_NT_HEADERS;

3.2.1Signature字段:

PE头的标识。双字结构。为50h, 45h, 00h, 00h. 即“PE\0\0”。

3.2.2 FileHeader字段:文件头

IMAGE_FILE_HEADER(文件头)结构包含了文件的物理层信息及文件属性。共20字节的数据,其结构如下:

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;				//运行平台
    WORD    NumberOfSections;		//文件的节区数目
    DWORD   TimeDateStamp;			//文件创建日期和时间
    DWORD   PointerToSymbolTable;	//指向符号表(用于调试)
    DWORD   NumberOfSymbols;		//符号表中符号个数(用于调试)
    WORD    SizeOfOptionalHeader;	//IMAGE_OPTIONAL_HEADER32结构大小
    WORD    Characteristics;		//文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

Machine:每个CPU都有唯一的机器码,具体如下:

 #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_SH3               0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP            0x01a3
#define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5               0x01a8  // SH5
#define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB             0x01c2  // ARM Thumb/Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_ARMNT             0x01c4  // ARM Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_AM33              0x01d3
#define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP         0x01f1
#define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS
#define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE           0x0520  // Infineon
#define IMAGE_FILE_MACHINE_CEF               0x0CEF
#define IMAGE_FILE_MACHINE_EBC               0x0EBC  // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64             0x8664  // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R              0x9041  // M32R little-endian
#define IMAGE_FILE_MACHINE_CEE               0xC0EE

NumberOfSections:指出节区数量,该值一定大与0,且当定义的节区数量与直接情况不同时,将发生运行错误。
SizeOfOptionalHeader:因为PE32+格式的文件使用的是IMAGE_OPTIONAL_HEADER64而非IMAGE_OPTIONAL_HEADER32,所以要指明可选头结构体大小。
Characteristics:该字段用于标识文件的属性(是否为可运行的形态,是否为dll等等)。需要记住0x0002为exe,0x2000为dll
在这里插入图片描述

014C		WORD    Machine;				//运行平台
0003		WORD    NumberOfSections;		//文件的节区数目
48025287	DWORD   TimeDateStamp;			//文件创建日期和时间
00000000	DWORD   PointerToSymbolTable;		//指向符号表(用于调试)
00000000	DWORD   NumberOfSymbols;			//符号表中符号个数(用于调试)
00E0		WORD    SizeOfOptionalHeader;	//IMAGE_OPTIONAL_HEADER32结构大小
010F		WORD	Characteristics;			//文件属性

3.2.3OptionalHeader字段:可选头

IMAGE_OPTIONAL_HEADER(可选头),因为文件头不足以定义PE文件属性,因此可选头中定义了更多的数据。总共224个字节,最后128个字节为数据目录(Data Directory),其结构如下:

typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic;						//标志字,32为10B,64为20B
    BYTE    MajorLinkerVersion;						//链接器主版本号
    BYTE    MinorLinkerVersion;						//链接器次版本号
    DWORD   SizeOfCode;								//所有含有代码表的总大小
    DWORD   SizeOfInitializedData;					//所有初始化数据表总大小
    DWORD   SizeOfUninitializedData;				//所有未初始化数据表总大小
    DWORD   AddressOfEntryPoint;		//程序执行入口RVA
    DWORD   BaseOfCode;								//代码表其实RVA
    DWORD   BaseOfData;								//数据表其实RVA
    DWORD   ImageBase;					//程序默认装入基地址
    DWORD   SectionAlignment;			//内存中表的对齐值
    DWORD   FileAlignment;				//文件中表的对齐值
    WORD    MajorOperatingSystemVersion;	        //操作系统主版本号
    WORD    MinorOperatingSystemVersion;	        //操作系统次版本号
    WORD    MajorImageVersion;						//用户自定义主版本号
    WORD    MinorImageVersion;						//用户自定义次版本号
    WORD    MajorSubsystemVersion;					//所需要子系统主版本号
    WORD    MinorSubsystemVersion;					//所需要子系统次版本号
    DWORD   Win32VersionValue;						//保留,通常设置为0
    DWORD   SizeOfImage;				//映像装入内存后的总大小
    DWORD   SizeOfHeaders;				//DOS头、PE头、节区表总大小
    DWORD   CheckSum;								//映像校验和
    WORD    Subsystem;								//文件子系统
    WORD    DllCharacteristics;						//显示DLL特性的旗标
    DWORD   SizeOfStackReserve;						//初始化堆栈大小
    DWORD   SizeOfStackCommit;						//初始化实际提交堆栈大小
    DWORD   SizeOfHeapReserve;						//初始化保留堆栈大小
    DWORD   SizeOfHeapCommit;						//初始化实际保留堆栈大小
    DWORD   LoaderFlags;							//与调试相关,默认值为0
    DWORD   NumberOfRvaAndSizes;		//数据目录表的项数
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

Magic:32位为10B,64位为20B

AddressOfEntryPoint:持有EP的RVA相当重要!

ImageBase:指出文件优先装入的地址。exe,dll文件被装载用户内存的0~7FFFFFFF中,sys文件被载入内核内存80000000~FFFFFFFF中。exe默认0x400000,dll默认0x100000。执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后将EIP的值设为ImageBase+AddresssOfEntryPoint。

SectionAlignment,FileAlignment:SectionAlignment节区在内存的最小单位,FileAlignment表示节区在磁盘的最小单位。两个值可以相同可以不同。

SizeOfImage:将PE文件加载到内存的时候,SizeOfImage指出了内存中所占大小,这与磁盘里面的大小是不一样的哦。

SizeOfHeader:指出整个PE头的大小。其值必须为FileAlignment的整数倍。第一节区所在位置必须与SizeOfHeader据文件开头的偏移的量相同。

Subsystem:用来区分系统驱动文件*.sys和用户可执行文件*.exe,*.dll

NumberOfRvaAndSizes:用来指定DataDirectory数组的个数。

DataDirectory
DataDirectory是OptionalHeader可选头的最后128个字节,也是IMAGE_NT_HEADERSPE头的最后一部分数据。它由16个IMAGE_DATA_DIRECTORY结构组成的数组构成,指向输出表、输入表、资源块等数据。IMAGE_DATA_DIRECTORY的结构如下:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;			//数据块的起始RVA
    DWORD   Size;					//数据块的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

16个数据表(DataDirectory)成员结构如下:

#define IMAGE_DIRECTORY_ENTRY_EXPORT             // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT             // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE           // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION          // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY           // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC          // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG              // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT          // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE       // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR          // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS                // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG        // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT       // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT                // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT       // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR     // COM Runtime descriptor

重点关注Export Directory,Import Directory,Resource Directory,TLS Directory,尤其是Export和Import,重中之重!!之后会单独将其列出来讲解。
好了,介绍完重要成员过后我们来看看notepad.exe的IMAGE_OPTIONAL_HEADER
在这里插入图片描述

010B		WORD    Magic;						//标志字,32为10B,64为20B
07			BYTE    MajorLinkerVersion;					//链接器主版本号
0A			BYTE	MinorLinkerVersion;					//链接器次版本号
00007800	DWORD   SizeOfCode;							//所有含有代码表的总大小
00008800	DWORD   SizeOfInitializedData;				//所有初始化数据表总大小
00000000	DWORD   SizeOfUninitializedData;			//所有未初始化数据表总大小
0000739D	DWORD   AddressOfEntryPoint;		//程序执行入口RVA
00001000	DWORD   BaseOfCode;							//代码表其实RVA
00009000	DWORD   BaseOfData;							//数据表其实RVA
01000000	DWORD   ImageBase;					//程序默认装入基地址
00001000	DWORD   SectionAlignment;			//内存中表的对齐值
00000200	DWORD   FileAlignment;				//文件中表的对齐值
0005		WORD    MajorOperatingSystemVersion;	    //操作系统主版本号
0001		WORD    MinorOperatingSystemVersion;	    //操作系统次版本号
0005		WORD    MajorImageVersion;					//用户自定义主版本号
0001		WORD    MinorImageVersion;					//用户自定义次版本号
0004		WORD    MajorSubsystemVersion;				//所需要子系统主版本号
0000		WORD    MinorSubsystemVersion;				//所需要子系统次版本号
00000000	DWORD   Win32VersionValue;					//保留,通常设置为0
00013000	DWORD   SizeOfImage;				//映像装入内存后的总大小
00000400	DWORD   SizeOfHeaders;				//DOS头、PE头、区块表总大小
00018ADA	DWORD   CheckSum;							//映像校验和
0002		WORD    Subsystem;							//文件子系统
8000		WORD    DllCharacteristics;					//显示DLL特性的旗标
00040000	DWORD   SizeOfStackReserve;					//初始化堆栈大小
00011000	DWORD   SizeOfStackCommit;					//初始化实际提交堆栈大小
00100000	DWORD   SizeOfHeapReserve;					//初始化保留堆栈大小
00001000	DWORD   SizeOfHeapCommit;					//初始化实际保留堆栈大小
00000000	DWORD   LoaderFlags;						//与调试相关,默认值为0
00000010	DWORD   NumberOfRvaAndSizes;		//数据目录表的项数
00000000	RVA of EXPORT Directory
00000000	size of EXPORT Directory
00007604	RVA of IMPORT Directory
000000C8	size of IMPORT Directory
0000B000	RVA of RESOURCE Directory
00007F20	size of RESOURCE Directory
00000000	RVA of EXCEPTION Directory
00000000	size of EXCEPTION Directory
00000000	RVA of SECURITY Directory
00000000	size of SECURITY Directory
00000000	RVA of BASERELOC Directory
00000000	size of BASERELOC Directory
00001350	RVA of DEBUG Directory
0000001C	size of DEBUG Directory
00000000	RVA of COPYRIGHT Directory
00000000	size of COPYRIGHT Directory
00000000	RVA of GLOBALPTR Directory
00000000	size of GLOBALPTR Directory
00000000	RVA of TLS Directory
00000000	size of TLS Directory
000018A8	RVA of LOAD_CONFIG Directory
00000040	size of LOAD_CONFIG Directory
00000250	RVA of BOUND_IMPORT Directory
000000D0	size of BOUND_IMPORT Directory
00001000	RVA of IAT Directory
00000348	size of IAT Directory
00000000	RVA of DELAY_IMPORT Directory
00000000	size of DELAY_IMPORT Directory
00000000	RVA of COM_DESCRIPTOR Directory
00000000	size of COM_DESCRIPTOR Directory
00000000	RVA of Reserve Directory
00000000	size of Reserve Directory

3.3节区表

在PE文件头与原始数据之间存在一个节区表(Section Table),其实就相当于每本书前面的目录,它是一个IMAGE_SECTION_HEADER结构数组,节区表包含每个节区在映像中的信息(如位置、长度、属性),分别指向不同的节区实体。全部有效结构的最后以一个NULL的IMAGE_SECTION_HEADER结构作为结束,所以节区表中总的IMAGE_SECTION_HEADER结构数量总比节的数量多一个。另外,节区表中 IMAGE_SECTION_HEADER 结构的总数总是由FileHeader里面的NumberOfSections 字段来指定的。
在这里插入图片描述

IMAGE_SECTION_HEADER结构定义如下:

typedef struct _IMAGE_SECTION_HEADER {
    Name							//8个字节的节区名
    union						
    {
        DWORD PhysicalAddress;
        DWORD VirtualSize;
    } Misc;                         //节区尺寸</span>
    DWORD VirtualAddress;			//节区的RVA地址
    DWORD SizeOfRawData;			//在文件中对齐后的尺寸
    DWORD PointerToRawData;			//在文件中偏移
    DWORD PointerToRelocations;		//在OBJ文件中使用,重定位的偏移
    DWORD PointerToLinenumbers;		//行号表的偏移(供调试使用地)
    WORD NumberOfRelocations;		//在OBJ文件中使用,重定位项数目
    WORD NumberOfLinenumbers;		//行号表中行号的数目
    DWORD Characteristics;			//节区属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Name:这是一个8位的ASCII(不是Unicode内码),用来定义节区名,多数节区名以“.”开始(如.Text),这个实际上不是必需的,注意如果块名超过了8个字节,则没有最后面的终止标志NULL字节,带有$的节区的名字会从编译器里将带有$的相同名字的区块被按字母顺序合并。
VirtualSize:指出实际的,被使用的节区大小,是节区在没有对齐处理前的实际大小。
VirtualAddress:该块是装载到内存中的RVA,这个地址是按内存页对齐的,总是OptionalHeaderSectionAlignment的整数倍,第一个块默认RVA为1000。
SizeofRawData:该块在磁盘中所占的大小,在可执行文件中,该字段包括经过OptionalHeaderFileAlignment调整后块的长度。例如FileAlignment的大小为200h,如果VirtualSize中的块长度为19Ah个字节,这一块保存的长度为200h个字节。
PointerToRawData:该块是在磁盘文件中的偏移,程序编译或汇编后生成原始数据,这个字段用于给出原始数据块在文件的偏移,如果程序自装载PE或COFF文件(而不是由OS装载),这种情况,必须完全使用线性映像方法装入文件,需要在该块处找到块的数据。
PointerToRelocations 在PE中无意义
PointerToLinenumbers 行号表在文件中的偏移值,文件调试的信息
NumberOfRelocations 在PE中无意义
NumberOfLinenumbers 该节区在行号表中的行号数目
Characteristics 节区属性,(如代码/数据/可读/可写)的标志,这个值可通过链接器的/SECTION选项设置.下面是比较重要的标志:
节区名称以及意义
在这里插入图片描述
每个节区的名称都是唯一的,不能有同名的两个节区。但事实上节的名称不代表任何含义,其存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的节区命名为“.Data” 或者说将包含数据的节区命名为“.Code” 都是合法的。当我们要从PE 文件中读取需要的节区时候,不能以节区的名称作为定位的标准和依据,正确的方法是按照 IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。

在Visual C++中,用#pragma来声明,告诉编译器插入数据到一个节区内:

#pragma data_seg(“MY_DATA”)

链接器能够合并节区。如果两个节区有相似、一致性的属性,那么它们在链接的时候能被合并成一个单一的节区。这取决于是否开启编译器的 /merge 开关。下面的链接器选项将.rdata与.text节区合并为一个.text节区:

/MERGE : .rdata = .text

注意:当合并节区时,因为这没有什么硬性规定。例如,把.rdata合并到.text里不会有什么问题,但是不应该将.rsrc、.reloc或者.pdata合并到其它的节区里。

3.3.1 节区的对齐

节区大小是要对齐的,有两种对齐值,一种用于磁盘文件内,另一种用于内存中。PE文件头指出了这两个值,他们可以不同。
OptionalHeader里边的FileAligment 定义了磁盘节区的对齐值。每一个节区从对齐值的倍数的偏移位置开始存放。而节区的实际代码或数据的大小不一定刚好是这么多,所以在多余的地方一般以00h 来填充,这就是节区间的间隙。例如,在PE文件中,一个典型的对齐值是200h ,这样,每个区块都将从200h 的倍数的文件偏移位置开始,假设第一个区块在400h 处,长度为90h,那么从文件400h 到490h 为这一节区的内容,而由于文件的对齐值是200h,所以为了使这一节区的长度为FileAlignment 的整数倍,490h 到 600h 这一个区间都会被00h 填充,这段空间称为区块间隙,下一个节区的开始地址为600h 。

OptionalHeader里边的SectionAligment 定义了内存中节区的对齐值。PE 文件被映射到内存中时,节区总是至少从一个页边界开始。一般在X86 系列的CPU 中,页是按4KB(1000h)来排列的

3.3.2 文件偏移与RVA

由于一些PE文件为减少体积,磁盘对齐值不是一个内存页1000h,而是 200h,当这类文件被映射到内存后,由于内存里面的对齐值一般是1000h,同一数据相对于文件头的偏移量在内存中和磁盘文件中是不同的,这样就存在着文件偏移地址与虚拟地址的转换问题。
文件被映射到内存,DOS文件头,PE文件头,区块表的偏移位置和大小都没有发生改变。而各区块映射到内存后,起偏移位置发生了改变。
文件被映射到内存称之为RVA to RAW

3.4 RVA to RAW.

就是PE文件从磁盘映射到内存,每个区块都要准确无误地完成文件偏移到内存地址的映射。
RAW - PointerToRawDate = RVA - VirtualAddress
RAW = RVA - VirtualAddress + PointerToRawDate

看看《逆向工程核心原理》上的例子:
在这里插入图片描述在这里插入图片描述

3.5 IAT(Import Address Table)

3.5.1dll

先介绍一下DLL(Dynamic Linked Library)的概念,中文翻译为动态链接库,它是Windows的根基。32位才引入这一概念。Windows操作系统使用数量庞大的库函数,而且支持多进程,若像以前一样每个程序运行都包含相同的库,将造成严重的内存浪费和磁盘浪费。在此背景下,Windows的设计师们引入DLL这一概念。优点很明显:
将库函数单独组成DLL文件,需要时再调用。
更新库时只需要更新对应的DLL文件,避免每次更新不必要的资源。
内存映射技术使得加载过后的DLL代码可以在多进程中使用。
加载DLL的方式有两种,一种是“显式链接”(Explicit Linking),程序使用DLL时再加载,使用完毕过后释放内存;另外一种是“隐式链接”(Implicit Linking),程序开始就一起加载DLL,程序终止时再释放内存。IAT与后者相关

3.5.2 IAMGE_IMPORT_DESCRITPTOR

IAMGE_IMPORT_DESCRITPTOR(也被称为IMPORT Directory Table)
IID( IAMGE_IMPORT_DESCRITPTOR)结构体中记录着PE文件要导入哪些库文件。导入多少库就有多少个IID,这些IID形成结构体数组,最后以NULL结构体结束。
IID的结构如下:

STRUCT IAMGE_IMPORT_DESCRIPTOR
{
    union
    {
        DWORD Characteristics;
        DWORD OriginalFirstThunk; //指向输入名称表(INT)的RVA,未指明大小,以NULL结束
    }
    DWORD TimeDateStamp;          //一个32位的时间标志
    DWORD ForwarderChain;         //这是一个被转向API的索引,一般为0
    DWORD Name;                  //DLL名字,是个以00结尾的ASCII字符的RVA地址
    DWORD FirstThunk;            //指向输入地址表(IAT)的RVA,未指明大小,以NULL结束
} ;

以下是IID结构体重要成员

OriginalFirstThunkINT(Import Name Table)地址(RVA)
Name库名字字符串地址(RVA)
FirstThunkIAT(Import Address Table)地址(RVA)

好了现在讲解一下PE装载器是如何把导入函数输入IAT顺序:

  • 读取IID的Name成员,获取库名称字符串,比如说kernel32.dll
  • 利用LoadLibrary装载库kernel32.dll
  • 读取IID的OriginalFirstThunk成员获取INT地址
  • 逐一读取INT中数组的值,获取相应IMAGEZ_IMPORT_BY_NAME地址(RVA)
  • 使用IMAGEZ_IMPORT_BY_NAME的Hint或者Name项获取相应函数的起始地址。
    —>GetProcAddress(“GetCurrentThreadld”)
  • 读取IID的FirstThunk获取IAT地址

现在有个问题,IID结构体在哪呢?它不在PE头而在PE体,但查找其位置的信息就位于IMAGE_OPTIONAL_HEADER32.DataDirectory[1]中,VirtualAddress的值就是IID的RVA。
为了方便查看,我们列出前三个成员

  • RVA of EXPORT Directory
  • size of EXPORT Directory
  • RVA of IMPORT Directory
  • size of IMPORT Directory
  • RVA of RESOURCE Directory
  • size of RESOURCE Directory

现在是不是已经晕头转向了,没关系,我们将以notepad.exe演示一遍。
在这里插入图片描述

如图所示,RVA是7604,计算偏移为7604-1000+400=6A04。此处即为IAMGE_IMPORT_DESCRITPTOR数组的所有成员,IID数组大小未定,但是最后以NULL结束,前五个框上的即为数组第一个结构体的五个成员
在这里插入图片描述

我们将其重要成员列出来:

RVA				成员			             RAW
00007990		OriginalFirstThunk(INT)		00006D90
00007AAC		Name			       		00006EAC
000012C4		FirstThunk(IAT)			    000006C4

1.库名称Name
Name是一个字符串指针,它指向导入函数的库文件名称,我们去6EAC看看
在这里插入图片描述
2.INT(NULL结尾)
INT是一个包含导入函数信息的结构体指针数组,也就是说只有获得了这些信息,才能在加载在进程中的库文件的库中求得相应函数起始地址。INT数组成员全是地址(RVA),以NULL结尾,每个地址分别指向一个IMAGE_IMPORT_BY_NAME结构体,说的那么高大上,其实就是指向函数名称。
在这里插入图片描述
我们跟踪一下RVA 7A7A—>RAW 6E7A
在这里插入图片描述
我们跟踪一下RVA 7A5E—>RAW 6E5E
在这里插入图片描述
3.FirstThunk(IAT)
FirstThunk也就是IAT(Import Address Table)数组,以NULL结尾,这个数组就是对应的库文件的数组,里面的成员为地址与INT数组的函数名字对应,
上面的IAT的RVA是12C4,RAW为06C4,我们跟踪看看
在这里插入图片描述
也就是说,comdlg32.dll库文件里面的PageSetupDlgW函数的地址为0x76344906,FindTextW函数地址为0x763385CE。

现在我们来总结一下流程:可选头里面的DataDirectory[1]结构体里面的第一个元素(4个字节一个元素)就是指向IAMGE_IMPORT_DESCRITPTOR数组的RVA,计算得到IID数组的偏移RAW,IID数组无特定大小,5个成员组成一个结构体,每个成员4个字节,都是RVA。结构体一共5✖4=20字节,导入多少函数就有多少这样的结构体,反正最后以NULL结尾。结构体里面的5个成员中,第2个和第3个没啥用,重点是第1个,第4和第5个成员,他们分别是OriginalFirstThunk(INT),Name和FirstThunk(IAT)的RVA,计算得到分别的RAW。
Name:查看导入库文件的名字,可以看到字符串。
INT:查看该库文件的函数信息,4个字节组成一个单位,表示函数名字的RVA,无特定大小,反正最后以NULL结尾。计算RAW,可以在得到的RAW看到函数名字。
IAT:保存着库文件函数的地址,是函数的地址,不是函数名字,注意不要和上面INT搞混,也是以NULL结尾,所指向的地址和INT里面的函数一一对应。

3.6 EAT(Export Address Table)

有上面的IAT,EXE文件来导入函数,肯定就有DLL文件用EAT来导出函数让其他进程使用。EAT也是Windows OS的核心机制,它使得不同的应用程序可以调用库文件中提供的函数。和IAT一样,PE文件可选头里面的也有一个IMAGE_EXPORT_DIRECTORY结构体,每个成员都为RVA。它是数据目录表(DateDirectory)的第一个元素DateDirectory[0]。

RVA of EXPORT Directory
size of EXPORT Directory
RVA of IMPORT Directory
size of IMPORT Directory
RVA of RESOURCE Directory
size of RESOURCE Directory

3.6.1IMAGE_EXPORT_DIRECTORY

IAMGE_EXPORT_DIRECTORY结构体如下:

typedef struct _IAMGE_EXPORT_DIRECTORY
{
    DWORD Characteristics;          //未使用,总为0
    DWORD TimeDateStamp;            //创建输出表创建时间(GMT时间)
    WORD MajorVersion;              //主版本号,一般为0
    WORD MinorVersion;              //次版本号,一般为0
    DWORD Name;                     //模块的真实名称
    DWORD Base;                     //基数,加上序数就是函数数组的索引值
    DWORD NumberOfFunctions;        //AddressOfFunctions阵列中的元素个数
    DWORD NumberOfNames;            //AddressOfNameS阵列中的元素个数
    DWORD AddressOfFunctions;       //指向函数地址数组
    DWORD AddressOfNames;           //函数名字的指针地址
    DWORD AddressOfNameOrdinals;    //指向输出序号数组
};

看到如此众多的成员让人头皮发麻,但是和IAT一样,没必要掌握所有。下面列出重要成员:

NumberOfFunctions			AddressOfFunctions阵列中的元素个数
NumberOfNames				AddressOfNameS阵列中的元素个数
AddressOfFunctions			指向函数地址数组
AddressOfNames				函数名字的指针地址
AddressOfNameOrdinals		指向输出序号数组

操作系统通过GetProcAddress()函数获得库中函数的地址,该API用EAT来获取API地址,GetProcAddress()函数的工作原理就显得尤为重要,也就是说,搞懂了其工作原理,就搞懂了EAT。接下来我们看看简单的流程:

  • 1.利用AddressOfNames成员转到函数名称数组。
  • 2.函数名称数组存储着字符串地址,通过挨个比较字符串,查找指定的函数名称。
  • 3.利用AddressOfNameOrdinals成员转到orinal(索引)数组。
  • 4.在ordinal数组中通过index查找对应的orinal值。
  • 5.利用AddressOfFunctions成员转到函数地址数组EAT。
  • 6.在函数地址数组中将orinal作为索引获得指定函数的起始地址

到这里我已经晕了,云里雾里的,接下来我们用kernel32.dll看一下。
在这里插入图片描述

IAMGE_EXPORT_DIRECTORY的RAW:262C-1000+400=1A2C,我们跟去1A2C看看
在这里插入图片描述

 成员  							 值				RAW
 Characteristics;            00000000			 -
 TimeDateStamp;              48025BE1			 -
 MajorVersion;                 0000			 	-
 MinorVersion;                 0000				 -
 Name;                       00004B8E			3F8E
 Base;                       00000001			 -
 NumberOfFunctions;          000003B9			 -
 NumberOfNames;              000003B9 			 -
 AddressOfFunctions;         00002654			1A54
 AddressOfNames;             00003538			2938
 AddressOfNameOrdinals;	     0000441C			381C

然后按照之前的流程看看。
1.函数名称数组
比如我们要查找函数AddAtomA,AddressOfNames的RAW为2938,使用winhex查看。
在这里插入图片描述

此处为4字节RVA组成的数组,数组元素个数为NumberOfNames(3BA).逐一跟随所有RVA的值可以发现函数名称字符串。
当我们逐一跟随到第二个时,就会发现我们的目标函数字符串,因为是数组的第二个元素,该函数的数组索引index为1
RVA:4BAA—>RAW:3FAA
在这里插入图片描述
2.Ordinal数组
Ordinal数组中各个元素大小为2个字节,AddressOfNameOrdinals 成员的值为RVA:0000441C----->RAW:381C
在这里插入图片描述

利用上面求到的index值作为数组下标带入Ordinal数组得到索引值ordinal为0001.

3.函数地址数组(EAT)
现在我们要查找AddAtomA函数的实际地址,AddressOfFunctions------>RVA:00002654----->RAW:1A54,这就是EXPORT数组的地址(各个元素大小为4个字节,里面保存着各个函数地址的RVA)将求得的ordinal作为数组下标带入进去,得到AddAtomA函数的RVA:35505—>RAW:
在这里插入图片描述
kernel32.dll的ImageBase为7C7C0000。因此AddAtomA函数的实际地址VA=7C800000+35505=7C835505‬
可以用OD验证一下。
在这里插入图片描述

NumberOfFunctions			AddressOfFunctions阵列中的元素个数
NumberOfNames				AddressOfNameS阵列中的元素个数
AddressOfFunctions			指向函数地址数组
AddressOfNames				函数名字的指针地址
AddressOfNameOrdinals		指向输出序号数组

现在我们来总结一下流程:可选头里面的DataDirectory[0]结构体里面的第一个元素(4个字节一个元素)就是指向IAMGE_EXPORT_DESCRITPTOR结构体的RVA,计算得到数组的偏移RAWIAMGE_EXPORT_DESCRITPTOR结构体一共占40个字节,最后五个成员十分重要。AddressOfNames是由每个元素4字节的RVA组成的数组,数组元素个数为NumberOfNames,逐一跟随所有RVA的值发现函数名称字符串。并以此数组下标得到index的值,AddressOfNameOrdinals成员就是由每个元素2个字节组成的Ordinal数组,将index作为下标得到Ordinal,AddressOfFunctions成员就是由函数的RVA组成的数组,将Ordinal作为下标得到函数的偏移值,加上基址便得到函数实际地址。

看到这里,你已经初步了解了PE文件格式,对PE文件结构也有了个大体印象,
初次发这种文章,排版,文字可能还有诸多不恰当之处,诸多纰漏还望不吝赐教,再次感谢你耐心看到此处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值