Solution SSTIC
Le challenge nous amène à télécharger une image disque d'une carte SD provenant d'une clé USB "étrange". Le fichier se dézippe sans problèmes et se monte sur un système linux sans difficultés. Un unique fichier est présent, appelé inject.bin. Ce nom de fichier est connu et fait penser aux clés USB Rubber Ducky, périphériques USB ayant l'apparence d'une clé, mais étant un clavier capable de taper une série de touches préprogrammées lorsqu'on l'insère dans un ordinateur.
La commande strings appliquée à ce fichier confirme l'hypothèse de la clé Rubber Ducky:
kevin@dell:~/sstic$ strings sdcard.img | tail -1 java -jar encoder.jar -i /tmp/duckyscript.txt kevin@dell:~/sstic$
L'auteur de l'encodeur duckyscript fournit un décodeur en perl ( https://code.google.com/p/ducky-decode/source/browse/trunk/ducky-decode.pl ) qui retranscrit en duckyscript le fichier inject.bin. Le fichier est une succession de commandes powershell:
GUI R DELAY 500 ENTER DELAY 1000 c m d ENTER DELAY 50 p o w e r s h e l l SPACE - e n c SPACE Z g B 1 A G 4 A Y w B (longue chaine base64) f Q A = 00a0 ENTER p o w e r s h e l l SPACE - e n c SPACE Z g B 1 A G 4 (etc...)Une lecture de la documentation de powershell nous apprend que powershell peut écrire du base64 à la volée. Il suffit donc de décoder ces lignes en base64. Nous obtenons 3389 blocs de code faisant 3776 octets sauf les deux derniers.
Chaque bloc de code (sauf le dernier) ajoute des données dans un fichier nommé stage2.zip si l'utilisateur windows s'appelle challenge2015sstic. Plutôt que de lancer chacun des scripts, il est beaucoup plus rapide d'extraire les données fournies en base64 et de les ajouter dans un fichier stage2.zip.
Le dernier bloc de code effectue juste une vérification sur le fichier généré.
Le code nécessaire à régénérer le stage2.zip n'est pas fourni, il s'agit de one-liner shell à chaque fois.
Une recherche forensics sur l'image disque montre la présence d'un fichier appelé build.sh. Il semble constitué de données non cohérentes et n'est pas essentiel à la résolution du challenge..
Le second stage est sans doute le niveau le plus étrange qu'il m'ait été donné d'étudier dans un challenge de sécurité informatique :)
Le fichier stage2.zip est constitué de 3 fichiers. Le premier, encrypted, ne donne pas de doutes sur sa nature. Le second, sstic.pk3 semble être celui à étudier et le dernier memo.txt indique les étapes à suivre pour résoudre le challenge: retrouver une clé cachée dans un jeu vidéo. Le suffixe .pk3 fait immédiatement penser à Quake.
Les fichiers .pk3 sont des archives zip. Son ouverture et le parcours de ses fichiers nous montre un certain nombre de textures contenant un logo, et trois lignes hexadécimales. Leur nombre (une centaine) empêche toute tentative de bruteforce par combinaison des valeurs inscrites.
Les données d'une map sont enregistrées dans le fichier binaire .bsp dans quake/map/. La lecture des chaines de caractères permet d'extraire quelques informations puisque nous y trouvons (entre autre):
"message" "SSTIC Challenge ! Are you ready ?!" "message" "Welcome n00b !" "message" "The secret area \n is now open during \n30 seconds !" "message" "Time to Rocket Jump ?!" "message" "Yes!\n You found my key !"On comprend qu'il faut lancer la map, trouver la "secret area" qui doit vraisemblablement contenir la clé. Avant de lancer apt-get install openarena, un tour sur internet donne les cheatcodes et GtKradiant permet d'obtenir une vue de dessus de la map:
Naviguer jusqu'à la zone secrète ne pose aucune difficulté, et la clé est indiquée avec des logos et des couleurs. Une seconde navigation dans la map en elle-même permet de trouver un certains nombres de textures et d'associer le logo et la couleur du texte pour créer la clé. Plusieurs textures étant cachées, j'ai choisi d'en trouver un maximum et de terminer par un bruteforce sur les dernières textures en allant directement lire toutes les images dans le dossier texture.
La clé reconstituée vaut: 9e2f31f78153296b3d9b0ba67695dc7cb0daf152b54cdc34ffe0d35526609fac , et le fichier encrypted se déchiffre comme un fichier zip.
L'IV utilisée vaut 0x5353544943323031352d537461676532, ce qui correspond au texte suivant en hexadécimal: SSTIC2015-Stage2, un clin d'oeil sans doute :)
Le fichier zip contient de nouveau trois fichiers, memo.txt, encrypted et paint.cap. Le fichier memo indique avoir enregistré la clé avec paint.
Le memo ne laisse pas de place à l'hésitation: le fichier pcap est une capture d'une souris USB, on en déduit que la clé a été écrite de manière graphique sous paint alors que le sniffer de trame USB était en route.
Un deuxième point retient l'attention et concerne le cipher: Serpent-1-CBC-With-CTS. Serpent n'est pas un algorithme qui fait partie d'openssl, il faut donc trouver une implémentation de Serpent (ou la recoder). Serpent existe en deux versions, la 0 qui est la soumission officielle et qui est obsolète et Serpent-1 qui est la version corrigée suite à la soumission. Le mode CTS n'est pas très courant non plus, la page wikipedia explique assez bien son fonctionnement.
Le format pcap est un format très simple, constitué d'un header, puis de données. Les données sont constituées d'un header et des datagramme tels qu'ils ont circulé sur le fil. Le header des données indique la taille des datagrammes. Le protocole d'échange de données d'une souris USB est monodirectionnel (souris vers PC) et est stocké sur 4 octets: Le premier indique si un bouton est cliqué en donnant son numéro (1 pour le clic gauche), le second et troisième sont des entiers signés indiquant la différence en pixel par rapport à la position précédente. Le dernier octet vaut 0. [Ceci est une description simpliste du protocole mais suffisant pour résoudre le niveau].
Un programme python permet de parser
le fichier pcap et d'en extraire les 4 octets de données utiles.
Ce programme va ensuite redessiner ce qui a été fait. Le résultat
est visible ici:
L'algorithme de hachage Blake256 n'est pas très courant. Le site web de référence https://131002.net/blake/ permet de télécharger et d'installer cette fonction de hachage.
Afin d'utiliser le mode CTS, j'ai choisi d'utiliser cryptoplus, une implémentation de Serpent écrite par doegox https://github.com/doegox/python-cryptoplus pour l'ensemble du fichier moins les deux derniers blocs (servant au CTS). Ces deux derniers blocs ont été déchiffré séparément par un autre programme python, puis concaténé au résultat déchiffré précédent.
Ce niveau est sans doute celui qui m'a le plus fait perdre de temps. J'ai utilisé le programme C en ligne de commande du site de référence Serpent pour valider/invalider des hypothèses. Mais malheureusement, l'affichage en sortie inverse les octets ! Donc toutes mes hypothèses se révélaient faussement erronées. J'ai alors remis en cause le hachage (fallait il inclure le retour chariot, les espaces en surnombre?), le versioning de Serpent, des informations cachées, etc..
On note l'humour des concepteurs du challenge. La phrase utilisée dans le dessin "The quick brown fox jumps over the lobster dog" fait penser à un pangramme de la langue anglaise: http://fr.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog. Pourquoi écrire un "lobster" dog au lieu d'un "lazy"? Je pense que la solution se trouve dans le challenge SSTIC 2012.
Un problème s'est posé pour la reconstruction de l'image: Fallait il extrapoler les pixels écrits entre deux mouvements de souris lorsque le bouton de la souris est cliqué ou bien "noircir" un pixel à chaque envoi d'information était il suffisant? Cette deuxième solution, plus simple à programmer, s'est révélée suffisante.
L'image dessinée par l'auteur du challenge contient la signature
de l'équipe qui a conçu le challenge. En effet, si on suit tous
les mouvements de souris, alors on aperçoit un dessin un peu plus
complet:
Et IMP est le nom de l'équipe.
L'IV choisi ne l'a pas été au hasard, puisqu'il vaut "SSTIC2015-Stage3".
Le script contient une variable data, une hash, puis un bloc commençant par
$=~[];. Cette succession de caractère est typique d'un code javascript obfusqué à l'aide de jjencode (http://utf-8.jp/public/jjencode.html).
La désobfuscation jjencode montre un second niveau d'obscurcissement fait par un renommage de variable. J'ai choisi de renommer manuellement les variables car le code n'est pas très long et afin de mieux comprendre la logique du javascript. Cette désobfuscation nous apprend que la variable "data" est une donnée chiffrée, dont la clé et l'IV sont déduites du User-Agent du navigateur. Plus précisément, l'IV est la suite des 16 caractères suivant une parenthèse ouvrante, et la clé est la suite des 16 caractères précédant une parenthèse fermante.
Les chaînes consituant un UserAgent de navigateur n'ont aucune contrainte. Il est impossible de bruteforcer une clé et un IV de 16 octets chacun pour déchiffrer le bloc data. La seule solution probable est qu'il s'agisse d'un User-Agent véritable.
Le site http://www.useragentstring.com permet de constater l'ampleur de la tâche. Heureusement, des informations présentes dans le code javascript vont nous permettre de réduire le scope de recherche.
La première aide est une ligne commentée dans le code:
//document['write']('<div style="display:none"><a target="blank" href="chrome://browser/content/preferences/preferences.xul">Back to preferences</a></div>');Le seul navigateur utilisant cette URL est firefox. Il est à noter que toutes les versions de Firefox comportent une chaine de caractères entre parenthèses dans le UserAgent.
La deuxième aide est lié au code javascript en lui-même qui fait appel à window.crypto.subtle, fonction disponible uniquement à partir de Firefox 34.0
La troisième aide concerne le fichier de sortie qui est encore un zip. Un zip a un header connu, ce qui peut aider lors du déchiffrement.
Armé des deux première informations, la liste des versions de Firefox n'est plus très grande à générer: la partie entre parenthèse s'écrit:
(Plateforme, système, version)avec des versions mineures non différenciées (la version 36.0.1 s'écrit 36.0 uniquement par exemple).
Pour bruteforcer, il est possible de générer la totalité des clés probable, puis de déchiffrer seulement 16 octets avec, et de XORer avec les 4 premiers caractères de l'IV puisque un header zip fait 4 octets.
Une des clés utilisée déchiffre le bloc data en un fichier zip valide: (Macintosh; Intel Mac OS X; rv:35.0)
La chaine user agent permettant de déchiffrer le stage4 étant celle d'un User Agent légitime, je me demande comment aurait réagi un des participants au concours si l'ouverture du fichier stage4.html aurait vu le déchiffrement réussir.
Le fichier stage5.zip contient deux fichiers: schematic.pdf et input.bin. La lecture de schematic.pdf laisse penser qu'un CPU ST20 est utilisé.
Le CPU ST20 n'est pas un CPU des plus classiques. Il n'existe pas d'émulateur "grand public", pas de support de gdb. La documentation sur internet est relativement succinte, et ce n'est pas une architecture souvent employée en challenge de sécurité informatique.
Une recherche sur internet permet de trouver les commandes de référence et un désassembleur (st20dis.exe). J'ai également lu des documentations sur d'autres Transputers proche en conception. Les points les plus importants sont les suivants: Le CPU a trois registres généraux, A, B et C, utilisables en pile uniquement (pas d'accès direct). Le registre I est le pointeur d'instruction, le registre W une adresse de Workspace (qui s'apparente à une stack). Les offsets d'adresses sauts sont relatifs. Concernant l'architecture, un ST20 est constitué de transputer, chacun pouvant être vu comme une unité de travail indépendante. Les Transputers peuvent communiquer entre eux en envoyant des données sur une adresse précise. Les adresses sont:
Pour comprendre le fonctionnement du programme, il est donc nécessaire de savoir quel bloc de code correspond à quel transputer, information manquante. Il a donc fallu poser un certain nombre d'hypothèses.
Avant de se lancer dans un reverse, j'ai analysé l'entropie du programme à l'aide de binwalk (binwalk -E input.bin). Le début du fichier à une entropie faible, mais la fin a une entropie proche de l'aléatoire. De manière empirique, la zone aléatoire semble démarrer vers l'offset 0x9b0. Le fichier présente une particularité très intéressante à cet endroit:
00000990 ff ff ff ff ff 17 63 6f 6e 67 72 61 74 75 6c 61 |......congratula| 000009a0 74 69 6f 6e 73 2e 74 61 72 2e 62 7a 32 fe f3 50 |tions.tar.bz2..P| 000009b0 dc 81 bc 97 27 89 ac 72 28 cb 50 a4 09 d3 18 17 |....'..r(.P.....|On émet l'hypothèse que tous les octets suivants congratulations.tar.bz2 sont le fichier encrypted. Un hachage de ces octets renvoie bien la valeur de référence fournie dans le pdf schematic.pdf, ce qui valide l'hypothèse. Une deuxième information intéressante concerne le nom et le format du fichier en clair.
Pour "entrer" dans le code, j'ai supposé que l'entry point est à l'offset "0". Le premier octet ne semble rien dire de particulier, mais les suivants prennent du sens au fur et à mesure de leur désassemblage, surtout à partir de 0x0000000e:
; New subroutine e+f8; References: 0, Local Vars: 76 0000000e: 64 b4 sub_e: ajw #-4c ; adjust workspace - Move workspace pointer 00000010: 2c 49 ldc #c9 ; load constant - A = n, B=A, C=B 00000012: 21 fb ldpi [loc_dd] ; Load pointer to instruction - A = next instruction + A //A vaut 0x000000dd -> next instruction est à 0x00000014, et 0x00000014+c9=0x000000dd // et à l'adresse 0x000000dd se trouve une chaine de caractère "Boot ok." 00000014: 24 f2 mint ; minimum integer - A = MostNeg 00000016: 48 ldc #8 ; load constant - A = n, B=A, C=B // [C] est à l'adresse de la chaine "Boot ok.", A vaut 8 (characters), B vaut 0x80000000 00000017: fb out ; output message - A bytes to channel B from address C //L'adresse 0x80000000 est le link0 outputCette lecture permet de s'assurer que le programme démarre bien à cet emplacement et est selon toute vraisemblance executé par le Transputer 0.
La suite du code est moins évidente. Pour reverser ce code, j'ai cherché deux choses: tout d'abord, découvrir des zones de code dans lesquelles des opérations logiques sont effectuées sur des octets (addition, XOR, ect..) et ensuite de trouver des boucles dans lesquelles des lectures et des écritures d'octets se faisaient via des INPUT et des OUTPUT. Une fois chaque bloc de code isolé, il faut chercher sur quel Transputer il s'applique.
Après un temps d'analyse, je suis arrivé aux conclusions suivantes:
00000078 --> 00000105 MAIN LOOP (transputer 0) Lecture de 1 byte en input 0, push de 0xc bytes vers output1,2,3; lecture de 1 byte en in1,2,3, puis out de 1 byte en out0 (Transputer 1, 2 et 3?) 00000106 --> 00000182 Lecture de 0xc byte en input0, push de 0xc byte en out1,2,3; lecture de 1 byte en in1,2,3, écriture d'1 byte en out0 00000183 --> 000001ff Lecture de 0xc byte en input0, push de 0xc byte en out1,2,3; lecture de 1 byte en in1,2,3, écriture d'1 byte en out0 00000200 --> 00000287 Lecture de 0xc byte en input0, push de 0xc byte en out1,2,3; lecture de 1 by te en in1,2,3, écriture d'1 byte en out0 (transputer 4 à 12?) 000004b9 --> 00000520 [fct in 4b9, fct out 4bf] Une Lecture de byte, une écriture de byte 00000521 --> 00000588 [in: 521, out:527] Une lecture de byte, une écriture de byte 00000589 --> 0000062c [in: 589; out: 58f] Une lecture de byte, une écriture de byte 0000062d --> 000006a8 [in: 62d; out: 633] Une lecture de byte, une écriture de byte 000006a9 --> 0000075c [in: 6a9; out 6af] Une lecture de byte, une écriture de byte 0000075d --> 000007c8 [in: 75d; out: 763] Une lecture de byte, une écriture de byte 000007c9 --> 00000878 [in:7c9; out: 7cf] Une lecture de byte, une écriture de byte 00000879 --> 00000900 [in:879; out: 87f] lecture de byte, écriture de byte, lecture de byte, écriture de byte 00000901 --> [in: 901; out:907] lecture de byte, lecture de byte, écriture de byte, écriture de byteLe reverse static de chacune des fonctions ne pose pas de problèmes particuliers. Le seul détail auquel il faut prendre garde est la présence de variables globales, dont l'état est conservé entre deux lectures/écritures d'octets. Pour l'association des blocs de codes aux transputers, j'ai choisi de le faire dans l'ordre d'apparition et cela a fonctionné. On remarque ensuite que les transputers 0, 1, 2 et 3 ne font rien d'autre qu'XORer les résultats, ce qui est commutatif.
Le mode de chiffrement est original: Chaque octet des données à déchiffrer est XOR avec deux fois la valeur de l'octet de la clé additionné de son rang, le tout modulo 0xFF:
Clair[i]=(chiffré[i] ^ (2x key[i]+1))&0xff. Puis une opération est effectuée sur la clé: Tous les octets de la clé sont envoyés aux transputers qui vont calculer un octet en résultat qui va remplacer key[i], et le déchiffrement reprend à i++.
Après quelques mises au point, j'ai un programme python qui déchiffre correctement le Test Vector (fourni en annexe). Mais pour déchiffrer le fichier, il manque la clé.
Les quelques clés "évidentes" (celle du TestVector, 000000000000, 11111111111 et autres valeurs standards) ne renvoient rien d'intéressant. Une étude est donc nécessaire.
J'ai présumé que la clé fasse 12 octets: c'est la taille de la clé du TestVector, et les transputers s'échangent des données qui font 12 octets. J'ai également présumé que le fichier de sortie était un tar.bz2. Les tar.bz2 présentent une particularité intéressante: leurs 10 premiers octets sont fixes:
$ hexdump -C a.bz2 | head -1 00000000 42 5a 68 39 31 41 59 26 53 59 78 23 35 bf 5a 26 |BZh91AY&SYx#5.Z&|. Le mode de déchiffrement utilisé permet donc de déduire les 10 premiers octets de la clé modulo 0xff:
Si Clair[i]=(Chiffré[i]^(2xKey[i]+i))&Oxff alors Key[i]=(Clair[i]^Chiffré[i])/2-i Ou Key[i]=(Clair[i]^Chiffré[i])/2-i + 0x80La combinaison de ces 2 solutions pour chacun des 10 premiers octets donne donc 1024 solutions. Il reste donc deux octets de la clé à trouver (256x256=65536), cela fait donc 65 millions de déchiffrements à bruteforcer.
L'implémentation python de référence que j'ai écrit déchiffre les 250ko chiffrés en à peu près huit secondes. Ce n'est donc pas satisfaisant pour bruteforcer l'ensemble des clés.
Pour accélérer le bruteforce, une réécriture du code python a permis de descendre à 7 secondes puis une utilisation de cython a fait descendre à 4 secondes. C'est mieux, mais reste insuffisant pour bruteforcer l'ensemble des clés.
Une analyse plus poussée du format bz2 montre une particularité intéressante lorsque les fichiers font environ 250ko (particularité qui n'est plus vraie pour des fichiers plus petits):
$ hexdump -C a.bz2 | head -2 00000000 42 5a 68 39 31 41 59 26 53 59 42 02 25 c8 00 19 |BZh91AY&SYB.%...| 00000010 81 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|Il devient donc possible de déchiffrer uniquement les 24 premiers octets du fichier chiffré et de vérifier si les octets 20 à 23 (par exemple) valent 0xff. La durée du déchiffrement de 24 octets est bien plus rapide et le petit nombre de clés valide permet de lancer le déchiffrement sur le fichier complet. Pour des raisons de rapidité, j'ai utilisé les 4 coeurs de mon PC en lançant 4 fois le programme python recompilé en cython, chacun sur un quart des clés. Au bout d'une heure, une dizaine de clés étaient sorties, dont la bonne:
5ed49b7156fce47de976dac5Le bruteforce n'a pas été mené au bout, la clé ayant été trouvée.
Sans doute le stage le plus plaisant pour la partie reverse, et l'idée de devoir faire un bruteforce intelligemment pour trouver la solution étant peu courant.
Le fichier congratulations.tar.bz2 ne donne pas la solution comme son titre laisse l'imaginer. Il s'agit d'une image au format jpg.
Le poids de l'image étant en contradiction avec sa géométrie (636x474 pixels pour +250ko), on se doute que des données sont cachées. binwalk donne immédiatement la solution:
$ binwalk congratulations.jpg DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 JPEG image data, JFIF standard 1.01 55248 0xD7D0 bzip2 compressed data, block size = 900k
La deuxième image est au format png. Encore une fois, le poids du png est en contradiction avec sa taille. Binwalk donne une information intéressante:
$ binwalk congratulations.png DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 PNG image, 636 x 474, 8-bit/color RGBA, non-interlaced 99 0x63 Zlib compressed data, default compression, uncompressed size >= 33020 133286 0x208A6 Zlib compressed data, best compression, uncompressed size >= 655360Et à l'offset 0x63 on observe la chaine sTic. Les images png étant constituées de chunk, on vérifie avec pngcheck si certains chunks sont anormaux:
$ pngcheck congratulations.png congratulations.png illegal reserved-bit-set chunk sTic ERROR: congratulations.pngJ'ai écrit un programme python qui extrait tous les chunks "sTic" et qui les concatène, puis les unzlib. On obtient encore un tar.bz2 qui s'avère contenir une image:
Une image tiff dont la taille correspond à son poids. Nous observons que chaque pixel est codé à l'aide de trois octets. Un remplissage de la partie noire avec gimp montre que la partie du haut n'est pas du noir unique, nous soupçonnons alors de la stéganographie.
Les premières méthodes de stéganographie ne donnent rien: Récupération du LSB, big endian, little indian, récupération du LSB sur une couleur unique (un octet sur trois) non plus. Un comptage statistique montre que le bit faible de la composante Red n'est quasiment jamais modifié. Et de fait, une stéganographie ne tenant compte que du bit le plus faible des composantes Green et Blue permet de reconstruire un tar.bz2 contenant un gif.
La dernière image est un .gif. Sa petite taille empĉhe d'imaginer
un fichier imbriqué de plus. L'analyse de ce .gif avec ImageMagick
indique que les 256 couleurs d'une palette sont utilisées ce qui
semble beaucoup. Une modification aléatoire de la palette de couleur
est essayée, et l'image devient:
Un zoom sous gimp permet de recopier l'adresse mail qui valide
le challenge :)
Merci aux "imps" pour ce challenge original et varié.