Dans cet article nous allons faire le write-up d’un challenge de reverse engineering plutôt facile et sympa que j’ai pu résoudre lors de ma qualification au CTF de l’ECW (European Cyber Week). C’était le premier challenge que je résolvais pour l’ECW et c’est celui-ci qui m’a donné envie de me lancer pleinement dans le CTF !
Énoncé du challenge
Find the key to the safe.
Prove to him that this was not a good idea.
Fichier du challenge
Recherches sur le format
Une fois la pièce jointe au challenge téléchargée, on se retrouve avec un fichier “chest.hex” qui semble alors être un fichier texte un peu spécial.
:100000000C9434000C9449000C9449000C94490061
:100010000C9449000C9449000C9449000C9449003C
:100020000C9449000C9449000C9449000C9449002C
:100030000C9449000C9449000C9449000C9449001C
:100040000C9449000C9449000C9449000C9449000C
:100050000C9449000C9449000C9449000C944900FC
:100060000C9449000C94490011241FBECFEFD8E036
:10007000DEBFCDBF11E0A0E0B1E0E8EAF1E002C0F0
:1000800005900D92A632B107D9F70E94CA000C94D0
:10009000D2000C9400001092C5008093C40088E147
:1000A0008093C10086E08093C20008959091C000C3
:1000B00095FFFCCF8093C60008950F931F93CF93B5
:1000C000DF93EC018C01060F111DC017D10721F041
:1000D00089910E945600F9CFDF91CF911F910F9126
:1000E00008958091C00087FFFCCF8091C6000895DD
:1000F0000F931F93CF93DF93EC018C01060F111D1B
:10010000C017D10721F00E9471008993F9CFDF91C8
:10011000CF911F910F910895CF93DF93CDB7DEB7A5
:100120002B970FB6F894DEBF0FBECDBF8BE0EBE18F
:10013000F1E0DE01119601900D928A95E1F76BE0F6
:10014000CE0101960E945D000E9471002B960FB6B1
:10015000F894DEBF0FBECDBFDF91CF910895FF921F
:100160000F931F93CF93DF93F82EC0E0D1E00CE103
:1001700011E0888184508F250E94560022960C172A
:100180001D07B9F78AE0DF91CF911F910F91FF9082
:100190000C94560087E60E944B000E948C000E943F
:0801A000AF00FBCFF894FFCF84
:1001A8004F5551505D62795D73546F577042435596
:1001B800717A3E3842454378773300456E746572EC
:0601C800206B65790A00BE
:00000001FF
Je décide de coller la première ligne du fichier sur Google pour débuter mes recherches sur ce fichier “.hex”. Je tombe alors sur plusieurs sources cohérentes qui parlent d’un format HEX (Intel) et de l’architecture AVR d’Atmel, comme sur ce Github : https://github.com/Hossam-Elbahrawy/16×2-LCD.
Compilation du firmware
On se rend alors compte que le programme n’est pas assemblé. Nous allons alors chercher comment assembler celui-ci pour se retrouver avec un binaire (firmware en l’occurence) que l’on pourra alors ouvrir avec IDA ou Ghidra par exemple.
Après quelques recherches, l’outil avr-objcopy permettrait de convertir notre fichier .hex en firmware au format. Celui-ci se trouve dans le package binutils-avr que l’on installe facilement.
$ sudo apt-get install binutils-avr
Lecture des listes de paquets... Fait
Construction de l'arbre des dépendances
Lecture des informations d'état... Fait
Les paquets suivants ont été installés automatiquement et ne sont plus nécessaires :
linux-hwe-5.4-headers-5.4.0-42 linux-hwe-5.4-headers-5.4.0-52 linux-hwe-5.4-headers-5.4.0-58 linux-hwe-5.4-headers-5.4.0-59 linux-hwe-5.4-headers-5.4.0-60
linux-hwe-5.4-headers-5.4.0-62 linux-hwe-5.4-headers-5.4.0-65 linux-hwe-5.4-headers-5.4.0-66 linux-hwe-5.4-headers-5.4.0-67 linux-hwe-5.4-headers-5.4.0-70
linux-hwe-5.4-headers-5.4.0-71 linux-hwe-5.4-headers-5.4.0-72 linux-hwe-5.4-headers-5.4.0-73 linux-hwe-5.4-headers-5.4.0-74 linux-hwe-5.4-headers-5.4.0-81
linux-hwe-5.4-headers-5.4.0-84
Veuillez utiliser « sudo apt autoremove » pour les supprimer.
Les NOUVEAUX paquets suivants seront installés :
binutils-avr
0 mis à jour, 1 nouvellement installés, 0 à enlever et 265 non mis à jour.
Il est nécessaire de prendre 1 475 ko dans les archives.
Après cette opération, 15,5 Mo d'espace disque supplémentaires seront utilisés.
Réception de :1 http://us.archive.ubuntu.com/ubuntu bionic/universe amd64 binutils-avr amd64 2.26.20160125+Atmel3.6.0-1 [1 475 kB]
1 475 ko réceptionnés en 3s (539 ko/s)
Sélection du paquet binutils-avr précédemment désélectionné.
(Lecture de la base de données... 630001 fichiers et répertoires déjà installés.)
Préparation du dépaquetage de .../binutils-avr_2.26.20160125+Atmel3.6.0-1_amd64.deb ...
Dépaquetage de binutils-avr (2.26.20160125+Atmel3.6.0-1) ...
Paramétrage de binutils-avr (2.26.20160125+Atmel3.6.0-1) ...
Traitement des actions différées (« triggers ») pour man-db (2.8.3-2ubuntu0.1) ...
On se retrouve alors avec toute une toolchain associée :
$ avr-
avr-addr2line avr-as avr-elfedit avr-ld avr-nm avr-objdump avr-readelf avr-strings
avr-ar avr-c++filt avr-gprof avr-ld.bfd avr-objcopy avr-ranlib avr-size avr-strip
Comme convenu on va utiliser avr-objcopy pour transformer notre “chest.hex” en un firmware du nom de “chest.bin”.
$ avr-objcopy -I ihex chest.hex -O binary chest.bin
$ file chest.bin
chest.bin: data
$ binwalk -A chest.bin # -A pour scanner des opcodes
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
fqjb3323@EB-L-OR6221619:~/ECW/chest$ binwalk -A chest.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
189 0xBD AVR8 instructions, function prologue
243 0xF3 AVR8 instructions, function prologue
355 0x163 AVR8 instructions, function prologue
On a bien assemblé/compilé notre binaire en AVR8 (la documentation est d’ailleurs trouvable ici) comme on peut le voir avec la sortie du binwalk, on va pouvoir passer à la phase de reverse engineering !
Reverse engineering
Avant tout, je commence par faire un strings sur le binaire pour voir si des chaines de caractères intéressantes s’y trouvent.
$ strings chest.bin
OUQP]by]sToWpBCUqz>8BECxw3
Enter key
Seulement 2 chaines de caractères ressortent : le message “Enter key” ainsi que ce qui ressemble à un flag chiffré… Allons voir ça de plus près avec un désassembleur !
Pour avoir testé ce challenge sur IDA et Ghidra, je dois pour une fois admettre préférer Ghidra (assez rare pour être souligné 🙃)…
Pour le choix du langage, je renseigne bien AVR8 en processeur et prends la configuration de base en version gcc (probablement compilé avec avr-gcc de la toolchain binutils-avr).
Je me mets ensuite à la recherche de la chaine de caractères identifiée plus tôt (celle semblable à un flag chiffré).
Je vais ensuite définir cette data comme une chaine de caractère pour que Ghidra la gère mieux (clic droit > data > TerminatedCString) et je renomme le label en “encrypted_flag”.
Vient ensuite la recherche de références à ce label : une seule apparaît.
Ni une ni deux, je regarde ce que fait la fonction qui utilise ce flag chiffré.
Ici cela saute rapidement au yeux : le programme va boucler pour copier notre “encrypted_flag” en “DAT_mem_0100” (que je renommerai par la suite “copy_enc_flag”).
Pour mieux comprendre, voici quelques explications :
– X, Y et Z sont des stack pointers, R[0-26] sont des registres généraux
– L’instruction lpm (load program memory) en code:0040 va charger l’octet contenu à Z=>encrypted_flag dans R0 et incrémenter Z
– L’instruction st (store) en code:0041 va stocker R0 dans X=>copy_enc_flag et incrémenter X
– La boucle sera répétée 0x26 fois (strlen(encrypted_flag + “Enter key\n”) + 1 pour le null byte)
Il va donc ensuite être intéressant de trouver où cette copie de notre chaine de caractère va être utilisée !
On voit alors une référence en lecture à cette copie du flag chiffré, la première référence étant le code que l’on vient d’examiner (d’où le contexte d’écriture). Allons regarder ça.
Cette fonction est encore une fois très simple. Elle va, pour chaque caractère de notre flag chiffré, soustraire 4 puis le XOR avec R15.
En effet à chaque tour le programme va :
– Mettre le caractère pointé par Y=>copy_enc_flag dans Wlo
– Soustraire 4 à Wlo
– XOR Wlo par R15
– Appeler une fonction dont on ne se préoccupera pas
– Ajouter 2 au pointeur Y (on va alors sauter un caractère à chaque tour de boucle)
Cela sera répété sur 0x1a soit la longueur du flag chiffré.
Flag
Il ne nous manque plus qu’une information : la valeur avec laquelle chaque caractère sera XORé. En effet, nous avons vu que chaque caractère va être XORé avec R15. En début de fonction, R15 est initialisé à Wlo : il nous faut remonter à l’appel de la fonction pour déterminer la valeur a Wlo lors de l’appel de celle-ci… Cette manière étant un peu fastidieuse j’ai plutôt opté pour un brute-force de la clé étant donné que celle-ci ne fait qu’un octet 🙂.
J’ai alors sortit mon ami Cyberchef : je commence par soustraire 4 à chaque caractère, fais un BF de toutes les clés possibles sur un octet et je recherche un flag qui commence par “E” (le format de flag étant “ECW{“). Bingo !
Je me retrouve alors avec le flag suivant :
E_CBWP{Wa^e]b01_cx4:0O1z}!
A ce moment je me souviens que Y est incrémenté de 2 dans la fonction et qu’il faut donc que je prenne un caractère sur 2 :
ECW{aeb1c401}
C’est ainsi ça que se terminait alors mon premier challenge de reverse engineering à l’ECW 🙂 !