Как дизассемблировать dll c

Обновлено: 04.07.2024

Одним из приоритетных направлений в разработке любого продукта является его отладка. Ведь мало написать удовлетворяющий требованиям исправно работающий код, так нужно ещё и протестить его в "токсических условиях", на предмет выявления всевозможных ошибок. Для этих целей, инженеры Microsoft включили в состав ОС полноценный механизм дебага в виде двух библиотек пользовательского режима: это Dbgeng.dll – основной движок отладки (Debug Engine), и Dbghelp.dll – вспомогательный процессор отладочных символов PDB (Program Database).

На моей Win-7, библиотека символов имеет размер 0.8 Мб и выдаёт на экспорт аж 205 стандартных API-функций, а вот вторая Dbgeng.dll в три раза тяжелее своего (со)брата с размером

2.5 Мб, зато экспортирует с выхлопной трубы всего 3 функции. От сюда следует, что эта либа от нас явно что-то скрывает, поскольку жалкие три процедуры никак не могут весить более двух мегабайт. В данной статье мы попытаемся заглянуть внутрь отладочного движка Engine, и в качестве примера вытащим из него полноценный дизассемблер инструкций процессоров х86.


1. Знакомство с механизмом отладки

Первые библиотеки отладки, так-же известные как файлы "Symbolic Debugger Engine", были созданы компанией Microsoft в 2001-году для операционной системы Windows-XP. Большая часть либы Dbghelp.dll содержит в себе функции с префиксом(Sym), что говорит об их принадлежности к символьному процессору. Они позволяют по указанному адресу вычислять имена функций, определять типы данных, а также номер строки и название файла, в котором эта строка находится. Поддерживаются и обратные операции, например поиск адреса функции по её имени. Это достаточно творческая единица, если знать как ею пользоваться ( см.документацию на сайте мелкософт).

В состав этой библиотеки входят и привычные нам функции, без каких-либо префиксов. Например потянув за всего одну EnumerateLoadedModules() можно получить список всех модулей DLL (вместе с виртуальной базой и размером), которые загружены в интересующее нас приложение. Поскольку вся черновая работа происходит в фоне, то в большинстве случаях это удобно. На входе, функция требует лишь дескриптор процесса (в примере ниже я передаю -1, т.е. текущий процесс), и адрес callback-процедуры, куда она в цикле будет сбрасывать информацию о модулях. Функция связана со-своей "обратной процедурой" невидимой нитью, так-что программный цикл выстраивать не нужно – обход на автомате прекращается, как только коллбэк возвращает родителю ошибку:

Enum.jpg

Всё идёт прекрасно до тех пор, пока мы не сталкиваемся с вызовом функций из основного движка-отладки Dbgeng.dll – здесь и начинается самое интересное. Эта библиотека построена по модели СОМ (Component-Object-Model), а значит и вызывать из неё функции нужно соответствующим образом. Но проблема в том, что в отличии от крестов С++ и прочих высокоуровневых языков, ни один из ассемблеров не поддерживает на данный момент технологию COM/ActiveX, и врядли уже будет поддерживать в будущем. Ассемблер – это язык низкого уровня, а прослойка СОМ находится в иерархии намного выше.

Как уже упоминалось, библиотека Dbgeng.dll выдаёт на экспорт всего 3-функции (см.в тотале по ctrl+q) – это DebugCreate() , DebugConnect() и DebugConnectWide() . Но под капотом у неё припрятаны ещё порядка 300 внутренних, неэкспортируемых обычным способом функций. Чтобы подобраться к ним, для начала нужно разобраться, что вообще такое COM-интерфейс и как его реализуют современные компиляторы – вот об этом и поговорим..


2. Component-Object-Model в ассемблере

COM – многокомпонентная, клиент-серверная модель объектов Microsoft, которая является продолжением OLE и фундаментальной основой многих других технологий, в том числе ActiveX и DCOM (Distributed COM, работа с сетью). Ключевым аспектом COM является то, что эта технология обеспечивает связь между клиентом (нашим приложением) и сервером (операционной системой) посредством "интерфейсов" . Именно интерфейс предоставляет клиенту способ узнать у сервера, какие конкретно возможности он поддерживает на текущий момент.

