Для создания приложений на платформе .NET можно использовать различные языки программирования — C#, VB.NET, F#, другие языки, которые имеют неофициальную поддержку .NET (типа Deflfi .NET и т.д.). При компиляции кода на любом из этих языков создается сборка — это может быть выполняемое приложение, либо библиотека классов, которая используется в других приложениях. Однако вне зависимости от того, какой язык используется, скомпилированная сборка содержит код в некоторой промежуточной форме, независимой от исходного языка программирования, целевой машины и ее операционной системы. Благодаря этому проекты на одном .NET-языке могут использовать сборки, написанные на другом .NET-совместимом языке программирования. При запуске приложения и соотвественно запуске файла сборки общеязыковая среда выполнения CLR загружает ее и преобразует ее код в машинный код для последующего выполнения.
Промежуточное представление приложений .NET, предназначенное для общеязыковой среды выполнения CLR. среды, включает в себя два основных компонента: метаданные и управляемый код (managed code). Метаданные — это система дескрипторов всех компонентов приложения (классов, структур и их элементов и т.д.) и их отношения. Управляемый код представляет функциональность методов приложения, закодированных в специальной бинарной форме, которая называется MSIL или Microsoft intermediate language (другие названия — Common Intermediate Language (CIL) или просто Intermediate Language (IL). Затем уже при выполнении сборки JIT-компилятор компилирует методы, закодированные в MSIL, в бинарный/машинный код текущей платформы, который затем собственно и выполняется.
13. Ломаем exe файл в поисках пароля.
Для работы с кодом IL Microsoft предоставляет ряд утилит, в частности, ildasm и ilasm.
ildasm. Декомпиляция кода IL
Утилита ildasm предназначена для декомпиляции сборки .NET и получения из нее кода IL. Для использования данной утилиты надо установить nuget-пакет Microsoft.NETCore.ILDAsm .
Стоит отметить, что пакет устанавливается глобально в кэш nuget, поэтому для других проектов не надо повторно его устанавливать. Например, на Windows для версии .NET 6.0.0 это каталог
C:Users[имя_пользователя].nugetpackagesmicrosoft.netcore.ildasm6.0.0runtimesnative
Например, в моем случае это каталог C:Userseugen.nugetpackagesmicrosoft.netcore.ildasm6.0.0runtimesnative . И в этом каталоге можно найти саму утилиту ildasm.
Стоит отметить, что в качестве альтернативы мы также можем установить пакет nuget Ildasm для конкретной платформы. Нужный пакет называется по шаблону runtime.[платформа].Microsoft.NETCore.ILDAsm . Например, некоторые пакеты:
Декомпиляция Андройд приложений (подробно)
- runtime.win-x64.Microsoft.NETCore.ILDAsm
- runtime.osx-x64.Microsoft.NETCore.ILDAsm
- runtime.linux-x64.Microsoft.NETCore.ILDAsm
Рассмотрим, как использовать эту утилиту. Для начала скомпилируем какую-нибудь программу на C#. Пусть это будет следующая простейшая программа на C# 10:
Console.WriteLine(«Hello, World!»);
После построения проекта в папке проекта в каталоге binDebugnet6.0 мы найдем скомпилированный файл сборки, который называется по имени проекта и имеет расширение dll.
Например, в моем случае проект называется MSILApp, соотвественно файл сборки называется MSILApp.dll, а полный путь к файлу сборки будет C:UserseugensourcereposcsharpConsoleMSILAppMSILAppbinDebugnet6.0
Теперь декомпилируем полученную сборку. Для этого передадим утилите ildasm путь к файлу сборки в виде
ildasm путь_к_сборке
В этом случае код IL сборки будет выводиться на консоль. Но мы также можем вывести его в файл. Для этого утилите ildasm нужно передать параметр /out, который указывает на путь к генерируемому файлу.
ildasm путь_к_сборке /out=файл_с_кодом.il
Итак, откроем командную строку/терминал и сначала с помощью команды cd перейдем к каталогу, где располагается файл сборки. И затем передадим утилите ildasm файл сборки и укажем имя выходного файла:
C:Userseugen>cd C:UserseugensourcereposcsharpConsoleMSILAppMSILAppbinDebugnet6.0 C:UserseugensourcereposcsharpConsoleMSILAppMSILAppbinDebugnet6.0>C:Userseugen.nugetpackagesmicrosoft.netcore.ildasm6.0.0runtimesnativeildasm MSILApp.dll /out=msilapp.il
И после выполнения команды в папке, где располагается файл сборки, также появится файл со сгенерированным кодом IL:
В моем случае код IL будет выглядеть следующим образом:
// Microsoft (R) .NET IL Disassembler. Version 6.0.0 // Metadata version: v4.0.30319 .assembly extern System.Runtime < .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_. .ver 6:0:0:0 >.assembly extern System.Console < .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_. .ver 6:0:0:0 >.assembly MSILApp < .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilationRelaxationsAttribute. ctor(int32) = ( 01 00 08 00 00 00 00 00 ) .custom instance void [System.Runtime]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute. ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78 // . T..WrapNonEx 63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 ) // ceptionThrows. // — The following custom attribute is added automatically, do not uncomment ——- // .custom instance void [System.Runtime]System.Diagnostics.DebuggableAttribute. ctor(valuetype [System.Runtime]System.Diagnostics.DebuggableAttribute/DebuggingModes) = ( 01 00 07 01 00 00 00 00 ) .custom instance void [System.Runtime]System.Runtime.Versioning.TargetFrameworkAttribute. ctor(string) = ( 01 00 18 2E 4E 45 54 43 6F 72 65 41 70 70 2C 56 // . NETCoreApp,V 65 72 73 69 6F 6E 3D 76 36 2E 30 01 00 54 0E 14 // ersion=v6.0..T.. 46 72 61 6D 65 77 6F 72 6B 44 69 73 70 6C 61 79 // FrameworkDisplay 4E 61 6D 65 00 ) // Name. .custom instance void [System.Runtime]System.Reflection.AssemblyCompanyAttribute. ctor(string) = ( 01 00 07 4D 53 49 4C 41 70 70 00 00 ) // . MSILApp.. .custom instance void [System.Runtime]System.Reflection.AssemblyConfigurationAttribute. ctor(string) = ( 01 00 05 44 65 62 75 67 00 00 ) // . Debug.. .custom instance void [System.Runtime]System.Reflection.AssemblyFileVersionAttribute. ctor(string) = ( 01 00 07 31 2E 30 2E 30 2E 30 00 00 ) // . 1.0.0.0.. .custom instance void [System.Runtime]System.Reflection.AssemblyInformationalVersionAttribute. ctor(string) = ( 01 00 05 31 2E 30 2E 30 00 00 ) // . 1.0.0.. .custom instance void [System.Runtime]System.Reflection.AssemblyProductAttribute. ctor(string) = ( 01 00 07 4D 53 49 4C 41 70 70 00 00 ) // . MSILApp.. .custom instance void [System.Runtime]System.Reflection.AssemblyTitleAttribute. ctor(string) = ( 01 00 07 4D 53 49 4C 41 70 70 00 00 ) // . MSILApp.. .hash algorithm 0x00008004 .ver 1:0:0:0 >.module MSILApp.dll // MVID: .imagebase 0x00400000 .file alignment 0x00000200 .stackreserve 0x00100000 .subsystem 0x0003 // WINDOWS_CUI .corflags 0x00000001 // ILONLY // Image base: 0x00000211BEA40000 // =============== CLASS MEMBERS DECLARATION =================== .class private auto ansi beforefieldinit Program extends [System.Runtime]System.Object < .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute. ctor() = ( 01 00 00 00 ) .method private hidebysig static void ‘$'(string[] args) cil managed < .entrypoint // Code size 12 (0xc) .maxstack 8 IL_0000: ldstr «Hello, World!» IL_0005: call void [System.Console]System.Console::WriteLine(string) IL_000a: nop IL_000b: ret >// end of method Program::’$’ .method public hidebysig specialname rtspecialname instance void .ctor() cil managed < // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [System.Runtime]System.Object. ctor() IL_0006: nop IL_0007: ret >// end of method Program. ctor > // end of class Program // ============================================================= // *********** DISASSEMBLY COMPLETE *********************** // WARNING: Created Win32 resource file msilapp.res
То есть это тот код, в который компилируется простейшая программа на C#, выводящая на строку «Hello World»
Стоит отметить, что на Windows при установке Visual Studio и .NET 4.X также устанвливается графическая утилита ildasm , которая в отличие от консольной утилиты позволяет графически посмотреть код IL сборки. В системе она расположена по пути
C:Program Files (x86)Microsoft SDKsWindowsv10.0AbinNETFX 4.8 Tools
Правда, она нацелена прежде всего на сборки, скомпилированные с помощью .NET Framework 4.x, но также позволяет просматривать (по крайней мере некоторые) сборки .NET Core/.NET5+
Источник: metanit.com
Создание исходного кода из сборок .NET во время отладки
Область применения:Visual Studio
Visual Studio для Mac
Visual Studio Code
При отладке приложения .NET вам может потребоваться просмотреть исходный код, которого у вас нет. Например, происходит прерывание в исключении, или стек вызовов используется для перехода к исходному расположению.
- Создание исходного кода (декомпиляция) возможно только для приложений .NET и построено на проекте ILSpy с открытым кодом.
- Декомпиляция доступна только в Visual Studio 2019 версии 16.5 и более поздних версий.
- Атрибут SuppressIldasmAttribute, примененный к сборке или модулю, не позволит Visual Studio выполнить декомпиляцию.
Создание исходного кода
Если при выполнении отладки исходный код недоступен, в Visual Studio отображается документ Исходный код не найден, а если отсутствуют символы для сборки, отображается документ Символы не загружены. Оба документа имеют параметр Декомпилировать исходный код, который создает код C# для текущего расположения. Созданный код C# можно использовать так же, как любой другой исходный код. Вы можете просматривать этот код, проверять переменные, устанавливать точки останова и т. д.
Символы не загружены
На следующем рисунке показано сообщение Символы не загружены.
Исходный код не найден
На следующем рисунке показано сообщение Исходный код не найден.
Создание и внедрение исходного кода для сборки
Можно создать не только исходный код для определенного расположения, но и весь исходный код для конкретной сборки .NET. Для этого перейдите в окно Модули и в контекстном меню сборки .NET, а затем выберите команду Декомпилировать источник в файл символов . Visual Studio создает файл символов для сборки, а затем внедряет исходный код в файл символов. На последующем этапе можно извлечь внедренный исходный код.
Извлечение и просмотр внедренного исходного кода
Исходные файлы, внедренные в файл символов, можно извлечь с помощью команды Извлечь исходный код в контекстном меню окна Модули.
Извлеченные исходные файлы добавляются в решение как прочие файлы. В Visual Studio функция «Прочие файлы» по умолчанию отключена. Чтобы включить эту функцию, установите флажок Инструменты>Параметры>Среда>Документы>Показывать прочие файлы в Обозревателе решений. Без включения этой функции вы не сможете открыть извлеченный исходный код.
Извлеченные исходные файлы отображаются в разделе прочих файлов в Обозревателе решений.
SourceLink
Для библиотек .NET или пакетов NuGet, включенных для SourceLink, можно также выполнять по шагам исходный код, задавать точки останова и использовать все функции отладчика. Дополнительные сведения см. в разделе Улучшение производительности во время отладки с помощью SourceLink.
Известные ограничения
Требуется режим прерывания выполнения
Создание исходного кода с помощью декомпиляции возможно только в том случае, если отладчик находится в режиме прерывания выполнения и приложение приостановлено. Например, Visual Studio переходит в режим прерывания, попадая в точку останова или в исключение. Вы можете легко активировать Visual Studio для прерывания при следующем запуске кода с помощью команды Прервать все («).
Ограничения декомпиляции
Создание исходного кода из промежуточного языка (IL), используемого в сборках .NET, имеет некоторые внутренние ограничения. Поэтому созданный исходный код не выглядит в точности как оригинальный исходный код. Большая часть различий сосредоточена в тех местах, где информация в оригинальном исходном коде не нужна во время выполнения. Например, во время выполнения не нужна такая информация, как пробелы, комментарии и имена локальных переменных. Рекомендуется использовать созданный исходный код, чтобы понять, как выполняется программа, а не в качестве замены оригинального исходного кода.
Отладка оптимизированных сборок или сборок выпуска
При отладке кода, декомпилированного из сборки, которая была скомпилирована с использованием оптимизаций компилятора, вы можете столкнуться со следующими проблемами:
- точки останова могут не всегда быть привязаны к соответствующим исходным расположениям;
- при пошаговом выполнении шаг может не всегда переходить в правильное место;
- имена локальных переменных могут быть неточными;
- некоторые переменные могут быть недоступны для оценки.
Дополнительные сведения можно найти в описании проблемы GitHub Интеграция ICSharpCode.Decompiler с отладчиком VS.
Надежность декомпиляции
Относительно небольшой процент попыток декомпиляции может привести к сбою. Это происходит из-за ошибки пустой ссылки точки последовательности в ILSpy. Мы устранили этот сбой путем перехвата таких проблем и корректного завершения попытки декомпиляции.
Дополнительные сведения можно найти в описании проблемы GitHub Интеграция ICSharpCode.Decompiler с отладчиком VS.
Ограничения при работе с асинхронным кодом
Результаты декомпиляции модулей с шаблонами кода async/await могут быть неполными или неудачными в целом. Шаблоны кода async/await и машины состояния yield state-machine в ILSpy реализованы только частично.
Дополнительные сведения можно найти в описании проблемы GitHub Состояние генератора PDB.
Только мой код
Параметр Just My Code (JMC) позволяет Visual Studio перешагнул систему, платформу, библиотеку и другие непользовательские вызовы. Во время сеанса отладки в окне Модули отображаются модули кода, которые отладчик воспринимает как «Мой код» (т. е. пользовательский код).
При декомпиляции оптимизированных модулей или модулей выпуска создается непользовательский код. Если отладчик прерывается в декомпилированном непользовательском коде, появляется окно Отсутствует источник. Чтобы отключить режим «Только мой код», перейдите в раздел Инструменты>Параметры (или Отладка>Параметры) >Отладка>Общие и снимите флажок Включить только мой код.
Извлеченный исходный код
Исходный код, извлеченный из сборки, имеет следующие ограничения.
- Имена и расположение созданных файлов нельзя настроить.
- Файлы являются временными и будут удалены Visual Studio.
- Файлы помещаются в одну папку без использования какой-либо иерархии, которая была в оригинальных исходных файлах.
- Имя каждого файла содержит хэш контрольной суммы файла.
Создается только код C#
При декомпиляции создаются только файлы исходного кода на C#. Возможность создавать файлы на каком-либо другом языке отсутствует.
Источник: docs.microsoft.com
Как работает декомпиляция в .Net или Java на примере .Net
Сегодня хотелось бы поговорить про декомпиляцию приложений (все применительно к той же Java, да и любому языку с некоторыми допущениями и ограничениями, но поскольку сам я — .Net разработчик, примеры будут совсем немного MSIL’овизированы 🙂 ).
- JetBrains dotPeek (поддержка R# хоткеев, сервер символов)
- Telerik JustDecompile (также не плохой, множество хоткеев)
- RedGate Reflector (аналог dotPeek, но платный. Изначально был основным в мире .Net, но пока был бесплатным)
- icsharpcode ILSpy (хороший, opensource. Полезен, когда вы сами пишете код, использующий Mono.Cecil, т.к. Это даст лучшее понимание его работы)
- 9rays Spices .Net Decompiler
- Dis# с функцией inplace editor
- Mono.Cecil (основной, самый крутой декомпилятор в мире .Net. На выходе получаете объектное «зеркало» содержимого сборки. Т.е. Максимально-упрощенно, без наворотов типа конвертации массива IL в DOM).
- ICSharpCode.Decompiler (надстройка над mono.cecil, переводящая array[MSIL] в DOM, где есть циклы, switches и if’ы. Является частью SharpDevelop/ILSpy)
- Harmony Core (аналогичное от меня, но сохраняющее информацию о символах. В среднем состоянии, не готова для прода, помощь приветствуется).
А теперь, хотелось бы описать как они работают (вам же интересно, как работает машинка от JetBrains?). Чтобы как минимум понять, насколько это сложно: написать свой декомпилятор .Net сборки обратно в код на C#.
Для начала, полный список выложенных на Хабре статей данного цикла
- Должен принимать на вход любую сборку: от CLR 1.* до 4.*
- Обязан поддерживать не только C# вывод, но и MSIL, VB.NET и вообще — на что фантазии и потребностей хватит.
- Возможность выбирать между различными версиями языка (например, C#), при этом не имея дублирования в реализации.
И теперь, когда требования определены, давайте подумаем, как устроена работа MSIL, и как это поможет нам в быстрой декомпиляции приложения.
В отличии от языка процессора, который вносит для нас некоторые сложности в процесс декомпиляции (регистры, оптимизации, возможность сделать одно действие несколькими способами), в MSIL все максимально просто. Если надо записать в локальную переменную нечто, то для этого есть всего одна команда. Другим способом записать в переменную значение не получится. Это свойство наделяет конечный компилятор (JITter) простотой в реализации с одной стороны… А с другой стороны наделяет простотой в реализации декомпилирующую сторону.
Второе свойство, каким обладает MSIL, это вычисления на стеке. Тут нет регистров. И единственная память, через которую идут все вычисления — это стек. Это абсолютно не значит что конечный процессор также все вычисляет через стек. Нет. Это значит что этой моделью для упрощения пользуется описание всех расчетов и вызовов на MSIL.
Что это значит для нас? Это значит что сложить два числа можно только одной командой, которая вне зависимости от параметров — одна. Это команда, вытащив данные для сложения из стека, складывает их и сохраняет результат не куда-либо, а обратно в стек. Это важно, потому что для нас, как для людей, пишущих декомпилятор это не породит огромного ветвления кода.
Теперь мы подошли к самому главному: как происходит процесс декомпиляции.
Ldc_i4 5 Ldc_i4 4 Add Stloc.1
Sum = 5 + 4;
Первая трудность, которая приходит в голову: положение инструкций может быть различным. Т.е., например, чтобы код выполнился, совсем не обязательно что между ldind_i4 и add не будет других инструкций. Например, совершенно валиден следующий код:
Ldc_i4 5 Ldc_i4 4 Ldc_i4 10 Stloc.2 // sum2 Add Stloc.1 // sum1
Что должно декомпилироваться, например, так:
Sum2 = 10 Sum1 = 5 + 4;
Во-вторых названия переменных в релизе могут отсутствовать. Т.е. без примесей, код будет таким:
= 10 = 5 + 4;
В третьих, что самое сложное, реализации if-else, while, do-while, switch могут отличаться. Этого касаются, в особенности, лямбды, yields, async/awaits и прочие языковые примочки, которые являются опциональными и на самом деле реализуются поверх обычных функций языка. Как все это учесть? На самом деле оба вопроса решаются всего двумя способами.
Стековая модель декомпиляции
Для декомпиляции пишется некий интерпретатор кода на MSIL, у которого есть свой стек и цикл интерпретации команд. На каждой итерации цикла, берется очередная не рассмотренная команда:
-
Если это не инструкция перехода, то мы смотрим, сколько значений на стеке требуется исследуемой командой. Далее мы достаем со стека два вычислительных узла, которые мы положили туда, как результаты вычисления предыдущих команд и создаем новый узел, ветвями которого являются взятые со стека узлы. Для примера выше это будет выглядит так:
Т.е. Сначала у нас есть на входе 4 команды. Первые две ничего не берут на вход, а только отдают — число. Соответственно, мы кладем их на стек (ldind_i4 4, ldind_i4 5). После чего мы берем очередную команду — Add.
Она принимает на вход два значения со стека. Поэтому мы считываем два узла с нашего стека и, схоранив их как параметры этой команды, сохраняем саму команду- на стек, поскольку у команды есть результат. А любой результат сохраняется на стеке.
Далее результат может быть передан в метод, либо участвовать в других арифметических операциях, либо возвращен с помощью инструкции ret.
Соответственно, если бы выражение было бы посложнее:
Ldc_i4 5 Ldc_i4 4 Ldc_i4 10 Mul Add Stloc.1 // value = 5 + (4 * 10)
То процесс создания DOM выглядел бы следующим образом:
После чего осуществляется окончательная сборка дерева:
Таким же образом конструируются вызовы методов. Только в случае методов, со стека будет забираться требуемое под вызов количество параметров и сохраняться в классе ноды вызова метода. Если метод возвращает значение, то нода вызова метода будет сложена в стек. Если нет — добавлена к группе готовых выражений.
Сборка дерева
Это все были подготовительные этапы. Далее, для модульности, создаются классы, которые распознают какую-либо одну конструкцию в дереве и переводят ее в другую. Например, если это if-else, то матчится наличие условного перехода такого, чтобы переход осуществлялся вперед.
Тогда узел преобразуется в if-else ноду, код за переходом помечается как else (negative if) нода, а код между условием и else нодой — как positive if нода. Если матчится как условный переход с переходом на прошлые инструкции, то это матчится как while цикл и дерево также перестраивается.
Соответственно, в зависимости от чистоты исполнения матчеров, на выходе мы получем преобразованное дерево под конкретный язык программирования. Далее, у каждого из языков программирования мы задаем множество матчеров, которые ему подходят. Например, циклы и условия подойдут всем, потому они будут присутствовать почти во всех пакетах. А вот, например, async/await — он только для C#. Потому, будет присутствовать тольк в его пакете.
Для ясности картины, как собираются if-else и while/do-while, рассмотрим примеры:
Сборка IF-ELSE блока
Сборка WHILE блока
Генерация кода
Последний этап матчинга — генерация кода по дереву. Тут не должно быть каких-то сложностей. Идеально, конечно, было бы круто подсасывать правила от R# или StyleCop. Благо, они в XML. Но в простейшем случае, мы пишем генератор, который принимает на вход дерево описания класса. Он сперва обязан проверить все дерево: содержит ли оно не поддерживаемые типы нод.
Если все в порядке, то обходится все дерево и для каждого узла вызывается соответствующий метод по шаблону проектирования Visitor, которому передается StringBuilder и соответствующая нода. Дополнительно, необходимо считать количество пробелов, которые надо отступать с начала каждой строки. На этом этапе все достаточно просто.
Генерация имен переменных
- Имена методов, которые не являются сгенерированными компилятором, в расчетах или результатах расчетов которых они используются. Пример: var . = this.Counterparty; -> . = counterparty.
- Данные, является ли переменная — переменной цикла. Т.е. Считается ли она только в теле цикла. Если она — целочисленная, то кандидаты на имя — index, i, j.
- Если переменная — в цикле foreach является элементом из итерируемой коллекции, то можно назвать ее [collectionName]Item либо просто item.
Источник: habr.com