Разберемся, как реверсировать неизвестный бинарник 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 TreeFunctionsentry.

Это стандартная точка входа, которую компоновщик вставляет перед функцией 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 Typesgetting_started_keygenDemanglerstd).