PE结构
一览图
-
PE磁盘文件与内存镜像结构图
DOS/NT头结构
DOS头结构
-
PE文件中的DOS部分由MZ格式的文件头和可执行代码部分组成,可执行代码被称为DOS块(DOS stub),MZ格式的文件头由
IMAGE_DOS_HEADER
结构定义,在C语言头文件winnt.h
中有对这个DOS结构详细定义,如下所示typedef struct _IMAGE_DOS_HEADER { WORD e_magic; // DOS的头部 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; // 指向了PE文件的开头(重要) } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
在DOS文件头中,第一个字段
e_magic
被定义为MZ
,标志着DOS文件的开头部分,最后一个字段e_lfanew
则指明了PE文件的开头位置,现在来说除了第一个字段和最后一个字段有些用处,其他字段几乎已经废弃了DOS头的大小是固定的,其大小为
0x3Ch
,以winXP
的notepad.exe
为例子解析如下
PE头结构
- 从DOS文件头的位置向下偏移
e_lfanew
字段的值的位置,就是真正的PE文件头的位置,该文件头是由IMAGE_NT_HEADERS
结构定义的,定义结构如下
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE文件标识字符
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
根据DOS解析的例子,在0x3Ch
开始的DWORD
中存储着e_lfanew
字段的值,其值指明了PE头相对于文件的偏移,用于定位PE文件
PE文件头的第一个DWORD
是一个标志,默认情况下它被定义为00004550h
也就是P,E两个字符另外加上两个零,而大部分的文件属性由标志后面的IMAGE_FILE_HEADER
和IMAGE_OPTIONAL_HEADER32
结构来定义
跟进IMAGE_FILE_HEADER
IMAGE_FILE_HEADER
的结构定义如下
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 运行平台
WORD NumberOfSections; // 文件的节数目
DWORD TimeDateStamp; // 文件创建日期和时间
DWORD PointerToSymbolTable; // 指向符号表(用于调试)
DWORD NumberOfSymbols; // 符号表中的符号数量
WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HANDLER32结构的长度
WORD Characteristics; // 文件的属性 exe=010fh dll=210eh
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
跟进IMAGE_OPTIONAL_HEADER32
IMAGE_OPTIONAL_HEADER32
的结构定义如下
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion; // 连接器版本
BYTE MinorLinkerVersion;
DWORD SizeOfCode; // 所有包含代码节的总大小
DWORD SizeOfInitializedData; // 所有已初始化数据的节总大小
DWORD SizeOfUninitializedData; // 所有未初始化数据的节总大小
DWORD AddressOfEntryPoint; // 程序执行入口RVA
DWORD BaseOfCode; // 代码节的起始RVA
DWORD BaseOfData; // 数据节的起始RVA
DWORD ImageBase; // 程序镜像基地址
DWORD SectionAlignment; // 内存中节的对其粒度
DWORD FileAlignment; // 文件中节的对其粒度
WORD MajorOperatingSystemVersion; // 操作系统主版本号
WORD MinorOperatingSystemVersion; // 操作系统副版本号
WORD MajorImageVersion; // 可运行于操作系统的最小版本号
WORD MinorImageVersion;
WORD MajorSubsystemVersion; // 可运行于操作系统的最小子版本号
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; // 内存中整个PE映像尺寸
DWORD SizeOfHeaders; // 所有头加节表的大小
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve; // 初始化时堆栈大小
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; // 数据目录的结构数量
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
IMAGE_DATA_DIRECTORY
数据目录列表,它由16
个相同的IMAGE_DATA_DIRECTORY
结构组成,这16
个数据目录结构定义很简单仅仅指出了某种数据的位置和长度,定义如下
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 数据起始RVA
DWORD Size; // 数据块的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
可执行文件不同阶段的存放方式
在执行一个可执行文件时,首先将文件从磁盘原样Copy到内存中,这段存储着和磁盘相同数据的内存称作
FileBuffer
。只是简单的Copy到内存中并不能够执行这个文件,想要运行它,我们还需要对它进行”拉伸“,磁盘存储的方式和运行时的存储方式的对齐大小可能不一样,在磁盘存放的对齐方式可能导致基准地址+偏移地址
的方式并不能正确的找到地址(类似编译器在生成可执行文件时会对其进行压缩,压缩后还以运行时的寻址方式显然不能正确寻址),所以需要通过“拉伸”使文件恢复到可以执行的状态
在磁盘和FileBuffer
时其值完全相同
大致结构如下
在ImageBuffer的存放方式
- 存放拉伸后的那段内存称为
ImageBuffer
- 改变
FileBuffer
的对齐大小,再存入内存中 - 对于起始地址,
ImageBuffer
的起始地址称作ImageBase