Реверсинг .Net Framework приложений и компонентов (часть 2)

😁
Олдфаг
Статус
Оффлайн
Регистрация
27 Ноя 2016
Сообщения
2,091
Реакции[?]
2,025
Поинты[?]
6K
Взлом компонентов
Статья повествует о принципиальных особенностях взлома .net компонентов. Для начала я раскажу что же такое компонент. Компонент это программный продукт для разработчика. Идея комерческих компонентов заключается в том, что бы экономить время. К примеру есть команда разработчиков, которые пишут корпоративный заказ, т.е. некая компания заказала у них разработку этой программы. По плану, этой команде предстоит также разработка некоего компопонента, но у програмистов и так дел по уши, как обычно они не успевают в срок :). Поэтому шеф чешет репу и думает, а стоит ли нанимать еще одного человека, для написания необходимого компонента и платить ему 1000$ в месяц, если можно купить за эти же деньги готовый компонент. Чаще всего выбирают последнее, все это не раз наблюдалось мною лично. Если бы не это комерческое обстоятельство, то врятли бы мне пришлось писать эту статью ;). Самые распространенные из компонентов, это компоненты представляющие собой элементы графического интерфейса: button, progress bar, editbox, listbox, combobox, grid и так далее. Такие компоненты называют еще контролами. Grid контрол, пожалуй, один из самых сложных контролов в реализации. Почти всегда этот контрол является центровым среди других котролов, и в месте с ним, делая хитрые маркетенговые телодвижения, пытаются продать другие, которые чаще всего просто не нужны. Мы сегодня будем рассматривать реверсинг компонетов, именно на примере Grid контрола. Как ты уже догадался Grid контрол это таблица. Эти контролы весьма разнообразны по функционалу, начиная от Grid'ов, умеющих работать напрямую с источниками данных DataSource, заканчивая Grid'ами, позволяющими создавать Nested Tables, т.е. вложенные таблицы. Но я выделяю главные характеристики Grid контрола: Во первых это скорость рендера, во вторых экономия памяти, скорость добавления новых элементов, надежность и удобство использования. Всего этого можно достичь реализовывая так называемую технологию Virtual Render Control. При этом происходит прорисовка не всего контрола, а только той части которая помещается в View Region(область которую может видеть пользователь). Конечно то что видит пользователь и то что рисует грид это два разных понятия. Вот к примеру у тебя в гриде есть 1000 елементов, но в данный момент ты можешь видеть только 20, но грид все равно прорисовывает все 1000, из них 980 он рисует в невидимой части. Но он же их рисует! На это тратит время. Поэтому грид должен заранее знать сколько может видеть пользователь и рисовать только то, что видит пользователь (в нашем случае рисоваться должно только 20 элементов), данная технология позволяет прорисовывать грид всегда с одинаковой скоростью + количество элементов никогда не будет влиять на скорость скролла. Особое внимание здесь можно уделить скролу. Все native Grid контролы, которые мне доводилось видеть и релизуют Virtual Render Control, делает скролинг построчно, т.е. при прокрутке текст двигается дискретно, как бы перемещаясь из одной невидимой строки в другую. Это более легкий и халявный способ реализации технологии Virtual Render Control, в отличие от попиксельного скролинга. Более того попиксельный скролинг, говорит о высоком уровне програмистов, которые писали этот контрол т.к. реализация попиксильного скролинга требует высокой производительности, как самого контрола в целом, так и модели доступа к элементу контрола. В свое время, когда я написал собственный IL дизасемблер, мне потребовался такой .Net контрол, который мог бы выделять пространства имен, классы, методы, в отдельные структуры Nested Tables. Я стал искать подходящий мне контрол, многие были красимы и удобными, но к моему удивлению\разочерованию, ни один из них не реализовывал технологию Virtual Render Control. Как результат это большие потери памяти, медленная прорисовка и невозможный скролинг. К примеру при дизасемблировании стандартной библиотеки mscorlib.dll и отображении в XceedGrid, было растраченно 1450 мегабайт памяти, элементы добавлялись 40 минут, а рендеринг происходил за 45 секунд. Ну где это виданно? Когда я написал в службу технической поддержки, мне посоветовали сделать так что бы все вложенные элементы были в свернутом виде, но тогда получается, что я должен постоянно щелкать на элементах которые мне нужны, а это обламывает, куда удобнее крутить третью кнопку мыши :). Впрочем остальные Grid'ы, не отличались сильно производительностью. Мода сейчас пошла на поддержку дизайнера форм, но зачем делать эту поддерку для элемента? Вот я этого ни когда не пойму, это же лишняя затраченная память помноженная на количество элементов, я даже стараюсь по возможности поля класса(константы) вынести из элемента куда-нибудь в статику, чтоб меньше памяти занимал и быстрее работал. Я ,конечно, понимаю красота (имеется ввиду красота и удобство разработки) требует жертв, но все-таки всегда был на стороне быстродействия и производительности. В итоге из-за таких нелепых и искушонных, легкостью использования тормазнутых технологий, програмистов .Net, написавших кучу тормазнутого софта, появилось стойкое убеждение, что .Net это великий тормоз. Как бы не так. Для решения этой проблемы мне пришлось написать собственный контрол. И при отображении листинга библиотеки mscorlib.dll, затрачивается всего 4.5 мегабайта памяти, рендер происходит за ~0.081 секунды, добавление всех елементов ~0.8 секунды и попиксильный скролинг. В моем Grid'e нет навароченной поддержки дизайнера форм и всяких примочек, зато он простой, быстрый и красивый.

