Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Mon livre « Cyberstructure »

Ève

On ne peut pas analyser tous les protocoles avec Netfilter

Première rédaction de cet article le 4 avril 2012
Dernière mise à jour le 27 juin 2012


Le système de traitement des paquets Netfilter est intégré dans le noyau Linux depuis des années. Il permet de créer des pare-feux, de faire de la limitation de trafic et bien d'autres choses encore. Il permet de n'appliquer une règle qu'à certains paquets, selon le protocole de transport utilisé, selon les adresses IP ou les ports source ou destination, etc. Pour les autres cas, le module u32 permet de chercher des correspondance dans n'importe quelle partie du paquet. Peut-on analyser tous les protocoles avec u32 ? (Attention, il va y avoir du binaire dans l'article.)

Prenons l'exemple du DNS. Certains champs du paquet DNS sont accessibles via un écart fixe par rapport au début du paquet. Ceux-là sont faciles à chercher avec u32. La syntaxe de ce dernier n'est pas triviale, mais il existe un excellent tutoriel. 0>>22&0x3C@ fait sauter l'en-tête IPv4. La longueur de l'en-tête (qui n'est pas constante) se trouve dans quatre bits du premier octet du paquet IP. On décale de 22 bits pour avoir ces bits et pour les multiplier par quatre (l'unité de longueur est le mot de quatre octets, cf. RFC 791, section 3.1), on masque avec 0x3c pour n'extraire que la longueur et on se déplace de cette quantité dans le paquet (c'est le rôle du signe @). Ensuite, on indique le déplacement après l'en-tête, pour UDP (le protocole le plus courant pour le DNS), c'est huit octets d'en-tête (RFC 768). Donc, 0>>22&0x3C@8 nous fait sauter les en-têtes IPv4 et UDP et arriver au début du paquet DNS.

Imaginons qu'on veuille sélectionner uniquement les requêtes, pas les réponses. Le RFC 1035, section 4.1.1, nous apprend que le bit permettant de différencier requêtes et réponses est le premier du troisième octet du paquet. Il est à zéro lorsque le paquet est une requête. On peut le sélectionner avec 10&0x80000000=0x0 (se déplacer de 10 octets, les huit d'UDP et les deux du premier champ DNS, le Query ID, puis masquer avec les quatre octets 0x80000000 - un bit à un - et tester). En combinant tout cela, on peut utiliser iptables pour faire une règle qui ne s'appliquera qu'aux requêtes DNS :

iptables --append ${CHAIN} --destination ${DEST} \
    --protocol udp --destination-port 53 \
    --match u32 --u32 "0>>22&0x3C@10&0x80000000=0x0" \
    --jump ${ACTION}

Pour n'avoir que les réponses, on utiliserait 0x80000000=0x80000000 pour tester si le bit est à un. Une alternative est d'utiliser l'opérateur de décalage de u32, le >> pour ne garder que le bit QR avant de le tester : 8>>15&0x01=0 au lieu de 10&0x4000=0x0000. (Il existe une application pratique, avec exemple, au filtrage des réponses.)

Maintenant, supposons qu'on veuille mettre en place une limitation de trafic par exemple pour faire face à une attaque par déni de service. J'avais déjà écrit un article à ce sujet où on limitait toutes les requêtes DNS. Maintenant, si je fais face à une attaque spécifique, par exemple utilisant les requêtes de type ANY en raison de leur fort taux d'amplification (la réponse est bien plus grosse que la requête) : puis-je appliquer une règle Netfilter seulement aux paquets où la requête est de type ANY ?

