目录
-
指针和虚拟内存
-
PE文件结构
-
汇编和函数栈帧
简介
本篇将介绍PE文件结构中重要的属性字段,说明节区和段和页的区别。介绍PE文件的装载。并介绍LIEF库。
PE文件结构
NOTE本节的结构可通过在Visual Studio中包含Windows.h来查阅。 例如:IMAGE_DOS_HEADER、IMAGE_OPTIONAL_HEADER64等。 学习时,建议打开一个十六进制编辑器一边查看结构一遍查看字节码 同时,本节均以64位为例。
总览

PE文件结构中最重要的内容如下:
-
DOS文件头:用于区别16位程序和x86/64的程序
-
NT头:
-
文件头:用于说明基本的PE程序属性
-
可选头:更多的PE程序属性
-
数据目录表:具体内容见后文DataDirectory
-
导出表
-
导入表/延迟导入
-
重定位表
-
资源地址
-
异常表
-
证书签名
-
调试信息
-
全局指针
-
线程本地存储
-
-
-
-
节表:描述节区属性
以下按顺序分别介绍
DOS_Header
DOS指Disk Operating System。是Windows出现之前的PC端主要操作系统,通过命令行界面进行操作。DOS是16位的操作系统
DOS头的主要作用是兼容以往的系统和Windows系统。在DOS系统中运行32/64位程序会自动打印DOSstub(dos存根)中的“这个程序不能在该环境下运行”的字符串。
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
DOS头具体结构如上。其中重要的值只有e_magic:4D5A作为文件标识(一般也称魔法数字或魔数magic)和e_lfanew即PE头的文件偏移。
NT_Header
nt头是文件头和可选头的合称,之所以叫NT,是因为区别于DOS,Windows推出的新一代操作系统系列内核架构:Windows NT。NT是New Technology的缩写。
nt头结构如下:
typedef struct _IMAGE_NT_HEADERS64 { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER64 OptionalHeader;} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;其中,Signature是魔数5045。后方跟随的是20字节的文件头和240字节的可选头
FILE_Header
文件头说明PE文件的基本属性
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics;} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine说明目标机器CPU架构,如果修改该值,程序将不能在正确的平台上运行,因为无法通过该值的检查。
NumberOfSections说明节表中节区的数量,Loader根据这个来解析节表。
TimeDateStamp指文件生成时间戳。
PointerToSymbolTable指向符号表偏移。如果不打开符号加入最终的可执行文件的选项,这里就是0。最终的符号文件为pdb文件。
NumberOfSymbols符号表中符号的数量。同上。
SizeOfOptionalHeader是可选头大小,通常来说PE32为E0字节,PE32+为F0字节。
Characteristics表示其他的文件属性,按位来设置。
Machine可选字段:
| 宏定义 | 值 | 含义 |
|---|---|---|
| IMAGE_FILE_MACHINE_UNKNOWN | 0x0000 | 未知或未指定的机器类型 |
| IMAGE_FILE_MACHINE_TARGET_HOST | 0x0001 | 表示与宿主机交互(而不是 WoW 客户端),主要用于工具 |
| IMAGE_FILE_MACHINE_I386 | 0x014c | Intel 386(x86 架构) |
| IMAGE_FILE_MACHINE_R3000 | 0x0162 | MIPS R3000,小端序(0x160 表示大端序) |
| IMAGE_FILE_MACHINE_R4000 | 0x0166 | MIPS R4000,小端序 |
| IMAGE_FILE_MACHINE_R10000 | 0x0168 | MIPS R10000,小端序 |
| IMAGE_FILE_MACHINE_WCEMIPSV2 | 0x0169 | MIPS WCE v2,小端序 |
| IMAGE_FILE_MACHINE_ALPHA | 0x0184 | DEC Alpha AXP |
| IMAGE_FILE_MACHINE_SH3 | 0x01a2 | Hitachi SH3,小端序 |
| IMAGE_FILE_MACHINE_SH3DSP | 0x01a3 | Hitachi SH3 DSP |
| IMAGE_FILE_MACHINE_SH3E | 0x01a4 | Hitachi SH3E,小端序 |
| IMAGE_FILE_MACHINE_SH4 | 0x01a6 | Hitachi SH4,小端序 |
| IMAGE_FILE_MACHINE_SH5 | 0x01a8 | Hitachi SH5 |
| IMAGE_FILE_MACHINE_ARM | 0x01c0 | ARM 小端序 |
| IMAGE_FILE_MACHINE_THUMB | 0x01c2 | ARM Thumb/Thumb-2 小端序 |
| IMAGE_FILE_MACHINE_ARMNT | 0x01c4 | ARM Thumb-2 小端序(Windows NT 用) |
| IMAGE_FILE_MACHINE_AM33 | 0x01d3 | Mitsubishi AM33 |
| IMAGE_FILE_MACHINE_POWERPC | 0x01F0 | IBM PowerPC,小端序 |
| IMAGE_FILE_MACHINE_POWERPCFP | 0x01f1 | IBM PowerPC 浮点支持 |
| IMAGE_FILE_MACHINE_IA64 | 0x0200 | Intel Itanium (IA-64) |
| IMAGE_FILE_MACHINE_MIPS16 | 0x0266 | MIPS16 |
| IMAGE_FILE_MACHINE_ALPHA64 | 0x0284 | DEC Alpha 64 位 |
| IMAGE_FILE_MACHINE_MIPSFPU | 0x0366 | MIPS 带 FPU |
| IMAGE_FILE_MACHINE_MIPSFPU16 | 0x0466 | MIPS16 带 FPU |
| IMAGE_FILE_MACHINE_AXP64 | 0x0284 | Alpha64(等同于 IMAGE_FILE_MACHINE_ALPHA64) |
| IMAGE_FILE_MACHINE_TRICORE | 0x0520 | Infineon TriCore |
| IMAGE_FILE_MACHINE_CEF | 0x0CEF | CEF(保留/特殊用途) |
| IMAGE_FILE_MACHINE_EBC | 0x0EBC | EFI Byte Code |
| IMAGE_FILE_MACHINE_AMD64 | 0x8664 | AMD64(x64 架构) |
| IMAGE_FILE_MACHINE_M32R | 0x9041 | Mitsubishi M32R,小端序 |
| IMAGE_FILE_MACHINE_ARM64 | 0xAA64 | ARM64 小端序 |
| IMAGE_FILE_MACHINE_CEE | 0xC0EE | CEE(Common Language Runtime,.NET/托管代码) |
Characteristics可选字段:
| 宏定义 | 值 | 含义 |
|---|---|---|
| IMAGE_FILE_RELOCS_STRIPPED | 0x0001 | 文件中已去掉重定位信息(如果加载地址不匹配则无法重定位)。 |
| IMAGE_FILE_EXECUTABLE_IMAGE | 0x0002 | 文件是可执行映像(没有未解析的外部引用)。 |
| IMAGE_FILE_LINE_NUMS_STRIPPED | 0x0004 | 已去掉行号信息(调试相关)。 |
| IMAGE_FILE_LOCAL_SYMS_STRIPPED | 0x0008 | 已去掉本地符号信息。 |
| IMAGE_FILE_AGGRESIVE_WS_TRIM | 0x0010 | 系统可激进地修剪工作集(很少使用)。 |
| IMAGE_FILE_LARGE_ADDRESS_AWARE | 0x0020 | 应用程序能处理大于 2GB 的地址空间(常见于 64 位或 /LARGEADDRESSAWARE 编译)。 |
| IMAGE_FILE_BYTES_REVERSED_LO | 0x0080 | 低位字节顺序反转(历史遗留)。 |
| IMAGE_FILE_32BIT_MACHINE | 0x0100 | 文件适用于 32 位机器。 |
| IMAGE_FILE_DEBUG_STRIPPED | 0x0200 | 调试信息已移到单独的 .DBG 文件。 |
| IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP | 0x0400 | 如果映像在可移动介质上,需复制到交换文件再运行。 |
| IMAGE_FILE_NET_RUN_FROM_SWAP | 0x0800 | 如果映像在网络上,需复制到交换文件再运行。 |
| IMAGE_FILE_SYSTEM | 0x1000 | 文件是 SYS。 |
| IMAGE_FILE_DLL | 0x2000 | 文件是 DLL。 |
| IMAGE_FILE_UP_SYSTEM_ONLY | 0x4000 | 文件只能在单处理器机器上运行。 |
| IMAGE_FILE_BYTES_REVERSED_HI | 0x8000 | 高位字节顺序反转(历史遗留)。 |
OPTIONAL_Header
PE文件由COFF格式演化而来,而COFF格式是用来描述obj文件的(编译过程中间文件或lib静态链接库文件)。COFF格式不能直接运行,不需要运行时信息。PE文件格式为了保持和COFF格式的兼容,将运行时的额外信息放在可选头中。
结构如下:
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:标识PE文件类型(32/64/rom)。
MajorLinkerVersion / MinorLinkerVersion:表示连接器主版本号和次版本号。
SizeOfCode:表示代码节总大小(比如.text)。
SizeOfInitializedData/SizeOfUninitializedData:分别表示已初始化(比如.data)和未初始化(比如.bss)数据节大小。
AddressOfEntryPoint:程序入口点RVA(相对于映像基地址的偏移)。加载器初始化完成后,从这里开始执行代码。
BaseOfCode:代码节起始RVA。
ImageBase:映像基地址。
SectionAlignment/FileAlignment:分别表示内存和文件中中节的对齐粒度,一般分别为0x1000和0x200 。
MajorOperatingSystemVersion / MinorOperatingSystemVersion:期望的操作系统版本号。
MajorImageVersion / MinorImageVersion:映像版本号,由开发者自定义。
MajorSubsystemVersion / MinorSubsystemVersion:子系统版本(子系统见Subsystem字段)。
Win32VersionValue:保留值。
SizeOfImage:映像总大小,按SectionAlignment对齐。
SizeOfHeaders:文件中头部(DOS,NT,Sections)的大小,按FileAlignment对齐。
CheckSum:系统校验和。
Subsystem:子系统类型:GUI,CUI,CE GUI,EFI。
DllCharacteristics:DLL特性标志位,具体内容见后表。
SizeOfStackReserve / SizeOfStackCommit:栈保留大小(默认栈的虚拟内存大小)和栈提交大小(默认栈的物理内存大小)。
SizeOfHeapReserve / SizeOfHeapCommit:堆保留大小和堆提交大小。
LoaderFlags:保留。
NumberOfRvaAndSizes:数据目录数组的长度,一般为16。
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]:数据目录数组,此处内容较为重要,具体内容见后。
Magic可选字段
| 宏 | 值 | 用途 |
|---|---|---|
| IMAGE_NT_OPTIONAL_HDR32_MAGIC | 0x10B | 表示 PE32(32位可执行文件) |
| IMAGE_NT_OPTIONAL_HDR64_MAGIC | 0x20B | 表示 PE32+(64位可执行文件) |
| IMAGE_ROM_OPTIONAL_HDR_MAGIC | 0x107 | 表示 ROM 映像(嵌入式/特殊用途) |
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 控制台子系统(历史遗留) |
| IMAGE_SUBSYSTEM_POSIX_CUI | 7 | POSIX 控制台子系统(历史遗留) |
| IMAGE_SUBSYSTEM_NATIVE_WINDOWS | 8 | 原生 Win9x 驱动 |
| IMAGE_SUBSYSTEM_WINDOWS_CE_GUI | 9 | Windows CE GUI 程序 |
| 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 | Windows Boot 应用程序 |
| IMAGE_SUBSYSTEM_XBOX_CODE_CATALOG | 17 | Xbox Code Catalog 映像 |
DllCharacteristics可选字段
| 宏 | 值 | 用途 |
|---|---|---|
| IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA | 0x0020 | 支持高熵 64 位地址空间(ASLR 更强) |
| IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE | 0x0040 | 映像可重定位(支持 ASLR) |
| IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY | 0x0080 | 强制代码完整性检查 |
| IMAGE_DLLCHARACTERISTICS_NX_COMPAT | 0x0100 | 支持 NX(DEP,禁止执行数据页) |
| IMAGE_DLLCHARACTERISTICS_NO_ISOLATION | 0x0200 | 不使用应用程序隔离(Manifest 隔离) |
| IMAGE_DLLCHARACTERISTICS_NO_SEH | 0x0400 | 不使用 SEH(结构化异常处理) |
| IMAGE_DLLCHARACTERISTICS_NO_BIND | 0x0800 | 不允许绑定导入表 |
| IMAGE_DLLCHARACTERISTICS_APPCONTAINER | 0x1000 | 要在 AppContainer 沙箱中运行 |
| IMAGE_DLLCHARACTERISTICS_WDM_DRIVER | 0x2000 | WDM 驱动程序 |
| IMAGE_DLLCHARACTERISTICS_GUARD_CF | 0x4000 | 支持 Control Flow Guard(CFG)安全特性 |
| IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE | 0x8000 | 终端服务器兼容 |
Data Directory 索引
| 宏 | 索引 | 用途 |
|---|---|---|
| IMAGE_DIRECTORY_ENTRY_EXPORT | 0 | 导出表(函数/变量导出信息) |
| IMAGE_DIRECTORY_ENTRY_IMPORT | 1 | 导入表(DLL 引用信息) |
| IMAGE_DIRECTORY_ENTRY_RESOURCE | 2 | 资源表(图标、字符串、对话框等) |
| IMAGE_DIRECTORY_ENTRY_EXCEPTION | 3 | 异常表(x64 异常处理信息) |
| IMAGE_DIRECTORY_ENTRY_SECURITY | 4 | 安全目录(数字签名,文件偏移寻址) |
| IMAGE_DIRECTORY_ENTRY_BASERELOC | 5 | 重定位表(地址修正信息) |
| IMAGE_DIRECTORY_ENTRY_DEBUG | 6 | 调试目录(PDB 路径、调试信息) |
| IMAGE_DIRECTORY_ENTRY_ARCHITECTURE | 7 | 架构专用数据(保留/弃用) |
| IMAGE_DIRECTORY_ENTRY_GLOBALPTR | 8 | 全局指针(IA-64 使用) |
| IMAGE_DIRECTORY_ENTRY_TLS | 9 | TLS 表(线程本地存储回调) |
| IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG | 10 | 加载配置表(安全策略、SEH 表等) |
| IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT | 11 | 绑定导入表(优化加载速度) |
| IMAGE_DIRECTORY_ENTRY_IAT | 12 | 导入地址表(运行时函数指针表) |
| IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT | 13 | 延迟导入表(首次调用时才加载 DLL) |
| IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR | 14 | COM/.NET 目录(CLR 头,托管代码支持) |
Section Header
section header是节表中的项,其大小由FILE_Header的NumberOfSections指定。
其结构如下:
#define IMAGE_SIZEOF_SHORT_NAME 8
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字节的节区名称,一般由.开头,后方填充0。
Misc:如果是目标文件,则为PhysicalAddress,若是PE文件,则是VirtualSize,表示节在内存中需要的大小。
VirtualAddress:表示节在内存中的RVA。
SizeOfRawData:节在文件中占用的字节数。
PointerToRawData:节在文件中的FOV(文件偏移)。
PointerToRelocations / NumberOfRelocations:obj文件中用来描述重定位信息,在PE文件中为0。
PointerToLinenumbers / NumberOfLinenumbers:PE文件中一般为0,具体调试信息在pdb文件中。
Characteristics节属性
Characteristics可选字段
| 宏定义 | 值 | 含义 |
|---|---|---|
| IMAGE_SCN_TYPE_REG | 0x00000000 | 保留字段 |
| IMAGE_SCN_TYPE_DSECT | 0x00000001 | 保留字段 |
| IMAGE_SCN_TYPE_NOLOAD | 0x00000002 | 保留字段,不加载该节 |
| IMAGE_SCN_TYPE_GROUP | 0x00000004 | 保留字段 |
| IMAGE_SCN_TYPE_NO_PAD | 0x00000008 | 保留字段 |
| IMAGE_SCN_TYPE_COPY | 0x00000010 | 保留字段 |
| IMAGE_SCN_CNT_CODE | 0x00000020 | 节包含代码(通常是 .text) |
| IMAGE_SCN_CNT_INITIALIZED_DATA | 0x00000040 | 节包含已初始化数据(如 .data) |
| IMAGE_SCN_CNT_UNINITIALIZED_DATA | 0x00000080 | 节包含未初始化数据(如 .bss) |
| IMAGE_SCN_LNK_OTHER | 0x00000100 | 保留字段 |
| IMAGE_SCN_LNK_INFO | 0x00000200 | 节包含注释或其他信息 |
| IMAGE_SCN_LNK_REMOVE | 0x00000800 | 节不会进入最终映像 |
| IMAGE_SCN_LNK_COMDAT | 0x00001000 | 节是 COMDAT(公共数据,链接器合并重复符号) |
| IMAGE_SCN_NO_DEFER_SPEC_EXC | 0x00004000 | 重置 TLB 中的推测异常处理位 |
| IMAGE_SCN_GPREL / IMAGE_SCN_MEM_FARDATA | 0x00008000 | 节内容可通过 GP(全局指针)访问,常见于 MIPS/IA-64 |
| IMAGE_SCN_MEM_PURGEABLE / IMAGE_SCN_MEM_16BIT | 0x00020000 | 保留字段 |
| IMAGE_SCN_MEM_LOCKED | 0x00040000 | 保留字段 |
| IMAGE_SCN_MEM_PRELOAD | 0x00080000 | 保留字段 |
| IMAGE_SCN_ALIGN_1BYTES | 0x00100000 | 1 字节对齐 |
| IMAGE_SCN_ALIGN_2BYTES | 0x00200000 | 2 字节对齐 |
| IMAGE_SCN_ALIGN_4BYTES | 0x00300000 | 4 字节对齐 |
| IMAGE_SCN_ALIGN_8BYTES | 0x00400000 | 8 字节对齐 |
| IMAGE_SCN_ALIGN_16BYTES | 0x00500000 | 16 字节对齐(默认) |
| IMAGE_SCN_ALIGN_32BYTES | 0x00600000 | 32 字节对齐 |
| IMAGE_SCN_ALIGN_64BYTES | 0x00700000 | 64 字节对齐 |
| IMAGE_SCN_ALIGN_128BYTES | 0x00800000 | 128 字节对齐 |
| IMAGE_SCN_ALIGN_256BYTES | 0x00900000 | 256 字节对齐 |
| IMAGE_SCN_ALIGN_512BYTES | 0x00A00000 | 512 字节对齐 |
| IMAGE_SCN_ALIGN_1024BYTES | 0x00B00000 | 1024 字节对齐 |
| IMAGE_SCN_ALIGN_2048BYTES | 0x00C00000 | 2048 字节对齐 |
| IMAGE_SCN_ALIGN_4096BYTES | 0x00D00000 | 4096 字节对齐 |
| IMAGE_SCN_ALIGN_8192BYTES | 0x00E00000 | 8192 字节对齐 |
| IMAGE_SCN_ALIGN_MASK | 0x00F00000 | 对齐掩码 |
| IMAGE_SCN_LNK_NRELOC_OVFL | 0x01000000 | 节包含扩展重定位 |
| IMAGE_SCN_MEM_DISCARDABLE | 0x02000000 | 节可被丢弃(如资源节加载后可释放) |
| IMAGE_SCN_MEM_NOT_CACHED | 0x04000000 | 节不可缓存 |
| IMAGE_SCN_MEM_NOT_PAGED | 0x08000000 | 节不可分页 |
| IMAGE_SCN_MEM_SHARED | 0x10000000 | 节可共享 |
| IMAGE_SCN_MEM_EXECUTE | 0x20000000 | 节可执行 |
| IMAGE_SCN_MEM_READ | 0x40000000 | 节可读 |
| IMAGE_SCN_MEM_WRITE | 0x80000000 | 节可写 |
| IMAGE_SCN_SCALE_INDEX | 0x00000001 | TLS 范围标志 |
DataDirectory
data directory是一个数组,存放了许多重要内容。此处较为重要的为导入表,导出表,重定位表。它们是PE加载器加载PE文件到内存时几乎必须的内容。其中,导入表指明了本PE文件使用了哪些其他PE文件的函数,导出表指明了其他PE文件可以使用本PE文件的哪些函数。重定位表指明了在不能加载到默认映像基地址时,该如何修正PE文件中的绝对偏移。
DIRECTORY_ENTRY_IMPORT
导入表记录了使用的外部函数。其每一项叫导入描述符IMPORT_DESCRIPTOR,每一个描述符都对应一个dll的所有函数。
描述符结构如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) } DUMMYUNIONNAME; DWORD TimeDateStamp; // 0 if not bound, // -1 if bound, and real date\time stamp // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) // O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders DWORD Name; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)} IMAGE_IMPORT_DESCRIPTOR;typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
OriginalFirstThunk:如果是0,则说明导入表结束。否则指向该描述符描述的dll的INT表(用以区分每个函数是通过名称还是通过序号导入的)
TimeDateStamp:0
ForwarderChain:转发引用标志,如果有,则为非零,表示INT表和IAT表第x个函数为第一个向前导出函数
Name:指向DLL名称的RVA
FirstThunk:指向IAT表(导入地址表),内容是每个函数地址
INT表和IAT表的结构相同,但是意义不同,其结构如下:
typedef struct _IMAGE_THUNK_DATA64 { union { ULONGLONG ForwarderString; // PBYTE ULONGLONG Function; // PDWORD ULONGLONG Ordinal; ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1;} IMAGE_THUNK_DATA64;typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64;ForwarderString:表示将该函数实现转发给另一个DLL。
Function:表示函数真实地址。
Ordinal:如果最高位是1,之后的位存放导出序号
AddressOfData:如果最高位为0,则是指向文件中保存函数名称的结构的RVA
AddressOfData结构如下:
typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; CHAR Name[1];} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;其中Hint为导出表序号,Name为名称。由于名称长度不固定,因此AddressOfData是RVA指针。
虽然它们的结构都是一个ULONGLONG,但是作为INT表,用来记录想要查询的函数的位置,只会是Ordinal和AddressOfData,而IAT表原值不重要,启动后,始终会覆盖为Function,即真实函数地址。具体的加载流程,即通过导入描述符设置真实地址的逻辑如下:
-
通过可选头得到导入描述符地址。
-
查看该导入描述符的Name字段得到dll名称
-
进入OriginalFirstThunk字段分析IMAGE_THUNK_DATA64结构数组
-
如果第一位为1,获取剩下位作为序号,寻找该DLL的导出表对应序号的函数地址,将该值填入FirstThunk字段中对应位置上
-
如果不为1,获取剩下位作为RVA寻找函数名称结构,先找该DLL导出表的根据Hint作为序号的函数,如果没找到,通过二分查找用名称找,最后将地址填入FirstThunk对应位置上
-
每次执行完这个操作,OriginalFirstThunk和FirstThunk指针同时+1,表示进行下一个函数的搜索,直到它们的结构为0
-
-
-
结束一个导入描述符后,指针+1,对下一个导入描述符做同样的操作,直到接结构全0
注意:运行时,所有使用DLL函数的地方,实际跳转均为IAT表对应位置,当IAT表填充为正确地址后,实际跳转才能被正常解析
DIRECTORY_ENTRY_EXPORT
导出表用于其他PE文件导入本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 from base of image DWORD AddressOfNames; // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;Characteristics:保留,全0
TimeDateStamp:时间戳
MajorVersion/MinorVersion:版本号
Name:指向该DLL名称的RVA
Base:起始序列号
NumberOfFunctions:AddressOfFunctions中元素总数
NumberOfNames:AddressOfNames中元素总数
AddressOfFunctions:EAT,函数地址RVA表
AddressOfNames:ENT,函数名称表
AddressOfNameOrdinals:EOT,序号索引表,以序号作为index,值为EAT的索引。
当其他PE文件想要寻找该PE文件的某个函数地址时:
-
通过文件头获取导出表地址
- 若是通过名称查询,找到AddressOfNames中该函数名称的序号,将该序号传入AddressOfNameOrdinals,取对应函数地址表的index,在AddressOfFunctions的对应index位获取地址
- 若是通过序号查询,让序号减去Base,得到AddressOfFunctions偏移,然后在对应位获取地址
-
此处获取的地址是RVA,需要加上ImageBase才是真实地址
DIRECTORY_ENTRY_BASERELOC
重定位表是为了防止不能在默认位置加载镜像而导致绝对地址报错的结构。它是块链,每个重定位块负责每个4KB中所有需要重定位的内容。
重定位块结构如下:
typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock;// WORD TypeOffset[1];} IMAGE_BASE_RELOCATION;typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;VirtualAddress:该块负责的页面的起始RVA
SizeOfBlock:本块的总大小。下一块的地址是该块起始地址加SizeOfBlock。该块负责的重定位项(每个需重定位的地址)是(SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2
TypeOffset:是重定位项数组,每位都是一个WORD类型的值
typedef struct _IMAGE_RELOC_ENTRY {//猜的结构,VS里面没找到 WORD Type : 4; WORD Offset : 12;} IMAGE_RELOC_ENTRY, *PIMAGE_RELOC_ENTRY;其中Type是对应等待修正的地址的类型,可用值如下:
| 宏定义名称 | 数值 | 适用架构 | 修复逻辑与说明 |
|---|---|---|---|
| ABSOLUTE | 0 | 所有 | 忽略。不做任何操作,通常用于块末尾的填充对齐。 |
| HIGH | 1 | 部分 (如 MIPS) | 高16位修复。将基址差值(Delta)的高16位加到目标位置。 |
| LOW | 2 | 部分 (如 MIPS) | 低16位修复。将基址差值的低16位加到目标位置。 |
| HIGHLOW | 3 | x86 (32位) | 32位全修复。32位程序最核心的类型,修复整个 32 位绝对地址。 |
| HIGHADJ | 4 | 特殊 (如 RISC) | 带进位的高位修复。修复高16位,但需考虑低16位的进位。占用两个条目空间。 |
| MIPS_JMPADDR | 5 | MIPS | 跳转地址修复。针对 MIPS 架构的 J 或 JAL 指令进行地址修正。 |
| ARM_MOV32 | 5 | ARM | MOV32 修复。修复 ARM 模式下由 MOV 指令对加载的 32 位常量。 |
| THUMB_MOV32 | 7 | ARM Thumb | Thumb MOV32 修复。针对 Thumb 指令集编码的 32 位立即数修复。 |
| IA64_IMM64 | 9 | IA64 (安腾) | 64位立即数修复。修复 IA64 架构指令束中的 64 位值。 |
| DIR64 | 10 | x64 (64位) | 64位全修复。64位程序(Win11/10主流)的核心类型,修复 8 字节绝对地址。 |
一般来说都是DIR64。
offset就是相对于本块VirtualAddress的偏移。指明了对应的需要重定位的地址。
重定位流程大致如下:
-
从文件头中找到重定位表首地址
-
取VirtualAddress
-
进入TypeOffset表项,对其中每一项,计算ImageBase+VirtualAddress+Offset作为目标地址。并查看Type,一般为DIR64,说明目标位置绝对地址长度为8字节。
-
将真实基址减去PE头中的基址作为差值,加到目标地址上作为重定位地址。
-
继续循环,直到循环次数为
(SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2说明该块修复完毕。
-
-
将当前重定位块首地址+SizeOfBlock,转到下一个重定位块,循环,直到结构全0
PE文件的基本加载流程
通过上面的结构可以看出,部分地址是FOV,部分地址是RVA。FOV是某个结构在文件中的偏移,RVA是某个结构在内存中基于ImageBase的偏移。PE文件的加载,就是将文件内容“映射”到虚拟内存,然后初始化重要结构,比如IAT,重定位表。
其中,映射的目的在于:文件一般以扇区对齐(FileAlignment),内存则是按页对齐,所有的属性都是页的属性,为了防止代码段,数据段放到一个页中引起属性冲突导致的安全性下降,必须要通过映射将节区分离。
段和节区在PE文件中是类似的东西。可以说几乎一样,节区是用于给编译器和链接器链接的。而段是用来给PE加载器执行时设置权限的。而页是实际的权限载体,加载器根据段的权限设置页的权限。
大致来说PE加载器执行程序的流程如下:(以下为简化步骤,只有极少代码可以仅通过该操作实现执行)
-
映射节区
-
修复IAT
-
执行重定位
-
跳转到入口点
可以尝试手动写一个PE加载器来实现这个流程,体会感觉。
LIEF库的使用
LIEF是支持多种语言的PE文件解析库,可以很方便地加载/修改PE文件内容。
具体API见:PE — LIEF Documentation
部分信息可能已经过时









