PE文件(二)PE头字段说明

PE头字段 = DOS头 + PE标记 + 标准PE头 + 可选PE头

我们今天分析一下PE头字段中所有重要成员的含义

DOS头

DOS头中我们需要去分析的是如下两个成员:

1.WORD e_magic:MZ标记,用于判断是否为可执行文件,即如果显示4D 5A,说明该文件是一个可执行文件,如:.sys/.dll/.exe等

2.DWORD e_lfanew:NT头(PE签名)相对于文件首地址的偏移,用于定位PE文件真正开始的地址,但该值不是固定的

PE标记

该PE标记的地址即e_Ifanew指向的地址

PE标记中我们要分析的成员只有这一个:

DWORD Signature;  该值存储的是PE两个字,用来表示该文件是PE文件,因此一个可执行文件应该同时满足MZ标记和PE标记,如果这两点不满足可能被修改过,或者就不是一个可执行文件

标准PE头

标准PE头中我们需要去分析的是如下几个成员:

1.WORD Machine:用于约定该文件能在什么样的CPU上运行:如果是0x00表示能在任何CPU上执行,如果是0x14C表示能在386及后续CPU上执行

2.WORD NumberOfSections:用于告知该PE文件中分节的总数(不包括DOS头、NT头、节表):如果要新增节或者合并节,就要修改这个值。

4.DWORD TimeDateStamp:时间戳:文件的创建时间(和操作系统的创建时间无关),当程序被编译器编译时,由编译器填写的

时间戳的使用案例:比如现在要给一个.exe文件加壳,有些加壳软件不光要提供给.exe文件,还需要其对应的.map文件。.map文件中记录了此.exe文件中的所有的函数的名字、地址、参数等信息。这两个文件由编译器编译源文件时生成,.map和.exe的时间戳是一致的。如果现在这个.exe文件被反复修改了以后,.exe文件的时间戳就会改变,但是map的时间戳没有更新,这会导致exe和map不同步。此时如果加壳还是按照map中的记录信息去加壳,就可能出现错误。所以很多加壳软件在加壳之前会检查exe和map文件的时间戳是否一致

5. WORD SizeOfOptionalHeader:可选PE头的大小,该大小不确定:32位PE文件默认E0h,64位PE文件默认为F0h,但其大小可以自定义

6. WORD Characteristics:特征:其大小16位的每个位都表示不同的特征,可执行文件值一般都是0x010F。当某位为1时,表示此文件有此位对应的特征,为0时表示没有此特征

characteristic每一位表示的含义如下图:

把内存中的值读出来后,即读作0x010F,此时化成二进制,第七位省略,其他的每一位都表示一个特征,如果为1,则表示此文件有此位对应的特征;为0表示没有此特征

可选PE头

可选PE头中我们需要去分析的是如下几个成员:

1.WORD Magic:说明文件类型:如果值为0x010B,表示是32位下的PE文件;如果值为0x020B,表示是64位下的PE文件

2.DWORD SizeOfCode:所有代码节大小的和,必须是FileAlignment的整数倍

比如文件只有一个代码节,大小为100h字节,如果文件对齐粒度是200h,那么会补0填充够200h字节,所以会显示200h(编译器填的);若文件有两个代码节,两个都是10字节,这个值应为400h。但是计算机发展到现在已经不使用这个值了,改了也没事,删除它程序也可以正常运行,但是现在之所以保留下来是因为向下兼容之前的文件

3.DWORD SizeOfInitializedData:已初始化数据(比如定义一个有值的全局变量,便是已初始化的数据)大小的和,必须是FileAlignment的整数倍:由编译器填写,目前计算机已经不使用这个值了,保留下来只是为了向下兼容。

4.DWORD SizeOfUninitializedData:未初始化数据(比如定义一个没有值的全局变量,便是未初始化的数据)大小的和,必须是FileAlignment的整数倍:编译器填的,现在没用了。

5.DWORD AddressOfEntryPoint:程序入口点OEP,即程序真正执行的起始地址:这个值是相对于文件基地址偏移的程序入口地址,而不是真正运行在内存中的程序入口地址。文件装入到4GB虚拟内存中的起始基地址 +相对于文件首地址偏移的程序入口地址,即imagebase + AddressOfEntryPoint,这个值才是真正运行在4GB虚拟内存中的程序入口地址。虽然该值在不同文件中有不同的值,但一般都是0x0040000开始,这是系统的默认ImageBase值。该值我们可以自定义修改,但要保证它可以执行

注意:程序入口在默认情况下一般都在.text节.code代码块当中,且OEP不是只能在.code代码块开始的位置,也可以从此块当中的任何合理位置,或者在其他节(如自定义.tttt节等)的任意合理位置,又或者在数据中。

6.DWORD BaseOfCode:代码开始的基址:即PE文件加载到内存后,所有代码的起始地址。编译器填写的,可以改,但不影响程序执行

7.DWORD BaseOfData:内存中所有数据开始的地址:编译器填的,程序运行用不到,想改就改

