
Tutorial passo a passo sobre como usar o modelo de IA local Gemma 4 E4B do Google para engenharia reversa de um crackme do Windows com Ghidra, incluindo configuração para inferência local e renomeação automatizada de funções/variáveis.
Estava brincando com o novo modelo local de pesos abertos Gemma E4B que o Google lançou e, para minha surpresa, obtive muito sucesso ao usá-lo para cenários locais offline de engenharia reversa. Queria escrever este tutorial para divulgar que a IA local já é boa o suficiente para muitas tarefas básicas de reversão, e que as coisas provavelmente vão melhorar rapidamente daqui para frente.
Uma das partes mais tediosas ao reverter um novo binário é logo no início, quando você não tem ideias sobre quais são as funções e variáveis importantes. Existem muitos truques que os reversores usam para começar, incluindo olhar referências a strings, diffing binário ou correspondência de funções semelhantes.
A IA é muito boa para ajudar aqui, e eu pessoalmente tive grande sucesso ao usar a API da OpenAI para anotar um binário ou limpar a saída do descompilador. No entanto, existem várias desvantagens em usar essas APIs:
Custo - Descompilação e desmontagem geram muitos tokens. As APIs cobram $ por token, então binários maiores podem exigir um bom $$$ para serem analisados. Se você estiver lidando com um alvo grande com muitos binários que são atualizados toda semana, esses custos podem aumentar rapidamente e são proibitivos para engenheiros reversos amadores.
Privacidade - Quando você usa uma API remota, o hospedeiro da IA tem conhecimento do que você está fazendo. Isso é um impeditivo em alguns cenários profissionais.
Controle - Quando você depende de uma API remota, não tem controle sobre quais modelos estão sendo servidos para você, ou qual a qualidade dos modelos. Se você depende deles para tarefas críticas, isso pode ser um problema quando eles ficam lentos ou saem do ar quando você precisa deles, ou quando a qualidade da saída deles degrada a ponto de se tornarem inúteis.
Executar seus próprios modelos de IA locais resolve alguns desses problemas:
Custo - Pode ser muito mais barato executar um modelo local do que depender de um serviço hospedado. Embora seu modelo local provavelmente seja menor e mais lento do que o de um bom provedor, se for bom o suficiente e rodar em um tempo razoável, pode fazer sentido economizar dinheiro executando seu próprio modelo, especialmente se você estiver processando grandes quantidades de dados para tarefas simples.
Privacidade - Quando você executa o modelo localmente, não há chamadas de rede acontecendo e você tem controle total sobre sua privacidade. Ninguém pode ver para que você está usando o modelo em sua própria máquina.
Controle - A beleza de um modelo de pesos abertos é que ninguém pode tirá-lo de você. OpenAI ou Anthropic podem um dia tornar seus modelos SotA indisponíveis, seja por aumentos de preço ou removendo explicitamente suas APIs. Mas com um modelo de pesos abertos, você está no controle do seu destino, para o bem ou para o mal.
Modelos de IA locais têm suas desvantagens:
Tamanho - Quanto maior o seu modelo, mais inteligente ele é. No entanto, a maioria dos modelos grandes não cabe em hardware de consumo. Por causa disso, se você está executando um modelo local, provavelmente está executando um modelo que é 10x a 100x menor que um modelo SotA (State-of-the-Art). Essa diminuição no tamanho leva diretamente a uma diminuição na inteligência do modelo, tornando-os inadequados para muitas tarefas que as pessoas consideram garantidas em modelos SotA como ChatGPT/Codex ou Claude.
Velocidade - Modelos locais provavelmente serão mais lentos em sua máquina do que ao usar uma API de IA. Novamente, isso se deve às limitações do hardware de consumo e a alguns dos truques que os provedores de API podem fazer e que geralmente não estão disponíveis para você.
Configuração - Executar modelos locais é como tentar rodar Linux em um laptop reformado vs entrar na loja da Apple e comprar um Macbook Air novo. A experiência do Codex e Claude Code é a experiência da loja da Apple em IA. A experiência do modelo de IA local é o cara de fedora na garagem batendo em um computador instável tentando fazê-lo funcionar. No mínimo, você precisa se preocupar com as seguintes coisas:
Não é uma jornada fácil, e muitas pessoas desistem e assumem que os modelos de IA locais não são adequados para a tarefa, porque nunca encontraram a combinação certa de hardware/modelo/configurações/prompt/harness para fazê-los funcionar para sua tarefa. Embora em muitos casos elas estejam certas, espero que este tutorial pelo menos ilumine o quão longe os modelos locais chegaram, como eles podem ajudar com engenharia reversa, e inspire as pessoas a dar uma chance à IA local.
(a senha do arquivo é crackmes.one). Hospedei um link alternativo aqui no repositório caso o link original saia do ar.
Ghidra 12.04 é usado para desmontagem e descompilação. Você precisará instalar OpenJDK 21 para usá-lo: https://github.com/nationalsecurityagency/ghidra
Enquanto trabalhava neste tutorial, codei com vibe (vibe-coding) com Claude Code um plugin para Ghidra para renomear funções e variáveis com IA. Você pode baixar o plugin aqui: https://github.com/markoglasgow/Ghidra_FastAIRenamer_Plugin
Para instalá-lo, basta mover o arquivo zip ghidra_12.0.4_PUBLIC_20260427_FastAIRenamerPlugin.zip para ${GHIDRA_HOME}\Extensions\Ghidra, então execute o Ghidra executando ${GHIDRA_HOME}\ghidraRun.bat. Para ativar o plugin, na tela inicial do Ghidra no menu superior selecione File -> Install Extensions, então no navegador de plugins marque a caixa ao lado de FastAIRenamerPlugin, então clique em Ok. O Ghidra solicitará que você reinicie, então faça isso imediatamente.
Para configurar o plugin, na próxima vez que o Ghidra iniciar, no menu superior vá para Tools -> Run Tool -> CodeBrowser. O Ghidra dirá "New Extensions detected. Would you like to configure them?". Clique em sim, novamente marque a caixa ao lado de FastAIRenamerPlugin, então clique em Ok. Quando o CodeBrowser abrir, no menu superior clique em Window -> Fast AI Renamer, então clique no botão Config. Aqui você poderá configurar seu modelo de IA. Feche a janela do plugin e a janela vazia do CodeBrowser quando terminar.
Nota: se você tiver problemas para carregar o plugin, pode ser necessário ativar o modo Desenvolvedor no Ghidra (File -> Configure -> caixa ao lado de Developer)
Nota: você sempre pode verificar se o plugin está carregado indo ao CodeBrowser, clicando em File -> Configure -> Ghidra Core -> clique no botão azul de configurar -> filtre por "FastAIRenamer" -> certifique-se de que a caixa ao lado do nome está marcada.
Nota: para desinstalar o plugin, primeiro abra o CodeBrowser, File -> Configure -> Ghidra Core -> clique no botão azul de configurar -> filtre por "FastAIRenamer" -> desmarque -> ok. Feche o CodeBrowser, então na janela inicial do Ghidra, File -> Install Extensions -> desmarque "FastAIRenamer". Finalmente feche o Ghidra e delete ghidra_12.0.4_PUBLIC_20260427_FastAIRenamerPlugin.zip de ${GHIDRA_HOME}\Extensions\Ghidra. Para garantir que a extensão foi deletada, na próxima vez que você executar o Ghidra, na janela inicial vá para Help -> Runtime Information -> Extension Points -> filtre por "FastAIRenamer" e certifique-se de que nada aparece. ufa.
Por fim, certifique-se de ter o Visual Studio ou algum outro ambiente de desenvolvimento C++ configurado para que você possa codificar com vibe (vibe coding) uma solução para o crackme quando chegar a hora.
As configurações de IA local das pessoas variam muito dependendo do hardware e do $$$ que elas têm disponíveis. Para mim:
Estou executando uma Nvidia GTX 3080, com Cuda 13.2 instalado. Você pode verificar sua versão do Cuda executando nvcc --version no seu Terminal do Windows.
Estou executando os quants https://huggingface.co/bartowski/google_gemma-4-E4B-it-GGUF/blob/main/google_gemma-4-E4B-it-Q8_0.gguf do bartowski. Pode ser superstição da minha parte, mas para tarefas analíticas tento executar com a menor quantidade de quantização possível, e evito-a completamente se puder.
Estou usando llama.cpp como meu servidor de inferência, com esta versão específica: llama-b8893-bin-win-cuda-13.1-x64.
Estou executando llama.cpp com as seguintes configurações de 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
Isto produz `75 tokens/sec`, o que é bastante rápido para IA local.
## Sem hardware? Sem problema
Se não tiver o hardware disponível ou estiver com problemas de configuração, mas ainda assim quiser acompanhar este tutorial, pode obter um login no OpenRouter e usar uma das APIs gratuitas:
https://openrouter.ai/models/?q=free
A Google está especificamente a oferecer `Gemma 4 31B` e `Gemma 4 26B-A4B` gratuitamente agora (por tempo limitado):
https://openrouter.ai/google/gemma-4-31b-it:free
https://openrouter.ai/google/gemma-4-26b-a4b-it:free
... apenas tenha em mente que estas APIs gratuitas são lentas, não fiáveis, e todos os dados que enviar para elas provavelmente serão registados nas suas análises internas e usados no seu próximo treino. Ainda assim, para os fins deste tutorial, estas APIs devem permitir que acompanhe.
Para configurar o plugin Ghidra para usar o seu login OpenRouter, abra a configuração do plugin e introduza o seguinte:```
Base URL: https://openrouter.ai/api/
API Key: <your OpenRouter API Key>
Model Name: qwen/qwen3-235b-a22b-2507
Este exemplo executará o plugin contra o modelo qwen3 aqui
Descompacte e execute crackmepls.exe, você verá uma tela de login padrão. Digite qualquer senha aleatória e receberá uma mensagem 'Acesso Negado':```
User: marko
Pass: 123
Access denied
Abra o Ghidra executando `ghidraRun.bat`. Na barra de ferramentas, selecione `File -> New Project`, deixe `Non-Shared Project` selecionado e clique em `Next >>`, depois selecione um diretório de projeto vazio e dê um nome ao projeto. Em seguida, clique em `Finish`.
Depois, clique em `File -> Import File`, selecione `crackmepls.exe` para adicioná-lo ao projeto. O Ghidra exibirá alguns detalhes sobre o arquivo, informando que é um arquivo `Portable Executable (PE)` para `x86:LE:64:default:windows`. Basta clicar em `OK` para aceitar sem alterar nada. Após um breve atraso, ele exibirá mais alguns detalhes sobre o arquivo; novamente, clique em `OK` para aceitar. Por fim, clique duas vezes em `crackmepls.exe` no projeto para abrir o Code Browser e iniciar a desmontagem.
Logo no início, o Ghidra exibirá uma mensagem dizendo `crackmepls.exe has not been analyzed. Would you like to analyze it now?`. Clique em `Yes` e, em seguida, no botão `Analyze` na próxima janela. Aguarde o Ghidra localizar, desmontar e descompilar todas as funções no binário. Você pode receber um ou dois erros durante a análise sobre arquivos PDB não encontrados; apenas clique em `Ok` e ignore-os.
Quando o Ghidra terminar de analisar o arquivo, você deverá ver algo como abaixo:

