Ulogd2, journalisation avancée avec Netfilter

Magazine
Marque
GNU/Linux Magazine
HS n°
Numéro
41
Mois de parution
avril 2009
Spécialité(s)


Résumé

Si « un homme sans passé est plus pauvre qu'un homme sans avenir » (Elie Wiesel), c'est pire encore pour un réseau. Sans journalisation, un réseau est aveugle : les attaques restent sans preuve, les problèmes de configuration restent incompréhensibles. Si Netfilter a toujours offert des outils intéressants pour la journalisation, ulogd2 étend encore ses capacités.


Body

1. Sources d'événements de Netfilter

1.1 Journalisation de paquets

La journalisation de paquets sous Netfilter s'effectue principalement au moyen de règles de filtrage dont la cible est une cible de journalisation (LOG, ULOG, NFLOG). Ces règles sont non terminales et peuvent utiliser tous les filtres disponibles. Par non terminale, on entend que le fait de rencontrer une règle de journalisation n'entraîne pas une décision sur le paquet (à l'inverse de DROP ou ACCEPT). Par conséquent, le schéma d'utilisation des cibles de journalisation le plus souvent rencontré est le suivant :

iptables -A FORWARD -p tcp --dport 23 -j LOG --log-prefix "Telnet is bad "

iptables -A FORWARD -p tcp --dport 23 -j DROP # DROP telnet explicite

L'administrateur décide de faire journaliser le paquet suivant un filtre et prend juste après la décision sur ce même paquet.

1.1.1 LOG

La cible LOG est la cible historique pour la journalisation des paquets dans Netfilter et même dans Ipchains, la couche pare-feu des noyaux 2.2. Il s'agit d'une journalisation noyau standard qui envoie les paquets à loguer dans syslog.

1.1.2 ULOG

Apparu avec Netfilter, ULOG est la première cible évoluée pour la journalisation des paquets. Les données sont envoyées via une socket Netlink depuis le noyau vers l'espace utilisateur. Qui dit « socket » dit « communication » et il est donc nécessaire d'avoir un logiciel à l'écoute pour récupérer les données et les journaliser. Le logiciel ulogd a été développé par Harald Welte pour remplir cette tâche. Il récupère les données du noyau et les stocke dans différents formats grâce à des plugins de sortie.

En dehors des possibilités offertes par ulogd, la cible ULOG présente certains avantages comme pouvoir envoyer les paquets de l'espace noyau par bouffée ce qui accroît les performances et économise certains médias de stockage.

1.1.3 NFLOG

Le noyau 2.6.14 a vu une refonte des infrastructures d'interaction entre l'espace utilisateur et le noyau dans Netfilter. Le sous-système nfnetlink a été introduit pour servir de fondement aux différents types de communication entre l'espace utilisateur et le noyau. Cette factorisation des mécanismes d'échanges a rendu nécessaire de développer un successeur à ULOG utilisant ce nouveau mode de communication. En dehors de cette vision développeur, ULOG souffrait d'une limitation assez grave qui était l'absence de support d'IPv6.

Cela a mené à l'introduction de la cible NFLOG qui reprend les fonctionnalités d’ULOG en utilisant la nouvelle infrastructure et ajoutant IPv6 à la liste des protocoles supportés. Si l'on détaille les options de NFLOG (souvent communes avec celle d’ULOG), on a :

--nflog-group NUM : numéro de canal utilisé pour la journalisation ;

--nflog-range NUM : nombre d'octets de données à copier ;

--nflog-threshold NUM : nombre de messages à stocker avant envoi par le noyau ;

--nflog-prefix STRING : chaîne de préfixe pour les messages de logs.

--nflog-group doit être choisi entre 0 et 65535. On évitera 0 qui est réservé aux messages système (voir plus bas). Cette valeur définit quel canal utiliser lors de l'envoi des messages. L'intérêt de multiplier les canaux est de pouvoir différencier les messages. Cela offre un moyen de circonvenir à un problème classique de Netfilter : il n'existe pas de moyen explicite de différencier un paquet bloqué d'un paquet accepté lorsque l'on journalise un paquet. En ayant plusieurs canaux, il est ainsi possible de définir une file pour les paquets bloqués et une file pour les paquets acceptés. L'ensemble des autres scénarios d'utilisation est laissé à la sagacité du lecteur.

--nflog-threshold spécifie le nombre de paquets stockés au niveau du noyau avant émission d'un message vers l'espace utilisateur. Il s'agit d'un tampon qui se remplit avec les paquets journalisés et qui est vidé lorsque le nombre de paquets maximums ou lorsque des paquets sont en attente depuis un certain temps (par défaut une seconde). L'utilisation de ce paramètre limite la charge système en limitant le nombre de communication entre l'espace noyau et l'espace utilisateur, mais ne permet pas une remontée immédiate des informations.

Si le noyau Linux est plus ancien que 2.6.29, la journalisation par NFLOG est dépendante du système de journalisation interne (voir section 1.2). Il est donc possible qu'un paquet journalisé par NFLOG soit envoyé vers l'espace utilisateur comme un paquet journalisé par LOG ou ULOG.

1.2 Journalisation de paquets système

1.2.1 Système de journalisation interne

Un certain nombre de modules Netfilter sont capables de détecter des problèmes assez complexes ou assez étranges. Par exemple, le module de suivi de connexions TCP peut détecter des paquets invalides. Les causes de l'invalidité peuvent être multiples (paquet trop court, mauvais checksum, combinaison de drapeaux invalide,...). L'obtention de ces informations conduit donc à la détection d'attaques ou de graves problèmes système ou réseau.

Un système a donc été mis en place pour donner la possibilité aux composants Netfilter d'envoyer des messages précis à l'espace utilisateur. Un système interne de journalisation des paquets appelé nf_log a été développé. Il offre à tous les modules la possibilité d'envoyer des messages au moyen d'un simple appel de fonction.

Cependant, comme on vient de le voir, les voies possibles pour la journalisation (LOG, ULOG, NFLOG) sont multiples et nf_log doit donc permettre de choisir vers quel sous-système envoyer les paquets (on parlera par la suite de logger). Comme ULOG, par exemple, est uniquement compatible IPv4, il ne peut servir d'infrastructure de journalisation pour l'ensemble des protocoles et le choix du système est donc dépendant du protocole. Une table de correspondance protocole/logger a donc été mise en place. Elle est interrogeable au moyen du fichier /proc/net/netfilter/nf_log. Pour faire simple, les indices utilisés pour les protocoles sont ceux définis dans les en-têtes des sources du noyau (constantes AF_*) et ils ne correspondent donc pas à la valeur bien connue des protocoles. Ainsi, pour IPv4, il faut regarder la valeur 2 ; pour IPv6, c'est la valeur 10. À la décharge des développeurs, on a différents types de protocoles ici, par exemple AF_BRIDGE (7) pour les paquets reçus en mode bridge et une correspondance avec les numéros de protocoles standards (IP par exemple) n'est donc pas possible.

Ce système est actuellement utilisé par une bonne partie des modules de suivi de connexions et par le module TRACE qui permet de suivre les décisions prises pour un paquet pour chaque hook de Netfilter.

1.2.2 Configuration du module de sortie

Tout bon administrateur voudra paramétrer le logger qui lui convient. La bonne nouvelle, c'est qu'il peut le faire. La mauvaise nouvelle, c'est qu'il faut soit la bonne version du noyau, soit un peu d'astuce. Commençons par le cas le pire : le noyau est plus ancien que 2.6.30 (Non, non, nous ne sommes pas devins, 2.6.30 n'est pas encore sorti à ce jour, mais nous sommes à la source des modifications qu'il induit ici). Là, la configuration est simple ou presque. Le premier module chargé gagne et récupère la fonction de journalisation pour les protocoles auxquels il s'enregistre. Enfin, cela est vrai pour tous les modules sauf pour nfnetlink_log. Celui-ci a une fonction d'enregistrement et de désenregistrement qui lui permet de récupérer la fonction de journalisation en cours de fonctionnement. Des logiciels comme ulogd2 peuvent ainsi se rattacher au système de journalisation interne.

Pour les noyaux plus récents, le fonctionnement est plus simple. Le répertoire /proc/sys/net/netfilter/nf_log contient un fichier par protocole et le contenu de chaque fichier est le logger utilisé pour ce protocole. On peut ainsi changer de logger en injectant le nom d'un logger enregistré dans le fichier correspondant à un protocole. Par exemple, pour activer ipt_LOG sur IPv4, on peut taper :

# echo "ipt_LOG">/proc/sys/net/netfilter/nf_log/2

Pour savoir quels sont les modules activables en tant que logger, on peut afficher le fichier /proc/net/netfilter/nf_log qui indique entre parenthèses les modules enregistrés :

# cat /proc/net/netfilter/nf_log

0 NONE (nfnetlink_log)

1 NONE (nfnetlink_log)

2 ipt_ULOG (ipt_LOG,nfnetlink_log,ipt_ULOG)

3 NONE (nfnetlink_log)

4 NONE (nfnetlink_log)

5 NONE (nfnetlink_log)

6 NONE (nfnetlink_log)

7 NONE (nfnetlink_log)

8 NONE (nfnetlink_log)

9 NONE (nfnetlink_log)

10 NONE (nfnetlink_log)

11 NONE (nfnetlink_log)

12 NONE (nfnetlink_log)

1.2.3 Modules de suivi de connexions

Les modules de suivi de connexions par protocoles utilisent généralement ce système pour journaliser les paquets invalides. Comme les informations envoyées sont particulièrement techniques, elles ne sont pas compréhensibles par le commun des mortels (il faut au moins savoir se connecter à PostgreSQL) et la notification de ces événements a donc été désactivée par défaut. Pour l'activer, sur l'ensemble des protocoles, il faut faire :

# echo 255>/proc/sys/net/netfilter/nf_conntrack_log_invalid

Pour l'activer sur un protocole donné (par exemple TCP), il suffit d'écrire le numéro du protocole (6 pour TCP par exemple) dans le fichier nf_conntrack_log_invalid.

1.3 Événements de connexions

Les dernières versions de Netfilter (depuis 2.6.14) proposent une système de remontée des événements du suivi de connexions vers l'espace utilisateur. En langage compréhensible, un message est envoyé à l'espace utilisateur dès qu'une connexion est créée, change d'état ou est détruite. Ce système est utilisé pour répliquer les connexions entre deux pare-feu et permet une bascule d'une machine vers l'autre sans coupure. Au niveau de la journalisation, l'intérêt premier est d'enregistrer la création, la destruction des connexions établies à travers le pare-feu en ayant, de plus, des informations de volumétrie et de durée. Le message envoyé à l'espace utilisateur reprend l'ensemble des paramètres d'une connexion :

tcp 6 431911 ESTABLISHED src=192.168.22.176 dst=91.21.73.151 sport=44189 dport=993 packets=1573 bytes=124587 src=91.21.73.151 dst=213.144.21.78 sport=993 dport=44189 packets=2408 bytes=2677410 [ASSURED] mark=10003 use=1

On remarque ainsi que l'entrée contient le détail des transformations effectuées lors de la traduction d'adresse. Ici, une connexion de 192.168.22.176 vers le port 993 (imaps) de 91.21.73.151 devient une connexion de 213.144.21.78 vers 91.21.73.151 sur le port 993. Ce détail sur les opérations de traduction d'adresse est intéressant, puisqu'il contient la trace des connexions vues de l'intérieur et de l'extérieur. Il est donc possible sur requête d'un tiers (une personne costumée par exemple) qui n'a que la vision externe du trafic de déterminer depuis quelle adresse IP interne provenait une connexion (malicieuse, malveillante, illégale, [réponse au choix]) dirigée vers un serveur externe.

La remontée des événements du suivi de connexions vers l'espace utilisateur se fait au moyen de la bibliothèque libnetfilter_conntrack qui est chargée de l'interaction avec le suivi de connexion.

2. Présentation d'ulogd2

2.1 Architecture

Ulogd2 est le démon chargé de journaliser les paquets envoyés en espace utilisateur par Netfilter/Iptables, via les cibles ULOG et NFLOG et les connexions par libnetfilter_conntrack.

Ulogd2 est un système flexible et basé sur des modules. Les modules sont utilisés, par exemple, pour enregistrer un flux de données au format PCAP ou encore pour écrire dans une base de données, mais ils peuvent aussi servir de source de données ou encore de filtre. Un des intérêts évidents est de pouvoir utiliser une base de données pour faire l'analyse et l'extraction de statistiques avec une interface graphique, comme NuLog (http://software.inl.fr/trac/wiki/EdenWall/NuLog2) Nous allons détailler leur utilisation par la suite.

Grâce à cette architecture, il est très facile de créer un nouveau module pour tout besoin spécifique, par exemple l'importation de données depuis une source externe ou l'exportation vers d'autres systèmes.

2.2 Principe des stacks

Ulogd2 utilise les modules pour former une chaîne (appelée stack). Il existe trois types de modules :

- source ;

- filtre ;

- sortie.

Une chaîne doit absolument avoir une source, et une sortie (elle peut avoir plusieurs plugins de filtrage). Il est possible de définir plusieurs chaînes dans la configuration.

Chaque plugin possède un type (par exemple, PGSQL), et doit être instancié (en utilisant un nom, choisi par l'utilisateur). De cette manière, il est possible d'utiliser un même plugin dans différentes chaînes (avec différents paramètres de configuration), et, par exemple, d'envoyer les données vers différentes destinations, en utilisant plusieurs chaînes.

Les données d'entrée et de sortie de chaque module sont appelées des clés. Dans une chaîne, chaque module demande une liste de clés au module précédent, et le module de sortie peut utiliser les clés du module d'entrée, et des modules de filtrage.

Par exemple, la ligne suivante :

stack=log2:NFLOG,base1:BASE,ifi1:IFINDEX,ip2str1:IP2STR,mac2str1:HWHDR,pgsql1:PGSQL

définit une chaîne avec les paramètres suivants :

- Le module d'entrée est NFLOG, donc il faudra utiliser la cible -j NFLOG d'iptables comme source.

- Les filtres sont IFINDEX, IP2STR, HWHDR.

- Les données seront enregistrées dans une base de données PostgreSQL.

Les différents modules d'entrées disponibles sont :

- ULOG : récupère des paquets depuis la source iptables ULOG.

- NFLOG : récupère des paquets depuis la source iptables NFLOG (apporte notamment le support IPv6 et ebtables).

- NFCT : récupère des paquets depuis le suivi de connexions Netfilter, en utilisant libnetfilter_conntrack.

Les modules de filtrage sont :

- BASE : convertit les données binaires du paquet en une structure utilisable par les autres modules.

- HWHDR : extrait les adresses MAC des en-têtes ethernet.

- IFINDEX : convertit un numéro d'interface réseau en nom d'interface (par ex. eth0).

- IP2BIN : extrait et normalise les adresses IP, sous forme binaire.

- IP2STR : extrait et normalise les adresses IP, sous forme de chaîne de caractères.

- MARK : ne laisse passer que les paquets dont la marque correspond à celle de la configuration.

- PRINTFLOW : convertit une connexion en chaîne de caractères.

- PRINTPKT : convertit un paquet en chaîne de caractères.

- PWSNIFF : essaye de détecter des séquences de login avec des mots de passe en clair.

Les modules de sortie :

- LOGEMU : enregistre les données dans un fichier texte.

- NACCT : exporte les données dans un format compatible nacct (accounting).

- OPRINT : enregistre les données dans un fichier texte, dans un format multiligne.

- PCAP : exporte les données au format PCAP.

- SYSLOG : enregistre les données en les envoyant à syslog.

- IPFIX : permet d'exporter les données au format IPFIX (IP Flow Information Export) [en cours de développement].

- Base de données : ulogd2 supporte de nombreuses bases de données (MySQL, PostgreSQL, etc.). Les bases supportées sont détaillées au chapitre suivant.

- Chaque module est chargé dynamiquement lors du démarrage d'ulogd2. Un module dispose de :

- variables de configuration : elles paramètrent le comportement du module ;

- clés d'entrée (input keys) : elles définissent les valeurs (obligatoires ou pas) utilisées par le module pour pouvoir fonctionner ;

- clés de sortie (output keys) : les valeurs de sorties du module.

Chaque clé dispose d'un type, et peut avoir une valeur par défaut. Pour avoir une chaîne fonctionnelle, il faut obligatoirement un module d'entrée, et un ou plusieurs modules de filtrage dont l'ensemble des clés de sortie correspondra aux clés d'entrée du dernier module, le module de sortie.

Des informations sur un module, ses possibilités de configuration et ses clés d'entrée et de sortie, peuvent être obtenues en utilisant la commande info d'ulogd2 :

# /opt/ulogd2/sbin/ulogd --info /opt/ulogd2/lib/ulogd/ulogd_filter_IP2STR.so

Name: IP2STR

Input keys:

       Key: oob.family (unsigned int 8)

       Key: oob.protocol (unsigned int 16)

       Key: ip.saddr (IP addr, optional)

       Key: ip.daddr (IP addr, optional)

       [...]

Output keys:

       Key: ip.saddr.str (string)

       Key: ip.daddr.str (string)

[...]

La logique du module IP2STR est simple. Il convertit les champs de type IP addr en des champs de type string. Ainsi, si la clé ip.saddr est présente, il la convertit en une chaîne de caractères qu'il stocke dans ip.saddr.str. Cette clé peut alors être utilisée par les autres plugins de la stack.

Dans le cas d'un module de sortie, il n'y a pas de clés de sorties, mais cela ne l'empêche pas d'avoir des options de configuration :

# /opt/ulogd2/sbin/ulogd --info /opt/ulogd2/lib/ulogd/ulogd_output_PCAP.so

Name: PCAP

Config options:

Var: file (String, Default: /var/log/ulogd.pcap)

 Var: sync (Integer, Default: 0)

Input keys:

 Key: raw.pkt (unsigned int 32)

 Key: raw.pktlen (unsigned int 32)

 Key: ip.totlen (unsigned int 16)

 Key: oob.time.sec (unsigned int 32)

 Key: oob.time.usec (unsigned int 32)

Output keys:

 Output plugin, No keys

2.3 Bases de données

2.3.1 Schémas et insertion des données

Lors de l'utilisation de bases de données pour des volumes de données conséquents, le problème de conception suivant revient régulièrement : faut-il privilégier le design (modèle en nième forme normale, nombre important de tables) au détriment de la rapidité, ou faut-il utiliser un schéma avec un nombre de tables restreint, et éventuellement de la duplication de données, pour augmenter les performances ?

Cette question (à débattre lors de longues soirées) n'a pas de solution simple, c'est pourquoi nous avons décidé, en développant ulogd2... ne pas décider ! Ulogd2 va utiliser une couche d'abstraction pour pouvoir enregistrer les données sans connaître le schéma SQL utilisé, et de cette manière laisser le choix à l'utilisateur.

Par défaut, ulogd2 utilise un schéma dans lequel les données sont réparties entre différentes tables. Les données communes (niveau IP) sont inscrites dans une table, les données TCP dans une autre, etc. Ce schéma assure donc une bonne cohérence des données. En revanche, il présente plusieurs difficultés :

- Un paquet contient des données à insérer dans plusieurs tables, qui dépendent des critères du paquet (protocole TCP ou UDP par exemple).

- Pour récupérer les données, une application devra extraire des données de plusieurs tables (en effectuant des jointures), également en fonction des critères du paquet. Ce point ne concerne pas directement ulogd2, mais il est préférable de prévoir l'utilisation des données pour pouvoir la simplifier.

 

ulogd2

 

Schéma de la base de données PgSQL

La problématique de l'insertion va être résolue en utilisant des procédures stockées dans la base de données. Ulogd2 va appeler une procédure unique, qui sera chargée de faire les insertions dans les différentes tables. Voici un exemple de procédure, en utilisant le langage PL/pgSQL, propre à PostgreSQL :

CREATE OR REPLACE FUNCTION INSERT_PACKET_FULL(

               IN value11 integer,

               ...)

RETURNS bigint AS $$

DECLARE

       t_id bigint;

DECLARE

               t_id := INSERT INTO tablename (field1,field2,...) VALUES ($1,$2,...);

               RETURN t_id;

END

$$ LANGUAGE plpgsql SECURITY INVOKER;

Cette procédure ne fait qu'insérer dans une table, mais elle pourrait également effectuer des opérations plus complexes.

Si l'on regarde ce qu'un module de sortie en base de données a dans le ventre, on peut être surpris (ça mange n'importe quoi un éléphant ou un dauphin...). Prenons le cas de PostgreSQL et dumpons les informations avec l'option info :

/opt/ulogd2/sbin/ulogd --info /opt/ulogd2/lib/ulogd/ulogd_output_PGSQL.so

Name: PGSQL

Config options:

 Var: table (String, Default: , Mandatory)

 Var: reconnect (Integer, Default: 2)

 Var: connect_timeout (Integer, Default: 0)

 Var: procedure (String, Default: , Mandatory)

 Var: db (String, Default: , Mandatory)

 Var: host (String, Default: )

 Var: user (String, Default: , Mandatory)

 Var: pass (String, Default: )

 Var: port (Integer, Default: 0)

 Var: schema (String, Default: public)

Input keys:

 No statically defined keys

Output keys:

 Output plugin, No keys

En ce qui concerne les options de configuration, il n'y a rien de bien exceptionnel à première vue. Ce qui est le plus bizarre concerne l'absence de clé d'entrée. Celles-ci sont en fait générées dynamiquement à partir des noms des champs de la table pointée par table. Les champs de la table sont convertis en remplaçant les underscores par des points, et ulogd cherche alors la correspondance avec les clés disponibles. S’il parvient à trouver toutes les clés, l'initialisation de la stack est réussie. L'ensemble ordonné de ces variables est alors utilisé comme liste des paramètres pour la procédure pointée par procedure. L'avantage de cette solution est de garantir l'adaptabilité des modules de bases de données à une large gamme d'opérations et une évolution des procédures stockées dans la base ne nécessite pas une recompilation d’ulogd, mais au plus un simple redémarrage de celui-ci si la liste des paramètres de la procédure a évolué.

2.3.2 Comparaison et utilisation des schémas

La lecture des données possède également une solution simple : il suffit d'encapsuler la lecture des données dans une vue, qui prendra en charge la collecte des données dans les différentes tables en utilisant des jointures. Par exemple :

CREATE OR REPLACE VIEW ulog AS

        SELECT _id,

        oob_time_sec,

        oob_time_usec,

        ...

        FROM ulog2 LEFT JOIN tcp ON ulog2._id = tcp._tcp_id LEFT JOIN udp ON ulog2._id = udp._udp_id

                LEFT JOIN sctp ON ulog2._id = sctp._sctp_id

                LEFT JOIN icmp ON ulog2._id = icmp._icmp_id

                LEFT JOIN mac ON ulog2.mac_id = mac._mac_id

                LEFT JOIN hwhdr ON ulog2._id = hwhdr._hw_id

                LEFT JOIN icmpv6 ON ulog2._id = icmpv6._icmpv6_id;

Ouf ! Nous avons donc réussi à transformer un schéma simple (qui utilisait des insertions) en quelque chose de plus lent et de plus complexe. Mais pourquoi ?

Avantages :

- Indépendance entre le code C et SQL : si le schéma SQL change, pas besoin de changer le code C.

- Le schéma SQL peut être spécifique à la base utilisée. Par exemple, PostgreSQL dispose de types de données spécifiques pour les adresses IP et MAC (inet et macaddr), alors que MySQL ne dispose pas de ces types (il faut par exemple enregistrer les données en binaire).

- Le schéma SQL est facile à étendre pour de nouvelles applications : il suffit d'ajouter une table, et de la relier à la table principale en utilisant l'id du paquet.

- La récupération de données est plus simple pour les applications comme NuLog, puisque la complexité du schéma est masquée par les procédures et les vues.

- Cela permet d'avoir un contrôle plus strict sur les données et leur cohérence. Par exemple, les adresses MAC ne sont stockées qu'une seule fois dans ce schéma, ce qui représente un gain de place conséquent (dans le schéma monolithique, les adresses sont écrites pour chaque paquet).

Inconvénients :

- Lenteur : l'utilisation de procédures, ainsi que le fait d'avoir plusieurs tables, ralentissent fortement les insertions. Ce schéma n'est donc pas recommandé dans le cas où les performances sont importantes.

- Le schéma est plus difficile à lire.

- Certaines bases ne disposent pas de procédures stockées (sqlite3 par exemple). Ce point est gênant, mais là encore une solution a été trouvée.

- Un deuxième schéma est disponible, qui enregistre toutes les données dans une seule table (schéma flat). Ce schéma, orienté performance, n'a donc pas besoin d'utiliser de procédure, puisque les données peuvent être insérées directement.

- Nous avons donc étendu Ulogd2 pour qu'il accepte INSERT comme nom de procédure, auquel cas il insère les données directement, sans procédures.

- Grâce à cette souplesse sur le schéma, ulogd2 est donc capable de s'adapter à un grand nombre de systèmes différents, sans besoin de modifications.

- Ulogd2 supporte les bases de données suivantes :

- MYSQL : enregistre les données dans une base MySQL.

- POSTGRESQL : enregistre les données dans une base PostgreSQL.

- SQLITE3 : enregistre les données dans une base sqlite.

- DBI : enregistre les données dans une base de données, en utilisant la couche d'abstraction libdbi. Cela permet notamment d'utiliser d'autres bases de données, comme Firebird, Sybase, MS-SQL ou encore Oracle et Ingres.

Le module dbi aurait pu être utilisé pour MySQL et PostgreSQL. Cependant, cela empêcherait l'utilisation des fonctions spécifiques de l'API de chaque base, par exemple l'API asynchrone de PostgreSQL.

Certaines applications supportent déjà ulogd2, comme notre analyseur de logs NuLog :

 

nulog2

 

3. Installation et configuration

Après cette longue présentation, vous êtes impatient de pouvoir utiliser ulogd2. Comme il s'agit d'un projet encore en développement, il n'existe pas encore de paquets pour les distributions... nous sommes donc obligés de passer par la case compilation.

3.1 Dépendances

Pour pouvoir compiler ulogd2, nous avons besoin de :

- Un compilateur (oui, elle était facile), nous prendrons donc gcc ;

- libnfnetlink(>= 0.39) ;

- libnetfilter_log(>= 0.0.15) ;

- libnetfilter_conntrack(>= 0.95) ;

- mysql-dev ;

- postgresql-dev ;

- libdbi-dev(optionnel) ;

- libpcap-dev(optionnel).

Après une série d'apt-get install, nous voilà donc prêts pour récupérer les sources d'ulogd2. Il n'existe pas encore de versions publiées d'ulogd2. Nous allons donc extraire les sources du dépôt git (que les âmes sensibles se rassurent, pas besoin d'apprendre la documentation de git).

git clone git://git.netfilter.org/ulogd2.git/

cd ulogd2

3.2 Compilation

Ulogd2 utilise les autotools. La compilation se fera donc de manière très classique.

./autogen.sh

./configure --prefix=/path/to/prefix

make

[sudo] make install

3.3 Configuration

La configuration d'ulogd2 est contenue dans le fichier ulogd.conf, qui est installé dans $PREFIX/etc/. La configuration est au format INI, et est séparée en différentes sections :

- la section globale, contenant la liste des modules à charger, la définition des chaînes, la configuration spécifique ulogd2, les options mémoire, etc.

- une section pour chaque instance de module, utilisée pour définir les paramètres propres à chaque instance.

La première étape est de choisir les modules d'entrée et de sortie, en fonction de votre système. NFLOG est présent dans tous les noyaux récents, et doit être préféré à ULOG chaque fois que c'est possible. Dans notre exemple, nous allons choisir les modules NFLOG et POSTGRESQL. La configuration doit être adaptée pour apporter les modifications qui suivent.

3.3.1 Section globale

Cette section permet de régler le comportement d'ulogd2, par exemple pour la gestion des ressources comme la mémoire.

[global]

# logfile for status messages

logfile="/var/log/ulogd.log"

# loglevel: debug(1), info(3), notice(5), error(7) or fatal(8)

loglevel=1

# socket receive buffer size (should be at least the size of the

# in-kernel buffer (ipt_ULOG.o 'nlbufsiz' parameter)

rmem=131071

# libipulog/ulogd receive buffer size, should be > rmem

bufsize=150000

3.3.2 Chargement des modules

Tous les modules utilisés par la suite doivent être chargés dans cette partie. La configuration des modules se fait dans des sections spécifiques.

plugin="/opt/ulogd/ulogd_inppkt_NFLOG.so"

plugin="/opt/ulogd/ulogd_output_PGSQL.so"

plugin="/opt/ulogd/ulogd_filter_IFINDEX.so"

plugin="/opt/ulogd/ulogd_filter_IP2STR.so"

plugin="/opt/ulogd/ulogd_filter_HWHDR.so"

3.3.3 Définition des chaînes

L'étape suivante est la définition des chaînes de modules. Pour fonctionner, ulogd2 doit disposer d'au moins une chaîne. La configuration pouvant parfois être assez astucieuse, le moyen le plus simple est de partir des chaînes données en exemple, en en décommentant une, et en l'adaptant au besoin.

Chaque module est utilisé sous la forme instancename:MODULE, où instancename est défini par l'utilisateur, et MODULE est le nom d'un module chargé précédemment.

# IPv4 logging via PostgreSQL

stack=log1:NFLOG,base1:BASE,ifi1:IFINDEX,ip2str1:IP2STR,mac2str1:HWHDR,pgsql1:PGSQL

# IPv4 logging via MySQL

#stack=log1:NFLOG,base1:BASE,ifi1:IFINDEX,ip2bin1:IP2BIN,mac2str1:HWHDR,mysql1:MYSQL

# this is a stack for IPv4 packet-based logging via LOGEMU

#stack=log1:NFLOG,base1:BASE,ifi1:IFINDEX,ip2str1:IP2STR,print1:PRINTPKT,emu1:LOGEMU

# this is a stack for IPv4 packet-based logging via LOGEMU with filtering on MARK

#stack=log1:NFLOG,mark1:MARK,base1:BASE,ifi1:IFINDEX,ip2str1:IP2STR,print1:PRINTPKT,emu1:LOGEMU

# this is a stack for flow-based logging via LOGEMU

#stack=ct1:NFCT,ip2str1:IP2STR,print1:PRINTFLOW,emu1:LOGEMU

# this is a stack for flow-based logging via OPRINT

#stack=ct1:NFCT,op1:OPRINT

Il est possible de définir différentes chaînes, en ajoutant autant de directives stack= que nécessaire. Chaque module utilisé une chaîne devra donc utiliser une instance différente.

3.3.4 Configuration des modules

Chaque instance définie précédemment doit être configurée. Si parfois aucune configuration n'est nécessaire, la définition d'une section vide pour l'instance est obligatoirement une initialisation correcte du module.

# IPv4 logging through NFLOG

[log1]

# netlink multicast group (the same as the iptables --nflog-group param)

group=1

# IPv6 logging through NFLOG

[log2]

group=2 # Group has to be different from the one used in log1

[emu1]

file="/var/log/ulogd_syslogemu.log"

sync=1

[sys2]

facility=LOG_LOCAL2

[pgsql1]

db="ulog2"

host="localhost"

user="ulog2"

table="ulog"

pass="ulog2_pass"

procedure="INSERT_PACKET_FULL"

3.4 Configuration de Postgresql

Il reste à créer la base de données, et l'utilisateur :

# su - postgres

$ createuser -P ulog2

$ createdb -O ulog2 ulog2

Pour pouvoir utiliser les procédures, le support du langage PL/pgSQL doit être ajouté à la base :

$ createlang plpgsql ulog2

Il ne reste plus qu'à insérer la structure initiale de la base (cette action ne se fait plus en tant qu'utilisateur postgres). Pour les non-habitués à PostgreSQL, on rappelle qu'il peut être nécessaire de modifier le fichier pg_hba.conf pour autoriser le mécanisme md5 en local.

$ psql -U ulog2 -h localhost ulog2 -f doc/pgsql-ulogd2.sql

Si tout se passe bien, vous n'avez plus qu'à démarrer ulogd2.

3.5 Démarrage d'ulogd2

Pour le moment, aucune donnée ne sera envoyée à ulogd2. Il reste à ajouter des règles iptables pour envoyer des données à NFLOG, et à démarrer ulogd2. Voici un exemple de règle iptables :

# iptables -A FORWARD -p tcp --dport 80 -m state --state NEW -j NFLOG --nflog-group 1

On peut vérifier à l'aide des compteurs que des paquets sont bien envoyés à NFLOG :

# iptables -vnL FORWARD

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)

pkts bytes target     prot opt in     out     source               destination

44582 2718K NFLOG      tcp -- *      *       0.0.0.0/0            0.0.0.0/0           tcp dpt:80 flags:0x17/0x02 state NEW nflog-group 1

Nous pouvons donc démarrer ulogd2. Avant de le démarrer en mode définitif, nous allons commencer par l'utiliser en avant-plan et vérifier que tout fonctionne :

# /opt/sbin/ulogd

Et là, c'est le drame. Ulogd2 rend la main, en n'affichant que le message :

Fatal error, check logfile "/var/log/ulogd.log".

Comme nous n'avons rien d'autre à faire, nous écoutons sagement ulogd2, et regardons le fichier /var/log/ulogd.log.

Attention, surtout si vous utilisez la commande tail, il faut scroller plusieurs fois avant d'arriver aux messages intéressants...

Wed Mar 26 22:19:54 2008 ulogd.c:698 cannot find key `timestamp' in stack

Wed Mar 26 22:19:54 2008 ulogd.c:807 destroying stack

Après quelques tentatives (ici, l'erreur venait du module BASE qui n'était pas utilisé dans la chaîne), ulogd se lance correctement. En effectuant quelques connexions qui vérifient la règle iptables, on obtient quelques paquets dans la base SQL :

ulog2=> select count(*) from ulog2;

count

-------

     3

(1 row)

On peut, par exemple, utiliser la vue view_tcp_quad pour obtenir une synthèse des connexions.

ulog2=> select * from view_tcp_quad;

_id | ip_saddr_str | tcp_sport | ip_daddr_str | tcp_dport

-----+--------------+-----------+---------------+-----------

   1 | 192.168.1.20 |     56035 | 91.121.73.151 |        80

   2 | 192.168.1.20 |     56037 | 91.121.73.151 |        80

   3 | 192.168.1.20 |     56038 | 91.121.73.151 |        80

(3 rows)

Tout fonctionne correctement, nous pouvons donc lancer ulogd2 en mode daemon :

# /opt/sbin/ulogd -d

4. Utilisation

4.1 Utilisation des labels

Nous avions évoqué au début de l'article la difficulté de différencier l'état (bloqué ou accepté) d'un paquet lors de la journalisation. Une solution partielle reposait sur l'utilisation de files de journalisation séparée grâce à l'option --nflog-group. Ulogd2 propose une option numeric_label dont la définition et l'usage sont laissés à l'administrateur. Une utilisation pratique est de définir une convention sur la décision prise sur le paquet, par exemple 0 pour bloqué, 1 pour accepté. Prenons comme cas d'exemple la journalisation des paquets acceptés et droppés par NFLOG dans une base PostgreSQL (base configurée comme décrit précédemment).

Commençons par définir deux stacks, la première pour les paquets droppés, la deuxième pour les paquets acceptés :

stack=logdrop:NFLOG,base1:BASE,ifi1:IFINDEX,ip2str1:IP2STR,mac2str1:HWHDR,pgsql1:PGSQL

stack=logaccept:NFLOG,base1:BASE,ifi1:IFINDEX,ip2str1:IP2STR,mac2str1:HWHDR,pgsql1:PGSQL

On a ainsi deux stacks qui diffèrent uniquement par leurs instances d'entrée dont voici la configuration :

[logdrop]

group=1

numeric_label=0

[logaccept]

group=1

numeric_label=1

Avec cette configuration, les paquets journalisés dans le groupe 1 doivent être les paquets droppés, et ceux journalisés dans le groupe 2 les paquets acceptés. On peut ainsi avoir le jeu de règles suivant.

iptables -A FORWARD -p tcp --dport 23 -j NFLOG --nflog-group 1 --nflog-prefix "Telnet packet"

iptables -A FORWARD -p tcp --dport 23 -j DROP

iptables -A FORWARD -p tcp --dport 22 -j NFLOG --nflog-group 2 --nflog-prefix "SSH packet"

iptables -A FORWARD -p tcp --dport 22 -j ACCEPT

Au niveau de la base de données, les paquets acceptés et droppés seront différenciés par le champ label qui vaudra 1 pour les paquets acceptés et 0 pour les paquets droppés.

4.2 Journalisation système

Comme vu au début de l'article, un certain nombre de modules utilisent une fonction de journalisation commune pour envoyer des paquets à l'espace utilisateur. Lorsque le module NFLOG est utilisé, la journalisation s'effectue sur le groupe 0. Pour simplifier les choses, lorsque ulogd2 détecte l'utilisation d'une instance d'entrée NFLOG paramétrée sur le groupe 0, il attribue les événements système à NFLOG pour l'ensemble des protocoles supportés.

La journalisation des paquets système dans un fichier par LOGEMU se fait donc simplement :

stack=log1:NFLOG,base1:BASE,ifi1:IFINDEX,ip2str1:IP2STR,print1:PRINTPKT,emu1:LOGEMU

[log1]

group=0

[emu1]

file="/var/log/ulogd_syslogemu.log"

sync=1

On définit une stack prenant en entrée une instance NFLOG et débouchant sur une instance de LOGEMU. La configuration de log1 est simple, puisque l'on se contente de la rattacher au groupe 0. Pour emu1, on spécifie le fichier de journalisation avec file et on force une journalisation synchrone avec l'option sync.

4.3 Journalisation des connexions dans PostgreSQL

On se propose de journaliser l'ensemble des connexions marquées 1 dans une base PostgreSQL. Nous allons donc devoir utiliser le module d'entrée NFCT et le module de sortie PGSQL. Pour filtrer sur la marque, nous allons utiliser le module mark qui interrompt le traitement des paquets par la stack si la marque du paquet ou de la connexion ne correspond pas à l'option spécifiée.

stack=ct1:NFCT,mark1:MARK,ip2str1:IP2STR,pgsql2:PGSQL

[ct1]

[pgsql2]

db="nulog"

host="localhost"

user="nupik"

table="ulog2_ct"

pass="changeme"

procedure="INSERT_CT"

[mark1]

mark = 1

Il y a peu d'originalité dans cette configuration. On notera seulement l'appel à la procédure "INSERT_CT" en lieu et place d'INSERT_PACKET_FULL". La modularité des modules de gestion de base rend en effet possible le passage d'une journalisation de paquets à une journalisation de connexions en changeant simplement les paramètres procedure et table.

4.4 nf3d, les journaux en 3D

Ulogd2 journalise une quantité de données conséquente et, pour la première fois, les données sont de deux types. Le type connexion est le plus nouveau et sa représentation n'est pas triviale. Si les paquets sont souvent représentés par des points d'un espace comportant plus ou moins de dimensions, l'évidence d'une représentation similaire n'est pas totale pour les connexions. La notion de date de début et de fin sont dans le même composant et il est tentant de visualiser ces deux points sur un même axe. La représentation naturelle d'une connexion est donc un segment.

Une connexion présente des similitudes avec une notion plus courante, la tâche dans un projet. Dans les deux cas, on a un début et une fin et un ensemble d'autres paramètres. On a aussi une notion de dépendances avec, par exemple, une connexion TCP qui dépend d'une connexion DNS pour résoudre le serveur cible de la connexion TCP. Il nous a donc paru intéressant de tenter de représenter les connexions sous forme de diagramme de Gantt.

 

nf3d-ordering

 

Vue des connexions

Les connexions sont représentées les unes en dessous des autres, la connexion ayant commencé en dernier est affichée au plus prêt et la plus ancienne dans le fond. Cette visualisation donne de bons résultats comme le montre la capture d'écran montrant une attaque brute force SSH.

 

nf3d-ssh-scan

 

Attaque brute force SSH

nf3d est construit au-dessus de python-visual et va chercher ses données dans la base PostgreSQL. Le toolkit python-visual est un formidable outil de maquettage d'interface 3D. Quelques lignes de codes suffisent à créer des objets simples et à définir les interactions avec la souris et le clavier.

nf3d réalise un lien entre une connexion journalisée et les paquets journalisés qui appartiennent à cette connexion. Il utilise pour cela une requête sur la base SQL en réalisant une jointure sur l'égalité des paramètres IPv4. Le principe est exprimé dans la requête suivante :

SELECT oob_time_sec+oob_time_usec/1000000 AS time, * FROM ulog2

 JOIN tcp on _id=_tcp_id

 JOIN ulog2_ct ON ip_saddr_str=orig_ip_saddr_str AND ip_daddr_str=orig_ip_daddr_str

    AND ip_protocol=orig_ip_protocol AND tcp_sport=orig_l4_sport AND tcp_dport=orig_l4_dport

 WHERE tcp.tcp_dport = 22 AND ulogd.oob_time_sec > $connexion_start

  AND ulogd_ct.oob_time_sec < $connexion_end

L'idée de cette requête est de constituer une jointure TCP en joignant la table ulog2 et la table tcp, puis de joindre les données obtenues avec la table des connexions ulog2_ct en utilisant une égalité des paramètres IP et TCP. En s'assurant ensuite que les paquets sont arrivés dans l'intervalle de vie de la connexion, on trouve alors l'ensemble des paquets et leur connexion d'appartenance.

La puissance des bases de données est donc mise à profit pour extraire des informations qualifiées et cohérentes sur les paquets et les connexions. La modularité du schéma composite et la journalisation des paquets et des connexions confère une grande souplesse aux requêtes possibles.

Conclusion

Ulogd2 est actuellement en phase de bêta. Il bénéficie d'une conception solide et fiable et est donc d'une stabilité suffisante pour la plupart des installations. La configuration ainsi que les options de configuration ne devraient plus beaucoup changer lors des développements à venir.

Les évolutions principales concernent la finalisation du support IPFIX et l'incorporation d'un module d'entrée « Unix socket » qui permettra d'injecter des données depuis un programme tournant sur la même machine. Une des modifications à venir est le support des filtres d'événements en espace noyau proposé par les dernières versions de libnetfilter_conntrack. Cette évolution permettra de réduire le nombre de messages à traiter en espace utilisateur et augmentera les performances globales.

Ulogd2 est donc une réécriture complète d’ulogd qui apporte de nouvelles fonctionnalités intéressantes tant par le support de IPv6 que par celui de la journalisation des connexions. La version 1.0 du projet devrait être publiée au courant de l'année et elle permettra aux utilisateurs d'envisager sereinement un remplacement de leur ulogd vieillissant par cette nouvelle version.

Références

- ulogd : http://www.netfilter.org/projects/ulogd/index.html

- Nulog : http://software.inl.fr/trac/wiki/EdenWall/NuLog

- nf3d : http://software.inl.fr/trac/wiki/nf3d

 



Article rédigé par

Par le(s) même(s) auteur(s)

Nftables, une révolution dans le pare-feu Linux

Magazine
Marque
GNU/Linux Magazine
HS n°
Numéro
76
Mois de parution
janvier 2015
Spécialité(s)
Résumé

Après 10 ans d'une domination implacable sur le monde des pare-feu open source, iptables est sur le point d'être remplacé par nftables. Les développeurs de Netfilter ont choisi de revoir leur copie et proposent un nouveau système de filtrage en rupture avec l'existant. Quelles sont leurs motivations et qu'apporte nftables par rapport à son vénérable ancêtre ?

Les derniers articles Premiums

Les derniers articles Premium

Cryptographie : débuter par la pratique grâce à picoCTF

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

L’apprentissage de la cryptographie n’est pas toujours évident lorsqu’on souhaite le faire par la pratique. Lorsque l’on débute, il existe cependant des challenges accessibles qui permettent de découvrir ce monde passionnant sans avoir de connaissances mathématiques approfondies en la matière. C’est le cas de picoCTF, qui propose une série d’épreuves en cryptographie avec une difficulté progressive et à destination des débutants !

Game & Watch : utilisons judicieusement la mémoire

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Au terme de l'article précédent [1] concernant la transformation de la console Nintendo Game & Watch en plateforme de développement, nous nous sommes heurtés à un problème : les 128 Ko de flash intégrés au microcontrôleur STM32 sont une ressource précieuse, car en quantité réduite. Mais heureusement pour nous, le STM32H7B0 dispose d'une mémoire vive de taille conséquente (~ 1,2 Mo) et se trouve être connecté à une flash externe QSPI offrant autant d'espace. Pour pouvoir développer des codes plus étoffés, nous devons apprendre à utiliser ces deux ressources.

Raspberry Pi Pico : PIO, DMA et mémoire flash

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Le microcontrôleur RP2040 équipant la Pico est une petite merveille et malgré l'absence de connectivité wifi ou Bluetooth, l'étendue des fonctionnalités intégrées reste très impressionnante. Nous avons abordé le sujet du sous-système PIO dans un précédent article [1], mais celui-ci n'était qu'une découverte de la fonctionnalité. Il est temps à présent de pousser plus loin nos expérimentations en mêlant plusieurs ressources à notre disposition : PIO, DMA et accès à la flash QSPI.

Les listes de lecture

11 article(s) - ajoutée le 01/07/2020
Clé de voûte d'une infrastructure Windows, Active Directory est l'une des cibles les plus appréciées des attaquants. Les articles regroupés dans cette liste vous permettront de découvrir l'état de la menace, les attaques et, bien sûr, les contre-mesures.
8 article(s) - ajoutée le 13/10/2020
Découvrez les méthodologies d'analyse de la sécurité des terminaux mobiles au travers d'exemples concrets sur Android et iOS.
10 article(s) - ajoutée le 13/10/2020
Vous retrouverez ici un ensemble d'articles sur les usages contemporains de la cryptographie (whitebox, courbes elliptiques, embarqué, post-quantique), qu'il s'agisse de rechercher des vulnérabilités ou simplement comprendre les fondamentaux du domaine.
Voir les 55 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous