Использование фреймворка Ghidra

Ghidra

Здравствуйте! В предыдущей статье мы рассказывали о фреймворке для реверс-инжиниринга Ghidra. Сегодня я поделюсь с вами опытом использования Ghidra на примере взлома крекми MalwareTech. Я не случайно выбрал именно ее. В одной из своих статей я рассказывал о том, как устроена виртуализация кода, и мы даже написали простенькую виртуалку. А сейчас я покажу, как взломать такую защиту используя Ghidra.

Как использовать фреймворк Ghidra

Скачать crackme можно с официального сайта MalwareTech, пароль к архиву — тоже MalwareTech.

Итак, с начала посмотрим, что в архиве. В архиве лежит исполняемый файл vm1.exe, файл дампа ram.bin и readme.txt, в котором написано, что мы имеем дело с восьмибитной виртуальной машиной. Файл дампа — не что иное, как кусок памяти, в котором вперемешку расположены рандомные данные и флаг, который нам необходимо отыскать. На время оставим файл дампа и посмотрим на vm1.exe через программу DiE.

Крэкми в анализаторе Detect It Easy
Крэкми в анализаторе Detect It Easy

DiE не показывает ничего интересного, с энтропией все в порядке. Значит, никакой навесной защиты нет, но проверить все равно стоило. Давайте загрузим этот файл в Ghidra и посмотрим, что она выдаст. Я приведу полный листинг приложения без функций (он совсем небольшой) — чтобы вы поняли, с чем мы имеем дело.

Как видите, код простой и легко читается. Давайте воспользуемся декомпилятором Ghidra и посмотрим, что он выдаст.

Я добавил отступы для удобочитаемости — отделил объявления переменных от остального кода. Код весьма простой: сначала выделяется память в куче GetProcessHeap -> HeapAlloc, далее в нее копируется 0x1fb(507) байт из DAT_00404040. Но у нас нет ничего интересного в 00404040! Вспоминаем, что в инструкции к крэкми говорилось, что ram.bin — это кусок памяти. Разумеется, если посмотреть размер файла, он оказывается равным 507 байт.

Загружаем ram.bin в HxD или любой другой шестнадцатеричный редактор и смотрим.

Файл ram.bin в HxD Hex Editor
Файл ram.bin в HxD Hex Editor

Увы, ничего внятного там не обнаруживаем. Но логика работы немного проясняется: DAT_0040423c — это ram.bin (наши выделенные 507 байт в куче). Давайте переименуем DAT_0040423c в RAM, чтобы было удобнее ориентироваться в коде. Далее заходим в функцию FUN_004022e0.

Графическое представление функции FUN_004022e0
Графическое представление функции FUN_004022e0

Вот декомпилированный код функции:

Поскольку мы все-таки знаем, что перед нами виртуальная машина, все становится более-менее понятно. Но чтобы действительно понять псевдокод, всегда нужно смотреть в дизассемблер, иначе псевдокод может запутать.

Использование ghidra. Псевдокод и дизассемблер Ghidra
Псевдокод и дизассемблер Ghidra

Я выделил инструкции, которые выполняют инкремент переменных на единицу. Помним, что у нас есть функция FUN_00402270, которая инициализируется тремя параметрами. Смотрим инициализацию первого параметра.

Очевидно, что берется байт из [RAM] и им инициализируется переменная. И такой же код при инициализации каждого аргумента функции, единственное отличие — меняются регистры, в которых будут аргументы функции FUN_00402270. В итоге вызов функции выглядит таким образом:

Итак, в FUN_00402270 передаются три параметра — три байта из [RAM], следующие друг за другом. Заходим в функцию FUN_00402270, вот ее псевдокод:

Здесь проверяется первый переданный в функцию байт, и, если он совпадает с 0x1, 0x2 или 0x3, обрабатываются следующие два аргумента. Парсинг первого параметра особенно явно читается в дизассемблерном листинге. По всей видимости, это интерпретатор команд виртуальной машины, который содержит всего три команды ВМ.

Использование ghidra. Графическое представление интерпретатора в Ghidra
Графическое представление интерпретатора в Ghidra

На этом этапе я остановлюсь немного подробнее, чтобы подвести промежуточный итог. Итак, мы имеем приложение, работающее с 507 байт памяти, дамп которых у нас есть — это ram.bin. Внутри этого дампа данные, интересные нам, перемешаны с другими, ненужными нам данными. Приложение vm1.exe читает побайтово память в поисках инструкций 0x1, 0x2 и 0x3, и, как только одна из них находится, обрабатываются следующие два байта после них.

Другими словами, мы имеем мнемонические команды (p-code, пи-код), которые работают со своими двумя аргументами, а область памяти в 507 байт — не что иное, как лента пи-кода, перемешанная с мусором. На самом деле не стоит пугаться мусора — обработка команд начнется с нахождения нужного байта опкода, и будут взяты следующие два значения, а мусор попросту пропущен.

P-code, или «пи-код», — реализация мнемоник для собственного интерпретатора команд. Его еще называют кодом «гипотетического процессора» — ведь, по сути, процессор для исполнения пи-кода написан кем-то самостоятельно.

Теперь давайте разберем запрограммированные опкоды команд, парсинг которых выполняет код, показанный выше. Я буду сразу приводить код на языке C, аналогичный дизассемблерному листингу.

Начнем восстанавливать логику работы виртуальной машины. Объявим char ram[507] — это будет память виртуальной машины. В этот массив при помощи функций fopen → fread → fwrite запишем содержимое файла ram.bin. Четыре строчки ассемблерного кода и переход — все просто: в массив ram по значению [EBP + param_2] перемещаем значение param_3. В коде это будет выглядеть таким образом:

Начинаем анализировать следующую подпрограмму:

Она очень похожа на предыдущую, это тоже аналог операции MOV, но здесь уже используется один из двух регистров виртуальной машины (DAT_00404240 в листинге), в который кладется значение из памяти ВМ. А с нашей точки зрения — из массива ram, который адресован param_2 в дизассемблерном коде, а в нашем — val_01. Другими словами, операция MOV reg,[mem].

Последняя подпрограмма в два раза сложнее — вместо четырех строчек кода здесь восемь! Мы берем значение из памяти (помните про наш массив ram, куда мы записали содержимое ram.bin?) и сохраняем его в регистр виртуальной машины (EDX), далее берем первое значение после мнемоники в пи-коде (ECX) и выполняем между ними операцию XOR. Результат кладем обратно в память.

На языке C это будет выглядеть таким образом:

Вот, собственно, и все. Виртуальная машина из трех команд восстановлена, осталось применить результаты нашего труда к файлу ram.bin, чтобы заполучить искомый флаг крэкми. Как я уже говорил, для этого читаем файл в char ram[507] и применяем декомпилятор кода ВМ. В качестве бонуса цикл выведет мнемоники виртуальной машины в удобочитаемом виде, а в конце напечатает искомый флаг. Я добавил в код уточняющие комментарии.

После выполнения этого кода мы получим дизассемблированную ВМ и флаг.

Результат работы восстановленной виртуальной машины
Результат работы восстановленной виртуальной машины

Заключение

Я надеюсь, что, прочитав статью, вы перестанете пугаться слов «виртуальная машина» или «пи-код». Конечно, в настоящих коммерческих протекторах вроде VMProtect или Themida все будет намного сложнее: там может применяться множество команд виртуальной машины, их мнемоники-коды могут постоянно меняться, встречаются виртуальные машины, разные антиотладочные и антидамповые приемы, написанные на пи-коде, и многое другое. Но первое представление вы получили.

Заодно мы более близко познакомились с инструментарием под названием Ghidra и совершили с помощью нее первый взлом, пусть даже крэкми!

Дима (Kozhuh)

Эксперт в кибербезопасности. Работал в ведущих компаниях занимающихся аналитикой компьютерных угроз. Анонсы новых статей в Телеграме.

Добавить комментарий