PE 格式详解与试验

Posted Debroon

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PE 格式详解与试验相关的知识,希望对你有一定的参考价值。

 


可执行文件结构分析

可执行文件有:

  • windows 的 PE,如 .exe、.dll、.ocx、.sys、.com。
  • Linux 的 ELF,如 .out、.so。

PE 整体结构图:

具体结构:

比较重要的几个部分:DOS 头、文件头、可选头、数据目录、节头和节区。

  • DOS头:32位签名以及魔数 0x00004550
  • 文件头:说明文件在什么平台、分几个区段、链接的时间、是可执行文件还是 DLL
  • 可选头:包含大量 PE 文件的重要信息
  • 数据目录:包含许多指向各节数据的指针
  • 节头和节区:PE文件的核心,病毒检测相关特征主要集中于此

 


DOS头

DOS头,是 PE 文件第一个结构,总共占 64 个字节。


第一个成员 e_magic,叫魔数,占 2 个字节,固定俩个字符 MZ 的内存编码,这是发明人的首字母缩写。

  • M 的内存编码:4D
  • Z 的内存编码:5A

但在 x86 平台,是低位优先,看到的是 5A4D,而不是 4D5A。

第二个成员 e_cbip,PE 文件最后那一页所包含的字节数。

第三个成员 e_cp,PE 文件包含多少页。

第四个成员 e_crlc,PE 文件有多少重地位个数。

第五个成员 e_cparhdr,PE 文件有多少断头。

第六个成员 e_minalloc,所需的最小附加段。

第七个成员 e_maxalloc,所需的最大附加段。

第八个成员 e_ss,栈底寄存器的初始值。

第九个成员 e_sp,栈顶寄存器的初始值。

第十个成员 e_csum,校验码,验证 PE 文件有木有被改。

第十一成员 e_ip,指令寄存器的初始值。

第十二成员 e_cs,段寄存器的初始值。

第十三成员 e_lfarlc,重定向表的文件地址。

第十四成员 e_ovno,覆盖号,早期内存小,运行大程序就需要这个。

第十五成员 e_res[4],保留字段。

第十六成员 e_oemid,oem 的 id。

第十七成员 e_oeminfo,oem 的信息。

第十八成员 e_res2[10],保留字段。

第十九成员 e_lfanew,PE 头的起始地址 0x0000003C。

  • 判断 PE 文件的有效性: e_magic(5A4D)、e_lfanew( PE,0,0,(x00004550) )

解析 DOS Header:

int peDosHeaderAnalyze(char *file)

	if(file==NULL)
		return -1;

	FILE *fp = NULL;
	IMAGE_DOS_HEADER dosheader;                  // PE 结构体公开的
	unsigned long pesig;
	
	fp = fopen(file, "r+b");                     // 打开文件
	if(fp == NULL)
		return -1;
	
	fread(&dosheader, sizeof(dosheader), 1, fp); // 读出 dos header 
	fseek(fp, dosheader.e_lfanew,SEEK_SET);      // 读出 dos header 最后一个成员 
	fread(&pesig, 4, 1, fp);                     // 读 4 个字节,PE 头签名 PE,0,0
	fclose(fp);
	
	printf("IMAGE_DOS_HEADER info:\\n");
	printf("e_magic  : %04x\\n", dosheader.e_magic); // "MZ"-->"ZM":0x5A4D
	printf("e_cblp  : %04x\\n", dosheader.e_cblp);
	printf("e_cp   : %04x\\n", dosheader.e_cp);
	printf("e_crlc  : %04x\\n", dosheader.e_crlc);
	printf("e_cparhdr : %04x\\n", dosheader.e_cparhdr);
	printf("e_minalloc: %04x\\n", dosheader.e_minalloc);
	printf("e_maxalloc: %04x\\n", dosheader.e_maxalloc);
	printf("e_ss   : %04x\\n", dosheader.e_ss);
	printf("e_sp   : %04x\\n", dosheader.e_sp);
	printf("e_csum  : %04x\\n", dosheader.e_csum);
	printf("e_ip   : %04x\\n", dosheader.e_ip);
	printf("e_cs   : %04x\\n", dosheader.e_cs);
	printf("e_lfarlc : %04x\\n", dosheader.e_lfarlc);
	printf("e_ovno  : %04x\\n", dosheader.e_ovno);
	printf("e_res[0] : %04x\\n", dosheader.e_res[0]);
	printf("e_oemid  : %04x\\n", dosheader.e_oemid);
	printf("e_oeminfo : %04x\\n", dosheader.e_oeminfo);
	printf("res2[0]  : %04x\\n", dosheader.e_res2[0]);
	printf("lfanew  : %08x\\n", dosheader.e_lfanew);
	
	return 0;

