Description du format de stockage forensique Encase/EWF

Magazine
Marque
MISC
Numéro
117
Mois de parution
septembre 2021
Spécialité(s)


Résumé

Dans le cadre de réponses sur incidents, les CERT/CSIRT peuvent être amenés à réaliser des analyses post-mortem à partir des disques ou plus généralement de supports de stockage. Ainsi, il est proposé dans cet article d’énumérer les requis pour collecter, stocker, utiliser efficacement une copie de disque en vue d’une analyse forensique. Nous détaillerons comment le format EWF met en œuvre ses besoins, et en décrirons suffisamment les principes techniques pour savoir accéder à un secteur précis dans l’image du disque.


Body

Avant de comprendre comment cela est mis en œuvre par le format de données, voyons d’abord ce que l’on attend d’un format capable d’imager un disque (en stocker une copie fidèle). Enfin, EWF signifie Expert Witness Format, ce qui est moins groovy qu’un groupe des années 70.

1. Propriétés nécessaires à un format d’image disque pour le forensique

Effectuer une copie secteur par secteur : c’est une méthode courante et la plus fidèle pour réaliser une image du support de stockage rapidement. Cela autorise également l’analyse ultérieure des 3 niveaux de données : 1 - l’amorce du disque, les partitions ; 2 - les systèmes de fichiers, les données effacées ou cachées ; 3 - ainsi que les métadonnées de ces 3 niveaux.

Aujourd’hui, il est courant d’avoir à copier plusieurs centaines de gigaoctets ou plusieurs téras, ainsi l’utilisation de la compression est indispensable.

Mais on doit pouvoir utiliser le fichier image comme « disque virtuel », pour notre analyse forensique, ce qui implique de pouvoir adresser directement n’importe quel secteur dans l’image, comme sur un disque physique. Cette dernière propriété est incompatible avec la compression de l’image complète. Il est donc logique de découper l’image en plusieurs parties, compressées, et il est pratique qu’elles contiennent un nombre entier de secteurs, car c’est l’unité élémentaire de stockage sur un support.

Afin de s’assurer que plusieurs copies d’un même disque sont identiques et de leur intégrité (requis par un juge dans le cadre d'une expertise judiciaire), on utilisera un algorithme de haché cryptographique comme MD5 ou SHA1. Le format devra également permettre de stocker les métadonnées du disque, comme le numéro de série, le modèle, et les propriétés du support, qu’il soit SSD ou magnétique.

Lors de la création de l’image, on doit archiver/compresser les secteurs à la volée, mais également les métadonnées nécessaires à la lecture ultérieure des secteurs « originaux ».

Le format EWF, développé vers 2002 par la société ASRDATA [ASRDATA], a été le premier format de stockage offrant les fonctionnalités citées précédemment, et qui justement font défaut au format « raw » historique : compression, métadonnées et contrôle d’intégrité.

2. Le format EWF / Encase / E01

2.1 Origines et multiples versions

Comme il existe de multiples versions du format EWF, dans la suite de l’article nous parlerons de la plus courante, la version 1, celle utilisant les caractères « E01 » comme extension pour le premier fichier de l’image, celle utilisée par Encase 4, implémentation de référence de l’éditeur Guidance Software. Aucune documentation officielle n’est disponible, hormis celle d’ASRDATA, très succincte. C’est Michael « Scudette » Cohen qui fût le premier à réaliser une implémentation open source dès 2004 [LIBEVF], travail repris en 2006 par Joachim Metz qui propose une spécification détaillée des variants EWF et une bibliothèque [LIBEWF]. Cette implémentation est utilisée par Ewfacquire [EWFACQUIRE]. FTK lmager [FTKIMAGER] utilise une implémentation maison (ADI) et compatible.

2.2 Description générale du format EWF

La description du format ne sera pas exhaustive, il y a [LIBEWF] pour cela, mais suffisante pour en comprendre les grands principes, ses avantages et ses inconvénients, et retrouver les données d’un secteur original.

2.2.1 Segments

Tout d’abord, l’image est divisée en plusieurs fichiers nommés « segments », par défaut de 1500Mo. Les extensions de fichiers de ces segments sont énumérées de E01 à E99, puis EAA, EAB, jusqu’à EZZ, puis FZZ, ... jusqu’à ZZZ, ce qui est compatible avec l’ordre lexicographique. Ceci est pratique, car lors de la création d’une image, les segments sont créés dans cet ordre, par ordre croissant des secteurs archivés. Dans l’exemple ci-dessous, on trouve l’image d’une clé USB de 4Go, découpée en 3 segments et fichiers :

06/02/2020 16:06     1 572 831 275 usb.E01
06/02/2020 16:08     1 572 852 914 usb.E02
06/02/2020 16:08       157 406 087 usb.E03

2.2.2 Compression par « chunk »

Dans chaque segment, les secteurs du disque original sont regroupés en séquence, par « chunks », ces derniers pouvant être compressés si cela est utile, lorsque le chiffrement est absent, par exemple. C’est un bon compromis entre compresser chaque secteur et le disque original en entier, comme avec le format « raw ». La taille d’un chunk est de 64 secteurs par défaut. Le format et l’algorithme de compression sont Zlib/Deflate [ZLIB][DEFLATE].

À l’intérieur des fichiers segments, l’information est organisée par « sections », notamment et dans cet ordre :

  • Header section : au début de chaque segment, contient le numéro de segment ;
  • les sections Volume ou Disk : elles contiennent le nombre total de chunks et des secteurs de l’image, la taille d’un chunk et d’un secteur ;
  • Sectors section : qui contient les chunks ;
  • Table section : qui contient des pointeurs sur les chunks du segment, compressés ou non ;
  • à la fin d’un segment, une section Next ou Done, pour indiquer qu’il existe un segment suivant, ou non ;
  • les sections Hash ou Digest pour stocker les hachés comme MD5 et SHA1, dans le dernier segment. SHA256 est également disponible, mais non utilisé par défaut.

De même que l’ordre des segments, l’ordre des sections dans un segment est chronologique à la lecture du média original, secteur par secteur : on écrit d’abord les métadonnées (sections Header et Volume), puis les données elles-mêmes (Sectors), puis comment accéder à ces données (Table).

Examinons maintenant plus en détail les segments et les sections qu’ils contiennent.

2.3 Voyons cela d’un peu plus près

Un segment commence la chaîne signature suivante (format Python) :

b'EVF\t\r\n\xff\x00'

puis une liste chaînée de sections comme sur la figure 1.

segments1bis-s 0

Figure 1 : Segments.

Toutes les sections commencent par une description générique, avec les données ci-dessous.

2.3.1 Entête générique des sections

Offset

Type

Taille en octets

Champ

Commentaire ou exemple

0

char

16

marqueur

b'sectors\x00\x00\x00\x00\x00\x00\x00\x00\x00'

16

long

1

next

Offset dans le segment

20

long

1

size

Y compris l’entête

24

char

40

padding

0

64

long

1

Crc

adler32

Voyons ensuite comment accéder aux chunks.

2.4 Quelques sections importantes

Les 2 sections les plus importantes sont Table et Sectors.

2.4.1 Section Table

Après l’entête de la section décrite précédemment, nous avons la structure suivante :

Offset

Type

Taille

Champ

Commentaire ou exemple

0

long

1

Chunk count

62184 par exemple par segment

4

byte

4

padding1

0

8

longlong

1

base

Offset de la section Sectors dans le segment

16

byte

4

padding2

0

20

long

1

Crc

Adler32 of data[:64]

24

long[]

Chunk count

pointers

Pointeurs dans la section Sectors (31 bits). Le bit#31 de point fort indique la compression

Chunk count+24

long

1

Crc

Adler32 de la table pointers[]

Cette section est stockée 2 fois par segment, dans la section « table » et « table2 », identiques, sans doute pour redondance. En effet, il serait difficile de retrouver sans erreur le début de chaque chunk sans cette « Table ».

2.4.2 Section Sectors

Après l’entête de la section, chaque chunk est stocké tel quel, en séquence, compressé ou non. À la fin de la section se trouve une somme de contrôle Adler32, pour vérifier l’intégrité des données des chunks.

2.4.3 Sections Disk et Volume

Comme déjà écrit, elles contiennent des données sur l’image, les chunks et les secteurs, de façon similaire. La section Volume semble une évolution de la section Disk.

2.4.4 Sections Hash et Digest

Ici sont stockés les hashs cryptographiques MD5 et/ou SHA1 de l’image originale complète (avant d’être découpée en chunks). Il est possible également d’ajouter SHA256.

Maintenant que nous avons bien compris la structure de données du format EWF, passons maintenant à la pratique : comment « monter » l’image, c’est-à-dire accéder aux données des secteurs originaux.

3. Montage de l’image comme disque virtuel

À partir des éléments décrits précédemment, voici les principes pour utiliser l’image comme disque virtuel, que nous compléterons à la fin de l’article par un exemple réel.

3.1 Algorithme général

  1. Énumérer les fichiers segments (ordre lexicographique)

  2. Pour chaque segment, vérifier la signature et parcourir la liste de sections

  3. Pour chaque section de type :

    • [Volume ou Disk] : stocker le nombre de chunk de l’image, sa taille, celle d’un secteur

    • [Sectors] : on viendra lire, comme un read(), les données des secteurs originaux ici. Y sont stockés les chunks du segment (compressé ou non, en séquence)

    • [Table] : On récupère ici le nombre de chunk du segment (taille de la table), l’offset de la section Sector (base). Dans la table, pour chaque chunk du segment, on trouve : un pointeur sur 31 bits (offset dans Sector) et le bit de point fort indiquant s’il est compressé (1=compressed)

Il faut créer un index des chunk pour l’image, pour retrouver dans quel segment se trouve un chunk, car la taille d’un chunk varie avec la compression. Il faut aussi noter la fin de la section Sectors, pour lire le dernier chunk du segment, car la taille compressée n’est stockée nulle part.

Voyons justement comme lire le secteur numéro « sector ».

3.2 Accès à un secteur arbitraire dans une image EWF

Pour réaliser la fonction seek_ewf(sector), voici l’algorithme :

1. chunk_number = sector // chunk_size
2. sector_in_chunk = sector % chunk_size
3. on détermine dans quel fichier segment se trouve le chunk (avec notre index) et son indice dans la section Table.
4. Avec la base et l’offset (section Table), on trouve le début du chunk dans le fichier (section Sectors)
4. on charge le chunk en mémoire et on le décompresse éventuellement
5. on accède au secteur dans le chunk, à l’offset sector_in_chunk * sector_size

3.3 Pas très hexy cet article

Ok, ok, voyons juste un extrait de la section sectors et de la section table, pour trouver le début d’un chunk compressé...

En figure 2, le début de la section « sectors » : en rouge le marqueur (à l’offset 0x6f2, si si), en vert (0x5db7e7cf) la section suivante qui est « table », en orange la taille de la section (0x5db7e0dd, la majeure partie du segment), en violet la valeur Adler32 et enfin en bleu, le début du chunk compressé. Comment trouve-t-on cet offset 0x73e ? Avec la section Table juste en dessous et qui suit dans le segment.

sectors-s

Figure 2

Voici, en figure 3, le début de la section « table », à l’offset prévu dans la section précédente (next en vert), on y trouve : en rouge le marqueur, en violet la somme Adler32, en orange le nombre de chunks dans le segment (0xf2e8, soit 62184) et donc combien de pointeurs dans la Table. En vert pâle, on trouve la base (offset 0x6f2 de la section Sectors), une autre Adler32 et enfin le pointeur sur le premier chunk : 0x800004c. Il est compressé et le début des données Deflate est à base=0x6f2 + 0x4c = 0x73e.

table-s

Figure 3

Enfin, voici quelques éléments qui montrent que le format n’a pas de description officielle : FTK Imager utilise une section Disk plutôt que Volume, et n’utilise pas le champ base de la section Table : les pointeurs stockés ont comme base 0, c’est-à-dire le début du segment. Heureusement, le champ contient cette valeur.

4. Un driver EWF minimaliste en Python

Mettons en musique ce que nous avons appris précédemment sur un véritable exemple : celui de la clé USB en 3 segments vu au début de l’article. On lance notre outil d’analyse ewf.py avec de la verbosité et le nom du premier fichier de l’image : « usb.E01 », les 2 autres segments étant « usb.E02 » et « usb.E03 ».

>python ewf.py -v 1 usb.E01

Le code va donc énumérer chaque fichier/segment, puis en analyser chaque section. Voici le résultat :

usb.E01
header(signature=b'EVF\t\r\n\xff\x00', one=1, segment_num=1, zero=0)
0x00000059: type:b'header2\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:fe size:f1
0x000000fe: type:b'header2\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:1ef size:f1
0x000001ef: type:b'header\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:28a size:9b
0x0000028a: type:b'volume\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:6f2 size:468
0x000006f2: type:b'sectors\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:5db7e7cf size:5db7e0dd
0x5db7e7cf: type:b'table\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:5dbbb3d7 size:3cc08
0x5dbbb3d7: type:b'table2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:5dbf7fdf size:3cc08
0x5dbf7fdf: type:b'next\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:5dbf7fdf size:0
usb.E02
header(signature=b'EVF\t\r\n\xff\x00', one=1, segment_num=2, zero=0)
0x00000059: type:b'data\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:475 size:468
0x00000475: type:b'sectors\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:5db993ee size:5db98f79
0x5db993ee: type:b'table\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:5dbcb42a size:3203c
0x5dbcb42a: type:b'table2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:5dbfd466 size:3203c
0x5dbfd466: type:b'next\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:5dbfd466 size:0
usb.E03
header(signature=b'EVF\t\r\n\xff\x00', one=1, segment_num=3, zero=0)
0x00000059: type:b'data\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:475 size:468
0x00000475: type:b'sectors\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:96134e3 size:961306e
0x096134e3: type:b'table\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:96183d7 size:4ef4
0x096183d7: type:b'table2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:961d2cb size:4ef4
0x0961d2cb: type:b'hash\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:961d33b size:70
0x0961d33b: type:b'done\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' next:961d33b size:0

Ci-dessus, chaque segment commence donc par un header « EVF », puis sur les lignes suivantes, chaque nom de section est préfixé par son offset dans le segment, et suffixé par les champs next et size.

Ci-dessous, l’outil affiche ensuite quelques statistiques résultant de l’analyse des métadonnées de l’image, vous vous souvenez de notre « algorithme général » plus haut ?

chunk_count:0x1ce80, sectors_per_chunk:0x40, bytes_per_sector:0x200, sector_count:0x73a000
md5: b'4e009a14d2f73b7bbc52a5ea3a1b5105'
segment #1, filename: usb.E01
  chunks count: 62184 (including uncompressed:22777, 36.63%)
  data offsets: first:0x73e last:0x5db767cb
  absolute chunk number ranges (0, 62183)
  end_of_sectors: 0x5db7e7cf
segment #2, filename: usb.E02
  chunks count: 51189 (including uncompressed:15321, 29.93%)
  data offsets: first:0x4c1 last:0x5db914ba
  absolute chunk number ranges (62184, 113372)
  end_of_sectors: 0x5db993ee
segment #3, filename: usb.E03
  chunks count: 5027 (including uncompressed:2896, 57.61%)
  data offsets: first:0x4c1 last:0x960b4df
  absolute chunk number ranges (113373, 118399)
  end_of_sectors: 0x96134e3
#0 boot=0x80 type=0x0c start=0x00002678 size=0x00737988

À noter que la valeur md5 ci-dessus a été calculée par l’outil sur les secteurs originaux et stockée dans la section hash, visible dans le segment #3.

Et si l’on calculait à nouveau le md5 de l’image, mais en utilisant seek() et read() de notre driver EWF ?

print( hexlify( compute_image_hash2( ewf, md5() ) ) )

avec la fonction suivante :

BUFFER_SIZE = 0x40*512-1 #to assess read()
def compute_image_hash2(ewf, md):
  ewf.seek(0)
  
  data = ewf.read( BUFFER_SIZE )
  while len(data) > 0:
    md.update( data )
    if len(data) < BUFFER_SIZE: #detect short read
     return md.digest()
    data = ewf.read( BUFFER_SIZE )
  return md.digest()

Bingo, on obtient :

b'4e009a14d2f73b7bbc52a5ea3a1b5105'

Et pour les curieux/ses, le code « jouet » en question est disponible sur GitHub [MINIEWF].

Conclusion : promesses tenues pour le format EWF ?

On ne va pas divulgâcher la fin, car ce format est utilisé depuis plus de 10 ans, mais oui, dans les grandes lignes : il offre la compression (avec les chunks), l’intégrité (avec Adler32), et l’on peut l’utiliser directement comme disque virtuel.

Quelles sont ces faiblesses ? On pourrait améliorer le contrôle d’intégrité des métadonnées (avec sha256), offrir le chiffrement à la volée des données, plusieurs flux de données (comme dans une archive ZIP) et pas simplement celui des secteurs, une compression plus rapide, un stockage moderne des métadonnées (JSON ?), une « chain of custody » de l’acquisition et de l’analyse du média garantie dans la blockchain ;-) ?

