Как стать автором
Обновить

Организация кода с интроспекцией в контексте обфускации и рефакторинга

Время на прочтение 3 мин
Количество просмотров 819
Платформа .NET предоставляет богатый API для доступа к метаданным во время выполнения программы. Но механизм интроспекции предполагает позднее связывание с программными элементами посредством задания их имён и сигнатуры через соответствующие структуры данных. Подобный код может привести к изменению логики работы программы на неверную после проведения рефакторинга(переименование, изменение порядка параметров) или обфускации метаданных. Решение этой проблемы заключается в применении синтаксического сахара, доступного в рамках технологии Expression Trees и языка C#.

Чтобы не ходить далеко, для примера напишем простой класс.

public class SimpleClass
{
  private readonly string theValue;

        public SimpleClass(string value, int ratio)
        {
            theValue = value;
        }

        public string Value
        {
            get { return theValue; }
        }
}

Добавим в этот класс свойства, предоставляющие доступ к его метаданным через механизм интроспекции:

 internal static PropertyInfo ValueProperty
        {
            get { return typeof(SimpleClass).GetProperty("Value"); }
        }

        internal static FieldInfo ValueField
        {
            get { return typeof(SimpleClass).GetField("theValue", BindingFlags.NonPublic | BindingFlags.Instance); }
        }

        internal static ConstructorInfo Constructor
        {
            get { return typeof(SimpleClass).GetConstructor(new[] { typeof(string), typeof( }); }
        }

Если выполнить вывод значения этих свойств в консоль, то получим строковые представления этих программных элементов. Однако, если выполнить перестановку параметров конструктора при помощи встроенного в IDE движка рефакторинга(например, в Visual Studio это меню Refactoring, пункт Reorder parameters), то свойство, отражающее метаданные конструктора, будет возращать пустую ссылку(т.е. null). А если ещё и применить обфускацию к сборке, то отразить подобным образом поле и свойство станет вообще невозможным(за исключением случая применения атрибута ObfuscationAttribute).
Для того, чтобы выйти из ситуации, вооружимся деревьями выражений, лямбда-выражениями и обобщениями. Из всего это склеим вспомогательный метод:

static TMember Reflect<TDelegate, TMember>(Expression<TDelegate> memberAccess)
            where TDelegate : class
            where TMember : MemberInfo
        {
            if (memberAccess.Body is MemberExpression)
               return ((MemberExpression)memberAccess.Body).Member as TMember;
            else if (memberAccess.Body is NewExpression)
                return ((NewExpression)memberAccess.Body).Constructor as TMember;
            else if (memberAccess.Body is MethodCallExpression)
                return ((MethodCallExpression)memberAccess.Body).Method as TMember;
            else return null;
        }

Этот метод принимает два параметра-типа: первый принимает делегат, используемый для разрешения сигнатуры лямбда-выражения, который будет передаваться в качестве аргумента, второй — тип отражаемого члена класса(например, FieldInfo для поля). Передавамое лямбда-выражения в качестве аргумента описывает доступ к интересующему нас члену класса. Внутри метода происходит обращение к телу лямбда-выражения, представленного в виде дерева выражения. Поскольку тело представляет собой доступ к члену класса, то анализируем тип тела на возможные варианты:
  • Доступ к свойству или полю;
  • Вызов оператора new(данное выражение содержит ссылку на конструктор);
  • Вызов метода.

Основная задача данного решения — уход от классических методов отражения с использованием флагов привязки, имён программных элементов и массивов, описывающих сигнатуры методов и конструкторов, поскольку эти методы не доступны для анализа движком рефакторинга и обфускаторами.
Теперь всё готово для написания кода отражения, изменяющегося автоматически в процессе рефакторинга и безопасного к алгоритмам обфускации.

internal static PropertyInfo ValueProperty
        {
            get { return Reflect<Func<SimpleClass, string>, PropertyInfo>(v => v.Value); }
        }

        internal static FieldInfo ValueField
        {
            get { return Reflect<Func<SimpleClass, string>, FieldInfo>(v => v.theValue); }
        }

        internal static ConstructorInfo Constructor
        {
            get { return Reflect<Func<string, int, SimpleClass>, ConstructorInfo>((a1, a2) => new SimpleClass(a1, a2)); }
        }

При таком подходе не используются строковые литералы для представления имён членов класса и массивы типов для описания сигнатуры конструктора.
Почему данный код остаётся рабочим после того, как по нему отработает обфускатор? Ответ кроется в том, как компилятор C# генерирует код для деревьев выражений. Если открыть сборку в ILDASM, то можно убедиться, что для загрузки метаданных члена класса используется инструкция LDTOKEN, которая оперирует числовым токеном, обозначающем расположение метаданных члена в соответствующей таблице внутри PE-файла.

Всегда есть НО


Данный способ подходит только для отражения программных элементов, доступных в текущей лексической области видимости.
Теги:
Хабы:
+3
Комментарии 0
Комментарии Комментировать

Публикации

Истории

Ближайшие события

PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн
Weekend Offer в AliExpress
Дата 20 – 21 апреля
Время 10:00 – 20:00
Место
Онлайн