Leçon 8.5 · Encodage, crypto & signatures· 50 min
Écrire des signatures réseau
Transformez le code réseau d’un échantillon et le trafic capturé en règles Suricata visant la structure durable du protocole, puis testez-les sur des pcap.
Objectifs
- Classer les indicateurs réseau par durabilité et expliquer pourquoi la structure du protocole survit aux IP et aux domaines
- Combiner preuves statiques et dynamiques pour classer chaque champ d’une requête comme codé en dur, issu de la configuration, dérivé de l’hôte ou aléatoire
- Écrire des règles Suricata avec sticky buffers, flow, modificateurs de content, pcre, flowbits, seuils et métadonnées
- Tester une règle sur un pcap du trafic malveillant et sur du trafic bénin, et la garder rapide
- Décrire ce qui fonctionne encore quand le trafic est chiffré : SNI, JA3/JA4, champs de certificat et journaux Zeek
Dans Écrire vos premières règles YARA, vous avez transformé des artefacts présents dans un fichier en une règle qui trouve le fichier suivant. Les signatures réseau font le même travail une couche plus loin. Un malware (logiciel malveillant) qui communique avec son opérateur doit mettre des octets sur le réseau, et ces octets traversent des capteurs que vous contrôlez : une instance Suricata sur un port miroir, un cluster Zeek, un journal de proxy. Une bonne règle réseau détecte d’un coup tous les hôtes infectés, y compris ceux dont la copie de l’échantillon a été repackée, renommée ou n’a jamais touché le disque.
Cette leçon clôt le module 8. Les leçons précédentes vous ont donné les compétences de décodage et d’extraction de configuration pour découvrir ce que un échantillon envoie et d’où il tire chaque champ. Ici, vous transformez ce savoir en détections, vous les testez sur du trafic capturé et vous les préparez pour le partage.
Niveaux d’indicateurs : du fragile au durable
Les indicateurs réseau ne vieillissent pas tous de la même façon. La « Pyramid of Pain » de David Bianco le résume : moins un indicateur coûte à l’attaquant à changer, moins il a de valeur pour vous.
| Indicateur | Exemple | Durée de vie | Où il a sa place |
|---|---|---|---|
| Adresse IP | 203.0.113[.]44 | Quelques jours ; hébergements cloud et proxys tournent | Listes de blocage, recherche rétrospective dans les journaux |
| Domaine | update-check[.]example | Quelques jours à quelques semaines ; les domaines sont bon marché | Sinkholes DNS, règles dns.query, flux de renseignement |
| URL complète | /lab/gate.php?id=ecefe37b... | Souvent une seule requête | Nulle part sous forme littérale : une partie est aléatoire |
| Structure de l’URI | /lab/gate.php?id= + 16 caractères hexadécimaux | Des mois ; la changer impose de changer aussi le serveur | Règles Suricata |
| Ensemble et ordre des en-têtes, user-agent inhabituel | LabAgent/1.0 plus X-Lab: 1 | Jusqu’à ce que l’auteur réécrive le client | Règles Suricata, scripts Zeek |
| Comportement du protocole | Beacons à intervalle fixe, handshake maison, empreinte TLS | Le plus long ; lié à la base de code | Règles, listes JA4, analyses comportementales |
Les IP et les domaines figurent quand même dans votre rapport de triage, neutralisés (defanged), car c’est ce qu’un SOC peut bloquer le plus vite aujourd’hui. Mais une règle qui dit seulement « trafic vers cette IP » est un IOC (indicateur de compromission) déguisé en règle. L’opérateur déplace son infrastructure et la règle se tait alors que l’implant continue de tourner. Les cibles durables sont celles qui sont figées dans le code du client : la forme de la requête, les champs que l’auteur a codés en dur, l’encodage de la charge utile. Le serveur doit analyser tout cela ; les changer implique donc de redéployer les deux extrémités.
Preuves statiques et dynamiques ensemble
Une seule capture issue d’une seule exécution est une base dangereuse pour une règle. Tout y paraît constant parce que vous n’avez qu’un exemple. Le travail de l’analyste consiste à classer chaque champ du trafic selon sa provenance :
| Provenance | Exemple dans une requête | Constant d’une exécution à l’autre ? | Constant d’un hôte à l’autre ? | Usage en signature |
|---|---|---|---|---|
| Codé en dur dans le binaire | LabAgent/1.0, /lab/gate.php, X-Lab: 1 | Oui | Oui | Correspondance littérale |
| Configuration | Hôte C2, intervalle de beacon, identifiant de campagne | Oui, par build | Oui, par build | Correspondance par campagne, ou sur le format |
| Dérivé de l’hôte | Hash du nom d’hôte ou du numéro de série du volume dans l’identifiant | Oui | Non | Correspondance sur le format, jamais sur la valeur |
| Aléatoire ou horodaté | Nonce, clé de session, horodatage | Non | Non | Correspondance sur la longueur et le jeu de caractères uniquement |
| Fourni par une bibliothèque | Accept, Connection, ordre des en-têtes de WinINet | Oui | Oui | Faible ; partagé avec des logiciels légitimes |
L’analyse dynamique vous dit ce qui varie. Exécutez l’échantillon plusieurs fois au laboratoire, idéalement sur deux VM portant des noms différents, capturez avec le dispositif de Simuler et capturer le trafic réseau, et comparez les requêtes. Un champ qui change entre deux exécutions sur le même hôte est aléatoire. Un champ stable sur un hôte mais différent sur un autre est dérivé de l’hôte.
L’analyse statique vous dit pourquoi il varie et quelle est toute sa plage de
valeurs. Trouvez le code réseau par les imports (InternetOpenA,
HttpOpenRequestW, WinHttpSendRequest, send, getaddrinfo ; voir Lire
les capacités à partir des imports), ou par les
résolveurs de hachage d’API quand les imports sont
cachés. Remontez les arguments. Le user-agent passé à InternetOpenA est
souvent un littéral, éventuellement chiffré par XOR
et récupérable avec les scripts de Scripter le déchiffrement des
chaînes. Le paramètre id peut provenir
d’un appel wsprintfA("%08x%08x", ...) alimenté par un générateur de nombres
aléatoires, ce qui vous indique qu’il comporte toujours 16 caractères
hexadécimaux en minuscules. C’est un fait que vous ne pourriez pas déduire sans
risque de cinq requêtes capturées.
Astuce : Les exécutions dynamiques manquent des chemins de code. Un échantillon qui se contente d’émettre des beacons dans votre laboratoire peut aussi avoir des requêtes d’envoi de fichiers, d’erreur et de « résultat de tâche » avec des chemins ou des en-têtes différents. Lisez le code qui construit les requêtes et listez tous les formats qu’il peut produire, puis décidez lesquels votre jeu de règles doit couvrir. La leçon Commande et contrôle approfondit la conception des protocoles.
Anatomie d’une règle Suricata
Les règles Suricata descendent de la syntaxe de Snort, mais Suricata moderne inspecte des champs de protocole analysés au moyen de sticky buffers plutôt que des offsets bruts dans la charge utile. Une règle comporte une action, un en-tête et des options entre parenthèses :
alert http $HOME_NET any -> $EXTERNAL_NET any (
msg:"MAL LabImplant HTTP beacon";
flow:established,to_server;
http.method; content:"GET";
http.uri; content:"/lab/gate.php?id="; startswith; fast_pattern;
pcre:"/^\/lab\/gate\.php\?id=[0-9a-f]{16}$/";
http.user_agent; content:"LabAgent/"; startswith;
threshold:type limit, track by_src, count 1, seconds 3600;
reference:url,example.invalid/TR-2026-0929-01;
classtype:trojan-activity;
sid:1000001; rev:2;
metadata:created_at 2026_09_29, updated_at 2026_09_29, confidence High;
)Dans les fichiers de règles, chaque règle doit tenir sur une ligne (ou sur des lignes jointes par une barre oblique inverse finale). Elle est ici étalée pour la lecture. Les éléments :
| Élément | Signification |
|---|---|
alert | Action. Les autres sont pass, drop et reject (les deux dernières exigent le mode IPS en ligne) |
http | Protocole. Suricata identifie lui-même la couche applicative, donc http correspond à du HTTP sur n’importe quel port. Existent aussi tcp, udp, dns, tls, quic et d’autres |
$HOME_NET any -> $EXTERNAL_NET any | Source, port source, sens, destination, port de destination. Les variables proviennent de suricata.yaml |
msg | Le texte de l’alerte. Rédigez-le pour la personne qui triera l’alerte |
flow:established,to_server | N’inspecter que les données client vers serveur d’une session TCP établie |
http.method, http.uri, http.user_agent | Sticky buffers : chaque content ou pcre qui suit s’applique à ce champ jusqu’à ce qu’un autre buffer soit nommé |
content | Un littéral à trouver. Les octets hexadécimaux se placent entre barres verticales (X-Lab suivi de 3a 20 pour « : ») ; un ! initial inverse la condition |
startswith, endswith, nocase, bsize | Modificateurs qui ancrent, assouplissent ou limitent en taille la correspondance précédente |
pcre | Une expression régulière compatible Perl appliquée au buffer courant |
threshold | Limite le débit des alertes : ici une par source et par heure, quelle que soit la fréquence des beacons de l’implant |
reference, classtype, metadata | Contexte pour les humains et les outils ; ignoré lors de la correspondance |
sid, rev | Identifiant unique de la règle et son numéro de révision |
Autres buffers dont vous aurez besoin : http.host, http.header (tous les
en-têtes de requête, normalisés, une ligne Name: value\r\n chacun),
http.header_names (noms uniquement, ce qui révèle l’ordre des en-têtes),
http.request_body, http.stat_code, dns.query, tls.sni,
tls.cert_subject, tls.cert_issuer, ja3.hash et ja4.hash. Pour un
protocole TCP brut sans parseur, vous vous rabattez sur content appliqué à la
charge utile avec depth, offset, distance et within.
Flowbits : un état à travers une conversation
Certains protocoles ne deviennent distinctifs que sur plusieurs messages. Les flowbits posent un drapeau nommé sur un flux dans une règle et le testent dans une autre :
alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"LAB handshake seen"; flow:established,to_server; content:"LABHELLO|00|"; depth:9; flowbits:set,lab.hello; flowbits:noalert; sid:1000010; rev:1;)
alert tcp $EXTERNAL_NET any -> $HOME_NET any (msg:"LAB server tasking after handshake"; flow:established,to_client; flowbits:isset,lab.hello; content:"TASK"; depth:4; sid:1000011; rev:1;)La première règle n’alerte jamais ; elle se contente de marquer le flux. La
seconde se déclenche sur une chaîne de quatre octets qui serait bien trop
générique seule, mais qui devient spécifique après le handshake du client.
xbits étend la même idée à plusieurs flux, suivis par hôte ou par paire d’IP.
Cibler les parties stables
Une fois les champs classés, l’écriture de règles devient mécanique :
- Ancrez-vous sur le littéral codé en dur le plus spécifique, dans le
buffer le plus étroit.
LabAgent/danshttp.user_agentvaut mieux que la même chaîne recherchée n’importe où dans la charge utile. - Décrivez les champs variables par leur forme, pas par leur valeur. Un
identifiant aléatoire devient
[0-9a-f]{16}; un blob base64 devient une classe de caractères et une plage de longueurs. Utilisezbsizeouurilenquand la longueur est fixe. - Exigez au moins deux caractéristiques indépendantes. Le chemin seul finira tôt ou tard par coïncider avec une application légitime ; chemin plus user-agent plus en-tête personnalisé, non.
- Prévoyez les versions auxquelles vous vous attendez. Recherchez
LabAgent/plutôt queLabAgent/1.0si la chaîne de version provient de la configuration et change à chaque build. - Méfiez-vous de l’imitation. Les auteurs copient de vrais user-agents de navigateurs, parfois avec une faute de frappe, une version obsolète ou une espace manquante. La faute est la signature ; le reste de la chaîne est du camouflage.
Performances
Suricata n’évalue pas chaque règle sur chaque paquet. Il regroupe les règles par protocole et par port, puis exécute un moteur de recherche multi-motifs (MPM) sur chaque buffer, à la recherche d’un fast pattern par règle. Seules les règles dont le fast pattern apparaît sont entièrement évaluées. C’est la même idée que les atomes de YARA.
- Par défaut, Suricata choisit lui-même le fast pattern, généralement le
contentle plus long dans le buffer de plus haute priorité. Désignez-en un meilleur avecfast_patternquand vous connaissez la chaîne la plus distinctive, comme le fait la règle du lab. - Une règle sans aucun
content, avec seulement unpcre, n’a rien sur quoi préfiltrer. Elle est évaluée pour chaque transaction qui correspond à son en-tête, ce qui coûte cher sur un lien chargé. Associez toujours une expression régulière à un littéral qui doit également être présent. - Les littéraux courts ou courants (
GET,.php,Mozilla) font de mauvais fast patterns, car ils correspondent en permanence. - Gardez l’en-tête restrictif :
alert httpplutôt quealert ip, etflow:to_serverchaque fois que vous inspectez des requêtes.
suricata --engine-analysis produit un rapport sur la façon dont chaque règle
chargée sera préfiltrée et signale les règles dont le fast pattern est médiocre
ou absent. Exécutez-le à chaque modification de règle.
Les réalités de l’ère TLS
La majeure partie du trafic malveillant actuel passe en HTTPS. Sans déchiffrement au niveau d’un proxy, la charge utile, l’URI et les en-têtes sont invisibles. Ce qui reste :
| Champ visible | Buffer Suricata | Remarques |
|---|---|---|
| Nom du serveur (SNI) | tls.sni | Choisi par le client ; fragile comme un domaine, mais visible. Encrypted Client Hello le masque là où il est déployé |
| Résolution DNS avant la connexion | dns.query | Sauf si l’échantillon utilise DNS over HTTPS |
| Empreinte du client | ja3.hash, ja4.hash | Résume le ClientHello. JA3 a cessé de fonctionner quand les navigateurs ont commencé à mélanger l’ordre des extensions ; JA4 les trie et reste stable |
| Empreinte du serveur | ja3s.hash | Caractérise la pile TLS du serveur |
| Sujet, émetteur et validité du certificat | tls.cert_subject, tls.cert_issuer | Les certificats autosignés ou par défaut des frameworks de C2 sont des signaux forts |
| Chronologie et tailles | conn.log de Zeek, enregistrements de flux | Des beacons réguliers de taille similaire ressortent sur plusieurs heures |
Une empreinte JA3 ou JA4 identifie une bibliothèque TLS et sa configuration, pas un programme. Un échantillon qui utilise la pile TLS de Windows partage son empreinte avec tous les autres clients WinHTTP de l’hôte ; traitez-la donc comme une preuve d’appoint. Elle devient forte quand le malware embarque sa propre bibliothèque TLS, comme le font de nombreux implants en Go et en Rust.
Attention : Quand le canal est chiffré de bout en bout, la détection réseau se réduit aux métadonnées. Les détections durables se déplacent alors vers le poste : YARA sur la mémoire après déchiffrement, télémétrie EDR sur le processus qui établit la connexion, et artefacts hôte de votre rapport de triage. Consignez ce déplacement dans votre rapport plutôt que de livrer une règle SNI en déclarant la famille couverte.
Zeek : le même trafic sous forme de journaux
Suricata répond à la question « ce motif s’est-il produit ? ». Zeek répond à
« que s’est-il passé ? » en transformant le trafic en journaux structurés :
conn.log (chaque connexion, avec durées et nombres d’octets), http.log
(méthode, hôte, URI, user-agent, statut), dns.log, ssl.log (SNI, version,
chaîne de certificats ; JA3/JA4 via des paquets) et files.log. Exécuter
zeek -r capture.pcap dans un répertoire vide produit ces journaux à partir
d’une capture.
Les deux outils se complètent. Une règle Suricata est précise et se déclenche
en temps réel. Les journaux Zeek vous permettent de chasser rétrospectivement
sur des mois avec une requête du type « toutes les entrées de http.log dont le
user-agent commence par LabAgent/ », ou de repérer des beacons en regroupant
conn.log par source et destination et en examinant la dispersion des
intervalles. Les scripts Zeek et son framework Intel peuvent aussi lever des
notifications ; les petites détections n’ont donc pas toujours besoin d’une
règle Suricata.
Partager et versionner les règles
Les règles sont du code, et un jeu de règles partagé est une base de code partagée :
- SID. La plage 1000000 à 1999999 est par convention réservée aux règles locales. Les jeux publics comme Emerging Threats ont leurs propres plages. Ne réutilisez jamais un SID pour une autre règle.
rev. Incrémentez-le à chaque modification, afin que capteurs, corrélations SIEM et collègues sachent quelle version s’est déclenchée.metadata. Des clés à la manière d’Emerging Threats commecreated_at,updated_at,malware_family,confidenceetsignature_severitypermettent aux outils de filtrer et d’acheminer les alertes.- Tests. Conservez à côté de chaque règle le pcap à partir duquel elle a été
écrite, avec un pcap bénin, et exécutez
suricata -rsur les deux dans la CI. - Distribution.
suricata-updatedéploie les sources de règles sur les capteurs, et les règles voyagent dans la section détection de votre rapport, marquées du TLP du rapport.
Lab : signature pour un beacon de laboratoire
Vous allez générer un pcap inoffensif contenant des « beacons » HTTP émis par
un client de démonstration vers 127.0.0.1, mêlés à du trafic de navigateur
bénin, écrire une règle Suricata ciblant les champs stables du beacon, puis la
tester. Aucun paquet n’est envoyé nulle part : le script construit les paquets
en mémoire et les écrit dans un fichier. Il vous faut Python 3 et, idéalement,
Suricata (brew install suricata, apt install suricata, ou le MSI Windows).
-
Créez un répertoire de travail avec son propre environnement virtuel :
bash mkdir m8c && cd m8c python3 -m venv .venv .venv/bin/pip install scapy dpkt -
Enregistrez
make_pcap.py. Il écrit neuf sessions TCP complètes : cinq beacons à 60 secondes d’intervalle, chacun avec un nouvel identifiant aléatoire, plus quatre requêtes bénignes. L’une des requêtes bénignes utilise délibérément le même chemin que le beacon, comme pourrait le faire une vraie application web :python """make_pcap.py - write a harmless pcap of lab "beacons" plus benign traffic.""" import random from scapy.all import Ether, IP, TCP, Raw, wrpcap random.seed(1337) # reproducible "random" IDs and ports CLIENT, SERVER = "127.0.0.2", "127.0.0.1" T0 = 1790676000.0 # 2026-09-29 10:00:00 UTC BROWSER_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36") packets = [] def session(t, path, headers, body=b"OK"): sport = random.randint(49152, 65535) cseq, sseq = random.getrandbits(32), random.getrandbits(32) c = Ether() / IP(src=CLIENT, dst=SERVER) s = Ether() / IP(src=SERVER, dst=CLIENT) req = f"GET {path} HTTP/1.1\r\nHost: {SERVER}\r\n" req += "".join(f"{k}: {v}\r\n" for k, v in headers) + "\r\n" req = req.encode() resp = (b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n" b"Content-Length: %d\r\nConnection: close\r\n\r\n" % len(body)) + body seq = [ c / TCP(sport=sport, dport=80, flags="S", seq=cseq), s / TCP(sport=80, dport=sport, flags="SA", seq=sseq, ack=cseq + 1), c / TCP(sport=sport, dport=80, flags="A", seq=cseq + 1, ack=sseq + 1), c / TCP(sport=sport, dport=80, flags="PA", seq=cseq + 1, ack=sseq + 1) / Raw(req), s / TCP(sport=80, dport=sport, flags="PA", seq=sseq + 1, ack=cseq + 1 + len(req)) / Raw(resp), c / TCP(sport=sport, dport=80, flags="FA", seq=cseq + 1 + len(req), ack=sseq + 1 + len(resp)), s / TCP(sport=80, dport=sport, flags="FA", seq=sseq + 1 + len(resp), ack=cseq + 2 + len(req)), c / TCP(sport=sport, dport=80, flags="A", seq=cseq + 2 + len(req), ack=sseq + 2 + len(resp)), ] for i, p in enumerate(seq): p.time = t + i * 0.001 packets.extend(seq) # Five beacons, one every 60 seconds, each with a fresh random 8-byte ID. for n in range(5): beacon_id = random.getrandbits(64).to_bytes(8, "big").hex() session(T0 + 60 * n, f"/lab/gate.php?id={beacon_id}", [("User-Agent", "LabAgent/1.0"), ("X-Lab", "1"), ("Accept", "*/*")], body=b"sleep=60") # Benign traffic, interleaved in time, for the false-positive check. session(T0 + 15, "/index.html", [("User-Agent", BROWSER_UA), ("Accept", "text/html")]) session(T0 + 95, "/static/app.js", [("User-Agent", BROWSER_UA), ("Accept", "*/*")]) session(T0 + 130, "/lab/gate.php?page=2", # path collision [("User-Agent", BROWSER_UA), ("Accept", "text/html")]) session(T0 + 200, "/health", [("User-Agent", "curl/8.7.1"), ("Accept", "*/*")]) packets.sort(key=lambda p: p.time) wrpcap("beacons.pcap", packets) print(f"wrote beacons.pcap: {len(packets)} packets, {len(packets) // 8} TCP sessions")bash .venv/bin/python make_pcap.pytext wrote beacons.pcap: 72 packets, 9 TCP sessions -
Examinez le trafic comme vous le feriez pour une vraie capture. Dans Wireshark, filtrez sur
http.request; ou utilisez tshark pour lister les beacons et le temps écoulé depuis le précédent (tshark 4.6.4 ici) :bash tshark -r beacons.pcap -Y 'http.user_agent contains "LabAgent"' \ -T fields -e frame.time_delta_displayed -e http.request.uritext /lab/gate.php?id=ecefe37b9e250d03 60.000000000 /lab/gate.php?id=2a6a7b5fbb5d75b8 60.000000000 /lab/gate.php?id=5c1ed35fca2410fd 60.000000000 /lab/gate.php?id=34775758b2767375 60.000000000 /lab/gate.php?id=a29c4db3d2083355Classez les champs comme le fait le tableau vu plus haut dans cette leçon. Le chemin, le user-agent et l’en-tête
X-Labne changent jamais ; l’identifiant change à chaque requête ; l’intervalle est fixe (dans un vrai malware, il proviendrait de la configuration, souvent avec une gigue). -
Écrivez
lab.rulesavec un premier jet naïf et une règle qui cible les parties stables. Chaque règle doit tenir sur une seule ligne dans le fichier :text # Naive first draft: the path alone. alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"LAB naive gate.php request"; flow:established,to_server; http.uri; content:"/lab/gate.php"; classtype:trojan-activity; sid:1000000; rev:1;) # Targets the stable parts: method, URI shape, user-agent prefix, custom header. alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"LAB LabAgent HTTP beacon to gate.php"; flow:established,to_server; http.method; content:"GET"; http.uri; content:"/lab/gate.php?id="; startswith; fast_pattern; pcre:"/^\/lab\/gate\.php\?id=[0-9a-f]{16}$/"; http.user_agent; content:"LabAgent/"; startswith; http.header; content:"X-Lab|3a 20|1|0d 0a|"; nocase; reference:url,example.invalid/TR-2026-0929-01; classtype:trojan-activity; sid:1000001; rev:1; metadata:created_at 2026_09_29, updated_at 2026_09_29, malware_family LabImplant, confidence High;) -
Exécutez Suricata sur le pcap. Le trafic du lab va de
127.0.0.2vers127.0.0.1; définissez doncHOME_NETsur le client,$EXTERNAL_NETvalant par défaut tout le reste.-Sne charge que ce fichier de règles, et-k nonedésactive la validation des sommes de contrôle, ce qui est courant avec des captures générées ou issues d’un déchargement matériel (offload) :bash mkdir -p logs suricata -r beacons.pcap -S lab.rules -l logs -k none \ --set 'vars.address-groups.HOME_NET=[127.0.0.2]' cat logs/fast.logAvec Suricata 8.0.7 (l’image Docker
jasonish/suricataconvient si vous ne voulez pas l’installer),fast.logcontient une ligne par alerte, indiquant le SID, le message, la classification et la paire d’IP. Triée par date, et classification abrégée :text 09/29/2026-10:00:00 [**] [1:1000000:1] LAB naive gate.php request [**] ... {TCP} 127.0.0.2:61140 -> 127.0.0.1:80 09/29/2026-10:00:00 [**] [1:1000001:1] LAB LabAgent HTTP beacon to gate.php [**] ... {TCP} 127.0.0.2:61140 -> 127.0.0.1:80 09/29/2026-10:01:00 [**] [1:1000000:1] LAB naive gate.php request [**] ... {TCP} 127.0.0.2:60056 -> 127.0.0.1:80 09/29/2026-10:01:00 [**] [1:1000001:1] LAB LabAgent HTTP beacon to gate.php [**] ... {TCP} 127.0.0.2:60056 -> 127.0.0.1:80 09/29/2026-10:02:00 [**] [1:1000000:1] LAB naive gate.php request [**] ... {TCP} 127.0.0.2:59232 -> 127.0.0.1:80 09/29/2026-10:02:00 [**] [1:1000001:1] LAB LabAgent HTTP beacon to gate.php [**] ... {TCP} 127.0.0.2:59232 -> 127.0.0.1:80 09/29/2026-10:02:10 [**] [1:1000000:1] LAB naive gate.php request [**] ... {TCP} 127.0.0.2:59337 -> 127.0.0.1:80 09/29/2026-10:03:00 [**] [1:1000000:1] LAB naive gate.php request [**] ... {TCP} 127.0.0.2:61108 -> 127.0.0.1:80 09/29/2026-10:03:00 [**] [1:1000001:1] LAB LabAgent HTTP beacon to gate.php [**] ... {TCP} 127.0.0.2:61108 -> 127.0.0.1:80 09/29/2026-10:04:00 [**] [1:1000000:1] LAB naive gate.php request [**] ... {TCP} 127.0.0.2:62217 -> 127.0.0.1:80 09/29/2026-10:04:00 [**] [1:1000001:1] LAB LabAgent HTTP beacon to gate.php [**] ... {TCP} 127.0.0.2:62217 -> 127.0.0.1:80logs/eve.jsoncontient les mêmes alertes avec toutes les métadonnées HTTP de chaque transaction. -
Si vous n’avez pas Suricata, le script suivant peut le remplacer. Ce n’est pas un IDS : il analyse chaque requête avec dpkt et n’applique que les options
content,nocase,startswithetpcrede chaque règle aux buffers correspondants, en ignorant tout le reste. Ses verdicts ci-dessous concordent, requête par requête, avec la sortie de Suricata de l’étape 5. Enregistrez-le sousmatch_rules.py:python """match_rules.py - a STAND-IN for Suricata. It understands only the sticky buffers http.method, http.uri, http.user_agent and http.header, and the options content (with |hex| bytes), nocase, startswith and pcre.""" import re, sys import dpkt def parse_rule(line): opts = line[line.index("(") + 1: line.rindex(")")] parts, cur, quoted = [], "", False for ch in opts: # split on ';' outside quotes if ch == '"' and not cur.endswith("\\"): quoted = not quoted if ch == ";" and not quoted: parts.append(cur.strip()); cur = "" else: cur += ch rule, checks, buf = {}, [], None for p in filter(None, parts): key, _, val = p.partition(":") if key in ("http.method", "http.uri", "http.user_agent", "http.header"): buf = key elif key == "content": raw = val.strip('"') raw = re.sub(r"\|([0-9a-fA-F ]+)\|", lambda m: bytes.fromhex(m.group(1)).decode("latin-1"), raw) checks.append({"buf": buf, "type": "content", "value": raw}) elif key in ("nocase", "startswith"): checks[-1][key] = True elif key == "pcre": body = val.strip('"') pat, flags = body[1:body.rindex("/")], body[body.rindex("/") + 1:] checks.append({"buf": buf, "type": "pcre", "re": re.compile(pat, re.I if "i" in flags else 0)}) elif key in ("msg", "sid"): rule[key] = val.strip('"') rule["checks"] = checks return rule def buffers(req): hdr = "".join(f"{k}: {v}\r\n" for k, v in req.headers.items()) return {"http.method": req.method, "http.uri": req.uri, "http.user_agent": req.headers.get("user-agent", ""), "http.header": hdr} def check(c, bufs): data = bufs[c["buf"]] if c["type"] == "pcre": return bool(c["re"].search(data)) needle = c["value"] if c.get("nocase"): data, needle = data.lower(), needle.lower() return data.startswith(needle) if c.get("startswith") else needle in data rules = [parse_rule(l) for l in open(sys.argv[1]) if l.startswith("alert")] for ts, frame in dpkt.pcap.Reader(open(sys.argv[2], "rb")): tcp = dpkt.ethernet.Ethernet(frame).data.data if not (isinstance(tcp, dpkt.tcp.TCP) and tcp.dport == 80 and tcp.data): continue req = dpkt.http.Request(tcp.data) bufs = buffers(req) hits = [r["sid"] for r in rules if all(check(c, bufs) for c in r["checks"])] ua = bufs["http.user_agent"][:14] print(f"{ts - 1790676000:6.1f}s {req.uri:36} {ua:14} " f"{'ALERT sid ' + ','.join(hits) if hits else '-'}")bash .venv/bin/python match_rules.py lab.rules beacons.pcaptext 0.0s /lab/gate.php?id=ecefe37b9e250d03 LabAgent/1.0 ALERT sid 1000000,1000001 15.0s /index.html Mozilla/5.0 (W - 60.0s /lab/gate.php?id=2a6a7b5fbb5d75b8 LabAgent/1.0 ALERT sid 1000000,1000001 95.0s /static/app.js Mozilla/5.0 (W - 120.0s /lab/gate.php?id=5c1ed35fca2410fd LabAgent/1.0 ALERT sid 1000000,1000001 130.0s /lab/gate.php?page=2 Mozilla/5.0 (W ALERT sid 1000000 180.0s /lab/gate.php?id=34775758b2767375 LabAgent/1.0 ALERT sid 1000000,1000001 200.0s /health curl/8.7.1 - 240.0s /lab/gate.php?id=a29c4db3d2083355 LabAgent/1.0 ALERT sid 1000000,1000001 -
Lisez le résultat en ingénieur détection. Les deux règles détectent les cinq beacons. La règle naïve se déclenche aussi sur le
/lab/gate.php?page=2du navigateur, un faux positif qui arriverait dans la file d’un SOC à chaque visite d’une application légitime utilisant ce chemin. Le SID 1000001 reste muet sur les quatre requêtes bénignes, car la requête du navigateur échoue à la fois aux contrôles du motif d’identifiant, du user-agent et de l’en-tête. Supprimez le SID 1000000. -
Rendez la règle prête pour la production. Ajoutez
threshold:type limit, track by_src, count 1, seconds 3600;pour qu’un hôte qui émet un beacon chaque minute lève une alerte par heure plutôt que soixante, passezrevà 2 et relancez l’étape 5 (l’étape 6 ignore les seuils). Les cinq beacons ne produisent plus qu’une seule alerte :text 09/29/2026-10:00:00 [**] [1:1000001:2] LAB LabAgent HTTP beacon to gate.php [**] ... {TCP} 127.0.0.2:61140 -> 127.0.0.1:80Exécutez ensuite
suricata --engine-analysis -S lab.rules -l logset ouvrezlogs/rules_analysis.txt. Pour le SID 1000001, il indiqueFast Pattern "/lab/gate.php?id=" on "http request uri (http_uri)" buffer, le content que vous avez marqué avecfast_pattern. -
Recherchez les faux positifs à grande échelle. Rejouez une capture bénigne plus volumineuse avec la même règle : un enregistrement de votre propre navigation dans la VM du laboratoire, ou des pcap bénins publics. Toute alerte à ce stade révèle une caractéristique que vous pensiez unique et qui ne l’est pas.
Questions : Quel champ du beacon classeriez-vous comme aléatoire, lequel
comme codé en dur, et quelle analyse statique le confirmerait ? Si le build
suivant envoyait LabAgent/1.1 et passait l’en-tête en minuscules
(x-lab: 1), le SID 1000001 se déclencherait-il encore, et quels modificateurs
en sont responsables ? Pourquoi fast_pattern est-il placé sur le content de
l’URI plutôt que sur GET ? Si l’auteur faisait passer le beacon en HTTPS avec
la pile TLS de Windows, quelles parties de cette règle fonctionneraient encore,
par quoi pourrait-on les remplacer, et que transmettriez-vous plutôt à l’équipe
en charge des postes ?
À retenir
- Les IP et les domaines sont des IOC à courte durée de vie ; les règles réseau durables ciblent ce que le code du client code en dur : structure de l’URI, en-têtes, encodages et comportement du protocole.
- Classez chaque champ comme codé en dur, issu de la configuration, dérivé de l’hôte, aléatoire ou fourni par une bibliothèque, en utilisant des exécutions dynamiques répétées pour savoir ce qui varie et l’analyse statique pour savoir pourquoi et dans quelle mesure.
- Une règle Suricata se compose d’une action, d’un en-tête et d’options ; les
sticky buffers tels que
http.uri,http.user_agent,dns.queryettls.snigardent les correspondances précises, et lesflowbitsajoutent un état à travers les messages. - Exigez plusieurs caractéristiques indépendantes, décrivez les champs variables
par leur forme et donnez à chaque règle un littéral distinctif comme fast
pattern ; ne livrez jamais une règle reposant uniquement sur
pcre. - Testez chaque règle sur un pcap du trafic malveillant et sur du trafic bénin,
puis versionnez-la avec
sid,rev, des métadonnées et ses captures de test. - Sous TLS, la détection se déplace vers le SNI, JA4, les certificats et la chronologie, et finalement vers le poste ; les journaux Zeek vous permettent de chasser rétrospectivement dans le même trafic.