PE文件结构
简介:
PE(PortableExecutable),即可移植的执行体。所有的Windows(包括Win9x、WinNT、Win CE ····)下的可执行文件(包括EXE、DLL、SYS、OCX、COM··--·)均使用PE文件结构,这些使用PE文件结构的可执行文件也成为PE文件。
作为一个普通的程序员也许没有必要掌握 PE文件结构,因为普通的程序员大多都是开发服务性、决策性、的软件。但是对于学习黑客编程,学习安全编程的程序员来说,那么掌握PE 文件结构的知识就非常重要了。因此,我们要进行对PE文件结构地学习。
PE结构有一个整体上的认识,要知道PE结构包含的结构体有DOS头、PE头(PE标识、文件头、可选头)、节表、节表数据等。

常用结构体介绍:
DOS头部其实是一个DOS头部,也可以叫MZ头,该部分用于DOS下加载可执行程序,是用IMAGE_DOS_HEADER来定义的
DOS存根是一段简单的程序,主要用于输出"This program cannot be run in DOS mode"类似的字符串。
PE头部是保存Windows系统加载可执行文件的重要信息。
节表:是PE文件对后续节的描述。
节表的数据:每个节实际上是一个容器,可以包含代码、数据等等,每个节可以有独立的内存权限,比如代码节默认有读/执行权限,节的名字和数量可以自己定义。
DOS头部详解IMAGE_DOS_HEADER:
DOS部分主要是为了兼容以前的DOS系统,DOS部分可以分为DOS MZ文件头(IMAGE_DOS_HEADER)和DOS块(DOS Stub)组成,PE文件的第一个字节位于一个传统的MS-DOS头部,称作IMAGE_DOS_HEADER,占用64个字节 ,其结构如下:
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;        // 一个WORD类型,值是一个常数0x4D5A,用文本编辑器查看该值位‘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;                    // 指向PE文件头的地址
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;DOS部分我们主要的是e_magic成员和e_lfanew成员,前者是标识PE指纹的一部分,后者则是寻找PE文件头的部分,除了这两个成员,其他成员全部用0填充都不会影响程序正常运行,所以我们不需要过多的对其他部分深究,DOS部分在16进制编辑器中看就是下图的部分:

DOS存根:我们可以看到e_lfanew指向PE文件头,我们可以通过它来寻找PE文件头,而DOS块的部分自然就是PE文件头和DOS MZ文件头中间的部分,这部分是由链接器所写入的,可以随意进行修改,并不影响程序的运行:
PE头部详解IMAGE_NT_HEADERS:
PE文件头由PE文件头标志,标准PE头,扩展PE头三部分组成。PE头部是真正用来装载Win32程序的头部,PE文件头标志是50 45 00 00,也就是PE,我们从结构体的角度看一下PE文件头的详细信息:
64位和32位结构体不同是IMAGE_OPTIONAL_HEADER的字节不同
typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature; 						//PE文件头标志 => 4字节
    IMAGE_FILE_HEADER FileHeader; 			//文件头标准PE头 => 20字节
    IMAGE_OPTIONAL_HEADER32 OptionalHeader; //扩展PE头 => 32位下224字节(0xE0) 64位下240字节(0xF0)
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;PE标识Signature:
PE标识占4个字节,winhex打开如下所示4550,ANSI码为PE

标准PE头IMAGE_FILE_HEADER:
标准PE头结构如下,有20个字节,主要存储运行(平台、节的数量、时间、拓展头大小、文件类型)我们可以从PE文件头标志后20个字节找到它,如下图:
typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine; 				//WORD类型占用2个字节,可以运行在什么平台上 任意:0 ,Intel 386以及后续:14C x64:8664
    WORD    NumberOfSections; 		//WORD类型占用2个字节,节的数量
    DWORD   TimeDateStamp; 			//DWORD类型占用4个字节,表示文件何时被创建的编译器填写的时间戳
    DWORD   PointerToSymbolTable;   //调试相关
    DWORD   NumberOfSymbols; 		//调试相关
    WORD    SizeOfOptionalHeader;   //WORD类型占用2个字节,标识扩展PE头大小
    WORD    Characteristics;        //WORD类型占用2个字节,文件的类型,该字段取值如下图
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