Переходим к матчасти
Твоему вниманию предостовляется компонент C1TrueDBGrid, взятый с сайта
Пожалуйста, авторизуйтесь для просмотра ссылки.
. Это Grid контрол одной из ведущих компаний по производству компонентов. Работа с этим компонентом будет производится следующим образом. Берем любой пример поставляемый с компонентом, компилируем его, в папке проекта появляется папка \debug\bin переходим в эту паку, дизасембилим его ildasm c1.win.c1truedbgrid.DLL /out=c1.win.c1truedbgrid.h. Для ассемблирования нужно в студии удалить из References этот контрол и только потом ilasm c1.win.c1truedbgrid.h /dll /debug и так при каждом ассемблировании, если этого не сделать, то при ассемблировании будет ошибка: Failed to write output file, error code=0x80070020, т.е. файл занят студией и она не дает его перезаписывать. Сделав все как надо у тебя появляется возможность трассировать компонент в том же окне студии, где и сам пример его использования. Но запустив первый раз мы сразу получаем ошибку: Сбой при проверке строгого имени для сборки 'C1.Win.C1TrueDBGrid'. Это цифровая подпись, именуемыя как Strong Name. В предыдущих статьях я уже много писал о ней так, что я повторятся не буду. Для ее удаления нужно закоментировать атрибуты .custom instance void [mscorlib]System.Reflection.AssemblyKeyFileAttribute::.ctor(string) и .publickey. Прошу не удалять код а именно помещать в коментарии, т.к. в этой статье я часто говорю номера строк, а если строки будут удалены то соответственно у тебя нумерация строк будет другая. Наше исследование начинается с того, что в конструкторе C1TrueDBGrid мы онаруживаем такую строку: this.0WB = LicenseManager.Validate(typeof(C1.Win.C1TrueDBGrid.C1TrueDBGrid), this); здесь контрол использует стандандартные возможности проверки лицензии с помощью класса System.ComponentModel.LicenseManager. Как видно из вызова, результат присваевается приватному полю класса private License 0WB; чтобы упростить нашу задачу по выявлению всех учасков кода которые обращаются к этому полю, мы просто удалим его. К сожалению при компеляции компилятор не выдает нам ошибки, в тех местах где это поле используется, но эти ошибки мы выявим когда запустим приложение. Открою вам несколько секретов быстрой навигации по IL коду в VS2003, нужно зайти в меню Edit-> Find & Replace -> Find или просто нажать Ctrl + F, поставить галочку в пункте Use и выбрать в комбобоксе опцию Wildcards, это вроде языка подстановочных символов в поисковике файлов виндовса, теперь что бы найти наш класс мы вводим такой запрос [.]class*C1TrueDBGrid, мы оказываемся в начале класса необходимое нам поле находится ниже, по запросу [.]field*0WB мы его находим в строке 74357, после этого мы его закоментируем. Заодно найдем и закоментируем заполнение этого поля в конструкторе класса [.]class*C1TrueDBGrid -> [.]method, здесь есть два обращения к этому полю в первом этому полю присваевается null, коментируем строки IL_00ae*IL_00b0(здесь и далее знак * указывает что нужно коментирывать все строки между этими метками включительно), во втором обращении происходит заполнение поля объектом License, о чем я говорил выше, коментируем IL_00c6*IL_00d7. Компилируем запускаем и О! триальное окно больше не появляется, это был самый быстрый взлом .net компонента, из всех которые я делал, около трех минут. Оказалось что это поле больше нигде не используется, теперь пожалуй стоит немного почисть компонент от мусора. К таковому я отношу две вещи, первое сама триальная форма и второе, когда в дизайнере формы мы добавляем контрол на форму, на нем можно в щелкнуть правой кнопкой, и в контекстном меню ты увидишь элемент меню: "About ComponentOne C1TrueDBGrid". При нажатии на него появляется триальное окно, которое мы собрались удалить. Самый быстрый и легкий способ узнать какой класс создает окно, это сделать бряк в тот момент когда когда оно появилось, т.е. нужно просто перейти в отладчик и нажать Ctrl + Break. И так возвращаем все изменения, сделанные нами в методе C1TrueDBGrid::.ctor, и запускаем когда появилось триальное окно брякаем. Что бы узнать в каком классе мы оказались делаем SerchUP [.]class -> [.]NameSpace и так мы в методе C1.Win.0V::Y3, посмотрим этот метод и класс в рефлекторе на наличие полезного кода там. Забыл сказать, что в последних версиях рефлектора, появилась наконецто возможность узнавать где используется данный класс, метод или поле. Для этого на на элементе дерева классов нужно вызвать контекстное меню, и нажать Analyser. В появивщемся окне есть два елемента Depends On и Used By, это "Зависит от" и "Используется в" соотвественно. Быстро просмотрев 19 классов C1.WIN в анализаторе, мы обнаружили, что только пять используются за пределами пространства имен C1.WIN,
Код:
1 internal class 0L : Attribute

