PE加载器——你爱的很沉重,可还是得看她想不想要

构造PE加载器,简而言之就是手动把PE文件在内存中展开,并且如果有重定位表的话手动修复重定位表,以及修复IAT表等一些重要数据。
源码:

#include<stdio.h>
#include<windows.h>
#include<winnt.h>
int main()
{
	HANDLE hFile = CreateFileA("你的测试程序的绝对地址", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	DWORD dwSize = GetFileSize(hFile, NULL);
	DWORD dwRubbish = 0;
	DWORD FileSize = GetFileSize(hFile, NULL);
	LPDWORD SizeToRead = 0;
	//申请一块和文件大小相同的新内存
	unsigned char * pBuf = new unsigned char[FileSize];
	//将这块内存用0填充
	ZeroMemory(pBuf, FileSize);
	//填充之后,将文件读取到这段内存空间里边,这个时候文件将保存在磁盘上的状态,也就是需要进行地址转换
	int i = ReadFile(hFile, pBuf, FileSize, SizeToRead, NULL);
	if (i == 0)
	{
		printf("文件读取失败");
	}
	//定位NT头
	PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBuf;
	PIMAGE_NT_HEADERS  pNt = (PIMAGE_NT_HEADERS)(pBuf + pDos->e_lfanew);
	/*到这里先梳理一下思路:
	1、要将PE文件在内存中展开   需要先把PE头、DOS头这些数据复制过去
	2、在此之前需要先申请一块内存来存放展开的PE文件
	3、申请的空间大小可以使用SizeOfImage来获取*/
	DWORD	ImageSize = pNt->OptionalHeader.SizeOfImage;
	LPVOID lpMemBuffer = VirtualAlloc(NULL, ImageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE)ZeroMemory(lpMemBuffer, ImageSize);//把申请到的空间先用0填充
	//先复制整个PE头的数据
	CopyMemory(lpMemBuffer,pBuf,pNt->OptionalHeader.SizeOfHeaders);
	//之后将节区表复制过去
	//首先定位节区表头
	PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)(pNt + sizeof(PIMAGE_NT_HEADERS));
	//获取节区表的数目
	DWORD SecNum = pNt->FileHeader.NumberOfSections;
	for (int i = 0; i < SecNum; i++)
	{
		//如果当前节区表的数据为空,复制下一个节区表的数据
		if (pSec->VirtualAddress == 0 || pSec->PointerToRawData == 0)
		{
			pSec++;
			continue;
		}
		//复制节区表数据的时候需要注意,复制到哪,从哪复制
		//首先复制到哪、从哪开始复制、复制多大的字节数据
		CopyMemory((LPBYTE)lpMemBuffer+pSec->VirtualAddress,pBuf+pSec->PointerToRawData,pSec->SizeOfRawData);
		pSec++;
	}
	//完成以上步骤之后,数据已经从磁盘上的状态完全映射到内存中去了,但是现在程序还不能运行,还需下一步的操作
	//注意释放内存
	free(pBuf);
	PIMAGE_DOS_HEADER pDos1 = (PIMAGE_DOS_HEADER)lpMemBuffer;
	PIMAGE_NT_HEADERS  pNt1 = (PIMAGE_NT_HEADERS)((LPBYTE)lpMemBuffer + pDos->e_lfanew);
	//之后的工作是修复重定位表
	//首先需要先定位重定位表
	PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)(pNt1->OptionalHeader.DataDirectory[5].VirtualAddress+pDos1);
	if (pReloc->VirtualAddress == 0)
	{
		printf("没有重定位表");
	}
	while (pReloc->VirtualAddress != 0 && pReloc->SizeOfBlock != 0)
	{
		//获取要修正的数据
		LPWORD pReData = (LPWORD)(pReloc + sizeof(PIMAGE_BASE_RELOCATION));
		//获取需要修正的数据的个数
		DWORD pReNum = (pReloc->SizeOfBlock - sizeof(PIMAGE_BASE_RELOCATION)) / sizeof(WORD);
		for (i = 0; i < pReNum; i++)
		{
			if (pReData[i] & 0xF000 == 0x3000)//这是一个需要进行修正的数据前4位代表类型
			{
				LPDWORD  TureData =(LPDWORD) ((LPBYTE)lpMemBuffer + pReloc->VirtualAddress + (pReData[i] & 0x0FFF));//后12位代表硬编码数据的地址,这个获取的是RVA
				*TureData = *TureData - pNt1->OptionalHeader.ImageBase + (DWORD)pDos1;  //上边获取到的RVA地址处的内容减去原来的基址、加上新的基址就是修复之后的值
				printf("重定位表修复中");
			}
		}
		pReloc = (PIMAGE_BASE_RELOCATION)((LPBYTE)pReloc + pReloc->SizeOfBlock);//移动重定位表指针、指向下一个数据结构
	}
	printf("重定位表修复完成");
	//接下来需要修复的是IAT表
	//首先定位导入表的位置
	PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)(pNt1->OptionalHeader.DataDirectory[1].VirtualAddress);
	//定义一个存储函数地址的指针
	FARPROC FuncAddr;
	//导入表的结束标志是全零的结构
	while (pImportTable->OriginalFirstThunk)
	{
		PIMAGE_THUNK_DATA pImportOri = PIMAGE_THUNK_DATA(pImportTable->OriginalFirstThunk);
		PIMAGE_THUNK_DATA pImportFir = PIMAGE_THUNK_DATA(pImportTable->FirstThunk);
		//首先获取需要加载的DLL的名称
		LPCSTR DllName = (LPCSTR)(pImportTable->Name+ (LPBYTE)lpMemBuffer);
		printf("%s需要修正", DllName);
		HMODULE hMoudle = LoadLibraryA(DllName);
		if (hMoudle == NULL)
		{
			printf("%s加载失败", DllName);
		}
		//接下来开始导入需要用到的函数
		//具体要用哪个函数从INT表里边拿取,之后将获取到的函数的地址放到IAT表里边
		i = 0;
		while (pImportOri[i].u1.AddressOfData)
		{
			PIMAGE_IMPORT_BY_NAME pImportName = (PIMAGE_IMPORT_BY_NAME)(pImportOri[i].u1.AddressOfData);
			if (pImportOri[i].u1.Ordinal & 0x80000000 == 1)
			{
				//序号导入
				FuncAddr=GetProcAddress(hMoudle, (LPCSTR)(pImportOri[i].u1.Ordinal& 0xFFFF));
			}
			else
			{
				//名称导入
				FuncAddr = GetProcAddress(hMoudle, (LPCSTR)(pImportName->Name));
			}
			pImportFir[i].u1.Function = FuncAddr;
			i++;
		}
		pImportTable++;
	}
	printf("IAT修复结束");
	//修改镜像大小
	pNt1->OptionalHeader.ImageBase = (DWORD)lpMemBuffer;
	//最后的操作就是修正程序的入口地址
	FARPROC EOP = (FARPROC)((LPBYTE)lpMemBuffer + pNt1->OptionalHeader.AddressOfEntryPoint);
	EOP();
	free(lpMemBuffer);
}

整个过程中需要注意的几点:
1:申请空间之后,使用完毕要及时释放已经不要的空间
2:注意在申请一段新的空间之后,使用0来填充这一段空间的用意是:在内存中展开的文件和磁盘上的文件的对齐粒度是不同的,我们先用0来将整段空间覆盖,之后,复制数据的时候只复制有数据存在的一部分,中间的数据不用管就好了.
3:复制节区的数据的时候,之所以从VirtualAddress开始复制,也是基于第二点原因
4:修复重定位表的时候需要注意:

//获取要修正的数据
		LPWORD pReData = (LPWORD)(pReloc + sizeof(PIMAGE_BASE_RELOCATION));
		//获取需要修正的数据的个数
		DWORD pReNum = (pReloc->SizeOfBlock - sizeof(PIMAGE_BASE_RELOCATION)) / sizeof(WORD);

这两行代码的意思,可以用下图进行解释:
在这里插入图片描述
需要修正的数据以数组的形式存在重定位表块,重定位表块的大小是将重定位表的数据结构也包含进去的,所以在计算休要重定位的数据的个数的时候,需要先把重定位表的数据结构的大小减去,之后除以WORD数据类型的大小,是因为.每一项需要修正的数据的数据类型是WORD类型的.
5:修复重定位数据的时候,需要先进性判断,该书是不是需要修复,这个可以通过

