Содержание статьи
- Все начинается с переменной
- Первичный осмотр
- Находим main в Ghidra
- Что говорят импорты
- Нитка, за которую будем тянуть, — переменная local_70
- Карта стека: где лежат переменные
- Оптимизированный пролог
- Карта стека главной функции main
- Проверка local_70 в GDB
- Что скрывается в local_68?
- Активное зондирование переменной
- Ручной эксперимент
- Скрипт, который делает все за нас
- Стратегия удвоения
- Экспериментальное подтверждение от обратного
- Эталонная программа
- Смотрим на std::string в GDB
- Активное зондирование эталонной программы
- Что видит Ghidra в эталонной программе
- Имена на основе анализа
- Union и реконструкция структуры std::string
- Union: одна память для разных типов
- Small String Optimization в libstdc++
- Создаем структуру в Ghidra
- Самопроверка: что в исходниках GCC
- Применяем структуру к крякми
- Как Ghidra понимает, что использовать
- Заключение
std:: string. Заодно увидим, почему в реверсе важно не доверять первой догадке и подтверждать выводы экспериментом.В этой статье не ставится цель охватить все возможные структуры данных, которые могут встретиться в бинарниках C++. Моя задача — показать способ рассуждения, а чтобы не теоретизировать, я взял конкретный пример простой крякми от автора Mazzotti — Getting started keygen.
Тебе достаточно скачать исследуемый бинарник и установить инструменты Ghidra и GDB. Я покажу варианты их применения и продемонстрирую, как слабые стороны статического анализа (Ghidra) гармонично исправляются методами динамического анализа (GDB) и наоборот.
В реверсе нет единственного правильного пути, поэтому я покажу свой: как ухватиться за нитку в виде непонятных имен и распутать весь клубок в структуру std::. На этом пути я буду напоминать, что реверс заставляет разбираться не только с байтами и регистрами, но и с собственным мышлением. Мы часто становимся жертвами когнитивных искажений, хватаемся за первую попавшуюся подсказку, ищем только те данные, которые подтверждают нашу гипотезу (предвзятость подтверждения), и стремимся к идеальной, но ненужной точности (склонность к перфекционизму). По ходу разбора крякми я буду отмечать эти моменты как часть процесса анализа.
Мы будем выполнять статический анализ и выдвигать гипотезы, затем проверять их в динамике, корректировать и снова возвращаться к статике. В итоге мы напишем скрипт, который автоматизирует рутину и заодно познакомит тебя с методом активного зондирования.
После подтверждения гипотез мы создадим кастомную структуру в 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 → Functions → entry.

Это стандартная точка входа, которую компоновщик вставляет перед функцией main. Открываем ее и видим вызов __libc_start_main — стандартной функции glibc, которая вызывает функцию FUN_001011f0:
void processEntry entry(undefined8 param_1,undefined8 param_2){ undefined1 auStack_8 [8]; __libc_start_main(FUN_001011f0,param_2,&stack0x00000008,0,0,param_1,auStack_8); do { /* WARNING: Do nothing block with infinite loop */ } while( true );}Первый аргумент __libc_start_main — это функция main. Сразу сменим имя FUN_001011f0 на main для удобства. Для этого нажимаем правой кнопкой мыши на имя и выбираем пункт Rename .

Мы успешно нашли функцию main, в которой видим уже знакомые строки «Enter a string of characters (no spaces):» и «Bro, what are you trying to do?».
undefined8 main(void){ int iVar1; ostream *poVar2; long in_FS_OFFSET; int local_7c; undefined1 *local_78; long local_70; undefined1 local_68 [16]; string local_58 [40]; long local_30; local_30 = *(long *)(in_FS_OFFSET + 0x28); local_78 = local_68; local_68[0] = 0; local_70 = 0; /* try { // try from 0010123e to 00101327 has its CatchHandler @ 0010132f */ poVar2 = std::operator<<((ostream *)std::cout,"Enter a string of characters (no spaces): "); FUN_00101430(poVar2); std::operator>>((istream *)std::cin,(string *)&local_78); if (5 < local_70 - 5U) { std::operator<<((ostream *)std::cout,"Bro, what are you trying to do?"); FUN_00101430(); /* WARNING: Subroutine does not return */ exit(0); } // Остальной код нас пока не интересует...}Мы на правильном пути. Осталось за что‑нибудь зацепиться и продолжить анализ.
Что говорят импорты
Прежде чем изучать декомпилированный код, посмотрим на таблицу импортов, которая содержит не меньше подсказок:
std::— оператор ввода, перегруженный для разных типов;operator> > std::— оператор вывода;operator< < std::— конструкторstring:: string std::;string std::— метод освобождения памяти строки.string:: _M_dispose

Программа активно работает со строками. К тому же Ghidra автоматически создала структуру std:: в окне Data (вкладка Data → getting_started_keygen → Demangler → std).

Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
