视频资源详见网盘搜索 或 在线的B站滴水逆向三期
其课件也能在CSDN或百度搜索到,以下部分为课件内容摘要,部分为自己的理解
最后附上详细注释的自写代码
PE(Portable Executable)文件,是windows上的可移植的可执行文件,常见的 .exe .dll .sys 等都是PE文件。那么PE文件和计算机网络的各种包头一样,都有自己独特的格式。
这里有张PE格式图的部分截图,完整版链接:https://pan.baidu.com/s/1ToG2wc_qOi0BfrLTeN0HPw
提取码:PEst
不要被这么多字段吓到,只有小部分是常用的
首先PE文件开头就是DOS头 DOS_HEADER,DOS头只需要看第一个字段magic和最后一个lfanew,magic字段为一个WORD即两字节,若为MZ则属于PE文件,lfanew是指向NT头NT_HEADER的相对偏移,即文件起始位置(magic字段所在位置)+ lfanew 即为NT头的位置
可以用winhex等十六进制查看器查看会有更直观的印象。lfanew字段到NT头之间会有一段说明字符串或者垃圾数据称为DOS stub
接下来NT头分为一个字段和两大部分,Signature字段,一大部分是FILE_HEADER(即COFF头或称文件头/标准PE头),另一大部分是OPTIONAL_HEADER 可选头。注意FILE_HEADER一共有0x14的长度,可以看成一个大的结构体,结构体内容就是图中_IMAGE_FILE_HEADER的部分。OPTIONAL_HEADER 可选头同理。
过了NT头,就是节表SECTION_HEADER 了。节表类似于结构体数组的形式,图所示会有多个同样结构的SECTION_HEADER重复。
以下就是PE头中的常用字段,这些字段现只需了解,具体的意义可能得等后续的文章讲解中逐渐都会用到后才能真正理解,后面用到了回头看也不迟
1、DOS头: | ||||||||
WORD e_magic * | "MZ标记" 用于判断是否为可执行文件. | |||||||
DWORD e_lfanew; * | PE头相对于文件的偏移,用于定位PE文件 | |||||||
2、标准PE头(COFF头): | ||||||||
WORD Machine; * | 程序运行的CPU型号:0x0 任何处理器/0x14C 386及后续处理器 | |||||||
WORD NumberOfSections; * | 文件中存在的节的总数,如果要新增节或者合并节 就要修改这个值. | |||||||
DWORD TimeDateStamp; * | 时间戳:文件的创建时间(和操作系统的创建时间无关),编译器填写的. | |||||||
DWORD PointerToSymbolTable; | ||||||||
DWORD NumberOfSymbols; | ||||||||
WORD SizeOfOptionalHeader; * | 可选PE头的大小,32位PE文件默认E0h 64位PE文件默认为F0h 大小可以自定义. | |||||||
WORD Characteristics; * | 每个位有不同的含义,可执行文件值为10F 即0 1 2 3 8位置1 | |||||||
3、可选PE头: | ||||||||
WORD Magic; * | 说明文件类型:10B 32位下的PE文件 20B 64位下的PE文件 | |||||||
BYTE MajorLinkerVersion; | ||||||||
BYTE MinorLinkerVersion; | ||||||||
DWORD SizeOfCode;* | 所有代码节的和,必须是FileAlignment的整数倍 编译器填的 没用 | |||||||
DWORD SizeOfInitializedData;* | 已初始化数据大小的和,必须是FileAlignment的整数倍 编译器填的 没用 | |||||||
DWORD SizeOfUninitializedData;* | 未初始化数据大小的和,必须是FileAlignment的整数倍 编译器填的 没用 | |||||||
DWORD AddressOfEntryPoint;* | 程序入口 | |||||||
DWORD BaseOfCode;* | 代码开始的基址,编译器填的 没用 | |||||||
DWORD BaseOfData;* | 数据开始的基址,编译器填的 没用 | |||||||
DWORD ImageBase;* | 内存镜像基址 | |||||||
DWORD SectionAlignment;* | 内存对齐 | |||||||
DWORD FileAlignment;* | 文件对齐 | |||||||
WORD MajorOperatingSystemVersion; | ||||||||
WORD MinorOperatingSystemVersion; | ||||||||
WORD MajorImageVersion; | ||||||||
WORD MinorImageVersion; | ||||||||
WORD MajorSubsystemVersion; | ||||||||
WORD MinorSubsystemVersion; | ||||||||
DWORD Win32VersionValue; | ||||||||
DWORD SizeOfImage;* | 内存中整个PE文件的映射的尺寸,可以比实际的值大,但必须是SectionAlignment的整数倍 | |||||||
DWORD SizeOfHeaders;* | 所有头+节表按照文件对齐后的大小,否则加载会出错 | |||||||
DWORD CheckSum;* | 校验和,一些系统文件有要求.用来判断文件是否被修改. | |||||||
WORD Subsystem; | ||||||||
WORD DllCharacteristics; | ||||||||
DWORD SizeOfStackReserve;* | 初始化时保留的堆栈大小 | |||||||
DWORD SizeOfStackCommit;* | 初始化时实际提交的大小 | |||||||
DWORD SizeOfHeapReserve;* | 初始化时保留的堆大小 | |||||||
DWORD SizeOfHeapCommit;* | 初始化时实践提交的大小 | |||||||
DWORD LoaderFlags; | ||||||||
DWORD NumberOfRvaAndSizes;* | 目录项数目 | |||||||
DOS PE OPTION | ||||||||
DOS + PE标记 + 标准PE头 + 可选PE |
接下来就是写代码读取PE文件的部分字段了,其他更多字段可以自行添加.
看懂代码或者自己写一遍代码才是完全理解了PE头结构和这些字段分布
使用window.h 都有相匹配的结构体,直接.或者->都能找到需要的字段,在VS中会识别出比这里更多的高亮,比如DWORD 、 PIMAGE_DOS_HEADER 之类的windows.h 才有的宏定义
代码中用到的exe 可自行找一个小点的独立的exe,比如网上搜些逆向用的crackme exe程序检测自写代码用
#include "stdio.h"
#include "windows.h"
inline LPVOID ReadPEFile(LPSTR lpszFile) //LPSTR 即 CHAR *
{
FILE *pFile = NULL;
DWORD fileSize = 0;
LPVOID pFileBuffer = NULL; //LPVOID 即 void *
//打开文件
pFile = fopen(lpszFile, "rb");
if (!pFile)
{
printf(" 无法打开 EXE 文件! ");
return NULL;
}
//读取文件大小
fseek(pFile, 0, SEEK_END);
fileSize = ftell(pFile);
fseek(pFile, 0, SEEK_SET);
//分配缓冲区
pFileBuffer = malloc(fileSize); //pFileBuffer指向一段 分配的exe文件这么大的内存
if (!pFileBuffer)
{
printf(" 分配空间失败! ");
fclose(pFile);
return NULL;
}
//将文件数据读取到缓冲区
size_t n = fread(pFileBuffer, fileSize, 1, pFile); //size_t 即 unsigned int
if (!n)
{
printf(" 读取数据失败! ");
free(pFileBuffer);
fclose(pFile);
return NULL;
}
//关闭文件
fclose(pFile);
return pFileBuffer; //最后返回指向 已填充exe文件内容 的新开辟空间 的指针
}
VOID h312()
{
char FilePath[] = "CRACKME.EXE";
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pFileHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
pFileBuffer = ReadPEFile(FilePath); //pFileBuffer即指向已装载到内存中的exe首部
if (!pFileBuffer)
{
printf("文件读取失败\n");
return;
}
//判断是否是有效的MZ标志
if (*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE) //pFileBuffer强转 WORD* 后 取指针指向的内容
{
printf("不是有效的MZ标志\n");
free(pFileBuffer);
return;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer; // 强转 DOS_HEADER 结构体指针
//打印DOS头
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) //基址pFileBuffer + lfanew 为 NTHeader首址
{
printf("不是有效的PE标志\n");
free(pFileBuffer);
return;
}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
//打印NT头
printf("*****************NT HEADERS*****************\n");
printf("NT:%x\n", pNTHeader->Signature);
pFileHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4); //NT头地址 + 4 为 FileHeader 首址
printf("****************FILE HEADERS****************\n");
printf("PE:%x\n", pFileHeader->Machine);
printf("节的数量:%x\n", pFileHeader->NumberOfSections);
printf("SizeOfOptionalHeader:%x\n", pFileHeader->SizeOfOptionalHeader);
//可选PE头
pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);//SIZEOF_FILE_HEADER为固定值且不存在于PE文件字段中
printf("**************OPTIOINAL HEADERS*************\n");
printf("Magic:%x\n", pOptionalHeader->Magic);
//释放内存
free(pFileBuffer);
}
运行程序打印内容如下:
********************DOS头********************
MZ标志:5a4d
PE偏移:100
*****************NT HEADERS*****************
NT:4550
****************FILE HEADERS****************
PE:14c
节的数量:6
SizeOfOptionalHeader:e0
**************OPTIOINAL HEADERS*************
Magic:10b
可使用 PETool 或 LoadPE 等小软件查看相应PE文件内容。对照打印内容检测自写的程序是否正确。