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; // DOS的头部
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
被定义为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;
// 将映射文件转为DOS结构,并判断开头是否为MZ
pDosHead = (PIMAGE_DOS_HEADER)ImageBase;
if (IMAGE_DOS_SIGNATURE != pDosHead->e_magic)
return FALSE;
if (Is64 == TRUE)
{
// 根据 IMAGE_DOS_HEADER 的 e_lfanew 的值得到 64位 NT 头的位置
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)
{
// 根据 IMAGE_DOS_HEADER 的 e_lfanew 的值得到 32位 NT 头的位置
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");