Este é o ponto de entrada padrão do MSVC. Clique duas vezes em `FUN_14000200c` e role para baixo.
Você deve ver referências na janela do descompilador para `__p___argv`, `__p___argc` e, em seguida, uma chamada de função `FUN_140001290` que os aceita como parâmetros. Esta é provavelmente a função `main()` do crackme, então clique duas vezes nela.

Uma vez na função `FUN_140001290`, role um pouco para baixo na janela do descompilador. Você verá referências às strings `User:` e `Pass:`, juntamente com referências a `basic_istream` (fluxo de entrada) e `basic_ostream` (fluxo de saída).

Essas strings correspondem às instruções de impressão e entrada que vimos quando executamos o crackme pela primeira vez, então sabemos que estamos na função `main()` do crackme.
Agora, neste ponto, começaria o trabalho tedioso, onde teríamos que sentar e renomear cada nome de variável e chamada de função para algo significativo, enquanto analisamos o que o binário está fazendo para resolver o crackme. Os engenheiros reversos costumavam fazer isso manualmente na desmontagem, mas felizmente a tecnologia e os dólares dos impostos nos deram este útil descompilador que pode ser programado para trabalhar com IA.
Então, em vez de fazer trabalho de verdade, vamos relaxar, desligar nossos cérebros e deixar nossa IA local fazer todo o trabalho para nós.
Durante a configuração deste tutorial, você deve ter instalado um plugin do Ghidra para ajudar na renomeação de funções e variáveis. Agora é a hora de usá-lo. No Ghidra, na barra de ferramentas na parte superior, clique em `Window` e depois em `Fast AI Renamer` para abrir o plugin.

