Windows编程系列:PE文件结构
PE文件结构
Portable Executable (PE),可移植的可执行文件。在Windows平台下,所有的可执行文件(包括.exe, .dll, .sys, .ocx, .com等)均使用PE文件结构。这些使用了PE文件结构的可执行文件也称为PE文件。
PE结构包含的结构体有DOS头,PE标识 、文件头、可选头、目录头、目录结构、节表等
整体结构如下:
从上图可以看出PE结构分为4大部分,其中每个部分又进行了细分。
从数据管理的角度来看,可以把PE文件大致分为两部分,
1、DOS头、PE头和节表属于PE文件的数据管理结构或数据组织结构部分,
2、节表数据才是PE文件真正的数据部分,其中包含着代码、数据、资源等内容。
DOS头
DOS头分为“MZ头部”和"DOS存根“。
”MZ头部“是真正的DOS头部,由于其开始处的两个字节为"MZ",因此DOS头也可以叫作MZ头部。
这个我们用十六进制编辑器随便打开一个exe就可以看到
该部分用于程序在DOS系统下加载,它的结构被定义为IMAGE_DOS_HEADER
IMAGE_DOS_HEADER定义
//大小为: 0x40(64)字节
#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
+0h WORD e_magic; // MZ标记 0x5a4d
+2h WORD e_cblp; // 最后(部分)页中的字节数
+4h WORD e_cp; // 文件中的全部和部分页数
+6H WORD e_crlc; // 重定位表中的指针数
+8H WORD e_cparhdr; // 头部尺寸以段落为单位
+0aH WORD e_minalloc; // 所需的最小附加段
+0cH WORD e_maxalloc; // 所需的最大附加段
+0eH WORD e_ss; // 初始的SS值(相对偏移量)
+10H WORD e_sp; // 初始的SP值
+12H WORD e_csum; // 补码校验值
+14H WORD e_ip; // 初始的IP值
+16H WORD e_cs; // 初始的SS值
+18H WORD e_lfarlc; // 重定位表的字节偏移量
+1aH WORD e_ovno; // 覆盖号
+1cH WORD e_res[4]; // 保留字
+24H WORD e_oemid; // OEM标识符(相对m_oeminfo)
+26H WORD e_oeminfo; // OEM信息
+29H WORD e_res2[10]; // 保留字
+3CH LONG e_lfanew; // NT头(PE标记)相对于文件的偏移地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
DOS存根是一段简单的程序,主要用于输出“This program cannot be run in DOS mode.”类似的提示字符串。
为什么PE结构的最开始位置有这样一段DOS头部呢?
为了该可执行程序可以兼容DOS系统。通常情况下,Win32下的PE程序不能在DOS下运行,因此保留了这样一个简单的DOS程序用于提示“不能运行于DOS模式下”。
+3CH LONG e_lfanew; // NT头(PE标记)相对于文件的偏移地址
+3CH 开始的四个字节标识PE头开始
PE头解析
PE文件头由PE文件头标志,标准PE头,扩展PE头三部分组成。PE文件头标志自然是50 40 00 00,也就是’PE’,我们从结构体的角度看一下PE文件头的详细信息
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE文件头标志 => 4字节
IMAGE_FILE_HEADER FileHeader; //标准PE头 => 20字节
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //扩展PE头 => 32位下224字节(0xE0) 64位下240字节(0xF0)
} IMAGE_N
PE头签名(4个字节)
文件头IMAGE_FILE_HEADER(20字节)
IMAGE_FILE_HEADER是IMAGE_NT_HEADERS结构体中的一个结构,紧接在PE标识符(Signature字段)的后面。
IMAGE_FILE_HEADER占用20个字节
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //可以运行在什么平台上 任意:0 ,Intel 386以及后续:14C x64:8664
WORD NumberOfSections; //节的数量 区段数
DWORD TimeDateStamp; //编译器填写的时间戳,什么时间被创建
DWORD PointerToSymbolTable; //调试相关
DWORD NumberOfSymbols; //调试相关
WORD SizeOfOptionalHeader; //标识扩展PE头大小
WORD Characteristics; //文件属性 => 16进制转换为2进制根据哪些位有1,可以查看相关属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine(1字节),标识在什么机器上运行
WORD 2字节,该字段表示可执行文件的目标CPU类型,取值如下:
这里我们看到的是64 86,也就是可以在上述对应环境运行
NumberOfSections:WORD 2字节
该字段表示PE文件的节区的个数.请注意,Windows 加载程序将节区限制为 96。
该字段的值为06 00,即为0x00000006,表示该PE文件的节区有6个。
.txt 代码段
.data 可读写的数据段
.rdata 只读写的数据段
.pdata 节是由用于异常处理的函数表项组成的数组
.idata 导入数据段
.edata 导出数据段
.rsrc资源段
.reloc 主要用于存储基址重定位表
使用工具LordPE可以看到有6个节区
TimeDataStamp:
DWORD 4字节,该字段表示 文件是何时被创建的,这个值是自1970/1/1以来用格林威治时间计算的秒数。
界面上可以看到这个字段的取值是0xC2B75AE3,我们写一个简单的程序计算一下
import datetime
# 给定的十六进制数
hex_seconds_since_epoch = 0xC2B75AE3
# 将十六进制转换为十进制
decimal_seconds_since_epoch = int(hex_seconds_since_epoch)
# 创建一个表示Unix纪元的datetime对象
epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
# 从Unix纪元开始增加秒数
dt = epoch + datetime.timedelta(seconds=decimal_seconds_since_epoch)
# 增加8小时来调整时区
dt_with_offset = dt.astimezone(tz=datetime.timezone(datetime.timedelta(hours=8)))
# 打印结果
print(dt_with_offset.strftime('%Y-%m-%d %H:%M:%S %Z%z'))
PointerToSymbolTable:
DWORD 4字节,符号表的偏移量(以字节为单位),如果没有 COFF 符号表,则为零。
NumberOfSymbols
DWORD 4字节,符号表中的符号数。
SizeOfOptionalHeader
WORD 2字节,该字段 指定IMAGE_OPTIONAL_HEADER结构的大小。
我们这里的值是F0 00,也就是0x000000F0。
注意:在计算IMAGE_OPTIONAL_HEADER的大小时,应该取SizeOfOptionalHeader的值,而不是使用sizeof()函数。
因为IMAGE_OPTIONAL_HEADER结构体的大小可能是会改变的。
Characteristics
WORD 2字节,指定文件的类型。取值如下:
我们这里是22 00 ,也就是0x00000022,这是一个组合值(IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_LARGE_ADDRESS_AWARE),代表这是一个可执行文件,且能处理超过2GB的内存。
可选头IMAGE_OPTIONAL_HEADER
可选头IMAGE_OPTIONAL_HEADER是IMAGE_NT_HEADERS结构体中的一个结构,紧接在IMAGE_FILE_HEADER类型之后。
可选头是对文件头的一个补充。文件头主要描述文件的相关信息,而可选头主要用来管理PE文件被操作系统装载时所需要的信息。
IMAGE_OPTIONAL_HEADER在现在的大部分资料中都被称为可选头。但该头部实际上是一个必须存在的头。
所以这里应该是最初在翻译时或者其它方面导致的错误,后人一直沿用了。Option也有选项之类的意思 。
IMAGE_OPTIONAL_HEADER的大小在IMAGE_FILE_HEADER的SizeOfOptionalHeader字段中给出。
我们这里的值是F0 00,也就是IMAGE_OPTIONAL_HEADER的大小是0x000000F0。
IMAGE_OPTIONAL_HEADER(可选头)在IMAGE_FILE_HEADER(文件头)之后
IMAGE_FILE_HEADER的结束位置在0x000010F,那么可选头的起始位置是0x00000110
通过下面的图可以比较清晰的看到
选头的结束位置是0x00000110 + 0x000000F0 -1 = 0x000001FF,如下图所示
通常情况下,可选头的结尾后面跟的是第一项节表的名称,就是值为.text的位置。
如上图所示:文件偏移0x0000200处的节点名称为".text",也就是说,可选头的结束位置在0x0000200偏移的前一字节,即0x00001FF
下面我们来详细看一下IMAGE_OPTIONAL_HEADER的定义
IMAGE_OPTIONAL_HEADER实际是一个宏,定义如下:
1 #ifdef _WIN64
2 typedef IMAGE_OPTIONAL_HEADER64 IMAGE_OPTIONAL_HEADER;
3 #else
4 typedef IMAGE_OPTIONAL_HEADER32 IMAGE_OPTIONAL_HEADER;
5 #endif
这里我们以x64为例,所以使用的是IMAGE_OPTIONAL_HEADER64类型,定义如下
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG 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;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
成员说明:
Magic
WORD 2字节,文件的状态类型,它可以是以下值之一
我们这里的值是0B 02,实际取值是0x0000020B,所以这是一个64位的可执行文件。
MinorLinkerVersion:链接器的次版本号
SizeOfCode:代码节的大小(以字节为单位),如果有多个代码块,则为所有此类块的总和。
SizeOfInitializedData:初始化的数据块的大小(以字节为单位),如果有多个已初始化的数据块,则为所有此类块的总和。
SizeOfUninitializedData:未初始化数据块的大小(以字节为单位),如果有多个未初始化的数据块,则为所有此类块的总和。
AddressOfEntryPoint:指向入口点函数(相对于映像基址)的指针。 对于可执行文件,这是起始地址。 对于设备驱动程序,这是初始化函数的地址。 入口点函数对于 DLL 是可选的。 如果没有入口点,则此成员为零。
BaseOfCode:指向代码部分开头(相对于映像基址)的指针。
ImageBase:文件被装入内存后的首选建议装载地址。对于EXE文件来说,通常情况下,该地址就是装载地址;对于DLL文件来说,可能就不是其装入内存后的地址了。
SectionAlignment:节表被装入内存后的对齐值。节表被映射到内存中需要对其的单位。在Win32下,通常情况下,该值为0x1000,也就是4KB大小。Windows操作系统的内存分页一般为4KB。此值必须大于或等于 FileAlignment 成员。
FileAlignment:节表在文件中的对齐值。通常情况下,该值为0x1000或0x200。在文件对齐值为0x1000时,由于与内存对齐值相同,可以加快装载速度。
而文件对齐值为0x200时,可以占用相对较少的磁盘空间。0x200是512字节,通常磁盘的一个扇区即为512字节。
说明:
程序无论是在内存中还是磁盘上,都无法恰好满足SectionAlignment和FileAlignment值的倍数,在不足的情况下需要补0值,这样就导致节与节之间存在了无用的空隙。这些空隙对于病毒之类程序而言就有了可利用的价值。
MajorOperatingSystemVersion:要求最低操作系统的主版本号。
MinorOperatingSystemVersion:要求最低操作系统的次版本号。
MajorImageVersion:可执行文件的主版本号。
MinorImageVersion:可执行文件的次版本号。
MajorSubsystemVersion:子系统的主版本号。
MinorSubsystemVersion:子系统的次要版本号。
Win32VersionValue:此成员为保留成员,必须为 0。
SizeOfImage:可执行文件装入内存后的总大小。该大小按内存对齐方式(SectionAlignment )对齐。
SizeOfHeaders:整个PE头部的大小。这个PE头部泛指DOS头、PE头、节表的总和大小。舍入为 FileAlignment 成员中指定的值的倍数。
CheckSum:校验和值。对于EXE文件通常为0;对于SYS文件,则必须有一个校验和。
SubSystem:运行此映像所需的子系统。 定义了以下值。
DllCharacteristics:指定DLL文件的特征,该值大部分时候为0,系统定义了以下值
SizeOfStackReserve:要为堆栈保留的字节数。 加载时只提交 由 SizeOfStackCommit 成员指定的内存;其余部分一次提供一页,直到达到此保留大小。
SizeOfStackCommit:要为堆栈提交的字节数。
SizeOfHeapReserve:要为本地堆保留的字节数。 加载时仅提交 由 SizeOfHeapCommit 成员指定的内存;其余部分一次提供一页,直到达到此保留大小。
SizeOfHeapCommit:要为本地堆提交的字节数。
LoaderFlags:此成员已过时。
NumberOfRvaAndSizes:数据目录项的个数。该个数在PSDK中有一个宏定义,
DataDirectory:数据目录表,由NumberOfRvaAndSizes个IMAGE_DATA_DIRECTORY结构体组成。该数组包含输入表、输出表、资源、重定位等数据目录项的RVA(相对虚拟地址)和大小。IMAGE_DATA_DIRECTORY结构体的定义如下:
1 typedef struct _IMAGE_DATA_DIRECTORY {
2 DWORD VirtualAddress;
3 DWORD Size;
4 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
成员说明:
Size:目录项的长度。
数据目录中的成员在数据中的索引如下定义所示:
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
在数据目录中,并不是所有的目录项都会有值很多目录项的值都为0。因为很多目录项的值为0,所以说数据目录项是可选的。
IMAGE_SECTION_HEADER
节表的位置在IMAGE_OPTIONAL_HEADER(可选头)后面。节表中的每个IMAGE_SECTION_HEADER中都存放着可执行文件被映射到内存中所在位置的信
息,节的个数由IMAGE_FILE_HEADER中的NumberOfSections给出。这个值我们在前面读取过,它是06 00,也就是0x00000006,所以该文件有6个节表。
IMAGE_SECTION_HEADER的大小为40字节,所以节表总共占用240个字节。
所以节表的起始位置在0x00000200,终止位置在0x00000200 + 0x000000F0(240) -1 = 0x000002EF处。如下图所示
IMAGE_SECTION_HEADER结构体的定义如下
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;
Name(8字节)
节表项的名称,节的名称用ASCII编码来保存。节名称的长度为IMAGE_SIZE_SHORT_NAME(8个字节),这是一个宏
如果字符串长度正好为 8 个字符,则没有终止字符。否则最后一个字符为终止字符。 对于较长的名称,此成员包含 (/) 的正斜杠,后跟十进制数的 ASCII 表示形式,该数字是字符串表中的偏移量。 可执行文件不使用字符串表,并且不支持长度超过 8 个字符的节名称。
我们这里第一个节表项前面8个字节的数据为2E 74 65 78 74 00 00 00,对应的字符为 “.text”
Misc
Misc是一个联合体,它有以下两个成员
Misc.PhysicalAddress
文件地址。
Misc.VirtualSize
加载到内存中的节的总大小(以字节为单位)。 如果此值大于 SizeOfRawData 成员,则节将填充零。
此字段仅对可执行文件有效,对于对象文件,应设置为 0。
VirtualAddress(RVA)
在内存中的虚拟偏移地址,加上ImageBase才是在内存中的真正地址实例中为00001000H
SizeOfRawData
该值为数据实际的节表项大小。 此值必须是 IMAGE_OPTIONAL_HEADER 结构的 FileAlignment 成员的倍数。 如果此值小于 VirtualSize 成员,则部分的其余部分将填充零。 如果节仅包含未初始化的数据,则成员为零。
实例中为00000C00H
PointerToRawData
该节表项在磁盘文件上的偏移地址。此值必须是 IMAGE_OPTIONAL_HEADER 结构的 FileAlignment 成员的倍数。 如果节仅包含未初始化的数据,请将此成员设置为零。
ROffset文件中偏移
.txt节表文件偏移400H,大小C00,400H+C00H-1=FFFH
PointerToRelocations:
A file pointer to the beginning of the relocation entries for the section. If there are no relocations, this value is zero.
这里的翻译总感觉差点意思 ,直接贴MSDN上的英文原版。
PointerToLinenumbers:
A file pointer to the beginning of the line-number entries for the section. If there are no COFF line numbers, this value is zero.
NumberOfRelocations:
该节表项重定向的条数。如果是可执行文件,该值应该设置为0
NumberOfLinenumbers:
The number of line-number entries for the section.
Characteristics:
节表项的属性,定义了以下值:
目录表(IMAGE_DATA_DIRECTORY)
导入表(Import Descriptor)结构解析
导入表是记录PE文件中用到的动态连接库的集合,一个dll库在导入表中占用一个元素信息的位置,这个元素描述了该导入dll的具体信息。如dll的最新修改时间、dll中函数的名字/序号、dll加载后的函数地址等。而一个元素即一个结构体,一个导入表即该结构体的数组,其结构体如下所示:
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;
①联合体值为0时(一般用Characteristics判断是否是0),表示这是导入表结构体数组最后一个元素,除了最后这一个元素,其它每一个结构体都保存了一个dll信息。联合体的值不为0时,用OriginalFirstThunk(RVA)来索引INT的地址。这张INT表存放了该dll的导出函数的信息(序号与函数名)。
②TimeDateStamp:当时间戳值为0时,表示未加载前IAT表与INT表完全相同;当时间戳不为0(为-1)时,表示IAT与INT表不同,IAT存储的是该dll的所有函数的绝对地址,这样在未加载前就直接填充函数地址的方式为函数地址的绑定,其地址是根据绑定导入表来确定的。也就是说当时间戳为-1时绑定导入表才有效,而真正的时间戳存放到绑定导入表中,否则无效。
③ForwarderChain:一般情况下我们也可以忽略该字段。在老版的绑定中,它引用API的第一个forwarder chain(传递器链表)。
④Name:RVA指向dll的名字字符串。
⑤FirstThunk:RVA指向IAT表。
参考链接
- https://www.cnblogs.com/zhaotianff/p/18186676
- https://cloud.tencent.com/developer/article/1968666