Usage de la cryptographie par les formats d'archives ZIP, RAR et 7z

MISC n° 092 | juillet 2017 | Laurent Clévy
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Quels sont les algorithmes et paramètres cryptographiques utilisés pour la vérification du mot de passe, la génération de clés et la confidentialité par ces formats de fichiers ?

Les formats d'archivage de type ZIP, RAR et 7z permettent de stocker plusieurs fichiers et répertoires sous forme compressée dans un unique fichier, appelé archive. Ces formats offrent aussi la possibilité de chiffrer les données et les métadonnées à partir d’un mot de passe (ou parfois d’un certificat). Comment sont stockées les données cryptographiques nécessaires, quels sont les algorithmes impliqués et de quelle manière sont-ils utilisés ? Nous allons le voir en détail pour les formats ZIP, RAR (v3 et v5) et 7z, car cela est peu ou pas documenté, hormis le code source dans certains cas

Un outil dédié en Python (unarcrypto.py) nous permettra, tout au long de l'article, d'illustrer et de vérifier les calculs avec des valeurs concrètes. Quelques extraits du code de cet outil illustreront l’article.

Dans la suite de l'article, un même fichier sera optionnellement compressé avec l'algorithme deflate ou simplement stocké, puis chiffré. Voici le contenu de ces données et la somme SHA-1.

>more tests\hello.txt

hello world,hello world

>sha1sum tests\hello.txt

76d7a5a8d72da80c19acbd0f20f90dabac0c52f6  tests\hello.txt

Nous pourrons ainsi vérifier les différentes étapes décrites dans l'article.

1. Format ZIP

Le format de compression ZIP existe depuis 1989 et est le plus simple des formats que nous allons examiner. Il est documenté assez tôt [1] et le chiffrement propriétaire disponible depuis la version 2.0 est connu pour sa faiblesse [2][3], ce qui est également décrit sur le site de WinZip [4]. Nous allons ainsi nous concentrer sur le chiffrement AES, à base de mot de passe, disponible depuis la v9.0 (2003), réputé solide, dû à Brian Gladman [5].

Nous n'allons pas entrer dans les détails du format ZIP, très bien documenté [1][4][6]. Voici le contenu d’une archive ZIP chiffrée avec le mot de passe « hello ». Dans le reste de l’article, les données dignes d’intérêt sont mises en couleurs. Ci-dessous, par exemple, les données compressées et chiffrées sont en orange, les autres données mises en valeur  sont justement celles utiles pour vérifier le mot de passe et déchiffrer :

>btype hello256_deflate.zip

0x000000:  504b0304 33000100 63007459 34490000    PK..3...c.tY4I..

0x000010:  00002c00 00001700 00000900 0b006865    ..,...........he

0x000020:  6c6c6f2e 74787401 99070002 00414503    llo.txt......AE.

0x000030:  080095e6 ddb92a00 5af77ab5 2e00a2d6    ......*.Z.z.....

0x000040:  69d7d41e f7d1712c 37fde9c9 dd4cd051 i.....q,7....L.Q

0x000050: ded17365 77cff026 1858eb2c 86b1504b    ..sew..&.X.,..PK

0x0000d0:  66000000 5e000000 0000                 f...^.....

Comme expliqué sur le site WinZip [4], à la fin du Local File Header (débutant ici par la signature des 4 octets 504b0304, en rouge), une section de données appelée AES extra data field décrit que le fichier est protégé par le chiffrement AES (valeur 0x9901 en little endian, en bleu ci-dessus), elle même suivie par une section appelée Encrypted file storage format.

Ci-dessus, la valeur 3 (en rose) indique que la clé AES fait 256 bits. Le sel possède la valeur 95e6...69d7 (en vert, 16 octets pour AES-256), la valeur pour vérifier le mot de passe est d41e (marron) et le code d’authentification est 77cf...86b1 (10 octets, en violet).

1.1. Génération de clés

C'est tout simplement l'algorithme standard PBKDF2-HMAC-SHA1 qui est utilisé, avec les paramètres suivants (notation PyCrypto) :

PASSWD_VERIF_LEN = 2

keys = PBKDF2(password, salt, dkLen=keyLen*2+PASSWD_VERIF_LEN, count=1000)

Pour AES avec une clé de 256 bits, keyLen vaut 32 (octets), donc PBKDF2 va générer 32+32+2 octets. Les 32 premiers sont la clé AES, les 32 suivants sont utilisés pour vérifier l’intégrité des données et les 2 derniers que le mot de passe fourni est correct.

1.2. Vérification du mot de passe et de l'intégrité des données chiffrées

On vérifie tout simplement que la valeur stockée (d41e) est bien égale aux 2 derniers octets issus de PBKDF2. Si le mot de passe est correct, on vérifie ensuite que les données à déchiffrer sont intactes.

myhmac = hmac.new( keys[keyLen:keyLen*2], fileData, sha1).digest()

On utilise la deuxième série de 32 octets issue de PBKDF2 comme clé de la fonction HMAC-SHA1 calculée sur la version chiffrée du fichier compressé : dans l'exemple, la suite d'octets en orange f7d1...7365. Ensuite, on compare les 10 premiers octets de ce hash avec la valeur « authentication code », ici  77cf...86b1.

