PE结构详解

PE结构详解

EXE和DLL文件之间的区别完全是语义上的,因为他们使用完全相同的PE格式。而唯一的区别就是用一个字段标识出这个文件是EXE还是DLL。

64位Windows只是对PE格式做了一些简单的修饰,新格式叫PE32+。并没有任何新的结构加进去,改变的只是简单的将32位字段扩展成64位。

PE格式定义的主要地方位于我们的头文件winnt.h,这个头文件中几乎能找到关于PE文件的所有定义
PE文件中的数据结构一般都有32位和64位之分,一般在名称上会表现出来:例如IMAGE_NT_HEADERS32 或 IMAGE_NT_HEADER64

我们的讲解主要是基于现在主流的32位PE格式来进行研究的,64位请依此类推,因为64位版本只是对32位做了一些简单的扩展,结构几乎是一摸一样的。


PE的基本概念
PE文件使用的是一个平面地址空间,所有代码和数据都被合并在一起,组成一个很大的结构。

文件的内容被分割为不同的区块,块中包含代码或数据。各个区块按页边界来对齐,区块没有大小限制,是一个连续的结构。

此外,每个块有自己在内存中的一套属性,比如说这个区块是否包含代码、是否只读或可读/写等。

认识PE文件不是作为单一内存映射文件被装入内存是很重要的。

Windows加载器(又称PE装载器)遍历PE文件并决定文件的哪一部分被映射,这种映射方式是将文件较高的偏移位置映射到较高的内存地址中。

当磁盘文件一旦被装入内存中,磁盘上的数据结构布局和内存中的数据结构布局是一致的。

这样如果知道在磁盘的数据结构中寻找一些内容,那么几乎都能在被装入到内存映射文件中找到相同的信息。

但数据之间的相对位置可能改变,其某项的偏移地址可能区别于原始的偏移位置,不管怎样,所有表现出来的信息都允许从磁盘文件偏移到内存偏移的转换。

基地址ImageBase:内存中的头地址,就是模块的句柄,通过HMODULE GetModuleHandle(LPCTSTR lpModuleName)函数可以获取

相对虚拟地址RVA:在内存中,每一个区块虚拟地址,相对于基地址的偏移(相减)

文件偏移地址:就是在PE文件在硬盘中,各个区块相对文件头的偏移


MS - DOS 头部
每个PE文件是以一个DOS程序开始的,有了它,一旦程序在DOS下执行,DOS才能识别出这是有效的执行体。

PE 文件的第一个字节起始于一个传统的MS-DOS 头部,被称作 IMAGE_DOS_HEADER。

MS-DOS头部占据了PE文件的头64个字节,描述它内容的结构如下:
//WINNT.H

typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE头部
USHORT e_magic; // 魔术数字
USHORT e_cblp; // 文件最后页的字节数
USHORT e_cp; // 文件页数
USHORT e_crlc; // 重定义元素个数
USHORT e_cparhdr; // 头部尺寸,以段落为单位
USHORT e_minalloc; // 所需的最小附加段
USHORT e_maxalloc; // 所需的最大附加段
USHORT e_ss; // 初始的SS值(相对偏移量)
USHORT e_sp; // 初始的SP值
USHORT e_csum; // 校验和
USHORT e_ip; // 初始的IP值
USHORT e_cs; // 初始的CS值(相对偏移量)
USHORT e_lfarlc; // 重分配表文件地址
USHORT e_ovno; // 覆盖号
USHORT e_res[4]; // 保留字
USHORT e_oemid; // OEM标识符(相对e_oeminfo)
USHORT e_oeminfo; // OEM信息
USHORT e_res2[10]; // 保留字
LONG e_lfanew; // 新exe头部的文件地址 DWORD双字,4个字节
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
第一个域e_magic,被称为魔术数字,它被用于表示一个MS-DOS兼容的文件类型。所有MS-DOS兼容的可执行文件都将这个值设为0x5A4D,表示ASCII字符MZ。MS-DOS头部之所以有的时候被称为MZ头部,就是这个缘故。还有许多其它的域对于MS-DOS操作系统来说都有用,但是对于Windows NT来说,这个结构中只有一个有用的域——最后一个域e_lfnew,一个4字节的文件偏移量,PE文件头部就是由它定位的。对于Windows NT的PE文件来说,PE文件头部是紧跟在MS-DOS头部和实模式程序残余之后的。


PE 文件头
PE 文件头(PE Header)紧挨着DOS stub
PE Header 是PE相关结构NT映像头(IMAGE_NT_HEADER)的简称,里边包含着许多PE装载器用到的重要字段。

执行体在支持PE文件结构的操作系统中执行时,PE装载器将从IMAGE_DOS_HEADER结构中的e_lfanew字段里找到PE Header的起始偏移量,加上基地址就得到PE文件头的指针。

PNTHeader = ImageBase + dosHeader -> e_lfanew

IMAGE_NT_HEADER 结构

typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; 04h //00004550h PE的标志
IMAGE_FILE_HEADER FileHeader;//一个结构,文件头的说明
IMAGE_OPTIONAL_HEADER OptionalHeader;//一个结构,文件头的说明
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;

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;

如何换算RVA和文件偏移呢?
当处理PE文件时候,任何的RVA必须经过到文件偏移的换算,才
能用来定位并访问文件中的数据,但换算却无法用一个简单的公式来
完成,事实上,唯一可用的方法就是最土最笨的方法:
步骤一:循环扫描区块表得出每个区块在内存中的起始RVA(根据
IMAGE_SECTION_HEADER 中的VirtualAddress字段),并根据区
块的大小(根据IMAGE_SECTION_HEADER 中的SizeOfRawData
字段)算出区块的结束RVA(两者相加即可),最后判断目标RVA是
否落在该区块内。
步骤二:通过步骤一定位了目标RVA处于具体的某个区块中后,那
么用目标RVA减去该区块的起始RVA,这样就能得到目标RVA
相对于起始地址的偏移量RVA2.
步骤三:在区块表中获取该区块在文件中所处的偏移地址(根据
IMAGE_SECTION_HEADER 中的PointerToRawData字段), 将这
个偏移值加上步骤二得到的RVA2值,就得到了真正的文件偏移地
址。








原文地址:https://www.cnblogs.com/poli/p/4959224.html