首先先介绍下什么是PEloader,它是相当于是一个PE加载器,运行该程序可以打开一个PE文件,如exe文件。而效果就如同双击该exe。PEloader可以帮助从本质上了解exe文件的格式和运行原理。然后我要介绍的是如何写一个简单的PEloader。
要完成这个任务需要的知识有对PE文件的基本了解,对指针的熟练运用和对指针的深入理解,然后C语言方面学完结构体基本就可以了,其次是对如何运用代码对文件进行操作有一些了解和会最基本的操作,如使用fread读取文件,用stat获取一个文件的一些基本信息。知道了这些,就可以着手于PEloader的建立了。其实比较好笑的一点是,在写这个之前我的PE文件格式并不是很深入了解,指针也只是大致知道,但是在敲PEloader的过程中,对于结构体、指针、内存与地址等的理解,在不断的调试中有了很大的进步。所以,写一个PEloader对于学习PE文件和指针原理是很有帮助的。
目录框架
1.将文件信息导入内存。
2.分配运行程序所需的内存,并装载PE头,同时检查PE头是否准确。
3.装载节区。
4.修复重定位表。
5.修复导入表。
1.将文件信息导入内存,再开始进行程序运行时的内存分配
这段要做的事情很简单,就是将exe的文件内容读取到内存中便于后期装载使用。
FILE* fp = fopen(fpp, "rb");//读取文件
if (fp == NULL)
{
printf("文件打开失败\n");
return 0;
}
fseek(fp, 0, SEEK_END);//指向尾端
int last = ftell(fp);
int fsize = last;//获取大小
fseek(fp, 0, SEEK_SET);//重新指向文件首端
PBYTE pBuf = (PBYTE)malloc(fsize);
fread(pBuf, fsize, 1, fp);//拷贝文件二进制信息到pBuf这块来,用来装载至另一块内存
2..分配运行程序所需的内存,并装载PE头,同时检查PE头是否准确
PEloader的代码并不需要花太久来写,但是写的过程中难免会有小问题,所以,在写的时候同时把检查的代码也写了有助于减少不必要的工作量。
比如写一段检查DOS头和NT头的代码,有些变量的声明我没有展示,但一般就是给它赋值的对象的强转类型。如pDH为PIMAGE_DOS_HEADER类型,即DOS头结构体指针。
pDH = (PIMAGE_DOS_HEADER)pBuf;
pNTH = (PIMAGE_NT_HEADERS32)(pBuf + pDH->e_lfanew);
if (pDH->e_magic != IMAGE_DOS_SIGNATURE)//检查DOS头和NT头
{
printf("DOS头错误。\n");
return 0;
}
if (pNTH->Signature != IMAGE_NT_SIGNATURE)
{
printf("NT头错误\n");
return 0;
}
通过可选头(OptionalHeader)找到文件在内存中运行时的大小,同时分配空间
//分配文件载入内存后的空间
PBYTE pe = (PBYTE)VirtualAlloc(NULL, pNTH->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
然后装载无需对齐的PE头
memset(pe, 0, pNTH->OptionalHeader.SizeOfImage);//初始化
DWORD sizeofPEH = pNTH->OptionalHeader.SizeOfHeaders;
//把PE头装载到分配的空间
memmove(pe, pBuf, sizeofPEH);
3.装载节区
其实节区虽然按理来说需要用0补齐,但是我们事先就把pe用0来初始化了,所以我们只要保证每个节区的开始数据也就是头部位置装载是正确的,那么这个节区按理来说就是装载到了正确的位置,但是我也没有深究节区在文件到内存的大小转化问题。
int num = pNTH->FileHeader.NumberOfSections;
//循环装载Section节区
for (int i = 0; i < num; i++)
{
if (pSH->PointerToRawData == NULL)
{
pSH = (PIMAGE_SECTION_HEADER)((PBYTE)pSH + (BYTE)(sizeof(IMAGE_SECTION_HEADER)));
continue;
}
memcpy(pe + pSH->VirtualAddress, pBuf + pSH->PointerToRawData, pSH->SizeOfRawData);//不需要再补0对齐了,因为一开始就全是0
pSH = (PIMAGE_SECTION_HEADER)((PBYTE)pSH + (BYTE)(sizeof(IMAGE_SECTION_HEADER)));//指向下一个节区头
}
4.修复重定位表
然后是关于重定位表的修复,这点要注意下类型的强转,因为很容易忽略或忘记了指针的类型赋值。
重定位表的修复就是找到Offset存储的地址,然后修改该地址存储的值,这一步的原因是有一些值在文件中是硬编码无法改变,而文件被加载的内存是不确定的,所以需要根据重定位表的记录来根据实际情况修改。
Address = (DWORD*)(pe + (pBR->VirtualAddress) + offset);//找到要修改的值的地址
*Address = *Address - pNTH->OptionalHeader.ImageBase + (DWORD)pe;//将值修改后并赋值回去
5.修复导入表
导入表的修复一共分两步,先是装载dll,然后由函数GetProcAddress得到dll中具体函数的地址,再赋值给相应位置的IAT。
首先加载dll,这里要注意自己的peloader要加载的是32位的还是64位的程序,把自己的peloader改成相应的位数程序即可。
char dllname[50] = { 0 };
strncpy(dllname, (char*)(pI->Name + pe), 49);
HMODULE hProcess = LoadLibraryA(dllname);//之前这里LoadLibrary找不到dll,因为加载的dll是32位的
然后是根据INT中的存储名来找到地址给pIAT相应位置赋值。这里有一点是IAT和INT结构体里是联合体,所以我们赋值时不用太在意赋值给结构体里的哪个元素。
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
最后就是装载地址,有两种情况,一种是INT内存储的是序号,另一种是存储的名字。
if (pINT->u1.AddressOfData & IMAGE_ORDINAL_FLAG32)
{
pIAT->u1.AddressOfData = (DWORD)(GetProcAddress(hProcess, (LPCSTR)(pINT->u1.Ordinal)));//是序号
}
else
{
pIAT->u1.AddressOfData = (DWORD)(GetProcAddress(hProcess, (LPCSTR)pFucname->Name)); //非序号
}
之后要对下一个IAT操作只需要IAT++即可,效果就是+了一个结构体大小。
最后就是进入要加载的程序的main函数运行它,有两种方法,一种是我没太看懂,是看其他师傅的,另一种是用汇编语言来jmp到main函数。第二种更易看。
至此,结束。
关于无法运行的话,要结合动调来看,启动main函数之前报错,那就结合winhex或010editor等工具和VS查看结构体是否找对,节区是否装载成功,也可以运行一份正确的peloader来同步检查自己的错误,如果到main也就是运行对象程序时失败,那就需要进入反汇编调试汇编指令,找到报错点,这一步推荐IDA和VS同步一起调试,方便查找错误。这里说一下我写的时候出现的几个错误,一个是节区装载时有点小问题导致qq运行时有一个循环读取到错误信息进入了死循环,还有就是我只是写了一个简易peloader,运行了一个dll调用dll的程序,没有修复dll等的代码,所以失败,但这个很难绷的错误点我花了三天才找到。
最后,这篇文章写的很潦草,之后有时间的话应该会再修改修改,写一篇更为详细的PEloader文章。
在这里放上我的源代码。
#include<stdio.h>
#include<Windows.h>
#define fpp "D:\\qq\\Bin\\QQScLauncher.exe"
#include<stdbool.h>
#include<stdlib.h>
#include <sys/stat.h>
typedef BOOL(__cdecl* ProcMain)();
int main()
{
PIMAGE_DOS_HEADER pDH = NULL;//前期的定义
PIMAGE_NT_HEADERS32 pNTH = NULL;
PIMAGE_FILE_HEADER pFH = NULL;
PIMAGE_OPTIONAL_HEADER pOH = NULL;
PIMAGE_SECTION_HEADER pSH = NULL;
FILE* fp = fopen(fpp, "rb");//读取文件
if (fp == NULL)
{
printf("文件打开失败\n");
return 0;
}
fseek(fp, 0, SEEK_END);//指向尾端
int last = ftell(fp);
int fsize = last;//获取大小
fseek(fp, 0, SEEK_SET);//重新指向文件首端
PBYTE pBuf = (PBYTE)malloc(fsize);
fread(pBuf, fsize, 1, fp);//拷贝文件二进制信息到pBuf这块来,用来装载至另一块内存
pDH = (PIMAGE_DOS_HEADER)pBuf;
pNTH = (PIMAGE_NT_HEADERS32)(pBuf + pDH->e_lfanew);
if (pDH->e_magic != IMAGE_DOS_SIGNATURE)//检查DOS头和NT头
{
printf("DOS头错误。\n");
return 0;
}
if (pNTH->Signature != IMAGE_NT_SIGNATURE)
{
printf("NT头错误\n");
return 0;
}
//根据NT头找到节区头
pSH = (PIMAGE_SECTION_HEADER)((PBYTE)(pNTH)+0x18 + (pNTH->FileHeader.SizeOfOptionalHeader));
//分配文件载入内存后的空间
PBYTE pe = (PBYTE)VirtualAlloc(NULL, pNTH->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (pe == NULL)
{
printf("分配空间失败\n");
return 0;
}
memset(pe, 0, pNTH->OptionalHeader.SizeOfImage);//初始化
DWORD sizeofPEH = pNTH->OptionalHeader.SizeOfHeaders;
//把PE头装载到分配的空间
memmove(pe, pBuf, sizeofPEH);
int num = pNTH->FileHeader.NumberOfSections;
//循环装载Section节区
for (int i = 0; i < num; i++)
{
if (pSH->PointerToRawData == NULL)
{
pSH = (PIMAGE_SECTION_HEADER)((PBYTE)pSH + (BYTE)(sizeof(IMAGE_SECTION_HEADER)));
continue;
}
memcpy(pe + pSH->VirtualAddress, pBuf + pSH->PointerToRawData, pSH->SizeOfRawData);//不需要再补0对齐了,因为一开始就全是0
pSH = (PIMAGE_SECTION_HEADER)((PBYTE)pSH + (BYTE)(sizeof(IMAGE_SECTION_HEADER)));//指向下一个节区头
}
//加载重定位表
PIMAGE_BASE_RELOCATION pBR = (PIMAGE_BASE_RELOCATION)(pe + pNTH->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);//pBR为Base_Relocation的缩写
int SizeofBR = pNTH->OptionalHeader.DataDirectory[5].Size;
if (pBR == NULL)
{
printf("重定位表错误。\n");
return 0;
}
//开始重定位//高危区
int h = 0;
while (true)//这个循环的对象是重定位表
{
if (pBR->SizeOfBlock == 0)
{
break;
}
PWORD TypeOffset = (PWORD)((PBYTE)pBR + 8);//54B927
num = (pBR->SizeOfBlock - 8) / 2;
for (int i = 0; i < num; i++)//这个的对象是每张重定位表的元素
{
WORD type = TypeOffset[i] >> 12;//低十二位才是数据的真正值,高四位是一个标记
WORD offset = TypeOffset[i] & 0x0FFF;//保留低十二位
PDWORD Address = 0;
if (type == 3)//标记正确
{ //节区已经装载好,重定位表在节区内,所以直接以pe为基址找
Address = (DWORD*)(pe + (pBR->VirtualAddress) + offset);//找到要修改的值的地址
*Address = *Address - pNTH->OptionalHeader.ImageBase + (DWORD)pe;//将值修改后并赋值回去
}
}
//重定位表不只是一张
pBR = (PIMAGE_BASE_RELOCATION)((PBYTE)pBR + pBR->SizeOfBlock);
}
PIMAGE_IMPORT_DESCRIPTOR pI = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pe + (DWORD)pNTH->OptionalHeader.DataDirectory[1].VirtualAddress);
if (pI == NULL)
{
printf("导入表错误\n");
return 0;
}
if (pI->Name == NULL)
{
printf("导入表函数名错误\n");
return 0;
}
SetDllDirectoryA("D:\\qq\\Bin");
while (pI->Name != NULL)
{
char dllname[50] = { 0 };
strncpy(dllname, (char*)(pI->Name + pe), 49);
HMODULE hProcess = LoadLibraryA(dllname);//之前这里LoadLibrary找不到dll,因为加载的dll是32位的
if (!hProcess)
{
char err[100];
sprintf(err, "未找到%s", dllname);
MessageBox(NULL, err, "Error", MB_OKCANCEL);
return 0;
}
PIMAGE_THUNK_DATA32 pINT = (PIMAGE_THUNK_DATA32)(pe + pI->OriginalFirstThunk);
PIMAGE_THUNK_DATA32 pIAT = (PIMAGE_THUNK_DATA32)(pe + pI->FirstThunk);
while ((DWORD)pINT->u1.AddressOfData != NULL)
{
PIMAGE_IMPORT_BY_NAME pFucname = (PIMAGE_IMPORT_BY_NAME)(pe + pINT->u1.AddressOfData);
if (pINT->u1.AddressOfData & IMAGE_ORDINAL_FLAG32)
{
pIAT->u1.AddressOfData = (DWORD)(GetProcAddress(hProcess, (LPCSTR)(pINT->u1.Ordinal)));//是序号
}
else
{
pIAT->u1.AddressOfData = (DWORD)(GetProcAddress(hProcess, (LPCSTR)pFucname->Name)); //非序号
}
pINT++;
pIAT++;
}
pI++;
}
ProcMain MMain = NULL;
MMain = (ProcMain)(pNTH->OptionalHeader.AddressOfEntryPoint + pe);
MMain();
return 0;
}