判断 PE 有效性:

bool isPeValid(char *file) 
	if(file == NULL)
		return FALSE;

	FILE *fp = NULL;
	IMAGE_DOS_HEADER dosheader;                   // PE 结构体公开的
	unsigned long pesig;
	
	fp = fopen(file, "r+b");                      // 打开文件
	if(fp == NULL)
		return false;
	
	fread(&dosheader, sizeof(dosheader), 1, fp);  // 读出 dos header 
	fseek(fp, dosheader.e_lfanew, SEEK_SET);      // 读出 dos header 最后一个成员 e_lfanew
	fread(&pesig, 4, 1, fp);                      // 读 4 个字节,PE 头签名 PE,0,0
	fclose(fp);
	
	if((dosheader.e_magic ==IMAGE_DOS_SIGNATURE) &&
		(pesig == IMAGE_NT_SIGNATURE))
		//dos header e_magic和PE头部里的signature必须为固定值:MZ(0x5A4D )和PE00(0x00004550)
		return true;

	return false;

调用:

int main(int argc, char* argv[])

	char *pefile="D:\\\\mfpedemo_x64debug.exe";

	if(isPeValid(pefile))
		peDosHeaderAnalyze(pefile);
	else
		printf("not a valid pe file\\n");

	return 0;

 


文件头

file_header 结构,三部分组成:

文件头用来说明该二进制文件将运行在何种机器之上、分几个区段、链接的时间、是可执行文件还是DLL等等。

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;

#ifdef _WIN64
typedef  IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS;
#else
typedef  IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS;
#endif

 

IMAGE_FILE_HEADER 占 20 个字节:

typedef struct _IMAGE_FILE_HEADER 
 WORD Machine;                           // 运行平台
 WORD NumberOfSections;                  // 块数目
 DWORD TimeDataStamp;                    // 时间日期标记
 DWORD PointerToSymbolTable;             // COFF 符号指针,这是程序调试信息
 DWORD NumberOfSymbols;                  // 符号数
 WORD SizeOfOptionalHeader;              // 可选部首长度,是 IMAGE_FILE_HEADER 的长度
 WORD Characteristics;                   // 文件属性
 IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

 

Machine(机器码)指定运行平台:

#define IMAGE_FILE_MACHINE_UNKNOWN 0

#define IMAGE_FILE_MACHINE_I386 0x014c     // Intel 386.

#define IMAGE_FILE_MACHINE_ALPHA 0x0184    // Alpha_AXP

#define IMAGE_FILE_MACHINE_POWERPC 0x01F0  // IBM PowerPC Little-Endian

#define IMAGE_FILE_MACHINE_AMD64 0x8664    // AMD64(K8)

Characteristics 文件属性取值,文件是否为可运行的状态,是否为DLL文件等信息:

#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 
// Relocation info stripped from file.

#define IMAGE_FILE_ EXECUTABLE_IMAGE 0x0002 
// File is executable. - 文件是否可以执行

#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 
// Line nunbers stripped from file.

#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 
// Local symbols stripped from file.

#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 
// Agressively trim working set

#define IMAGE_FILE_LARGE_ADDRESS_AMARE 0x0020 
// App can handle >2gb addresses

#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 
// Bytes of machine word are reversed.

#define IMAGE_FILE_32BIT_MACHINE 0x0100 
// 32 bit word nachine.

#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 
// Debugging info stripped .DBG file - 是否包含调试信息

#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 

