SHAdow
Le 23 février, une équipe de chercheurs a annoncé avoir cassé SHA-1 en pratique, en générant une collision.
À partir de leur travail, il est possible de produire de nouvelles paires de fichiers PDF arbitrairement différents qui auront la même signature SHA-1. Par exemple :
$ sha1sum shadow1.pdf shadow2.pdf
fffe36a1d6f0a76a585af4f3838a4a46b6714f0c shadow1.pdf
fffe36a1d6f0a76a585af4f3838a4a46b6714f0c shadow2.pdf
$ sha256sum shadow1.pdf shadow2.pdf
502ccf8ecee10176d891fa4aeab295edec22b95141c2ae16d85f13b39879e37e shadow1.pdf
2546d272df653c5a99ef0914fa6ed43b336f309758ea873448154ebde90cdfe1 shadow2.pdf
J’explique dans ce billet le principe, et je fournis un outil qui produit, à partir de deux images JPEG, deux fichiers PDF différents de même SHA-1.
Réutilisation
En fabriquant leur collision, les auteurs ont pris soin de la rendre réutilisable :
Furthermore, the prefix of the colliding messages was carefully chosen so that they allow an attacker to forge two PDF documents with the same SHA-1 hash yet that display arbitrarily-chosen distinct visual content.
Aujourd’hui, nous allons jouer aux attaquants.
La réutilisation de la collision repose sur le fait qu’avec SHA-1, ajouter un suffixe identique à une collision existante produit encore une collision :
SHA1(A) == SHA1(B) ==> SHA1(A|X) == SHA1(B|X)
(où X|Y
est la concaténation de X
et de Y
)
Autrement dit, vous prenez les fichiers qui produisent une collision, vous ajoutez les mêmes octets aux deux, vous obtenez le même SHA-1 :
$ { cat shattered-1.pdf; echo bonjour; } | sha1sum
4bfd4b804da3aa207b29d6f1300dde507988dc4b -
$ { cat shattered-2.pdf; echo bonjour; } | sha1sum
4bfd4b804da3aa207b29d6f1300dde507988dc4b -
Il est donc trivial de créer de nouvelles collisions.
Mais pour qu’elles aient un intérêt, encore faut-il :
- que les fichiers produits soient valides ;
- qu’une différence entre les fichiers soit visible par l’utilisateur.
Différences
Les différences entre shattered-1.pdf
et shattered-2.pdf
se situent entre
les adresses 0xc0
et 0x13f
:
Nous devrons donc, quoi qu’il arrive, conserver les 0x140
(320) premiers
octets : il s’agira forcément d’un fichier PDF.
Pour analyser la structure sur un exemple minimal, je vous conseille l’exemple
fourni à la dernière page du papier (good.pdf
et bad.pdf
) :
<< -- base64 -d | tar xj
QlpoOTFBWSZTWbL5V5MABl///////9Pv///v////+/////HDdK739/677r+W3/75rUNr4Aa/AAAAAAA
CgEVTRtQDQAaA0AAyGmjTQGmgAAANGgAaMIAYgGgAABo0AAAAAADQAIAGQ0MgDIGmjQA0DRk0AaMQ0D
QAGIANGgAAGRoNGQMRpo0GIGgBoGQAAIAGQ0MgDIGmjQA0DRk0AaMQ0DQAGIANGgAAGRoNGQMRpo0GI
GgBoGQAAIAGQ0MgDIGmjQA0DRk0AaMQ0DQAGIANGgAAGRoNGQMRpo0GIGgBoGQAAIAGQ0MgDIGmjQA0
DRk0AaMQ0DQAGIANGgAAGRoNGQMRpo0GIGgBoGQAABVTUExEZATTICnkxNR+p6E09JppoyamjGhkm0a
mmIyaekbUejU9JiGnqZqaaDxJ6m0JkZMQ2oaYmJ6gxqMyE2TUzJqfItligtJQJfYbl9Zy9QjQuB5mHQ
RdSSXCCTHMgmSDYmdOoOmLTBJWiCpOhMQYpQlOYpJjn+wQUJSTCEpOMekaFaaNB6glCC0hKEJdHr6Bm
UIHeph7YxS8WJYyGwgWnMTFJBDFSxSCCYljiEk7HZgJzJVDHJxMgY6tCEIIWgsKSlSZ0S8GckoIIF+5
51Ro4RCw260VCEpWJSlpWx/PMrLyVoyhWMAneDilBcUIeZ1j6NCkus0qUCWnahhk5KT4GpWMh3vm2nJ
WjTL9Qg+84iExBJhNKpbV9tvEN265t3fu/TKkt4rXFTsV+NcupJXhOhOhJMQQktrqt4K8mSh9M2DAO2
X7uXGVL9YQxUtzQmS7uBndL7M6R7vX869VxqPurenSuHYNq1yTXOfNWLwgvKlRlFYqLCs6OChDp0HuT
zCWscmGudLyqUuwVGG75nmyZhKpJyOE/pOZyHyrZxGM51DYIN+Jc8yVJgAykxKCEtW55MlfudLg3KG6
TtozalunXrroSxUpVLStWrWLFihMnVpkyZOrQnUrE6xq1CGtJlbAb5ShMbV1CZgqlKC0wCFCpMmUKSE
kvFLaZC8wHOCVAlvzaJQ/T+XLb5Dh5TNM67p6KZ4e4ZSGyVENx2O27LzrTIteAreTkMZpW95GS0CEJY
hMc4nToTJ0wQhKEyddaLb/rTqmgJSlkpnALxMhlNmuKEpkEkqhKUoEq3SoKUpIQcDgWlC0rYahMmLuP
Q0fHqZaF4v2W8IoJ2EhMhYmSw7qql27WJS+G4rUplToFi2rSv0NSrVvDUpltQ8Lv6F8pXyxmFBSxiLS
xglNC4uvXVKmAtusXy4YXGX1ixedEvXF1aX6t8adYnYCpC6rW1ZzdZYlCCxKEv8vpbqdSsXl8v1jCQv
0KEPxPTa/5rtWSF1dSgg4z4KjfIMNtgwWoWLEsRhKxsSA9ji7V5LRPwtumeQ8V57UtFSPIUmtQdOQfs
eI2Ly1DMtk4Jl8n927w34zrWG6Pi4jzC82js/46Rt2IZoadWxOtMInS2xYmcu8mOw9PLYxQ4bdfFw3Z
Pf/g2pzSwZDhGrZAl9lqky0W+yeanadC037xk496t0Dq3ctfmqmjgie8ln9k6Q0K1krb3dK9el4Xsu4
4LpGcenr2eQZ1s1IhOhnE56WnXf0BLWn9Xz15fMkzi4kpVxiTKGEpffErEEMvEeMZhUl6yD1SdeJYbx
zGNM3ak2TAaglLZlDCVnoM6wV5DRrycwF8Zh/fRsdmhkMfAO1duwknrsFwrzePWeMwl107DWzymxdQw
iSXx/lncnn75jL9mUzw2bUDqj20LTgtawxK2SlQg1CCZDQMgSpEqLjRMsykM9zbSIUqil0zNk7Nu+b5
J0DKZlhl9CtpGKgX5uyp0idoJ3we9bSrY7PupnUL5eWiDpV5mmnNUhOnYi8xyClkLbNmAXyoWk7GaVr
M2umkbpqHDzDymiKjetgzTocWNsJ2E0zPcfht46J4ipaXGCfF7fuO0a70c82bvqo3HceIcRlshgu73s
eO8BqlLIap2z5jTOY+T2ucCnBtAtva3aHdchJg9AJ5YdKHz7LoA3VKmeqxAlFyEnQLBxB2PAhAZ8Kvm
uR6ELXws1Qr13Nd1i4nsp189jqvaNzt+0nEnIaniuP1+/UOZdyfoZh57ku8sYHKdvfW/jYSUks+0rK+
qtte+py8jWL9cOJ0fV8rrH/t+85/p1z2N67p/ZsZ3JmdyliL7lrNxZUlx0MVIl6PxXOUuGOeArW3vuE
vJ2beoh7SGyZKHKbR2bBWO1d49JDIcVM6lQtu9UO8ec8pOnXmkcponBPLNM2CwZ9kNC/4ct6rQkPkQH
McV/8XckU4UJCy+VeTA==
--
Aiguillage
Notre objectif est que les quelques octets différents entre les deux fichiers PDF déterminent l’image à afficher.
Il serait en théorie possible d’appliquer cet aiguillage au niveau de la structure du PDF, mais c’est en fait au niveau du JPEG qu’il sera implémenté :
PDFs with the same MD5 hash have previously been constructed by Gebhardt et al. [12] by exploiting so-called Indexed Color Tables and Color Transformation functions. However, this method is not effective for many common PDF viewers that lack support for these functionalities. Our PDFs rely on distinct parsings of JPEG images, similar to Gebhardt et al.’s TIFF technique [12] and Albertini et al.’s JPEG technique [1].
Le format JPEG
Voici le strict nécessaire à savoir sur le format JPEG pour notre besoin.
Une image est stockée entre les marqueurs 0xffd8
(Start Of Image) et
0xffd9
(End Of Image).
Il est possible d’insérer autant de commentaires que l’on veut, grâce au
marqueur 0xfffe
suivi de sa taille sur 2 octets (en big-endian). La taille
compte le header de taille, mais pas le marqueur initial.
Par exemple, si je veux insérer le commentaire “Hello” au tout début, mon fichier JPEG ressemblera à ceci :
ff d8 ff fe 00 07 48 65 6c 6c 6f … ff d9
[SOI] [EOI]
[[COM] [LEN] H e l l o]
<------------------->
7
Et c’est à peu près tout ce qu’il y a à savoir.
L’astuce
Mettons en évidence la première différence entre les fichiers en collision.
Dans le fichier 1 :
000000b0 -- -- -- -- -- -- -- -- -- -- -- -- -- ff fe 01
000000c0 73 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
Dans le fichier 2 :
000000b0 -- -- -- -- -- -- -- -- -- -- -- -- -- ff fe 01
000000c0 7f -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
Chacun définit un bloc de commentaires, mais pas de mêmes tailles. Dans le
fichier 1, le début du prochain bloc sera à l’adresse 0x232
(0xbf + 0x173
),
alors que dans le fichier 2 il sera à l’adresse 0x23e
(0xbf + 0x17f
).
Nous avons donc trouvé notre aiguillage ; nous allons maintenant utiliser des commentaires JPEG pour cacher soit la première image, soit la seconde.
Pour l’exploiter jusqu’au bout, il suffit de disposer les commentaires astucieusement pour que les deux versions représentent des images parfaitement valides.
Nous allons donc commencer en 0x232
un bloc de commentaires, ayant une taille
permettant de recouvrir l’intégralité de l’image que nous allons stocker en
0x23e
. Et inversement, nous devons démarrer un commentaire à la fin de l’image
stockée en 0x23e
pour cacher la deuxième image.
Comparons sur le résultat ce qu’observe un parseur qui parcourt chacun des fichiers.
(GG
et HH
sont les deux images à stocker. J1
et J2
sont les longueurs
des sauts pour enjamber chacune des images.)
Le fichier 1 est parsé ainsi :
00000090 -- -- -- -- -- ff d8 -- -- -- -- -- -- -- -- --
…
000000b0 -- -- -- -- -- -- -- -- -- -- -- -- -- ff fe 01
000000c0 73 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
…
00000230 -- -- ff fe J1 J1 -- -- -- -- -- -- -- -- GG GG
00000240 GG GG GG GG GG GG GG GG GG GG GG GG GG GG GG GG
…
i GG GG GG GG GG GG ff fe J2 J2 HH HH HH HH HH HH
i+0x10 HH HH HH HH HH HH HH HH HH HH HH HH HH HH HH HH
…
j HH HH ff d9 -- -- -- -- -- -- -- -- -- -- -- --
Le fichier 2 est parsé différemment :
00000090 -- -- -- -- -- ff d8 -- -- -- -- -- -- -- -- --
…
000000b0 -- -- -- -- -- -- -- -- -- -- -- -- -- ff fe 01
000000c0 7f -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
…
00000230 -- -- ff fe J1 J1 -- -- -- -- -- -- -- -- GG GG
00000240 GG GG GG GG GG GG GG GG GG GG GG GG GG GG GG GG
…
i GG GG GG GG GG GG ff fe J2 J2 HH HH HH HH HH HH
i+0x10 HH HH HH HH HH HH HH HH HH HH HH HH HH HH HH HH
…
j HH HH ff d9 -- -- -- -- -- -- -- -- -- -- -- --
Les structures JPEG sont donc valides dans les deux fichiers. L’image affichée
dépendra de l’octet stocké à l’adresse 0xc0
, valant soit 0x73
, soit 0x7f
.
Maintenant, il nous reste à rendre notre PDF valide.
Le header participant à la collision SHA-1 (donc figé) définit des configurations dans des sections séparées (donc non figées) :
00000000 25 50 44 46 2d 31 2e 33 0a 25 e2 e3 cf d3 0a 0a |%PDF-1.3.%......|
00000010 0a 31 20 30 20 6f 62 6a 0a 3c 3c 2f 57 69 64 74 |.1 0 obj.<</Widt|
00000020 68 20 32 20 30 20 52 2f 48 65 69 67 68 74 20 33 |h 2 0 R/Height 3|
00000030 20 30 20 52 2f 54 79 70 65 20 34 20 30 20 52 2f | 0 R/Type 4 0 R/|
00000040 53 75 62 74 79 70 65 20 35 20 30 20 52 2f 46 69 |Subtype 5 0 R/Fi|
00000050 6c 74 65 72 20 36 20 30 20 52 2f 43 6f 6c 6f 72 |lter 6 0 R/Color|
00000060 53 70 61 63 65 20 37 20 30 20 52 2f 4c 65 6e 67 |Space 7 0 R/Leng|
00000070 74 68 20 38 20 30 20 52 2f 42 69 74 73 50 65 72 |th 8 0 R/BitsPer|
00000080 43 6f 6d 70 6f 6e 65 6e 74 20 38 3e 3e 0a 73 74 |Component 8>>.st|
Ainsi, la largeur (Width
) est définie dans l’objet 2
, la hauteur (Height
)
dans l’objet 3
, etc.
Ces objets sont à définir à la suite des fichiers JPEG embarqués. Pour
comprendre leur format, le plus simple est de lire le fichier good.pdf
que je
recommandais plus haut :
On y trouve la définition des objets (entre autres les dimensions de l’image) :
2 0 obj
8
endobj
3 0 obj
8
endobj
4 0 obj
/XObject
endobj
5 0 obj
/Image
endobj
6 0 obj
/DCTDecode
endobj
7 0 obj
/DeviceRGB
endobj
8 0 obj
1693
endobj
9 0 obj
<<
/Type /Catalog
/Pages 10 0 R
>>
endobj
10 0 obj
<<
/Type /Pages
/Count 1
/Kids [11 0 R]
>>
endobj
11 0 obj
<<
/Type /Page
/Parent 10 0 R
/MediaBox [0 0 8 8]
/CropBox [0 0 8 8]
/Contents 12 0 R
/Resources
<<
/XObject <</Im0 1 0 R>>
>>
>>
endobj
12 0 obj
<</Length 30>>
stream
q
8 0 0 8 0 0 cm
/Im0 Do
Q
endstream
endobj
Ensuite vient la table de références croisées ; elle indique l’offset de chacun des objets définis, dans l’ordre :
xref
0 13
0000000000 65535 f
0000000017 00000 n
0000001861 00000 n
0000001879 00000 n
0000001897 00000 n
0000001922 00000 n
0000001945 00000 n
0000001972 00000 n
0000001999 00000 n
0000002020 00000 n
0000002076 00000 n
0000002142 00000 n
0000002309 00000 n
À chaque ajout ou suppression de caractères dans la définition des objets, cette table doit être mise à jour.
Le fichier se termine par un trailer, contenant l’offset de la table de références :
trailer << /Root 9 0 R /Size 13>>
startxref
2391
%%EOF
Ces offsets sont un peu fastidieux à modifier à la main, mais ça fonctionne.
SHAdow
J’ai donc écrit un petit outil qui applique toutes ces opérations automatiquement: SHAdow.
Il prend en entrée deux images JPEG (de moins de 64K, puisque la taille d’un commentaire est codé sur 2 octets), ainsi que leurs dimensions (afin d’éviter d’utiliser des dépendances pour les extraire) :
./shadow.py img1.jpg img2.jpg 200 200
Il génère deux fichiers dans le répertoire courant, shadow1.pdf
et
shadow2.pdf
.
Il ne reste qu’à vérifier qu’ils ont bien le même SHA-1 :
sha1sum shadow1.pdf shadow2.pdf