Challenge DGSE/ESIEE - Steganosaurus

Avertissement : vous allez pénétrer dans les arcanes de la DGSE, il vous saurait gré de lire ce WriteUp avec la compilation du service en fond sonore, cliquez ici ou la

© Nobu Tamura

Sacrés agents du Service Action ! Ils nous ont dégoté une clé USB abandonnée dans un camion de livraison. Elle y contient apparemment les plus sombres secrets de notre ennemie juré : Evil Country. On nous transmet pour analyse un fichier nommé message, représentant le filesystem de cette clé.

Le challenge est classé dans la catégorie Forensic, commençons par passer un petit coup de commande file, et un tour de moulinette avec binwalk :

$ 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
$ binwalk -e message
DECIMAL       HEXADECIMAL     DESCRIPTION                                                                                                                       
--------------------------------------------------------------------------------                                                                                
2114048       0x204200        PNG image, 1000 x 514, 8-bit/color RGBA, non-interlaced                                                                           
3061760       0x2EB800        Zip archive data, at least v2.0 to extract, compressed size: 318, uncompressed size: 442, name: kotlin/ranges/UIntProgressionItera
tor.kotlin_metadata                                                                                                                                             
3085941       0x2F1675        Zip archive data, at least v2.0 to extract, compressed size: 459, uncompressed size: 725, name: kotlin/collections/HashMap.kotlin_
metadata                                                                                                                                                        
3086472       0x2F1888        Zip archive data, at least v2.0 to extract, compressed size: 255, uncompressed size: 320, name: kotlin/SuspendKt.kotlin_metadata  
3086789       0x2F19C5
--- 3< snip ---

Effectivement, file reconnait un système de fichier FAT32, binwalk quant a lui va passer au peigne fin chaque octet du fichier pour essayer de trouver des en-têtes de format de fichier connus. Ainsi il trouve un fichier PNG d’une résolution de 1000x514, des archives Zip etc… L’option -e permet d’extraire automatiquement ce qu’il trouve.

Gardons les résultats de binwalk pour plus tard, et essayons de monter le fichier pour accéder à cette partition FAT32 :

$ sudo mount message mnt/
$ ls -l mnt/
total 37327
-rwxr-xr-x 1 root root      532 Oct 15 17:48 readme
-rwxr-xr-x 1 root root 38221331 Jul  8 16:02 steganausorus.apk

Deux fichiers sont présents, dont un possédant l’extension en .apk, qui nous fait penser naturellement au format Android Application Package. l’APK est un paquet au format Zip servant de conteneur pour une application Android. Regardons le readme, et parce que ça ne mange pas de pain un petit coup de file sur l’APK pour s’assurer du format :

$ 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

$ file steganosaurus.apk
steganausorus.apk: Zip archive data, at least v2.0 to extract

D’après le readme il fleure bon que l’on va devoir analyser un algorithme contenu dans une application. De plus la commande file reconnait une archive Zip. On est quasiment certain d’être en présence d’une application Android, regardons sans plus attendre son contenu :

$ unzip -l steganosaurus.apk
Archive:  steganausorus.apk                                                                                                                                     
  Length      Date    Time    Name                                                                                                                              
---------  ---------- -----   ----                                                                                                                              
     4224  1980-00-00 00:00   AndroidManifest.xml                                                                                                               
     1159  1980-00-00 00:00   META-INF/CERT.RSA                                                                                                                 
    34878  1980-00-00 00:00   META-INF/CERT.SF                                                                                                                  
    34835  1980-00-00 00:00   META-INF/MANIFEST.MF                                                                                                              
        6  1980-00-00 00:00   META-INF/androidx.activity_activity.version                                                                                       
        6  1980-00-00 00:00   META-INF/androidx.arch.core_core-runtime.version                                                                                  
        6  1980-00-00 00:00   META-INF/androidx.core_core.version                                                                                               
        6  1980-00-00 00:00   META-INF/androidx.customview_customview.version                                                                                   
        6  1980-00-00 00:00   META-INF/androidx.fragment_fragment.version                                                                                       
        6  1980-00-00 00:00   META-INF/androidx.lifecycle_lifecycle-livedata-core.version                                                                       
        6  1980-00-00 00:00   META-INF/androidx.lifecycle_lifecycle-livedata.version                                                                            
        6  1980-00-00 00:00   META-INF/androidx.lifecycle_lifecycle-runtime.version                                                                             
        6  1980-00-00 00:00   META-INF/androidx.lifecycle_lifecycle-viewmodel.version  
