Предотвращение обратного проектирования

При распространении программ, разработанных под платформу .NET, следует учесть, что байт-код содержит всю мета-информацию о вашем коде. Что, кстати, справедливо не только для .NET но и для Java, поскольку именно поэтому и работают механизмы отражения (reflection) в данных платформах. Следовательно, стороннему разработчику ничего не стоит восстановить исходный C# код приложения, воспользовавшись любым из широкодоступных инструментов анализа байт-кода, например, ILspy.

Наиболее распространённые цели анализа кода: изучение деталей реализации как с образовательной, так и с коммерческой целями, получение исходного кода для последующего использования в других проектах, «взлом» систем защиты коммерческих приложений.

Способы усложнения процесса восстановления кода могут быть разными, но наиболее распространёнными можно считать следующие:

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

Шифрование сборок

Управляемую сборку можно сохранить в виде отдельного файла, не имеющего расширения dll или exe. Дополнительно, файл сборки можно зашифровать и изменить способ хранения, скажем, загружать из базы данных и т.п. Динамическую загрузку сборки следует реализовать в обработчике события AssemblyResolve. Ниже приведен пример кода, выполняющий загрузку сборки «HiddenLibrary» из ресурса приложения.

AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(Program.ResolveAssembly);
//...
internal static Assembly ResolveAssembly(object sender, ResolveEventArgs args)
{
   if (args.Name.Contains("HiddenLibrary"))
   {
     string rs = "HiddenLibrary.data";
     Assembly assembly = Assembly.GetExecutingAssembly();
     using (System.IO.Stream stream = assembly.GetManifestResourceStream(rs))
     {
        if (stream == null)
          return null;
        byte[] data = new byte[stream.Length];
        stream.Read(data, 0, data.Length);
        return Assembly.Load(data);
     }
   }
   return null;
}	

Обфускация

Обфускацией, или запутыванием (так же, часто используются термин затенение), называется процесс замены имён классов, методов, свойств, переменных и пространств имён на имена, затрудняющие анализ восстановленного кода человеком. Имена могут заменятся на строки, состоящие из однотипных символов, например «a», «b», «c», символы других алфавитов, например, китайского, или вовсе на псевдографические символы. Переименование может дополнятся шифрованием строковых ресурсов, замусориванием потока исполнения кодом, который не влияет на логику работы но затрудняет анализ потока исполнения и так далее.

Для наглядности ниже приведен пример восстановления исходного текста программы инструментом ILSpy незатенённого кода, и то, как выглядит тот же самый класс NodeCoordinatesForm, восстановленный из затенённого программой Dotfuscator кода. Кстати, в затенённом коде можно увидеть практический пример применения символа @ для использования зарезервированных слов в качестве имён пользовательских типов данных, в данном случае — класса.

Не затенённый кодЗатенённый код

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

Dotfuscator, входящий в поставку Visual Studio, в режиме библиотеки исключает из переименования следующие объекты: 

В режиме обфускации приложения переименовывается всё, за исключением переопределённых методов и свойств классов, унаследованных от классов реализация которых находится во внешних библиотеках и явно исключенных с помощью атрибута System.Reflection.ObfuscationAttribute элементов.

Скрытые проблемы запутанного кода

Подписанные сборки

В процессе запутывания меняется байт-код сборки поэтому все подписанные сборки после обфускации необходимо переподписать.

Механизмы отражения и стандартной локализации .NET

Код, использующий строковые константы (имена методов, классов и т.п.) в механизмах отражения перестанет работать. В частности, перестанут работать стандартные механизмы локализации форм (Windows.Forms). Для сохранения работоспособности стандартного механизма локализации следует все формы и элементы управления форм явно исключить из процесса запутывания с помощью атрибута System.Reflection.ObfuscationAttribute.

Автоматическое тестирование

Из-за смены имён элементов интерфейса возникнут сложности с использованием инструментов автоматического тестирования, например, Test Complete, в тестах, использующих имена элементов интерфейса (форм, тектовых полей, кнопок и т.п.).

Стандартные механизмы сериализации

Использование стандартных механизмов сериализации .NET для хранения данных, например в файле,  приведёт к тому, что данные, сохранённые в одной версии программы, не будет читаться другой её версией, поскольку нет никаких гарантий того, что в процессе запутывания сериализуемые классы будут переименовываться одинаково. Все сериализуемые данные следует явно исключить из процесса запутывания с помощью атрибута System.Reflection.ObfuscationAttribute.