#define IMAGE_FILE_NET_RUN _FROH _SWAP 0x0800
// If Image is on Net.

#define IMAGE_FILE_SYSTEM 0x1000 
// System File. - 是否是系统文件

#define IMAGE_FILE_DLL 0x2000
// File is a DLL. - 是否是 DLL 文件

#define IMAGE_FILE_UP_SYSTEM_CNLY 0x4000

#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 
// Bytes of machine word are reversed.

从 PE 头进入 IMAGE_FILE_HEADER,只需要在成员 e_ifanew + 4 个字节即可。
 


可选头

可选头虽然叫作可选,但是却包含了大量PE文件的重要信息。

Optioal_header 分 32、64 位,俩者相差不大。


可以下载软件 CFF Explorer 直接查看。


其中,比较重要的几个字段:

  • Magic,标记32位和64位可选头,为IMAGE_OPTIONAL_HEADER32时,magic码为10B,为IMAGE_OPTIONAL_HEADER64时,magic码为20B
  • MajorLinkerVersion和MinorLinkerVersion,链接器的版本号
  • SizeOfCode,代码段的长度
  • MajorOperatingSystemVersion、MinorOperatingSystemVersion,所需操作系统的版本号
  • MajorImageVersion、MinorImageVersion,映像的版本号
  • MajorSubsystemVersion、MinorSubsystemVersion,所需子系统版本号
  • SizeOfImage,映像的大小
  • SizeOfHeaders,所有文件头的大小
  • Subsystem,运行该PE文件所需的子系统。

 


PE RVA 地址与文件地址转换


这些节在内存中是按照 0x1000 对齐,

PE 首先加载到一个基地址:ImageBase(0x40000,也可能随机)

  • VA:虚拟地址
  • RVA:相对虚拟地址,RVA = VA - ImageBase
  • VOffset:该节起始地址对于 ImageBase 的偏移量,如 .text 块(0x0401000) - 基地址(0x0400000)
  • ROffset:该节起始地址对于文件起始位置的偏移量,如 .text 块(0x0401000) - 文件起始地址(0x0000000)


比如你获得了一条虚拟地址,这些条件都知道 ImageBase、VOffset、ROffset、VA,请算某一个虚拟地址 VA,对应的文件偏移地址:fRVA。

  • fRVA - ROffset = RVA - VOffset
  • fRVA = RVA - VOffset + ROffset
  • RVA = VA - ImageBase
  • fRVA = VA - ImageBase - VOffset + ROffset

上述等式原理:虚拟地址和文件地址对于字起始的偏移是固定的。
 


块表 Section Header


块表结构:

可以下载软件 CFF Explorer 直接查看。
 


导入表 Data Directory

typedef struct _IMAGE_DATA_DIRECTORY 
	DWORD VirtualAddress;    // RVA 相对虚拟地址
	DWORD Size;
 IMAGE_DATA_DIRECTORY, *PIMAGEDATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTOF_ENTRIES 16

Data Directory[16],那 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 
// 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 Deseriptors

#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 
// COM RuntimE_Descriptor


可以下载软件 CFF Explorer 直接查看。
 


基址重定位 reloc

EXE的默认加载地址是 0x400000,DLL的默认加载地址是 0x100000。

如果加载的基地址不是默认的地址(比如多个 DLL 加载),那么使用固定地址就需要重新定位。

这些代码地址就会记录在 reloc 表里。
 


C语言解析 PE 结构

#include <windows.h>
#include <stdio.h>
#pragma warning(disable:4996)

bool isPeValid(char *file) 

	if(file==NULL)
		return FALSE;

	FILE *fp;
	IMAGE_DOS_HEADER dosheader;
	unsigned long pesig;
	
	fp = fopen(file,"r+b");
	if(fp == NULL)
		return FALSE;
	
	fread(&dosheader,sizeof(dosheader),1,fp);
	fseek(fp,dosheader.e_lfanew,SEEK_SET);
	fread(&pesig,4,1,fp);
	fclose(fp);
	
	if((dosheader.e_magic ==IMAGE_DOS_SIGNATURE) &&
		(pesig == IMAGE_NT_SIGNATURE)) 
		// dos header e_magic和PE头部里的signature必须为固定值:MZ(0x5A4D )和PE00(0x00004550)
		return TRUE;

	return FALSE;


