PE
常见后缀
- .exe,可执行程序,常见pe文件类型,包含程序入口点和完整执行逻辑
- .dll,动态链接库,提供可被多个程序共享的函数和资源,无法直接运行
- .sys,系统驱动程序,运行于内核态,用于硬件交互或系统级功能
- .ocx,activex控件,当特殊dll
- .scr,屏幕保护程序,改后缀exe
- .cpl,控制面板小程序,系统配置界面,当dll
- .efi,可扩展固件接口文件,用于UEFI固件启动,PE扩展
PE文件的两种状态
pe文件分为运行态和非运行态
- 非运行态:当一个pe文件尚未被运行时,数据存储在磁盘中
- 运行态:当一个pe文件被打开后,pe文件相关数据将被装在到内存中
文件结构
基于coff(Common Object File Format)扩展,文件头加节区
PE 文件结构概览表(32位 PE32)
| 结构名称 | 对应 C 数据结构 | 默认占用空间 (字节) | 备注 |
|---|---|---|---|
| DOS MZ 头 | _IMAGE_DOS_HEADER | 64 | 固定大小 (0x40) |
| DOS Stub | (无固定结构) | 不固定 | 仅用于 DOS 提示,如果不运行在 DOS 下可忽略 |
| PE 文件头 (总) | _IMAGE_NT_HEADERS | 248 | 包含以下三部分 (4 + 20 + 224) |
| ├── PE 签名 | Signature (DWORD) | 4 | 值为 PE\0\0 (0x00004550) |
| ├── 标准 PE 头 | _IMAGE_FILE_HEADER | 20 | 包含机器类型、节数量等基础信息 |
| └── 扩展 PE 头 | _IMAGE_OPTIONAL_HEADER | 224 | 注: 64位程序此处为 240 字节 |
| 节表 (Section Table) | _IMAGE_SECTION_HEADER | 40 | 每个节表项的大小。总大小 = 40 × 节数量 |
| 节数据 (Sections) | (无特定结构体) | 不固定 | 实际的代码、数据、资源等,大小由节表决定 |
DOS头部
可以往里面塞东西,也不会影响后面的pe结构,DOS Header固定64字节,兼容dos系统执行,一个mz开头,4D 5A 90 00,还有个DOS Stub,大小不固定,通常会填满到128字节
关键字段:
- e_magic:dos签名,固定为0x5A4D(字符串MZ)
- e_lfanew:pe文件头偏移量(32位或64位),dos头过渡到pe头关键指针
定义的结构体位为,定义于winnt.h
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE 头部结构体
// --- 核心标识 ---
WORD e_magic; // [0x00] 魔数 (Magic number),固定为 "MZ"
// --- DOS 程序加载信息 (现代 Windows 通常忽略) ---
WORD e_cblp; // [0x02] 文件最后一页的字节数
WORD e_cp; // [0x04] 文件总页数
WORD e_crlc; // [0x06] 重定位项的数量
WORD e_cparhdr; // [0x08] 头部大小 (以段/Paragraph为单位)
WORD e_minalloc; // [0x0A] 所需的最小附加段数
WORD e_maxalloc; // [0x0C] 所需的最大附加段数
WORD e_ss; // [0x0E] 初始 SS (堆栈段) 相对值
WORD e_sp; // [0x10] 初始 SP (堆栈指针) 值
WORD e_csum; // [0x12] 校验和
WORD e_ip; // [0x14] 初始 IP (指令指针) 值
WORD e_cs; // [0x16] 初始 CS (代码段) 相对值
WORD e_lfarlc; // [0x18] 重定位表在文件中的地址
WORD e_ovno; // [0x1A] 覆盖号 (Overlay number)
// --- 保留及 OEM 字段 ---
WORD e_res[4]; // [0x1C] 保留字段 (4个字)
WORD e_oemid; // [0x24] OEM 标识符 (用于 e_oeminfo)
WORD e_oeminfo; // [0x26] OEM 信息 (具体由 e_oemid 定义)
WORD e_res2[10]; // [0x28] 保留字段 (10个字)
// --- PE 头部指针 (核心字段) ---
LONG e_lfanew; // [0x3C] 新 EXE 头部 (PE Header) 的文件地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
结构体定义
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // 0x00: 魔数 "MZ" (4D 5A)
WORD e_cblp; // 0x02
// ......
LONG e_lfanew; // 0x3C: 指向 PE 头的偏移量 (这是结构体的最后一个成员)
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
还有自写代码读取dos mz头吗,吓哭了
// PE.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <malloc.h>
#include <windows.h>
int main(int argc, char* argv[])
{
//创建DOS对应的结构体指针
_IMAGE_DOS_HEADER* dos;
//读取文件,返回文件句柄
HANDLE hFile = CreateFileA("C:\\Documents and Settings\\Administrator\\桌面\\dbghelp.dll",GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,0,0);
//根据文件句柄创建映射
HANDLE hMap = CreateFileMappingA(hFile,NULL,PAGE_READONLY,0,0,0);
//映射内容
LPVOID pFile = MapViewOfFile(hMap,FILE_MAP_READ,0,0,0);
//类型转换,用结构体的方式来读取
dos=(_IMAGE_DOS_HEADER*)pFile;
//输出结构体的第一个成员,以十六进制输出
printf("%X\n",dos->e_magic);
return 0;
}
PE头
前四个字节 50 45 00 00pe标识,20个字节标准pe头,扩展pe头看结构体,有哪些,然后之后文件对齐之后的大小,头的大小一定是文件倍数
32位结构体
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE文件头标识
IMAGE_FILE_HEADER FileHeader; //标准PE头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //扩展PE头 32位
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
64位结构体
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature; //PE文件头标识
IMAGE_FILE_HEADER FileHeader; //标准PE头
IMAGE_OPTIONAL_HEADER64 OptionalHeader; //扩展PE头 64位
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
PE签名
类型为dword,存储在signature变量中
标准PE头(IMAGE_FILE_HEADER)(又称coff文件头)
结构体
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;//可以运行在什么样的CPU上,如果它的值为0x0则表示可以运行在任意的CPU上,支持在Intel 386以及后续的型号CPU运行则值为0x14c,支持64位的CPU型号则值为0x8664。数据宽度word2字节,程序支持的cpu
WORD NumberOfSections;//表示节的数量,也就是节表中有几个结构体,word2字节,不大于96
DWORD TimeDateStamp;//编译器填写的时间戳 与文件属性里面(创建时间、修改时间)无关,dword4字节
DWORD PointerToSymbolTable;//调试相关,指向符号表,dword4字节
DWORD NumberOfSymbols;//调试相关,符号表中的符号个数,dword4字节
WORD SizeOfOptionalHeader;//可选PE头的大小(32位PE文件:0xE0 64位PE文件:0xF0)word2字节
WORD Characteristics;//文件属性,数据位拼接而成,word2字节
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine
IMAGE_FILE_MACHINE 常量对照表
| 返回的常量 (Constant) | Value (Hex) | 说明 (Description) |
|---|---|---|
IMAGE_FILE_MACHINE_UNKNOWN | 0x0 | 假定此字段的内容适用于任何计算机类型 |
IMAGE_FILE_MACHINE_AM33 | 0x1d3 | 松田 (Matsushita) AM33 |
IMAGE_FILE_MACHINE_AMD64 | 0x8664 | x64 (AMD64 或 Intel 64) |
IMAGE_FILE_MACHINE_ARM | 0x1c0 | ARM 小端序 (Little Endian) |
IMAGE_FILE_MACHINE_ARM64 | 0xaa64 | ARM64 小端序 (Little Endian) |
IMAGE_FILE_MACHINE_ARMNT | 0x1c4 | ARM Thumb-2 小端序 (Little Endian) |
IMAGE_FILE_MACHINE_EBC | 0xebc | EFI 字节代码 (EFI Byte Code) |
IMAGE_FILE_MACHINE_I386 | 0x14c | Intel 386 或更高版本的处理器 (x86 32位) |
IMAGE_FILE_MACHINE_IA64 | 0x200 | Intel Itanium 处理器系列 |
IMAGE_FILE_MACHINE_LOONGARCH32 | 0x6232 | LoongArch 32 位处理器系列 (龙芯) |
IMAGE_FILE_MACHINE_LOONGARCH64 | 0x6264 | LoongArch 64 位处理器系列 (龙芯) |
IMAGE_FILE_MACHINE_M32R | 0x9041 | 三菱 M32R 小端序 |
IMAGE_FILE_MACHINE_MIPS16 | 0x266 | MIPS16 |
IMAGE_FILE_MACHINE_MIPSFPU | 0x366 | 使用 FPU 的 MIPS |
IMAGE_FILE_MACHINE_MIPSFPU16 | 0x466 | 具有 FPU 的 MIPS16 |
IMAGE_FILE_MACHINE_POWERPC | 0x1f0 | PowerPC 小端序 |
IMAGE_FILE_MACHINE_POWERPCFP | 0x1f1 | 支持浮点运算的 PowerPC |
IMAGE_FILE_MACHINE_R4000 | 0x166 | MIPS 小端序 (通常指 R4000) |
IMAGE_FILE_MACHINE_RISCV32 | 0x5032 | RISC-V 32 位地址空间 |
IMAGE_FILE_MACHINE_RISCV64 | 0x5064 | RISC-V 64 位地址空间 |
IMAGE_FILE_MACHINE_RISCV128 | 0x5128 | RISC-V 128 位地址空间 |
IMAGE_FILE_MACHINE_SH3 | 0x1a2 | Hitachi SH3 |
IMAGE_FILE_MACHINE_SH3DSP | 0x1a3 | Hitachi SH3 DSP |
IMAGE_FILE_MACHINE_SH4 | 0x1a6 | Hitachi SH4 |
IMAGE_FILE_MACHINE_SH5 | 0x1a8 | Hitachi SH5 |
IMAGE_FILE_MACHINE_THUMB | 0x1c2 | Thumb |
IMAGE_FILE_MACHINE_WCEMIPSV2 | 0x169 | MIPS 小端 WCE v2 |
| 常见: |
- 0x014C (I386):代表 32位程序。
- 0x8664 (AMD64):代表 64位 程序。
NumberOfSections
machine后面的两个字节,说明区段数量
区段:操作系统加载程序时,根据内存属性(权限)来管理,常见区段
- .text或.code,指令代码,可读,可执行
- .data,已经初始化的全局变量,可读,可写
- .rdata或.idata,存放常量(const或者字符串),导入表(调用了哪些api),只读
- .rsrc,存放的程序的ui资源,只读
- .reloc,重定位,如果程序不能在预期的内存地址(基址)加载,要修正时用的数据,比如开启了ASLR(随机基址的)的程序要用
SizeOfOptionalHeader
表示可选标头大小,可执行文件必需
转换为十进制就是大小,SizeOfOptionalHeader变量中的数据对于32位PE文件通常为00E0h,对于64位PE文件通常为00F0h。这里的h是用来表示这个数据为16进制。上图我们可以看到SizeOfOptionalHeader变量当中的数据为00 E0,扩展PE头大小为224,文件为32位PE文件。
Characteristics
看最后两个字节,转换为二进制,然后对应的1的含义在下表
IMAGE_FILE_HEADER – Characteristics 字段详解
| 数据位 (Bit) | 常量符号 (Constant) | 为 1 时的含义 |
|---|---|---|
| 0 | IMAGE_FILE_RELOCS_STRIPPED | 文件中不存在重定位信息 |
| 1 | IMAGE_FILE_EXECUTABLE_IMAGE | 文件是可执行的 |
| 2 | IMAGE_FILE_LINE_NUMS_STRIPPED | 不存在行信息 |
| 3 | IMAGE_FILE_LOCAL_SYMS_STRIPPED | 不存在符号信息 |
| 4 | IMAGE_FILE_AGGRESSIVE_WS_TRIM | 调整工作集 |
| 5 | IMAGE_FILE_LARGE_ADDRESS_AWARE | 应用程序可处理大于 2GB 的地址 |
| 6 | (保留) | 此标志保留 |
| 7 | IMAGE_FILE_BYTES_REVERSED_LO | 小尾方式 (Little Endian) |
| 8 | IMAGE_FILE_32BIT_MACHINE | 只在 32 位平台上运行 |
| 9 | IMAGE_FILE_DEBUG_STRIPPED | 不包含调试信息 |
| 10 | IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP | 不能从可移动盘运行 (需复制到交换文件) |
| 11 | IMAGE_FILE_NET_RUN_FROM_SWAP | 不能从网络运行 (需复制到交换文件) |
| 12 | IMAGE_FILE_SYSTEM | 系统文件(如驱动程序),不能直接运行 |
| 13 | IMAGE_FILE_DLL | 这是一个 DLL 文件 |
| 14 | IMAGE_FILE_UP_SYSTEM_ONLY | 文件不能在多处理器计算机上运行 |
| 15 | IMAGE_FILE_BYTES_REVERSED_HI | 大尾方式 (Big Endian) |
扩展PE头
标准PE头中的SizeOfOptionalHeader用于标识扩展PE头的大小,大小不固定,每个pe文件都有一个扩展pe头,用于向加载程序提供信息
有标头magic编号保证格式兼容性,可选的标头magic用于确定pe文件是pe32还是re32+可执行文件
可选标头,每个映像文件都有一个用于向加载程序提供信息的可选标头。 此标头是可选标头,因为某些文件(特别是对象文件)没有此标头。 对于映像文件,此标头是必需的。 对象文件可以具有可选标头,但通常此标头在对象文件中没有函数,只是为了增加其大小。可选标头的大小不是固定的。 COFF 标头中的 SizeOfOptionalHeader 字段必须用于验证对特定数据目录的文件的探测是否未超出 SizeOfOptionalHeader。
还应该使用可选标头的 NumberOfRvaAndSizes 字段来确保对特定数据目录条目的探测不会超出可选标头。
可选标头幻数确定映像是 PE32 还是 PE32+ 可执行文件。
映射:
- 坐标系变化,偏移变成地址(FOA变成VA),foa,相对于文件开头的距离,虚拟地址 (VA) = 基址 (Image Base) + RVA,因为cpu执行指令需要内存中的绝对地址
- 赋予权限,映射text段或者data段这些,让他们变得不可写之类的
- 动态修正,重定位,找到调用东西的真实地址
1. 32位与64位结构体对比
64位相比于32位区别并不大,主要是删去了一个成员(BaseOfData),以及五个成员的数据类型由 DWORD 变为 ULONGLONG。
| 成员 | 32位类型 | 64位类型 | 说明 |
|---|---|---|---|
| BaseOfData | DWORD | 无此成员 | 数据段基址(64位移除) |
| ImageBase | DWORD | ULONGLONG | 内存镜像基址 |
| SizeOfStackReserve | DWORD | ULONGLONG | 栈保留大小 |
| SizeOfStackCommit | DWORD | ULONGLONG | 栈提交大小 |
| SizeOfHeapReserve | DWORD | ULONGLONG | 堆保留大小 |
| SizeOfHeapCommit | DWORD | ULONGLONG | 堆提交大小 |
注:因区别不明显,以下分析以 32位结构体 为例。
32位扩展 PE 头成员概览
扩展 PE 头成员较多,加粗是重点
| 成员 | 数据宽度 | 说明 |
|---|---|---|
| Magic | WORD (2字节) | 镜像文件的状态,可用于判断程序是32位还是64位 |
| MajorLinkerVersion | BYTE (1字节) | 链接器的主要版本号 |
| MinorLinkerVersion | BYTE (1字节) | 链接器的次要版本号 |
| SizeOfCode | DWORD (4字节) | 代码段的大小 |
| SizeOfInitializedData | DWORD (4字节) | 初始化数据段的大小 |
| SizeOfUninitializedData | DWORD (4字节) | 未初始化数据段的大小 |
| AddressOfEntryPoint | DWORD (4字节) | 程序入口 (OEP) |
| BaseOfCode | DWORD (4字节) | 代码开始的基址 |
| BaseOfData | DWORD (4字节) | 数据开始的基址 |
| ImageBase | DWORD (4字节) | 内存镜像基址 |
| SectionAlignment | DWORD (4字节) | 内存对齐 |
| FileAlignment | WORD (2字节) | 文件对齐 |
| MajorOperatingSystemVersion | WORD (2字节) | 标识操作系统版本号 (主) |
| MinorOperatingSystemVersion | WORD (2字节) | 标识操作系统版本号 (次) |
| MajorImageVersion | WORD (2字节) | PE文件自身的版本号 (主) |
| MinorImageVersion | WORD (2字节) | PE文件自身的版本号 (次) |
| MajorSubsystemVersion | WORD (2字节) | 运行所需子系统版本号 (主) |
| MinorSubsystemVersion | WORD (2字节) | 运行所需子系统版本号 (次) |
| Win32VersionValue | DWORD (4字节) | 子系统版本的值,必须为0 |
| SizeOfImage | DWORD (4字节) | Image大小 (内存中) |
| SizeOfHeaders | DWORD (4字节) | 所有头+节表按照文件对齐后的大小 |
| CheckSum | DWORD (4字节) | 校验和 |
| Subsystem | WORD (2字节) | 子系统类型 |
| DllCharacteristics | WORD (2字节) | 文件特性 (不只是针对DLL) |
| SizeOfStackReserve | DWORD (4字节) | 初始化时保留的栈大小 |
| SizeOfStackCommit | DWORD (4字节) | 初始化时实际提交的栈大小 |
| SizeOfHeapReserve | DWORD (4字节) | 初始化时保留的堆大小 |
| SizeOfHeapCommit | DWORD (4字节) | 初始化时实际提交的堆大小 |
| LoaderFlags | DWORD (4字节) | 调试相关 (已过时) |
| NumberOfRvaAndSizes | DWORD (4字节) | 目录项数目 |
| DataDirectory[16] | 结构体数组 | 数据目录表,指向导出表、导入表等 |
Magic
镜像文件的状态,用于区分 PE 文件的类型。
| 宏定义 | 值 | 含义 |
|---|---|---|
| IMAGE_NT_OPTIONAL_HDR32_MAGIC | 0x10b | 32位可执行映像 |
| IMAGE_NT_OPTIONAL_HDR64_MAGIC | 0x20b | 64位可执行映像 |
| IMAGE_ROM_OPTIONAL_HDR_MAGIC | 0x107 | ROM 镜像 |
版本号与段大小
- MajorLinkerVersion / MinorLinkerVersion: 链接器版本号。
- SizeOfCode / SizeOfInitializedData / SizeOfUninitializedData: 分别为代码段、初始化数据段、未初始化数据段的大小。
这些值是文件对齐后的大小,由编译器填写,不一定准确
AddressOfEntryPoint (OEP)
- 指向入口点函数的指针,是相对于 ImageBase 的偏移(RVA)。
- 对于可执行文件:程序的起始执行地址。
- 对于驱动程序:初始化函数的地址。
- 对于 DLL:可选,若无入口点则为 0。
BaseOfCode / BaseOfData
- 指向代码段/数据段开头的指针(RVA)。
注: 编译器填写,不一定准确。
ImageBase
Image (PE文件) 载入内存时第一个字节的首选地址(基址)。该值通常是 64K 的倍数。
| 文件类型 | 默认值 |
|---|---|
| DLL | 0x10000000 |
| 应用程序 (EXE) | 0x00400000 |
| Windows CE | 0x00010000 |
Alignment (对齐)
- SectionAlignment: 内存中的节对齐大小。默认是系统页面大小。
- FileAlignment: 文件中的节对齐大小。通常为 512 (0x200) 到 64K 之间的2的幂。
规则:SectionAlignment必须大于或等于FileAlignment。
SizeOfImage
- 内存中整个 PE 文件的映射尺寸。
- 大小包括所有头和节。
- 必须是
SectionAlignment的整数倍。
SizeOfHeaders
- 所有头 + 节表按照 文件对齐 后的大小。
- 计算公式:
text SizeOfHeaders = Align( e_lfanew (DOS头指向PE头的偏移) + 4 (PE签名 Signature) + sizeof(IMAGE_FILE_HEADER) + sizeof(IMAGE_OPTIONAL_HEADER) + sizeof(IMAGE_SECTION_HEADER) * 节数量, FileAlignment )
CheckSum
PE 文件校验和。关键系统进程、驱动程序、引导时加载的 DLL 会进行验证。
Subsystem
运行此映像所需的子系统。
| 宏定义 | 值 | 含义 |
|---|---|---|
| IMAGE_SUBSYSTEM_UNKNOWN | 0 | 未知的子系统 |
| IMAGE_SUBSYSTEM_NATIVE | 1 | 不需要子系统 (驱动/本机系统进程) |
| IMAGE_SUBSYSTEM_WINDOWS_GUI | 2 | Windows 图形用户界面 (GUI) |
| IMAGE_SUBSYSTEM_WINDOWS_CUI | 3 | Windows 字符模式 (控制台) |
| IMAGE_SUBSYSTEM_OS2_CUI | 5 | OS/2 CUI 子系统 |
| IMAGE_SUBSYSTEM_POSIX_CUI | 7 | POSIX CUI 子系统 |
| IMAGE_SUBSYSTEM_WINDOWS_CE_GUI | 9 | Windows CE 系统 |
| IMAGE_SUBSYSTEM_EFI_APPLICATION | 10 | EFI 应用程序 |
| IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER | 11 | EFI 引导服务驱动 |
| IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER | 12 | EFI 运行时服务驱动 |
| IMAGE_SUBSYSTEM_EFI_ROM | 13 | EFI ROM 镜像 |
| IMAGE_SUBSYSTEM_XBOX | 14 | Xbox 系统 |
| IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION | 16 | 启动应用程序 |
DllCharacteristics
定义了 Image 的 DLL 特性(也可用于 EXE)。
| 宏定义 | 值 | 含义 |
|---|---|---|
| (Reserved) | 0x0001 – 0x0008 | 保留,必须为 0 |
| IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA | 0x0020 | 64位地址空间 ASLR (高熵) |
| IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE | 0x0040 | 支持 ASLR (加载时重定位) |
| IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY | 0x0080 | 强制代码完整性检查 |
| IMAGE_DLLCHARACTERISTICS_NX_COMPAT | 0x0100 | 支持 DEP (数据执行保护) |
| IMAGE_DLLCHARACTERISTICS_NO_ISOLATION | 0x0200 | 不应被隔离 |
| IMAGE_DLLCHARACTERISTICS_NO_SEH | 0x0400 | 不使用结构化异常处理 (SEH) |
| IMAGE_DLLCHARACTERISTICS_NO_BIND | 0x0800 | 不要绑定映像 |
| IMAGE_DLL_CHARACTERISTICS_APPCONTAINER | 0x1000 | 在 AppContainer 中执行 |
| IMAGE_DLLCHARACTERISTICS_WDM_DRIVER | 0x2000 | WDM 驱动 |
| IMAGE_DLL_CHARACTERISTICS_GUARD_CF | 0x4000 | 支持控制流保护 (CFG) |
| IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE | 0x8000 | 终端服务器感知 |
栈与堆
- SizeOfStackReserve: 初始化保留的栈大小。
- SizeOfStackCommit: 初始化实际提交的栈大小。
- SizeOfHeapReserve: 初始化保留的堆大小。
- SizeOfHeapCommit: 初始化实际提交的堆大小。
DataDirectory (数据目录)
指向数据目录中第一个 IMAGE_DATA_DIRECTORY 结构的指针。每个条目包含地址(RVA)和大小。
| 索引 | 宏定义 | 含义 |
|---|---|---|
| 0 | IMAGE_DIRECTORY_ENTRY_EXPORT | 导出表 |
| 1 | IMAGE_DIRECTORY_ENTRY_IMPORT | 导入表 |
| 2 | IMAGE_DIRECTORY_ENTRY_RESOURCE | 资源表 |
| 3 | IMAGE_DIRECTORY_ENTRY_EXCEPTION | 异常表 |
| 4 | IMAGE_DIRECTORY_ENTRY_SECURITY | 安全表 |
| 5 | IMAGE_DIRECTORY_ENTRY_BASERELOC | 基地址重定位表 |
| 6 | IMAGE_DIRECTORY_ENTRY_DEBUG | 调试表 |
| 7 | IMAGE_DIRECTORY_ENTRY_ARCHITECTURE | 架构数据 (保留0) |
| 8 | IMAGE_DIRECTORY_ENTRY_GLOBALPTR | 全局指针 RVA |
| 9 | IMAGE_DIRECTORY_ENTRY_TLS | TLS 表 (线程本地存储) |
| 10 | IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG | 加载配置表 |
| 11 | IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT | 绑定导入表 |
| 12 | IMAGE_DIRECTORY_ENTRY_IAT | 导入地址表 (IAT) |
| 13 | IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT | 延迟导入表 |
| 14 | IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR | COM 描述符表 (.NET) |
| 15 | (Reserved) | 保留 |
节表
描述数据块,上文pe文件从磁盘映射到内存中,节表描述这种映射关系,节表的每一行实际上是一个节标题。 此表紧跟可选标头(如果有)。因为文件头不包含指向节表的直接指针。节表的位置是通过计算标头后第一个字节的位置来确定的。节表中的条目数由文件标头中的 NumberOfSections 字段提供。 节表中的条目从 1 (1) 开始编号。 代码和数据内存部分条目按链接器选择的顺序排列。
节的va必须由链接器分配以便于按升序和相邻,必须是可选标头中 SectionAlignment 值的倍数。
| Offset | 大小 | 字段 | 说明 |
|---|---|---|---|
| 0 | 8 | 名称 | 一个 8 字节、null 填充的 UTF-8 编码字符串。如果字符串长度正好为 8 个字符,则不存在终止 null。对于较长的名称,此字段包含斜杠 (/),后跟十进制数的 ASCII 表示形式,该数字是字符串表中的偏移量。可执行映像不使用字符串表,并且不支持长度超过 8 个字符的节名称。如果对象文件中的长名称被发送到可执行文件,则会截断它们。 |
| 8 | 4 | VirtualSize | 加载到内存中的节的总大小。如果此值大于 SizeOfRawData,则节为零填充。此字段仅对可执行映像有效,应将对象文件设置为零。 |
| 12 | 4 | VirtualAddress | 对于可执行映像,是部分加载到内存中时相对于映像库的第一个字节的地址。对于对象文件,此字段是应用重定位前第一个字节的地址;为简单起见,编译器应将此设置为零。否则,它是在重定位期间从偏移量中减去的任意值。 |
| 16 | 4 | SizeOfRawData | ) 对象文件 (节的大小,或图像文件) 磁盘 (上初始化数据的大小。对于可执行映像,这必须是可选标头中的 FileAlignment 的倍数。如果小于 VirtualSize,则部分的其余部分为零填充。由于 SizeOfRawData 字段是舍入的,但 VirtualSize 字段不是,因此 SizeOfRawData 也可能大于 VirtualSize。如果节仅包含未初始化的数据,则此字段应为零。 |
| 20 | 4 | PointerToRawData | 指向 COFF 文件中节的第一页的文件指针。对于可执行映像,这必须是可选标头中的 FileAlignment 的倍数。对于对象文件,值应在 4 字节边界上对齐,以获得最佳性能。如果节仅包含未初始化的数据,则此字段应为零。 |
| 24 | 4 | PointerToRelocations | 指向节重定位条目开头的文件指针。对于可执行映像,如果没有任何重定位,则设置为零。 |
| 28 | 4 | PointerToLinenumbers | 指向节的行号条目开头的文件指针。如果没有 COFF 行号,则此值设置为零。映像的此值应为零,因为 COFF 调试信息已弃用。 |
| 32 | 2 | NumberOfRelocations | 节的重定位条目数。对于可执行映像,此值设置为零。 |
| 34 | 2 | NumberOfLinenumbers | 节的行号条目数。映像的此值应为零,因为 COFF 调试信息已弃用。 |
| 36 | 4 | 特征 | 描述部分特征的标志。有关详细信息,请参阅 节标志。 |
VA和FOA转换
- VA:在内存中的虚拟地址
- RVA:相对虚拟地址
- FOA:文件偏移地址
流程
va到foa
- 得到PVA的值:RVA = VA – ImageBase
- 判断RVA是否处于PE文件头中
- 在文件头中则FOA=RVA,不在则判断RVA位于哪个节,差值 = RVA – 节.VirtualAddress(RVA),FOA = 节.PointerToRawData + 差值
代码实现
// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
#include <math.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64 0x8664
//VA转FOA 32位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa32(UINT va, _IMAGE_DOS_HEADER *dos,_IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//得到RVA的值:RVA = VA - ImageBase
UINT rva = va - nt->OptionalHeader.ImageBase;
//输出rva
printf("rva:%X\n", rva);
//找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
UINT PeEnd = (UINT)dos->e_lfanew+sizeof(_IMAGE_NT_HEADERS);
//输出PeEnd
printf("PeEnd:%X\n", PeEnd);
//判断rva是否位于PE文件头中
if (rva < PeEnd) {
//如果rva位于PE文件头中,则foa==rva,直接返回rva即可
printf("foa:%X\n", rva);
return rva;
}
else {
//如果rva在PE文件头外
//判断rva属于哪个节
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
//计算内存对齐后节的大小
UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize ,(UINT)sectionArr[i]->SizeOfRawData ) / (double)nt->OptionalHeader.SectionAlignment)* nt->OptionalHeader.SectionAlignment;
if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
//找到所属的节
//输出内存对齐后的节的大小
printf("SizeInMemory:%X\n", SizeInMemory);
break;
}
}
if (i >= nt->FileHeader.NumberOfSections) {
//未找到
printf("没有找到匹配的节\n");
return -1;
}
else {
//计算差值= RVA - 节.VirtualAddress
int offset = rva - sectionArr[i]->VirtualAddress;
//FOA = 节.PointerToRawData + 差值
int foa = sectionArr[i]->PointerToRawData + offset;
printf("foa:%X\n", foa);
return foa;
}
}
}
//VA转FOA 64位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa64(UINT va, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS64* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//得到RVA的值:RVA = VA - ImageBase
UINT rva = va - nt->OptionalHeader.ImageBase;
//输出rva
printf("rva:%X\n", rva);
//找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS64);
//输出PeEnd
printf("PeEnd:%X\n", PeEnd);
//判断rva是否位于PE文件头中
if (rva < PeEnd) {
//如果rva位于PE文件头中,则foa==rva,直接返回rva即可
printf("foa:%X\n", rva);
return rva;
}
else {
//如果rva在PE文件头外
//判断rva属于哪个节
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
//计算内存对齐后节的大小
UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize ,(UINT)sectionArr[i]->SizeOfRawData ) / (double)nt->OptionalHeader.SectionAlignment)* nt->OptionalHeader.SectionAlignment;
if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
//找到所属的节
//输出内存对齐后的节的大小
printf("SizeInMemory:%X\n", SizeInMemory);
break;
}
}
if (i >= nt->FileHeader.NumberOfSections) {
//未找到
printf("没有找到匹配的节\n");
return -1;
}
else {
//计算差值= RVA - 节.VirtualAddress
int offset = rva - sectionArr[i]->VirtualAddress;
//FOA = 节.PointerToRawData + 差值
int foa = sectionArr[i]->PointerToRawData + offset;
printf("foa:%X\n", foa);
return foa;
}
}
}
int main(int argc, char* argv[])
{
//创建DOS对应的结构体指针
_IMAGE_DOS_HEADER* dos;
//读取文件,返回文件句柄
HANDLE hFile = CreateFileA("C:\\Users\\lyl610abc\\Desktop\\GlobalVariety.exe", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, 0);
//根据文件句柄创建映射
HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, 0);
//映射内容
LPVOID pFile = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
//类型转换,用结构体的方式来读取
dos = (_IMAGE_DOS_HEADER*)pFile;
//输出dos->e_magic,以十六进制输出
printf("dos->e_magic:%X\n", dos->e_magic);
//创建指向PE文件头标志的指针
DWORD* peId;
//让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
peId = (DWORD*)((UINT)dos + dos->e_lfanew);
//输出PE文件头标志,其值应为4550,否则不是PE文件
printf("peId:%X\n", *peId);
//创建指向可选PE头的第一个成员magic的指针
WORD* magic;
//让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
//输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
printf("magic:%X\n", *magic);
//根据magic判断为32位程序还是64位程序
switch (*magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
{
printf("32位程序\n");
//确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS* nt;
//让PE文件头指针指向其对应的地址
nt = (_IMAGE_NT_HEADERS*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);
//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**) malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);
//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while(cnt< nt->FileHeader.NumberOfSections){
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER)*cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}
VaToFoa32(0x4198B0,dos, nt, sectionArr);
break;
}
case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
{
printf("64位程序\n");
//确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS64* nt;
nt = (_IMAGE_NT_HEADERS64*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);
//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);
//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}
break;
}
default:
{
printf("error!\n");
break;
}
}
return 0;
}
foa到va
- 判断foa是否位于pe文件头中
- 是则,foa=rva,不是则判断位于哪个节,差值 = FOA – 节.PointerToRawData ,RVA = 差值 + 节.VirtualAddress(RVA)
- VA = ImageBase + RVA
代码实现
// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
#include <math.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64 0x8664
//VA转FOA 32位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa32(UINT va, _IMAGE_DOS_HEADER *dos,_IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//得到RVA的值:RVA = VA - ImageBase
UINT rva = va - nt->OptionalHeader.ImageBase;
//输出rva
printf("rva:%X\n", rva);
//找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
UINT PeEnd = (UINT)dos->e_lfanew+sizeof(_IMAGE_NT_HEADERS);
//输出PeEnd
printf("PeEnd:%X\n", PeEnd);
//判断rva是否位于PE文件头中
if (rva < PeEnd) {
//如果rva位于PE文件头中,则foa==rva,直接返回rva即可
printf("foa:%X\n", rva);
return rva;
}
else {
//如果rva在PE文件头外
//判断rva属于哪个节
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
//计算内存对齐后节的大小
UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize ,(UINT)sectionArr[i]->SizeOfRawData ) / (double)nt->OptionalHeader.SectionAlignment)* nt->OptionalHeader.SectionAlignment;
if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
//找到所属的节
//输出内存对齐后的节的大小
printf("SizeInMemory:%X\n", SizeInMemory);
break;
}
}
if (i >= nt->FileHeader.NumberOfSections) {
//未找到
printf("没有找到匹配的节\n");
return -1;
}
else {
//计算差值= RVA - 节.VirtualAddress
UINT offset = rva - sectionArr[i]->VirtualAddress;
//FOA = 节.PointerToRawData + 差值
UINT foa = sectionArr[i]->PointerToRawData + offset;
printf("foa:%X\n", foa);
return foa;
}
}
}
//VA转FOA 64位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa64(UINT va, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS64* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//得到RVA的值:RVA = VA - ImageBase
UINT rva = va - nt->OptionalHeader.ImageBase;
//输出rva
printf("rva:%X\n", rva);
//找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS64);
//输出PeEnd
printf("PeEnd:%X\n", PeEnd);
//判断rva是否位于PE文件头中
if (rva < PeEnd) {
//如果rva位于PE文件头中,则foa==rva,直接返回rva即可
printf("foa:%X\n", rva);
return rva;
}
else {
//如果rva在PE文件头外
//判断rva属于哪个节
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
//计算内存对齐后节的大小
UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize ,(UINT)sectionArr[i]->SizeOfRawData ) / (double)nt->OptionalHeader.SectionAlignment)* nt->OptionalHeader.SectionAlignment;
if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
//找到所属的节
//输出内存对齐后的节的大小
printf("SizeInMemory:%X\n", SizeInMemory);
break;
}
}
if (i >= nt->FileHeader.NumberOfSections) {
//未找到
printf("没有找到匹配的节\n");
return -1;
}
else {
//计算差值= RVA - 节.VirtualAddress
UINT offset = rva - sectionArr[i]->VirtualAddress;
//FOA = 节.PointerToRawData + 差值
UINT foa = sectionArr[i]->PointerToRawData + offset;
printf("foa:%X\n", foa);
return foa;
}
}
}
//FOA转VA 32位
//第一个参数为要转换的在文件中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT FoaToVa32(UINT foa, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS);
//判断FOA是否位于PE文件头中
if (foa < PeEnd) {
//如果foa位于PE文件头中,则foa==rva,直接返回foa+ImageBase即可
printf("va:%X\n", foa+nt->OptionalHeader.ImageBase);
return foa + nt->OptionalHeader.ImageBase;
}
else {
//如果foa在PE文件头外
//判断foa属于哪个节
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
if (foa >= sectionArr[i]->PointerToRawData && foa < (sectionArr[i]->PointerToRawData + sectionArr[i]->SizeOfRawData)) {
//找到所属的节
break;
}
}
if (i >= nt->FileHeader.NumberOfSections) {
//未找到
printf("没有找到匹配的节\n");
return -1;
}
else {
//计算差值= FOA - 节.PointerToRawData
UINT offset = foa - sectionArr[i]->PointerToRawData;
//RVA = 差值 + 节.VirtualAddress(RVA)
UINT rva = offset+ sectionArr[i]->VirtualAddress;
printf("va:%X\n", rva + nt->OptionalHeader.ImageBase);
return rva + nt->OptionalHeader.ImageBase;
}
}
return 0;
}
//FOA转VA 64位
//第一个参数为要转换的在文件中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT FoaToVa64(UINT foa, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS64* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS64);
//判断FOA是否位于PE文件头中
if (foa < PeEnd) {
//如果foa位于PE文件头中,则foa==rva,直接返回foa+ImageBase即可
printf("va:%X\n", foa + nt->OptionalHeader.ImageBase);
return foa + nt->OptionalHeader.ImageBase;
}
else {
//如果foa在PE文件头外
//判断foa属于哪个节
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
if (foa >= sectionArr[i]->PointerToRawData && foa < (sectionArr[i]->PointerToRawData + sectionArr[i]->SizeOfRawData)) {
//找到所属的节
break;
}
}
if (i >= nt->FileHeader.NumberOfSections) {
//未找到
printf("没有找到匹配的节\n");
return -1;
}
else {
//计算差值= FOA - 节.PointerToRawData
UINT offset = foa - sectionArr[i]->PointerToRawData;
//RVA = 差值 + 节.VirtualAddress(RVA)
UINT rva = offset + sectionArr[i]->VirtualAddress;
printf("va:%X\n", rva + nt->OptionalHeader.ImageBase);
return rva + nt->OptionalHeader.ImageBase;
}
}
return 0;
}
int main(int argc, char* argv[])
{
//创建DOS对应的结构体指针
_IMAGE_DOS_HEADER* dos;
//读取文件,返回文件句柄
HANDLE hFile = CreateFileA("C:\\Users\\sixonezero\\Desktop\\GlobalVariety.exe", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, 0);
//根据文件句柄创建映射
HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, 0);
//映射内容
LPVOID pFile = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
//类型转换,用结构体的方式来读取
dos = (_IMAGE_DOS_HEADER*)pFile;
//输出dos->e_magic,以十六进制输出
printf("dos->e_magic:%X\n", dos->e_magic);
//创建指向PE文件头标志的指针
DWORD* peId;
//让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
peId = (DWORD*)((UINT)dos + dos->e_lfanew);
//输出PE文件头标志,其值应为4550,否则不是PE文件
printf("peId:%X\n", *peId);
//创建指向可选PE头的第一个成员magic的指针
WORD* magic;
//让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
//输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
printf("magic:%X\n", *magic);
//根据magic判断为32位程序还是64位程序
switch (*magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
{
printf("32位程序\n");
//确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS* nt;
//让PE文件头指针指向其对应的地址
nt = (_IMAGE_NT_HEADERS*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);
//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**) malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);
//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while(cnt< nt->FileHeader.NumberOfSections){
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER)*cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}
VaToFoa32(0x4198B0,dos, nt, sectionArr);
FoaToVa32(0x176B0, dos, nt, sectionArr);
break;
}
case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
{
printf("64位程序\n");
//确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS64* nt;
nt = (_IMAGE_NT_HEADERS64*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);
//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);
//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}
VaToFoa32(0x4198B0,dos, nt, sectionArr);
FoaToVa32(0x176B0, dos, nt, sectionArr);
break;
}
default:
{
printf("error!\n");
break;
}
}
return 0;
}
RVA和FOA之间的差异归根结底就是在于文件对齐和内存对齐的差异上
前面补充
关于正着读和倒着读
计算机使用小端序可以加快运行速度,所以计算机用于计算,寻址,判断大小的数据都要倒着看,包括地址、偏移量、大小、计数、标志位、特征值。从右往左,高地址往低地址
本身的字节数组形式存在的数据,设计给人看的名字(比如text那些),要正着读
但是mz和pe的标志,本质是数据,但被设计成了字符串形式
在hex编辑器里正着显示,代码里和分析数据时得倒着写
关于对象和映像
对象文件,常见后缀obj,就是编译c或者cpp文件时生成的o,不需要被执行,只是半成品,直接以coff文件头开始,没有ms dos头
映像文件,常见后缀,exe,dll,sys,链接器linker产物,是成品,将多个obj文件和库文件lib拼装在一起形成最终文件(映像解释,结构被设计为可以直接映射到内存上运行,在磁盘样子和加载到内存中很想,为了兼容,必须以msdos头和存根开始,然后是pe签名,然后是coff文件头)
两者布局区别
对象文件
[ COFF 文件头 ] <--- 文件直接从这里开始 (偏移量 0)
[ 节表 ]
[ 原始数据... ]
映像文件
[ MS-DOS 头 ]
[ MS-DOS 存根 ]
[ PE 签名 ]
[ COFF 文件头 ] <--- 这里才是 COFF 头,结构与上面一样,但位置靠后
[ 可选头 (Optional Header) ] <--- 映像文件特有
[ 节表 ]
[ 原始数据... ]
关于dos
头和存根,头指元数据,描述文件的作用,结构化数据,告诉window系统(加载器)处理文件的方式,描述文件数据结构,存根(stub)是一段可以执行的机器码,兼容性处理,ms ods stub是一个完整的16位dos程序,存储在pe文件的头部
dos,磁盘操作系统
stub作用,防报错,就是比如16位系统识别不了,就直接通过stub终止程序,但是window,dos头那里有偏移量,就会跳过这个stub
一些零碎概念
| 名称 | 描述 |
|---|---|
| 属性证书 | 用于将可验证声明与映像关联的证书。许多不同的可验证声明可以与文件相关联;其中最有用的一个声明是软件制造商的声明,此声明指示映像的消息摘要应该是什么。消息摘要类似于检验和,只是很难伪造。因此,很难修改文件以使其具有与原始文件相同的消息摘要。可以使用公钥或私钥加密方案来验证声明是否是由制造商发出的。本文档描述了有关属性证书的详细信息,但不允许将其插入到图像文件中。 |
| 日期/时间戳 | 在 PE 或 COFF 文件中的多个位置用于不同目的的戳记。在大多数情况下,每个戳记的格式与 C 运行时库中的时间函数使用的格式相同。有关异常,请参见调试类型中 IMAGE_DEBUG_TYPE_REPRO 的说明。如果戳记值为 0 或 0xFFFFFFFF,则它不表示实际或有意义的日期/时间戳。 |
| 文件指针 | 链接器(对于目标文件)或加载器(对于映像文件)处理之前文件本身中项的位置。换句话说,这是存储在磁盘上的文件内的位置。 |
| 链接器 | 随 Microsoft Visual Studio 一起提供的链接器引用。 |
| 对象文件 | 作为链接器输入提供的文件。链接器生成一个映像文件,而此映像文件又用作加载器的输入。术语“目标文件”并不一定意味着与面向对象的编程有任何联系。 |
| 已保留,必须为 0 | 字段的描述,指示该字段的值对于生成器来说必须为零,而使用者必须忽略该字段。 |
| 相对虚拟地址 (RVA) | 在映像文件中,这是项目加载到内存并从中减去映像文件基地址后的地址。项目的 RVA 几乎总是与其在磁盘上文件中的位置(文件指针)不同。 在目标文件中,RVA 的意义不大,因为未分配内存位置。在这种情况下,RVA 将是一个段内的地址(此表后面将进行描述),稍后在链接期间会对此地址应用重定位。为简单起见,编译器应只将每个部分中的第一个 RVA 设置为零。 |
| 部分 | PE 或 COFF 文件中代码或数据的基本单位。例如,目标文件中的所有代码都可以组合在单个部分中,或者(取决于编译器行为)每个函数都可以占用自己的部分。部分越多,文件开销就越大,但链接器能够更有选择性地链接代码。一个部分类似于 Intel 8086 体系结构中的段。一个部分中的所有原始数据都必须连续加载。此外,映像文件可以包含多个具有特殊用途的部分,例如 .tls 或 .reloc 。 |
| 虚拟地址 (VA) | 与 RVA 相同,只不过不减去映像文件的基址。该地址称为 VA,因为 Windows 会为每个进程创建一个独立于物理内存的不同 VA 空间。对于几乎所有目的,VA 应只被视为一个地址。VA 不如 RVA 那么可预测,因为加载器可能不会在其首选位置加载映像。 |
| 英文术语 | 中文术语 | 描述 |
|---|---|---|
| Virtual Address (VA) | 虚拟地址 | 进程虚拟内存空间中的绝对地址。 |
| Relative Virtual Address (RVA) | 相对虚拟地址 | 内存地址相对于模块基址的偏移。 |
| File Pointer | 文件指针 | 文件在磁盘上的物理偏移位置。 |
| Section | 节 / 段 | 代码或数据的逻辑分组,具有相同的内存属性(如只读、可执行)。 |
| Data Directory | 数据目录 | 位于可选头末尾的数组,指向各种表(导入、导出、资源等)。 |
| Thunk | 转换层 / 桩代码 | 这里通常指导入地址表 (IAT) 中的项。 |
ELF
比起PE,ELF分了section给编译器和segment给加载器,处理外部函数的时候调用了GOT/PLT(延迟绑定),PE则用的IAT
elf格式由system v标准定义,设计更灵活,支持可执行文件,共享库,目标文件等多种类型
常见后缀
- 无后缀或.bin,可执行文件如/bin/ls、/usr/bin/python3,通常无后缀
- .so,共享对象,类似windows的dll,提供动态链接功能
- .a,静态链接库,包含多个目标文件归档,编译时会被完整嵌入可执行文件
- .o,目标文件,编译器输出的中间文件,要链接器处理后生成可执行文件
- .ko,内核模块,linux内核的驱动程序,运行于内核空间,相当于window里面的.sys(驱动程序),运行在ring 0(最高权限),比起普通elf,ko驱动没有main,只有module_init()(插入模块时执行)和对应的exit,会看到大量重定位表,因为要被“链接”进内核
- .mod,部分系统的模块文件,如早期unix功能类似ko,包含了模块的版本信息(Version Magic)和符号依赖关系。
结构
| 区域位置 | 组件名称 (中文) | 英文对应 | 说明 |
|---|---|---|---|
| 顶部 | ELF 文件头 | ELF Header | 文件的总索引,包含魔数、版本等。 |
| 上部 | 程序头部表 | Program Header Table | 描述段(Segment)信息,用于执行视图。 |
| 中部 | .text | Code Section | 代码节(指令)。 |
| 中部 | .data | Data Section | 数据节(已初始化全局变量)。 |
| 中部 | .bss | BSS Section | 未初始化数据节。 |
| 中部 | .symtab | Symbol Table | 符号表(函数名、变量名)。 |
| 中部 | .debug | Debug Info | 调试信息。 |
| 中部 | .line | Line Number Table | 行号表(源码行号对应)。 |
| 中部 | … | Others | 其他各类节。 |
| 底部 | 节区头部表 | Section Header Table | 描述节(Section)信息,链接视图的核心,记录了上面所有节的位置。 |
ELF头部
文件起始位置,固定大小(32位为52字节,64位位64字节)
结构体
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT]; // ELF标识符
Elf32_Half e_type; // 文件类型
Elf32_Half e_machine; // 目标架构
Elf32_Word e_version; // ELF版本
Elf32_Addr e_entry; // 程序入口点
Elf32_Off e_phoff; // 程序头表偏移
Elf32_Off e_shoff; // 节头表偏移
Elf32_Word e_flags; // 处理器特定标志
Elf32_Half e_ehsize; // ELF头部大小
Elf32_Half e_phentsize; // 程序头表项大小
Elf32_Half e_phnum; // 程序头表项数量
Elf32_Half e_shentsize; // 节头表项大小
Elf32_Half e_shnum; // 节头表项数量
Elf32_Half e_shstrndx; // 节名称字符串表索引
} Elf32_Ehdr;
其中,e_ident字段的前4个字节固定为0x7f、’E’、’L’、’F’,这是ELF文件的魔数标识。
程序头表(Program Header Table)
描述段(segment)信息,用于程序加载和执行
结构体
typedef struct {
Elf32_Word p_type; // 段类型
Elf32_Off p_offset; // 段在文件中的偏移
Elf32_Addr p_vaddr; // 段在内存中的虚拟地址
Elf32_Addr p_paddr; // 物理地址(通常与虚拟地址相同)
Elf32_Word p_filesz; // 段在文件中的大小
Elf32_Word p_memsz; // 段在内存中的大小
Elf32_Word p_flags; // 段标志(读/写/执行)
Elf32_Word p_align; // 段对齐方式
} Elf32_Phdr;
节头表(Section Header Table)
描述节的信息(section),主要用于链接和调试
typedef struct {
Elf32_Word sh_name; // 节名称(字符串表索引)
Elf32_Word sh_type; // 节类型
Elf32_Word sh_flags; // 节标志
Elf32_Addr sh_addr; // 节在内存中的地址
Elf32_Off sh_offset; // 节在文件中的偏移
Elf32_Word sh_size; // 节大小
Elf32_Word sh_link; // 链接到其他节的索引
Elf32_Word sh_info; // 附加信息
Elf32_Word sh_addralign; // 节对齐方式
Elf32_Word sh_entsize; // 条目大小(如果有)
} Elf32_Shdr;
节
节存储实际内容,常见节有:
- .text:可执行代码
- .data:已初始化的全局变量
- .bss:未初始化的全局变量(不占用文件空间)
- .rodata:只读数据
- .symtab:符号表
- .strtab:字符串表
- .rel.*:重定位信息
- .dynamic:动态链接信息
- .interp:程序解释器路径(如/lib/ld-linux.so.2)
ELF文件的两种视图
链接视图
对应节头表,将文件按功能划分为多个节,主要用于静态链接过程,特点是粒度细(就是分类细),便于编译器生成和链接器处理,实际执行中,链接器会将多个节合并成段提高空间利用率
执行视图
对应程序头表,描述了怎么将文件内容映射到进程的虚拟地址空间,关注段
按照权限合并,比如text和rodata都只读,data和bss都可读可写
常见段类型
- PT_LOAD,通常一个程序至少两个ptload段,一个只读,一个读写
- PT_DYNAMIC,动态链接段,对应节.dynamic,描述需要哪个so库(DT_NEEDED),符号表,重定位表,初始化函数,调用外部库函数
- PT_INTERP,解释器路径,对应节.interp,通常是字符串,路径指向动态链接器,运行前准备
- PT_NOTE,对应节.note.ABU-tag等,辅助说明
- PT_TLS,线程局部存储,对应节,.tdata,.tbss,告诉系统如何为每个线程初始化变量
- PT_GNU_STACK,栈控制,虚拟段,告诉栈是否可执行
PT,给操作系统内核看,决定段属性,DT,给动态链接器看,决定动态链接细节
静态链接
静态链接过程
在编译时将库代码直接复制到最终可执行文件的链接方式,过程如
- 编译器生成目标文件.o
- 链接器扫描所有目标文件和静态库.a
- 解析所有符号引用
- 将使用的库代码复制到可执行文件
- 生成完全独立的可执行文件
特点:地址预确定,链接阶段为所有符号分配最终虚拟地址
静态加载过程
- 解析elf文件头部,验证文件格式
- 根据程序头表将各段映射到链接时确定的固定虚拟地址
- 建立内存管理结构(mm_struct)管理虚拟内存区域(VMA)
- 初始化页表映射
- cpu直接从elf头部的e_entry获取入口地址开始执行
所有内存访问都使用绝对地址,通过MMU硬件将链接时确定的虚拟地址转换为物理地址。整个过程完全复用链接阶段确定的地址布局,无需运行时重定位。
特点:程序自包含,但可执行文件体积大
动态链接
动态链接过程
程序运行时才加载链接和链接所需库
- 编译器生成.o
- 链接器记录动态库.so以来信息
- 生成包含为解析符号的可执行文件
- 程序运行时,动态链接器(ld.so)
- 查找并加载所需共享库
- 解析符号引用
- 执行重定位操作
特点:地址延迟确定,实际地址在运行时才解析
动态加载过程
- 内核映射主程序的段到内存
- 加载动态链接器(ld.so)
- 动态链接器按需加载依赖的共享库
- 通过GOT和PLT机制完成符号解析和地址重定位
- 所有库代码采用位置无关代码(PLC)技术,支持多进程共享
- 支持ASLR(地址空间布局随机化)
特点:节省内存,依赖管理复杂









