Skip to content

Leçon 3.1 · Triage statique· 40 min

Identifier et hacher les fichiers

Identifiez un fichier par ses magic bytes plutôt que par son nom, puis prenez son empreinte avec des hashs cryptographiques, d’imports, flous et Rich.

Objectifs

  • Identifier le type réel d’un fichier à partir de ses magic bytes avec file, TrID et Detect It Easy
  • Expliquer ce que prouvent les hashs cryptographiques, et pourquoi un seul octet modifié les rend inutiles pour mesurer la similarité
  • Utiliser imphash, ssdeep, TLSH et le hash du Rich header pour regrouper des échantillons apparentés
  • Effectuer des recherches de réputation par hash sans divulguer de fichiers confidentiels
  • Tenir un journal de triage sur lequel d’autres analystes peuvent s’appuyer

Le triage commence par deux questions qui semblent triviales : quel est ce fichier, et l’avons-nous déjà vu ? Si vous vous trompez sur la première, vous ouvrez un script dans un parseur PE, ou un exécutable dans un lecteur PDF. Si vous négligez la seconde, vous passez une journée à analyser par rétro-ingénierie quelque chose qu’un collègue a terminé le mois dernier.

Cette leçon ouvre le module 3. Vous connaissez désormais les formats PE et ELF de l’intérieur ; à partir d’ici, vous appliquez ces connaissances rapidement, dans l’ordre que suit un vrai triage. D’abord l’identité : le type réel du fichier, puis un ensemble de hashs qui répondent chacun à une question différente.

Les noms et les extensions mentent

Le nom et l’extension du fichier sont choisis par celui qui l’a envoyé. Windows se sert de l’extension pour décider quoi faire lors d’un double-clic, et c’est précisément pour cela que les attaquants en abusent :

  • invoice.pdf.exe, avec l’option « Masquer les extensions des fichiers dont le type est connu » activée, s’affiche comme invoice.pdf.
  • Un caractère de forçage droite-à-gauche (U+202E) inséré dans report[U+202E]fdp.exe amène l’Explorateur à afficher le nom reportexe.pdf.
  • Un PE renommé en update.dat ou thumbs.db ne s’exécutera pas sur un clic, mais un loader peut toujours le mapper. L’extension ne dit rien du contenu.

D’où la première règle du triage : ignorez le nom, lisez les octets. Consignez le nom d’origine dans vos notes comme un élément de preuve, jamais comme un fait sur le fichier.

Magic bytes

La plupart des formats binaires commencent par une signature fixe, le nombre magique (magic number). Vous en avez déjà rencontré deux : MZ pour les fichiers PE (En-têtes PE) et \x7fELF (ELF pour les analystes de malware). Voici ceux que vous verrez le plus souvent lors du triage de malwares (logiciels malveillants) :

FormatPremiers octets (hex)ASCIIRemarques
Exécutable PE / DOS4D 5AMZÀ confirmer avec PE\0\0 à e_lfanew
ELF7F 45 4C 46.ELFL’octet 4 donne 32/64 bits, l’octet 5 le boutisme
ZIP (et DOCX, XLSX, JAR, APK)50 4B 03 04PK..Office Open XML est un ZIP ; regardez à l’intérieur
PDF25 50 44 46 2D%PDF-Les lecteurs tolèrent des données parasites avant
Fichier composé OLED0 CF 11 E0 A1 B1 1A E1Anciens .doc/.xls/.ppt, .msi
7-Zip37 7A BC AF 27 1C7z..'.
RAR52 61 72 21 1A 07Rar!..Suivi de 00 (v4) ou 01 00 (v5)
Raccourci Windows (LNK)4C 00 00 00 01 14 02 00L...Taille de l’en-tête, puis le CLSID du shell link
gzip1F 8BCourant pour les charges utiles Linux
Cabinet4D 53 43 46MSCF

Une signature est nécessaire, pas suffisante. PK vous dit « conteneur ZIP », pas s’il s’agit d’un document Word, d’une archive Java ou d’une application Android. Et de nombreux formats, comme les scripts, les fichiers HTA, les fichiers batch et PowerShell, n’ont aucun nombre magique : ce sont simplement du texte.

Attention : Les parseurs sont souvent plus tolérants que ne le laissent penser les signatures. Les lecteurs PDF acceptent un en-tête qui n’est pas à l’offset 0, et les outils d’archivage trouvent un ZIP dont le répertoire central se trouve à la fin d’un autre fichier. Un fichier peut être à la fois un GIF valide et un ZIP valide (un « polyglotte »). Si une vérification de type et le comportement d’un outil divergent, ne vous fiez à aucun des deux tant que vous n’avez pas regardé les octets.