L'étape suivante logique est de déchiffrer les données.

1.3. Déchiffrement

Voici ci-dessous l'algorithme utilisé (AES) et ses paramètres :

NUM_COUNTER_BITS=128

ctr = Counter.new(nbits=NUM_COUNTER_BITS, initial_value=1, little_endian=True)

cleartext = AES.new( keys[:keyLen], AES.MODE_CTR, counter=ctr ).decrypt(fileData)

AES est utilisé en mode CTR, avec un compteur de 128 bits, de valeur initiale 1 (en little endian).

Après déchiffrement, on obtient donc la chaîne d'octets en clair cb48cdc9c95728cf2fca49d141620300, ici compressée avec l'algorithme deflate. La décompression se fait ainsi :

decompressed = zlib.decompress(cleartext, -15)

Voyons enfin comment notre outil extrait les valeurs pertinentes :

>python unarcrypto.py -p hello hello256_deflate.zip -s 76d7a5a8d72da80c19acbd0f20f90dabac0c52f6 -v 2

0x00005e: central entry: PK☺☻ name hello.txt compressed 44 uncompressed 23 compression 99

0x000000: local entry: PK♥♦ name  hello.txt size 23 compressed 44 method 99 extraLen 11 crc 0

 extra: 9901 vendor 2 vendorId AE strength 3 method 8

  salt 95e6ddb92a005af77ab52e00a2d669d7 pv d41e auth code 77cff0261858eb2c86b1

   passwd verif OK ? True,   authCode OK ? True

   aes key 2e69d2abca00601d0f0fcac4e9586e6266c58dce7b066b93d1de353f6a0ec605

sha1 decompressed OK ?  True

On retrouve les valeurs avec les mêmes couleurs que précédemment. La clé AES-256 calculée est affichée également, ce qui peut permettre au lecteur de vérifier ce qui précède.

Le format ZIP n'offre pas la protection des métadonnées, ce qui est le cas du format RAR, que nous allons examiner maintenant.

2. Format RAR v3

2.1 Aperçu du format

Le format RAR v3 (2002) est documenté officiellement [7], mais la façon dont sont protégés les en-têtes n'est pas explicitée, hormis le code source de unrar 3.1.0. C'est Marc Bevand qui le décrit en 2010 [8].

Regardons d'abord la structure générale du fichier, à commencer par une archive sans mot de passe et sans compression (méthode « store »), ci-dessous.

>btype hello_nopw_store.rar

0x000000:  52617221 1a0700cf 90730000 0d000000    Rar!.....s......

0x000010:  00000000 02b97420 902e0017 00000017    ......t ........

0x000020:  00000002 ba83e8ef 73593449 14300900    ........sY4I.0..

0x000030:  20000000 68656c6c 6f2e7478 7400f03f     ...hello.txt..?

0x000040:  14716865 6c6c6f20 776f726c 642c6865    .qhello world,he