В терминологии языка С++ интерфейс – это абстрактный базовый класс, все методы которого являются виртуальными. То-есть вызов этих методов осуществляется через специальную таблицу-указателей, известную как vTable . Например, вызов метода QueryInterface из интерфейса IUnknown будет выглядеть так: IUnknown::QueryInterface() . На сайте rsdn имеется увесистый хаб , выделенный специально под описание всех нюансов СОМ-технологии.

Если-же посмотреть на СОМ глазами ассемблера, то интерфейс представляет собой ничто-иное, как обычную структуру в памяти. Чтобы придерживаться общих правил, мы будем называть её так-же, т.е. "vTable". В свою очередь методы – это лишь иное название уже привычных нам API-функций, а указатели на эти функции хранятся внутри интерфейса. Таким образом, интерфейс можно рассматривать как массив указателей на СОМ-функции. Влиться в эту тему поможет ветка на wasm.in , где представлены материалы по СОМ с реальными примерами на ассемблере – "допризывникам" настоятельно рекомендуется к прочтению.

В операционной системе Win имеется огромное количество СОМ-интерфейсов и это не удивительно, ведь Microsoft строит системы используя объектно-ориентированный подход программирования, а частью ООП является как-раз-таки OLE/ActiveX/СОМ. Чтобы из этого общего пула у нас была возможность выбрать и использовать в своих программах конкретный интерфейс, система назначает ему уникальный идентификатор GUID . Собрав в единую базу, Win хранит все эти идентификаторы в своём кусте реестра под названием "HKEY_CLASSES_ROOT\Interface". Если выбрать любой GUID в левом окне, то в правом получим отождествлённое с этим идентификатором, название интерфейса:

Reg_GUID.jpg

СОМ-сервер операционной системы имеет базовый интерфейс под названием "IUnknown" . Он глобален и все остальные наследуются именно от него. GUID этого интерфейса имеет значение . В своей тушке интерфейс хранит указатели на три метода (функции) и на ассемблере будет выглядеть так:

Как видим, при помощи IUnknown и его метода QueryInterface() можно найти адрес любого СОМ-интерфейса в системе, но только при условии, что мы знаем GUID искомого (нужно будет передать его в качестве аргумента этому методу). Важно запомнить, что базовый интерфейс IUnknown входит в состав буквально всех СОМ-интерфейсов, занимая первые три указателя в нём. СОМ-сервер инкапсулирует его во-все интерфейсы, чтобы вести над ними учёт.

Например, когда мы получаем от сервера ссылку (указатель) на какой-нибудь интерфейс, его метод AddRef() на автомате увеличивает внутренний счётчик-обращений к данному интерфейсу. Если-же интерфейс нам больше не нужен, мы должны вызвать его метод Release() , который соответственно уменьшит этот счётчик на 1. Сервер периодически парсит счётчики активных интерфейсов и если обнаруживает в нём нуль, то из-за ненадобности сразу выгружает его из памяти. Так реализуется "время жизни" СОМ-интерфейсов, и это стандартная схема учёта системных структур, в памяти Win.


3. Структура СОМ-библиотеки Dbgeng.dll

Будем считать, что прошлись по макушкам СОМ-технологии, и теперь рассмотрим её реализацию внутри главного героя этой статьи – библиотеки Dbgeng.dll. В каком-то смысле, эта библиотека сама является полноценным СОМ-сервером, поскольку GUID'ы её интерфейсов не прописаны в системном реестре Win, хотя библиотека и является детищем самой Microsoft. Из этических соображений, все разработчики СОМ-интерфейсов обязаны сопровождать свой продукт полной документацией, чтобы армия прикладных программистов могла использовать незнакомые интерфейсы в своих программах. Связано это с тем, что не зная GUID мы просто не сможем найти ни один интерфейс в системе, и соответственно лишимся возможности вызывать из него методы.