file et libmagic

Sous Linux et macOS, file compare le contenu à la base de signatures et de tests structurels de libmagic :

bash
cp hello.exe invoice.pdf
file invoice.pdf
xxd -l 16 invoice.pdf
text
invoice.pdf: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows
00000000: 4d5a 9000 0300 0000 0400 0000 ffff 0000  MZ..............

L’extension dit PDF, les octets disent PE32+. file -z regarde à l’intérieur des fichiers compressés, et file -k continue après la première correspondance, ce qui aide avec les polyglottes.

TrID et Detect It Easy

Sous Windows, deux outils font le même travail avec des points forts différents :

  • TrID évalue un fichier par rapport à des milliers de définitions communautaires et affiche les candidats classés avec des pourcentages. Il est utile pour les formats obscurs que libmagic ne connaît pas.
  • Detect It Easy (DIE) va plus loin pour les exécutables : au-delà du format, ses signatures nomment le compilateur, l’éditeur de liens, le packer ou le protecteur, comme MSVC, mingw-w64, Go, .NET, UPX ou Themida, et il affiche l’entropie par section. Pour un PE, DIE est généralement le premier outil à ouvrir. Son volet packer fait l’objet d’une leçon dédiée, Détecter les packers et l’entropie.

Tous trois sont heuristiques. Lorsqu’ils divergent, ouvrez une vue hexadécimale et vérifiez la signature vous-même.

Hashs cryptographiques : l’identité

Un hash cryptographique associe à n’importe quelle entrée un condensé de taille fixe. Deux propriétés comptent pour un analyste :

  • Identité. En pratique, un même condensé signifie les mêmes octets : un hash nomme donc un fichier de façon unique et exacte. C’est pourquoi les hashs sont la clé primaire de toute base d’échantillons, de tout rapport de sandbox (bac à sable) et de tout flux d’IOC (indicateurs de compromission).
  • Effet avalanche. Changez un seul bit n’importe où et environ la moitié des bits du condensé basculent. Le nouveau hash ne vous dit rien de la proximité entre les fichiers.
AlgorithmeCondenséStatutOù on le voit encore
MD5128 bitsCollisions praticables depuis 2004Anciens flux d’IOC, consoles EDR, imphash
SHA-1160 bitsCollision pratique démontrée en 2017 (SHAttered)Git, outils historiques, VirusTotal
SHA-256256 bitsAucune attaque pratique connueLa norme à consigner et à partager

Les collisions comptent lorsqu’un attaquant peut fabriquer deux fichiers ayant le même hash, par exemple pour faire placer un jumeau bénin sur liste blanche. Pour nommer des échantillons et partager des IOC, MD5 et SHA-1 fonctionnent encore, mais consignez SHA-256 comme identifiant de référence et ajoutez les autres, car de nombreux outils et flux les attendent encore.

powershell
Get-FileHash .\sample.bin -Algorithm SHA256
certutil -hashfile .\sample.bin SHA256
bash
sha256sum sample.bin    # macOS: shasum -a 256 sample.bin

L’effet avalanche est aussi la limite. Un hash de fichier est un IOC parfait pour ce fichier exact et inutile pour le build suivant. Les opérateurs de malwares le savent : les générateurs (builders) qui inscrivent un identifiant de campagne unique dans chaque copie, ou qui ajoutent des octets aléatoires à l’overlay (voir Ressources et overlays), donnent à chaque victime un SHA-256 différent. C’est pourquoi les hashs se trouvent au bas de la « pyramide de la douleur » (pyramid of pain) : les changer ne coûte rien à l’attaquant.

Astuce : Hachez le fichier reçu et tout ce que vous en extrayez (le ZIP, le document qu’il contient, l’EXE déposé), et consignez la relation parent-enfant. Les équipes de réponse qui fouillent les postes ont besoin du hash du fichier qui atterrit réellement sur le disque, et c’est rarement la pièce jointe de l’e-mail.

Hashs de similarité : les parents

Pour trouver des fichiers apparentés, il faut des hashs qui changent un peu quand le fichier change un peu. Quatre sont utilisés au quotidien.

Imphash

