Hack'in 2025 - Reverse - Animal Fabuleux
Achievement : First Blood sur ce challenge lors de l’événement !
Fichier fourni :
animal_fabuleux
L’objectif est de trouver le bon argument (flag) à passer à ce programme.
Analyse
Section titled “Analyse”Un premier file nous indique que nous avons affaire à un binaire ELF 64-bit.
~/Downloads/Hackin file animal_fabuleuxanimal_fabuleux: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=703e8337a1f696300ff09ab3426e26afa98bda38, for GNU/Linux 3.2.0, strippedJe remarque aussi que celui-ci est stripped, ce qui rendra le travail un peu moins agréable, mais cela reste un inconvénient mineur.
Ensuite effectuer strings filtré par |grep flags révèle les chaînes suivantes :
~/Downloads/Hackin strings animal_fabuleux |grep flagwrong flagwell done! you can use this flag to validate the challenge: HNx04{%s}Usage: %s <flag>Enfin, avant de commencer le désassemblage, j’essaie de lancer le programme et constate cette erreur :
~/Downloads/Hackin ./animal_fabuleux blablabla./animal_fabuleux: error while loading shared libraries: libunicorn.so.2: cannot open shared object file: No such file or directoryCela nous donne un indice énorme, le programme utilise une librairie nommée Unicorn
Après une rapide recherche google, j’apprends ce qu’est Unicorn :
“Unicorn is a lightweight multi-platform, multi-architecture CPU emulator framework.”
Ma première réaction a été “Oula, les problèmes”, et réfléchir à changer de challenge. Mais j’ai décidé de ne pas me laisser décourager par l’aura intimidante de celui-ci.
Après avoir installé la librairie, voici le résultat du programme :
~/Downloads/Hackin ./animal_fabuleux testaaawrong flagJ’ai remarqué qu’entre son lancement et l’affichage de “wrong flag”, il y avait un délai, ce qui est probablement une mesure anti-bruteforce.
De plus, nous pouvons nous attendre à ce que la chaîne de retour si nous trouvons le bon flag soit : well done! you can use this flag to validate the challenge : HNx04{cequejaientré}, comme nous l’avons vu dans les strings
Takeaway :
- Binaire stripped, pas de symbols utile
- Le flag est au format
HNx04{%s} - Une librairie d’émulation de CPU est utilisé (Unicorn)
Désassemblage
Section titled “Désassemblage”Après avoir passé le programme dans Binary Ninja, je constate que le plus gros de la logique se déroule dans la fonction main. Celle-ci se décompose en 3 parties :
- Vérification du contexte
- Initialisations
- Boucle logique principale
Vérification du contexte
Section titled “Vérification du contexte”
Le programme demande exactement deux arguments (son propre nom, puis le flag)
Puis il attend une seconde (la mesure anti-bruteforce)
Et vérifie si l’argument 1 (notre flag) a bien une longueur de 8 caractères. Dans le cas contraire la fonction sub_4011d9 (qui après inspection affiche la chaîne wrong flag) est appelé et le programme s’arrête.
Nous pouvons donc renommer sub_4011d9 en showFailText et continuer.
Takeaway : le flag fait 8 caractères
Initialisations
Section titled “Initialisations”
Ici, nous repérons plusieurs utilisations de fonctions commençant par uc_, en remontant à leurs définitions, nous constatons que ce sont des fonctions externes :
Ce sont à coup sûr les fonctions venant de UniCorn (UC).
N’ayant pas trouvé de documentation explicite pour ces fonctions, je me suis rabattu sur le tutoriel sur le site officiel d’Unicorn, qui donne une vue d’ensemble de comment s’utilisent ces fonctions. À partir de cela j’ai donc déduit l’utilité de chacune d’entre elles et la signification de leurs paramètres.
- uc_open
- uc_mem_map
- uc_mem_write
- uc_emu_start
- uc_mem_read (non présent dans l’exemple)
Prenons ces fonctions une à une :
uc_open
Section titled “uc_open”
Nous voyons que uc_open prend un paramètre d’architecture, de mode, et un pointeur sur (supposément) une structure uc_engine.
Dans le fichier unicorn.h trouvable sur github, nous pouvons voir les valeurs associées à ces flags.

Dans notre cas, l’appel était uc_open(2, 0, &var_20) ce qui correspond à uc_open(UC_ARCH_ARM64, UC_MODE_ARM, &var_20).
Le mode pourrait aussi être UC_MODE_LITTLE_ENDIAN, mais cela revient probablement au même si ARM utilise le little endian par défaut.
Takeaway: Le processeur simulé est de l’architecture ARM64 (AArch64).
uc_mem_map / uc_mem_write
Section titled “uc_mem_map / uc_mem_write”
uc_mem_map semble permettre de définir une plage de mémoire utilisable pour l’émulation du processeur, quant à uc_mem_write, elle semble permettre d’écrire un nombre de bytes à une adresse donnée. Nous avons donc deux fonctions permettant de manipuler les données écrites dans la mémoire virtuelle.
À partir de ces informations, nous pouvons donc modifier les noms/types des variables pour y voir plus clair :

Ce que fait ce code est donc d’initialiser l’émulation du CPU virtuel avec deux zones mémoire. La première se situant à l’adresse 0x1000 jusqu’à 0x1400 (appelons la stack 1), et la deuxième s’étendant de 0x4000 à 0x104000 (appelons la stack 2). Cette dernière a reçu l’écriture d’une suite d’instructions sous forme d’opcodes que nous pouvons trouver dans la mémoire statique du programme.

Enfin, il y a aussi une mystérieuse constante qui est initialisée avec la valeur 0x4c53, que j’ai donc nommée someConstant_4c53.
Takeaway :
- Deux zones de mémoire virtuelles sont en place :
0x1000-0x1400-> zone pour les données0x4000-0x104000-> zone pour le code - Une constante inutilisée pour l’instant existe, sa valeur est
0x4c53
Boucle logique principale
Section titled “Boucle logique principale”
Rendu à ce moment du désassemblage, il nous reste quelques variables à déduire :
- sub_4011ef -> fonction affichant le flag, c’est donc le code que nous visons
- var_c_1 -> semble être un compteur s’incrémentant à chaque passage dans la boucle, si nous parvenons à le rendre égal ou supérieur à 2, nous aurons le flag
- rax_35 -> clairement un stockage d’erreur
- var_30 -> sûrement un booléen, doit être false pour ne pas sortir de la boucle et incrémenter la variable var_c_1 qui nous permettra d’avoir le flag.
- data_40210f -> contient deux null-bytes
- var_11_1 -> parait parfaitement inutile
Avant de passer à l’interprétation, réécrivons le nom des variables pour clarifier :

Que fait donc cette partie du programme ?
Le but pour avoir le flag est de réussir à boucler au moins deux fois dans le while, sans break, afin que counter >= 2 soit true et afficher le flag.
Pour ce faire nous devons donc comme dit éviter le break dans le if (isWrongFlag), et cette variable est définie par la lecture de ce qu’il se trouve à l’adresse 0x1005 dans notre stack virtuelle (stack 1).
À partir de là, nous devons comprendre ce qu’il se passe dans le CPU virtuel pour déterminer comment garder isWrongFlag à false.
Tout d’abord la boucle while actuelle ne manipule qu’uniquement la stack 1, cela fait sens, car la stack 2 est réservé aux instructions et ne doit donc pas être modifiée.
Le premier mem_write écrit dans un premier temps 4 caractères à l’adresse 0x1000, venant de notre argv[1] (donc notre flag passé en argument).
Ces 4 caractères sont sélectionnés avec un décalage (offset) de counter * 4. Donc offset 0 au premier tour, et offset 4 au second.
Autrement dit, si nous passons le flag 12345678 à notre programme, la chaîne 1234 sera écrite à l’adresse 0x1000 lorsque counter est à 0 (première exécution de la boucle while), puis 5678 sera écrit quand counter == 1.
Le deuxième mem_write écrit 1 byte à l’adresse 0x1004 (donc juste après les 4 caractères du dessus), venant de la constante de l’adresse de someConstant (+ counter) qui est égale à 0x4c53. Sachant que ce système utilise le little endian et que nous n’écrivons qu’un byte, le 0x53 uniquement sera lu en premier.
Puis une fois que counter sera à 1, l’adresse de someConstant + 1 pointera vers le 0x4c
Enfin, le troisième mem_write écrit un null-byte (0x00), à l’adresse 0x1005, afin de remettre à zéro le booléen de validité du flag, dans la stack virtuelle.
Finalement, nous avons la fonction uc_emu_start qui est appelé et qui lance l’émulation dans ces conditions grâce aux données insérées dans la stack jusqu’à présent.
Nous connaissons désormais tous les tenants et aboutissants, la seule chose restante à comprendre afin de former un flag est ce qu’il se passe dans les instructions du processeur ARM. Ce dernier contient la clé du crackage de ce challenge.
Reverse du processeur virtuel
Section titled “Reverse du processeur virtuel”Voici la liste d’opcode écrit dans la stack 2 (d’instructions) du processeur virtuel : 800082d200004039081400d10a0082d2940080524b0140396b01084a7f01007180000054250080d2a40082d2850000f9080900914a050091940600519f020071a1feff5400
N’ayant jamais fait d’ARM auparavant, j’ai dû jongler entre le désassembleur en ligne et de la documentation (assistée par LLM pour accélérer la compréhension des subtilités de l’architecture) pour traduire les opcodes en logique compréhensible.
Grâce au désassembleur de Shell-Storm, nous pouvons spécifier notre architecture, et récupérer de l’assembleur ARM, lisible permettant de travailler :
Afin de comprendre ce que cela signifie, essayons de traduire cela en Pseudo-C littéralement :
Nous avons une base, mais cela reste peu clair, donc après avoir analysé le sens de mon propre code, voici l’algorithme qui est exécuté sur le CPU ARM :

