PE结构是Windows
系统下最常用的可执行文件格式,理解PE文件格式不仅可以理解操作系统的加载流程,还可以更好的理解操作系统对进程和内存相关的管理知识,DOS头是PE文件开头的一个固定长度的结构体,这个结构体的大小为64字节(0x40)。DOS头包含了很多有用的信息,该信息可以让Windows操作系统使用正确的方式加载可执行文件。从DOS文件头IMAGE_DOS_HEADER
的e_lfanew
字段向下偏移003CH
的位置,就是真正的PE文件头的位置,该文件头是由IMAGE_NT_HEADERS
结构定义的,IMAGE_NT_HEADERS是PE文件格式的一部分,它包含了PE头和可选头的信息,用于描述PE文件的结构和属性。
2.2 DOS文件头详细解析 DOS头是PE文件开头的一个固定长度的结构体,这个结构体的大小为64字节(0x40)。DOS头包含了很多有用的信息,该信息可以让Windows操作系统使用正确的方式加载可执行文件。一个DOS头通常会包含以下一些主要信息:
Magic Number: 接下来64字节
的文件内容的开始是以MZ(Mark Zbikowski)
2个字符(即0x4D, 0x5A)
开头,被称为DOS
签名。
PE头偏移:DOS头中的e_lfanew
(这是一个类型为LONG的成员)指示了PE头的偏移量,即PE头的起始位置距离DOS头的偏移量,Windows操作系统根据DOS头的这个属性来定位PE头的位置。
DOS头结束标识:保留用于以后增加的内容, 用于确认DOS头的结束,通常被赋值给字节0x0B。
如上图所示,图中的4D5A
则表示这是一个PE文件,其下08010000
则代表DOS头的最后一个数据集e_lfanew
字段,该字段指向了PE头的开始50450000
用于表示NT头的其实位置,而途中的英文单词则是一个历史遗留问题,在某些时候可通过删除此标识已让PE文件缩小空间占用,总的来说DOS头是PE文件中的一个重要的标志,它使得Windows操作系统能够在正确的位置开始加载可执行文件。由于DOS头中包含了PE头的偏移位置,Windows操作系统可以很容易地找到PE头,并通过PE头来加载程序并执行。
DOS头结构时PE文件中的重要组成部分,PE文件中的DOS部分由MZ格式的文件头和可执行代码部分组成,可执行代码被称为DOS块(DOS stub),MZ格式的文件头由IMAGE_DOS_HEADER
结构定义,在C语言头文件winnt.h
中有对这个DOS结构详细定义,如下所示:
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; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4 ]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10 ]; LONG e_lfanew; } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
在DOS文件头中,第一个字段e_magic
被定义为MZ
,标志着DOS文件的开头部分,最后一个字段e_lfanew
则指明了PE文件的开头位置,现在来说除了第一个字段和最后一个字段有些用处,其他字段几乎已经废弃,当读者通过调用OpenPeFile
打开一个PE文件时,则下一步我们需要实现对PE文件有效性及位数的判断,并以此作为参考在后续的解析中使用不同的变量长度。
首先将镜像转换为PIMAGE_DOS_HEADER
格式,并通过pDosHead->e_magic
属性找到PIMAGE_NT_HEADERS
结构,然后判断其是否符合PE文件规范,这里需要注意32位于64位PE结构所使用的的结构定义略有不同,代码中已经对其进行了区分。
BOOL IsPeFile (HANDLE ImageBase, BOOL Is64 = FALSE) { PIMAGE_DOS_HEADER pDosHead = NULL ; if (ImageBase == NULL ) return FALSE; pDosHead = (PIMAGE_DOS_HEADER)ImageBase; if (IMAGE_DOS_SIGNATURE != pDosHead->e_magic) return FALSE; if (Is64 == TRUE) { PIMAGE_NT_HEADERS64 pNtHead64 = NULL ; pNtHead64 = (PIMAGE_NT_HEADERS64)((DWORD64)pDosHead + pDosHead->e_lfanew); if (pNtHead64->Signature != IMAGE_NT_SIGNATURE) return FALSE; } else if (Is64 == FALSE) { PIMAGE_NT_HEADERS pNtHead32 = NULL ; pNtHead32 = (PIMAGE_NT_HEADERS)((DWORD)pDosHead + pDosHead->e_lfanew); if (pNtHead32->Signature != IMAGE_NT_SIGNATURE) return FALSE; } return TRUE; } int main (int argc, char * argv[]) { BOOL PE = IsPeFile(OpenPeFile("c://pe/x86.exe" ), 0 ); if (PE == TRUE) { printf ("程序是标准的PE文件 \n" ); } else { printf ("非标准程序 \n" ); } system("pause" ); return 0 ; }
运行此段代码,则读者可以看到如下图所示的输出结果,程序会首先判断读入文件的pDosHead->e_magic
是否为IMAGE_DOS_SIGNATURE
用以验证是否为DOS头,接着通过IMAGE_DOS_HEADER
的e_lfanew
值得到NT头
部位置,并以此进一步判断是否为PE文件;
接下来则是读入PE文件中DOS头的重点部分,读者通过DosHeader
指针,即可依次遍历出IMAGE_DOS_HEADER
结构中的所有参数信息,这段代码可以总结为如下案例;
int main (int argc, char * argv[]) { BOOL PE = IsPeFile(OpenPeFile("c://pe/x86.exe" ), 0 ); if (PE == TRUE) { printf ("\t\t\t 十六进制 \t 十进制 \n" ); printf ("DOS标志: %08X \t %08d \n" , DosHeader->e_magic, DosHeader->e_magic); printf ("文件最后一页的字节数: %08X \t %08d \n" , DosHeader->e_cblp, DosHeader->e_cblp); printf ("文件中的页面: %08X \t %08d \n" , DosHeader->e_cp, DosHeader->e_cp); printf ("重定位: %08X \t %08d \n" , DosHeader->e_crlc, DosHeader->e_crlc); printf ("段落中标题的大小: %08X \t %08d \n" , DosHeader->e_cparhdr, DosHeader->e_cparhdr); printf ("至少需要额外段落: %08X \t %08d \n" , DosHeader->e_minalloc, DosHeader->e_minalloc); printf ("所需的最大额外段落数: %08X \t %08d \n" , DosHeader->e_maxalloc, DosHeader->e_maxalloc); printf ("初始(相对)SS值: %08X \t %08d \n" , DosHeader->e_ss, DosHeader->e_ss); printf ("初始SP值: %08X \t %08d \n" , DosHeader->e_sp, DosHeader->e_sp); printf ("校验和: %08X \t %08d \n" , DosHeader->e_csum, DosHeader->e_csum); printf ("初始IP值: %08X \t %08d \n" , DosHeader->e_ip, DosHeader->e_ip); printf ("初始(相对)CS值: %08X \t %08d \n" , DosHeader->e_cs, DosHeader->e_cs); printf ("重新定位表的文件地址: %08X \t %08d \n" , DosHeader->e_lfarlc, DosHeader->e_lfarlc); printf ("叠加编号: %08X \t %08d \n" , DosHeader->e_ovno, DosHeader->e_ovno); printf ("保留字: %08X \t %08d \n" , DosHeader->e_res, DosHeader->e_res); printf ("OEM标识符 %08X \t %08d \n" , DosHeader->e_oemid, DosHeader->e_oemid); printf ("OEM信息 %08X \t %08d \n" , DosHeader->e_res2, DosHeader->e_res2); printf ("PE指针: %08X \t %08d \n" , DosHeader->e_lfanew, DosHeader->e_lfanew); } else { printf ("非标准程序 \n" ); } system("pause" ); return 0 ; }
编译并运行上述代码片段,则读者可看到如下图所示的输出效果,此时DOS头部数据将被全部完整的输出;
2.3 PE文件头详细解析 从DOS文件头IMAGE_DOS_HEADER
的e_lfanew
字段向下偏移003CH
的位置,就是真正的PE文件头的位置,该文件头是由IMAGE_NT_HEADERS
结构定义的,IMAGE_NT_HEADERS是PE文件格式的一部分,它包含了PE头和可选头的信息,用于描述PE文件的结构和属性。
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
IMAGE_NT_HEADERS由IMAGE_NT_SIGNATURE
(标识符)和IMAGE_FILE_HEADER
(文件头)组成。其中,IMAGE_NT_SIGNATURE
用于标识该文件是否为有效的PE文件,IMAGE_FILE_HEADER
则用于描述可执行文件的基本结构信息,包括机器类型、段的数量、时间戳、符号表指针、符号表数量、可选头大小以及文件的各种标志和属性等。
如上_IMAGE_NT_HEADERS
文件头的第一个DWORD
是一个标志,默认情况下它被定义为00004550h
也就是P,E
两个字符另外加上两个零,而大部分的文件属性由标志后面的IMAGE_FILE_HEADER
和IMAGE_OPTIONAL_HEADER32
结构来定义。
我们跟进IMAGE_FILE_HEADER
这个结构,文件头结构体IMAGE_FILE_HEADER
是IMAGE_NT_HEADERS
结构体中的一个结构体,紧接在PE标识符的后面,IMAGE_FILE_HEADER
结构体的大小为20字节,起始位置为0x000000CC
结束位置在0x000000DF
,这个IMAEG_FILE_HEADER
结构体中包含了PE文件的大部分基础信息其结构的定义如下:
#define _IMAGE_FILE_HEADER 20 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;
此外IMAGE_NT_HEADERS
还包含了IMAGE_OPTIONAL_HEADER
可选头的信息,用于描述PE文件的高级结构信息,包括各种代码段、数据段、栈大小、堆大小、程序入口点、镜像基址等等。
我们继续跟进_IMAGE_NT_HEADERS
结构体里面的第二个结构IMAGE_OPTINAL_HEADER
,该头结构非常重要要,里面存储着程序的数据目录表,可选头紧挨着文件头,文件头的结束位置在0x000000DF
,那么可选头的起始位置为0x000000E0
,可选头的大小在文件头中已经给出,其大小为0x00E0
字节,其结束位置为0x000000E0 + 0x00E0 – 1 = 0x000001BF
,可选头非常容易辨别,只需要找到PE字眼就是了。
可选头是对文件头的一个扩展,文件头主要描述文件的相关信息,而可选头主要用来管理PE文件被操作系统装载时所需要的信息,该头是有32位版本与64位版本之分的,其实IMAGE_OPTIONAL_HEADER
是一个宏,定义如下所示;
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b #define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b #define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107 #ifdef _WIN64 typedef IMAGE_OPTIONAL_HEADER64 IMAGE_OPTIONAL_HEADER;typedef PIMAGE_OPTIONAL_HEADER64 PIMAGE_OPTIONAL_HEADER;#define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR64_MAGIC #else typedef IMAGE_OPTIONAL_HEADER32 IMAGE_OPTIONAL_HEADER;typedef PIMAGE_OPTIONAL_HEADER32 PIMAGE_OPTIONAL_HEADER;#define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR32_MAGIC #endif
32位版本和64位版本的选择是根据是否定义了_WIN64
而决定的,这里只讨论其32位的版本,IMAGE_OPTIONAL_HEADER32
的定义如下所示;
typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; 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;
从上方结构体定义中可知,最后一个结构属性IMAGE_DATA_DIRECTORY
其又指向了数据目录列表,该表由16个相同的IMAGE_DATA_DIRECTORY
结构组成,这16个数据目录结构定义很简单,仅仅指出了某种数据的位置和长度,该结构的定义如下;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
有了上方的解析流程,读者应该能理解如何实现分析PE头了,首先读者找到DOS
头,并从该头部找到NT
头,当读者得到了NT头就可以根据NT头向下分别解析FileHeader
及OptionalHeader
中的参数,根据参数定义依次输出即可得到所有的NT头部数据,其完整代码如下所示;
int main (int argc, char * argv[]) { BOOL PE = IsPeFile(OpenPeFile("c://pe/x86.exe" ), 0 ); if (PE == TRUE) { printf ("\t\t\t 十六进制 \t 十进制 \n" ); printf ("NT标志: 0x%08X \t %08d \n" , NtHeader->Signature, NtHeader->Signature); printf ("运行平台: 0x%08X \t %08d \n" , NtHeader->FileHeader.Machine, NtHeader->FileHeader.Machine); printf ("区段数目: 0x%08X \t %08d \n" , NtHeader->FileHeader.NumberOfSections, NtHeader->FileHeader.NumberOfSections); printf ("时间日期标志: 0x%08X \t %08d \n" , NtHeader->FileHeader.TimeDateStamp, NtHeader->FileHeader.TimeDateStamp); printf ("特征值: 0x%08X \t %08d \n" , NtHeader->FileHeader.Characteristics, NtHeader->FileHeader.Characteristics); printf ("可选头部大小: 0x%08X \t %08d \n" , NtHeader->FileHeader.SizeOfOptionalHeader, NtHeader->FileHeader.SizeOfOptionalHeader); printf ("文件符号标志: 0x%08X \t %08d \n" , NtHeader->FileHeader.NumberOfSymbols, NtHeader->FileHeader.NumberOfSymbols); printf ("文件符号指针: 0x%08X \t %08d \n" , NtHeader->FileHeader.PointerToSymbolTable, NtHeader->FileHeader.PointerToSymbolTable); printf ("入口点: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.AddressOfEntryPoint, NtHeader->OptionalHeader.AddressOfEntryPoint); printf ("镜像基址: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.ImageBase, NtHeader->OptionalHeader.ImageBase); printf ("镜像大小: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.SizeOfImage, NtHeader->OptionalHeader.SizeOfImage); printf ("代码基址: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.BaseOfCode, NtHeader->OptionalHeader.BaseOfCode); printf ("内存对齐: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.SectionAlignment, NtHeader->OptionalHeader.SectionAlignment); printf ("文件对齐: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.FileAlignment, NtHeader->OptionalHeader.FileAlignment); printf ("子系统: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.Subsystem, NtHeader->OptionalHeader.Subsystem); printf ("首部大小: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.SizeOfHeaders, NtHeader->OptionalHeader.SizeOfHeaders); printf ("校验和: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.CheckSum, NtHeader->OptionalHeader.CheckSum); printf ("RVA 数及大小: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.NumberOfRvaAndSizes, NtHeader->OptionalHeader.NumberOfRvaAndSizes); printf ("主操作系统版本: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.MajorOperatingSystemVersion, NtHeader->OptionalHeader.MajorOperatingSystemVersion); printf ("从操作系统版本: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.MinorOperatingSystemVersion, NtHeader->OptionalHeader.MinorOperatingSystemVersion); printf ("主映像版本: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.MajorImageVersion, NtHeader->OptionalHeader.MajorImageVersion); printf ("从映像版本: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.MinorImageVersion, NtHeader->OptionalHeader.MinorImageVersion); printf ("主子系统版本: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.MajorSubsystemVersion, NtHeader->OptionalHeader.MajorSubsystemVersion); printf ("从子系统版本: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.MinorSubsystemVersion, NtHeader->OptionalHeader.MinorSubsystemVersion); printf ("Win32版本: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.Win32VersionValue, NtHeader->OptionalHeader.Win32VersionValue); printf ("DLL标识: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.DllCharacteristics, NtHeader->OptionalHeader.DllCharacteristics); printf ("SizeOfStackReserve: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.SizeOfStackReserve, NtHeader->OptionalHeader.SizeOfStackReserve); printf ("SizeOfStackCommit: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.SizeOfStackCommit, NtHeader->OptionalHeader.SizeOfStackCommit); printf ("SizeOfHeapReserve: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.SizeOfHeapReserve, NtHeader->OptionalHeader.SizeOfHeapReserve); printf ("SizeOfHeapCommit: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.SizeOfHeapCommit, NtHeader->OptionalHeader.SizeOfHeapCommit); printf ("LoaderFlags: 0x%08X \t %08d \n" , NtHeader->OptionalHeader.LoaderFlags, NtHeader->OptionalHeader.LoaderFlags); } else { printf ("非标准程序 \n" ); } system("pause" ); return 0 ; }
当程序被运行后,则可输出NT头中针对FileHeader
及OptionalHeader
表中的所有内容,输出效果图如下图所示;
此外针对数据目录表的枚举,也将变得很容易实现,一般而言通过NtHeader->OptionalHeader.NumberOfRvaAndSizes
读者可得到数据目录表的数量,当得到了数据目录表的数量后则可通过循环的方式依次输出DataDirectory[x]
数组中每一个变量的参数信息,根据每次循环的不同则输出不同的参数;
DWORD RVAtoFOA (DWORD rva) { auto SectionTables = IMAGE_FIRST_SECTION(NtHeader); WORD Count = NtHeader->FileHeader.NumberOfSections; for (int i = 0 ; i < Count; ++i) { DWORD Section_Start = SectionTables[i].VirtualAddress; DWORD Section_Ends = SectionTables[i].VirtualAddress + SectionTables[i].SizeOfRawData; if (rva >= Section_Start && rva < Section_Ends) { return rva - SectionTables[i].VirtualAddress + SectionTables[i].PointerToRawData; } } return -1 ; } int main (int argc, char * argv[]) { BOOL PE = IsPeFile(OpenPeFile("c://pe/x86.exe" ), 0 ); if (PE == TRUE) { int Data_Size = NtHeader->OptionalHeader.NumberOfRvaAndSizes; printf ("编号 \t 目录RVA \t 目录FOA \t Size长度(十进制) \t Size长度(十六进制) \t 功能描述 \n" ); for (int x = 0 ; x < Data_Size; x++) { printf ("%03d \t 0x%08X \t 0x%08X \t %08d \t\t 0x%08X \t\t" , x + 1 , NtHeader->OptionalHeader.DataDirectory[x].VirtualAddress, RVAtoFOA(NtHeader->OptionalHeader.DataDirectory[x].VirtualAddress), NtHeader->OptionalHeader.DataDirectory[x].Size, NtHeader->OptionalHeader.DataDirectory[x].Size); switch (x) { case 0 : printf ("Export symbols \n" ); break ; case 1 : printf ("Import symbols \n" ); break ; case 2 : printf ("Resources \n" ); break ; case 3 : printf ("Exception \n" ); break ; case 4 : printf ("Security \n" ); break ; case 5 : printf ("Base relocation \n" ); break ; case 6 : printf ("Debug \n" ); break ; case 7 : printf ("Copyright string \n" ); break ; case 8 : printf ("Globalptr \n" ); break ; case 9 : printf ("Thread local storage (TLS) \n" ); break ; case 10 : printf ("Load configuration \n" ); break ; case 11 : printf ("Bound Import \n" ); break ; case 12 : printf ("Import Address Table \n" ); break ; case 13 : printf ("Delay Import \n" ); break ; case 14 : printf ("COM descriptor \n" ); break ; case 15 : printf ("NoUse \n" ); break ; default : printf ("None \n" ); break ; } } } else { printf ("非标准程序 \n" ); } system("pause" ); return 0 ; }
运行上述程序,则读者可看到如下图所示的输出信息,至此针对数据目录表的枚举也就实现了;