-- Assembly C1.Win.C1TrueDBGrid

2 internal class 0M : Attribute
-- Assembly C1.Win.C1TrueDBGrid

3 internal class 0Q : Attribute
-- Assembly C1.Win.C1TrueDBGrid

4 internal class ProviderInfo : LicenseProvider
-- C1.Win.C1TrueDBGrid.TDBDropDesigner::DisplayAboutBox(object sender, EventArgs e);
-- C1.Win.C1TrueDBGrid.TDBGridDesigner::DisplayAboutBox(object sender, EventArgs e);

5 internal interface U
-- class C1TrueDBGrid : Frame, U, IC1ControlPrintable
поэтому если мы удалим эти 19 классов, компонент неплохо похудеет :), зачем нашей программе лишний вес. Как видно из таблички первые три класса используются ввиде атрибутов сборки. Эти атрибуты можно найти в том же месте где мы закоментировали .publickey. они выглядчт так:
Код:
 .custom instance void C1.Win.'0M'::.ctor(string) = ( 01 00 24 32 31 42 31 31 44 35 37 2D 39 34 37 38   // ..$21B11D57-9478
                                                       2D 34 32 30 65 2D 41 32 42 32 2D 34 43 36 41 41   // -420e-A2B2-4C6AA
                                                       45 46 39 38 45 34 36 00 00 )                      // EF98E46..
  .custom instance void C1.Win.'0Q'::.ctor(string,
                                           string,
                                           string) = ( 01 00 24 73 75 70 70 6F 72 74 2E 74 64 62 67 72   // ..$support.tdbgr
                                                       69 64 2E 6E 65 74 40 63 6F 6D 70 6F 6E 65 6E 74   // id.net@component
                                                       6F 6E 65 2E 63 6F 6D 3A 6E 65 77 73 3A 2F 2F 6E   // one.com:news://n
                                                       65 77 73 2E 63 6F 6D 70 6F 6E 65 6E 74 6F 6E 65   // ews.componentone
                                                       2E 63 6F 6D 2F 43 6F 6D 70 6F 6E 65 6E 74 31 2E   // .com/Component1.
                                                       70 75 62 6C 69 63 2E 6E 65 74 2E 74 64 62 67 72   // public.net.tdbgr
                                                       69 64 00 00 00 )                                  // id...
  .custom instance void C1.Win.'0L'::.ctor(string,
                                           string) = ( 01 00 02 32 34 24 34 34 33 45 43 39 35 30 2D 38   // ...24$443EC950-8
                                                       37 44 33 2D 34 31 34 34 2D 41 42 38 33 2D 39 36   // 7D3-4144-AB83-96
                                                       38 39 45 44 37 44 33 30 43 33 00 00 )             // 89ED7D30C3..
