
На последнем воркшопе по реверсу три из десяти участников застряли на первой же задаче: ELF-бинарник просил пароль, Ghidra показывала декомпилированный код, но вместо привычного C — стена из local_38, param_1, DAT_001020a0. Через пятнадцать минут разбора каждый решил задачу. Проблема была не в сложности бинарника — просто никто до этого не объяснил, как читать то, что выплёвывает декомпилятор. Эта статья — тот самый разбор, от запуска Ghidra до найденного пароля, без пропущенных шагов.
Прежде чем импортировать первый бинарник, убедитесь, что машина потянет анализ. Ghidra написана на Java и при активной декомпиляции жрёт ресурсов ощутимо больше, чем кажется по размеру архива: Подробнее — в нашем обзоре бинарный анализ уязвимостей.
Скачайте ZIP-архив последнего релиза с официального GitHub-репозитория Ghidra. Никакого инсталлятора — распакуйте архив и запустите ghidraRun (Linux/macOS) или ghidraRun.bat (Windows). При первом запуске укажите путь к JDK. Ghidra активно поддерживается: коммиты в репозитории появляются еженедельно, релизы выходят каждые несколько месяцев.
Для сборки из исходников (свежая master-ветка с последними исправлениями) потребуется Gradle версии, указанной в DevGuide.md репозитория. Команды: gradle -I gradle/support/fetchDependencies.gradle init (загрузка зависимостей), затем gradle buildGhidra — дистрибутив окажется в build/dist. На Linux с русской локалью GCC может споткнуться о переведённые сообщения — лечится префиксом LANG=C gradle .... Но для Ghidra обучения и первых CTF задач готового релиза хватит за глаза — сборка из исходников оправдана, когда нужны свежие фиксы декомпилятора.
Запустили Ghidra — перед вами окно Project Manager. Создайте проект: File → New Project → Non-Shared Project. Укажите папку и имя (например, ctf_practice). Проект в Ghidra — контейнер для всех результатов анализа: дизассемблированный код, переименованные переменные, комментарии. Работает как IDB-файл в IDA Pro, но хранит данные для нескольких бинарников сразу, поддерживает совместную работу через удалённую базу и позволяет переиспользовать библиотеки типов между файлами.
Перетащите файл crackme в окно проекта или выберите File → Import File. Ghidra сама определит формат (ELF, PE, Mach-O) и покажет диалог с метаданными: архитектура (x86-64, ARM, MIPS), компилятор (GCC, MSVC), тип линковки (static/dynamic). Для типичного CTF-бинарника увидите ELF / x86:LE:64:default / GCC. Жмём OK.
Ghidra предложит запустить автоматический анализ — соглашайтесь. Набор анализаторов сделает основную грязную работу: найдёт функции, построит перекрёстные ссылки (cross-references, они же xrefs), вытащит строки, восстановит сигнатуры стандартных библиотечных вызовов. На crackme размером 10–50 КБ анализ занимает секунды. Дефолтные настройки подходят для CTF reverse engineering задач в подавляющем большинстве случаев — не трогайте ничего, пока не разберётесь, за что отвечает каждый анализатор.
Двойной клик на импортированном файле открывает CodeBrowser — основное рабочее окно для анализа бинарных файлов. Ключевые панели:
Listing (центр) — дизассемблированный код. Инструкции mov, cmp, call, jz — машинный язык процессора в читаемом виде.
Decompiler (справа) — псевдокод C, восстановленный декомпилятором. Если окно не открыто: Window → Decompile. Это главный инструмент для разбора логики — именно здесь вы будете проводить 80% времени.
Symbol Tree (слева) — дерево функций, импортов, экспортов. Ищите main здесь — это точка входа программы. Если бинарник «not stripped», имена функций будут человекочитаемыми.
Defined Strings — список строк, найденных в бинарнике. Открывается через Window → Defined Strings. Именно с этого окна начинается 90% разборов CTF задач — строки типа «Enter password» или «Wrong!» ведут прямо к интересующей функции.
Все окна синхронизированы: клик на строку в декомпиляторе подсвечивает соответствующий блок ассемблера, и наоборот. Удобно переключаться между высокоуровневым псевдокодом и низкоуровневым листингом, когда что-то выглядит подозрительно.
Это ключевой навык для решения CTF reverse engineering задач. Псевдокод C декомпилятора — не исходный код: компилятор необратимо уничтожает имена переменных, комментарии, часть типовой информации. Но для понимания логики crackme этого уровня абстракции хватает с запасом. Декомпилятор Ghidra восстанавливает циклы, условия, вызовы функций — и позволяет найти проверку пароля, не читая ни строки ассемблера.
Когда декомпилятор показывает local_38, iVar1, param_2 — это не шифр. Это автоименование по фиксированным правилам:
param_1, param_2 — параметры текущей функции. В main(int param_1, char **param_2) первый — argc (число аргументов командной строки), второй — argv (массив строк-аргументов). Если функция принимает один аргумент типа указатель — param_1 и будет этим указателем.
local_XX — локальные переменные на стеке. Число XX — смещение от начала стекового кадра в hex. local_38 означает: переменная лежит на расстоянии 0x38 (56) байт от базового указателя rbp. Размер намекает на тип: local_10 на x86-64 (8 байт) — скорее всего указатель или long; local_1 (1 байт) — char или bool; local_38 с объявлением char[40] — строковый буфер.
iVar1, uVar2, cVar3 — временные переменные. Префикс указывает тип: i = int, u = unsigned int, l = long, c = char, b = bool. Ghidra выводит тип из контекста использования.
DAT_001020a0 — глобальная переменная или константа по фиксированному адресу в памяти. Двойной клик перенесёт к этому адресу в листинге, где часто обнаруживается строковая константа с паролем или сообщением. Вот тут-то и начинается самое интересное.
FUN_00101234 — функция, для которой имя не удалось определить (символьные таблицы ELF отсутствуют или stripped). Число — адрес точки входа.
Правый клик на переменной → Rename Variable (или клавиша L) — и local_38 превращается в user_input. Это не косметика: переименование делает логику программы читаемой для человека. Ghidra сохраняет все переименования в проекте и обновляет каждое вхождение по всему коду. Аналогично работает с функциями: FUN_00101234 → check_password — и каждый вызов этой функции в проекте станет осмысленным.
Начинайте с того, что уже понимаете. Видите printf("Enter password: ") — значит, следующая переменная, куда пишет fgets или scanf, это буфер пользовательского ввода. Переименуйте. Видите strcmp() — переименуйте его аргументы. Через десять переименований загадочная стена превращается в понятную программу.
В задачах по обратной разработке проверка реализуется одним из характерных способов:
strcmp() или strncmp() с захардкоженной строкой. Простейший вариант — пароль лежит в открытом тексте прямо в бинарнике.== с эталонным значением. Пароль разбросан по коду, но каждый символ виден.0xedb88320 в коде. Увидите такое число — знайте, это CRC32.Разберём типовой crackme — ELF-бинарник x86-64, который при запуске просит ввести пароль и выводит результат проверки. Такие задачи составляют основу категории reverse на PicoCTF, HackTheBox и crackmes.one.
Перед загрузкой в дизассемблер выполните две команды в терминале. Первая — file crackme — покажет тип: ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped. Фраза «not stripped» — хорошая новость: символьные таблицы ELF сохранены, имена функций (main, check, verify) будут видны в Symbol Tree. Stripped-бинарник — дополнительная головная боль. Кстати, та же техника (намеренное удаление символов) используется атакующими в малвари — MITRE ATT&CK описывает её как Stripped Payloads (T1027.008).
Вторая команда — strings crackme | grep -i "pass\|flag\|correct\|wrong" — иногда сразу показывает строки-маркеры. Но полагаться только на strings не стоит: в задачах посложнее строки зашифрованы или генерируются в рантайме.
В CodeBrowser откройте Window → Defined Strings. Ищите строки, связанные с результатом: «Access granted», «Wrong password», «Correct», «Try again». Нашли «Wrong password!»? Это якорь — теперь нужно определить, какая функция ссылается на эту строку.
Кликните на найденную строку правой кнопкой → References → Show References to Address. Ghidra покажет список мест в коде, где строка упоминается — обычно одно или два. Двойной клик перенесёт в функцию с логикой проверки. Метод поиска через строки и xrefs — фундаментальный приём статического анализа, работающий в любом дизассемблере: Ghidra, IDA, Radare2.
В сыром виде перед переименованием Ghidra покажет примерно следующее:
undefined8 FUN_00101169(void) {
char local_38[40];
printf("Enter password: ");
fgets(local_38, 0x28, _stdin);
if (strcmp(local_38, &DAT_00102010) == 0) {
puts(&DAT_00102025);
} else {
puts(&DAT_00102035);
}
return 0;
}
Вот она — та самая «стена», на которой застревают новички. Ключевое действие: двойной клик на DAT_00102010. Ghidra перенесёт к адресу, где хранятся данные — и вы увидите строку-пароль в виде ASCII-текста. После переименования переменных и функций (клавиша L на каждом элементе) тот же код превращается в понятный псевдокод:
void main(void) {
char user_input[40];
printf("Enter password: ");
fgets(user_input, 0x28, stdin);
if (strcmp(user_input, "CTF{r3v3rs3_101}") == 0) {
puts("Access granted!");
} else {
puts("Wrong password!");
}
}
Логика прозрачна: fgets читает ввод в буфер, strcmp сравнивает со строкой "CTF{r3v3rs3_101}". Совпадение (возвращается 0) — доступ открыт. Пароль найден без чтения ассемблера.
Запустите бинарник в виртуальной машине и введите найденный пароль. Если не срабатывает — вероятно, fgets читает ввод вместе с символом \n, а strcmp сравнивает строго. Передайте пароль через echo -n "CTF{r3v3rs3_101}" | ./crackme — флаг -n убирает перенос строки.
Не каждый crackme отдаёт пароль через Defined Strings. Если список строк пуст или содержит мусор — бинарник, вероятно, обфусцирован или упакован. Тут начинается следующий уровень.
Откройте Program Tree в левой панели CodeBrowser. Если вместо стандартных секций .text, .data, .rodata видны секции UPX0, UPX1 — бинарник сжат упаковщиком UPX. В классификации MITRE ATT&CK это Software Packing (T1027.002): реальный код сжат и распаковывается в память при запуске. Автор CTF writeup по FatMike's CrackMe (fewstreet.com) столкнулся именно с этим: Ghidra не нашла ни одной функции кроме _entry, потому что весь значимый код был запакован.
Решение для UPX тривиально: ставите его (apt install upx на Kali/Ubuntu) и выполняете upx -d crackme -o crackme_unpacked. Загружаете распакованный файл в Ghidra — строки и функции появятся. Для нестандартных упаковщиков без публичного распаковщика потребуется ручная распаковка или динамический анализ с дампом памяти — но это уже тема для отдельного разбора.
В задачах среднего уровня пароль или флаг хранится в зашифрованном виде и расшифровывается перед сравнением. Типичный признак в декомпиляторе — цикл, где каждый байт массива XOR'ится с ключом: конструкция вида result[i] = encrypted[i] ^ key[i % key_len]. MITRE ATT&CK описывает это как Deobfuscate/Decode Files or Information (T1140). В разборе FatMike's CrackMe автор нашёл именно такой XOR-цикл с 24-символьным ключом. Видите подобный паттерн — извлеките массив encrypted и значение key из бинарника и напишите три строки на Python для расшифровки. Серьёзно, три строки.
Некоторые crackme проверяют наличие отладчика: вызов ptrace(PTRACE_TRACEME) в Linux, IsDebuggerPresent() в Windows, чтение /proc/self/status на предмет TracerPid. MITRE ATT&CK классифицирует это как Debugger Evasion (T1622). (Кстати, в Atomic Red Team тесты для T1622 реализованы только под Windows, хотя сама техника применима и на Linux.) При статическом анализе в Ghidra эти проверки видны в псевдокоде, но не срабатывают — Ghidra не запускает код. Антиотладочные трюки на статический анализ не действуют. Это важное преимущество подхода: вы препарируете бинарник в безопасной среде, без риска выполнения чего-то неприятного.
Ghidra — не единственный выбор для обратной разработки. Trade-off таблица для осознанного выбора инструмента:
| Критерий | Ghidra | IDA Free | Radare2 |
|---|---|---|---|
| Стоимость | Бесплатно, open source | Бесплатно (урезанная версия) | Бесплатно, open source |
| Декомпилятор | Встроенный, полноценный | Нет (IDA Pro от $500+) | Через плагины (r2ghidra, r2dec) |
| Интерфейс | GUI на Java | Нативный GUI | Командная строка |
| Порог входа | Средний | Низкий | Высокий (сотни команд) |
| Встроенный отладчик | Экспериментальный | Нет (в бесплатной версии) | Полноценный |
| Скриптинг | Java, Python | IDAPython доступен | Python, множество языков |
| Поддержка архитектур | Десятки (x86, ARM, MIPS, PPC) | Ограничен в Free | Десятки |
| Статус поддержки | Активный, еженедельные коммиты | Коммерческий продукт | Активная разработка |
| Когда использовать | CTF, обучение, анализ прошивок, малвари | Быстрый визуальный анализ графов | Автоматизация, скриптинг, CLI-workflow |
| Когда НЕ использовать | Нужен надёжный встроенный отладчик | Нужна декомпиляция бесплатно | Первое знакомство с RE |
Для CTF reverse engineering задач Ghidra — оптимальный выбор. Бесплатный декомпилятор — решающее преимущество. Лицензия IDA Pro стоит от $500 (по данным blog.ry4n.org), что делает её недоступной для студентов и тех, кто только входит в реверс. IDA Free даёт отличный graph view для визуализации потока управления, но без декомпилятора решение задач требует чтения чистого ассемблера — а это совсем другой уровень. Radare2 мощен для автоматизации через скрипты, но его CLI-интерфейс с сотнями однобуквенных команд — слишком крутой порог для новичка. Если в будущем понадобится динамический анализ — добавьте GDB (Linux) или x64dbg (Windows) как отдельный инструмент.
Знать, где инструмент врёт, важнее, чем знать его возможности. Вот что регулярно ломается при декомпиляции бинарника в Ghidra:
Декомпилятор путает типы указателей: массив char[] отображается как int*, структура — как набор отдельных переменных. В разборе CrackMe от FatMike (fewstreet.com) автор обнаружил, что Ghidra трактовала массив из 24 символов как 24 отдельных глобальных переменных (DAT_xxxxxx, DAT_xxxxxx+1, ...). Решение: выделите адрес в листинге, нажмите C (Clear), затем T (Retype) и задайте char[24]. Псевдокод C мгновенно станет читаемым.
Автоанализ иногда не распознаёт блок байтов как функцию — особенно при косвенных вызовах или обфускации. В том же FatMike's CrackMe Ghidra не нашла перекрёстных ссылок на ключевую функцию check_input, потому что вызов был замаскирован внутри switch-конструкции. Решение: перейдите к неразобранным байтам, нажмите D (Disassemble), затем F (Define Function). После переанализа (Analysis → Auto Analyze) связи восстановятся.
Компилятор с флагом -O2 или -O3 инлайнит функции, разворачивает циклы, переставляет инструкции для конвейера процессора. Декомпилятор пытается восстановить логику, но результат может быть далёк от оригинала. Если конструкция в псевдокоде выглядит абсурдно — переключитесь на листинг ассемблера. Ассемблер отражает реальный машинный код и никогда не врёт.
Ghidra не запускает код. Если пароль генерируется динамически — на основе текущего времени, MAC-адреса, идентификатора системы — статический анализ покажет алгоритм генерации, но не конечное значение. Для таких случаев нужен динамический анализ: отладчик GDB или x64dbg, точка останова на момент сравнения, чтение значения из регистра. Статический и динамический подходы дополняют друг друга — большинство реальных задач требует обоих.
Минимальный алгоритм решения типового crackme:
file crackmestrings crackme | grep -i "pass\|flag"strcmp, strncmp, циклы сравненияНавыки чтения псевдокода и работы с перекрёстными ссылками из этого разбора — не только CTF. Те же приёмы работают при анализе реальной малвари: вредоносы хранят адреса C2-серверов в захардкоженных строках (MITRE ATT&CK: Credentials In Files, T1552.001), прячут конфигурацию за XOR-обфускацией (T1027, Obfuscated Files or Information), пакуют код UPX'ом или кастомным паковщиком. Разница между CTF-задачей и реальным сэмплом — в масштабе и уровне защиты, не в методе.
По моему опыту, основной барьер для входа в обратную разработку — не инструменты и не ассемблер. Барьер — непонимание C. Большинство людей, которые «не могут» решить reverse-задачу, не знают, что strcmp возвращает 0 при совпадении строк. Не понимают, чем fgets отличается от scanf. Не видели, как for-цикл превращается в пару cmp/jl инструкций. Мой совет неизменен уже второй год: прежде чем учиться реверсу, потратьте неделю на C. Напишите программу, которая принимает пароль через argv, проверяет его и печатает результат. Скомпилируйте с -O0 и с -O2. Откройте оба варианта в Ghidra. Посмотрите, во что превратился ваш код. Когда увидите собственный printf("Hello") в виде puts("Hello") после оптимизации — декомпилятор перестанет быть чёрным ящиком. Обратная разработка начинается с прямой: кто не писал код, тот не прочтёт его после компилятора. Если хочешь пройти этот путь системно, а не собирать по кускам — на IB Basics закрывают базу за два месяца, от нуля до первых реальных задач.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...