Windows编程系列:PE文件结构

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值