                      .:[ Attaques sur le format ZIP ]:.
		                 sirius_black

Introduction
------------
Tout le monde connait le format ZIP.  C'est le format le plus couramment utilis
avec GZ quand on a besoin de compresser des donnes. C'est en tout cas le format
le plus utilis sous Windows.  Cela fait de lui le format  d'archive  connatre
absolument et  malgr cela  il est encore  plein de ressources  (utilisation par
la stganographie par exemple).
Dans cet article  on va donc tudier comment est foutu  ce vieux format cr par
PKWare,  dcortiquer la  mthode de cryptage dite  'classique',  voir comment on
peut infcter des fichiers zips etc.

Premire tude du format zip
----------------------------
Les informations prsentes dans cet article se basent sur les spcifications ZIP
par PKWare (version 6.2.0) ainsi que sur mes propres observations du format.

Le format .zip est assez bizarre. En rgle gnral un fichier commence par un ou
plusieurs enttes  la suite dcrivant le contenu du fichier. Dans le cas de zip
c'est un  peu diffrent...  Il s'agit plutt  d'une  concatnation de  plusieurs
fichiers,  chacun avec son entte.  On ne retrouve  pas un entte  principal qui
prvient qu'il y a  n entres dans l'archive zip,  en fait on le dcouvre au fur
et  mesure de l'tude du fichier.

Voici la structure gnrale d'un fichier zip :

 [ local file header 1 ]
 [ file data 1 ]
 [ data descriptor 1 ]
 ...
 ...
 ...
 [ local file header n ]
 [ file data n ]
 [ data descriptor n ]
 [ archive decryption header ] (EFS)
 [ archive extra data record ] (EFS)
 [ central directory ]
 [ zip64 end of central directory record ]
 [ zip64 end of central directory locator ]
 [ end of central directory record ]

Dans le cadre  de l'article nous  n'avons pas  besoin de connatre  compltement
l'entte.  Il faut  juste  savoir  que  les  versions  rcentes  du  format  ZIP
permettent l'utilisation de mthode cryptographiques "modernes", ce qui explique
l'ajout de nouvelles sections dans le format ZIP.
Si on retire ces sections en question on arrive  un format simplifi qui est le
suivant :

 [ local file header 1 ]
 [ file data 1 ]
 [ data descriptor 1 ]  (on ignorera les data descriptor)
 ...
 ...
 ...
 [ local file header n ]
 [ file data n ]
 [ data descriptor n ]  (on ignorera les data descriptor)
 [ central directory ]
 [ end of central directory record ]

Les data descriptor  n'ont aucun rapport avec l'encryption,  c'est pour cela que
je  les ai  mis dans  cette  version  simplifie,  toutefois  ils sont  rarement
prsents.  Si vous codez un  programme en  rapport avec le  format zip n'oubliez
pas de vrifier leurs prsences.

Pour expliquer simplement le format zip, on y trouve les choses suivantes :
L'entte d'une premire entre (fichier ou rpertoire)
Il y a ensuite le contenu du fichier. Celui-ci peut tre compress, crypt...
Evidemment  si il  s'agit d'un rpertoire  il  n'y aura  aucune  donnes et vous
tomberez immdiatement sur l'entre suivante.

Entte de l'entre suivante.
Contenu de l'entre (data).

...
(comme a pour toutes les entres du zip)
...

Vient ensuite la structure 'central directory' qui ressemble tonemment  ce que
l'on a vu pour l'instant. L'explication est qu'avec les volutions du format zip
il fallait plus de  donnes pour dfinir  une entre...  Agrandir les local file
header ?  Impossible  !  On ne peut pas modifier  comme a un format  du jour au
lendemain ! La solution  utilise par  PKWare est  d'avoir  recours   un second
header qui possde plus d'informations sur les entres.