--- 3< snip ---

AndroidManifest.xml… c’est manifestement une application Android ! Pour naviguer et décompiler ce paquet APK, nous utiliserons l’outil Jadx Dex to Java decompiler :

$ jadx-gui steganosaurus.apk

Une fois dans Jadx avec le fichier steganosaurus.apk chargé, nous cliquons sur le fichier Resources/AndroidManifest.xml qui est l’un des plus importants d’une application Android. En effet ce fichier est obligatoire et contient les informations générales au bon fonctionnement de cette dernière. C’est un excellent point de départ pour faire connaissance avec steganausorus.

L’application requière l’accès à 3 ressources :

  • écriture sur un stockage externe (carte sd…)
  • lecture sur un stockage externe (carte sd…)
  • Accès Internet
Permissions demandées par l'application

La balise <application> et ses attributs nous renseignent sur l’application elle même, comme son nom qui est stegapp dans android:label et surtout son point d’entrée dans le code par android:name=io.flutter.app.FlutterApplication, précisant quelle est la première classe à appeler.

Balise <application>

Cette classe n’est d’ailleurs pas écrite par l’auteur de l’application, mais elle correspond au kit de développement Flutter, l’analyser serait une perte de temps.

Flutter a été créé par Google et met à disposition tout un tas de classes pour développer des interfaces graphiques mutliplateforme sur la base d’un seul langage le Dart. Le Dart quant à lui est un langage de programmation orienté objet se rapprochant syntaxiquement du C, il génère principalement du code natif.

Le code écrit par l’un des membres d’Evil Country se situe surement dans l’application Flutter/Dart elle même. Il va nous falloir trouver un moyen le récupérer et de la décompiler, car malheureusement Jadx comprend que le format .dex Dalvik Executable Format.

Heureusement, nous avons une des plus impressionnante compétence d’un cétéfeur (un gars qui fait des CTF), celle de pouvoir diagonaliser l’intégralité d’une doc d’un gros SDK en un temps record et le don sacré dans la composition de bons mots-clés pour sniper la ressource nécessaire, c’est à dire celle-ci : Reverse Engineering Flutter Apps :

  • “Pardon Monsieur? Si l’application est restée en mode debug on peut récupérer le code source avec en bonus les commentaires dans le fichier kernel_blob.bin de l’apk ?”
  • “Oui Madame!”
  • “Pôpôpôôôô!”

Target localisée ! Pour extraire le fichier il suffit d’ouvrir le paquet APK dans 7Zip par exemple, ou tout simplement de le dézipper. Le contenu est censé être du bytecode + peut-être le code en clair, voyons ce que nous trouve la commande strings :

$ strings -10 kernel_blob.bin
--- 3< snip ---
<org-dartlang-sdk:///third_party/dart/sdk/lib/_http/http.dart
'       // Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// @dart = 2.6
library dart._http;
import 'dart:async';
import 'dart:collection'
        HashMap,
        HashSet,
        Queue,
        ListQueue,
        LinkedList,
        LinkedListEntry,
        UnmodifiableMapView;
import 'dart:convert';
--- 3< snip ---

Well Done Evil Country! Qu’est ce c’est beau et tellement rare d’avoir un code lisible et indenté depuis la sortie de la commande strings, un moment rare, restons quelques minutes à contempler…

Comme nous sommes des pros de la DGSE, on va extraire proprement le code lisible du byte code, pour avoir une belle base de travail. En regardant un peu le code on s’aperçoit qu’il contient un paquet de lignes, et qu’elles correspondent au code du SDK Flutter. Il nous faut trouver et isoler le code du fameux QASKAB. Il doit bien y avoir un point d’entrée matérialisé par une fonction ou un truc du genre… Concentration… Diagonalisation… Following the white rabbit… Et Bim ici. Oui un pauvre main(), manque total d’originalité de la part du Dart.

On tapote frénétiquement sur n dans Vim avec le pattern mal choisi main(). Il nous faut tomber sur le bon. Pour cela on remarque en amont de chaque main() une en-tête de début de fichier Dart, avec le chemin vers celui-ci émanant de la machine du développeur. Il devient facile de faire le distinguo entre un main() perdu de la bibliothèque Flutter et celui-là par exemple :

Extrayons ce fichier. Pour cela nous utilisons ici une recette ultra sophistiquée :

  • vim kernel_blog.bin
  • 530089GV530380Gy (on respecte la casse hein)
  • :e le_code.dart
  • p:wq

C’est une recette de chef, comprendra qui pourra. Sinon on peut le faire à la souris, mais ça va mal passer à la Piscine car c’est les ptis gars du GCHQ qui font ça.

On a beau être en couple avec Vim depuis longtemps, mais quand il s’agit d’analyser et surtout de naviguer efficacement dans du code inconnu, rien ne vaut la souris (“tu viens pas de dire que c’est au GCHQ qu’on fait ça?” “non mais ça c’était dans l’autre paragraphe”), pour cela voici plusieurs armes suivant les caractéristiques du soldat :

  • VsCode si t’as pas de barbe
  • VsCodium barbe naissante
  • Emacs barbe de classe Stallman

ces 3 éditeurs de code / IDE ont pour point commun d’avoir une extension Vim, (voir même Evil… Country?!) ouf l’honneur du Service est sauf.

N’oubliez pas d’installer les extensions qui vont bien : Flutter et Dart. Ouvrez votre code, et la c’est noël à Disney Land, de la couleur partout :

L’intérêt de VsCode ici, c’est qu’il va nous permettre d’avoir rapidement la doc juste en passant la souris sur les classes de bibliothèque, et de faire ressortir les variables dans tout le code quand on place le curseur sur l’une d’elle (matérialisé par un encadré de couleur rose…)

Bien que le langage Dart nous est inconnu au Service, il est assez facile à comprendre. De toutes les manières on a pas le temps d’apprendre a coder en Dart, en plus si vous avez vu la série Le Bureau Des Légendes, vous êtes au courant que l’on est à peine deux au service informatique, il y a lui et moi.

Visiblement cette application bidouille une image en entrée pour en ressortir une autre. Avant d’aller plus loin, allons voir a quoi ressemble cette APK en l’exécutant sur notre téléphone de service… Ah bah non… on ne peut toujours pas installer d’application (ni même Candy Crush…) cette mise à jour n’arrivera donc jamais ! Rabattons nous sur un émulateur.

Une fois notre téléphone allumé, il nous reste plus qu’a pousser l’application sur ce dernier :

$ adb devices
List of devices attached
emulator-5554   device
$ adb push steganausorus.apk /sdcard/Download/
steganausorus.apk: 1 file...MB/s (38221331 bytes in 0.350s)

On paramètre le téléphone pour installer des applications non vérifiées, puis on tapote dé-li-ca-te-ment sur Stegapp :

Un message secret à cacher dans une image… un nom évocateur… nous sommes très probablement sur une application de stéganographie (quel flair!). On test avec une belle image de Labrador pour voir le résultat… Punaise ça rame ! En effet le machin souffre d’un serieux problème de performance (on a été prévenu), ça va être râpé pour l’analyse dynamique…

Dans VsCode par exemple il possible (avec les extensions) de créer une nouvelle application Flutter, de coller le code que l’on a extrait, puis de faire une analyse dynamique en mode live debug.

