记下笔记以及一点小思考,让诸位见笑啦
在学习了PE加载器的实现原理后,我发现这个加载PE的过程与PE映像切换技术(Process Hollowing)有点类似,区别在于后者并没有去填充内存中PE文件的IAT表与进行重定位。那同样是把PE文件手动从硬盘里加载到内存中运行,为什么PE映像切换技术就没有去对内存中PE文件的IAT表进行操作呢?
为了叙述的完整,先介绍一下PE映像切换技术与PE加载器的具体原理。

效果就是套一个傀儡进程的壳来执行我们希望执行的其他PE文件,《逆向工程核心原理》中给出的代码实现如下:
1、创建挂起的傀儡进程
2、卸载掉原来的模块
3、写入新的文件
4、恢复现场
在看雪知识库的Windows安全-系统篇-PE格式-PE文件的加载章节收录了许多PE加载器的实现文章。实现上只要模拟操作系统加载PE文件的方式来做就可以,简单来说分为以下几个步骤:
1.申请一块内存,将PE文件由硬盘加载到内存中,这部分与上文基本相同
2.修复重定位
根据重定位表的内容,把PE文件中的对应位置的硬编码地址进行重定位
例如,假设该PE文件的默认基址为0x00400000,加载到内存后的基址为0x00100000

就是把RVA为10F5处的硬编码地址0x00467C28重定位为0x00167C28,即减去原基址再加上实际的基址

3.加载导入表
先根据IDT里的dll名称用LoadLibrary()加载对应的dll

然后到对应dll的导入表项(理论上应该去INT中,但实际上这俩表内容是一样的)中用GetProcAddress()获取导入的函数地址,并写到导入表的相应位置

4.跳转到PE的入口点处执行
问题的关键就是因为PE映像切换技术使用了CreateProcess()来挂起创建一个傀儡进程,当恢复线程执行后还会进行一些进程初始化的工作,所以不用我们手工的去填IAT表和进行重定位。

进程初始化有一部分实在新的进程中进行的,就比如IAT表的填充和重定位。当初始线程启动时,首先会执行KiThreadStartup,把目标线程的IRQL从DPC级降低到APC级。然后调用PspUserThreadStartup,将用户空间ntdll.dll中的函数LdrInitializeThunk作为APC函数挂入APC队列,再企图返回到用户空间,执行LdrInitializeThunk,正是在这个函数中,进行了IAT表的填充以及重定位。
《逆向工程核心原理》
Process Hollowing and Portable Executable Relocations
常见进程注入的实现及内存dump分析——Process Hollowing(冷注入)
PE加载器的简单实现
一种保护应用程序的方法 模拟Windows PE加载器,从内存资源中加载DLL
《深入解析Windows操作系统》
《漫谈兼容内核》
CreateProcess(NULL, FakeProcesssPath, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)
CreateProcess(NULL, FakeProcesssPath, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)
// 通过PEB获取傀儡进程的映像基址
GetThreadContext(pi->hThread, &ctx)
ReadProcessMemory(
pi->hProcess,
(LPCVOID)(ctx.Ebx + 8), // ctx.Ebx = PEB, ctx.Ebx + 8 = PEB.ImageBase
&dwFakeProcImageBase,
sizeof(DWORD),
NULL) )
...
// 卸载原傀儡进程映像
pFunc = GetProcAddress(GetModuleHandle(L"ntdll.dll"), "ZwUnmapViewOfSection");
(PFZWUNMAPVIEWOFSECTION)pFunc)(pi->hProcess, (PVOID)dwFakeProcImageBase);
// 通过PEB获取傀儡进程的映像基址
GetThreadContext(pi->hThread, &ctx)
ReadProcessMemory(
pi->hProcess,
(LPCVOID)(ctx.Ebx + 8), // ctx.Ebx = PEB, ctx.Ebx + 8 = PEB.ImageBase
&dwFakeProcImageBase,
sizeof(DWORD),
NULL) )
...
// 卸载原傀儡进程映像
pFunc = GetProcAddress(GetModuleHandle(L"ntdll.dll"), "ZwUnmapViewOfSection");
(PFZWUNMAPVIEWOFSECTION)pFunc)(pi->hProcess, (PVOID)dwFakeProcImageBase);
// 从硬盘上读取目标PE文件
hFile = CreateFile(RealProcessPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
dwFileSize = GetFileSize(hFile, NULL);
ReadFile(hFile, pRealFileBuf, dwFileSize, &dwBytesRead, NULL);
CloseHandle(hFile);
...
// 根据PE文件的偏移量得到各个PE头
// DOS头在PE文件的最开始
PIMAGE_DOS_HEADER pIDH = (PIMAGE_DOS_HEADER)pRealFileBuf;
// NT头的偏移量 = pIDH->e_lfanew, 可选头相对于NT头的偏移量 = 0x18
PIMAGE_OPTIONAL_HEADER pIOH = (PIMAGE_OPTIONAL_HEADER)(pRealFileBuf + pIDH->e_lfanew + 0x18);
// 节区头的偏移量 = NT头的偏移量 + NT头的大小, 因为节区头位于NT头的后面
PIMAGE_SECTION_HEADER pISH = (PIMAGE_SECTION_HEADER)(pRealFileBuf + pIDH->e_lfanew + sizeof(IMAGE_NT_HEADERS));
// 在傀儡进程中,目标PE文件基址的地址处,分配目标PE文件大小的内存
pRealProcImage = (LPBYTE)VirtualAllocEx(
pi->hProcess,
(LPVOID)pIOH->ImageBase,
pIOH->SizeOfImage,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE)
// 写入PE头
WriteProcessMemory(
pi->hProcess,
pRealProcImage,
pRealFileBuf,
pIOH->SizeOfHeaders,
NULL);
// 写入各节区
for( int i = 0; i < pIFH->NumberOfSections; i++, pISH++ ){
if( pISH->SizeOfRawData != 0 ){
// 这里注意要将各节区写到对应的 映像基址+RVA 处的内存中
if( !WriteProcessMemory(
ppi->hProcess,
pRealProcImage + pISH->VirtualAddress,
pRealFileBuf + pISH->PointerToRawData,
pISH->SizeOfRawData,
NULL) ){
printf("WriteProcessMemory(%.8X) failed!!! [%d]\n",
pRealProcImage + pISH->VirtualAddress, GetLastError());
return FALSE;
}
}
}
// 从硬盘上读取目标PE文件
hFile = CreateFile(RealProcessPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
dwFileSize = GetFileSize(hFile, NULL);
ReadFile(hFile, pRealFileBuf, dwFileSize, &dwBytesRead, NULL);
CloseHandle(hFile);
...
// 根据PE文件的偏移量得到各个PE头
// DOS头在PE文件的最开始
PIMAGE_DOS_HEADER pIDH = (PIMAGE_DOS_HEADER)pRealFileBuf;
// NT头的偏移量 = pIDH->e_lfanew, 可选头相对于NT头的偏移量 = 0x18
PIMAGE_OPTIONAL_HEADER pIOH = (PIMAGE_OPTIONAL_HEADER)(pRealFileBuf + pIDH->e_lfanew + 0x18);
// 节区头的偏移量 = NT头的偏移量 + NT头的大小, 因为节区头位于NT头的后面
[培训]传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!