Python 6

Programmation réseau (Python 3)

Courrier : SMTP, POP3, IMAP4 et NNTP

Envoyer un courrier: smtplib

In [1]:
# Pour envoyer un message
txt = """Le cheval de mon cousin 
ne mange du foin que le dimanche."""

# Les en-tête From:, To:, etc doivent être inclus dans le message et suivis de deux lignes vides
fromaddr = 'jyt@univ-mlv.fr'
toaddr = 'jythibon@gmail.com'
subj = 'Test'
# Le saut de ligne est '\r\n'
msg = ("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n" % (fromaddr, toaddr, subj)) + txt
In [2]:
print(msg)
From: jyt@univ-mlv.fr
To: jythibon@gmail.com
Subject: Test

Le cheval de mon cousin 
ne mange du foin que le dimanche.
In [3]:
from smtplib import SMTP
# possible depuis une adresse free
server = SMTP('smtp.free.fr')

server.set_debuglevel(1) # permet de voir ce qui se passe
server.sendmail(fromaddr, toaddr, msg)
server.quit()
send: 'ehlo [127.0.0.1]\r\n'
reply: b'250-smtp5-g21.free.fr\r\n'
reply: b'250-PIPELINING\r\n'
reply: b'250-SIZE 78643200\r\n'
reply: b'250-VRFY\r\n'
reply: b'250-ETRN\r\n'
reply: b'250-STARTTLS\r\n'
reply: b'250-ENHANCEDSTATUSCODES\r\n'
reply: b'250-8BITMIME\r\n'
reply: b'250 DSN\r\n'
reply: retcode (250); Msg: b'smtp5-g21.free.fr\nPIPELINING\nSIZE 78643200\nVRFY\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN'
send: 'mail FROM:<jyt@univ-mlv.fr> size=123\r\n'
reply: b'250 2.1.0 Ok\r\n'
reply: retcode (250); Msg: b'2.1.0 Ok'
send: 'rcpt TO:<jythibon@gmail.com>\r\n'
reply: b'250 2.1.5 Ok\r\n'
reply: retcode (250); Msg: b'2.1.5 Ok'
send: 'data\r\n'
reply: b'354 End data with <CR><LF>.<CR><LF>\r\n'
reply: retcode (354); Msg: b'End data with <CR><LF>.<CR><LF>'
data: (354, b'End data with <CR><LF>.<CR><LF>')
send: b'From: jyt@univ-mlv.fr\r\nTo: jythibon@gmail.com\r\nSubject: Test\r\n\r\nLe cheval de mon cousin \r\nne mange du foin que le dimanche.\r\n.\r\n'
reply: b'250 2.0.0 Ok: queued as 799955FFA9\r\n'
reply: retcode (250); Msg: b'2.0.0 Ok: queued as 799955FFA9'
data: (250, b'2.0.0 Ok: queued as 799955FFA9')
send: 'quit\r\n'
reply: b'221 2.0.0 Bye\r\n'
reply: retcode (221); Msg: b'2.0.0 Bye'
Out[3]:
(221, b'2.0.0 Bye')

Code 221: tous les messages ont été envoyés.

Pour construire des messages plus complexes, on utilise email.

In [12]:
with open('textfile','w') as f:
    f.write('''Le cheval de mon cousin\nne mange du foin que le dimanche.\n''')
In [14]:
from email.message import EmailMessage
# On va attacher une photo
import imghdr

with open('textfile') as f:
    msg = EmailMessage()
    msg.set_content(f.read())

msg['Subject'] = 'Test'
msg['From'] = 'jyt@univ-mlv.fr'
msg['To'] = 'jythibon@gmail.com'

msg.preamble = 'You will not see this in a MIME-aware mail reader.\n'

with open('haddock.jpg', 'rb') as f:
    img_data = f.read()


msg.add_attachment(img_data, maintype='image', subtype=imghdr.what(None, img_data))
In [15]:
imghdr.what(None, img_data)
Out[15]:
'jpeg'
In [16]:
from smtplib import SMTP
with SMTP('smtp.free.fr') as server:
    server.send_message(msg)

Bien reçu

On trouvera d'autres exemples plus complexes ici.

Inspecter une boîte mail : poplib

Exemple avec un compte free. Il faut bidouiller un peu pour analyser les messages, qui se présentent sous forme d'une liste de bytes.

In [83]:
import re
pat = b'charset=(.+?) '
def summary(m):
    res = [l for l in m if l.startswith(b'From: ') or l.startswith(b'Subject: ')]
    s = re.search(pat, b' '.join(m))
    if s: return [x.decode(s.group(1).decode('ascii')) for x in res]
    else: return res
In [84]:
import getpass, poplib

