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