Главная / Блог / Реверс-инжиниринг в Ghidra для начинающих: находим скрытый флаг в CTF-бинарнике

12 мин.00

Реверс-инжиниринг в Ghidra для начинающих: находим скрытый флаг в CTF-бинарнике

Реверс-инжиниринг в Ghidra для начинающих: находим скрытый флаг в CTF-бинарнике

Реверс-инжиниринг в Ghidra для начинающих: находим скрытый флаг в CTF-бинарнике

Три reverse-задачи с crackmes.one за один вечер — первую ковырял полтора часа, вторую закрыл за двадцать минут, третью решил за пять. Все три — ELF x86-64, все три — проверка флага с XOR-обфускацией, примерно одинаковая сложность. Разница не в знании ассемблера наизусть, а в конкретном алгоритме работы с декомпилятором: куда кликнуть, какое окно открыть, как прочитать псевдокод и накидать solver на Python за минуту. Реверс-инжиниринг в Ghidra для начинающих — это не про зубрёжку мнемоник x86, а про этот алгоритм. Дальше — полный разбор на практическом примере.

Требования к окружению для анализа ELF файлов в Ghidra

Работает если: JDK 21+ установлен, Ghidra 11.x скачана с GitHub, минимум 4 ГБ RAM. Не работает если: JDK ниже 17, RAM менее 4 ГБ — Ghidra на JVM намертво зависает при автоанализе бинарников крупнее 5 МБ. Подробнее — в нашем подробном разборе бинарный анализ уязвимостей.

  • ОС: Linux (Ubuntu 20.04+) или Windows 10/11. Ghidra кроссплатформенна, все шаги воспроизводимы на обеих системах.
  • JDK 21: на Linux — sudo apt install openjdk-21-jdk, проверка — java -version должна показать 21.x. На Windows — установщик с adoptium.net.
  • Ghidra 11.x: ZIP-архив с github.com/NationalSecurityAgency/ghidra/releases. Инсталлятора нет — распаковка и запуск ./ghidraRun (Linux) или ghidraRun.bat (Windows).
  • RAM: 4 ГБ — минимум, 8 ГБ — рекомендация. Ghidra на JVM жрёт 1.5–3 ГБ при автоанализе типовых CTF-бинарников.
  • Виртуальная машина: обязательна при анализе незнакомых бинарников вне CTF-контекста. VirtualBox или VMware Workstation Player, сеть внутри ВМ отключена.
  • Дополнительно: утилита ltrace (перехват библиотечных вызовов, обычно предустановлена на Linux), Python 3 для solver-скриптов.

Разведка до дизассемблера: три команды за 30 секунд

Прежде чем запускать Ghidra и ждать автоанализ, три команды в терминале могут закрыть задачу мгновенно — или дать контекст, который сэкономит полчаса блужданий по дизассемблеру.

Первая — file crackme. Показывает формат (ELF или PE), архитектуру (x86-64, ARM, MIPS), тип линковки и ключевую деталь: stripped бинарник или not stripped. Если not stripped — в Ghidra будут имена функций, включая main. Если stripped — готовьтесь искать точку входа вручную, об этом ниже.

Вторая — strings crackme | grep -i flag. По опыту задач на picoCTF и crackmes.one — удивительное количество задач начального уровня решается именно этой командой. Пароль или флаг тупо лежит в бинарнике открытым текстом. Строки формата flag{...}, CTF{...}, характерные фразы Correct!, Wrong password, Access Granted — всё это подсказки, указывающие на функцию проверки. Даже если флаг не в открытом виде, строки дают карту бинарника: какие сообщения программа выводит, какие файлы открывает, какие библиотеки тянет.

Третья — ltrace ./crackme. Перехватывает вызовы библиотечных функций в реальном времени. Если программа сравнивает ввод через strcmp, ltrace покажет оба аргумента: strcmp("ваш_ввод", "настоящий_пароль"). Работает если: бинарник динамически слинкован, strcmp не заинлайнен компилятором. Не работает если: статическая линковка, или gcc оптимизировал strcmp в inline-код — ltrace вызов не перехватит. Проверка занимает две секунды — пропускать нет смысла.

