
Tutoriel étape par étape sur l'utilisation du modèle d'IA local Gemma 4 E4B de Google pour rétro-ingénier un crackme Windows avec Ghidra, incluant la configuration pour l'inférence locale et le renommage automatisé des fonctions/variables.
Je jouais avec le nouveau modèle local open weights Gemma E4B que Google a publié, et à ma grande surprise, je rencontrais un grand succès dans son utilisation pour des scénarios de rétro-ingénierie locaux hors ligne. J'ai voulu écrire ce tutoriel pour faire connaître que l'IA locale est maintenant assez bonne pour de nombreuses tâches de base de rétro-ingénierie, et que les choses vont probablement s'améliorer rapidement à partir de là.
L'une des parties les plus fastidieuses de la rétro-ingénierie d'un nouveau binaire est au tout début, quand on n'a aucune idée de ce que sont les fonctions et variables importantes. Il existe de nombreuses astuces que les rétro-ingénieurs utilisent pour commencer, notamment en regardant les références de chaînes, le binaire diffing ou la correspondance de fonctions similaires.
L'IA est très utile ici, et personnellement j'ai eu beaucoup de succès en utilisant l'API OpenAI pour annoter un binaire ou nettoyer la sortie du décompilateur. Cependant, l'utilisation de ces API présente plusieurs inconvénients :
Coût - La décompilation et le désassemblage génèrent des tonnes de tokens. Les API facturent par token, donc les binaires plus gros peuvent coûter beaucoup d'argent à analyser. Si vous traitez une cible volumineuse avec de nombreux binaires qui se mettent à jour chaque semaine, ces coûts peuvent s'accumuler rapidement et sont prohibitifs pour les ingénieurs amateurs.
Confidentialité - Lorsque vous utilisez une API distante, l'hôte de l'IA a un aperçu de ce que vous faites. C'est un élément rédhibitoire dans certains scénarios professionnels.
Contrôle - Lorsque vous dépendez d'une API distante, vous n'avez aucun contrôle sur les modèles qui vous sont servis, ni sur la qualité des modèles. Si vous comptez sur eux pour des tâches critiques, cela peut être problématique lorsqu'ils deviennent lents ou tombent en panne au moment où vous en avez besoin, ou lorsque la qualité de leur sortie se dégrade au point de devenir inutile.
Exécuter vos propres modèles IA locaux résout certains de ces problèmes :
Coût - Cela peut être beaucoup moins cher d'exécuter un modèle local que de dépendre d'un service hébergé. Bien que votre modèle local soit probablement plus petit et plus lent que celui d'un bon fournisseur, s'il est assez bon et fonctionne en un temps raisonnable, il peut être judicieux d'économiser de l'argent en exécutant votre propre modèle, surtout si vous traitez de grandes quantités de données pour des tâches simples.
Confidentialité - Lorsque vous exécutez le modèle localement, il n'y a pas d'appels réseau et vous contrôlez totalement votre confidentialité. Personne ne peut voir pour quoi vous utilisez le modèle sur votre propre machine.
Contrôle - La beauté d'un modèle open weights est que personne ne peut vous l'enlever. OpenAI ou Anthropic pourraient un jour rendre leurs modèles SotA indisponibles, soit par des augmentations de prix, soit en supprimant explicitement leurs API. Mais avec un modèle open weights, vous contrôlez votre destin, pour le meilleur ou pour le pire.
Les modèles IA locaux ont cependant leurs inconvénients :
Taille - Plus votre modèle est grand, plus il est intelligent. Cependant, la plupart des grands modèles ne peuvent pas tenir sur du matériel grand public. Pour cette raison, si vous exécutez un modèle local, il y a de fortes chances que vous exécutiez un modèle 10 à 100 fois plus petit qu'un modèle SotA (State-of-the-Art). Cette diminution de taille entraîne directement une diminution de l'intelligence du modèle, les rendant inadaptés à de nombreuses tâches que les gens considèrent comme acquises dans des modèles SotA comme ChatGPT/Codex ou Claude.
Vitesse - Les modèles locaux fonctionneront probablement plus lentement sur votre machine que lors de l'utilisation d'une API IA. Là encore, cela est dû aux limites du matériel grand public et à certaines astuces que les fournisseurs d'API peuvent utiliser et qui ne sont généralement pas disponibles pour vous.
Configuration - Exécuter des modèles locaux, c'est comme essayer de faire fonctionner Linux sur un ordinateur portable reconditionné plutôt que d'entrer dans un Apple Store et d'acheter un tout nouveau MacBook Air. L'expérience Codex et Claude Code est l'expérience Apple Store de l'IA. L'expérience des modèles IA locaux, c'est le gars avec un fedora dans son garage qui tape sur un ordinateur bancal pour essayer de le faire fonctionner. Au minimum, vous devez vous préoccuper des éléments suivants :
Ce n'est pas un parcours facile, et beaucoup de gens abandonnent en supposant que les modèles IA locaux ne sont pas à la hauteur de la tâche, parce qu'ils n'ont jamais trouvé la bonne combinaison de matériel/modèle/paramètres/invite/harnais pour les faire fonctionner pour leur tâche. Bien que dans de nombreux cas ils aient raison, j'espère que ce tutoriel au moins mettra en lumière le chemin parcouru par les modèles locaux, comment ils peuvent aider à la rétro-ingénierie, et inspirera les gens à essayer l'IA locale.
(le mot de passe de l'archive est crackmes.one). J'ai hébergé un lien alternatif ici dans le dépôt au cas où le lien original ne fonctionnerait plus.
Ghidra 12.04 est utilisé pour le désassemblage et la décompilation. Vous devrez installer OpenJDK 21 pour l'utiliser : https://github.com/nationalsecurityagency/ghidra
En travaillant sur ce tutoriel, j'ai codé en vibe avec Claude Code un plugin Ghidra pour renommer les fonctions et variables avec l'IA. Vous pouvez télécharger le plugin ici : https://github.com/markoglasgow/Ghidra_FastAIRenamer_Plugin
Pour l'installer, déplacez simplement le fichier zip ghidra_12.0.4_PUBLIC_20260427_FastAIRenamerPlugin.zip vers ${GHIDRA_HOME}\Extensions\Ghidra, puis lancez Ghidra en exécutant ${GHIDRA_HOME}\ghidraRun.bat. Pour activer le plugin, dans l'écran initial de Ghidra, dans le menu supérieur, sélectionnez File -> Install Extensions, puis dans le navigateur de plugins, cochez la case à côté de FastAIRenamerPlugin, puis cliquez sur Ok. Ghidra vous demandera de redémarrer, faites-le immédiatement.
Pour configurer le plugin, au prochain démarrage de Ghidra, allez dans le menu supérieur Tools -> Run Tool -> CodeBrowser. Ghidra dira "New Extensions detected. Would you like to configure them?". Cliquez sur Yes, puis cochez à nouveau la case à côté de FastAIRenamerPlugin, puis cliquez sur Ok. Quand le CodeBrowser s'ouvre, dans le menu supérieur, cliquez sur Window -> Fast AI Renamer, puis cliquez sur le bouton Config. Ici, vous pourrez configurer votre modèle IA. Fermez la fenêtre du plugin et la fenêtre vide du CodeBrowser une fois terminé.
Note : si vous rencontrez des problèmes pour charger le plugin, vous devrez peut-être activer le mode Développeur dans Ghidra (File -> Configure -> case à cocher à côté de Developer)
Note : vous pouvez toujours vérifier si le plugin est chargé en allant dans CodeBrowser, en cliquant sur File -> Configure -> Ghidra Core -> cliquer sur le bouton bleu de configuration -> filtrer par "FastAIRenamer" -> vous assurer que la case à côté de son nom est cochée.
Note : pour désinstaller le plugin, ouvrez d'abord CodeBrowser, File -> Configure -> Ghidra Core -> cliquer sur le bouton bleu de configuration -> filtrer par "FastAIRenamer" -> décocher -> ok. Fermez CodeBrowser, puis dans la fenêtre initiale de Ghidra, File -> Install Extensions -> décocher "FastAIRenamer". Enfin, fermez Ghidra et supprimez ghidra_12.0.4_PUBLIC_20260427_FastAIRenamerPlugin.zip de ${GHIDRA_HOME}\Extensions\Ghidra. Pour vous assurer que l'extension est supprimée, la prochaine fois que vous exécutez Ghidra, dans la fenêtre initiale, allez à Help -> Runtime Information -> Extension Points -> filtrer par "FastAIRenamer" et assurez-vous que rien n'apparaît. Ouf.
Enfin, assurez-vous d'avoir Visual Studio ou un autre environnement de développement C++ configuré pour pouvoir coder en vibe une solution au crackme le moment venu.
Les configurations d'IA locale des gens varieront considérablement en fonction du matériel et de l'argent dont ils disposent. Pour moi :
J'utilise personnellement une Nvidia GTX 3080, avec Cuda 13.2 installé. Vous pouvez vérifier votre version de Cuda en exécutant nvcc --version dans votre terminal Windows.
J'utilise les quantifications https://huggingface.co/bartowski/google_gemma-4-E4B-it-GGUF/blob/main/google_gemma-4-E4B-it-Q8_0.gguf de bartowski. C'est peut-être une superstition de ma part, mais pour les tâches analytiques, j'essaie de fonctionner avec le moins de quantification possible, et de l'éviter complètement si je le peux.
J'utilise llama.cpp comme serveur d'inférence, avec cette version spécifique : llama-b8893-bin-win-cuda-13.1-x64.
J'exécute llama.cpp avec les paramètres CLI suivants :```
..\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
Cela donne `75 tokens/sec`, ce qui est assez rapide pour une IA locale.
## Pas de matériel ? Pas de problème
Si vous ne disposez pas du matériel nécessaire ou si vous rencontrez des problèmes de configuration, mais que vous souhaitez tout de même suivre ce tutoriel, vous pouvez obtenir un identifiant OpenRouter et utiliser l'une des API gratuites :
https://openrouter.ai/models/?q=free
Google propose spécifiquement `Gemma 4 31B` et `Gemma 4 26B-A4B` gratuitement en ce moment (pour une durée limitée seulement) :
https://openrouter.ai/google/gemma-4-31b-it:free
https://openrouter.ai/google/gemma-4-26b-a4b-it:free
... gardez simplement à l'esprit qu'avec ces API gratuites, elles sont lentes, peu fiables, et toutes les données que vous leur envoyez seront probablement enregistrées dans leurs analyses internes et utilisées lors de leur prochaine session d'entraînement. Néanmoins, pour les besoins de ce tutoriel, ces API devraient vous permettre de suivre.
Pour configurer le plugin Ghidra afin d'utiliser votre identifiant OpenRouter, ouvrez la configuration du plugin et saisissez ce qui suit :```
Base URL: https://openrouter.ai/api/
API Key: <your OpenRouter API Key>
Model Name: qwen/qwen3-235b-a22b-2507
Cet exemple exécutera le plugin contre le modèle qwen3 ici
Décompressez et exécutez crackmepls.exe, vous devriez voir un écran de connexion standard. Saisissez un mot de passe au hasard, et vous obtiendrez un message 'Accès refusé' :```
User: marko
Pass: 123
Access denied
Ouvrez Ghidra en exécutant `ghidraRun.bat`. Dans la barre d'outils, sélectionnez `File -> New Project`, laissez `Non-Shared Project` sélectionné et cliquez sur `Next >>`, puis sélectionnez un répertoire de projet vide et donnez un nom au projet. Cliquez ensuite sur `Finish`.
Ensuite, cliquez sur `File -> Import File`, puis sélectionnez `crackmepls.exe` pour l'ajouter au projet. Ghidra affichera des détails sur le fichier, vous indiquant qu'il s'agit d'un fichier `Portable Executable (PE)` pour `x86:LE:64:default:windows`. Cliquez simplement sur `OK` pour accepter sans rien modifier. Après un court délai, d'autres détails sur le fichier s'afficheront, cliquez à nouveau sur `OK` pour accepter. Enfin, double-cliquez sur `crackmepls.exe` dans le projet pour ouvrir le navigateur de code et commencer le désassemblage.
Au tout début, Ghidra affichera un message : `crackmepls.exe n'a pas été analysé. Voulez-vous l'analyser maintenant ?`. Cliquez sur `Yes` puis sur le bouton `Analyze` dans la fenêtre suivante, et attendez que Ghidra localise, désassemble et décompile toutes les fonctions du binaire. Vous pourriez recevoir une ou deux erreurs pendant l'analyse concernant des fichiers PDB introuvables, cliquez simplement sur `Ok` et ignorez-les.
Une fois que Ghidra a terminé l'analyse du fichier, vous devriez voir quelque chose comme ci-dessous :

Il s'agit de votre point d'entrée MSVC standard. Double-cliquez sur `FUN_14000200c` et faites défiler vers le bas.
Vous devriez voir des références dans la fenêtre du décompilateur à `__p___argv`, `__p___argc`, puis un appel de fonction `FUN_140001290` qui les accepte comme paramètres. Il s'agit probablement de la fonction `main()` du crackme, double-cliquez donc dessus.

Une fois dans la fonction `FUN_140001290`, faites défiler un peu vers le bas dans la fenêtre du décompilateur. Vous verrez des références de chaîne aux chaînes `User:` et `Pass:`, ainsi que des références à `basic_istream` (flux d'entrée) et `basic_ostream` (flux de sortie).

Ces chaînes correspondent aux instructions d'affichage et de saisie que nous avons vues lors de la première exécution du crackme, nous savons donc que nous sommes dans la fonction `main()` du crackme.
Maintenant, à ce stade, le travail fastidieux commencerait, où nous devrions nous asseoir et renommer chaque nom de variable et chaque appel de fonction en quelque chose de significatif, en analysant ce que fait le binaire pour résoudre le crackme. Les ingénieurs en rétro-ingénierie devaient auparavant le faire manuellement en assembleur, mais heureusement, la technologie et les impôts nous ont offert ce décompilateur astucieux qui peut être scripté pour fonctionner avec l'IA.
Donc, au lieu de faire un vrai travail, asseyons-nous, éteignons notre cerveau, et laissons notre IA locale faire tout le travail à notre place.
Lors de la configuration de ce tutoriel, vous avez dû installer un plugin Ghidra pour aider au renommage des fonctions et des variables. C'est le moment de l'utiliser. Dans Ghidra, dans la barre d'outils en haut, cliquez sur `Window` puis `Fast AI Renamer` pour ouvrir le plugin.

Vous devriez voir l'interface utilisateur du plugin, qui est un ensemble de boutons et une zone de texte. Commencez par cliquer sur le bouton "Config", puis assurez-vous que tout est configuré pour communiquer correctement avec votre IA locale. Voici à quoi cela ressemble sur mon ordinateur :

Vous pouvez fermer la fenêtre de configuration en cliquant sur "Save", puis cliquez sur le bouton `Rename Variables`. Une fenêtre de progression avec un dragon devrait apparaître, et vous pourriez entendre votre ordinateur commencer à peiner pendant qu'il exécute le modèle d'IA local pour renommer toutes les variables dans la fenêtre du décompilateur :

Une fois que l'IA a terminé (cela prend environ 10 à 20 secondes sur ma machine), vous devriez voir une description des variables renommées dans la zone de texte du plugin, et les variables elles-mêmes devraient être renommées dans la fenêtre du décompilateur :

Tous les modèles d'IA, mais surtout les petits modèles locaux, sont intrinsèquement peu fiables, vous pourriez donc voir quelques erreurs dans cette dernière étape. Nous y reviendrons plus tard. En attendant, vous pouvez toujours relancer l'IA en cliquant à nouveau sur le bouton `Rename Variables`, jusqu'à obtenir des résultats sans erreur qui vous semblent satisfaisants.
## Résoudre le Crackme
À ce stade, nous avons une sortie de décompilateur joliment annotée, où tous les noms de variables ont été renommés en quelque chose de significatif. Traditionnellement, un ingénieur en rétro-ingénierie pourrait maintenant lire ce code et commencer à élaborer une solution pour le Crackme. Cependant, j'ai pensé qu'il serait intéressant d'essayer de faire résoudre le crackme par l'IA locale. Souvenez-vous, nous n'utiliserons pas notre cerveau aujourd'hui.
Tout d'abord, j'ai fait en sorte que l'IA termine l'annotation de l'ensemble du binaire en cliquant sur le bouton `Rename ALL Functions and Variables`. Cela permet d'attribuer des noms à tous les appels de fonction utilisés dans cette fonction du crackme, ce qui nettoie encore plus la sortie du décompilateur. Sur ma machine, cela prend environ 15 minutes.
Lorsque vous utilisez `llama.cpp` avec les paramètres CLI que j'ai listés sous `Local AI Setup`, vous pouvez ouvrir une interface de chat avec votre modèle local en naviguant dans le navigateur vers [http://localhost:8090/](http://localhost:8090/)
Ici, j'ai saisi la sortie du décompilateur de la fonction du crackme (vous pouvez l'obtenir en cliquant sur le bouton "Decompile Function" dans l'interface du plugin, ou simplement en la copiant depuis la fenêtre du décompilateur sur le côté droit), et j'ai demandé à l'IA de coder une solution pour nous :
======================================================================
======================================================================
======================================================================
Je veux que vous m'aidiez à écrire une solution pour un crackme. Ci-dessous se trouve la liste du décompilateur d'une fonction d'un crackme, où l'utilisateur saisit un nom d'utilisateur et un mot de passe, puis l'accès est accordé s'il a fourni le bon mot de passe. Le mot de passe est calculé dans la fonction.```
/* 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;
}
Veuillez lire la liste du décompilateur, puis complétez mon code C++ ci-dessous pour calculer le mot de passe correct pour un nom d'utilisateur saisi :``` #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;
}
## The Generated Solution
La première fois qu'elle a généré une solution, elle était incorrecte. Je l'ai incluse dans [tut1.txt](https://github.com/markoglasgow/gemma_crackme_tutorial/blob/HEAD/tut1.txt) de ce dépôt. À première vue, la solution _semble_ correcte, mais échoue lorsque nous l'essayons. Les IA sont fondamentalement peu fiables, et c'en est un parfait exemple.
Peu importe, nous pouvons simplement ouvrir un nouveau chat, copier-coller l'invite ci-dessus, et essayer d'obtenir de l'IA une nouvelle solution. Dans mon cas, j'ai obtenu une solution fonctionnelle au deuxième essai :```
#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;
}
J'ai inclus le chat qui a généré la solution sous forme de solution_chat.html dans ce dépôt. Il incluait le flux de raisonnement, ce que j'ai trouvé assez cool pour un si petit modèle fonctionnant localement.
Quoi qu'il en soit, nous pouvons compiler la solution avec Visual Studio puis l'exécuter pour générer une combinaison nom d'utilisateur/mot de passe valide. Nous entrons ensuite le nom d'utilisateur/mot de passe dans le crackme et vérifions que nous l'avons résolu :

gg
Comme nous l'avons vu plusieurs fois tout au long de ce tutoriel, l'IA est intrinsèquement peu fiable. Considérez les deux captures d'écran ci-dessous :


La première capture d'écran provient d'une étape antérieure du tutoriel, tandis que la seconde a été prise à la même étape, mais avec une nouvelle exécution différente du renommage par IA. Remarquez dans la fenêtre du décompilateur comment, dans la capture du haut, le tampon de saisie du mot de passe est étiqueté &pass_input_buffer, alors que dans la capture du bas, le même tampon est étiqueté &username_buffer_1. Dans la capture du bas, l'IA nous ment, et nous appelons ces mensonges des « hallucinations ».
Toute IA vous mentira et hallucinera. Plus le modèle est petit, plus les hallucinations sont fréquentes. Plus le modèle est grand, plus vous aurez de chance et moins il hallucinera. Plus vous payez, plus vous avez de chance. Cependant, aucun modèle d'IA n'est immunisé contre les hallucinations, et vous ne devez jamais oublier cela lorsque vous travaillez avec eux.
L'IA est la mieux adaptée aux tâches où vous pouvez vérifier la sortie, ou lorsque la sortie est suffisamment peu critique pour qu'une précision parfaite ne soit pas nécessaire. Dans le cas du rétro-ingénierie, l'IA peut nous aider à jeter un œil rapidement à un binaire inconnu et nous faire gagner beaucoup de travail manuel fastidieux. Mais comme nous l'avons vu avec le mauvais renommage qu'elle a effectué, elle peut aussi nous conduire sur une mauvaise piste. Traitez toujours les résultats générés par l'IA avec prudence et vérifiez-les toujours si vous le pouvez.
La meilleure façon d'utiliser l'IA est de vérifier ses résultats automatiquement. Par exemple, si vous faites écrire du code par l'IA, il serait idéal d'avoir un script qui teste la sortie de l'IA par rapport à une suite de tests que vous avez écrite à la main. Dans le cas où l'IA génère une solution pour notre crackme, nous aurions pu écrire une suite de tests qui teste chaque solution générée par l'IA par rapport au crackme automatiquement, puis accepter une solution qui génère de bons mots de passe pour des milliers de noms d'utilisateur différents. Ensuite, nous pourrions exécuter l'IA en boucle jusqu'à ce qu'elle écrive une solution qui réussisse nos tests.
Nous avons résolu ce crackme avec un modèle relativement petit (Gemma 4 E4B). Ce modèle a 8 milliards de paramètres, contrairement aux modèles de pointe existants qui ont des centaines de milliards voire des billions de paramètres. Un modèle plus petit ne pourra pas raisonner correctement, hallucinera plus fréquemment et deviendra inutile plus rapidement à mesure que sa fenêtre de contexte se remplit.
Dans un véritable exercice de rétro-ingénierie, nous pourrions devoir rétro-ingénier des fonctions qui sont plus grandes que la fenêtre de contexte de notre petit modèle, ou qui ont une logique trop alambiquée pour que son intelligence limitée puisse raisonner. Nous devons connaître les limites de nos modèles lorsque nous les utilisons, afin de comprendre à l'avance où et comment ils nous feront défaut.
À mesure que le matériel grand public progresse de pair avec l'architecture des modèles, je pense que nous serons en mesure d'exécuter localement des modèles plus grands et meilleurs, ce qui mettra à portée de nos modèles locaux des tâches de rétro-ingénierie encore plus avancées.
Le plugin Ghidra « Fast AI Renamer » que j'ai écrit génère actuellement un résumé pour chaque fonction qu'il analyse, et il a la capacité d'exporter en masse les listages C et assembleur du projet sur lequel il travaille. J'aimerais écrire un outil qui calcule un embedding pour chaque résumé de fonction, puis l'indexe dans une base de données vectorielle ou SQLite, et ensuite nous pourrions interroger la base de données pour trouver des fonctions en fonction de requêtes en langage naturel.
Le plugin Ghidra utilise actuellement un flux de travail simple sans conversation à plusieurs tours pour le renommage, et repose sur l'analyse d'un CSV à partir de la sortie de l'IA pour effectuer sa tâche. J'ai implémenté les choses de cette façon parce que je ne pensais pas qu'un petit modèle d'IA serait capable de gérer les longues conversations et les multiples appels d'outils inhérents à un flux de travail agentique. Je pourrais avoir tort, et d'autres façons de solliciter l'IA devraient être explorées. Néanmoins, pour les petits modèles locaux, je pense qu'il est préférable de limiter autant que possible la taille de la fenêtre de contexte.
Je ne suis pas sûr de la vulnérabilité du plugin Ghidra à l'injection de prompt, et tout professionnel de la sécurité devrait se méfier de l'utiliser pour analyser des malwares ou des binaires hostiles. En général, cela s'applique à tous les outils de rétro-ingénierie pilotés par IA, qui pourraient avoir des serveurs MCP non authentifiés sur localhost, ou des outils disponibles pour des fonctions sensibles pouvant exécuter du code. Soyez prudent.
Étant donné que nous utilisons un petit modèle à usage général pour la rétro-ingénierie, je me demande à quel point ses performances peuvent être améliorées en le réglant finement sur des conversations distillées à partir d'un modèle plus grand et plus intelligent ?
Enfin, la fonctionnalité d'exportation C/asm du plugin Ghidra fonctionne bien avec Claude Code, et j'ai pu utiliser Claude Code + Sonnet pour écrire une solution correcte pour ce crackme du premier coup, en lui fournissant simplement la décompilation annotée du crackme et en lui demandant de trouver une solution. Je me demande s'il existe des compétences que nous pourrions écrire pour Claude Code qui pourraient l'aider à mieux analyser un binaire à travers les fichiers texte de la sortie du décompilateur, sans passer par un serveur MCP vers Ghidra pour effectuer l'analyse. Par exemple, nous pourrions introduire une compétence pour Claude Code qui lui permettrait d'inspecter les octets dans la mémoire virtuelle du binaire, de sorte que s'il voit des données référencées dans les listages du désassembleur/décompilateur, il puisse voir ce que cette mémoire contient sans avoir à passer par Ghidra.
Merci à Kryptos d'avoir créé le Crackme, et à Crackmes.one de l'héberger. Je leur suis reconnaissant d'avoir repris là où crackmes.de s'est arrêté (RIP).
Merci à Google d'avoir publié un modèle local étonnant et ingénieux.
Merci à la NSA d'utiliser mes impôts pour créer un outil de rétro-ingénierie génial, et merci à Ryan Kurtz pour tout le soutien qu'il apporte sur le Github de Ghidra.