Maintenant que l’on y voit un peu plus clair, il n’y a plus qu’a lire la documentation qui nous est fournie (le code quoi). Un tapotage sur le bouton Start Hide & seek game fait quelques vérifications et appelle la fonction steggapp avec pour paramètres le chemin vers l’image _image, et le texte à dissimuler myController.text:

dans la fonction Build()

Chaque caractère du message à dissimuler va être transformé en binaire sous forme d’une chaîne de caractère ASCII de type String à l’aide de la fonction MessageToBinaryString() (si c’est pas explicite ça). Un caractère occupera systématiquement 8 bits, si il fait moins on le capitonne par la gauche(padLeft) avec des 0. Par exemple, BOB donnera 01000010 01001111 01000010 (sans espace) :

String MessageToBinaryString(String pMessage){
    String Result;
    Result="";
    List<int> bytes = utf8.encode(pMessage);
    bytes.forEach((item) { Result+=item.toRadixString(2).padLeft(8,'0');});
    return Result;
  }

Le résultat de la fonction est stocké dans binaryStringmessage :

binaryStringmessage = MessageToBinaryString(pMessage);

L’image est ensuite lue et convertie au format RGBA puis redimensionnée de sorte que l’image ai une largeur de 1000 pixels. Cette première constante est très importante, car l’image finale embarquant le message fera systématiquement 1000 pixels de large. C’est un indicateur fort. D’autre part, ces 1000px ne nous évoquent pas quelque chose? Il disait quoi déjà le binwalk du début ?

A.Image aimage =A.Image.fromBytes(decodedImage.width,decodedImage.height, imgintlist, format: A.Format.rgba);
A.Image resisedimage=A.copyResize(aimage,width:1000);

Un petit rappel sur le format RGBA32 (ou en français RVBA pour Rouge Vert Bleu Alpha) ne va pas faire de mal pour comprendre ce qui va suivre. Un pixel de notre image est un savant mélange d’intensité de rouge, de vert et de bleu (rappelez vous les trucs moches que vous faisiez en Art Plastique). On parle de canal pour chaque couleur, du coup nous avons 3 canaux Rouge Vert Bleu, plus 1 autre appelé Alpha qui se rapporte à l’intensité de transparence du pixel. Comme nous sommes dans un format RGBA32 pour 32bits(4 octets ), il faut qu’un unique pixel tienne dans…. 32 bits ! On a 4 canaux à faire entrer là-dedans, et pour éviter les disputes on fait un partage équitable, 32bits/4 canaux = 8 bits. Bon voilà, chacun pourra prendre une valeur codée sur 8 bits, c’est à dire de 0 à 255 en notation décimale.

Il faut aussi respecter l’ordre de lecture du format RGBA32 :

Revenons à notre image redimensionnée stockée dans resisedimage, l’application va parcourir cette image - donc un tableau de 32bits (= 1 pixel) - puis transformer cette valeur en binaire sous forme d’une chaîne de caractère. Un petit problème se pose pendant la conversion en binaire avec toRadixString, en effet l’ordre des canaux vont être inversés, on se retrouve avec du ABGR. On ne rentrera pas dans les détails, c’est une sombre histoire de Boutisme (pas la religion hein). Pour contrer ce problème on remet dans l’ordre les canaux puis on concatène tous les pixels dans une MEGA chaîne de caractère MegaString. Remarque importante, avec le premier substring(8) on ignore complètement le canal Alpha.

MegaString="";
for (int i = 0;i < resisedimage.length;i++){
  RRGGBBString=resisedimage[i].toRadixString(2).padLeft(32, '0').substring(8); // ByeBye l'Alpha
  PixelString=RRGGBBString.substring(16,24)+ // Rouge
              RRGGBBString.substring(8,16)+  // Vert
              RRGGBBString.substring(0,8);   // Bleu
  MegaString+=PixelString;
}

Les choses sérieuses commencent, une boucle while va mouliner tant que l’on a pas traité tous les chiffres binaires de la chaîne binaryStringmessage :

while(messaggelength < binaryStringmessage.length ) {

Juste avant cette boucle une nouvelle variable Megastringtosearch est initialisée avec pour valeur le contenu de MegaString tronqué de ses N premiers bits. Où N = taille de MegaString / 4:

String Megastringtosearch= MegaString.substring((MegaString.length/4).round());

Megastringtosearch va être notre dictionnaire pour coder notre message (binaryStringmessage). Dans le while précédent, une nouvelle boucle est créée. C’est une boucle de recherche. Elle va essayé de déterminer la plus grande suite de bits possibles d’un seul bloc correspondant à notre message secret. La recherche se fait dans Megastringtosearch. On note la position offset et la longueur lengthtostore de notre trouvaille :

// offsettostore != -1, tant que indexOf trouve une suite
// substringtoFind.length<=messagetohide.length-1, tant qu'on ne dépasse pas notre message
while(offsettostore !=-1 && substringtoFind.length<=messagetohide.length-1){
  lengthtostore = substringtoFind.length; // on stocke la taille depuis la pos dans Megastringtosearch
  offset = offsettostore; // on stocke la position dans Megastringtosearch
  // tant qu'on trouve on ajoute un nouveau bit à la recherche
  substringtoFind = messagetohide.substring(0, substringtoFind.length + 1);
  // notre chaîne de bits est trouvable dans Megastrintosearch, sinon indexOf retourne -A
  offsettostore = Megastringtosearch.indexOf(substringtoFind);
}

Toujours dans notre première boucle, un test est réalisé pour évaluer si l’on a trouvé l’intégralité de notre message dans les données de l’image. Si c’est le cas on stoppe la première boucle, sinon on continue à chercher un nouveau morceau restant de notre message sous forme de bits. Pour chaque bloc trouvé, on stocke la paire de valeurs [offset, lenghtostore] dans le tableau offsetarray:

// Tout trouvé ?
if(substringtoFind.length == messagetohide.length  ){
  int lastoffsettostore=Megastringtosearch.indexOf(substringtoFind);
  // ça dépasse un peu?
  if(lastoffsettostore==-1){
    // on stocke les résultats dans offsetarray + le bit qui dépasse
    offsetarray.add([offset, lengthtostore]); 
    offsetarray.add([Megastringtosearch.indexOf(substringtoFind[-1]),1]);
  }
  // ça dépasse pas:
  else{
    offsetarray.add([Megastringtosearch.indexOf(substringtoFind),substringtoFind.length]);
    var lastitem=offsetarray.last;      }
  //
  messaggelength+=substringtoFind.length;
}
// on a trouvé qu'un bloc
else {
  // on retire le bloc trouvé
  messagetohide = messagetohide.substring(substringtoFind.length - 1);
  messaggelength += substringtoFind.length;
  // on stocke le résultat
  offsetarray.add([offset, lengthtostore]);
  offsettostore = 0;
  lengthtostore = 1;
  offset = 0;
  // on recommence à trouver un nouveau bloc
  substringtoFind = messagetohide.substring(0, 1);
}

Bon on récapitule un peu, on a dans l’ordre :

  • notre message secret est transformé en binaire sous forme ASCII 101010100...
  • on fait de même avec les pixels de l’image 10101011...
  • on fait une table de recherche avec les données de cette image moins les 25% du début
  • on essaye de trouver une suite identique dans notre message et dans la table
  • on stocke la position de cette suite sous forme [position,longueur] dans offsetarray
  • on fait cette recherche pour l’intégralité de notre message

offsetarray devient le précieux résultat permettant de retrouver le message secret à l’aide de la position/longueur. Il faut donc pouvoir stocker ce tableau dans l’image. C’est justement l’étape qui suit.

Pour cela une nouvelle variable de type String fait son apparition : stringtowrite. Comme son nom l’indique (merci Evil Country) elle va contenir notre tableau d’offset qui sera écrit au tout début de l’image. Voilà à quoi va ressembler ce début d’image :

et son code :

int offsetdatasize = resisedimage.length * 8 * 3;
int lenghtdatasize = binaryStringmessage.length;
int lenghtsizebit = lenghtdatasize.toRadixString(2).length;
int datasizebit = offsetdatasize.toRadixString(2).length;
String stringtowrite = "";
// taille de offsetarray + lenghtdatasize
stringtowrite += offsetarray.length.toRadixString(2).padLeft(datasizebit, '0') +
                  lenghtsizebit.toRadixString(2).padLeft(datasizebit, '0');

// on ajoute toutes les paires [offset,longueur] 
offsetarray.forEach((listofdata) {
  stringtowrite += listofdata[0].toRadixString(2).padLeft(datasizebit, '0') +
                    listofdata[1].toRadixString(2).padLeft(lenghtsizebit, '0');
});

dataSizeBit correspond au nombre de bits contenus dans la valeur calculée resised.length * 8 * 3. Comme son nom l’indique, elle va nous renseigner sur la taille d’une donnée de l’en-tête, ou d’un bloc du schéma précédent si l’on préfère. A chaque fois que l’on voudra extraire un message secret d’Evil Country il nous faudra faire le calcul ci-dessus pour déterminer comment lire l’en-tête. Note importante, dans le code Dart length retourne le nombre de pixels (RGBA), la taille n’est donc pas en octet, mais un multiple de 4 octets. Pour avoir la taille en octet il suffit de diviser par 4 : Length/4.

  • Le premier bloc de notre en-tête contient le nombre de paires [offset,longueur] à lire.
  • Le deuxième bloc contient la variable lengthsizebite déterminée en comptant le nombre de bits contenus dans le message secret. Elle indique la dimension d’un bloc longueur qui eux n’ont pas la taille dataSizeBit.
  • le reste des blocs correspondent aux paires à lire suivant le nombre récupéré dans le premier point.

Voilà ! Avec ces informations on peut déjà coder notre propre décodeur ! Mais… encore faut-il avoir une image à décoder !? Une image qui d’après notre analyse fait 1000 pixels de largeur, et qui en plus devrait avoir des pixels étranges en son début…

Vous vous souvenez du petit coup de binwalk en début de page ? Il nous avait pas trouvé justement un PNG de 1000 x 514, 8-bits/color RGBA ? Alors ? Bon bah je vais vous le dire alors, oui on a un PNG planqué qui a du être normalement extrait si vous avez bien utilisé l’option -e de binwalk. Allons voir ça de plus près :

$ file _message.extracted/204200 
message.extracted/204200: PNG image data, 1000 x 514, 8-bit/color RGBA, non-interlaced
$ eog _message.extracted/204200

Oh la belle image! Les petites barres veulent surement dire quelque chose, comptons les…. NON. Tiens… Il y a des pixels étranges dans le coin supérieur gauche :

Cette image a l’air de contenir un grand secret injecté par l’algorithme que l’on a analysé plus haut. Réalisons le décodeur. Pour changer du Python on va le faire en GoLang… En fait pour ne rien vous cacher, on a pas vraiment le choix, les gars du Service Action sont tous des militaires endurcis, il suffit de dire “NoGo” pour les mettre en pétard, du coup on s’est mis au Go, on est plus serein.

Commençons par faire une petite fonction qui prend en entrée un fichier et nous retourne une image RGBA :

func readFileToImgRGBA(f *os.File) (*image.RGBA, error) {
	srcimg, err := png.Decode(f)
	if err != nil {
		return nil, fmt.Errorf("readImgInRGBA: %w", err)
	}
	// on récupère les paramètres de notre image
	bounds := srcimg.Bounds()
	// on crée une image vide avec le format RGBA et les paramètres de notre image
	img := image.NewRGBA(bounds)
	// on copie notre image, en bref on vient de la convertir en RGBA comme dans l'application
	draw.Draw(img, bounds, srcimg, image.Point{0, 0}, draw.Src)
	return img, nil
}

Pour ceux qui ne connaissent pas trop le Go, l’opérateur := permet de faire de l’inférence de type (il a dit quoi la ?). En gros on se casse pas la tête à choisir un type, on laisse le compilateur choisir, comme avec auto en C++.

On se fait plaisir et on fait une autre petite fonction qui prend en entrée un tableau d’octets. Elle va nous mouliner tout ça pour en sortir une chaîne de caractère binaire. Plutôt utile n’est-ce pas !

// convertToBitsString convertit chaque octet en chiffre
// binaire sous forme ASCII. Il capitonne de 0 par la gauche
// si le nombre de caractère est inférieur à 8 (bits)
func convertToBitsString(data []byte) *string {
	var build strings.Builder // strings.Builder car performant
	for _, b := range data {
		fmt.Fprintf(&build, "%08b", b) // on convertit + capitonnage
	}
	strbuild := build.String() // on transforme en  string
	return &strbuild
}

Vous vous rappelez que l’on n’utilise pas le canal Alpha de l’image pour construire le dictionnaire Megastringtosearch tronqué depuis MegaString. Il nous faut une fonction pour filtrer ça: imgToRGB prend une image en RGBA pour retourner un tableau d’octet en RGB sans A:

func imgToRGB(img *image.RGBA) (res []byte) {
  Y := img.Bounds().Max.Y
  X := img.Bounds().Max.X
  for y := 0; y < Y; y++ {
    for x := 0; x < X; x++ {
      r := img.RGBAAt(x, y).R
      g := img.RGBAAt(x, y).G
      b := img.RGBAAt(x, y).B
      res = append(res, r, g, b)
    }
  }
  return
}

Et pour finir voici la fonction principale, qui est non optimisée mais qui réutilise les même noms de variable et la même structure que dans le code Dart :

func decode(imgRGBA *image.RGBA) (flag string) {
	// on supprime le canal Alpha, RGBA => tableau octet format RGB
	imgbytes := imgToRGB(imgRGBA)
	// on calcul la taille de l'image RGBA en mémoire
	imgOrigSize := imgRGBA.Bounds().Max.X * imgRGBA.Bounds().Max.Y * 4

	// on calcule la taille dataSizeBit
	imgPixelLength := imgOrigSize / 4 // ouais c'est pour la lisibilité le /4
	offsetDataSize := imgPixelLength * 8 * 3
	dataSizeBit := len(fmt.Sprintf("%b", offsetDataSize))

	// on convertit les octets de l'image en binaire ASCII
	megaString := *(convertToBitsString(imgbytes))

	// on tronque le début de megaString
	megaStringLen := len(megaString) / 4
	megaOffset := int64(math.Round(float64(megaStringLen)))
	megaStringToSearch := megaString[megaOffset:] // notre dictionnaire

	// strconv.ParseUint("10101011", base 2, nombre de bits à lire) = valeur en UINT
	arraySize, _ := strconv.ParseUint(megaString[0:dataSizeBit], 2, dataSizeBit)
	megaString = megaString[dataSizeBit:]

	lengthSizeBit64, _ := strconv.ParseUint(megaString[0:dataSizeBit], 2, dataSizeBit)
	lengthSizeBit := int(lengthSizeBit64)
	megaString = megaString[dataSizeBit:]

	msgDecoded := ""
	for i := uint64(0); i < arraySize; i++ {
		offset, _ := strconv.ParseUint(megaString[0:dataSizeBit], 2, dataSizeBit) // offset N
		megaString = megaString[dataSizeBit:]
		length, _ := strconv.ParseUint(megaString[0:lengthSizeBit], 2, lengthSizeBit) // longueur N
		megaString = megaString[lengthSizeBit:]
		msgDecoded += megaStringToSearch[offset : offset+length] // notre message décodé mais sous forme binaire
	}

	// transformation du binaire => ASCII
	for i := 0; i < len(msgDecoded); i += 8 {
		val, _ := strconv.ParseUint(msgDecoded[i:i+8], 2, 8)
		flag += fmt.Sprintf("%c", val)
	}

	return flag
}

Verdict :

$ go run decoder.go _message.extracted/204200 
le message secret: DGSEESIEE{FL****R3}

FIN BRUTALE