本文主要来自相关系列博文(breaksoftware的csdn博客 https://blog.csdn.net/breaksoftware/article/details/7710323)。
自己,稍微调整,整理。
MS 2.0节是PE文件格式中第一个“节”。其大致结构如下:
在VC\PlatformSDK\Include\WinNT.h文件中有对MS-DOS 2.0兼容EXE文件头的完整定义
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
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; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
这个结构占用0x40个字节,其中我们将主要关注两个成员变量:e_magic和e_lfanew。
以我xp电脑上notepad为例,我们使用UE打开C:\windows\notepad.exe
可以发现IMAGE_DOS_HEADER结构中e_magic对应的数据位0x5A4D(MZ),e_lfanew对应的是0x000000E0。这个两个数据是这个结构体中最需要关心的两个成员变量。幻数(Magic Num)这个概念是用于区分一个格式文件的类型,就像一个人的姓,知道你姓啥之后,就可以明确你是不是我们族人。同样,解析这些文件的程序也会去尝试读取这样的幻数,以确认这个文件符合它要求的。在我所知道的一些格式中,他们的幻数往往是这个格式发明者的名称缩写(或者是格式后缀)。我们这个MS-Dos 2.0兼容EXE文件头中的幻数MZ也是纪念他的发明者,可以想到,这个名字应该不是盖茨,因为MZ和Bill Gates(BG)一点也没关系,也不是Paul Allen(PA),更不可能是销售出生的Steve Ballmer。它是Mark Zbikowski,中文翻译是马克·茨柏克沃斯基。
那么为什么PE格式文件会有个Dos文件头呢?Dos系统时代,有两种(我所知道的,我压根没经历过那个年代)可执行文件格式,一种是.exe为后缀的文件,其结构是MZ格式。另一种是以.com为后缀的文件,其结构是COM格式。从Wiki上对MZ格式的介绍可以看出来,MZ格式要比COM格式要新,MZ格式头中包含了重定向信息(本文第一个图中),且其支持可执行体大于64KiB。如今我们电脑上PE可执行文件的后缀也是.exe,为了让该后缀程序在Dos和Nt间有个过渡,我们需要让Dos系统能知道它不能“正确”执行该Exe文件。于是我们PE可执行文件一开始处便插入了一个MS-Dos 2.0兼容Exe文件头,Dos系统加载我们PE文件时,从一开始读取我们文件,发现是“DOS下可执行程序”,于是成功且顺利的执行我们的程序中DOS系统可执行部分,这部分DOS程序输出“该程序不能在DOS上”执行的提示。
现在我们来看下MS-2.0节结构图和我们结构体的对应关系:
MS-Dos 2.0兼容Exe文件头 对应于IMAGE_DOS_HEADER中e_magic到e_ovno
未使用 对应于 e_res[4],虽说这段没使用,但是我还是觉得这段很有意思的。我在做注册表沙箱时,研究了下某公司的沙箱,可是它的沙箱不让regedit.exe进入沙箱运行,于是我就改了e_res[4]这段数据中部分,从而让修改后的regedit.exe在它的沙箱中运行。为什么呢?很容易想象,“MD5+签名”是安全公司一大“安全准绳”。我改了这个没啥用的数据段,不会影响程序运行,但是会使MD5不同,且签名被破坏。这段地址是(文件起始偏移0x1C)
OEM标志 对应于 e_oemid
OEM信息 对应于 e_oeminfo
OEM信息和PE文件头偏移 之间存在一段空白,这段空白对应于 e_res2[10],这段数据和之前e_res[4]一样,改改也无妨。这段地址是(偏移0x28)
PE文件头偏移 对应于 e_lfanew,其位于0x3C偏移处。
MS-Dos 2.0占位程序和重定向表和未使用数据段如下图,因为我也没仔细研究过这个结构,所以也不能准确区分出哪块是占位程序,哪块是重定向表,哪块是未使用段。
从上面的数据我们可以看到,如果我们程序运行在Dos下,会输出“This program connot be run in Dos mode"。
那么NT系统加载我们的PE可执行程序呢?它不会去执行DOS占位程序,而会跳到PE头位置继续读取和执行。PE头位置就是e_lfanew字段的值,该值是PE头和文件头的之间的偏移量。如本例中就是0x000000E0。我们去该偏移去查看数据
看到PE了么?这个PE是PE头的Magic Num。我会在之后介绍PE文件头及其相关知识。
简短总结,以我自己手上的 notepad++.exe 这个文件为例,如下图:
第一个红色框内就是上面文章讲的主要内容,最后四个字节是 0x00000120。如箭头所指向的地址。以5045开头。这一部分以及之后的是稍后的内容。
==============================================================================
首先说一下VC中对应的数据结构。“签名、COFF文件头和可选文件头”这三部分信息组合在一起是一个叫IMAGE_NT_HEADERS的结构体。
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;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
其中Signature对应于“签名”,FileHeader对应于“COFF文件头”,OptionalHeader对应于“可选文件头”。
对于PE镜像文件,Signature对应的数据是0x00004550(‘PE\0\0’)。对于如何找到这个位置,在前一篇文章中已经有了解说:从文件头偏移0x3C读取一个DWORD大小的数据,从文件头偏移该数据长度,就到了Signature的起始位置。
看一下COFF文件头结构
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;
以notepad为例
Signature 的值就是 0x00004550。
IMAGE_FILE_HEADER 有7个字段,图中用红色框标注。
Machine字段为0x014C,其对应的信息是“Intel 386或其后续处理器及兼容处理器”。
NumberOfSections是0x0003,它是个非常重要的字段,表示节的数目。PE文件是由一系列“节”构成的,比较常见的是.text和.data等节,这样的独立的区块是用来存储“代码”、“数据”和“资源”等信息的。如xp上notepad,从数据中我们可以看到它有3个节,我们用其他工具分析得到它确实存在如下3个节。
TimeDateStamp是0x41107CC3,该字段记录的是文件创建时间离1970年1月1日00:00的秒数。
(可以参考工具网站进行时间转换:http://tool.chinaz.com/Tools/unixtime.aspx)
PointerToSymbolTable是0x00000000,该字段记录了该PE文件中调试信息符号表。由于符号表信息是在程序运行时不需要加载进入内存的,所以这个偏移使用的是相对文件头偏移RA。目前微软推荐是:将映像文件调试符号表信息独立的放在PDB文件中,所以不会在PE文件中再保存调试符号表信息,于是这个字段应该为0。当然这并不是硬性要求,我发现我电脑上就存在很多该字段不为0的文件。刚开始时我也不是很明白它们为什么要使用这个字段,特别是其指向的字符表个数(NumberOfSymbols)为0!!你说既然大小为0,那你指向有什么意思呢?其实这种设计是非常有深意的,我会在之后的章节中介绍这种深意。
NumberOfSymbols是0x00000000,该字段记录了该PE文件中调试信息符号表元素个数。对于映像文件,该字段为0(非硬性要求),,理由在PointerToSymbolTable中已经说明。通过NumberOfSymbols和PointerToSymbolTable,我们可以找到字符串表起始位置,因为字符串表紧跟在符号表之后。
SizeOfOptionalHeader是0x00E0,该字段用于描述“可选文件头”的大小。之后会看到“可选文件头”的中有个具有16个元素是数组,该数组保存了一系列“块信息”,但是并不是所有文件都有全部的“块信息”,于是链接器在链接生成PE文件时,也是根据实际存在的“块信息”位置(以后会说明为什么是位置而不是数量)去填充这个数组的。也就是说我们可能只是填充了1个元素,而剩下的15个元素直接被砍掉,而不是在内存中使用0来填充。
这儿就引入一个问题,就是我们不能从“签名”位置开始,就直接memcpy一段IMAGE_NT_HEADERS大小的空间到一个IMAGE_NT_HEADERS对象中。因为“可选文件头”还要看“COFF文件头”中的SizeOfOptionalHeader数据。
Characteristics字段用于标记该文件属性,notepad.exe该字段值为0x010F。下面我们来解释下该组合属性。
标志 | 值 | 说明 |
---|---|---|
IMAGE_FILE_RELOCS_STRIPPED | 0x0001 | 仅适用于映像文件。它表明此文件不包含机制重定位信息,于是它只能被加载到其首选基地址。如果首选基地址不可用,则加载器会报错。链接器默认会移除可执行文件中的重定位信息。一般情况下,Exe文件会设置该值(如notepad.exe,但ntoskrnl.exe就没设置),而因为DLL文件为了其良好的兼容性是不会去设置这个值的(如Kernel32.dll、User32.dll等)。 |
IMAGE_FILE_EXECUTABLE_IMAGE | 0x0002 | 仅适用于映像文件。它用于表明该文件是合法的,可以被运行。如果没有设置,则代表链接出现问题。这个一般都会设置。 |
IMAGE_FILE_LINE_NUMS_STRIPPED | 0x0004 | COFF行号信息已经被移除。不赞成使用该标志。但是我发现notepad.exe、Kernel32.dll、User32.dll等都设置了该标志。而一般我们编译的PE文件是不设置该项的。 |
IMAGE_FILE_LOCAL_SYMS_STRIPPED | 0x0008 | COFF符号表中有关局部符号的项已经被移除。不赞成使用该标志。但是我发现notepad.exe、Kernel32.dll、User32.dll等都设置了该标志。而一般我们编译的PE文件是不设置该项的。 |
IMAGE_FILE_AGGRESSIVE_WS_TRIM | 0x0010 | 该标志已经被废弃。 |
IMAGE_FILE_LARGE_ADDRESS_ AWARE | 0x0020 | 应用程序可以处理大于2GB的地址。 |
0x0040 | 为未来保留的字段。 | |
IMAGE_FILE_BYTES_REVERSED_LO | 0x0080 | 小尾,LSB在MSB前面。不赞成使用该标志。windows xp就是小尾。 |
IMAGE_FILE_32BIT_MACHINE | 0x0100 | 适用于32位系统。我的xp系统上DLL和Exe文件基本都设置了该标志。 |
IMAGE_FILE_DEBUG_STRIPPED | 0x0200 | 调试信息已经从该映像文件中移除。 |
IMAGE_FILE_REMOVABLE_RUN_ FROM_SWAP | 0x0400 | 如果该文件是在移动介质上,需要将其完全加载到交换文件中。 |
IMAGE_FILE_NET_RUN_FROM_SWAP | 0x0800 | 如果该文件是在网络介质上,需要将其完全加载到交换文件中。 |
IMAGE_FILE_SYSTEM | 0x1000 | 该映像文件是一个系统文件,不是一个用户文件。 |
IMAGE_FILE_DLL | 0x2000 | 此文件是DLL文件。 |
IMAGE_FILE_UP_SYSTEM_ONLY | 0x4000 | 该文件仅能运行于单处理机器上。 |
IMAGE_FILE_BYTES_REVERSED_HI | 0x8000 | 大尾,LSB在MSB后面。 |
我观察了我系统上几个文件,发现以下规律:
1 Sys和Exe的该属性为0x010E或者0x010F。
2 DLL文件该属性一般为0x210E。DLL文件一般不会设IMAGE_FILE_RELOCS_STRIPPED(0x0001),因为它为了良好的兼容性,不能设置它必须要被加载的地址。一个Exe可能会加载多个DLL,如果系统“不小心”把某个DLL加载到0x70000000,那么如果有某个DLL设置了IMAGE_FILE_RELOCS_STRIPPED并将其首选加载地址正好也设置为0x70000000,那么系统为该Exe加载这个DLL将会失败。但是的确存在这样的文件,比如我电脑上ResourceCache.dll。DLL文件肯定要设置IMAGE_FILE_DLL。所以即使某个DLL文件的后缀名改了,你可以结合这个“特征码”来还原其真面目。
这儿我还要说一个认知的误区。 IMAGE_FILE_32BIT_MACHINE标志可以用于标志这个文件是适用于32位系统,但是如果仅仅通过该标志就去鉴别这个文件是32位文件还是64位文件是不正确的。我也不知道微软为什么设计了该标志而没有严格限制这个标志。我通过扫描我电脑里所有文件,发现了一个可能具有指导性的鉴别策略:
1 如果没有设置 IMAGE_FILE_32BIT_MACHINE但是设置了IMAGE_FILE_LARGE_ADDRESS_ AWARE的文件是64位文件。没有设置IMAGE_FILE_32BIT_MACHINE意味着该文件可能是64位程序,而设置了IMAGE_FILE_LARGE_ADDRESS_ AWARE,则说明该文件可以处理大于2G的空间的内存,则该文件是64位文件。如我本机上wwst64.exe。
2 除了以上判断之外的其他可能标志该文件是32位文件。
比如设置了IMAGE_FILE_32BIT_MACHINE而没有设置IMAGE_FILE_LARGE_ADDRESS_ AWARE,则说明这个文件可以处理2G以内内存空间,是32位文件;
比如没有设置IMAGE_FILE_32BIT_MACHINE和IMAGE_FILE_LARGE_ADDRESS_ AWARE,怎么解释呢?反正它不是64位文件,因为不能处理大于2G内存空间,那它只能是32位文件了。如我本机上文件sqlite3.dll。
比如设置了IMAGE_FILE_32BIT_MACHINE和IMAGE_FILE_LARGE_ADDRESS_ AWARE,那说明这是个可以处理大于2G内存空间的32位文件。如我本机上AcroBroker.exe。
接下来是 IMAGE_OPTIONAL_HEADER32 和 IMAGE_OPTIONAL_HEADER64
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// 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;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
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位版本和32位版本很类似:没有BaseOfCode属性;ImageBase、SizeofStackReserve、SizeOfStackCommit、SizeOfHeapReserve和SizeOfHeapCommit等5个属性由32位版的DWORD改成ULONGLONG。看下详细的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 位的结构是 96 字节 + IMAGE_DATA_DIRECTORYDataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
可见64 位的结构是 112 字节 + IMAGE_DATA_DIRECTORYDataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
我们观察这个32位版本结构体,可以看到该结构体包含两块数据:Standard fields和NT additional fields。我们可以猜想到,该结构体应该在第一个NT操作系统之前就存在了,只是当时其内容只有Standard fields(以后称为标准域)下的内容,后来NT系统增加了NT additional fields(以后称为扩展域)下元素。
此处需要特别注意一点,我们看两个在WinNT.h中定义的结构体
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;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
该结构给出了PE文件头的结构体布局,但是切记,这仅仅是布局。我们千万不要想当然的认为直接从PE头部开始将IMAGE_NT_HEADERS32(64)结构体大小的数据拷贝到该结构体对象中。
memcpy( &ImageNTHeader32,lpPEStart,sizeof(IMAGE_NT_HEADERS32 );// 这是错误的!!
为什么?因为一个文件中不一定有完整的IMAGE_OPTIONAL_HEADER32(64)结构体对象信息。原因在前面做了介绍,IMAGE_FILE_HEADER中字段SizeOfOptionalHeader指定了该文件中保存的“可选文件头”真实长度,我们应该根据该元素来给IMAGE_OPTIONAL_HEADER32(64)对象赋值。
我们的文件是使用IMAGE_OPTIONAL_HEADER32还是IMAGE_OPTIONAL_HEADER64结构体呢?可能有人会记起,我们在前面介绍了判断文件是32位还是64位的方法,我们是否可以通过该判断的结果来判断是哪种结构体呢?最开始我也是这么想的,后来我发现我电脑上 Microsoft Visual Studio 10.0\VC\lib\amd64\Microsoft.VisualC.STLCLR.dll 文件是个64位文件但是使用了IMAGE_OPTIONAL_HEADER32 结构体!!!是不是很惊讶!我不知道微软这么设计的原因,但是我知道了通过之前判断是否为64位文件来决定可选文件头结构体类型是错误的。那如何判断呢?
其实是有标记的。紧跟着 IMAGE_FILE_HEADER 结构体的肯定是 IMAGE_OPTIONAL_HEADER32(64)的Magic字段。如果该字段是0x010B,则是使用了IMAGE_OPTIONAL_HEADER32(称为PE32);如果是0x020B,则使用了IMAGE_OPTIONAL_HEADER32(称为PE32+)。切记 PE32 和 PE32+ 和这个文件是32位文件还是64位文件是没有关系的!它们是两种不同的概念!切记要分清。
以下是两张图示例,也可用这两张图结合上面的内容再分析回顾以下
PE32 0x0B:
PE32+ 020B:
现在我们将重心放到IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];这个数组元素,我在《可选文件头1》中对此有了点描述,而且我还说可选文件头大小要看这个数组元素的“位置”(而不是个数)来决定的。现在我来细说下。先看下微软的声明
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
DataDirectory保存了指向“块信息”的目录信息,其中包括偏移(除了IMAGE_DIRECTORY_ENTRY_SECURITY元素是相对文件偏移RA,其他都是相对虚拟首地址偏移RVA)和大小。如果某文件只包含IMAGE_DIRECTORY_ENTRY_EXPORT(0) 、IMAGE_DIRECTORY_ENTRY_IMPORT(1) 和IMAGE_DIRECTORY_ENTRY_BASERELOC(5)等三个目录,则IMAGE_DIRECTORY_ENTRY_EXCEPTION(2)、IMAGE_DIRECTORY_ENTRY_SECURITY(3)和IMAGE_DIRECTORY_ENTRY_SECURITY(4)的信息都要被填充0。于是IMAGE_FILE_HEADER::SizeOfOptionalHeader所指定的可选文件头大小为DataDirectory之前的元素总大小加上6(最后一个目录IMAGE_DIRECTORY_ENTRY_BASERELOC所在的位置5+1)*sizeof(IMAGE_DATA_DIRECTORY)。这就说明了为什么可选文件头大小是根据目录的位置而不是数量来决定的。
再回头看这个 PE32+ 的内容。根据 IMAGE_FILE_HEADER 结构中定义 SizeOfOptionalHeader 值是 0x00F0,前面说到 IMAGE_OPTIONAL_HEADER64 是 112 字节+ IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]。0x00F0 是240。240 - 112 = 128 字节。而一个IMAGE_DATA_DIRECTORY 是八个字节。128 = 8 X 8。也就是这里只有数组长度是8,而不是
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
同理上面 PE32图中可以看到 SizeOfOptionalHeader 的值是0x00E0, 0x00E0 - 96 = 224 - 96 = 128。
也就是他们的 DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] 长度是一样的,都只有八个。
================================================================================
下篇博文我们将详细说一下IMAGE_OPTIONAL_HEADER32和IMAGE_OPTIONAL_HEADER64中其他元素的意义。
下面这部分字段含义尚未特别验证,先记录下来
Magic字段是可选文件头幻数,0x10b是32位版,0x20b是64位版。注意该属性不能说明这个文件是64位文件还是32位文件,至于判断是多少位文件的方案我在《PE2》中已经有了说明。
MajorLinkerVersion和MinorLinkerVersion分别对应于链接器的版本号,比如我电脑上VS2005编译的文件的这两个版本号是8.0;VS2008编译的是9.0;VS2010编译的是10.0。
MajorOperatingSystemVersion和MinorOperatingSystemVersion是所需要的最低的系统版本号的主版本号和次版本号。我看了下我电脑上文件,基本上是4.0。
MajorImageVersion和MinorImageVersion是映像文件的主版本号和次版本号。注意:我们在资源中定义的文件版本号不是通过这两个属性来体现的!目前我也没找到在VC工程设置中可以设置这两个属性的地方。
Subsystem是该文件运行于的子系统信息。一般我们在windows平台上遇到的是2,它对应于IMAGE_SUBSYSTEM_WINDOWS_GUI。
MajorSubsystemVersion和MinorSubsystemVersion是子系统的版本号。熟悉windows的朋友应该知道,微软刚开始设计系统时,是设计成一个平台性质——可以运行3个子系统(OS/2、POSIX和Windows)的系统。这个就是这两个属性的由来。
SectionAlignment是当映像文件加载到内存中时节的对齐值,该大小使用字节来衡量的。它必须要大于我们之后介绍的FileAlignment。它的默认值是相应系统的页面大小。
FileAlignment 是映像文件节中的对齐值,它也是用字节来衡量的。英文文档中说该字段的值要在2^9 ~ 216之间,我扫描了下我的系统,发现我系统中文件并不是如此,特别是sys文件,它们的FileAlignment小于29(512)。
SizeOfCode是文件中代码段的总共大小。要注意一点,这个大小和.text的大小不一定一致,因为有些代码可能还保存在其他节中。如我电脑上AliAppLoader.exe文件,其SizeOfCode大小是0x1D600,而.text节大小只有0x1D400,另外的0x200是在.orpc这个节中。
SizeOfInitializedData是文件中所有已经初始化数据节的大小。和SizeOfCode一样,初始化数据不一定只在一个节中。
SizeOfUninitializedData是文件中所有未初始化数据节的大小。和SizeOfCode一样,未初始化数据不一定只在一个节中。
Win32VersionValue是保留字段,应该为0。那么目前这个字段就是程序不关心的了,我们可以利用这个位置保存一些私密信息。
SizeOfImage的官方说明是该映像文件被加载入内存时的大小,理论上它应该是SectionAlignment的倍数。但是实际并非如此,我发现我电脑上很多文件的该字段不是SectionAlignment的倍数,而有时SizeOfImage是该文件在磁盘上的大小。可以见得这个不是一个关键字段。
SizeOfHeaders的官方解释是MS-DOS占位程序、PE文件头和节头的总大小,且其应该是FileAlignment的倍数。但是实际上,我发现我电脑上很多文件的该字段并非FileAlignment的倍数。
CheckSum字段是映像文件的校验和。其计算算法保存在imagehlp.dll中,导出函数名为CheckSumMappedFile。我发现我电脑上很多文件的该PE字段和计算出来的不等。官方解释说当驱动程序、在引导时被加载的Dll以及加载到关键windows进程中的DLL都需要校验该字段以确认其合法性。
SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve和SizeOfHeapCommit分别对应于保留的栈大小、提交的栈大小、保留的堆大小和提交的堆大小。
LoaderFlags字段是保留字段,应该为0,当然你可以不把它设为0。
NumberOfRvaAndSizes是用来指明DataDirectory元素的个数。这儿我们要说一下,我们在IMAGE_FILE_HEADER::SizeOfOptionalHeader得到了可选文件头的大小,而影响可选文件头大小的就是DataDirectory元素的个数(NumberOfRvaAndSizes),那么IMAGE_FILE_HEADER::SizeOfOptionalHeader和NumberOfRvaAndSizes之间应该存在着一种换算关系。
DllCharacteristics是属性字段,我们看个官方说明
Constant | Value | Description |
| 0x0001 | Reserved, must be zero. |
| 0x0002 | Reserved, must be zero. |
| 0x0004 | Reserved, must be zero. |
| 0x0008 | Reserved, must be zero. |
IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE | 0x0040 | DLL can be relocated at load time. |
IMAGE_DLL_CHARACTERISTICS_FORCE_INTEGRITY | 0x0080 | Code Integrity checks are enforced. |
IMAGE_DLL_CHARACTERISTICS_NX_COMPAT | 0x0100 | Image is NX compatible. |
IMAGE_DLLCHARACTERISTICS_ NO_ISOLATION | 0x0200 | Isolation aware, but do not isolate the image. |
IMAGE_DLLCHARACTERISTICS_ NO_SEH | 0x0400 | Does not use structured exception (SE) handling. No SE handler may be called in this image. |
IMAGE_DLLCHARACTERISTICS_ NO_BIND | 0x0800 | Do not bind the image. |
| 0x1000 | Reserved, must be zero. |
IMAGE_DLLCHARACTERISTICS_ WDM_DRIVER | 0x2000 | A WDM driver. |
IMAGE_DLLCHARACTERISTICS_ TERMINAL_SERVER_AWARE | 0x8000 | Terminal Server aware. |
MAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE是说Dll可以在加载时被重定向,我发现我电脑上文件SDKDBLib.dll是特例,它没有设置这个属性,这个文件也没有设置IMAGE_DLLCHARACTERISTICS_ NO_SEH,即该文件不使用SEH。
以上部分内容未特别验证,在此用一个例子分析一下看各数据是多少:
前面说到 010B 是PE32 ,结构如下,长度是 96字节 + 数组,根据上图将数据用注释卸载后面
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic; // 0x010B
BYTE MajorLinkerVersion; // 0x0E
BYTE MinorLinkerVersion; // 0x00
DWORD SizeOfCode; // 0x0017b400
DWORD SizeOfInitializedData; // 0x00147e00
DWORD SizeOfUninitializedData; // 0x00000000
DWORD AddressOfEntryPoint; // 0x0010dc5b
DWORD BaseOfCode; // 0x00001000
DWORD BaseOfData; // 0x0017d000
//
// NT additional fields.
//
DWORD ImageBase; // 0x00400000
DWORD SectionAlignment; // 0x00000100
DWORD FileAlignment; // 0x00000200
WORD MajorOperatingSystemVersion; // 0x0005
WORD MinorOperatingSystemVersion; // 0x0001
WORD MajorImageVersion; // 0x0001
WORD MinorImageVersion; // 0x0000
WORD MajorSubsystemVersion; // 0x0005
WORD MinorSubsystemVersion; // 0x0001
DWORD Win32VersionValue; // 0x00000000
DWORD SizeOfImage; // 0x002C8000
DWORD SizeOfHeaders; // 0x00000400
DWORD CheckSum; // 0x002BC111
WORD Subsystem; // 0x0002
WORD DllCharacteristics; // 0x8140
DWORD SizeOfStackReserve; // 0x00100000
DWORD SizeOfStackCommit; // 0x00000100
DWORD SizeOfHeapReserve; // 0x00010000
DWORD SizeOfHeapCommit; // 0x00000100
DWORD LoaderFlags; // 0x00000000
DWORD NumberOfRvaAndSizes; // 0x00000010
//说明下面这个数组长度是16,占用 16 X 8 = 128 字节。和前面分析吻合
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
===========================================================================
接上面内容之后就是一些节信息,以下图为例有7个节,每个节40字节(至于为什么是 7 和40稍后再讲)。分布如下:
下面说为什么是 7 和 40
再回顾前面说的定义
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;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
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;
可以看到 NumberOfSections 的位置是在 IMAGE_NT_HEADERS32(64)结构的第6.7字节(从0开始)。也就是 0x0007。这就是7的来历。
再看下保存节信息的结构体IMAGE_SECTION_HEADER ,和上图对照看就容易理解了。
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
可以看到上面结构就是40个字节。
Name是使用UTF-8编码,以\0结尾,大小为8Byte的字符串。它是节的名称,如上图中的.text、.data和.rsrc。看到Name的长度为8,你是不是在想到:小于,等于和大于?现在我们就讨论下如果节名长度小于、等于和大于8的情况。
节名长度小于8 的情况。这个场景最简单了,不足的位用\0填充。
节名长度等于8的情况。因为结构大小是固定的,所以我们不可能找到一个空余的位置放置\0,那么这8byte就全部填充名字了。
以下是我收集的节名信息
IMAGESETCTIONNAME g_ImageSectionName[] = {
{'.','b','s','s','\0','\0','\0','\0'},
{'.','c','o','r','m','e','t','a'},
{'.','d','a','t','a','\0','\0','\0'},
{'.','d','e','b','u','g','$','F'},
{'.','d','e','b','u','g','$','P'},
{'.','d','e','b','u','g','$','S'},
{'.','d','e','b','u','g','$','T'},
{'.','d','r','e','c','t','v','e'},
{'.','e','d','a','t','a','\0','\0'},
{'.','i','d','a','t','a','\0','\0'},
{'.','i','d','l','s','y','m','\0'},
{'.','p','d','a','t','a','\0','\0'},
{'.','r','d','a','t','a','\0','\0'},
{'.','r','e','l','o','c','\0','\0'},
{'.','r','s','r','c','\0','\0','\0'},
{'.','s','b','s','s','\0','\0','\0'},
{'.','s','d','a','t','a','\0','\0'},
{'.','s','r','d','a','t','a','\0'},
{'.','s','x','d','a','t','a','\0'},
{'.','t','e','x','t','\0','\0','\0'},
{'.','t','l','s','\0','\0','\0','\0'},
{'.','t','l','s','$','\0','\0','\0'},
{'.','v','s','d','a','t','a','\0'},
{'.','x','d','a','t','a','\0','\0'}
};
像.debug$F这样的就是占用了8个byte的
节名长度大于8的情况。这个场景怎么办?结构体大小固定,我们不能越界写!那我们只能在其他地方去写了,然后在这个位置保存我们写入数据的偏移即可!是的,PE规范就是采用的这样的思想,只是稍微有点不同:以/开始,其后跟着一个表示偏移量的十进制数字字符串,如/4(0x2f 0 0x34 0x00 0x00 0x00 0x00 0x00 0x00)。这个数字是相对字符串表起始位置的偏移RA,我们的真实的节名就保存在字符串表中。我在我电脑上找到一个这样的文件avcodec-52.dll。我们先看Stud_PE的分析结果
可以看到Stud_PE对第5节的名字的解析是错的的,那正确的是什么?现在我们要回顾《PE文件和COFF文件格式分析——签名、COFF文件头和可选文件头1》,该文中我埋了一个伏笔,我把段提出来
PointerToSymbolTable是0x00000000,该字段记录了该PE文件中调试信息符号表。由于符号表信息是在程序运行时不需要加载进入内存的,所以这个偏移使用的是相对文件头偏移RA。目前微软推荐是:将映像文件调试符号表信息独立的放在PDB文件中,所以不会在PE文件中再保存调试符号表信息,于是这个字段应该为0。当然这并不是硬性要求,我发现我电脑上就存在很多该字段不为0的文件。刚开始时我也不是很明白它们为什么要使用这个字段,特别是其指向的字符表个数(NumberOfSymbols)为0!!你说既然大小为0,那你指向有什么意思呢?其实这种设计是非常有深意的,我会在之后的章节中介绍这种深意。
NumberOfSymbols是0x00000000,该字段记录了该PE文件中调试信息符号表元素个数。对于映像文件,该字段为0(非硬性要求),,理由在PointerToSymbolTable中已经说明。通过NumberOfSymbols和PointerToSymbolTable,我们可以找到字符串表起始位置,因为字符串表紧跟在符号表之后。
看了这段后,我想你应该对那个伏笔有了解答。想想也挺有意思,微软不推荐在文件中包含调试信息,于是PointerToSymbolTable和NumberOfSymbols就是应该废弃的。可是这两个数据却关联着字符串表。字符串表大部分时候可以不使用,但是如果DLL中存在超过8byte的节名时又不得不用,于是只好让PointerToSymbolTable指向字符串表开始,而NumberOfSymbols为0。
现在我们来看下上面那个Stud_PE分析出错的文件的文件头信息
我们去0x001c1600+4的位置去寻找该节名字,该节名位.eh_frame,长度是9byte。
==================================================================================
以下是我的测试结果相关图:
/4应该就是偏移。四个字节
0x00A97600 的来历:
===============================================================================
这儿要特别说明一点,可执行文件的节名长度是不会超过8的。即使obj文件中节名存在超过8的,也会在链接进入可执行文件时被截断。
VirtualSize属性是节加载进入内存后,节在内存中的大小。如果它比SizeOfRawData大,则多余的部分是用0x00填充的。这个性质非常重要,它是关系到RVA和RA之间换算的一个基础。
VirtualAddress属性是节加载进入内存后其第一个字节相对于映像基址的偏移(RVA)。
SizeOfRawData是磁盘映像文件中该节的已初始化数据的大小。对于可执行文件来说,它必须是IMAGE_OPTIONAL_HEADER32(64)::FileAlignment的倍数。.如果该节中仅包含未初始化的数据,则该字段为0。
PointerToRawData是磁盘映像文件中该节相对于映像基址的偏移(RA)。对于可执行文件来说,它的值要是IMAGE_OPTIONAL_HEADER32(64)::FileAlignment的倍数。如果该节中仅包含未初始化的数据,则该字段为0。
PointerToRelocations指向节中重定位项开头的相对映像基址的偏移(RA)。可执行文件或者不能重定向的文件该字段应该为0。
PointerToLinenumbers指向节中行号项的相对映像基址偏移(RA)。因为已经不推荐在PE文件中包含调试信息,所以该字段一般为0。
NumberOfRelocations是节中重定位项的个数。可执行文件和不可以重定位的文件该字段为0。
NumberOfLinenumbers是节中行号项的个数。因为已不推荐PE文件中包含调试信息,所以该字段一般为0。
Characteristics描述节的特征。
Flag | Value | Description |
| 0x00000000 | Reserved for future use. |
| 0x00000001 | Reserved for future use. |
| 0x00000002 | Reserved for future use. |
| 0x00000004 | Reserved for future use. |
IMAGE_SCN_TYPE_NO_PAD | 0x00000008 | The section should not be padded to the next boundary. This flag is obsolete and is replaced by IMAGE_SCN_ALIGN_1BYTES. This is valid only for object files. |
| 0x00000010 | Reserved for future use. |
IMAGE_SCN_CNT_CODE | 0x00000020 | The section contains executable code. |
IMAGE_SCN_CNT_INITIALIZED_DATA | 0x00000040 | The section contains initialized data. |
IMAGE_SCN_CNT_UNINITIALIZED_ DATA | 0x00000080 | The section contains uninitialized data. |
IMAGE_SCN_LNK_OTHER | 0x00000100 | Reserved for future use. |
IMAGE_SCN_LNK_INFO | 0x00000200 | The section contains comments or other information. The .drectve section has this type. This is valid for object files only. |
| 0x00000400 | Reserved for future use. |
IMAGE_SCN_LNK_REMOVE | 0x00000800 | The section will not become part of the image. This is valid only for object files. |
IMAGE_SCN_LNK_COMDAT | 0x00001000 | The section contains COMDAT data. For more information, see section 5.5.6, “COMDAT Sections (Object Only).” This is valid only for object files. |
IMAGE_SCN_GPREL | 0x00008000 | The section contains data referenced through the global pointer (GP). |
IMAGE_SCN_MEM_PURGEABLE | 0x00020000 | Reserved for future use. |
IMAGE_SCN_MEM_16BIT | 0x00020000 | For ARM machine types, the section contains Thumb code. Reserved for future use with other machine types. |
IMAGE_SCN_MEM_LOCKED | 0x00040000 | Reserved for future use. |
IMAGE_SCN_MEM_PRELOAD | 0x00080000 | Reserved for future use. |
IMAGE_SCN_ALIGN_1BYTES | 0x00100000 | Align data on a 1-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_2BYTES | 0x00200000 | Align data on a 2-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_4BYTES | 0x00300000 | Align data on a 4-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_8BYTES | 0x00400000 | Align data on an 8-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_16BYTES | 0x00500000 | Align data on a 16-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_32BYTES | 0x00600000 | Align data on a 32-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_64BYTES | 0x00700000 | Align data on a 64-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_128BYTES | 0x00800000 | Align data on a 128-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_256BYTES | 0x00900000 | Align data on a 256-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_512BYTES | 0x00A00000 | Align data on a 512-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_1024BYTES | 0x00B00000 | Align data on a 1024-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_2048BYTES | 0x00C00000 | Align data on a 2048-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_4096BYTES | 0x00D00000 | Align data on a 4096-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_8192BYTES | 0x00E00000 | Align data on an 8192-byte boundary. Valid only for object files. |
IMAGE_SCN_LNK_NRELOC_OVFL | 0x01000000 | The section contains extended relocations. |
IMAGE_SCN_MEM_DISCARDABLE | 0x02000000 | The section can be discarded as needed. |
IMAGE_SCN_MEM_NOT_CACHED | 0x04000000 | The section cannot be cached. |
IMAGE_SCN_MEM_NOT_PAGED | 0x08000000 | The section is not pageable. |
IMAGE_SCN_MEM_SHARED | 0x10000000 | The section can be shared in memory. |
IMAGE_SCN_MEM_EXECUTE | 0x20000000 | The section can be executed as code. |
IMAGE_SCN_MEM_READ | 0x40000000 | The section can be read. |
IMAGE_SCN_MEM_WRITE | 0x80000000 | The section can be written to. |
IMAGE_SCN_LNK_NRELOC_OVFL 标志表明节中重定位项的个数超出了节头中为每个节保留的16 位所能表示的范围。如果设置了此标志并且节头中的NumberOfRelocations 域的值是0xffff,那么实际的重定位项个数被保存在第一个重定位项的VirtualAddress 域(32 位)中。如果设置了IMAGE_SCN_LNK_NRELOC_OVFL
标志但节中的重定位项的个数少于0xffff,则表示出现了错误。