Retrouvez cet article dans : Misc 17
Junior était relativement fier de son code, cependant dans un sursaut d'intelligence et d'humilité, il décida de le faire relire à quelques connaissances : un collègue développeur, un architecte senior et enfin un cryptologue en herbe (au passage, notons que des relectures minutieuses de code sont généralement très productives : on estime que l'on peut passer de 1 bug toutes les 55 lignes à 1 bug toutes les 10000 lignes [CC++, Reas03]).
Voici les résultats obtenus.
Ce que le développeur a trouvé
Commençons par les erreurs trouvées par le collègue de Junior :
|
|
-----Message d'origine-----
De : Modeste Collegue
Envoyé : vendredi 11 juin 2004 16:10
À : Junior
Objet : Ton code est pourri
Salut Junior,
J'ai jeté un rapide coup d’œil à ton code.
Y a tt de même des problèmes. Et notamment, tu
es bien trop confiant. Tu devrais regarder
- la RAM
- la fonction buildMsgAuth de chapclient.c() et
 doCheck() de chapserver.c
- tester tes allocations mémoire |
Examen de la RAM
Effectivement, tous les mots de passe (
passwd dans
chapclient.c, et
gDB dans
chapserver.c) sont conservés en mémoire sans être chiffrés. Sous Windows, un attaquant disposant des droits " debug " peut les retrouver en parcourant la mémoire avec un outil tel que WinHex
[WinHex] (voir Figure 1). Ceci est également faisable sous Unix, en utilisant ptrace par exemple
[MISC14].
On y retrouve sans peine l'identifiant 'pigeon' et le mot de passe 'secpass'. Au moins pour la base de données des utilisateurs (
gDb), il aurait été judicieux de ne pas mémoriser des mots de passe en clair, mais plutôt des hashés. Bien entendu, coder les mots de passe " en dur " dans le code est également à proscrire. Par exemple, une ligne de code telle que
strcpy(gDb[0].passwd,"secpass");
serait un trou de sécurité encore plus évident : par examen du binaire (avec la commande 'strings' par exemple), un attaquant (sans droits particuliers) retrouverait immédiatement le mot de passe 'secpass'.
De plus, même avec un hashé du mot de passe, rien ne sert de le conserver en mémoire. Par exemple, s'il se retrouve dans un core dump, un attaquant patient avec un bon dictionnaire saura quoi en faire. Ainsi, une fois que le hashé n'est plus utile, " nettoyer " la mémoire, par exemple avec la fonction bzero().
Integer Overflow
Junior a oublié d'examiner les cas où le client ou le serveur CHAP sont malicieux. Dans le code source de chapclient.c (fonction
buildMsgAuth, lignes 10-15), les lignes suivantes posent problème :
|
|
 len_challenge = strlen(challenge);
 todigest = (char*) malloc(len_passwd+len_challenge);
 memcpy(todigest,passwd,len_passwd);
 memcpy(todigest+len_passwd,challenge,len_challenge); |
En effet, le code source suppose que le challenge renvoyé par le serveur est une chaîne (par 'chaîne', on entend un buffer de caractères terminés par '\0') – en passant, ceci n'est d'ailleurs pas une très bonne idée. Mais que se passe-t-il si on est connecté à un serveur malicieux (ou buggé) qui n'envoie pas une chaîne ?
strlen(challenge) peut alors être potentiellement beaucoup plus grand que les 30 caractères prévus. Si len_passwd + len_challenge dépasse la taille maximale d'un integer, on se trouve alors face à un integer overflow [Phra02], et todigest peut n'avoir que très peu d'espace alloué. Les memcpy qui suivent écriront alors dans des zones de mémoire non allouées. Le même problème potentiel se retrouve dans le code source de chapserver.c (fonction doCheck, lignes 15-20) :
|
|
len_passwd = strlen(gDb[index].passwd); |
Cette fois-ci, on suppose que le mot de passe de l'utilisateur sera une chaîne, mais si la base de données des utilisateurs est vérolée, on se trouve confronté à un integer overflow.
Plus de mémoire disponible
Junior a également oublié de tester ses allocations mémoire. Que se passe-t-il s'il n'y a plus d'espace mémoire ? Par exemple, à la ligne 17 de la fonction doCheck dans chapserver.c, l'allocation du buffer n'est pas testée :
|
|
    todigest = (char*) malloc(len_passwd+len_challenge); |
Donc, en cas de pénurie, un pointeur retourné sera NULL, et à la première utilisation de todigest l'application plantera.
Il s'agit là avant tout d'une erreur de programmation, mais un attaquant peut s'en servir pour faire planter l'application : il remplit l'espace mémoire exprès, et provoque ainsi une erreur du client ou du serveur CHAP. De telles failles ont déjà été exploitées dans d'autres systèmes, voir par exemple [VFS99].
Ce que l'architecte a trouvé
Quant à l'architecte, il appela immédiatement Junior au bout du fil : " Junior ? Oui, j'ai vu ton code. Passe me voir, j'ai trouvé quelques petits points... ".
Pas d'authentification du serveur
On constate en effet que le client s'authentifie auprès du serveur mais pas réciproquement. Ainsi, l'utilisateur n'a aucune garantie de bien dialoguer avec un serveur honnête. Dans certains cas, pour privilégier la simplicité, ceci peut être acceptable.
Cependant, dans la majorité des cas, il faut faire attention aux conséquences : que se passe-t-il si le serveur envoie des paquets complètement erronés ? Aussi, que se passe-t-il si les messages du serveur ne parviennent pas au client ? Si les primitives de communication sendMsg et recvMsg n'implémentent pas de timeout, on risque fort d'attendre à l'infini un défi du serveur…
Trop bavard
Le serveur d'authentification de Junior est trop bavard. En particulier, la fonction lookupName() dans chapserver.c affichera " utilisateur introuvable " si l'identifiant saisi est inconnu, mais rien du tout (si ce n'est "Authentification: REFUSE" dans doCheck) si le mot de passe est incorrect. Cette différence donne inutilement trop d'informations à l'attaquant. Il aurait fallu afficher seulement
"Authentification: REFUSE" dans tous les cas. Ainsi, l'attaquant n'aurait pas pu savoir si l'erreur provenait du mot de passe ou de l'identifiant.
Au passage, nous signalons également que pour une sécurité robuste, il faut également faire attention à la rapidité d'exécution de l'algorithme de validation de l'authentification. Si réaliser qu'on a un mauvais identifiant prend 0,001 seconde à l'algorithme, mais 0,005 s pour un mauvais mot de passe, on est alors sujet à des " timing attacks " (voir [MISC6]).
Ce que le cryptologue a trouvé
Pour sa part, le cryptologue remonta aimablement les erreurs suivantes à Junior, avec une petite annotation tout de même " je donne des cours de cryptologie à l'Université, si ça vous intéresse... ".
MD5 n'est pas un MAC
MD5 est une fonction de hashage, mais dans le code source de Junior elle est utilisée avec une clé, en tant que MAC (Message Authentication Code). Très exactement, la réponse au défi d'authentification est le calcul :
|
|
MD5(mot de passe || texte), || signifiant la concaténation |
Or, d'une manière générale, il est risqué d'utiliser une fonction de hashage en guise de MAC avec tout simplement la clé en préfixe (voir [HAC, §9.64]).
En effet, il est alors possible sous certaines conditions de calculer un hashé valide sans avoir la clé. Si on considère qu'une fonction de hashage H est constituée d'une fonction de compression f utilisée en boucle alors à l'étape i, le hashé de x est : hi=f(hi-1,x))
Donc si l'on connaît
R=H(mot de passe || texte), et si texte'=texte || ajout, alors on a :
|
|
R'= H(mot de passe || texte') = H(mot de passe || texte || ajout)
= f(H(mot de passe || texte), ajout) =f(R,ajout). |
On calcule donc R' à l'aide de R et de " ajout ", sans avoir besoin du mot de passe. H est une fonction de hashage valide, mais un très mauvais
MAC.
Notons tout de même au passage que cette démonstration est simplifiée. En effet, MD5 n'est pas une simple fonction de compression utilisée en boucle : au préalable, le texte est paddé. Ce qu'il faut essentiellement retenir de la démonstration, c'est que les conditions pour faire une bonne fonction de hashage et un
MAC ne sont pas les mêmes. En conséquence, il aurait été plus sûr d'utiliser un algorithme tel que
HMAC. Plus d'informations sont disponibles dans
[HAC].
MD5 n'est pas " recommandé "
Junior utilise MD5, or cela fait des années qu'il n'est plus " recommandé " d'utiliser cette fonction. Plus exactement, des travaux de Dobbertin ont prouvé que la fonction de compression de MD5 était sujette à des collisions (on rappelle qu'on parle de " collision " lorsque deux textes initiaux t et t' donnent le même hashé – ce qui est bien entendu à éviter ! ). La démonstration a même été faite par l'exemple en moins de 10 heures sur un PC de 1996 [Rob96]. Il faut cependant nuancer cette trouvaille : on avait trouvé des collisions sur la fonction de compression mais pas sur MD5 dans son intégralité. Les cryptologues ne pouvaient donc pas dire que MD5 était " cassé ", mais juste pré-dire qu'il le serait dans un avenir proche. La prédiction s'est révélée justifiée, et tout récemment, lors de la conférence Crypto'2004, des chercheurs ont réussi à trouver des collisions sur MD5 même [WFLY04]. Dans la pratique, il resterait encore à exploiter cette faille (par exemple générer exprès des collisions pour tout texte), mais quoi qu'il en soit, la sécurité de MD5 est sérieusement compromise. Pourquoi s'embarrasser d'un tel algorithme, alors qu'il en existe d'autres bien plus sûrs tels que SHA-1, SHA-256, SHA-512 etc. ?
Un mot de passe n'est pas une clé sûre
Le mot de passe entré par l'utilisateur est directement utilisé en tant que clé. Or, d'une manière globale, la solidité des algorithmes de cryptographie est basée sur la difficulté à trouver cette clé. Hélas, un mot de passe a bien moins d'entropie qu'une véritable clé " aléatoire ", et offre donc une sécurité amoindrie. Il est donc fortement " conseillé " d'utiliser des mécanismes tels que ceux décrits dans [PKCS#5] pour utiliser un mot de passe utilisateur en tant que clé cryptographique.
Par ailleurs, un mot de passe de 8 caractères est généralement, hélas, d'une solidité douteuse. Le nombre de bits d'une clé se calcule par la formule log2(nm), où n est la taille du jeu de caractères utilisables et m la taille du mot de passe. Pour un mot de passe de 8 caractères utiles (on rappelle que dans le code de Junior le 9ème est forcément le caractère '\0'), si l'utilisateur se sert d'un jeu de 36 caractères alphanumériques et 10 caractères spéciaux (ce qui est déjà optimiste pour la majeure partie des utilisateurs), on obtient approximativement une clé de 44 bits. En guise de comparaison, DES utilise des clés de 56 bits et AES utilise au minimum 128 bits…
Le défi n'est pas suffisamment aléatoire
Le protocole d'authentification par défi repose sur une génération aléatoire du défi. Si l'attaquant peut deviner le futur défi, cela perd de son intérêt. Or utiliser directement l'heure en tant qu'aléa n'est franchement pas très sérieux. Dans l'implémentation de Junior, le défi n'est modifié que toutes les minutes (voir la fonction buildMsgChallenge de chapserver.c). Il est fort possible d'effectuer un rejeu durant cette période là .
Junior aurait au moins pu utiliser le générateur pseudo-aléatoire fourni dans OpenSSL (fonctions RAND_xxx). Attention à ne pas oublier de l'initialiser avec des données aléatoires (RAND_add).
Remerciements
Je remercie tout particulièrement Philippe Biondi, Christophe Grenier et Frédéric Raynal pour leurs suggestions et remarques.
Références
- [CC++] Anonyme, C and C++ are obsolete, papier n°2, Mars 2003, http://www.cs.rutgers.edu/~rmartin/teaching/spring03/cs553/papers01/
- [HAC] A. Menezes, P. van Oorschot, S. Vanstone, Handbook of Applied Cryptography, CRC Press, ISBN 0-8493-8523-7, Octobre 1996.
- [HMAC] H. Krawczyk, M. Bellare, R. Canetti, HMAC: Keyed-Hashing for Message Authentication, RFC 2104, Février 1997, http://www.faqs.org/rfcs/rfc2104.html
- [MISC6] G. Bart, " Récupérez votre code PIN ou une clé RSA avec un chronomètre ", MISC n°6, mars/avril 2003.
- [MISC14] P. Biondi, S., " Reverse engineering sous Unix ", MISC n°14, juillet/août 2004.
- [PKCS#5] PKCS #5, " Password Based Cryptography Standard  ", v2.0, http://www.rsasecurity.com/rsalabs/pkcs/pkcs-5/
- [Phra02] Blexim, Basic Integer Overflows", Phrack Issue 60, No. 10/16, 28 décembre 2002, http://www.phrack.org/phrack/60/p60-0x0a.txt
- [Reas03] Reasoning, How Open Source and Commercial Software Compare, British Computing Society, SIGiST - 1er août 2003
- [Rob96] M. J. B. Robshaw, "Â On Recent Results for MD2, MD4 and MD5Â ", RSA Laboratories' Bulletin, no. 4, 12 Novembre 1996.
- [SSL] OpenSSL, http://www.openssl.org
- VFS99] C. M. Hannum, FreeBSD vfs_cache Memory Consumption DoS, 21 septembre 1999, http://www.osvdb.org/displayvuln.php?osvdb_id=1079
- WinHex] WinHex, http://www.x-ways.net/winhex/index-m.htmlÂ
- [WFLY04] Xiaoyun Wang, Dengguo Feng, Xuejia Lai, Hongbo Yu, Collisions for hash functions MD4, MD5, HAVAL-128 and RIPEMD, Crypto'2004.Â
Retrouvez cet article dans : Misc 17