Если все три команды не выдали флаг — он обфусцирован или собирается в рантайме. Переходим к Ghidra.

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

Запуск ./ghidraRun открывает менеджер проектов. Последовательность: File → New Project → Non-Shared Project (работаете один) → выбор директории и имени → OK. Далее File → Import File → указываете бинарник. Ghidra сама определит формат и архитектуру — для стандартных ELF и PE менять параметры не нужно.

Двойной клик по импортированному файлу открывает CodeBrowser — основное рабочее окно. Ghidra спросит «Analyze now?» — соглашайтесь с дефолтами. Автоанализ занимает от нескольких секунд до пары минут: за это время Ghidra определяет границы функций, строит перекрёстные ссылки (xref), вытаскивает текстовые строки и опознаёт библиотечные вызовы.

Четыре окна для статического анализа бинарных файлов

Интерфейс CodeBrowser выглядит перегруженным — десятки панелей, кнопок, деревьев. Для решения CTF-задач по реверсу хватает четырёх окон, остальные закрывайте без сожалений.

Symbol Tree (левая панель) — дерево символов: функции, импорты, экспорты, метки. Здесь ищете main — раскрываете ветку Functions, находите main, двойной клик перемещает оба центральных окна к этой функции. Разверните Imports — увидите используемые API-вызовы. ptrace намекает на антиотладку, fopen/fread — на работу с файлами, socket/connect — на сетевое взаимодействие. Моментальный снимок возможностей бинарника ещё до чтения кода.

Listing (центральная панель) — дизассемблированный код с адресами слева и мнемониками справа: MOV, CMP, JNE, CALL. Критическая деталь: подсветка строки здесь синхронизирована с декомпилятором. Кликаете на CMP EAX, 0 в листинге — справа подсвечивается соответствующий if (result == 0). Для тех, кто учится читать ассемблер, эта связка «мнемоники слева — C-код справа» бесценна: видите JNZ и сразу понимаете, что это if (...) goto.

Decompiler (правая панель) — псевдокод на C, сгенерированный декомпилятором Ghidra. Главное оружие новичка в реверсе. Вместо десятков ассемблерных инструкций — C-подобный код. Не идеальный: переменные именуются iVar1, local_28, типы иногда определяются как undefined8 — но читаемый после минимальной работы руками.

Defined Strings (Window → Defined Strings) — список всех текстовых строк из бинарника. Сообщения пользователю, пути к файлам, захардкоженные ключи. Двойной клик на строке переносит к ней в листинге, затем правый клик → References → Show References to Address — и вы внутри функции, которая эту строку использует. Часто это и есть функция проверки пароля.

Декомпиляция кода Ghidra: переименование как ключевой навык

Декомпилятор не знает оригинальных имён переменных. Вместо password вы видите local_28, вместо char input[100]char local_78[104], вместо осмысленного типа — undefined8. Глобальные переменные получают имена по адресу: DAT_00104020 — ячейка памяти по адресу 0x00104020, и пока вы её не переименуете, псевдокод остаётся стеной из шума.

Переименование — навык, который превращает эту стену в понятную программу. Правый клик на переменной → Rename Variable (или клавиша L). Видите, что local_28 передаётся вторым аргументом в strcmp? Переименуйте в user_input. Строка по адресу DAT_00104020 содержит "Enter password:"? Назовите prompt_msg. Функция FUN_00401234 проверяет ввод? Дайте ей имя check_input.

Тип undefined8 — 8 байт данных, которые Ghidra не смогла опознать автоматически. Для возвращаемого значения main это практически всегда int. Правый клик на типе → Retype Variable → int. Аналогично для параметров функций: Edit Function Signature позволяет задать типы аргументов, и декомпилированный код мгновенно становится читабельнее.

После 5–10 переименований псевдокод из нечитаемого блока превращается в обычный C-код. Ghidra синхронизирует имена между декомпилятором и листингом автоматически — переименовали переменную справа, она обновилась слева. Процесс кажется рутиной, но это и есть ядро работы реверсера: вы не «ломаете» бинарник, а постепенно восстанавливаете смысл программы. (Звучит медитативно — и на практике так и есть.)

Два типа CTF-задач по реверсу: password checker и flag checker

Типичная задача категории reverse для начинающих: дан бинарник, нужно найти флаг. Определить тип задачи — первое, что стоит сделать, потому что от этого зависит весь подход.

Password checker — программа принимает пароль на вход, при правильном вводе печатает флаг. Цель: найти пароль или обойти проверку. Пароль часто виден в аргументе strcmp/memcmp в открытом виде или вычисляется из константы. Альтернативный путь — пропатчить условный переход, заставив программу принять любой ввод.

Flag checker — программа принимает сам флаг как ввод и проверяет его корректность. Обход проверки бесполезен: даже если заставить бинарник напечатать «Correct!», флаг вы не получите — он и был вашим вводом. Здесь единственный путь — вскрыть алгоритм проверки из декомпилятора и написать обратное преобразование.

Быстрая диагностика в декомпиляторе: видите strcmp(user_input, "some_string") — password checker, "some_string" и есть пароль. Видите цикл, который поэлементно трансформирует ввод и сравнивает результат с массивом байт — flag checker, нужен solver-скрипт.

Пошаговый разбор бинарника в Ghidra: от импорта до флага

Разберём типичный сценарий — flag checker с XOR-обфускацией. Этот паттерн встречается в каждой второй задаче начального уровня. В терминах MITRE ATT&CK обфускация данных внутри бинарника — Obfuscated Files or Information (T1027): данные скрыты от простого просмотра утилитой strings.

Шаг 1 — внешняя разведка. file выводит ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped. Хорошо — имена функций доступны. strings crackme | grep -i flag не находит явного флага — обфусцирован. ltrace ./crackme показывает fgets, puts, но strcmp не вызывается — значит, сравнение поэлементное, без библиотечной функции.

Шаг 2 — импорт и автоанализ. Создаём проект в Ghidra, импортируем бинарник, запускаем автоанализ с дефолтными настройками. Ждём 10–30 секунд.

Шаг 3 — находим точку входа. Symbol Tree → Functions → main. Двойной клик. Декомпилятор показывает псевдокод. Через Defined Strings находим "Correct" и "Wrong" — переходим по xref к функции, которая их использует. Это или main, или функция, вызываемая из main.

Шаг 4 — читаем и переименовываем. После переименования переменных типичный flag checker выглядит так:

char user_input[32];
int expected[] = {0x48, 0x4e, 0x49, 0x47, 0x83, 0x82};
puts("Enter the flag:");
fgets(user_input, 32, stdin);
for (int i = 0; i < 6; i++) {
    if (((user_input[i] ^ 5) + 5) != expected[i]) {
        puts("Wrong!"); return;
    }
}
puts("Correct!");

Логика: каждый символ ввода XOR'ится с 5, к результату прибавляется 5, затем сравнивается с элементом массива expected. Обратная операция: берём байт из expected, вычитаем 5, XOR'им с 5 — получаем исходный символ флага.

Шаг 5 — solver на Python:

expected = [0x48, 0x4e, 0x49, 0x47, 0x83, 0x82]
flag = ''.join(chr((b - 5) ^ 5) for b in expected)
print(flag)

Запускаем — получаем флаг. Подаём его в бинарник — видим Correct!. Весь процесс от открытия Ghidra до флага занял десять минут, три из которых — автоанализ.

Stripped-бинарник: поиск флага в CTF без символов

Если file показывает stripped, Symbol Tree не содержит main. В терминах MITRE ATT&CK удаление символов из бинарника — техника Stripped Payloads (T1027.008): затруднение анализа путём удаления отладочной информации. Ghidra найдёт функции, но назовёт их FUN_00401000, FUN_00401050 — и дальше разбирайтесь сами.

Приём 1 — через строки. Window → Defined Strings → находите "Enter password" или "Wrong" → двойной клик → References → Show References to Address. Ссылка ведёт к функции-обработчику. Работает если: бинарник содержит текстовые строки (почти всегда на CTF). Не работает если: строки тоже обфусцированы — тогда Defined Strings не покажет ничего полезного.

