构造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字段.