
Исследование CrackMeBench (arxiv, 2025) проверило, как LLM-агенты справляются с классическими crackme-задачами. Лучшая модель решила 3 из 8 публичных калибровочных задач при пятиминутном лимите и трёх попытках. Причины провала зафиксированы: чрезмерное доверие декомпилятору, путаница между закодированными данными и финальными строками, неверная интерпретация формата ввода-вывода. Те же ошибки совершают и новички на CTF — только у человека есть шанс выработать методологию, после чего типовой crackme решается за 10-15 минут. Разберём пошаговый реверс-инжиниринг в Ghidra на примерах трёх паттернов проверки пароля: от открытого strcmp до XOR-шифрования и кастомных хеш-функций.
Перед запуском Ghidra стоит потратить 30 секунд на командную строку. Иногда задача решается вообще без дизассемблера — и это не шутка, а суровая реальность crackmes.one. Подробнее — в нашем руководстве по бинарный анализ уязвимостей.
Команда file crackme покажет формат (ELF/PE), архитектуру (x86/x64/ARM), тип линковки (static/dynamic). Для CTF reverse engineering задач это определяет дальнейший набор инструментов: динамическая линковка означает, что ltrace перехватит библиотечные вызовы.
strings crackme | grep -iE "flag|pass|wrong|correct" — грубый приём, но работает. По опыту решения задач на crackmes.one, удивительное количество crackme начального уровня хранят пароль открытым текстом в секции данных. Видите в выводе строку формата flag{...} или CTF{...} — проверяйте сразу, задача может закрыться за секунды.
Третий приём — ltrace ./crackme. Утилита перехватывает вызовы библиотечных функций в реальном времени. Если программа сравнивает ваш ввод с эталоном через strcmp, ltrace покажет оба аргумента: strcmp("your_input", "real_password"). Компилятор иногда инлайнит strcmp, и ltrace его не увидит — но проверка занимает две секунды, пропускать её глупо.
Эти три команды работают как фильтр. Ни одна не дала результата — пароль зашифрован или проверка нестандартная, переходим к статическому анализу бинарных файлов в Ghidra.
Создание проекта и импорт: File → New Project → Non-Shared Project, затем File → Import File. Ghidra сама определит формат и архитектуру. На вопрос "Analyze now?" соглашайтесь с дефолтными настройками — для типовых ELF и PE хватит за глаза. Установку опустим: нужен JDK 21+ и ZIP-архив с github.com/NationalSecurityAgency/ghidra/releases, установщика нет — распаковка и запуск ghidraRun.
После автоанализа интерфейс CodeBrowser выглядит перегруженным. Для разбора crackme в Ghidra нужны четыре окна, остальные смело закрывайте.
Symbol Tree (левая панель) — дерево символов: функции, импорты, экспорты. Тут ищем main. Раскрываете ветку Functions, двойной клик по main — оба центральных окна прыгают к этой функции. Если в импортах мелькнул WriteProcessMemory или VirtualAlloc — бинарник может модифицировать собственный код в рантайме, и чисто статический анализ будет неполным.
Listing (центральная панель) — дизассемблированный код. Адреса слева, мнемоники справа: MOV, CMP, JNE, CALL. Двойной клик на адресе CALL переносит внутрь вызываемой функции, Alt+← возвращает назад. Правый клик → Patch Instruction позволяет менять инструкции — пригодится в разделе про патчинг.
Decompile (правая панель) — декомпиляция в Ghidra, псевдокод на C. Главное оружие при анализе алгоритма проверки пароля. Подсветка строки в декомпиляторе подсвечивает соответствующий ассемблер в Listing — и наоборот. Видите if (iVar1 == 0) справа — смотрите CMP и JNZ слева. Кстати, нажатие F1 при наведении на любой элемент интерфейса вызывает контекстную справку (совет из курса Introduction to Reverse Engineering with Ghidra на Hackaday.io) — пользуйтесь, пока осваиваетесь.
Если main в Symbol Tree нет — бинарник stripped. В терминах MITRE ATT&CK это техника Stripped Payloads (T1027.008) из тактики Defense Evasion: символы удалены, чтобы затруднить анализ. Решение: ищите вызов __libc_start_main в секции Imports или Functions. Даже в stripped-бинарниках динамические библиотеки вроде libc сохраняют свои символы. Один из аргументов __libc_start_main — адрес main. Переходите по нему двойным кликом — вы в основной функции.
Четвёртое окно — Defined Strings (Window → Defined Strings). Все текстовые строки из бинарника как на ладони. В каждом crackme есть "Wrong password", "Correct!", "Access Denied" — это маяки для поиска проверки пароля в бинарнике.
Нашли "Wrong password"? Правый клик → References → Show References to Address. Ghidra покажет все функции, ссылающиеся на эту строку. Переходите к первой — вы внутри функции-обработчика. Не нужно препарировать весь бинарник: scope сужен до одной функции и одного условного перехода.
Переименование переменных — навык, без которого в Ghidra делать нечего. Декомпилятор именует переменные как local_28, iVar1, DAT_00104020. Правый клик → Rename Variable (или клавиша L): увидели, что local_28 передаётся в strcmp — переименуйте в user_input. Строка DAT_00104020 содержит "Enter password:" — переименуйте в prompt_msg. После 5-10 переименований нечитаемый псевдокод превращается в нормальный C-код. Ghidra синхронизирует имена между декомпилятором и листингом автоматически.
Тип undefined8 — 8 байт данных, тип которых Ghidra не определила. Для возвращаемого значения main это почти всегда int. Правый клик → Retype Variable → задаёте тип вручную. Для решения задачи необязательно, но код станет читабельнее.
Реверс простых crackme сводится к распознаванию одного из нескольких типичных паттернов. Каждый следующий — модификация предыдущего.
Простейший паттерн: программа берёт ввод и напрямую сравнивает с захардкоженной строкой через strcmp. В декомпиляторе Ghidra для начинающих это выглядит почти как исходник:
void main(void) {
char user_input[104];
puts("Enter password:");
gets(user_input);
if (strcmp(user_input, s_S3cr3tP@ss_00104020) == 0) {
puts("Access Granted!");
} else {
puts("Access Denied!");
}
}
Вместо литеральной строки "S3cr3tP@ss" Ghidra покажет s_S3cr3tP@ss_00104020 — та же строка по адресу 0x00104020. Двойной клик на имени перенесёт в Listing, где она лежит в открытом виде. Такие задачи решаются ещё до Ghidra — через strings или ltrace. Но узнавать этот паттерн в декомпиляторе полезно: следующие уровни строятся на его модификациях.
Автор crackme прячет эталонный пароль: каждый байт строки XOR-ится с ключом перед компиляцией. При проверке программа либо расшифровывает эталон и сравнивает с вводом, либо шифрует ввод и сравнивает с зашифрованным эталоном. В декомпиляторе — цикл с побитовой операцией:
for (i = 0; i < len; i++) {
if ((user_input[i] ^ 0x42) != encrypted_pass[i]) {
puts("Wrong!");
return;
}
}
puts("Correct!");
Ключ 0x42 — однобайтовый пример. В реальных crackme ключ бывает многобайтовым (циклический XOR: key[i % key_len]), или каждый следующий байт ключа вычисляется от предыдущего. Распознать этот паттерн можно по характерным признакам в Decompile View: цикл с переменной-счётчиком, оператор ^ внутри тела, обращение к массиву по индексу, условный переход к "Wrong" или "Correct".
Решение: вытаскиваете массив encrypted_pass из бинарника (Ghidra покажет его содержимое в Listing по адресу), прогоняете XOR с тем же ключом — и получаете пароль. Три строки на Python: прочитать байты, XOR, вывести результат. Ключ виден прямо в декомпилированном коде — это immediate-значение в операции ^.
Третий уровень: программа не хранит эталон вообще. Вместо этого вычисляет хеш или контрольную сумму введённого пароля и сравнивает с числовой константой. В декомпиляторе — цикл, аккумулирующий значение через умножение, сложение, битовые сдвиги. Результат сравнивается с магическим числом: if (hash_result == 0xdeadbeef).
Дальше зависит от сложности хеша. Если он простой — сумма ASCII-кодов или hash = hash * 31 + char — обращается аналитически или подбирается перебором. Если хеш криптографический (SHA-256, MD5) — crackme явно не для начинающих, нужен другой подход: rainbow tables или z3 solver.
Ещё вариант — посимвольная проверка: каждый символ пароля проверяется отдельным условием. В декомпиляторе это серия if: if (input[0] == 'p') { if (input[1] == '@') { ... } }. Пароль читается прямо из кода, символ за символом. Встречается на crackmes.one в задачах уровня easy.
Декомпиляция в Ghidra — штука мощная, но не безошибочная. По данным CrackMeBench, одна из главных причин провала при решении crackme — over-trusting decompilers. Вот конкретные ситуации, когда стоит перепроверять Decompile View через Listing.
Неверные типы данных. Ghidra не знает оригинальных типов. Если функция возвращает char *, а декомпилятор показывает undefined8, псевдокод может выглядеть как арифметика с числами, хотя на деле это работа со строками. Правый клик → Retype Variable, задаёте char * — декомпилированный код мгновенно перестроится.
Оптимизированные циклы. Компилятор с -O2 может развернуть цикл сравнения строк в серию отдельных CMP и JNE для каждого символа. Декомпилятор иногда показывает это как цепочку вложенных if, хотя в оригинале был один strcmp. Сверяйтесь с Listing: видите последовательные CMP byte ptr [RAX+N], immediate_value — это развёрнутый цикл, и пароль читается по immediate-значениям каждого CMP. Они и есть ASCII-коды символов пароля.
Строки, собранные на стеке. Бывает, строки формируются не из статической секции данных, а прямо на стеке — серией MOV DWORD PTR [RSP+offset], immediate_value. Декомпилятор покажет присваивание числовых значений локальным переменным. А в Listing те же immediate-значения — ASCII-коды. Четырёхбайтовое значение 0x73736150 в little-endian — строка "Pass". Увидели загадочные числовые константы перед сравнением — переключайтесь на Listing и интерпретируйте как ASCII.
Путаница с закодированными данными. CrackMeBench фиксирует эту ошибку как "confusing encoded data with final strings": агент (или человек) видит в бинарнике строку и принимает её за пароль, хотя это промежуточный этап — зашифрованный эталон или seed для генерации настоящего пароля. В разборе crackme ELF CrackPass с root-me (описан на freeCodeCamp) строка "THEPASSWORDISEASYTOCRACK" — не пароль, а seed, из которого генерируется настоящий пароль через отдельную функцию. Я на это попадался не раз — и каждый раз обидно.
Правило: если декомпилятор показывает что-то странное в районе сравнения — переключайтесь на Listing. Кликните на подозрительную переменную в Decompile — подсветится соответствующий ассемблер. Связка двух окон работает в обе стороны и помогает поймать моменты, когда декомпилятор врёт.
Если вытащить пароль из логики не получается — хеш слишком сложный, обфускация запутанная — есть альтернативный подход: пропатчить условный переход так, чтобы программа принимала любой ввод. На CTF это засчитывается. Дизассемблирование exe файлов и патчинг — два навыка, которые работают в связке.
Находите в Listing инструкцию условного перехода после сравнения. Обычно JNZ (jump if not zero) или JNE (jump if not equal) — переход к ветке "Wrong password". Правый клик → Patch Instruction → меняете мнемонику JNZ на JZ (jump if zero). Логика инвертирована: программа считает правильным любой ввод, кроме настоящего пароля. Вводите произвольную строку — получаете "Access Granted" или вывод флага.
В разборе crackme ELF CrackPass на freeCodeCamp автор применил три патча последовательно: обход ptrace-проверки (JNS → JMP), обход валидации символов (JZ → JMP), инверсию финального сравнения (JNZ → JZ). Результат: программа сама вывела сгенерированный пароль при вводе произвольной строки.
Для экспорта пропатченного бинарника: File → Export Program → формат Binary или Original File. Запускаете модифицированный файл — он отработает с новой логикой.
Оговорка: патчинг не работает на задачах, где флаг вычисляется из правильного ввода (flag = f(password)). Если программа при корректном пароле выводит статическую строку — патчинг решает задачу. Если флаг генерируется на основе ввода — придётся восстановить алгоритм полностью.
Большинство новичков в CTF reverse тратят время не на то. Изучают ассемблер x86 по справочникам Intel, читают про calling conventions и устройство стека — а потом не могут найти строку "Wrong password" через Defined Strings. Проблема не в незнании архитектуры, а в отсутствии навигационного навыка: умения быстро сузить scope от целого бинарника до одной функции и одного условного перехода.
Ассемблер нужен, но позже — когда декомпилятор перестанет справляться. Для 90% crackme начального и среднего уровня хватает Decompile View и умения переименовывать переменные. Через год регулярной практики ассемблер начинает читаться сам — не потому что зубрили мнемоники, а потому что видели тысячу раз одни и те же CMP/JNZ связки в паре с псевдокодом.
Я убеждён, что вход в реверс через декомпилятор, а не через учебник по ассемблеру — единственно разумный путь для 2025 года. Спорить с этим будут те, кто начинал в 2005-м с OllyDbg и пустого notepad. Они правы для своего времени, но порог входа с тех пор должен был снизиться — и Ghidra его снизила. Если хочешь не только crackme, а пройти полную подготовку к OSCP с лабами на каждый вектор — WAPT покрывает и реверс, и веб, и бинарную эксплуатацию в одном треке с ментором.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...