0x000050:  6c6c6f20 776f726c 64c43d7b 00400700    llo world.={.@..

La spécification nous indique que le format est une suite de « blocs », dont l'en-tête possède le format ci-dessous :

   Each block begins with the following fields:

HEAD_CRC       2 bytes     CRC of total block or block part

HEAD_TYPE      1 byte      Block type

HEAD_FLAGS     2 bytes     Block flags

HEAD_SIZE      2 bytes     Block size

ADD_SIZE       4 bytes     Optional field - added block size

   Field ADD_SIZE present only if (HEAD_FLAGS & 0x8000) != 0

Les 7 premiers octets (en rouge ci-dessus) s’interprètent ainsi : 0x6152 (CRC), 0x72 (type « Marker »), 0x211a (drapeaux) et 0x07 (taille, 2 octets).

Ensuite, nous avons en bleu un bloc de type 0x73 (type « Archive »), suivi par en orange un bloc de type 0x74 (File). Ensuite, on reconnaît les données du fichier archivé :  «hello world,hello world ». Enfin, en rose, l'archive finit par le bloc de type 0x7b (« Terminator »). En résumé, voici l'analyse par notre outil :

>py -3.3 unarcrypto.py hello_nopw_store.rar -v 2

Block header: crc 6152 type 72 (marker) flags 0x1a21 size 7  addsize 0

Block header: crc 90cf type 73 (archive) flags 0x0 size 13  addsize 0

  headersEncrypted False

Block header: crc b902 type 74 (file) flags 0x9020 size 46  addsize 0

  pack_size 23 unp_size 23 file_crc efe883ba method 0x30,  name_size 9 b'hello.txt' addsize 0 high 0

  sha1 correct ? True

  file crc OK ?  True

Block header: crc 3dc4 type 7b (terminator) flags 0x4000 size 7  addsize 0

Dans cette partie, le but n'était pas de décrire exhaustivement le format, mais de voir ce qui change lorsque l'on protège le contenu du fichier d'abord, puis également les en-têtes.

2.2 Chiffrement des données

Voyons maintenant le contenu d'une archive cette fois protégée par le mot de passe « hello », toujours sans compression.

>btype hello_pw_store.rar

0x000000:  52617221 1a0700cf 90730000 0d000000    Rar!.....s......

0x000010:  00000000 dc447424 94360020 00000017    .....Dt$.6. ....

0x000020:  00000002 ba83e8ef 73593449 1d300900    ........sY4I.0..

0x000030:  20000000 68656c6c 6f2e7478 74728be5     ...hello.txtr..

0x000040:  8c227f8d b400f03f 147183dc ec7a8875    .".....?.q...z.u

0x000050:  50327f0a 211ae7c0 7794ef7b f83e71ef    P2..!...w..{.>q.

0x000060:  2cb78a60 d392fe51 01fbc43d 7b004007    ,..`...Q...={.@.

0x000070:  00                                     .

On peut remarquer que les deux premiers blocs sont identiques : Marker (type 72, en rouge) et Archive (type 73, en bleu). Le bloc Terminator (rose) est également identique.

Pour gagner du temps, voici comment notre outil interprète la structure du bloc File, qui a évolué :

Block header: crc 44dc type 74 (file) flags 0x9424 size 54  addsize 0

  pack_size 32 unp_size 23 file_crc efe883ba method 0x30,  name_size 9 b'hello.txt' addsize 0 high 0

  has password

  has ext_time

  file salt b'728be58c227f8db4'

  iv b'd783b8caa2d69c2a1647fc507093900a' key b'b07ae0eb64d9ee270db61416bb23205f'

  sha1 correct ? True

  file crc OK ?  True

Les valeurs IV et Key sont calculées par l'outil, mais non stockées. Nous allons y revenir, bien sûr.

Reprenons un à un les changements observés dans le bloc « File » : les drapeaux changent de 0x9020 à 0x9424, pour indiquer l'usage d'un mot de passe et d'un sel. Avec le chiffrement, la taille stockée (pack_size) est de 32 octets au lieu de 23, car elle est alignée sur la taille d’un bloc chiffré, AES étant utilisé en mode CBC. Le sel est utilisé en conjonction avec le mot de passe pour générer la clé AES et l'IV.

Voici l'algorithme en question, extrait du code source de rarfile de Marki Kreen [9] :

seed = psw.encode('utf-16le') + salt

iv = EMPTY

h = sha1()

for i in range(16):

 for j in range(0x4000):

  cnt = S_LONG.pack(i * 0x4000 + j)

  h.update(seed + cnt[:3])

  if j == 0:

   iv += h.digest()[19:20]

key_be = h.digest()[:16]

key_le = pack("<LLLL", *unpack(">LLLL", key_be))

return key_le, iv

Il s'agit donc d'un algorithme propriétaire, basé sur 218 itérations (16 x 0x4000) de la fonction SHA-1, avec un sel de 8 octets et un compteur de 4 octets. En sortie, nous avons donc l'IV de 16 octets (mode CBC) et la clé AES de 128 bits (16 octets).

Le déchiffrement à partir du mot de passe se fait ainsi :

iv, key = Rar3.keyDerivation(salt, password)

cleartext = AES.new(bytes(key), AES.MODE_CBC, bytes(iv)).decrypt(encrypted)

2.3 Chiffrement des en-têtes

Enfin, voyons comment sont protégés les en-têtes :

Le bloc Marker (en rouge) ne change pas,

>btype hello_pw_store_headers.rar

0x000000:  52617221 1a0700ce 99738000 0d000000    Rar!.....s......

0x000010:  00000000 379475b0 6e303955 6a22183b    ....7.u.n09Uj".;

0x000020:  1f60f48f ea05b409 f1a2b9ca 38b44a5d    .`..........8.J]

0x000070:  c4cfc0cd 5f4c9a03 297cfe7a 379475b0    ...._L..)|.z7.u.

0x000080:  6e303955 3afc3e1a 8513e58d 7c636eac    n09U:.>.....|cn.

0x000090:  13c402fa                               ....

le bloc Archive (en bleu) change pour les drapeaux (0x80). Le reste de l’archive est chiffré.

Aidons-nous de notre outil pour ne pas nous perdre en détail :

Block header: crc 6152 type 72 (marker) flags 0x1a21 size 7  addsize 0

Block header: crc 99ce type 73 (archive) flags 0x80 size 13  addsize 0

  headersEncrypted True

header salt b'379475b06e303955'

iv b'e3dfe7498ad0faf3325f9ee9283a396c' key b'a002f7af8fc3b153436abb226f298747'

encrypted headers: AES key is OK

Block header: crc 4cd type 74 (file) flags 0x9424 size 54  addsize 0

  pack_size 32 unp_size 23 file_crc efe883ba method 0x30,  name_size 9 b'hello.txt' addsize 0 high 0

  has password

  has ext_time

  file salt b'379475b06e303955'

  iv b'e3dfe7498ad0faf3325f9ee9283a396c' key b'a002f7af8fc3b153436abb226f298747'

  sha1 correct ? True

  file crc OK ?  True

header salt b'379475b06e303955'

