Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #127, ИЮЛЬ 2009 г.

Трудности перевода. Учимся ломать .NET-сборки

m0r0 m0r0@inbox.ru


Первая версия .NET впервые была представлена Microsoft в 2002 году. С тех пор утекло много воды, и на подходе уже 4-я версия фреймворка. Число софтин растет быстрее, чем размножаются хомячки, а ты боишься к ним даже приблизиться. Пора разобраться, что к чему, и восстановить справедливость.

Загадочный фреймворк

Говоря о .NET, следует признать, что в Microsoft вовсе не дураки работают, и иногда в недрах корпорации рождаются поистине интересные решения. Я не буду вдаваться в подробности относительно противостояния .NET и Java, проводить детальные сравнения и меряться всем, чем можно. Факт налицо - .NET развивается и уже включен в официальную поставку новых ОС (Vista/2008/7) от Microsoft.

Microsoft предоставляет не только среды разработки и исполнения, но и богатые библиотеки классов, позволяющие реализовать практически любой функционал пользовательского приложения. Совместно с Hewlett-Packard и Intel были стандартизованы CLI, C# и C++/CLI (ECMA-335, ECMA-334, ECMA-372, соответственно). Это открыло дорогу для Novell с их проектом Mono, реализующим данные стандарты и позволяющим запускать приложения .NET на других, безоконных операционных системах. Microsoft делает ставку на .NET как на лидирующую среду для создания приложения под ОС Windows, а, значит, и нам следует отнестись к ней с уважением и научиться справляться с потенциальными трудностями.

