====== Fabriquer des paquets réseaux avec Scapy ======
====== Remarques préalables ======
* il n'est pas nécessaire de renseigner tous les champs. Ce sont alors les valeurs par défaut qui sont utilisées ;
* on encapsule simplement les couches réseaux (du modèle OSI), des plus basses aux plus élevées avec l'opérateur **/** ;
* la résolution DNS est automatique automatique.
====== Créer une trame simple Ethernet simple ======
* créer une trame en mémoire et l'afficher
>>> trame = Ether()
>>> trame.show()
###[ Ethernet ]###
WARNING: Mac address to reach destination not found. Using broadcast.
dst= ff:ff:ff:ff:ff:ff
src= 00:00:00:00:00:00
type= 0x9000
>>>
* Une trame éthernet est crée en instanciant la **classe Ether()**.
* la **méthode show()** de la classe affiche les informations de la trame. Comme aucun paramètre n'est fourni, ce sont les valeurs par défaut qui sont utilisées pour les attributs **dst**, **src** et **type**.
{{ :dev:python:scapy:trameethernet.png |}}
* renseigner les attributs de la trame en ajoutant l'adresse MAC du destinataire :
>>> trame.dsp = 'ac:84:c9:db:fb:c0'
>>> trame.show()
>>> trame.dst='ac:84:c9:db:fb:c0'
>>> trame.show()
###[ Ethernet ]###
dst= ac:84:c9:db:fb:c0
src= 00:00:00:00:00:00
type= 0x9000
>>>
Il est bien sur possible de préciser cette adresse MAC à la création de la trame :
>>> trame = Ether(dst='ac:84:c9:db:fb:c0')
* **Envoi** de la **trame Ethernet** sur le réseau. Utilisation de la fonction **sendp()** :
>>> sendp(trame)
.
Sent 1 packets.
>>>
Le point **"."** représente un envoi.
La trame Ethernet a été envoyée mais :
* c'est une **coquille vide**
* car cette trame ne contient aucune donnée.
Il faut maintenant **encapsuler** des données d'un protocole des **couches supérieures** dans cette trame vide.
La commande **sendp()** permet d'envoyer un paquet créé (forgé) à partir du **niveau 2** (couche Ethernet).
La commande **send()** permet d'envoyer un paquet créé (forgé) à partir du **niveau 3** (couche IP). Les informations du niveau 2 (la couche ethernet) sont alors automatiquement renseigné par scapy.
====== Créer une trame Ethernet contenant un paquet ICMP ======
La **commande ping** qui utilise le **protocole ICMP** permet :
* d'envoyer un paquet ICMP **echo-request** à un hôte distant,
* et à indiquer si un paquet ICMP **echo-reply** a été renvoyé.
* création d'un paquet ICMP **echo-request** :
>>> ping = ICMP()
>>> ping.show()
###[ ICMP ]###
type= echo-request
code= 0
chksum= None
id= 0x0
seq= 0x0
>>>
Par défaut, **l'instanciation** de la **classe ICMP()** met le type du ping à **echo-request**.
Page **Wikipedia** sur le protocole ICMP :
* https://fr.wikipedia.org/wiki/Internet_Control_Message_Protocol
Quelques précisions sur le fonctionnement du protocole ICMP :
* ICMP se situe au même niveau que le protocole IP,
* mais qu'il soit à un niveau équivalent au protocole IP, un paquet ICMP doit néanmoins être encapsulé dans un datagramme IP.
Pour **envoyer** un paquet **ICMP**, il faut :
* **encapsuler** le paquet ICMP dans un **datagramme IP**,
* **encapsuler** à son tour le datagramme IP dans une **trame Ethernet**.
Avec Scapy, **l'encapsulation** entre protocoles se réalise avec l'opérateur **/ (slash)**.
* **Création d'une trame Ethernet** encapsulant un paquet **ICMP** destiné à être envoyé à l'adresse 192.168.1.1 (Box Internet) :
>>> trame = Ether() / IP(dst='192.168.1.1') / ICMP()
>>> trame.show()
###[ Ethernet ]###
dst= ac:84:c9:db:fb:c0
src= 00:15:5d:01:c6:02
type= 0x800
###[ IP ]###
version= 4
ihl= None
tos= 0x0
len= None
id= 1
flags=
frag= 0
ttl= 64
proto= icmp
chksum= None
src= 192.168.1.159
dst= 192.168.1.1
\options\
###[ ICMP ]###
type= echo-request
code= 0
chksum= None
id= 0x0
seq= 0x0
>>>
**Seule** l'**adresse IP** du destinataire a été **renseignée**. Cependant Scapy a complété **automatiquement** les autres champs :
* pour les informations de la couche 2 Ethernet :
* les **adresses MAC** source et destination : attributs **dst** et src
* le **type** de trame Ethernet : attribut **type**
* pour la couche 3 IP :
* les adresses IP source et destination
* les autres informations de la couche 3.
* envoi de la trame Ethernet :
>>> sendp(trame)
.
Sent 1 packets.
>>>
Le paquet est envoyé mais **aucune réponse n'est reçue**. Pour cela il faut utiliser les fonctions suivantes qui permettent **d'envoyer** la trame et de **recevoir** la réponse :
* **srp()** qui renvoie **deux** objets :
* le premier contient les **paquets émis** et leurs **réponses associée**s,
* l'autre contient les **paquets sans réponse**.
* **srp1()** fonction **plus simple** car ne renvoie **renvoie** qu'un seul objet, **la première réponse**.
>>> rep, non_rep = srp(trame)
Begin emission:
..Finished to send 1 packets.
.*
Received 4 packets, got 1 answers, remaining 0 packets
>>> rep
>>> non_rep
>>>
Avec Scapy :
* un point **"."** représente un envoi,
* une étoile **"*"** représente une réponse.
Il y a **une** réponse et **zéro** échec. De plus la réponse est un **paquet ICMP**. Pour visualiser le contenu de la réponse, il suffit de regarder le **contenu** de la variable **rep** :
>>> rep.show()
0000 Ether / IP / ICMP 192.168.1.159 > 192.168.1.1 echo-request 0 ==> Ether / IP / ICMP 192.168.1.1 > 192.168.1.159 echo-reply 0 / Padding
>>>
La variable **rep** contient une **liste de couples de paquets** :
* le paquet **envoyé**,
* et le paquet **reçu** (la réponse).
Ici il n'y a qu'un seul couple de paquets puisqu'il n'y a qu'un seul envoi et une seule réponse. La variable **rep** est une liste et est manipulable comme une liste en python.
Le résultat est un **couple** (tuple à deux valeurs) :
* on affiche le **paquet émis** (le ICMP echo-request) avec **rep[0][0].show()**,
* et on affiche le** paquet reçu** en réponse avec **rep[0][1].show()**.
>>> rep[0]
(>>, >>>)
>>> rep[0][0].show()
###[ Ethernet ]###
dst= ac:84:c9:db:fb:c0
src= 00:15:5d:01:c6:02
type= 0x800
###[ IP ]###
version= 4
ihl= None
tos= 0x0
len= None
id= 1
flags=
frag= 0
ttl= 64
proto= icmp
chksum= None
src= 192.168.1.159
dst= 192.168.1.1
\options\
###[ ICMP ]###
type= echo-request
code= 0
chksum= None
id= 0x0
seq= 0x0
>>> rep[0][1].show()
###[ Ethernet ]###
dst= 00:15:5d:01:c6:02
src= ac:84:c9:db:fb:c0
type= 0x800
###[ IP ]###
version= 4
ihl= 5
tos= 0x0
len= 28
id= 43315
flags=
frag= 0
ttl= 64
proto= icmp
chksum= 0x4dbd
src= 192.168.1.1
dst= 192.168.1.159
\options\
###[ ICMP ]###
type= echo-reply
code= 0
chksum= 0xffff
id= 0x0
seq= 0x0
###[ Padding ]###
load= '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>>
* pour ne visualiser que le paquet ICMP :
>>> rep[0][1][ICMP].show()
###[ ICMP ]###
type= echo-reply
code= 0
chksum= 0xffff
id= 0x0
seq= 0x0
###[ Padding ]###
load= '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>>
**Important pour la suite** :
* quand il y a une **réponse**, **Scapy** indique que le type est **echo-reply**,
* en fait le champ type contient alors la valeur 0 : **type = 0** ! pour indiquer qu'il y a une réponse
* revoir la page ICMP de Wikipedia à ce sujet : https://fr.wikipedia.org/wiki/Internet_Control_Message_Protocol
>>> rep[0][1][ICMP].type
0
>>>
Utilisation de la fonction **srp1()** qui ne renvoie qu'un seul objet, la **première réponse** :
>>> rep = srp1(trame)
Begin emission:
.Finished to send 1 packets.
..*
Received 4 packets, got 1 answers, remaining 0 packets
>>> rep.show()
###[ Ethernet ]###
dst= 00:15:5d:01:c6:02
src= ac:84:c9:db:fb:c0
type= 0x800
###[ IP ]###
version= 4
ihl= 5
tos= 0x0
len= 28
id= 43316
flags=
frag= 0
ttl= 64
proto= icmp
chksum= 0x4dbc
src= 192.168.1.1
dst= 192.168.1.159
\options\
###[ ICMP ]###
type= echo-reply
code= 0
chksum= 0xffff
id= 0x0
seq= 0x0
###[ Padding ]###
load= '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>>
====== Créer un datagramme IP contenant un paquet ICMP ======
Si on ne s'**intéresse qu'à la partie IP** des paquets à gérer, on utilise alors les fonctions suivantes sans s'occuper du niveau 2 Ethernet qui est alors **automatiquement** renseigné par Scapy :
* **send()** équivalent à **sendp()**,
* **sr()** équivalent à **srp()**,
* **sr1()** équivalentes à **srp1()**.
>>> paquet = IP(dst='192.168.1.1') / ICMP()
>>> rep = sr1(paquet)
Begin emission:
..Finished to send 1 packets.
.*
Received 4 packets, got 1 answers, remaining 0 packets
>>> rep.show()
###[ IP ]###
version= 4
ihl= 5
tos= 0x0
len= 28
id= 43318
flags=
frag= 0
ttl= 64
proto= icmp
chksum= 0x4dba
src= 192.168.1.1
dst= 192.168.1.159
\options\
###[ ICMP ]###
type= echo-reply
code= 0
chksum= 0xffff
id= 0x0
seq= 0x0
###[ Padding ]###
load= '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>>
====== Envoi d'un paquet sur un hôte non existant ======
>>> paquet = IP(dst='192.168.1.200') / ICMP()
>>> rep = sr1(paquet)
Begin emission:
.....................................................WARNING: Mac address to reach destination not found. Using broadcast.
Finished to send 1 packets.
..............................................................................................................................................................................................................................................................................................................................^C
Received 371 packets, got 0 answers, remaining 1 packets
>>>
AttributeError: 'NoneType' object has no attribute 'show'
>>> rep
>>>
Arrêt de l'envoi **(CTRL + C)** après quelques secondes et la variable **rep** est vide.
Il est possible de préciser une **limite de temps** en secondes à la fonction sr1().
>>> rep = sr1(paquet, timeout=0.5)
Begin emission:
..................................................WARNING: Mac address to reach destination not found. Using broadcast.
Finished to send 1 packets.
.................
Received 67 packets, got 0 answers, remaining 1 packets
>>>
Pour **visualiser** les autres paramètres de la **fonction sr1()** utiliser la commande:
>>> help(sr1)
==== Retour à Python : la bibliothèque Scapy ... ====
* [[dev:python:scapy:accueil|Python : la bibliothèque Scapy pour manipuler les paquets réseau]]