[Serializable]
[System.Reflection.ObfuscationAttribute(Exclude = true, ApplyToMembers = true)]
internal class CurveDataRecord
{
  //реализация класса
}

Примеры

Проверка кода на затемнённость

Автоматическая проверка кода на затенённость.

[Conditional("RELEASE")]
internal static void IsObfuscated()
{
  Type appContext = _assembly.GetType("Characteristic.Application.AppContext");
  if (appContext != null)
    throw new InvalidOperationException(@"Исполняемый код не затемнён.");
}

Пример решения проблемы универсальной загрузки

В тех случаях, когда для универсальной загрузки или обработки данных, поступающих из внешних источников необходимо «привязать» поле, свойство или метод класса к некоему текстовому идентификатору, скажем, к имени колонки, в коде реализующем обмен данными вместо строковых констант следует использовать расширение стандартной метаинформации на основе атрибутов. Наглядным примером может служить реализация ORM моделей инициализации полей объектов данными, полученными из СУБД.

[Column(Storage="_Name", DbType="NVarChar(1000)")]
public string Name
{
   get { /*...*/}
   set { /*...*/}
}

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

Атрибут привязки поля или свойства к колонке табличных данных

Фрагмент атрибута, описывающего привязку к данным.

public class RecordBindingAttribute : Attribute
{
    private readonly ModificationType _modificationType;
    private readonly bool _customLoad;
    private readonly bool _nullable;
    private readonly string _columnName;
    private readonly DbType _dbType;

    public enum ModificationType
    {
      Editable,
      InitByDatabase,
      InitOnly,
      Optional
    }

    public RecordBindingAttribute(DbType dbType, string columnName, ModificationType modificationType, bool nullable)
      : this(dbType, columnName, modificationType)
    {
	  _columnName = columnName;
      _modificationType = modificationType;
      _dbType = dbType;
      _nullable = nullable;
    }


    public string ColumnName
    {
      get { return _columnName; }
    }
    public DbType DBType
    {
      get { return _dbType; }
    }
    public bool Nullable
    {
      get { return _nullable; }
    }
    public bool CustomLoad
    {
      get { return _customLoad; }
    }
    public bool InitOnly
    {
      get { return _modificationType == ModificationType.InitOnly;}
    }
    public bool DatabaseInitialized
    {
      get { return _modificationType == ModificationType.InitByDatabase; }
    }
    public bool IsOptional
    {
      get { return _modificationType == ModificationType.Optional; }
    }
    
}

Загрузка объекта из базы данных

Фрагмент кода, использующий вышеопределённый атрибут для автоматического создания объектов, на основе данных, полученных из запроса к базе данных.

T entityObject = new T();
List<KeyValuePair<MemberInfo, RecordBindingAttribute>> bindings = GetDatabaseBindings(entityObject.GetType());

for (int b = 0; b < bindings.Count; b++)
{
   RecordBindingAttribute recordBinding = bindings[b].Value;
   if (!dt.Columns.Contains(recordBinding.ColumnName))
   {
     if (!recordBinding.IsOptional)
       throw new ApplicationException(string.Format(@"Data table doesn't contain column named {0}", recordBinding.ColumnName));
     continue;
   }
   //...
}

for (int i = 0; i < dt.Rows.Count; i++)
{
  entityObject = new T();
  DataRow record = dt.Rows[i];

  for (int b = 0; b < bindings.Count; b++)
  {
     RecordBindingAttribute recordBinding = bindings[b].Value;

     object fieldValue = null;

     MemberInfo field = bindings[b].Key;

     if (!record.Table.Columns.Contains(recordBinding.ColumnName))
     {
        if (!recordBinding.IsOptional)
          throw new ApplicationException(string.Format(@"Data table doesn't contain column named {0}", recordBinding.ColumnName));
        continue;
     }
     if (!record.IsNull(recordBinding.ColumnName))
       fieldValue = ExtensionMethods.ConvertFromDBType(record[recordBinding.ColumnName], GetType(field));

     if (fieldValue != null)
       SetValue(entityObject, field, fieldValue);
     else
     {
       if (recordBinding.Nullable)
         SetValue(entityObject, field, null);
       else if (!recordBinding.IsOptional)
         throw new ApplicationException(string.Format(@"Can not load value for not-nullable field {0} from column {1}", 
         GetType(field).FullName, recordBinding.ColumnName));
   }
}