Le problème est que, si les premiers champs du paquet DNS (comme Flags) sont à une distance fixe du début du paquet, ce n'est plus le cas par la suite. u32 sait traiter les cas (comme l'en-tête IPv4) où la distance est indiquée dans le paquet mais ne sait pas faire une analyse complète (en termes pédants, u32 n'est pas un langage de Turing).

Or, dans un paquet DNS (cf. RFC 1035, section 4.1.2), le type de requête (champ QTYPE pour Query Type) est situé après une structure compliquée, le nom demandé (champ QNAME pour Query Name). Le langage d'u32 ne connait que décalage et masquage, alors qu'il nous faudrait des tests et des boucles (ou bien la récursion), pour pouvoir suivre les différents composants (labels) du QNAME (qui peuvent être en nombre quelconque). Si u32 n'a pas cela, d'ailleurs, ce n'est pas par paresse ou ignorance de son auteur, mais parce que ce langage est conçu pour des analyses ultra-rapides, et dont la durée est prévisible (ce qui n'est plus le cas dès qu'on met des boucles).

Donc, limiter uniquement les requêtes ANY ne semble pas possible avec Netfilter. C'est pour cela que les exemples DNS qu'on trouve sur le Web ne traitent que des cas simples comme les attaques dites NS . ( Upward Referrals) où le QNAME est toujours le même (en l'occurrence un simple point, la racine du DNS). On peut alors filtrer avec 0>>22&0x3C@20>>24=0 (sauter 20 octets pour arriver au QNAME qui doit être uniquement composé d'un octet longueur valant zéro).

Une solution astucieuse est de commencer par la fin du paquet et non pas son début, pour tomber sur le QTYPE avant le QNAME. Mais cela ne marche pas non plus car les requêtes DNS modernes ont en général à la fin une section additionnelle, également de longueur variable (conséquence du RFC 6891).

Damien Wyart me signale qu'un module DNS pour Netfilter, qui résoudrait ce problème, est en cours de développement. Je ne l'ai pas encore testé, mais, à la lecture, ce code a le même défaut que pas mal de règles iptables ou tcpdump qu'on trouve sur le Web. Il analyse le paquet DNS en partant de la fin, pour éviter d'avoir à décoder le QNAME. Résultat, il ne marche pas du tout avec EDNS, qui ajoute une section additionnelle à la fin.

Une solution partielle, qui ne résoudrait pas le cas général, serait de faire une règle spécifique à un nom de domaine, si l'attaque utilise toujours le même (car on connait alors la longueur du QNAME et on peut le sauter). C'est limité mais mieux que rien. Comme l'attaquant peut changer facilement le QNAME utilisé, le mieux est d'utiliser un petit programme qui prenne en paramètre un nom et fabrique le code u32. C'est le cas du script generate-netfilter-u32-dns-rule.py :


% python generate-netfilter-u32-dns-rule.py --qname isc.org
0>>22&0x3C@20&0xFFFFFFFF=0x03697363&&0>>22&0x3C@24&0xFFFFFFFF=0x036f7267&&0>>22&0x3C@28&0xFF000000=0x00000000

Le programme peut aussi s'utiliser ainsi dans un script shell :

# L'action donnée ici est juste un exemple
action="LOG --log-prefix DNS-ANY-query-$domain"
rule=$(python generate-netfilter-u32-dns-rule.py --qname $domain --qtype ANY)
iptables --append ${CHAIN} --destination ${DEST} --protocol udp --destination-port 53 \
    --match u32 --u32 "$rule" \
    --jump ${action}

Autre approche, voici (dû à Frédéric Bricout) les commandes tc pour faire une limitation de trafic à 2 kb/s, exclusivement sur les requêtes ripe.net (0472697065 = ripe - avec la longueur en premier octet, 036e6574 = net) :

tc qdisc add dev eth1 root handle 1: htb default 30
tc filter add dev eth1 parent 1: protocol ip prio 10 u32 \
      match u32 0x04726970 0xffffffff at 40 match u32 0x65036e65 0xffffffff at 44 \
      match u32 0x740000ff 0xffffffff at 48 police rate 2kbit buffer 10k drop flowid :1

Une autre idée, que j'emprunte à Kim Minh Kaplan, consiste à jouer sur la longueur des noms demandés :


iptables -N DNSratelimit
# Y ajouter les règles de limitations de débit

iptables -N DNSrules

# Empêche les requêtes ANY pour .
iptables -A DNSrules -j DNSratelimit -m u32 --u32 "0>>22&0x3C@20&0xFFFFFF00=0x0000FF00"

# Empêche les requêtes ANY pour un TLD
# 21 = 20 pour IP+UDP+DNS + 1 pour l'octet de longueur
iptables -A DNSrules -j DNSratelimit -m u32 --u32 "0>>22&0x3C@20>>24&0xFF@21&0xFFFFFF00=0x0000FF00"

# Envoyer les requêtes DNS à la bonne chaîne
iptables -A INPUT -p udp --dport 53 -j DNSrules --match u32 --u32 "0>>22&0x3C@10&0x80000000=0"

Il faudrait aussi y ajouter des règles pour éviter certains traitements erronnés, etc. Malheureusement cette technique se heurte à une autre limitation de u32 :


# Empêche les requêtes pour domaine directement sous un TLD
# 22 is 20 for IP+UDP+DNS and 2 length byte
iptables -A DNSrules -m u32 --u32 "0>>22&0x3C@20>>24&0xFF@21>>24&0xFF@22&0xFFFFFF00=0x0000FF00"

Ce code échoue :

iptables v1.4.12.2: u32: at char 48: too many operators
Try `iptables -h' or 'iptables --help' for more information.

Et, effectivement, la documentation stipule « no more than 10 numbers (and 9 operators) per location ». Donc, cette voie ne semble pas idéale.

Maintenant, la question philosophique : les concepteurs du format des paquets DNS auraient-ils dû faire un format plus amical aux analyseurs ?

Merci à Kim Minh Kaplan pour sa relecture attentive du code binaire et pour la bogue détectée.

Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)

Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)