Главная / Блог / Reverse engineering в Ghidra: crackme с нуля

12 мин.00

Reverse engineering в Ghidra: crackme с нуля

Reverse engineering в Ghidra: crackme с нуля

На crackmes.one лежат сотни задач для тренировки реверса — после решения нескольких десятков замечаешь закономерность: проверка пароля в бинарниках начального уровня сводится к одной из трёх схем. Открытый strcmp, XOR с фиксированным ключом, побайтовое сравнение в цикле. Сами схемы элементарные. Проблема в другом: интерфейс Ghidra с десятком панелей и переменными iVar1 выглядит как кабина пилота — до самой проверки человек просто не добирается, потому что тонет в окнах. Здесь — полный маршрут: от запуска бинарника в терминале до извлечённого пароля через декомпилятор Ghidra. Без воды, с конкретным crackme.

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

Каждый crackme стоит начинать не с Ghidra, а с командной строки. Три утилиты экономят от секунд до часов — и иногда закрывают задачу целиком. Подробнее — в нашем обзоре бинарный анализ уязвимостей.

file crackme — первая точка контакта. Покажет формат (ELF, PE, Mach-O), архитектуру (x86-64, ARM, MIPS), тип линковки и наличие символов. Если в выводе stripped — в Ghidra не будет функции main в Symbol Tree, искать точку входа придётся через строки. Если statically linked — бинарник тащит библиотечные функции внутри себя, список функций в дизассемблере будет длиннющим.

strings crackme | grep -i flag — вторая проверка. Выводит все читаемые строки: сообщения пользователю, пути к файлам, иногда пароли открытым текстом. По опыту решения задач на crackmes.one, заметная часть бинарников начального уровня закрывается этой командой за секунды. Даже когда пароль не лежит в открытом виде, строки "Wrong!" и "Correct!" подскажут, какие функции потом искать через перекрёстные ссылки (xrefs) внутри Ghidra.

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

На CTF-блицах разница между первым и десятым местом измеряется секундами: кто начал с strings — решает задачу за десять секунд. Кто сразу полез в Ghidra — за десять минут.

Тестовый crackme с XOR-проверкой

Для демонстрации разбора crackme в Ghidra возьмём бинарник с XOR-кодированием — схема, которую суют в большинство CTF reverse заданий начального и среднего уровня. В реальной задаче исходного кода у вас не будет, но для обучения reverse engineering полезно видеть обе стороны — что автор написал и что показывает декомпилятор:

#include <stdio.h>
int main() {
    char input[16], key[] = {0x1B,0x42,0x33,0x3C,0x09,0x6E,0x39,0x50};
    printf("Password: "); scanf("%15s", input);
    for (int i = 0; i < 8; i++)
        if ((input[i] ^ key[i]) != "X0r_B1t5"[i]) { puts("Wrong!"); return 1; }
    printf("Correct!\n"); return 0;
}

Компилируем: gcc -o crackme crackme.c. Запускаем ./crackme, вводим что-нибудь — "Wrong!". Задача: получить "Correct!" без знания пароля.

Проверяем strings crackme | grep -i correct — строка видна, но самого пароля в открытом виде нет. ltrace ./crackme после ввода "test" не показывает strcmp — сравнение побайтовое, в цикле, без вызова библиотечной функции. Значит одно: время открывать Ghidra и препарировать декомпилированный код.

Как пользоваться Ghidra: проект и анализ бинарных файлов

Ghidra требует JDK 17+. На Linux: sudo apt install openjdk-21-jdk. На Windows — установщик с adoptium.net. Запуск через ghidraRun или ghidraRun.bat. Никакой установки — распаковали архив и запустили. Бесплатный декомпилятор от АНБ, который шесть лет назад перевернул рынок (до этого IDA Pro стоила тысячи долларов, а альтернатива — голый ассемблер в radare2 с консольным интерфейсом).

Создание проекта: File → New Project → Non-Shared Project, выберите директорию и имя. Один проект — одна задача. Переименования переменных, комментарии и аннотации сохраняются между сессиями — можно вернуться через неделю и увидеть весь наработанный контекст.

Импорт бинарника: File → Import File или перетащите файл в окно проекта. Ghidra автоматически определит формат (ELF), архитектуру (x86-64), разрядность. Для типовых бинарников дефолтные настройки подходят.

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

Навигация: четыре окна для статического анализа Ghidra

После анализа интерфейс кажется перегруженным. Для решения crackme нужны четыре окна — остальные смело закрывайте.

Symbol Tree (левая панель) — дерево символов программы. Разверните Functions, найдите main. Двойной клик перенесёт Listing и Decompiler к этой функции. Разверните Imports — увидите API-вызовы (printf, scanf, puts). Моментальный снимок того, что бинарник умеет. Если в импортах WriteProcessMemory или VirtualAlloc — код может модифицировать себя в рантайме, стоит насторожиться.

Listing (центральная панель) — ассемблерный код: адреса слева, мнемоники справа (MOV, CMP, XOR, JNE). Двойной клик на адресе CALL переносит внутрь функции, Alt+← возвращает назад. Правый клик → Patch Instruction позволяет менять инструкции прямо в листинге — пригодится позже.

Decompiler (правая панель) — C-подобный псевдокод от встроенного декомпилятора. Главное оружие при reverse engineering с нуля. Ключевая связка: подсветка строки в декомпиляторе подсвечивает соответствующий ассемблер x86 в Listing и наоборот. Видите if (iVar1 == 0) в псевдокоде — кликаете — в Listing подсвечиваются CMP и JNZ. После десятка таких сопоставлений ассемблер перестаёт пугать. Связь между if в декомпиляторе и парой CMP + условный переход в листинге становится осязаемой — тот момент, когда дизассемблирование программ начинает «щёлкать».

Defined Strings (меню Window → Defined Strings) — все текстовые строки из бинарника. Двойной клик переносит к строке в Listing. Правый клик → References → Show References to Address покажет xrefs — перекрёстные ссылки на места использования. Именно через xrefs на "Wrong!" или "Correct!" находится целевая функция с логикой проверки.

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

Разбор crackme в Ghidra: от строк до логики проверки

Открываем Defined Strings. В списке находим "Wrong!". Клик — Listing перейдёт к адресу строки в секции данных. Правый клик → References → Show References to Address. Ghidra покажет функцию, из которой строка вызывается. Двойной клик по xref перенесёт внутрь — в панели Decompiler появится декомпилированный псевдокод. Отсюда начинается самое интересное.

Декомпиляция кода Ghidra: читаем XOR-проверку

Вот что показывает декомпилятор для нашего crackme (реальный вывод Ghidra может отличаться в деталях форматирования):

undefined8 main(void) {
  char local_28 [16];
  long local_18 = 0x50396e093c33421b;
  printf("Password: ");
  __isoc99_scanf("%15s",local_28);
  for (int iVar1 = 0; iVar1 < 8; iVar1++) {
    if ((local_28[iVar1] ^ *(byte *)((long)&local_18
        + (long)iVar1)) != "X0r_B1t5"[iVar1])
      { puts("Wrong!"); return 1; }
  }
  printf("Correct!\n");
}

Первое, что бросается в глаза — имена переменных. local_28, local_18, iVar1 — Ghidra называет переменные по смещению на стеке. Переименуйте сразу: правый клик на local_28 → Rename Variable → input. Аналогично local_18 → key_packed. Ghidra синхронизирует имена между декомпилятором и листингом автоматически. Ещё один полезный приём: правый клик на функции → Edit Function Signature — исправление типов параметров сразу делает псевдокод читабельнее. Не ленитесь — через десять минут без переименований FUN_00401234 с iVar1...iVar7 превратится в непроходимое болото.