Le format AFF4 [AFF4] n’offre pas encore le chiffrement par exemple, mais certaines évolutions de EWF, si : Encase 7 et EWF2 proposent ainsi la compression bzip2 et le chiffrement AES256, mais cela reste non documenté.

Remerciements

Merci à Nidhal Ben Aloui et au rédacteur en chef pour leurs relectures attentives et suggestions.

Références

[ASRDATA] E01 Compression Format, ASRDATA, around 2002, http://www.asrdata.com/whitepaper-html/

[LIBEVF] libevf code source, Michael Cohen, 2008, https://github.com/py4n6/pyflag/blob/master/src/lib/libevf.c

[EWFACQUIRE] Ewfacquire code source, Joachim Metz, 2006-2021, https://github.com/libyal/libewf/blob/main/ewftools/ewfacquire.c

[LIBEWF] EWF specification, Joachim Metz, 2006-2020, https://github.com/libyal/libewf/blob/main/documentation/Expert%20Witness%20Compression%20Format%20(EWF).asciidoc

[FTKIMAGER] https://accessdata.com/products-services/forensic-toolkit-ftk/ftkimager

[ZLIB] ZLIB Compressed Data Format Specification version 3.3, 1996, https://datatracker.ietf.org/doc/html/rfc1950

[DEFLATE] DEFLATE Compressed Data Format Specification version 1.3, 1996, https://datatracker.ietf.org/doc/html/rfc1951