M = poplib.POP3('pop.free.fr')
print(M.getwelcome())
M.user('jeanyves.thibon')
print('Password: ')
M.pass_(getpass.getpass())
numMessages = len(M.list()[1])
print (numMessages, 'Messages\n')
print(M.list())

for i in range(1, numMessages + 1):
    (header, msg, octets) = M.retr(i)
    print("Message %d:" % i)
    for x in summary(msg): print(x)
    print ('----------')
   
b'+OK POP3 ready <1517859903.1573380580@popn1>'
Password: 
········
4 Messages

(b'+OK 4 messages', [b'1 7421', b'2 6949', b'3 109962', b'4 114489'], 36)
Message 1:
From: Bob Sedgewick <rs@cs.princeton.edu>
Subject: [Aofa] new lecture videos
----------
Message 2:
From: Mark Wilson <mc.wilson@auckland.ac.nz>
Subject: Re: [Aofa] new lecture videos
----------
Message 3:
From: =?UTF-8?Q?Conrado_Mart=c3=adnez_Parra?= <conrado@cs.upc.edu>
Subject: [Aofa] call for nominations for the 2020 Flajolet Lecture Prize
----------
Message 4:
From: Daniele Gardy <daniele.gardy@uvsq.fr>
Subject: [Aofa] CFP: CLA'19
----------

Avec IMAP4: imaplib

Les détails se trouvent dans la RFC3501.

In [1]:
import getpass, imaplib

M = imaplib.IMAP4('monge.univ-mlv.fr')
M.starttls()
M.login('slc', getpass.getpass())
M.select()
typ, data = M.search(None, 'ALL')
for num in data[0].split():
#    typ, data = M.fetch(num, '(RFC822)')
    typ, data = M.fetch(num, "(FLAGS BODY[HEADER.FIELDS (DATE FROM)])")
    print('Message %s\n%s\n' % (num, data[0][1]))
M.close()
M.logout()
········
Message b'1'
b'From: Jean-Yves Thibon <jythibon@gmail.com>\r\nDate: Tue, 12 Nov 2019 09:46:11 +0100\r\n\r\n'

Message b'2'
b'Date: Tue, 12 Nov 2019 09:49:00 +0100\r\nFrom: jyt@univ-mlv.fr\r\n\r\n'

Out[1]:
('BYE', [b'Logging out'])

imaplib est assez peu convivial. Il vaut mieux installer IMAPClient.

Protocoles supportés

La liste des modules se trouve ici

La plupart de ces modules font appel à socket, que l'on peut utiliser directement pour de la programmation bas niveau.

On dispose également de socketserver, une plateforme pour faciliter l'écriture des serveurs, et de ssl, pour les connections sécurisées par TLS/SSL.

Les socquettes

La classe fondamentale est socket.socket, qui retourne un objet du type Socket :

socket([family[, type[, proto]]]) # -> socket object

Les paramètres les plus courants sont, pour TCP/IP et UDP/IP

family=socket.AF_INET, type=socket.SOCK_STREAM 
                            socket.SOCK_DGRAM

On peut accéder à la couche 2, mais c'est mal documenté (voir plus loin).

Exemple: seveur d'écho

# echoserv.py
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('',8888)) # '': toutes les interfaces disponibles
s.listen(5)       # nombre maximum de connexions en attente

try:
    while True:
        newSocket, address = s.accept()
        print ("Connected from", address)
        while True:
            receivedData = newSocket.recv(1024)
            if not receivedData: break
            newSocket.sendall(receivedData)
        newSocket.close()
        print ("Disconnected from", address)
finally:
    s.close()

Exemple de client

# echocli.py
from socket import *
s = socket(AF_INET, SOCK_STREAM)
s.connect(('127.0.01',8888))
print ("Connected to server")
data = b"""Le cheval de mon cousin
ne mange du foin que le dimanche."""
for line in data.splitlines():
    s.sendall(line)
    print ("Sent: ", line.decode('utf8'))
    response = s.recv(1024)
    print ("Received: ", response.decode('utf8'))
s.close()

Résultat

jyt@monge ~/python $ python3 echoserv.py
Connected from ('82.225.166.14', 32801)
Disconnected from ('82.225.166.14', 32801)

######################################

[jyt@liszt Python]$ python3 echocli.py
Connected to server
Sent: Le cheval de mon cousin
Received Le cheval de mon cousin
Sent: ne mange du foin que le dimanche.
Received ne mange du foin que le dimanche.