Второе — число 0x50396e093c33421b. Выглядит как мусор, но это массив ключа {0x1B, 0x42, 0x33, 0x3C, 0x09, 0x6E, 0x39, 0x50}, который компилятор упаковал в одно 64-битное значение (little-endian: младший байт 0x1B по младшему адресу). Доступ к отдельным байтам идёт через *(byte *)((long)&local_18 + (long)iVar1) — по сути key[i]. Упаковка массивов в скаляры — фирменная особенность декомпилятора Ghidra, к ней привыкаешь быстро. На первых задачах этот момент вызывает ступор — зачем тут какое-то число? А это просто массив, свёрнутый в одну переменную.

Третье — логика: input[i] ^ key[i] != "X0r_B1t5"[i]. Каждый символ ввода XOR-ится с байтом ключа и сравнивается с символом строки "X0r_B1t5". Если хоть одна пара не совпала — "Wrong!". Теперь извлекаем пароль.

Crackme решение пошагово: обратная операция XOR

XOR обратим: если A ^ B = C, то A = C ^ B. Пароль вычисляется как target[i] ^ key[i]. Строка "X0r_B1t5" видна прямо в псевдокоде, ключ 0x50396e093c33421b — тоже. Декодируем:

key = bytes.fromhex('1b42333c096e3950')
target = b"X0r_B1t5"
password = ''.join(chr(t ^ k) for t, k in zip(target, key))
print(password)  # CrAcK_Me

Запускаем ./crackme, вводим CrAcK_Me — "Correct!". Задача решена аналитически: поняли алгоритм и вычислили правильный ввод. Чистое решение — через понимание логики, а не обход проверки.

XOR-паттерн — самый распространённый в crackme начального и среднего уровня. Признаки в декомпилированном коде: оператор ^ внутри цикла, массив «случайных» байтов рядом, сравнение результата с фиксированной строкой. Увидели это сочетание — сразу извлекайте оба операнда и XOR-ьте. В более сложных CTF reverse заданиях XOR применяется каскадно или с rolling key, но принцип тот же.

Патчинг бинарника: альтернативное решение crackme

Когда алгоритм сложнее XOR и вычислить пароль аналитически долго — есть второй путь. Не разбирать логику, а тупо изменить условие проверки. В ассемблере условие if (...) { puts("Wrong!"); } превращается в пару инструкций: CMP (сравнение) и JNE/JNZ (jump if not equal). Замена JNZ на JZ (jump if zero) инвертирует условие: программа примет любой неправильный пароль.

В Ghidra: подсветите строку с if в декомпиляторе. Listing перейдёт к ассемблерной инструкции JNZ. Правый клик → Patch Instruction (или Ctrl+Shift+G). Меняете мнемонику JNZ на JZ. Декомпилятор мгновенно обновит псевдокод — условие инвертируется. Красота.

Для сохранения пропатченного бинарника: File → Export Program, формат Binary. Запускаете — "Correct!" на любой ввод.

На CTF обе стратегии засчитываются. Но есть подвох: если флаг формируется из самого пароля (формат flag{пароль}), патчинг не поможет — программа выведет "Correct!", но флаг будет содержать вашу произвольную строку вместо настоящего ответа. Чистое решение через анализ декомпилированного кода надёжнее.

Патчинг условных переходов применяется и при обходе anti-debug проверок. В EN-источнике с freecodecamp описан crackme с root-me, где бинарник вызывает ptrace(PTRACE_TRACEME) — программа завершается под отладчиком. Решение: найти условный переход после ptrace и заменить JNS на JMP, полностью обойдя проверку.

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

В нашем примере Ghidra нашла main автоматически — бинарник содержал отладочные символы. На CTF так везёт редко. Если file crackme показывает stripped, функция main не появится в Symbol Tree.

Три способа найти точку входа в логику:

Через строки. Defined Strings → "Wrong!" → xref → целевая функция. Работает в подавляющем большинстве случаев — на задачах начального уровня это практически безотказный приём. Ghidra назовёт функцию FUN_00401234 — переименуйте через Rename Function.

Через entry. Если строки зашифрованы, ищите entry в Symbol Tree. Внутри будет вызов __libc_start_main (для Linux ELF). Первый аргумент этого вызова — адрес main. Двойной клик по адресу перенесёт к целевой функции.

