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

Раскрываем код

Крис Касперски

Хакер, номер #113, стр. 113-068-1

Дизассемблирование C# программ от A до Z

Победоносное шествие .NET-платформы по миру продолжается, а вменяемых руководств по взлому что-то не видно. Сегодня я расскажу, как дизассемблировать Visual Basic/C# сборки, патчить в hiew'е, вести отладку на уровне байт-кода. Сейчас мы заправим мозговые баки свежей порцией авиационного керосина, выведем двигатели на взлетный режим и дадим хороший старт, написав свой собственный crackme и тут же взломав его всеми доступными средствами.

Технология .NET готовится праздновать свой юбилей. За минувшее время было написано множество коммерческих программ (и малвари в том числе), но как только дело доходит до того, чтобы заглянуть внутрь p-кода на предмет «отломать» пару ненужных байт, выясняется, что достойных хакерских инструментов нет. И, судя по всему, не появится. Поэтому приходится использовать, что есть, хакерствуя в весьма стесненных обстоятельствах, словно шахтеры в забое!

Что нам понадобится

Итак, сейчас я перечислю хакерский арсенал, который нам понадобится в нелегком ремесле.

1. Visual Studio 2008 Express. Последняя версия знаменитой «студии» от MS, распространяющаяся бесплатно и включающая в себя все необходимое для полноценной работы: трансляторы Visual Basic, C#, а также новый компилятор Cи++, способный транслировать Си++ программы в байт-код. На W2K устанавливаться категорически отказалась, а на Server 2003 встала с полпинка (off-line install занимает 894 Мбайт в .iso-формате): http://www.microsoft.com/express/download/.

2. Mono 2.0 Beta for Windows. Альтернативная реализация .NET от компании Novell, поддерживающая различные версии Linux и Windows, нормально встала на W2K, но потребовала MS Framework. Кроме того комплект поставки неполный (в частности, нет дизассемблера), а откомпилированные сборки жутко тормозят, зато дистрибутив занимает 71 Мбайт: http://www.go-mono.com/mono-downloads/download.html.

3. Standard ECMA-335/Common Language Infrastructure (CLI)/4th edition (June 2006). Полная версия европейского стандарта, описывающая архитектуру виртуальной машины, байт-код, структуру ассемблерных сборок, в общем, солидный 556-страничный документ из серии «маст хэв» (на английском языке, в формате pdf): www.ecma-international.org/publications/standards/Ecma-335.htm. А вот прямая ссылка: www.ecma-international.org/publications/files/ECMA-ST/Ecma-335.pdf.

4. CIL Instruction Set. Краткий справочник оп-кодов с «C# Online.NET free encyclopedia» – жутко неполный, но удобный и не содержащий воды (на английском языке): http://en.csharp-online.net/CIL_Instruction_Set.

5. Введение в MSIL. Цикл статей от Кенни Керр в переводе Aquila (одного из основателей WASM). Представляет собой подробное описание архитектуры виртуальной машины и концепций языка C#, рекомендуемое к прочтению всеми начинающими хакерами (на русском языке): www.wasm.ru/series.php?sid=22.

6. Common Intermediate Language. Поверхностное описание CIL-языка (общего для всех языков платформы .NET) на Wikipedia со ссылками на кучу полезных ресурсов (на английском языке): http://en.wikipedia.org/wiki/Common_Intermediate_Language.

7,8,9,10... Прочие примочки и описания. Полный листинг ищи на нашем DVD.

Пишем свой первый crackme

Глубоководное погружение в байт-код .NET-программ начинается! Пристегиваем ремни, проверяя все ли на месте, и запускаем Microsoft Visual Studio или FAR + Colorer. Работать в IDE, конечно, удобнее (особенно начинающим, ведь среда автоматически отображает список методов для каждого класса – не надо постоянно лазить в справочники), однако это порочный путь, абстрагирующий нас от машины, и мы – хакеры старого поколения – предпочитаем консольные текстовые редакторы с подсветкой синтаксиса или даже без таковой.

Текст нашего первого crackme, написанного на C#, в простейшем случае выглядит так. Это консольная программа, вручную набранная в текстовом редакторе. IDE пихает сюда много лишнего, затрудняющего понимание, но на скомпилированном коде это никак не отражается.

Исходный текст программы n2k_crackme_01h.cs

using System; // использовать классы основной системной библиотеки

class nezumi // имя класса - произвольно и может быть любым

{

static void Main()

{

string s; // объявляем переменную s типа строка

// запрашиваем у пользователя пароль

System.Console.Write("enter password:");

s = System.Console.ReadLine();

if (s == "nezumi") // сравниваем введенный пароль с эталонным

System.Console.WriteLine("hello, master!");

else

System.Console.WriteLine("fuck you, hacker!");

}

}

В среде IDE сборка .NET-программ осуществляется клавишей <F6>, а из командой строки (при этом csc.exe должен находиться в путях, чего не происходит при установке по умолчанию):

$ csc.exe n2k_crackme_01h.cs

В Mono вместо csc.exe используется файл mcs/mcs.bat, но, независимо от способа сборки, мы получаем n2k_crackme_01h.exe, готовый к запуску. После чего нас спросят пароль и, если мы введем его неверно – пошлют.

Компиляция осуществляется путем указания ключа /CLR в командной строке компилятора CL.EXE (рядом с которым можно указать ключ /Ox для форсирования максимальной оптимизации):

$cl.exe /clr hello-clr.cpp

Полученный файл hello-clr.exe представляет собой смесь управляемого байт-кода с большим количеством вызовов неуправляемого машинного кода из различных библиотек – плюс, тянет за собой RTL, написанную на байт-коде. Это позволяет сочетать высокую скорость выполнения с управляемостью и безопасностью. Впрочем, «чистые» C# сборки все-таки выигрывают как в размерах, так и в скорости. Чуть позже ты убедишься в этом самостоятельно, а пока же поверь мне на слово.

Первые эксперименты

Загружаем подопытный n2k_crackme_01h.exe в HIEW, дважды давим <ENTER> для перевода редактора в дизассемблерный режим, жмем <F5> и попадаем в точку входа, где красуется команда jmp _CorExeMain ; mscoree.dll. Это весь машинный код, какой только есть.

Дальнейшее расследование показывает, что jmp находится в самом конце секции .text, за которой располагаются секции ресурсов и перемещаемых элементов, а выше — байт-код виртуальной машины. Просматривая его в hex-mode, мы обнаружим все текстовые строки (и пароль в том числе!) в формате Unicode, причем, перед строкой находится байт, определяющий длину строки. Узнав оригинальный пароль мы, конечно, без труда смогли бы «взломать» crackme – однако редкая программа хранит пароли открытым текстом, да и неинтересно это.

Лучше загрузим в HIEW другой исполняемый файл, написанный на Си++ – hello-clr.exe. Он так же вызывает CorExeMain. В отличие от «чистой» .NET сборки, написанной на C#, здесь присутствует большое количество «переходников» к машинному коду – функциям strcmp, gets, printf, напрямую вызываемым из библиотеки MSVCR90.DLL. За это отвечает механизм P/Invoke, позволяющий создавать «гибридные» программы, часть из которых транслируется в машинный код, а часть – в интерпретируемый байт-код, что серьезно затрудняет взлом. Не волнуйся, когда мы возьмемся за дело, никакой P/Invoke нас не остановит! А пока загрузим .NET-сборку в нормальный дизассемблер.

Техника дизассемблирования

Загружаем n2k_crackme_01h.exe в IDA Pro и видим, что ничего ужасного в CIL-коде нет. Напоминает байт-код виртуальной Java-машины. IDA Pro не только создает перекрестные ссылки, но и показывает опкоды, и расставляет комментарии к командам, избавляя от необходимости каждый раз заглядывать в справочник (ECMA-335/Partition III/CIL Instruction Set).

Чтобы дизассемблер был дружелюбнее к хакеру, необходимо выполнить следующие действия. В меню «Options» выбрать пункт «Text representation». Там указать количество байт для отображения опкода («Number of opcode bytes») – шести вполне хватит. В разделе «Line prefixes» нужно сбросить все галочки, кроме «Function offsets», после чего вновь возвратиться в меню «Options», зайти в «Comments» и взвести «Display auto comments» для автоматического отображения комментариев ко всем инструкциям (впрочем, при наличии некоторого опыта работы с CIL-кодом это можно и не делать).

Поклонники графической версии IDA Pro могут задействовать графы, упрощающие (на самом деле – усложняющие) понимание структуры программы. Тут уж, как говорится, на вкус и цвет все фломастеры разные. Лично я никогда не пользовался графами и другим не советую.

А теперь посмотрим, на что способен штатный дизассемблер от Microsoft, по умолчанию расположенный в каталоге C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\. Зовется он ilasm.exe. Загружаем в него n2k_crackme_01h.exe.

Хм, в общем-то, довольно неплохая картина получилась, а навигация по классам выполнена даже лучше, чем в IDA Pro, причем намного лучше (примечание: чтобы ildasm.exe отображал опкоды инструкций, необходимо в меню View взвести галочку «Show bytes»).

Теперь самое время исследовать дизассемблерный текст нашей программы. Тут все ясно без травы и даже без комментариев (особенно тем, кто знаком с виртуальными машинами со стековой организацией). Если опустить детали, получается следующее: программа вызывает System.Console::Write(«enter password»), после чего считывает строку в переменную V_0 и вызывает функцию System.String::Equality(V_0, «nezumi») для проверки строк на соответствие. В случае их несовпадения на экран выводится строка «fuck off, hacker!», управление на которую передается машинной командой brtrue.s IL_0031 (с опкодом 2Dh 0Dh). Получившуюся выкладку ищи на диске.

Чтобы заставить программу воспринимать все пароли как правильные, двухбайтовый условный переход brtrue.s IL_0031 необходимо заменить на пару однобайтовых команд nop (опкод – 00h, а вовсе не 90h, как на x86). Или же… заменить brtrue.s IL_0031 на brFALSE.s IL_0031. Тогда любой неправильный пароль будет восприниматься как правильный и, соответственно, наоборот. Открыв ECMA-335, мы узнаем, что инструкция brfalse.s имеет опкод 2Ch – это все, что нам необходимо знать для взлома программы.

Техника патча

Так, где наш HIEW? Готов ко взлому? Как нам определить местоположение байта, который мы собрались захачить? Ведь виртуальные адреса в контексте CIL-кода неуместны!

Воспользуемся дедовским способом и поищем последовательность байт (сигнатуру), обитающую в окрестностях целевой команды. В нашем случае это может быть 2Dh 0Dh 72h 2F 00h 00h 70h 28h (об обратном порядке байт не забываем, да? Ildasm автоматически «нормализует» аргументы команд, IDA Pro – нет, показывая их такими, какие они есть: наименее значимый байт располагается по младшему адресу). Короче, вбиваем заданную последовательность в поиск и, убедившись, что это вхождение – единственное, переводим HIEW в режим записи по <F3>. Заменяем 2Dh на 2Ch, сохраняем изменения в файле по <F9> и выходим.

Запускаем хакнутый файл и... о чудо! Он работает! Теперь любой, наугад взятый пароль, например, «123456» воспринимается как правильный. Конечно, если программа снабжена цифровым сертификатом подлинности или использует механизм контроля целостности собственного кода, этот номер не пройдет, но ведь взлом надо же с чего-то начинать!

Техника отладки

Спору нет, дизассемблер – весьма популярный инструмент для исследования программ. Популярный, но не единственный. Во многих случаях отладчик оказывается намного более предпочтительным. Вместо того чтобы гадать какое значение имеет переменная в данной точке (в дизассемблере), гораздо практичнее заглянуть в нее отладчиком.

И вот тут выясняется любопытная вещь. На уровне исходных текстов Microsoft Visual Studio справляется с отладкой на ура, но готовые бинарные сборки, увы, не поддерживает. Для работы с ними необходимо использовать ICorDebug-интерфейс, встроенный в ядро платформы .NET и реализующий базовые отладочные возможности (установка точек останова, пошаговое исполнение и т.д.), предоставляя их в виде набора API-функций. Все .NET-отладчики, которые я видел, являются достаточно тонкими обертками вокруг ICorDebug Interface и наследуют его худшие черты, а именно – невозможность отлаживать программы без символьной (отладочной) информации, автоматически удаляемой из всех Release-проектов. Выходит, что мы можем отлаживать только свои собственные программы?! Нехорошо!

Расследование показало, что штатному .NET отладчику (зовущемуся mdbg.exe, где «m» – сокращение от managed, то есть управляемый код) для нормальной работы вполне достаточно pdb-файла. Вот только как этот файл получить? IDA Pro может подготовить map-файл, но готовых конверторов map2pdb в Сети что-то не наблюдается, а писать самому — лениво и непродуктивно.

К счастью, существует весьма простой и элегантный путь. Дизассемблируем бинарную сборку штатной утилитой ildasm.exe, после чего ассемблируем ее заново штатным же транслятором ilasm.exe, не забыв указать «волшебный» ключик /pdb для генерации отладочной информации. Поскольку ildasm.exe поддерживает ресурсы и корректно их дампит, то предложенный способ работает в подавляющем большинстве случаев, что мы сейчас и продемонстрируем.

Запускаем ildasm.exe с настройками по умолчанию, загружаем в него n2k_crackme_01h.exe (оригинальный, а не хакнутый). В меню File находим пункт Dump (или нажимаем <CTRL-D>), в появившемся окне «Dump options» (смотри рисунок) оставляем все галочки в состоянии по умолчанию. Главное, чтобы была взведена галочка «Dump IL Code»! После нажимаем <OK> и вводим имя файла для дампа (например, «cracked»).

По окончании дизассемблирования на диске образуются два файла – cracked.il с ассемблерным текстом программы и cracked.res с ресурсами. Cracked.il представляет собой обыкновенный текстовой файл, который можно править в любом текстовом редакторе, при необходимости заменяя «brtrue.s IL_0031» на «brfalse.s IL_0031». Но сейчас нас в первую очередь интересует не патч, а отладка.

Берем штатный ассемблер и собираем файл следующим образом:

$ilasm.exe cracked.il /pdb

На диске образуются файлы cracked.exe и cracked.pdb, готовые к загрузке в отладчик. Что примечательно – отладочная информация непосредственно в сам исполняемый файл не записывается. Это очень хорошо, иначе нам пришлось бы потом оттирать ее оттуда или мириться с увеличением размера поломанного exe, что вряд ли входит в наши планы.

ОК, набираем в командной строке «$mdbg.exe cracked.exe» и… оказываемся в консольном окне отладчика, автоматически останавливающегося на первой команде функции Main (передает нам бразды правления). А что такого крутого и хорошего мы можем сделать?

Начнем с просмотра окрестной, за что отвечает команда «show» или ее более короткий алиас «sh». Результат выглядит так:

Результат работы команды «sh», показывающей IL-код

run cracked.exe # запущена бинарная сборка cracked.exe

STOP: Breakpoint Hit # точка останова в функции main

43: IL_0000: nop # следующая выполняемая команда