iv b'e3dfe7498ad0faf3325f9ee9283a396c' key b'a002f7af8fc3b153436abb226f298747'

encrypted headers: AES key is OK

Block header: crc 3dc4 type 7b (terminator) flags 0x4000 size 7  addsize 0

En résumé, la méthode précédente pour le contenu du fichier est appliquée aux blocs File et Terminator.

D'ailleurs, il n'existe pas officiellement de moyen de vérifier que le mot de passe est correct avant déchiffrement, sauf lorsque les en-têtes sont également protégés, car la version chiffrée du bloc Terminator possède une version en clair connue, et la version chiffrée se trouve être les 16 derniers octets de l'archive, précédés du sel. C'est la manière décrite par Marc Bevand [8] pour tester le mot de passe d'une archive RAR v3 : déchiffrer les 16 derniers octets et les comparer avec la version en clair, constante et connue.

On peut noter un fait étrange : un même sel est utilisé 3 fois, alors que la structure du fichier permet d'en utiliser 3 différents. Ceci est en contradiction avec le principe même d'un sel : faire en sorte que le chiffré soit différent pour un même clair, en générant un IV ou une clé différents, par exemple. Ceci est clairement une faiblesse d'implémentation. Or, cette archive a été créée avec WinRAR 5.40, la plus récente à ce jour. Cela est probablement le cas pour tous les fichiers présents dans l’archive, sans doute pour des raisons de performance.

3. Format RAR v5

3.1. Aperçu du format RAR, version 5

En avril 2013, le format RAR a été modifié, en version 5 [10], comme le moyen d'utiliser la cryptographie [11] :

« Changes in RAR 5.0 encryption algorithm: a) encryption algorithm is changed from AES-128 to AES-256 in CBC mode. Key derivation function is based on PBKDF2 using HMAC-SHA256; »

Commençons par observer, comme d'habitude, le contenu d'une archive en clair et non compressée :

>btype hello5_nopw_store.rar

0x000000:  52617221 1a070100 3392b5e5 0a010506 Rar!....3.......

0x000010: 00050101 80800010 b7372725 02030b97    .........7'%....

0x000020:  00049700 20ba83e8 ef800000 0968656c    .... ........hel

0x000030:  6c6f2e74 78740a03 02bf2b20 ff1e13d2    lo.txt....+ ....

0x000040:  0168656c 6c6f2077 6f726c64 2c68656c    .hello world,hel

0x000050:  6c6f2077 6f726c64 1d775651 03050400    lo world.wVQ....

Avec le même code couleur que précédemment, on voit que la structure principale ne change pas : nous avons une suite de blocs 'Marker', 'Main', 'File' et 'End'. Notre outil nous donne plus de détails.

>py -3.3 unarcrypto.py -p hello hello5_nopw_store.rar -s 76d7a5a8d72da80c19acbd0f20f90dabac0c52f6 -v 2

Block header: crc e5b59233 headerSize 10 headerType 1 (Main) headerFlags 5

Block header: crc 2737b710 headerSize 37 headerType 2 (File) headerFlags 3

  extraSize 11 fileFlags 4 dataSize 23 unpackedSize 23 dataCRC 0xefe883ba comprInfo 0x0 hostOS 0 filename b'hello.txt'

  innerExtraSize 10 extraType 3 (Time) extraData: b'02bf2b20ff1e13d201'

    winFileTime b'bf2b20ff1e13d201'

  sha1 correct ? True

Block header: crc 5156771d headerSize 3 headerType 5 (End) headerFlags 4

3.2. Chiffrement des données, vérification du mot de passe

Cette fois-ci, les valeurs utiles au chiffrement sont stockées dans le File Encryption Record [10].

>btype tests\hello5_pw_store.rar

0x000000:  52617221 1a070100 3392b5e5 0a010506    Rar!....3.......

0x000010:  00050101 80800057 a0c0d556 02033ca0    .......W...V..<.

0x000020:  00049700 2019742f 29800000 0968656c    .... .t/)....hel

0x000030:  6c6f2e74 78743001 00030f3e 8ecf5188    lo.txt0....>..Q.

0x000040:  a0ceae32 cc0fdfc9 ab998082 59524114    ...2........YRA.

0x000050:  45b8610c cbe6b3eb 05b81591 179e3524    E.a...........5$

0x000060:  5a115c37 8116830a 0302bf2b 20ff1e13    Z.\7.......+ ...

0x000070:  d201980e 59a480bc 042c0e56 8e82f0a5    ....Y....,.V....

0x000080:  39c66d7b ecb24a41 065f2909 b73cdec2    9.m{..JA._)..<..

0x000090:  ce101d77 56510305 0400                 ...wVQ....

Ça commence à l'offset 0x3a : 0x0f (kdfCount, le nombre de tours pour HMAC-SHA256), un sel de 16 octets (en orange ci-dessus), un IV de 16 octets (en vert) et enfin une valeur de vérification 'checkValue' pour tester le mot de passe et l’intégrité des données (en marron). Les données chiffrées sont entre les offsets 0x72 et 0x92, en violet.