Движок-отладки Dbgeng.dll отлично документирован в репозитории мягких – общие сведения о нём можно почерпнуть по этому линку . Что касается описания непосредственно имеющихся в наличии методов и GUID всех интерфейсов, то они находятся в заголовочном файле Dbgeng.h , электронная версия которого лежит здесь . Судя по этому хидеру, в данную библиотеку включён не один, а целая дюжина связанных с отладкой различных интерфейсов, и в каждом из них имеются свои функции (методы). Исторически, 16-байтные GUID интерфейсов принято обозначать как IID, что подразумевает "Interface-Identifier".

Одной из примечательных особенностей СОМ-интерфейсов является их масштабируемость. Так, если мы захотим изменить уже существующий интерфейс, то достаточно написать недостающие методы, и добавить указатели на них в конец прежнего интерфейса. К примеру, каждый из представленных выше 13-ти фейсов имеет дополнительные экземпляры, к именам которых добавляется порядковый номер по типу: IDebugClient (основной интерфейс), и дальше IDebugClient2 (3,4,5,6,7). Каждый последующий экземпляр включает в себя какие-то свежие методы и ему назначается новый GUID, в результате чего интерфейс шагает в ногу со-временем.

DbgengFile.jpg


Посмотрим на рисунок ниже, где представлена обобщённая структура библиотеки Dbgeng.dll.
Чтобы воспользоваться услугами сервера-отладки, мы должны сначала активировать его функцией CoInitialize() из библиотеки подсистемы исполнения OLE32.dll. Теперь нужно создать "клиента отладки" функцией DebugCreate() из либы Dbgeng.dll, передав ей в виде аргумента GUID интерфейса "IDebugClient::". Это основной интерфейс клиента, где собраны часто используемые им (т.е. нашим приложением) методы.

Если зайти отладчиком OllyDbg в функцию DebugCreate() по [F7], то можно обнаружить, что она проделывает массу полезной работы – например копирует из тушки движка в пространство пользователя различные структуры, находит через GetProcAddress() и подключает вспомогательные функции отладки из библиотеки Ntdll.dll типа: DbgEvent() , DbgBreakPoint() и многое другое. Именно эта функция создаёт полный контекст отладки в памяти ОЗУ, и нам остаётся лишь вызывать методы из требуемых СОМ-интерфейсов:

Dbgeng.jpg

Значит передаём функции DebugCreate() GUID интерфейса "IDebugClient::", на что функция возвращает нам адрес этого интерфейса в памяти. Если вернуться к рис.выше, то можно обнаружить, что первые три метода в любом интерфейсе, есть копия базового интерфейса "IUnknown::", а первый метод – как-раз нужный нам QueryInterface() . Он ожидает на входе два аргумента – это GUID искомого интерфейса, и указатель на переменную, куда метод сохранит его адрес.

Особое внимание нужно обратить на способ вызова СОМ-методов в ассемблере. Дело в том, что помимо обозначенных прототипом аргументов, мы всегда должны добавлять ещё один лишний аргумент – в спецификации его назвали "This" и представляет он собой адрес интерфейса. Другими словами, перед вызовом любого метода из какого-либо интерфейса, мы должны явно указать серверу, из какого именно осуществляем вызов. Этот аргумент(This) всегда является первым аргументом метода – вот пример:

Query.jpg


Посмотрим на результат работы программы..
Интерфейсы, у которых адресом является нуль, не реализованы в движке-отладки Dbgeng.dll и вызывать из них методы нельзя (получим исключение Access-Violation с кодом 0xC0000005, т.к. будет попытка чтения адреса нуль). Ну с интерфейсами Client::[6,7] и Control::[5,6,7] всё понятно – как видим, это обновы предыдущих и добавлены они только начиная с Win-8. Однако мне так и не удалось найти ответа, почему отсутствуют интерфейсы Breakpoint:: и Callbacks. Ради эксперимента я даже пробовал подключать не системную библиотеку Dbgeng.dll, а переименовав подсовывал программе либу ядерного отладчика WinDbg, и всё-равно получал аналогичную картину. После нескольких попыток было решено оставить этот вопрос открытым, до лучших времён.

