Главная / Блог / Реверс-инжиниринг в Ghidra для начинающих: разбираем первый crackme пошагово

16 мин.00

Реверс-инжиниринг в Ghidra для начинающих: разбираем первый crackme пошагово

Реверс-инжиниринг в Ghidra для начинающих: разбираем первый crackme пошагово

Реверс-инжиниринг в Ghidra для начинающих: разбираем первый crackme пошагово

Второй crackme в моей практике занял двадцать минут — при том что первый сожрал три с лишним часа. Сложность примерно одинаковая: оба ELF x86_64, оба с проверкой пароля через strcmp, оба без упаковки. Разница — в последовательности действий. strings по бинарнику, переход по перекрёстной ссылке на строку «Wrong», три переименования переменных в декомпиляторе — и условие флага как на ладони. Реверс-инжиниринг в Ghidra для начинающих не требует глубокого знания ассемблера: декомпилятор берёт на себя основную работу. Эта статья — пошаговый разбор crackme в Ghidra по методологии, которая превращает часы блуждания по интерфейсу в минуты целенаправленного анализа.

Требования к окружению

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

  • ОС: Windows 10/11 или Linux (Ubuntu 20.04+). На Linux утилиты file, strings, ltrace доступны из коробки — это ускоряет первый этап анализа. Ghidra работает одинаково на обеих платформах.
  • RAM: минимум 4 ГБ, рекомендуется 8 ГБ. Ghidra крутится на JVM — при автоанализе бинарников средней сложности съедает 2–3 ГБ.
  • JDK 21+ — через пакетный менеджер (sudo apt install openjdk-21-jdk) или с adoptium.net.
  • Ghidra 11.x — скачивается с github.com/NationalSecurityAgency/ghidra/releases. Установки нет: распаковка ZIP и запуск ghidraRun (Linux) или ghidraRun.bat (Windows).
  • Виртуальная машина — обязательна для работы с незнакомыми бинарниками. VirtualBox или VMware Player, сеть внутри ВМ выключена.
  • Опционально: x64dbg (Windows) или GDB с плагином GEF (Linux) для динамического анализа, когда статического недостаточно.

Ghidra — фреймворк от NSA, выпущенный как open-source в 2019 году. Главное преимущество перед IDA Free (версия 9.x): бесплатный встроенный декомпилятор, который превращает машинный код в C-подобный псевдокод. В IDA Free декомпилятор Hex-Rays недоступен — только дизассемблер и Graph View. Для reverse CTF для начинающих разница критична: без декомпилятора придётся читать чистый ассемблерный листинг с первого дня. Из альтернатив — radare2 (текущая версия 6.2.0), мощный консольный фреймворк со своей кривой обучения. Для первого crackme это лишний барьер: CLI radare2 — отдельный навык, который не стоит осваивать параллельно с реверсом. Я пробовал — голова распухает от двух незнакомых интерфейсов одновременно.

Что такое crackme и где брать задачи

Crackme — специально собранный бинарник для тренировки навыков обратной разработки. Два основных типа: password checker (программа просит пароль, при правильном вводе выдаёт сообщение об успехе или флаг) и flag checker (программа проверяет, является ли введённая строка корректным флагом). Подход к обоим одинаковый: найти функцию проверки и восстановить из неё условие прохождения.

Основной источник задач — crackmes.one. Сотни crackme разной сложности с фильтрацией по платформе (Linux, Windows), языку (C, C++, Assembly) и рейтингу. Для старта берите задачи с рейтингом 1.0–2.0 под Linux — они собраны GCC без обфускации и решаются базовой методологией. PicoCTF и HackTheBox тоже содержат категорию reverse — от совсем простых до продвинутых.

На crackmes.one к каждой задаче прикладываются решения от других пользователей. Не подглядывайте до первой попытки, но если застряли на два часа — чужой writeup покажет, где именно вы свернули не туда. Это не читерство, это обучение.

Методология: как подходить к незнакомому бинарнику

Reverse engineering с нуля часто выглядит как хаотичное тыканье по дизассемблеру. Системный подход экономит часы. Вот последовательность, которая работает на большинстве CTF-задач начального и среднего уровня:

Шаг 1 — внешняя разведка. Запустить file, strings, ltrace до открытия Ghidra. Иногда задача решается прямо на этом этапе.

Шаг 2 — определить препятствия. Бинарник упакован (UPX, ASPack)? Stripped (нет символов)? Статически слинкован? Каждый случай требует отдельного подхода до начала анализа логики.

