LOADING

加载过慢请开启缓存 浏览器默认开启

PELoader

2023/3/18

PELoader?

首先要清楚PELoader是干什么的。

PELoader就相当于一个PE文件加载器,通过运行PELoader可以打开一个PE程序,该PE程序并不是系统为你打开的。运行起来的效果一般和双击是一样的。个人认为也是可以通过PEloader修改文件在内存中的数据的。

通过写PEloader可以让我们更清楚的了解PE文件的结构,以及运行原理,这对于理解计算机底层是十分有帮助的。

IDE选择

推荐使用Visual Studio

师傅在辅导我写PEloader时,指导我使用了Visual Studio 2022。VS的调试功能十分强大,可以在逐步调试时查看内存,将C代码反汇编等等。在代码不能顺利运行时可以更好的进行调试。

VS的安全检查比较严格,自己的代码运行时需要关闭安全检查。还有完成后要注意自己要运行的程序是32位还是64位的程序,以及VS本身是x86运行还是x64运行。

main()函数

因为代码量不小,所以我选择了分为main函数和头文件,再调用头文件中的函数。

在main函数中

1.文件路径

我们需要获得文件的地址,也就是文件路径字符串。

2.寻找文件

根据文件路径,通过fopen()函数找到文件,并给予权限。

if ((fp = fopen(file, "rb")) == NULL)
{
    printf("Open file fail");
    exit(1);
}

3.分配”磁盘”

计算出文件大小,并分配出一块等大小的内存区域(我声明为pBuf)

fseek(fp, 0, SEEK_END);
last = ftell(fp);
fsize = last;

4.复制

将文件的二进制信息全部复制到这块内存区域中

fseek(fp, 0, SEEK_SET);
pBuf = (PBYTE)malloc(fsize);
fread(pBuf, fsize, 1, fp);

Check一下PE

在将文件读入malloc()给予的内存空间中后,我们拥有了对文件进行操作的权限。在此我将pBuf视为在磁盘中,稍后再将“磁盘”中的文件信息传入内存。

pBuf进行一个检查,check以下该文件是不是PE文件,若不是,则退出程序。

判断PE文件方法即为判断DOS头中的e_magic和NT头中的Signature签名。

if (pDOSheader->e_magic == IMAGE_DOS_SIGNATURE) {
    if (pNTheader->Signature == IMAGE_NT_SIGNATURE) {
        return true;
        }
    }

LoadPE

这里开始进行最主要的加载过程

1.定位

利用传入的参数pBuf,得到DOS头,NT头,节区头并赋值

PIMAGE_DOS_HEADER pDOSheader = (PIMAGE_DOS_HEADER)pBuf;//赋值DOS头

PIMAGE_NT_HEADERS32 pNTheader = (PIMAGE_NT_HEADERS32)(pBuf + pDOSheader->e_lfanew);//赋值NT头

PIMAGE_SECTION_HEADER pSecheader = (PIMAGE_SECTION_HEADER)((PBYTE)(pNTheader)+ 0x18 + (pNTheader->FileHeader.SizeOfOptionalHeader));//赋值节区头

2.分配”内存”

通过VirtualAlloc()函数分配出一片和PE文件同样大的内存区域~,这个大小在可选头的SizeOfImage有保存。我们将这片区域pAlloc视为“内存”中。

PBYTE pAlloc = (PBYTE)VirtualAlloc(NULL, pNTheader->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

然后对这片区域利用memset()进行初始化。之后就可将在“磁盘”中的pBuf的文件头加载到“内存”区域的pAlloc。pAlloc的地址就是程序运行时的加载地址。

3.加载节区

通过一二步之后,准备工作就完成了。接下来要开始循环加载节区信息。

通过学习PE文件的知识,我们知道了磁盘文件或内存的节区大小必定为FileAlignmentSectionAlignment值的整数倍,若不为整数倍,计算机会自动补00对齐。

通过010 editor观察文件易发现节区头的VirtualAddressPointToRawData是已经对齐好的,我们不需要写偏移函数来手动对齐。所以只需要

memmove(pAlloc + pSecheader->VirtualAddress, pBuf + pSecheader->PointerToRawData, pSecheader->Misc.VirtualSize);

就可将节区加载进内存。但后面要补的0该怎么办呢?其实早在初始化pAlloc时就已经将不加载数据的区域补了0,并不需要担心。

加载完一个节区之后不要忘记进入下一个节区继续加载

pSecheader = (PIMAGE_SECTION_HEADER)((PBYTE)pSecheader + (BYTE)(sizeof(IMAGE_SECTION_HEADER)));

4.加载重定位表

加载完节区之后,我们还有重定位表和导入表没有处理。先处理重定位表。

值得注意的是有的PE文件并不存在重定位表,若重定位表地址为00000000,则我们应直接跳过重定位表的处理;有的PE文件重定位表也不止一个,不过可选头中给的重定位表的大小是所有重定位表的总和,利用这个大小即可知道是否处理完了重定位表。

先定位找到重定位表,并确定重定位表的大小

PIMAGE_BASE_RELOCATION pBaseReloc = (PIMAGE_BASE_RELOCATION)(pNTheader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress + pAlloc);

int SizeOfBaseReloc = pNTheader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;

这样就算有多个重定位表,我们也能找到各个重定位表的地址和大小了。

pBaseReloc->VirtualAddress;
pBaseReloc->SizeOfBlock;

通过这两个数据即可得到块的数量和每个块的数据。

通过对重定位表的学习,我们已清楚重定位原理。先利用位操作得到每个块的前四位,判断每个块的类型,一般为3;并同时得到后12位的偏移地址

WORD type = TypeOffset[i] >> 12;
WORD offset = TypeOffset[i] & 0x0FFF;

偏移地址加上VirtualAddress可得出一个地址,该地址存储着程序的硬编码地址,应读取该值之后减去ImageBase值,再加上实际加载地址pAlloc,得到实际重定位后的硬编码地址。

*((DWORD*)(offset + (pBaseReloc->VirtualAddress) + pAlloc)) - pNTheader->OptionalHeader.ImageBase + (DWORD)pAlloc;

我们将这个值覆盖存储到原先的硬编码地址,重定位就完成了。

若还有多个重定位表,记得在加载完成后给pBaseReloc加上一个SizeOfBlock

pBaseReloc = (PIMAGE_BASE_RELOCATION)((PBYTE)pBaseReloc + pBaseReloc->SizeOfBlock);

接着回去加载下一个重定位表

5.加载导入表

重定位表处理完成后,就该处理导入表了。

导入表拥有多个dll文件,dll文件中也有很多函数,要循环加载。

先声明得到导入表的位置

PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pNTheader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + pAlloc);

pImport->Name就是dll文件的文件名字符串的地址,再加上加载地址pAlloc找到字符串,利用win32的LoadLibrary()函数加载dll文件进入内存。

加载完dll之后就可以计算IAT和INT的地址,开始加载dll中的函数了。

IAT和INT下都有一个联合体(union),为u1

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;

拥有四个元素,这四个元素公用了同一个DWORD,即为同一个值。但该值可拥有多种作用,便有了多种写法。

在导入函数时,先判断一下函数是使用哪种方法导入的。

若是用序号导入函数,则INT中存储的地址最高位即为1。故进行判断

if (pINT->u1.AddressOfData & IMAGE_ORDINAL_FLAG32)

若为真,则函数使用序号导入,而不使用字符串来查找函数

pIAT->u1.AddressOfData = (DWORD)(GetProcAddress(hProcess, (LPCSTR)(pINT->u1.Ordinal)));

若为假,就可能需要使用字符串查找函数地址

pIAT->u1.AddressOfData = (DWORD)(GetProcAddress(hProcess, pFucname->Name));

函数导入完毕,别忘了

pINT++;
pIAT++;

导入完一个dll之后也要

pImport++;

这里的+1是加了IMAGE_IMPORT_DESCRIPTOR一个结构体的大小,而不是一个字节,所以可以跳到下一个dll的导入表。

进入main()函数

至此,已load完成。接下来就是利用函数指针,指向EntryPoint,即可调用主函数,运行程序。

Congratulation!

顺利的话程序这样就已经运行起来了,恭喜。

致谢

非常感谢Jev0n,s0rry,l0v3ming师傅的帮助。由于自身过于愚笨,全篇PEloader写的bug百出,内容思路也仿照(抄)Jev0n师傅的loader,忙了师傅们一天才帮忙调完可以运行。完成度也很低,只能做到最低要求的运行起一个PE文件。

不过最终运行起来的时候还是挺开心的,从中学到了不少东西,复习了PE文件结构的相关知识,并认识的更深了。师傅说学好windows逆向,熟悉PE结构是十分重要的。

我希望我之后能誊出时间写一份完成度更高的PEloader。

谢谢师傅们的指导(鞠躬)

MyPEloader链接

https://github.com/lo1see/SomeGarbage