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.
1. Simple et souvent efficace
1.1 Identifier et rechercher
Un premier réflexe est de calculer les valeurs de hachés cryptographiques usuelles (MD5, SHA1 et SHA256), ce qui permet d'identifier de manière unique le fichier par son contenu, puis de chercher dans une base de Threat Intelligence interne ou une base publique, comme [hashlookup] ou [virustotal]. Nous savons ainsi si notre fichier est déjà connu et éventuellement analysé, s'il correspond à un fichier légitime ou à une menace, et le cas échéant le contexte de cette dernière : marqueurs associés pour pivoter ou les rechercher, attaquants, modes opératoires et secteurs économiques visés. Même si seul l'algorithme SHA256 est encore considéré comme fiable, les autres valeurs de hachés sont encore largement utilisées.
De plus, connaître le haché d'un fichier est parfois équivalent à en connaître le contenu, car de nombreuses bases permettent de les télécharger en connaissant simplement cette valeur [bazaar]. Enfin, révéler une valeur de haché publiquement (comme une simple recherche sur Virus Total) peut aussi faire comprendre à l'adversaire que vous avez identifié son vecteur d'attaque, il est donc préférable d'effectuer d'abord ses recherches en interne.
L'attaquant peut aussi s'arranger pour modifier très légèrement le fichier à chaque campagne d'attaque, afin d'obtenir un haché totalement différent et brouiller les pistes. L'algorithme de haché [ssdeep] est justement conçu pour être tolérant à des différences locales : deux fichiers similaires auront des valeurs ssdeep proches.
1.2 Des entrailles visibles ?
Autre méthode simple, on peut extraire les chaînes de caractères ASCII ou UNICODE, c'est-à-dire une suite d'un moins 4 caractères « imprimables ». Voici ci-dessous les premières et les dernières chaînes extraites d'un exemple, grâce à l'outil de Sysinternals. L'équivalent existe sous GNU/Linux.
Tout d'abord, il faut noter que peu de chaînes sont visibles, ce qui est inhabituel. On note aussi les 4 valeurs UPX au début et les 7 dernières lignes. KERNEL32.DLL est une librairie dynamique, et les 6 lignes suivantes sont les fonctions appartenant à cette DLL (Dynamically Linked Library). Ces 6 noms suggèrent : le chargement de bibliothèque, l'obtention de l'adresse d'un processus et des fonctions de gestion de la mémoire dynamique (à l'exécution), la terminaison d'un processus. La documentation de l'API Windows donne plus de détails si besoin, et nous y reviendrons. [malAPI] décrit l'usage par les malwares de l'API Windows pour l'évasion ou l'espionnage, par exemple.
Mais l'essentiel est que, dans cet exemple, rien qu'en cherchant les chaînes imprimables, on peut formuler 2 hypothèses : 1 - la rareté des chaînes indique que le fichier est en majorité chiffré ou compressé, ou alors les chaînes sont encodées, et 2 - cet exécutable charge lui-même des DLL et alloue, libère et modifie les propriétés de zones mémoires.
Avec de l'entraînement, on peut reconnaître des messages d'erreurs de la bibliothèque standard C++ ou C, par exemple. Avec un peu de chance (ou un malware bas de gamme), on peut reconnaître une adresse IP, une URL ou les commandes d'un RAT.
En cherchant UPX sur Internet, on trouve l'outil [UPX], un compresseur d'exécutable, ce qui valide plusieurs de nos hypothèses : la compression, le chargement dynamique de DLL et les gestions dynamiques de zones mémoires.
1.3 Recherche de signatures
Une approche moins naïve est de rechercher des motifs, par exemple des signatures [yara] ou avec l'outil [binwalk], qui permet également de calculer les variations d'entropie sur un fichier.
Les 3 méthodes que nous avons vues jusqu'ici fonctionnent d'ailleurs avec n'importe quel type de fichier, y compris les exécutables sous d'autres systèmes.
Mais revenons à la définition d'un malware : il s'agit d’un logiciel comportant des capacités malveillantes (malicious software) et doit donc être exécuté par la machine. Nous n'avons pas utilisé cette caractéristique jusqu'ici.
2. Exécutable ?
Nous verrons dans une seconde partie en détail la structure Portable Executable (PE), aujourd'hui nous retiendrons simplement qu'un exécutable est découpé en plusieurs sections, chacune d'entre elles ayant un rôle et des propriétés (caractéristiques) particulières liées à son usage. Ces différentes sections vont être chargées en mémoire par le loader Windows : la structure PE a été spécialement conçue pour mettre en œuvre cette correspondance (mapping) d'un fichier vers un processus prêt à fonctionner, d'où le nom « d'exécutable ».
Par exemple, la section contenant le code machine est souvent nommée .text, et en mémoire, cette portion possède la propriété « exécutable », mais non modifiable (absence de la caractéristique write). Le point d'entrée, c'est-à-dire l'adresse mémoire de l'instruction exécutée en premier se situe en principe dans cette section. Autres exemples de sections, celle nommée souvent .rdata contient les constantes (seulement read) et .data des variables (aussi write). Les sections de données ne sont pas en principe « exécutable ».
Les noms de sections sont indicatifs et ne sont pas utilisés lors du chargement du fichier PE par le loader Windows, par contre les caractéristiques en mémoire execute, read, write, etc. le sont. Adam « hexacorn » collectionne d'ailleurs les noms de sections exotiques [pe_section_names] et leurs origines.
2.1 Des sections classiques
Avant notre premier exemple, voici comment lire la table ci-dessous : pour chaque section, la colonne vsize est la taille en mémoire (processus) et psize est la taille dans le fichier PE. V pour Virtuelle et P pour Physique. De même, pour les colonnes vaddr et paddr qui sont les adresses. Les adresses mémoires (virtuelles) sont relatives à l'adresse de base du process (ImageBase), souvent 0x400000 pour un PE avec du code x86.
Ci-dessous, il s'agit d'un exécutable très classique : la section .text est la seule avec la caractéristique mem_execute, sans propriété write et le point d'entrée s'y trouve (0x1000 <= 0x4342 <= 0x9000). De même pour les 2 autres sections : les caractéristiques sont cohérentes avec les noms des sections connus.
2.2 Plus exotiques
Revenons ci-dessous sur l'exemple du début, celui duquel nous avons extrait les chaînes de caractères. Nous avons ajouté à notre table des sections une colonne supplémentaire (entr), la valeur d'entropie.
Tiens, on retrouve nos chaînes commençant par UPX, il s'agit donc des noms des sections. On note 2 anomalies : deux sections execute dont la première (UPX0) est vide sur le disque (psize = 0), mais à 0x8000 en mémoire (colonne vsize) ! Le point d'entrée est dans la section UPX1. On note également la très forte entropie de UPX1 à 7,8 qui est très proche de 8, le maximum.
Rappelons-nous la fonction d'UPX : il compresse des fichiers PE (et ELF sous GNU/Linux, d'ailleurs) qui sont décompressés « à la volée » lors de l'exécution, vers leur forme originale. On a donc un fichier PE original et un fichier PE « compressé », qui dans notre exemple possède son point d'entrée à l'adresse virtuelle 0x5ce0, dans la section UPX1 à forte entropie. Cette dernière contient donc sans doute la section .text / exécutable originale sous forme compressée et le code de décompression. Quelle était la taille originale sur disque de la section .text d'origine ? Il s'agit de psize = 0x8000, valeur que nous retrouvons comme taille virtuelle de UPX0 (dans le processus) : vsize = 0x8000. Le code dans UPX1 décompresse donc le code original .text vers la section UPX0, d'où l'usage des fonctions VirtualAlloc et VirtualProtect, puis « saute » vers le point d'entrée original (à 0x4342), ce qui explique aussi pourquoi UPX0 est write et execute à la fois !
Vous l'avez deviné : le fichier compressé avec les sections nommées UPX... est la version compressée et fonctionnelle du fichier PE juste au-dessus.
À retenir : une section mémoire à la fois write et execute est typique des packers et du parasitage de processus, c'est-à-dire lorsqu'un malware injecte du code malveillant dans un processus légitime, comme dans celui d'un navigateur pour espionner les mots de passe.
Voyons un dernier exemple de sections ci-dessous :
Cet exécutable comporte une section nommée .sxdata, et également des données après la fin de la dernière section, « après » la fin de la structure PE officielle, à l'offset 0x2c600 (paddr=0x2a600 + psize=0x2000). Cette partie que l'on appelle overlay possède une forte entropie de 7,99 et commence par les 2 octets 7z. Enfin, en extrayant les chaînes de caractères, on trouve 7z.sfx et 7z.sfx.exe : fin du suspense, il s'agit donc d'un exécutable 7-zip auto-extractible (self extractible, SFX). La structure PE est cependant différente de celle d'UPX : les données compressées sont dans l'overlay et non dans une section, comme UPX1.
2.3 Automatiser la recherche de caractéristiques de fichiers PE
Même si les propriétés d'un PE 7-zip SFX que nous venons d'identifier ne sont pas forcément signes d'un comportement malveillant, c'est une belle occasion pour écrire une signature Yara (fichier 7z_sfx.yara) :
Grâce aux modules pe et math, la signature ci-dessus détecte : la présence de l'overlay et son entropie >=7,5, la section nommée .sxdata, les valeurs des champs CompanyName et FileDescription (situées dans la section .rsrc) et l'import de la fonction SetFileAttributesW de la librairie partagée KERNEL32.dll.
Elle s'utilise ainsi :
Sans être caractéristiques des malwares, les packers offrent deux propriétés importantes aux attaquants pour protéger leurs exécutables : ils cachent une partie des données internes (code et données) avec la compression, ce qui force le plus souvent à une analyse dynamique, et ainsi retarde le diagnostic. Une autre transformation similaire est le chiffrement, détectable également avec une entropie importante, mais l'analyste doit alors déduire la clé et l'algorithme utilisé. Certaines signatures Yara comme [findcrypt-yara] détectent les constantes utilisées par la plupart des algorithmes cryptographiques. La présence d'un overlay est plutôt inhabituelle.
Il est important de vérifier aussi si le fichier PE est signé numériquement, par quelle organisation, et si le certificat utilisé est valide, de confiance ou non.
2.4 Tout en un ?
Existe-t-il un outil avec une bibliothèque de signatures des packers et autres PE exotiques ? Oui, l'outil de référence est Detect It Easy [DIE] et possède des centaines de signatures, malheureusement dans son propre langage.
Voici les résultats sur un script AutoIt compilé (langage pour les malwares ou les équipes IT), par la version texte de DIE :
Et notre 7-zip SFX :
Mais, comment fait cet outil pour connaître également la version du compilateur et linker utilisés ? Vous souvenez-vous de la chaîne de caractères Rich extraite au début de l'article ? Ces infos y sont cachées et cela sera détaillé dans la deuxième partie. Nous verrons également comment sont indiqués l'architecture et le type (console ou GUI).
Conclusion
Aujourd'hui, nous avons vu comment identifier un fichier grâce à ses valeurs de hachés cryptographiques, comment extraire de l'information des chaînes de caractères ou par leur absence, déceler des anomalies. Enfin, nous avons entrevu l'organisation du format PE en sections et leurs rôles, leurs caractéristiques en mémoire et le principe des packers, et enfin l'importance de la mesure de l'entropie.
Dans cette première partie, nous avons vu quelques approches statiques (sans observation de l'exécution) pour déceler les caractéristiques typiques des malwares Windows. Nous reviendrons en détail dans la 2e partie sur la structure PE et continuerons avec d'autres outils et méthodes.
Remerciements
Merci aux patient(e)s relecteurs(trices) pour leurs suggestions bienveillantes :-)
Références
[hashlookup] CIRCL hash lookup is a public API to lookup hash values against known database of files https://hashlookup.circl.lu
[virustotal] Analyze suspicious files and URLs to detect types of malware, automatically share them with the security community, https://www.virustotal.com
[bazaar] https://bazaar.abuse.ch/browse
[malAPI] https://malapi.io/
[ssdeep] Fuzzy hashing program, https://ssdeep-project.github.io/ssdeep/index.html
[UPX] The Ultimate Packer for eXecutables, https://upx.github.io/
[Yara] https://github.com/InQuest/awesome-yara
[binwalk] https://github.com/ReFirmLabs/binwalk
[pe_section_names] PE Section names – re-visited,
https://www.hexacorn.com/blog/2016/12/15/pe-section-names-re-visited/
[findcrypt-yara] https://github.com/polymorf/findcrypt-yara