samedi 7 janvier 2023

Dynamically replace IL code of method body at runtime

Please note that I have no advanced knowledge of code injection nor IL code treatment. In fact I have less than basic knowledge for this and I needed to research much time to write the code at the end of this post.


I'm under .NET 4.8, I've found this answer which I think it suggests that I could modify at runtime the IL code (of the method body) of a compiled method. That is what I would like to do.

In the body of a compiled method I would like to inject a Call OpCode, in this example at the very begining (as first instruction of the method body) to call another method, but I would like to be have flexibility to choose the position where to inject these new IL instructions.

The problem is that when I try to put all this in practice, and due my lack of experience in this matter, I end up getting an AccessViolationException with this error message:

Attempted to read or write protected memory. This is often an indication that other memory is corrupt.

I think probably I'm breaking the IL Code and that is corrupting the memory and maybe it's just a matter of fixing the order of the OpCodes, or maybe it could be a memory issue in caso of that I'm not properly writing into the unmanaged memory when patching the original IL Code.

How can I fix this problem?.

Here is the code that I'm using. In this example I'm tying to modify the method with name "TestMethod" to insert an instruction in its body to call the other method with name "LoggerMethod":

Imports System.Reflection
Imports System.Reflection.Emit

Public NotInheritable Class Form1 : Inherits Form

    Public Sub TestMethod()
        Console.WriteLine("Test Method Call.")
    End Sub

    Public Sub LoggerMethod(<CallerMemberName> Optional memberName As String = "")
        Console.WriteLine($"Logger method call by '{memberName}'.")
    End Sub

    Private Sub Form1_Shown(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Shown

        Dim testMethodInfo As MethodInfo =
            GetType(Form1).GetMethod("TestMethod", BindingFlags.Instance Or BindingFlags.Public)

        Dim loggerMethodInfo As MethodInfo =
            GetType(Form1).GetMethod("LoggerMethod", BindingFlags.Instance Or BindingFlags.Public)

        ' Using a DynamicMethod is just the way I found 
        ' for using ILGenerator to generate the wanted IL code to inject. 
        Dim dynMethod As New DynamicMethod("qwerty_dyn", Nothing, Type.EmptyTypes, restrictedSkipVisibility:=True)
        Dim ilGen As ILGenerator = dynMethod.GetILGenerator(streamSize:=16)
        ilGen.Emit(OpCodes.Call, loggerMethodInfo)
        ' ilGen.EmitCall(OpCodes.Call, loggerMethodInfo, Nothing)

        ILHelper.InjectILCode(testMethodInfo, GetIlAsByteArray2(dynMethod), position:=0)
        Me.TestMethod()

        ' testMethodInfo.Invoke(Me, BindingFlags.Default, Type.DefaultBinder, Type.EmptyTypes, Thread.CurrentThread.CurrentCulture)

    End Sub

End Class
Public NotInheritable Class ILHelper

    Private Sub New()
    End Sub

    Public Shared Sub InjectILCode(method As MethodInfo, newIlCode As Byte(), position As Integer)

        Dim body As MethodBody = method.GetMethodBody()
        Dim ilOpCodes As Byte() = body.GetILAsByteArray()

        If position < 0 Then
            Throw New ArgumentException($"Position must be equals or greater than zero.")
        End If

        If position > ilOpCodes.Length Then
            Throw New IndexOutOfRangeException($"Position {position} is greater than the IL byte array length ({ilOpCodes.Length}).")
        End If

        Dim newIlOpCodes((newIlCode.Length + ilOpCodes.Length) - 1) As Byte

        ' Add new IL instructions first.
        For i As Integer = 0 To newIlCode.Length - 1
            newIlOpCodes(i) = newIlCode(i)
        Next

        ' Continue with adding the original IL instructions.
        For i As Integer = 0 To ilOpCodes.Length - 1
            newIlOpCodes(position + newIlCode.Length + i) = ilOpCodes(i)
        Next

        ' This helps to visualize the byte array differences:
        Console.WriteLine($"Old IL Bytes: {String.Join(", ", ilOpCodes)}")
        Console.WriteLine($"NeW Il Bytes: {String.Join(", ", newIlOpCodes)}")

        Dim methodHandle As RuntimeMethodHandle = method.MethodHandle

        ' Makes sure the target method gets compiled. 
        ' https://reverseengineering.stackexchange.com/a/21014
        RuntimeHelpers.PrepareMethod(methodHandle)

        Dim hGlobal As IntPtr = Marshal.AllocHGlobal(newIlOpCodes.Length)
        Marshal.Copy(newIlOpCodes, 0, hGlobal, newIlOpCodes.Length)

        Dim methodPointer As IntPtr = methodHandle.GetFunctionPointer()
        Marshal.WriteIntPtr(methodPointer, hGlobal)

        Marshal.FreeHGlobal(hGlobal)

    End Sub

End Class
Public Module DynamicMethodExtensions

    <Extension>
    Public Function GetIlAsByteArray2(dynMethod As DynamicMethod) As Byte()

        Dim ilGen As ILGenerator = dynMethod.GetILGenerator()
        Dim fiIlStream As FieldInfo

        ' https://stackoverflow.com/a/4147132/1248295
        ' Conditional for .NET 4.x because DynamicILGenerator class derived from ILGenerator.
        If Environment.Version.Major >= 4 Then
            fiIlStream = ilGen.GetType().BaseType.GetField("m_ILStream", BindingFlags.Instance Or BindingFlags.NonPublic)
        Else ' This worked on .NET 3.5
            fiIlStream = ilGen.GetType().GetField("m_ILStream", BindingFlags.Instance Or BindingFlags.NonPublic)
        End If

        Return TryCast(fiIlStream.GetValue(ilGen), Byte())

    End Function

    ' THIS IS NOT WORKING FOR ME, ArgumentException IS THROWN.
    ' --------------------------------------------------------
    '<Extension>
    'Public Function GetIlAsByteArray(dynMethod As DynamicMethod) As Byte()
    '
    '    Dim resolver As Object = GetType(DynamicMethod).GetField("m_resolver", BindingFlags.Instance Or BindingFlags.NonPublic).GetValue(dynMethod)
    '    If resolver Is Nothing Then
    '        Throw New ArgumentException("The dynamic method's IL has not been finalized.")
    '    End If
    '    Return DirectCast(resolver.GetType().GetField("m_code", BindingFlags.Instance Or BindingFlags.NonPublic).GetValue(resolver), Byte())
    '
    'End Function

End Module




Aucun commentaire:

Enregistrer un commentaire