PE结构详解(一)
转载请标明出处![此文章引用大量小甲鱼,和evil.eagle的博客内容]
首先什么是PE文件:【摘自百度百科】PE文件的全称是Portable Executable,意为可移植的执行体是,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)
PE文件的结构如下图所示:
可以看到,从头到尾分别是DOS头,PE头,块表,区块(诸如.text块,.data块这些都称之为区块,也有称作为“节”的)。
我们看到磁盘里面的PE文件映射到内存中时,位置发生了些变化。但是我们同时也发现,它映射到内存中时,各个结构的顺序是不会发生变化的。注意到变化发生其实就是偏移地址发生变化。【实际当中 DOS头,PE头和块表是不会变的,上图有点小问题(块表它的箭头应该是直的)。】
下面就来介绍:
当一个PE文件被加载到内存中以后,我们称之为“映象”(image)[也有成为模块的],一般来说,PE文件在硬盘上和在内存里是不完全一样的,被加载到内存以后其占用的虚拟地址空间要比在硬盘上占用的空间大一些,这是因为各个节在硬盘上是连续的,而在内存中是按页对齐的,所以加载到内存以后节之间会出现一些“空洞”。
因为存在这种对齐,所以在PE结构内部,表示某个位置的地址采用了两种方式,针对在硬盘上存储文件中的地址,称为原始存储地址或物理地址表示距离文件头的偏移;另外一种是针对加载到内存以后映象中的地址,称为相对虚拟地址(RVA),表示相对内存映象头的偏移。
然而CPU的某些指令是需要使用绝对地址的,比如取全局变量的地址,传递函数的地址编译以后的汇编指令中肯定需要用到绝对地址而不是相对映象头的偏移,因此PE文件会建议操作系统将其加载到某个内存地址(这个叫基地址),编译器便根据这个地址求出代码中一些全局变量和函数的地址,并将这些地址用到对应的指令中。
【
因为每个程序都有其4GB的虚拟内存分配空间(注意:是虚拟内存,只有当真正要实现程序的某个功能的时候,才会把虚拟内存里的数据映射到真正的物理地址当中),当程序被加载到虚拟内存时,它就会被加载到之前设定的基地址。(如果有意外的话,还有重定向)
】
了解了一些PE的基本概念,我们接下来详细介绍里面的各个内容
其中DOS头,其实他是一个结构,内容如下:
IMAGE_DOS_HEADER STRUCT { +00h WORD e_magic // Magic DOS signature MZ(4Dh 5Ah) DOS可执行文件标记 +02h WORD e_cblp // Bytes on last page of file +04h WORD e_cp // Pages in file +06h WORD e_crlc // Relocations +08h WORD e_cparhdr // Size of header in paragraphs +0ah WORD e_minalloc // Minimun extra paragraphs needs +0ch WORD e_maxalloc // Maximun extra paragraphs needs +0eh WORD e_ss // intial(relative)SS value DOS代码的初始化堆栈SS +10h WORD e_sp // intial SP value DOS代码的初始化堆栈指针SP +12h WORD e_csum // Checksum +14h WORD e_ip // intial IP value DOS代码的初始化指令入口[指针IP] +16h WORD e_cs // intial(relative)CS value DOS代码的初始堆栈入口 +18h WORD e_lfarlc // File Address of relocation table +1ah WORD e_ovno // Overlay number +1ch WORD e_res[4] // Reserved words +24h WORD e_oemid // OEM identifier(for e_oeminfo) +26h WORD e_oeminfo // OEM information;e_oemid specific +29h WORD e_res2[10] // Reserved words +3ch LONG e_lfanew // Offset to start of PE header 指向PE文件头 } IMAGE_DOS_HEADER ENDS
DOS头的作用是兼容MS-DOS操作系统中的可执行文件,对于32位PE文件来说,DOS所起的作用就是显示一行文字,提示用户:我需要在32位windows上才可以运行。(这也算兼容?好吧,至少你能在DOS中打开他)
里面这么多破东西,是不是看晕了,其实我们只需要关注2个内容就行:
第一个:DWORD e_magic:一个WORD类型,值是一个常数0x4D5A,用文本编辑器查看该值位‘MZ’,可执行文件必须都是'MZ'开头, 表示他是DOS的执行文件。。
最后一个:LONG e_lfanew存放PE头的文件偏移量。偏移量就是基地址+偏移量=PE头的物理地址。
其实第一个也不用管,,只看最后一个,它里面的值表示DOS头之后PE头的偏移地址,也就是说我们可以根据这个来直接找到PE头。
这个PE头 才是WIN32程序的真正入口。
这个PE头也是一个结构,结构如下:
首先是IMAGE_NT_HEADERS 结构的定义: IMAGE_NT_HEADERS STRUCT { +0h DWORD Signature // +4h IMAGE_FILE_HEADER FileHeader // +18h IMAGE_OPTIONAL_HEADER32 OptionalHeader // } IMAGE_NT_HEADERS ENDS
Signature 字段:
在一个有效的 PE 文件里,Signature 字段被设置为00004550h, ASCII 码字符是“PE00”。标志这 PE 文件头的开始。
“PE00” 字符串是 PE 文件头的开始,DOS 头部的 e_lfanew 字段正是指向这里。
IMAGE_FILE_HEADER 结构
typedef struct _IMAGE_FILE_HEADER { +04h WORD Machine; // 运行平台 +06h WORD NumberOfSections; // 文件的区块数目 +08h DWORD TimeDateStamp; // 文件创建日期和时间 +0Ch DWORD PointerToSymbolTable; // 指向符号表(主要用于调试) +10h DWORD NumberOfSymbols; // 符号表中符号个数(同上) +14h WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER32 结构大小 +16h WORD Characteristics; // 文件属性 } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
(1)Machine:可执行文件的目标CPU类型.
Value |
Meaning |
IMAGE_FILE_MACHINE_I386 0x014c |
x86 |
IMAGE_FILE_MACHINE_IA64 0x0200 |
Intel Itanium |
IMAGE_FILE_MACHINE_AMD64 0x8664 |
x64 |
(2)NumberOfSection: 区块的数目。(注:区块表是紧跟在 IMAGE_NT_HEADERS 后边的)
(3)TimeDataStamp: 表明文件是何时被创建的。
这个值是自1970年1月1日以来用格林威治时间(GMT)计算的秒数,这个值是比文件系统(FILESYSTEM)的日期时间更加精确的指示器。如何将这个值翻译请看:
提示:VC的话可以用_ctime 函数或者 gmtime 函数。
(4)PointerToSymbolTable: COFF 符号表的文件偏移位置,现在基本没用了。
(5)NumberOfSymbols: 如果有COFF 符号表,它代表其中的符号数目,COFF符号是一个大小固定的结构,如果想找到COFF 符号表的结束位置,则需要这个变量。
(6)SizeOfOptionalHeader: 紧跟着IMAGE_FILE_HEADER 后边的数据结构(IMAGE_OPTIONAL_HEADER)的大小。(对于32位PE文件,这个值通常是00E0h;对于64位PE32+文件,这个值是00F0h )。
(7)Characteristics: 文件属性,有选择的通过几个值可以运算得到。( 这些标志的有效值是定义于 winnt.h 内的 IMAGE_FILE_xxx 的值,具体含义见下表。普通的EXE文件这个字段的值一般是 0100h,DLL文件这个字段的值一般是 210Eh。)小甲鱼温馨提示:多种属性可以通过 “或运算” 使得同时拥有!
The characteristics of the image. This member can be one or more of the following values.
Value |
Meaning |
IMAGE_FILE_RELOCS_STRIPPED 0x0001 |
Relocation information was stripped from the file. The file must be loaded at its preferred base address. If the base address is not available, the loader reports an error. |
IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 |
The file is executable (there are no unresolved external references). |
IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 |
COFF line numbers were stripped from the file. |
IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 |
COFF symbol table entries were stripped from file. |
IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 |
Aggressively trim the working set. This value is obsolete as of Windows 2000. |
IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 |
The application can handle addresses larger than 2 GB. |
IMAGE_FILE_BYTES_REVERSED_LO 0x0080 |
The bytes of the word are reversed. This flag is obsolete. |
IMAGE_FILE_32BIT_MACHINE 0x0100 |
The computer supports 32-bit words. |
IMAGE_FILE_DEBUG_STRIPPED 0x0200 |
Debugging information was removed and stored separately in another file. |
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 |
If the image is on removable media, copy it to and run it from the swap file. |
IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 |
If the image is on the network, copy it to and run it from the swap file. |
IMAGE_FILE_SYSTEM 0x1000 |
The image is a system file. |
IMAGE_FILE_DLL 0x2000 |
The image is a DLL file. While it is an executable file, it cannot be run directly. |
IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 |
The file should be run only on a uniprocessor computer. |
IMAGE_FILE_BYTES_REVERSED_HI 0x8000 |
The bytes of the word are reversed. This flag is obsolete. |
可以看出,PE文件头定义了PE文件的一些基本信息和属性,这些属性会在PE加载器加载时用到,如果加载器发现PE文件头中定义的一些属性不满足当前的运行环境,将会终止加载该PE。
另一个重要成员,别看他名字叫可选头,其实一点都不能少,不过,它在不同的平台下是不一样的,例如32位下是IMAGE_OPTIONAL_HEADER32,而在64位下是IMAGE_OPTIONAL_HEADER64。为了简单起见,我们只看32位。
IMAGE_OPTIONAL_HEADER 结构,正如名字的意思,这是一个可选映像头,是一个可选的结构,但是呢,实际上上节课我们讲解的 IMAGE_FILE_HEADER 结构远远不足以来定义 PE 文件的属性。因此,这些属性在 IMAGE_OPTIONAL_HEADER 结构中进行定义。因此这两个结构联合起来,才是一个完整的 “PE文件结构” 。
接着我们来谈谈 IMAGE_OPTIONAL_HEADER 结构,正如名字的意思,这是一个可选映像头,是一个可选的结构,但是呢,实际上上节课我们讲解的 IMAGE_FILE_HEADER 结构远远不足以来定义 PE 文件的属性。因此,这些属性在 IMAGE_OPTIONAL_HEADER 结构中进行定义。
因此这两个结构联合起来,才是一个完整的 “PE文件结构” 。
那么我们接着就来谈谈
IMAGE_OPTIONAL_HEADER32 结构的定义 typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // +18h WORD Magic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh) +1Ah BYTE MajorLinkerVersion; // 链接程序的主版本号 +1Bh BYTE MinorLinkerVersion; // 链接程序的次版本号 +1Ch DWORD SizeOfCode; // 所有含代码的节的总大小 +20h DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小 +24h DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小 +28h DWORD AddressOfEntryPoint; // 程序执行入口RVA +2Ch DWORD BaseOfCode; // 代码的区块的起始RVA +30h DWORD BaseOfData; // 数据的区块的起始RVA // // NT additional fields. 以下是属于NT结构增加的领域。 // +34h DWORD ImageBase; // 程序的首选装载地址 +38h DWORD SectionAlignment; // 内存中的区块的对齐大小 +3Ch DWORD FileAlignment; // 文件中的区块的对齐大小 +40h WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号 +42h WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号 +44h WORD MajorImageVersion; // 可运行于操作系统的主版本号 +46h WORD MinorImageVersion; // 可运行于操作系统的次版本号 +48h WORD MajorSubsystemVersion; // 要求最低子系统版本的主版本号 +4Ah WORD MinorSubsystemVersion; // 要求最低子系统版本的次版本号 +4Ch DWORD Win32VersionValue; // 莫须有字段,不被病毒利用的话一般为0 +50h DWORD SizeOfImage; // 映像装入内存后的总尺寸 +54h DWORD SizeOfHeaders; // 所有头 + 区块表的尺寸大小 +58h DWORD CheckSum; // 映像的校检和 +5Ch WORD Subsystem; // 可执行文件期望的子系统 +5Eh WORD DllCharacteristics; // DllMain()函数何时被调用,默认为 0 +60h DWORD SizeOfStackReserve; // 初始化时的栈大小 +64h DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小 +68h DWORD SizeOfHeapReserve; // 初始化时保留的堆大小 +6Ch DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小 +70h DWORD LoaderFlags; // 与调试有关,默认为 0 +74h DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来 // 一直是16 +78h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录表 } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
事实上,这个结构中的大部分字段都不重要,大家可以从注释中理解它们的含义,小甲鱼将比较重要的字段在下边跟大家详细讲解。另外,这玩意千万不要去背啊,我们要把绝大多数的时间拿来改变,而不是记住。不用做笔记,把这篇文章转载到您的博客就行。
AddressOfEntryPoint字段
指出文件被执行时的入口地址,这是一个RVA地址(RVA的含义在下一节中详细介绍)。如果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要将这个入口地址指向附加的代码就可以了。
ImageBase字段
指出文件的优先装入地址。也就是说当文件被执行时,如果可能的话,Windows优先将文件装入到由ImageBase字段指定的地址中,只有指定的地址已经被**模块使用时,文件才被装入到**地址中。链接器产生可执行文件的时候对应这个地址来生成机器码,所以当文件被装入这个地址时不需要进行重定位操作,装入的速度最快,如果文件被装载到**地址的话,将不得不进行重定位操作,这样就要慢一点。
对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被**模块占据,所以EXE总是能够按照这个地址装入,这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被**的DLL使用,所以DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 字段中,DLL 文件对应的 IMAGE_FILE_RELOCS_STRIPPED 位总是为0,而EXE文件的这个标志位总是为1。
在链接的时候,可以通过对link.exe指定/base:address选项来自定义优先装入地址,如果不指定这个选项的话,一般EXE文件的默认优先装入地址被定为00400000h,而DLL文件的默认优先装入地址被定为10000000h。
SectionAlignment 字段和FileAlignment字段
SectionAlignment字段指定了节被装入内存后的对齐单位。也就是说,每个节被装入的地址必定是本字段指定数值的整数倍。而FileAlignment字段指定了节存储在磁盘文件中时的对齐单位。
Subsystem字段
指定使用界面的子系统,它的取值如表17.3所示。这个字段决定了系统如何为程序建立初始的界面,链接时的/subsystem:**选项指定的就是这个字段的值,在前面章节的编程中我们早已知道:如果将子系统指定为Windows CUI,那么系统会自动为程序建立一个控制台窗口,而指定为Windows GUI的话,窗口必须由程序自己建立。
界面子系统的取值和含义
取 值 |
Windows.inc中的预定义值 |
含 义 |
0 |
IMAGE_SUBSYSTEM_UNKNOWN |
未知的子系统 |
1 |
IMAGE_SUBSYSTEM_NATIVE |
不需要子系统(如驱动程序) |
2 |
IMAGE_SUBSYSTEM_WINDOWS_GUI |
Windows图形界面 |
3 |
IMAGE_SUBSYSTEM_WINDOWS_CUI |
Windows控制台界面 |
5 |
IMAGE_SUBSYSTEM_OS2_CUI |
OS2控制台界面 |
7 |
IMAGE_SUBSYSTEM_POSIX_CUI |
POSIX控制台界面 |
8 |
IMAGE_SUBSYSTEM_NATIVE_WINDOWS |
不需要子系统 |
9 |
IMAGE_SUBSYSTEM_WINDOWS_CE_GUI |
Windows CE图形界面 |
DataDirectory字段
这个字段可以说是最重要的字段之一,它由16个相同的IMAGE_DATA_DIRECTORY结构组成,虽然PE文件中的数据是按照装入内存后的页属性归类而被放在不同的节中的,但是这些处于各个节中的数据按照用途可以被分为导出表、导入表、资源、重定位表等数据块,这16个IMAGE_DATA_DIRECTORY结构就是用来定义多种不同用途的数据块的(如表17.4所示)。IMAGE_DATA_DIRECTORY结构的定义很简单,它仅仅指出了某种数据块的位置和长度。
IMAGE_DATA_DIRECTORY STRUCT
VirtualAddress DWORD ? ;数据的起始RVA
isize DWORD ? ;数据块的长度
IMAGE_DATA_DIRECTORY ENDS
数据目录列表的含义
索 引 |
索引值在Windows.inc中的预定义值 |
对应的数据块 |
0 |
IMAGE_DIRECTORY_ENTRY_EXPORT |
导出表 |
1 |
IMAGE_DIRECTORY_ENTRY_IMPORT |
导入表 |
2 |
IMAGE_DIRECTORY_ENTRY_RESOURCE |
资源 |
3 |
IMAGE_DIRECTORY_ENTRY_EXCEPTION |
异常(具体资料不详) |
4 |
IMAGE_DIRECTORY_ENTRY_SECURITY |
安全(具体资料不详) |
5 |
IMAGE_DIRECTORY_ENTRY_BASERELOC |
重定位表 |
6 |
IMAGE_DIRECTORY_ENTRY_DEBUG |
调试信息 |
7 |
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE |
版权信息 |
8 |
IMAGE_DIRECTORY_ENTRY_GLOBALPTR |
具体资料不详 |
9 |
IMAGE_DIRECTORY_ENTRY_TLS |
Thread Local Storage |
10 |
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG |
具体资料不详 |
11 |
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT |
具体资料不详 |
12 |
IMAGE_DIRECTORY_ENTRY_IAT |
导入函数地址表 |
13 |
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT |
具体资料不详 |
14 |
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR |
具体资料不详 |
15 |
未使用 |
|
在PE文件中寻找特定的数据时就是从这些IMAGE_DATA_DIRECTORY结构开始的,比如要存取资源,那么必须从第3个IMAGE_DATA_DIRECTORY结构(索引为2)中得到资源数据块的大小和位置;同理,如果要查看PE文件导入了哪些DLL文件的哪些API函数,那就必须首先从第2个IMAGE_DATA_DIRECTORY结构得到导入表的位置和大小。