在.NET 红队领域,尤其是涉及到底层内存操作、代码注入相关的场景,比如自定义加载器、安全测试工具开发等,常常会遇到一个令人困惑的现象:同样的 ShellCode 加载逻辑,在.NET Framework 4.8 及之前版本中能够正常运行,没有任何报错,但迁移到.NET Core 或者.NET 5 及更高版本后,程序会直接崩溃。这个问题并非偶然,而是.NET 平台在架构演进过程中,为了提升安全性而做出的内存保护策略调整所导致的。
迁移到.NET Core 或者.NET 5 及更高版本.NET 6、.NET 7、.NET 8、.NET 9 后,程序会直接崩溃,并抛出System.AccessViolationException异常,异常信息显示 “Attempted to read or write protected memory. This is often an indication that other memory is corrupt.”。
要彻底解决这个问题,我们需要先深入理解.NET Framework 与.NET Core/.NET 5 + 在 JIT 编译内存段权限设置上的核心差异,再针对性地给出符合新版本内存安全规则的解决方案。
要理解 ShellCode 运行失效的本质,首先需要明确一个关键概念:
JIT 编译后的代码存放位置及其内存权限。在.NET 平台中,无论是.NET Framework 还是.NET Core/.NET 5+,当程序运行时,.NET 等高级语言代码会先被编译为中间语言,然后在程序执行过程中,由即时编译器将 IL 代码动态编译为机器码,这些机器码会被存放在特定的内存段中,供 CPU 直接执行。
2.1 宽松的RWX
- 可读取(R):CPU 可以从该内存段读取数据;
- 可写入(W):程序可以向该内存段写入数据;
- 可执行(X):CPU 可以将该内存段中的数据作为机器码执行。
对于 ShellCode 加载场景而言,这种 RWX 权限策略非常 “友好”—— 开发者无需额外处理内存权限,直接将 ShellCode 写入 JIT 编译后的内存段,或者在该内存段中分配空间存放 ShellCode,都可以正常执行。这也是为什么在.NET Framework 中,Loader 回调 ShellCode 不会报错的核心原因。
但从安全性角度来看,RWX 权限是一把 “双刃剑”。它不仅允许开发者正常写入和执行合法代码,也为恶意代码提供了可乘之机 —— 恶意代码可以轻易地将自身写入具备 RWX 权限的内存段并执行,从而绕过系统的内存安全防护。因此,这种宽松的内存权限策略,在安全性日益重要的今天,逐渐暴露出明显的缺陷。
2.2 严格的RX
这种权限调整带来的变化是:保留 “可读取(R)” 和 “可执行(X)”:确保 JIT 编译后的合法机器码能够被正常读取和执行,不影响程序的正常运行;移除 “可写入(W)”:禁止任何程序向该内存段写入数据,无论是合法的 ShellCode 还是恶意代码,都无法修改该内存段中的内容。
这一调整直接命中了 ShellCode 运行的 “痛点”—— 当试图将 ShellCode 写入 JIT 编译后的 RX 权限内存段时,由于内存段不具备 “可写入” 权限,系统会立即触发内存保护机制,抛出System.AccessViolationException异常,提示 “尝试读取或写入受保护的内存”,程序随之崩溃。
需要注意的是,这种 RX 权限并非 一成不变 .NET Core/.NET 5 + 的运行时并非完全禁止修改内存权限,而是通过更严格的机制来管理内存权限的变更。
运行时自身会在必要时,通过VirtualProtect函数动态调整内存权限,但编译完成后会立即将权限改回 RX,以此平衡效率与安全。而对于开发者自定义的 ShellCode 写入操作,运行时不会主动开放写入权限,必须手动处理。
2.3 调整内存权限
既然.NET Core/.NET 5 + 中 JIT 内存段的默认权限是 RX,无法直接写入 ShellCode,那么解决方案的核心就很明确了:在写入 ShellCode 之前,通过VirtualProtect函数将目标内存段的权限临时修改为 “可执行 + 可读写(PAGE_EXECUTE_READWRITE,权限值 0x40)”,写入完成后再根据需求决定是否恢复为 RX 权限。
using System;using System.Runtime.InteropServices;publicclassShellCodeExecutor{// 声明VirtualProtect函数(P/Invoke) [DllImport("kernel32.dll", SetLastError = true)]privatestaticexternboolVirtualProtect( IntPtr lpAddress, UIntPtr dwSize,uint flNewProtect,outuint lpflOldProtect);// 声明VirtualAlloc函数(用于分配可执行内存) [DllImport("kernel32.dll", SetLastError = true)]privatestaticextern IntPtr VirtualAlloc( IntPtr lpAddress, UIntPtr dwSize,uint flAllocationType,uint flProtect);// 声明VirtualFree函数(用于释放内存) [DllImport("kernel32.dll", SetLastError = true)]privatestaticexternboolVirtualFree( IntPtr lpAddress, UIntPtr dwSize,uint dwFreeType);// 内存保护属性常量privateconstuint PAGE_EXECUTE_READWRITE = 0x40; // 可执行、可读、可写privateconstuint PAGE_READWRITE = 0x04; // 可读、可写(默认分配时的权限)privateconstuint MEM_COMMIT = 0x1000; // 提交内存(分配物理内存)privateconstuint MEM_RELEASE = 0x8000; // 释放内存}
首先通过VirtualAlloc分配一块足够存放 ShellCode 的内存(默认权限为PAGE_READWRITE,即可读可写但不可执行),然后调用VirtualProtect将该内存的权限修改为PAGE_EXECUTE_READWRITE,为后续写入和执行 ShellCode 做准备。
.NET Core 及.NET 5 + 版本中 ShellCode 运行失效的问题,本质上是平台从 “效率优先” 向 “安全优先” 演进的必然结果。通过将 JIT 内存段的默认权限从 RWX 调整为 RX,.NET 平台显著提升了对恶意代码注入的抵御能力,但也对依赖底层内存操作的开发者提出了更高的要求 —— 必须遵循系统的内存安全规则,通过VirtualProtect等 API 手动管理内存权限。
免责声明:此文所提供的信息只为网络安全人员对自己所负责的网站、服务器等进行检测或维护参考,未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。任何未经授权的网络渗透、入侵或对他人网络破坏的活动而造成的直接或间接后果和损失,均由使用者为自身的行为负责并承担全部的法律和连带责任,与本号及作者无关,请务必遵循相关法律法规。本文所提供的工具仅用于学习和本地安全研究和测试,禁止用于其他方面。
以上相关的知识点已收录于新书《.NET安全攻防指南》,全书共计25章,总计1010页,分为上下册,横跨.NET Web代码审计与红队渗透两大领域。
上册深入剖析.NET Web安全审计的核心技术,帮助读者掌握漏洞发现与修复的精髓;下册则聚焦于.NET逆向工程与攻防对抗的实战技巧,揭秘最新的对抗策略与技术方法。
20+专栏文章
海量资源和文档
专属成员交流群
已入驻的大咖们
欢迎加入我们
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...