
Пошаговое руководство по использованию локальной модели ИИ Google Gemma 4 E4B для реверс-инжиниринга Windows crackme с помощью Ghidra, включая настройку локального вывода и автоматическое переименование функций и переменных.
Я экспериментировал с новой моделью с открытыми весами Gemma E4B, которую выпустила Google, и к моему удивлению, она показала отличные результаты в сценариях локального офлайн-реверс-инжиниринга. Я решил написать этот туториал, чтобы распространить информацию о том, что локальный ИИ теперь достаточно хорош для многих базовых задач реверсинга, и в будущем ситуация, вероятно, будет быстро улучшаться.
Одна из самых утомительных частей реверсинга нового бинарника — это самый первый этап, когда у вас нет никакого представления о том, какие функции и переменные важны. Существует множество трюков, которыми пользуются реверсеры для начала, включая просмотр ссылок на строки, бинарное диффингование или поиск похожих функций.
ИИ здесь очень помогает, и лично я добился больших успехов, используя API OpenAI для разметки бинарника или очистки вывода декомпилятора. Однако у использования этих API есть несколько недостатков:
Стоимость — Декомпиляция и дизассемблирование генерируют огромное количество токенов. API взимают плату за токен, поэтому анализ больших бинарников может обойтись в довольно крупную сумму. Если вы работаете с большой целью, содержащей множество бинарников, которые обновляются каждую неделю, эти расходы могут быстро накапливаться и становятся непомерными для реверсеров-любителей.
Конфиденциальность — При использовании удаленного API хост ИИ имеет доступ к информации о том, что вы делаете. Это неприемлемо в некоторых профессиональных сценариях.
Контроль — Когда вы полагаетесь на удаленный API, у вас нет контроля над тем, какие модели вам предоставляются, или над качеством этих моделей. Если вы полагаетесь на них для критически важных задач, это может стать проблемой, когда они замедляются или перестают работать в тот момент, когда они вам нужны, или когда качество их вывода ухудшается до такой степени, что они становятся бесполезными.
Запуск собственных локальных моделей ИИ решает некоторые из этих проблем:
Стоимость — Запуск локальной модели может быть гораздо дешевле, чем использование хостингового сервиса. Хотя ваша локальная модель, вероятно, меньше и медленнее, чем у хорошего провайдера, если она достаточно хороша и работает за приемлемое время, может иметь смысл сэкономить деньги, запуская свою модель, особенно если вы обрабатываете большие объемы данных для простых задач.
Конфиденциальность — Когда вы запускаете модель локально, сетевые вызовы не происходят, и вы полностью контролируете свою конфиденциальность. Никто не может видеть, для чего вы используете модель на своем собственном компьютере.
Контроль — Прелесть модели с открытыми весами в том, что никто не может её у вас отнять. OpenAI или Anthropic могут однажды сделать свои самые современные модели недоступными, либо за счет повышения цен, либо путем явного удаления своих API. Но с моделью с открытыми весами вы контролируете свою судьбу, к лучшему или к худшему.
Однако у локальных моделей ИИ есть и свои недостатки:
Размер — Чем больше модель, тем она интеллектуальнее. Однако большинство больших моделей не помещаются на потребительском оборудовании. Из-за этого, если вы запускаете локальную модель, скорее всего, вы запускаете модель, которая в 10-100 раз меньше самой современной (State-of-the-Art) модели. Это уменьшение размера напрямую приводит к снижению интеллекта модели, делая их непригодными для многих задач, которые люди считают само собой разумеющимися в современных моделях, таких как ChatGPT/Codex или Claude.
Скорость — Локальные модели, вероятно, будут работать на вашем компьютере медленнее, чем при использовании API ИИ. Опять же, это связано с ограничениями потребительского оборудования и некоторых хитростей, которые могут применять провайдеры API и которые обычно недоступны для вас.
Настройка — Запуск локальных моделей похож на попытку установить Linux на восстановленный ноутбук вместо того, чтобы зайти в магазин Apple и купить новый Macbook Air. Опыт Codex и Claude Code — это опыт магазина Apple в мире ИИ. Опыт локальных моделей ИИ — это парень в федоре в своем гараже, долбящийся с глючным компьютером, пытаясь заставить его работать. Как минимум, вам нужно беспокоиться о следующих вещах:
Это непростой путь, и многие сдаются, полагая, что локальные модели ИИ не справляются с задачей, потому что они так и не нашли правильного сочетания оборудования/модели/настроек/промптинга/обвязки, чтобы заставить их работать для своей задачи. Хотя во многих случаях они правы, я надеюсь, что этот туториал хотя бы прольет свет на то, как далеко продвинулись локальные модели, как они могут помочь в реверс-инжиниринге, и вдохновит людей попробовать локальный ИИ.
(пароль к архиву — crackmes.one). Я разместил альтернативную ссылку здесь в репозитории на случай, если оригинальная ссылка перестанет работать.
Ghidra 12.04 используется для дизассемблирования и декомпиляции. Вам понадобится установить OpenJDK 21 для его использования: https://github.com/nationalsecurityagency/ghidra
Во время работы над этим туториалом я средствами vibe-coding с Claude Code создал плагин для Ghidra для переименования функций и переменных с помощью ИИ. Вы можете скачать плагин отсюда: https://github.com/markoglasgow/Ghidra_FastAIRenamer_Plugin
Чтобы установить его, просто переместите zip-файл ghidra_12.0.4_PUBLIC_20260427_FastAIRenamerPlugin.zip в ${GHIDRA_HOME}\Extensions\Ghidra, затем запустите Ghidra, выполнив ${GHIDRA_HOME}\ghidraRun.bat. Чтобы активировать плагин, в начальном окне Ghidra в верхнем меню выберите File -> Install Extensions, затем в браузере плагинов установите флажок рядом с FastAIRenamerPlugin и нажмите Ok. Ghidra предложит перезапуститься, сделайте это сразу.
Чтобы настроить плагин, при следующем запуске Ghidra в верхнем меню перейдите в Tools -> Run Tool -> CodeBrowser. Ghidra сообщит: "New Extensions detected. Would you like to configure them?". Нажмите "Yes", затем снова установите флажок рядом с FastAIRenamerPlugin и нажмите "Ok". Когда откроется CodeBrowser, в верхнем меню нажмите Window -> Fast AI Renamer, затем нажмите кнопку Config. Здесь вы сможете настроить свою модель ИИ. После завершения закройте окно плагина и пустое окно CodeBrowser.
Примечание: если у вас возникли проблемы с загрузкой плагина, возможно, вам потребуется включить режим разработчика в Ghidra (File -> Configure -> установить флажок рядом с Developer).
Примечание: вы всегда можете проверить, загружен ли плагин, перейдя в CodeBrowser, выбрав File -> Configure -> Ghidra Core -> нажать синюю кнопку Configure -> отфильтровать по "FastAIRenamer" -> убедиться, что флажок рядом с его именем установлен.
Примечание: чтобы удалить плагин, сначала откройте CodeBrowser, выберите File -> Configure -> Ghidra Core -> нажать синюю кнопку Configure -> отфильтровать по "FastAIRenamer" -> снять флажок -> OK. Закройте CodeBrowser, затем в начальном окне Ghidra выберите File -> Install Extensions -> снять флажок "FastAIRenamer". Наконец, закройте Ghidra и удалите ghidra_12.0.4_PUBLIC_20260427_FastAIRenamerPlugin.zip из ${GHIDRA_HOME}\Extensions\Ghidra. Чтобы убедиться, что расширение удалено, при следующем запуске Ghidra в начальном окне перейдите в Help -> Runtime Information -> Extension Points -> отфильтровать по "FastAIRenamer" и убедитесь, что ничего не отображается. Уф.
Наконец, убедитесь, что у вас настроена Visual Studio или другая среда разработки C++, чтобы вы могли в стиле vibe-coding написать решение для crackme, когда придет время.
Настройки локального ИИ у людей будут сильно различаться в зависимости от доступного оборудования и бюджета. У меня:
Лично я использую Nvidia GTX 3080 с установленным Cuda 13.2. Вы можете проверить свою версию Cuda, выполнив nvcc --version в терминале Windows.
Я использую квантизации https://huggingface.co/bartowski/google_gemma-4-E4B-it-GGUF/blob/main/google_gemma-4-E4B-it-Q8_0.gguf от bartowski. Возможно, это моё суеверие, но для аналитических задач я стараюсь использовать минимально возможную квантизацию и, по возможности, избегать её вовсе.
Я использую llama.cpp в качестве сервера вывода, конкретно эту версию: llama-b8893-bin-win-cuda-13.1-x64.
Я запускаю llama.cpp со следующими настройками CLI:```
..\llama-b8893-bin-win-cuda-13.1-x64\llama-server.exe ^
--port 8090 ^
--threads 12 ^
--n-gpu-layers 256 ^
--no-mmap ^
--model "google_gemma-4-E4B-it-Q8_0.gguf" ^
--ctx-size 32768 ^
--temp 1.0 ^
--top-k 64 ^
--top-p 0.95 ^
--offline
Это даёт `75 токенов/сек`, что довольно быстро для локального ИИ.
## Нет оборудования? Не проблема
Если у вас нет подходящего оборудования или возникли проблемы с настройкой, но вы всё же хотите следовать этому руководству, вы можете зарегистрироваться в OpenRouter и использовать один из бесплатных API:
https://openrouter.ai/models/?q=free
Google специально предлагает `Gemma 4 31B` и `Gemma 4 26B-A4B` бесплатно прямо сейчас (только ограниченное время):
https://openrouter.ai/google/gemma-4-31b-it:free
https://openrouter.ai/google/gemma-4-26b-a4b-it:free
... просто имейте в виду, что эти бесплатные API медленные, ненадёжные, и все данные, которые вы им отправляете, вероятно, будут записаны в их внутреннюю аналитику и использованы в следующем тренировочном раунде. Тем не менее, для целей этого руководства эти API позволят вам следовать инструкциям.
Чтобы настроить плагин Ghidra для использования вашего логина OpenRouter, откройте конфигурацию плагина и введите следующее:```
Base URL: https://openrouter.ai/api/
API Key: <your OpenRouter API Key>
Model Name: qwen/qwen3-235b-a22b-2507
Этот пример запустит плагин против модели qwen3 здесь
Распакуйте и запустите crackmepls.exe, вы должны увидеть стандартный экран входа. Введите любой случайный пароль, и вы получите сообщение 'Доступ запрещён':```
User: marko
Pass: 123
Access denied
Откройте Ghidra, запустив `ghidraRun.bat`. На панели инструментов выберите `File -> New Project`, оставьте выбранным `Non-Shared Project` и нажмите `Next >>`, затем выберите пустой каталог проекта и дайте проекту имя. Затем нажмите `Finish`.
Затем нажмите `File -> Import File`, выберите `crackmepls.exe`, чтобы добавить его в проект. Ghidra отобразит некоторые сведения о файле, сообщая, что это `Portable Executable (PE)` файл для `x86:LE:64:default:windows`. Просто нажмите `OK`, чтобы принять это без изменений. После небольшой задержки появятся дополнительные сведения о файле, снова просто нажмите `OK`, чтобы принять. Наконец, дважды щелкните на `crackmepls.exe` в проекте, чтобы открыть Code Browser и начать дизассемблирование.
В самом начале Ghidra отобразит сообщение: `crackmepls.exe has not been analyzed. Would you like to analyze it now?`. Нажмите `Yes`, а затем кнопку `Analyze` в следующем окне, и подождите, пока Ghidra найдет, дизассемблирует и декомпилирует все функции в бинарном файле. Возможно, во время анализа возникнет одна или две ошибки о том, что файлы PDB не найдены, просто нажмите `Ok` и проигнорируйте их.
Когда Ghidra закончит анализ файла, вы должны увидеть что-то вроде этого:

Это стандартная точка входа MSVC. Дважды щелкните на `FUN_14000200c` и прокрутите вниз.
Вы должны увидеть в окне декомпилятора ссылки на `__p___argv`, `__p___argc`, а затем вызов функции `FUN_140001290`, которая принимает их в качестве параметров. Вероятно, это функция `main()` crackme, поэтому дважды щелкните по ней.

Попав в функцию `FUN_140001290`, прокрутите немного вниз в окне декомпилятора. Вы увидите строковые ссылки на строки `User:` и `Pass:`, а также ссылки на `basic_istream` (входной поток) и `basic_ostream` (выходной поток).

Эти строки соответствуют операторам вывода и ввода, которые мы видели при первом запуске crackme, поэтому мы знаем, что находимся в функции `main()` crackme.
На данном этапе начинается утомительная работа, когда нам нужно сесть и переименовать каждое имя переменной и вызов функции во что-то осмысленное, анализируя, что делает бинарный файл, чтобы решить crackme. Раньше реверс-инженерам приходилось делать это вручную в дизассемблере, но, к счастью, технологии и налоги принесли нам этот удобный декомпилятор, который можно использовать с помощью скриптов и AI.
Поэтому вместо реальной работы давайте расслабимся, отключим голову и позволим нашему локальному AI сделать всю работу за нас.
Во время настройки этого руководства вы должны были установить плагин Ghidra для помощи с переименованием функций и переменных. Настало время его использовать. В Ghidra на панели инструментов вверху нажмите `Window`, затем `Fast AI Renamer`, чтобы открыть плагин.

Вы должны увидеть интерфейс плагина, который представляет собой набор кнопок и текстовую область. Начните с нажатия кнопки `Config`, затем убедитесь, что всё настроено для правильного общения с вашим локальным AI. Вот как это выглядит на моем компьютере:

Вы можете закрыть окно конфигурации, нажав `Save`, а затем нажмите кнопку `Rename Variables`. Должно появиться окно прогресса с драконом, и вы можете услышать, как ваш компьютер начинает работать, запуская локальную модель AI для переименования всех переменных в окне декомпилятора:

Когда AI завершит работу (занимает около 10-20 секунд на моей машине), вы должны увидеть описание того, какие переменные были переименованы, в текстовой области плагина, а сами переменные должны быть переименованы в окне декомпилятора:

Все модели AI, особенно маленькие локальные, по своей сути ненадежны, поэтому на последнем шаге вы можете увидеть некоторые ошибки. Мы еще вернемся к этому позже. А пока вы всегда можете просто перезапустить AI, снова нажав кнопку `Rename Variables`, пока не получите результаты без ошибок, которые вас устраивают.
## Решение Crackme
На данный момент у нас есть красиво размеченный вывод декомпилятора, где все имена переменных переименованы во что-то осмысленное. Традиционно реверс-инженер мог бы сейчас прочитать этот код и начать разрабатывать решение для Crackme. Однако я подумал, что было бы здорово попробовать заставить локальный AI решить crackme за нас. Помните, сегодня мы не будем использовать свой мозг.
Сначала я попросил AI завершить разметку всего бинарного файла, нажав кнопку `Rename ALL Functions and Variables`. Это служит для присвоения имен всем вызовам функций, используемым в этой функции crackme, что еще больше очищает вывод декомпилятора. На моей машине это занимает около 15 минут.
Когда вы используете `llama.cpp` с параметрами CLI, которые я указал в разделе `Local AI Setup`, вы можете открыть чат-интерфейс для вашей локальной модели, перейдя в браузере по адресу [http://localhost:8090/](http://localhost:8090/)
Здесь я ввел вывод декомпилятора функции из crackme (вы можете получить его, нажав кнопку `Decompile Function` в интерфейсе плагина или просто скопировав из окна декомпилятора справа) и попросил AI написать решение для нас:
======================================================================
======================================================================
======================================================================
Я хочу, чтобы вы помогли мне написать решение для crackme. Ниже представлен листинг декомпилятора функции из crackme, где пользователь вводит имя пользователя и пароль, и доступ предоставляется, если пароль правильный. Пароль вычисляется в этой функции.```
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
/* **Reasoning:**
The function takes user input (a username/input and a password) via standard input. It processes
the user input by calculating a complex, custom checksum/hash. It then compares the provided
password input against a target buffer (likely a stored hash or secret). Finally, it determines
and outputs whether "Access granted" or "Access denied," indicating the function serves as an
authentication routine. */
undefined8 authenticate_user(undefined8 param_1,undefined8 param_2,undefined8 max_len)
{
uint user_checksum;
int iVar1;
undefined8 ****temp_free_ptr;
ulonglong user_input_byte_index;
char *result_message;
undefined1 *status_buffer;
undefined8 ****data_buffer_ptr;
ulonglong pass_input_length_1;
bool access_granted;
undefined8 uStack_d0;
undefined1 status_buffer_small [8];
undefined1 status_buffer_large [32];
undefined8 ***allocated_ptrs [2];
size_t compare_length;
ulonglong ptr_metadata_size;
undefined8 ***user_input_buffer;
undefined8 uStack_78;
ulonglong user_input_length;
ulonglong user_input_length_param;
undefined8 ***pass_input_buffer;
undefined8 uStack_58;
size_t pass_input_length;
ulonglong pass_input_length_param;
ulonglong checksum_seed;
code *code_pointer;
undefined8 ***pass_input_buffer_ptr;
undefined8 ***user_input_buffer_ptr;
ulonglong user_input_length_1;
status_buffer = status_buffer_small;
checksum_seed = MAGIC_VALUE_1 ^ (ulonglong)status_buffer_small;
user_input_byte_index = 0;
uStack_78 = 0;
user_input_length = 0;
user_input_length_param = 0xf;
user_input_buffer = (undefined8 ****)0x0;
uStack_58 = 0;
pass_input_length = 0;
pass_input_length_param = 0xf;
pass_input_buffer = (undefined8 ****)0x0;
formatted_string_output((basic_ostream<char,struct_std::char_traits<char>_> *)cout_exref,"User: ");
extract_token_from_stream((basic_istream<char,struct_std::char_traits<char>_> *)cin_exref,
(longlong *)&user_input_buffer,max_len);
formatted_string_output((basic_ostream<char,struct_std::char_traits<char>_> *)cout_exref,"Pass: ");
extract_token_from_stream((basic_istream<char,struct_std::char_traits<char>_> *)cin_exref,
(longlong *)&pass_input_buffer,max_len);
user_input_length_1 = user_input_length_param;
user_input_buffer_ptr = user_input_buffer;
user_checksum = 0;
pass_input_length_1 = user_input_byte_index;
if (user_input_length != 0) {
do {
temp_free_ptr = &user_input_buffer;
if (0xf < user_input_length_param) {
temp_free_ptr = (undefined8 ****)user_input_buffer;
}
user_checksum =
((int)user_input_byte_index + 1) *
(int)*(char *)((longlong)temp_free_ptr + user_input_byte_index) +
(int)pass_input_length_1;
user_checksum = user_checksum * 8 ^ user_checksum;
user_input_byte_index = user_input_byte_index + 1;
pass_input_length_1 = (ulonglong)user_checksum;
} while (user_input_byte_index < user_input_length);
}
int_to_string_dynamic(allocated_ptrs,user_checksum * 0x539 ^ 0x5a5a);
pass_input_length_1 = pass_input_length_param;
pass_input_buffer_ptr = pass_input_buffer;
temp_free_ptr = &pass_input_buffer;
if (0xf < pass_input_length_param) {
temp_free_ptr = (undefined8 ****)pass_input_buffer;
}
data_buffer_ptr = allocated_ptrs;
if (0xf < ptr_metadata_size) {
data_buffer_ptr = (undefined8 ****)allocated_ptrs[0];
}
if (compare_length == pass_input_length) {
if (compare_length == 0) {
access_granted = true;
}
else {
iVar1 = memcmp(data_buffer_ptr,temp_free_ptr,compare_length);
access_granted = iVar1 == 0;
}
}
else {
access_granted = false;
}
if (0xf < ptr_metadata_size) {
temp_free_ptr = (undefined8 ****)allocated_ptrs[0];
status_buffer = status_buffer_small;
if (0xfff < ptr_metadata_size + 1) {
temp_free_ptr = (undefined8 ****)allocated_ptrs[0][-1];
data_buffer_ptr =
(undefined8 ****)((longlong)allocated_ptrs[0] + (-8 - (longlong)temp_free_ptr));
status_buffer = status_buffer_small;
if ((undefined8 ****)0x1f < data_buffer_ptr) {
code_pointer = (code *)swi(0x29);
(*code_pointer)(5);
temp_free_ptr = data_buffer_ptr;
status_buffer = status_buffer_large;
}
}
*(undefined8 *)(status_buffer + -8) = 0x140001424;
free(temp_free_ptr);
}
result_message = "Access granted\n";
if (!access_granted) {
result_message = "Access denied\n";
}
*(undefined8 *)(status_buffer + -8) = 0x140001443;
formatted_string_output((basic_ostream<char,struct_std::char_traits<char>_> *)cout_exref,result_message);
if (0xf < pass_input_length_1) {
temp_free_ptr = (undefined8 ****)pass_input_buffer_ptr;
if (0xfff < pass_input_length_1 + 1) {
temp_free_ptr = (undefined8 ****)pass_input_buffer_ptr[-1];
data_buffer_ptr =
(undefined8 ****)((longlong)pass_input_buffer_ptr + (-8 - (longlong)temp_free_ptr));
if ((undefined8 ****)0x1f < data_buffer_ptr) {
code_pointer = (code *)swi(0x29);
(*code_pointer)(5);
status_buffer = status_buffer + 8;
temp_free_ptr = data_buffer_ptr;
}
}
*(undefined8 *)(status_buffer + -8) = 0x14000147d;
free(temp_free_ptr);
}
if (0xf < user_input_length_1) {
temp_free_ptr = (undefined8 ****)user_input_buffer_ptr;
if (0xfff < user_input_length_1 + 1) {
temp_free_ptr = (undefined8 ****)user_input_buffer_ptr[-1];
data_buffer_ptr =
(undefined8 ****)((longlong)user_input_buffer_ptr + (-8 - (longlong)temp_free_ptr));
if ((undefined8 ****)0x1f < data_buffer_ptr) {
code_pointer = (code *)swi(0x29);
(*code_pointer)(5);
status_buffer = status_buffer + 8;
temp_free_ptr = data_buffer_ptr;
}
}
*(undefined8 *)(status_buffer + -8) = 0x1400014b7;
free(temp_free_ptr);
}
*(undefined8 *)(status_buffer + -8) = 0x1400014c9;
return 0;
}
Пожалуйста, прочитайте листинг декомпилятора, а затем завершите мой код на C++ ниже, чтобы вычислить правильный пароль для введённого имени пользователя:``` #include #include #include #include
int main() { std::string username; std::cout << "User: "; std::cin >> username;
uint32_t password_value = 0;
std::cout << "Pass: " << password_value << "\n";
return 0;
}
## Сгенерированное решение
В первый раз, когда было сгенерировано решение, оно оказалось неверным. Я включил его в [tut1.txt](https://github.com/markoglasgow/gemma_crackme_tutorial/blob/HEAD/tut1.txt) в этом репозитории. На первый взгляд решение _выглядит_ правильным, но при попытке его применить оно не работает. ИИ принципиально ненадёжны, и это отличный тому пример.
Ничего страшного, мы можем просто открыть новый чат, скопировать приведённый выше запрос и попросить ИИ сгенерировать для нас новое решение. В моём случае я получил работающее решение со второй попытки:```
#include <cstdio>
#include <cstdint>
#include <string>
#include <iostream>
#include <sstream>
#include <iomanip>
/**
* @brief Simulates the custom checksum calculation defined in the crackme function.
*
* @param username The input string (username).
* @return int The final calculated user_checksum.
*/
int calculate_username_checksum(const std::string& username) {
// user_checksum is treated as an 'int' (32-bit signed integer) during the core calculation.
int user_checksum = 0;
// pass_input_length_1 acts as the state variable and is a ulonglong (64-bit).
// It starts at 0, as pass_input_length_1 = user_input_byte_index (which is 0) initially.
uint64_t state_pass_length_1 = 0;
for (size_t i = 0; i < username.length(); ++i) {
// i = user_input_byte_index
// Step 1: Calculate the intermediate value T1.
// The decompiler implies this calculation happens using standard int math
// (truncating/wrapping if intermediate values exceed 32 bits).
// (int)user_input_byte_index + 1
int index_term = (int)(i + 1);
// (int)*(char *)((longlong)temp_free_ptr + user_input_byte_index)
// This is just the ASCII value of the character.
int char_term = (int)username[i];
// (int)pass_input_length_1
// The state is cast down to an int for this arithmetic operation.
int state_term = (int)state_pass_length_1;
// T1 = ((int)user_input_byte_index + 1) * (int)char + (int)state
int intermediate_t1 = index_term * char_term + state_term;
// Step 2: Apply the XOR/Multiplication hash function.
// user_checksum = user_checksum * 8 ^ user_checksum;
// Note: The original code structure suggests the update happens here,
// but since we are recalculating the hash step-by-step, we use the definition:
// user_checksum = T1 * 8 ^ T1;
// Since the original code overwrites user_checksum completely with the hash
// of the current character, we use the T1 derived above.
int current_checksum = intermediate_t1;
current_checksum = current_checksum * 8 ^ current_checksum;
// Update the final running checksum
user_checksum = current_checksum;
// Step 3: Update the state variable for the next iteration.
state_pass_length_1 = (uint64_t)user_checksum;
}
return user_checksum;
}
int main()
{
std::string username;
std::cout << "User: ";
std::cin >> username;
// 1. Calculate the final checksum using the username
int final_user_checksum = calculate_username_checksum(username);
// 2. Calculate the final hash/secret value
// FinalHashValue = user_checksum * 0x539 ^ 0x5a5a
// Note: The arithmetic here is done on the final 32-bit 'int' checksum.
uint32_t final_hash_value = (uint32_t)final_user_checksum * 0x539 ^ 0x5a5a;
// 3. Convert the final hash value into a string (as implied by int_to_string_dynamic)
// Since the password needs to match the memory contents (memcmp), it must be the string representation.
std::stringstream ss;
ss << final_hash_value;
std::string required_password = ss.str();
// We output the required password string.
std::cout << "Pass: " << required_password << "\n";
return 0;
}
Я включил чат, который сгенерировал решение, как solution_chat.html в этот репозиторий. В нём был поток рассуждений, что, по-моему, довольно круто для такой крошечной модели, работающей локально.
В любом случае, мы можем скомпилировать решение в Visual Studio, а затем запустить его, чтобы сгенерировать валидную комбинацию имени пользователя и пароля. Затем мы вводим имя пользователя и пароль в crackme и проверяем, что решили его:

gg
Как мы уже много раз видели на протяжении этого учебника, ИИ по своей природе ненадёжен. Рассмотрите два скриншота ниже:


Первый скриншот из более ранней части учебника, а второй был сделан на том же шаге, но при другом запуске переименования ИИ. Обратите внимание в окне декомпилятора: на верхнем скриншоте буфер ввода пароля помечен как &pass_input_buffer, тогда как на нижнем скриншоте тот же буфер помечен как &username_buffer_1. На нижнем скриншоте ИИ нам лжёт, и эту ложь мы называем «галлюцинациями».
Все ИИ будут вам лгать и галлюцинировать. Чем меньше модель, тем чаще галлюцинации. Чем больше модель, тем вам больше повезёт и тем меньше они будут галлюцинировать. Чем больше вы платите, тем больше вам везёт. Однако ни одна модель ИИ не застрахована от галлюцинаций, и вы никогда не должны забывать об этом, работая с ними.
ИИ лучше всего подходит для задач, где вы можете проверить результат, или где результат достаточно малозначим, чтобы идеальная точность не была необходима. В случае обратной разработки ИИ может помочь нам быстро взглянуть на неизвестный бинарный файл и сэкономить много утомительной ручной работы. Но, как мы видели на примере плохого переименования, он также может завести нас не туда. Всегда относитесь к результатам, созданным ИИ, с осторожностью и всегда проверяйте их, если можете.
Лучший способ использования ИИ — автоматически проверять его результаты. Например, если вы просите ИИ написать код, было бы замечательно иметь скрипт, который тестирует вывод ИИ на соответствие написанному вами вручную набору тестов. В случае, когда ИИ генерирует решение для нашего crackme, мы могли бы написать набор тестов, который автоматически проверяет каждое сгенерированное ИИ решение на crackme, а затем принять решение, которое генерирует хорошие пароли для тысяч разных имён пользователей. Затем мы могли бы запускать ИИ в цикле, пока он не напишет решение, прошедшее наши тесты.
Мы решили этот crackme с помощью относительно небольшой модели (Gemma 4 E4B). Эта модель имеет 8 миллиардов параметров, в отличие от существующих SotA-моделей, которые имеют сотни миллиардов, если не триллионы параметров. Меньшая модель не сможет хорошо рассуждать, будет галлюцинировать чаще и станет бесполезной быстрее по мере заполнения её контекстного окна.
В реальном упражнении по обратной разработке нам, возможно, придётся реверс-инжинирить функции, которые больше контекстного окна нашей небольшой модели, или которые имеют логику, слишком запутанную для её ограниченного интеллекта. Нам нужно быть знакомыми с ограничениями наших моделей при их использовании, чтобы заранее понимать, где и как они нас подведут.
По мере того, как потребительское оборудование развивается рука об руку с архитектурой моделей, я считаю, что мы сможем запускать более крупные и лучшие модели локально, что сделает ещё более продвинутые задачи обратной разработки достижимыми для наших локальных моделей.
Плагин Ghidra «Fast AI Renamer», который я написал, в настоящее время пишет сводку для каждой анализируемой функции и имеет возможность массового экспорта листингов C и asm проекта, над которым работает. Я хотел бы написать инструмент, который вычисляет встраивание для каждой сводки функции, а затем индексирует его в векторной базе данных или SQLite, и затем мы сможем запрашивать базу данных для поиска функций по запросам на естественном языке.
Плагин Ghidra в настоящее время использует простой рабочий процесс без многократного диалога для переименования и полагается на разбор CSV из вывода ИИ для выполнения своей задачи. Я реализовал это таким образом, потому что не думал, что небольшая модель ИИ сможет справиться с длинными разговорами и множественными вызовами инструментов, присущими агентному рабочему процессу. Возможно, я ошибаюсь, и следует изучить альтернативные способы запросов к ИИ. Тем не менее, для небольших локальных моделей я считаю, что лучше максимально ограничить размер контекстного окна.
Я не уверен, насколько уязвим плагин Ghidra к инъекциям в промпт, и специалисты по безопасности должны быть осторожны при его использовании для анализа вредоносных или враждебных бинарных файлов. В целом, это относится ко всем инструментам обратной разработки на основе ИИ, которые могут иметь неаутентифицированные MCP-серверы на локальном хосте или инструменты, доступные для чувствительных функций, которые могут обеспечить выполнение кода. Будьте осторожны.
Учитывая, что мы используем небольшую модель общего назначения для обратной разработки, интересно, насколько можно улучшить её производительность, донастроив её на диалогах, дистиллированных из более крупной и более интеллектуальной модели?
Наконец, функциональность экспорта C/asm плагина Ghidra хорошо работает с Claude Code, и я смог использовать Claude Code + Sonnet для написания правильного решения этого crackme с первой попытки, просто передав ему размеченную декомпиляцию crackme и сказав найти решение. Интересно, есть ли какие-то навыки, которые мы можем написать для Claude Code, чтобы сделать его лучше в анализе бинарного файла через текстовые файлы его вывода декомпилятора, а не заставлять его проходить через MCP-сервер к Ghidra для выполнения анализа. Например, мы могли бы ввести навык для Claude Code, который позволил бы ему просматривать байты в виртуальной памяти бинарного файла, так что если он видит, что на какие-то данные ссылаются в листингах дизассемблера/декомпилятора, он может увидеть, что содержится в этой памяти, без необходимости обращаться к Ghidra.
Спасибо Kryptos за создание Crackme и Crackmes.one за его размещение. Я ценю, что они подхватили то, на чём остановился crackmes.de (RIP).
Спасибо Google за выпуск удивительной и удобной локальной модели.
Спасибо АНБ за использование моих налоговых долларов для создания крутого инструмента обратной разработки, и спасибо Ryan Kurtz за всю поддержку, которую он оказывает на GitHub Ghidra.