NDH2k11 Prequals - Crypto300
By ymvunjq on Wednesday, April 6 2011, 20:00 :: Prequals ndh2k11 :: Permalink
La crypto300 est sous la forme d'un ensemble de fichiers python :
crypto300> ls braid.py client.py flag.py network.py peer.py server.py
La première partie de cette épreuve consiste donc à comprendre que font ces fichiers. Heureusement le code est clair et bien écrit.
- braid.py : Ce fichier est en réalité le coeur de l'épreuve dans lequel toutes les opérations cryptographiques sont faites. Le cryptosystème fonctionne à base de permutations.
- client.py : Ce fichier contient le code du client (nous) qui va se connecter au serveur.
- server.py : Ce fichier contient le code du serveur hébergé sur les machines de la ndh, sur lequel on va se connecter.
- flag.py : Ce fichier contient un fake flag, le saint graal pour nous... celui qui sera envoyé par le serveur, enfin on espère.
- network.py : Ce fichier contient des classes implémentant l'accès bas niveau au réseau.
- peer.py : Ce fichier contient des classes implémentant un transfert sécurisé des informations.
Une première exécution du client nous donne le résultat suivant :
crypto300> python client.py [Crypto300 sample client] [i] Welcome on Goofyleaks. Can I haz ur public kay ? [+] Your leaked flag: ##NOT ALLOWED##
Ca commence mal, on a pas le droit d'accéder au flag. Une analyse plus fine du code de network.py et notamment de la fonction run de ServerWorker nous donne un peu plus d'informations sur ce qui se passe réellement :
...
if str2hex(pubkey) in self.allowed_pubkeys:
# authenticated ! (public key is allowed)
enc_data = self.peer.encryptData(GOODBOY_FLAG)
self.__send(enc_data)
else:
# well, public key is not allowed, send error
enc_data = self.peer.encryptData('##NOT ALLOWED##')
...
Si on a pas la bonne clé publique, on aura pas le bon flag. La bonne clé se trouve dans le fichier server.py :
...
# bind peer instance to a socket (and set up a single allowed public key)
self.s = ServerSocket(peer,allowed_pubkeys=['0F0C11040108060B05150E1000090A030D1312140207'])
...
On commence à comprendre un peu plus le problème... On sait quelle est la clé publique, mais il va falloir trouver la clé privée correspondant à cette clé publique.
Une analyse plus détaillée des fichiers nous permet de bien comprendre la communication qui est faite entre le client et le serveur :
| CLIENT | SERVEUR |
|---|---|
| Connection | |
| Envoi MOTD | |
| Réception MOTD | |
| Envoi Clé Publique Serveur | |
| Réception Clé Publique Serveur | |
| Envoi Clé Publique Client | |
| Réception clé Publique client | |
| Génération secret partagé avec clé publique serveur | Génération secret partagé avec clé publique client |
| Si bonne cle publique chiffre bon flag flag, sinon chiffre mauvais flag | |
| Envoi flag chiffré | |
| Réception flag chiffré | |
| Déchiffrement flag chiffré |
Le secret partagé est en réalité un sha1 obtenu à partir d'opérations sur la clé publique de l'interlocuteur et d'une valeur commune entre le client et le serveur 0D1214040108060F050C0E0207030A151009000B1311 que l'on peut trouver client.py et server.py. Ce sha1 sert comme clé à l'algorithme blowfish pour chiffrer le flag envoyé par le serveur. Bien évidemment, la génération du secret partagé aboutit à la même valeur chez le client et chez le serveur s'ils possèdent les clés privées correspondantes aux clés publiques envoyées. Ici impossible de faire quoique ce soit, la vulnérabilité n'est pas dans le chiffrement du flag, mais dans l'obtention de la clé permettant ce chiffrement.
La génération de la clé privée et de la clé publique chez le client et chez le serveur se font à partir de la même valeur commune citée plus haut. La dérivation se fait dans braid.py dans le constructeur de la classe BraidKey. La clé privée est d'abord initialisée à la valeur 000102030405060708090A0B0C0D0E0F101112131415 (soit 22 octets différents) puis les 11 premiers octets sont mélangés pour le serveur et les 11 derniers pour le client. La clé publique est ensuite obtenue à partir de cette clé privée, à partir de deux méthodes : combine et reverse.
Il y a donc 11! clés privées différentes (11 octets fixes puis 11 octets mélangés) soit 39916800. A la vue des writeup déjà écrits sur cette épreuve, la majorité des teams se sont arrêtés là et ont bruteforcé l'ensemble des 11! clés possibles. Malheureusement mon PC ayant déjà trop chauffé durant la crypto200, nous avons essayé de réduire l'espace de recherche pour simplifier le calcul.
Pour cela il faut bien comprendre les méthodes combine et reverse.
- combine
La méthode combine mélange un tableau en fonction d'un autre.
Tableau 1
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 0B | 0C | 0D | 0E | 0F | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 15 | 14 | 13 | 12 | 11 | 10 | 0F | 0E | 0D | 0C | 0B | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A |
Tableau 2
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 0B | 0C | 0D | 0E | 0F | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 15 | 14 | 13 | 12 | 11 | 10 | 0F | 0E | 0D | 0C | 0B |
La méthode Tab1.combine(Tab2) donne :
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 0B | 0C | 0D | 0E | 0F | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0B | 0C | 0D | 0E | 0F | 10 | 11 | 12 | 13 | 14 | 15 | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A |
On prend l'élément 0 de tab1 (ici 0x15) et on prend le 0x15 ème élément de tab2 (donc B), qui deviendra l'élément 0 de Tab1.combine(Tab2).
- reverse
La méthode reverse retourne un tableau par rapport à ses indices :
Tab1.reverse() donne :
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 0B | 0C | 0D | 0E | 0F | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0A | 09 | 08 | 07 | 06 | 05 | 04 | 03 | 02 | 01 | 00 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 0F | 0E | 0D | 0C | 0B |
On prend l'élément 0 de tab1 (ici 0x15) et 0x15 ème élément devient donc l'élément 0 de Tab1.reverse().
Maintenant que ces deux méthodes sont bien assimilées regardons comment les éléments s'enchainent pour obtenir la publique en fonction de la clé privé et surtout regardons s'il est possible de reverser le processus.
L'analyse (simplifiée) de braid.py donne ça :
Priv = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]; Priv.shuffle(offset/2); # La deuxième partie de Priv est mélangée PrivR = Priv.reverse(); T = K.combine(PrivR); Pub = Priv.combine(T);
K est la valeur connue citée plus haut, et T ne se trouve pas dans le programme mais je l'ajoute pour simplifier le calcul. On connait Pub (la clé que l'on doit envoyer au serveur pour qu'il nous envoie le bon flag), on connait K, et on connait la moitié de Priv.
Posons ce que l'on connait :
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 0B | 0C | 0D | 0E | 0F | 10 | 11 | 12 | 13 | 14 | 15 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | |
| ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | |
| 0D | 12 | 14 | 04 | 01 | 08 | 06 | 0F | 05 | 0C | 0E | 02 | 07 | 03 | 0A | 15 | 10 | 09 | 00 | 0B | 13 | 11 | |
| ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | |
| 0F | 0C | 11 | 04 | 01 | 08 | 06 | 0B | 05 | 15 | 0E | 10 | 00 | 09 | 0A | 03 | 0D | 13 | 12 | 14 | 02 | 07 |
Dire que l'on ne connait pas pas PrivR est faux. On en connait au moins une partie. Etant donné que PrivR = Priv.reverse(), on connait la première moitié de PrivR, elle est égale à la première moitié de Priv (seulement car les 11 premiers octets de Priv sont 0,1,2,3,4,5,6,7,8,9,10,11).
De la même façon, on connait une partie de T car Pub = Priv.combine(T). Donc la première moitié de T est égale à Pub encore une fois seulement car les 11 premiers octets de Priv sont 0,1,2,3,4,5,6,7,8,9,10,11).
On met à jour notre tableau :
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 0B | 0C | 0D | 0E | 0F | 10 | 11 | 12 | 13 | 14 | 15 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | |
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | |
| 0D | 12 | 14 | 04 | 01 | 08 | 06 | 0F | 05 | 0C | 0E | 02 | 07 | 03 | 0A | 15 | 10 | 09 | 00 | 0B | 13 | 11 | |
| 0F | 0C | 11 | 04 | 01 | 08 | 06 | 0B | 05 | 15 | 0E | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | |
| 0F | 0C | 11 | 04 | 01 | 08 | 06 | 0B | 05 | 15 | 0E | 10 | 00 | 09 | 0A | 03 | 0D | 13 | 12 | 14 | 02 | 07 |
On sait que T = K.combine(PrivR), donc on peut essayer de voir si certains octets de PrivR peuvent être découverts.
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 0B | 0C | 0D | 0E | 0F | 10 | 11 | 12 | 13 | 14 | 15 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | |
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | ? | 15 | 0F | 0E | 0B | ? | ? | 0C | ? | 11 | ? | |
| 0D | 12 | 14 | 04 | 01 | 08 | 06 | 0F | 05 | 0C | 0E | 02 | 07 | 03 | 0A | 15 | 10 | 09 | 00 | 0B | 13 | 11 | |
| 0F | 0C | 11 | 04 | 01 | 08 | 06 | 0B | 05 | 15 | 0E | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | |
| 0F | 0C | 11 | 04 | 01 | 08 | 06 | 0B | 05 | 15 | 0E | 10 | 00 | 09 | 0A | 03 | 0D | 13 | 12 | 14 | 02 | 07 |
Vu que PrivR = Priv.reverse() on finit par découvrir certains octets de Priv.
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 0B | 0C | 0D | 0E | 0F | 10 | 11 | 12 | 13 | 14 | 15 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 0F | 12 | ? | 0E | 0D | ? | 14 | ? | ? | ? | 0C | |
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | ? | 15 | 0F | 0E | 0B | ? | ? | 0C | ? | 11 | ? | |
| 0D | 12 | 14 | 04 | 01 | 08 | 06 | 0F | 05 | 0C | 0E | 02 | 07 | 03 | 0A | 15 | 10 | 09 | 00 | 0B | 13 | 11 | |
| 0F | 0C | 11 | 04 | 01 | 08 | 06 | 0B | 05 | 15 | 0E | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | |
| 0F | 0C | 11 | 04 | 01 | 08 | 06 | 0B | 05 | 15 | 0E | 10 | 00 | 09 | 0A | 03 | 0D | 13 | 12 | 14 | 02 | 07 |
Il ne reste donc plus que 5 octets inconnus dans la clé privée, on a donc diminué l'espace de recherche de 11! à 5! soit 120. Il suffit de tester ces 120 possibilités et de voir celles qui respectent le padding PKCS5 fait lors de l'envoi du flag. Une seule réponse correspond pour la clé 000102030405060708090a0f12110e0d151410130b0c qui nous permet de déchiffrer le flag : Br4iDCrypto_i5_b3au7ifu11
