Niveau :
Résumé : perl, libipq, IN PTR
Le problème
Les firewall ne proposent jamais de faire une règle qui couperait la connexion en fonction du domaine de l'appelant. C'est gênant car on aimerait bien pouvoir devenir indépendant de l'ip de notre partenaire.
Si on ouvre un accès à un ami, pourquoi devrait-il nous avertir lorsqu'il change d'ip alors qu'il a fait correctement son travail en mettant à jour son DNS.
En réalité il y a de bonnes raisons à cela, en effet le domaine ne se trouve pas dans le paquet il faut donc aller le chercher quelque part. Et cela implique :
- qu'on base une règle sur quelque chose qu'on ne maîtrise pas, voire pire, qu'un attaquant pourrait maîtriser : la base dns de la source
- qu'on devient dépendant du résolveur dns et donc de ses failles
- qu'il est possible que le firewall empêche le firewall d'aller chercher l'info sur le serveur dns
- qu'il faut attendre que le dns réponde et donc les performances d'un tel firewall risquent d'être catastrophiques
Les deux derniers problèmes sont purement techniques et ont donc une solution technique acceptable moyennant quelques compromis. Par contre les deux premiers problèmes concernent la sécurité et il faut donc garder en tête que si on met en place une telle règle, elle est là pour nettoyer les flux qui traversent notre firewall, mais qu'elle n'apporte que peu de sécurité par rapport à l'absence de règle.
La solution
Maintenant que nous savons qu'il ne faut pas le faire, voyons comment faire :-)
Tout d'abord il y a deux possibilités, se baser sur le forward DNS (IN A et IN AAAA) ou sur le reverse dns (IN PTR). Aujourd'hui je vous propose le pire des cas, le reverse DNS, puisque votre configuration est directement tributaire de la volonté de l'attaquant.
Commençons par un prototype, donc en perl, parce que je le veux bien.
Le point important ici est de faire la résolution dns en espace utilisateur. Pour cela il faut d'utiliser libipq, la bibliothèque permettant de communiquer entre l'espace utilisateur et netfilter la partie noyau du firewall linux.
Il nous faut donc :
- libipq : IPTables::IPv4::IPQueue
- parser les paquets ip : NetPacket::IP
- faire des requêtes dns : Net::DNS
Et voici le script tout simple, le reste de l'article est en commentaire dans le script.
#!/usr/bin/perl # Reverse DNS based firewall # (c) Benoit Peccatte # License: GPLv2 # # Usage : Change configuration section # iptables -A ... -j QUEUE # ./rdns.pl & # use NetPacket::IP; # en 1er parce qu'il ne supporte pas le strict use warnings; # pour la beauté du code use strict; # idem use Data::Dumper; # pour le debug use Net::DNS; # Pour les requetes use IPTables::IPv4::IPQueue qw(:constants); ############################################## # Configuration ############################################## my $policy=1; # Politique par défaut : accepter # règle pour les adresses de destination my $toRules = [ # chaque regle est traitee dans l'ordre, first wins qr/.*.test.com/ => 0, # 0->refuse qr/.*.google.com/ => 1, # 1->accept ]; # règle pour les adresses de source # undef = pas besoin de de faire de requete dns pour les IP sources my $fromRules=undef; ################################ # C'est parti ################################ # un resolveur my $res = Net::DNS::Resolver->new; # une connexion libipq my $queue = new IPTables::IPv4::IPQueue(); # on veut le contenu des paquets pour lire l'ip $queue->set_mode(IPQ_COPY_PACKET, 2048); # on boucle sur les paquets donnes par le firewall while(my $msg = $queue->get_message()) { # on parse le paquet my $packet = NetPacket::IP->decode($msg->payload); # on recupere la decision sur le paquet my $verdict = getpacketverdict($packet); # et on dit au firewall de l'appliquer if($verdict == 1) { $queue->set_verdict($msg->packet_id(), NF_ACCEPT); } else { $queue->set_verdict($msg->packet_id(), NF_DROP); } } exit 0; ####################################### # Les foncitons importantes ####################################### # Liste tous les domaines en provenance du reverse dns # Une requête IN PTR qu'on met dans une liste @domains sub getdomains { my ($ip) = @_; my $query = $res->query("$ip", "PTR"); my @domains=(); if ($query) { foreach my $rr ($query->answer) { next unless $rr->type eq "PTR"; push @domains, $rr->rdatastr; } } else { warn "query failed: ", $res->errorstring, "\n"; } return @domains; } # Trouve les domaines associées a une IP et # passe en revue toutes les regles pour voir laquelle matche # La premiere qui matche gagne sub getverdict { my ($ip, $list) = @_; my @domains = getdomains($ip); for( my $i=0; $i<scalar(@$list); $i+=2) { for my $d (@domains) { return $list->[$i+1] if( $d =~ $list->[$i] ); } } return -1; } # Recupere le resultat de la premiere regle qui matche pour # un paquet donné. # Matche d'abord les ip sources avec fromRules, puis les ip destination avec toRules sub getpacketverdict { my ($packet) = @_; my $ipto = $packet->{dest_ip}; my $ipfrom = $packet->{src_ip}; if(defined $fromRules) { my $code = getverdict($ipfrom, $fromRules); return $code if($code != -1); } if(defined $toRules) { my $code = getverdict($ipto, $toRules); return $code if($code != -1); } return $policy; } # C'est tout simple ... !
Usage
Il faut dire à iptables de nous envoyer les paquets à filtrer. Étant donné que ipq ne permet que DROP ou ACCEPT comme décision, vous devez écrire votre firewall de façon à ce que ce soit la dernière règle de votre firewall. On lance notre firewall en arrière plan et hop :
# iptables -A INPUT -p tcp --dport 22 -j QUEUE # ./rdns.pl &
Pour l'améliorer, on peut faire le même avec des outils différents, on peut changer :
- Le langage : C (parce que je le veux bien)
- La communication avec netfilter : libnetfilter_queue (parce que libipq est dépréciée)
- L'asynchronisme : utiliser adns pour faire des requêtes DNS asynchrones
Comments