и так коментируем строки 73*88. Далее у нас два обработчика события в классах TDBDropDesigner и TDBGridDesigner, когда удаляют метод обработчика события, в месте с ним необходимо удалить строки кода, которые навешивают на него EventHandler. Правильные програмисты делают это в методе инициализации void Initialize, который создаются автоматически при создании форм и контролов, и вызывается из конструктора класса. Начнем с метода TDBDropDesigner::DisplayAboutBox, находим его в листинге: [.]class*TDBDropDesigner -> DisplayAboutBox*object и коментируем в строках 142891*142901. Теперь удалим инициализацию EventHandler, заходим в метод [.]class*TDBDropDesigner -> Initialize. В рефлекторе(C#) то, что нам нужно закоментировать выглядит так: this.get_Verbs().Add(new DesignerVerb(11.Z1("About ComponentOne C1TrueDBGrid..."), new EventHandler(this.DisplayAboutBox))), а в IL этот код находится по меткам IL_0018*IL_003e, его нужно закоментировать. Сделав это мы убили еще одного зайца, теперь в контекстном меню контрола, когда мы находимся в дизайнере формы, не будет меню "About ComponentOne C1TrueDBGrid" :). Но увидеть эти изменения мы не сможем просто перекомпилировав контрол. Дело в том, что студия имеет свой собственный референс и дизайнер форм смотрит не наш модифицированный контрол, а оригинал. Что бы увидеть изменения нужно зайти в дизайнер форм, удалить с формы контрол, добавить на панель контролов наш контрол, и нарисовать этот грид еще раз, теперь мы можем видеть все изменения. Теперь тоже самое проделаем с классом TDBGridDesigner. Находим метод [.]class*TDBGridDesigner -> Initialize, коментируем инициализацию EventHandler по меткам IL_0018*IL_003e, далее находим метод обрабатывающий данное событие DisplayAboutBox*object и коментируем строки 143177*143187.
И так остался последний пятый пункт. Класс C1TrueDBGrid наследует реализацию от интерфейса C1.Win.U, смотрим в рефлекторе этот интерфейс он содержит всего один метод Assembly GetCallingAssembly(), значит в классе C1TrueDBGrid тоже есть этот метод т.к. он наследует от этого интерфейса. Нам придется его удалить, т.к. теперь его вызывать ни кто не будет, но на всякий случай перед удалением нужно проверить анализером где он используется. Проверив это, мы видим, что метод ни где не используется, но на самом деле он используется по хитрому. Всмомним самое начало статьи, там где мы делали исправление в конструкторе класса C1TrueDBGrid, this.0WB = LicenseManager.Validate(typeof(C1.Win.C1TrueDBGrid.C1TrueDBGrid), this). В этой строке передается, в виде параметра, ссылка this(ссылка на текущий класс), LicenseManager в свою очередь проверяет, имеет ли этот обьект атрибут <LicenseProvider(...)>, в виде параметра должен выступать класс производный от System.ComponentModel.LicenseProvider. Если все нормально LicenseManager создает обьект, с заданным в атрибуте типом, где и происходит проверка ключа. В нашем случае ключь проверяется в классе С1.win.ProviderInfo. Рассказывая тебе об этом у меня появилась мысль, что было бы не плохо удалить этот атрибут тоже. Схема этого встроенного лицензирования сводится к тому, что платформа .Net делает CallBack, создавая обьект указанный в виде параметра атрибута <LicenseProvider(...)>, наверно в МС пологают, что в хорошо обфусцированном коде будет тяжело найти проверку ключа, при условии что вызов проверки будет через этот странный CallBack :). До написания этой статьи я представления не имел, как это все работает, но разобрался с этим менее чем за пару минут. Вернемся к нашему правковому делу. Вопревых нужно удалить интерфейс С1.win.U из списка наследуемых интерфейсов. Находим класс [.]class*C1TrueDBGrid и видим строку implements C1.Win.U, она выглядеть должна так implements /*C1.Win.U,*/ . Немного ниже есть и атрибут: .custom instance void [System]System.ComponentModel.LicenseProviderAttribute его тоже коментируем. Теперь находим метод GetCallingAssembly(), он находится в строках 74511*74521. Наконецто мы избавились от всего кода который использует классы пространства имен C1.Win, теперь мы можем благополучно их удалить. Находим начало C1.Win [.]Namespace*C1.Win и начиная со строки 144314 до строки 156166 закоментируем этот код. Целых одинадцать тысяч IL кода. Но это еще не все. все классы и пространства имен существуют в листинге в коротком формате. Это своего рода каталог, который описывает структуру сборки, классы в нем пустые. Указывается только название класса и его атрибуты, такие как базовый класс, список интерфейсов, которые он обязуется реализовывать. Поэтому здесь мы тоже должны продублировать изменения. Закоментируем интерфейс C1.Win.U у класса C1TrueDBGrid в строке 606 и закоментируем 19 классов в строках 1408*1499. Теперь уделим внимание не нужным ресурсам. Это ресурсы которые использовали триальные формы, т.к. их нет нужно удалить и сами ресурсы из IL листинга. Описание ресурсов начинается со строки 102. Необходимо закоментировать ресурсы: C1.Win.LicensingForm.resources, C1.Win.BetaAboutForm.resources, C1.Win.AboutForm.resources. Незабудь вернуть наши исправления в конструкторе класса C1TrueDBGrid, которые мы делали в начале статьи, но потом отменили их. Компилируем запускаем и видим все работает прекрасно. Теперь скомпилируем еще раз но без флага /debug, в итоге размер файла 784 килобайта, а размер оригинала 888 килобайт. Своими манипуляциями мы сэкономили 104 килобайта, думаю 15 минут работы стоили того :).

Еще хотел бы отметить следующий момент. Если .Net компоненты используют другие сборки, тогда может быть задействованна круговая ссылка зависимости, т.е. в Reference сборки1 будет ссылка на сборку2, а в Reference сборки2 будет ссылка на сборку1. При таком раскладе при модификации любой из сборок, будет ошибка загрузки сборки, нужно убрать информацию о версиях и публичных ключах этих сборок. вот пример ссылки на другую сборку:
Код:
.assembly extern System.Windows.Forms
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
  .ver 1:0:3300:0
}
А выглядеть она должна так:
Код:
.assembly extern System.Windows.Forms
{
}
Таже самая проблема возникнет, если ты изменяешь сборку от которой зависит несколько других сборок. Во всех сборках придется сделать исправление.

В заключение
хотел сказать, что бы использовать взломанные компонеты, некоторые личности :) упаковывают\шифруют эти сборки и хранят их либо во внешнем файле либо в ресурсах и при запуске программы расшифровывают и запускают динамически. Вот простой пример который иллюстрирует это:
Код:
private void RunCrypt(string File)
{
        System.IO.FileStream FS2 = new System.IO.FileStream(File, System.IO.FileMode.Open);
        System.Security.Cryptography.CryptoStream CS2 = GetIO(FS2, System.Security.Cryptography.CryptoStreamMode.Read);
        BinaryReader BR = new System.IO.BinaryReader(CS2);
        string a = BR.ReadString();
        int L = BR.ReadInt32();
        Byte[] B = BR.ReadBytes(L + 1);
        BR.Close();

        System.Reflection.Assembly Asm = System.Reflection.Assembly.Load(B, null);
        Type ModType = Asm.GetType("AnyNamespace.FormMain", true, true);
        object Obj = Asm.CreateInstance(ModType.FullName);
    Obj.GetType().InvokeMember("Show", System.Reflection.BindingFlags.Public, null, null, null);
}
   
private System.Security.Cryptography.CryptoStream GetIO(System.IO.FileStream FS, System.Security.Cryptography.CryptoStreamMode Mode)
{
        System.Security.Cryptography.DESCryptoServiceProvider Des = null;
        byte[] K = new byte[]{132, 55, 34, 88, 23, 1, 254, 187, 26, 56, 78, 255, 37, 143, 201, 5};
        byte[] V = new byte[]{};
        Des.Key = K;
        Des.IV = V;

        System.Security.Cryptography.ICryptoTransform Trans;
        if (Mode == System.Security.Cryptography.CryptoStreamMode.Write)
        {
            Trans = Des.CreateEncryptor();
        }
        else
        {
            Trans = Des.CreateEncryptor();
        }
        return new System.Security.Cryptography.CryptoStream(FS, Trans, Mode);
}
 
Сверху Снизу