接上一篇的理论知识:PE文件格式
这一篇是实战,其实读取非常简单,WIndows也为我们提供了内存对齐的结构体:
//DOS头
PIMAGE_DOS_HEADER
//NT头(包括PE标识+Image_File_Header+OptionHeader)
PIMAGE_NT_HEADERS
//标准PE头
PIMAGE_FILE_HEADER
其实PIMAGE_NT_HEAD_ERS里的IMAGE_FILE_HEADER已经包含了PIMAGE_FILE_HEADER结构体
可以根据个人所需读取字节来定义想要的结构体大小
具体参见理论篇
这里我们编写一个函数用于解析指定PE文件格式:
第一步先声明:
int JyPe(const char* file){
}
第二步将所需结构体声明出来
//DOS头
PIMAGE_DOS_HEADER pImageDosHeader;
//NT头(包括PE标识+Image_File_Header+OptionHeader)
PIMAGE_NT_HEADERS pImageNtHeaders;
//标准PE头
PIMAGE_FILE_HEADER pImageFileHeader;
第三步,将文件映射到内存,这里你也可以直接在文件里按字节对齐的方式读取:
//打开文件
HANDLE hFile;
hFile = CreateFile(file, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0);
if (hFile == NULL)
{
printf("打开文件失败\n");
system("pause");
return 0;
}
//创建映射关系
hMapObject = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (hMapObject == NULL)
{
printf("创建文件映射内核对对象失败\n");
system("pause");
return 0;
}
//获取映射内存首地址
uFileMap = (PUCHAR)MapViewOfFile(hMapObject, FILE_MAP_READ, 0, 0, 0);
if (uFileMap == NULL)
{
printf("映射到进程地址空间失败\n");
system("pause");
return 0;
}
第四步读取dos头,理论知识里说过,dos的hand就在pe文件起始位置,在内存映射里我们直接在首地址按结构体字节对齐读取就可以了:
上面说的Dos结构体的定义:
PIMAGE_DOS_HEADER
这个定义是个指针
我们通过它直接指向内存映射的首地址就可以了,节省空间便于操作,这就是指针的好处
pImageDosHeader = (PIMAGE_DOS_HEADER)uFileMap;
if (pImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("不是PE结构\n");
system("pause");
return 0;
}
在理论知识里说过,e_magic是指向dos标识的,其标志是mz,Windows给我们提供了对比宏,直接比对就可以知道是不是正确指向了。
定位到NT PE的头,这里涉及到一个rva地址到虚拟地址转换的过程
注意e_lfanew立马存放着PE HAND的偏移地址,但是这只是偏移地址,俗称rva,虚拟偏移地址,不是逻辑地址,逻辑地址是真实物理地址转换时的一个别名,这里是虚拟地址转换
uFileMap指向文件映射内存的首地址,也就是基地址,完整虚拟地址公式是:基地址+RAV地址
//定位到NT PE头
pImageNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)uFileMap + pImageDosHeader->e_lfanew);
第二步获取资源目录管理器里的导入表虚拟地址,也就是EAT的地址,里面包含着此程序用的导入API
//导入表的相对虚拟地址(RVA)
ULONG rva_ofimporttable = pImageNtHeaders->OptionalHeader.DataDirectory[1].VirtualAddress;
这一步,我们要通过偏移得到实际的导入API地址,所以我们要进行一个偏移计算,写一个函数用于将RVA地址转化为相对偏移地址
ULONG RvaToOffset(IMAGE_NT_HEADERS * pNtHeader, ULONG Rva)
{
}
第一个参数是Nt头,第二个参数是要转换的Rva地址
注意,这里我们是通过节表来转化的,详细可以参见理论知识
首先定义一个节表类型,并指向NtHand里的节地址
//PE节
IMAGE_SECTION_HEADER *p_section_header;
取
//取得第一个节表项
p_section_header = (IMAGE_SECTION_HEADER *)
((BYTE *)pNtHeader + sizeof(IMAGE_NT_HEADERS));
以NtHand为基,指向偏移量
然后我们在取得表的数目
//取得节表项数目
ULONG sNum;
sNum = pNtHeader->FileHeader.NumberOfSections;
这里我需要说一下,为什么要用节表来取得偏移地址,上面拿到的是RVA地址,相对的虚拟地址,也不可以直接基址+RVA地址,因为导入表在节表里,我们拿到的EAT导入表是存在于节表地址里的,而节表又不止这一个地址,所以我们需要找到节表的基地址,然后循环遍历直到找到我们自己的EAT表:
for (int i = 0; iName);
if ((p_section_header->VirtualAddress <= Rva) && Rva<(p_section_header->VirtualAddress + p_section_header->SizeOfRawData))
{
return Rva - p_section_header->VirtualAddress + p_section_header->PointerToRawData;
}
p_section_header++;
}
这里判断虚拟地址是否小于等于我们EAT表的地址并且EAT表的地址小于当前表虚拟地址加上整个表与磁盘文件对应的大小
这样就把控我们表的范围在EAT表之内了
(p_section_header->VirtualAddress <= Rva) && Rva<(p_section_header->VirtualAddress + p_section_header->SizeOfRawData)
公式转换:
return Rva - p_section_header->VirtualAddress + p_section_header->PointerToRawData;
RVA是相对的EAT虚拟地址减去当前节的虚拟地址加上位于磁盘文件中的偏移地址,就是在内存中的偏移地址
RAW(磁盘地址) = RVA(相对虚拟地址) - VirtualAddress + PointerToRawData
RVA(相对虚拟地址
) = VA(虚拟地址) - ImageBase(基址)
下一步我们在取的刚刚获取到的磁盘地址,然后加上内存基地址,就是位于内存中的实际偏移地址:
//取得导入表的地址
IMAGE_IMPORT_DESCRIPTOR *pImportTable = (IMAGE_IMPORT_DESCRIPTOR *)((char*)uFileMap + offset_importtable);
这里我们声明一个结构体:
IMAGE_IMPORT_DESCRIPTOR null_iid;
memset(&null_iid, 0, sizeof(null_iid));
因为我们等下要用链表的形式递增加直到结束区段
//每个元素代表了一个引入的DLL。
for (int i = 0; memcmp(pImportTable + i, &null_iid, sizeof(null_iid)) != 0; i++)
{
char *dllName = (char*)(uFileMap + RvaToOffset(pImageNtHeaders, pImportTable[i].Name));
//拿到了DLL的名字
printf("模块[%d]: %s\n", i, (char*)dllName);
PIMAGE_THUNK_DATA32 pThunk = (PIMAGE_THUNK_DATA32)(uFileMap + RvaToOffset(pImageNtHeaders, pImportTable[i].FirstThunk));
while (pThunk->u1.Ordinal != NULL)
{
PIMAGE_IMPORT_BY_NAME pname = (PIMAGE_IMPORT_BY_NAME)(uFileMap + RvaToOffset(pImageNtHeaders, pThunk->u1.AddressOfData));
printf("函数编号: %d 名称: %s\n", pname->Hint, pname->Name);
pThunk++;
}
}
这里巧妙的运用memcmp来判断结构体指针指向的地方与当前结构体字节数量是否一致,如果不是一致代表已经指向别的内存区了,就可以return掉了。
运行结果:
完整代码:
#include "windows.h"
#include
ULONG RvaToOffset(IMAGE_NT_HEADERS * pNtHeader, ULONG Rva)
{
//PE节
IMAGE_SECTION_HEADER *p_section_header;
//取得第一个节表项
p_section_header = (IMAGE_SECTION_HEADER *)
((BYTE *)pNtHeader + sizeof(IMAGE_NT_HEADERS));
//取得节表项数目
ULONG sNum;
sNum = pNtHeader->FileHeader.NumberOfSections;
for (int i = 0; iName);
if ((p_section_header->VirtualAddress <= Rva) && Rva<(p_section_header->VirtualAddress + p_section_header->SizeOfRawData))
{
return Rva - p_section_header->VirtualAddress + p_section_header->PointerToRawData;
}
p_section_header++;
}
return 0;
}
int JyPe(const char* file){
//DOS头
PIMAGE_DOS_HEADER pImageDosHeader;
//NT头(包括PE标识+Image_File_Header+OptionHeader)
PIMAGE_NT_HEADERS pImageNtHeaders;
//标准PE头
PIMAGE_FILE_HEADER pImageFileHeader;
HANDLE hMapObject;
//DOS头
PUCHAR uFileMap;
//打开文件
HANDLE hFile;
hFile = CreateFile(file, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0);
if (hFile == NULL)
{
printf("打开文件失败\n");
system("pause");
return 0;
}
//创建映射关系
hMapObject = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (hMapObject == NULL)
{
printf("创建文件映射内核对对象失败\n");
system("pause");
return 0;
}
//获取映射内存首地址
uFileMap = (PUCHAR)MapViewOfFile(hMapObject, FILE_MAP_READ, 0, 0, 0);
if (uFileMap == NULL)
{
printf("映射到进程地址空间失败\n");
system("pause");
return 0;
}
pImageDosHeader = (PIMAGE_DOS_HEADER)uFileMap;
if (pImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("不是PE结构\n");
system("pause");
return 0;
}
//定位到NT PE头
pImageNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)uFileMap + pImageDosHeader->e_lfanew);
//导入表的相对虚拟地址(RVA)
ULONG rva_ofimporttable = pImageNtHeaders->OptionalHeader.DataDirectory[1].VirtualAddress;
//根据相对虚拟(rva)地址计算偏移地址(offset)
ULONG offset_importtable = RvaToOffset(pImageNtHeaders, rva_ofimporttable);
if (!offset_importtable)
{
printf("获取导入表偏移地址失败\n");
system("pause");
return 0;
}
//取得导入表的地址
IMAGE_IMPORT_DESCRIPTOR *pImportTable = (IMAGE_IMPORT_DESCRIPTOR *)((char*)uFileMap + offset_importtable);
IMAGE_IMPORT_DESCRIPTOR null_iid;
memset(&null_iid, 0, sizeof(null_iid));
//每个元素代表了一个引入的DLL。
for (int i = 0; memcmp(pImportTable + i, &null_iid, sizeof(null_iid)) != 0; i++)
{
char *dllName = (char*)(uFileMap + RvaToOffset(pImageNtHeaders, pImportTable[i].Name));
//拿到了DLL的名字
printf("模块[%d]: %s\n", i, (char*)dllName);
PIMAGE_THUNK_DATA32 pThunk = (PIMAGE_THUNK_DATA32)(uFileMap + RvaToOffset(pImageNtHeaders, pImportTable[i].FirstThunk));
while (pThunk->u1.Ordinal != NULL)
{
PIMAGE_IMPORT_BY_NAME pname = (PIMAGE_IMPORT_BY_NAME)(uFileMap + RvaToOffset(pImageNtHeaders, pThunk->u1.AddressOfData));
printf("函数编号: %d 名称: %s\n", pname->Hint, pname->Name);
pThunk++;
}
}
system("pause");
}
我们取消打印节的字段,就可以看到当前程序使用哪些模块,模块对应的函数名
同时也可以获取地址:
知道了地址,我们在转换成off偏移地址,在去修改它,那么就实现了API HOOK,其余可以参见我的关于API HOOK的介绍,可以学习,在结合本篇文章可以轻松实EAT表方式的API HOOK
修改后的完整代码:
#include
#include
ULONG RvaToOffset(IMAGE_NT_HEADERS * pNtHeader, ULONG Rva)
{
//PE节
IMAGE_SECTION_HEADER *p_section_header;
//取得第一个节表项
p_section_header = (IMAGE_SECTION_HEADER *)
((BYTE *)pNtHeader + sizeof(IMAGE_NT_HEADERS));
//取得节表项数目
ULONG sNum;
sNum = pNtHeader->FileHeader.NumberOfSections;
for (int i = 0; iName);
if ((p_section_header->VirtualAddress <= Rva) && Rva<(p_section_header->VirtualAddress + p_section_header->SizeOfRawData))
{
return Rva - p_section_header->VirtualAddress + p_section_header->PointerToRawData;
}
p_section_header++;
}
return 0;
}
int JyPe(const char* file){
//DOS头
PIMAGE_DOS_HEADER pImageDosHeader;
//NT头(包括PE标识+Image_File_Header+OptionHeader)
PIMAGE_NT_HEADERS pImageNtHeaders;
//标准PE头
PIMAGE_FILE_HEADER pImageFileHeader;
HANDLE hMapObject;
//DOS头
PUCHAR uFileMap;
//打开文件
HANDLE hFile;
hFile = CreateFile(file, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0);
if (hFile == NULL)
{
printf("打开文件失败\n");
system("pause");
return 0;
}
//创建映射关系
hMapObject = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (hMapObject == NULL)
{
printf("创建文件映射内核对对象失败\n");
system("pause");
return 0;
}
//获取映射内存首地址
uFileMap = (PUCHAR)MapViewOfFile(hMapObject, FILE_MAP_READ, 0, 0, 0);
if (uFileMap == NULL)
{
printf("映射到进程地址空间失败\n");
system("pause");
return 0;
}
pImageDosHeader = (PIMAGE_DOS_HEADER)uFileMap;
if (pImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("不是PE结构\n");
system("pause");
return 0;
}
//定位到NT PE头
pImageNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)uFileMap + pImageDosHeader->e_lfanew);
//导入表的相对虚拟地址(RVA)
ULONG rva_ofimporttable = pImageNtHeaders->OptionalHeader.DataDirectory[1].VirtualAddress;
//根据相对虚拟(rva)地址计算偏移地址(offset)
ULONG offset_importtable = RvaToOffset(pImageNtHeaders, rva_ofimporttable);
if (!offset_importtable)
{
printf("获取导入表偏移地址失败\n");
system("pause");
return 0;
}
//取得导入表的地址
IMAGE_IMPORT_DESCRIPTOR *pImportTable = (IMAGE_IMPORT_DESCRIPTOR *)((char*)uFileMap + offset_importtable);
IMAGE_IMPORT_DESCRIPTOR null_iid;
memset(&null_iid, 0, sizeof(null_iid));
//每个元素代表了一个引入的DLL。
for (int i = 0; memcmp(pImportTable + i, &null_iid, sizeof(null_iid)) != 0; i++)
{
char *dllName = (char*)(uFileMap + RvaToOffset(pImageNtHeaders, pImportTable[i].Name));
//拿到了DLL的名字
printf("模块[%d]: %s\n", i, (char*)dllName);
PIMAGE_THUNK_DATA32 pThunk = (PIMAGE_THUNK_DATA32)(uFileMap + RvaToOffset(pImageNtHeaders, pImportTable[i].FirstThunk));
while (pThunk->u1.Ordinal != NULL)
{
PIMAGE_IMPORT_BY_NAME pname = (PIMAGE_IMPORT_BY_NAME)(uFileMap + RvaToOffset(pImageNtHeaders, pThunk->u1.AddressOfData));
printf("函数编号: %d 名称: %s\n 地址:%x\n", pname->Hint,pname->Name, pThunk->u1.AddressOfData);
pThunk++;
}
}
system("pause");
}