Из остальных интерфейсов можно смело вызывать их методы. Например, лист методов интерфейса IDebugClient::[2,3,4] выглядит так.. а остальные – перечислены в созданном мной инклуде Dbgeng.inc (см.скрепку). Обратите внимание, как добавляются расширенные интерфейсы к предыдущим. Каждый из них включает в себя полный список всех/своих предков, и только в конце добавляются новые.

4. Практика – пишем дизассемблер

Теперь, на финишной прямой, собрав воедино всё/вышеизложенное напишем дизассемблер, одноимённый метод которого лежит в интерфейсе IDebugControl. Чтобы на поверхность всплыла исключительно полезная составляющая кода, я ограничился дизаcсемблированием лишь текущей программы. В идеале, нужно было дать возможность юзеру выбирать исполняемый файл, но в этом случае "пайлоад" утонул-бы в массе дополнительных функций. Здесь главное понять суть, а окружение – это уже второстепенная задача и дело вкуса. Значит алго будет такой:

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

Dbgeng_Disassm.jpg


Здесь я добавил некоторую вспомогательную информацию в шапке, чтобы продемонстрировать расположение интерфейсов и их методов. Так, первые две строчки указывают на наше пользовательское пространство памяти, куда функция DebugCreate() любезно сбросила указатели на интерфейсы. А вот сами методы находятся уже внутри библиотеки Dbgeng.dll, о чём свидетельствует их адреса с базой 0x5D0D0000 . Если вызов метода возвращает в EAX=0 , значит он прошёл успешно (константа S_OK), иначе в EAX получим следующие коды ошибок:


5. Заключение.

Программирование СОМ-интерфейсов открывает перед нами огромные возможности, поскольку в своих/больших штанинах они прячут достаточно интересные методы, подобраться к которым можно только через указатель на интерфейс, аля GUID. По модели СОМ построена добрая половина системных библиотек – объектная модель позволяет нам работать с такими механизмами как WMI (инструментарий Windows), технологией DirectX, с библиотекой Shell32.dll и многое другое. Как упоминалось выше, любой СОМ-интерфейс обязан быть документированным, поэтому проблем не возникает – главное уловить логическую нить, а дальше уже дело техники.

Дизассемблер IL является сопутствующим инструментом ассемблера IL (Ilasm.exe). Ildasm.exe принимает переносимый исполняемый файл (PE-файл), содержащий код на промежуточном языке (IL), и создает на его основе текстовый файл, который может служить входным файлом для Ilasm.exe.

Эта программа автоматически устанавливается вместе с Visual Studio. Для запуска этого средства используйте Командную строку разработчика или PowerShell для разработчиков в Visual Studio.

В командной строке введите следующее.

Синтаксис

Параметры

Перечисленные ниже параметры допустимы для файлов EXE, DLL, OBJ, LIB и WINMD.

Параметр Описание
/out= filename Создает выходной файл с заданным параметром filename вместо вывода результатов в графический пользовательский интерфейс.
/rtf Выводит данные в формате RTF. Не может использоваться с параметром /text.
/text Отображает результаты в окне консоли вместо вывода в графический пользовательский интерфейс или выходной файл.
/html Выводит данные в формате HTML. Может использоваться только с параметром /output.
/? Отображает синтаксис команд и параметров для средства.

Перечисленные ниже дополнительные параметры допустимы для файлов EXE, DLL и WINMD.

PUB — открытый;

PRI — закрытый;

FAM — семейство;

ASM — сборка;

FAA — семейство и сборка;

FOA — семейство или сборка;

PSC — закрытая область.

Перечисленные ниже параметры допустимы для файлов EXE, DLL и WINMD только при выводе в файл или окно консоли.

Перечисленные ниже параметры допустимы для файлов EXE, DLL, OBJ, LIB и WINMD только при выводе в файл или окно консоли.

MDHEADER — показывать сведения и размеры заголовка метаданных;

HEX — показывать сведения в шестнадцатеричном и текстовом формате;

CSV — показывать количество записей и размеры кучи;

UNREX — показывать неразрешенные внешние элементы;

SCHEMA — показывать сведения о заголовке и схеме метаданных;

RAW — показывать необработанные таблицы метаданных;

HEAPS — показывать необработанные кучи;

VALIDATE — проверять согласованность метаданных.

Перечисленные ниже параметры допустимы для LIB-файлов только при выводе в файл или окно консоли.

Параметр Описание
/objectfile= filename Вывод метаданных отдельного объектного файла из заданной библиотеки.

Параметры программы Ildasm.exe не учитывают регистр и распознаются по первым трем буквам. Например, команда /quo эквивалентна команде /quoteallnames. Разделителем параметра и его аргумента может служить двоеточие (:) или знак равенства (=). Например, команда /output: имя_файла эквивалентна команде /output= имя_файла.

Примечания

Программа Ildasm.exe работает только с PE-файлами, расположенными на жестком диске. Программа не обрабатывает файлы, установленные в глобальном кэше сборок.

Текстовый файл, созданный программой Ildasm.exe, можно передавать в качестве входных данных в ассемблер IL (Ilasm.exe). Это полезно, к примеру, при компиляции кода на языке программирования, не поддерживающем все атрибуты метаданных среды выполнения. После компиляции кода и обработки результатов с помощью Ildasm.exe можно вручную добавить недостающие атрибуты в полученный текстовый файл IL. Чтобы создать окончательный исполняемый файл, следует обработать этот текстовый файл ассемблером IL.

На данный момент такая технология не применяется к PE-файлам, содержащим встроенный машинный код (например, к PE-файлам, созданным компилятором Microsoft Visual C++).

Для просмотра метаданных и дизассемблированного кода PE-файлов в иерархическом представлении в виде дерева применяется графический пользовательский интерфейс по умолчанию дизассемблера IL. Чтобы запустить графический пользовательский интерфейс, введите в командной строке команду ildasm без аргумента имя_PE-файла и без параметров. В меню Файл можно перейти к PE-файлу, который требуется загрузить в программу Ildasm.exe. Чтобы сохранить метаданные и дизассемблированный код, отображаемый для выбранного PE-файла, выберите в меню Файл команду Дамп. Чтобы сохранить только иерархическое представление в виде дерева, выберите в меню Файл команду Дерево дампа. Дополнительные инструкции по загрузке файла в программу Ildasm.exe и интерпретации выходных данных см. в руководстве по Ildasm.exe, которое находится в папке Samples в Windows SDK.

Если программе Ildasm.exe задан аргумент имя_PE-файла, содержащий внедренные ресурсы, будет создано несколько выходных файлов: текстовый файл с IL-кодом и RESOURCES-файл для каждого внедренного управляемого ресурса (название файла соответствует названию ресурса в метаданных). Если в аргумент имя_PE-файла внедрены неуправляемые ресурсы, будет создан RES-файл с именем, указанным для IL-вывода в параметре /output.

Для входных файлов OBJ и LIB программа Ildasm.exe отображает только описания метаданных. IL-код для файлов этих типов не дизассемблируется.

Сведения о версии

Примеры

Следующая команда выводит метаданные и дизассемблированный код PE-файла MyHello.exe в стандартный графический пользовательский интерфейс программы Ildasm.exe.

Следующая команда дизассемблирует файл MyFile.exe и сохраняет выходной текст ассемблера IL в файле MyFile.il.

Следующая команда дизассемблирует файл MyFile.exe и выводит выходной текст ассемблера IL в окно консоли.

Если файл MyApp.exe содержит внедренные управляемые и неуправляемые ресурсы, при выполнении следующей команды будет создано четыре файла: MyApp.il, MyApp.res, Icons.resources и Message.resources.

Следующая команда дизассемблирует метод MyMethod класса MyClass в файле MyFile.exe и выводит результат в окно консоли.

В предыдущем примере допустимо наличие нескольких методов с именем MyMethod и различными сигнатурами. Следующая команда дизассемблирует метод экземпляра MyMethod с типом возвращаемого значения void и типами параметров int32 и string.

Чтобы извлечь метод static (метод Shared в Visual Basic), следует опустить ключевое слово instance . Типы классов, которые не являются простыми типами (такими как int32 и string ), должны включать пространство имен и перед ними необходимо указывать ключевое слово class . Перед внешними типами должно быть указано имя соответствующей библиотеки в квадратных скобках. Следующая команда дизассемблирует статический метод с именем MyMethod , имеющий один параметр типа AppDomain, и возвращает значение типа AppDomain.

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

Отказ от ответственности

Стандартное отречение: вся информация, предоставленная ниже, преследует исключительно благие цели (анализ вредоносного программного обеспечения, например) и мыщъх не несет никакой ответственности (ни явной, ни предполагаемой) за любой возможный ущерб или потерянную выгоду от ее использования.

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

Microsoft Visual Studio

Рисунок 1. Отсюда можно бесплатно скачать последнюю версию Microsoft Visual Studio.

Реализация платформы .NET

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

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 off, hacker!" );
>
>

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

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

Компилятор Cи++, входящий в состав Microsoft Visual Studio 2008, умеет транслировать программы не только в машинный, но и в байт-код, позволяя нам использовать все прелести .NET платформы из привычных плюсов (трансляция "чистых" Си программ в байт-код все еще не поддерживается).

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

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

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

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

.NET сборка

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

Внешний вид .NET-сборки

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

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

Консольная версия IDA

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

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

Графическая версия IDA

Штатный дизассемблер

Рисунок 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).

Листинг 5. Результат работы штатного дизассемблера ildasm.exe.

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

Дизассемблирование Cи++ сборки

Длинные имена методов класса (сокращенные для экономии бумаги), конечно, на первых порах вызывают шевеление волос на голове, но потом к ним быстро привыкаешь, автоматически "вычленяя" привычные "позывные" типа printf, gets, etc, однако структура кода далека от совершенства и на его анализ уходит намного больше времени, что, кстати говоря, представляет собой не такой уж "тупой" защитный прием от начинающих хакеров. Просто компилируем свои Си++ программы с ключом /CLR и хрен кто их взломает.

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

Так, где там наш 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. Хакнутая программа любой пароль воспринимает как правильный.

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

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

Расследование показало, что штатному .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".

Дамп двоичной .NET-сборки

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

Листинг 7. Ассемблирование сдампленного файла штатным ассемблером.

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

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

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

Листинг 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).

Листинг 9. Сеанс работы с отладчиком mdbg.exe (команды, вводимые хакером, выделены полужирным шрифтом).

Ниже, для наглядности тот же самый сеанс работы с отладчиком продемонстрирован в графическом виде (см. рис. 11):

Сеанс работы с отладчиком

Рисунок 11. Сеанс работы с отладчиком mdbg.exe "как он есть".

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

Dotnet IL Editor

Рисунок 12. Dotnet IL Editor - IL-отладчик с GUI-интерфейсом.

Заключение

Мыщъх надеется, что данная статья обеспечит хороший старт, ну а остальное - дело времени, техники и бесчисленных экспериментов!

Коллекция .NET crackmes

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

  • ?, help:
    вывод встроенной справки, "help команда" - подробная справка по команде;

Сегодня мы будем смотреть дизассемблированный код инструкций if, for, while, switch , которые написаны на языке Си.

1605795529033.jpg

1605795529033.jpg

Инcтрукция if

Данную инструкцию довольно просто отличить в дизассемблированном виде от других инструкций. Её отличительное свойство - одиночные инструкции условного перехода je, jne и другие команды jump.

1605790962062.jpg 1605790989407.jpg

Напишем небольшую программу на языке Си и дизассемблируем её с помощью radare2. Разницы между IDA PRO и radare2 при дизассемблировании этих программ не было обнаружено, поэтому я воспользуюсь radare2. Вы можете использовать IDA PRO.

IDA PRO

radare2

1605792900362.jpg

1605792900362.jpg

Код на Си

Компилируем при помощи gcc. Команда gcc -m32 prog_if.c -o prog_if . -m32 означает, что компилироваться код будет под архитектуру x86.

Чтобы посмотреть на код в radare2, напишем команду r2 prog_if . Далее прописываем aaa для анализа кода и переходим к функции main s main . Посмотрим на код с помощью команды pdf .

Дизассемблированный вариант в radare2

1605792900362.jpg

1605792900362.jpg

Первым делом в программе происходит объявление переменных ( int x; int y ), а затем значение 1 перемещается в var_ch (это переменная x) и значение 2 в var10h (это переменная y). Далее идёт сравнение (cmp) 1 и 2 ( cmp edx, dword [var_10h] ). Эти значения не равны. Значит jne ( jump if not equal) перейдёт по адресу 0x000011e1. Проще всего инструкцию if запомнить и определить в режиме графов (команда VV для для radare2 или клавиша пробел для IDA).

Режим графов

1605792914861.jpg

1605792914861.jpg

Немного усложним задачу. Добавим вложенные инструкции. Попробуйте проанализировать этот код.

Код на Си

Дизассемблированный вариант в radare2

1605792935104.jpg

1605792935104.jpg

Режим графов

1605792950680.jpg

1605792950680.jpg

В режиме графов это воспринимать намного проще.

Инструкция for

Циклы for всегда состоят из четырех этапов: инициализации, сравнения, выполнения инструкций и инкремента/декремента. По этим этапам мы будем отличать цикл for в ассемблерном коде от других.

Код на Си

Дизассемблированный вариант в radare2

1605792973718.jpg

1605792973718.jpg

1 - инициализации переменной var_ch (x = 0)
2 - сравнение, а затем jle. ( пока x не будет меньше или равен 2, выполнять цикл.)
3 - выполнения инструкций (printf)
4 - инкремент переменной var_
ch (++x)

Режим графов

1605792987678.jpg

1605792987678.jpg

Инструкция while

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

Код на Си

Дизассемблированный вариант в radare2

1605793002542.jpg

1605793002542.jpg

1 - инициализации переменной var_4h (status = 0)
2 - сравнение, а затем je. ( пока x равен 0, выполнять цикл.)
3 - выполнения инструкций (func
1, printf, change_status)

Режим графов

1605793019409.jpg

1605793019409.jpg

Инструкция switch

Конструкция switch обычно компилируется двумя способами: по примеру условного выражения или как таблица переходов.

Компиляция по примеру условного выражения

Код на Си

Дизассемблированный вариант в radare2

1605793045836.jpg

1605793045836.jpg

1 - инициализации переменной var_4h (i = 3)
2 - выполнения инструкций (add, printf)

Чтобы понять какой "case" выбран, происходит сравнение (cmp, а затем je, jne) переменной i с значением case.

Режим графов

1605793347813.jpg screen13.jpg screen_13_2.jpg

Глядя на этот код, сложно (если вообще возможно) сказать, что представлял собой оригинальный исходный текст — конструкцию switch или последовательность выражений if . В обоих случаях код выглядит одинаково, поскольку оба выражения используют множество инструкций cmp и je или jne.

Таблица переходов

Следующий пример ассемблерного кода часто можно встретить в больших смежных выражениях switch. Мы добавим case 4 и инструкцию по умолчанию.

Код на Си

Дизассемблированный вариант в radare2

1605793648917.jpg 1605793656018.jpg

1 - инициализации переменной var_4h (i = 3)
2 - выполнения инструкций (add, printf)

Вот этот дизасcемблированный код довольно сложно быстро отличить от if и вообще понять что и как тут. В режиме графов всё будет более понятно.

Режим графов

1605793750977.jpg 1605793673414.jpg 1605793684884.jpg 1605793691266.jpg

Режим графов - ваш друг в дизасcемблировании :)

На этом всё. Рекомендую попробовать самому написать программы на Си, скомпилировать и изучить дизасcемблированный код. Практика и ещё раз практика!

Читайте также: