PE文件学习
PE 文件的学习:
1.了解
PE( Portable Execute)文件是Windows下可执行文件的总称,常见的有 DLL,EXE,OCX,SYS 等。它是微软在 UNIX 平台的 COFF(通用对象文件格式)基础上制作而成。最初设计用来提高程序在不同操作系统上的移植性,但实际上这种文件格式仅用在 Windows 系列操作系统下。**PE文件是指 32 位可执行文件,也称为PE32。64位的可执行文件称为 PE+ 或 PE32+,是PE(PE32)的一种扩展形式(请注意不是PE64)**。
事实上,一个文件是否是 PE 文件与其扩展名无关,PE文件可以是任何扩展名。那 Windows 是怎么区分可执行文件和非可执行文件的呢?我们调用 LoadLibrary 传递了一个文件名,系统是如何判断这个文件是一个合法的动态库呢?这就涉及到PE文件结构了。
2.结构学习
一、DOS头
DOS头 的作用是兼容 MS-DOS 操作系统中的可执行文件,对于 32位PE文件来说,DOS 所起的作用就是显示一行文字,提示用户:我需要在32位windows上才可以运行。我认为这是个善意的玩笑,因为他并不像显示的那样不能运行,其实已经运行了,只是在 DOS 上没有干用户希望看到的工作而已,好吧,我承认这不是重点。但是,至少我们看一下这个头是如何定义的:
1 | typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header |
二、NT头
顺着 DOS 头中的 e_lfanew,我们很容易可以找到 NT头,这个才是 32位PE文件中最有用的头,定义如下:
1 | typedef struct _IMAGE_NT_HEADERS { |
1.Signature
类似于 DOS头中的 e_magic,其高16位是0,低16是0x4550,用字符表示是 ‘PE‘ 。⭐⭐⭐
2.IMAGE_FILE_HEADER(文件头)
1 | typedef struct _IMAGE_FILE_HEADER { |
Machine:该文件的运行平台,是 x86、x64 还是 I64 等等;
NumberOfSections: 该PE文件中有多少个节,也就是节表中的项数。
TimeDateStamp: PE文件的创建时间,一般有连接器填写。
PointerToSymbolTable: COFF文件符号表在文件中的偏移。
NumberOfSymbols: 符号表的数量。
SizeOfOptionalHeader: 紧随其后的可选头的大小。
Characteristics: 可执行文件的属性
3.IMAGE_OPTIONAL_HEADER(可选头)
别看他名字叫可选头,其实一点都不能少,不过,它在不同的平台下是不一样的,例如32位下是IMAGE_OPTIONAL_HEADER32,而在64位下是IMAGE_OPTIONAL_HEADER64。为了简单起见,我们只看32位。
1 | typedef struct _IMAGE_OPTIONAL_HEADER { |
Magic:表示可选头的类型。
1
2
3MajorLinkerVersion 和 MinorLinkerVersion:链接器的版本号。
SizeOfCode:代码段的长度,如果有多个代码段,则是代码段长度的总和。
SizeOfInitializedData:初始化的数据长度。
SizeOfUninitializedData:未初始化的数据长度。
AddressOfEntryPoint:程序入口的 RVA,对于exe这个地址可以理解为WinMain的RVA。对于DLL,这个地址可以理解为DllMain的RVA,如果是驱动程序,可以理解为DriverEntry的RVA。当然,实际上入口点并非是WinMain,DllMain和DriverEntry,在这些函数之前还有一系列初始化要完成,当然,这些不是本文的重点。⭐⭐⭐
BaseOfCode:代码段起始地址的RVA。
BaseOfData:数据段起始地址的RVA。
ImageBase:映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。⭐⭐⭐
SectionAlignment:节对齐,PE中的节被加载到内存时会按照这个域指定的值来对齐,比如这个值是0x1000,那么每个节的起始地址的低12位都为0。⭐⭐⭐
FileAlignment:节在文件中按此值对齐,SectionAlignment必须大于或等于FileAlignment。
MajorOperatingSystemVersion、MinorOperatingSystemVersion:所需操作系统的版本号,随着操作系统版本越来越多,这个好像不是那么重要了。
MajorImageVersion、MinorImageVersion:映象的版本号,这个是开发者自己指定的,由连接器填写。
MajorSubsystemVersion、MinorSubsystemVersion:所需子系统版本号。
Win32VersionValue:保留,必须为0。
SizeOfImage:映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。
SizeOfHeaders:所有文件头(包括节表)的大小,这个值是以FileAlignment对齐的。
CheckSum:映象文件的校验和。
Subsystem:运行该PE文件所需的子系统。
SizeOfStackReserve:运行时为每个线程栈保留内存的大小。
SizeOfStackCommit:运行时每个线程栈初始占用内存大小。
SizeOfHeapReserve:运行时为进程堆保留内存大小。
SizeOfHeapCommit:运行时进程堆初始占用内存大小。
LoaderFlags:保留,必须为0。
NumberOfRvaAndSizes:数据目录的项数,即下面这个数组的项数。
⭐⭐⭐⭐⭐DataDirectory:数据目录,这是一个数组,数组的项定义如下:
1
2
3
4typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //是一个RVA的偏移地址
DWORD Size; // 对应表的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;这两个数有什么用呢 ?一个是地址,一个是大小,可以看出这个数据目录项定义的是一个区域。那他定义的是什么东西的区域呢?前面说了,DataDirectory 是个数组,数组中的每一项对应一个特定的数据结构,包括导入表,导出表等等,根据不同的索引取出来的是不同的结构,头文件里定义各个项表示哪个结构,如下面的代码所示:
1 |
|
三.IMAGE_SECTION_HEADER(节表)
1 | typedef struct _IMAGE_SECTION_HEADER { |
四.节
每个区块的名称都是唯一的,不能有同名的两个区块。
但事实上节的名称不表示任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data” (一般为.text)或者说将包含数据的区块命名为“.Code”(一般为.rdata等) 都是合法的。
当我们要从PE 文件中读取需要的区块的时候,不能以区块的名称作为定位的标准和依据,正确方法是
在节头(IMAGE_SECTION_HEADER)中。要定位节的地址,你可以按照以下步骤进行:
- 获取文件头:从 PE 文件的起始部分读取 DOS 头(
IMAGE_DOS_HEADER),然后找到 PE 头的位置。 - 读取可选头:从 PE 头中读取可选头(
IMAGE_OPTIONAL_HEADER32),确认文件类型(如 PE32)。 - 读取节表:可选头后面会跟着一个或多个节头。节头的数量在可选头中指定,通常可以通过
NumberOfSections字段获取。 - 计算节的地址:每个节头都有一个
VirtualAddress字段,表示该节在内存中的地址。你可以通过遍历节头来查找特定节的信息。 - 加载地址:当 PE 文件被加载到内存中时,你可以将
VirtualAddress和基址相加,以确定节的实际内存地址。
各种节的描述:
五.其他注意
1.RVA 和文件偏移换算
在 PE 文件格式中,RVA(Relative Virtual Address)和文件偏移之间的换算是非常重要的。以下是相关术语和它们在 PE 文件结构中的定义,以及如何进行换算的方法:
术语及其定义
RVA(Relative Virtual Address):
- 定义:相对虚拟地址,是指某个节或数据在内存中的相对地址,通常是相对于加载基址(
ImageBase)的偏移。 - 结构:在
IMAGE_SECTION_HEADER中的VirtualAddress字段。
- 定义:相对虚拟地址,是指某个节或数据在内存中的相对地址,通常是相对于加载基址(
FOA(File Offset Address):
- 定义:文件偏移地址,指的是文件中某个特定数据或结构的偏移位置。
- 结构:在
IMAGE_SECTION_HEADER中的PointerToRawData字段。
ImageBase:
- 定义:基址,指 PE 文件在内存中加载时的起始地址。
- 结构:在
IMAGE_OPTIONAL_HEADER中的ImageBase字段。
RVA 与 FOA 之间的换算
1. 从 RVA 转换到 FOA
给定一个 RVA,可以使用以下公式转换为 FOA:
1 | FOA = RVA - Section.VirtualAddress + Section.PointerToRawData |
- 解释:
Section.VirtualAddress:对应节头中的VirtualAddress字段。Section.PointerToRawData:对应节头中的PointerToRawData字段。
2. 从 FOA 转换到 RVA
给定一个 FOA,可以使用以下公式转换为 RVA:
1 | RVA = FOA - Section.PointerToRawData + Section.VirtualAddress |
示例
假设有以下节头信息:
- 节名:
.text - VirtualAddress:
0x1000 - PointerToRawData:
0x200
那么,对于某个特定的 RVA 0x1010,可以计算 FOA 如下:
1 | FOA = 0x1010 - 0x1000 + 0x200 = 0x210 |
如果要将 FOA 0x210 转换回 RVA:
1 | RVA = 0x210 - 0x200 + 0x1000 = 0x1010 |
总结
- RVA 是节在内存中的相对虚拟地址。
- FOA 是节在文件中的具体偏移。
- 使用
IMAGE_SECTION_HEADER中的VirtualAddress和PointerToRawData字段进行 RVA 和 FOA 之间的换算。
这些计算在 PE 文件的解析、调试和分析中是非常重要的,理解这些术语及其对应的结构定义将有助于处理 PE 文件格式。
同时计算时也要考虑对齐会对RVA 与 FOA 之间的换算影响
节对齐和文件对齐是 PE 文件格式中两个重要的概念,它们的主要区别如下:
节对齐(Section Alignment)
定义:
- 节对齐是指在 PE 文件加载到内存中时,节在内存中的对齐要求。它决定了每个节的起始地址必须是某个特定值的倍数。
位置:
- 节对齐的值在
IMAGE_OPTIONAL_HEADER结构中,字段名称为SectionAlignment。
- 节对齐的值在
作用:
- 确保加载到内存中的节按页对齐,以优化内存访问和提高性能。通常,
SectionAlignment的值为 4096 字节(一个内存页的大小)。
- 确保加载到内存中的节按页对齐,以优化内存访问和提高性能。通常,
加载时的对齐:
- 节对齐是在 PE 文件加载到内存后生效的。加载过程会根据
SectionAlignment进行调整。
- 节对齐是在 PE 文件加载到内存后生效的。加载过程会根据
文件对齐(File Alignment)
定义:
- 文件对齐是指在 PE 文件中,节在文件中的对齐要求。它决定了每个节在文件中开始的位置必须是某个特定值的倍数。
位置:
- 文件对齐的值同样在
IMAGE_OPTIONAL_HEADER结构中,字段名称为FileAlignment。
- 文件对齐的值同样在
作用:
- 确保在 PE 文件中,节的起始位置按照
FileAlignment对齐,以便于文件的读取和解析。通常,FileAlignment的值为 512 字节或 4096 字节。
- 确保在 PE 文件中,节的起始位置按照
未加载时的对齐:
- 文件对齐是在 PE 文件未加载时适用的。它影响文件的存储格式,而不是内存中的布局。
总结
- 节对齐:适用于 PE 文件加载到内存后的内存布局,确保节按页对齐。
- 文件对齐:适用于 PE 文件的存储格式,确保节在文件中按字节对齐。
因此,节对齐和文件对齐是针对不同上下文的对齐要求,它们的值和作用有所不同。
2.PE文件与内存映射
就是把 PE 文件 从 硬盘中 放到 *内存中***,然后 CPU 从 内存中读取指令并执行。
文件中使用偏移(offset),内存中使用 VA(Virtual Address,虚拟地址)来表示位置。
VA 指进程虚拟内存的绝对地址,RVA(Relative Virtual Address,相对虚拟地址)是指从某基准位置(ImageBase)开始的相对地址。VA 与 RVA 满足下面的换算关系: RVA + ImageBase = VA
PE 头内部信息大多是 RVA 形式存在。
原因在于(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他的 PE文件(DLL)。
此时必须通过重定向(Relocation)将其加载到其他空白的位置,若 PE头信息使用的是 VA,则无法正常访问。
因此使用 RVA 来重定向信息,即使发生了重定向,只要相对于基准位置的相对位置没有变化,就能正常访问到指定信息,不会出现任何问题。
**当 PE 文件被执行时,PE 装载器会为 *进程* 分配 4GB 的 *虚拟地址空间**( Virtual address spaces 官方文档:**https://docs.microsoft.com/zh-tw/windows-hardware/drivers/gettingstarted/virtual-address-spaces\* *)*,然后把程序所占用的磁盘空间作为虚拟内存映射到这个4GB的虚拟地址空间中。一般情况下,会映射到 *虚拟地址空间* 中的 0X400000的位置。
Last
写的PE文件分析器🌃🌃🌃
1 |
|
1 | 使用命令:./pe.exe target.exe |