int peDosHeaderAnalyze(char *file) 
	if(file==NULL)
		return -1;

	FILE *fp;
	IMAGE_DOS_HEADER dosheader;
	unsigned long pesig;
	
	fp = fopen(file,"r+b");
	if(fp == NULL)
		return -1;
	
	fread(&dosheader,sizeof(dosheader),1,fp);
	fseek(fp,dosheader.e_lfanew,SEEK_SET);
	fread(&pesig,4,1,fp);
	fclose(fp);
	
	printf("IMAGE_DOS_HEADER info:\\n");
	printf("e_magic  : %04x\\n",dosheader.e_magic);//"MZ"-->"ZM":0x5A4D
	printf("e_cblp  : %04x\\n",dosheader.e_cblp);
	printf("e_cp   : %04x\\n",dosheader.e_cp);
	printf("e_crlc  : %04x\\n",dosheader.e_crlc);
	printf("e_cparhdr : %04x\\n",dosheader.e_cparhdr);
	printf("e_minalloc: %04x\\n",dosheader.e_minalloc);
	printf("e_maxalloc: %04x\\n",dosheader.e_maxalloc);
	printf("e_ss   : %04x\\n",dosheader.e_ss);
	printf("e_sp   : %04x\\n",dosheader.e_sp);
	printf("e_csum  : %04x\\n",dosheader.e_csum);
	printf("e_ip   : %04x\\n",dosheader.e_ip);
	printf("e_cs   : %04x\\n",dosheader.e_cs);
	printf("e_lfarlc : %04x\\n",dosheader.e_lfarlc);
	printf("e_ovno  : %04x\\n",dosheader.e_ovno);
	printf("e_res[0] : %04x\\n",dosheader.e_res[0]);
	printf("e_oemid  : %04x\\n",dosheader.e_oemid);
	printf("e_oeminfo : %04x\\n",dosheader.e_oeminfo);
	printf("res2[0]  : %04x\\n",dosheader.e_res2[0]);
	printf("lfanew  : %08x\\n",dosheader.e_lfanew);
	
	return 0;


int peFileHeaderAnalyze(char *file) 
	if(file==NULL)
		return -1;

	FILE *fp;
	IMAGE_DOS_HEADER dosheader;
	IMAGE_FILE_HEADER fileheader;
	unsigned long pesig;

	fp = fopen(file,"r+b");
	if(fp == NULL)
		return -1;

	fread(&dosheader,sizeof(dosheader),1,fp);
	fseek(fp,dosheader.e_lfanew,SEEK_SET);
	fread(&pesig,4,1,fp);
	fread(&fileheader, sizeof(fileheader), 1, fp);

	fclose(fp);

	printf("IMAGE_FILE_HEADER info:\\n");
	printf("Sinature\\t\\t: %08x\\n",pesig);
	printf("Machine\\t\\t\\t: %04x\\n",fileheader.Machine);
	printf("NumberOfSections\\t: %08x\\n",fileheader.NumberOfSections);
	printf("TimeDateStamp\\t\\t: %08x\\n",fileheader.TimeDateStamp);
	printf("PointerToSymbolTable\\t: %08x\\n",fileheader.PointerToSymbolTable);
	printf("NumberOfSymbols\\t\\t: %04x\\n",fileheader.NumberOfSymbols);
	printf("SizeOfOptionalHeader\\t: %04x\\n",fileheader.SizeOfOptionalHeader);
	printf("Characteristics\\t\\t: %04x\\n",fileheader.Char

以上是关于PE 格式详解与试验的主要内容,如果未能解决你的问题,请参考以下文章

PE文件格式详解,第一讲,DOS头文件格式

PE文件格式详解,第二讲,NT头文件格式,以及文件头格式

PE文件格式详解

PE文件格式详解

PE文件格式详解,第三讲,可选头文件格式,以及节表

PE 文件格式 详解 二