Шаг 3 — найти точку входа в логику. Не начинать с main. Начинать с Defined Strings: найти строку обратной связи («Wrong», «Correct»), перейти по xref к функции-обработчику.

Шаг 4 — восстановить логику проверки. Внутри целевой функции искать: приём ввода (scanf, fgets), трансформацию ввода (XOR, сдвиги, хеширование), сравнение с эталоном (strcmp, memcmp, прямой cmp).

Шаг 5 — извлечь или обойти. Два пути: вычислить правильный ввод из логики (чистый путь) или пропатчить условный переход JNZJZ (грязный путь). На CTF засчитываются оба.

Попытка сразу нырнуть в main и читать код сверху вниз — типичная ошибка новичка. Строки и xref дают точечный вход в нужную функцию, минуя сотни строк инициализации и библиотечного кода. Порядок имеет значение.

Разведка до дизассемблера: file, strings, ltrace

Статический анализ бинарных файлов не начинается с Ghidra. Тридцать секунд в терминале экономят полчаса внутри дизассемблера.

file crackme — показывает формат, архитектуру, тип линковки. Результат вроде ELF 64-bit LSB executable, x86-64, dynamically linked определяет, какие инструменты применимы дальше. Видите statically linkedltrace не поможет. Видите stripped — в Ghidra не будет имён функций.

strings crackme | grep -i "pass\|flag\|correct\|wrong" — вытаскивает читаемые строки и фильтрует по ключевым словам. По опыту решения задач на crackmes.one и PicoCTF — удивительное количество задач начального уровня решаются одной этой командой: пароль или флаг лежит в бинарнике открытым текстом. Если в выводе мелькает flag{...} или CTF{...} — проверяйте немедленно. Бывает обидно потратить час в Ghidra, а потом обнаружить, что пароль валялся в .data в чистом виде.

ltrace ./crackme — перехватывает вызовы библиотечных функций при запуске. Вводите любой пароль и в терминале видите strcmp("test", "r3alP@ss") = 1 — оба аргумента на экране. Ограничения: ltrace доступен только на Linux, работает с динамически слинкованными бинарниками. GCC иногда инлайнит strcmp, делая его невидимым для перехвата. Но проверка занимает две секунды — пропускать нет смысла.

file crackme
# ELF 64-bit LSB executable, x86-64, dynamically linked
strings crackme | grep -i "pass\|flag\|correct\|wrong"
# Wrong password!
# Access Granted!
ltrace ./crackme <<< "test"
# strcmp("test", "S3cr3tP@ss") = -1  # ненулевое значение означает несовпадение

Три команды закрывают каждый пятый crackme начального уровня без Ghidra. Если strings и ltrace не дали пароля — он зашифрован, трансформирован или генерируется в рантайме. Тут начинается дизассемблирование.

Импорт и статический анализ бинарного файла в Ghidra

Ghidra организована через проекты: File → New Project → Non-Shared Project → указать директорию и имя. Затем File → Import File → выбрать бинарник.

При импорте Ghidra автоматически определяет формат (ELF, PE, Mach-O), архитектуру и разрядность. Появляется окно с метаданными — для типовых CTF-бинарников менять ничего не нужно.

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

Результат: в Symbol Tree появляется список обнаруженных функций (включая main, если бинарник не stripped), в Imports — перечень используемых API (strcmp, printf, fgets). Эти два списка уже дают представление о том, что бинарник делает, до чтения единой строки кода.

Как пользоваться Ghidra: навигация в CodeBrowser

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

Symbol Tree (левая панель) — дерево символов: функции, импорты, экспорты. Разверните ветку Functions, найдите main, двойной клик переместит обе центральные панели к этой функции. Раздел Imports — моментальный снимок возможностей бинарника. Видите strcmp — программа сравнивает строки. Видите scanf или fgets — принимает ввод. Если в импортах WriteProcessMemory или VirtualAlloc — бинарник может модифицировать код в рантайме (и тут уже стоит насторожиться).

Listing (центральная панель) — листинг ассемблерного кода. Адреса слева, мнемоники справа: MOV, CMP, JNE, CALL. На начальном этапе используйте эту панель как справочную: основная работа идёт в декомпиляторе. Двойной клик на адресе CALL переносит внутрь вызываемой функции, Alt+← возвращает назад.