Você deve ver a interface do plugin, que consiste em vários botões e uma área de texto. Comece clicando no botão "Config" e, em seguida, certifique-se de que tudo esteja configurado para se comunicar corretamente com sua IA local. Aqui está como fica no meu computador:

Você pode fechar a janela de configuração clicando em "Save" e, em seguida, clique no botão `Rename Variables`. Uma janela de progresso com um dragão aparecerá, e você pode ouvir seu computador começar a trabalhar enquanto ele executa o modelo de IA local para renomear todas as variáveis na janela do descompilador:

Quando a IA terminar (leva cerca de 10 a 20 segundos na minha máquina), você deverá ver uma descrição de quais variáveis ela renomeou na área de texto do plugin, e as próprias variáveis devem ser renomeadas na janela do descompilador:

Todos os modelos de IA, especialmente os pequenos locais, são inerentemente não confiáveis, então você pode ver alguns erros nesta última etapa. Falaremos sobre isso novamente mais tarde. Enquanto isso, você pode sempre executar a IA novamente clicando no botão `Rename Variables` até obter resultados sem erros que pareçam satisfatórios.
## Resolvendo o Crackme
Neste ponto, temos uma saída do descompilador bem marcada, onde todos os nomes de variáveis foram renomeados para algo significativo. Tradicionalmente, um engenheiro reverso poderia agora ler este código e começar a esboçar uma solução para o Crackme. No entanto, achei que seria interessante tentar fazer a IA local resolver o crackme para nós. Lembre-se, não usaremos nosso cérebro hoje.
Primeiro, fiz a IA terminar de marcar todo o binário para nós clicando no botão `Rename ALL Functions and Variables`. Isso serve para atribuir nomes a todas as chamadas de função usadas nesta função do crackme, o que limpa ainda mais a saída do descompilador. Na minha máquina, isso leva cerca de 15 minutos.
Quando você estiver usando o `llama.cpp` com os parâmetros de CLI que listei em `Local AI Setup`, pode abrir uma Interface de Chat para seu modelo local navegando no navegador para [http://localhost:8090/](http://localhost:8090/)
Aqui, inseri a saída do descompilador da função do crackme (você pode obtê-la clicando no botão "Decompile Function" na interface do plugin, ou simplesmente copiando-a da janela do descompilador à direita) e pedi à IA para codificar uma solução para nós:
======================================================================
======================================================================
======================================================================
Quero que você me ajude a escrever uma solução para um crackme. Abaixo está a listagem do descompilador de uma função de um crackme, onde o usuário digita um nome de usuário e uma senha, e o acesso é concedido se eles fornecerem a senha correta. A senha é calculada na função.```
/* 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;
}
Por favor, leia a listagem do descompilador e, em seguida, complete meu código C++ abaixo para calcular a senha correta para um nome de usuário inserido:``` #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;
}
## A Solução Gerada
Na primeira vez que foi gerada uma solução, ela estava incorreta. Incluí-a em [tut1.txt](https://github.com/markoglasgow/gemma_crackme_tutorial/blob/master/tut1.txt) neste repositório. À primeira vista, a solução _parece_ correta, mas falha quando a testamos. As IAs são fundamentalmente não confiáveis, e este é um exemplo perfeito disso.
Não importa, podemos simplesmente abrir um novo chat, copiar e colar o prompt acima e tentar fazer com que a IA gere uma nova solução para nós. No meu caso, consegui uma solução funcional na segunda tentativa:```
#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;
}
Eu incluí o chat que gerou a solução como solution_chat.html neste repositório. Ele incluía o fluxo de raciocínio, o que achei muito legal para um modelo tão pequeno rodando localmente.
De qualquer forma, podemos compilar a solução com o Visual Studio e executá-la para gerar uma combinação válida de nome de usuário/senha. Em seguida, inserimos o nome de usuário/senha no crackme e verificamos que resolvemos:

gg
Como vimos várias vezes ao longo deste tutorial, a IA é inerentemente não confiável. Considere as duas capturas de tela abaixo:


A primeira captura de tela é de antes no tutorial, enquanto a segunda foi tirada na mesma etapa, mas com uma nova execução da renomeação pela IA. Observe na janela do descompilador como, na captura de tela superior, o buffer de entrada da senha está rotulado como &pass_input_buffer, enquanto na captura de tela inferior o mesmo buffer está rotulado como &username_buffer_1. Na captura de tela inferior, a IA está mentindo para nós, e chamamos essas mentiras de "alucinações".
Toda IA vai mentir para você e alucinar. Quanto menor o modelo, mais frequentes as alucinações. Quanto maior o modelo, mais sorte você terá e menos elas alucinarão. Quanto mais você paga, mais sorte tem. No entanto, nenhum modelo de IA está imune a alucinações, e você nunca deve esquecer isso ao trabalhar com eles.
A IA é mais adequada para tarefas onde você pode verificar a saída, ou onde a saída tem baixo risco o suficiente para que a precisão perfeita não seja necessária. No caso da engenharia reversa, a IA pode nos ajudar a dar uma olhada rápida em um binário desconhecido e nos poupar muito trabalho manual tedioso. Mas, como vimos com a renomeação ruim que ela fez, também pode nos levar pelo caminho errado. Sempre trate os resultados gerados por IA com cautela e sempre os verifique se possível.
A melhor maneira de usar IA é verificar seus resultados automaticamente. Por exemplo, se você está fazendo a IA escrever código, seria ótimo ter um script que testa a saída da IA em relação a algum conjunto de testes que você escreveu manualmente. No caso da IA gerando uma solução para o nosso crackme, poderíamos ter escrito um conjunto de testes que testa cada solução gerada pela IA automaticamente contra o crackme, e então aceitar uma solução que gerasse boas senhas para milhares de nomes de usuário diferentes. Assim, poderíamos executar a IA em loop, até que ela escrevesse uma solução que passasse em nossos testes.
Resolvemos este crackme com um modelo relativamente pequeno (Gemma 4 E4B). Este modelo tem 8 bilhões de parâmetros, ao contrário dos modelos de última geração que existem que têm centenas de bilhões, senão trilhões de parâmetros. Um modelo menor não conseguirá raciocinar bem, alucinará com mais frequência e se tornará inútil mais rapidamente à medida que sua janela de contexto se enche.
Em um exercício real de engenharia reversa, podemos ter que reverter funções que são maiores do que a janela de contexto do nosso modelo pequeno, ou que têm lógica muito complicada para sua inteligência limitada raciocinar. Precisamos estar familiarizados com as limitações de nossos modelos ao usá-los, para entendermos de antemão onde eles nos falharão e como.
À medida que o hardware de consumo avança lado a lado com a arquitetura de modelos, acredito que poderemos executar modelos maiores e melhores localmente, o que colocará tarefas de engenharia reversa ainda mais avançadas ao alcance de nossos modelos locais.
O plugin "Fast AI Renamer" do Ghidra que escrevi atualmente escreve um resumo para cada função que analisa, e tem a capacidade de exportar em massa as listagens C e asm do projeto em que está trabalhando. Gostaria de escrever uma ferramenta que calcule um embedding para cada resumo de função e depois indexe em um banco de dados vetorial ou SQLite, e então possamos consultar o banco de dados para encontrar funções de acordo com consultas em linguagem natural.
O plugin do Ghidra atualmente usa um fluxo de trabalho simples sem conversa de múltiplas voltas para renomeação, e depende da análise de um CSV da saída da IA para realizar sua tarefa. Implementei as coisas dessa maneira porque não achava que um modelo de IA pequeno seria capaz de lidar com longas conversas e múltiplas chamadas de ferramentas inerentes a um fluxo de trabalho agêntico. Posso estar errado, e formas alternativas de solicitar a IA devem ser exploradas. Ainda assim, para modelos locais pequenos, acho melhor limitar o tamanho da janela de contexto o máximo possível.
Não tenho certeza de quão vulnerável o plugin do Ghidra é à injeção de prompt, e qualquer profissional de segurança deve ter cuidado ao usá-lo para analisar malware ou binários hostis. Em geral, isso se aplica a todas as ferramentas de engenharia reversa alimentadas por IA, que podem ter servidores MCP não autenticados em localhost, ou ferramentas disponíveis para funções sensíveis que podem alcançar execução de código. Tenha cuidado.
Dado que estamos usando um modelo pequeno de propósito geral para engenharia reversa, pergunto-me quanto seu desempenho pode ser melhorado ajustando-o finamente em conversas destiladas de um modelo maior e mais inteligente?
Finalmente, a funcionalidade de exportação C/asm do plugin do Ghidra funciona bem com Claude Code, e consegui usar Claude Code + Sonnet para escrever uma solução correta para este crackme na primeira tentativa, apenas alimentando-o com a descompilação anotada do crackme e dizendo para encontrar uma solução. Pergunto-me se existem habilidades que podemos escrever para o Claude Code que possam torná-lo melhor em analisar um binário através dos arquivos de texto da saída do descompilador, sem fazê-lo passar por um servidor MCP para o Ghidra para realizar a análise. Por exemplo, poderíamos introduzir uma habilidade no Claude Code que permitisse inspecionar os bytes na Memória Virtual do binário, para que, se ele vir alguns dados sendo referenciados a partir das listagens do desassembly/descompilador, possa ver o que essa memória contém sem ter que passar pelo Ghidra.
Obrigado ao Kryptos por fazer o Crackme, e ao Crackmes.one por hospedá-lo. Agradeço por terem continuado de onde o crackmes.de parou (RIP).
Obrigado ao Google por lançar um modelo local incrível e elegante.
Obrigado à NSA por usar meus impostos para fazer uma ferramenta de engenharia reversa legal, e obrigado ao Ryan Kurtz por todo o suporte que ele fornece no Github do Ghidra.