Challenge cybersécurité : Opération Brigitte Friang (Partie 2)

Publié par Mickael Alibert le

Temps de lecture : 13 minutes

L’an dernier, la DGSE proposait un challenge sur le thème de la cybersécurité, le challenge Richelieu. Il s’agit de résoudre une suite d’énigmes faisant appel à des connaissances dans de nombreux domaines tels que la cryptographie, le hacking, la stéganographie… Je n’ai pas participé, mais j’ai suivi le parcours de Pierre-Yves et Charles-Henri, consultants à Zenika Nantes, il me trottait donc l’envie de participer de manière active à une expérience semblable si l’occasion se présentait à nouveau.

Cette année, du 24 octobre au 11 novembre, en partenariat avec l’ESIEE Paris, un nouveau challenge est proposé, l’Opération Brigitte Friang. Pris par le quotidien, j’avais laissé de côté cette information, jusqu’à ce que le second confinement français vienne libérer de la place dans mon agenda. C’est donc comme cela que je me suis lancé dans ce challenge le 30 octobre.

Ce retour d’expérience est composé de trois articles. Chacun d’entre eux vous permettra de suivre mon cheminement, mes succès et mes échecs sur les différentes épreuves du challenge. Il vous montrera également comment de petites négligences sur la sécurité peuvent avoir de lourdes conséquences, telles que des vols de données ou de l’impersonnalisation. En fin de chaque article, je ferai un retour sur quelques-unes de ces erreurs, qui sont bien plus répandues qu’on ne le pense, et sur ce qui aurait pu être mis en place pour concevoir une solution moins exploitable par des personnes malveillantes.

Si vous ne l’avez pas lu, mon retour d’expérience sur la première partie du challenge est disponible ici.

Blaise Pascal – Service Algo

Algorithmie

Le dernier challenge disponible est celui de Blaise Pascal, du service Algo. Il commence par un défi sur deux fichiers. L’un des fichiers est l’original, et le second est une communication du gouvernement d’Evil Country interceptée. Les deux fichiers ne sont pas identiques alors que leur contenu devrait être le même.

Extraction de données cachées

HEALTH ASPECTS
OF
CHEMICAL AND BIOLOGICAL
WEAPONS
Report of a WHO Group of Consultants

Extrait du message original

HbEALTH ASPECTS
aOF
CsHEMIeCAL6 AND4 BIOLOGICAL:
W/EAPONS
Rep9ortj of a /WHO Group4 of CoAnsultants

Extrait du message intercepté

En comparant les premières lignes des deux fichiers, on constate que des caractères ont été ajoutés dans le fichier intercepté. On peut également remarquer que les premiers caractères ajoutés forment la chaîne base64:/9j/, qui correspond aux premiers caractères d’une image en base64. Les deux fichiers faisant plus de 6700 lignes, il est évident qu’il faudra utiliser un outil pour extraire le message du fichier intercepté.

HbEALTH ASPECTS
aOF
CsHEMIeCAL6 AND4 BIOLOGICAL:
W/EAPONS
Rep9ortj of a /WHO Group4 of CoAnsultants

Extrait du message intercepté

Un peu effrayé par la taille des fichiers, j’ai perdu beaucoup de temps à essayer de trouver une solution de stéganographie déjà existante. C’est en discutant avec des amis que nous avons identifié une solution extrêmement simple. En supprimant l’ensemble des retours à la ligne et espaces, nous aurions deux longues chaînes de caractères, dont il ne nous resterait qu’à extraire les caractères différents. 

Trente secondes et 17 lignes de code plus tard, nous obtenions un script python qui nous permettait d’intégrer le résultat dans une page html pour pouvoir le visualiser avec un navigateur.

Script d’extraction des données ajoutées

Pour être compatible avec la balise image, on modifie juste un caractère et on obtient une image de recrutement pour Evil gouv, avec une url.

Message de recrutement d’Evil Gouv

On peut donc accéder à l’url suivante : https://www.challengecybersec.fr/22caeee05cb8b2a49133be134a5e9432, qui nous amène sur une page contenant une épreuve de recrutement pour Evil Gouv.

Gestion d’entrepôts de stockage

Exercice de recrutement

La page nous demande de télécharger une archive, dans laquelle se trouve un pdf qui nous explique le problème rencontré. Evil Gouv a besoin de stocker en lieu sûr des objets de différentes valeurs. Mais les agents souhaiteraient que la valeur totale des objets atteigne des objectifs précis pour chaque entrepôt. 

Pour chaque entrepôt, on nous donne un fichier contenant l’objectif de valeurs et les valeurs de chacun des objets possibles à stocker (nous ne sommes pas obligés de stocker tous les objets mais seulement ceux dont la somme des valeurs s’approche le plus possible en dessous de notre objectif). En retour, nous devons renvoyer le nombre d’objets à stocker et leur index dans la liste. 

Exemple:

Pour l’entrepôt avec ces caractéristiques (objectif 40, 4 objets de valeurs allant de 8 à 20):

40
20 10 11 8

On renverra la solution (3 objets à stocker puisque l’objet à la position 0 = 20 + l’objet à la position 2 = 11 +  l’objet à la position 3 = 8= 39):

3
0 2 3

Ces calculs sont relativement simples dans le cas d’une petite liste d’objets, puisqu’il est facile de calculer toutes les possibilités de valeur finale. Mais nous avons quatre entrepôts à remplir, du petit au très grand. 

Entrepot A (20 objets disponibles):

200 
92 86 16 20 48 85 49 73 94 10 39 51 85 68 54 83 10 92 60 49

Entrepot B (75 objets disponibles):

7000
27 145 92 [...] 51 95 101 57

Entrepot C (15000 objets disponibles):

1000000000
93081 25924 15680 [...] 123029 26959 48394

Entrepot D (3000 objets disponibles):

2000000000
1517833 413371 1152828 [...] 694753 1576490 379289

La difficulté ici ne réside pas dans la résolution du problème pour les deux premiers entrepôts, mais pour les deux derniers entrepôts, en raison de la grande valeur de l’objectif, mais aussi des objets à stocker.

Après avoir tenté d’implémenter plusieurs algorithmes avec des amis, nous avons identifié que ce problème était en fait l’équivalent d’un problème mathématique connu, le problème du sac à dos. Dans ce problème, on cherche à savoir quoi mettre dans son sac à dos pour une randonnée, en fonction du poids des objets et de leur utilité. De nombreuses solutions existent pour résoudre ce problème, mais bon nombre d’entre elles sont adaptées à des petites valeurs, et ne le font pas de manière optimisée. 

Le problème du sac à dos

Le résultat de ces essais était finalement assez constant, un pc qui prend feu au bout de 2 minutes, et l’OOM killer qui tue le processus Python. 

32Go consommés par python

Après un grand nombre d’essais, à la limite du désespoir et prêt à me replonger dans le défi hardware et crypto, c’est au fil d’une discussion avec Pierre-Yves qu’il me conseille de regarder du côté d’un petit éditeur de logiciel, qui propose une suite de solutions à des problèmes classiques d’optimisation combinatoire. Direction donc le site d’OR-Tools de Google pour y chercher une éventuelle implémentation d’une solution au problème du sac à dos. La solution est bien disponible ici, il ne reste donc plus qu’à adapter quelques paramètres et à l’essayer. 

Script d’exploitation de la solution de Google

On adapte la valeur, le poids, l’objectif, on lance le script et 5 secondes plus tard on obtient le résultat, pour les quatre entrepôts. Après quelques jours d’essais via des codes récupérés un peu partout, je suis vraiment sidéré du résultat, et surtout de ne pas avoir trouvé le site avant. On peut envoyer nos résultats, et nous obtenons la page suivante, qui contient un lien pour le prochain défi :

Résultats obtenus

La page sur laquelle nous sommes redirigés est une page d’un blog, The Evil Newspaper.

Récupération de données d’un site internet