Grâce à cela, nous savons enfin que la variable mystère someConstant est utilisée comme “clé de chiffrement”, et nous permet de trouver le flag.
En effet, comme nous l’avons vu auparavant, cet algorithme sera appelé deux fois pour les deux moitiés de notre flag à 8 caractères. Et grace à ce code, nous voyons que la lettre du flag passée doit être égale à la clé passée, + un ajout de 2 à cette clé à chaque passage. Sans oublier que la clé passée à l’origine se voit soustraire 5.
Nous avons donc pour les 4 premières lettres (key passed: 0x53) :
0x53 - 0x5 = 0x4E
-> (+0x2) = 0x50
-> (+0x2) = 0x52
-> (+0x2) = 0x54
Puis pour les 4 lettres suivantes (key passed: 0x4c) :
0x4c - 0x5 = 0x47
-> (+0x2) = 0x49
-> (+0x2) = 0x4B
-> (+0x2) = 0x4D
Notre flag théorique final est donc \x4e\x50\x52\x54\x47\x49\x4b\x4d
Qui équivaut à NPRTGIKM

Et voilà GG !
Améliorations méthodologiques possibles
Section titled “Améliorations méthodologiques possibles”C’est maintenant en écrivant ce write-up que je suis tombé sur l’Unicorn Engine API Documentation, ce dernier contient une bonne quantité d’informations dont j’aurais eu besoin pour comprendre le fonctionnement des fonctions de cette librairie sans avoir à les déduire depuis le tutoriel.
Cela m’aurait permis de gagner du temps, mais d’un autre côté avoir été contraint de deviner le fonctionnement de l’API a été un excellent exercice ayant renforcé ma compréhension de la logique bas niveau.
Un autre point d’amélioration possible aurait été de désassembler la chaîne d’opcode elle même,
afin de me donner accès directement à du pseudo C, permettant ainsi une compréhension rapide de cette partie.
Cependant n’ayant jamais fait d’ARM auparavant je pense qu’être passé par la manière manuelle a aussi été une opportunité d’en apprendre sur cette architecture,
ce qui me fera gagner du temps sur mes prochains challenges.
Pour la suite, il faudra que je sache utiliser du désassemblage automatisé pour la rapidité,
tout en ayant appris cette architecture pour être capable de descendre à l’assembleur pour comprendre les subtilités quand nécessaire.
Remerciements
Section titled “Remerciements”Merci à toute l’équipe du Hack’in pour l’événement, et au créateur de ce super challenge !
J’en garde un très bon souvenir, et ai hâte de la prochaine édition :)
(Et merci pour la banane Wannacry et les pins ahah)