Decompile (правая панель) — псевдокод на C. Клик на функции в Symbol Tree показывает здесь её декомпилированный вариант. Подсветка синхронизирована между панелями: выделяете строку в декомпиляторе — в Listing подсвечивается соответствующий блок ассемблера. Для изучения реверс-инжиниринга эта связка бесценна: видите if (iVar1 == 0) справа, смотрите CMP и JNZ слева — связь между высокоуровневым и низкоуровневым кодом становится осязаемой. После пары десятков таких сопоставлений ассемблер перестаёт выглядеть набором случайных символов.

Defined Strings (Window → Defined Strings) — все текстовые строки бинарника. «Wrong password», «Correct!», «Access Granted!» — отсюда начинается поиск флага в CTF. Двойной клик на строке перемещает Listing на адрес её хранения.

Function Graph (Window → Function Graph) — граф потока управления, аналог Graph View в IDA. Цветные стрелки между блоками показывают ветвление: зелёная — условие истинно, красная — ложно. При первом открытии масштаб слишком мелкий — правый клик → Properties → Start Fully Zoomed In исправляет это. Нажатие F1 на любом элементе интерфейса вызывает контекстную справку — пользуйтесь, пока осваиваетесь.

Декомпиляция в Ghidra: от undefined8 к читаемому псевдокоду

Декомпилятор не знает, как автор назвал переменные. Вместо passwordlocal_28. Вместо осмысленного типа — undefined8 (8 байт данных с неопределённым типом; для возвращаемого значения main это почти всегда int). Глобальные переменные именуются по адресу: DAT_00104020. Функции без отладочных символов — FUN_00401234.

Выглядит как стена из случайных имён. Три приёма кардинально меняют ситуацию:

Переименование переменных. Правый клик → Rename Variable (или клавиша L). Увидели, что local_28 передаётся вторым аргументом в strcmp? Переименуйте в user_input. Строка DAT_00104020 содержит "Enter password:"? Назовите prompt_msg. Ghidra синхронизирует переименования между декомпилятором и листингом автоматически. После пяти-десяти таких правок нечитаемый псевдокод превращается в программу, логика которой очевидна. При разборе crackme в Ghidra этот момент — как если бы туман рассеялся. И он стоит каждой минуты.

Изменение типов. Правый клик на тип → Retype Variable. undefined8int, undefined4char *. Декомпилированный код мгновенно становится чище: вместо арифметики над неизвестными типами — нормальные операции со строками и числами.

Правка сигнатур функций. Правый клик на функции → Edit Function Signature. Укажите возвращаемый тип, количество и типы параметров. Ghidra пересчитает декомпиляцию и во вложенных вызовах. Если FUN_00401234 принимает char * и возвращает int — после правки сигнатуры код внутри этой функции тоже станет понятнее.

Код из окна декомпилятора копируется напрямую — удобно для writeup-ов и CTF-отчётов.

Решение crackme пошагово: поиск условия флага

Находим целевую функцию через Defined Strings

Не начинайте с main. Начинайте со строк.

Откройте Window → Defined Strings. Найдите строку обратной связи — «Wrong password», «Try Again», «Correct!», «Access Granted». Двойной клик по строке перемещает Listing на адрес её хранения. Правый клик → References → Show References to Address — Ghidra покажет список функций, ссылающихся на эту строку.

Перейдите по xref (перекрёстной ссылке). Вы окажетесь внутри функции, которая выводит это сообщение — целевой функции, где проверяется ввод. В декомпиляторе справа уже виден её псевдокод. Этот приём — ядро дизассемблер Ghidra туториал-методологии: строки ведут прямо к логике, минуя десятки служебных функций.

Если строка используется в нескольких местах, Ghidra покажет все xref — выберите тот, где рядом видны вызовы strcmp, memcmp или инструкции CMP. Это и есть место проверки.

Восстанавливаем логику проверки пароля

Типичный password checker после автоанализа и переименования переменных выглядит в декомпиляторе так (пример на основе типовых паттернов с crackmes.one):

void main(void) {
    char user_input[64];
    puts("Enter password:");
    fgets(user_input, 64, stdin);
    if (strcmp(user_input, s_S3cr3tP@ss_00104020) == 0) {
        puts("Access Granted!");
    } else {
        puts("Wrong password!");
    }
}

Логика на поверхности: ввод сравнивается с захардкоженной строкой. Двойной клик на s_S3cr3tP@ss_00104020 перемещает к строке в памяти — вот и пароль.

Crackme посложнее трансформируют ввод перед сравнением. В декомпиляторе это выглядит как цикл между fgets и strcmp. Внутри цикла — операция трансформации: XOR, арифметический сдвиг, побитовая операция. Задача — найти эту операцию и применить обратную к эталонной строке.