8. DWORD ImageBase: 内存镜像基址:该值可以看作模块之间的对齐粒度,也是程序真正运行以后装载到自己的虚拟4GB内存后的起始位置。imagebase一般都是0x00400000(具体原因参考上一篇对齐部分),这是系统的默认值,不能超过0x80000000,这是因为我们写的程序的数据只能在内存的2GB用户区中,不能占用2GB系统区。

模块的概念:一个.exe文件可能是由一堆PE文件组成的,每个pe文件都有自己的imagebase,他们共用一个4GB虚拟内存。这是因为.exe文件本身是一个PE文件,满足PE结构,但是.exe中可能还用到了很多.dll,每一个.dll也是一个PE文件,也满足PE结构,这些.dll有自己的功能和作用,当它们拼凑到一个.exe文件中时,.exe文件就有了完整的功能。这时又称.exe文件有很多模块构成,每一个.dll都是一个模块

pe文件(各个模块)不能从0开始的原因是因为内存保护,我们前面学过,free一个动态分配内存的指针后,一定要将指针指向NULL,那么当指针指向NULL后,这个指针指向的地址就是0x0,从0开始往后偏移一定范围的地址,操作系统都把他的地址上内存空了出来,那么此时指针访问这部分的数据,编译器会立马报错,也就是说限制了指针使其不能为所欲为的访问内存。

9.DWORD SectionAlignment:内存对齐粒度:可执行文件运行时装入4GB虚拟内存中的对齐粒度,一般为0x1000字节

10.DWORD FileAlignment:文件对齐粒度:可执行文件在硬盘的对齐粒度,一般为0x200字节,还有的是0x1000字节,和内存对齐粒度相等

举例说明内存对齐和文件对齐:(day27.1-PE结构概况中详细说明过)

11. DWORD  SizeOfImage:内存中整个PE文件的映射尺寸:即文件运行时在4GB虚拟内存中的整个文件数据大小。该值可以比实际的值大,但必须是SectionAlignment的整数倍

12. DWORD SizeOfHeaders:所有头+节表按照文件对齐后的大小:即DOS头 + 垃圾数据 + PE签名 + 标准PE头 + 可选PE头 + 节表,按照文件对齐后的大小。必须是FileAlignment的整数倍,否则加载会出错

举例:比如一个可执行文件的所有头和节表加起来大小为0x1800字节,但是因为要满足文件对齐粒度0x1000,示意图SizeOfHeaders的值应该为0x2000,

13. DWORD CheckSum:校验和:一些重要的系统文件、驱动文件等对此有要求,用来判断文件是否被修改(但是可以修改这个值)

校验方法举例:把PE文件中的所有数据中两个字节的值(十六进制)两两相加(如值1 2 3 4,相加:1+2,3+4),最后将所有两两相加的结果再求和得到一个数,存放到checksum表示的4字节内存中,内存可溢出

14. DWORD SizeOfStackReserve:初始化时保留的栈大小 (最大值)

15. DWORD SizeOfStackCommit:初始化时实际提交的栈大小

16. DWORD SizeOfHeapReserve:初始化时保留的堆大小 (最大值)

17. DWORD SizeOfHeapCommit:初始化时实际提交的堆大小

18. DWORD NumberOfRvaAndSizes:目录项数目如果该值是0x10(16),表示下面的_IMAGE_DATA_DIRECTORY DataDirectory[16]成员有16个结构体,该结构体所占内存固定每个占8字节。这些结构体用于告诉我们编译器在编译文件时往exe文件中所添加的数据。具体如下图所示:

可执行文件的读取到装入内存过程

文件装入内存流程如图所示:

1.编译器生成.exePE文件

比如我们使用VC,编写程序以后按下F7,编译器会编译生成对应的.exe可执行文件,此时编译器计算生成PE文件的所有数据,比如imagebase或者OEP等全部字段信息,最后将.exe文件保存到硬盘中

2.文件数据读到内存(FileBuffer)

当文件数据读取到内存中时,其数据存储是完全照搬文件在硬盘上的数据。此时文件的格式并非windows运行格式,所以Windows操作系统还无法运行它

3.将文件装载到内存镜像(ImageBuffer)

将文件从FileBuffer装入ImageBuffer,即将文件从imagebase开始对齐拉伸成内存对齐形式的一块内存。这个过程就是将文件装入自己的4GB虚拟内存中,称为PE loader。我们称将文件拉伸装载后的内存为imageBuffer,即内存镜像。此时文件的格式满足windows运行格式。对于硬盘对齐粒度等于内存对齐粒度的文件不需要进行拉伸

此时文件在4GB虚拟内存中的起始地址,就是imagebase,一般为0x00400000,接着就可以通过imagebase + addressofentrypoint找到文件装载到内存后真正的程序入口地址;或者用imagebase加上一些偏移地址值就可以得到文件其他内容在运行时装入4GB内存后的地址

4.操作系统将虚拟地址转化成物理地址

FileBuffer和ImageBuffer提到的所有地址都是虚拟地址,操作系统最后会将这些虚拟地址转换为物理地址,这样才算真正的装入到真实内存中。

5.CUP运行

CPU将装入真正内存中的文件进行分配执行,此时一个.exe文件才算真正的执行起来了

作业

提取文件PE头部信息到控制台

#include<stdio.h>
#include<Windows.h>

char* ReadPEFile(const char* lpszFile)
{
	FILE* pFile = fopen(lpszFile, "rb");
	DWORD fileSize = 0;
	char* pFileBuffer = NULL;
	if (!pFile)
	{
		printf("无法打开EXE文件");
		return NULL;
	}
	fseek(pFile, 0, SEEK_END);
	fileSize = ftell(pFile);
	fseek(pFile, 0, SEEK_SET);
	pFileBuffer = (char*)malloc(sizeof(char)*fileSize);
	if (!pFileBuffer)
	{
		printf("分配空间失败");
		fclose(pFile);
		return NULL;
	}
	size_t i = fread(pFileBuffer, fileSize, 1, pFile);
	if (!i)
	{
		printf("读取数据失败!");
		free(pFileBuffer);
		fclose(pFile);
		return NULL;
	}
	fclose(pFile);
	return pFileBuffer;
}
BOOL PrintNTHeaders(const char* lpszFile)
{
	IMAGE_DOS_HEADER  *pDosHeader;
	IMAGE_NT_HEADERS *pNTHeader;
	IMAGE_FILE_HEADER *pPEHeader;
	IMAGE_OPTIONAL_HEADER *pOptionHeader;
	char* pFileBuffer = ReadPEFile(lpszFile);
	pDosHeader = (IMAGE_DOS_HEADER*)pFileBuffer;
	if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
	{
		printf("不是有效MZ标志,打印结束\n");
		free(pFileBuffer);
		pFileBuffer = NULL;
		return FALSE;
	}
	printf("\nDOC头\n");
	printf("e_magic = %04x\n", pDosHeader->e_magic);
	printf("e_lfanew = %08x\n", pDosHeader->e_lfanew);
	printf("\nNT头\n");
	pNTHeader = (IMAGE_NT_HEADERS*)((char*)pFileBuffer + pDosHeader->e_lfanew);
	if (pNTHeader->Signature != IMAGE_NT_SIGNATURE)
	{
		printf("不是有效的PE标志,打印结束\n");
		free(pFileBuffer);
		pFileBuffer = NULL;
		return FALSE;
	}
	printf("Signature: %08x\n", pNTHeader->Signature);
	printf("\nPE头\n");
	pPEHeader = (IMAGE_FILE_HEADER*)((char*)pNTHeader + 4);
	printf("Machine:%04X\n", pPEHeader->Machine);
	printf("NumberOfSections:%04X\n", pPEHeader->NumberOfSections);
	printf("TimeDateStamp:%08X\n", pPEHeader->TimeDateStamp);
	printf("SizeOfOptionalHeader:%04X\n", pPEHeader->SizeOfOptionalHeader);
	printf("Characteristics:%04X\n", pPEHeader->Characteristics);
	printf("\n可选PE头\n");
	pOptionHeader = (IMAGE_OPTIONAL_HEADER*)((char*)pPEHeader + sizeof(IMAGE_FILE_HEADER));
	printf("Magic:%04X\n",pOptionHeader->Magic);
	printf("SizeOfCode:%08X\n", pOptionHeader->SizeOfCode);
	printf("SizeOfInitializedData:%08X\n", pOptionHeader->SizeOfInitializedData);
	printf("SizeOfUninitializedData:%08X\n", pOptionHeader->SizeOfUninitializedData);
	printf("AddressOfEntryPoint:%08X\n", pOptionHeader->AddressOfEntryPoint);
	printf("BaseOfCode:%08X\n", pOptionHeader->BaseOfCode);
	printf("BaseOfData:%08X\n", pOptionHeader->BaseOfData);
	printf("ImageBase:%08x\n", pOptionHeader->ImageBase);
	printf("SectionAlignment:%08X\n", pOptionHeader->SectionAlignment);
	printf("FileAlignment:%08X\n", pOptionHeader->FileAlignment);
	printf("SizeOfImage:%08X\n", pOptionHeader->SizeOfImage);
	printf("SizeOfHeaders:%08X\n", pOptionHeader->SizeOfHeaders);
	printf("CheckSum:%08X\n", pOptionHeader->CheckSum);
	printf("SizeOfStackReserve:%08X\n", pOptionHeader->SizeOfStackReserve);
	printf("SizeOfStackCommit:%08X\n", pOptionHeader->SizeOfStackCommit);
	printf("SizeOfHeapReserve:%08X\n", pOptionHeader->SizeOfHeapReserve);
	printf("SizeOfHeapCommit:%08X\n", pOptionHeader->SizeOfHeapCommit);
	free(pFileBuffer);
	pFileBuffer = NULL;
}
int main(int argc, char* argv[])
{
	const char* lpszFile = "C:\\Windows\\notepad.exe";
	PrintNTHeaders(lpszFile);
	return 0;
}

  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值