扩展PE头IMAGE_OPTIONAL_HEADER:
IMAGE_OPTIONAL_HEADER被称为"可选头"。虽然被称为可选头,但是必须存在的一个头,之所以称作为"可选头"认为是在该头的数据目录数组中,有的数据目录项(后面会讲到)是可有可无的,这部分内容是可选的,因此成为可选头。
可选头紧挨着文件头,例如:文件头的结束位置为0x0000011F,那么可选头起始位置为0x00000120,
在32位和64位系统上大小是不同的,在32位系统上有224个字节(64位系统上有240个字节 0xF0)

32位结构如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //
    WORD    Magic;						//指定文件状态的类型 PE32: 10B PE64: 20B
    BYTE    MajorLinkerVersion;			//主链接版本号
    BYTE    MinorLinkerVersion;			//次链接版本号
    DWORD   SizeOfCode;					//所有含有代码的区块的大小 编译器填入 没用(可改)
    DWORD   SizeOfInitializedData;		//所有初始化数据区块的大小 编译器填入 没用(可改)
    DWORD   SizeOfUninitializedData;	//所有含未初始化数据区块的大小 编译器填入 没用(可改)
    DWORD   AddressOfEntryPoint;		//程序入口RVA
    DWORD   BaseOfCode;					//代码区块起始RVA
    DWORD   BaseOfData;					//数据区块起始RVA
    //
    // NT additional fields.
    //
    DWORD   ImageBase;						//内存镜像基址(程序默认载入基地址)
    DWORD   SectionAlignment; 				//内存中对齐大小
    DWORD   FileAlignment; 					//文件中对齐大小(提高程序运行效率)
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;					//内存中整个PE文件的映射的尺寸,可比实际值大,必须是SectionAlignment的整数倍
    DWORD   SizeOfHeaders; 					//所有的头加上节表文件对齐之后的值
    DWORD   CheckSum;						//映像校验和,一些系统.dll文件有要求,判断是否被修改
    WORD    Subsystem;						
    WORD    DllCharacteristics;				//文件特性,不是针对DLL文件的,16进制转换2进制可以根据属性对应的表格得到相应的属性
    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;64位结构如下:
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;
这里介绍32位的结构,因为32位结构覆盖完了64位所有属性:
(1)Magic:该成员变量指定了文件的状态类型
(2)MajorLinkerVersion:主链接版本号
(3)MinorLinkerVersion:次链接版本号
(4)SizeOfCode:代码节的大小,如果有多个代码节的话,就是它们的总和。该处是指所有包含可执行属性的节的大小。
(5)SizeOflnitializedData: 已初始化数据块的大小。
(6)SizeOfUninitializedData:未初始化数据块的大小。
(7)AddressOfEntryPoint:程序执行的入口地址,该地址是一个相对虚拟地址,该地址简称EP。如果加壳后,找到了该地址,就被称作了OEP。该地址指向的不是main(),也不是WinMain()的地址该地址指向了运行库代码的地址。对于DLL这个值的意义不大,因为DLL甚至可以没有DIIMain()函数,没有DIIMain()是无法捕获DLL的4个消息的
(8)BaseOfCode:代码段的起始相对虚拟地址。
(9)BaseOfData:数据段的起始相对虚拟地址。
(10)ImageBase:文件被装入内存后的首选建议装载地址。对于EXE文件来说,通常情况下该地址就是装载地址:对于DLL 来说,可能就不是其装入内存后的地址了。程序加载到内存的起始地址
(11)SectionAlignment:节被装入内存后的对齐值。节被映射到内存中需要对齐的单位。通常情况下0x1000,也就是4KB大小。Windows操作系统的内存分页一般为4KB
(12)FileAlignment:节在文件中的对齐值。节在磁盘上是对齐单位。
(13)MajorOperatingSystemVersion:要求最低操作系统的主版本号。
(14)MinorOperatingSystemVersion:要求最低操作系统的次版本号。
(15)MajorImageVersion:可执行文件的主版本号。
(16)MinorImageVersion:可执行文件的次版本号
(I7)MajorSussystemVersion:要求最低子系统的主版本号。
(18)MinorSubsystemVersion:要求最低子系统的次版本号。
(19)Win32VersionValue:该成员变量是被保留的。
(20)SizeOflmage:可执行文件装入内存后的总大小。该大小按内存对齐方式对齐
(21)SizeOfHeaders:PE头的大小,这个PE 头泛指DOS头、PE头、节表的总和大小。
(22)CheckSum:校验和对于EXE文件通常为0对SYS文件则必须有一个校验和
(23)Subsystem:可执行文件的子系统类型。
(24)DIICharacteristics:指定DLL文件的属性。该值大部分时候为0。
(25)SizefStackReserve:为线程保留的栈大小
(26)SizeOfStackCommit:为线程已经提交的找大小
(27)SizeOfHeapReserve:为线程保留的堆大小
(28)SizeOfHeapCommit:为线程已经提交的堆大小
(29)LoaderFlags:被废弃的成员值。MSDN上的原话为“This member is obsolete”。但是该值在某些情况下还是会被用到的,比如针对旧版OD时,修改该值会起到反调试的作用
(30)NumberOfRvaAndSizes:数据目录项的个数。
(31)DataDirectory:数据目录表,由NumberOfRvaAndSize个IMAGE_DATA_DIRECTORY结构体组成。该数组中包含了输入表、输出表、资源等数据的RVA。IMAGE_DATA_DIRECTORY 的定义如下
typedef struct _IMAGE_DATA_DIRECTORY ( DWORD VirtualAddress; DWORD Size; ) IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;
该结构体的第一个变量为该目录的相对虚拟地址的起始值,第二个是该目录的长度。数据目录中的部分成员在数组中的索引如图所示。
节表详解IMAGE_SECTION_HEADER:
节表的位置在IMAGE_OPTIONAL_HEADER的后面,节表中的每个IMAGE_SECTION_HEADER中都存放着可执行文件被映射到内存中所在位置的信息。其中节的个数由IMAGE_FILE_HEADER中的NumberOfSections给出。节表的结构如下,40个字节: DWORD 4个字节 WORD占2个字节
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME]; //ASCII字符串 可自定义 只截取8个字节
    union {								   //该节在没有对齐之前的真实尺寸,该值可以不准确
            DWORD   PhysicalAddress;  //文件地址
            DWORD   VirtualSize;  //加载到内存中的节的总大小   
    } Misc;
    DWORD   VirtualAddress; 			   //加载到内存中的部分的第一个字节的地址,相对于映像基地址RVA
    DWORD   SizeOfRawData;				   //节在文件中对齐的大小
    DWORD   PointerToRawData;			   //节区在文件中的偏移
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;			   //节的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;(1)Name: 该成员变量保存节的名称,节的名称用ASCII编码来保存。节名的长度IMAGE_SIZEOF_SHORT_NAME,这是一个宏,该宏的定义如下:
#define IMAGE_SIZEOF_SHORT_NAME 8
节名的长度为8个字节,多余的字节会被自动截断。通常情况下节名以“.”为开始,当然这是编译器的习惯。我们看一下图的前8个字节的内容为“2E74657874000000”其对应ASCII字符为“text
(2)VirtualSize: 区段装载到内存中的RVA,该值表示节区的大小,是在对齐处理前的区块的大小
(3)VirtualAddress: 该节区载入到内存后的相对虚拟地址。这个地址是按内存进行对齐的,是sectionAlignment的整数倍
(4)SizeOfRawData:该节区在磁盘上所占用的空间大小,该值通常是对齐后的值该字段包括经过FileAlignment调整后块的长度。例如FileAlignment的大小为200h,如果VirtualSize中的块长度为19Ah个字节,这一块保存的长度为200h个字节,用0进行填充。
(5)PointerToRawData:该节区在磁盘文件上的偏移值。
(6)PointerToRelocations:在exe文件中没用意义,在obj文件中,表示本块重定位信息的偏移量。
(7)PointerToLinenumbers:行号表在文件中的偏移量。这是文件的调试信息。
(8)NumberOfRelocations:在exe中无意义。
(9)NumberOfLinenumbers:该块在行号表中的行号数目
(10)Characteristics:节区属性。该属性的值如图所示。


