一、PE文件结构
PE文件按顺序由DOS头、NT头(包含标准PE头和可选PE头)、节表以及节区部分组成
DOS头
typedef struct IMAGE_DOS_HEADER
{
+0h WORD e_magic //DOS可执行文件标记,若其所存值为MZ(4Dh 5Ah),则是可执行文件
+2h WORD e_cblp
+4h WORD e_cp
+6h WORD e_crlc
+8h WORD e_cparhdr
+0ah WORD e_minalloc
+0ch WORD e_maxalloc
+0eh WORD e_ss
+10h WORD e_sp
+12h WORD e_csum
+14h WORD e_ip
+16h WORD e_cs
+18h WORD e_lfarlc
+1ah WORD e_ovno
+1ch WORD e_res[4]
+24h WORD e_oemid
+26h WORD e_oeminfo
+29h WORD e_res2[10]
+3ch DWORD e_lfanew //(RVA相对虚地址)指向PE文件头(即从DOS头起始处向后数这么多个字节处为真正的PE文件开始的地方)
}IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
需要重点记忆的是第一个WORD数据e_magic和最后一个DWORD数据e_lfanew。
e_magic是一种标记,如果值为MZ(4Dh 5Ah),则是可执行文件;而e_lfanew则是一个RVA,它指向的是PE文件真正开始的地方
被选中的数据就是e_lfanew,由于大小端序的原因,它的值是0x00000100,因此,PE真正的起始位置是0x00000100地址处
而从e_lfanew到0x00000100之间的这段区域通常存储一些不是很重要的数据,如注释等,可以暂时忽略
NT头
typedef struct IMAGE_NT_HEADERS
{
+0h DWORD Signature
+4h IMAGE_FILE_HEADER FileHeader //标准PE头
+18h IMAGE_OPTIONAL_HEADER32 OptionalHeader //可选PE头
}IMAGE_NT_HEADERS,*PIMAGE_NT_HEADERS;
NT头只包含了三个成员,其中FileHeader是标准PE头,占20个字节。OptionalHeader是可选PE头,大小不确定
标准PE头
0x00 WORD Machine; //运行平台(CPU型号,值为14C(4C 01)即表示是386及后续处理器,为0则是任何处理器)
0x02 WORD NumberOfSections; //文件存在节的总数,若要新增节或者合并节则需要修改这个值
0x04 DWORD TimeDataStamp;//文件创建时间,是从1970年至今的秒数,由编译器填写
0x08 DWORD PointerToSymbolicTable;
0x0C DWORD NumberOfSymbols;
0x10 WORD SizeOfOptionalHeader;//可选PE头的大小,32位PE文件默认为0XE0,64位PE文件默认为0XF0,大小可自定义
0x12 WORD Characteristics;//文件的属性值,由16位值为0或1组成,每个位代表的属性不一样
NumberOfSections记录了该PE文件所存在的节的总数,若后续涉及新增节或者合并节之类的操作就需要修改这个值
Characteristics的大小有16位,每个位都有不同的含义,用来定义PE文件的属性
可选PE头
typedef struct _IMAGE_OPTIONAL_HEADER
{
//
// Standard fields
//
+18h WORD Magic; // 标志字,32位普通可执行文件(010Bh),64位普通可执行文件(020Bh)
+1Ah BYTE MajorLinkerVersion;
+1Bh BYTE MinorLinkerVersion;
+1Ch DWORD SizeOfCode; // 所有含代码的节的总大小,必须是FileAlignment(文件对齐)的整数倍,编译器填写,没用
+20h DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小,必须是FileAlignment(文件对齐)的整数倍,编译器填写,没用
+24h DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小,必须是FileAlignment(文件对齐)的整数倍,编译器填写,没用
+28h DWORD AddressOfEntryPoint; // 程序执行入口(RVA)
+2Ch DWORD BaseOfCode; // 代码的区块的起始基址(RVA),编译器填写,没用
+30h DWORD BaseOfData; // 数据的区块的起始基址(RVA),编译器填写,没用
//
// NT additional fields. 以下是属于NT结构增加的领域
//
+34h DWORD ImageBase; // 程序的优先装载地址
+38h DWORD SectionAlignment; // 内存对齐(1000h)
+3Ch DWORD FileAlignment; // 文件对齐(200h(旧)1000(新))
+40h WORD MajorOperatingSystemVersion;
+42h WORD MinorOperatingSystemVersion;
+44h WORD MajorImageVersion;
+46h WORD MinorImageVersion;
+48h WORD MajorSubsystemVersion;
+4Ah WORD MinorSubsystemVersion;
+4Ch DWORD Win32VersionValue;
+50h DWORD SizeOfImage; // 映像装入内存后的总尺寸,可以比实际的尺寸大,但必须是SectionAlignment的整数倍
+54h DWORD SizeOfHeaders; // 所有头 + 区块表的尺寸大小 严格按照FileAlignment对齐
+58h DWORD CheckSum; // 映像的校检和,用来检测文件是否被修改,可修改值
+5Ch WORD Subsystem;
+5Eh WORD DllCharacteristics;
+60h DWORD SizeOfStackReserve; // 初始化时的栈大小
+64h DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
+68h DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
+6Ch DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
+70h DWORD LoaderFlags;
+74h DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16
+78h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
AddressOfEntryPoint是程序执行的入口地址;ImageBase是进程的基址,假设它的值为400000h,则PE文件会被装载到这个地址处(RVA);NumberOfRvaAndSizes定义了数据目录表的项数,一直是16
代码
下面附上代码
//#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <Windows.h>
#define FILE_PATH "C:\\Users\\Allure_Love\\Desktop\\notepad.exe"
LPVOID ReadPEFile(LPCSTR lpszFile) //const char[]相当于const char*,LPSTR相当于char*,两者不兼容,所以可以把LPSTR改成LPCSTR
{
FILE *pFile;
DWORD fileSize = 0;
LPVOID pFileBuffer = NULL;
//打开文件
errno_t err;
err = fopen_s(&pFile,lpszFile,"rb"); //"rb"只读
if (!pFile)
{
printf("无法打开.exe文件!");
}
//读取文件大小
fseek(pFile,0,SEEK_END);
//fseek(FILE * stream, long offset, int fromwhere); //函数设置文件指针stream的位置,以fromwhere为基址,偏移offset个字节的位置
fileSize = ftell(pFile);
fseek(pFile, 0, SEEK_SET);
//分配缓冲区
pFileBuffer = malloc(fileSize);
if (!pFileBuffer)
{
printf("分配文件失败!");
fclose(pFile); //关闭文件
return NULL;
}
//将文件数据读取到缓冲区
size_t n = fread(pFileBuffer, fileSize, 1, pFile); //size_t无符号整数,sieof()返回的结果类型
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) 从给定流stream读取nmemb个size大小的数据到ptr所指向的数组中
if (!n)
{
printf("读取数据失败!");
free(pFileBuffer);
fclose(pFile);
return NULL;
}
//关闭文件
fclose(pFile);
return pFileBuffer;
}
void PrintNTHeaders()
{
//初始化
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL; //DOS头指针
PIMAGE_NT_HEADERS pNTHeader = NULL; //NT头指针
PIMAGE_FILE_HEADER pPEHeader = NULL; //标准PE头指针
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL; //可选PE头指针ReadPEFile(FILE_PATH)
pFileBuffer = ReadPEFile(FILE_PATH);
if (!pFileBuffer)
{
printf("文件读取失败!");
return;
}
if (*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE)
{
printf("不是有效的MZ标志!");
free(pFileBuffer); //释放指针内存
return;
}
//打印DOS头
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
printf("***************DOS头****************\n");
printf("MZ标志:%x\n",pDosHeader->e_magic);
printf("PE偏移:%x\n", pDosHeader->e_lfanew);
//判断是否是有效的PE标志
if (*((PDWORD)((DWORD)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE)
{
printf("不是有效的PE标志!");
free(pFileBuffer);
return;
}
//打印NT头
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
printf("***************NT头****************\n");
printf("NT:%x\n", pNTHeader->Signature);
//打印标准PE头
pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);
printf("*************标准PE头****************\n");
printf("PE:%x\n", pPEHeader->Machine);
printf("节的数量:%x\n", pPEHeader->NumberOfSections);
printf("可选PE头的大小:%x\n", pPEHeader->SizeOfOptionalHeader);
//打印可选PE头
pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)(((DWORD)pPEHeader)+ IMAGE_SIZEOF_FILE_HEADER); //const IMAGE_SIZEOF_FILE_HEADER = 20 标准PE头大小
printf("*************可选PE头****************\n");
printf("Optional_PE:%x\n", pOptionalHeader->Magic);
//释放内存
free(pFileBuffer);
}
int main()
{
PrintNTHeaders();
return 0;
}