if (pReData[i] & 0xF000 == 0x3000)

这一行代码进行实现,因为需要修正的数据项是一个WORD类型的数据,而这个数据的前4位正式纪录了这样的信息,后4位记录的信息就是硬编码数据的实际地址.
6:获得硬编码数据的实际偏移地址之后(这个地址实际上是一个RVA),找到对应地址的内容减去原来的ImageBase的值再加上实际的加载的位置,就是修复之后的数据,实际加载的基址就是我们申请的空间的起始地址.
7:在整个过程中,用到了内存空间里的磁盘数据转换为文件内存中的镜像,需要注意的是,我们在这其中使用了两块内存地址,其中一块用来存放文件在磁盘上的状态,而另一块就是我们手动将文件在内存中展开,在编写过程中要注意数据的转换以及地址的及时释放.
8:修复IAT表的顺序和遍历导入表的顺序大同小异,只需牢记,INT是导入名称表\IAT是导入地址表.找导入函数的名称去INT,找导入函数的地址去IAT.
因此我们修复IAT表的过程大概如下:
1:获取导入的DLL模块
2:加载对应的模块,执行LoadLibrary()函数
3:获取导入函数的名称或者序号
4:执行GetProcAddress()函数
5:定位IAT表的地址
6:将GetProcAddress()函数的返回值存储到IAT表中,循环执行,直到遇到全零的导入表数据结构.

最后的工作就是修复镜像的大小以及修改入口点的地址.

总结:其实还有一点,是比较重要的,代码中有对于节区表数据是否有效的判断,但是没有对其进行处理,采用的方法是,如果无效不进行复制.直接复制下一个节区的数据,其实这样处理会有隐患造成.如果确实是有节区数据不存在的状况发生的话.我们复制过去的节区表的个数相对于原来的个数就会相应的减少,因此我们在程序的最后会需要加上对于节区表的校验.也很简单,可以设置一个临时变量,如果存在数据无效的节区表我们对其进行计数,在程序的最后再修正PE头里边的SectionOfNumber字段.

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PE(Portable Executable)加载是一种用于加载和执行Windows可执行文件(.exe)的组件。PE加载可以加载并执行使用MFC(Microsoft Foundation Classes)开发的可执行文件。 MFC是一种用C++编写的框架,旨在简化Windows应用程序的开发。MFC提供了许多类和函数,可以用于创建窗口、处理用户输入、绘制图形等任务。使用MFC开发的应用程序被编译为可执行文件,可以通过PE加载加载和执行。 当PE加载加载一个使用MFC开发的可执行文件时,它会首先解析PE文件的头部,确定文件的结构和特性。然后,加载会遍历PE文件的节表,加载每个节到内存中,并按照特定的顺序将节中的代码和数据复制到进程的地址空间中。 对于使用MFC开发的可执行文件,加载会识别并加载与MFC相关的节。这些节包含MFC框架所需的代码和数据。加载会将这些MFC相关的节加载到进程的地址空间中,并正确地设置它们的属性和访问权限。 加载会查找MFC所需的依赖项,例如MFC库文件和其他相关的动态链接库(DLL)。它会自动加载并链接这些库文件,以确保应用程序正常运行。 一旦MFC相关的代码和数据加载完毕,加载会调用程序的入口点,并开始执行代码。MFC框架会根据应用程序的逻辑,处理用户输入、响应事件、调用相应的MFC类和函数,最终显示和更新屏幕上的内容。 总之,PE加载可以正确加载和执行使用MFC开发的可执行文件。它负责将MFC框架相关的代码和数据加载到进程的地址空间中,并与必要的依赖项进行链接,以确保应用程序能够正常运行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值