On obtient donc la clé AES avec PBKDF2 itéré 2kdfCount fois, et une seconde valeur utile avec 32 tours en plus :

key = PBKDF2(passwd, salt, dkLen=32, count=1<<count, prf=lambda p,s:HMAC.new(p,s,SHA256).digest() )

v2 = PBKDF2(password, salt, dkLen=32, count=(1<<count)+32, prf=... )  

Il faut vérifier séparément l'égalité avec les 8 premiers et 4 derniers octets de checkValue (en marron) :

PASSWD_CHECK_SIZE = 8  

PASSWD_SUM_CHECK_SIZE = 4  

pwcheck = bytearray(Rar5.PASSWD_CHECK_SIZE)

for i in range(Rar5.SHA256_LEN):

  pwcheck[i % Rar5.PASSWD_CHECK_SIZE] ^= v2[i]

check1 = pwcheck==checkValue[:Rar5.PASSWD_CHECK_SIZE]

check1 est transformé en une valeur v2 de 8 octets, qui doit être égale aux 8 premiers octets de checkValue. Enfin, les 4 premiers octets du SHA-256 de check1 doivent être égaux aux 4 octets de fin de checkValue.

check2 = sha256(pwcheck).digest()[:Rar5.PASSWD_SUM_CHECK_SIZE]==checkValue[Rar5.PASSWD_CHECK_SIZE:]

print(self.depth*'  '+'passwd check OK ?',check1, ', hash value OK ?',check2 )

Lorsque ces vérifications sont correctes, on peut déchiffrer les données :

aeskey, v1, v2 = self.keyDerivation(password, self.salt, self.kdfCount)

c1, c2 = self.passwordCheck( v2, self.checkValue )  

decrypted = AES.new( bytes(aeskey), AES.MODE_CBC, bytes(self.iv) ).decrypt( filedata )

3.3. Protection des en-têtes

Cette fois, c'est l'Archive Encryption header [10] qui stocke les données pour (dé)chiffrer les en-têtes d'une archive RAR5. Avec les mêmes couleurs que précédemment, à partie de l'offset 0x11, on peut identifier le kdfCount (0x0f), le sel (16 octets, en orange) et la checkValue (10 octets, en marron), ci-dessous :

>btype hello5_pw_store_headers.rar

0x000000:  52617221 1a070100 76e573f1 21040000    Rar!....v.s.!...

0x000010:  010f4607 a33dd66a 62ce11fc f92dacaf    ..F..=.jb....-..

0x000020:  18a45540 9bb46375 e9a413a0 92b3aeeb    ..U@..cu........

0x000030:  b6f529dd 7e91d39d f9512c56 9eba94f2    ..).~....Q,V....

0x000040:  9ba5c0da 2189f001 19ab7939 2389593d    ....!.....y9#.Y=