Vous avez déjà rencontré l’imphash dans Imports, exports et l’IAT : un MD5 calculé sur la liste d’imports normalisée. Il ignore entièrement le code et les données, si bien qu’il survit aux chaînes modifiées, au code recompilé et aux données parasites ajoutées, tant que la liste d’imports et son ordre restent identiques. Il entre en collision pour les fichiers packés et pour les tables d’imports minuscules ou dominées par le runtime.

ssdeep : hachage par morceaux déclenché par le contexte

ssdeep (Kornblum, 2006) découpe le fichier en morceaux à des frontières choisies par le contenu, et non à des offsets fixes. Un hash glissant calculé sur une petite fenêtre mobile déclenche une frontière chaque fois que sa valeur remplit une condition qui dépend d’une taille de bloc. Chaque morceau est haché et réduit à un seul caractère Base64, et les caractères sont concaténés :

text
192:PFNFU39HUbMetCyMYUdHQNEmeyVxHk/wjE6zCPiMpnHHmyVAgsZguy0HLVDSDRmb:Pu9HXjyVxEpUCNzVBuy0HLuscq
 │   │                                                             │
 │   └ one character per chunk at the block size                   └ same, at double block size
 └ block size

Comme les frontières dépendent du contenu local, insérer des octets à un endroit ne décale que les morceaux voisins, et le reste de la signature reste inchangé. Comparer deux signatures donne un score de 0 à 100 fondé sur le nombre de modifications nécessaires pour transformer une chaîne en l’autre. Les limites :

  • Les signatures ne sont comparables que si leurs tailles de bloc sont égales ou diffèrent d’un facteur deux. Des fichiers de tailles très différentes obtiennent toujours 0.
  • Les petits fichiers produisent des signatures courtes et des scores instables.
  • La compression, le chiffrement ou le packing réécrivent chaque octet : deux copies packées du même programme semblent donc sans rapport.

TLSH

TLSH, le Trend Micro Locality Sensitive Hash, adopte une autre approche. Il fait glisser une fenêtre de 5 octets sur le fichier, compte les triplets d’octets dans 128 compartiments, et encode le compte de chaque compartiment sous forme de quartile. Il ajoute un en-tête construit à partir de la longueur du fichier et d’une somme de contrôle. Comparer deux condensés TLSH donne une distance : 0 signifie quasi identique, et elle augmente sans plafond fixe à mesure que les fichiers divergent. Contrairement à ssdeep, TLSH compare des fichiers de tailles différentes et se laisse plus difficilement tromper par une réorganisation du contenu, ce qui explique que plusieurs grandes plateformes de threat intelligence l’indexent. Il a besoin d’au moins 50 octets suffisamment variés et renvoie TNULL sinon.

Le hash du Rich header

Pour les fichiers compilés avec MSVC, le Rich header enregistre la chaîne de compilation qui a produit chaque objet. Un hash de son contenu décodé regroupe les échantillons issus du même environnement de build, même entre projets différents. VirusTotal l’affiche, et pefile en calcule un avec pe.get_rich_header_hash(). mingw-w64, Go et Delphi ne produisent pas de Rich header, le hash est donc vide, et les attaquants peuvent copier ou falsifier l’en-tête.

Quel hash répond à quelle question

HashSurvit àCasse surIdéal pour
SHA-256RienToute modificationIdentité exacte, IOC, déduplication
ImphashModifications du code et des donnéesChangements de la liste d’imports, packingMême base de code, builds différents
ssdeepModifications locales, données ajoutéesFort changement de taille, packingQuasi-doublons, variantes de même taille
TLSHModifications, réorganisation, changement de taillePacking, chiffrementRegroupement à grande échelle
Rich hashTout sauf la chaîne de compilationChaînes non MSVC, falsificationMême machine de build

Aucun hash de similarité n’est fiable à lui seul. Deux hashs indépendants qui concordent constituent une piste ; ajoutez des chaînes, du code ou une infrastructure en commun et cela devient un constat.

Recherches de réputation, en toute sécurité

Une fois les hashs en main, demandez-vous si le monde connaît déjà ce fichier : d’abord votre propre EDR et votre base d’échantillons, puis des services publics comme VirusTotal et MalwareBazaar. Recherchez par hash ; ne téléversez pas.

La différence compte. Une recherche par hash ne révèle presque rien : le condensé ne peut pas être inversé pour retrouver le fichier. Un téléversement livre le fichier entier, et sur la plupart des services publics il devient accessible aux autres abonnés, qui peuvent télécharger les fichiers soumis. Cela crée deux risques distincts :

  • Fuite de données. Un document de phishing taillé pour votre organisation peut contenir de vrais noms, des noms de projets internes ou un tableur volé. Un malware qui embarque une configuration propre à la victime peut contenir votre domaine, vos paramètres de proxy, voire des identifiants. Le téléverser revient à publier tout cela à des inconnus.
  • Alerter l’attaquant. Les opérateurs de campagnes ciblées surveillent les services publics pour y repérer leurs propres hashs. Le premier téléversement d’un implant unique leur apprend qu’ils ont été découverts, souvent avant que votre réponse à incident ne soit prête.

Attention : La même prudence s’applique aux URL et aux domaines que vous extrayez. Demander à un scanner public de « scanner » une URL l’amène à récupérer cette URL depuis sa propre infrastructure, ce que l’opérateur peut voir. Recherchez plutôt l’indicateur neutralisé (defanged) dans les données historiques, et suivez la politique de partage de votre organisation (par exemple le Traffic Light Protocol) avant que quoi que ce soit ne sorte de vos murs.

Si une recherche par hash ne renvoie rien, c’est aussi un résultat : l’échantillon est nouveau, ciblé ou fraîchement recompilé. Notez-le et poursuivez le reste du triage.

Tenir un journal de triage

Chaque fichier que vous manipulez obtient une ligne, consignée avant de commencer à l’expérimenter. Un tableur suffit ; c’est la discipline qui compte :

text
date        sha256 (full)   md5 / sha1   size    type (magic + DIE)        original name / source
imphash     ssdeep          tlsh         rich    parent sha256             lookup result + date
analyst     notes (defanged indicators, next steps)

Consignez la date de la recherche : « 0 détection » ne signifie pas la même chose le premier jour et un mois plus tard. Consignez le hash parent pour qu’une charge utile extraite puisse toujours être rattachée à l’e-mail ou à l’archive d’où elle provient. Et gardez le journal avec le dossier, pas dans votre tête, pour que l’analyste suivant puisse reprendre là où vous vous êtes arrêté.

Lab : quels hashs voient la famille ?

