- Главная
- Блог
- Информационная безопасность
- Реверсинг .NET приложений
Реверсинг .NET приложений
В трех предыдущих статьях мы рассмотрели основы реверсинга приложений под Windows с использованием отладчика и дизассемблера. Однако, иногда, для того, чтобы понять логику работы приложения совершенно необязательно разбираться в ассемблерных инструкциях.
Приложения под Windows, написанные на .NET можно исследовать без помощи x64dbg и IDA Pro. Для того, чтобы понять общие принципы реверсинга таких приложений, сначала поговорим о том, что такое .NET, для чего используется данный фреймворк и какими инструментами его можно исследовать.
Платформа .NET
В 2002 году корпорация Майкрософт выпустила первую версию фреймворка .NET. Основной принцип работы данной платформы заключается в использовании общеязыковой среды исполнения Common Language Runtime, которая подходит для различных языков программирования. Функциональные возможности CLR доступны в любых языках программирования, использующих эту среду. В настоящее время .NET Framework развивается в виде .NET.
В чем суть данной технологии? Приложение для .NET Framework, написанное на любом поддерживаемом данной технологией языке программирования, сначала переводится компилятором в единый для .NET промежуточный байт-код языка Common Intermediate Language (CIL). По своему синтаксису CIL отчасти напоминает ассемблер.
Компилятор любого языка, поддерживающий платформу .NET, транслирует код с языков высокого уровня платформы .NET на язык CIL. В частности, код на языке CIL генерируют все компиляторы .NET компании Microsoft, входящие в среду разработки Microsoft Visual Studio (C#, Managed C++, Visual Basic .NET, Visual J# .NET).
Язык CIL
По своей сути язык CIL можно назвать ассемблером виртуальной машины .NET. Но, при этом, язык CIL содержит некоторые достаточно высокоуровневые конструкции, повышающие его уровень по сравнению с ассемблером для любой реально существующей машины, и писать код непосредственно на CIL легче, чем на ассемблере для реальных машин.
Байт-код CIL, может быть выполнен специальной виртуальной машиной Common Language Runtime (CLR). И если мы используем виртуальную машину CLR встроенный в неё JIT компилятор на лету (just in time) преобразует промежуточный байт код в машинные коды нужного процессора.
Отдельно стоит поговорить метаданных. Метаданные в .NET это определённые структуры данных, которые добавляются в код CIL для описания высокоуровневой структуры кода. Метаданные описывают все классы и члены классов, определённые в данной сборке и в других сборках, вызываемых данной. Также, метаданные для метода содержат полное описание этого метода, включая его класс а также сборку для данного класса, его возвращаемый тип и все параметры этого метода.
При исполнении кода CIL в среде CLR, делается проверка того, что метаданные вызываемого метода совпадают с метаданными, хранящимися в вызывающем методе. Это позволяет гарантировать, что метод может быть вызван именно с корректным числом параметров и именно с корректными типами параметров.
В рамках данной статьи мы не будем подробно рассматривать работу с CIL, так как для наших задач реверсинга нам не нужно подробно изучать данный язык. Но в качестве примера посмотрим небольшой код на CIL.
.assembly Hello {}
.method public static void Main() cil managed
{
.entrypoint
.maxstack 1
ldstr "Hello, world!"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
Данный пример просто выводит на экран текст Hello World! Теперь давайте посмотрим, какие инструменты нам потребуются непосредственно для реверсинга.
Инструменты для реверсинга
Как уже упоминалось ранее, для исследования .NET приложений нам не нужен дизассемблер или отладчик. Однако, здесь есть аналогичные инструменты, например .NET Reflector. Общие принципы работы с данной утилитой достаточно просты – после установки мы можем вызвать ее с помощью нажатия правой кнопки мыши по любому выполнимому файлу. Если этот файл написан на .NET, утилита отобразит его содержимое.
При открытии приложения в .NET Reflector мы получаем исходный код этого приложения в восстановленном виде.
Здесь мы можем наблюдать общую структуру приложения под Windows примерно в таком виде, как оно выглядело, когда было проектом для Visual Studio или аналогичной среды разработки.
Анализаторы .NET
В рамках данной статьи мы не будем рассматривать обфусцированные .NET приложения, однако полезно познакомиться со средствами анализа .NET приложений на наличие обфускации или упаковки. Прежде всего это Detect It Easy.
Принцип работы утилиты DIE следующий. Сначала необходимо определить тип файла, а затем последовательно производится загрузка всех сигнатур, которые лежат в соответствующей папке db. То есть, в принципе функционал DIE можно дополнять самостоятельно, просто помещая в папку db дополнительные сигнатуры.
А в настоящее время программа определяет следующие типы исполняемых файлов:
- MSDOS Исполняемые файлы MS-DOS (вряд ли потребуется на практике).
- PE исполняемые файлы Windows.
- ELF исполняемые файлы Linux.
- MACH исполняемые файлы Mac OS.
- Все остальные бинарные файлы.
После запуска анализа утилита показывает список возможных протекторов, которыми возможно был защищен данный файл. Основная проблема работы с подобными средствами заключается в том, что зачастую они выдают ложные срабатывания, указывая те протекторы, которые на самом деле не использовались.
Но анализаторы часто могут ошибаться, неверно определяя механизм обфускации или упаковки. Поэтому, при анализе лучше не полагаться на один инструмент, и воспользоваться вторым анализатором.
Здесь в качестве примера я предлагаю воспользоваться утилитой DnSpy, которая также анализирует файл на наличие обфускаторов. dnSpy — это утилита для декомпиляция приложений на языке программирования C#.
И в том, и в другом случае мы либо получим исходный код приложения, либо отчет с предположениями об используемых механизмах обфускации.
Цель использования таких анализаторов заключается в том, чтобы понять, чем упаковано то или иное приложение. Изначально мы можем попытаться открыть файл с помощью .NET Reflector, а в случае, если код обфусцирован (как на картинке ниже), мы прибегаем к помощи анализаторов типа DIE и dnSpy.
Разбираем CrackMe
Как я уже говорил, в рамках этой статьи мы не будем рассматривать обфусцированные приложения. Вместо этого мы разберем обычный крякмикс.
После запуска этот CrackMe выводит такое вот окно с запросом пароля. Ничего необычного, выглядит как классическое приложение под Windows.
Далее откроем этот файл в .NET Reflector. Для этого достаточно просто нажать правую кнопку мыши на этом файле и выбрать Browse with .NET Reflector.
В результате получаем содержимое данного файла практически в виде исходного проекта. Все, кто знаком с разработкой приложений под Windows, например в Visual Studio знают, что для создания оконного приложения нужна форма, на которую помещаются различные компоненты: кнопки, поля ввода, метки и другие компоненты. Также у этих компонентов есть параметр видимости, то есть мы можем сделать какую-либо надпись невидимой. Давайте посмотрим внимательно, на содержимое раздела Initial Components. Здесь есть два интересных параметра: thisLabel3.Text=”You’ve done it!” и thisLabel3.Visible=false. Можно предположить, что первый выводит на экран сообщение о вводе правильного пароля. А второй параметр скрывает это поле при первоначальном запуске программы.
Таким образом, мы понимаем, как выглядит сообщение о вводе правильного пароля. Теперь давайте посмотрим обработчики событий. В частности, нас будет интересовать button1_Click, то что происходит при нажатии кнопки Check в основной форме.
Давайте посмотрим этот код более подробно. Примечательно, что для данной программы .NET Reflector смог восстановить изначальные названия процедур и переменных. Как можно заметить названия некоторых процедур написаны по-польски.
В первой строке процедуры button1_Click у нас берется содержимое текстового поля this.textBox1.Text и далее производится проверка содержимого данного поля с помощью процедуры this.reg.Testuj(text). Если это условие выполняется, присваиваем некоторым полям определенные настройки видимости.
private void button1_Click(object sender, EventArgs e)
{
string text = this.textBox1.Text;
if (this.reg.Testuj(text)) // <- Проверка условия
{
this.label2.Visible = false;
this.button1.Visible = false;
this.textBox1.Visible = false;
this.label3.Visible = true;
}
Посмотрим, что же делает эта процедура. Прежде всего мы видим, примечательную “заглушку”: если введенный пароль имеет длину, отличную от четырех, то он будет заменен на некоторое постоянное значение (9231456). Вы можете самостоятельно убедиться в том, что это значение точно не подходит в качестве пароля.
Кстати, подобные приемы используются во многих крякмиксах, когда мы первым делом проверяем длину введенного пользователем пароля, и если она не отвечает определенным требованиям, то дальше мы ничего не решаем
public bool Testuj(string heslo)
{
if (heslo.Length != 4)
{
heslo = "9231456"; // если длина не 4 символа, то присваиваем какое-то заранее неверное значение.
}
for (int i = heslo.Length - 1; i > -1; i--)
{
int znak = Convert.ToInt32(heslo[i]); // берем цифры по отдельности
znak = this.Prepocet(znak); // конвертируем их
if (this.Xor(znak) != 0) // снова конвертируем и если не 0 то неудачное завершение
{
return false; // неудачное завершение
}
}
return true; // удачное завершение
}
Если длина пароля составляет 4 символа, то каждый символ индивидуально проверяется на соответствие некоторому условию. Если все цифры совпадают, то пароль правильный.
Итак, мы рассмотрим методы CrackMe1.Reg.Prepocet и CrackMe1.Reg.Xor:
private int Prepocet(int znak)
{
znak *= 80;
znak -= 0xf4b;
znak ^= 0x145;
znak *= znak + 0xffb;
return znak;
}
private int Xor(int znak)
{
return (znak ^= 0x1589c0);
}
Как видно из этих процедур, здесь производится некоторое логическое преобразование каждого из введенных символов с последующей проверкой результатов. Самый простой способ найти подходящие символы это перебрать все 62 буквенно-цифровых символа. Здесь мы поступим по аналогии с материалом предыдущей статьи, только там мы писали кейген, а здесь у нас есть только одно значение и мы должны сами его подобрать. Поэтому мы можем взять исходный код этих двух процедур и написать программу для перебора всех возможных вариантов для буквенно-цифровых символов. В приведенном ниже примере на C#, процедуры Prepocet и Xor для простоты были сведены в один метод:
static void Main(string[] args)
{
string alphanumeric = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
for(int i = 0; i < alphanumeric.Length; i++)
{
char character = Convert.ToChar(alphanumeric.Substring(i, 1));
if (Check(Convert.ToInt32(character)))
{
Console.WriteLine(character);
}
}
}
private static bool Check(int value)
{
value *= 80;
value -= 0xf4b;
value ^= 0x145;
value *= value + 0xffb;
value ^= 0x1589c0;
return (value == 0);
}
После осуществления перебора получаем достаточно забавный результат. Оказывается, что единственный буквенно-цифровой символ, который подходит это 1.То есть рабочий пароль для нашего крякми это 1111.
Проверим:
Заключение
Этой статьей мы завершаем наш цикл, посвященный реверсингу приложений под Windows. Сегодня мы поговорили о том, что из себя представляют приложения написанные на .NET и как можно их исследовать.
Конечно в рамках данных статей была рассмотрена только малая часть материалов, посвященных реверсингу приложений. Наш курс глубже рассматривает различные темы обратного инжиниринга приложений, включая исследование вредоносных файлов, деобфускацию, шифрование и многое другое.