Разберемся, как реверсировать неизвестный бинарник C++ и восстанавливать скрытые структуры данных по косвенным признакам. Совместим статический анализ в Ghidra с динамическим анализом в GDB, проверим гипотезы экспериментами и разберемся, как устроено хранение коротких и длинных строк в std::string. Попутно увидим, почему в реверсе важно не доверять первой догадке и подтверждать выводы экспериментом.
В этой статье я показываю способ рассуждения на конкретном примере — простом крякме от автора Mazzotti под названием Getting started keygen.
Тебе достаточно скачать исследуемый бинарник и установить инструменты Ghidra и GDB. Я покажу варианты их применения и продемонстрирую, как слабые стороны статического анализа гармонично исправляются методами динамического анализа и наоборот.
В реверсе нет единственного правильного пути. Я покажу свой: как ухватиться за нитку в виде непонятных имен и распутать весь клубок в структуру std::string. На этом пути буду напоминать, что реверс заставляет разбираться не только с байтами и регистрами, но и с собственным мышлением. Часто мы становимся жертвами когнитивных искажений, хватаемся за первую попавшуюся подсказку, ищем только те данные, которые подтверждают нашу гипотезу, и стремимся к идеальной, но ненужной точности. По ходу разбора крякми я буду отмечать эти моменты как часть процесса анализа.
Будем выполнять статический анализ и выдвигать гипотезы, затем проверять их в динамике, корректировать и снова возвращаться к статике. В итоге напишем скрипт, который автоматизирует рутину и заодно познакомит тебя с методом активного зондирования.
После подтверждения гипотез создадим кастомную структуру в Ghidra и применим ее к исследуемому образцу.
Все начинается с переменной
Первичный осмотр
Первое, что мы сделаем, — это посмотрим на файл крякми и запустим его в изолированной среде.
Утилита file дает общую информацию:
$ file getting_started_keygen getting_started_keygen: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=226fff4aea936ab7426bf11dcd1e334c1e053104, for GNU/Linux 3.2.0, stripped
Файл динамически слинкован (внешние функции подтягиваются из разделяемых библиотек) и очищен от отладочных символов: имен функций и переменных нет. Это значит, что Ghidra будет показывать безликие FUN_xxx и local_xx, а мы будем определять их смысл вручную.
Команда strings вытаскивает из бинарника читаемые строки:
$ strings getting_started_keygen ... @0H9 Enter a string of characters (no spaces): Bro, what are you trying to do? Enter correct number (no spaces): OMG! You did it! :3 Send help pls. 9*3$' ...
Запустим бинарник в изолированной среде и посмотрим, что он от нас хочет:
$ ./getting_started_keygen Enter a string of characters (no spaces): test Bro, what are you trying to do?
Программа запросила строку, и мы ввели слово test, но оно не прошло проверку. Попробуем ввести еще что-нибудь:
$ ./getting_started_keygen Enter a string of characters (no spaces): example Enter correct number (no spaces): 5 Send help pls.
На этот раз нам повезло и программа запросила какое-то число. Правда, его мы тоже не угадали.
Находим main в Ghidra
Чтобы не гадать, загрузим бинарник в Ghidra для статического анализа. Хоть в программе и удалены отладочные символы, точка входа (entry) никуда не делась. Ghidra показывает ее в окне Symbol Tree → Functions → entry.
Это стандартная точка входа, которую компоновщик вставляет перед функцией main. Открываем ее и видим вызов __libc_start_main — стандартной функции glibc, которая вызывает функцию FUN_001011f0.
Первый аргумент __libc_start_main — это функция main. Сразу смещим имя FUN_001011f0 на main для удобства. Для этого нажимаем правой кнопкой мыши на имя и выбираем пункт Rename Function.
Мы успешно нашли функцию main, в которой видим уже знакомые строки «Enter a string of characters (no spaces):» и «Bro, what are you trying to do?».
В коде видим локальные переменные, включая local_58 размером 40 байт, который Ghidra уже определил как string. Функция проверяет длину введенной строки и выводит сообщение об ошибке, если она не превышает 5 символов.
Что говорят импорты
Прежде чем изучать декомпилированный код, посмотрим на таблицу импортов, которая содержит немало подсказок:
-
std::operator>>— оператор ввода, перегруженный для разных типов; -
std::operator<<— оператор вывода; -
std::string::string— конструкторstd::string; -
std::string::_M_dispose— метод освобождения памяти строки.
Программа активно работает со строками. К тому же Ghidra автоматически создала структуру std::string в окне Data Type Manager (вкладка Data Types → getting_started_keygen → Demangler → std).