Commentaires

  • Les fonctions et les constantes ont généralement les mêmes noms qu'en C

  • Mais les signatures ne sont généralement pas les mêmes

  • On ne trouve pas tout dans la doc de Python (cf. man socket)

  • Et toutes les constantes ne sont pas définies (PF_INET n'est pas définie, alors qu'elle devrait être égale àAF_INET)

  • Cela dit, on peut reprendre en Python les exercices standards du cours de réseau

  • L'interpréteur permet d'expérimenter facilement

Serveur d'écho - version concurrente (module threading)

# echoconc.py
import threading, time
from socket import *
Host = ''
Port = 8888

s = socket(AF_INET, SOCK_STREAM)
s.bind((Host, Port))
s.listen(5)

def now():
    return time.ctime(time.time()).encode('ascii')

def handleClient(connection):
    time.sleep(5) # pour les besoins de la demo
    while 1:
        data = connection.recv(1024)
        if not data: break
        connection.send(b'Echo=>%b - at %b' % (data, now()))
    connection.close()

def dispatcher():
    while 1:
        connection, address = s.accept()
        print ('Server connected by', address,'at', now())
        t = threading.Thread(target=handleClient, args=(connection,))
        t.start()

dispatcher()

Un client SNTP (Simple Network Time Protocol)

Pour interroger un serveur de temps (public) :

  • on envoie un datagramme de 48 octets commençant par 0x1b sur son port 123

  • le serveur renvoie 48 octets (12 mots de 32 bits), le 11ème contient le nombre de secondes depuis le 1er janvier 1900, 0 h.

  • Le 12ème donne les microsecondes

  • On le décode au moyen du module struct

  • La RFC 2030 décrit les différents champs

  • la chaîne '!12I' décode 12 entiers longs non signés (I) en big-endian (! = network order)

      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |LI | VN  |Mode |    Stratum    |     Poll      |   Precision   |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                          Root Delay                           |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                       Root Dispersion                         |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                     Reference Identifier                      |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                                                               |
      |                   Reference Timestamp (64)                    |
      |                                                               |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                                                               |
      |                   Originate Timestamp (64)                    |
      |                                                               |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                                                               |
      |                    Receive Timestamp (64)                     |
      |                                                               |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                                                               |
      |                    Transmit Timestamp (64)                    |
      |                                                               |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                 Key Identifier (optional) (32)                |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                                                               |
      |                 Message Digest (optional) (128)               |
      |                                                               |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Le programme

# sntp.py
import struct, sys
from socket import *
from time import ctime

TIME1970 = 2208988800 # sec depuis 01/01/1900 00:00

if len(sys.argv) < 2:
    srv = '150.254.183.15'
else:
    srv = sys.argv[1]

s = socket(AF_INET, SOCK_DGRAM)
data = b'\x1b' + b'\0'*47
s.sendto(data, (srv, 123))
data, addr = s.recvfrom(1024)
if data:
    print ("Received from: ", addr)
    try:
        t = struct.unpack('!12I', data)[10]
        t -= TIME1970
        print ('\tTime = %s' % ctime(t))
    except: print (data)

Résultat

[jyt@scriabine reseau]$ python3 sntp.py 
Received from:  ('150.254.183.15', 123)
        Time = Tue Nov 12 10:26:53 2019

Socquettes brutes (raw sockets)

Permettent l'implantation de protocoles de plus bas niveau. Exemple : ICMP -- envoi d'une demande d'écho (cf. ping, traceroute)

sd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) 
# On positionne les options avec setsockopt
# SOL = Socket Option Level, SO = Socket Option
sd.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)    # autorise l'adresse 
                                              # de diffusion (ping -b)
sd.setsockopt(SOL_SOCKET, SO_RCVBUF, 60*1024) # augmente la taille du
                                              # tampon en consequence

sd.settimeout(5) # les socquettes sont bloquantes par defaut

Construction manuelle d'un paquet

Chaque type de paquet sera représenté par une classe dont les attributs seront les paramètres et dont la méthode spéciale __bytes__ permettra l'assemblage.

Structure d'un paquet ICMP :

\begin{verbatim}
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Type      |     Code      |          Checksum             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Identifier          |        Sequence Number        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Data ...
   +-+-+-+-+-

class Icmp_ER(object):                       # Echo request
    def __init__(self,ident,seqnum):         #
        self.id = ident                      # identifiant
        self.seq = seqnum                    # numéro de séquence
        self.type = b'\x08'                  # type "echo request"
        self.code = b'\x00'                  # seul code possible dans ce cas
        self.chks = 0

    def __bytes__(self):
        tc = struct.pack('!cc', self.type,self.code)  # 2 octets (char)
        idseq = struct.pack('!HH', self.id, self.seq) # 2 entiers cours non signés (unsigned char)
        s = checksum(tc + idseq)                      # la somme de contrôle est calculéee sur 
        self.chks = s                                 # type, code, id, seq
        return tc + struct.pack('!H',s)+idseq         # et on emballe le tout ...

Par exemple (checksum est définie plus loin)

>>> p=Icmp_ER(1,2)
>>> s=bytes(p)
>>> s
b'\x08\x00\xf7\xfc\x00\x01\x00\x02'
>>> struct.unpack('!ccHHH',s)
('\x08', '\x00', 63484, 1, 2)
>>> checksum(s[:2]+s[4:])
63484
>>> checksum(s)
0

Le code complet

# icmp_echo.py
import struct, os, sys, array
from socket import *

SIZE = 60*1024
ON = 1
BUFSIZE = 1500
PID = os.getpid()
SEQ = 0

srv = ('193.55.63.80',0) # Port 0 sans importance

def checksum(pkt):                # https://tools.ietf.org/html/rfc1071
    if len(pkt) % 2 == 1:
        pkt += "\0"
    s = sum(array.array("H", pkt))
    s = (s >> 16) + (s & 0xffff)
    s += s >> 16
    s = ~s
    return (((s>>8)&0xff)|s<<8) & 0xffff


class Icmp_ER(object):                       # Echo request
    def __init__(self,ident,seqnum):
        self.id = ident
        self.seq = seqnum
        self.type = b'\x08'
        self.code = b'\x00'
        self.chks = 0

    def __bytes__(self):
        tc = struct.pack('!cc', self.type,self.code)
        idseq = struct.pack('!HH', self.id, self.seq)
        s = checksum(tc + idseq)
        self.chks = s
        return tc + struct.pack('!H',s)+idseq



try:
    sd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)
except:
    sys.stderr.write("socket() failed\n")
    sys.exit(-1)

sd.setsockopt(SOL_SOCKET, SO_BROADCAST, ON)
sd.setsockopt(SOL_SOCKET, SO_RCVBUF, SIZE)

sd.settimeout(5)

data = bytes(Icmp_ER(PID,SEQ))
sd.sendto(data, srv)

while data:

    data, addr = sd.recvfrom(BUFSIZE)
    x =(len(data),)+addr
    print ("Received %d bytes from %s, port %d" % x)

Résultat

[root@scriabine]# python3 icmp_echo.py 
Received 28 bytes from 193.55.63.80, port 0
Traceback (most recent call last):
  File "icmp_echo.py", line 57, in 
    data, addr = sd.recvfrom(BUFSIZE)
socket.timeout: timed out

Accès à la couche 2 : une requête ARP

On peut accéder au niveau 2 (ex. ethernet) avec les socquettes brutes de la famille PF_PACKET.

sd = socket(PF_PACKET, SOCK_RAW)
sd.bind(('eth1', 0x806)) # 0x806 = paquets ARP

On va le tester avec une requête ARP (who-has). Il s'agit de récuperer l'adresse MAC d'une machine à partir de son adresse IP.

La documentation n'indique pas clairement s'il faut inclure l'en-tête ethernet dans le paquet. En expérimentant, on voit qu'il le faut ...

data = ether + arp
sd.send(data)
ans = sd.recv(1024)

Reste à construire les paquets arp et ether.

L'en-tête ARP


    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |          Hardware type        |       Protocol type           |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |Hard addr len | Proto addr len |       Opcode                  |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Source hardware address                    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Source protocol address                    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Destination hardware address               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Destination protocol address               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+


L'en-tête ETHERNET

C'est le plus simple, il montre la voie à suivre ... Dans /usr/src/linux/include/linux/if_ether.h, on trouve

struct ethhdr
{
        unsigned char   h_dest[ETH_ALEN];       /* destination eth addr */
        unsigned char   h_source[ETH_ALEN];     /* source ether addr    */
        unsigned short  h_proto;                /* packet type ID field */
} __attribute__((packed));

ETH_ALEN vaut 6, d'où en Python, quelque chose du genre

class Ether(object):

    def __init__(self,dhw,shw, ptype):
        self.shw = shw      # ex: '00:23:AE:B1:6C:C2'
        self.dhw = dhw      # ex: 'ff:ff:ff:ff:ff:ff'
        self.type = ptype   # ex: 0x806

    def __bytes__(self):
        return (struct.pack( '!6B',*mac2bytes(self.dhw))
                + struct.pack('!6B',*mac2bytes(self.shw))
                + struct.pack('!H',self.type))

mac2bytes fait ce qu'on imagine.

Une classe pour les requêtes ARP

On pourrait définir tout de suite la classe générale Arp mais pour faire simple on va préremplir les attributs correspondant à une requête who-has.

class Arp_who_has(object):
    def __init__(self, hw_src, ip_src, hw_dst, ip_dst):
        self.hwdst = mac2bytes(hw_dst)  # adresse MAC du destinataire
        self.hwsrc = mac2bytes(hw_src) # adresse MAC de la source
        self.ipsrc = ip2bytes(ip_src)   # adresse IP se la source
        self.ipdst = ip2bytes(ip_dst)   # adresse IP du destinataire
        self.hwtype = 0x1               # ARPHRD_ETHER    1 (Ethernet 10Mbps)
        self.ptype = 0x800              # protocole IP
        self.hwlen= 6                   # MAC = 6 octets
        self.plen = 4                   # IP = 4 octets
        self.op= 1                      # ARPOP_REQUEST   1  (ARP request)

    def __bytes__(self):
        w = struct.pack('!HHBBH', self.hwtype, self.ptype,
                                  self.hwlen, self.plen, self.op)
        return w + self.hwsrc + self.ipsrc + self.hwdst + self.ipdst

Le code complet

# arp_req.py
# python 3
import struct, os, sys, uuid
from socket import *

ETH_BROADCAST = 'ff:ff:ff:ff:ff:ff'
ETH_UNSPECIFIED = '00:00:00:00:00:00'

def getMacAddress(): # Récupération de l'adresse MAC
    s = '%012X' % uuid.getnode()
    return ':'.join([s[2*i:2*(i+1)] for i in range(6)])

# Conversions bytes <-> strings pour les adresses
def bytes2ip(s):
    return '.'.join([str(int(i)) for i in s])

def bytes2mac(s):
    return '%02x:%02x:%02x:%02x:%02x:%02x' % tuple(map(int,s))

def mac2bytes(mac):
    return bytes([int(i,16) for i in mac.split(':')])

def ip2bytes(ip):
    return bytes([int(i) for i in ip.split('.')])

# paquets ethernet
class Ether(object):

    def __init__(self,dhw,shw, ptype):
        self.shw = shw      # ex: '00:23:AE:B1:6C:C2'
        self.dhw = dhw      # ex: 'ff:ff:ff:ff:ff:ff'
        self.type = ptype   # ex: 0x806

    def __bytes__(self):
        return (struct.pack( '!6B',*mac2bytes(self.dhw))
                + struct.pack('!6B',*mac2bytes(self.shw))
                + struct.pack('!H',self.type))

# paquets ARP
class Arp_who_has(object):
    def __init__(self, hw_src, ip_src, hw_dst, ip_dst):
        self.hwdst = mac2bytes(hw_dst)  # adresse MAC du destinataire
        self.hwsrc = mac2bytes(hw_src) # adresse MAC de la source
        self.ipsrc = ip2bytes(ip_src)   # adresse IP se la source
        self.ipdst = ip2bytes(ip_dst)   # adresse IP du destinataire
        self.hwtype = 0x1               # ARPHRD_ETHER    1 (Ethernet 10Mbps)
        self.ptype = 0x800              # protocole IP
        self.hwlen= 6                   # MAC = 6 octets
        self.plen = 4                   # IP = 4 octets
        self.op= 1                      # ARPOP_REQUEST   1  (ARP request)

    def __bytes__(self):
        w = struct.pack('!HHBBH', self.hwtype, self.ptype,
                                  self.hwlen, self.plen, self.op)
        return w + self.hwsrc + self.ipsrc + self.hwdst + self.ipdst



try:
    sd = socket(PF_PACKET, SOCK_RAW)
    sd.bind(('eth0', 0x806))
except:
    sys.stderr.write("socket() failed\n")
    sys.exit(-1)

sd.settimeout(5)


HOST = gethostbyname(gethostname())
MAC = getMacAddress()
ether = bytes(Ether(ETH_BROADCAST, MAC, 0x806))


arp = bytes(Arp_who_has(MAC, HOST, ETH_UNSPECIFIED, '192.168.1.3'))


data = ether + arp
sd.send(data)
ans = sd.recv(1024)


rarp = struct.unpack('!HccBBH6s4s6s4s',ans[14:42])
print ('%s is at %s' % (bytes2ip(rarp[7]), bytes2mac(rarp[6])))

Résultat

[root@scriabine]# python3 arp_req.py 
192.168.1.3 is at 08:00:37:d3:7c:4b

Conclusions

  • On peut ...

  • Mais c'est pas la peine ...

  • d'autres l'ont déjà fait pour nous

Il existe de nombreuses bibliothèques de manipulation de paquets.

Par exemple (par ordre de complexité)

dpkt

On trouve une classe

