# Write-up Steganosaurus ###### tags: `DGSE` `2020` `Brigitte_Friang` `Steganosaurus` ## Présentation du challenge Nos agents ont trouvé dans le camion de livraison une clef USB. Nous vous transférons le filesystem de cette dernière et espérons que votre grande capacité de réflexion permettra de révéler les secrets les plus sombres d'Evil Country ! Le flag est de la forme DGSEESIEE{x} avec x une chaine de caractères. (Attention au DGEESIEE, une erreur de typo s'est glissée dans le flag) message (SHA256=3889febebd6b1d35c057c3ba3f6f722798f029d6d0321b484305922a3d55d4d8) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/message ## Montage de l'image On fait un `file` sur le fichier pour obtenir des informations sur le format de fichier : ``` > file message message: DOS/MBR boot sector, code offset 0x58+2, OEM-ID "mkfs.fat", Media descriptor 0xf8, sectors/track 32, heads 64, hidden sectors 7256064, sectors 266240 (volumes > 32 MB), FAT (32 bit), sectors/FAT 2048, reserved 0x1, serial number 0xccd8d7cd, unlabeled ``` On voit qu'il s'agit d'une image FAT alors on essaye de monter le filesystem : ``` > sudo mkdir /mnt/steg && sudo mount -t auto message /mnt/steg && cd /mnt/steg ``` Liste des fichiers : ``` > tree -a . ├── readme ├── steganausorus.apk └── .Trash-1000 ├── files │ └── flag.png └── info └── flag.png.trashinfo ``` Contenu du readme : ``` > cat readme Bonjour evilcollegue ! Je te laisse ici une note d'avancement sur mes travaux ! J'ai réussi à implémenter complétement l'algorithme que j'avais présenté au QG au sein d'une application. Je te joins également discrétement mes premiers résultats avec de vraies données sensibles ! Ils sont bons pour la corbeille mais ça n'est que le début ! Je t'avertis, l'application souffre d'un serieux defaut de performance ! je m'en occuperai plus tard. contente-toi de valider les résultats. Merci d'avance For the worst, QASKAB ``` ## L'image `flag.png` On ouvre l'image `flag.png` et on voit que dans le coin gauche en haut que les pixels on été modifiés. Un passage de l'image dans [Aperi'Solve](https://aperisolve.fr/) n'a rien donné. C'est surement un algorithme maison de stéganoraphie. ![](https://i.imgur.com/ACwSxVX.png) ## L'application mobile On part sur l'APK. On l'ouvre avec Android-Studio et l'exécute dans une AVD (Adroid Virtual Device). Ici on voit que l'application est faite en [Dart](https://dart.dev/) avec le framework [FLutter](https://flutter.dev/). Sur l'émulateur, une fois l'application lancée, on tape un texte à cacher, on selectionne une image précédemment chargée, on clique sur `Start Hide & seek game` et ... rien. L'application bug. ## Reverse engineering On reverse engineering l'APK à l'aide de [Apktool](https://ibotpeaches.github.io/Apktool/). Après de longues recherches, on tombe sur ce post [stack overflow](https://stackoverflow.com/questions/53666487/flutter-debug-apk-decompile-to-get-source-code). Victoire, on retrouve bien le code source dans le fichier `kernel_blob.bin` !! Là, Dart et Flutter c'est la découverte. Mais on voit que l'on peut exécuter du code en [server apps](https://dart.dev/tutorials/server/get-started). Cela nous permet de nous passer de l'AVD et debugger le code source plus rapidement. Le code se trouve [ici](https://hackmd.io/@Flave/r15b9E6Fw) > Allo chérie, ça va debugger ! Après modification (pour ne plus utiliser les librairies de flutter qui ne fonctionnent pas pour les server apps) et réparation du code source (boucle while infinie, brakets manquants, suppression des variables inutiles, renommage des variables, ...), on fait fonctionner le code ! Extrait du code qui boucle: ```dart int messaggelength=0; String messagetohide=binaryStringmessage; String substringtoFind; substringtoFind=messagetohide.substring(0,1); String Stringbuilttest=""; var offsetarray = new List(); int offsettostore; int lengthtostore; int offset; String Megastringtosearch= MegaString.substring((MegaString.length/4).round()); //print("performing data calculation"); while(messaggelength < binaryStringmessage.length ) { offsettostore=Megastringtosearch.indexOf(substringtoFind); //print(Megastringtosearch.substring(offsettostore,offsettostore+substringtoFind.length)); while(offsettostore !=-1 && substringtoFind.length<=messagetohide.length-1){ lengthtostore = substringtoFind.length; offset = offsettostore; substringtoFind = messagetohide.substring(0, substringtoFind.length + 1); offsettostore = Megastringtosearch.indexOf(substringtoFind); } ``` `messaggelength` est instancié à 0 et `binaryStringmessage.length` est toujours supérieur à 0, donc on tombe dans une boucle infinie. > En lisant le code, on comprend pourquoi Steganosaurus. ## Algorithme Après des heures à comprendre le code, on en arrive aux explications suivantes: * L'utilisateur rentre son message à cacher et selectionne une image. * Le message est converti en string binaire. * L'image est redimensionnée avec une largeur de 1000 pixels. * Chaque pixel est converti en binaire pour créer une string RGB binaire. * Bit par bit, on recherche les plus longues sequences binaires possibles du message qui existent dans les 3 derniers quarts de l'image. On fait cette recherche tant que tout le message n'est pas retrouvé en entier. Pour chaque séquence retrouvée on enregistre son offset et sa taille dans `offsetarray` du code source. ![](https://i.imgur.com/zziNX4b.png) :::info :bulb: Il peut y avoir au maximum autant d'offset-taille que de bits qui constitue le message à cacher. ::: * On écrit dans `stringtowrite` des paquets de 24 bits et 8 bits suivant cette partie du code source: ```dart int offsetdatasize = resisedimage.length * 8 * 3; // 514000 * 8 * 3 = 12336000 // 12336000 = 101111000011101110000000 codé sur 24 bits donc datasizebit = 24 int datasizebit = offsetdatasize.toRadixString(2).length; // Même méthode ici, mais avec la longueur du message à cacher. int lenghtdatasize = binaryStringmessage.length; int lenghtsizebit = lenghtdatasize.toRadixString(2).length; String stringtowrite = ""; // On écrit deux paquets de 24 bits, un représente la taille des couples offset-taille // Un autre représente le nombre de bits sur lequel sont codés les tailles des couples offset-taille stringtowrite += offsetarray.length.toRadixString(2).padLeft(datasizebit,'0') + lenghtsizebit.toRadixString(2).padLeft(datasizebit,'0'); offsetarray.forEach((listofdata){ listofdata.forEach((data){ // On écrit les couples offset taille. stringtowrite+=listofdata[0].toRadixString(2).padLeft(datasizebit,'0') + listofdata[1].toRadixString(2).padLeft(lenghtsizebit,'0'); }); }); ``` Ce qui donne en relisant les premiers bits de l'image `flag.png`: ```python # 24 bits pour dire combien il y a d'offset et de taille à retrouver # Ce paquet explique le premier pixel noir (3*8 bits à zéro, sauf un). '000000000000000000001000' # 24 bits pour dire sur combien de bits sont codés les tailles, soit 8 bits. # Ce paquet explique le deuxième pixel noir. + '000000000000000000001000' # Premier offest (24 bits) + première taille (8 bits) + '000000111001010111011111' + '00010011' # Deuxième offest (24 bits) + deuxième taille (8 bits) + '001111111001101101100010' + '00011000' + ... ``` * On ajoute à `stringtowrite` le reste de l'image binaire. * On recontruit les pixels en lisant `stringtowrite` 8 bits par 8 bits. * On recontruit l'image. * On sauvegarde l'image. ## Recherche du flag Pour retrouver le flag dans l'image, on fait un script python qui reverse le code Dart de l'application. ```pyhton=0 # -*- coding: utf-8 -*- from PIL import Image # On ouvre l'image flag.png im = Image.open('flag.png') # On calcule offset_data_size # Ici offset_data_size_bit = 24. width, height = im.size offset_data_size = width * height * 8 * 3 offset_data_size_bit = int(len(bin(offset_data_size)) - 2) # On reconstruit MegaString (tous les pixels en binaire) mega_string = '' for pixel in im.getdata(): mega_string += "{0:b}".format(pixel[0]).zfill(8) mega_string += "{0:b}".format(pixel[1]).zfill(8) mega_string += "{0:b}".format(pixel[2]).zfill(8) i = 0 # Les premiers 24 bits nous indiquent combien de sous flag sont a retrouver. # Ici offset_array_length = 8. offset_array_length = int( mega_string[i*offset_data_size_bit:(i+1)*offset_data_size_bit], 2 ) i = 1 # Les 24 bits suivants nous indiquent sur combien de bits sont codés # les tailles des offsets. # Ici message_to_hide_lenght_size_bit = 8. message_to_hide_lenght_size_bit = int( mega_string[i*offset_data_size_bit:(i+1)*offset_data_size_bit], 2 ) # On reconstruit Megastringtosearch car les morceaux du flag sont # cachés dans les 3 derniers quarts de l'image. mega_string_to_search = mega_string[round(len(mega_string)/4):] i = 2 # Pour simplifier la recherche des offsets dans mega_string, # on enlève les 48 premiers bits de mega_string. mega_string = mega_string[i*offset_data_size_bit:] sub_flag_bin_list = [] packet_bits = offset_data_size_bit + message_to_hide_lenght_size_bit for i in range(offset_array_length): # On prend un paquet de 24 + 8 bits var = mega_string[i*packet_bits:(i+1)*packet_bits] # On retrouver les offsets et les tailles offset = int(var[:offset_data_size_bit], 2) lenght = int(var[offset_data_size_bit:], 2) sub_flag_bin_list.append( mega_string_to_search[offset:offset+lenght] ) # On reconstruit le flag codé en binaire flag_bin = ''.join(sub_flag_bin_list) # On convertie le flag en ascii flag_ascii = '' for i in range(0, len(flag_bin), 8): flag_ascii += chr(int(flag_bin[i:i+8], 2)) # Et voilà print(flag_ascii) ``` Les couples offset-taille sont: ``` 234975 - 19 => 01000100 01000111 010 => DG... 4168546 - 24 => 10011 01000101 01000101 010 => ...EE... 1009570 - 26 => 10011 01001001 01000101 01000 => ...IE... 414 - 18 => 101 01111011 0100011 => ...{... 1039951 - 22 => 0 01001100 00110100 01000 => ...L4... 1978908 - 24 => 111 01001001 01010011 01001 => ...IS... 4195338 - 22 => 000 00110011 01010010 001 => ...3R... 18140 - 13 => 10011 01111101 => ...} ``` Le flag est `DGSEESIEE{FL4GISH3R3}` BOOOM !! :rocket: