PE文件解析(1):Dos头与NT头

什么是PE文件?
PE文件是在windows平台可执行的文件。
包括:.exe(可执行程序),dll(动态链接库).sys(驱动程序)

这是PE文件的基本结构:
在这里插入图片描述

DOS头

Dos头是PE文件的起始位置,它

typedef struct _IMAGE_DOS_HEADER {      
    WORD   e_magic;      //PE文件的标识,PE文件的标识一定是5A4D             
    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;  //Dos头距离Nt头的距离,是一个偏移                  
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS头部分,有用的就两个参数:e_magic 和 e_lfanew

  • e_magic可以确定它是否是一个PE文件
  • e_lfanew是一个偏移,通过它可以找到NT头部分,即标准PE文件的位置。

使用010editor来解析PE文件: 这是一个标准DOS头的组成部分。
e_magic: 5A4D 标识它是一个PE文件(exe dll)等
e_lfanew: 0110,这是一个偏移地址,表示从Dos头开始位置到标准NT部分的偏移,这里为110。
在这里插入图片描述

使用代码解析DOS头部分

  1. 使用CreateFile打开文件句柄
  2. GetFileSize获取文件读取的数量
  3. ReadFile读取文件内容,存储到一个数组中,这个数组存储的就是一个PE文件的基址,即是一个起始地址。
  4. FileBuff可以被转换为DOS头。
BOOL PE::Load(const char* DllName)
{
	HANDLE MainHandle =  CreateFileA(
		DllName,
		GENERIC_READ | GENERIC_WRITE,
		FILE_SHARE_READ,
		NULL,
		OPEN_EXISTING,
		FILE_ATTRIBUTE_NORMAL,
		NULL
	);
	DWORD FileSize =  GetFileSize(MainHandle, NULL);
	char* FileBuff = new char[FileSize]{NULL};
	DWORD RealSize = 0;
	BOOL IsSuccess =  ReadFile(MainHandle, FileBuff, FileSize, &RealSize, NULL);
	if (!IsSuccess)
	{
		printf("读取文件失败!\n");
		return FALSE;
	}
	pDosHeader = (PIMAGE_DOS_HEADER)FileBuff;
	return TRUE;
}

NT头

NT头结构体:

IMAGE_NT_HEADERS STRUCT
{
+0h       DWORD    Signature
+4h       IMAGE_FILE_HEADER    FileHeader
+18h      IMAGE_OPTIONAL_HEADER32   OptionalHeader
} IMAGE_NT_HEADERS ENDS
  1. 前四个字节是一个标识,即PE文件的标识
  2. 第二个参数是一个标准PE文件的一个结构体
  3. 第三个参数是一个可选PE文件的结构体

Signature: 一个5A4D的标识,标识是一个PE文件。

这里表示的就是NT标识的部分:
他的全四个字节是一个DWORD的标识,Signature字段被设置为4550h,ASCII码为”PE00“。
在这里插入图片描述
可以看到它的偏移地址就是110h ,别忘了我们上面的DOS头部分的e_lfanew的值就是110h ,它表示的就是Dos头到NT头的偏移地址。

注意:我们使用010editor来查看的是偏移地址,不是真正的地址,需要加上FileBuff的基址,才是真正的地址。


标准NT头

标准NT头也叫做文件头。

structIMAGE_FILE_HEADER
{
	WORD Machine;//运行平台
	WORD NumberOfSections;//区块表的个数
	DWORD TimeDataStamp;//文件创建时间,是从1970年至今的秒数
	DWORD PointerToSymbolicTable;//指向符号表的指针
	DWORD NumberOfSymbols;//符号表的数目
	WORD SizeOfOptionalHeader;//IMAGE_NT_HEADERS结构中OptionHeader成员的大小,对于win32平台这个值通常是0x00e0
	WORD Characteristics;//文件的属性值
}

这里面比较重要的字段是:

  1. NumberOfSections:表示区段的个数,这个很重要,之后我们会讲到。
  2. SizeOfOptionalHeader:表示可选NT头大小,通过它我们可以得到区段表的位置。

这里是标准NT头的部分:
在这里插入图片描述

代码实现标准PE头:

PIMAGE_NT_HEADERS pNtHeader;	//NT头
PIMAGE_FILE_HEADER pFileHeader;	//标准NT头

//首先要找到NT头的位置:基址+偏移地址
NtHeader = (PIMAGE_NT_HEADERS)(FileBuff + pDosHeader->e_lfanew);
pFileHeader =&pNtHeader->FileHeader;

pNtHeader包含了标准NT头与可选NT头,取地址即可以到达。


可选NT头

typedefstruct_IMAGE_OPTIONAL_HEADER
{
+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_

重要的有:

  1. AddressOfEntryPoint:!!!非常重要,它是程序的入口点,即OEP,它是一个偏移,需要加上基址,才是真正的在程序中的地址。
  2. ImageBase:程序的基址,在没有随机基址的情况下,ImageBase + AddressOfEntryPoint 就是真正的程序地址,我们就找到了程序的代码执行起始位置。
  3. NumberOfRvaAndSizes:数据目录表,非常重要,这里面包含了了导出表,导入表,重定位表,等各种信息,我们之后会解释。

这里是可选PE头的部分
在这里插入图片描述

代码解析:

PIMAGE_OPTIONAL_HEADER pOptionalHeader//可选NT头

//得到可选NT头的
pOptionalHeader = &pNtHeader->OptionalHeader;
posted @ 2022-10-30 21:43  hugeYlh  阅读(63)  评论(0编辑  收藏  举报  来源