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