地址转换器:
RVA和FOA的转换:

RVA :相对虚拟地址(Relative Virtual Address),PE 文件中的各种数据结构中涉及地址的字段大部分都是以 RVA 表示的。
VA是当PE 文件被装载到内存中后,某个数据位置相对于文件头的偏移量。实际的内存地址
FOA (文件偏移地址):PE文件储存在磁盘上时, 某个数据的位置相对于文件头的偏移量,称为文件偏移地址(File Offset)或物理地址(RAW Offset)文件偏移地址从PE文件的第一个字节开始计 数,起始值为0。用十六进制工具 (例如WinHex、C32等)打开文件 所显示的地址就是文件偏移地址。
RVA = 内存地址(VA) - ImageBase
RVA 转换为 FOA:
某数据的文件偏移 = 该数据所在节的起始文件偏移 + (某数据的RVA –该数据所在节的起始RVA)
PointerToRawData: 该节区在磁盘文件上的偏移值 VitualAddress: 该节区载入到内存后的相对虚拟地址 Misc.VirtualSize: 表示节的大小,是在对齐处理前的区块的大小 SizeOfHeaders: PE头的大小,这个PE 头泛指DOS头、PE头、节表的总和大小 SectionAlignment:加载到内存中的节的对齐方式,以字节为单位。此值必须大于或等于 FileAlignment成员。默认值是系统的页面大小。 FileAlignment:图像文件中各个部分的原始数据的对齐方式,以字节为单位。该值应该是 2 的幂,介于 512 和 64K(含)之间。默认值 512。如果SectionAlignment成员小于系统页面大小,则该成员必须与 SectionAlignment相同。
第一种情况:文件对齐跟内存对齐一样的情况,那么这样就可以直接去找 RVA的地址了 这个地址也就是FOA 第二种情况:文件对齐和内存对齐不一样的情况 1.如果RVA属于文件头部(DOS头 + PE头 + 节表),头部大小是文件对齐大小的整数倍! 那么不需要进行计算了,因为DOS头和PE头和节表在文件中和在内存中展开都是一样的,直接从开始位置寻 找到RVA个字节即可,就是找RVA,也就是FOA(文件偏移地址)pOptionalHeader->SizeOfHeaders 2.判断 RVA 位于哪个节 RVA >= 节.VirtualAddress RVA <= 节.VirtualAddress + 当前内存对齐后的大小(节.Misc.VirtualSize) 差值 = RVA - 节.VirtualAddress 3.FOA = 节.PointerToRawData + 差值
RVA转换FOA例子:
创建一个a64.exe变量:
#include <iostream>
#include <stdlib.h>
int Test = 0x12345678;
int main(int argc, char* argv[])
{
    printf("全局变量地址 = %X \r\n", &Test);
    printf("全局变量值 = %X \r\n", Test);
    getchar();
}输出的值是VA(内存地址)

