Инъекции CLR: замена методов во время выполнения - Инъекция метода

ОГЛАВЛЕНИЕ

Инъекция метода

Мы хотим заменить код более простым - для этого у нас два метода, источник, назначение и необходимо заменить назначение источником. Далее следует подпись для нашего метода замены:
public static void ReplaceMethod(MethodBase source, MethodBase dest)

Инъекция IL

Изначально мы хотели заменить IL, но мы столкнулись с некоторыми проблемами. Кажется, CLR ведет себя по-разному в зависимости от метода сборки и наличия прикрепленного отладчика. Так же кэшируются вызовы JIT (вспомогательный код заменяется). Нам необходимо заставить CLR каким-то образом аннулировать кэш JIT. Это получилось выполнить, но только в режиме отладчика, и приходится сохранять какое-то состояние каждого метода для того, чтобы CLR мог повторно скомпилировать его при помощи JIT.

Также мы пробовали использовать принудительный метод, но и здесь мы встретили некоторые препятствия. Идея заключалась в программной замене методов из управляемого кода. Казалось, можно зацепить JIT, использовать RuntimeHelpers.PrepareMethod ,чтобы заставить метод быть скомпилированным JIT, а затем изменить структуру CORINFO_METHOD_INFO, которая передается в наш подключенный метод. Передача состояния между управляемым и автономными подключенными методами оказалась проблематичной. Если мы вызываем любой управляемый метод из подключенного, то у нас переполнение стека. В то же время деактивация JIT-кэша слишком сложна и, опять-таки, это возможно только в режиме отладки.

Другим подходом может быть использование автономных программных интерфейсов метаданных (API). Мы могли бы считать RVA- таблицы метаданных методов, использовать RVA и адрес основания модуля для нахождения IL-адреса в памяти, а затем переписать поверх него. Это тоже проблематично, потому что длина источника IL в битах должна быть меньше, чем пункт назначения. Тем более что есть нечто большее, чем просто IL - у нас также есть tiny или fat заголовок IL и, вероятно, структуры SEH и т.д. После того, как метод будет вызван, как только JITStub будет заменен, мы столкнемся с той же проблемой, что и в других методах.

После этого тупика с различными подходами мы решили попробовать отличный метод. Вместо замены IL мы заменим компонующий автокод, который выдает JIT. С таким подходом мы можем заботиться о деактивации кэширования, заголовках IL, SEH и т.д.

Инъекции после отладки JIT (Post JIT Injection)

Наш новый подход обеспечивает компиляцию как исходных, так и методов назначения, находит в памяти таблицу методов и заменяет адрес JITStub назначения адресом источника. Мы можем заменить метод неограниченное число раз и нам не нужно думать о кэшировании метода, но нам необходимо отыскать несколько вещей в памяти.

Мы будем использовать RuntimeTypeHandle и RuntimeMethodHandle для нахождения таблицы методов и расположения метода в памяти. RuntimeMethodHandle указывает на 8-битную структуру в памяти, называемую MethodDescription. Это тот же адрес, что и в колонке MethodDesc, при использовании команды SOS !DumpMT -MD. Структура содержит индекс метода в таблице методов. Затем мы можем использовать RuntimeTypeHandle для нахождения таблицы методов. Таблица методов начинается с 40 бит после адреса RuntimeTypeHandle.

Динамические методы работают по-другому. Мы не смогли найти документацию, но мы нашли адрес JITStub используя отладчик памяти. Динамический метод не раскрывает свой RuntimeMethodHandle , поэтому мы должны использовать Reflection, чтобы получить его. Мы нашли адрес JITStub через 24 бита после адреса дескриптора динамического метода.

public static IntPtr GetMethodAddress(MethodBase method)
{
    if ((method is DynamicMethod))
    {
        unsafe
        {
            byte* ptr = (byte*)GetDynamicMethodRuntimeHandle(method).ToPointer();
            if (IntPtr.Size == 8)
            {
                ulong* address = (ulong*)ptr;
                address += 6;
                return new IntPtr(address);
            }
            else
            {
                uint* address = (uint*)ptr;
                address += 6;
                return new IntPtr(address);
            }
        }
    }

    RuntimeHelpers.PrepareMethod(method.MethodHandle);

    unsafe
    {
        // некоторые dwords
        int skip = 10;

        // считывание  индекса метода.
        UInt64* location = (UInt64*)(method.MethodHandle.Value.ToPointer());
        int index = (int)(((*location) >> 32) & 0xFF);

        if (IntPtr.Size == 8)
        {
            // получение таблицы методов
            ulong* classStart = (ulong*)method.DeclaringType.TypeHandle.Value.ToPointer();
            ulong* address = classStart + index + skip;
            return new IntPtr(address);
        }
        else
        {
            // получение таблицы методов
            uint* classStart = (uint*)method.DeclaringType.TypeHandle.Value.ToPointer();
            uint* address = classStart + index + skip;
            return new IntPtr(address);
        }
    }
}

private static IntPtr GetDynamicMethodRuntimeHandle(MethodBase method)
{
    if (method is DynamicMethod)
    {
        FieldInfo fieldInfo = typeof(DynamicMethod).GetField("m_method",
                              BindingFlags.NonPublic|BindingFlags.Instance);
        return ((RuntimeMethodHandle)fieldInfo.GetValue(method)).Value;
    }
    return method.MethodHandle.Value;
}

После того, как мы получим расположение адресов JITStub, мы просто должны изменить значение. Наш метод замены продемонстрирован ниже:

public static void ReplaceMethod(IntPtr srcAdr, MethodBase dest)
{
    IntPtr destAdr = GetMethodAddress(dest);
    unsafe
    {
        if (IntPtr.Size == 8)
        {
            ulong* d = (ulong*)destAdr.ToPointer();
            *d = *((ulong*)srcAdr.ToPointer());
        }
        else
        {
            uint* d = (uint*)destAdr.ToPointer();
            *d = *((uint*)srcAdr.ToPointer());
        }
    }
}
public static void ReplaceMethod(MethodBase source, MethodBase dest)
{
    if (!MethodSignaturesEqual(source, dest))
    {
        throw new ArgumentException("Сигнатуры методов не совпадают.",
                                    "source");
    }
    ReplaceMethod(GetMethodAddress(source), dest);
}