Executing Native Code in .NET Without External DLLs
This example demonstrates how to execute native x86 machine code directly from .NET and, conversely, how to call a managed .NET method from that native code — all without loading any external libraries.
How it works
The core idea is to allocate a chunk of memory, mark it as executable, and then point a delegate to it.
Get a Pointer to Managed Code: We use
MethodHandle.GetFunctionPointer()to get the memory address ofMath.Max.Allocate Executable Memory: Standard .NET memory is not executable for security reasons (DEP). We use the Win32 API
VirtualAllocto request a region withExecuteReadWritepermissions.Calculate Relative Offsets: The x86
CALLinstruction (0xE8) uses relative addressing. We calculate the distance between our native buffer and the targetMath.Maxfunction.Write Shellcode: We manually write the x86 opcodes into the allocated memory.
Execute via Delegate:
Marshal.GetDelegateForFunctionPointerwraps our raw memory address into a callable .NET delegate.
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
class Program
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate ulong LongFunction(uint arg1, uint arg2);
static void Main(string[] args)
{
// 1. Get the address of the managed method Math.Max(uint, uint)
MethodInfo methodInfo = typeof(Math).GetMethod("Max",
BindingFlags.Static | BindingFlags.Public, null,
new Type[] { typeof(uint), typeof(uint) }, null);
IntPtr functionPtr = methodInfo.MethodHandle.GetFunctionPointer();
Console.WriteLine($"Math.Max address: 0x{functionPtr.ToInt64():X}");
// 2. Allocate executable memory (x86 architecture)
IntPtr region = VirtualAlloc(IntPtr.Zero, (UIntPtr)4096,
AllocationType.Commit, MemoryProtection.ExecuteReadWrite);
// 3. Calculate relative address for the 'call' instruction
// The offset is: TargetAddress - (CurrentInstructionAddress + InstructionLength)
// 14 is the offset from the start of our code to the instruction following 'call'
uint relativeAddr = (uint)functionPtr - (uint)region - 14;
byte[] addrBytes = BitConverter.GetBytes(relativeAddr);
// 4. Prepare x86 machine code: f(x, y) = max(x, y) * max(x, y)
byte[] code = new byte[]
{
0x55, // push ebp
0x89, 0xE5, // mov ebp, esp
0x8B, 0x4D, 0x08, // mov ecx, [ebp + 8] (arg1)
0x8B, 0x55, 0x0C, // mov edx, [ebp + 12] (arg2)
0xE8, addrBytes[0], addrBytes[1], addrBytes[2], addrBytes[3], // call Math.Max
0xF7, 0xE0, // mul eax (square the result of Max)
0x5D, // pop ebp
0xC3 // ret
};
// 5. Copy code to allocated memory and create a delegate
Marshal.Copy(code, 0, region, code.Length);
Console.WriteLine($"Native code memory: 0x{region.ToInt64():X}");
LongFunction func = (LongFunction)Marshal.GetDelegateForFunctionPointer(region, typeof(LongFunction));
// 6. Execute the magic
ulong result = func(0x10, 0x20); // max(16, 32)^2 = 32^2 = 1024
Console.WriteLine($"Result: {result}");
// Cleanup
VirtualFree(region, UIntPtr.Zero, ReleaseType.Release);
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr VirtualAlloc(IntPtr address, UIntPtr length, AllocationType allocationType, MemoryProtection memoryProtection);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool VirtualFree(IntPtr address, UIntPtr length, ReleaseType releaseType);
[Flags]
public enum AllocationType : uint
{
Commit = 0x1000
}
[Flags]
public enum ReleaseType : uint
{
Release = 0x8000
}
[Flags]
public enum MemoryProtection : uint
{
ExecuteReadWrite = 0x40
}
}
Important Considerations
Architecture: This specific shellcode is x86 (32-bit). It will crash on x64 due to different calling conventions and register sizes (e.g.,
RAXinstead ofEAX).JIT Inlining: In a real-world scenario, the JIT compiler might inline
Math.Max, or its function pointer might change. For a stable "managed-to-native" bridge, you usually useRuntimeHelpers.PrepareMethod.Security: Writing to executable memory is a common technique in exploits. Modern systems with Arbitrary Code Execution (ACE) protection might block such operations.




