Exepack.NET, часть 1

Концепция загрузчикаДавно мечтал написать exe-packer — упаковщик для исполняемых файлов. Дурацкая мечта, прямо скажем, но у меня в запасе еще много таких.

Как недавно мне удалось выяснить, написать такой упаковщик для .NET-программ можно за три часа. Наверное, можно и быстрее, но у меня получилось за три с копейками. Это такая приятная особенность .NET: почти все уже сделано до нас.

А сделано до нас вот что:

  • управляемый загрузчик Assembly.Load() или Assembly.LoadFrom()
  • загрузчик ресурсов Assembly.GetManifestResourceStream()
  • класс DeflateStream для упаковки и распаковки данных.

В простейшем случае .NET-программа — это один EXE-файл, который ссылается на системные сборки типа mscorlib, System.dll и так далее. Когда я обработаю этот файл упаковщиком, у меня должен получиться другой EXE-файл, который делает то же самое, но места занимает меньше. Естественно, это будет тоже .NET-программа, так называемый загрузчик.

Для начала я прикинул, как будет выглядеть этот самый загрузчик. Он будет загружать откуда-нибудь исходную сборку и запускать ее в своей же виртуальной машине CLR, в дефолтовом домене приложений. Сказано — сделано:

using System;
using System.Reflection;
 
class Loader
{
  static void Main(string[] args)
  {
    Assembly asm = Assembly.LoadFrom("app.exe");
 
    if (asm.EntryPoint.GetParameters().Length == 0)
      asm.EntryPoint.Invoke(null, new object[0]);
    else
      asm.EntryPoint.Invoke(null, new object[] { args });
  }
}

В данном случае сборка грузится из файла app.exe. Единственная тонкость, на которую тут надо обратить внимание: метод Main может быть с параметрами, а может — без. Поэтому перед запуском приходится проверять, с каким вариантом мы имеем дело. Более того, этот метод может быть void, а может возвращать значение типа int, это тоже надо бы учитывать (у меня тут, как видите, не учитывается).

Идем дальше. Загружать сборку из файла, понятно, некрасиво. У нас в результате должен быть один-единственный EXE-шник, в котором упаковано все. Поэтому сборку нужно грузить из наших же ресурсов. Когда я буду компилировать новую версию загрузчика, я попрошу компилятор C# присобачить файлик app.exe к моей программе в качестве ресурса. Есть такой удобный ключик у консольного компилятора — /res:app.exe. Ресурс в результате так и будет называться — app.exe.

Осталось разобраться, как загрузить сборку из ресурса. Выясняется, что это тоже довольно просто: вытаскиваем нужный Stream из ресурса с именем hello.exe, считываем в буфер (массив byte[]) и загружаем из него сборку с помощью Assembly.Load():

// csc loader2.cs /res:app.exe
 
using System;
using System.IO;
using System.Reflection;
 
class Loader
{
  static void Main(string[] args)
  {
    using (Stream stream = Assembly.GetExecutingAssembly().
      GetManifestResourceStream("app.exe"))
    {
      Assembly asm = Assembly.Load(
        new BinaryReader(stream).ReadBytes((int)stream.Length));
 
      if (asm.EntryPoint.GetParameters().Length == 0)
        asm.EntryPoint.Invoke(null, new object[0]);
      else
        asm.EntryPoint.Invoke(null, new object[] { args });
    }
  }
}

Проверяем — работает! Как нетрудно догадаться, если перед компиляцией загрузчика файл app.exe упаковать, а после загрузки, соответственно, распаковать — это и будет практически готовым упаковщиком.

В реальной жизни .NET-программы редко представляют собой единственный EXE-файл. Обычно это целая пачка сборок — файлов EXE и DLL. Что делать с такими программами, я расскажу в следующий раз.

  1. Max:

    Добрый день!
    Полезная информация, спасибо.
    Мне, лично было очень интересно, пригодится.

    Спасибо.

  1. There are no trackbacks for this post yet.

Leave a Reply