点击蓝字
关注我们
始于理论,源于实践,终于实战
老付话安全,每天一点点
激情永无限,进步看得见
严正声明
本号所写文章方法和工具只用于学习和交流,严禁使用文章所述内容中的方法未经许可的情况下对生产系统进行方法验证实施,发生一切问题由相关个人承担法律责任,其与本号无关。
特此声明!!!
PE 文件格式是微软操作系统中可执行的二进制文件格式。凡是可以直接以二进制形式被系统加栽执行的文件都是PE文件,是一种文件格式。常见的有 DLL,EXE,OCX,SYS 等。一个文件是否是PE文件与其扩展名无关,PE文件可以是任何扩展名。
PE文件的结构:PE文件由DOS文件头、DOS加载模块(DOS stub)、 PE文件头、区段表与区段构成的。示意图如下:
(一)DOS头
分为“MZ头部”和"DOS存根(DOS stub)"。MZ头部是真正的DOS头部,由于其开始处的两个字节为"MZ",因此DOS头也可以叫作MZ头部。该部分用于程序在DOS系统下加载,它的结构被定义为IMAGE_DOS_HEADER。使用十六进制查看器,观察DOS头部:
为什么会有两个与DOS相关的结构,因为它是为了兼容性,PE加载器会根据DOS文件头最后面标志跳过DOS加载模块而直接转到PE文件头上。
//大小为: 0x40(64)字节
typedefstruct _IMAGE_DOS_HEADER {// DOS .EXE header
WORD e_magic; // MZ标记 0x5a4d
WORD e_cblp; // 最后(部分)页中的字节数
WORD e_cp; // 文件中的全部和部分页数
WORD e_crlc; // 重定位表中的指针数
WORD e_cparhdr; // 头部尺寸以段落为单位
WORD e_minalloc; // 所需的最小附加段
WORD e_maxalloc; // 所需的最大附加段
WORD e_ss; // 初始的SS值(相对偏移量)
WORD e_sp; // 初始的SP值
WORD e_csum; // 补码校验值
WORD e_ip; // 初始的IP值
WORD e_cs; // 初始的SS值
WORD e_lfarlc; // 重定位表的字节偏移量
WORD e_ovno; // 覆盖号
WORD e_res[4]; // 保留字
WORD e_oemid; // OEM标识符(相对m_oeminfo)
WORD e_oeminfo; // OEM信息
WORD e_res2[10]; // 保留字
LONG e_lfanew; // NT头(PE标记)相对于文件的偏移地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
该结构体中需要掌握的字段 只有两个,分别是第一个字段 e_magic和最后一个字段e_lfanew。
e_magic:DOS可执行文件的标识,占用2字节,该位置保存着字符是“MZ",该标识符在Winnt.h头文件中有一个宏定义,在0x00000000的位置保存着2字节的内容0x5A4D(见上图)。这里使用的是小尾(小端)方式存储,即高位保存高字节,低位保存低字节,所以上图中写的是4D 5A,这也是适合阅读顺序。
注:
大端模式(Big-Endian):在内存中,多字节数据类型的高位字节存储在低地址处,而低位字节存储在高地址处。这种模式与我们阅读数字的方式相似,即先读高位,后读低位。
小端模式(Little-Endian):在内存中,多字节数据的低位字节存储在低地址处,而高位字节存储在高地址处。这种模式与我们阅读数字的方式相反,即先读低位,后读高位。
如0x0102这样一个数据,
使用大端方式存储,存储方式为:01 02
使用小端方式存储,存储方式为:02 01
(二)PE 头
PE头部保存着Windows系统加载可执行文件的重要信息(用来装载Win32程序)。它的结构被定义为IMAGE_NT_HEADERS。
IMAGE_NT_HEADERS由三部分组成:
-
IMAGE_NT_SIGNATURE
-
IMAGE_FILE_HEADER
-
IMAGE_OPTIONAL_HEADER
-
IMAGE_NT_Signature
Signature是PE标识符,标识该文件是否是PE文件。占用4个字节,即 50 45
Signature在winnt.h中有一个宏定义IMAGE_NT_SIGNATURE,
-
文件头IMAGE_FILE_HEADER
IMAGE_FILE_HEADER是IMAGE_NT_HEADERS结构体中的一个结构,紧接在PE标识符(Signature字段)的后面。
IMAGE_FILE_HEADER占用20个字节。
Machine:WORD 2字节,该字段表示可执行文件的目标CPU类型,在16位查看器中可以看到:
x64 程序:64 86
x86 程序:4C 01(即 0x014C)
NumberOfSections:WORD 2字节,该字段表示PE文件的节区的个数.请注意,Windows 加载程序将节区限制为 96。
该字段的值为08 00,即为0x00000008,表示该PE文件的节区有8个。
-
可选头IMAGE_OPTIONAL_HEADER
可选头IMAGE_OPTIONAL_HEADER是IMAGE_NT_HEADERS结构体中的一个结构,紧接在IMAGE_FILE_HEADER类型之后。选头主要用来管理PE文件被操作系统装载时所需要的信息。
DataDirectory:数据目录表
由NumberOfRvaAndSizes个IMAGE_DATA_DIRECTORY结构体组成。该数组包含输入表、输出表、资源、重定位等数据目录项的RVA(相对虚拟地址)和大小。
(三)区段表
PE文件头的数椐目录表后是区段表,区段表用来描述位于其后各个区段的各种厲性。 PE文件最少要有一个区段才能被加载运行。区段表是由数个首尾相连的IMAGE_SECTION_HEADER结构体数组构成的。
IMAGE_SECTION_HEADER详解
节表中的每个IMAGE_SECTION_HEADER中都存放着可执行文件被映射到内存中所在位置的信
息,节的个数由IMAGE_FILE_HEADER中的NumberOfSections给出。
IMAGE_SECTION_HEADER的大小为40字节,所以节表总共占用240个字节。
(四)PE文件的内存映射
理解pe文件与虚拟内存之间的映射关系的重要概念:
-
文件偏移地址(File Offset) : PE文件数据在硬盘中存放的地址就称为文件偏移地址,例如我们使用WinHex查看文件时的Offset就是文件偏移地址。所谓的文件偏移地址就是指文件在磁盘上存放时相对于文件开头的偏移。
-
装载基址(Imugt; Base):装载基址是指PE文件装入内存时的基地址 ,如果非要将其与文件偏移地址关联,我们就可以理解为“文件偏移地址的装载基址为0x00000000' 当然这是错误的,但是这样更有利于理解; 一般情况EXE文件的装载基址都为0x00400000,而DLL文件一般都是0x10000000。但这并不是绝对的,都是可以更改的,特別是DLL文件更是如此。
-
虚拟内存地址(Virtual Address, VA) 虚拟内存地址就是指PE文件被装入内存之后的地址
-
相对虚拟地址(ReiMive Virtual Address,RVA):相对虚拟地址指的是在没有计算装载基址的情况下的内存地址。
虚拟内存映射关系如下:
物理内存512MB,而进程虚拟化内存4GB是为什么?
虚拟内存只是进程的一笔虚拟财富,或者说是虚拟空间,只有当需要进行实际的内存操作时,虚拟内存管理才会将“虚拟地址”与“物理地址”联系起来。
虚拟内存地址、装载基址与相对虚拟地址之间的关系如下:
虚拟内存地址(VA)=装载基址(Image Base) +相对虚拟地址(RVA)
ImageBase:PE文件加载到内存时的基地址(默认EXE为0x400000,DLL为0x10000000)。
RVA(Relative Virtual Address):相对于ImageBase的偏移(RVA = VA - ImageBase)。
File Offset:数据在磁盘文件中的物理偏移。
Section Alignment & File Alignment:
内存中节区(Section)按Section Alignment对齐(通常0x1000)。
文件中节区按File Alignment对齐(通常0x200)。
PE文件映射到虚拟内存:
来源网络
首先,PE文件的0字节(第一个字节)所对应的虚拟内存地址为0x00400000,这个就是装载基址(Image Base)了。文件偏移地址(File Offset)是相对于文件开始处0字节的偏移,而相对虚拟地址(RVA)则是相对于装载基址0x00400000处的偏移。
首先,PE文件的数据一般是以0x200字节为基本单位进行存放的。当一个数据区段(Section)不足0x200字节时,余下的空间则用“00”填充。当一个数据区段超过0x200字节时,下一个数据区段将0x200数据块分配给这个空间使用,余下的空间仍然是用“00”填充。因此PE文件的区段大小永远是0x200的整数倍。
其次,当PE文件装入内存时,PE文件的数据存放一般将以0x1000字节为基本单位进行组织存放。其分配标准与磁盘数据组织方式一样,不足0x1000字节的则用“00”填充,超过0x1000的就新分配一个0x1000的数据块。因此内存中的区段大小永远是0x1000的整数倍。
为什么要将虚拟内存地址转换为文件偏移?
内存映射文件
当程序访问某个虚拟内存地址时,系统需要知道该地址对应的数据在文件中的具体位置(文件偏移),以便:
读取:从文件的正确位置加载数据到物理内存。
写入:将修改后的数据同步回文件的正确位置。
动态链接库(DLL/SO)加载
程序运行时,动态库的代码和数据会被映射到虚拟内存中,但这些内容最初存储在磁盘上的库文件(如.dll或.so)中。当程序调用动态库函数或访问全局变量时,系统需将虚拟地址转换为文件偏移,以从磁盘加载对应的代码或数据(按需分页)。
调试与核心转储(Core Dump)
调试器(如GDB)需要将崩溃时的虚拟内存地址映射回源代码或二进制文件的位置(文件偏移+符号),方便定位问题。系统将进程的内存状态保存到文件时,需记录虚拟地址与文件偏移的对应关系,便于后续分析。
逆向工程与二进制分析
分析二进制文件(如恶意软件)时,工具(如IDA Pro)需要将虚拟地址转换为文件偏移,以查看磁盘上二进制文件的原始布局(如节区/Section的起始位置)。
内存分页机制
当进程访问的虚拟内存页面未加载到物理内存时,系统触发缺页异常(Page Fault),此时需根据虚拟地址找到对应的文件偏移(如映射的文件或交换空间),再从磁盘读取数据。
计算方式:
相对虚拟地址(RVA)=虚拟内存地址(VA)-裝载基址(Image Base)
转换步骤
1. 从 VA 转换为 File Offset,
从 RVA(相对虚拟地址)计算文件偏移(File Offset)
步骤:
-
计算RVA:RVA = VA - ImageBase。
-
定位RVA所属的节区(Section)。
-
使用节区的VirtualAddress和PointerToRawData计算文件偏移:
文件偏移 = RVA - 节区虚拟地址(VirtualAddress) + 节区文件偏移(PointerToRawData)
示例:
VirtualAddress = 0x1000
PointerToRawData = 0x400
VA = 0x401520,ImageBase = 0x400000 → RVA = 0x1520。
查节区表发现.text节的:
计算:
File Offset=0x1520−0x1000+0x400=0x920File Offset=0x1520−0x1000+0x400=0x920
2. 从 File Offset 转换为 VA
步骤:
定位文件偏移所属的节区。
使用节区的PointerToRawData和VirtualAddress计算RVA:
RVA = 文件偏移 - 节区文件偏移(PointerToRawData) + 节区虚拟地址(VirtualAddress)
计算VA:VA = RVA + ImageBase。
示例:
PointerToRawData = 0x400
VirtualAddress = 0x1000
File Offset = 0x920,.text节的:
计算:
RVA=0x920−0x400+0x1000=0x1520VA=0x1520+0x400000=0x401520RVA=0x920−0x400+0x1000=0x1520VA=0x1520+0x400000=0x401520
枯燥乏味的知识点,学起来有点费劲。但是不积跬步无以至千里。日拱一卒,每天一点点的进步。
END
为了更好的交流学习,我特此创建了一个“网安全民兵连先锋队”微信群,供大家一块交流学习,互通有无:
原文始发于微信公众号(老付话安全):Windows PE文件格式详解(一)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论