[p#:0, t#:0] mdbg> sh # просим отладчик показать окрестности кода по "sh"

40 .maxstack 2

41 .locals init (string V_0, bool V_1)

43:* IL_0000: nop # команда выполняемая следующей

44 IL_0001: ldstr "enter password:"

45 IL_0006: call [mscorlib]System.Console::Write(string)

[p#:0, t#:0] mdbg>

Остальные команды отладчика можно найти во встроенной справке (вызываемой командой help) или же в одноименной врезке. Сейчас нас интересует техника работы с отладчиком. Ну, техника, как техника. Никаких принципиальных отличий от x86 не появилось.

При просмотре ассемблерного файла cracked.il находим команду «IL_0020: stloc.1», которая стягивает со стека результат сравнения двух строк, возвращенный функцией System.String::op_Equality. За ней следует команда «IL_0021: ldloc.1», загружающая полученное значение в локальную переменную V_1, в зависимости от содержимого которой команда «IL_0022: brtrue.s IL_0031» прыгает на метку IL_0031 (неверный пароль). Или… не прыгает. Все ясно! Нам нужно установить точку останова на команде «IL_0020: stloc.1», расположенной в 55'ой строке файла cracked.il, а дальше мы уже сориентируется. Весь сеанс работы с отладчиком mdbg.exe ищи на DVD в полномасштабной статье.

Если кому-то родители запрещают использовать консоль, что ж, к его услугам Dotnet IL Editor – бесплатный IL-отладчик с GUI-интерфейсом, однако mdbg.exe мне как-то больше по душе (к тому же под него расширения всякие можно писать).

Впрочем, выбор отладчика непринципиален. Важна сама техника исследования .NET-программ, которую мы только что и продемонстрировали.

Заключение

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

А пока имеет смысл потренироваться на простых, несильно защищенных коммерческих программах (которые можно найти в Сети), малвари (взятой оттуда же) и crackme, залежи которых находятся на сайте www.crackmes.de (где даже есть специальный раздел, посвященный исключительно платформе .NET).

Я надеюсь, что эта статья обеспечит хороший старт, а остальное — дело времени, техники и бесчисленных экспериментов!

Основные команды отладчика mdbg.exe

  • help: вывод встроенной справки, «help команда» – подробная справка по команде;
  • a[ttach]: подключение к активному .NET-процессу;
  • b[reak]: установка точки останова или отображение уже существующих;
  • ca[tch]: просмотр событий (evens), вызывающих останов отладчика;
  • conf[ig]: просмотр опций отладки/конфигурирования отладчика;
  • del[ete]: удаление точки останова;
  • de[tach]: отключение от отлаживаемого .NET-процесса;
  • g[o]: продолжение работы программы;
  • n[ext]: Step Over;
  • o[ut]: Steps Out;
  • s[tep]: Step Into;
  • p[rint]: вывод содержимого локальных переменных;
  • q[uit]: выход из программы;
  • r[un]: запуск программы под отладчиком;
  • set: изменение значение переменных;
  • setip: установка текущей выполняемой команды;
  • sh[ow]: показ окрестностей выполняемого кода.

Терминологическое болото

  • .NET: древняя, как мир, идея 2х стадийной компиляции; сначала программа (написанная хоть на Java, хоть на Visual Basic, хоть на Cи++, хоть на C#, хоть на F#) транслируется в промежуточный байт-код, который окончательно транслируется в «родной» двоичный код на конкретной целевой машине или же исполняется в режиме интерпретации;
  • CLR: Common Language Runtime («общая среда выполнения языков») – компонент Microsoft .NET Framework, включающий себя виртуальную машину и необходимые библиотеки;
  • CIL: Common Language Infrastructure – «спецификация общеязыковой инфраструктуры», определяющая архитектуру исполнительной системы, базовые классы, синтаксис и мнемонику байт-кода;
  • MSIL: Microsoft Intermediate Language («промежуточный язык от Microsoft») – байт-код виртуальной .NET машины в реализации от Microsoft;
  • IL: Intermediate Language («промежуточный язык») – байт-код виртуальной .NET машины, стандартизованный в рамках ECMA-335.
Содержание