Les fonctions de hachage occupent une place à part entière dans le monde de la cryptographie. Ses propriétés intrinsèques leur confèrent des fonctionnalités très intéressantes. Parmi les plus rencontrées :

  • Empreinte d'un programme pour vous prémunir contre toute installation d'une version corrompue
  • Empreinte d'un certificat électronique
  • Stockage des mots de passe
  • Stockage d'une position d'échecs dans une table de hachage

J'avais déjà abordé ces fonctions dans un précédent article : http://fiddy.free.fr/blog/index.php?post/2009/01/18/L-art-de-briser-SSL-%C3%A0-cause-de-MD5.

Je rappelle ci-dessous quelques propriétés que doivent respecter les fonctions de hachage :

  • résistance à la première pré-image : réputé difficile de trouver un antécédent connaissant son image.
  • résistance à seconde pré-image : réputé difficile de trouver une entrée différente possédant le même condensé.
  • résistance aux collisions : réputé difficile de trouver deux entrées possédant le même condensé.

Dans cet article, je vais vous montrer un autre cas d'école d'exploitation de collisions en MD5.

Un peu de théorie...

Ce cas est rendu possible grâce à la formule :

Soit A et A' deux collisions (ie MD5(A)=MD5(A')) alors, on a MD5(A || B) = MD5(A' || B)

L'idée est donc de créer deux exécutables :

  • HelloWorld : le début du fichier contient les octets du vecteur A, le reste R est la concaténation des programmes P et Q
  • Malware : le début du fichier contient les octets du vecteur B, le reste R est la concaténation des prograjmmes P et Q

La partie R embarque la charge saine "HelloWorld" et la charge malicieuse "Malware".

La difficulté est donc de trouver une astuce pour que le programme B ait un comportement différent selon le fichier dans lequel il est écrit.

Note : non, ce n'est pas possible que le comportement soit fonction du nom du fichier puisqu'une justement les fichiers auront le même nom pour mieux berner la victime.

Nous allons empaqueter le programme. Un programme empaqueté (qu'on appellera binaire par la suite) est un fichier binaire non exécutable qui à l'aide d'un dépaqueteur libérera un exécutable. Ici, le dépaqueteur libérera le programme "HelloWorld" du binaire s'il contient le vecteur A en début de fichier, sinon la charge malveillante "Malware".

Passons aux choses sérieuses...

Le TP se fera en plusieurs étapes :

  1. Recherche d'une collision
  2. Création de HelloWorld et de Malware
  3. Création d'un outil pour générer le binaire
  4. Création du dépaqueteur
  5. Et le meilleur : la démonstration

Génération d'une collision

Il existe pléthore d'outils de génération de collisions de MD5 sur le net. J'utilise donc une collision MD5 quelconque :

d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70
d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70

Avec un peu de recherche sur les moteurs de recherche, vous générerez vos propres collisions.

Vérification

import binascii, hashlib
A='d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70' B='d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70'
vect1=binascii.a2b_hex(A) vect2=binascii.a2b_hex(B)
hashlib.md5(vect1).hexdigest() => '79054025255fb1a26e4bc422aef54eb4'
hashlib.md5(vect2).hexdigest() => '79054025255fb1a26e4bc422aef54eb4'

Les deux chaînes hexa ont bel et bien le même MD5.

Création de HelloWorld et de Malware

Contenu de HelloWorld.c

#include <stdio.h>
/*HelloWorld*/
int main(void) {
    puts("Bonjour tout le monde !!!");
    getchar();
    return 0;
}

Génération de l'exécutable

$ gcc -Wall -pedantic HelloWorld.c -o HelloWorld.exe

Contenu de Malware.c

#include <stdio.h>
/*Malware*/
int main(void) {
    puts("Ceci est un malware, ou pas...");
    getchar();
    return 0;
}

Génération de l'exécutable

$ gcc -Wall -pedantic Malware.c -o Malware.exe

Création d'un outil pour générer le binaire

Nous allons nous servir du langage Python 3.

import binascii
A='d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70' B='d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70'
#On convertit les chaînes en vecteurs
vect1=binascii.a2b_hex(A)
vect2=binascii.a2b_hex(B)

#Création des binaires
packHelloworld=open('HelloWorld.bin','wb') packMalware=open('Malware.bin','wb')

#Lecture des exécutables helloworld=open('HelloWorld.exe', 'rb').read() malware=open('Malware.exe', 'rb').read()

#On écrit vect1 + helloworld + malware dans le fichier
packHelloworld.write(vect1)
packHelloworld.write(helloworld) packHelloworld.write(malware)

#On écrit vect2 + helloworld + malware dans le fichier packMalware.write(vect2) packMalware.write(helloworld) packMalware.write(malware)
#Fermeture des fichiers packHelloworld.close() packMalware.close()

Vérification

$ md5sum HelloWorld.bin; sha1sum HelloWorld.bin
8d3812393b08a0fb472e0377ccf81443 *HelloWorld.bin
c9e495cd93641cf8925ab28923db359fdd75a8ca *HelloWorld.bin
$ md5sum Malware.bin; sha1sum Malware.bin
8d3812393b08a0fb472e0377ccf81443 *Malware.bin
60e6fb662a54b9a0831b84fd0c4c6b4d9ce1bfb4 *Malware.bin

Nous avons fabriqué deux binaires différents (la preuve puisque les SHA-1 sont différents) ayant le même MD5.

Création du dépaqueteur

Le dépaqueteur doit à partir de HelloWorld.bin (resp. Malware.bin) récupérer HelloWorld.exe (resp. Malware.exe). Pour rappel, Malware.bin et HelloWorld.bin auront bien sûr le même nom. Seul le début des deux binaires sera différent (utilisation des deux vecteurs de collision).

Vous avez sans doute remarqué la forte similitude entre les deux chaînes hexa A et B. Déterminons les offsets des bytes divergents :

import binascii
A='d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70' B='d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70'
vect1=binascii.a2b_hex(A) vect2=binascii.a2b_hex(B)
for i in range(len(vect1)):
       if(vect1[i]!=vect2[i]):
            print("vect1[%d]=0x%x, vect2[%d]=0x%x" %(i, vect1[i], i, vect2[i]))

Résultat du script

vect1[19]=0x87, vect2[19]=0x7
vect1[45]=0x71, vect2[45]=0xf1
vect1[59]=0xf2, vect2[59]=0x72
vect1[83]=0xb4, vect2[83]=0x34
vect1[109]=0xa8, vect2[109]=0x28
vect1[123]=0x2b, vect2[123]=0xab

Nous allons par exemple prendre l'offset 19 et le comparer avec 0x87. S'il y a égalité, alors le dépaqueteur extrairera la charge saine sinon la charge malveillante. Nous avons également besoin de connaître les tailles des vecteurs ainsi que des fichiers HelloWorld.exe et Malware.exe.

$ print("len(vect1)=%d, len(vect2)=%d" % (len(vect1), len(vect2)))
len(vect1)=128, len(vect2)=128
$ print("len(helloworld)=%d, len(malware)=%d" % (len(helloworld), len(malware)))
len(helloworld)=29220, len(malware)=29220

Sans surprise, les tailles des deux vecteurs ont la même taille. En revanche, la taille des deux exécutables peut être différente. Le depaqueteur attendra en paramètre le binaire à dépaqueter.

import sys
TailleVect=128
TailleHelloWorld=29220
TailleMalware=29220

binaire=open(sys.argv[1],'rb').read()
output=open('output.exe', 'wb')

if binaire[19]==0X87:
    #extraction de HelloWorld.exe
    output.write(binaire[TailleVect:TailleVect+TailleHelloWorld])
else:
    #extraction de Malware.exe
    output.write(binaire[TailleVect+TailleHelloWorld:TailleVect+TailleHelloWorld+TailleMalware])

output.close()

Démonstration

Pour mieux mettre en exergue la supercherie, j'ai renommé les deux binaires en Jeu.bin. Je ne détaillerai pas d'exemples d'attaque pour ne pas donner d'idées. Mais, les lignes suivantes parlent d'elles-même.
$ md5sum Jeu.bin
8d3812393b08a0fb472e0377ccf81443 *Jeu.bin
$ python depaqueteur.py Jeu.bin
$ output.exe
Bonjour tout le monde !!!
$ md5sum Jeu.bin
8d3812393b08a0fb472e0377ccf81443 *Jeu.bin
$ python depaqueteur.py Jeu.bin
$ output.exe
Ceci est un malware, ou pas...

A travers cet article, vous avez vu comment exploiter une collision MD5. Il s'agit d'un exemple simple avec un empaqueteur. A titre d'exercice, vous pouvez largement améliorer les programmes. Par exemple, vous pouvez mettre les tailles des exécutables (HelloWorld.exe et Malware.exe) dans le binaire. Ici, on s'en rend compte facilement de l'arnaque en lisant depaqueteur.py. Mais il peut être écrit dans un langage compilé pour empêcher une lecture simple du programme.

Il est possible de faire encore mieux en se passant du système de packetage en exploitant d'autres combines. Cela est un peu plus compliqué et fera peut-être l'objet d'un prochain article. D'ici-là, je vous conseille d'utiliser d'autres fonctions de hachage.