class Packet(object):
    """Base packet class, with metaclass magic to generate members from
    self.__hdr__.

dont dérivent tous les types de paquets.

Par exemple, arp.py contient diverses constantes (du genre ARP_PRO_IP = 0x0800) et une classe traduisant la définition de struct arphdr dans if_arp.h (avec les paramètres pour une requête):

class ARP(dpkt.Packet):
    __hdr__ = (
        ('hrd', 'H', ARP_HRD_ETH),
        ('pro', 'H', ARP_PRO_IP),
        ('hln', 'B', 6),        # hardware address length
        ('pln', 'B', 4),        # protocol address length
        ('op', 'H', ARP_OP_REQUEST),
        ('sha', '6s', ''),
        ('spa', '4s', ''),
        ('tha', '6s', ''),
        ('tpa', '4s', '')
        )

Le programme arp_req.py s'écrirait plus simplement ainsi :

# python 3
# arp_req_dpkt.py
import struct, sys, os, uuid
from socket import *
from dpkt import ethernet, arp

def getMacAddress(): # Récupération de l'adresse MAC
    s = '%012X' % uuid.getnode()
    return ':'.join([s[2*i:2*(i+1)] for i in range(6)])

def bytes2ip(s):
    return '.'.join([str(int(i)) for i in s])
    #return s

def bytes2mac(s):
    #return s
    return '%02x:%02x:%02x:%02x:%02x:%02x' % tuple(map(int,s))


def mac2bytes(mac):
    return bytes([int(i,16) for i in mac.split(':')])

def ip2bytes(ip):
    return bytes([int(i) for i in ip.split('.')])




ETH_BROADCAST = 'ff:ff:ff:ff:ff:ff'
ETH_UNSPECIFIED = '00:00:00:00:00:00'
HOST = gethostbyname(gethostname())
MAC = getMacAddress()

# plus simple ici
ar = arp.ARP()
ar.sha = mac2bytes(MAC)
ar.tha = mac2bytes(ETH_UNSPECIFIED)
ar.spa = ip2bytes(HOST)
ar.tpa = ip2bytes( '192.168.1.3')
eth = ethernet.Ethernet()
eth.src = mac2bytes(MAC)
eth.dst = mac2bytes(ETH_BROADCAST)
eth.data = ar
eth.type = ethernet.ETH_TYPE_ARP


# mais il faut encore gérer les socquettes directement
try:
    sd = socket(PF_PACKET, SOCK_RAW)
    sd.bind(('eth0', 0x806))
except:
    sys.stderr.write("socket() failed\n")
    sys.exit(-1)

sd.settimeout(2)
data = bytes(eth) + bytes(ar)
sd.send(data)
ans = sd.recv(1024)
r = struct.unpack('!HccBBH6s4s6s4s',ans[14:42])
print ('%s is at %s' % (bytes2ip(r[7]), bytes2mac(r[6])))

impacket

Assemblage de paquets et décodage. Utilisation avec Pcapy recommandée (interface Python/libpcap).

Même principe que dans dpkt et les exemples forgés à la main. On doit encore gérer les socquettes.

La suite est plus simple avec le module ImpactDecoder.

# python 3
# arp-impkt.py
from socket import *
from impacket import ImpactDecoder, ImpactPacket

arp = ImpactPacket.ARP()
arp.set_ar_hln(6)
arp.set_ar_pln(4)
arp.set_ar_op(1)
arp.set_ar_hrd(1)
arp.set_ar_spa((192, 168, 1, 4))
arp.set_ar_tpa((192, 168, 1, 3))
arp.set_ar_sha((0x00, 0x23, 0xae, 0xb1, 0x6c, 0xc2))
arp.set_ar_pro(0x800)

eth = ImpactPacket.Ethernet()
eth.contains(arp)
eth.set_ether_shost((0x00, 0x23, 0xae, 0xb1, 0x6c, 0xc2))
eth.set_ether_dhost((0xff, 0xff, 0xff, 0xff, 0xff, 0xff))

try:
    sd = socket(PF_PACKET, SOCK_RAW)
    sd.bind(('eth0', 0x806))
except:
    sys.stderr.write("socket() failed\n")
    sys.exit(-1)

sd.settimeout(5)
sd.send(eth.get_packet())
ans = sd.recv(1024)
reth = ImpactDecoder.EthDecoder().decode(ans)
print (reth) # juste pour voir

rarp = reth.child()
print (rarp) # c'est comme on pense

fmt = '%d.%d.%d.%d is at %02X:%02X:%02X:%02X:%02X:%02X'
# mais la deniere ligne suffit
print (fmt % (tuple(rarp.get_ar_spa())+tuple(rarp.get_ar_sha())))

Résultat

[root@scriabine]# python3 arp-impkt.py 
Ether: 08:00:37:d3:7c:4b -> 00:23:ae:b1:6c:c2
ARP format: ARPHRD ETHER opcode: REPLY
8:0:37:d3:7c:4b -> 0:23:ae:b1:6c:c2
192.168.1.3 -> 192.168.1.4

0000 0000 0000 0000 0000 0000 0000 0000    ................
0000                                       ..

ARP format: ARPHRD ETHER opcode: REPLY
8:0:37:d3:7c:4b -> 0:23:ae:b1:6c:c2
192.168.1.3 -> 192.168.1.4

0000 0000 0000 0000 0000 0000 0000 0000    ................
0000                                       ..

192.168.1.3 is at 08:00:37:D3:7C:4B

Scapy

Beaucoup plus puissant que les précédents. L'exmple ARP se résume à la session interactive suivante :

>>> a=ARP()
>>> a.pdst='192.168.2.148'
>>> b=Ether()
>>> b.src=a.hwsrc
>>> ans, unans = srp(b/a)
Begin emission:
*Finished to send 1 packets.

Received 1 packets, got 1 answers, remaining 0 packets
>>> print '%s is at %s'%(ans[0][1].payload.psrc, 
                         ans[0][1].payload.hwsrc)
192.168.2.148 is at 00:c0:ca:1a:06:75

Examinons les détails.

[root@liszt scapy]# scapy
WARNING: No route found for IPv6 destination :: (no default route?)
Welcome to Scapy (2.0.0.11 beta)
>>> a=ARP()
>>> a.show()
###[ ARP ]###
  hwtype= 0x1
  ptype= 0x800
  hwlen= 6
  plen= 4
  op= who-has
  hwsrc= 00:0f:ea:af:79:15
  psrc= 192.168.2.171
  hwdst= 00:00:00:00:00:00
  pdst= 0.0.0.0
>>> a.pdst='192.168.2.148'
>>> b=Ether()
>>> b.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= 0x0
>>> b.src=a.hwsrc
>>> ans, unans = srp(b/a)
Begin emission:
*Finished to send 1 packets.

Received 1 packets, got 1 answers, remaining 0 packets
>>> ans

>>> ans[0]
(>, 
 >>)
>>> ans[0][1].payload
>
>>> print '%s is at %s' % (ans[0][1].payload.psrc, ans[0][1].payload.hwsrc)
192.168.2.148 is at 00:c0:ca:1a:06:75

Scapy connaît un grand nombre de protocoles, et définit une classe pour chaque type de paquet (même logique que précédemment). Les instances sont créées avec des valeurs par défaut et sont dès le début des paquets valides.

On visualise les attributs avec la méthode {\tt show()} et on les modifie à volonté.

On peut ensuite empiler les protocoles avec l'opérateur $/$ :

>>> a=TCP()
>>> b=IP()
>>> c=Ether()
>>> p = c/b/a
>>> p
<Ether  type=0x800 |<IP  frag=0 proto=tcp |<TCP  |>>>

On accède aux différentes couches avec une syntaxe de type dictionnaire, ou avec l'attribut payload :

>>> p[IP].dst = '192.168.2.148'
>>> p
<Ether  type=0x800 |<IP  frag=0 proto=tcp dst=192.168.2.148 
                                                |<TCP  |>>>
>>> p[TCP]
<TCP  |>
>>> p.payload
<IP  frag=0 proto=tcp dst=192.168.2.148 |<TCP  |>>
>>> p.payload.payload
<TCP  |>
>>> p.haslayer(TCP)
1
>>> p.haslayer(ARP)
0

Par exemple, l'interrogation du serveur de temps pourrait se faire avec

>>> p = IP(dst='150.254.183.15')/UDP(sport=11111, dport=123)/('\x1b' + '\0'*47)
>>> x,y = sr(p)

On n'a plus à gérer les socquettes. Les paquets sont envoyés par la fonction {\tt send} (couche 3) ou {\tt sendp} (couche 2).

Si on attend une réponse, on utilise {\tt sr, srp, sr1, srp1}. Ces fonctions retournent un couple de listes {\tt (ans, unans)}. La première est une liste de couples (stimulus, réponse). La seconde contient les paquets sans réponse.

>>> x
<Results: TCP:0 UDP:1 ICMP:0 Other:0>
>>> x[0][1][UDP][Raw].load
'\x1c\x01\x00\xf0  ... [snip] ...\xcdB\xa3\xfd\xdbJ\xba8'

On n'a plus à gérer les socquettes. Les paquets sont envoyés par la fonction send (couche 3) ou sendp (couche 2).

Si on attend une réponse, on utilise sr, srp, sr1, srp1.

Ces fonctions retournent un couple de listes (ans, unans).

La première est une liste de couples (stimulus, réponse).

La seconde contient les paquets sans réponse.

>>> x
<Results: TCP:0 UDP:1 ICMP:0 Other:0>
>>> x[0][1][UDP][Raw].load
'\x1c\x01\x00\xf0  ... [snip] ...\xcdB\xa3\xfd\xdbJ\xba8'

Chaque champ d'un paquet peut être un ensemble. On crée ainsi un ensemble de paquets ayant toutes les combinaisons de valeurs possibles dans ces ensembles.

Pour détecter les machines ayant un serveur web sur le réseau de classe C contenant le serveur de l'IGM (FR-UMLV-8), on peut par exemple émettre

>>> a=IP(dst="igm.univ-mlv.fr/24")/TCP(dport=80)
>>> ans, unans = sr(a,timeout=3)
Begin emission:
.***Finished to send 256 packets.

Received 4 packets, got 3 answers, remaining 253 packets

La méthode ans.nsummary() permet de visualiser les réponses.

On peut les filtrer, et choisir ce que l'on veut voir :

>>> ans.nsummary(lfilter = lambda(s,r): r[TCP].flags & 2, 
                 prn = lambda(s,r): r[IP].src)
0001 193.55.63.80
0002 193.55.63.149

La clause flags & 2 sélectionne les paquets qui ont SYN = 1.

Ici le butin est modeste, on n'a trouvé qu'un deuxième serveur.

Un traceroute -I (envoie des paquets ICMP) pourrait s'écrire

>>> ans,unans=sr(IP(dst='193.55.63.80', 
                    ttl=(1,25),
                    id=RandShort())/ICMP(),timeout=5)
Begin emission:
******************Finished to send 25 packets.
*****
Received 23 packets, got 23 answers, remaining 2 packets
>>> for snd,rcv in ans:
...   print snd.ttl, rcv.src, isinstance(rcv.payload, ICMP)
...
1 192.168.2.1 True
2 82.225.166.254 True
3 78.254.3.126 True
...

On obtient la liste des protocoles supportés avec la commande ls() :

>>> ls()
ARP        : ARP
ASN1_Packet : None
BOOTP      : BOOTP
CookedLinux : cooked linux
DHCP       : DHCP options
... (plus de 300)

et la liste des fonctions avec {\tt lsc()} :

>>> lsc()
arpcachepoison      : Poison target's cache with (your MAC,victim's IP) couple
arping              : Send ARP who-has requests to determine which hosts are up
bind_layers         : Bind 2 layers on some specific fields' values
corrupt_bits        : Flip a given percentage or number of bits from a string
...

Il y a beaucoup d'outils précodés. Par exemple sniff, qui capture le traffic, accepte des filtres et des fonctions de présentation. Le code suivant espionne le courrier, et capture les mots de passe :

a=sniff(filter="tcp and (port 25 or port 110)",
        prn=lambda x: 
                x.sprintf("%IP.src%:%TCP.sport% 
                           -> %IP.dst%:%TCP.dport% 
                           %2s,TCP.flags% : %TCP.payload%"))

C'est la méthode sprintf() qui permet une présentation claire.

sprintf(self, fmt, relax=1) méthode des instances de scapy.layers.inet.IP.

sprintf(format, [relax=1]) -> str où format est une chaîne qui peut inclure des directives.

Une directive commence et finit par `%` :
 `%[fmt[r],][cls[:nb].]field%`.

fmt est une directive de printf, "r" est pour "raw substitution" (ex: IP.flags=0x18 au lieu de SA), nb est le numéro de la couche voulue (ex: pour les paquets IP/IP, IP:2.src est src de la couche IP supérieure). Cas particulier : "%.time%" est la date de création. Ex :

p.sprintf("%.time% %-15s,IP.src% -> %-15s,IP.dst% %IP.chksum% "
                   "%03xr,IP.proto% %r,TCP.flags%")

Le format peut inclure des conditionnelles : {layer:string} string est la chaîne à insérer si layer est présent.

Si layer est precedée de "!", le result est inversé. Les conditions peuvent être imbriquées :

p.sprintf("This is a{TCP: TCP}{UDP: UDP}{ICMP:n ICMP} packet")
      p.sprintf("{IP:%IP.dst% {ICMP:%ICMP.type%}{TCP:%TCP.dport%}}")
Pour obtenir `"{"` et `"}"`, utiliser `"%("` et `"%)"`.





Scapy permet de belles présentations graphiques, s'il est installé avec les dépendances idoines :

  • plot() : demande Gnuplot-py, qui demande GnuPlot et NumPy

  • graphiques 2D : psdump() pdfdump() demandent PyX

  • graphes : conversations() demande Graphviz et ImageMagick

  • graphiques 3D : trace3D() demande VPython

Pour une présentation détaillée, voir ici.

In [ ]: