一:什么是PE解释器
PE解释器通常指的是Portable Executable(PE)文件格式的解释器。PE是一种可执行文件和库文件的标准格式,主要用于32位和64位版本的Windows操作系统。PE文件包含程序的二进制代码、数据、资源以及与可执行文件相关的其他信息。PE解释器是用于解释和执行PE文件的工具或程序。
在Windows操作系统中,PE文件是常见的可执行文件格式,用于存储应用程序、驱动程序和动态链接库(DLL)。PE解释器负责读取和解析PE文件的结构,执行其中的二进制代码,并在系统上运行相应的程序。
PE文件的结构包括头部信息、节表、导入表、导出表等部分。PE解释器会按照这些结构解析文件,并执行其中包含的程序逻辑。对于DLL文件,PE解释器还负责处理动态链接,将程序运行所需的函数和资源链接到系统中。
二:必须知道的知识点
地址空间:这个地址空间指的是PE文件被加载到内存的空间,是一个虚拟的地址空间,之所以不是物理空间是因为数据在内存中的位置经常在变,这样既可以节约内存开支又可以避开错误的内存位置。这个地址空间的大小为4G,但其中供程序装载的空间只有2G而且还是低2G空间,高2G空间则被用于装载内核DLL文件,所以也被称作内核空间。
文件映射:PE文件在磁盘上的状态和在内存中的状态是不一样的,我们把PE文件在磁盘上的状态称作FileBuffer
,在内存中的状态称为ImageBuffer
。当PE文件通过装载器装入内存是会经过“拉伸”的过程,所以它在FileBuffer
状态下和ImageBuffer
状态下的大小是不一样的。这个拉伸的具体过程会在讲完PE头结构后进行介绍。大致的图解如下:
VA:英文全称是Virual Address
,简称VA,中文意思是虚拟地址。指的是文件被载入虚拟空间后的地址。
ImageBase:中文意思是基址,指的是程序在虚拟空间中被装载的位置。
RVA:英文全称是Relative Virual Address
,简称RVA,中文意思是相对虚拟地址。可以理解为文件被装载到虚拟空间(拉伸)后先对于基址的偏移地址。计算方式:RVA = VA(虚拟地址) - ImageBase(基址)。它的对齐方式一般是以1000h为单位在虚拟空间中对齐的(传说中的4K对齐),具体对齐需要参照IMAGE_OPTIONAL_HEADER32
中的SectionAlignment
成员。
FOA:英文全称是File Offset Address
,简称FOA,中文意思是文件偏移地址。可以理解为文件在磁盘上存放时相对于文件开头的偏移地址。它的对齐方式一般是以200h为单位在硬盘中对齐的(512对齐),具体对齐需要参照IMAGE_OPTIONAL_HEADER32
中的FileAlignment
成员。
三:完整代码目录介绍及前期准备
前期准备我们先完成PE.h部分和部分函数实现
#ifndef _PE_H_ //如果标识符_PE_H_没有被定义,那么执行下面的代码,防止重复包含同一个头文件。
//通常在头文件的开始部分使用这样的条件编译指令,以确保头文件只被包含一次,避免重
复定义和冲突。
#define _PE_H_
/*
这段代码是一个条件编译的语法,用于防止重复包含同一个头文件。
首先,#ifndef 是条件编译指令的一部分,意为 "if not defined",用于检查给定的标识符是否已经定义。
在这里,_PE_H_ 是一个标识符,通常用来表示头文件的宏定义。
接着,#define 是用于定义宏的指令。这里将`_PE_H_` 定义为一个标记,用来表示头文件的宏定义。
综合起来,这段代码的意思是:如果标识符`_PE_H_`没有被定义,那么执行下面的代码,
防止重复包含同一个头文件。通常在头文件的开始部分使用这样的条件编译指令,
以确保头文件只被包含一次,避免重复定义和冲突。
*/
#include "stdio.h"
#include "stdlib.h"
#include "windows.h"
#define FILE_PATH "C:/Users/Qiu_JY/Desktop/PETool 1.0.0.5.exe"
int GetFileLength(FILE* pf, DWORD* Length);
int MyReadFile(void** pFileAddress);
int MyReadFile_V2(void** pFileAddress, PCHAR FilePath);
int MyWriteFile(PVOID pFileAdderss, DWORD FileSize, LPSTR FilePath);
int FOA_TO_RVA(PVOID pFileAdderss, DWORD FOA, PDWORD pRVA);
int RVA_TO_FOA(PVOID FileAdderss, DWORD RVA, PDWORD pFOA);
#endif
//表示条件编译块的结束,它与之前的条件编译指令一起,
//将一个代码块包括起来,只有在满足条件的情况下才会编译这部分代码。
/*
\`#endif` 是条件编译指令的一部分,用于结束一个条件编译块。
在条件编译指令中,`#ifndef` 和 `#ifdef` 通常会与 `#endif` 配对使用。`#endif` 表示条件编译块的结束,
它与之前的条件编译指令一起,将一个代码块包括起来,只有在满足条件的情况下才会编译这部分代码。
在这个特定的例子中,`#ifndef` 和 `#define` 之间的代码被用于防止重复包含同一个头文件,
而 `#endif` 表示条件编译块的结束,意味着在这个代码块中定义的宏和代码只会在满足条件时编译和执行。
总之,`#endif` 的作用是结束一个条件编译块,将被包括在其中的代码从编译范围中排除。
*/
# define _CRT_SECURE_NO_WARNINGS
# include "PE.h"
int GetFileLength(FILE *pf, DWORD *Length)
{
int ret = 0;
fseek(pf, 0, SEEK_END);//将文件指针移动到文件末尾。这一步相当于定位文件末尾,
//以便后续通过 ftell 获取文件长度。
*Length = ftell(pf);//使用 ftell 函数获取当前文件指针的位置,即文件长度。
//将获取到的长度存储在 Length 指向的内存位置。
fseek(pf, 0, SEEK_SET);//将文件指针移回文件开头。这一步是为了不影响文件的后续读取或操作。
return ret;
}
int MyReadFile(void **pFileAddress)
{
int ret = 0;
DWORD Length = 0; //双字节类型
//打开文件
FILE* pf = fopen(FILE_PATH, "rb");//打开模式为二进制读取模式
if (pf == NULL)
{
ret = -1;
printf("func ReadFile() Error!\n");
return ret;
}
//获取文件长度
ret = GetFileLength(pf, &Length);
if (ret != 0 && Length == -1)
{
ret = -2;
printf("func GetFileLength() Error!\n");
return ret;
}
//分配空间
*pFileAddress = (PVOID)malloc(Length);
if (*pFileAddress == NULL)
{
ret = -3;
printf("func malloc() Error!\n");
return ret;
}
memset(*pFileAddress, 0, Length);//使用 memset 函数将 *pFileAddress 指向的内存块清零。
//读取文件进入内存
fread(*pFileAddress, Length, 1, pf);//使用 fread 函数将文件内容读取到 *pFileAddress 指向的内存中,
//读取长度为 Length 字节
fclose(pf);
return ret;
}
int MyReadFile_V2(void** pFileAddress, PCHAR FilePath)//增加了一个 PCHAR 类型的参数 FilePath,用于指定要打开的文件路径。
{
int ret = 0;
DWORD Length = 0;
//打开文件
FILE* pf = fopen(FilePath, "rb");
if (pf == NULL)
{
ret = -1;
printf("func ReadFile() Error!\n");
return ret;
}
//获取文件长度
ret = GetFileLength(pf, &Length);
if (ret != 0 && Length == -1)
{
ret = -2;
printf("func GetFileLength() Error!\n");
return ret;
}
//分配空间
*pFileAddress = (PVOID)malloc(Length);
if (*pFileAddress == NULL)
{
ret = -3;
printf("func malloc() Error!\n");
return ret;
}
memset(*pFileAddress, 0, Length);
//读取文件进入内存
fread(*pFileAddress, Length, 1, pf);
fclose(pf);
return ret;
}
int MyWriteFile(PVOID pFileAddress, DWORD FileSize, LPSTR FilePath)
{
int ret = 0;
FILE *pf = fopen(FilePath, "wb");
if (pf == NULL)
{
ret = -5;
printf("func fopen() error :%d!\n", ret);
return ret;
}
fwrite(pFileAddress, FileSize, 1, pf);
fclose(pf);
return ret;
}
int FOA_TO_RVA(PVOID FileAddress, DWORD FOA, PDWORD pRVA)
{
int ret = 0;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)(FileAddress);
PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionGroup = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
//FOA在文件头中 或 SectionAlignment 等于 FileAlignment 时RVA等于FOA
if (FOA < pOptionalHeader->SizeOfHeaders || pOptionalHeader->SectionAlignment == pOptionalHeader->FileAlignment)
{
*pRVA = FOA;
return ret;
}
//FOA在节区中
for (int i = 0; i < pFileHeader->NumberOfSections; i++)
{
if (FOA >= pSectionGroup[i].PointerToRawData && FOA < pSectionGroup[i].PointerToRawData + pSectionGroup[i].SizeOfRawData)
{
*pRVA = pSectionGroup[i].VirtualAddress + FOA - pSectionGroup[i].PointerToRawData;
return ret;
}
}
//没有找到地址
ret = -4;
printf("func FOA_TO_RAV() Error: %d 地址转换失败!\n", ret);
return ret;
}
int RVA_TO_FOA(PVOID FileAddress, DWORD RVA, PDWORD pFOA)
{
int ret = 0;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)(FileAddress);
PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionGroup = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
//RVA在文件头中 或 SectionAlignment 等于 FileAlignment 时RVA等于FOA
if (RVA < pOptionalHeader->SizeOfHeaders || pOptionalHeader->SectionAlignment == pOptionalHeader->FileAlignment)
{
*pFOA = RVA;
return ret;
}
//RVA在节区中
for (int i = 0; i < pFileHeader->NumberOfSections; i++)
{
if (RVA >= pSectionGroup[i].VirtualAddress && RVA < pSectionGroup[i].VirtualAddress + pSectionGroup[i].Misc.VirtualSize)
{
*pFOA = pSectionGroup[i].PointerToRawData + RVA - pSectionGroup[i].VirtualAddress;
return ret;
}
}
//没有找到地址
ret = -4;
printf("func RAV_TO_FOA() Error: %d 地址转换失败!\n", ret);
return ret;
}
其中这俩函数由其重要,后面一定要明白如何转换:
int FOA_TO_RVA(PVOID FileAddress, DWORD FOA, PDWORD pRVA)
{
int ret = 0;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)(FileAddress);
PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionGroup = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
//定位操作
//FOA在文件头中 或 SectionAlignment 等于 FileAlignment 时RVA等于FOA
if (FOA < pOptionalHeader->SizeOfHeaders || pOptionalHeader->SectionAlignment == pOptionalHeader->FileAlignment)
{
*pRVA = FOA;
return ret;
}
//FOA在节区中
for (int i = 0; i < pFileHeader->NumberOfSections; i++)
{
if (FOA >= pSectionGroup[i].PointerToRawData && FOA < pSectionGroup[i].PointerToRawData + pSectionGroup[i].SizeOfRawData)
{
*pRVA = pSectionGroup[i].VirtualAddress + FOA - pSectionGroup[i].PointerToRawData;
return ret;
}
}
/*
这段代码是在遍历PE文件的节区表(Section Table)时,通过比较文件偏移地址(File Offset Address,FOA)来确定指定地址(FOA)所属的虚拟地址(RVA)。
具体解释如下:
pFileHeader->NumberOfSections表示PE文件头中记录的节区数量。
for (int i = 0; i < pFileHeader->NumberOfSections; i++)` 是一个循环,遍历每个节区。
if (FOA >= pSectionGroup[i].PointerToRawData && FOA < pSectionGroup[i].PointerToRawData + pSectionGroup[i].SizeOfRawData)` 是条件语句,检查 FOA 是否在当前节区的文件偏移地址范围内。如果是,则说明 FOA 属于该节区。
*pRVA = pSectionGroup[i].VirtualAddress + FOA - pSectionGroup[i].PointerToRawData;计算 FOA 对应的虚拟地址(RVA),其中 pSectionGroup[i].VirtualAddress是节区的虚拟地址,pSectionGroup[i].PointerToRawData是节区的文件偏移地址。
return ret;返回计算得到的 RVA。
这段代码的作用是根据给定的 FOA,找到对应的 RVA。在PE文件中,FOA 是文件偏移地址,而 RVA 是虚拟地址。通过遍历节区表,判断 FOA 属于哪个节区,然后通过计算得到对应的 RVA。这样的操作通常用于将文件偏移地址转换为内存中的虚拟地址,以便在内存中正确定位数据或代码。
*/
//没有找到地址
ret = -4;
printf("func FOA_TO_RAV() Error: %d 地址转换失败!\n", ret);
return ret;
}
int RVA_TO_FOA(PVOID FileAddress, DWORD RVA, PDWORD pFOA)
{
int ret = 0;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)(FileAddress);
PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionGroup = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
//RVA在文件头中 或 SectionAlignment 等于 FileAlignment 时RVA等于FOA
if (RVA < pOptionalHeader->SizeOfHeaders || pOptionalHeader->SectionAlignment == pOptionalHeader->FileAlignment)
{
*pFOA = RVA;
return ret;
}
//RVA在节区中
for (int i = 0; i < pFileHeader->NumberOfSections; i++)
{
if (RVA >= pSectionGroup[i].VirtualAddress && RVA < pSectionGroup[i].VirtualAddress + pSectionGroup[i].Misc.VirtualSize)
{
*pFOA = pSectionGroup[i].PointerToRawData + RVA - pSectionGroup[i].VirtualAddress;
return ret;
}
}
//没有找到地址
ret = -4;
printf("func RAV_TO_FOA() Error: %d 地址转换失败!\n", ret);
return ret;
}