Une structure Central Directory ressemble donc  a :
 [ file header 1 ]
 ...
 ...
 ...
 [ file header n ]
 [ digital signature ] (que l'on ignorera car l encore pas toujours prsente)

Il faut savoir que les headers suivent le mme ordre que les local file headers.
Ainsi la premire entre  au dbut du fichier est aussi  la premire   avoir un
header dans la structure central directory.

Aprs  tout  a on arrive  finalement sur  l'enregistrement  de fin  du  central
directory (end of central directory record).

La conclusion  de cette tude  globale est que  le format zip a  souffert de ces
volutions : certaines informations que l'on a sur une entre sont en double, ce
qui est loin d'tre gnial pour  un format principalement destin  gagner de la
place.

Etude du local file header
--------------------------
Les enttes de chaque entres se divisent de cette faon :

local file header signature   4 octets (0x04034b50)
version needed to extract     2 octets
general purpose bit flag      2 octets
compression method            2 octets
last mod file time            2 octets
last mod file date            2 octets
CRC-32                        4 octets
compressed size               4 octets
uncompressed size             4 octets
file name length              2 octets
extra field length            2 octets

file name (taille variable, spcifie dans le header)
extra field (taille variable, spcifie dans le header)

[*] Notre header commence par la signature ZIP 0x04034b50.
Chaque section du format zip commence par une signature de la forme 0xXXXX4b50.
La notation  0xABCD o  ABCD sont  des octets au format  hexadcimale correspond
 la suite d'octets 0xC 0xD 0xA 0xB.
Donc les 4 premiers octets d'un fichier zip sont dans l'ordre :
0x4b 0x50 0x04 0x03
Avec 0x4b 0x50 = 'PK' (pour PKWare).

[*] Aprs  la  signature  de l'entte  on trouve  le numro  de version  minimum
ncessaire  l'extraction de l'entre hors de l'archive.
Cette information  est trs  utile  car on  peut en  dduire  facilement  si des
options ont t utilises. Une version 2.0 peut nous indiquer un fichier protg
par mot  de passe.  Une version  1.0  indique  un  fichier non  protg  car les
protections n'ont t ajoutes qu'avec la version 2.0.  Une version suprieure 
2.0 indique trs  probablement l'utilisation  d'une mthode cryptographique plus
forte.

[*] On a ensuite une srie de flags qui se tiennent sur 2 octets.  Les bits sont
les suivants :
 Bit 0 : si activ, indique que le fichier est crypt
 ...
 Bit 3 : si actif il ne faut pas prendre en compte le CRC du local file header
         mais celui du data descriptor.
 ...
 Bit 6 : Encodage fort
 Bit 7  11 : non utiliss (message subliminal : stganographie :p )
 Bit 12, 14, 15 : rservs par PKWare

[*] Compression Method : la mthode de compression utilise
Les valeurs possibles vont de 0  12.
0  -> le fichier est non compress
8  -> le fichier est compress avec l'algorithme deflate
12 -> le fichier est compress avec l'algorithme BZIP2
Nous nous intresserons uniquement aux cas o aucune compression n'a t mise en
oeuvre.  Il est assez facile de grer  la compression deflate en ayant recours 
la zlib.

[*] Date et Heure de dernire modification du fichier (2 octets chacun)
La date et l'heure sont au format MSDOS, ce qui n'est pas top  grer sous Linux
Voici de quoi changer a :
heures = Heure >> 11
minutes = (Heure>>5) & 63
jour = Date & 0x1f
mois = (Date>>5) & 0x0f
annes = (date>>9)+80
La formule pour les annes n'est pas parfaite,  vous de l'amliorer...

[*] Le CRC-32 sur 4 octets a le mme rle qu'un hash MD5,  il permet de vrifier
l'intgrit du fichier.

[*] Taille compresse et taille dcompresse
Cela nous permet de calculer le ratio de compression. S'il s'agit d'un rpertoi-
re, les tailles sont fixes  0.

[*] Nous avons  ensuite la longueur du  nom de fichier  ainsi que la longueur du
champ optionnel.  Le champ optionnel est gnralement utilis  par les logiciels
de compression pour  y mettre leur 'patte'  ( une valeur qui sert  dire  "cette
archive a t compresse par LameZip...").

Aprs nous avons  videmment le nom de fichier ainsi  que le champ optionnel eux
mme.

Donnes
-------
Les donnes prennent la place spcifie par la taille compresse.

Central Directory Structure
---------------------------
Comme  dit prcdemment,  cette structure  est  uniquement  compose  d'en-ttes
complmentaires.
Les enttes sont composs de la faon suivante :
[*] Signature du Central File Header     4 octets (0x02014b50 = PK 0x02 0x01)
[*] Version made by                      2 octets
[*] Version needed to extract            2 octets
[*] General purpose bit flag             2 octets
[*] Compression method                   2 octets
[*] last mod file time                   2 octets
[*] last mod file date                   2 octets
[*] CRC-32                               4 octets
[*] Compressed size                      4 octets
[*] Uncompresses size                    4 octets
[*] File name length                     2 octets
[*] Extra field length                   2 octets
[*] File comment length                  2 octets
[*] Disk number start                    2 octets
[*] Internal file attributes             2 octets
[*] External file attributes             4 octets
[*] Relative offset of local header      4 octets

[*] File name    (taille variable, spcifie dans le header)
[*] Extra field  (taille variable, spcifie dans le header)
[*] File comment (taille variable, spcifie dans le header)

La plupart des informations taient dj prsentes dans le local file header. Ce
nouveau header apporte  des informations ncessaires  comme  "Disk number start"
qui indique sur quelle archive se trouve le dbut d'un fichier lorsque l'archive
est splitte en plusieurs fichiers zip.
La valeur  "internal file attributes" permet surtout de savoir si le fichier est
un fichier texte ou un fichier binaire.
Ca se  complique  un peu avec  l'adresse  relative du local  file header.  Cette
valeur  dsigne  l'emplacement en octet  du header  (dans sa version  courte)  
partir du dbut du fichier.
On va prendre un exemple simple  :  un dossier nomm 'lapin' contient un fichier
nomm 'carote'. Le rpertoire 'lapin' est la premire entre dans l'archive. Son
header se trouve donc  la position 0 (roffset=0).
Dans un cas classique (sans la prsence d'une section supplmentaire) le roffset
du fichier 'carote' sera 36.
On compte d'abord la taille du header du rpertoire 'lapin' : 30
On ajoute la taille du mot 'lapin/' : 6
On ajoute la taille du champ extra : 0 pour l'exemple
On ajoute la taille des donnes : 0 car 'lapin' est un rpertoire.

Compliquons les choses en rajoutant un fichier 'civet' dans ce mme rpertoire..
Les entres seront alors :
lapin/        (longueur =  6, data =    0, extra = 0)
lapin/carote  (longueur = 12, data = 1152, extra = 5)
lapin/civet

Le roffset de l'entre 'lapin/civet' sera :
(30 + 6) + (30 + 12 + 5 + 1152) = 1235
ou encore
roffset de l'entre prcdente + taille prise par l'entre prcdente.

Notez bien que zip reconnait les rpertoires  au fait que leur noms se terminent
par le caractre '/'.
Dans le  cas o on  souhaiterait ajouter  une entre   une archive  il est donc
prfrable de  rajouter cette  archive  la fin plutt qu'au dbut  (pour viter
d'avoir  recalculer tous les roffsets).

End of Central Directory Record
-------------------------------
Voici enfin la dernire section

[*] End of central dir signature            4 octets (0x06054b50 = PK 0x06 0x05)

[*] Number of this disk                                             2 octets
Numrote le fichier zip dans le cas d'une archive splite

[*] Number of this disk with the start of the central directory     2 octets
Le numro de fichier zip sur lequel commence la section Central Directory

[*] Total number of entries in the central directory on this disk  2 octets
Nombre d'entres dans le central directory qui se trouve sur le fichier courant

[*] Total number of entries in the central directory               2 octets
Nombre total d'entres dans la structure central directory

[*] Size of the central directory                                  4 octets
La taille de la structure central directory entire
Il faut savoir que la taille d'un header de central directory est de 46.

[*] Offset of start of central directory with respect to the       4 octets
    starting disk number
Adresse relative  au dbut du  fichier qui  indique la  position du  dbut de la
structure central directory.

[*] .ZIP file comment length                                       2 octets
La taille d'un commentaire sur l'archive entire

[*] .ZIP file comment (taille variable dfinie juste avant)

Dans le cas d'une archive en un seul morceau on aura :

Number of this disk : 0
Nb of this disk with the start of the central directory : 0
Total number of entries in the cdir of this disk : X
Total number of entries in the cdir : X
Size of the central directory : Y
Offset of start of cdir : Z
.ZIP file comment length : V

avec X = nombres d'entres dans la structure central directory

Quelques notes
--------------
Le format zip fait parfois des surprises... La spcification contient une partie
"Notes gnrales" qui contient des informations  ne pas oublier.
- Les chaines ne sont pas termines par un zro terminal
- Une section  "special spanning signature"  peut se trouver parfois juste aprs
  la premire entre dans les local file headers  (aprs l'entte + les donnes)
  La signature est 0x08074b50 (PK 0x08 0x07)
  La taille de cette section est de 16 octets, signature inclue

Protection des archives ZIP par mot de passe
--------------------------------------------
Depuis la  version 2.0 de ZIP,  il est  possible  de protger  ses documents  en
cryptant une  archive par  un  mot de passe.  Evidemment il  est possible  de ne
crypter  qu'une  entre si  on le  dsire car  on a vu  que  l'encryption  tait
signale par le bit 0 du local file header.

Pour mieux comprendre la protection de PKWare (Traditional PKWARE Encryption) on
va utiliser un exemple simple.

(sirius@lotfree) (exemples) $ fortune > fortune.txt
(sirius@lotfree) (exemples) $ cat fortune.txt 
La science est asymptote  la vrit, elle l'approche sans cesse et ne
la touche jamais.
        -+- Hugo, Victor -+-
(sirius@lotfree) (exemples) $ ls -l fortune.txt 
-rw-r--r--    1 sirius   sirius        111 2005-03-02 20:53 fortune.txt
(sirius@lotfree) (test) $ crc32 fortune.txt 
86b84fd3

Vous avez maintenant toutes les infos ncessaires sur le fichier que nous allons
zipper...

On cre une archive zip qui contient notre fichier fortune.txt. Aucune compress-
ion n'est faite.
(sirius@lotfree) (exemples) $ zip -0 fortune.zip fortune.txt 
  adding: fortune.txt (stored 0%)

On fait une deuxime archive semblable sauf qu'elle est protge par un password
(sirius@lotfree) (exemples) $ zip -0 fortune_crypt.zip fortune.txt -e
Enter password:  <-- je tappe 'test'
Verify password: <-- je retape test
  adding: fortune.txt (stored 0%)

Etudions les fichiers zip que nous avons obtenu :
(sirius@lotfree) (exemples) $ ls -l *.zip
-rw-r--r--    1 sirius   sirius        293 2005-03-02 21:00 fortune_crypt.zip
-rw-r--r--    1 sirius   sirius        265 2005-03-02 20:57 fortune.zip

On a donc 28 octets de plus pour le fichier crypt...

Utilisons zipinfo pour savoir o se trouve la diffrence (output rsum) :
(sirius@lotfree) (exemples) $ zipinfo -v fortune.zip
  minimum software version required to extract:     1.0
  compression method:                               none (stored)
  file security status:                             not encrypted
  extended local header:                            no
  32-bit CRC value (hex):                           86b84fd3
  compressed size:                                  111 bytes
  uncompressed size:                                111 bytes
  length of filename:                               11 characters
  length of extra field:                            13 bytes
  length of file comment:                           0 characters

Et pour le version crypte :
(sirius@lotfree) (exemples) $ zipinfo -v fortune_crypt.zip
  minimum software version required to extract:     1.0
  compression method:                               none (stored)
  file security status:                             encrypted
  extended local header:                            yes
  32-bit CRC value (hex):                           86b84fd3
  compressed size:                                  123 bytes
  uncompressed size:                                111 bytes
  length of filename:                               11 characters
  length of extra field:                            13 bytes
  length of file comment:                           0 characters

A noter que  zipinfo donne une version qui ne doit pas tre la version du format
zip utilis mais la version du zip de Linux...  (normalement il devrait afficher
2.0 pour l'archive crypte)

O se trouve  nos 28 octets de diffrences  ?  D'abord il y a un 'extended local
header'  dans la version crypte.  Cette structure fait 16 octets en comptant sa
signature.
Mais la diffrence la plus flagrante se situe au niveau de la taille compresse:
La version crypte contient un champ data de 123 octets alors que la version non
crypte fait 111 octets (normal puisqu'il n'y a aucune compression).
Cela fait donc 12 octets de diffrences supplmentaires... le compte y est...

Passons les fichiers  l'afficheur hexadcimal :
(sirius@lotfree) (exemples) $ hexdump -C fortune.zip 
00000000  50 4b 03 04 0a 00 00 00  00 00 aa a6 62 32 d3 4f  |PK........b2O|
00000010  b8 86 6f 00 00 00 6f 00  00 00 0b 00 15 00 66 6f  |.o...o.......fo|
00000020  72 74 75 6e 65 2e 74 78  74 55 54 09 00 03 af 19  |rtune.txtUT....|
00000030  26 42 2b 1a 26 42 55 78  04 00 e8 03 e8 03 4c 61  |&B+.&BUx....La|
00000040  20 73 63 69 65 6e 63 65  20 65 73 74 20 61 73 79  | science est asy|
00000050  6d 70 74 6f 74 65 20 e0  20 6c 61 20 76 e9 72 69  |mptote  la vri|
00000060  74 e9 2c 20 65 6c 6c 65  20 6c 27 61 70 70 72 6f  |t, elle l'appro|
00000070  63 68 65 20 73 61 6e 73  20 63 65 73 73 65 20 65  |che sans cesse e|
00000080  74 20 6e 65 0a 6c 61 20  74 6f 75 63 68 65 20 6a  |t ne.la touche j|
00000090  61 6d 61 69 73 2e 0a 09  2d 2b 2d 20 48 75 67 6f  |amais...-+- Hugo|
000000a0  2c 20 56 69 63 74 6f 72  20 2d 2b 2d 0a 50 4b 01  |, Victor -+-.PK.|
000000b0  02 17 03 0a 00 00 00 00  00 aa a6 62 32 d3 4f b8  |.........b2O|
000000c0  86 6f 00 00 00 6f 00 00  00 0b 00 0d 00 00 00 00  |.o...o..........|
000000d0  00 00 00 00 00 a4 81 00  00 00 00 66 6f 72 74 75  |..........fortu|
000000e0  6e 65 2e 74 78 74 55 54  05 00 03 af 19 26 42 55  |ne.txtUT....&BU|
000000f0  78 00 00 50 4b 05 06 00  00 00 00 01 00 01 00 46  |x..PK..........F|
00000100  00 00 00 ad 00 00 00 00  00                       |........|

On remarque tout de suite que le fichier n'est ni compress ni encrypt :p

(sirius@lotfree) (exemples) $ hexdump -C fortune_crypt.zip     
00000000  50 4b 03 04 0a 00 09 00  00 00 aa a6 62 32 d3 4f  |PK........b2O|
00000010  b8 86 7b 00 00 00 6f 00  00 00 0b 00 15 00 66 6f  |.{...o.......fo|
00000020  72 74 75 6e 65 2e 74 78  74 55 54 09 00 03 af 19  |rtune.txtUT....|
00000030  26 42 c0 1a 26 42 55 78  04 00 e8 03 e8 03 09 bf  |&B.&BUx.....|
00000040  c2 2a 8a 09 88 09 fe dd  7f 02 e2 ca 42 3a b1 75  |*......B:u|
00000050  70 f5 80 37 16 c6 18 77  28 95 7d c9 2d 53 22 6a  |p.7..w(.}-S"j|
00000060  77 0a b1 22 52 d8 ed 58  b7 33 df 95 cc d9 03 56  |w."RX3..V|
00000070  3e ca 34 38 10 e0 66 af  c5 45 b2 82 36 1f d6 1d  |>48.fE.6..|
00000080  83 49 31 47 db cf ff ac  93 76 3e 41 0f 10 78 77  |.I1G.v>A..xw|
00000090  83 b1 68 bf 20 8f 22 33  c1 b6 8b 8c 9b e6 fb c3  |.h ."3...|
000000a0  35 34 9a 40 83 e1 e2 c9  08 7f c0 12 52 51 3b 80  |54.@....RQ;.|
000000b0  d4 2c 1d a4 68 7d d8 7b  24 50 4b 07 08 d3 4f b8  |,.h}{$PK..O|
000000c0  86 7b 00 00 00 6f 00 00  00 50 4b 01 02 17 03 0a  |.{...o...PK.....|
000000d0  00 09 00 00 00 aa a6 62  32 d3 4f b8 86 7b 00 00  |.....b2O.{..|
000000e0  00 6f 00 00 00 0b 00 0d  00 00 00 00 00 00 00 00  |.o..............|
000000f0  00 a4 81 00 00 00 00 66  6f 72 74 75 6e 65 2e 74  |......fortune.t|
00000100  78 74 55 54 05 00 03 af  19 26 42 55 78 00 00 50  |xtUT....&BUx..P|
00000110  4b 05 06 00 00 00 00 01  00 01 00 46 00 00 00 c9  |K..........F...|
00000120  00 00 00 00 00                                    |.....|
00000125

C'est le moment de se jeter dans les spcifications PKWare. On y lit :
--cut--cut--
PKZIP encrypts the compressed data stream.  Encrypted files must
be decrypted before they can be extracted.

Each encrypted file has an extra 12 bytes stored at the start of
the data area defining the encryption header for that file.  The
encryption header is originally set to random values, and then
itself encrypted, using three, 32-bit keys.  The key values are
initialized using the supplied encryption password.  After each byte
is encrypted, the keys are then updated using pseudo-random number
generation techniques in combination with the same CRC-32 algorithm
used in PKZIP and described elsewhere in this document.
--cut--cut--

Pour ceux qui speak pas la  langue de Sid Vicious a veut dire que le champ data
de la version crypte commence par  un header de 12 octets qui nous servira pour
le dcryptage :)
Ici notre header encrypt est 09BFC22A8A098809FEDD7F02.
On apprend aussi que le dcryptage utilise 3 cls de 32 bits (quatre octets).
La spcification donne  quelques  algorithmes  pour  expliquer  le mcanisme  de
dcryptage.

Voici la technique pour dcrypter les donnes :
1. Initialiser les trois cls de 32 bits avec le mot de passe
2. Dcrypter le header avec avec les trois cls
3. Dcrypter le reste des donnes

Tout ce mcanisme tourne autour d'une table CRC.

Initialisation des cls d'encryption
------------------------------------
loop for i <- 0 to length(password)-1
    update_keys(password(i))
end loop

Avec update_keys() dfinie de la faon suivante :

update_keys(char):
  Key(0) <- crc32(key(0),char)
  Key(1) <- Key(1) + (Key(0) & 000000ffH)    <-- & correspond  un AND
  Key(1) <- Key(1) * 134775813 + 1
  Key(2) <- crc32(key(2),key(1) >> 24)
end update_keys

crc32(old_crc,char)  met   jour  un  crc   partir du  crc  prcdent  et  d'un
caractre. (je ne vais pas vous expliquer ici comment fonctionne le crc :p )

Dcryptage du header
--------------------
Donc on modifie nos cls avec le mot de passe. On dcrypte ensuite le header (on
suppose que le header se trouve dans la variable buffer) :

loop for i <- 0 to 11
    C <- buffer(i) ^ decrypt_byte()        <-- '^' correspond au XOR
    update_keys(C)
    buffer(i) <- C
end loop

O decrypt_byte() est dfini de cette faon :

unsigned char decrypt_byte()
    local unsigned short temp
    temp <- Key(2) | 2                  <-- '|' correspond  un OR
    decrypt_byte <- (temp * (temp ^ 1)) >> 8     <-- '>>' correspond  un
end decrypt_byte                                      dcalage des bits  droite

On a alors le header dcrypt.
Dans notre cas, en utilisant le bon mot de passe,  on arrive  un header qui une
fois dcrypt vaut : D607CFCA528A13060910AAA6.

L on se dit que  la suite est simple : on dcrypte les donnes, on lance l'algo
du CRC dessus et si le CRC  correspond  celui donne dans le  local file header
alors c'est bon...
Evidemment si le  fichier est compress il faudrait  aussi le dcompresser avant
de pouvoir calculer son CRC.
Pour une attaque  par brute-force ou  par dictionnaire  ce ne  serais  pas 'top'
puisque  le  dcryptage  des  donnes,  la  dcompression  et le calcul  du  CRC
prendrait normment de temps :(
Heureusement la spcification rajoute :

--cut--cut--
After the header is decrypted,  the last 1 or 2 bytes in Buffer
should be the high-order word/byte of the CRC for the file being
decrypted, stored in Intel low-byte/high-byte order.  Versions of
PKZIP prior to 2.0 used a 2 byte CRC check; a 1 byte CRC check is
used on versions after 2.0.  This can be used to test if the password
supplied is correct or not.
--cut--cut--

On apprend que la validit  d'un mot de passe peut  (en partie) se vrifier avec
le header dcrypt.  En effet les derniers octets  du header dcrypt correspon-
dent  des octets se trouvant dans le local file header.

Note : la spcification se contredit puisque tout  l'heure  elle prtendait que
le header contenait des donnes alatoires. En fait la spcification donne juste
de quoi  dchiffrer les  donnes avec  le bon mot de  passe mais  n'explique pas
comment on  peut casser  le cryptage bien qu'il est rpt   plusieurs reprises
que l'encryptage de PKWare n'est pas sr...
J'ai donc du tudier  les sources de plusieurs  logiciels de cassage de zip pour
combler les lacunes de la spcification.

Reprenons le dbut du dump hexadcimal du fichier crypt :
00000000  50 4b 03 04 0a 00 09 00  00 00 aa a6 62 32 d3 4f  |PK........b2O|
00000010  b8 86 7b 00 00 00 6f 00  00 00 0b 00 15 00 66 6f  |.{...o.......fo|

Le '09' correspond aux flags. Ici le bit 0 et le bit 3 sont activs.
'AAA6' correspond  l'heure de dernire modification.
'6232' correspond  la date (jour/mois/annes) de dernire modification.
'D34FB886' correspond au CRC.

Et notre header dcrypt vaut : D607CFCA528A13060910AAA6
On a effectivement une  correspondance entre les deux derniers octets et l'heure
donne dans le local file header.
Comme la  spcification le prcise on ne doit prendre en compte qu'un octet  (le
dernier), ici c'est l'octet A6.
On peut donc  brute-forcer le mot de passe,  dcrypter le header  et pour chaque
cas voir si on a cet octet (12me octet du local file header)  qui correspond au
dernier octet du header dcrypt...
Mais si on s'en tient l on risque de trouver plusieurs mots de passe car il y a
un bon nombre de colisions.  Il faut alors valider ce  mot de passe en calculant
le CRC du fichier dcrypt.

Une autre info dont la spcification ne parle  pas du tout et sur laquelle je me
suis arrach  les cheveux  c'est que  des fois l'octet  correspondant  n'est pas
celui de l'heure mais le dernier octet du CRC...
En tudiant les sources de zipcracker je suis parvenu  la conclusion suivante :

si le bit 3 du flag est activ alors on prend l'octet de l'heure
sinon on prend en compte l'octet du CRC.

Dcryptage des donnes
----------------------
La dernire tape consiste alors  dcrypter les donnes et  calculer le crc.
Dcryptage :

loop until done
    read a character into C
    Temp <- C ^ decrypt_byte()
    update_keys(temp)
    output Temp
end loop

Voici un code un C qui dcrypte nos donnes avec le mot de passe :
--cut--cut--
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

/* ceci est un dump du fichier crypt tel qu'il tait dans le fichier zip */
char buff[]="\x09\xbf\xc2\x2a\x8a\x09\x88\x09\xfe\xdd\x7f\x02\xe2\xca\x42\x3a"
            "\xb1\x75\x70\xf5\x80\x37\x16\xc6\x18\x77\x28\x95\x7d\xc9\x2d\x53"
            "\x22\x6a\x77\x0a\xb1\x22\x52\xd8\xed\x58\xb7\x33\xdf\x95\xcc\xd9"
            "\x03\x56\x3e\xca\x34\x38\x10\xe0\x66\xaf\xc5\x45\xb2\x82\x36\x1f"
            "\xd6\x1d\x83\x49\x31\x47\xdb\xcf\xff\xac\x93\x76\x3e\x41\x0f\x10"
            "\x78\x77\x83\xb1\x68\xbf\x20\x8f\x22\x33\xc1\xb6\x8b\x8c\x9b\xe6"
            "\xfb\xc3\x35\x34\x9a\x40\x83\xe1\xe2\xc9\x08\x7f\xc0\x12\x52\x51"
            "\x3b\x80\xd4\x2c\x1d\xa4\x68\x7d\xd8\x7b\x24";

extern unsigned long mycrc32(unsigned long k,char c);

/* Nos trois cls */
unsigned long k0,k1,k2;

void update_keys(char c)
{
  k0 = mycrc32(k0,c);
  k1 = k1 + (k0 & 0x000000ff);
  k1 = k1 * 134775813L + 1;
  k2 = mycrc32(k2,(char)(k1>>24));
}

unsigned char decrypt_byte(void)
{
  unsigned char res;
  unsigned short temp;
  temp=(unsigned short)(k2|2);
  res=(unsigned char)((temp * (temp ^ 1)) >> 8);
  return res;
}

int main(int argc,char *argv[])
{
  char result[112];
  int i;
  char pass[]="test";
  unsigned char c;
  char buffer[12];

  /* Les valeurs initiales pour les cls, elles sont dfinies dans la spcif */
  k0=305419896L;
  k1=591751049L;
  k2=878082192L;

  for(i=0;i<strlen(pass);i++)
    update_keys(pass[i]);

  for(i=0;i<12;i++)
  {
    c=buff[i] ^ decrypt_byte();
    update_keys(c);
    buffer[i]=c;
  }

  printf("Decrypted header: ");
  for(i=0;i<12;i++)
    printf("%.2hhX",buffer[i]);
  printf("\n---------\n");
	
  for(i=0;i<sizeof(result);i++)
  {
    c = buff[i+12];
    c = c ^ decrypt_byte();
    update_keys(c);
    result[i]=c;
  }
  
  for(i=0;i<sizeof(result)-1;i++)
    printf("%c",result[i]);
  return 0;
}
--cut--cut--

(sirius@lotfree) (exemples) $ ./decrypt
Generating crc table...
Decrypted header: D607CFCA528A13060910AAA6
---------
La science est asymptote  la vrit, elle l'approche sans cesse et ne
la touche jamais.
        -+- Hugo, Victor -+-

Mission accomplished.  Par contre   vous d'inclure  des fonctions  de CRC  (par
exemple en utilisant la zlib).

Une attaque par brute-force suit donc l'algo suivant :
pour tout password
{
  initialiser les cls
  dcrypter le header
  si le dernier octet du header correspond  l'octet du local file header
  {
    dcrypter les donnes
    calculer le crc
    si le crc est le bon on affiche le pass et on quitte
  }
}

Infection d'un fichier zip
--------------------------
Pourquoi infecter un fichier zip ? Et comment ?
C'est vrai que l'infection de fichier zip n'est pas vraiment intressante.  Il y
a quand mme eu quelques cas de virii qui se propageaient par fichiers zip. Tout
le monde a  entendu parl de  ce virus qui se propageait  par mail sous la forme
d'un fichier zip protg par mot de passe. Le corps du mail contenait un message
qui disais  peut prs "Pour ouvrir l'archive, entre le mot de passe abcd".
Pourquoi de telles complications alors que un excutable aurait pu tre lanc de
faon automatique sous Outlook ?
Ce qui a pouss le crateur de  ce virus  crypter l'excutable c'est sans doute
le fait que les antivirus ne peuvent pas scanner le zip s'il n'ont pas le mot de
passe (tous les antivirus rcents peuvent ouvrir les archives tar.gz,  zip,  rar
et mme les fichiers uuencods.)
Ensuite le  crateur du virus  a jou sur  la curiosit des  personnes qui rece-
vaient le mail.  Pour  l'anecdote  je connais quelqu'un  qui a fait un  stage au
CNRS et  qui travaillait avec l'admin rseau.  L'admin a fait circul un mail en
interne prvenant  qu'un virus se  propageait  sous la  forme d'une  archive zip
crypte et  qu'il ne fallait  surtout pas l'ouvrir...  Le problme semblait donc
rsolu pourtant quelques jours plus tard,  l'admin reoit un mail de l'un de ses
collgues qui disait  peu prs :
"Je suis dsol j'ai reu le virus  et j'ai pas pu m'empcher de regarder ce qu'
il y avait dans l'archive..."

Evidemment il s'agit peut-tre d'un cas de gros boulet :D
Mais au final  le crateur de  virus a bien russi   utiliser la  curiosit des
gens :p

L'infection que  je vais dcrire  dans la suite  de cet article  n'est pas  trs
avance.  On pourrait par exemple scanner des  fichiers zip et infecter tous les
excutables dans ces fichiers zip,  ou tout simplement remplacer  une entre par
une autre.
Ici on va simplement  ajouter une entre   une archive zip,  cette mthode peut
donc marcher en utilisant la curiosit des gens.

L'ajout d'un  fichier dans une  archive  zip sous  Linux se  fait normalement en
utilisant  l'option -g.  Ici nous allons juste  rajouter une  deuxime fortune 
notre archive.

(sirius@lotfree) (exemples) $ fortune > fortune2.txt
(sirius@lotfree) (exemples) $ cat fortune2.txt 
Le vrai moyen d'tre tromp, c'est de se croire plus fin que les autres.
        -+- Franois de La Rochefoucauld (1613-1680), Maximes 127 -+-
(sirius@lotfree) (exemples) $ ls -l fortune2.txt 
-rw-r--r--    1 sirius   sirius        136 2005-03-03 11:13 fortune2.txt
(sirius@lotfree) (exemples) $ crc32 fortune2.txt
3c253319

(sirius@lotfree) (exemples) $ zip fortune.zip -g -0 fortune2.txt 
  adding: fortune2.txt (stored 0%)
(sirius@lotfree) (exemples) $ zipinfo fortune.zip 
Archive:  fortune.zip   535 bytes   2 files
-rw-r--r--  2.3 unx      111 bx stor  2-Mar-05 20:53 fortune.txt
-rw-r--r--  2.3 unx      136 bx stor  3-Mar-05 11:13 fortune2.txt
2 files, 247 bytes uncompressed, 247 bytes compressed:  0.0%

Une tude approfondie nous prouve que le fichier fortune2.txt a bien t rajout
en fin de l'archive.
Le local file header de fortune.txt n'a videmment pas chang.  Cela est vident
puisque les infos de  ce header sont spcifiques  l'entre et ne comportant pas
par exemple  d'adresses relatives etc.  Toutefois la structure central directory
de ce  mme fichier  n'a  pas  chang non  plus.  Le seul  attribut  extrieur 
l'entre est  l'adresse relative  du local  header  partir du dbut du fichier,
hors cette adresse n'a pas chang  car nous avons ajout un fichier aprs et non
avant.

Ajouter une entre en  fin d'archive nous  facilite donc fortement la tache.  Si
nous  avions  du la  placer au  dbut,  nous aurions  du recalculer  toutes  les
adresses relatives :(

Le plus  gros du  travail  se  fera sur  l'enregistrement  de fin  du rpertoire
central (end of central directory record).
On supposera que nous infectons une archive complte (non splitte).
Les valeurs  modifier dans cette structure sont les suivantes :

Total number of entries in the central directory of this disk
Total number of entries in the central directory

Ces deux valeurs sont  incrmenter de une unit.

Size of the central directory
Nouvelle valeur = ancienne valeur + 46 + length(nom fichier)
avec 46 = taille d'une structure central directory
Evidemment on n'utilisera ni champ optionnel ni commentaires pour simplifier les
choses.


Offset of start  of central directory  with respect to the  starting disk number
Nouvelle valeur = ancienne valeur + 30 + usize + length(nom fichier)
avec 30 = longeur d'un local header et usize la taille des donnes  (les donnes
ne sont pas compresses)

Dans la pratique
----------------
La modification d'un  fichier zip  poserait trop  de problmes comme on souhaite
ajouter des donnes en plein milieu du fichier...
Le plus simple est de crer un  fichier en criture (.inject.zip) et d'ouvrir le
fichier  infecter (fortune.zip) en lecture. 
Nous lisons ensuite le fichier fortune.zip sections aprs sections.
L'alorithme est  peu prs le suivant :

Tant qu'on tombe sur des structures Local File Header, on copie les donnes en
brut d'un fichier  l'autre.

Ds qu'on est sur une autre structure on injecte notre Local File Header  nous.
On note aussi  l'emplacement car il  faudra le spcifier  dans le CDIR  que nous
allons crer.

On recopie les CDIRs prsents dans le fichier tout comme on a recopi les LFH.

Ds qu'on tombe sur la signature de  l'enregistrement de fin de CDIR, on injecte
notre Central Directory,  en oubliant pas de mettre l'offset rcupr  prcdem-
ment.

On enregistre alors la structure de fin pour la modifier (nombre d'entres, etc)
et on l'crit dans notre fichier.

Note: pour les structures ayant des champs variables  (nom de fichier,  donnes,
commentaires...)  il faut bien videmment dcrypter  les headers et recopier les
donnes en brut.

Voici un petit code qui illustre le processus d'infection :

--cut--cut--
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

/* Les autres fichiers sont dans le repertoire zinfect */
#include "zip.h"

/* dans cet exemple la plupart des valeurs sont en dur */
#define FORTUNE "Le vrai moyen d'tre tromp, c'est de se croire plus fin que les autres.\n\
\t-+- Franois de La Rochefoucauld (1613-1680), Maximes 127 -+-\n"
#define FILE_NAME "fortune2.txt"
#define TMP_FILE ".infect.zip"
#define VICTIM "fortune.zip"

int main(int argc,char *argv[])
{
  struct lfh z;
  struct lfh head;
  struct cdir cd;
  struct cdir cds;
  struct eocdir eocd;
  int fdin, fdout;
  unsigned long sign;
  char *s;
  char buf[CDIR_LEN+1];
  unsigned long reste, x;
  unsigned long pos_lfh;
  
  /* On remplit les structures de l'entre  rajouter */
  z.version=cd.vmadeby=cd.vneeded=10; /* v1.0 */
  z.gpf=cd.gpf=0;                     /* pas de cryptage... */
  z.comp=cd.comp=0;                   /* pas de compression */
  z.time=cd.time=22963;
  z.date=cd.date=12899;
  z.crc=cd.crc=0x3c253319L;           /* ! le CRC doit tre exact ! */
  z.usize=cd.usize=sizeof(FORTUNE)-1;
  z.zsize=cd.zsize=sizeof(FORTUNE)-1;
  z.name_len=cd.name_len=strlen(FILE_NAME);
  z.xtra_len=cd.xtra_len=0;

  cd.comm_len=0;  /* pas besoin de s'emmerder avec les champs optionnels */
  cd.disk_num=0;
  cd.int_attr=1;  /* 1 pour ascii, 0 pour binaire */
  cd.ext_attr=32; /* euh pas le courage de savoir pourquoi */

  /* Cette variable contiendra l'ofset o injecter le Local File Header */
  pos_lfh=0L;
  fdin=open(VICTIM,O_RDONLY);
  fdout=creat(TMP_FILE,S_IRUSR|S_IWUSR);
  while(1)
  {
    /* On lit 4 octets (taille d'une signature zip) */
    if((x=read(fdin,buf,sizeof(LFH_SIGN)))==-1)
    {
      exit(1);
    }
    bcopy(buf,&sign,sizeof(LFH_SIGN));
    /* S'il s'agit d'un LFH ou d'une structure Special Spanning : copie brute */
    if(sign==LFH_SIGN || sign==SPSPAN_SIGN)
    {
      write(fdout,&sign,sizeof(LFH_SIGN));
      if(sign==LFH_SIGN)
      {
	if((x=read(fdin,buf,LFH_LEN-sizeof(LFH_SIGN)))==-1)
	{
	  perror("read");
	  exit(1);
	}
	/* On doit lire les headers pour grer les noms de fichiers etc */
	buff2lfh(buf,&head);
	write(fdout,buf,x);
	if(head.name_len!=0)
	{
	  s=(char*)malloc(head.name_len);
	  if(read(fdin,s,head.name_len)==-1)
	  {
	    perror("read");
	    exit(1);
	  }
	  write(fdout,s,head.name_len);
	  free(s);
	}
	if(head.xtra_len!=0)
	{
	  s=(char*)malloc(head.xtra_len);
	  if(read(fdin,s,head.xtra_len)==-1)
	  {
	    perror("read");
	    exit(1);
	  }
	  write(fdout,s,head.xtra_len);
	  free(s);
	}
	if(head.zsize!=0)
	{
	  reste=head.zsize;
	  while((x=read(fdin,buf,(reste>sizeof(buf))?sizeof(buf):reste)))
	  {
	    reste-=x;
	    write(fdout,buf,x);
	  }
	}
      }
      /* Special Spanning : 12 octets de donnes */
      else
      {
	read(fdin,buf,12);
	write(fdout,buf,12);
      }
    }
    else
    {
      /* Pour savoir si on a dj infect le LFH */
      if(!pos_lfh)
      {
        /* On injecte notre header + filename + data */
	pos_lfh=lseek(fdin,0,SEEK_CUR)-4;
	writelfh(fdout,z);
	write(fdout,FILE_NAME,strlen(FILE_NAME));
	write(fdout,FORTUNE,sizeof(FORTUNE)-1);
      }
      if(sign==CDIR_SIGN)
      {
        /* recopie... */
	write(fdout,&sign,sizeof(CDIR_SIGN));
	if((x=read(fdin,buf,CDIR_LEN-sizeof(CDIR_SIGN)))==-1)
	{
	  perror("read");
	  exit(1);
	}
	buff2cdir(buf,&cds);
	write(fdout,buf,x);
	if(cds.name_len!=0)
	{
	  s=(char*)malloc(cds.name_len);
	  if(read(fdin,s,cds.name_len)==-1)
	  {
	    perror("read");
	    exit(1);
	  }
	  write(fdout,s,cds.name_len);
	  free(s);
	}
	if(cds.xtra_len!=0)
	{
	  s=(char*)malloc(cds.xtra_len);
	  if(read(fdin,s,cds.xtra_len)==-1)
	  {
	    perror("read");
	    exit(1);
	  }
	  write(fdout,s,cds.xtra_len);
	  free(s);
	}
	if(cds.comm_len!=0)
	{
	  s=(char*)malloc(cds.comm_len);
	  if(read(fdin,s,cds.comm_len)==-1)
	  {
	    perror("read");
	    exit(1);
	  }
	  write(fdout,s,cds.comm_len);
	  free(s);
	}
      }
      else if(sign==EOCDIR_SIGN) /* c'est le moment d'insrer notre cdir */
      {
	cd.roffset=pos_lfh;
	writecdir(fdout,cd);
	write(fdout,FILE_NAME,strlen(FILE_NAME));
	if((x=read(fdin,buf,EOCDIR_LEN-sizeof(EOCDIR_SIGN)))==-1)
	{
	  perror("read");
	  exit(1);
	}
	/* Le plus dur du code... */
	buff2eocdir(buf,&eocd); /* On dcode le header */
	eocd.nb_cd++;           /* On incrmente le nombre d'entres */
	eocd.nb_ent_cd++;       /* pareil */
	eocd.cd_size+=CDIR_LEN+strlen(FILE_NAME); /* 46 + longeur nom fichier */
	/* L'offset du Central Directory a chang... on le met  jour */
	eocd.first_disk+=LFH_LEN+strlen(FILE_NAME)+sizeof(FORTUNE)-1;
	/* On crit le nomveau EOCDIR */
	writeeocdir(fdout,eocd);
	break;
      }
      else /* l'appot con pris ! (un header qu'on ne gre pas) */
      {
	break;
      }
    }
  }
  /* Terminus, tout le monde descend */
  close(fdout);
  close(fdin);

  /* On remplace le fichier original par la version infecte */
  unlink(VICTIM);
  link(TMP_FILE,VICTIM);
  unlink(TMP_FILE);
  return 0;
}
--cut--cut--

(sirius@lotfree) (infect) $ gcc -c zinfect.c
(sirius@lotfree) (infect) $ gcc -c lzutil.c
(sirius@lotfree) (infect) $ gcc -o zinfect *.o
(sirius@lotfree) (infect) $ ls -l fortune.zip 
-rw-r--r--    1 sirius   sirius        265 2005-03-06 13:13 fortune.zip
(sirius@lotfree) (infect) $ unzip -l fortune.zip 
Archive:  fortune.zip
  Length     Date   Time    Name
--------    ----   ----    ----
     111  03-02-05 20:53   fortune.txt
--------                   -------
     111                   1 file
(sirius@lotfree) (infect) $ ./zinfect 
(sirius@lotfree) (infect) $ ls -l fortune.zip 
-rw-------    1 sirius   sirius        501 2005-03-09 23:47 fortune.zip
(sirius@lotfree) (infect) $ unzip -l fortune.zip 
Archive:  fortune.zip
  Length     Date   Time    Name
--------    ----   ----    ----
     111  03-02-05 20:53   fortune.txt
     136  03-03-05 11:13   fortune2.txt
--------                   -------
     247                   2 files

w00t !! ;)
