«Bro, what…?» #1. Загружаем бинарник в Ghidra: где прячется функция main? Серия 3

«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
Окно импортирования бинарника в проект Ghidra

Ghidra покажет окно с информацией о файле – жмем OK. Далее появится окно с краткой информацией о загруженной программе:

Результат импортирования бинарника в проект Ghidra
Результат импортирования бинарника в проект Ghidra

Двойной клик по файлу в окне Active Project открывает CodeBrowser – основное окно анализа.

Вам будет предложено провести базовый анализ файла. Просто нажмите Analyze, сейчас мы не будем вникать в настройки:

Первичный анализ загруженного бинарника
Первичный анализ загруженного бинарника

Вы увидите несколько панелей:

Внешний вид CodeBrowser
Внешний вид CodeBrowser

Познакомимся с нужными для нас:

  • Symbol Tree (слева) – дерево символов. Здесь функции, переменные, импорты. Это наша карта навигации.
  • Decompiler (справа) – декомпилятор. Показывает псевдокод на C. Это то, что мы будем читать большую часть времени. Ghidra пытается восстановить высокоуровневую логику из машинного кода.
  • Listing (по центру) – листинг ассемблера. Сырой машинный код с инструкциями. Когда декомпилятор ошибается или показывает странности, мы смотрим сюда.
  • Console (внизу) – консоль Ghidra. Показывает сообщения анализатора, ошибки, предупреждения.

Если раскрыть раздел Functions в Symbol Tree, то вы не найдете понятных имен, например, main:

Дерево символов крякми getting_started_keygen
Дерево символов крякми getting_started_keygen

Вместо этого имена вроде 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, где мы видим ассемблерный код:

Листинг функции entry нашей crackme
Листинг функции entry нашей crackme

Ассемблер оставим на потом. Посмотрите на код в декомпиляторе:

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. Это стандартная функция из библиотеки libc, которая:

  • Инициализирует стандартные потоки ввода-вывода (stdin, stdout, stderr).
  • Вызывает конструкторы глобальных объектов C++.
  • Вызывает функцию main.
  • Завершает программу с кодом возврата из main.

Первый аргумент __libc_start_main – это указатель на функцию main. В нашем случае это FUN_001011f0.

Двойной клик по FUN_001011f0 переносит нас в тело функции. Смотрим на заголовок в окне – она называется FUN_001011f0.

Правый клик на имени функции – Rename Function (или клавиша L) – вводим main. Теперь в декомпиляторе видим понятное имя.

Изменение имени функции
Изменение имени функции

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

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); } // ... }

Видим знакомую строку "Enter a string of characters (no spaces): " – ту самую, что мы нашли через strings в первом разделе. Видим условие if (5 < local_70 - 5U), которое выводит "Bro, what are you trying to do?" – именно эту фразу видели при запуске с короткой строкой.

Обратите внимание на блок инициализации прямо перед запросом ввода:

local_78 = local_68;

Три переменные с похожими именами (local_78, local_70, local_68) подготавливаются к работе строго одна за другой. А сразу после ввода данных переменная local_70 используется в проверке, которая определяет, пропустит ли программа нас дальше.

Выглядит это как единый механизм, но пока мы не знаем, как именно они связаны. Являются ли они независимыми переменными или частями чего-то большего? Запомним этот паттерн. Одно можно сказать точно: local_70 участвует в проверке, очень похожей на проверку длины строки. Но это пока лишь наблюдение.

Посмотрим, какие еще подсказки дает нам таблица импортов, чтобы сузить круг поиска.

Конец третьей серии

Мы нашли main через точку входа и увидели странные переменные: local_78, local_70, local_68. Они инициализируются строго друг за другом, а затем участвуют в проверке длины строки. Являются ли они независимыми переменными или частями чего-то большего? Чтобы сузить круг поиска, давайте посмотрим на таблицу импортов.

Продолжение в следующей серии...

P.S.

Полный текст статьи:

Подписывайтесь на мой канал в Telegram или ВКонтакте.