PE文件结构详解
合集下载
相关主题
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,事 实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是任何扩展名。那Windows是怎么区分可执行 文件和非可执行文件的呢?我们调用LoadLibrary传递了一个文件名,系统是如何判断这个文件是一个合法 的动态库呢?这就涉及到PE文件结构了。 PE文件的结构一般来说如下图所示:从起始位置开始依次是DOS头,NT头,节表以及具体的节。
NumberOfSections:该PE文件中有多少个节,也就是节表中的项数。 TimeDateStamp:PE文件的创建时间,一般有连接器填写。 PointerToSymbolTable:COFF文件符号表在文件中的偏移。 NumberOfSymbols:符号表的数量。 SizeOfOptionalHeader:紧随其后的可选头的大小。 Characteristics:可执行文件的属性,可以是下面这些值按位相或。
这里写图片描述
这种表示方式叫做虚拟地址(VA)。也许有人要问,既然有VA这么简单的表示方式为什么还要有前面的 RVA呢?因为虽然PE文件为自己指定加载的基地址,但是windows有茫茫多的DLL,而且每个软件也有自己 的DLL,如果指定的地址已经被别的DLL占了怎么办?如果PE文件无法加载到预期的地址,那么系统会帮 他重新选择一个合适的基地址将他加载到此处,这时原有的VA就全部失效了,NT头保存了PE文件加载所需 的信息,在不知道PE会加载到哪个基地址之前,VA是无效的,所以在PE文件头中大部分是使用RVA来表示 地址的,而在代码中是用VA表示全局变量和函数地址的。那又有人要问了,既然加载基址变了以后VA都失 效了,那存在于代码中的那些VA怎么办呢?答案是:重定位。系统有自己的办法修正这些值,到后续重定 位表的文章中会详细描述。既然有重定位,为什么NT头不能依靠重定位采用VA表示地址呢(十万个为什 么)?因为不是所有的PE都有重定位,早期的EXE就是没有重定位的。我们都知道PE文件可以导出函数让 其他的PE文件使用,也可以从其他PE文件导入函数,这些是如何做到的?PE文件通过导出表指明自己导 出那些函数,通过导入表指明需要从哪些模块导入哪些函数。导入和导出表的具体结构会在单独的文章中 详细解释。
第2页 共13页
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
// DOS .EXE header // Magic number // Bytes on last page of file // Pages in file // Relocations // Size of header in paragraphs // Minimum extra paragraphs needed // Maximum extra paragraphs needed // Initial (relative) SS value // Initial SP value // Checksum // Initial IP value
0x0168 // MIPS little‐endian 0x0169 // MIPS little‐endian WCE v2 0x0184 // Alpha_AXP 0x01a2 // SH3 little‐endian 0x01a3 0x01a4 // SH3E little‐endian 0x01a6 // SH4 little‐endian 0x01a8 // SH5 0x01c0 // ARM Little‐Endian 0x01c2 0x01d3 0x01F0 // IBM PowerPC Little‐Endian 0x01f1 0x0200 // Intel 64 0x0266 // MIPS 0x0284 // ALPHA64 0x0366 // MIPS 0x0466 // MIPS IMAGE_FILE_MACHINE_ALPHA64 0x0520 // Infineon 0x0CEF 0x0EBC // EFI Byte Code 0x8664 // AMD64 (K8) 0x9041 // M32R little‐endian 0xC0EE
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
我们只需要关注两个域:
e_magic:一个WORD类型,值是一个常数0x4D5A,用文本编辑器查看该值为‘MZ’,可执行文件必须都是 'MZ'开头。
e_lfanew:为32位可执行文件扩展的域,用来表示DOS头之后的NT头相对文件起始地址的偏移。
二、NT头
顺着DOS头中的e_lfanew,我们很容易可以找到NT头,这个才是32位PE文件中最有用的头,定义如下:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
第1页 共13页
然而CPU的某些指令是需要使用绝对地址的,比如取全局变量的地址,传递函数的地址编译以后的汇编指 令中肯定需要用到绝对地址而不是相对映象头的偏移,因此PE文件会建议操作系统将其加载到某个内存地 址(这个叫基地址),编译器便根据这个地址求出代码中一些全局变量和函数的地址,并将这些地址用到 对应的指令中。例如在IDA里看上去是这个样子:
一、DOS头
DOS头的作用是兼容MS-DOS操作系统中的可执行文件,对于32位PE文件来说,DOS所起的作用就是显示 一行文字,提示用户:我需要在32位windows上才可以运行。我认为这是个善意的玩笑,因为他并不像显示 的那样不能运行,其实已经运行了,只是在DOS上没有干用户希望看到的工作而已,好吧,我承认这不是 重点。但是,至少我们看一下这个头是如何定义的:
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
每个域的具体含义如下: Machine:该文件的运行平台,是x86、x64还是I64fine IMAGE_FILE_MACHINE_UNKNOWN #define IMAGE_FILE_MACHINE_I386 #define IMAGE_FILE_MACHINE_R3000 #define IMAGE_FILE_MACHINE_R4000
0 0x014c 0x0162 0x0166
// Intel 386. // MIPS little‐endian, 0x160 big‐endian // MIPS little‐endian
第4页 共13页
#define IMAGE_FILE_MACHINE_R10000 #define IMAGE_FILE_MACHINE_WCEMIPSV2 #define IMAGE_FILE_MACHINE_ALPHA #define IMAGE_FILE_MACHINE_SH3 #define IMAGE_FILE_MACHINE_SH3DSP #define IMAGE_FILE_MACHINE_SH3E #define IMAGE_FILE_MACHINE_SH4 #define IMAGE_FILE_MACHINE_SH5 #define IMAGE_FILE_MACHINE_ARM #define IMAGE_FILE_MACHINE_THUMB #define IMAGE_FILE_MACHINE_AM33 #define IMAGE_FILE_MACHINE_POWERPC #define IMAGE_FILE_MACHINE_POWERPCFP #define IMAGE_FILE_MACHINE_IA64 #define IMAGE_FILE_MACHINE_MIPS16 #define IMAGE_FILE_MACHINE_ALPHA64 #define IMAGE_FILE_MACHINE_MIPSFPU #define IMAGE_FILE_MACHINE_MIPSFPU16 #define IMAGE_FILE_MACHINE_AXP64 #define IMAGE_FILE_MACHINE_TRICORE #define IMAGE_FILE_MACHINE_CEF #define IMAGE_FILE_MACHINE_EBC #define IMAGE_FILE_MACHINE_AMD64 #define IMAGE_FILE_MACHINE_M32R #define IMAGE_FILE_MACHINE_CEE
下图是一张真实的PE文件头结构以及其各个域的取值:
第3页 共13页
Signature:类似于DOS头中的e_magic,其高16位是0,低16是0x4550,用字符表示是'PE‘。 IMAGE_FILE_HEADER是PE文件头,c语言的定义是这样的:
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics;
DOS头是用来兼容MS-DOS操作系统的,目的是当这个文件在MS-DOS上运行时提示一段文字,大部分情 况下是:This program cannot be run in DOS mode.还有一个目的,就是指明NT头在文件中的位置。 NT头包含windows PE文件的主要信息,其中包括一个‘PE’字样的签名,PE文件头 (IMAGE_FILE_HEADER)和PE可选头(IMAGE_OPTIONAL_HEADER32),头部的详细结构以及其具 体意义在PE文件头文章中详细描述。 节表:是PE文件后续节的描述,windows根据节表的描述加载每个节。 节:每个节实际上是一个容器,可以包含代码、数据等等,每个节可以有独立的内存权限,比如代码节默 认有读/执行权限,节的名字和数量可以自己定义,未必是上图中的三个。 当一个PE文件被加载到内存中以后,我们称之为“映象”(image),一般来说,PE文件在硬盘上和在内存 里是不完全一样的,被加载到内存以后其占用的虚拟地址空间要比在硬盘上占用的空间大一些,这是因为 各个节在硬盘上是连续的,而在内存中是按页对齐的,所以加载到内存以后节之间会出现一些“空洞”。 因为存在这种对齐,所以在PE结构内部,表示某个位置的地址采用了两种方式,针对在硬盘上存储文件中 的地址,称为原始存储地址或物理地址表示距离文件头的偏移;另外一种是针对加载到内存以后映象中的 地址,称为相对虚拟地址(RVA),表示相对内存映象头的偏移。
typedef struct _IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip;
NumberOfSections:该PE文件中有多少个节,也就是节表中的项数。 TimeDateStamp:PE文件的创建时间,一般有连接器填写。 PointerToSymbolTable:COFF文件符号表在文件中的偏移。 NumberOfSymbols:符号表的数量。 SizeOfOptionalHeader:紧随其后的可选头的大小。 Characteristics:可执行文件的属性,可以是下面这些值按位相或。
这里写图片描述
这种表示方式叫做虚拟地址(VA)。也许有人要问,既然有VA这么简单的表示方式为什么还要有前面的 RVA呢?因为虽然PE文件为自己指定加载的基地址,但是windows有茫茫多的DLL,而且每个软件也有自己 的DLL,如果指定的地址已经被别的DLL占了怎么办?如果PE文件无法加载到预期的地址,那么系统会帮 他重新选择一个合适的基地址将他加载到此处,这时原有的VA就全部失效了,NT头保存了PE文件加载所需 的信息,在不知道PE会加载到哪个基地址之前,VA是无效的,所以在PE文件头中大部分是使用RVA来表示 地址的,而在代码中是用VA表示全局变量和函数地址的。那又有人要问了,既然加载基址变了以后VA都失 效了,那存在于代码中的那些VA怎么办呢?答案是:重定位。系统有自己的办法修正这些值,到后续重定 位表的文章中会详细描述。既然有重定位,为什么NT头不能依靠重定位采用VA表示地址呢(十万个为什 么)?因为不是所有的PE都有重定位,早期的EXE就是没有重定位的。我们都知道PE文件可以导出函数让 其他的PE文件使用,也可以从其他PE文件导入函数,这些是如何做到的?PE文件通过导出表指明自己导 出那些函数,通过导入表指明需要从哪些模块导入哪些函数。导入和导出表的具体结构会在单独的文章中 详细解释。
第2页 共13页
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
// DOS .EXE header // Magic number // Bytes on last page of file // Pages in file // Relocations // Size of header in paragraphs // Minimum extra paragraphs needed // Maximum extra paragraphs needed // Initial (relative) SS value // Initial SP value // Checksum // Initial IP value
0x0168 // MIPS little‐endian 0x0169 // MIPS little‐endian WCE v2 0x0184 // Alpha_AXP 0x01a2 // SH3 little‐endian 0x01a3 0x01a4 // SH3E little‐endian 0x01a6 // SH4 little‐endian 0x01a8 // SH5 0x01c0 // ARM Little‐Endian 0x01c2 0x01d3 0x01F0 // IBM PowerPC Little‐Endian 0x01f1 0x0200 // Intel 64 0x0266 // MIPS 0x0284 // ALPHA64 0x0366 // MIPS 0x0466 // MIPS IMAGE_FILE_MACHINE_ALPHA64 0x0520 // Infineon 0x0CEF 0x0EBC // EFI Byte Code 0x8664 // AMD64 (K8) 0x9041 // M32R little‐endian 0xC0EE
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
我们只需要关注两个域:
e_magic:一个WORD类型,值是一个常数0x4D5A,用文本编辑器查看该值为‘MZ’,可执行文件必须都是 'MZ'开头。
e_lfanew:为32位可执行文件扩展的域,用来表示DOS头之后的NT头相对文件起始地址的偏移。
二、NT头
顺着DOS头中的e_lfanew,我们很容易可以找到NT头,这个才是32位PE文件中最有用的头,定义如下:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
第1页 共13页
然而CPU的某些指令是需要使用绝对地址的,比如取全局变量的地址,传递函数的地址编译以后的汇编指 令中肯定需要用到绝对地址而不是相对映象头的偏移,因此PE文件会建议操作系统将其加载到某个内存地 址(这个叫基地址),编译器便根据这个地址求出代码中一些全局变量和函数的地址,并将这些地址用到 对应的指令中。例如在IDA里看上去是这个样子:
一、DOS头
DOS头的作用是兼容MS-DOS操作系统中的可执行文件,对于32位PE文件来说,DOS所起的作用就是显示 一行文字,提示用户:我需要在32位windows上才可以运行。我认为这是个善意的玩笑,因为他并不像显示 的那样不能运行,其实已经运行了,只是在DOS上没有干用户希望看到的工作而已,好吧,我承认这不是 重点。但是,至少我们看一下这个头是如何定义的:
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
每个域的具体含义如下: Machine:该文件的运行平台,是x86、x64还是I64fine IMAGE_FILE_MACHINE_UNKNOWN #define IMAGE_FILE_MACHINE_I386 #define IMAGE_FILE_MACHINE_R3000 #define IMAGE_FILE_MACHINE_R4000
0 0x014c 0x0162 0x0166
// Intel 386. // MIPS little‐endian, 0x160 big‐endian // MIPS little‐endian
第4页 共13页
#define IMAGE_FILE_MACHINE_R10000 #define IMAGE_FILE_MACHINE_WCEMIPSV2 #define IMAGE_FILE_MACHINE_ALPHA #define IMAGE_FILE_MACHINE_SH3 #define IMAGE_FILE_MACHINE_SH3DSP #define IMAGE_FILE_MACHINE_SH3E #define IMAGE_FILE_MACHINE_SH4 #define IMAGE_FILE_MACHINE_SH5 #define IMAGE_FILE_MACHINE_ARM #define IMAGE_FILE_MACHINE_THUMB #define IMAGE_FILE_MACHINE_AM33 #define IMAGE_FILE_MACHINE_POWERPC #define IMAGE_FILE_MACHINE_POWERPCFP #define IMAGE_FILE_MACHINE_IA64 #define IMAGE_FILE_MACHINE_MIPS16 #define IMAGE_FILE_MACHINE_ALPHA64 #define IMAGE_FILE_MACHINE_MIPSFPU #define IMAGE_FILE_MACHINE_MIPSFPU16 #define IMAGE_FILE_MACHINE_AXP64 #define IMAGE_FILE_MACHINE_TRICORE #define IMAGE_FILE_MACHINE_CEF #define IMAGE_FILE_MACHINE_EBC #define IMAGE_FILE_MACHINE_AMD64 #define IMAGE_FILE_MACHINE_M32R #define IMAGE_FILE_MACHINE_CEE
下图是一张真实的PE文件头结构以及其各个域的取值:
第3页 共13页
Signature:类似于DOS头中的e_magic,其高16位是0,低16是0x4550,用字符表示是'PE‘。 IMAGE_FILE_HEADER是PE文件头,c语言的定义是这样的:
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics;
DOS头是用来兼容MS-DOS操作系统的,目的是当这个文件在MS-DOS上运行时提示一段文字,大部分情 况下是:This program cannot be run in DOS mode.还有一个目的,就是指明NT头在文件中的位置。 NT头包含windows PE文件的主要信息,其中包括一个‘PE’字样的签名,PE文件头 (IMAGE_FILE_HEADER)和PE可选头(IMAGE_OPTIONAL_HEADER32),头部的详细结构以及其具 体意义在PE文件头文章中详细描述。 节表:是PE文件后续节的描述,windows根据节表的描述加载每个节。 节:每个节实际上是一个容器,可以包含代码、数据等等,每个节可以有独立的内存权限,比如代码节默 认有读/执行权限,节的名字和数量可以自己定义,未必是上图中的三个。 当一个PE文件被加载到内存中以后,我们称之为“映象”(image),一般来说,PE文件在硬盘上和在内存 里是不完全一样的,被加载到内存以后其占用的虚拟地址空间要比在硬盘上占用的空间大一些,这是因为 各个节在硬盘上是连续的,而在内存中是按页对齐的,所以加载到内存以后节之间会出现一些“空洞”。 因为存在这种对齐,所以在PE结构内部,表示某个位置的地址采用了两种方式,针对在硬盘上存储文件中 的地址,称为原始存储地址或物理地址表示距离文件头的偏移;另外一种是针对加载到内存以后映象中的 地址,称为相对虚拟地址(RVA),表示相对内存映象头的偏移。
typedef struct _IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip;