如何在C#中像C++一样从内存调用函数?带参数Shellcode调用异常解决
我之前踩过一模一样的坑!核心问题出在调用约定不匹配和托管/非托管边界的参数传递规则上,尤其是你还需要Shellcode跳回C#函数,这两点没处理好就会出现内存读不了的情况。下面一步步给你拆解解决方案:
1. 先搞定调用约定:这是带参数崩溃的核心原因
C++里默认用cdecl调用约定(栈由调用者清理),但C#的Marshal.GetDelegateFromFunctionPointer默认用的是StdCall(Windows下,栈由被调用者清理)。两种约定的栈处理逻辑完全不同,无参数时栈平衡问题没暴露,带参数直接就乱了。
解决方法:用[UnmanagedFunctionPointer]特性给委托明确指定和Shellcode一致的调用约定。比如Shellcode是C++写的默认cdecl,那委托要这么声明:
// 匹配Shellcode的调用约定,参数类型要和Shellcode严格对应 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void MyShellcodeDelegate(int param1, IntPtr param2);
如果你的Shellcode用的是stdcall(比如Windows API风格),就把CallingConvention.Cdecl换成CallingConvention.StdCall。
2. 内存分配要正确:必须加执行权限
分配内存时要确保有PAGE_EXECUTE_READWRITE权限(写完后可以改成PAGE_EXECUTE_READ更安全),否则DEP会阻止执行。用VirtualAlloc来分配:
[DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect); // 分配可执行内存 IntPtr shellcodeMem = VirtualAlloc( IntPtr.Zero, (uint)shellcode.Length, AllocationType.Commit | AllocationType.Reserve, MemoryProtection.PAGE_EXECUTE_READWRITE ); // 把Shellcode写入内存 Marshal.Copy(shellcode, 0, shellcodeMem, shellcode.Length);
3. Shellcode跳回C#函数:别直接用托管函数地址
托管函数的地址可能被JIT动态编译改变,或者被GC回收,直接跳会导致内存错误。正确做法是把C#函数包装成非托管回调:
// 声明回调的委托,同样要匹配调用约定 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void MyManagedCallback(int result); // 必须保持委托的引用!防止GC回收 private static MyManagedCallback _callbackInstance = MyCallbackFunction; // 你的C#回调函数 private static void MyCallbackFunction(int result) { Console.WriteLine($"Shellcode返回的结果:{result}"); } // 获取回调的稳定非托管地址 IntPtr callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackInstance); // 把这个地址传给Shellcode(作为参数或者写入Shellcode指定位置)
⚠️ 重点:一定要保留_callbackInstance的静态引用,不然GC会把委托回收,导致回调地址失效。
4. 正确调用Shellcode
把内存地址转换成委托后直接调用即可:
MyShellcodeDelegate shellcodeFunc = (MyShellcodeDelegate)Marshal.GetDelegateFromFunctionPointer( shellcodeMem, typeof(MyShellcodeDelegate) ); // 传入参数,这里的参数类型要和委托、Shellcode完全匹配 shellcodeFunc(123, callbackPtr);
额外避坑提示
- 架构必须匹配:如果Shellcode是x86的,C#程序必须编译成x86;x64同理,不能用AnyCPU,否则位数不匹配会直接导致参数传递混乱。
- 参数类型严格对应:C++里的
char*对应C#的IntPtr(或者string,但IntPtr更可控),指针类型别乱转,不然封送会出错。 - 内存清理:用完后记得用
VirtualFree释放内存,避免内存泄漏。
完整示例代码
下面是一个可运行的小例子,Shellcode会接收参数并调用C#回调:
using System; using System.Runtime.InteropServices; class ShellcodeTest { // 导入kernel32的内存操作函数 [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect); [DllImport("kernel32.dll")] private static extern bool VirtualFree(IntPtr lpAddress, uint dwSize, FreeType dwFreeType); // 枚举定义 [Flags] private enum AllocationType { Commit = 0x1000, Reserve = 0x2000 } [Flags] private enum MemoryProtection { PAGE_EXECUTE_READWRITE = 0x40 } [Flags] private enum FreeType { MEM_RELEASE = 0x8000 } // Shellcode委托(cdecl调用约定) [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void ShellcodeFunc(int input, IntPtr callback); // C#回调委托 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void ManagedCallback(int output); // 保留回调引用,防止GC回收 private static ManagedCallback _callback = OnShellcodeCallback; static void Main(string[] args) { // 示例x86 Shellcode:把第一个参数传给回调,然后返回 // 指令:push ecx; call [esp+4]; add esp,8; ret byte[] shellcode = new byte[] { 0x51, 0xFF, 0x54, 0x24, 0x04, 0x83, 0xC4, 0x08, 0xC3 }; // 获取回调的非托管地址 IntPtr callbackPtr = Marshal.GetFunctionPointerForDelegate(_callback); // 分配可执行内存 IntPtr shellcodeMem = VirtualAlloc(IntPtr.Zero, (uint)shellcode.Length, AllocationType.Commit | AllocationType.Reserve, MemoryProtection.PAGE_EXECUTE_READWRITE); Marshal.Copy(shellcode, 0, shellcodeMem, shellcode.Length); // 转换并调用Shellcode ShellcodeFunc func = (ShellcodeFunc)Marshal.GetDelegateFromFunctionPointer(shellcodeMem, typeof(ShellcodeFunc)); func(666, callbackPtr); // 释放内存 VirtualFree(shellcodeMem, 0, FreeType.MEM_RELEASE); } private static void OnShellcodeCallback(int output) { Console.WriteLine($"从Shellcode收到数据:{output}"); } }
内容的提问来源于stack exchange,提问作者blastpolak