Одним из несомненных преимуществ .NET является поддержка нескольких языков программирования. В первую очередь, это заостренный C (C#) и, конечно же, Basic в формате VB.NET. В рамках одного проекта можно комбинировать сборки, разработанные на различных языках. Среда сама обеспечит корректность исполнения конечного решения. Каким же образом это достигается? Реализованы концепции песочницы, CLI и JIT.

Ольга, прощай!

На самом деле, все достаточно просто (здесь опять можно вспомнить о Java, но мы этого делать не будем). Независимость от языка программирования и от конечной платформы достигается за счет компиляции приложения не в нативный, а в промежуточный байт-код. В терминологии Microsoft он носит название MSIL (Microsoft Intermediate Language), а после стандартизации все называют его просто CIL (Common Intermediate Language) или, еще круче, - IL.

CLR (Common Language Runtime) отвечает за компиляцию байт-кода в нативный, обеспечивая его выполнение на целевой системе. При этом используется подход JIT (Just-In-Time), согласно которому компиляция производится оперативно по мере необходимости.

Посмотрим, что представляет собой .NET-приложение глазами Ольги. Для этого создадим простенькое приложение Windows.Forms, содержащее одну кнопку и вызывающее MessageBox с сообщением «Hello, World» при ее нажатии. Не правда ли, очень оригинально?

Создал? Тогда грузи сборку в Ольгу и наслаждайся результатом. Приложение загрузилось, а в окне дизассемблера – пусто. Обидно до соплей. Точку входа не поймали и чего дальше делать – непонятно. Жмем паузу и переходим к отладке программы. Ставим условную точку останова на TranslateMessage для перехвата нажатий левой кнопки мыши (WM_LBUTTONUP: MSG == 202). Кликаем и погружаемся в транс – казалось бы, элементарный вызов MessageBox заставляет Ольгу совершать кучу действий и не дает никакого понимания логики работы программы. Наверное, не все так уж и ужасно, но сей факт заставляет задуматься о целесообразности использования классического отладчика для работы с .NET-сборками.

Итак, продолжим поиск новых друзей-отладчиков.

Братья-близнецы: ILASM и ILDASM

Динамическое компилирование делает процедуру отладки практически невозможной для сложных приложений. Но зачем пытаться отлаживать нативный код, когда можно подняться выше и работать на уровне IL?

Первая проблема – как этот самый код получить, имея под рукой только бинарник. Для этого сама Microsoft предлагает инструмент под названием ILDASM. Название выбрано неспроста (надеюсь, ты уже догадался, что к чему). ILDASM устанавливается автоматически при установке Visual Studio, так что, поверь, он у тебя уже есть.

Запускаем консоль студии (или просто подгружаем переменные среды с помощью скрипта vsvars32.bat) и запускаем ildasm. Открываем наше приветливое приложение и утыкаемся в нечто похожее на диаграмму классов. Развертывая метод любого из классов, получаем его IL-код. С кодом мы разберемся немного позже, а сейчас проведем эксперимент. Выбираем меню File -> Dump и сохраняем код всего приложения в текстовый файл Sample1.il. После этого запускаем ILASM (угадай для чего): ilasm Sample1.il. Получаем файл с непонятным расширением exe, запускаем и видим до боли знакомую форму.

Перед нами простейший алгоритм патчинга приложений:

  • дизассемблируем код в IL-язык;
  • дампим в текстовый файл;
  • корректируем текст в любимой среде программирования aka «Блокнот»;
  • собираем приложение из IL-кода.

Учим матчасть

Итак, IL. Неплохо хотя бы немного представлять, как писать на IL, скажешь ты, и будешь абсолютно прав. Чтобы разбираться в логике работы, а тем более, иметь возможность ее изменять, нам нужно изучить азы языка. Этим сейчас и займемся.

Для начала определимся с инструментом. «Блокнот» – великая вещь, но хотелось бы иметь под рукой среду, обеспечивающую комфортную работу на всем цикле разработки: подсветку синтаксиса, автоматическую компиляцию и линковку, отладку. Напрашивается Visual Studio, ан нет, Microsoft не реализует поддержку IL. Возможно, есть какие-нибудь плагины, но мне их найти не удалось.

По большому счету, можно программировать прямо в IL, поэтому будем искать среду программирования и отладки. Проектов таких два: DILE и ILIDE#. Первый – редактор, позволяющий отлаживать исполняемый код. Самое смешное, что как раз редактировать он и не умеет (классно назвали). Второй проект представляет собой полноценную среду программирования. Однако попытка импортировать код, полученный из ILDASM, приводит к невозможности компиляции (классная IDE). В конечном итоге выбор пал на среду SharpDevelop, в которой можно создавать проекты с поддержкой IL. Такую же поддержку предоставляет MonoDevelop, однако у меня он работать отказался, а то, что не работает, идет в топку. Недостатки SharpDevelop: отсутствует поддержка автодополнения и невозможна работа с диаграммами классов.

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

Код MainClass.il

.assembly HelloWorld
{
}
.method static void main()
{
// Указываем, что функция является точкой входа
.entrypoint
// Определяем максимальную глубину стека при вызове функций
.maxstack 4
// Инициализируем локальные переменные
.locals init (int32 first, int32 second, int32 result)

// Загружаем в стек строку
ldstr "First number: "
// Выводим на консоль
call void [mscorlib]System.Console::Write(string)
// Считываем введенную пользователю строку
call string [mscorlib]System.Console::ReadLine()
// Преобразуем к типу int32. Здесь нужно использовать обработку исключений,
// но для простоты она опущена
call int32 [mscorlib]System.Int32::Parse(string)
// Сохраняем в локальной переменной
stloc first

ldstr "Second number: "
call void [mscorlib]System.Console::Write(string)
call string [mscorlib]System.Console::ReadLine()
call int32 [mscorlib]System.Int32::Parse(string)
stloc second

// Загружаем переменные в стек и складываем
ldloc first
ldloc second
add
stloc result

ldstr "{0} + {1} = {2}"
ldloc first
// Преобразуем в ссылочный тип
box int32
ldloc second
box int32
ldloc result
box int32
call void [mscorlib]System.Console::WriteLine(string, object, object, object)
call int32 [mscorlib]System.Console::Read()
// Очищаем стек
pop
// Делаем возврат
ret
}

Вся программа состоит из набора директив и команд. Директивы начинаются с символа точки и реализуют декларативные функции:

  • .assembly – объявляет определение манифеста и указывает, какой сборке принадлежит текущий модуль;
  • .method – объявляет метод;
  • .entrypoint – объявляет, что указанный метод реализует точку входа в приложение;
  • .maxstack – указывает максимальное количество слотов в стеке для передачи параметров функции;
  • .locals – объявляет локальные переменные метода.

Описание всех директив ты найдешь в официальном документе Common Language Infrastructure (CLI), Partition II: Metadata Definition and Semantics на нашем диске.
Вся логика работы приложения реализуется с помощью команд. Полный перечень команд ты можешь найти в MSDN (http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.ldlen.aspx). Учти, что символы подчеркивания нужно заменять на символы точки. Любая операция в MSIL исполняется на стеке. Перед вызовом какой-либо смысловой команды, например, вызова функции, в стек кладутся ее параметры. Это очень похоже на ассемблер, однако в отличие от распространенных конвенций вызова stdcall и cdecl, переменные кладутся в том порядке, в котором они определены в прототипе функции. За установку переменных в стек отвечают команды семейства ld…. Например, ldloc загружает локальную переменную по ее имени или порядковому номеру, ldstr предназначена для загрузки строки. Обратную операцию осуществляют команды st… (store), забирающие значение с вершины стека в локальную переменную. Очисткой стека занимается сама вызываемая функция, – после чего на вершину стека она кладет возвращаемое значение.

После объявления переменных (конструкция init() предназначена для инициализации переменных значениями по умолчанию) производится вызов функции Write. Для этого в стек кладется строка с сообщением, которое будет выведено на экран. Далее используется команда call, вызывающая статический метод Write из класса System.Console сборки mscorlib. Обрати внимание: при вызове указывается прототип функции, – чтобы компилятор мог определить, какую из перегрузок использовать. При вызове нестатических методов перед параметрами в стек нужно положить ссылку на экземпляр класса, а при вызове после call нужно поставить ключевое слово instance. Для вызова виртуальных методов применяй callvirt. Далее вызывается функция ReadLine, и строка парсится в число, которое сохраняется в первой переменной. Перед вызовом функции вывода на экран результатов переменные в стеке подвергаются преобразованию в ссылочные типы (обертки) с помощью команды box32. Остальное должно быть ясно без пояснений.

Еще один важный момент для реверсера – это отлов ветвлений. В IL ветвления создаются с использованием семейства команд b… (от branch). За детализацией беги на MSDN.

Попробуй сломай меня

Уже чувствуешь себя крутым? Тогда пора что-нибудь да поломать. Идем на любой сайт с челенджами и скачиваем задание из раздела cracking в стиле .NET. Я тебе покажу на одном из них, как все просто и красиво. Из этических соображений раскрывать ресурс я не буду, но если интересно, можешь выяснить и сам, посмотрев внимательно внутрь бинарника.

Приложение app14 содержит на форме текстбокс и кнопку. В текстбокс нужно ввести правильную фразу, тогда обработчик кнопки выведет кодовое слово, которое и является решением. При неправильной фразе выводится какая-то лабуда.

Забыл сказать: для редактирования IL очень удобно использовать утилу Red Gate’s .NET Reflector с установленным плагином Reflexil. Утила хороша тем, что на основе IL, восстанавливает код на некоторых языках высокого уровня, включая шарп. Реверсинг превращается в увлекательное занятие анализа исходных кодов.

Итак, загружаем в Reflector нашего подопытного и смотрим саммари по классам и методам. Видим два класса – Encrypt и, собственно, класс формы под названием goes. Можно вкуривать в реализацию Encrypt – она несложная, но я предпочел посмотреть, что же происходит при нажатии заветной кнопки. Обработчик ищется элементарно, по прототипу функции. Она должна принимать два параметра: первый типа object и второй типа EventArgs. Таких функций всего две и по названию очевидно, что нам интересна функция vla_Click. Выбираем по правой кнопке меню Disassemble и видим чистый c#!

Код обработчика нажатия кнопки в CrackMe

private void vla_Click(object sender, EventArgs e)
{
int[] numArray = new int[12];
VariantType type = (VariantType) Conversions.ToInteger(MyProject.Computer.Registry.GetValue(@"HKEY_CURRENT_USER\valid", "", 0));
decimal num = new decimal(((((double) type) + 1.5) * ((double) type)) + 0.025);
if (this.txt.Text == "somerandomvl")
{
Interaction.MsgBox(RuntimeHelpers.GetObjectValue(Encrypt.ParseandDecrypt(Conversions.ToString(Encrypt.ParseandEncrypt("ydXX!if not txt is blah youfailed")))), MsgBoxStyle.Exclamation, "585mfg9gf");
}
else
{
Interaction.MsgBox(RuntimeHelpers.GetObjectValue(Encrypt.ParseandDecrypt("ydXX!if not txt is blah youfailed")), MsgBoxStyle.Exclamation, "585mfg9gf");
}
if (Convert.ToDouble(num) == (((((double) type) + 1.5) * ((double) type)) + 0.025))
{
if ((type == VariantType.Empty) | (MyProject.Application.Info.DirectoryPath.Length.ToString().Length == 0x1ca))
{
Interaction.MsgBox("nope", MsgBoxStyle.Information, "sorry");
}
else if ((((this.txt.Text != this.txt.Text) & false) | false) | (type == VariantType.Null))
{
this.txt.Text = Conversions.ToString(Encrypt.ParseandDecrypt("fm`{f}kpwrn"));
}
}
}

Выполняется куча каких-то проверок и сравнений, которым надо удовлетворить. Мое же внимание привлекла последняя инструкция this.txt.Text = Conversions.ToString(Encrypt.ParseandDecrypt("fm`{f}kpwrn"));.

Ну да, при правильном раскладе в текстбокс попадает значение, представляющее собой расшифрованную строку “fm`{f}kpwrn”. Собственно алгоритм расшифровывания реализуется функцией ParseandDecrypt из класса Encrypt. Дальше можно либо разбираться в алгоритме (зачем оно нам надо?), либо скопировать код функции и скомпилировать собственный проект, либо просто сделать один верный прыжок.

Запускаем reflexil из меню Tools и видим IL-код. Нужно идентифицировать место последнего if и прыгнуть с него на выявленную команду. В IL нет операторов ветвления, поэтому они реализуются в виде последовательности простых команд сравнения и прыжков. Здесь все как в ассемблере. Итак, ищем последний переход перед выводом MessageBox с сообщением nope.

Локализация перехода

IL_00f2: ldc.i4 0x1ca
IL_00f7: ceq
IL_00f9: or
IL_00fa: brfalse.s IL_0110

IL_00fc: ldstr "nope"
IL_0101: ldc.i4.s 64
IL_0103: ldstr "sorry"
IL_0108: call valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxResult [Microsoft.VisualBasic]Microsoft.VisualBasic.Interaction::MsgBox(object, valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxStyle, object)

Меняем brfalse на brtrue, чтобы прыжок совершить, ну а чтобы избежать проверки else if, меняем адрес IL_0110 на IL_013d.

Вывод решения в текстбоксе формы

IL_013d: ldarg.0
IL_013e: callvirt instance class [System.Windows.Forms]System.Windows.Forms.TextBox app14.goes::get_txt()
IL_0143: ldstr "fm`{f}kpwrn"
IL_0148: call object app14.Encrypt::ParseandDecrypt(string)
IL_014d: call string [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Conversions::ToString(object)
IL_0152: callvirt instance void [System.Windows.Forms]System.Windows.Forms.TextBox::set_Text(string)
IL_0157: ret

В левой панели кликаем на название бинарника, затем нажимаем кнопку Save as и сохраняем его под именем app14_patched. Запускаем, вводим любую фразу и в текстбоксе получаем кодовое слово ihatethereg!!!

Нам не страшен серый волк

Неужели все действительно так просто? На самом деле и да, и нет. Если мы (и еще туева хуча людей) знаем о возможности восстановления кода, об этом должны были позаботиться в Microsoft. Да и другие конторы не прочь предоставить решения по защите от анализа и реверса. В целом так и есть, только решения эти далеко не всегда обеспечивают реальную защиту.

Начнем с обфускации. В поставку Visual Studio входит утила под названием Dotfuscator. Ее задача – усложнить работу реверсера за счет изменения имен классов, методов и переменных, а также шифрования строк. Она так и делает, но анализ кода от этого не усложняется. Ну да, теперь методы называются типа a, b… Тот же обработчик мы легко идентифицируем по определению делегата, а шифрованные строки все равно будут расшифрованы. Так что, ни хрена это не защита.

Второй метод связан со статическим шифрованием всей сборки. В бинарник вставляется нативный код, который расшифровывает сборку при загрузке приложения и защищает ее от статического анализа. Решений таких – ну просто очень много. Одно из самых известных – CodeVeil от XHEO. Реально крутая тулза! Попробуем зашифровать сборку и открыть ее в Reflector. Оба-на, ошибка: «Module … does not contain CLI header». Reflector, ILDASM и другие утилы даже не идентифицируют ее как .NET-сборку. Между прочим, в Professional-версии утила стоит 1200 зеленых рублей, а лозунг ее – «Don’t just confuse hackers. Stop them». Впечатляет и вдохновляет!

Ломается такая защита в два счета. Давай подумаем. Если сборка расшифровывается в памяти перед передачей управления CLR, значит, нужно просто снять дамп памяти. Загружаем «защищенную» по самые никуда сборку в память и цепляемся к ней WinHex’ом (Tools -> Open Ram; далее выбираем процесс и модуль в нем). Копируем в файл содержимое памяти: CTRL+A, затем Edit -> Copy All -> Into New File и сохраняем под именем app14_unveiled.exe. Пытаемся запустить и ловим ошибку инициализации. Видимо, где-то поползли секции. Открываем в Reflector и, о чудо, код как на ладони; правда, не работает Reflexil, вылетая с исключением. Запускаем ILDASM, делаем дамп IL и компилируем… – получаем новую полностью работоспособную сборку со снятой защитой. Во как!

Справедливости ради стоит отметить, что в текущей версии CodeVeil 3.2, по заявлению разработчиков, реализована динамическая защита во время исполнения, что сильно затрудняет задачу. Необходимо детально исследовать нативный код шифровщика. Мне кажется, что создание unpacker’а – лишь вопрос времени. Пока никто эту задачу не решил, так что дерзай, возможно, получится у тебя.

Таким образом, программисту приходится полагаться только на самого себя. Можно шифровать сборки, динамически расшифровывать их во время исполнения и подгружать с помощью Application.Load. В общем, все как в старом добром Assembler’е. А можно воспользоваться виртуальными контейнерами типа ThinApp или Xenocode. Ну а лучше всего и вовсе реализовывать критические участки с помощью неуправляемого кода.

Из России с любовью

Помни, что только практика способствует твоему развитию. Как видишь, все не так уж сложно, но, заходя на сайт глобального рейтинга www.wechall.net, я с грустью обнаруживаю Россию на 25-м месте. В рейтинге зарегистрировано всего 5 российских хакеров-участников! Давай поддержим нашу страну: регистрируйся на сайтах-челленджах и бросай баллы в копилку Родины.

Все описанные действия совершены под музыку Джо Кокера. Слушай блюз и будь счастлив!

DVD

На диске ты найдешь все упомянутые в статье тулзы, официальное описание CIL от Microsoft, а также препарируемый CrackMe и его пропатченную версию.

WWW

Для описания команд IL бегом на MSDN: http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes_fields.aspx

Интересуют средства защиты .NET-приложений? Тогда тебе сюда:

INFO

Наилучшую защиту своих сборок от анализа ты получишь в случае использования Xenocode PostBuild (http://www.xenocode.com/products/postbuild-for-net), позволяющего компилировать приложение в нативный код со статическим включением сторонних сборок.

Содержание