0x000050:  dbe07c9a 5bd3537b 157c0a38 a855edff    ..|.[.S{.|.8.U..

0x000060:  d5b7e4c0 1e36e5c1 100c955f 8ba9fefd    .....6....._....

0x0000d0:  4a6528d8 ae7427cc 4cae5b0b 6183b95d    Je(..t'.L.[.a..]

0x0000e0:  a7f72ec4 6fc66196 cc5de676 d6b5991a    ....o.a..].v....

0x0000f0:  4e85b625 0884627e dcff7c0c fc83        N..%..b~..|...

Ainsi, tous les blocs qui suivent l’en-tête Archive Encryption, sont précédés par l'IV de 16 octets (en bleu ci-dessus) qui permettra leur déchiffrement par AES-256 en mode CBC. Dans le détail, pour connaître la taille exacte du bloc, on commence par déchiffrer les 16 premiers octets, on extrait la taille, puis on déchiffre le bloc en entier. Voici le résultat ci-dessous :

>py -3.3 unarcrypto.py -p hello hello5_pw_store_headers.rar -s 76d7a5a8d72da80c19acbd0f20f90da

bac0c52f6 -v 2

Block header: crc f173e576 headerSize 33 headerType 4 (Encryption) headerFlags 0

encrVersion 0 encrFlags 1 kdfCount 15 salt b'4607a33dd66a62ce11fcf92dacaf18a4' checkValue b'55409bb46375e9a413a092b3'

  AES key b'955142f8b883fed673d632333a2c2c1d1a9712fa9a0e4bca2cfe47e4019ce6db'

  v2 b'52c7626f5eadd90d3c32bb2d7e72decb2d52a029cbd8fb2016e7e2df88721542'

  passwd check OK ? True , hash value OK ? True

EncryptedHeaders

  iv= b'aeebb6f529dd7e91d39df9512c569eba'

  Block header: crc e5b59233 headerSize 10 headerType 1 (Main) headerFlags 5

  iv= b'593ddbe07c9a5bd3537b157c0a38a855'

  Block header: crc 1cc698ab headerSize 86 headerType 2 (File) headerFlags 3

    extraSize 60 fileFlags 4 dataSize 32 unpackedSize 23 dataCRC 0xefe883ba comprInfo 0x0 hostOS 0 filename b'hello.txt'

    innerExtraSize 48 extraType 1 (Encryption) extraData:

      encrVersion 0 encrFlags 1 kdfCount 15 salt b'4607a33dd66a62ce11fcf92dacaf18a4' iv b'818141032c342fb3a3ddbc39336cf05e' checkValue b'55409bb46375e9a413a092b3'

    innerExtraSize 10 extraType 3 (Time) extraData: b'02bf2b20ff1e13d201'

      winFileTime b'bf2b20ff1e13d201'

    AES key b'955142f8b883fed673d632333a2c2c1d1a9712fa9a0e4bca2cfe47e4019ce6db'

    v2 b'52c7626f5eadd90d3c32bb2d7e72decb2d52a029cbd8fb2016e7e2df88721542'

    passwd check OK ? True , hash value OK ? True

    sha1 correct ? True

  iv= b'b95da7f72ec46fc66196cc5de676d6b5'

  Block header: crc 5156771d headerSize 3 headerType 5 (End) headerFlags 4

Header CRC OK ? True

WinRAR 5, contrairement à la version 3, utilise un IV différent pour chaque en-tête. L'utilisation de la fonction standard PBKDF2-HMAC-SHA256 correspond également aux bonnes pratiques actuelles. Il est également maintenant possible (de par le format) de choisir le nombre de tours de PBKDF2, par défaut à 215.

Terminons notre comparaison avec le format d'archive le plus complexe et le plus flexible : 7z.

4. Format 7z

Le format 7z est décrit sommairement [12], mais il est plus difficile de retrouver comment sont protégés les en-têtes et le contenu du fichier, même si Igor Pavlov lui-même en donne les grandes lignes en novembre 2014 [13]. Nous allons également décrire dans les grandes lignes l'organisation d'une archive 7z.

4.1. Aperçu de la structure d'une archive

Comme d'habitude, mettons un peu de couleurs au contenu d'une archive :

C:\Users\laurent>btype e:\dev\unarcrypto\tests\hello_nopw_store.7z

0x000000:  377abcaf 271c0004 949f32bd 17000000    7z..'.....2.....

0x000010:  00000000 4a000000 00000000 ccba3ba1    ....J.........;.

0x000020:  68656c6c 6f20776f 726c642c 68656c6c    hello world,hell

0x000030:  6f20776f 726c6401 04060001 09170007    o world.........

0x000040:  0b010001 01000c17 00080a01 ba83e8ef    ................

0x000050:  00000501 11150068 0065006c 006c006f    .......h.e.l.l.o

0x000060:  002e0074 00780074 00000014 0a0100bf    ...t.x.t........

0x000070:  2b20ff1e 13d20115 06010020 00000000    + ......... ....

0x000080:  00      

Les 32 premiers octets (en rouge ci-dessus) contiennent l’en-tête et comment trouver les métadonnées (en noir à la fin). À partir de l'octet 0x20, on trouve les données du fichier, ici non compressées (en vert).

L'offset vers les métadonnées est stocké sur 8 octets, à l'offset 12, soit 0x17, à partir de la fin de l’en-tête, soit 0x17+0x20 = 0x37.

>py -3.3 unarcrypto.py hello_nopw_store.7z -v 2

7z header

  maj 0 min 4 crc bd329f94 offset 0x17 size 0x4a nextCrc a13bbacc

  next = 0x37

  header crc OK ? True

  next section crc OK ? True

Les trois premiers octets à l'offset 0x37 sont 01,04,06. Afin de ne pas entrer dans les détails du format 7z, voici, ci-dessous, l'analyse de notre outil de la structure de l'archive, de type arbre : il s'agit d'un Header (property de type 1), qui contient les properties MainStreamInfo (type 4) et FilesInfo (5). MainStreamInfo contient PackInfo (type 6), UnpackInfo (7) et SubStreamInfo (8).

property=0x1 (Header)

  property=0x4 (MainStreamsInfo)

    property=0x6 (PackInfo)

      packPos 0

      numPackStreams 1

      property=0x9 (Size)

      size 0x17/23

    property=0x0 (End)

    property=0x7 (UnPackInfo)

      property=0xb (Folder)

      numFolders 1

      numCoders 1

         b'00' copy

        nBonds= 0

      property=0xc (CodersUnPackSize)

      unpackSize 0x17/23

    property=0x0 (End)

    property=0x8 (SubStreamsInfo)

      property=0xa (Crc)

      crc efe883ba

    property=0x0 (End)

  property=0x0 (End)

  property=0x5 (FilesInfo)

    property=0x11 (Names)

    numFiles 1

    size 21

     " hello.txt  "

    property=0x14 (mTime)

     b'0100bf2b20ff1e13d201'

    property=0x15 (Attributes)

     b'010020000000'

  property=0x0 (End)

property=0x0 (End)

sha1 correct ? True

crc32 correct ? True

Pour rester concis : PackInfo décrit la taille compressée (23) et l'offset vers les données (0, à partir de 0x20), UnPackInfo donne le 'Codec', c'est-à-dire 'copy', et la taille décompressée (23). Pour le reste, les noms des propriétés parlent d’eux-mêmes. Voyons maintenant le chiffrement des données.

4.2. Chiffrement des données

Ci-dessous, aidés de codes couleur, nous reconnaissons l’en-tête rouge, les données en vert sur 32 octets, et à l'offset 0x40, les métadonnées commençant par les octets 01,04,06...

>btype hello_pw_store.7z

0x000000:  377abcaf 271c0004 bc9ff1aa 20000000    7z..'....... ...

0x000010:  00000000 6a000000 00000000 7a25cfcf    ....j.......z%..

0x000020:  89b7d04b e00b3483 919d4f24 970ee024    ...K..4...O$...$

0x000030:  c446dd07 76bedbbb bff804b1 1204d82b    .F..v..........+

0x000040:  01040600 01092000 070b0100 022406f1    ...... ......$..

Aidons-nous de notre outil, et observons ce qui change :

>py -3.3 unarcrypto.py -p hello hello_pw_store.7z -s 76d7a5a8d72da80c19acbd0f20f90dabac0c52f6 -v 2

...

property=0x1 (Header)

  property=0x4 (MainStreamsInfo)

    property=0x6 (PackInfo)

      packPos 0

      numPackStreams 1

      property=0x9 (Size)

      size 0x20/32

    property=0x0 (End)

    property=0x7 (UnPackInfo)

      property=0xb (Folder)

      numFolders 1

      numCoders 2

         b'06f10701' 7zAES

        There Are Attributes

        propertiesSize 10: b'53073d86deae0075b499'

        iterations: 2^19, ivLen 8 , IV b'3d86deae0075b499'

        key: b'9f9182616d15e57fc1337920303b38b2ea0a592b953e4c5049014f850995e3ff'

         b'00' copy

        nBonds= 1

          packIndex 1, unpackIndex 0

      property=0xc (CodersUnPackSize)

      unpackSize 0x17/23

      unpackSize 0x17/23

    property=0x0 (End)

    property=0x8 (SubStreamsInfo)

...

property=0x0 (End)

PackInfo indique une taille « compressée » de 32 octets, mais surtout, nous avons maintenant 2 « codecs » différents : un « AES » et un « copy ». Le Codec AES contient un nombre d’itérations pour dériver la clé, une taille d'IV et la valeur de l'IV. Le format peut aussi stocker une valeur de sel, non utilisée ici, et par défaut de 8 octets à 0. Voici l'algorithme propriétaire utilisé, ici 219 itérations de sha256(password || compteur) :

hash = sha256()

pwUtf16le = password.encode('utf-16le')

""" test vector: password=hello, salt=0

 hash.update(680065006c006c006f00 0000000000000000),

 hash.update(680065006c006c006f00 0100000000000000)"""

for n in range(self.rounds):

  hash.update( pwUtf16le + pack('<Q',self.salt) )

  self.salt += 1

return hash.digest()  

On déchiffre ensuite comme d'habitude :

decrypted = AES.new( bytes(key), AES.MODE_CBC, bytes(iv) ).decrypt(packedData)[:unpackedSize]

4.3. Chiffrement des en-têtes

Mettons maintenant de la couleur sur une archive dont les en-têtes sont protégés :

0x000000:  377abcaf 271c0004 8bf23aae 90000000    7z..'.....:.....

0x000010:  00000000 26000000 00000000 ebc5f1af    ....&...........

0x000020:  9db13cf9 7f321b5b d7693cea b782d7c8    ..<..2.[.i<.....

0x000030:  7b8befc0 1aa488ab 7c0b6d8b 03909122    {.......|.m...."

0x000040:  6b30e49b dcff5a70 8d9285cf 206db17e    k0....Zp.... m.~

0x0000a0:  e495ba84 4cd72cd5 a16aa117 49170a18    ....L.,..j..I...

0x0000b0:  17062001 09700007 0b010001 2406f107    .. ..p......$...

0x0000c0:  010a5307 682473f9 408c4d7c 0c6a0a01    ..S.h$s.@.M|.j..

0x0000d0:  351adb08 0000                          5.....

Ci-dessus, nous avons d'abord l’en-tête 7z (en rouge), puis le fichier chiffré (vert), puis les en-têtes chiffrés (orange), enfin les en-têtes pour accéder aux métadonnées protégées qui précèdent. Mais, mais, la séquence en noir change ! Elle devient 17,06... voyons ça :

>py -3.3 unarcrypto.py -p hello tests\hello_pw_store_headers.7z -s 76d7a5a8d72da80c19acbd0f20f90dabac0c52f6 -v 2

...

property=0x17 (EncodedHeader)

  property=0x6 (PackInfo)

    packPos 32

    numPackStreams 1

    property=0x9 (Size)

    size 0x70/112

  property=0x0 (End)

  property=0x7 (UnPackInfo)

    property=0xb (Folder)

    numFolders 1

    numCoders 1

       b'06f10701' 7zAES

      There Are Attributes

      propertiesSize 10: b'5307682473f9408c4d7c'

      iterations: 2^19, ivLen 8 , IV b'682473f9408c4d7c'

      key: b'9f9182616d15e57fc1337920303b38b2ea0a592b953e4c5049014f850995e3ff'

      nBonds= 0

    property=0xc (CodersUnPackSize)

    unpackSize 0x6a/106

    property=0xa (Crc)

    crc 08db1a35

  property=0x0 (End)

property=0x0 (End)

...

Eh oui ! Nous avons d'abord une propriété nommée EncodedHeader (type 0x17), dont PackInfo nous dit qu'il s'agit de la deuxième partie chiffrée (index 1), à partie de l'offset 32 (0x40 dans le fichier). Et nous avons les attributs adéquats dans UnPackInfo pour déchiffrer les en-têtes originaux, identiques à ceux observés dans notre section précédente.

...

decrypted headers CRC OK ? True

property=0x1 (Header)

  property=0x4 (MainStreamsInfo)

    property=0x6 (PackInfo)

      packPos 0

      numPackStreams 1

      property=0x9 (Size)

      size 0x20/32

    property=0x0 (End)

    property=0x7 (UnPackInfo)

      property=0xb (Folder)

      numFolders 1

      numCoders 2

         b'06f10701' 7zAES

        There Are Attributes

        propertiesSize 10: b'530722a6129a1c599906'

        iterations: 2^19, ivLen 8 , IV b'22a6129a1c599906'

        key: b'9f9182616d15e57fc1337920303b38b2ea0a592b953e4c5049014f850995e3ff'

         b'00' copy

        nBonds= 1

          packIndex 1, unpackIndex 0

      property=0xc (CodersUnPackSize)

      unpackSize 0x17/23

      unpackSize 0x17/23

    property=0x0 (End)

 …

property=0x0 (End)

sha1 correct ? True

crc32 correct ? True

Conclusion

Nous avons vu dans cet article pour les formats d'archives courants que sont ZIP, RAR et 7z : où sont stockées les données cryptographiques, les algorithmes de génération de clés et les détails d'utilisation d'AES, utilisé dans tous les cas. Même s'il n'était pas question, dans cet article, d'analyser la sécurité des implémentations en profondeur, on peut constater, premier point, que les fonctionnalités de chiffrement utilisent des algorithmes standards, ce qui n’était pas le cas il y a quelque temps (cf. ZipCrypto ou le mécanisme de dérivation de RAR3).

Le second est que, dorénavant, les clés de chiffrement proviennent directement du mot de passe de l’utilisateur. En particulier, le mot de passe ne sert plus à déchiffrer les clés de chiffrement de l’archive, comme c’était le cas avec ZipCrypto. Ce design s’est avéré catastrophique sur certaines versions de PKZIP utilisant un générateur d’aléa faible. Dans tous les formats actuels, l’aléa n’est utilisé que pour générer un sel. L’utilisation d’un mauvais générateur a donc un impact moindre.Vous pouvez retrouver le code de notre outil sur GitHub [14].

Remerciements

Merci à Jean-Baptiste Bédrune pour sa relecture attentive et ses suggestions.

Références

[1] PKWARE Inc, APPNOTE.TXT - .ZIP File Format Specification version 6.3.4, 2014 https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT

[2] Eli Biham and Paul C. Kocher, A known plaintext attack on the PKZIP Stream Cipher, 1994, http://link.springer.com/chapter/10.1007%2F3-540-60590-8_12#page-1

[3] Michael Stay, ZIP Attacks with Reduced Known Plaintext, 2002,
http://math.ucr.edu/~mike/zipattacks.pdf

[4] AES Encryption Information: Encryption Specification AE-1 and AE-2, janvier 2009, http://www.winzip.com/win/en/aes_info.html

[5] Brian Gladman, A Password Based File Encryption Utility, novembre 2008, http://www.gladman.me.uk/cryptography_technology/fileencrypt/

[6] Zip 101 poster, Ange Albertini, https://github.com/corkami/pics/blob/master/binary/zip101/zip101.pdf

[7] RAR version 4.11 - Technical information, http://www.forensicswiki.org/w/images/5/5b/RARFileStructure.txt

[8] Marc Bevand, Brute Forcing RAR Archives Encrypted with the « -⁠hp » Option, juin 2010, http://blog.zorinaq.com/brute-forcing-rar-archives-encrypted-with-the-hp-option/

[9] Marko Kreen, rarfile, RAR archive reader for Python, https://pypi.python.org/pypi/rarfile/

[10] RAR 5.0 archive format, http://www.rarlab.com/technote.htm

[11] Changes in RAR 5.0 encryption algorithm, http://www.rarlab.com/rarnew.htm

[12] 7z Format description (4.59), http://cpansearch.perl.org/src/BJOERN/Compress-Deflate7-1.0/7zip/DOC/7zFormat.txt et http://www.7-zip.org/sdk.html

[13] Igor Pavlov, Encryption and CRC information in 7z, novembre 2014, https://sourceforge.net/p/sevenzip/discussion/45798/thread/7cb978dc/

[14] Laurent Clévy, unarcrypto.py, an educational tool to depict the use of cryptography for password verification, header and content encryption by popular archivers : zip, 7zip, rar v3 and v5, mai 2017, https://github.com/lclevy/unarcrypto