PE结构

如何识别PE文件

PE指纹:在用编辑工具以十六进制打开文件时文件头部的特征。
如图:在这里插入图片描述
在最开始两个字节写的是MZ,在0x3C~0x3F 4个字节位置 写的是0x80,代表PE头在文件开始偏0x80处,对应PE两个字母,这样的一种对应关系叫PE指纹。

PE格式概貌

在这里插入图片描述
以随意一个PE文件用编辑工具打开为例(此处用的Winhex)
首先我们看看 IMAGE_DOS_HEADER里有什么
(最近发现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;

直观的如图所示
在这里插入图片描述
然后是DOS块
在这里插入图片描述
DOS块的大小任意,里面的数据也任意,并不影响程序的执行。

然后是PE文件头(4字节)与标准PE头(20字节) _IMAGE_FILE_HEADER

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;   //PE可选头的大小
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

在这里插入图片描述
方括号内非阴影部分为PE头,阴影部分为标准头。

然后是可选头(大小可变,大小数据在标准头中) _IMAGE_OPTIONAL_HEADER

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_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

在这里插入图片描述
1是SectionAlignment ,2是FileAlignment,3是SizeOfHeaders。
之后每0x40个字节一个节表。

标准PE头详解

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;                          //指定运行的cpu型号
    WORD    NumberOfSections;			//节表成员数量
    DWORD   TimeDateStamp;			//时间戳  由编译器填写
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;   //PE可选头的大小
    WORD    Characteristics;				//标记文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

Machine:0表示任意cpu ,014c表示intel 386及后续,8664表示x64。
TimeDateStamp:表示从1970.1.1开始过的秒数,此参数可变,无实际意义。
Characteristics

0 它表明此文件不包含基址重定位信息,因此必须被加载到其首选基地址上。如果基地址不可用,加载器会报错。
1 它表明此镜像文件是合法的。看起来有点多此一举,但又不能少。
2,3,4 保留,必须为0。
5 应用程序可以处理大于2GB的地址。
6,7 保留,必须为0。
8 机器类型基于32位体系结构。
9 调试信息已经从此镜像文件中移除。
10 如果此镜像文件在可移动介质上,完全加载它并把它复制到交换文件中。几乎不用
11 如果此镜像文件在网络介质上,完全加载它并把它复制到交换文件中。几乎不用
12 此镜像文件是系统文件,而不是用户程序。
13 此镜像文件是动态链接库(DLL)。
14 此文件只能运行于单处理器机器上。
15 保留,必须为0。

栗子:0x0102 二进制形式 0000 0001 0000 0010
注意 从右往左编号
即第一位为1,第9位为1。

扩展PE头详解

(声明:32位程序与64位程序的扩展头PE头是俩个不同的结构体,虽然差别不大,此处以32位为例)


typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic; 								//标识是32位程序还是64位程序
    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;	//在内存中文件大小,必须为SectionAlignment的整数倍
    DWORD   SizeOfHeaders;//文件头包括节表的大小(内存硬盘大小不变)
    DWORD   CheckSum; 		//校检和 用于判断文件是否被修改
    WORD    Subsystem;			//子系统
    WORD    DllCharacteristics;	//文件特性,非针对DLL
    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;

Magic:32位:10B , 32位+:20B
CheckSum:通过将程序每两字节的数据相加(溢出就让他溢出)最后加上文件大小得出一个值。
Subsystem

#define IMAGE_SUBSYSTEM_UNKNOWN              0   // Unknown subsystem.
#define IMAGE_SUBSYSTEM_NATIVE               1   // Image doesn't require a subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_GUI          2   // Image runs in the Windows GUI subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_CUI          3   // Image runs in the Windows character subsystem.
#define IMAGE_SUBSYSTEM_OS2_CUI              5   // image runs in the OS/2 character subsystem.
#define IMAGE_SUBSYSTEM_POSIX_CUI            7   // image runs in the Posix character subsystem.
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS       8   // image is a native Win9x driver.
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI       9   // Image runs in the Windows CE subsystem.
#define IMAGE_SUBSYSTEM_EFI_APPLICATION      10  //
#define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER  11   //
#define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER   12  //
#define IMAGE_SUBSYSTEM_EFI_ROM              13
#define IMAGE_SUBSYSTEM_XBOX                 14
#define IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION 16

DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]

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

节表

节表是n个IMAGE_SECTION_HEADER STRUCT。存储各个节的信息。

IMAGE_SECTION_HEADER STRUCT{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 8个字节的节区名称
union {
  DWORD PhysicalAddress;       
  DWORD VirtualSize;            
}Misc  //此节在读取到内存中的总大小,单位是字节。如果此值大于 SizeOfRawData 成员的话,此节将被0填充。此值仅当可执行镜像且object文件必须被设置为0时有效。
DWORD VirtualAddress;         // 在内存中相对基址的偏移。
DWORD SizeOfRawData;            // 在文件中对齐后的尺寸
DWORD PointerToRawData;        // 在文件中的偏移量
			DWORD PointerToRelocations;     // 在OBJ文件中使用,重定位的偏移
			DWORD PointerToLinenumbers;   // 行号表的偏移(供调试使用地)
			WORD NumberOfRelocations;      // 在OBJ文件中使用,重定位项数目
			WORD NumberOfLinenumbers;    // 行号表中行号的数目             //此四个为调试相关
DWORD Characteristics;       // 节属性如可读,可写,可执行等
}IMAGE_SECTION_HEADER 