Конкретный пример из описания FatMike's CrackMe на fewstreet.com: бинарник содержал цикл, XOR-ивший каждый символ ввода с массивом-константой длиной 24 байта. Результат передавался в функцию, которую автор идентифицировал как CRC32 по характерному полиному 0xedb88320. Финальная проверка: if (crc_result == 0x5a6aa47d && input_length == 0x16). Два условия одновременно — длина строки 22 символа и совпадение CRC32-хеша XOR-результата. Без Ghidra и переименования переменных эта цепочка выглядела бы как стена из DAT_ и FUN_.

Два пути — вычислить пароль или пропатчить переход

Вычислять пароль из логики — чистый путь. Инвертировать условный переход — грязный. На CTF оба засчитываются, грязный нередко быстрее.

В декомпиляторе видите if (strcmp(...) == 0). В Listing эта конструкция — пара инструкций: CALL strcmpTEST EAX, EAXJNZ addr (jump if not zero — перепрыгнуть блок «Correct», если строки не совпали). Правый клик на JNZ → Patch Instruction → заменить на JZ. Логика инвертирована: любой неправильный пароль считается правильным.

Пример из разбора CrackMe на freecodecamp.org: автор патчил три инструкции — JNSJMP для обхода антиотладки (ptrace), JZJMP для пропуска валидации символов, JNZJZ для инверсии финального сравнения. Три байта — и программа выдала пароль при любом вводе.

Экспорт пропатченного бинарника: File → Export Program → формат Original File → сохранить. Запускаете, вводите произвольную строку — программа выдаёт флаг.

Когда патчинг не работает: если программа проверяет собственную целостность (self-checksumming), если флаг вычисляется из самого введённого пароля (правильный ввод — часть вычисления), или если бинарник использует антиотладочные техники, срабатывающие при модификации кода. Тогда — только полный реверс логики.

Типичные паттерны проверки в crackme

После десятков решённых задач паттерны начинают повторяться. Пять основных конструкций, которые встречаются в crackme начального и среднего уровня, и как они выглядят в декомпиляторе Ghidra:

Прямое сравнение строк. strcmp(user_input, hardcoded_string). Самый простой вариант — строка-эталон лежит в секции .data открытым текстом. Решается strings до открытия Ghidra или одним кликом в Defined Strings.

XOR-кодирование. Цикл вида for (i = 0; i < len; i++) input[i] ^= key[i % key_len] с последующим сравнением результата с массивом-эталоном. В декомпиляторе распознаётся по оператору ^ внутри цикла. Решение: взять эталонный массив из памяти (двойной клик на адрес → Data → выбрать тип char[N]), XOR-ить его тем же ключом — получается оригинальный пароль. XOR — операция обратная сама себе, что делает этот паттерн одним из самых приятных для решения.

Посимвольная проверка. Вместо единого strcmp — серия условий: if (input[0] != 'A') goto fail; if (input[1] != 'B') goto fail; ... или цикл if (input[i] != expected[i] + offset). Каждое условие содержит один символ пароля. Собираете их последовательно — пароль готов.

Хеш-сравнение. Ввод хешируется кастомной функцией, результат сравнивается с числовой константой. Признак: вложенная функция с магическим числом (0xedb88320 для CRC32, 0x5381 или 0x1505 для DJB2-вариантов). Если хеш-функция простая — реверсите алгоритм. Если сложная — брутфорс по символам с проверкой хеша.

Многоэтапная трансформация. Несколько FUN_-функций вызываются последовательно, каждая модифицирует буфер. Из описания crackme на freecodecamp.org: одна функция копировала ввод (кастомный memcpy), вторая генерировала эталон из строки-константы THEPASSWORDISEASYTOCRACK, затем шло сравнение через strcmp. Каждую FUN_-функцию нужно декомпилировать отдельно, переименовать и понять её роль в цепочке.

Общий принцип: ищите константы. Магические числа, строки в .data, фиксированные ключи — это опорные точки, от которых раскручивается вся логика.

Stripped-бинарники и упакованные файлы: когда стандартный подход не работает

Не у каждого бинарника есть main в Symbol Tree. Stripped-бинарники — файлы с удалённой отладочной информацией — содержат адреса без имён. Этот приём аналогичен технике Stripped Payloads (MITRE ATT&CK T1027.008), которую атакующие используют для затруднения анализа payload'ов. В контексте crackme это не атака, а особенность сборки.

