Rédacteur : Trance
Date de création : 02/04/2007
Section : Sécurité > Cryptographie
Imprimer cet article : en noir et blanc ou en couleurs
Le MD5 est un algorithme de hachage bien connu qui génère un hash de taille fixe, quelque soit la taille des données originales. Il est donc normal qu'il existe des collisions, c'est à dire des données distinctes mais qui produisent le même hash en sortie. Cet article explique comment exploiter ces collisions, en générant deux fichiers packés de même taille et de même hash MD5, mais ayant des comportement tout à fait différents lorsqu'on les extraits. Les applications sont considérables...
Comme tout algorithme de hashage, le MD5 peut être victime de collisions. En effet, cegenre d'algorithme retourne une chaîne d'octets (hash) de taille fixe, même si le fichier original est plus long ; il est donc surjectif. Il est donc théoriquement possible de trouver deux fichiers possédant des contenus différents mais qui retournent le même hash une fois passés à la moulinette de l'algorithme. Dans un premier temps, la question est : comment trouver ces collisions ?
En fait, des chercheurs se sont déjà posés la question bien avant la rédaction de cet article... et en ont déjà trouvé. Nous allons voir par la suite qu'il suffit d'en connaitre une (c'est à dire un couple de données ayant le même hash) pour produire autant que l'on veut. La recherche de collisions, longue et fastidieuse, nous sera donc épargnée.
Dans la suite, et pour simplifier, nous appelerons vecteur une suite d'octets. Nous noterons ces octets en héxadécimal. Les opérations sur les vecteurs sont les opérations habituelles, mais nous en retiendrons ici deux seulement :
Le principe de base sur lequel cet article se base est le suivant, et il est important qu'il soit bien compris :
Si x, y et q sont trois vecteurs quelconques, et si md5(x) = md5(y), alors md5(x+q) = md5(y+q).
Autrement dit, si nous avons deux vecteurs x et y qui ont le même hash md5, et si nous ajouttons à chacun de ces vecteurs un troisième vecteur q, alors les hash md5 des deux vecteurs résultats resteront égaux ! Il suffit donc d'avoir x et y, et nous serons capables de produire autant de collisions que nous voudrons.
Attention, comme nous l'avons vu, le + n'est pas commutatif. Ainsi, la relation : "si md5(x) = md5(y) alors md5(q+x) = md5(q+y)" est FAUSSE ! x et y doivent impérativement être à gauche du +, donc en tête des vecteurs.
Ce que l'on aimerait faire avec ces collisions serait de produire deux fichiers exécutables de même hash, masi ayant des comportements différents. Cependant, comme nous l'avons vu, la relation de base impose que les deux vecteurs x et y de même hash doivent en tête des fichiers. Et normalement, en tête d'un fichier exécutable, on trouve le header... L'idéal serait donc d'avoir deux headers différents de même hash. Malheureusement, c'est difficile à obtenir. Nous allons donc procéder différemment, et utiliser un format spécial (une sorte d'archive) que nous allons créer. Nous allons produire deux archives et dans chacune nous placerons en tête un des vecteurs x et y de même hash. Ensuite, le contenu restant des deux archives devra être identique.
Nous allons coder deux programmes. Tout d'abord un packer qui construira les deux archives (de même hash) en fonction de deux fichiers f1 et f2. Ces deux fichiers n'ont pas le même hash, et ont des comportement différents. Nous pouvons supposer par exemple que f1 est un fichier bienveillant et que f2 est malveillant :) ... Ensuite, nous allons concevoir un unpacker qui servira à extraire les fichiers des archives. C'est au moment de l'extraction que le bon fichier (f1 ou f2) sera extrait, en fonction du vecteur de tête.
Voici le format d'archive que nous allons utiliser :
Nous pouvons bien vérifier que chaque archive est de la forme v + q avec v qui est soit x, soit y. Les deux archives ont donc le même hash. On notera aussi que les deux archives contient à la fois le code du fichier bienveillant et du malveillant. Le but du packer que nous allons coder sera de nous produire ces deux archives en fonction de f1 et f2 (et des vecteurs x et y).
L'unpacker devra extraire f1 OU f2 de chaque archive, selon qu'on l'utilise avec l'archive a1 ou a2. Ainsi, si on l'utilsie sur a1, il devra extraire f1, et si on l'applique à a1, il extraira f2. Comment va-t-il distinguer l'archive ? Simplement en regardant certains octets de x et y. En effet, ces deux vecteurs sont différents, donc ils diffèrent au moins sur un octet. Il suffit de regarder quel est cet octet afin de savoir quelle est l'archive (a1 ou a2) et donc quel fichier extraire !
Note : Il est important de bien comprendre que même si a1 et a2 sont de même hash, les fichiers f1 et f2 n'ont pas le même hash ! En effet, le but de la manoeuvre est d'obtenir deux fichiers (a1 et a2) de même hash, mais qui produisent des fichiers différents à l'extraction (f1 et f2).
Concernant le but et le fonctionnement du packer, tout est dit dans le paragraphe précédent. Nous allons donc traduire tout cela en algorithme. J'en profite pour préciser que j'ai développé cet outil sous Linux, mais il doit normalement être compilable sous Windows, à quelques petites modifs près. Comme on peut s'y attendre, l'algorithme proprement dit est très simple. Le voici, en langage de description :
Comme vous le voyez, c'est quasiment du C ! Nous allons détailler par la suite les fonctions lireFichier(), getTaille() et ecrireFichier().
Comme nous avons deux outils à coder (le packer et l'unpacker), et que dans chaque nous allons user et abuser des fonctions de lecteure / ecriture de fichiers, j'ai jugé bon de regrouper les fonctions de manipulation de fichiers dans une bibliothèque à part, que j'ai nommée "fichiers_bib.c". Voici son code :
Je pense qu'elle est suffisamment commentée pour être compréhensible. Son header est le suivant :
Attaquons-nous au packer ! Son code est à peu de chose près le même qu'en langage de description...
Son header n'a rien d'exceptionnel ; il se contente de définir les deux fameux vecteurs x et y, ou plutôt vect1 et vect2
Les deux veceturs se ressemblent fortement ; les différences sont notées sur vect2 par le symbole /**/.
Pour compiler :
Maintenant, testons notre packer avec deux programmes, eux aussi en C :
Désolé pour le manque d'originalité... On compile :
Ok, les deux programmes marchent. On constate au passage qu'ils n'ont pas le même hash MD5 :
On les passe au packer fraichement créé :
Moment de vérité ! Les deux archives ont-elle le même hash ?...
Eh oui ! Et en plus, elles ont la même taille !
Il est dont dur de les différencier. La seule solution pour le détecter serait de faire un diff :
Notre packer fonctionne donc. Passons maintenant à l'unpacker.
Le packer doit extraire f1 si l'archive qu'on lui passe est a1, et f2 si c'est a2. Comme nous l'avons dit il se base sur un octet qui diffère dans x et y (vect1 et vect2). L'algorithme est très simple, et très proche du C, donc nous ne verrons que le code final du programme :
La distinction des deux archives se fait comme on le voit sur le 124ème octet de vect1 et vect2. Grâce à cet octet, on extrait soit f1, soit f2.
Pour compiler, c'est la même chose :
Maintenant, testions-le sur nos deux archives gentil.bin et mechant.bin :
Et voila ! Nous avons donc réussi à obtenir deux archives de même hash, de même taille, mais possédant des comportements différents quand on les extrait.
C'est bien beau tout ça, mais à quoi ça peut servir en pratique ?
Réponse : plein de choses... Imaginons par exemple que vous possédez un site où vous publiez des applications que vous avez développées. Pour une raison x ou y, vous les diffusez non pas sous forme exécutable, mais sous forme packée. Vous diffusez donc le packer permettant de les décompresser sur votre site.
Imaginons que vous êtes malveillant. Vous souhaitez infecter les utilsiateurs avec un rootkits lambda (backdoor, keylogger, etc). Vous développez donc dans un premier temps votre rootkit. Vous concevez ensuite une application banale censée être utile (où du moins que les utilisateurs téléchargeront). Mais avant de la diffuser, vous la packez avec le rootkit, en utilisant md5pack. Vous obtenez donc deux archives ; une qui, une fois extraite, donnera l'application classique, et une qui donnera le rootkit. Vous diffusez dans un premier temps la première, et publisez aussi son hash md5 sur votre site. En effet cette pratique est courante et permet aux utilisateurs d'être sûrs que l'application est la bonne et n'a pas été corrompue.
Les utilisateurs la téléchargent, et voient qu'elle marche bien. Puis, lorsque vous êtes prets, vous échangez l'archive "gentille" sur votre site contre la "méchante". Les utilisateurs la téléchargent, et peuvent même vérifier sa taille et son hash md5. Tout semble normal et ils ne se douteront de rien. Pourant, quand ils la passeront a l'unpacker... c'est le rootkit qu'ils lanceront !
Cette application peut donc faire pas mal de dégats. Mais il faut toutefois relativiser :
Vous pouvez télécharger les programmes md5pack et md5unpack ici.
je tiens à préciser que je n'ai rien inventé dans cet article. Je me suis beaucoup basé sur ces références, en particulier sur la 1ere. Mon travail a consisté à adapter cet article au C, et au français...