union MiscSizeOfRawData
union里的数值可能会比SizeOfRawData里的值大
原因是如果这个节是用来存全局变量的,若全局变量没有初始值,则在文件中不会为其分配空间,也就是说当无初值的全局变量的数量达到一定程度时 ,union Misc > SizeOfRawData
Characteristics:讲一下怎么查吧… 懒得搬了
将四字节数据由小端展开,按顺序转化成二进制,二进制数从右往左编号,从0开始。

导出表

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;            // 指向本导出文件名
    DWORD   Base;           //导出函数的最小的序号(起始序号)
    DWORD   NumberOfFunctions;     // 总的导出函数的个数
    DWORD   NumberOfNames;      // 这个是有名称的函数的个数,因为有的导出函数是没有名字的,只有序号
    DWORD   AddressOfFunctions;     // RVA 导出函数地址表(所有函数)
    DWORD   AddressOfNames;         // RVA 导出函数名称表(有名称的函数)
    DWORD   AddressOfNameOrdinals;  // RVA 导出函数序号表(有名称的函数)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

假设一def文件定义如下(不知道的百度动态链接库的编写)
在这里插入图片描述
三张子表如下
在这里插入图片描述
函数地址表是按函数序号排列,若序号有间隔,则间隔的序号位置以0填充。
序号表是方便名称表进行索引的,序号表的成员数 == 名称表成员数。
当我们使用GetProcessAdress时,参数里若填函数名称,则其会遍历函数名称表,找到名称表里对应的下标(Div为0,Mul为1),再拿此下标到序号表里去索引(4的下标为0,1的下标为1),最后拿序号表里索引到的值作为地址表的下标索引函数地址。
参数里若填序号,则会将序号-Base(结构体内成员,此例中Base=12)作为下标直接索引函数地址表。

导入表

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            //导入表结束标志
        DWORD   OriginalFirstThunk;         //RVA指向一个结构体数组(INT表) 数组
    };
    DWORD   TimeDateStamp;                  //时间戳
    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;                           //RVA指向dll名字,以0结尾
    DWORD   FirstThunk;                     //RVA指向一个结构体数组(IAT表)
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;

导入表往往有多个,也就是说导入表实际是一个结构体数组,此结构体数组以20个字节的0作为结束标志。
INT,IAT指向同样的结构,如下

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        //RVA 指向_IMAGE_IMPORT_BY_NAME      
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

_IMAGE_THUNK_DATA32里存的就是一个4字节数据
这个4字节数据若最高位为1,则将1变为0后的数据就是导入函数的序号
如果最高位不为1,则此4字节数据为一个RVA,指向结构体_IMAGE_IMPORT_BY_NAME

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;       //可能为0,编译器决定,如果不为0,是函数在导出表中的索引
    BYTE    Name[1];    //函数名称,以0结尾,由于不知道到底多长,所以干脆只给出第一个字符,找到0结束
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

注意:文件被加载前IAT与INT相同,但加载后IAT里存的将会是各个函数的地址。(暂不深究)

导入表大致结构图:
在这里插入图片描述

重定位表

这是一张神奇的表。
因为一个进程往往有许多模块,及许多PE文件,难免会有PE文件的ImageBase相同,此时windows装载器会更换冲突模块的基址,但,基址改了,模块里记录的地址没有改(如全局变量),这时候就需要重定位表来更改地址。(至于重定位表怎么知道更改后地址的,我觉得是装载器干的好事)

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;            页存储的RVA
    DWORD   SizeOfBlock;              本结构体大小,以字节为单位
//  WORD    TypeOffset[1];						这个在定义中被注释掉了
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

这张表,也有很多很多个。
SizeOfBlock:这个数字会大于8字节,多出的部分会用于记录对于同结构体中相对于VirtualAddress的偏移。
多出的部分是一个word型数组,里面元素的高四位代表的值有特殊含义。
在这里插入图片描述
若高4位为0011,则剩下的低12位就是偏移了。
即重定位地址 = 模块重定位基址+本结构体成员VirtualAddress+word数组元素低12位

为什么要这样设计呢,因为大多数的地址会呈现出连续分布,这样能节省空间。
顺带一句,VirtualAddress是以4KB(内存分页大小)一段一段的,所以使用12个位就能索引。
附一篇重定位博文:https://www.cnblogs.com/iBinary/p/7690069.html
最后,附一张PE神图:在这里插入图片描述
附录:
IMAGE_FILE_HEADER.Characteristics各个位详解:

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:表示文件的字节顺序如果不是机器所期望的,那么在读出之前要进行
交换。在可执行文件中它们是不可信的(操作系统期望按正确的字节顺序执行程序)。

发布了25 篇原创文章 · 获赞 13 · 访问量 2974

猜你喜欢

转载自blog.csdn.net/qq_43445167/article/details/89323025