Автор: (c)Крис Касперски ака мыщъх
Победоносное шествие .NET-платформы по миру все продолжается и продолжается, а вменяемых руководств по взлому как-то не наблюдается... Сегодня мы расскажем, как дизассемблировать Visual Basic/C# сборки (включая Cи++ программы, откомпилированные в байт-код), как их патчить в hiew'е, вести отладку на уровне байт-кода, словом - заправим мозговые баки свежей порцией авиационного керосина, выведем двигатели на взлетный режим и дадим хороший старт, написав свой собственный crackme и тут же взломав его всеми доступными средствами.
Технология .NET готовится отпраздновать свой юбилей. За это время было написано множество коммерческих программ (и малвари в том числе), но как только дело доходит до того, чтобы заглянуть внутрь p-кода на предмет "отломать" пару ненужных байт, выясняется, что достойных хакерских инструментов нет, и судя по всему, не появится, поэтому приходится использовать то, что есть, хакерствуя в весьма стесненных обстоятельствах, словно шахтеры в забое!
Стандартное отречение: вся информация, предоставленная ниже, преследует исключительно благие цели (анализ вредоносного программного обеспечения, например) и мыщъх не несет никакой ответственности (ни явной, ни предполагаемой) за любой возможный ущерб или потерянную выгоду от ее использования.
Рисунок 1. Отсюда можно бесплатно скачать последнюю версию Microsoft Visual Studio.
Рисунок 2. Единственная (на данный момент) реализация платформы .NET вне Microsoft.
Глубоководное погружение в байт-код .NET-программ начинается! Пристегиваем ремни, проверяя - все ли на месте и... запускаем Microsoft Visual Studio или... FAR + Colorer. Работать в IDE, конечно, удобнее (особенно начинающим, когда среда автоматически отображает список методов для каждого класса - не надо постоянно лазить в справочники), однако это порочный путь, абстрагирующий нас от машины и мы - хакеры старого поколения - предпочитаем консольные текстовые редакторы с подсветкой синтаксиса или... даже без таковой.
Текст нашего первого crackme, написанного на C#, (который мы будем ломать всеми силами) в простейшем случае выглядит так (см. листинг 1). Это консольная программа, вручную набранная в текстовом редакторе. IDE пихает сюда много лишнего, затрудняющего понимание, однако на скомпилированном коде это никак не отражается.
Листинг 1. Исходный текст программы n2k_crackme_01h.cs.
В среде IDE сборка .NET-программ осуществляется клавишей <F6>, а из командой строки (при этом csc.exe должен находится в путях, чего не происходит при установке по умолчанию):
$csc.exe n2k_crackme_01h.cs
Листинг 2. Компиляция C#-программы из командной строки.
В Mono вместо csc.exe используется файл mcs/mcs.bat, но независимо от способа сборки, мы получаем n2k_crackme_01h.exe, готовый к непосредственному запуску, после которого нас спросят пароль и если мы введем его неверно - пошлют на хрен.
Компилятор Cи++, входящий в состав Microsoft Visual Studio 2008, умеет транслировать программы не только в машинный, но и в байт-код, позволяя нам использовать все прелести .NET платформы из привычных плюсов (трансляция "чистых" Си программ в байт-код все еще не поддерживается).
Листинг 3. Пример простейшей Cи++ программы, написанной с учетом специфики .NET.
Компиляция осуществляется путем указания ключа /CLR в командной строке компилятора CL.EXE (рядом с которым можно указать ключ /Ox для форсирования максимальной оптимизации):
$cl.exe /clr hello-clr.cpp
Листинг 4. Трансляция Си++ программы в .NET сборку из командной строки.
Полученный файл hello-clr.exe представляет собой смесь управляемого байт-кода с большим количеством вызовов неуправляемого машинного кода из различных библиотек (плюс тянет за собой RTL, написанную на байт-коде), что позволяет сочетать достаточно высокую скорость выполнения с управляемостью и безопасностью, однако "чистые" C# сборки все-таки выигрывают как в размерах, так и в скорости. Чуть позже мы убедимся в этом самостоятельно, а пока же поверим мыщъх'у на слово.
Загружаем подопытный n2k_crackme_01h.exe в HIEW, дважды давим на <ENTER> для перевода редактора в дизассемблерный режим, жмем <F5> и попадаем в точку входа, где красуется команда jmp _CorExeMain ; mscoree.dll (см. рис. 3). Это и есть весь машинный код, который только есть (простите за каламбур).
Рисунок 3. Так выглядит "честная" .NET сборка в hex-редакторе.
Дальнейшее расследование показывает, что jmp находится в самом конце секции .text, за которой располагаются секции ресурсов и перемещаемых элементов, а выше - байт-код виртуальной машины, просматривая который в hex-mode, мы обнаружим все текстовые строки (и пароль в том числе!) записанные в формате Unicode, причем перед строкой находится байт, определяющий длину строки. Узнав оригинальный пароль, мы, конечно, без труда смогли бы "взломать" crackme, однако редкая программа хранит пароли открытым текстом, да и неинтересно это.
Лучше загрузим в HIEW другой исполняемый файл, написанный на Си++ - hello-clr.exe. Он также вызывает CorExeMain (см. рис. 4), но в отличие от "чистой" .NET сборки, написанной на C#, здесь присутствует большое количество "переходников" к машинному коду - функциям strcmp, gets, printf, напрямую вызываемых из библиотеки MSVCR90.DLL, за что отвечает механизм P/Invoke, позволяющий создавать "гибридные" программы, часть из которых транслируется в "живой" машинный код, а часть - в интерпретируемый байт-код, что серьезно затрудняет взлом, однако никакой P/Invoke нас не остановит! Но это будет потом, а сейчас мы покурим и загрузим .NET-сборку в нормальный дизассемблер.
Рисунок 4. Внешний вид .NET-сборки, полученной путем трансляции Си++ программы.
Загружаем n2k_crackme_01h.exe в IDA Pro и видим (см. рис. 5), что ничего ужасного в CIL-коде нет. Напоминает байт-код виртуальной Java-машины. IDA Pro не только создает перекрестные ссылки, но даже показывает опкоды и расставляет комментарии к командам, чтобы не было нужды каждый раз заглядывать в справочник (ECMA-335/Partition III/CIL Instruction Set).
Рисунок 5. Консольная версия IDA Pro дизассемблирует .NET сборку.
Впрочем, чтобы заставить дизассемблер быть более дружелюбным к хакеру, необходимо выполнить следующие действия: в меню "Options" выбрать пункт "Text representation", там указать количество байт для отображения опкода ("Number of opcode bytes") - шести хватит вполне, а в разделе "Line prefixes" сбросить все галочки, кроме "Function offsets", после чего вновь возвратиться в меню "Options", зайти в "Comments" и взвести "Display auto comments" для автоматического отображения комментариев ко всем инструкциям (впрочем, при наличии некоторого опыта работы с CIL-кодом этого можно и не делать).
Поклонники графической версии IDA Pro могут задействовать графы (см. рис. 6), упрощающие (на самом деле - усложняющие) понимание структуры программы, но тут уж как говорится - на вкус и цвет все фломастеры разные. Лично мыщъх никогда не пользовался графами и другим не советует.
Рисунок 6. Графическая версия IDA Pro дизассемблирует .NET сборку.
А теперь посмотрим, на что способен штатный дизассемблер от Microsoft, по умолчанию расположенный в каталоге C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\ и зовущийся ilasm.exe. Загружаем в него n2k_crackme_01h.exe и... хм, в общем-то, довольно неплохая картина получилась (см. рис. 7), а навигация по классам выполнена даже лучше, чем в IDA Pro, причем намного лучше (примечание: чтобы ildasm.exe отображал опкоды инструкций, необходимо в меню View взвести галочку "Show bytes")
Рисунок 7. Штатный дизассемблер ildasm.exe за работой.
Теперь самое время исследовать дизассемблерный текст нашей программы (см. листинг 5). Ну, тут все ясно без травы и даже без комментариев (особенно тем, кто знаком с виртуальными машинами со стековой организацией). Если опустить детали, то получается следующее: программа вызывает System.Console::Write("enter password"), после чего считывает строку в переменную V_0 и вызывает функцию System.String::Equality(V_0, "nezumi") для провеки строк на соответствие и в случае их несовпадения на экран выводится строка "fuck off, hacker!", управление на которую передается машинной командой brtrue.s IL_0031 (с опкодом 2Dh 0Dh).
.method private hidebysig static void Main(string[] args) cil managed // SIG: 00 01 01 1D 0E { .entrypoint // Method begins at RVA 0x2050 // Code size 61 (0x3d) .maxstack 2 .locals init (string V_0, bool V_1) IL_00: /* 00 | */ nop IL_01: /* 72 | (70)000001 */ ldstr "enter password:" IL_06: /* 28 | (0A)000003 */ call void [mscorlib]System.Console::Write(string) IL_0b: /* 00 | */ nop IL_0c: /* 28 | (0A)000004 */ call string [mscorlib]System.Console::ReadLine() IL_11: /* 0A | */ stloc.0 IL_12: /* 06 | */ ldloc.0 IL_13: /* 72 | (70)000021 */ ldstr "nezumi" IL_18: /* 28 | (0A)000005 */ call bool [mscorlib]System.String::Equality(str,str) IL_1d: /* 16 | */ ldc.i4.0 IL_1e: /* FE01| */ ceq IL_20: /* 0B | */ stloc.1 IL_21: /* 07 | */ ldloc.1 IL_22: /* 2D | 0D */ brtrue.s IL_0031 IL_24: /* 72 | (70)00002F */ ldstr "hello, master!" IL_29: /* 28 | (0A)000006 */ call [mscorlib]System.Console::WriteLine(str) IL_2e: /* 00 | */ nop IL_2f: /* 2B | 0B */ br.s IL_003c IL_31: /* 72 | (70)00004D */ ldstr "fuck off, hacker!" IL_36: /* 28 | (0A)000006 */ call [mscorlib]System.Console::WriteLine(str) IL_3b: /* 00 | */ nop IL_3c: /* 2A | */ ret } // end of method nezumi::Main
Листинг 5. Результат работы штатного дизассемблера ildasm.exe.
Логично - чтобы заставить программу воспринимать все пароли как правильные, двухбайтовый условный переход brtrue.s IL_0031 необходимо заменить на пару однобайтовых команд nop (опкод - 00h, а вовсе не 90h как на x86). Или же... заменить brtrue.s IL_0031 на brFALSE.s IL_0031, тогда любой неправильный пароль будет восприниматься как правильный и, соответственно, наоборот. Открыв ECMA-335, мы узнаем, что инструкция brfalse.s имеет опкод 2Ch - и это все, что нам необходимо знать для взлома программы.
А теперь загрузим в ildasm.exe бинарную сборку, выданную Си++ компилятором с ключом /CLR, и посмотрим, чем она отличается от "нормальной" .NET-сборки. Если забыть о том, что Си++ сборка тащит за собой весьма тяжеловесный RTL (в котором для нас нет ровным счетом ничего интересного) и сосредоточиться исключительно на функции main, то можно обнаружить, что все не так уж и страшно (см. листинг 6).
Длинные имена методов класса (сокращенные для экономии бумаги), конечно, на первых порах вызывают шевеление волос на голове, но потом к ним быстро привыкаешь, автоматически "вычленяя" привычные "позывные" типа printf, gets, etc, однако структура кода далека от совершенства и на его анализ уходит намного больше времени, что, кстати говоря, представляет собой не такой уж "тупой" защитный прием от начинающих хакеров. Просто компилируем свои Си++ программы с ключом /CLR и хрен кто их взломает.
.method assembly static int32 modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) main() cil managed { .vtentry 1 : 1 // Code size 59 (0x3b) .maxstack 2 .locals (valuetype '<CppImplementationDetails>'.$ArrayType$$$BY0GGG@D V_0) IL_00: ldsflda $ArrayType$ IL_05: call modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)printf(*) IL_0a: pop IL_0b: ldloca.s V_0 IL_0d: call int8 gets( * ) IL_12: pop IL_13: ldloca.s V_0 IL_15: ldsflda $ArrayType$$ IL_1a: call modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) strcmp(,) IL_1f: brfalse.s IL_002e IL_21: ldsflda $ArrayType$ IL_26: call modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)printf(*) IL_2b: pop IL_2c: br.s IL_0039 IL_2e: ldsflda $ArrayType$ IL_33: call modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)printf(*) IL_38: pop IL_39: ldc.i4.0 IL_3a: ret } // end of method 'Global Functions'::main
Листинг 6. Дизассемблирование .NET-сборки, полученной путем трансляции Си++ программы.
Так, где там наш HIEW?! Готов ко взлому или... еще не готов? Как нам определить местоположение байта, который мы собрались захачить? Ведь виртуальные адреса в контексте CIL-кода вообще неуместны!
Воспользуемся дедовским способом и поищем последовательность байт (сигнатуру), обитающую в окрестностях целевой команды. В данном случае это может быть 2Dh 0Dh 72h 2F 00h 00h 70h 28h (об обратном порядке байт не забываем, да? ildasm автоматически "нормализует" аргументы команд, IDA Pro - нет, показывая их такими, какие они есть - наименее значимый байт располагается по младшему адресу).
Короче, вбиваем заданную последовательность в поиск и убедившись в том, что данное вхождение - единственное, переводим HIEW в режим записи по <F3>, заменяем 2Dh на 2Ch (см. рис. 8), сохраняем изменения в файле по <F9> и выходим.
Рисунок 8. Поиск сигнатуры в HIEW'е и bit-hack (исправление "неправильного" байта на "правильный").
Запускаем хакнутый файл и... о чудо!!! Он работает!!! Теперь любой, наугад взятый пароль - например, "123456" воспринимается как правильный (см. рис. 9). Конечно, если программа снабжена цифровым сертификатом подлинности или использует механизм контроля целостности собственного кода, этот номер уже не пройдет, но... ведь надо же с чего-то начинать ломать!
Рисунок 9. Хакнутая программа любой пароль воспринимает как правильный.
Спору нет, дизассемблер - весьма популярный инструмент для исследования программ. Популярный, но не единственный и во многих случаях отладчик оказывается намного более предпочтительным. Вместо того, чтобы гадать - какое значение имеет переменная в данной точке (в дизассемблере), гораздо практичнее заглянуть в нее отладчиком.
И вот тут выясняется довольно любопытная вещь. На уровне исходных текстов 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" (см. рис. 10) оставляем все галочки в состоянии по умолчанию. Главное, чтобы была взведена галочка "Dump IL Code", после чего нажимаем <OK> и вводим имя файла для дампа - например, "cracked".
Рисунок 10. Дамп двоичной .NET-сборки в ассемблерный файл штатным дизассемблером ildasm.exe.
По окончании дизассемблирования на диске образуются два файла - cracked.il с ассемблерным текстом программы и cracked.res - с ресурсами. Cracked.il представляет собой обыкновенный текстовой файл, который можно править в любом текстовом редакторе, при необходимости заменяя "brtrue.s IL_0031" на "brfalse.s IL_0031", но сейчас нас в первую очередь интересует не патч, а отладка.
Берем штатный ассемблер и собираем файл следующим образом:
$ilasm.exe cracked.il /pdb
Листинг 7. Ассемблирование сдампленного файла штатным ассемблером.
На диске образуются файлы cracked.exe и cracked.pdb, готовые к загрузке в отладчик (что примечательно - отладочная информация непосредственно в сам исполняемый файл не записывается, что очень и очень хорошо, иначе нам пришлось бы потом убирать ее оттуда или мириться с увеличением размера поломанного exe, что вряд ли входит в наши планы).
Ок, набираем в командной строке "$mdbg.exe cracked.exe" и... оказываемся в консольном окне отладчика, автоматически останавливающегося на первой команде функции Main, передавая нам бразды правления. А что такого крутого и хорошего мы можем сделать?!
Начнем с просмотра окрестностей, за что отвечает команда "show" или ее более короткий алиас "sh", результат работы которого выглядит так (см. листинг 8):
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>
Листинг 8. Результат работы команды "sh", показывающей IL-код.
Остальные команды отладчика можно найти во встроенной справке (вызываемой командой 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, ну а дальше мы уже сориентируемся (см. листинг 9).
$D:\KPNC\C#>mdbg cracked.exe # грузим бинарную сборку в отладчик MDbg (Managed debugger) v3.5.21022.8 (RTM.021022-0800) started. Copyright (C) Microsoft Corporation. All rights reserved. run cracked.exe # отладчик запускает бинарную сборку STOP: Breakpoint Hit # отладчик останавливается на main 43: IL_0000: nop # окрестности функции main [p#:0, t#:0] mdbg> b 55 # брякаемся на строку 55 Breakpoint #1 bound (line 55 in cracked.il) # отладчик говорит, что с бряком все ОК [p#:0, t#:0] mdbg> g # продолжаем выполнение программы enter password:password # вводим в качестве пароля "password" STOP: Breakpoint 1 Hit # !!! срабатывает наша точка останова 55: IL_0020: stloc.1 # мы брякнулись на команде stloc.1 [p#:0, t#:0] mdbg> p # просим отладчик распечатать переменные V_0="password" # V_0 хранит введенный пароль V_1=False # V_1 содержит значение False args=array [0] # аргументы программы нам не интересны [p#:0, t#:0] mdbg> n # выполняем следующую команду 56: IL_0021: ldloc.1 # следующая команда загружает V_1 [p#:0, t#:0] mdbg> p # печатаем содержимое переменных еще раз V_0="password" # V_0 не изменилась V_1=True # V_1 ИЗМЕНИЛАСЬ (вот где собака!!!) args=array [0] # -//- [p#:0, t#:0] mdbg> set V_1=0 # просим отладчик записать в V_1 число 0 V_1=False # теперь V_1 вновь содержит False [p#:0, t#:0] mdbg> n # выполняем следующую команду 57: IL_0022: brtrue.s IL_0031 # а следующей команда у нас - ветвление! [p#:0, t#:0] mdbg> n # ну, и куда это ветвление нас заведет? 59: IL_0024: ldstr "hello, master!" # после изменения V_1 мы на верном пути! [p#:0, t#:0] mdbg> g # продолжаем выполнение программы hello, master! # программа пишет "hello, master!" STOP: Process Exited # процесс завершается mdbg> q # выходим из отладчика
Листинг 9. Сеанс работы с отладчиком mdbg.exe (команды, вводимые хакером, выделены полужирным шрифтом).
Ниже, для наглядности тот же самый сеанс работы с отладчиком продемонстрирован в графическом виде (см. рис. 11):
Рисунок 11. Сеанс работы с отладчиком mdbg.exe "как он есть".
Если кому-то религия запрещает использовать консоль, что ж - к его услугам Dotnet IL Editor - бесплатный IL-отладчик с GUI-интерфейсом (см. рис. 12), однако mdbg.exe мыщъх'у как-то больше по душе, да к тому же под него расширения всякие можно писать.
Рисунок 12. Dotnet IL Editor - IL-отладчик с GUI-интерфейсом.
Впрочем, выбор отладчика не принципиален. Важна сама суть - техника исследования .NET-программ, которую мы только что и продемонстрировали.
Разумеется, в рамках одной-единственной статьи просто невозможно охватить все аспекты взлома .NET-программ. В частности, совершенно нетронутой осталась тема упаковщиков бинарных сборок и протекторов, распаковывать которые приходится руками, но это по любому тема отдельного большого разговора.
А пока же имеет смысл потренироваться на простых несильно защищенных коммерческих программах (которые можно найти в Сети), малвари (взятой оттуда же) и crackme, залежи которых находятся на сайте www.crackmes.de (см. рис. 13) и где даже есть специальный раздел, посвященный исключительно платформе .NET.
Мыщъх надеется, что данная статья обеспечит хороший старт, ну а остальное - дело времени, техники и бесчисленных экспериментов!
Рисунок 13. Коллекция .NET crackmes на одноименном сайте.