Leçon 8.4 · Encodage, crypto & signatures· 55 min
Extraire les configurations de malwares
Transformez la configuration embarquée d’un échantillon en JSON normalisé via un extracteur localiser-décoder-analyser-restituer, et partagez-la avec soin.
Objectifs
- Expliquer ce que contient la configuration d’un malware et pourquoi c’est le résultat le plus précieux d’une analyse
- Énumérer les emplacements des configurations : ressources, overlays, blobs .data, paramètres .NET, registre et récupérations à l’exécution
- Construire un extracteur selon le schéma localiser–décoder–analyser–restituer qui produit du JSON normalisé
- Décrire comment MWCP, les extracteurs CAPE et les projets de type RATDecoders organisent leurs extracteurs, et en empaqueter un pour MWCP
- Rendre un extracteur robuste aux évolutions de version et partager les configurations de façon responsable
De tous les résultats que produit une analyse, la configuration est celui que le reste de l’équipe réclame en premier. Un rapport de triage abouti dit ce qu’est un échantillon ; la configuration dit à qui il parle et comment. Extrayez-la en une heure et vous remettez au SOC un ensemble d’indicateurs actifs et aux ingénieurs détection de quoi chasser, bien avant la fin d’une rétro-ingénierie complète. Cette leçon porte sur la production de cette configuration sous forme de données propres et structurées — et sur l’écriture d’un extracteur qui continue à la produire à mesure que la famille évolue.
Elle s’appuie sur les deux compétences qui l’encadrent dans le module 8 : reconnaître les encodages et identifier la crypto vous disent comment la configuration est protégée, et scripter le déchiffrement des chaînes vous fournit les techniques de récupération d’arguments et de réimplémentation que vous réutilisez ici sur une structure plus grande.
Ce que contient une configuration
La configuration d’une famille de malwares (logiciels malveillants) est l’ensemble des valeurs qui changent d’une campagne à l’autre alors que le code reste le même. Les extraire vous apprend presque tout ce qui compte sur le plan opérationnel :
| Champ | Pourquoi c’est important |
|---|---|
| Hôtes et ports C2 | IOC réseau ; le cœur de toute liste de blocage ou de toute chasse |
| Listes de C2 de repli / de secours | Une infrastructure que vous manqueriez autrement |
| Identifiant de campagne ou de botnet | Regroupe les échantillons en campagnes ; pivot d’attribution |
| Clés de chiffrement / clés RC4 | Déchiffrent le trafic C2 et les charges utiles ultérieures |
| Intervalle de sommeil / de beacon | Signature comportementale ; ajuste les fenêtres de détection |
| Noms de mutex | IOC hôte ; une détection fiable et peu coûteuse |
| Chemins et noms de fichiers d’installation | Remédiation ; où nettoyer |
| Clés de registre | Emplacements de persistance à supprimer |
| Indicateurs de fonctionnalités | Ce que l’échantillon est configuré pour faire réellement |
| Chaîne de version | Suit le développement de la famille dans le temps |
Mettez cette liste en regard du tableau des résultats de Qu’est-ce que l’analyse de malwares ? : une seule configuration extraite alimente à la fois les IOC (indicateurs de compromission), le contenu de détection, les consignes de remédiation et le renseignement. C’est pourquoi l’extraction de configuration, et non la rétro-ingénierie complète, est l’étape qui crée le plus de valeur analytique par heure.
Où se trouvent les configurations
Avant de pouvoir décoder une configuration, il faut la trouver. Les familles la cachent dans une poignée d’endroits prévisibles :
- Ressources PE. Le répertoire
.rsrcest un classique : la configuration se trouve dans une ressource au nom personnalisé, souvent chiffrée, parfois déguisée en icône ou en bitmap. La leçon Ressources, overlays et autres cachettes explique comment les énumérer. - Overlays. Les données ajoutées après la dernière section, au-delà de la fin du PE telle que la décrivent les en-têtes, sont invisibles aux parseurs naïfs et constituent une cachette de configuration courante.
- Blobs
.data/.rdata. Un tableau d’octets dans une section de données, déchiffré au démarrage. Généralement précédé ou entouré d’un marqueur — une chaîne magique ou une séquence d’octets que le code recherche. Les marqueurs sont l’élément le plus utile sur lequel un extracteur puisse s’appuyer. - Classes de paramètres .NET. Comme vu dans Analyser les malwares
.NET, les familles managées concentrent la
configuration dans une classe de paramètres statique initialisée dans le
.cctor, souvent en Base64 puis AES. - Le registre ou un fichier déposé. Certaines familles écrivent leur configuration sur l’hôte à l’installation et la relisent ensuite ; la configuration n’est donc pas du tout dans l’échantillon d’origine — vous la récupérez sur l’hôte infecté ou dans un dump mémoire.
- Récupérée à l’exécution. Le pire cas : l’échantillon télécharge sa configuration depuis le C2 à la première exécution. L’extraction statique vous donne la clé de déchiffrement et l’URL ; la configuration elle-même exige un C2 réel ou simulé.
Une entropie élevée dans une ressource, un overlay ou un
blob .data est l’indice que l’un d’eux contient une configuration chiffrée.
Le schéma de l’extracteur
Tout extracteur de configuration, aussi différente que soit la famille, comporte les mêmes quatre étapes. Les garder séparées est ce qui rend un extracteur lisible, testable et maintenable.
- Localiser. Trouvez les octets de la configuration. Une règle YARA sur le marqueur ou sur le code de décodage est le mécanisme standard, car elle survit aux recompilations mineures et vous donne l’offset. À défaut de marqueur, localisez la ressource par son nom, l’overlay en analysant le PE, ou le blob par une xref depuis la routine de décodage.
- Décoder. Défaites la protection. C’est là que les compétences en encodage et en crypto vues plus tôt dans le module portent leurs fruits : réimplémentez exactement le XOR, le RC4, l’AES ou le schéma maison, en réutilisant la clé récupérée dans le code.
- Analyser. Imposez une structure aux octets décodés. Les configurations
sont typées : un octet de longueur puis un hôte, une table de champs à
offsets fixes, une liste préfixée par sa longueur, un blob sérialisé
(Protocol Buffers, JSON, un flux .NET
BinaryFormatter). Lisez chaque champ avec son vrai type. - Restituer. Produisez des données normalisées — du JSON avec des noms de champs stables — afin que chaque échantillon de la famille produise la même forme, quels que soient les champs présents. C’est la normalisation qui permet aux outils en aval de consommer uniformément les configurations de nombreuses familles.
Astuce : Écrivez les quatre étapes sous forme de quatre fonctions, même pour un usage ponctuel. Quand la variante suivante ne change que la clé, vous modifiez
decodeet rien d’autre ; quand elle ajoute un champ, vous modifiezparse. Un extracteur qui mélange localisation et analyse dans une même boucle doit être réécrit pour chaque variante.
Frameworks existants
Vous partez rarement d’un fichier vide. Plusieurs frameworks ouverts vous fournissent la tuyauterie — chargement des échantillons, fonctions crypto, un schéma de configuration, un exécuteur — si bien que votre code se réduit à la localisation, au décodage et à l’analyse propres à la famille.
| Projet | Mainteneur | Forme | Usage |
|---|---|---|---|
| DC3-MWCP | DoD Cyber Crime Center | Framework où chaque parseur est une classe Python qui émet un rapport standard de champs typés | Sortie d’extracteurs standardisée sur de nombreuses familles |
| Extracteurs CAPE | Open source (kevoreilly) | Modules Python par famille exécutés automatiquement sur les charges utiles dumpées dans la sandbox CAPE | Extraction intégrée à l’unpacking et à la détonation |
| malduck | CERT Polska | Bibliothèque de fonctions crypto, PE et de structures, plus un modèle d’extracteur piloté par YARA | Briques de base et exécuteur d’extracteurs moderne |
| Projets de type RATDecoders | Communauté (p. ex. lignée StormShield, Malwoverview) | Un répertoire de scripts autonomes par famille | Voir comment une famille précise est analysée ; référence rapide |
Sous le capot, ils sont organisés de la même façon : un registre
d’extracteurs par famille, chacun déclarant comment reconnaître sa famille
(généralement une règle YARA) et comment produire une configuration, plus un
exécuteur qui associe un échantillon à un extracteur et normalise le
résultat. MWCP appelle cette unité un parser ; CAPE l’appelle un module
extractor ; malduck l’appelle une classe extractor avec une yara_rule
embarquée. En apprendre un vous prépare aux autres.
Empaqueter votre extracteur pour MWCP est simple : héritez de la classe de base
des parseurs, implémentez la méthode run pour localiser-décoder-analyser
l’échantillon qui lui est fourni, et rapportez les champs via l’objet de rapport
standard (report.add(...) avec des métadonnées typées telles qu’une adresse de
socket, un mutex ou une clé de chiffrement). MWCP gère ensuite l’entrée, la
mise en forme de la sortie et l’exécution de votre parseur aux côtés des
autres. Le même corps localiser-décoder-analyser que vous écrirez ci-dessous
s’insère dans cette méthode presque sans modification.
Robustesse : écrire pour la variante suivante
Le code que vous avez analysé est une compilation parmi d’autres d’une famille vivante. Le prochain échantillon sera différent — une nouvelle clé, un champ réordonné, un indicateur supplémentaire, un marqueur déplacé — et un extracteur fragile renvoie silencieusement des données fausses, ce qui est pire que de ne rien renvoyer.
- Échouez bruyamment. Si le marqueur est absent, si le décodage produit des hôtes non imprimables ou si une longueur dépasse la fin du tampon, levez une erreur claire et sortez avec un code non nul. Une configuration fausse mais plausible empoisonne tous les systèmes en aval qui lui font confiance.
- Validez ce que vous analysez. Vérifiez qu’un hôte ressemble à un hôte, qu’un port est dans la plage autorisée, qu’une longueur correspond aux octets disponibles. Ces contrôles vous permettent aussi d’écarter les fausses correspondances du marqueur — les mêmes octets magiques peuvent apparaître comme littéral de comparaison dans le code, et pas seulement comme la vraie configuration.
- Testez sur plusieurs échantillons. Un seul échantillon est une anecdote. Rassemblez-en quelques-uns de la même famille et exécutez l’extracteur sur tous ; les champs qui varient sont la vraie configuration, les champs qui ne changent jamais sont peut-être des artefacts de votre analyse. Conservez ces échantillons comme tests de non-régression, exactement comme le pipeline d’analyse conserve un corpus de non-régression pour les règles.
- Versionnez votre extracteur. Quand vous le modifiez pour une nouvelle variante, notez laquelle et gardez l’ancien comportement testable, afin de savoir si un résultat différent signifie que l’échantillon a changé ou que votre code a changé.
- N’exécutez pas l’échantillon pour obtenir la configuration, sauf nécessité. L’extraction statique est reproductible et sûre ; ne vous rabattez sur un débogueur ou sur l’émulation que lorsque la configuration est récupérée ou déchiffrée à l’aide d’un état qui n’existe qu’à l’exécution.
Attention : L’obfuscation visant les analystes vise souvent aussi les extracteurs. Le hachage d’API, l’aplatissement du flot de contrôle et la dérivation de clé propre à chaque build sont conçus pour casser une règle fondée sur un marqueur. Quand une localisation YARA cesse de correspondre d’un build à l’autre, ancrez la règle sur le code de décodage plutôt que sur les données de configuration, qui changent plus souvent.
Partager les configurations de façon responsable
Une configuration extraite est du renseignement, et une partie en est sensible. Les domaines C2 et les noms de mutex peuvent être largement partagés et ont leur place dans les listes de blocage et les flux. Mais une configuration peut aussi contenir un identifiant de victime, un nom d’hôte interne intégré à un build ciblé, ou une infrastructure dont l’exposition signale à l’attaquant qu’il a été repéré. Appliquez la même discipline que la leçon sur les pipelines applique aux envois d’échantillons : partagez selon le protocole traffic light (TLP) de l’incident, préférez un partage au niveau des hashs et des IOC à la publication d’échantillons entiers, et neutralisez (defang) les URL et les hôtes dans tout rapport afin qu’une configuration collée dans un ticket ou une messagerie ne soit pas cliquable. Des plateformes comme MISP et MWDB existent pour partager des configurations avec structure et contrôle d’accès ; un paste public, non.
Lab : un extracteur avec un marqueur, un décodeur et un échec bruyant
Vous allez ajouter une petite configuration chiffrée à un échantillon inoffensif,
puis écrire un extracteur qui localise le marqueur avec YARA, décode et analyse
le blob en JSON, et échoue avec une erreur claire sur une variante où le
marqueur est absent. Continuez dans le répertoire m8b de la leçon précédente,
ou préparez-en un nouveau avec mingw-w64 et un venv contenant pefile et
yara-python.
-
Étendez
gen.pypour qu’il produise un blob de configuration : un marqueur ASCII de 8 octets en clair, suivi des champs passés au XOR avec une clé d’un octet. Le qualificatifvolatileempêche-O2de replier tout le décodage en constantes, de sorte que les octets encodés apparaissent réellement dans.data, comme ils le feraient dans un vrai échantillon.python import struct MARKER, CFG_KEY = b"LABCFG01", 0x3D host, cid = b"update.example.com", b"lab-demo" body = struct.pack("<B", len(host)) + host body += struct.pack("<H", 8443) + struct.pack("<H", 60) # port, sleep body += struct.pack("<B", len(cid)) + cid blob = MARKER + bytes(b ^ CFG_KEY for b in body) # marker left clear with open("lab_data.h", "a") as f: f.write("static volatile unsigned char CFG_BLOB[] = { %s };\n" % ", ".join("0x%02x" % b for b in blob)) f.write("#define CFG_BLOB_LEN %d\n" % len(blob)) -
Ajoutez le décodage à l’exécution dans
lab_sample.cpour que l’échantillon compilé soit cohérent avec lui-même (il affiche la configuration qu’il transporte) :c /* in main(), after the strings: locate marker, XOR remainder with 0x3D */ volatile unsigned char *p = CFG_BLOB; char marker[8]; for (int i = 0; i < 8; i++) marker[i] = p[i]; if (memcmp(marker, "LABCFG01", 8) == 0) { unsigned char cfg[64]; for (int i = 0; i < CFG_BLOB_LEN - 8; i++) cfg[i] = p[8 + i] ^ 0x3D; int hlen = cfg[0]; char host[64]; memcpy(host, cfg + 1, hlen); host[hlen] = '\0'; unsigned port = cfg[1 + hlen] | (cfg[2 + hlen] << 8); unsigned sleep = cfg[3 + hlen] | (cfg[4 + hlen] << 8); int ilen = cfg[5 + hlen]; char id[64]; memcpy(id, cfg + 6 + hlen, ilen); id[ilen] = '\0'; printf("config: host=%s port=%u sleep=%u id=%s\n", host, port, sleep, id); }Recompilez et vérifiez qu’il affiche la configuration :
bash .venv/bin/python gen.py x86_64-w64-mingw32-gcc -O2 -s -o lab_sample.exe lab_sample.ctext config: host=update.example.com port=8443 sleep=60 id=lab-demo -
Notez une subtilité avant d’écrire l’extracteur : la chaîne du marqueur apparaît deux fois dans le binaire. Une fois comme littéral de comparaison du
memcmpdans.text(suivi de code), et une fois comme vrai blob de configuration dans.data(suivi du corps passé au XOR). L’extracteur doit essayer chaque correspondance et n’accepter que celle qui s’analyse correctement — une petite leçon sur la nécessité de valider, et pas seulement de localiser.bash python3 -c "d=open('lab_sample.exe','rb').read(); i=0 while (i:=d.find(b'LABCFG01', i)) >= 0: print(hex(i), d[i+8:i+13].hex()); i+=1"text 0x2055 48394424380f # .text: comparison literal, then code 0x2200 2f484d595c # .data: 0x2f = 0x12 ^ 0x3d, the real body -
Écrivez
config_extract.pyselon le schéma localiser-décoder-analyser-restituer :python #!/usr/bin/env python3 """config_extract.py - locate, decode and parse the config into JSON.""" import json, struct, sys import yara CFG_KEY = 0x3D RULE = r''' rule lab_config_marker { strings: $marker = "LABCFG01" condition: $marker }''' class BadConfig(Exception): pass def parse_blob(data, off): """Decode (XOR) and parse one candidate; raise on anything implausible.""" body = bytes(b ^ CFG_KEY for b in data[off + 8:]) # skip 8-byte marker try: host_len = body[0] host = body[1:1 + host_len] pos = 1 + host_len port, sleep = struct.unpack_from("<HH", body, pos) pos += 4 id_len = body[pos] cid = body[pos + 1:pos + 1 + id_len] except (IndexError, struct.error) as e: raise BadConfig(f"structure does not unpack: {e}") if not (1 <= host_len <= 64 and len(host) == host_len): raise BadConfig(f"implausible host length {host_len}") if not all(0x20 <= c < 0x7f for c in host): raise BadConfig("host is not printable") if not (1 <= port <= 65535): raise BadConfig(f"port out of range: {port}") if not (1 <= id_len <= 64 and len(cid) == id_len): raise BadConfig("implausible campaign id") return {"c2_host": host.decode(), "c2_port": port, "sleep_seconds": sleep, "campaign_id": cid.decode(), "_source_offset": hex(off)} def extract(path): data = open(path, "rb").read() matches = yara.compile(source=RULE).match(data=data) hits = [inst.offset for m in matches for inst in m.strings[0].instances] \ if matches else [] if not hits: raise BadConfig("marker LABCFG01 not found: not this family, or a new variant") for off in hits: # try each hit; skip the code literal try: return parse_blob(data, off) except BadConfig: continue raise BadConfig(f"marker found at {[hex(h) for h in hits]} but none parsed") def main(): if len(sys.argv) != 2: sys.exit(f"usage: {sys.argv[0]} sample.exe") try: print(json.dumps(extract(sys.argv[1]), indent=2)) except BadConfig as e: print(f"error: {e}", file=sys.stderr) sys.exit(2) if __name__ == "__main__": main() -
Exécutez-le sur l’échantillon :
bash .venv/bin/python config_extract.py lab_sample.exejson { "c2_host": "update.example.com", "c2_port": 8443, "sleep_seconds": 60, "campaign_id": "lab-demo", "_source_offset": "0x2200" }L’extracteur a ignoré le marqueur de
.textà0x2055— le décodage à cet endroit donne une longueur d’hôte invraisemblable, queparse_bloba donc rejetée — et a analysé le vrai blob à0x2200. -
Prouvez qu’il échoue bruyamment sur une variante. Écrasez le marqueur pour simuler un build qui l’aurait modifié, puis relancez :
bash python3 -c "d=bytearray(open('lab_sample.exe','rb').read()); i=0 while (i:=d.find(b'LABCFG01', i)) >= 0: d[i:i+8]=b'ZZZZZZZZ'; i+=8 open('lab_variant.exe','wb').write(d)" .venv/bin/python config_extract.py lab_variant.exe; echo "exit=$?"text error: marker LABCFG01 not found: not this family, or a new variant exit=2Le code de sortie non nul et le message clair sont l’essentiel : un pipeline qui exécute cet extracteur apprend que la configuration n’a pas été trouvée, au lieu d’enregistrer silencieusement une configuration vide ou fausse.
-
Empaquetez-le, sur le principe, pour MWCP. Déplacez le corps de
extractdans la méthoderund’une sous-classe de parseur, rapportez chaque champ avec son type (le C2 comme adresse de socket, l’identifiant comme identifiant de mission/campagne, l’intervalle comme entier), et laissez MWCP gérer l’entrée et la sortie JSON. Associez la règle YARA pour que le framework n’exécute votre parseur que sur les échantillons qui correspondent. La logique localiser-décoder-analyser ne change pas — seulement la manière dont le résultat est rapporté.
Questions : Pourquoi le fait d’ancrer la règle YARA sur le marqueur plutôt
que sur le code de décodage rend-il l’extracteur plus fragile face à un nouveau
build, et quand choisiriez-vous l’une ou l’autre option ? Quel contrôle de
validation a rejeté la correspondance du marqueur dans .text, et que se
passerait-il sans lui ? Si une seconde variante conservait le marqueur mais
réordonnait les champs, laquelle des quatre étapes modifieriez-vous ? Quel champ
de cette configuration traiteriez-vous comme sensible et ne placeriez-vous pas
dans une liste de blocage publique, et pourquoi ? Comment transformeriez-vous les
trois ou quatre échantillons d’une famille en test de non-régression pour cet
extracteur ?
À retenir
- La configuration — hôtes et ports C2, clés, identifiants, intervalles, mutex, chemins, indicateurs — est le résultat le plus précieux d’une analyse : elle alimente à la fois les IOC, les détections, la remédiation et le renseignement.
- Les configurations se trouvent dans les ressources, les overlays, les blobs
.data, les classes de paramètres .NET, le registre ou une récupération à l’exécution ; une entropie élevée dans l’un de ces emplacements en est l’indice. - Construisez chaque extracteur en localisant (YARA sur un marqueur ou sur le code de décodage), en décodant (réimplémentation de la crypto), en analysant (champs typés) et en restituant (JSON normalisé), sous forme d’étapes séparées.
- Réutilisez les frameworks — MWCP, extracteurs CAPE, malduck, scripts de type RATDecoders — qui s’organisent tous autour d’un extracteur par famille et d’un exécuteur ; votre code se limite à la partie propre à la famille.
- Rendez-le robuste : échouez bruyamment, validez chaque champ analysé, testez sur plusieurs échantillons et versionnez l’extracteur, afin qu’un nouveau build ne produise jamais une configuration fausse mais plausible.
- Partagez les configurations selon le TLP de l’incident, préférez le partage au niveau des IOC et des hashs et les plateformes structurées aux pastes publics, et neutralisez les hôtes dans chaque rapport.