Vous allez compiler un programme bénin, en dériver de petites variantes et mesurer la réaction de chaque hash. Les commandes utilisent mingw-w64 sous Linux ou macOS ; n’importe quel Python 3.9+ convient pour le script.

  1. Écrivez le programme de base :

    c
    // triage.c - benign lab program
    #include <windows.h>
    #include <stdio.h>
    
    static const char *BANNER = "lab build A: checking for updates";
    static const char *SERVER = "http://update.example.com/lab";
    
    int main(void) {
        SYSTEMTIME st;
        GetLocalTime(&st);
        printf("%s\n", BANNER);
        printf("would contact %s at %02d:%02d\n", SERVER, st.wHour, st.wMinute);
        Sleep(100);
        return 0;
    }
  2. Compilez les variantes. Chacune diffère de v1_base.exe d’une seule manière contrôlée :

    bash
    x86_64-w64-mingw32-gcc -O0 -s -o v1_base.exe triage.c
    x86_64-w64-mingw32-gcc -O0 -s -o v1_again.exe triage.c           # same source, rebuilt
    sed 's/lab build A/lab build B/' triage.c > triage_b.c
    x86_64-w64-mingw32-gcc -O0 -s -o v2_string.exe triage_b.c         # one character changed
    x86_64-w64-mingw32-gcc -O2 -s -o v3_O2.exe triage.c               # different optimisation
    x86_64-w64-mingw32-gcc -O0    -o v4_symbols.exe triage.c          # not stripped
    sed 's/Sleep(100);/Sleep(100); printf("%lu\\n", GetTickCount());/' triage.c > triage_c.c
    x86_64-w64-mingw32-gcc -O0 -s -o v6_import.exe triage_c.c         # one extra import
    cp v1_base.exe v5_flipped.exe
    printf '\x01' | dd of=v5_flipped.exe bs=1 \
        seek=$(( $(wc -c < v1_base.exe) - 16 )) conv=notrunc          # one byte near the end
  3. Installez les bibliothèques dans un environnement virtuel. ppdeep est une implémentation de ssdeep en Python pur ; python-ssdeep fonctionne aussi si libfuzzy est installée.

    bash
    python3 -m venv venv && . venv/bin/activate
    pip install pefile ppdeep py-tlsh
  4. Calculez chaque hash et comparez-le à celui du build de base :

    python
    # hashes.py - compare identity and similarity hashes across variants
    import sys, hashlib, pefile, ppdeep, tlsh
    
    def row(path):
        data = open(path, "rb").read()
        pe = pefile.PE(data=data)
        return {"file": path,
                "sha256": hashlib.sha256(data).hexdigest(),
                "imphash": pe.get_imphash(),
                "ssdeep": ppdeep.hash(data),
                "tlsh": tlsh.hash(data)}
    
    rows = [row(p) for p in sys.argv[1:]]
    base = rows[0]
    print(f"{'vs ' + base['file']:<18}{'sha256':>8}{'imphash':>9}{'ssdeep':>8}{'tlsh':>6}")
    for r in rows[1:]:
        same = lambda k: "same" if r[k] == base[k] else "diff"
        print(f"{r['file']:<18}{same('sha256'):>8}{same('imphash'):>9}"
              f"{ppdeep.compare(base['ssdeep'], r['ssdeep']):>8}"
              f"{tlsh.diff(base['tlsh'], r['tlsh']):>6}")
    bash
    python3 hashes.py v1_base.exe v1_again.exe v2_string.exe v5_flipped.exe \
        v3_O2.exe v6_import.exe v4_symbols.exe

    Une exécution avec GCC 15 de mingw-w64 a produit le tableau ci-dessous. Vos chiffres varieront selon la version du compilateur, et les distances TLSH bougent de quelques points entre des builds par ailleurs identiques, mais la tendance devrait se maintenir. ssdeep est un score de similarité (plus il est élevé, plus les fichiers sont proches) ; TLSH est une distance (plus elle est faible, plus ils sont proches).

    text
    vs v1_base.exe      sha256  imphash  ssdeep  tlsh
    v1_again.exe          diff     same     100     1
    v2_string.exe         diff     same      99     3
    v5_flipped.exe        diff     same     100     1
    v3_O2.exe             diff     same      65    32
    v6_import.exe         diff     diff      71    48
    v4_symbols.exe        diff     same       0   511
  5. Expliquez v1_again.exe. Même source, même compilateur, et pourtant un SHA-256 différent. Trouvez les octets qui diffèrent :

    bash
    cmp -l v1_base.exe v1_again.exe

    Les différences se trouvent dans le TimeDateStamp COFF et le CheckSum de l’en-tête optionnel (voir En-têtes PE). Chaque build produit un nouveau hash de fichier, même sans aucune modification du code.

  6. Vérifiez le hash du Rich header. Exécutez python3 -c "import pefile; print(repr(pefile.PE('v1_base.exe').get_rich_header_hash()))". Un build mingw renvoie une chaîne vide. Si vous disposez de MSVC, compilez triage.c avec cl deux fois, depuis des versions différentes de Visual Studio, et comparez.

  7. Identifiez chaque variante avec file et, si vous avez une VM Windows, passez-les dans Detect It Easy et TrID. Renommez-en une en .pdf et vérifiez qu’aucun des outils ne s’en soucie.

  8. Commencez votre journal de triage avec une ligne par variante.

Questions : Pourquoi v4_symbols.exe obtient-il 0 avec ssdeep mais tout de même une distance TLSH finie ? Quel hash relierait encore v6_import.exe à la base si l’auteur l’avait aussi packé avec UPX ? Quels deux hashs publieriez-vous comme IOC pour cette « famille », et lesquels garderiez-vous en interne comme pivots de regroupement ? Qu’arriverait-il aux cinq hashs si le générateur ajoutait 4 Ko d’octets aléatoires à l’overlay ?

À retenir

  • Identifiez les fichiers par leurs magic bytes et leur structure, jamais par leur nom ou leur extension ; file, TrID et DIE sont des heuristiques que vous confirmez avec une vue hexadécimale.
  • SHA-256 nomme un fichier exact. Une recompilation, un horodatage ou un seul octet modifié donne un nouveau hash : les hashs de fichier sont donc des IOC précis mais fragiles.
  • Imphash, ssdeep, TLSH et le hash du Rich header survivent chacun à des changements différents. Utilisez-en au moins deux ensemble pour regrouper, et sachez que le packing met en échec la plupart d’entre eux.
  • Recherchez les hashs, ne téléversez pas les fichiers : les téléversements peuvent divulguer des données de victimes et avertir l’attaquant.
  • Consignez chaque fichier avec ses hashs, son type, sa source, son parent et la date de recherche avant d’aller plus loin.