Plusieurs approches ont été suggérées, comme chercher directement la chaîne de caractères ou explorer le pointeur correspondant pour identifier la chaîne de sortie. Je vous encourage vivement à les consulter, car elles permettent d’apprendre une multitude de choses passionnantes.
Pour ma part, voici la démarche que j’ai suivie pour afficher le flag contenu dans le fichier aaarg
. Mon objectif principal était de comprendre comment exécuter correctement le binaire afin qu’il produise la réponse attendue.
Étape 1 : Identifier le type de fichier
La première étape consiste à identifier le type du fichier. Sous Linux, cela est relativement simple avec la commande suivante :
$ file aaarg
aaarg: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=f5b07c01242cc5987bed7730c2762ae0491b5ddc, stripped
Un passage sur Wikipédia révèle que le format ELF (Executable and Linkable Format) est un standard pour les exécutables sous les systèmes Unix.
Étape 2 : Lancer le fichier
En exécutant le fichier, on constate qu’il ne produit aucun message d’erreur, mais qu’aucun contenu visible ne s’affiche non plus :
$ chmod +x aaarg
$ ./aaarg
$
$ ./aaarg foo
$
$ ./aaarg bar
$
Étape 3 : Analyse avec Cutter
Pour aller plus loin, j’ai utilisé Cutter, un outil de reverse engineering avec interface graphique, téléchargeable ici : Cutter v2.3.4. Une fois le binaire ouvert dans Cutter, je me suis concentré sur la fonction main
, principale dans tout programme écrit en C.
Voici ce que j’ai découvert dès le début :
0x00401195 cmp edi, 2 ; 2 ; argc
La ligne compare edi
avec 2
, et immédiatement après :
0x00401198 jl 0x401218
jl (Jump if Less
) : Cela signifie “sauter si la valeur est inférieure”. Si edi (le nombre d’arguments) est strictement inférieur à 2, le programme redirige l’exécution à l’adresse 0x00401218 :
0x00401218 ret
Un simple return
, arrêtant ainsi l’exécution. Cela m’a incité à examiner plus attentivement le code entre ces deux points. Cependant, pour simplifier l’analyse, j’ai décidé d’utiliser un décompilateur plus performant : Ghidra.
Étape 4 : Utilisation de Ghidra
Installation et configuration
Pour installer Ghidra, vous pouvez consulter le guide officiel disponible ici : Guide d’installation de Ghidra. Suivez ces étapes pour une première utilisation réussie :
- Téléchargez et décompressez l’archive correspondant à votre système d’exploitation.
- Lancez Ghidra via le script fourni (
ghidraRun
). - Ouvrez le fichier binaire que vous souhaitez analyser.
- Acceptez de lancer l’analyse et laissez les options par défaut des analyzers activées.
- Cliquez sur le bouton “Analyze” pour démarrer l’analyse.
Découverte de la fonction principale
Une fois l’analyse terminée, dirigez-vous vers le menu Window > Decompiler
. Cela ouvre une fenêtre qui permet de visualiser le code pseudo-C reconstitué à partir du binaire. Voici un aperçu de ce que vous pourriez obtenir :
Dans la fenêtre Symbol Tree, vous remarquerez un dossier intitulé Functions
. Ce dossier contient toutes les fonctions identifiées dans le programme. Cependant, la fonction principale (main
) n’est pas toujours nommée explicitement comme telle. Voici un exemple :
Astuce pour identifier main
avec Cutter
Pour les programmes où main
n’est pas directement identifiable dans Ghidra, une autre méthode consiste à utiliser Cutter. En examinant les fonctions dans Cutter, vous pouvez remarquer que l’adresse 0x00401190
correspond à la fonction principale. Cette correspondance permet de cibler précisément main
. Voici une capture d’écran pour illustrer :
On selectionne donc dans Ghidra la fonction appelée “FUN_00401190”.
Une fois analysée, la fonction a été reconstruite sous cette forme :
undefined8 FUN_00401190(int param_1, long param_2)
{
undefined8 uVar1;
ulong uVar2;
char *local_10;
uVar1 = 1;
if (1 < param_1) {
uVar2 = strtoul(*(char **)(param_2 + 8), &local_10, 10);
uVar1 = 1;
if ((*local_10 == '\0') && (uVar1 = 2, uVar2 == (long)-param_1)) {
uVar2 = 0;
do {
putc((int)(char)(&DAT_00402010)[uVar2], stdout);
uVar2 = uVar2 + 4;
} while (uVar2 < 0x116);
putc(10, stdout);
uVar1 = 0;
}
}
return uVar1;
}
Merci pour la précision ! Voici une version plus développée et bien écrite de l’étape 5, tout en conservant les explications originales et les images :
Étape 5 : Comprendre les conditions clés
Pour simplifier la lecture et rendre le code plus compréhensible, il est conseillé de renommer les paramètres et les fonctions dans l’interface de Ghidra. Cela permet de clarifier les rôles de chaque élément analysé. Voici un exemple :
Une fois cela fait, on identifie une condition particulièrement importante dans le code décompilé :
if ((*local_10 == '\0') && (uVar1 = 2, uVar2 == (long)-argc)) {
Cette condition peut être décomposée pour mieux comprendre son fonctionnement :
Première partie : (*local_10 == '\0')
Cette vérification assure que toute la chaîne passée en argument a été correctement convertie en entier. Autrement dit, il ne doit pas y avoir de caractères non numériques dans la chaîne. Pour confirmer cela, on se réfère à l’utilisation de la fonction strtoul
et de son deuxième paramètre, local_10
.
Deuxième partie : (uVar2 == (long)-argc)
Cette condition compare la valeur convertie (uVar2
) avec -argc
, qui représente l’opposé du nombre total d’arguments passés au programme.
Cela signifie qu’au moins un argument doit être fourni, et que cet argument doit être égal à -argc
. Mais cela soulève une question : quel est cet argument ? Est-ce le deuxième ? Le troisième ?
Pour répondre à cette question, on examine une partie spécifique du code, visible ici :
À première vue, on remarque un décalage de +8
, ce qui pourrait laisser penser qu’il s’agit du huitième argument. Cependant, en explorant davantage, on découvre un cast avec (char **)
, ce qui modifie la logique :
- Étape 1 : Prendre l’adresse de
argv
, qui contient les arguments passés au programme. - Étape 2 : Ajouter un offset de 8 octets, correspondant à la taille d’un pointeur en architecture x86-64.
- Étape 3 : L’adresse obtenue pointe sur
argv[1]
, soit le deuxième élément du tableau des arguments.
Ainsi, l’argument en question est bien argv[1]
.
Étape 6 : Tester l’hypothèse
Avec ces informations, j’ai tenté la commande suivante, qui semblait logique :
$ ./aaarg -2
FCSC{f9a38adace9dda3a9ae53e7aec180c5a73dbb7c364fe137fc6721d7997c54e8d}
Et voilà 😊