Leçon 7.3 · Comportements malveillants· 50 min
Commande et contrôle
Repérer le C2 dans les journaux réseau (rythme des beacons, anomalies DNS, pivots TLS), l’extraire des échantillons et le contenir sans perdre de preuves.
Objectifs
- Expliquer ce que transporte un canal de C2, pourquoi c’est la phase la plus observable d’une intrusion et comment elle s’inscrit dans la tactique ATT&CK TA0011
- Décrire les artefacts observables du polling HTTP(S), des canaux DNS, du C2 via des services légitimes, du pair-à-pair et des DGA
- Extraire les paramètres de C2 d’un échantillon et les consigner sous une forme exploitable par les ingénieurs détection et les intervenants
- Chasser le C2 dans les journaux Zeek par score de beacon, connexions longues, destinations rares, pivots DNS et TLS, et raisonner sur les sosies bénins
- Choisir entre sinkholing, blocage et isolement de l’hôte en tenant compte des compromis sur les preuves et la délimitation du périmètre
Mécanismes de persistance traitait de ce qu’une intrusion laisse sur le disque. La commande et contrôle (C2), c’est ce qu’elle fait sur le réseau. Un implant qui s’est installé puis s’est tu attend toujours des instructions, et pour les recevoir il doit sortir par votre réseau, encore et encore, aussi longtemps que dure l’intrusion.
Vous avez déjà vu les pièces depuis l’établi de l’analyste. Simuler et capturer le trafic réseau gardait le trafic d’un échantillon à l’intérieur du laboratoire et lisait les intervalles de beacon et les empreintes TLS dans une capture ; Extraire les configurations de malwares sortait la liste des C2 du binaire ; Écrire des signatures réseau transformait le format des requêtes en règles Suricata. Cette leçon relie ces compétences à l’autre versant du métier : trouver le C2 dans des journaux que vous n’avez pas générés, le bloquer, et s’en servir pour déterminer jusqu’où s’étend un incident.
À quoi sert le C2
Du point de vue du défenseur, un canal de C2 transporte trois types de trafic :
| Sens | Ce qu’il transporte | Ce que vous voyez en général |
|---|---|---|
| Check-in (de l’implant vers le serveur) | Identifiant de l’hôte, utilisateur, OS, privilèges, « quelque chose pour moi ? » | Petites requêtes régulières ; souvent de la même taille à chaque fois |
| Tâches (du serveur vers l’implant) | Commandes : exécuter une commande shell, lister des fichiers, dormir plus longtemps, se désinstaller | Réponses généralement vides ou minuscules ; de temps à autre, une plus grosse |
| Résultats et staging (dans les deux sens) | Sortie des commandes, données volées qui sortent ; outils et étapes suivantes qui entrent | Rafales qui cassent le motif : gros envois, gros téléchargements |
ATT&CK regroupe ces techniques sous la tactique TA0011, avec des sous-techniques pour le protocole (T1071, Application Layer Protocol), la façon de trouver le serveur (T1568, Dynamic Resolution), l’usage de services tiers (T1102, Web Service), le chiffrement (T1573), les proxys (T1090) et les canaux de repli (T1008). L’exfiltration et le transfert d’outils empruntent le même canal mais relèvent d’autres tactiques, ce qui explique qu’un seul implant corresponde souvent à une demi-douzaine d’identifiants.
Le C2 est la phase la plus détectable pour une raison simple : il se répète. L’accès initial a lieu une fois. La persistance est écrite une fois. Le vol d’identifiants peut avoir lieu une fois par hôte. Mais un implant interroge son serveur toutes les quelques secondes, minutes ou heures pendant des semaines, et chaque interrogation franchit une frontière que vous pouvez instrumenter : un résolveur DNS, un proxy, un pare-feu, une sonde Zeek. Chaque répétition est une occasion de plus de l’attraper, et la répétition elle-même est une signature. C’est aussi la phase qui offre le plus de levier : coupez le canal et l’opérateur perd d’un coup le contrôle de tous les hôtes infectés, même ceux que vous n’avez pas encore trouvés.
Familles de canaux et leur apparence
Les auteurs choisissent un canal pour sa fiabilité et sa capacité à se fondre dans le décor. Les familles ci-dessous sont présentées telles que vous les rencontrez dans les journaux, pas telles que vous les construiriez.
Polling HTTP(S)
La conception la plus courante : l’implant dort, se réveille, envoie une requête, lit la réponse et se rendort. Le serveur ne peut pas pousser d’informations : la latence est donc égale à l’intervalle de sommeil, et la plupart des frameworks ajoutent un jitter aléatoire à ce sommeil. Les frameworks à profils malléables permettent à l’opérateur de déguiser la requête en trafic web ordinaire.
Artefacts observables : connexions régulières d’un hôte vers une destination ; tailles de requêtes et de réponses constantes pendant les périodes d’inactivité ; un user-agent qui ne correspond pas au navigateur de l’hôte ; des URI qui se répètent ou suivent un même motif ; du TLS vers un domaine récent ou une IP nue ; des certificats auto-signés, fraîchement émis ou réutilisés sur des domaines sans rapport. Avec du domain fronting ou un CDN en frontal, le SNI et l’IP de destination appartiennent à un fournisseur réputé, et seuls le rythme et le contexte au niveau de l’hôte le trahissent.
Canaux DNS
Le DNS est autorisé en sortie de presque tous les réseaux, directement ou via un
redirecteur. Le tunnel DNS encode les données dans
le nom interrogé (<encoded-chunk>.t.example.net) et reçoit les réponses dans des
enregistrements TXT, CNAME, NULL ou A. L’opérateur contrôle le serveur faisant
autorité pour le domaine parent : chaque requête lui parvient donc à travers vos
propres résolveurs.
Artefacts observables : de nombreux sous-domaines uniques sous un même domaine parent ; des labels longs (jusqu’à la limite de 63 caractères) composés d’alphabets base32, base64 ou hexadécimal ; un volume de requêtes élevé d’un client vers ce domaine ; des types d’enregistrements inhabituels comme TXT ou NULL depuis des postes de travail ; des réponses aux TTL courts. Le DNS over HTTPS déplace le même motif dans du HTTPS vers un résolveur public, où il ressemble à du TLS vers ce résolveur depuis un processus qui ne devrait pas l’utiliser.
Services cloud et collaboratifs légitimes
Publier des commandes via l’API d’un bot de messagerie, un dépôt d’hébergement de code, un site de paste ou un dossier de stockage cloud donne à l’implant une destination qu’aucune liste de blocage ne signalera (C2 via service web). Une variante, le dead drop resolver, ne stocke que l’adresse du vrai C2 dans un profil ou une publication publics.
Artefacts observables : des endpoints d’API (et non le front-end web) contactés par des hôtes ou des processus qui n’ont rien à y faire ; un serveur ou un compte de service qui parle à une API de messagerie ; l’interrogation régulière d’un même chemin de stockage ; le bon domaine avec le mauvais processus, ce que vous ne voyez que si la télémétrie du proxy ou de l’EDR enregistre le nom du processus. Les analyses purement réseau peinent ici ; c’est là que la jointure entre données endpoint et données réseau porte ses fruits.
Pair-à-pair
Dans une conception pair-à-pair, il n’y a pas de serveur unique : les hôtes infectés se relaient les commandes, à partir de listes de pairs que l’implant transporte et met à jour. En interne, certains frameworks chaînent les implants via des pipes nommés SMB ou du TCP pour qu’un seul hôte parle à l’extérieur.
Artefacts observables : des postes de travail qui acceptent des connexions entrantes d’autres postes de travail, souvent sur des ports élevés ou inhabituels ; de l’UDP ou du TCP sortant vers de nombreuses adresses IP résidentielles sans résolution DNS préalable ; le même port fixe chez de nombreux pairs externes. Le démantèlement est difficile faute de domaine unique à saisir ; la détection est généralement plus facile, car le trafic de poste à poste est rare dans un réseau bien segmenté.
Algorithmes de génération de domaines
Un DGA dérive une liste de domaines candidats à partir d’une graine et, généralement, de la date du jour. L’implant les essaie tour à tour ; l’opérateur n’a besoin d’en enregistrer qu’un. Bloquer un domaine ne sert à rien, car la liste de demain est différente. Le fast flux est l’astuce complémentaire côté IP, qui fait tourner de nombreuses adresses derrière un même nom.
Artefacts observables : des rafales de réponses NXDOMAIN depuis un hôte, des dizaines à des milliers de noms non enregistrés sur une courte fenêtre ; des noms sans mots du dictionnaire, à forte entropie de caractères, de longueur et de domaine de premier niveau constants ; finalement un nom qui se résout, suivi des connexions qui comptent. Retrouvez l’algorithme dans l’échantillon et vous pourrez calculer tous les domaines futurs, les préenregistrer ou les sinkholer, et confronter les journaux DNS passés aux dates passées.
Extraire les détails du C2 d’un échantillon
La chasse démarre plus vite quand vous savez exactement ce que vous cherchez. Passez l’échantillon au crible des sources que vous connaissez déjà :
- Extraction de configuration. La configuration contient généralement la liste des C2, les ports, les URI, le sommeil et le jitter, l’identifiant de campagne et une éventuelle clé de chiffrement. Un extracteur construit comme dans Extraire les configurations de malwares vous donne tout d’un coup, et pour chaque échantillon apparenté.
- Chaînes. L’analyse des chaînes et le
déchiffrement scripté des chaînes
retrouvent les user-agents, les modèles d’URI, les noms d’en-têtes et les
chaînes de format telles que
%s/%08x/submit. - Code réseau. Les imports comme
WinHttpOpen,InternetConnectA,DnsQuery_Aougetaddrinfo, et les fonctions qui les entourent, montrent quels champs sont codés en dur, lesquels viennent de la configuration et lesquels sont calculés par hôte. Cherchez une boucle autour de la séquence envoi-sommeil-réception : elle révèle l’appel de sommeil, l’arithmétique du jitter et l’éventuel ordre de repli. - Capture dynamique. Une exécution contrôlée montre le trafic tel que le serveur le voit. Laissez-la tourner assez longtemps pour observer plusieurs check-ins, et souvenez-vous que les sandboxes (bacs à sable) qui accélèrent les sommeils faussent le rythme.
Consignez le résultat dans un tableau, une ligne par canal, car chaque champ alimente un consommateur différent :
| Champ | Exemple | Qui l’utilise |
|---|---|---|
| Hôtes et ports, dans l’ordre de repli | cdn-static.example[.]org:443, puis 203.0.113[.]50:8443 | Blocage, recherches DNS et proxy |
| Méthode de résolution | Liste statique ; DGA à graine datée ; dead drop sur un site de paste | Plan de sinkholing, chasse rétroactive |
| URI et méthodes | GET /assets/<16 hex>.js, POST /assets/upload | Recherches proxy, Suricata |
| Intervalle et jitter | 60 s, jitter de 10 % | Seuils des analyses de beacons |
| User-agent et en-têtes | Vieille chaîne de navigateur avec une coquille | Recherches proxy, Suricata |
| Détails TLS | SNI, JA4 du client, sujet, émetteur, numéro de série, validité et SHA-256 du certificat | Pivots dans les journaux TLS, recherches CT |
| Encodage et crypto | Clé XOR, clé RC4, alphabet base64 | Décodage du trafic capturé |
| Observé ou code seul | « chemin POST vu dans le code, jamais observé » | Niveau de confiance du rapport |
Astuce : Consignez l’intervalle et le jitter tels que l’échantillon les définit, et non tels que votre sandbox les a observés. Les valeurs configurées sont ce que vos analyses doivent chercher en production ; les valeurs observées ne sont justes que si rien n’a accéléré ou retardé l’exécution.
Chasser le C2 dans les journaux réseau
L’échantillon vous donne des indicateurs. La chasse trouve les infections que vos
indicateurs manquent : autres builds, autres familles, autres canaux. Zeek est la
source de données de référence pour ce travail : conn.log enregistre chaque
connexion avec ses horodatages, ses nombres d’octets et sa durée ; dns.log
chaque requête et réponse ; ssl.log le SNI et les détails de la négociation ;
x509.log les certificats ; http.log les requêtes qu’il peut voir. Les journaux
de proxy et de pare-feu portent des champs similaires sous d’autres noms.
Périodicité et jitter des beacons
Un beacon est une paire hôte-destination dont les connexions arrivent à intervalles réguliers. Prenez les temps inter-arrivées (les écarts entre connexions successives) et demandez-vous à quel point ils sont resserrés :
- Le coefficient de variation (écart-type divisé par la moyenne) est proche de 0 pour une minuterie fixe et proche de 1 ou au-delà pour une activité humaine. Un jitter uniforme de plus ou moins J pour cent donne un CV d’environ J divisé par la racine carrée de 3 : un jitter de 10 % tombe donc vers 0,058.
- L’écart absolu médian (MAD), rapporté à l’écart médian, est robuste : une poignée de beacons manqués pendant la mise en veille d’un portable ne le fausseront pas.
- Le nombre de connexions compte, car deux écarts réguliers ne prouvent rien. Calculez le score sur une longue fenêtre, 24 heures ou plus en production, pour que les beacons lents aient le temps d’apparaître.
- La constance des tailles : un implant inactif envoie la même requête et reçoit la même réponse « aucune tâche », si bien que les nombres d’octets bougent à peine.
Aucune mesure ne suffit à elle seule. Le lab montre pourquoi : la navigation par rafales produit un écart médian de quelques secondes au sein des rafales, ce qui donne au MAD une apparence régulière alors que le CV la démasque. RITA, l’outil open source d’Active Countermeasures, note les beacons selon les mêmes principes sur des journaux Zeek à grande échelle ; c’est l’étape suivante naturelle après le script de cette leçon.
Connexions longues, destinations rares et anomalies DNS
Tous les canaux ne fonctionnent pas par polling. Certains maintiennent une connexion ouverte pendant des heures et y poussent les tâches. Additionnez la durée des connexions par paire et regardez le haut du classement : vous y trouverez des clients de visioconférence, des VPN et du streaming, et parfois quelque chose qui n’est rien de tout cela.
La rareté est le filtre le plus utile que vous puissiez ajouter à n’importe laquelle de ces analyses. Une destination contactée par tous les hôtes de l’entreprise est un produit ; une destination contactée par un seul hôte, vue pour la première fois récemment et avec un domaine récent mérite un coup d’œil. Pour le DNS, calculez par client le nombre de réponses NXDOMAIN, de sous-domaines uniques par domaine parent et de requêtes TXT ou NULL, et notez les labels selon leur longueur et leur entropie. Attendez-vous à des faux positifs dus aux CDN, qui placent des labels ressemblant à des hashs dans les noms d’hôtes, et aux produits de sécurité, qui encodent des recherches dans le DNS.
Pivots TLS : JA3, JA4 et certificats
Quand vous ne pouvez pas lire la charge utile, la négociation sert d’empreinte. Un hash JA3 ou JA4 du client identifie la pile TLS et sa configuration ; le certificat du serveur identifie la façon dont l’opérateur a configuré le serveur. Une fois un hôte confirmé, pivotez vers l’extérieur : quels autres hôtes internes ont présenté le même JA4 client à une destination que personne d’autre n’utilise ? Quelles autres destinations ont servi un certificat de même sujet, émetteur, numéro de série ou clé publique ? JA3 est devenu bruité pour les navigateurs, qui randomisent désormais l’ordre des extensions ; JA4 trie ces champs et y résiste. Aucun des deux n’identifie une famille à lui seul : ils ne sont forts que lorsqu’ils sont rares dans votre environnement et combinés à un signal de destination ou de rythme.
Pivoter sur l’infrastructure, avec OPSEC
À partir d’un seul domaine ou certificat de C2 confirmé, vous pouvez souvent retrouver le reste de l’infrastructure de l’opérateur, et la chasser en entier dans vos journaux avant que l’implant ne bascule vers une solution de secours.
- Le DNS passif enregistre quels noms se sont résolus vers quelles IP au fil du temps. Pivotez du domaine vers ses IP, de ces IP vers tous les autres domaines vus dessus, et écartez l’hébergement mutualisé.
- Les journaux de Certificate Transparency publient tous les certificats reconnus publiquement. Les interroger par domaine, par chaîne d’organisation ou par motif de nommage permet de trouver des certificats émis pour des domaines apparentés, parfois avant que ces domaines ne soient utilisés.
- Vos propres journaux sont la source passive la plus riche de toutes : tout hôte interne ayant un jour résolu ou contacté l’infrastructure entre dans le périmètre.
Attention : Restez passif. Résoudre un nom de C2 suspecté depuis le résolveur de l’entreprise, le visiter ou le scanner signale à un opérateur qui surveille son DNS faisant autorité ou son serveur web que quelqu’un regarde. Soumettre un échantillon ciblé à une sandbox publique peut avoir le même effet, tout comme rechercher un indicateur unique sur un service tiers qui partage les requêtes. Utilisez des jeux de données passifs, une infrastructure de recherche approuvée et les règles de votre équipe, comme indiqué dans la section OPSEC de la leçon sur le laboratoire réseau.
Le confinement et ses compromis
Toute action de confinement modifie aussi ce que vous pouvez encore apprendre. Décidez avec le responsable de l’incident, et dans le bon ordre : délimitez d’abord le périmètre là où c’est sans risque, puis coupez.
| Action | Ce qu’elle vous apporte | Ce qu’elle coûte |
|---|---|---|
| Sinkhole DNS (RPZ ou surcharge du résolveur) redirigeant les noms de C2 vers un serveur interne | Chaque hôte infecté se signale en interrogeant ou en contactant le sinkhole ; couvre les hôtes que vous n’avez pas trouvés | Les IP codées en dur, le DoH et le P2P le contournent ; l’opérateur voit les check-ins s’arrêter |
| Blocage au proxy ou au pare-feu | Coupure immédiate des canaux connus sur tout le parc | L’implant peut basculer vers un canal de secours ou un DGA que vous n’avez pas cartographié ; vous perdez la visibilité sur ce qu’il aurait fait ensuite |
| Isolement de l’hôte via l’EDR | Bloque tout le trafic sauf vers la console EDR ; garde la mémoire et les processus en vie pour la collecte | Un hôte à la fois ; l’opérateur remarque que cet hôte s’éteint |
| Réinstallation ou mise hors tension | Retire l’implant de cet hôte | Détruit les preuves en mémoire (clés, charges utiles dépaquetées, connexions actives) si elles n’ont pas été collectées avant |
Avant d’agir, collectez : une image mémoire d’au moins un hôte infecté, les fichiers et la persistance de l’implant, et les fenêtres de journaux pertinentes. Cartographiez les canaux de repli à partir de la configuration pour que le blocage du canal principal ne pousse pas l’implant vers un canal que vous ne surveillez pas. Dans la mesure du possible, contenez tous les canaux et tous les hôtes connus dans une seule fenêtre coordonnée, selon la même logique que le nettoyage de la persistance.
Rapporter le C2
Le rapport de triage liste les indicateurs réseau ; pour un incident, développez le C2 dans sa propre section :
- Chaque canal du tableau d’extraction ci-dessus, avec les identifiants ATT&CK, les indicateurs neutralisés (defanged) et, pour chacun, s’il a été observé ou seulement vu dans le code.
- Les détections qui se sont déclenchées ou qui ont été écrites : identifiants de règles, noms d’analyses, requêtes.
- Le périmètre : quels hôtes ont émis des beacons, première et dernière observation, et comment cela a été établi (hits sur le sinkhole, journaux DNS, analyses de beacons).
- Les actions de confinement menées, quand, et ce qu’elles ont pu masquer.
- Le niveau de partage de chaque indicateur, comme dans partager les configurations de manière responsable.
Lab : noter les beacons et les labels DNS dans des journaux Zeek synthétiques
Ce lab se place uniquement du côté détection. Vous générez quatre heures de
données conn.log et dns.log synthétiques pour six postes de travail au format
JSON de Zeek, avec un hôte qui émet des beacons vers une destination de
démonstration, puis vous classez chaque paire hôte-destination selon sa
probabilité d’être un beacon. Toutes les adresses proviennent des plages réservées
à la documentation et tous les noms sont des noms d’exemple réservés. Rien ne se
connecte à quoi que ce soit.
-
Préparez un dossier de travail et un environnement virtuel (bibliothèque standard uniquement) :
bash mkdir m7c && cd m7c python3 -m venv .venv .venv/bin/python --versiontext Python 3.14.7 -
Générez les journaux. Le générateur écrit des rafales de navigation irrégulières pour chaque hôte, une vérification de mise à jour logicielle toutes les 15 minutes sur chaque hôte, une longue session de visioconférence, et un implant sur
10.20.0.17qui dort 60 s avec 10 % de jitter :python # gen_logs.py - write synthetic Zeek-style conn.log and dns.log (JSON lines) import json, random random.seed(7) START = 1790668800 # 2026-09-29 08:00:00 UTC END = START + 4 * 3600 # four hours of traffic HOSTS = [f"10.20.0.{n}" for n in (11, 12, 13, 14, 15, 17)] BEACON_HOST = "10.20.0.17" # Destinations: name -> IP (documentation ranges only) SITES = {f"site{i:02d}.example.com": f"198.51.100.{i + 10}" for i in range(1, 25)} UPDATES = ("updates.example.net", "192.0.2.80") C2 = ("cdn-static.example.org", "203.0.113.50") MEET = ("meet.example.com", "198.51.100.200") conns, dns, uid = [], [], 0 def conn(ts, src, dst, port, dur, ob, rb, service="ssl", proto="tcp"): global uid uid += 1 conns.append({"ts": round(ts, 6), "uid": f"C{uid:06d}", "id.orig_h": src, "id.orig_p": random.randint(49152, 65535), "id.resp_h": dst, "id.resp_p": port, "proto": proto, "service": service, "duration": round(dur, 6), "orig_bytes": ob, "resp_bytes": rb, "conn_state": "SF"}) def lookup(ts, src, name, ip): dns.append({"ts": round(ts - 0.02, 6), "id.orig_h": src, "query": name, "qtype_name": "A", "rcode_name": "NOERROR", "answers": [ip], "TTLs": [300.0]}) # 1. Human browsing: bursts of connections at irregular times for h in HOSTS: t = START + random.uniform(0, 600) while t < END: name = random.choice(list(SITES)) lookup(t, h, name, SITES[name]) for _ in range(random.randint(1, 6)): conn(t, h, SITES[name], 443, random.uniform(0.2, 30), random.randint(400, 3000), random.randint(2000, 400000)) t += random.uniform(0.1, 5) t += random.expovariate(1 / 240) # mean 4 min between bursts # 2. Benign periodic: software update check every 15 min, all hosts, no jitter for h in HOSTS: t = START + random.uniform(0, 900) while t < END: lookup(t, h, *UPDATES) conn(t, h, UPDATES[1], 443, 0.35, 517, 1420) t += 900 # 3. Benign long connection: one meeting client holding a session open lookup(START + 3600, "10.20.0.12", *MEET) conn(START + 3600, "10.20.0.12", MEET[1], 443, 5400, 38_000_000, 41_000_000) # 4. The implant: 60 s sleep with 10 % jitter, small consistent requests t = START + 1234.5 while t < END: lookup(t, BEACON_HOST, *C2) conn(t, BEACON_HOST, C2[1], 443, random.uniform(0.25, 0.4), random.randint(410, 430), random.randint(250, 262)) t += 60 * random.uniform(0.9, 1.1) conns.sort(key=lambda c: c["ts"]) dns.sort(key=lambda d: d["ts"]) with open("conn.log", "w") as f: f.writelines(json.dumps(c) + "\n" for c in conns) with open("dns.log", "w") as f: f.writelines(json.dumps(d) + "\n" for d in dns) print(f"conn.log: {len(conns)} connections, dns.log: {len(dns)} queries")bash .venv/bin/python gen_logs.py head -1 conn.logtext conn.log: 1422 connections, dns.log: 637 queries {"ts": 1790668949.570667, "uid": "C000391", "id.orig_h": "10.20.0.13", "id.orig_p": 55295, "id.resp_h": "198.51.100.24", "id.resp_p": 443, "proto": "tcp", "service": "ssl", "duration": 19.054157, "orig_bytes": 1892, "resp_bytes": 59160, "conn_state": "SF"}Les noms de champs sont ceux de Zeek : le script de notation de l’étape suivante lit donc aussi un vrai
conn.logécrit avec la journalisation JSON de Zeek activée. -
Écrivez le script de notation des beacons. Il regroupe les connexions par source, destination et port, calcule quatre sous-scores entre 0 et 1 et en fait la moyenne. Il compte aussi combien d’hôtes internes parlent à chaque destination, et liste les paires au temps de connexion cumulé le plus élevé :
python # beacon_score.py - rank host->destination pairs in a Zeek conn.log by beacon likelihood import json, statistics as st, sys from collections import defaultdict MIN_CONNS = 10 # too few connections -> no meaningful timing statistics COUNT_SATURATE = 100 # this many connections in the window earns the full count score def load(path): with open(path) as f: return [json.loads(line) for line in f if line.strip()] conns = load(sys.argv[1]) names = {} # resp IP -> last name that resolved to it for d in load(sys.argv[2]) if len(sys.argv) > 2 else []: for ip in d.get("answers") or []: names[ip] = d["query"] pairs, dur_sum, talkers = defaultdict(list), defaultdict(float), defaultdict(set) for c in conns: key = (c["id.orig_h"], c["id.resp_h"], c["id.resp_p"]) pairs[key].append((c["ts"], c["orig_bytes"])) dur_sum[key] += c["duration"] or 0 talkers[c["id.resp_h"]].add(c["id.orig_h"]) def score(events): ts = sorted(t for t, _ in events) gaps = [b - a for a, b in zip(ts, ts[1:])] med = st.median(gaps) mad = st.median(abs(g - med) for g in gaps) cv = st.pstdev(gaps) / st.mean(gaps) sizes = [b for _, b in events] size_cv = st.pstdev(sizes) / st.mean(sizes) if st.mean(sizes) else 1.0 parts = { "cv": max(0.0, 1 - cv), # regular gaps -> low CV "mad": max(0.0, 1 - mad / med) if med else 0.0, # robust to a few outliers "count": min(1.0, len(events) / COUNT_SATURATE), "size": max(0.0, 1 - size_cv), # same request size every time } return sum(parts.values()) / len(parts), med, cv, parts rows = [] for (src, dst, port), ev in pairs.items(): if len(ev) < MIN_CONNS: continue total, med, cv, p = score(ev) rows.append((total, src, f"{dst}:{port}", names.get(dst, "?"), len(ev), med, cv, p, len(talkers[dst]))) rows.sort(reverse=True) all_hosts = len({c["id.orig_h"] for c in conns}) print(f"{'score':>5} {'src':<11} {'dst':<19} {'name':<24} {'n':>4} {'median':>7} " f"{'cv':>5} {'madS':>5} {'cntS':>5} {'sizeS':>5} {'hosts':>5}") for total, src, dst, name, n, med, cv, p, hosts in rows[:8]: print(f"{total:5.3f} {src:<11} {dst:<19} {name:<24} {n:4d} {med:6.1f}s " f"{cv:5.3f} {p['mad']:5.3f} {p['count']:5.3f} {p['size']:5.3f} {hosts:3d}/{all_hosts}") print("\nLongest cumulative connection time per pair:") for key, secs in sorted(dur_sum.items(), key=lambda kv: -kv[1])[:3]: src, dst, port = key print(f" {src:<11} {dst + ':' + str(port):<19} {names.get(dst, '?'):<24} " f"{secs / 3600:5.2f} h over {len(pairs[key])} connection(s)") -
Exécutez-le :
bash .venv/bin/python beacon_score.py conn.log dns.logtext score src dst name n median cv madS cntS sizeS hosts 0.971 10.20.0.17 203.0.113.50:443 cdn-static.example.org 220 60.1s 0.055 0.954 1.000 0.985 1/6 0.790 10.20.0.17 192.0.2.80:443 updates.example.net 16 900.0s 0.000 1.000 0.160 1.000 6/6 0.790 10.20.0.15 192.0.2.80:443 updates.example.net 16 900.0s 0.000 1.000 0.160 1.000 6/6 0.790 10.20.0.14 192.0.2.80:443 updates.example.net 16 900.0s 0.000 1.000 0.160 1.000 6/6 0.790 10.20.0.13 192.0.2.80:443 updates.example.net 16 900.0s 0.000 1.000 0.160 1.000 6/6 0.790 10.20.0.12 192.0.2.80:443 updates.example.net 16 900.0s 0.000 1.000 0.160 1.000 6/6 0.790 10.20.0.11 192.0.2.80:443 updates.example.net 16 900.0s 0.000 1.000 0.160 1.000 6/6 0.398 10.20.0.11 198.51.100.14:443 site04.example.com 11 3.5s 2.957 0.889 0.110 0.591 6/6 Longest cumulative connection time per pair: 10.20.0.12 198.51.100.200:443 meet.example.com 1.50 h over 1 connection(s) 10.20.0.13 198.51.100.22:443 site12.example.com 0.13 h over 26 connection(s) 10.20.0.14 198.51.100.19:443 site09.example.com 0.12 h over 27 connection(s) -
Lisez le classement en analyste. L’implant arrive premier : 220 connexions avec un écart médian de 60,1 s et un CV de 0,055, proche des 0,058 que prédit un jitter uniforme de 10 %, et des tailles de requêtes qui bougent à peine. La vérification de mise à jour est plus régulière (CV de 0) et n’est classée plus bas que parce qu’elle compte 16 connexions en quatre heures ; sur une semaine, elle égalerait ou dépasserait l’implant. Ce qui les distingue, c’est le contexte, pas le rythme : la destination de mise à jour est contactée par les six hôtes selon un calendrier identique (
6/6), se résout depuis un nom d’éditeur que vous pouvez vérifier dans votre inventaire logiciel, et chaque hôte la répète selon le même calendrier de 15 minutes. La destination de l’implant est contactée par un seul hôte (1/6). La paire de navigation de la dernière ligne montre pourquoi le MAD ne peut pas être utilisé seul : son écart médian est de 3,5 s, mesuré au sein des rafales, si bien que le score MAD atteint un flatteur 0,889, tandis que le CV de 2,957 la trahit. Dans la liste des durées, la session de 1,5 heure versmeet.example.comest la connexion longue bénigne ; confirmez-la avec le processus ou la catégorie du proxy avant de l’écarter. -
Ajoutez le filtre de rareté. Ne gardez que les destinations contactées par un seul hôte :
bash .venv/bin/python beacon_score.py conn.log dns.log | awk '$NF == "1/6"'text 0.971 10.20.0.17 203.0.113.50:443 cdn-static.example.org 220 60.1s 0.055 0.954 1.000 0.985 1/6En production, vous filtreriez selon la prévalence sur une baseline plus longue (une destination est rare si peu d’hôtes l’ont contactée au cours des 30 derniers jours), et non au sein d’une seule fenêtre de quatre heures.
-
Notez les labels DNS. Placez quelques noms dans
queries.txt: des noms d’hôtes normaux, un label haché de style CDN, trois noms de style DGA, un label de tunnel en base32 et un nom long mais lisible par un humain :text www.example.com mail.example.org login.example.net cdn-static.example.org d3k9x2mfq0z7ab.cdn.example.net xjqvkzrwpbtmnc.example kq7zf0p3xw9lm2vr.example tgbhuiplmwqzxa.example ovzwk4r5mrsw23z3nbxxg5b5o5ztany.t.example.net internationalization-docs.example.compython # label_score.py - score DNS labels by length and character entropy import math, sys from collections import Counter def entropy(s): counts = Counter(s) return max(0.0, -sum(n / len(s) * math.log2(n / len(s)) for n in counts.values())) def leftmost_label(name): return name.split(".")[0].lower() names = [line.strip() for line in open(sys.argv[1]) if line.strip()] print(f"{'query':<46} {'len':>3} {'H':>5} {'digits':>6} verdict") for name in names: lab = leftmost_label(name) h = entropy(lab) digits = sum(ch.isdigit() for ch in lab) / len(lab) suspicious = len(lab) >= 12 and h >= 3.3 print(f"{name:<46} {len(lab):3d} {h:5.2f} {digits:6.2f} " f"{'DGA-like' if suspicious else '-'}")bash .venv/bin/python label_score.py queries.txttext query len H digits verdict www.example.com 3 0.00 0.00 - mail.example.org 4 2.00 0.00 - login.example.net 5 2.32 0.00 - cdn-static.example.org 10 2.92 0.00 - d3k9x2mfq0z7ab.cdn.example.net 14 3.81 0.36 DGA-like xjqvkzrwpbtmnc.example 14 3.81 0.00 DGA-like kq7zf0p3xw9lm2vr.example 16 4.00 0.31 DGA-like tgbhuiplmwqzxa.example 14 3.81 0.00 DGA-like ovzwk4r5mrsw23z3nbxxg5b5o5ztany.t.example.net 31 4.09 0.26 DGA-like internationalization-docs.example.com 25 3.43 0.00 DGA-likeLes trois noms de style DGA et le label base32 (qui se décode en
user=demo;host=ws07) sont signalés, comme prévu. Deux noms bénins le sont aussi : le label haché du CDN et le long et lisibleinternationalization-docs. L’entropie d’une chaîne courte est plafonnée par sa longueur (un label de 14 caractères ne peut dépasser log2(14), soit environ 3,81 bits) : longueur et entropie sont donc corrélées, et un long label composé de vrais mots franchit le seuil. La notation des labels est un préfiltre. Associez-la aux signaux par hôte auxquels les DGA et les tunnels ne peuvent pas échapper : rafales de NXDOMAIN, nombreux sous-domaines uniques sous un même parent, et un domaine parent que personne d’autre dans l’entreprise ne résout.
Questions : Si l’implant dormait 8 heures avec 30 % de jitter, de quoi le
script de notation aurait-il besoin (longueur de fenêtre, COUNT_SATURATE,
pondérations) pour le classer encore au-dessus de la vérification de mise à jour ?
Quel champ unique de conn.log ajouteriez-vous au score de taille pour repérer
une réponse « aucune tâche » toujours de même longueur, et pourquoi un gros envoi
pourrait-il casser le motif sans que l’hôte soit sain pour autant ? Comment
confirmeriez-vous que updates.example.net est bénin sans le contacter ? Pour
10.20.0.17, listez dans l’ordre les actions de confinement que vous
entreprendriez, et ce que vous collecteriez en premier. Quelle caractéristique
supplémentaire empêcherait internationalization-docs d’être signalé sans perdre
les noms DGA ?
À retenir
- Le C2 transporte check-ins, tâches, résultats et staging ; il se répète aussi longtemps que dure l’intrusion, ce qui en fait la phase qui offre le plus d’occasions de détection et le plus de levier pour le confinement.
- Chaque famille de canaux laisse ses propres traces : petites requêtes régulières pour le polling HTTP, sous-domaines longs à forte entropie pour les tunnels DNS, appels d’API depuis le mauvais processus pour le C2 via services cloud, trafic de poste à poste ou vers des pairs résidentiels pour le P2P, et rafales de NXDOMAIN pour les DGA.
- Extrayez le C2 de l’échantillon via la configuration, les chaînes, le code réseau et une exécution contrôlée, et consignez les hôtes, l’ordre de repli, les URI, l’intervalle, le jitter, le user-agent, les détails TLS et de certificat, ainsi que ce qui a été observé ou seulement déduit.
- Les analyses de beacons combinent régularité du rythme (CV et MAD), nombre de connexions et constance des tailles ; c’est la rareté à travers les hôtes qui distingue un implant d’une vérification de mise à jour tout aussi régulière.
- Pivotez depuis un indicateur confirmé via JA4, les certificats, le DNS passif et Certificate Transparency, sans jamais toucher vous-même l’infrastructure.
- Collectez les preuves et cartographiez les canaux de repli avant de contenir ; le sinkholing trouve les hôtes, le blocage les coupe, l’isolement préserve la mémoire, et chacun vous cache quelque chose.