Приём 2 — через entry и __libc_start_main. Находите функцию entry в Symbol Tree. Внутри неё первый аргумент вызова __libc_start_main — адрес main. Двойной клик по адресу, переименовываете функцию. Работает если: ELF, скомпилированный gcc с glibc — подавляющее большинство CTF-задач на Linux. Не работает если: статическая линковка без glibc или нестандартный CRT (musl, dietlibc).

Приём 3 — Function ID. Analysis → One Shot → Function ID. Ghidra сопоставит функции с известными библиотечными сигнатурами. Часть FUN_-функций получит осмысленные имена, оставшиеся «неопознанные» — кандидаты на main и пользовательскую логику. По сути — Ghidra отсеивает библиотечный мусор, и вам остаётся только авторский код.

Обфускация сложнее XOR: паттерны среднего уровня

XOR с константой — простейший случай. На задачах среднего уровня встречаются паттерны, которые выглядят страшнее, но решаются по тому же принципу «записать обратный алгоритм»:

  • XOR с ключом переменной длины — каждый байт ввода XOR'ится с байтом ключа по модулю его длины: input[i] ^ key[i % key_len]. В декомпиляторе виден как цикл с операцией %. Обратная операция идентична — XOR симметричен.
  • Побитовые сдвиги и ротацииROL/ROR в ассемблере, в декомпиляторе — (x << n) | (x >> (8 - n)). Обратная операция — сдвиг в противоположную сторону.
  • Сравнение через хеш — программа хеширует ввод и сравнивает с эталоном. Если хеш самописный — часто обратим. Если стандартный (SHA, MD5) — brute force коротких строк с маской по формату флага.
  • Anti-debug через ptrace — бинарник вызывает ptrace(PTRACE_TRACEME, 0, 0, 0) и меняет поведение при отладке. Для статического анализа в Ghidra это не помеха: вы читаете код, а не запускаете его.

Принцип для любой обфускации один: декомпилятор показывает прямой алгоритм проверки. Ваша задача — записать обратный. Если прямая операция (input ^ key) + offset == expected, обратная — (expected - offset) ^ key == input. Математика уровня пятого класса, просто записанная в hex.

Патчинг условного перехода: грязный путь для password checker

Для password checker — когда нужен не сам пароль, а флаг, который программа печатает после успешной проверки — есть путь быстрее полного анализа алгоритма.

В Listing найдите условный переход после strcmp или cmp: это JNZ (jump if not zero) или JNE (jump if not equal). Правый клик → Patch Instruction → замените JNZ на JZ — инвертируете условие. Экспорт через File → Export Program → формат Original File. Запускаете пропатченный бинарник — программа принимает любой ввод.

Работает если: password checker, флаг печатается программой после проверки. Не работает если: flag checker — патч покажет «Correct!», но флаг останется неизвестным, потому что программа его не хранит, а только проверяет ваш ввод.

На CTF оба подхода засчитываются одинаково. Патчинг экономит минуты на блиц-раундах. Грязный путь — не позорный. Позорный — ковырять алгоритм сорок минут, когда задачу можно было закрыть инверсией одного байта.

За пару лет решения reverse-задач я пришёл к выводу, который вызывает споры: 80% задач начального и среднего уровня решаются без глубокого знания ассемблера. Декомпилятор Ghidra сделал реверс доступным для людей без низкоуровневого бэкграунда. Да, undefined8 и DAT_00104020 выглядят пугающе. Но после десятка переименований вы читаете C-подобный код, а не стену мнемоник.

Типичная ошибка новичка — начинать с изучения интерфейса: кнопки, плагины, скрипты, настройки. Ловушка. Интерфейс Ghidra осваивается за два-три решённых бинарника. Навык, который реально определяет скорость — «прочитать декомпилятор → понять алгоритм → написать обратное преобразование» — нарабатывается только практикой. Не чтением статей, не просмотром YouTube, а конкретным решением задач. Возьмите пять crackme уровня 1–2 на crackmes.one и пройдите их подряд. Первый — мучительно. Второй — уже виден паттерн. К пятому ориентируетесь в Ghidra на автопилоте. Ассемблер при этом учится сам собой — через синхронизацию листинга и декомпилятора, без зубрёжки справочников. Те, кто тратит месяц на «теоретическое изучение x86» перед первым реверсом — делают ровно наоборот.

🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».

Поделиться

0 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...