Windows核心编程_PE文件格式解析

接上一篇的理论知识: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掉了。

 

运行结果:

Windows核心编程_PE文件格式解析_第1张图片

 

 

完整代码:

#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");
}

 

我们取消打印节的字段,就可以看到当前程序使用哪些模块,模块对应的函数名

同时也可以获取地址:

Windows核心编程_PE文件格式解析_第2张图片

 

知道了地址,我们在转换成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");
}

 

你可能感兴趣的:(Windows核心编程)