Radare2 это фреймворк для анализа бинарных файлов. Он включает в себя большое количество утилит. Изначально он развивался как шестнадцатеричный редактор для поиска и восстановления данных, затем он обрастал функционалом и на текущий момент стал мощным фреймворком для анализа данных. В этой статье я расскажу как с помощью фреймворка Radare2 произвести анализ логики работы программы, а также опишу основные элементы языка ассемблера, которые необходимы для проведения реверс инжиниринга.
Radare2 представляет собой комплект из нескольких утилит:
- radare2 (r2) — Шестнадцатеричный редактор, дизассемблер и отладчик с расширенным интерфейсом командной строки. Позволяет работать с различными устройствами ввода вывода, такими как диски, удаленные устройства, отлаживаемые процессы и др., а также работать с ними как с простыми файлами.
- rabin2 — Используется для получения информации об исполняемых бинарных файлах.
- rasm2 — Позволяет производить преобразования из опкода в машинный код и обратно. Поддерживает большое количество архитектур.
- rahash2 — Утилита, предназначенная для расчета контрольных сумм. Поддерживает множество алгоритмов, позволяет получить контрольную сумму целого файла, его части или произвольной строки.
- radiff2 — Утилита, для сравнения бинарных файлов, поддерживает множество алгоритмов, умеет сравнивать блоки кода исполняемых файлов.
- rafind2 — Утилита для поиска последовательности байт.
- ragg2 — Утилита для компиляции небольших программ.
- rarun2 — Утилита, способная запускать анализируемую программу с различными настройками окружения.
- rax2 — Небольшой калькулятор, позволяющий производить простые вычисления в различных системах счисления.
Основным недостатком, который препятствует распространенности фреймворка, является отсутствие качественного GUI. Имеются сторонние реализации, но сожалению они не слишком удобные. Также стоит отметить наличие встроенного веб интерфейса.
Radare2 чаще всего применяется как инструмент реверс инжиниринга, в качестве продвинутого дизассемблера. Рассматривать Radare2 мы будем именно как дизассембрер и произведем анализ простого crackme.
Введение в ассемблер
Прежде чем начинать анализ программы стоит остановиться на основных моментах, которые необходимы для понимания кода ассемблера. Описание основных инструкций ассемблера заслуживает отдельной статьи, поэтому здесь будут приведены только основные группы инструкций.
- Инструкции копирования (mov, movsx, movzx)
- Инструкции логических операций (and, or, xor, test)
- Инструкции арифметических операций (add, sub)
- Инструкции управления последовательностью выполнения программы (jmp, jne, ret)
- Инструкции работы с прерываниями (int)
- Инструкции вводы-вывода (in, out)
По умолчанию Radare2 использует синтаксис intel, для которого характерен следующий формат записи:
инструкция операнд ;комментарий
Основные инструкции могут иметь один или два операнда. В случае работы с двумя операндами формат записи примет следующий вид:
инструкция операнд1, операнд2 ;комментарий
Многие инструкции, такие как and, sub, add сохраняют результат вычисления в первый операнд.
Язык ассемблера не поддерживает операции, в которых оба операнда находятся в памяти. Поэтому приходится одно или оба значения помещать в регистры, которые в дальнейшем будут использоваться в качестве операндов. Таким образом мы плавно подошли к определению регистров.
Регистры — это очень быстрые ячейки памяти, которые находятся в процессоре. Они работают гораздо быстрее оперативной памяти или кэша, но объем хранимой в них памяти очень мал. В процессоре архитектуры х86 (х86-32) имеется 8 регистров общего назначения размером 32 бита. Процессоры архитектуры amd64 (х86-64) имеют 16 регистров общего назначения размером 64 бита. Более подробная информация представлена в таблице ниже.
Исследуем crackme
Разберем анализ исполняемых файлов на примере простейшего crackme, полученного отсюда https://github.com/geyslan/crackmes. Запустим программу и посмотрим на ее поведение. Сразу видим приглашение ввести пароль, попробуем ввести 123456.
Пароль мы не угадали, программа просит повторить попытку и завершает свою работу. Начнем анализ, для этого запустим радар командой «r2 -A crackme». Аргумент -А нужен для того, чтобы радар сразу провел анализ функций, эквивалент команде aa. Командой izz выведем текстовые строки, которые содержатся в программе.
Здесь мы видим несколько строк, две нам уже встречались во время запуска программы. Также видим строку которая, предположительно, выводится в случае ввода верного пароля. Данная строка хранится по адресу 0x08048888, запоминаем этот адрес.
Выполним команду afl для получения списка функций.
Здесь мы видим помимо библиотечных функций, а также функцию entry0 которая, как понятно из названия, является точкой входа программы. Функция main это начальная точка выполнения всех программ написанных на С/С++. Из названия остальных функций сложно сделать вывод об из роли в программе.
Посмотрим код функции main, выполнив pdf @ main. Здесь мы видим несколько вызовов функций. Первый вызов — это функция fwrite, которая производит вывод строки приглашения. Второй — функция fgets производит чтение с устройства ввода и помещает введенные данные в память. Далее следует вызов двух функций неизвестного назначения. Затем еще два вызова fwrite. Нас интересует участок кода, в котором происходит обращение к адресу строки, который мы запомнили ранее.
Здесь мы видим, что строка будет выводится если не произойдет условный переход «jne 0x804875e», для этого, на момент выполнения «test eax, eax», значение регистра eax должно быть равно 0. Можно предположить, что функция fcn.08048675, выполняемая ранее, производит проверку пароля, и в случае если пароль верный записывает в eax 0. Следовательно если убрать условный переход, то программа вне зависимости от введенного пароля, будет считать что введен верный пароль. Это можно сделать различными способами, например перед проверкой принудительно выставить значение регистра eax в 0. Подменить адрес перехода или просто убрать переход, заменив его опкодами nop.
Мы попробуем последний вариант, для этого переоткроем файл в режиме записи, выполнив команду oo+. Затем перейдем по адресу 0x08048735 и выполним команду "wa nop;nop". В результате мы заменили условный переход на два опкода nop.
Запустим программу и попробуем ввести пароль.
Отлично, мы успешно пропатчили программу. В случае более сложной программы такое решение может сработать не совсем корректно и в результате программа может вести себя совсем не так как предполагалось. Можно пойти более сложным путем и узнать верный пароль, для этого необходимо проанализировать функции fcn.08048675 и fcn.08048642. Начнем с fcn.08048642, выполним pdf @ fcn.08048642.
Проанализировав код, видим, что функция принимает два аргумента, хотя один из них не используется. В теле функции выполняется цикл со счетчиком. mov dword [local_4h], 0 инициализирует счетчик значением 0. Далее производится безусловный переход на адрес 0x0804866d, где производится сравнение счетчика со значением 5. Если значение счетчика меньше 5, то производится переход к адресу 0x08048651. Здесь в регистр edx производится запись значения счетчика, затем в регистр eax запись значения второго аргумента, скорее всего это указатель на введенную нами строку. Далее значения этих регистров складывается, в результате мы получим адрес со смещением счетчика, относительно указателя на нашу строку.
Результат сложения сохраняется в регистре edx. Затем производится аналогичное действие, только результат сохраняется в eax. В следующей строке, операнд movzx производит копирование байта, на который указывает адрес в eax в младшую часть этого регистра, al. После этого производится операция исключающее или, между байтом в регистре eax и 0x6c. Результат записывается по адресу, который хранится в edx. Затем к счетчику прибавляется 1. Если счетчик меньше 5, то цикл повторяется.
После того как счетчик принимает значение 5, происходит выход из цикла и завершение функции. Таким образом производится обход введенной нами строки и изменение каждого символа в ней. Исходя из максимального значения счетчика делаем вывод что пароль состоит из 6 байт.
Далее вызывается функция fcn.08048675, которая принимает 2 аргумента, адрес введенного нами, преобразованного пароля и адрес 0x8049b60, назовем их строка 1, и строка 2, а адреса на символы внутри них, соответственно, указатель 1 и указатель 2. Данная функция состоит из цикла внутри которого производится несколько проверок. В начале итерации цикла производится запись указателя строки 1 в eax, затем в edx записывается значение по указателю на. Тоже самое повторяется для строки 2, только значение записывается в eax. Затем младшие байты этих регистров сравниваются.
В случае если значения не одинаковые, производится выход из цикла и переход к адресу 0x0804867a, где значения байтов, на которые ссылаются оба указателя, проверяются на нулевое значение. В случае если оба байта имеют ненулевое значение, производится увеличение указателей на 1. В случае, если байты не равны или один из них равен 0, выполняется код по адресу 0x080486b0, в котором производится проверка значения по указателю два. Если значение равно 0, то в регистр eax записывается 0, иначе 0xffffffff или -1. Далее производится выход из функции.
Как видим данная функция просто сравнивает две строки и в случае если они одинаковые, возвращает 0, иначе -1. Также мы можем сделать вывод, что верный пароль хранится по адресу 0x8049b60. Как мы узнали ранее ее длинна составляет 6 байт, прочитаем ее.
Попробуем сделать обратное преобразование первого символа, для этого выполнив команду «? 1b^0x6c» и получим первый символ «w».
В результате мы получим строку whyn0t. Проверим ее, предварительно заменив пропатченный вариант на исходный.
Пароль верный, мы успешно решили данный crackme.
Автор: Дмитрий Лобзин