VA 4001C000
Imagebase 40000000
RVA = VA -ImageBase = 1C000
1.判断RVA是否在PE文件头中
001FF <1C000 不在PE文件头中

2.判断属于哪个节:
.data.VirtualAddress >=1C000 且 1C000<1C200(.data.VirtualAddress+.data.SizeOfRawData) 所以属于这个data节区中
3.FOA = 节.PointerToRawData + 差值(RVA - 节.VirtualAddress)
=B200+1C000-1C000=B200
下图就找到了对应的值:

在winhex中修改为78 56 44 12运行a64.exe成功

代码:
VA->Offset
void CPEmfcDlg::OnBnClickedButtoncalc() { //获取输入的VA DWORD dwRva; CString strRVA; GetDlgItem(IDC_EDIT_VAshow)->GetWindowText(strRVA); //字符串转换16进制 _stscanf_s(strRVA.GetBuffer(), _T("%x"), &dwRva); CString str; DWORD offset =VA2Offset(dwRva, m_pNtHdr, m_pSecHdr); str.Format("%08X", offset); SetDlgItemText(IDC_EDIT_offsetshow, str); //SetDlgItemText(IDC_EDIT_quduan, (LPCTSTR)m_pSecHdr[nInNum].Name); } DWORD VA2Offset(DWORD dwVa, PIMAGE_NT_HEADERS m_pNtHdr, PIMAGE_SECTION_HEADER m_pSecHdr) { //imagebase文件基地址 DWORD dwImageBase = m_pNtHdr->OptionalHeader.ImageBase; //文件对齐和内存对齐相等且不在文件头中 if (m_pNtHdr->OptionalHeader.FileAlignment == m_pNtHdr->OptionalHeader.SectionAlignment || dwVa <= m_pNtHdr->OptionalHeader.SizeOfHeaders) { return dwVa; } else { for (int nInNum = 0; nInNum < m_pNtHdr->FileHeader.NumberOfSections; nInNum++) { if (dwVa >= dwImageBase + m_pSecHdr[nInNum].VirtualAddress && dwVa <= dwImageBase + m_pSecHdr[nInNum].VirtualAddress + m_pSecHdr[nInNum].Misc.VirtualSize) { return m_pSecHdr[nInNum].PointerToRawData + dwVa - m_pSecHdr[nInNum].VirtualAddress - m_pNtHdr->OptionalHeader.ImageBase; } } } }saaaa
小结:
本小节主要介绍了pe文件的文件结构(除了数据目录表,后篇会讲解数据目录表的作用),为主要是学习pe文件的笔记,比较基础。
本文作者:xiaoshuaishuai, 转载请注明来自FreeBuf.COM
