- 建议:微软文档写的属实好
https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
如何识别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 Misc与SizeOfRawData:
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个位就能索引。
- PE重定位操作原理
- 在应用进程中查找硬编码的地址位置
- 读取值后,减去ImageBase(VA->RVA)(原本这些值是以IB为基准的硬编码值)
- 加上实际加载的地址(RVA->VA)
资源表
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; 资源属性
DWORD TimeDateStamp; 时间戳
WORD MajorVersion; 资源大版本号
WORD MinorVersion; 资源小版本号
WORD NumberOfNamedEntries; 按照名称命名的数量
WORD NumberOfIdEntries; 按照ID命名的数量
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
在资源表后紧跟着资源目录项(子目录),如下:
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31; 位段: 低31位飘逝偏移 定义了目录项的名称或者ID
DWORD NameIsString:1; 位段: 高位, 如果这位为1,则表示31位的偏移指向的是一个Unicode字符串的指针偏移
}; 这里列出结构体,自己去看,IMAGE_RESOURCE_DIR_STRING_U 里面是字符串长度还有字符串,不是\0结尾
DWORD Name;
WORD Id;
};
union {
DWORD OffsetToData; 偏移RVA因为是联合体,所以有不同的解释
struct {
DWORD OffsetToDirectory:31; 看高位,如果高位是1,那么RVA偏移指向的是新的(根目录)
DWORD DataIsDirectory:1;
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
数据项:
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; //数据的偏移 重要
DWORD Size; //数据的大小 重要
DWORD CodePage; //代码页(一般为0)
DWORD Reserved; //保留
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
附一篇重定位博文: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:表示文件的字节顺序如果不是机器所期望的,那么在读出之前要进行
交换。在可执行文件中它们是不可信的(操作系统期望按正确的字节顺序执行程序)。