Через импорты. Если бинарник динамический и использует scanf или printf — кликните на эти функции в Imports, посмотрите xrefs. Они приведут к функциям с пользовательским вводом.

Anti-reversing техники в crackme и реальном малваре

Crackme среднего уровня и реальное вредоносное ПО применяют техники, формализованные в MITRE ATT&CK:

Упаковка — Software Packing (T1027.002, Defense Evasion). Бинарник сжат UPX или аналогичным пакером. Ghidra не находит функций кроме entry, Defined Strings пуст, в Program Tree видны секции UPX0/UPX1. Для UPX решение тривиальное: upx -d crackme в терминале. Для других пакеров — ручная распаковка через отладчик и дамп памяти (тут уже повозиться придётся). Как обнаружить: file иногда показывает UPX compressed, CFF Explorer (Windows) отображает пакер в File Info.

Обнаружение отладчика — Debugger Evasion (T1622, Defense Evasion/Discovery). Вызов ptrace(PTRACE_TRACEME) или IsDebuggerPresent() на Windows. При статическом анализе в Ghidra: находите вызов в импортах, переходите по xref, патчите условный переход после проверки. Минутное дело, когда знаешь что искать.

Обфускация — Obfuscated Files or Information (T1027, Defense Evasion). Строки зашифрованы, control flow запутан, мусорные инструкции между реальными. Для базовой обфускации декомпилятор справляется. Для серьёзной — пишете скрипты деобфускации в Ghidra Script Manager (поддерживает Java и Python), что по терминологии MITRE соответствует обратному процессу — Deobfuscate/Decode Files or Information (T1140).

От первого crackme к CTF reverse заданиям

Методология не меняется с ростом сложности задач. Порядок действий на каждом бинарнике один: внешняя разведка (file, strings, ltrace), определение препятствий (stripped? packed? anti-debug?), поиск целевой функции через строки и xrefs, анализ логики в декомпиляторе, извлечение ответа аналитически или через патчинг.

Типичные усложнения на CTF среднего уровня: многоступенчатая проверка (несколько функций проверяют разные части ввода), кастомные алгоритмы (RC4, TEA, кастомный хеш вместо XOR), проверка через хеш (сравнивается SHA256 ввода с эталоном — вычислить обратно невозможно, патчинг единственный путь), нестандартные архитектуры (ARM, MIPS — декомпилятор Ghidra поддерживает их из коробки).

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

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

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

Единственный способ освоить реверс — решать задачи. Одна решённая задача учит больше, чем десять прочитанных writeup-ов. Берите любой crackme с crackmes.one, открывайте Ghidra и проходите по шагам из этой статьи. Первый займёт час. Пятый — десять минут. Если хотите отработать XOR и патчинг в контролируемой среде — на HackerLab есть лабы с reverse-задачами ровно такого уровня.

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

Поделиться

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

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

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

Читайте также

Race Condition в CTF: эксплуатация на практике

12 мин.

7

Race Condition в CTF: эксплуатация на практике

Три типа race condition, single-packet attack за 4 мкс, Turbo Intruder скрипты и разбор реальных CVE. Практическое руководство для CTF-игроков и пентестеров.

28 СЕНТЯБРЬ, 2026

Как писать write-up после CTF: структура и ошибки

13 мин.

18

Как писать write-up после CTF: структура и ошибки

Готовый шаблон write-up для CTF с примером, 7 ошибок новичков и инструменты для заметок. Пошаговый гайд от CTF-игрока.

27 СЕНТЯБРЬ, 2026

Pwntools для начинающих: buffer overflow на CTF

13 мин.

10

Pwntools для начинающих: buffer overflow на CTF

Пошаговый разбор ret2win: установка pwntools, поиск offset через cyclic, написание эксплойта на Python за 8 строк. Отладка через GDB и типичные ошибки новичков.

27 СЕНТЯБРЬ, 2026