
На CTF прошлого сезона задача категории reverse за 300 очков ушла нерешённой у половины команд. Бинарник принимал серийный номер, прогонял через побайтовый XOR с константным массивом и сверял результат с зашитым эталоном. Три человека из нашей команды два часа читали ассемблерный листинг вручную — безрезультатно. Я открыл тот же файл в Ghidra, нашёл функцию проверки через строку «Try Again!» за полторы минуты, прочитал псевдокод декомпилятора, вытащил оба массива и написал кейген на Python за десять строк. Задача закрыта.
Ниже — разбор этого подхода целиком: от импорта бинарника до работающего кейгена, генерирующего валидный серийный номер.
Перед тем как запускать дизассемблер, потратьте тридцать секунд на командную строку. Три утилиты экономят от минут до часов. Подробнее — в нашем подробном разборе бинарный анализ уязвимостей.
file crackme покажет тип файла: ELF 64-bit, PE 32-bit, архитектуру процессора. От этого зависит, какой процессорный модуль Ghidra подхватит при импорте.
strings crackme | grep -i "pass\|correct\|wrong\|try" вытащит все читаемые строки и отфильтрует по ключевым маркерам. По опыту решения задач на crackmes.one — удивительное количество crackme начального уровня решается одной этой командой. Пароль лежит открытым текстом прямо в бинарнике. Серьёзно.
ltrace ./crackme test 2>&1 | grep strcmp перехватит вызовы библиотечных функций в реальном времени. Если программа использует strcmp для сравнения ввода с захардкоженным паролем, ltrace покажет оба аргумента: strcmp("test", "real_password"). Когда компилятор инлайнит strcmp — ltrace его не увидит. Но проверка занимает секунды, пропускать её незачем.
Для работы нужны JDK 21+ и Ghidra 11.x — скачивается ZIP-архивом с GitHub (github.com/NationalSecurityAgency/ghidra/releases), без инсталлятора. Запуск: ghidraRun на Linux, ghidraRun.bat на Windows. Минимум 8 ГБ RAM — Ghidra работает поверх JVM и при автоанализе жрёт 2–4 ГБ. Анализируйте незнакомые бинарники только в изолированной виртуальной машине с отключённой сетью. Даже crackme с crackmes.one может содержать неожиданный payload — лучше перестраховаться.
Импорт бинарника: File → New Project → Non-Shared Project → File → Import File. Ghidra определит формат (ELF, PE, Mach-O) и архитектуру автоматически. Двойной клик по файлу откроет CodeBrowser — основное рабочее окно. На вопрос «Analyze now?» соглашайтесь с дефолтными опциями. За несколько секунд автоанализ определит границы функций, построит перекрёстные ссылки (XREF), вытащит текстовые строки и опознает библиотечные вызовы. После этого можно работать.
Главный приём статического анализа исполняемого файла при работе с crackme — идти от строк к коду, а не наоборот. Текстовые маркеры («Wrong password», «Try Again!», «Correct!», «Access Granted») хранятся в секции данных бинарника и не удаляются при стандартной компиляции. Даже в stripped-бинарниках, где убраны имена функций (Stripped Payloads, T1027.008 по MITRE ATT&CK), строковые литералы остаются нетронутыми.
В CodeBrowser: Window → Defined Strings. Откроется таблица со всеми текстовыми строками бинарника. Ищите характерные маркеры: «Try Again», «Correct», «Good», «Bad», «Access Denied». Нашли нужную строку? Правый клик → References → Show References to Address. Ghidra покажет все адреса, откуда эта строка вызывается — перекрёстные ссылки (XREF, cross-reference). Двойной клик на ссылке перенесёт прямо в функцию, которая выводит сообщение. Это и есть функция проверки пароля — или функция, которая вызывает проверку.
Если XREF показывает несколько адресов — начните с того, что ближе к началу функции. Если строка используется в единственном месте — задача упрощается на порядок.
Тот же принцип работает в IDA Free (Shift+F12 для строк, X для XREF) и в radare2 версии 5.9.8 (команды iz для строк, axt @addr для перекрёстных ссылок). Но Ghidra делает это бесплатно и с декомпилятором в комплекте — у IDA Free декомпилятора нет.
Правая панель CodeBrowser — декомпилятор Ghidra. Он превращает машинный код обратно в псевдокод, похожий на C. Не один-в-один с оригиналом, с артефактами — но для анализа crackme хватает за глаза. Главное преимущество перед чтением ассемблера: вы видите if, for, while, вызовы функций и арифметику в привычном синтаксисе.
Первое, что бросается в глаза — нечитаемые имена: local_28, iVar1, DAT_00104020, FUN_00401234. Ghidra понятия не имеет, как автор назвал переменные. Переименование — ключевой навык работы с декомпилятором. Правый клик на переменную → Rename Variable (или клавиша L). Увидели, что local_28 передаётся аргументом в strcmp? Переименуйте в user_input. Глобальная DAT_00104020 содержит текст «Enter password:»? Назовите prompt_msg. Тип undefined8 для возвращаемого значения main — переназначьте на int через Retype Variable.
После 5–10 переименований псевдокод из набора iVar и DAT_ становится читаемым. Как будто со страницы снимают слой шума, и под ним проступает логика программы.
Внутри целевой функции ищите три блока: получение ввода (scanf, fgets, gets или WinAPI GetDlgItemTextA), трансформация ввода (побитовые операции, вызовы подфункций, циклы) и сравнение с эталоном (strcmp, memcmp, побайтовое сравнение в цикле или условный переход после cmp в ассемблере). Если между получением ввода и сравнением стоит вызов неизвестной функции (FUN_00401234) — дважды кликните на ней, чтобы декомпилятор показал её код. Часто именно в таких вложенных функциях прячется алгоритм трансформации.
Простейшие crackme используют strcmp с захардкоженным паролем — такие решаются командой strings и не требуют кейгена. Обратная разработка ПО начинается по-настоящему, когда пароль не хранится в открытом виде: ввод проходит через трансформацию, результат сравнивается с ожидаемым значением. Тут дизассемблирование crackme переходит от «найти строку» к «понять алгоритм».
Побайтовый XOR с константным массивом — самый распространённый алгоритм проверки в crackme начального и среднего уровня. Типичная схема (хорошо видна на примере FatMike's CrackMe#1 с crackmes.one): каждый символ ввода XOR-ится с элементом константного массива, результат сравнивается с эталонным массивом. Совпало — «Correct!», не совпало — «Try Again!».
Вот как выглядит функция сравнения пароля в декомпиляторе Ghidra после переименования переменных:
// Пример для демонстрации концепции — типичный вывод декомпилятора
void check_serial(char *user_input) {
byte xor_key[] = {0x5f, 0x4a, 0x7d, 0x2b, 0x38, 0x6e};
byte expected[] = {0x3a, 0x2f, 0x1c, 0x4e, 0x5b, 0x0d};
for (int i = 0; i < 6; i++) {
if ((user_input[i] ^ xor_key[i]) != expected[i]) {
puts("Try Again!"); return;
}
}
puts("Key is Correct!");
}
До переименования эта же функция выглядит как стена из uVar2 = bVar1 ^ *(byte *)(iVar3 + iVar4) — именно в этот момент большинство начинающих закрывают Ghidra. А зря. Стоит переименовать четыре переменные — и логика проступает: ввод XOR-ится с ключом, результат сверяется с эталоном.
Как распознать XOR-паттерн в нечитаемом псевдокоде: ищите оператор ^ внутри цикла (for, while, do...while). Если в том же цикле стоит сравнение (!=, ==) с условным return или break — перед вами функция проверки с XOR-трансформацией. Ещё один маркер: массивы byte или char фиксированной длины, инициализированные hex-значениями. Ghidra иногда не распознаёт их как массивы и показывает серию отдельных глобальных переменных (DAT_00409480, DAT_00409481, DAT_00409482...). Чтобы исправить: выделите первый элемент в Listing, нажмите T и задайте тип byte[N], где N — длина массива. После этого декомпилятор начнёт корректно показывать индексированный доступ.
В более сложных crackme к XOR добавляется контрольная сумма. Характерный маркер CRC32 — константа 0xedb88320 (полином CRC32) в одной из вызываемых подфункций. В том же FatMike's CrackMe#1 XOR-результат проверяется не побайтово, а через CRC32-хэш: if (crc32(xor_output) == 0x5a6aa47d) при дополнительной проверке длины ввода (input_length == 0x16, то есть 22 символа). Такой crackme сложнее — кейген должен подобрать ввод, дающий нужный CRC32, что уже требует перебора. Но базовый подход не меняется: найти функцию через строки, прочитать псевдокод, понять трансформацию.
Встречается и арифметический вариант: каждый символ ввода умножается на константу, складывается с предыдущим результатом, итоговая сумма сравнивается с эталоном. Тут ищите операторы *, +, % в цикле. Принцип восстановления тот же — записать формулу, применить обратную операцию.
Кейген — программа, генерирующая валидный серийный номер или пароль. Для XOR-based crackme написание keygen на Python тривиально, если алгоритм проверки восстановлен из декомпилятора.
Ключевое свойство XOR: операция обратима сама в себя. Если result = input ^ key, то input = result ^ key. Массивы xor_key и expected уже вытащены из декомпилятора Ghidra. Осталось XOR-нуть expected с xor_key:
# Кейген: обращаем XOR-проверку серийного номера
xor_key = [0x5f, 0x4a, 0x7d, 0x2b, 0x38, 0x6e]
expected = [0x3a, 0x2f, 0x1c, 0x4e, 0x5b, 0x0d]
serial = ''.join(chr(e ^ k) for e, k in zip(expected, xor_key))
print(f"Valid serial: {serial}")
Пять строк кода. Запускаете — получаете валидный серийный номер. Вводите в crackme — «Key is Correct!». Задача решена не обходом проверки, а пониманием алгоритма.
Откуда взять конкретные байты массивов: в Ghidra дважды кликните на имя массива (например, DAT_00409480) в окне декомпилятора. Ghidra перенесёт вас в Listing на адрес массива, где отобразится последовательность байтов. Выпишите их или скопируйте через правый клик → Copy Special → Byte String.
Для crackme с арифметическими трансформациями (сложение, умножение, побитовые сдвиги) принцип аналогичен: восстановите формулу из псевдокода, примените обратную операцию. Сложение → вычитание. Сдвиг влево → сдвиг вправо. Умножение → деление (с учётом целочисленной арифметики и возможного переполнения — тут аккуратнее).
Отдельный случай — crackme с привязкой к имени пользователя (name-serial). Программа принимает имя и серийный номер, вычисляет хэш от имени и сравнивает с серийным. Кейген для таких crackme принимает имя на вход и вычисляет серийный по той же формуле. Тут не нужно обращать хэш — нужно воспроизвести прямое вычисление. По сути, скопировать алгоритм из декомпилятора в Python и запустить с нужным именем.
Если в цепочке трансформаций стоит криптографическая хэш-функция (CRC32, MD5, SHA), обратить её алгебраически нельзя. Для CRC32 возможен перебор — пространство 32-битных значений конечно. Для MD5/SHA перебор нецелесообразен, и задача сводится к другому подходу: подбор коллизий или ограничение пространства ввода. На начальных уровнях crackme такое встречается редко.
Альтернатива кейгену — патчинг бинарника: изменение байтов, чтобы программа принимала любой ввод. Классический приём реверс-инжиниринга — замена условного перехода JNE (Jump if Not Equal, опкод 0x75) на JE (Jump if Equal, опкод 0x74) или на два NOP (No Operation, опкод 0x90). Один-два байта — и проверка инвертирована или вырезана полностью.
В Ghidra: правый клик на ассемблерной инструкции в Listing → Patch Instruction. Меняете мнемонику, экспортируете через File → Export Program. В x64dbg: точка останова (F2) на условном переходе после TEST EAX, EAX или CMP EAX, 0, при срабатывании — двойной клик и замена инструкции. Изменение через отладчик действует до перезапуска; постоянный патч — через редактирование байтов в файле.
Патчинг решает задачу «обойти проверку». Но не решает задачу реверс-инжиниринга.
На CTF флаг часто генерируется программой из корректного серийного номера. Патч убирает проверку, но не подставляет валидный ввод — флаг не будет вычислен. Для crackme с привязкой к имени (name-serial) патч даст «Correct!» для чего угодно, но не сгенерирует правильный серийный для конкретного имени. Навык восстановления алгоритма и написания кейгена — то, что реально проверяют на CTF и спрашивают на собеседованиях.
Патчинг полезен как промежуточный шаг: инвертировать условие, чтобы подтвердить — вы нашли правильное место в коде. Но конечная цель анализа crackme в Ghidra — кейген.
Два случая, когда стандартный подход «строки → XREF → функция» начинает буксовать.
Stripped-бинарники. Имена функций удалены (Stripped Payloads, T1027.008 по MITRE ATT&CK): вместо main вы увидите FUN_00401000, вместо check_password — FUN_00401234. Строковые литералы при этом остаются в секции .rodata (ELF) или .rdata (PE) — strip их не трогает. Подход не меняется: Defined Strings → XREF → переход к функции. Единственное отличие — все функции придётся переименовывать вручную.
Для поиска main в stripped ELF: откройте entry в Symbol Tree, найдите вызов __libc_start_main — его первый аргумент содержит адрес main. В PE-бинарниках main обычно вызывается из entry через 1–2 промежуточные функции CRT-инициализации. Ищите функцию, которая вызывает GetCommandLineA или содержит цикл обработки ввода.
Упакованные бинарники. При использовании пакеров (Software Packing, T1027.002 по MITRE ATT&CK) Ghidra не найдёт ни строк, ни функций кроме entry. Характерный признак: секции UPX0, UPX1 в Program Tree вместо стандартных .text, .data. Бинарник сжат UPX. Решение: распакуйте до анализа командой upx -d crackme. После распаковки — стандартный импорт и автоанализ в Ghidra.
Для кастомных пакеров (не UPX) потребуется динамический анализ: запуск под отладчиком, ожидание самораспаковки, дамп памяти. На начальных уровнях crackmes.one и в CTF для новичков UPX покрывает подавляющее большинство случаев. Windows Defender, кстати, может срабатывать на бинарники с обфускацией — ещё одна причина работать в изолированной VM.
Отдельная ситуация — зашифрованные строки (Obfuscated Files or Information, T1027 по MITRE ATT&CK). Если автор crackme пропустил строки «Try Again!» и «Correct!» через XOR или base64 до компиляции, Defined Strings их не покажет. Подсказка: ищите в списке функций вызовы, которые принимают массив байтов и возвращают строку. Двойной клик на такой функции часто раскрывает простой дешифровщик. Расшифрованные строки уже ведут к функции проверки стандартным путём через XREF.
Девяносто процентов русскоязычных разборов crackme заканчиваются одинаково: нашли JNE, заменили на NOP, «Access Granted!», автор доволен. На CTF и в реальном реверсе это не работает. Патчинг — обход, а не понимание. Человек, который занопил условный переход, не ответит на вопрос «какой алгоритм использовался для генерации серийного номера». А именно этот вопрос задают на собеседованиях и проверяют задачи категории reverse.
За последние два года я прошёл несколько десятков crackme с crackmes.one. Паттерн чёткий: задачи уровня 1–2 решаются через strings и strcmp, кейген не нужен. Уровень 3–4 — XOR с константой, арифметические преобразования, иногда CRC32. Без восстановления алгоритма не обойтись. Уровень 5+ — виртуальные машины, самомодифицирующийся код, anti-debug ловушки. Между уровнями 2 и 4 — пропасть, которую не перепрыгнуть одним знанием NOP и JMP.
Совет, который я бы дал себе в начале: не пытайтесь читать весь ассемблерный листинг. Декомпилятор Ghidra даёт 80% понимания за 20% усилий. Оставшиеся 20% — переименование переменных и распознавание паттернов: XOR в цикле, характерные константы вроде 0xedb88320, структуры if/else после вызова функции сравнения. Каждый следующий crackme решается быстрее предыдущего — не потому что запоминаешь ассемблер наизусть, а потому что начинаешь видеть шаблоны в псевдокоде.
Тот первый бинарник с CTF, который сожрал два часа, сегодня занял бы десять минут. Вся разница — в умении читать вывод декомпилятора и обращать алгоритм. Скачайте любой crackme уровня 3 с crackmes.one, откройте в Ghidra и попробуйте написать кейген до того, как полезете в writeup. Если XOR-паттерн из этой статьи попался — вы справитесь минут за двадцать.
🚀 Хочешь закрепить на практике? CTF-задачи и лабы ждут на HackerLab.
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...