[AFF4] Advanced Forensic Format v4 imager and documentation, 2017, http://docs.aff4.org/en/latest/

[MINIEWF] https://github.com/lclevy/miniEwf



Article rédigé par

Par le(s) même(s) auteur(s)

Analyse statique des exécutables Windows, un aperçu

Magazine
Marque
MISC
Numéro
120
Mois de parution
mars 2022
Spécialité(s)
Résumé

Il existe deux méthodes principales pour trier rapidement des exécutables suspects : les lancer dans une sandbox (analyse dynamique) et l'analyse... statique. Nous proposons un article en deux parties pour décrire cette dernière appliquée aux fichiers Windows Portable Executable (PE). Dans ce numéro, nous verrons comment utiliser certaines propriétés de ce format, déceler des anomalies, dont certaines peuvent révéler le caractère malveillant du fichier. Dans un prochain numéro, nous étudierons en détail le format PE lui-même, comment le loader Windows l'utilise, et comment certains malwares le contournent pour mieux passer sous le radar.

Description du système de fichiers ExFAT

Magazine
Marque
MISC
Numéro
118
Mois de parution
novembre 2021
Spécialité(s)
Résumé

Le système de fichiers ExFAT se rencontre dans l’embarqué, par exemple sur les cartes mémoire des appareils photo numériques ou celles des smartphones Android. Nous allons voir dans cet article comment fonctionne ce système de stockage très simple, et les améliorations par rapport à FAT32. Nous irons jusqu’à comprendre comment localiser les métadonnées, lire le contenu d’un fichier et d’un répertoire.

Les derniers articles Premiums

Les derniers articles Premium

Bénéficiez de statistiques de fréquentations web légères et respectueuses avec Plausible Analytics

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Pour être visible sur le Web, un site est indispensable, cela va de soi. Mais il est impossible d’en évaluer le succès, ni celui de ses améliorations, sans établir de statistiques de fréquentation : combien de visiteurs ? Combien de pages consultées ? Quel temps passé ? Comment savoir si le nouveau design plaît réellement ? Autant de questions auxquelles Plausible se propose de répondre.

Quarkus : applications Java pour conteneurs

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Initié par Red Hat, il y a quelques années le projet Quarkus a pris son envol et en est désormais à sa troisième version majeure. Il propose un cadre d’exécution pour une application de Java radicalement différente, où son exécution ultra optimisée en fait un parfait candidat pour le déploiement sur des conteneurs tels que ceux de Docker ou Podman. Quarkus va même encore plus loin, en permettant de transformer l’application Java en un exécutable natif ! Voici une rapide introduction, par la pratique, à cet incroyable framework, qui nous offrira l’opportunité d’illustrer également sa facilité de prise en main.

Les listes de lecture

11 article(s) - ajoutée le 01/07/2020
Clé de voûte d'une infrastructure Windows, Active Directory est l'une des cibles les plus appréciées des attaquants. Les articles regroupés dans cette liste vous permettront de découvrir l'état de la menace, les attaques et, bien sûr, les contre-mesures.
8 article(s) - ajoutée le 13/10/2020
Découvrez les méthodologies d'analyse de la sécurité des terminaux mobiles au travers d'exemples concrets sur Android et iOS.
10 article(s) - ajoutée le 13/10/2020
Vous retrouverez ici un ensemble d'articles sur les usages contemporains de la cryptographie (whitebox, courbes elliptiques, embarqué, post-quantique), qu'il s'agisse de rechercher des vulnérabilités ou simplement comprendre les fondamentaux du domaine.
Voir les 66 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous