首页
社区
课程
招聘
[原创]PE映像切换技术(Process Hollowing)不需要填充IAT表和进行重定位的原因
发表于: 2021-6-8 11:03 12563

[原创]PE映像切换技术(Process Hollowing)不需要填充IAT表和进行重定位的原因

2021-6-8 11:03
12563

记下笔记以及一点小思考,让诸位见笑啦

在学习了PE加载器的实现原理后,我发现这个加载PE的过程与PE映像切换技术(Process Hollowing)有点类似,区别在于后者并没有去填充内存中PE文件的IAT表与进行重定位。那同样是把PE文件手动从硬盘里加载到内存中运行,为什么PE映像切换技术就没有去对内存中PE文件的IAT表进行操作呢?

为了叙述的完整,先介绍一下PE映像切换技术与PE加载器的具体原理。

image-20210607201713573

效果就是套一个傀儡进程的壳来执行我们希望执行的其他PE文件,《逆向工程核心原理》中给出的代码实现如下:

1、创建挂起的傀儡进程

2、卸载掉原来的模块

3、写入新的文件

4、恢复现场

在看雪知识库的Windows安全-系统篇-PE格式-PE文件的加载章节收录了许多PE加载器的实现文章。实现上只要模拟操作系统加载PE文件的方式来做就可以,简单来说分为以下几个步骤:

1.申请一块内存,将PE文件由硬盘加载到内存中,这部分与上文基本相同

2.修复重定位

根据重定位表的内容,把PE文件中的对应位置的硬编码地址进行重定位

例如,假设该PE文件的默认基址为0x00400000,加载到内存后的基址为0x00100000

image-20210608095023797

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

image-20210608094556603

3.加载导入表

先根据IDT里的dll名称用LoadLibrary()加载对应的dll

image-20210608100629664

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

image-20210608100842238

4.跳转到PE的入口点处执行

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

image-20210608103328608

进程初始化有一部分实在新的进程中进行的,就比如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头的后面

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 6
支持
分享
最新回复 (6)
雪    币: 7373
活跃值: (4075)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
顶一个。。
2021-6-9 10:10
0
雪    币: 25
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
顶一个
2021-6-22 16:53
0
雪    币: 43
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
楼主有一点说的不准确,如果原imagebase无法virtualalloc到的话,是需要进行手动修复重定位表的
2021-7-7 02:33
0
雪    币: 1367
活跃值: (2121)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
5
mb_aydzspbs 楼主有一点说的不准确,如果原imagebase无法virtualalloc到的话,是需要进行手动修复重定位表的
确实,感谢补充
2021-7-7 15:06
0
雪    币: 876
活跃值: (584)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
2021-7-8 07:56
0
雪    币: 206
活跃值: (1991)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
收藏吃灰
2021-7-8 11:57
0
游客
登录 | 注册 方可回帖
返回
//