《C++黑客编程解密》05 - PE文件结构
PE 文件结构
前三个部分是数据组织结构部分,节表数据才是PE真正的数据部分
- DOS 头
- PE 头
保存windows系统加载可执行文件的重要信息。PE头部有IMAGE_NT_HEADERS
定义,其包含了IMAGE_NT_SIGNATRUE
IMAGE_FILE_HEADER
IMAGE_OPTIONAL_HEADER
三部分。PE头的位置固定不变,其有DOS头某个字段给出。 - 节表
程序的组织按照各属性的不同被保存在不同节中,PE头部之后就是一个数组结构的节表。结构体位IMAGE_SECTION_HEADER
文件有几个节就有几个结构体组成的数组。节表保存了各个节的属性、文件位置、内存位置等相关信息。 - 节表数据
DOS 头部 IMAGE_DOS_HEADER
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number DOS可执行文件的标识符,字符“MZ”: #define IMAGE_DOS_SIGNATURE 0x5A4D // MZ
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 PE头的起始位置
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
在pe头和dos结构体之间有一个dos存根,这是一个没有用的DOS程序。可以将这一段内容删除然后将pe头前移,也可以在此处填充其他数据
PE头部有 IMAGE_NT_HEADERS
这是一个宏,具体分为32位和64位两个版本:
#ifdef _WIN64
typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS;
#else
typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS;
#endif
32位的具体实现:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE 标识符,用于判断是否是pe文件,就是: #define IMAGE_NT_SIGNATURE 0x00004550 // PE00
IMAGE_FILE_HEADER FileHeader; // 文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
IMAGE_FILE_HEADER
是 IMAGE_NT_HEADERS 中的一个结构体,紧跟在PE标识符后面,大小位20字节,起始位置取决于PE头起始位置,PE头位置决定于 IMAGE_DOS_HEADER
中 e_lfanew
的位置。除了 IMAGE_DOS_HEADER 以外,其他头部起始位置都依赖PE头部起始位置。
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 目标CPU
WORD NumberOfSections; // PE文件节区个数
DWORD TimeDateStamp; // 文件创建时间戳
DWORD PointerToSymbolTable; // 很少使用
DWORD NumberOfSymbols; // 很少使用
WORD SizeOfOptionalHeader; // 指定 IMAGE_OPTIONAL_HEADER 结构的大小,可选结构体的大小会变化不能使用sizeof,获取大小时要使用该字段
WORD Characteristics; // 指定文件类型
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
#define IMAGE_SIZEOF_FILE_HEADER 20
IMAGE_OPTIONAL_HEADER
这个结构体必须存在,该头的数据目录数组中有的目录项可有可无。“可选头”不可选,内部的数据目录项可选。其紧挨着文件头。其起始位置就是PE标识开始位置加上前一个结构体的长度20字节,结束位置就是节名称“.txt”的前一个字节。
可选头是对文件头的一个补充,文件头描述文件相关信息,可选头管理PE文件被装载时需要的信息。
#ifdef _WIN64
typedef IMAGE_OPTIONAL_HEADER64 IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER64 PIMAGE_OPTIONAL_HEADER;
#define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR64_MAGIC
#else
typedef IMAGE_OPTIONAL_HEADER32 IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER32 PIMAGE_OPTIONAL_HEADER;
#define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR32_MAGIC
#endif
32位结构体如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
/*
* IMAGE_NT_OPTIONAL_HDR_MAGIC 0x10b 可执行文件
* IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107 ROM 文件
*/
WORD Magic; // 文件状态
BYTE MajorLinkerVersion; // 主连结版本号
BYTE MinorLinkerVersion; // 次连接版本号
DWORD SizeOfCode; // 代码节大小,偶尔有多个代码节则为所有代码节大小的和
DWORD SizeOfInitializedData; // 已初始化数据块的大小
DWORD SizeOfUninitializedData; // 未初始化数据块的大小
DWORD AddressOfEntryPoint; // 程序入口地址,相对虚拟地址 EP(EntryPoint),指向第一条要执行的代码。加壳后改变该字段的值。该字段指向的不是main函数地址,而是运行库的启动代码地址
DWORD BaseOfCode; // 代码段起始相对虚拟地址
DWORD BaseOfData; // 数据段起始相对地址
//
// NT additional fields.
//
DWORD ImageBase; // 文件被装入内存后首选建议装载地址
DWORD SectionAlignment; // 节表装入内存后的对齐值,Win32下通常为0x1000 就是4KB
DWORD FileAlignment; // 页表在文件中对齐值,为0x1000与内存一致方便加载,为0x200就是512字节节省空间
WORD MajorOperatingSystemVersion; // 最低os主版本号
WORD MinorOperatingSystemVersion; // 最低os次版本号
WORD MajorImageVersion; // 可执行文件主版本号
WORD MinorImageVersion; // 可执行文件次版本号
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue; // 保留
DWORD SizeOfImage; // 装入内存后总大小,内存对齐
DWORD SizeOfHeaders; // DOS头、PE头、节表的总大小
DWORD CheckSum; // 校验和值,exe通常为0,sys文件必须有
WORD Subsystem; // 可执行文件的子系统类型
WORD DllCharacteristics; // DLL文件属性,一般为 0
DWORD SizeOfStackReserve; // 为线程保留的栈大小
DWORD SizeOfStackCommit; // 为线程提交的栈大小
DWORD SizeOfHeapReserve; // 为线程保留的堆大小
DWORD SizeOfHeapCommit; // 为线程提交的堆大小
DWORD LoaderFlags; // 过时
DWORD NumberOfRvaAndSizes; // 数据目录项个数 #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录表,由NumberOfRvaAndSizes决定个数。包含输入表、输出表、资源、重定位等数据目录项的相对虚拟地址
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 目录项的相对虚拟地址起始值
DWORD Size; // 该目录项的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
在数据目录项中并不是所有目录项都有值,很多目录项值为0,所以称为可选。
节表IMAGE_SECTION_HEADER
节表位置在 IMAGE_OPTIONAL_HEADER 后面,每个结构体都存放在可执行文件被映射到内存后所在的位置信息,节的个数由 IMAGE_FILE_HEADER 中的 NumberOfSections 给出。一个节表项占40个字节。
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // ASCII码格式保存的节表项名称,超过8字节自动截断。如“.txet”
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;
#define IMAGE_SIZEOF_SECTION_HEADER 40
PE 结构的3种地址
- VA (虚拟地址):PE文件映射到内存后的地址
- RAV (相对虚拟地址):内存地址相对于映射基地址的偏移地址
- FileOffset (文件偏移地址):相对于PE文件在磁盘上的文件开头的偏移地址
PE 文件在磁盘和在内存结构是一样的。不同的是,在磁盘上,文件是按照 IMAGE_OPTIONAL_HEADER 的 FileAlignment 对齐的;内存中,按照 IMAGE_OPTIONAL_HEADER 的 SectionAlignment 对齐的。FileAlignment 以磁盘上扇区为单位,最小512字节;SectionAlignment 是以内存分页为单位对齐,通常4K。一般情况二者相同,这样磁盘文件和内存映像结构完全一样。当二者值不同时存在一定差异。主要区别是为了对齐填充了许多0。
EXE文件在内存中的起始地址为 IMAGE_OPTIONAL_HEADER 的 ImageBase 字段,DLL文件的映射地址不固定。
3种地址的转换