Page d’accueil de The Evil Newspaper

Sur cette page, on nous décrit les consignes d’un nouvel exercice de recrutement. L’objectif est de retrouver sur chacun des articles du blog un champ message-digest, et de concaténer ces champs pour l’ensemble des articles du blog, avant de les passer dans un hash md5. 

Exemple d’article

Chaque article possède un identifiant, facilement identifiable dans l’URL (ex: blog/post/978 pour le post 978). Il ne nous reste donc plus qu’à trouver l’article avec l’identifiant le plus haut, et de faire un petit script nous permettant de récupérer le message-digest sur chaque post de blog de 1 jusqu’au dernier identifiant. Le dernier post de blog répondant sur l’id 1000, on peut rédiger le script en python.

Script de récupération de tous les posts

Ce script va accéder à chaque page, chercher dans le code source de la page le contenu du champ message-digest (l’identifiant de la balise est partial-proof), puis écrire les messages obtenus les uns à la suite des autres. Il ne reste plus qu’à encoder le résultat en md5, et on trouve la valeur suivante: 1410e53b7550c466c76fc7268a8160ae

La valeur ressemblant aux URL que nous obtenons depuis le début du challenge, on accède à la page https://challengecybersec.fr/1410e53b7550c466c76fc7268a8160ae

Rétro-ingénierie

Page d’accueil des archives classifiées d’Evil Gouv

Cette page est une nouvelle fois une page très simple, avec un champ utilisateur, un champ de mot de passe, et un bouton. En inspectant le code source, on trouve un fichier javascript appelé login.js. Les autres fichiers n’étant à première vue pas particuliers, nous allons concentrer nos efforts sur le script de login, dans l’espoir d’y trouver une faille.

Le javascript contenant la fonction de login est de la forme de l’extrait suivant: 

var _0x5f46=['\x37\x3c\x30\x6c\x3c\x6e\x69\x30\x33\x3c\x6c\x3c\x6c\x3c\x [...] 1>0x7e){}}return _0x4b4483[_0x19fd('\x30\x78\x35')]('');}

C’est un code de 3300 caractères, sur une seule ligne. Il semble avoir été obfusqué (technique permettant notamment de rendre un code simple illisible, en rendant aléatoire les noms de variables, de fonctions, et en injectant du code inutile), ce qui va grandement ralentir son analyse et sa rétro-ingénierie.
Il existe de nombreux sites permettant de formater un script sous une forme plus lisible, tels que JS Nice. Une fois le code formaté, on peut passer à l’identification de son point d’entrée (la fonction lancée lorsque l’on appuie sur le bouton “se connecter”). 

Extrait du code source de la page d’accueil

Pour cela, on revient sur le code source de la page, et on extrait la partie contenant les champs de login et de mot de passe. Un clic sur le bouton “Se connecter” lance la fonction “DisplayValidity(‘inText’)”. Cette fonction utilise le nom d’utilisateur (inText) pour vérifier si nous pouvons nous connecter. Coup de chance, nous n’avons donc pas besoin de trouver de mot de passe, puisque celui n’est pas utilisé ! Dans la fonction de validation, le login est ensuite envoyé à une fonction appelée “_0x10dbec”. Si cette fonction retourne un résultat différent de 0, nous serons redirigés vers une URL contenant notre login. 

Nous devons donc réussir à faire en sorte que le login soit de la forme d’une URL, et que notre fonction retourne autre chose que 0, et nous serons connectés ! La fonction _0x10dbec est bien présente dans le fichier javascript obfusqué, c’est donc notre point d’entrée pour la rétro-ingénierie. 

Fonction d’entrée dans le JS obfusqué

La première étape que j’ai identifiée était de vérifier que tout le code que j’avais sous les yeux était bien utilisé lors de notre login. Rien ne serait plus frustrant que de se faire des nœuds au cerveau pour essayer de comprendre un code qui ne sert même pas. Pour ce premier niveau d’étude, partant du principe que les fonctions appelées le sont par leur nom et pas par une composition de plusieurs variables, comme décrit ici, je vais juste prendre chaque nom de fonction et de variables, et vérifier qu’il est présent au moins deux fois dans mon fichier. Si ce n’est pas le cas, ça veut très probablement dire que la fonction n’est pas utilisée, et que je peux donc la supprimer. 

Exemple de fonction présent dans le code

Cette première étape est un succès, le code passe de 127 lignes à 94 ! J’en profite dans la foulée pour remplacer tous les appels à des valeurs dans des tableaux, et les appels à la fonction _0x19fd, qui n’est pas particulièrement longue, mais dont le comportement est facile à analyser. Un seul paramètre des deux possibles est utilisé dans la fonction, auquel on soustrait zéro (cette opération représente sûrement du bruit ajouté par l’obfuscation). Puis, on retourne la valeur du tableau _0x5f46 à la position passée en argument. Cette fonction n’est donc juste qu’un appel à un tableau comme les autres. Pour le vérifier, nous pouvons utiliser la plateforme de bac à sable Playcode, dans laquelle nous allons tester le code.

Code d’origine
Code simplifié

Une fois toutes ces variables remplacées, la lecture est suffisamment simple pour attaquer la rétro-ingénierie. On reprend donc la fonction d’entrée désormais bien simplifiée.

Fonction d’entrée simplifiée

Pour obtenir un login correct, et que la fonction ne retourne pas 0, il faut que notre login passé dans les fonctions func1 puis func2 soit égal à la chaîne de caractères river. Nous allons donc commencer par essayer de partir de la fin de func2, avec le résultat river, pour remonter au résultat attendu par func1. 

Fonction 2

Pour inverser une fonction, on part du résultat, et on remonte dans la fonction en inversant chaque bloc un par un. Dans le sens inverse, la fonction joint tous les caractères dans un tableau pour former le résultat que nous attendons, et dans la boucle précédente, une opération binaire est réalisée sur l’entrée de la fonction, puis la valeur binaire obtenue est convertie en caractères et insérée dans le tableau de résultat. Pour inverser cette fonction, il suffit donc juste d’inverser le calcul bit à bit. On a le calcul suivant:

binary = randomized_login XOR random_string AND 15

Le AND 15 est calculé d’abord (précédence des opérateurs), et a pour effet de ne conserver que les 4 derniers bits de random_string. L’inverse d’une opération XOR est un XOR, ce qui nous donne pour la fonction le code suivant.

Reverse de la fonction 2

Le résultat obtenu, nous pouvons effectuer la même opération pour func1.

Fonction 1

Dans le sens inverse, cette fonction joint une nouvelle fois tous les caractères dans un tableau pour former le résultat que nous avons trouvé précédemment. Puis elle utilise le tableau random_array qui contient 40 nombres comme des indices pour un tableau de caractères. Finalement, si le login que nous indiquons fait moins de 40 caractères, elle insère des caractères depuis le random_string. Rien de très complexe, il nous suffit d’inverser le mapping fait par random_array et de voir si nous avons à la fin du résultat des caractères de random_string pour les supprimer.

Reverse de la fonction 2

Bingo, nous avons un résultat qui ressemble bien aux URL qui valident le challenge depuis le début. Retour à la page de login pour tester notre découverte…

Identifiant correct

C’est un succès, nous avons trouvé le login permettant d’aller plus loin, le navigateur nous redirige donc sur l’URL, mais c’est une 404. Deux solutions sont alors possibles, cette page 404 est notre prochaine épreuve, ou alors nous n’avons pas trouvé la bonne solution. Après une analyse de la page qui renvoie un 404, c’est bien une erreur et pas notre prochain challenge. Un peu dépité, je décide de me lancer dans du débug sur le code inversé. Si la solution trouvée n’est pas bonne, comment l’identifiant peut-il être valide ?

Erreur de redirection

J’accuse immédiatement l’opération bit à bit. Mes cours de logique binaire remontent à quelques années maintenant, j’ai sûrement manqué quelque chose. Après de nombreuses simulations sur la plateforme de bac à sable Javascript, j’en arrive à l’évidence que j’ai encore une bonne mémoire de mes cours, et que je ne me suis pas trompé. La nuit déjà bien entamée, avec le bon espoir que celle-ci me porte conseil, je décide de m’arrêter là pour mieux me relancer le lendemain.  

Finalement, c’est dans une discussion avec Pierre-Yves qui, après m’avoir confirmé une nouvelle fois que mon inversion de l’opération bit à bit est bonne, me fait comprendre que je n’ai peut-être pas LA bonne réponse pour la fonction de login, mais UNE solution parmi plusieurs. En revenant sur la fonction func1, nous avions identifié un tableau qui réordonnait les caractères du mot de passe transmis pour l’utiliser ensuite.

Tableau de positions

Dans ce tableau, les index 0 et 4 sont répétés deux fois, ce qui signifie que lorsque nous avons inversé la fonction, nous avons inséré deux fois les valeurs aux index 0 et 4. Dans le sens normal de la fonction, cela veut dire que les caractères 0 et 4 sont totalement ignorés, ce qui veut dire que pour trouver le lien suivant, nous devons réaliser un petit script qui va essayer toutes les possibilités pour nous jusqu’à trouver une page qui réponde. 

Script de Bruteforce

Pour les caractères inconnus, en partant des URLs précédentes nous utiliserons l’alphabet en minuscule et des chiffres, ce qui donne un total de 362 possibilités. On lance le script, et ce dernier testera plus de 88% des possibilités avant d’obtenir le résultat : 5f3949527e73ad93b73b070bb12cde1292bbcde5

Nous sommes redirigés sur une page contenant des archives d’opération d’Evil Gouv.  

Archives d’Evil Gouv

On y trouve des informations sur 3 opérations passées:

Opération Maléfice
Opération Machination
Opération Diablerie

Parmi les nombreuses informations disponibles, on apprend que les méchants sont des méchants, et qu’ils aiment les cornichons. Rien de bien utile dans ces deux premières archives, mais l’Opération Diablerie nous apporte des informations sur une opération à venir à la fin du mois de novembre. Nous y trouvons également un lien pour accéder à l’étape suivante, une plateforme de renseignement, qui s’avère être l’arène du CTF Brigitte FRIANG.

Accueil de la page de l’opération Diablerie

Comment limiter/éviter ces attaques ? 

Blaise Pascal – Service Algo
Etape 1:

La solution utilisée pour transmettre le message est assez évidente, rendant ce message peu difficile à récupérer. Transmettre un message dans une image serait beaucoup plus efficace et discret.

Etape 3:
Le comportement d’un utilisateur normal sur un blog est de lire un article avant de naviguer vers le suivant. Un utilisateur ne fera donc que rarement appel à plusieurs pages de blog. Il existe de nombreuses techniques qui permettent de mettre en place une limitation de requêtes sur un serveur Web, empêchant la récupération du blog automatique rapide, tel que le Rate Limiting sur Nginx.

Etape 4:
L’authentification sur le site est gérée par des fonctions dans un javascript côté client. Quel que soit le niveau d’obfuscation, il sera toujours possible pour un utilisateur malveillant de retrouver le fonctionnement de l’authentification et de forger des identifiants valides. Le site ne vérifie que le login, et pas le mot de passe, simplifiant encore plus le travail de recherche d’identifiants valides.
Afin de rendre la recherche d’identifiants valides beaucoup plus complexe, voire impossible, il aurait été préférable de traiter l’authentification côté serveur. Dans ce cas, les identifiants entrés sont envoyés au serveur, validés par ce dernier, et c’est cette validation qui est renvoyée au client.

Si vous avez des questions sur cet article, ou que vous souhaitez plus de détails techniques sur l’un des sujets abordés, n’hésitez pas à me contacter sur Twitter @Alibert_m.

Catégories : Sécurité

Mickael Alibert

Practice leader - Lead SRE