学习感悟,如果错误,还望指出
目录
前言
PE格式是在Windows上的可执行程序需要遵守的格式规范。
接下来介绍一下PE头中的一部分成员的含义。
DOS头
大小是固定的64字节
struct _IMAGE_DOS_HEADER ,
{ 0x00 WORD e_magic;//表明此文件是不是PE文件,如果是PE文件的话,值为4D5A,ASCII值对应MZ。
0x02 WORD e_cblp;
0x04 WORD e_cp;
0x06 WORD e_crlc;
0x08 WORD e_cparhdr;
0x0a WORD e_minalloc;
0x0c WORD e_maxalloc;
0x0e WORD e_ss;
0x10 WORD e_sp;
0x12 WORD e_csum;
0x14 WORD e_ip;
0x16 WORD e_cs;
0x18 WORD e_lfarlc;
0x1a WORD e_ovno;
0x1c WORD e_res[4];
0x24 WORD e_oemid;
0x26 WORD e_oeminfo;
0x28 WORD e_res2[10];
0x3c DWORD e_lfanew; //指明PE标识,也就是NT头的开始位置的偏移是多少,大小不固定,因为在DOS头结束,和NT头开始之间的区域是操作系统留给编译器使用的空闲区域,大小不确定,所以需要这个成员来存储NT头部的偏移。
};
struct _IMAGE_NT_HEADERS
{ 0x00 DWORD Signature; //PE标识,也就是上图中DOS头最后一个成员所指向的地方。
0x04 _IMAGE_FILE_HEADER FileHeader; //标准PE头
0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader;//可选PE头
};
标准PE头
大小为20字节
struct _IMAGE_FILE_HEADER
{
0x00 WORD Machine;//标识此程序可以在什么CPU上允许,如果是0x00的话说明在任何CPU上都 可以允许此程序,如果是014c,标识在386及其之后的CPU上可以执行。
0x02 WORD NumberOfSections;//去掉PE头部的节区数量
0x04 DWORD TimeDateStamp;//时间戳,因为编译器在编译的时候会生成一个MAP文件,MAP文 件,MAP文件里面记录了此程序的函数的名字,和地址,里面有一个时间戳,记录了MAP文件的生成时间,这个时间是和exe程序相匹配的,有的加壳软件在进行加壳的时候,会需要提供MAP文件,识别MAP文件的时间戳和exe是否匹配。
0x08 DWORD PointerToSymbolTable;
0x0c DWORD NumberOfSymbols;
0x10 WORD SizeOfOptionalHeader;//记录了可选PE头的大小,32位默认是E0,64位默认是F0,大小可以调整。
0x12 WORD Characteristics; //每一位都有特定的含义,可执行程序是10F,级0,1,2,3,8位置1
};
可选PE头
大小不确定,32位默认为E0,64系统默认为F0,可以自己定义大小。
struct _IMAGE_OPTIONAL_HEADER
{
0x00 WORD Magic; //说明文件类型,10B说明是32位下的PE文件,20B说明64位下的PE文件
0x02 BYTE MajorLinkerVersion;
0x03 BYTE MinorLinkerVersion;
0x04 DWORD SizeOfCode; //所有代码节的和,必须是FileAlignment的整数倍
0x08 DWORD SizeOfInitializedData;//所有已初始化数据的和,必须是FileAlignment的整数倍
0x0c DWORD SizeOfUninitializedData;//所有未初始化数据的和,必须是FileAlignment的整数倍
0x10 DWORD AddressOfEntryPoint; //简称OEP,程序的入口地址,需要配合ImageBase来定位程序的入口地址。这里相信大家可能会有疑问,为什么不直接定位到入口地址,还需要使用ImageBase+OEP偏移的方式来进行定位,因为一个EXE程序,很可能不止由一个PE文件构成,如果直接写死,而这个位置又已经被别的PE文件占据了,那么就出事了,如果采用这种偏移的方式,ImageBase改变之后,通过OEP进行偏移还是可以进行定位,程序还是可以跑起来。
0x14 DWORD BaseOfCode;//代码节的基址。
0x18 DWORD BaseOfData; //数据节的基址。
0x1c DWORD ImageBase; //程序在加载入内存的一个基址,也就是起始地址。
0x20 DWORD SectionAlignment; //内存对齐,1000字节
0x24 DWORD FileAlignment;//硬盘对齐,200字节
这里说一下,一个exe程序存储到硬盘上,如果直接通过一个16进制的编辑器打开,不会做任何改动,和在硬盘中是一样的,但是这个程序虽然加载如内存,但是不能跑起来,如果硬盘对齐和内存对齐尺寸不一样,那么就存在一个拉伸的过程,举个例子,代码节在硬盘上占389个字节,然后硬盘给他分配400字节(因为要进行对齐),那么允许这个程序时,拉到硬盘中,就会被拉伸到1000字节,如果硬盘对齐和内存对齐是一致的,就没有这个拉伸的过程了。
0x28 WORD MajorOperatingSystemVersion;
0x2a WORD MinorOperatingSystemVersion;
0x2c WORD MajorImageVersion;
0x2e WORD MinorImageVersion;
0x30 WORD MajorSubsystemVersion;
0x32 WORD MinorSubsystemVersion;
0x34 DWORD Win32VersionValue;
0x38 DWORD SizeOfImage; //程序在内存中的映射尺寸,可以设置的比原来的尺寸更长,但是必须是SectionAlignment的整数倍
0x3c DWORD SizeOfHeaders;//所有的头加上节表的大小。必须是文件对齐的整数倍。(DOS头+PE标识+标准PE头+可选PE头+节表)
0x40 DWORD CheckSum; //校验和,其实很简单,就是把数据从开始到结束加起来,存到此成员中,自然溢出。
0x44 WORD Subsystem;
0x46 WORD DllCharacteristics;
0x48 DWORD SizeOfStackReserve; //初始化的时候保留栈的大小
0x4c DWORD SizeOfStackCommit;//初始化时实际提交栈的大小
0x50 DWORD SizeOfHeapReserve; //初始化时保留堆区的大小
0x54 DWORD SizeOfHeapCommit;//初始化时实际提交堆的大小
0x58 DWORD LoaderFlags;
0x5c DWORD NumberOfRvaAndSizes;
0x60 _IMAGE_DATA_DIRECTORY DataDirectory[16];
};
过程
下面是我用C写的解析PE头的数据(只限于PE头),方法比较笨,如果大家有更好的办法请告知我,感谢。
我的思路是,把PE头的每个成员的信息用数组记录下来,然后依次匹配读取。
#Analyze_Of_PE_Header.h
"""Analyze_Of_PE_Header.h"""
#pragma once
typedef char BYTE;
typedef short WORD;
typedef int DWORD;
//DOS头信息
extern const BYTE* _IMAGE_DOS_HEADER[19];
//DOS头每个成员的大小
extern DWORD DOS_LENGTH[21];
//标准PE头信息
extern const BYTE* _IMAGE_FILE_HEADER[7];
//标准PE头每个成员的大小
extern DWORD FILE_PE_LENGTH[7];
//可选PE头信息
extern const BYTE* _IMAGE_OPTIONAL_HEADER[30];
//可选PE头每个成员的大小
extern DWORD _OPTIONAL_HEADER_LENGTH[30];
char* ReadFile(char*);
bool Analyse_PE_Head(char*);
#Analyze_Of_PE_Header.cpp
#define _CRT_SECURE_NO_WARNINGS
#include "Analyze_Of_PE_Header.h"
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#DOS头信息
const BYTE* _IMAGE_DOS_HEADER[19]
{
"e_magic",
"e_cblp",
"e_cp",
"e_crlc",
"e_cparhdr",
"e_minalloc",
"e_maxalloc",
"e_ss",
"e_sp",
"e_csum",
"e_ip",
"e_cs",
"e_lfarlc",
"e_ovno",
"e_res[4]",
"e_oemid",
"e_oeminfo",
"e_res2[10]",
"e_lfanew"
};
DWORD DOS_LENGTH[21]
{
2,2,2,2,2,2,2,2,2,2,2,2,2,2,8,2,2,20,4
};
#标准PE头信息
const BYTE* _IMAGE_FILE_HEADER[7]
{
"Machine",
"NumberOfSections",
"TimeDateStamp",
"PointerToSymbolTable",
"NumberOfSymbols",
"SizeOfOptionalHeader",
"Characteristics"
};
DWORD FILE_PE_LENGTH[7]
{
2,2,4,4,4,2,2
};
//可选PE头信息
const BYTE* _IMAGE_OPTIONAL_HEADER[30]
{
"Magic",
"MajorLinkerVersion",
"MinorLinkerVersion",
"SizeOfCode",
"SizeOfInitializedData",
"SizeOfUninitializedData",
"AddressOfEntryPoint",
"BaseOfCode",
"BaseOfData",
"ImageBase",
"SectionAlignment",
"FileAlignment",
"MajorOperatingSystemVersion",
"MinorOperatingSystemVersion",
"MajorImageVersion",
"MinorImageVersion",
"MajorSubsystemVersion",
"MinorSubsystemVersion",
"Win32VersionValue",
"SizeOfImage",
"SizeOfHeaders",
"CheckSum",
"Subsystem",
"DllCharacteristics",
"SizeOfStackReserve",
"SizeOfStackCommit",
"SizeOfHeapReserve",
"SizeOfHeapCommit",
"LoaderFlags",
"NumberOfRvaAndSizes"
};
DWORD _OPTIONAL_HEADER_LENGTH[30]
{
2,1,1,4,4,
4,4,4,4,4,
4,4,2,2,2,
2,2,2,4,4,
4,4,2,2,4,
4,4,4,4,4
};
char* ReadFile(char* p)
{
FILE* fp = fopen(p, "rb");
if (fp == NULL)
{
printf("文件打开失败\n");
return NULL;
}
fseek(fp, 0, SEEK_END);
int len = ftell(fp);
char* buf = (char*)malloc(len);
if (buf == NULL)
{
printf("内存分配失败\n");
return NULL;
}
fseek(fp, 0, SEEK_SET);
fread(buf, 1, len, fp);
fclose(fp);
return buf;
}
bool Analyse_PE_Head(char* p)
{
char* Buf = ReadFile(p);
char* BufBackUp = Buf;
int Turn = 0;
if (Buf == NULL)
{
return false;
}
//DOS头
WORD* Test = (WORD*)Buf;
if ((*Test) != 0x5A4D)
{
return false;
}
printf("DOS头部:\n");
int i = 0;
while (i < 19)
{
printf("%s: ", _IMAGE_DOS_HEADER[i]);
if (DOS_LENGTH[i] == 2)
{
WORD* Ptemp = (WORD*)Buf;
Buf += 2;
printf("%x\n", (*Ptemp));
}
else if (DOS_LENGTH[i] == 4)
{
DWORD* Ptemp = (DWORD*)Buf;
Buf += 4;
//记录PE头偏移
Turn = (*Ptemp);
printf("%x\n", (*Ptemp));
}
else
{
WORD* Ptemp = (WORD*)Buf;
int j = 0;
while (j < (DOS_LENGTH[i] / 2))
{
printf("%x", *Ptemp);
Buf += 2;
Ptemp++;
j++;
}
printf("\n");
}
i++;
}
printf("\n-----------------------------------------------\n");
//PE头跳转
Buf = BufBackUp + Turn;
//NT头开始
int* temp = (int*)Buf;
if ((*temp) != 0x4550)
{
return false;
}
printf("PE标识:%x\n", (*temp));
Buf += 4;
//标准PE头
printf("标准PE头\n");
i = 0;
while (i < 7)
{
printf("%s: ", _IMAGE_FILE_HEADER[i]);
if (FILE_PE_LENGTH[i] == 2)
{
WORD* Ptemp = (WORD*)Buf;
Buf += 2;
printf("%x\n", (*Ptemp));
}
else
{
DWORD* Ptemp = (DWORD*)Buf;
Buf += 4;
printf("%x\n", (*Ptemp));
}
i++;
}
printf("\n-----------------------------------------------\n");
//可选PE头
printf("可选PE头\n");
i = 0;
while (i < 30)
{
printf("%s: ", _IMAGE_OPTIONAL_HEADER[i]);
if (_OPTIONAL_HEADER_LENGTH[i] == 2)
{
WORD* Ptemp = (WORD*)Buf;
Buf += 2;
printf("%x\n", (*Ptemp));
}
else if (_OPTIONAL_HEADER_LENGTH[i] == 1)
{
BYTE* Ptemp = (BYTE*)Buf;
Buf++;
printf("%x\n", (*Ptemp));
}
else
{
DWORD* Ptemp = (DWORD*)Buf;
Buf += 4;
printf("%x\n", (*Ptemp));
}
i++;
}
printf("\n-----------------------------------------------\n");
return true;
}
#main.cpp
#include <stdio.h>
#include "Analyze_Of_PE_Header.h"
int main(void)
{
char path[] = "C:\\Program Files (x86)\\NetSarang\\Xshell 7\\Xshell.exe";
bool result = Analyse_PE_Head(path);
if (result == NULL)
{
printf("解析失败\n");
}
return 0;
}
这是运行结果
总结
但是我还有一个思路就是根据不同的头部,建立不同的结构体,但是在对应结构体成员的查找上遇到了问题,没有想到实现的办法,所以采用了这种办法。