PE结构
PE结构
PE文件是Windows操作系统下使用的可执行文件格式。它是微软在UNIX平台的COFF(通用对象文件格式)基础上制作而成。最初设计用来提高程序在不同操作系统上的移植性,但实际上这种文件格式仅用在Windows系列操作系统下。
PE文件是指32位可执行文件,也称为PE32。64位的可执行文件称为PE+或PE32+,是PE(PE32)的一种扩展形式
知识点
文件偏移地址(File Offset Address, FOA)
PE文件中的数据的地址,就是在文件内部的地址,也可以理解成在文件系统中相对于文件头的偏移地址。在PE文件内部,数据是按数据节存放的,但每一个数据节都是0x200字节的倍数,不足的用0x00补齐.
数据在PE文件中的地址叫文件偏移地址,这是文件在磁盘上存放时相对于文件开头的偏移。
虚拟内存地址(Virtual Address, VA)
PE文件被加载开始运行后,进程的可执行代码、数据所在的地址空间称为虚拟地址空间,代码和数据在该空间内的地址称为虚拟地址
PE文件中的指令被装入内存后的地址。
相对虚拟地址 (RVA)
相对虚拟地址是虚拟地址相对于映射基址的偏移量。
内存地址相对于映射基址(即装载地址)的偏移量。在装入内存之后,将按照内存数据标准存放,每一个数据节都是0x1000字节的倍数,不足的用0x00补齐
装载基址(Image base)
PE装入内存时的基地址。默认情况下,EXE文件在内存中的基地址时0x00400000, DLL文件是0x10000000。这些位置可以通过修改编译选项更改。
节偏移
定义节偏移=RVA-文件偏移
内存中数据节相对于装载基址的偏移量和文件中数据节的偏移量的差异称为节偏移。
虚拟内存地址、映射基址、相对虚拟内存地址的关系
VA = Image Base + RVA
-
文件偏移是相对于文件开始处0字节的偏移,相对虚拟地址则是相对于装载基址0x00400000处的偏移。
-
(1)PE文件中的数据按照磁盘数据标准存放,以0x200字节为基本单位进行组织,PE数据节的大小永远是0x200的整数倍。
-
(2)当代码装入内存后,将按照内存数据标准存放,并以0x1000字节为基本单位进行组织,内存中的节总是0x1000的整数倍。
计算内存数据 在文件中偏移
方法一:
- 通过VA转换RVA
403006 - 40000 = 3006 VA - 转载地址
- 找到RVA所在的节
3006落在了.data节中
- 计算.data 节其实RVA和其实FOA的差值
3000 - 800 = 2800
- 通过RVA减去差值
3006 - 2800 =806
方法二:
- 通过VA转换RVA
403006 - 40000 = 3006 VA - 转载地址
- 找到RVA所在的节
3006落在了.data节中
- 计算RVA在节内的偏移
3006 - 3000 =6
- 将节内偏移加上该节的起始FOA
6+ 800 = 806
PE结构讲解
DOS头
DOS头在winnt.h
文件中定义如下:
typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE头部
USHORT e_magic; '// DOS签名“MZ-->Mark Zbikowski(设计了DOS的工程师)” -> 4D 5A '
USHORT e_cblp; // 文件最后页的字节数 -> 00 90 -> 144
USHORT e_cp; // 文件页数 -> 00 30 -> 48
USHORT e_crlc; // 重定义元素个数 -> 00 00
USHORT e_cparhdr; // 头部尺寸,以段落为单位 -> 00 04
USHORT e_minalloc; // 所需的最小附加段 -> 00 00
USHORT e_maxalloc; // 所需的最大附加段 -> FF FF
USHORT e_ss; // 初始的SS值(相对偏移量) -> 00 00
USHORT e_sp; // 初始的SP值 -> 00 B8 -> 184
USHORT e_csum; // 校验和 -> 00 00
USHORT e_ip; // 初始的IP值 -> 00 00
USHORT e_cs; // 初始的CS值(相对偏移量) -> 00 00
USHORT e_lfarlc; // 重分配表文件地址 -> 00 40 -> 64
USHORT e_ovno; // 覆盖号 -> 00 00
USHORT e_res[4]; // 保留字 -> 00 00 00 00 00 00 00 00
USHORT e_oemid; // OEM标识符(相对e_oeminfo) -> 00 00
USHORT e_oeminfo; // OEM信息 -> 00 00
USHORT e_res2[10]; // 保留字 -> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
LONG e_lfanew; '// 指示NT头的偏移(根据不同文件拥有可变值) -> 00 00 00 C0 -> 192' PE头对于文件的偏移地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
当一个PE文件被执行时,PE装载器首先检查DOS header里的PE header的偏移量。如果找到,则直接跳转到PE header的位置。
dos头是PE文件结构的第一个头,用于保存对DOS系统的兼容,并且用于定位真正的PE头。图中的B0 00 00 00
也就是e_lfanew
指向了PE头的偏移。
查看e_lfanew
指向位置是否为50 45 00 00
即可判断是否为一个PE文件。
DOS头中4D 5A
到e_lfanew
即B0 00 00 00
中间数据和e_lfanew
到指向PE头偏移位置可有可无。
文件头 NT
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // 类似于DOS头中的e_magic -> 00 00 45 50 -> PE标识符
IMAGE_FILE_HEADER FileHeader; // IMAGE_FILE_HEADER是PE文件头,定义如下
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
- IMAGE_FILE_HEADER:其中有4个重要的成员,若设置不正确,将会导致文件无法正常运行。
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; '// 每个CPU拥有唯一的Machine码 -> 4C 01 -> PE -> 兼容32位Intel X86芯片'
WORD NumberOfSections; '// 指文件中存在的节段(又称节区)数量,也就是节表中的项数 -> 00 04 -> 4
// 该值一定要大于0,且当定义的节段数与实际不符时,将发生运行错误。'
DWORD TimeDateStamp; // PE文件的创建时间,一般有连接器填写 -> 38 D1 29 1E
DWORD PointerToSymbolTable; // COFF文件符号表在文件中的偏移 -> 00 00 00 00
DWORD NumberOfSymbols; // 符号表的数量 -> 00 00 00 00
WORD SizeOfOptionalHeader; '// 指出IMAGE_OPTIONAL_HEADER32结构体的长度。-> 00 E0 -> 224字节
// PE32+格式文件中使用的是IMAGE_OPTIONAL_HEADER64结构体,
// 这两个结构体尺寸是不相同的,所以需要在SizeOfOptionalHeader中指明大小。'
WORD Characteristics; '// 标识文件的属性,exe则为010f dll文件则为210e
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
可选头
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; '// 魔数 32位为0x10B,64位为0x20B,ROM镜像为0x107'
BYTE MajorLinkerVersion; // 链接器的主版本号 -> 05
BYTE MinorLinkerVersion; // 链接器的次版本号 -> 0C
DWORD SizeOfCode; // 代码节大小,一般放在“.text”节里,必须是FileAlignment的整数倍 -> 40 00 04 00
DWORD SizeOfInitializedData; // 已初始化数大小,一般放在“.data”节里,必须是FileAlignment的整数倍 -> 40 00 0A 00
DWORD SizeOfUninitializedData; // 未初始化数大小,一般放在“.bss”节里,必须是FileAlignment的整数倍 -> 00 00 00 00
DWORD AddressOfEntryPoint; '// 指出程序最先执行的代码起始地址(RVA) -> 00 00 10 00',对于dll文件来说如果没有入口函数这个值为0,对于驱动该值时初始化函数的地址。
DWORD BaseOfCode; // 代码基址,当镜像被加载进内存时代码节的开头RVA。必须是SectionAlignment的整数倍 -> 40 00 10 00
DWORD BaseOfData; // 数据基址,当镜像被加载进内存时数据节的开头RVA。必须是SectionAlignment的整数倍 -> 40 00 20 00
// 在64位文件中此处被并入紧随其后的ImageBase中。
DWORD ImageBase; '// 当加载进内存时,镜像的第1个字节的首选地址。
// WindowEXE默认ImageBase值为00400000,DLL文件的ImageBase值为10000000,也可以指定其他值。
// 执行PE文件时,PE装载器先创建进程,再将文件载入内存,
// 然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint'
'// PE文件的Body部分被划分成若干节段,这些节段储存着不同类别的数据。'
DWORD SectionAlignment; '// SectionAlignment指定了节段在内存中的最小单位, -> 00 00 10 00'
DWORD FileAlignment; '// FileAlignment指定了节段在磁盘文件中的最小单位,-> 00 00 02 00
// SectionAlignment必须大于或者等于FileAlignment'
WORD MajorOperatingSystemVersion;// 主系统的主版本号 -> 00 04
WORD MinorOperatingSystemVersion;// 主系统的次版本号 -> 00 00
WORD MajorImageVersion; // 镜像的主版本号 -> 00 00
WORD MinorImageVersion; // 镜像的次版本号 -> 00 00
WORD MajorSubsystemVersion; // 子系统的主版本号 -> 00 04
WORD MinorSubsystemVersion; // 子系统的次版本号 -> 00 00
DWORD Win32VersionValue; // 保留,必须为0 -> 00 00 00 00
DWORD SizeOfImage; '// 当镜像被加载进内存时的大小,包括所有的文件头。向上舍入为SectionAlignment的倍数。
// 一般文件大小与加载到内存中的大小是不同的。 -> 00 00 50 00'
DWORD SizeOfHeaders; '// 所有头的总大小,向上舍入为FileAlignment的倍数。
// 可以以此值作为PE文件第一节的文件偏移量。-> 00 00 04 00'
DWORD CheckSum; // 镜像文件的校验和 -> 00 00 B4 99
WORD Subsystem; '// 运行此镜像所需的子系统 -> 00 02 -> 窗口应用程序
// 用来区分系统驱动文件(*.sys)与普通可执行文件(*.exe,*.dll),
// 参考:https://blog.csdn.net/qiming_zhang/article/details/7309909#3.2.3'
WORD DllCharacteristics; // DLL标识 -> 00 00
DWORD SizeOfStackReserve; // 最大栈大小。CPU的堆栈。默认是1MB。-> 00 10 00 00
DWORD SizeOfStackCommit; // 初始提交的堆栈大小。默认是4KB -> 00 00 10 00
DWORD SizeOfHeapReserve; // 最大堆大小。编译器分配的。默认是1MB ->00 10 00 00
DWORD SizeOfHeapCommit; // 初始提交的局部堆空间大小。默认是4K ->00 00 10 00
DWORD LoaderFlags; // 保留,必须为0 -> 00 00 00 00
DWORD NumberOfRvaAndSizes; '// 指定DataDirectory的数组个数,由于以前发行的Windows NT的原因,它只能为16。 -> 00 00 00 10'
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; '// 数据目录数组。详见下文。'
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
来看到AddressOfEntryPoint
和ImageBase
,ImageBase
是一个内存的虚拟地址,而AddressOfEntryPoint
是相对虚拟地址。真正可执行文件的虚拟地址是ImageBase
+AddressOfEntryPoint
。但并不是绝对的因为ImageBase
是建议装载地址。如果装载地址已经被使用过了系统会重新分配一块内存。
DataDirectory[] 数据目录数组:数组每项都有被定义的值,不同项对应不同数据结构。重点关注的IMPORT和EXPORT,它们是PE头中的非常重要的部分
# define IMAGE_DIRECTORY_ENTRY_EXPORT 0 导出表
# define IMAGE_DIRECTORY_ENTRY_IMPORT 1 导入表
# define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 资源目录
# define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 异常目录
# define IMAGE_DIRECTORY_ENTRY_SECURITY 4 安全目录
# define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 重定位基本表
# define IMAGE_DIRECTORY_ENTRY_DEBUG 6 调试目录
# define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 描术字串
# define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 机器值
# define IMAGE_DIRECTORY_ENTRY_TLS 9 TLS目录
# define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 载入配值目录
# define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 绑定导入表
# define IMAGE_DIRECTORY_ENTRY_IAT 12 导入地址表
# define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 延迟载入描述
# define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 COM信息
IMAGE_DIRECTORY_ENTRY_EXPORT
导出表的地址和大小,IMAGE_DIRECTORY_ENTRY_IMPORT
存放导入目录表的地址和大小。IMAGE_DIRECTORY_ENTRY_RESOURCE
则是存放资源表的地址和大小。
导出表
一般的简单pe文件并不存在导出表,一般的dll都有导出表,因为写有导出函数可以给到别的pe文件调用功能函数
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;
导入表
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //RVA 指向IMAGE_THUNK_DATA结构数组
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain;
DWORD Name; //RVA,指向dll名字,该名字以 两个字节的0结尾
DWORD FirstThunk; //RVA,指向IMAGE_THUNK_DATA结构数组
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
在内存中
Name:Name是一个RVA,指向的是dll的文件名
OriginalFirstThunk:OriginalFirstThunk也是一个RVA,这个RVA指向的是INT表 Import Name Table
,这个表中保存的hi所有导入的函数RVA。
FirstThunk:RVA,指向的是IAT表 Import Address Table
,这个表存放的是所有导入函数的RVA地址。
在文件中
Name:Name是一个RVA,指向的是dll的文件名
OriginalFirstThunk:OriginalFirstThunk也是一个RVA,这个RVA指向的是INT表 Import Name Table
,这个表中保存的hi所有导入的函数RVA。
FirstThunk:RVA,指向的是IAT表 Import Name Table
,这个表保存的是所有导入函数的名称。
下面再来看到图
文件中
内存中
在文件中OriginalFirstThunk
和FirstThunk
中指向的内容是一样的,但映射到内存后FirstThunk
指向的内容会从 Import Name Table
变为 Import Address Table
。
用个实例来查看一下
获取到他的RVA地址,将其转换成FOA地址
节表
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:指出实际的,被使用的区块大小,是区块在没有对齐处理前的实际大小.如果VirtualSize > SizeOfRawData,那么SizeOfRawData是可执行文件初始化数据的大小(SizeOfRawData – VirtualSize)的字节用0来填充。这个字段在OBJ文件中被设为0。
- VirtualAddress:该块时装载到内存中的RVA,注意这个地址是按内存页对齐的,她总是SectionAlignment的整数倍,在工具中第一个块默认RVA为1000,在OBJ中为0。
- SizeofRawData:该块在磁盘中所占的大小,在可执行文件中,该字段包括经过FileAlignment调整后块的长度。例如FileAlignment的大小为200h,如果VirtualSize中的块长度为19Ah个字节,这一块保存的长度为200h个字节。
- PointerToRawData:该块是在磁盘文件中的偏移,程序编译或汇编后生成原始数据,这个字段用于给出原始数据块在文件的偏移,如果程序自装载PE或COFF文件(而不是由OS装载),这种情况,必须完全使用线性映像方法装入文件,需要在该块处找到块的数据。
- PointerToRelocations 在PE中无意义
- PointerToLinenumbers 行号表在文件中的偏移值,文件调试的信息
- NumberOfRelocations 在PE中无意义
- NumberOfLinenumbers 该块在行号表中的行号数目
- Characteristics 块属性,(如代码/数据/可读/可写)的标志,这个值可通过链接器的/SECTION选项设置.下面是比较重要的标志:
通常,区块中的数据在逻辑上是关联的。PE 文件一般至少都会有两个区块:一个是代码块,另一个是数据块。每一个区块都需要有一个截然不同的名字,这个名字主要是用来表达区块的用途。例如有一个区块叫.rdata,表明他是一个只读区块。注意:区块在映像中是按起始地址(RVA)来排列的,而不是按字母表顺序。
另外,使用区块名字只是人们为了认识和编程的方便,而对操作系统来说这些是无关紧要的。微软给这些区块取了个有特色的名字,但这不是必须的。当编程从PE 文件中读取需要的内容时,如输入表、输出表,不能以区块名字作为参考,正确的方法是按照数据目录表中的字段来进行定位。