Разбор crackme на Linux x86-64: гибридный анализ с Ghidra и GDB
Первая статья цикла о гибридном анализе crackme Getting Started Keygen: рассматриваем бинарник под Linux x86-64, находим main в Ghidra, расшифровываем стек и подтверждаем гипотезы в GDB.

Вы запускаете программу, о которой ничего не знаете. Она вежливо просит строку, а в ответ на ваш ввод насмешливо отвечает: «Bro, what are you trying to do?» И правда, что мы пытаемся сделать? Пытаемся понять ее. Перед нами crackme – программа-головоломка, написанная для тренировки навыков обратной разработки: бинарник под Linux x86-64 без исходников и документации, который издевается над каждым, кто не смог его разгадать. Это первая статья цикла из трех о гибридном анализе крякми Getting Started Keygen (Mazzottis). Мы осмотрим файл штатными утилитами, найдем main в Ghidra, расшифруем «бессмысленные» имена переменных в реальную раскладку стека и подтвердим гипотезы в GDB. По пути встретим оптимизированный пролог без RBP, «хитрое» беззнаковое условие и std::string, спрятавшийся в трех переменных, которые на первый взгляд никак не связаны.
Знакомство с бинарником: первые шаги
Первым делом берем штатные утилиты: file, readelf, strings. file сообщает, что это ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped. Отсутствие stripping – хороший знак: имена символов сохранены, значит, мы сразу увидим main. readelf -h показывает, что это исполняемый файл для Linux, с точкой входа 0x401060. strings выводит несколько строк: «Enter the key:», «Bro, what are you trying to do?», «Key is valid!» и другие. Это подтверждает, что программа запрашивает ключ и проверяет его.
Запускаем бинарник: он просит ввести строку. Вводим «test» – получаем насмешливый ответ. Программа явно не принимает произвольные строки. Наша задача – понять, что она считает правильным ключом. Для этого нужен дизассемблер и отладчик. Выбираем Ghidra для статического анализа и GDB для динамического.
Анализ в Ghidra: находим main
Открываем бинарник в Ghidra. Функция main обнаруживается сразу – она стандартная для C++, с вызовами std::cout и std::cin. Декомпилятор Ghidra показывает примерно следующее: main вызывает функцию, которая выводит приглашение, затем считывает строку в переменную, затем вызывает функцию проверки, и в зависимости от результата выводит либо «Key is valid!», либо насмешку.
Однако декомпилятор присваивает переменным бессмысленные имена вроде local38, local30, local28. Это сбивает с толку. Чтобы понять, что происходит, нужно вручную восстановить раскладку стека. Смотрим на ассемблерный код main: видим, что пролог не использует RBP – он оптимизирован, используется только RSP. Это типично для компиляторов с оптимизацией -O2. Также замечаем, что для хранения строки выделяется три переменные: это указывает на std::string, который состоит из указателя на данные, размера и вместимости.
Расшифровка стека: std::string в трех переменных
В Ghidra мы видим, что main вызывает функцию, которая принимает указатель на строку. В ассемблере видно, что перед вызовом проверки в регистры загружаются адреса трех переменных. Это классическая реализация std::string в libstdc++: объект занимает 32 байта (для x86-64): указатель на данные (8 байт), размер (8 байт) и вместимость (8 байт), плюс возможно локальный буфер для коротких строк (SSO). В нашем случае три переменные по 8 байт – это и есть std::string.
Разбираем: local38 – это указатель на данные, local30 – размер, local28 – вместимость. В функции проверки, вероятно, используются эти поля. Чтобы убедиться, запускаем GDB, ставим брейкпоинт на main и смотрим на значения переменных после ввода строки. Вводим «test» – видим, что local38 указывает на буфер внутри объекта (SSO), local30 = 4, local28 = 15. Это подтверждает, что это std::string.
Хитрое беззнаковое условие
В функции проверки встречается условие типа if (len 5) или что-то подобное. Но в ассемблере видно, что сравнение выполняется с беззнаковым переходом (ja, jb). Это важно: если программист написал знаковое сравнение, а компилятор использовал беззнаковое, это может привести к ошибкам. В нашем случае, вероятно, длина ключа должна быть строго определенной, и беззнаковое сравнение защищает от отрицательных значений (что невозможно для sizet, но могло быть для int). Разбираем: в функции проверки сначала извлекается размер строки, затем сравнивается с константой. Например, cmp rax, 0x10; ja fail – если длина больше 16, то переход на fail. Это означает, что ключ должен быть не длиннее 16 символов.
Подтверждение в GDB: динамический анализ
GDB позволяет проверить гипотезы. Ставим брейкпоинт на функцию проверки, запускаем программу с вводом «test», смотрим регистры и память. Видим, что в rdi передан указатель на объект std::string. Далее в функции проверки извлекается длина, сравнивается с константой. Мы можем перехватить управление и изменить значение регистра, чтобы обойти проверку, но это не наша цель – мы хотим понять алгоритм.
Постепенно мы восстанавливаем логику: программа требует, чтобы ключ имел определенную длину и, вероятно, удовлетворял какому-то хеш-условию. Это станет ясно во второй и третьей частях цикла.
Кого затронет и как
Эта статья полезна всем, кто интересуется обратной разработкой: студентам, изучающим низкоуровневое программирование, разработчикам, желающим понять, как работают оптимизации компилятора, и специалистам по безопасности, которые анализируют вредоносное ПО. В российском контексте, где курсы по reverse engineering становятся все популярнее, такие разборы помогают на практике освоить Ghidra и GDB.
Что будет дальше
Во второй части цикла мы проведем мутационное тестирование и реконструируем скрытую структуру, а в третьей – полностью разберем хеш-функцию и восстановим алгоритм на Python. Следите за обновлениями, чтобы узнать, как написать кейген для этого crackme.
Итог
Первый контакт с crackme на Linux x86-64 – это увлекательное путешествие в мир обратной разработки. Мы научились использовать штатные утилиты, находить main в Ghidra, расшифровывать стек и подтверждать гипотезы в GDB. Теперь вы готовы к следующему шагу – мутационному тестированию и восстановлению алгоритма. Продолжайте исследовать, и вскоре вы сможете решать более сложные задачи.