- Главная
- Блог
- Информационная безопасность
- Основы реверсинга, начинаем ломать
Основы реверсинга, начинаем ломать
Автор: Андрей Бирюков.
Специально для Академии Кодебай
В предыдущей статье мы установили все необходимые для реверсинга инструменты: отладчик, дизассемблер и другие дополнительные средства. В этой статье разберем на примерах основные моменты, связанные с реверсивным инжинирингом, посмотрим разбор Crackme.
Начнем с основ языка Ассемблер, базовые знания которого нам потребуются для решения крякмиксов.
Регистры и стек
Обработку данных в процессоре осуществляют специальные ячейки, известные как регистры. Для простоты понимания регистры можно сравнить с переменными в языках высокого уровня. В них можно хранить значения и выполнять определенные операции, но количество этих регистров ограничено архитектурой процессора.
Регистры в процессоре x86-64 можно разделить на четыре категории: регистры общего назначения, специальные регистры для приложений, сегментные регистры и специальные регистры режима ядра. В рамках выполняемых задач, нас будут интересовать прежде всего регистры общего назначения (general-purpose registers), которые в основном и используются в приложениях на ассемблере.
Также в наших статьях мы будем говорить о 32-битной архитектуре процессора, так как все рассматриваемые крякмиксы написаны именно под нее. Начнем с того, что процессор архитектуры x86 имеет восемь 32-битных регистров общего назначения, регистр флагов и указатель инструкций. Регистры общего назначения:
-
EAX (Accumulator): для арифметических операций
-
ECX (Counter): для хранения счетчика цикла
-
EDX (Data): для арифметических операций и операций ввода-вывода
-
EBX (Base): указатель на данные
-
ESP (Stack pointer): указатель на верхушку стека
-
EBP (Base pointer): указатель на базу стека внутри функции
-
ESI (Source index): указатель на источник при операциях с массивом
-
EDI (Destination index): указатель на место назначения в операциях с массивами
-
EIP: указатель адреса следующей инструкции для выполнения
-
EFLAGS: регистр флагов, содержит биты состояния процессора
Можно получить доступ к частям 32-битных регистров с меньшей разрядностью. Например, младшие 16 бит 32-битного регистра EAX обозначаются как AX. К регистру AX можно обращаться как к отдельным байтам, используя имена AH (старший байт) и AL (младший байт). Но не для всех регистров можно получить такой доступ. На иллюстрации ниже представлены регистры общего назначения и наличие возможности доступа к их младшим частям.
Как видно, младшие части доступны только у EAX, EDX, ECX, EBX. При этом если сами регистры (например EAX) имеют разрядность 32 бита, то младшая часть AX будет содержать 16 бит. При этом мы можем обращаться к младшей части (AL) и старшей части (AH) регистра AX. К старшей части регистра EAX (биты 16-31) мы не можем обратиться напрямую, для их получения необходимо прибегнуть к дополнительным действиям, например операциям побитового сдвига.
Пока информации по регистрам нам будет достаточно. По мере необходимости мы будем возвращаться к этой теме.
Рассмотрим еще одну важную тему это работа со стеком. Стек - это структура данных, работающая по принципу: первым вошел – последним вышел (First In Last Out). Мы можем использовать стек для хранения значений регистров. Для того, чтобы поместить значение регистра в стек используется инструкция PUSH, а для извлечения инструкция POP.
Инструкции
Продолжая тему инструкций, по сути команд, используемых в ассемблере, рассмотрим несколько основных инструкций. Работу с регистром с помощью PUSH и POP мы уже рассмотрели. Пожалуй, самой распространенной инструкцией является MOV. Она копирует данные из одного места в другое и имеет следующий синтаксис:
MOV destination, source
Инструкция принимает два операнда. Первый операнд - destination представляет расположение, куда надо поместить данные. В качестве такого места может выступать регистр процессора или адрес в памяти. Второй операнд - source указывает на источник данных, в качестве которого может выступать регистр процессора, адрес в памяти или непосредственный операнд - число. То инструкция mov копирует данные из source в destination. При этом оба операнда не могут быть одновременно адресами в памяти.
Вот несколько примеров:
MOV EAX, 0x0a
Загружаем в EAX значение 0x0a.
MOV EBX, EAX
Загружаем в EBX значение EAX.
MOV AH, AL
В этом примере мы работаем с 16-битными частями EAX загружая в AH значение AL.
MOV [BX], AX
А здесь мы загружаем в адрес [BX], значение регистра AX.
Но конструкции вида:
MOV [BX],[AX]
Недопустимы, так как оба операнда не могут быть одновременно адресами в памяти.
Инструкции для работы со стеком PUSH и POP также как и инструкция MOV могут работать с различными сущностями. Так, мы можем поместить в стек содержимое всего регистра:
PUSH ECX
Первые 16 бит:
PUSH DX
Или целочисленное значение:
PUSH 0xfa
При этом, поместив в стек значение одной сущности, мы можем затем извлечь из стека это значение в другую сущность. Так, в предыдущих примерах мы последней поместили в стек константу 0xfa, но извлечь ее мы можем в AL, с помощью инструкции:
POP AL
Сохраненное значение DX мы извлечем в BX
POP BX
И, наконец, значение EAX мы извлечем в ячейку памяти, расположенную по адресу 0x87654321:
POP [0x87654321]
Еще одна полезная инструкция при решении крякмиксов это CMP. Она используется для сравнения двух операндов. То есть, эта команда сравнивает два числа, проверяя их равенство.
CMP ЗНАЧЕНИЕ1, ЗНАЧЕНИЕ2
При этом ЗНАЧЕНИЕ1 может быть одним из следующих: область памяти, регистр общего назначения. А ЗНАЧЕНИЕ2 может быть областью памяти, также регистром общего назначения или непосредственным значением (например, числом).
И команды JMP и CALL. Команда JMP выполняет безусловный переход в указанное место.
JMP МЕТКА
При этом, МЕТКА это адрес перехода, которому передается выполнение кода.
Команда CALL выполняет похожие действия - вызывает процедуру. Синтаксис:
CALL ИМЯ
Но здесь в поле ИМЯ может быть имя процедуры, метка, переменная, регистр или непосредственное значение адреса.
После выполнения вызова с помощью CALL при получении команды RET будет выполнен возврат к инструкции, которая следовала после CALL. …
CALL Label1
MOV AX,0x0d
… Label1:
…
RET
В приведенном примере в соответствии с инструкцией CALL будет выполнен переход по метке Label1, далее по команде RET мы вернемся к инструкции MOV AX,0x0d.
На этом нам пока теории будет достаточно и можно переходить к практической части.
О патчинге
Программы используют различные механизмы защиты. Типичным случаем является необходимость ввести код для отключения какого-либо защитного механизма, альтернативой является отключение защиты после активации по сети. При этом по сети также передается некоторый код. Решить проблему с кодом можно двумя способами: изменив код в исходном приложении, например убрав проверку правильности ввода (патчинг) или же можно исследовать алгоритм по которому генерируется правильный код и затем сгенерировать нужный код своими силами. В первом случае мы вносим изменения в исходное приложение, во втором случае нет.
Посмотрим пример патчинга. В качестве подопытного у нас выступит очень простой Crackme.
Простой запуск выполнимого файла приведет к появлению следующего сообщения:
Собственно, нас даже ни о чем не спросили, а сразу выдали это сообщение. Приступим к исследованию.
Откроем файл в x64dbg. Нажав один раз выполнение мы попадаем в начальную точку, с которой начнется выполнение кода программы.
Первой идет еще не знакомая нам инструкция LEA. Данная команда вычисляет эффективный адрес источника, в данном случае 0х403018 и помещает его в приемник – регистр ECX. На скриншоте правее от этой строке отладчик показывает, что именно находится по этому адресу: текст “Project PolyPhemous …”.
Второй строкой идет инструкция CMP, которая сравнивает содержимое ячейки памяти 0х403040 с единицей. В случае если значение не равно единице (инструкция JNE в следующей строке) выполняется переход на адрес 0х401022. В противном случае последовательно выполняем инструкции, идущие далее, в частности загружаем в EDX адрес 0х403000 по которому находится строка Registered!!! Также в стек помещаются несколько служебных значений (команды PUSH) и вызывается функция операционной системы MessageBoxA, которая выводит окно с сообщением.
В случае перехода на адрес 0х401022 формируется сообщение Unregistered и, далее также выводится окно.
Здесь возможны несколько вариантов решения: можно убрать или поменять проверку условия, можно убрать условный переход, можно поправить область памяти.
Но для начала в целях обучения выполним по шагам каждую из приведенных инструкций. Нажимая F7, по очереди выполним каждую из команд. Очевидно, что в текущий код выведет сообщение Unregistered, так как выполняется переход по команде JNE. Давайте посмотрим какое значение находится по адресу 0х403040.
Для этого в меню отладчика выберем вкладку Карта памяти и далее сегмент .data.
В современных приложениях сегменты кода и данных разделены. Выберем этот сегмент и убедимся, что по адресу 0х403040 находится 0.
Давайте попробуем поменять это значение. Для начала нажмем Ctrl+F2 и перезапустим приложение в отладчике. Далее нажмем F9, после этого снова перейдем в карту памяти и отобразим содержимое адреса 0х403040. По правой кнопке мыши выберем Изменить значение и укажем единицу.
Снова запустим программу на выполнение. Получаем сообщение Registered.
Прежде чем, реализовать другие способы патчинга познакомимся с таким полезным инструментом отладки как Точки останова (Breakpoints). Мы можем остановить выполнение программы на любом шаге просто поставив брейкпоинт на этот адрес.
Снова перезапустим программу, нажав Ctrl+F2. Выберем вторую строку CMP… и нажмем на ней F2. Точка останова поставлена. Теперь, если запустить программу на выполнение, она остановится на этой строке.
Далее попробуем убрать проверку JNE … Для простоты мы заменим эту команду инструкциями NOP. NOP – сокращение от No Operations, то есть эта команда ничего не делает. В первых процессорах эта команда использовалась для искусственного замедления работы программ, аналог команды sleep в языках высокого уровня.
Мы не можем просто удалить ненужную команду в отладчике, но вместо этого мы можем ее заменить. Как видно в среднем столбце на рисунке, инструкции JNE соответствуют байты 0х75 и 0х13. Мы заменим NOP оба этих байта. Для этого нажмите правую кнопку мыши на строке с JNE и выберите Ассемблировать. Далее укажите в строке ввода nop и выберите Заполнить командами NOP.
Инструкция JNE успешно заменена. В результате получаем значение Registered.
И в завершении рассмотрим еще один способ патчинга. Мы не будем заменять JNE командами NOP, а вместо этого заменим ее командой JE – переход, если равно. Для этого также откроем окно Ассемблировать на строке с JNE и заменим эту команду JE. При этом адрес перехода оставим неизменным. К слову, если напишем некорректную команду, в правом нижнем углу будет выведено соответствующее сообщение. Если мы ввели все корректно, то будет сообщение Инструкция интерпретирована успешно.
И снова получаем сообщение Registered.
Подведем итоги
В этой статье мы достаточно подробно рассмотрели основы Ассемблера – регистры, стек и наиболее распространенные команды. Также мы рассмотрели пример патчинга – изменения выполнимого файла с целью обхода каких-либо проверок или защитных механизмов. В реальности различные кряки обходят механизмы проверки лицензий в приложениях, просто заменяя несколько байт в выполнимых файлах. Но у патчинга есть существенный недостаток – изменение даже одного бита в файле приводит к изменению его контрольной суммы – параметра, который используют средства защиты для контроля целостности файла. Выполнимые файлы не изменяются в процессе работы, поэтому их контрольная сумма должна оставаться неизменной.
Альтернативой патчингу является написание генератора ключей – кейгена. То есть мы не будем вносить изменения в файл, а вместо этого попытаемся разгадать принцип, по которому генерируются ключи. Именно этим мы займемся в следующей статье.
В нашем курсе мы подробно разбираем решение различных крякмиксов для того, чтобы получить практику реверсинга перед анализом реальных приложений.