«Bro, what…?» #1. Загружаем бинарник в Ghidra: где прячется функция main? Серия 3
Мы остановились на том, что консольных утилит недостаточно для понимания алгоритма. Чтобы заглянуть внутрь, нам понадобится Ghidra – бесплатный инструмент от АНБ, который умеет превращать машинный код в псевдокод на C/C++. Но с чего начать анализ, если бинарник зачищен (stripped), и в списке функций нет привычного main?
Раздел 2. Знакомство с Ghidra: поиск main и первые гипотезы
В предыдущем разделе мы осмотрели файл снаружи: узнали архитектуру, извлекли строки, даже запустили программу и увидели ее поведение. Но мы так и не поняли, почему программа по-разному реагирует на test и example.
Теперь наша задача заглянуть внутрь. Для этого мы воспользуемся Гидрой – инструментом, который умеет превращать машинный код в читаемый псевдокод на C/C++.
В этом разделе мы загрузим crackme в Ghidra, найдем main через точку входа, увидим переменные local_78, local_70, local_68, изучим таблицу импортов и разберемся с деманглингом.
Начнем с загрузки файла и знакомства с интерфейсом Ghidra.
2.1. Окна Ghidra
Запускаем Ghidra и создаем новый проект: File – New Project – Non-Shared Project – Next – Finish. Затем импортируем файл: File – Import File – выбираем скачанную crackme getting_started_keygen:
Ghidra покажет окно с информацией о файле – жмем OK. Далее появится окно с краткой информацией о загруженной программе:
Двойной клик по файлу в окне Active Project открывает CodeBrowser – основное окно анализа.
Вам будет предложено провести базовый анализ файла. Просто нажмите Analyze, сейчас мы не будем вникать в настройки:
Вы увидите несколько панелей:
Познакомимся с нужными для нас:
- Symbol Tree (слева) – дерево символов. Здесь функции, переменные, импорты. Это наша карта навигации.
- Decompiler (справа) – декомпилятор. Показывает псевдокод на C. Это то, что мы будем читать большую часть времени. Ghidra пытается восстановить высокоуровневую логику из машинного кода.
- Listing (по центру) – листинг ассемблера. Сырой машинный код с инструкциями. Когда декомпилятор ошибается или показывает странности, мы смотрим сюда.
- Console (внизу) – консоль Ghidra. Показывает сообщения анализатора, ошибки, предупреждения.
Если раскрыть раздел Functions в Symbol Tree, то вы не найдете понятных имен, например, main:
Вместо этого имена вроде FUN_001011f0. Это потому что наш бинарник stripped – отладочная информация удалена. Но не волнуйтесь, функцию main мы найдем.
2.2. Поиск main через точку входа
В первом разделе извлекли строки и нашли среди них Enter a string of characters (no spaces):. Теперь давайте найдем функцию, которая их выводит. Но вместо того чтобы искать строки напрямую, пойдем другим путем – от точки входа в программу.
2.2.1. Точка входа в программу
Когда операционная система запускает программу, она не передает управление сразу в функцию main. Сначала выполняется служебная функция _start (в Ghidra она называется entry), которая подготавливает окружение: инициализирует стек, загружает библиотеки, настраивает обработчики сигналов. И только потом _start вызывает main.
2.2.2. Находим entry в Ghidra
В окне Symbol Tree раскрываем раздел Functions и находим функцию entry. Это первая функция, с которой начинается выполнение программы. Двойной клик переносит нас в окно Listing, где мы видим ассемблерный код:
Ассемблер оставим на потом. Посмотрите на код в декомпиляторе:
Смотрим на вызов __libc_start_main. Это стандартная функция из библиотеки libc, которая:
- Инициализирует стандартные потоки ввода-вывода (stdin, stdout, stderr).
- Вызывает конструкторы глобальных объектов C++.
- Вызывает функцию main.
- Завершает программу с кодом возврата из main.
Первый аргумент __libc_start_main – это указатель на функцию main. В нашем случае это FUN_001011f0.
Двойной клик по FUN_001011f0 переносит нас в тело функции. Смотрим на заголовок в окне – она называется FUN_001011f0.
Правый клик на имени функции – Rename Function (или клавиша L) – вводим main. Теперь в декомпиляторе видим понятное имя.
Смотрим на декомпилированный код функции main:
Видим знакомую строку "Enter a string of characters (no spaces): " – ту самую, что мы нашли через strings в первом разделе. Видим условие if (5 < local_70 - 5U), которое выводит "Bro, what are you trying to do?" – именно эту фразу видели при запуске с короткой строкой.
Обратите внимание на блок инициализации прямо перед запросом ввода:
Три переменные с похожими именами (local_78, local_70, local_68) подготавливаются к работе строго одна за другой. А сразу после ввода данных переменная local_70 используется в проверке, которая определяет, пропустит ли программа нас дальше.
Выглядит это как единый механизм, но пока мы не знаем, как именно они связаны. Являются ли они независимыми переменными или частями чего-то большего? Запомним этот паттерн. Одно можно сказать точно: local_70 участвует в проверке, очень похожей на проверку длины строки. Но это пока лишь наблюдение.
Посмотрим, какие еще подсказки дает нам таблица импортов, чтобы сузить круг поиска.
Конец третьей серии
Мы нашли main через точку входа и увидели странные переменные: local_78, local_70, local_68. Они инициализируются строго друг за другом, а затем участвуют в проверке длины строки. Являются ли они независимыми переменными или частями чего-то большего? Чтобы сузить круг поиска, давайте посмотрим на таблицу импортов.
Продолжение в следующей серии...
P.S.
Полный текст статьи: