Analyse statique des exécutables Windows : la structure PE

Magazine
Marque
MISC
Numéro
121
Mois de parution
mai 2022
Spécialité(s)


Résumé

Il existe deux principales méthodes pour trier rapidement des exécutables suspects : les lancer dans une sandbox (analyse dynamique) et l'analyse... statique. Lors de la première partie, nous avons énuméré et discuté de certaines caractéristiques du format Portable Executable (PE) pour déceler des anomalies parfois utilisées par les auteurs de malwares. Nous proposons dans cette deuxième partie de détailler la structure PE, tout en continuant à étudier comment elle est utilisée par le code malveillant.


Body

1. Les prérequis d'un format « exécutable »

1.1 Fichier tu étais, processus tu deviendras !

Pour rappel, les fichiers au format PE décrivent un exécutable (.exe) pour la plateforme Windows, c'est-à-dire un format de fichier dont le contenu (découpé en sections) est destiné à devenir un processus. Il y a généralement une section contenant le code (nommée souvent « .text »), une section pour les données constantes (« .rdata »), une pour les ressources graphiques entre autres (« .rsrc »), etc. Les .dll utilisent aussi le format d'une manière très similaire.

Dans le fichier PE, les adresses sont dites « physiques » et en mémoire (celle du futur processus), elles sont « virtuelles » et relatives à une adresse de base : Relative Virtual Address. Les systèmes d'exploitation utilisent en effet un adressage virtuel : chaque processus croit disposer d'un adressage qui lui est propre.

Voici pour rappel le premier exemple de sections donné dans la 1re partie :

ImageBase 0x400000, AddressOfEntryPoint 0x4342
sections at 0x1c8
   name        vsize    vaddr    psize    paddr charact
00 .text    00007c38 00001000 00008000 00001000 MEM_READ MEM_EXECUTE CNT_CODE
01 .rdata   00000d08 00009000 00001000 00009000 MEM_READ CNT_INITIALIZED_DATA
02 .data    000029b8 0000a000 00002000 0000a000 MEM_WRITE MEM_READ CNT_INITIALIZED_DATA

Les colonnes psize / paddr concernent les sections physiques sur disque, et vsize / vaddr leurs versions en mémoire. La colonne des caractéristiques liste les propriétés des sections en mémoire : « execute », « write » ou « read »...

1.2 Windows et ses bibliothèques partagées

Pour communiquer via le réseau, manipuler des fenêtres ou des fichiers, les fonctions nécessaires sont regroupées par le système d'exploitation sous forme de bibliothèques, le plus souvent chargées dynamiquement (.dll sous Windows, .so sous UNIX). Par exemple, la fonction FindFirstFileA, permettant de connaître le premier fichier dans un répertoire, est située dans la bibliothèque KERNEL32.DLL. Il faut donc charger cette DLL en mémoire afin que le code principal (.exe) puisse utiliser l'implémentation de cette fonction.

Par ailleurs, la partie « imports » du format PE liste les fonctions externes utilisées et où les trouver (dans quel fichier DLL). Au contraire, la partie « exports » liste des fonctions offertes (implémentées) par une DLL. Le système doit aussi vérifier que le fichier est compatible avec l'architecture (x86, x64, ARM), la version du système, etc.

Le format PE est donc conçu pour permettre au « loader » Windows de réaliser ces actions, voyons cela en détail.

2. Structure du format Windows PE

Le format se décompose en 3 parties principales : plusieurs entêtes, une table des sections et les sections elles-mêmes.

Ce poster de Ero Carrera Ventura [pe_pdf] donne les structures de données utilisées par le loader de Windows, dont les limites ont été âprement testées par Ange Albertini [corkami_pe]. Nous nous focaliserons sur l'essentiel.

Voici également la structure principale telle que donnée par l'outil PE Bear [pe_bear].

pe2 image1-s

Figure 1

3. Entêtes

3.1 DOS (préhistoire)

L'entête MZ ou DOS ci-dessous est historique, d'une taille fixe de 64 octets et pointe vers l'entête PE, ici à l'offset 0xd0 (valable à la fin de cette partie, sur 32 bits Intel) :

0x000000: 4d5a9000 03000000 04000000 ffff0000    MZ
...
0x000030: 00000000 00000000 00000000 d0000000    

Ensuite, on trouve du code Intel 16 bits affichant le texte ci-dessous, c'est le DOS stub, au cas où vous exécuteriez ce fichier dans une fenêtre DOS.

0x000040: 0e1fba0e 00b409cd 21b8014c cd215468    ........!..L.!Th
0x000050: 69732070 726f6772 616d2063 616e6e6f    is program canno
0x000060: 74206265 2072756e 20696e20 444f5320    t be run in DOS
0x000070: 6d6f6465 2e0d0d0a 24000000 00000000    mode....$.......

3.2 Rich et File header (époque moderne)

À la suite, on peut trouver l'entête « Rich » [rich], non documentée par Microsoft. Cette partie n'existe que si le binaire a été construit par Visual Studio et elle contient des détails sur les outils ayant servi à sa fabrication. Ces données se reconnaissent grâce au mot-clé « Rich » et sont codées avec une clé (xor) se situant juste après, ici 0x044f8dee. Ces données permettent de connaître la chaîne de compilation et donc reconnaître 2 malwares de la même origine, par exemple. Cela peut également servir à tromper l'analyste [devils].

0x000080: 402ee3bd 044f8dee 044f8dee 044f8dee    @....O...O...O..
...
0x0000b0: ec5087ee 484f8dee 52696368 044f8dee    .P..HO..Rich.O..

Voici les données décodées par PE Bear :

pe2 image2 rich-s 0

Figure 2

Juste après, ci-dessous, l'entête File (aussi appelé NT header) commence par l'identifiant 50450000 (« PE ») sur 4 octets. Puis une première partie (image_file_header) de 20 octets, où l'on trouve notamment : l'architecture cible (x86 ou x64), le nombre de sections (ici 3), un horodatage (a7316436, 1er décembre 1998). On trouve l'adresse de cet entête (0xd0) à la fin de l'entête DOS.

0x0000d0: 50450000 4c010300 a7316436 00000000    PE..L....1d6....
0x0000e0: 00000000 e0000f01 0b010600 00800000    ................

Voici en clair :

pe2 image3-s

Figure 3

On peut vérifier en ligne que Visual Studio 6.0 date de 1998, ce qui est cohérent avec l'horodatage de l'entête PE ci-dessus. Ces informations ne semblent donc pas forgées par l'attaquant.

3.3 Optional header

Ensuite vient image_optional_header, qui diffère légèrement pour les PE 32 et 64 bits. Pour les .exe, cette partie est obligatoire et contient des informations très importantes : à commencer par EntryPoint et ImageBase. Pour rappel, la deuxième est l'adresse de base en mémoire du processus, et la première le déplacement (adresse virtuelle relative – RVA – à cette base) où le processeur exécutera la première instruction du PE.

On y trouve aussi toutes les valeurs nécessaires à l'initialisation des données du processus : pile, tas, taille du code et des données, vérification de la version de l'OS et de l'architecture, etc.

À la fin de l'optional_header, se trouve la table à 16 entrées dite DataDirectory, où l'interprétation des 2 valeurs (VirtualAddress, Size) dépend de l'indice dans cette table.

0x000148:                  00000000 00000000
0x000150: d4980000 28000000 00000000 00000000
...
0x0001a0: 00000000 00000000 00900000 c4000000
0x0001b0: 00000000 00000000 00000000 00000000
0x0001c0: 00000000 00000000

Ci-dessous, les 2 seules entrées décodées : en case 1 (VA=0x98d4, size=0x28), on trouve comment localiser la table des imports et en case 12 (0x9000, 0xc4), la table des adresses des imports, on y reviendra.

01 import          0x000098d4 0x00000028
12 iat             0x00009000 0x000000c4

Voici la signification des entrées de 0 à 15 utilisées, et entre parenthèses, les sections associées le cas échéant :

  • 0 = export_table (.edata) ;
  • 1 = import_table (.idata) ;
  • 2 = resources_table (.rsrc) ;
  • 3 = exceptions_table (.pdata) ;
  • 4 = certificate_table ;
  • 5 = base_relocation_table (.reloc) ;
  • 6 = debug_table (.debug) ;
  • 8 = global_pointer ;
  • 9 = threat_local_storage (.tls) ;
  • 10 = load_configuration_dir ;
  • 11 = bound_import_dir ;
  • 12 = import_address_table ;
  • 13 = delay_load_import_desc ;
  • 14 = CLR_runtine_desc (.cormeta).

Nous allons nous contenter par la suite de détailler comment sont stockés les noms des DLL et les fonctions importées associées. Pour plus d'informations, voir la documentation officielle de Microsoft [pe].

4. Entêtes des sections

Après la table Data Directory, on trouve les entêtes des sections étudiées dans la première partie sur plusieurs exemples. Mais voici tout de même à quoi ressemblent les données brutes sur disque :

  • Voici les entêtes pour les 3 premières sections :
0x0001c8:                  2e746578 74000000    .........text...
0x0001d0: 387c0000 00100000 00800000 00100000    8|..............
0x0001e0: 00000000 00000000 00000000 20000060    ............ ..`
0x0001f0: 2e726461 74610000 080d0000 00900000    .rdata..........
0x000200: 00100000 00900000 00000000 00000000    ................
0x000210: 00000000 40000040 2e646174 61000000    ....@..@.data...
0x000220: b8290000 00a00000 00200000 00a00000    .)....... ......
0x000230: 00000000 00000000 00000000 400000c0    ............@...
  • Voici la structure de chaque entête, une pour chaque section (ici 3) :

Offset

Type

Nom

Contenu

0

UTF-8

Name (zero terminated)

.text par exemple

8

ulong

VirtualSize

Taille en mémoire (vsize)

12

ulong

VirtualAddress

Adresse en mémoire (vaddr)

16

ulong

SizeOfRawData

Taille dans le fichier (psize)

20

ulong

PointerToRawData

Adresse dans le fichier (paddr)

...

 

 

 

36

ulong

Characteristics

Propriétés de la section : code, r, w, x...

Nous avons déjà expliqué la signification de ces valeurs sur l'exemple rappelé en début de cet article.

5. Import des fonctions

Voyons comment le loader importe les fonctions (situées dans différentes DLL Windows).

On utilise la table des imports, dont l'adresse virtuelle (VA) est à la case 1 de Data Directory. Essentiellement, les entrées de cette table comportent 3 champs qui nous intéressent :

  • OriginalFirstThunk, une VA (16 bits LE, offset 0) vers la table ILT (import lookup table) ;
  • Name, une VA vers le nom de la DLL, terminée par un 0 (16 bits, offset 6) ;
  • FirstThunk, une VA (16 bits, offset 8) vers la table IAT (import address table) identique à ILT sur disque.

5.1 Trouver le nom des DLL...

Cherchons donc, pour notre exemple, le nom de la 1re DLL.

name        vsize    vaddr    psize    paddr    charact
00 .text    00007c38 00001000 00008000 00001000 MEM_READ MEM_EXECUTE CNT_CODE
01 .rdata   00000d08 00009000 00001000 00009000 MEM_READ CNT_INITIALIZED_DATA
02 .data    000029b8 0000a000 00002000 0000a000 MEM_WRITE MEM_READ CNT_INITIALIZED_DATA

Ci-dessus, la table des imports est à l'adresse virtuelle 0x98d4. Dans la colonne vaddr, cette adresse est dans la section « .rdata », soit 0x8da à partir du début de cette section en mémoire. Donc pour trouver notre table sur disque, il faut ajouter cette valeur à l'adresse physique de la section, colonne paddr. La table des imports est dans le fichier PE, à l'offset 0x9000 (paddr) + 0x8da. La démarche est identique pour toutes les adresses virtuelles (VA). Ici, le fait que paddr et vaddr soient identiques est un cas particulier.

0x0098d4:          fc980000 00000000 00000000
0x0098e0: e2990000 00900000 00000000 00000000

Ci-dessus, nous avons OriginalFirstThunk = 0x98fc, Name = 0x99e2 et FirstThunk = 0x9000. Voici ci-dessous le contenu à l'adresse 0x99e2 :

0x0099e2:     4b45 524e454c 33322e64 6c6c0000    ..KERNEL32.dll..

5.2 ... Et des fonctions importées

FirstThunk désigne IAT, comme à la case 12 dans Data Directory :

0x0098fc:                            369b0000
0x009900: d0990000 f0990000 fe990000 129a0000

et OriginalFirstThunk désigne ILT. Elles sont bien identiques sur le disque.

0x009000: 369b0000 d0990000 f0990000 fe990000
0x009010: 129a0000 269a0000 349a0000 409a0000

Dans les tables ci-dessus, chaque entrée (32 bits pour x86, 64 bits pour x64) comme 0x9b36, 0x99d0 pointe un nom de la fonction dans la DLL pointée par Name. Cette liste se termine par une entrée à zéro. 0x9b36 contient le nom « WideCharToMultiByte », fonction disponible dans KERNEL32.DLL :

0x009b30: 46696c65 0000d202 57696465 43686172    File....WideChar
0x009b40: 546f4d75 6c746942 79746500 1b00436c    ToMultiByte...Cl
0x009b50: 6f736548 616e646c 6500e401 4d756c74    oseHandle...Mult

Dernière précision, si pour une entrée de la IAT ou ILT, le bit de poids fort est à 1 (31 ou 63e), alors la fonction est importée par ordinal (identifiant numérique unique pour la DLL) et non par nom.

Par expérience, un exécutable avec un très grand nombre de fonctions importées est sans doute un interpréteur, comme Delphi, AutoIt ou PyInstaller. Comme les imports d'un PE (ou d'un ELF) en disent beaucoup sur les fonctions internes d'un malware, beaucoup se débrouillent pour avoir une IAT ou ILT invalides et la reconstruisent dynamiquement à l'exécution.

Pour finir, voici 2 exécutables compressés avec 7-zip SFX, j'ai ajouté une colonne avec le md5 pour chaque section :

   name        vsize    vaddr    psize    paddr charact  md5
00 .text    00023cc1 00001000 00023e00 00000400 60000020 cf813d6e56456d6079557b67c7062bf8
01 .rdata   00005456 00025000 00005600 00024200 40000040 73cc8d93d92fa0203af77deb2f8d341d
02 .data    00004efc 0002b000 00000c00 00029800 c0000040 d8c128249e2288fa94d6fd0841b26c69
03 .sxdata  00000004 00030000 00000200 0002a400 c0000240 35925cfdc1176bd9ffc634a58b40ec17
04 .rsrc    00001f08 00031000 00002000 0002a600 40000040 60e3afc2d81b1cb83c70a4d9a5dce84c
overlay: offset 0x2c600, size 0x5500, entr 7.990, md5:e8bd29878de5a9ebcf950a685c337636, b"7z\xbc"
 
 
   name        vsize    vaddr    psize    paddr charact  md5
00 .text    00023cc1 00001000 00023e00 00000400 60000020 cf813d6e56456d6079557b67c7062bf8
01 .rdata   00005456 00025000 00005600 00024200 40000040 73cc8d93d92fa0203af77deb2f8d341d
02 .data    00004efc 0002b000 00000c00 00029800 c0000040 d8c128249e2288fa94d6fd0841b26c69
03 .sxdata  00000004 00030000 00000200 0002a400 c0000240 35925cfdc1176bd9ffc634a58b40ec17
04 .rsrc    00001f08 00031000 00002000 0002a600 40000040 60e3afc2d81b1cb83c70a4d9a5dce84c
overlay: offset 0x2c600, size 0x1c47, entr 7.970, md5:1975acc807eac7c351577065cd42a347, b"7z\xbc"

Que remarquez-vous ? Eh oui, le contenu des 5 sections est identique, car leurs hachés sont identiques aussi, seul l'overlay diffère. Pour conclure que 2 zones contiennent les mêmes données, seul un hash (SHA-256 de préférence) doit être utilisé, pas uniquement la taille.

Si vous souhaitez manipuler les PE, la bibliothèque [LIEF] est indispensable. Elle permet même de modifier les exécutables d'autres systèmes comme GNU/Linux ou macOS.

6. Au-delà du format PE

6.1 Interprétation du code

D'autres outils utilisent une analyse du graphe de contrôle pour déduire les fonctionnalités internes ou des similarités entre malwares, de la réutilisation de code. On peut citer [intezer] et [capa], ce dernier permettant à la communauté d'écrire elle-même des signatures détectant les portions de code probablement malveillantes.

6.2 Lire le code

Enfin, on peut bien sûr lire le code avec un désassembleur ou un décompilateur selon le langage original : avec IDA Free ou Ghidra pour l'assembleur, le C/C++, [dnSpy] pour le .NET. Certains exécutables embarquent un interpréteur, comme Delphi, AutoIt ou Python, il faut alors utiliser les outils appropriés comme [idr] pour le 1er, [autoit-ripper] pour le 2e et [uncompyle6] [pyinstxtractor] pour le 3e, avant de pouvoir analyser le code original. Les binaires issus des langages Rust et Go sont plus difficiles à analyser, mais IDA depuis la 7.6 a marqué un réel progrès dans leur analyse.

Conclusion

L'analyse statique rapide ou triage des exécutables PE peut se limiter à répondre à la question : « ce fichier est-il malveillant ou non ? », simplement en décelant l'usage de protections retardant l'analyse ou de fonctionnalités destinées à cacher. Pour cela, l'approche proposée se résume ainsi : utiliser les hachés, pour le fichier et les sections, l'entropie, les DLL et fonctions importées. Les différents horodatages sont-ils cohérents ? Le nombre et les propriétés des sections sont-ils hors normes, existe-t-il un overlay ? Que nous apprend la recherche de signatures ? Le fichier est-il signé et par qui l'est-il ?

Remerciements

Merci à Laurent Cheylus et au rédacteur en chef pour leurs conseils.

Références

[pe_pdf] Portable Executable Format, Ero Carrera Ventura, déc. 2005,
http://www.openrce.org/reference_library/files/reference/PE%20Format.pdf

[corkami_pe] https://github.com/corkami/pocs/tree/master/PE?s=09

[pe_bear] PE Bear, par Hasherezade, https://hshrzd.wordpress.com/pe-bear/

[rich] Rich Headers, https://www.virusbulletin.com/virusbulletin/2020/01/vb2019-paper-rich-headers-leveraging-mysterious-artifact-pe-format/

[devils] https://securelist.com/the-devils-in-the-rich-header/84348/?s=09

[pe] PE format, Microsoft, https://docs.microsoft.com/en-us/windows/win32/debug/pe-format

[LIEF] Library to Instrument Executable Formats, QuarksLab, https://lief-project.github.io/

[inside_pe] Matt Pietrek, Microsoft, 2002,
https://docs.microsoft.com/en-us/archive/msdn-magazine/2002/february/inside-windows-win32-portable-executable-file-format-in-detail et
https://docs.microsoft.com/en-us/archive/msdn-magazine/2002/march/inside-windows-an-in-depth-look-into-the-win32-portable-executable-file-format-part-2

[intezer] brevet https://patents.justia.com/patent/10824722

[capa] https://github.com/mandiant/capa

[dnSpy] https://github.com/dnSpy/dnSpy

[idr] https://github.com/crypto2011/IDR

[autoit-ripper] https://github.com/nazywam/AutoIt-Ripper

[uncompyle6] https://pypi.org/project/uncompyle6/

[pyinstxtractor] https://github.com/extremecoders-re/pyinstxtractor



Article rédigé par

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

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.

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.

Les derniers articles Premiums

Les derniers articles Premium

Sécurisez vos applications web : comment Symfony vous protège des menaces courantes

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

Les frameworks tels que Symfony ont bouleversé le développement web en apportant une structure solide et des outils performants. Malgré ces qualités, nous pouvons découvrir d’innombrables vulnérabilités. Cet article met le doigt sur les failles de sécurité les plus fréquentes qui affectent même les environnements les plus robustes. De l’injection de requêtes à distance à l’exécution de scripts malveillants, découvrez comment ces failles peuvent mettre en péril vos applications et, surtout, comment vous en prémunir.

Bash des temps modernes

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

Les scripts Shell, et Bash spécifiquement, demeurent un standard, de facto, de notre industrie. Ils forment un composant primordial de toute distribution Linux, mais c’est aussi un outil de prédilection pour implémenter de nombreuses tâches d’automatisation, en particulier dans le « Cloud », par eux-mêmes ou conjointement à des solutions telles que Ansible. Pour toutes ces raisons et bien d’autres encore, savoir les concevoir de manière robuste et idempotente est crucial.

Présentation de Kafka Connect

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

Un cluster Apache Kafka est déjà, à lui seul, une puissante infrastructure pour faire de l’event streaming… Et si nous pouvions, d’un coup de baguette magique, lui permettre de consommer des informations issues de systèmes de données plus traditionnels, tels que les bases de données ? C’est là qu’intervient Kafka Connect, un autre composant de l’écosystème du projet.

Le combo gagnant de la virtualisation : QEMU et KVM

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

C’est un fait : la virtualisation est partout ! Que ce soit pour la flexibilité des systèmes ou bien leur sécurité, l’adoption de la virtualisation augmente dans toutes les organisations depuis des années. Dans cet article, nous allons nous focaliser sur deux technologies : QEMU et KVM. En combinant les deux, il est possible de créer des environnements de virtualisation très robustes.

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 67 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous