Les conférences en sécurité informatique organisent régulièrement des challenges – ou CTF - qui consistent à réussir des épreuves de différents types : récupération de données sur des serveurs web, compréhension d'un fichier exécutable, etc. Nous avons récemment participé au CTF de la conférence Hack.lu 2013. Notre équipe de trois ingénieurs/chercheurs a terminé dans le « Hall of Fame » des équipes locales. Nous vous donnons dans cet article un aperçu de quelques épreuves types que nous avons réussies.
1. Introduction
Les CTF – Capture the Flag – sont des challenges de sécurité dans lesquels les équipes de hackers s'affrontent pour réussir le maximum d'épreuves en un temps limité. Les équipes sont constituées de un à une dizaine de hackers (et parfois plus). Les épreuves comportent chacune un score permettant de classer les équipes. Il est assez fréquent que ces compétitions aient lieu lors de conférences liées à des problématiques de sécurité ou cybercriminalité, et notamment sur les techniques de protection des infrastructures informatique et réseau : lutte contre les malwares, contre les intrusions...
Les épreuves se situent en général dans quatre domaines :
- Le décryptage d'un texte chiffré. Il s'agit alors de trouver une ruse pour le déchiffrer, avec en général quelques indices fournis. Dans la très grande majorité des cas, une attaque « brute-force » est bien entendu inefficace.
- L'attaque d'un site Internet. La solution passe par l'utilisation de failles au niveau du serveur : faille dans la sécurisation de la base de données (injections SQL), faille dans le serveur web (exemple : faille connue, mais non patchée), faille dans l'installation de l'ordinateur hébergeant le service web.
- Exécutable. Un fichier exécutable est fourni en format elf par exemple. L'exécution de ce fichier se traduit par la demande d'un mot de passe, ou d'une clé pour l'exécuter. Il faut alors comprendre le code assembleur du fichier exécutable (rétro-ingénierie).
- Divers. Il s'agit de tous les autres challenges ;) Parfois, ces épreuves sont reliées à un matériel spécifique (Arduino, Teensy...), ou dans un registre complètement différent en nécessitant par exemple du social engineering ou l'élaboration d'une vidéo.
Nous détaillons par la suite quelques challenges que nous avons réussis dans le cadre du CTF de la conférence Hack.Lu 2013 [1]. Le CTF était organisé par les Fluxfingers, une équipe renommée de hackers habitués aux CTFs puisqu'ils ont terminé l'année 2013 au douzième rang mondial. Plus de 700 équipes étaient engagées dans ce challenge qui a duré 48h. L'objectif de chaque challenge était de trouver un jeton à entrer sur le site Internet du CTF : l'entrée de ce jeton créditait l'équipe d'un certain nombre de points a priori proportionnel à la difficulté de l'épreuve en question. Comme dans tout CTF, il n'était ni autorisé aux équipes de se communiquer des résultats, ni d'attaquer le serveur central des challenges.
2. « Geolocation Flag » : accéder à un site Internet depuisun maximum de pays différents
Une épreuve consistait à accéder à une URL depuis différents pays du monde. Pour chaque accès depuis un pays différent, l'équipe recevait 1 point. Pour notre part, nous avons utilisé les techniques suivantes :
1. Le montage de connexions via le réseau Tor (Outil Vidalia), et la connexion à cette URL. L'ensemble était réalisé par un script « fait maison »: attente qu'une connexion Tor soit montée, accès au site Internet, arrêt de la connexion, etc. L'approche a bien fonctionné, mais n'a permis de faire des accès que depuis des pays qui possèdent des utilisateurs créant des nœuds Tor, surtout des pays en Europe et en Amérique du Nord. Une trentaine d'accès ont pu être réalisés grâce à cette technique.
Le script est le suivant (bash, Mac OS X) :
#!/bin/bash
RANGE=5
CPT=0
#TORSOCKS_CONF_FILE=$HOME/torsocks.conf
#echo $TORSOCKS_CONF_FILE
while true
do
echo "Starting vidalia"
open /Applications/TorBrowser_en-US.app/Contents/MacOS/Vidalia.app
echo "Waiting for Tor connection"
sleep 15
echo "Getting CTF page"
rm $HOME/tmp/2Y*
cd $HOME/tmp; torsocks wget --no-check-certificate https://149.13.33.74/ref/2Y1McRL2MvhPUt5
echo "Got CTF page"
killall Vidalia
sleep 1
done
Ce script peut être amélioré en backlistant progressivement les pays acquis des points de sortie Tor, au niveau du fichier torrc :
2. L'utilisation de web-proxies situés dans des pays différents. Ces proxies se trouvent aisément sur les moteurs de recherche classique. Cette approche manuelle s'est révélée assez fastidieuse : beaucoup de ces proxies ont une durée de vie très courte, d'où le nombre élevé de coups d'épée dans l'eau… mais cette technique nous a permis d'accéder à presque 80 pays.
Une équipe qui a pu obtenir plus de pays que nous – typiquement, des pays peu ouverts ou petits, par exemple Saint-Vincent-et-les-Grenadines (.vc) - a utilisé une approche originale – mais peu éthique ? - : l'envoi d'un e-mail à des hôtels situés dans ces pays pour une demande de disponibilités de chambre. Cet e-mail contenait un lien masqué vers l'URL à valider depuis différents pays...
3. « Robot Plans » : comprendre une image Android
Une des épreuves expliquait avec humour avoir capturé un robot alors qu'il était en train de se soulager dans un buisson, puis l'avoir interrogé, et finalement n'avoir réussi qu'à en tirer son « module de communication Android ». La pièce jointe, image.tar.gz, n'est autre que le contenu d'un système de fichiers Android. Le décompresser ne pose aucun problème, mais où donc chercher un indice, un mot de passe ? Il y a tant de fichiers ! Nous avons regardé le contenu de la carte mémoire (/mnt/sdcard) pour y trouver des templates Word ou Excel sans intérêt, et aussi la ROM d'un jeu Game Boy Advance. Nous avons même poussé le vice jusqu'à exécuter ce jeu dans un émulateur – merci pour la super partie de Mario – mais ceci ne nous a pas aidés à trouver le mot de passe.
Nous avons fini par trouver un indice dans /data/system/gesture.key :
Au passage, remarquez la présence de points entre chaque lettre, ce qui déjoue les recherches de chaînes de caractères (par exemple, nous avions évidemment cherché passwd, password, hack.lu etc. dans l'image du système de fichiers).
Sous Android, le fichier gesture.key sert théoriquement à mémoriser les mouvements qui permettent de déverrouiller le téléphone. Le téléphone comporte 9 points virtuels et ces mouvements indiquent dans quel sens les connecter. Les mouvements sont sauvegardés dans le fichier gesture.key, hashés par l'algorithme SHA1. Si cela vous intéresse, l'encodage des mouvements est très bien illustré dans [3].
Hélas, nous n'avons pas le contenu réel de gesture.key. Un indice cependant : « backup ». Nous allons donc voir les fichiers présents dans /data/backup. On constate alors qu'ils contiennent tous quelque chose qui ressemble énormément à un hash SHA-1 :
06c65f3452c48504305234dc005f5efbeb3d185f
./image/data/backup $ cat 4bm94d
960530aaddf48e841afe6e4b34b913effc92554f
Nous pensons alors que ces hash correspondent aux fameux mouvements qui ont été effacés de gesture.key. Il faut trouver à quel mouvement correspond tel hash - ceci étant facilité par l'existence de rainbow tables à cet usage sur Internet – puis dessiner les mouvements correspondants. Cette opération est grandement facilitée par [4] qui va jusqu'à dessiner le mouvement pour nous à l'écran.
Nous avons donc toute une collection de mouvements qui semblent dessiner des chiffres: 6, 7, 8, 7, 8, 7... Forcément, cela doit encoder le mot de passe à trouver. Mais dans quel ordre prendre ces mouvements ? Nous remarquons alors que tous les fichiers de backup ne sont pas créés exactement à la même heure. Il paraît logique de commencer par les premiers créés et de terminer par les plus récents. Nous ordonnons les mouvements : 7, 5, 7, 3, 7, 6… Pris deux par deux, cela nous fait penser à de l'ASCII : 75=K, 73=I, 76=L, etc. Au final, nous obtenons le message:
Méchants robots ! Et voilà ce qui permet de valider l'épreuve... (Hélas, dans notre cas, nous avons terminé cette étape 10 minutes après la fin du challenge et n'avons donc pas pris les points. Dommage !).
4. « RoboAuth » : la rétro-ingénierie d'un fichier exécutable Windows
Dans cette épreuve, on nous fournit un fichier, nommé RoboAuth.exe, et on sait qu'il faut saisir deux mots de passe (password1_password2) pour valider l'épreuve.
RoboAuth.exe est un exécutable Windows qui requiert un premier mot de passe. Dès que l'on se trompe, l'exécution se termine.
Une première inspection en hexadécimal du binaire ne révèle aucun mot de passe. Nous démarrons donc un désassembleur (OllyDbg ou IDA Pro, par exemple) et examinons l'assembleur. Il est facile de repérer la chaîne « You passed level1! » et l'on regarde où elle est utilisée :
.text:00401B5F mov [esp+164h+var_160], eax
.text:00401B63 lea eax, [ebp+var_157] ; mot de passe saisi par l'utilisateur
.text:00401B69 mov [esp+164h+var_164], eax
.text:00401B6C call strcmp ; comparaison
.text:00401B71 test eax, eax ; sont-ils identiques ?
.text:00401B73 jnz short loc_401B8D ; quitter si mauvais mot de passe
.text:00401B75 mov [esp+164h+var_164], offset aYouPassedLevel ; "You passed level1!"
Tout de suite, on comprend que le programme compare (strcmp) la chaîne attendue avec la chaîne saisie, et affiche le message d'encouragement si elles sont identiques. Il suffit alors de mettre un point d'arrêt sur la comparaison, d'exécuter le programme et inspecter les valeurs en mémoire pour y trouver le sésame :
Mais alors, pourquoi ne voyait-on pas cette chaîne r0b0RUlez! ? Parce qu'elle n'est pas écrite telle quelle dans l'exécutable. Dans le code ci-dessus, le mot de passe attendu se trouve dans var_143. Or, en regardant plus haut, on trouve ces instructions :
.text:00401A7B mov [ebp+var_13F], 'elUR'
.text:00401A85 mov [ebp+var_13B], '!z'
Cela explique tout ! Le mot de passe était présent, mais en 3 tronçons et nous ne l'avions pas remarqué. Il était là pourtant, sous nos yeux et même un hexdump aurait pu nous mettre sur la voie :
00000e80 ff 52 55 6c 65 66 c7 85 c5 fe ff ff 7a 21 c6 85 |.RUlef......z!..|
Le programme passe alors dans une deuxième routine qui demande un deuxième mot de passe. Là, ça se corse un peu. Le programme appelle l'interruption 3. Cette interruption x86, appelée « Trap to Debugger » en anglais, permet de créer un point d'arrêt logiciel et complique un peu notre tâche d'exécution pas à pas dans un désassembleur. Il suffit de se positionner plus loin dans le code : il y a une boucle qui compare caractère par caractère le deuxième mot de passe attendu avec le mot de passe fourni. Nous y plaçons un point d'arrêt, et pour chaque caractère, nous notons la valeur attendue. Au passage, notez la présence d'une opération « XOR 2 » en guise de « chiffrement » de chaque caractère du deuxième mot de passe.
.text:0040154C mov eax, [ebp+arg_0] ; mettre un point d'arret
.text:0040154F movzx edx, byte ptr [eax]
.text:00401552 mov eax, [ebp+arg_4]
.text:00401555 movzx eax, byte ptr [eax]
.text:00401558 xor eax, 2 ; "dechiffrement" du caractere
.text:0040155B cmp dl, al ; comparer les 2 caracteres
.text:0040155D jz short suivant ; mettre un point d'arret
.text:0040155F mov eax, 1
.text:00401564 jmp short erreur_quitter
.text:00401566 suivant:
.text:00401566 add [ebp+arg_0], 1 ; mettre un point d'arret
.text:0040156A add [ebp+arg_4], 1
.text:0040156E lecture:
.text:0040156E mov eax, [ebp+arg_4]
.text:00401571 movzx eax, byte ptr [eax]
.text:00401574 cmp al, 2
.text:00401576 jnz short comparaison ; mettre un point d'arret
.text:00401578 mov eax, 0
Comme la procédure de vérification termine le programme à la moindre erreur, il est nécessaire avant chaque comparaison de modifier en mémoire la valeur saisie (nous avons entré n'importe quoi) pour qu'elle corresponde à la valeur attendue.
Heureusement, le mot de passe n'est pas trop long, et au final, nous trouvons le 2ème mot de passe : w3lld0ne.
Pour valider l'épreuve, il ne nous reste plus qu'à envoyer au serveur r0b0RUlez!_w3lld0ne.
5. « Packed » : retro-ingénierie et cryptographie
Un fichier nommé « packed » est fourni. Son examen permet de comprendre qu'il contient les parties suivantes :
- d'abord une première séquence vide ;
- puis la chaîne : #disabled-encoding: _rot___[...]__13 ;
- puis une séquence binaire ;
- puis un fichier PDF ;
- puis une séquence de caractères « bizarres » (« chiffrés » en rot13) ;
- puis un fichier encodé.
L'extraction du contenu du fichier PDF ne révèle a priori rien, et son ouverture permet de visualiser un « No hint given ». La dernière partie est un fichier ODT encodé en base 64. Son ouverture avec LibreOffice permet d'afficher un « Still no hint given » et son contenu ne semble rien cacher de particulier.
Nous nous sommes donc focalisés sur la partie en rot13. Elle contient le code python suivant :
cipher="H51\\\'Ux2J&+(3Z;Uxcx0Xxs\x13h\x014$V!R($R>\t/)R!\x01<.\x13,N-aP4M4aRuG1-VuU0 GuH+a@0W=3R9\x01>(_0\x01,8C0Rx GuN6\"V|\x1ezKZ3\x014$]}R!2\x1d4S?7\x1au\x1fxs\t_\x01xa\x13<Gx)R&Ip2J&\x0f93T#zj\x1c\x1ap\x13rk\x00g\x01e|\x13g\x19ju\x0ba\x18jt\x02o+xa\x13u\x01xa\x13%S1/Gu\x03\x1b.\\:N7.\\:N4o\x13\x0cN-3\x133M9&\x13<Rx A2WjiZ{DvaX0Xjh\x136N6\"R!\x01\x07rC0p\x138a\x1dc22ieu\x161Fw+=-@0\x1bRa\x13u\x01(3Z;UxcR\'F.s\x1c>D!s\x13<Rx,Z&R1/Tw+R"
n=0; import hashlib, sys;
try: key = sys.argv[1]
except IndexError: sys.exit ("x\x9c\xf3N\xadT0T\xc8\xcd,.\xce\xccKW\xc8\xccSH,J/\x03\x00M\x97\x07\\".decode("mvc"))
f =getattr (hashlib ,"x\x9c\xcbM1\x05\x00\x02G\x01\x07".decode("mvc"))
while n < (5 *10 **6 ): key = (f (key ).digest ()); n = n +1
key = key[:5 ].upper()
while len (key )< len (cipher ): key = key * 2
plain = "".join (map (chr ,[ord (a)^ord (b)for a ,b in zip (cipher, key)]))
try: exec plain
except: print "x\x9c\x0b/\xca\xcfKW\xf0N\xadT\x04\x00\x14d\x03x".decode ("mvc"), repr(plain)
On notera que le code ci-dessus n'est pas directement utilisable, car la chaîne "mvc" qui apparaît à plusieurs reprises est elle-même codée en rot13 et doit donc être remplacée par "zip").
La variable cipher de ce code python comporte un texte chiffré. Le programme prend une clé en entrée, puis l'utilise pour déchiffrer le message avec un algorithme un peu surprenant. Tout d'abord, il effectue 5 millions de md5 sur la clé, puis extrait les 5 premiers caractères du résultat, et les passe en majuscule. La clé de 5 caractères ainsi obtenue est ensuite utilisée pour décoder le message chiffré en appliquant un xor sur chaque groupe de 5 caractères. Le message décodé est alors exécuté : il s'agit donc de code python.
Comment décoder ? Notre première idée a été d'utiliser la première partie non alphanumérique du fichier « packed », avant le PDF, pour essayer de voir si cela ne serait pas la clé… Trop simple, échec !
La deuxième approche fut de programmer une attaque brute force sur les 5 caractères de la clé. Avec 20000 tentatives seulement par seconde sur notre ordinateur, ce brute force aurait pris presque une année : cela n'est bien sûr pas compatible avec les 48h du challenge ...
La troisième approche fut la bonne : l'utilisation d'un simple xor comme technique de chiffrement est extrêmement faible, chacun des 5 caractères de la clé est appliqué indépendamment des autres. On peut donc déterminer les 5 caractères un à un en réalisant cinq attaques par force brute (seulement 256 valeurs à tester pour chaque caractère !). Comme on sait que le message décodé doit contenir du code python, il est simple de déterminer pour chaque caractère laquelle des 256 valeurs est la bonne en vérifiant que la partie du message qu'il chiffre se compose de caractères alphanumériques et de ponctuation.
Une fois décodé, le message contenait le code python ci-dessous :
print "Key 2 = leetspeak(what do you call a file that is several file types at once)?"
if len(sys.argv) > 2:
if hash(sys.argv[2])%2**32 == 2824849251:
print "Coooooooool. Your flag is argv2(i.e. key2) concat _3peQKyRHBjsZ0TNpu"
else:
print "argv2/key2 is missing"
Et voilà que le code contient encore une nouvelle énigme : « what do you call a file that is several file types at once ».
Un indice posté sur le site évoquait un animal qui change de couleur.
Nous avons pensé au caméléon.
Après quelques essais, nous avons trouvé que le flag était « ch4m3l30n_3peQKyRHBjsZ0TNpu ».
6. « Pay TV » : attaque d'un site Internet
Un site Internet présente une image de robots regardant une télévision « cryptée ». La télévision en elle-même comporte une image GIF animée qui imite un brouillage noir et blanc. En bas à gauche de l'image est présentée une fenêtre de saisie « password ». Bien entendu, l'entrée d'un mot de passe permettra sans doute d'afficher une image lisible sur la télévision, image qui contiendra – nous l'espérons - le jeton de validation du challenge.
Notre première idée fut d'analyser le contenu de l'image GIF animée, en espérant y trouver le mot de passe. La GIF animée comporte 6 images. Nous avons manipulé sous GIMP les différentes images, en les superposant, ou en effectuant des opérations de type xor. Sans succès.
Le site cachait deux indices qui nous ont aiguillés vers la solution.
Tout d'abord, un journal est posé à côté de la télévision. Il comporte l'indication « Side channel attacks », cf. la figure ci-jointe remise à l'endroit.
Ensuite, en étudiant les sources JavaScript du site, nous avons remarqué un commentaire très intéressant dans le code qui construit la requête pour envoyer un mot de passe :
xhr.send('key=' + encodeURIComponent(key)/* + '&debug'*/)
Ainsi donc, il serait possible d'activer un mode debug ?
Alors qu'une requête normale envoyant un mot de passe erroné obtient une réponse JSON comme celle-ci :
En activant le mode debug, on reçoit une réponse comme celle-là :
Nous avons tout de suite compris que ces deux valeurs start et end étaient des estampilles temporelles (en secondes depuis l'epoch Unix) qui nous permettaient d'obtenir la durée de traitement de la requête.
Nous avons supposé que le temps de traitement de chaque caractère du mot de passe pourrait être différent selon que le caractère soit correct ou pas. Il serait dès lors possible de trouver le mot de passe caractère par caractère.
Nous avons écrit un script qui affiche le temps de traitement de tous les mots de passe longs d'un seul caractère (en se limitant aux caractères alphanumériques) :
#! perl
use JSON;
use LWP::UserAgent;
use LWP::Protocol::https;$ENV{'PERL_LWP_SSL_VERIFY_HOSTNAME'} = 0;
my $ua = LWP::UserAgent->new;
my $url = 'https://ctf.fluxfingers.net:1316/gimmetv';
foreach my $char (0..9,'a'..'z','A'..'Z') {
my $pw_candidate = "$char";
my $response = $ua->post($url, 'Content-Type' => 'application/x-www-form-urlencoded', 'Content' => "key=${pw_candidate}&debug");
my $resp_content = $response->content;
my $json_obj = decode_json($resp_content);
my $duration = $json_obj->{'end'} - $json_obj->{'start'};
print "$pw_candidate: $duration\n";
if($json_obj->{'success'} eq 'true') {
print "Password found !!\n";
print "Response: $resp_content\n";
exit;
}
}
Nous avons constaté que le temps de traitement était négligeable pour tous les caractères, sauf pour le « A » qui durait environ 0.1s. Nous avons donc pensé que ce devait être le premier caractère du mot de passe.
Nous avons modifié notre script pour tester cette fois tous les mots de passe longs de deux caractères dont le premier caractère est « A » :
my $pw_candidat = "A$char";
[...]
Cette fois encore, un des mots de passe testés avait une durée de traitement plus longue de 0.1s que les autres.
Nous avons itéré ce procédé, et lorsque nous eûmes enfin trouvé le mot de passe dans son intégralité, la réponse JSON contenait :
La chaîne OH_THAT_ARTWORK! s'affichait sur l'écran de télévision des robots, une fois le mot de passe trouvé, et c'était bien la solution du challenge.
Conclusion
Cet article a présenté la résolution de certaines épreuves du CTF de Hack.Lu 2013. Notre modeste équipe nommée PicOwn de 3 personnes a terminé classée 6ème en local (et donc, elle est dans le Hall of Fame!) et 97ème en tout (sur plus de 700 équipes engagées).
Merci aux organisateurs qui ont proposé 21 épreuves très bien pensées et variées, parfois vraiment difficiles, surtout en cryptographie qui était pourtant un de nos thèmes de prédilection.
Les CTF de Hack.lu des quatre dernières années sont accessibles en ligne [2] et nous vous invitons à vous y frotter et, pourquoi pas, à participer à la prochaine édition, localement ou à distance !
D'autres « writeups » de ce CTF sont également disponibles en ligne [5].
Remerciements. Merci à notre relecteur de PollyPocket... et félicitations pour leur score !
Références
[1] Hack.lu : http://2013.hack.lu/
[2] Archives des CTF de Hack.lu : https://ctf.fluxfingers.net/
[4] Android Hash to Gesture : https://barney.0x539.se/android/
[5] Writeups CTF Hack.lu 2013 : https://ctftime.org/event/97/tasks/