This is the third part of the .NET Inside Out series. For your convenience you can find other parts in the table of contents in Part 1 – Virtual and non-virtual calls in C#

Warning — clickbait title!
We are not going to override sealed function, however, we are going to substitute one method with another using some machine code. Let’s go.

Introduction

Sometimes we would like to stub a function in order to use different one in tests. There are dozens of mocking libraries in C# which easily can do that, however, they usually require the method to be virtual. There are also libraries like Fakes able to modify event sealed types. We are going to do something similar.

Every method is a piece of machine code. If we simply modify this code to jump somewhere else, it will do that. If we jump to some other method with matching signature, we can easily “override sealed function”. Enough of theory, let’s see the code.

Implementation

public static void HijackMethod(MethodInfo source, MethodInfo target)
{
	RuntimeHelpers.PrepareMethod(source.MethodHandle);
	RuntimeHelpers.PrepareMethod(target.MethodHandle);

	var sourceAddress = source.MethodHandle.GetFunctionPointer();
	var targetAddress = (long)target.MethodHandle.GetFunctionPointer();

	int offset = (int)(targetAddress - (long)sourceAddress - 4 - 1); // four bytes for relative address and one byte for opcode

	byte[] instruction = {
		0xE9, // Long jump relative instruction
		(byte)(offset & 0xFF),
		(byte)((offset >> 8) & 0xFF),
		(byte)((offset >> 16) & 0xFF),
		(byte)((offset >> 24) & 0xFF)
	};

	Marshal.Copy(instruction, 0, sourceAddress, instruction.Length);
}

This method takes two method descriptors: source method to override and target method to use as a jump target. We first prepare the methods (so they are compiled to machine code), next we extract their addresses and calculate the jump. Finally, we generate simple machine code which does the jump. First byte (0xE9) is an opcode of jump instruction, next four bytes represent relative destination address. Finally, we copy the code in place.

We can use the following test program:

using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace MethodHijacker
{
    class TestClass
    {
        public static string ReturnString()
        {
            return "Original string";
        }

        public static string ReturnStringHijacked()
        {
            return "Modified string";
        }

        public static string StringProperty { get; set; }

        public string NonStaticReturnStringHijacked()
        {
            return "Nonstatic modified string";
        }

        public string NonStaticStringProperty { get; set; }
    }

    class Program
    {
        static void Main()
        {
            // String method
            Console.WriteLine(TestClass.ReturnString());

            MethodHijacker.HijackMethod(typeof (TestClass).GetMethod(nameof(TestClass.ReturnString)),
                typeof (TestClass).GetMethod(nameof(TestClass.ReturnStringHijacked)));

            Console.WriteLine(TestClass.ReturnString());

            // String property
            TestClass.StringProperty = "Test";

            Console.WriteLine(TestClass.StringProperty);

            MethodHijacker.HijackMethod(typeof(TestClass).GetProperty(nameof(TestClass.StringProperty)).GetMethod,
                typeof(TestClass).GetMethod(nameof(TestClass.ReturnStringHijacked)));

            Console.WriteLine(TestClass.StringProperty);

            // Nonstatic property
            var instance = new TestClass();

            instance.NonStaticStringProperty = "Test nonstatic";

            Console.WriteLine(instance.NonStaticStringProperty);
            
            MethodHijacker.HijackMethod(typeof(TestClass).GetProperty(nameof(TestClass.NonStaticStringProperty)).GetMethod,
                typeof(TestClass).GetMethod(nameof(TestClass.NonStaticReturnStringHijacked)));

            Console.WriteLine(instance.NonStaticStringProperty);
        }
    }
}

The code was tested on x86 and x64 Windows 10 with .NET 4.5.