Что делать: Defined Strings по-прежнему работают. Строки «Enter password» или «Wrong» никуда не исчезают при стрипе — они хранятся в секции данных отдельно от символов функций. Найдите строку → перейдите по xref → окажетесь в безымянной FUN_00401234. Переименуйте её в main или check_password и работайте по стандартной схеме. Отсутствие имён добавляет пять минут работы, не больше.

Упаковка. Пустой список Defined Strings и секции UPX0/UPX1 в Program Tree вместо стандартных .text/.data — признак UPX-упаковки. В таком состоянии Ghidra не находит ни одной функции кроме _entry. По данным разбора CrackMe#1 на fewstreet.com, CFF Explorer подтвердил упаковку UPX 2.90, после распаковки командой upx -d packed.exe -o unpacked.exe импорт в Ghidra дал полноценный список строк и функций. Если file или первые байты файла содержат UPX! — попробуйте распаковать. В CTF-задачах начального уровня этого хватает за глаза. Пакеры посерьёзнее (ASPack, Themida) требуют ручной распаковки через отладчик — отдельная тема.

Статическая линковка. Imports пуст, Ghidra находит сотни безымянных FUN_-функций — библиотечный код вкомпилирован прямо в бинарник. Встроенный плагин Function ID Database (Analysis → One Shot → Function ID) сопоставляет сигнатуры с известными библиотеками и переименовывает функции автоматически. После его прогона появляются знакомые printf, strcmp, malloc — и бинарник снова читаем.

Ограничения статического анализа: когда Ghidra недостаточно

Ghidra — инструмент статического анализа. Программа не запускается, код не выполняется. Это безопасно (критично для анализа малвари, использующей техники обфускации — см. MITRE ATT&CK T1027), но имеет границы.

Обфусцированный поток управления. Если автор crackme использует непрямые вызовы (вычисляемые адреса переходов, виртуальные машины, self-modifying code), декомпилятор Ghidra выдаёт кашу. Тут нужен отладчик: x64dbg на Windows или GDB с GEF на Linux. Ставите брейкпоинт на strcmp или memcmp — отладчик покажет аргументы в момент вызова, даже если статически определить их невозможно.

Антиотладка. Некоторые crackme проверяют наличие отладчика через ptrace(PTRACE_TRACEME, ...) (Linux) или IsDebuggerPresent() (Windows). В Ghidra эти вызовы видны в импортах и декомпилированном коде — их можно пропатчить до запуска.

Ключи, генерируемые в рантайме. Если пароль вычисляется из системного времени, MAC-адреса или других рантайм-данных — статический анализ даст алгоритм, но не конкретное значение. Динамический анализ покажет результат вычисления для конкретного запуска.

Для первых десяти-пятнадцати crackme на crackmes.one эти ограничения не встретятся. Но знать о них стоит, чтобы не тратить часы на попытки декомпилировать то, что декомпилятору не по зубам.


За полтора года решения CTF-задач по реверсу пришёл к выводу: большинство новичков тормозят не на анализе, а на входном барьере. Типичная история — открыть книгу по x86-ассемблеру, три недели зубрить мнемоники, потерять мотивацию до загрузки первого бинарника. Это неработающая последовательность.

Декомпилятор Ghidra сейчас достаточно хорош, чтобы начинать с псевдокода и параллельно осваивать ассемблер. Видишь if (iVar1 == 0) в декомпиляторе — смотришь CMP EAX, 0 и JNZ в листинге слева. Через пару десятков сопоставлений связь между C и машинным кодом ложится в голову без зубрёжки. Через сотню — читаешь листинг на автомате.

Проблема преподавания reverse engineering с нуля — в порядке подачи. Академический путь идёт сверху вниз: архитектура процессора → формат ELF → система команд → практика (если хватит терпения). Работающий путь обратный: взять crackme сложности 1.0 с crackmes.one, прогнать strings, загрузить в Ghidra, найти strcmp — и получить дофаминовый удар от первого решённого задания. Теория подтягивается, когда появляются вопросы из практики: «а что такое JNZ?», «почему EAX обнулился?». Вопросы, выросшие из реального разбора, запоминаются в разы быстрее ответов, заученных заранее.

Первые пять crackme на crackmes.one — один вечер. После него возвращаться к ассемблеру уже не страшно, а интересно. Попробуйте — и посчитайте, сколько минут уйдёт на второй.

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

Поделиться

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

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

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