scapy le couteau suisse du NetOps

scapy le couteau suisse du NetOps

Scapy est un programme Python interactif et une lib Python qui permet de forger, envoyer, sniffer et décoder des paquets réseau, avec une approche très souple pour le test, l’analyse et la découverte réseau.

Ce que Scapy sait faire

Scapy sert surtout à trois choses : construire des paquets, les envoyer, puis analyser les réponses reçues.
Il peut aussi remplacer partiellement des outils comme hping, arping, p0f, tcpdump ou tshark pour certains usages ciblés. Son point fort est de renvoyer le paquet complet reçu, pas juste une interprétation binaire du type “ouvert/fermé”, ce qui permet d’analyser soi-même les détails utiles.[scapy.readthedocs]

Les usages les plus courants en sécurité réseau sont :

  • Découverte et cartographie réseau.
  • Scan de ports et validation de filtrage.
  • Traceroute TCP/ICMP.
  • Sniffing et analyse de trafic.
  • Fuzzing de champs de protocoles.
  • Tests d’hypothèses sur des stacks réseau ou des équipements.

Démarrage propre

Pour travailler proprement, utilise un environnement isolé, Python récent compatible, et lance Scapy avec les privilèges nécessaires seulement quand il faut émettre ou sniffer des paquets. Scapy documente aussi l’usage de conf.use_pcap, conf.L3socket, et conf.L2socket pour choisir les sockets réseau selon la plateforme et les besoins. Sur certaines plateformes, le pare-feu local peut répondre à la place de ta machine ou gêner les tests ; il faut alors adapter la configuration locale avec prudence.

Exemple minimal :

from scapy.all import *
a = IP(dst="192.0.2.10")/ICMP()
a.show()
ans = sr1(a, timeout=2)

Philosophie d’usage

La bonne manière d’utiliser Scapy, c’est de raisonner en couches réseau et en faits observables. Tu construis un stimulus précis, tu observes la réponse brute, puis tu interprètes toi-même le résultat. C’est beaucoup plus fiable que de dépendre d’une conclusion automatique, surtout dans des environnements filtrés, asymétriques ou mal configurés.

Trois réflexes utiles :

  • Toujours commencer par un paquet simple.
  • Ajouter une seule variable à la fois.
  • Sauvegarder les captures et rejouer ensuite l’analyse offline.

Flux de travail efficace

Un flux efficace ressemble souvent à ceci :

  1. Construire un paquet de base.
  2. Vérifier sa structure avec show() et hexdump().
  3. Envoyer avec sr1(), sr(), send(), ou sendp() selon la couche.
  4. Capturer les réponses avec filtres.
  5. Rejouer l’analyse sur un PCAP avec rdpcap() ou sniff(offline=...).

Exemple pratique :

pkt = IP(dst="192.0.2.10", ttl=1)/TCP(dport=80, flags="S")
pkt.show2()
ans, unans = sr(pkt, timeout=2)

Construction de paquets

Scapy utilise le symbole / pour empiler les couches, ce qui rend la construction très lisible. Les valeurs par défaut sont intelligentes : IP source, checksum, ports usuels, type Ethernet, etc. sont souvent remplis automatiquement. C’est pratique, mais en sécurité il faut parfois forcer explicitement les champs pour éviter les ambiguïtés d’analyse.

Exemples utiles :

Ether()/IP(dst="198.51.100.1")/TCP(dport=443, flags="S")
IP(dst=["198.51.100.1","198.51.100.2"])/ICMP()
IP(ttl=(1,5))/UDP(dport=[53,123])

Sniffing et analyse

sniff() est central pour l’apprentissage, car il permet d’observer le trafic tel qu’il est réellement vu par l’interface. Tu peux filtrer avec BPF, traiter paquet par paquet avec prn=, ou capturer hors ligne depuis un fichier PCAP. Scapy supporte aussi des sessions pour réassembler certains flux TCP et défragmenter l’IP à la volée, ce qui est très utile pour analyser des protocoles comme HTTP ou TLS.

Bonnes pratiques :

  • Filtre toujours au plus tôt.
  • Mets store=False si tu n’as pas besoin de tout conserver.
  • Utilise prn=lambda p: p.summary() pour du tri rapide.
  • Passe ensuite à show() ou sprintf() pour extraire les bons champs.

Envoi et réception

La distinction est simple :

  • send() : couche 3.
  • sendp() : couche 2.
  • sr() : envoi + réception à couche 3.
  • srp() : envoi + réception à couche 2.

Scapy peut gérer des ensembles de paquets et renvoyer les couples requête/réponse, ce qui est idéal pour des tests de portée ou de filtrage. Pour les scans, mieux vaut exploiter les réponses brutes que de se contenter d’un état interprété, car un ICMP peut signifier plusieurs choses selon le contexte.

Exemple :

ans, unans = sr(IP(dst="192.0.2.0/30")/TCP(dport=[22,80,443], flags="S"), timeout=2)
ans.summary()

Scan et découverte

Scapy permet de faire des scans très ciblés, par exemple un SYN scan minimal avec sr1(IP()/TCP(flags="S")) ou des tests multi-hôtes/multi-ports avec sr(). Il peut aussi générer des tableaux exploitables pour visualiser rapidement quels ports répondent et comment. L’intérêt n’est pas de “faire du bruit”, mais de formuler une hypothèse précise sur le réseau, puis de la vérifier.

Pour apprendre proprement :

  • Scan un seul port.
  • Puis une petite plage.
  • Puis plusieurs hôtes.
  • Puis change un seul paramètre à la fois : TTL, flags TCP, options, MSS, fenêtre, etc.

Fuzzing et robustesse

fuzz() est très utile pour la recherche de comportements inattendus, car il randomise les champs non calculés automatiquement. C’est particulièrement intéressant pour des protocoles simples ou des équipements réseau que tu veux tester de façon contrôlée. En revanche, il faut l’utiliser avec discipline, car du fuzzing mal cadré peut provoquer du bruit, des faux positifs ou des perturbations sur le réseau.

Bonne méthode :

  • Définis une base valide.
  • Fuzz uniquement une couche ou quelques champs.
  • Journalise ce que tu envoies.
  • Compare les réponses sur plusieurs itérations.

Automatisation et scripts

Scapy reste un framework Python, donc tu peux intégrer boucles, fonctions, structures de données et parsing avancé. C’est là qu’il devient vraiment puissant : corréler plusieurs séries de tests, normaliser des résultats, créer des mini-outils spécialisés, ou automatiser des vérifications récurrentes. Les objets PacketList, Results, Unanswered, summary(), make_table(), nsummary() et filter() sont très pratiques pour produire des analyses lisibles.

Exemple :

targets = ["192.0.2.10", "192.0.2.11"]
ports = [22, 80, 443]
ans, unans = sr(IP(dst=targets)/TCP(dport=ports, flags="S"), timeout=2)
open_only = ans.filter(lambda s, r: TCP in r and r[TCP].flags & 0x12 == 0x12)
open_only.summary()

Bonnes pratiques

Voici les bonnes pratiques que je te recommande :

  • Utiliser Scapy dans un lab ou sur des systèmes autorisés uniquement.
  • Travailler avec des comptes non permanents et des privilèges minimaux.
  • Capturer sur un fichier puis analyser offline dès que possible.
  • Écrire des scripts courts, lisibles et versionnés.
  • Toujours valider la structure du paquet avant envoi.
  • Toujours fixer timeout, retry, inter et les filtres pour maîtriser le comportement.[github]

Côté sécurité opérationnelle, il faut aussi surveiller les effets de bord locaux : réponses RST du noyau, pare-feu personnel, offload NIC, ou traitement par la pile locale qui fausse les résultats. En environnement pro, c’est important pour éviter de conclure à tort qu’un équipement distant a répondu d’une certaine manière.[github]

Astuces de travail

Quelques astuces qui font gagner du temps :

  • Utilise show2() au lieu de show() si tu veux voir les champs recalculés comme les checksums.[blog.shuvangkardas]
  • Utilise sprintf() pour extraire seulement les champs utiles.
  • Utilise hide_defaults() après désassemblage si l’affichage est trop verbeux.
  • Utilise wrpcap() et rdpcap() pour rejouer et comparer.
  • Utilise AsyncSniffer si tu veux arrêter le sniffing proprement par code.

Exemple d’extraction ciblée :

pkt.sprintf("%IP.src% -> %IP.dst%  %TCP.sport% -> %TCP.dport%  %TCP.flags%")

Pièges fréquents

Les erreurs classiques avec Scapy sont :

  • Mélanger couche 2 et couche 3.
  • Oublier que certaines fonctions demandent des privilèges élevés.
  • Interpréter trop vite une absence de réponse.
  • Laisser le pare-feu local perturber les tests.
  • Oublier que les champs par défaut peuvent masquer un problème de compréhension.[github]

Un autre piège courant est de se fier à un seul paquet alors que la réalité réseau peut dépendre de l’état, du timing, du chemin, ou d’un équipement intermédiaire. Pour l’apprentissage, répéter la même expérience plusieurs fois et comparer les réponses est souvent plus instructif qu’un gros scan.

Cheatsheet

Installation

# installation simple sous Debian
sudo apt install python3-scapy

# ou via Python & venv
python3 -m venv .venv
source .venv/bin/activate
python -m pip install scapy
python -m pip show scapy

Lancement

sudo scapy
python
from scapy.all import *

Création

IP()
IP(dst="192.0.2.1")
IP(dst="192.0.2.1")/ICMP()
Ether()/IP()/TCP()
IP()/TCP()/"GET / HTTP/1.0\r\n\r\n"

Inspection

pkt.show()
pkt.show2()
pkt.summary()
ls(IP)
hexdump(pkt)
raw(pkt)
pkt.command()
pkt.json()

Manipulation

pkt[IP].ttl = 10
del pkt[IP].ttl
pkt.haslayer(TCP)
pkt[TCP].flags
pkt.getlayer(IP)
pkt[IP].fields

Envoi

send(IP(dst="192.0.2.1")/ICMP())
sendp(Ether()/IP(dst="192.0.2.1")/ICMP(), iface="eth0")
sr(IP(dst="192.0.2.1")/ICMP())
sr1(IP(dst="192.0.2.1")/ICMP())
srp(Ether()/ARP(pdst="192.0.2.0/24"))
srp1(Ether()/ARP(pdst="192.0.2.1"))

Réception

sniff(count=10)
sniff(filter="icmp", iface="eth0")
sniff(prn=lambda p: p.summary(), store=False)
AsyncSniffer(iface="eth0", prn=lambda p: p.summary(), store=False)

Sauvegarde

wrpcap("capture.pcap", pkts)
pkts = rdpcap("capture.pcap")
pkts = sniff(offline="capture.pcap")

Scan

sr(IP(dst="192.0.2.1")/TCP(dport=80, flags="S"))
sr(IP(dst="192.0.2.1")/TCP(dport=[22,80,443], flags="S"))
ans, unans = sr(IP(dst="192.0.2.0/30")/TCP(dport=[22,80], flags="S"))
ans.summary()
ans.make_table(lambda s, r: (s.dst, s.dport, r.sprintf("%TCP.flags%")))

Fuzzing

fuzz(IP()/UDP()/DNS())
fuzz(IP(dst="192.0.2.1")/UDP()/Raw(b"A"*20))

Filtrage et extraction

pkt.sprintf("%IP.src%:%TCP.sport% -> %IP.dst%:%TCP.dport% %TCP.flags%")
ans.filter(lambda s, r: TCP in r and r[TCP].flags & 0x12 == 0x12)
ans.nsummary()

TCP / traceroute

sr(IP(dst="192.0.2.1", ttl=(1,30))/TCP(flags="S"))
traceroute(["192.0.2.1", "198.51.100.1"])

ARP

arping("192.0.2.0/24")
srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="192.0.2.0/24"))

DNS

sr1(IP(dst="192.0.2.53")/UDP()/DNS(rd=1, qd=DNSQR(qname="example.com")))

TCP options utiles

TCP(options=[("MSS", 1460), ("SAckOK", b""), ("WScale", 7), ("Timestamp", (123, 0))])

Utilitaires utiles

conf.use_pcap = True
conf.iface
conf.ifaces
conf.L3socket
conf.L2socket

Variables et boucles

for p in packets:
    print(p.summary())

Export/Import brut

b = bytes(pkt)
pkt2 = Ether(b)

Tableaux

ans.make_table(lambda s, r: (s.dst, s.dport, r.src))
ans.filter(lambda s, r: TCP in r).make_table(lambda s, r: (s.dst, s.dport, "X"))

Lecture rapide des réponses TCP

if TCP in r and r[TCP].flags & 0x12 == 0x12:
    print("SYN-ACK")
elif TCP in r and r[TCP].flags & 0x14 == 0x14:
    print("RST-ACK")

Plan d’apprentissage

  1. Construction de paquets.
  2. Lecture et décodage.
  3. send()/sr()/sniff().
  4. Filtrage et extraction.
  5. PCAP et analyse offline.
  6. Scan et traceroute contrôlés.
  7. Fuzzing ciblé.
  8. Mini-outils spécialisés Python.

Ressources utiles

La documentation officielle Scapy est la meilleure base pour les concepts, les fonctions et les limites pratiques. Le dépôt officiel et la section “troubleshooting” sont aussi utiles pour les problèmes de sockets, de firewall et de comportement local.