----------------------------------------------------------------------------------------- VI. Log Cleaners : Concepts et Programation Neofox ----------------------------------------------------------------------------------------- [ Introduction ] Dans ce article, nous allons aborder le fonctionnement d'un log cleaner classic et expliquer les différentes notions utiles à sa réalisation ; on verra ensuite des méthodes de nettoyage plus preformantes et les notions de C sur lesquelles elles s'appuient. ++++ | Partie I : Fonctionnement standard | ++++ => Concept de base => Les fonctions C => Illustration => Critiques [ Avertissement ] Je ne vais pas revenir ici sur le rôle de chaque fichier de log. En effet, d'une part le rôle de utmp/wtmp/lastlog est bien connu de tous, d'autre part, le nom, le rôle et la position des autres fichiers de log, varient en fonction de l'OS. Je ne vais pas non plus en rapeller le fonctionnement vu la simplicité de la chose. On passe donc directement à la suite. A. Le Concept : ________________ [ Comment un cleaner efface vos traces ] Nous allons nous concentrer sur le nettoyage de utmp, wtmp et lastlog. Vous vous connectez à une machine X, au compte "toto", depuis "source.com". Une entrée est donc ajoutée aux fichiers utmp qui a loggé votre présence, à wtmp qui enregistre les détails de la connection, et à lastlog. Alors, comment est-ce qu'un log cleaner efface vos traces ? Eh bien, en fait, tout dépend du log cleaner comme nous le verrons plustard, mais en règle générale, le prog commençe par ouvrir le un fichier, disons, utmp, puis il prend la première entrée et la plaçe dans une structure prédéfinie. Il compare alors le champ "name" de cette structure ; si ce champ ne correspond pas au nom d'utilisateur que vous avez entré en argument, c'est que l'entrée n'est pas celle que vous voulez effaçer. Dans ce cas, le prog vide la structure puis la remplit avec la seconde entrée du fichier. Il compare à nouveau le champ "user" et ainsi de suite. Si en revanche le champ "user" correspond, alors le prog en déduit que c'est l'entrée que vous voulez effaçer. Il bourre alors la structure avec des "0". Il crée ensuite un décalage en arrière dans le fichier wtmp en cours de lecture, de manière à se retrouver pile au début de l'entrée à effaçer. Il imprime ensuite le contenu de la structure ( des 00000 ) par dessus l'ancienne entrée révélant votre présence. Puis il referme le fichier. L'entrée que vous vouliez effaçer a donc été écrasée par une suite de "0", et le fichier en question ne contient plus traçe de votre présence. On récapitule pour que tout soit clair : ¤ ouverture du fichier en lecture/écriture. ¤ la premiere entrée est plaçée dans une structure. ¤ comparaison des champs de la structure avec les arguments. ¤ la structure est remplie de 0 si l'entrée est la bonne. ¤ décalage vers l'arrière dans le fichier ouvert. ¤ ecriture dans le fichier par dessus l'entrée originale. ¤ fermeture du fichier. [ Les commandes who et last ] Comme wtmp ne peuvent pas être lus directement, vous devez avoir recours à la commande "last" pour afficher la liste des dernières connections. Le mode de fonctionnement est le même pour les deux autres fichiers. La commande "who" ou "w" permet d'afficher le contenu de utmp ; de la même manière que pour wtmp, la suite de 0 recouvrant vos traces n'est pas prise en compte par "who" lors de l'affichage des entrées. Notez que dans le cas de wtmp, nous ne voulons pas effaçer toutes les connections de l'utilisateur "toto" mais seulement la dernière, celle correspondant à notre connection à ce compte. C'est donc uniquement la dernière entrée pour cet user qui devra être effaçée de wtmp. Nous allons voir les fonctions de C mises en oeuvre pour effectuer le "nettoyage". B. Programmation : __________________ Ouvrir un fichier en lecture/écriture, vous savez faire - avec la fonction open() et le flag O_RDWR - donc on ne vas pas s'y attarder plus longtemps. D'autres fonctions en revenche méritent quelques explications. [ Les structures ] Il y a deux types de structures à connaître pour comprendre la suite ; il s'agit de "utmp" et "lastlog". La première structure est utilisée à la fois pour traiter les entrées de utmp et celles de wtmp ; en effet, les champs de cette structure sont adaptés aussi bien pour les entrées du premier fichier que pour celles du second. La structure de type "lastlog" quant à elle, ben rien de spécial, sinon qu'elle sert à reçevoir les entrées de /var/log/lastlog. Voici un exemple simplifée de ces structures : struct utmp { char *ut_user; char *ut_host; char *ut_time; } struct lastlog { char *ll_host; } Les champs ut_user contiendront le nom d'user de la personne connectée, les champs ut_host/ll_host, le nom d'hôte de la machine cliente et ut_time, la date et l'heure de la connection. [ bzero() ] Le rôle de la fontction bzero() est de remplir un bloc d'octets de 0. Voici sa syntaxe : #include void bzero (void *pointeur, int n); bzero met a zero les n premiers octets pointés "pointeur". Le pointeur peut trés bien pointer vers une structure de type utmp par exemple, ce qui donnerait : int size; struct utmp u; size = sizeof(u); bzero (&u, size); Il y a cela dit une autre fonction capable d'assurer le même rôle : memeset(); [ memset() ] Comme bzero(), memset() peut remplir une structure de 0, mais aussi d'autres données de type int. Sa syntaxe : #include memset (void *s, int c, int n); Ici, memset() remplit les n premiers octets pointés par s, avec l'octet "n". De la même manière, on peut lui faire remplir une structure de "0". Prenons l'exemple d'une structure lastlog : int size; struct lastlog last; size = sizeof(last); memset(&last, 0, size); L'effet est le même que celui obtenu à l'aide de bzero(). [ lseek() ] La fonction read() vient de lire la première entrée d'un de nos trois fichiers de log, et l'a plaçée dans la structure appropriée. bzero() a rempli cette structure de 0. Nous voullons copier la suite de 0 par dessus l'ancienne entrée qui vient d'être lue. Il faut donc "rembobiner", reculer la "tête de lecure/écriture" au début de l'entrée conçernée. Voici la syntaxe de lseek() : #include #include lseek (int fd, int n, int flag); Ici, "fd" désigne le descripteur de fichier pointant sur le fichier ouvert en lecture/écriture ; "n" est la taille en octets du décalage que l'on souhaite créer ; enfin, "flag" désigne l'action à effectuer. Plusieurs types d'acction sont possibles : ¤ SEEK_SET => Place la tête de lecture/écriture à n octets à partir du début du fichier. ¤ SEEK_CUR => Avance la tête de lecture/écriture de n octets. ¤ SEEK_END => Place tête de lecture/écriture à la fin du fichier + n octets. Ces flags sont venus remplaçer les 3 anciens flags que voici : ¤ L_SET => Donne "n" comme la nouvelle position de la tête dans le fichier ( = SEEK_SET ). ¤ L_INCR => Incrémente la position courrante de n octets ( = SEEK_CUR ). ¤ L_XTND => Déplace la tête de n octets à partir de la fin du fichier, et peut de cette façon en augmenter la taille ( = SEEK_END ). Cela dit, nous utiliserons les nouveau flags ; nous n'en utiliseront même qu'un seul : SEEK_CUR. Ce dernier fait avançer la tête de lecutre/écriture de n octets. Seulement, comme nous l'avons précisé, nous voulons reculer dans le fichier. On va donc déplaçer la tête avec le flag SEEK_CUR de -n octets. [ write() ] Nous venons de reculer dans le fichier ouvert et la tête de lecture/écriture se trouve maintenant au début de l'entrée à effaçer. La structure dans laquelle a été plaçé l'entrée est remplie de 0. Nous voulons remplaçer l'entrée du fichier par la suite de 0, ce qui revient à écrire le contenu de la structure dans le fichier, par dessus l'entrée. On va utiliser write() ; sa syntaxe est la suivante : #include write (int fd, char *buf, size); "fd" est le descripteur de fichier et "buf" le buffer contenant les données de taille "size" à écrire en direction de fd. Dans notre cas, les données à écrire sont contenues dans la structure, ce qui va donner : int fd; struct utmp u; size = sizeof(u); write (fd, &u, size); Voila, l'entrée correspondant à votre connection est mainenant remplaçée par une suite de 0. Nous allons voir avec une petite illustration l'action respective de chaque commande. [ strcmp() et strncmp() ] La fonction strcmp() ( = "string compare" ) sert à comparer deux chaines de type char. Si ces 2 chaines sont égales, elle retourne 0. La fonction strncmp() quant à elle, a le même rôle si ce n'est qu'elle compare les "n" premiers octets de ces deux chaines. Certains cleaners utilisent la seconde, nous verrons plustard pourquoi. Ces fonctions sont simples et trés courantes, aussi les avez vous surement déja utilisées. Dans un souci d'exhaustivité, voici la syntaxe de strcmp() : #include strcmp(chaine1, chaine2); En cas d'erreur, cette fonction retourne -1 et si les chaines ne correspondent pas, c'est une valeur < 0 qui est renvoyée. On va donc gérer l'erreur : if(strcmp(chaine1, chaine2)!=0){ fprintf(stderr,"Les chaines ne sont pas égales !\n"); exit(1); } Les cleaners se servent de cette fonction pour comparer le champ ut_name de la structure utmp, avec le nom d'utilisateur dont vous voulez effaçer les traçes. Le nom en question est généralement entré en arguement : struct utmp u; if(strcpy(u.ut_name,argv[1])==0){ fprintf(stdout,"L'entrée lue dans la structure utmp est la bonne\n"); fprintf(stdout,"On l'effaçe!\n"); } C. Illustration : _________________ | v = la tête de lecture/écriture. A = l'entrée à effaçer. B = l'entrée suivante. | position de la tête au départ, structure vide. v Ouverture du fichier : AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBB | la structure contient AAA... La première entrée est lue <_____lecture______>v et plaçée dans la strucutre : AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBB | la structure contient maintenant 000... bzero remplit la structure V avec des 000000 : AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBB | la tête est en position et la strucure contient toujours 000... On recule la tête v avec lseek() : AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBB write() écrit le contenu | de la structure à la plaçe v de l'ancienne entrée : 00000000000000000000BBBBBBBBBBBBBBBBBBBBBBBB J'espère que vous avez à présent une idée claire de la manière dont un log cleaner standard gère les fichiers utmp/wtmp/lastlog. Mais nous allons voir, que cette méthode est perfectible. D. Critiques : ______________ [Zap2 zappé ] La méthode dont je viens de vous parler est celle de Zap2. On ouvre le fichier, on met la première entrée dans une structure, on cherche la bonne entrée, on bourre la structure de 0, on se décale, on copie le contenu de la structure, et on ferme le fichier. Simple, efficace, mais détectable ... En effet, si un vérificateur d'intégrité tourne sur la machine, celui-ci peut se baser sur la présence suspecte d'une chaine de zéros dans un fichier de log pour alerter de la présence d'un visisteur indésirable. Zap2 se fait vieux, d'autres ont pris la relève. - | Partie II : Nouvelle Méthode | - => Fonctionnement => Programmation A. Fonctionnement : ___________________ [ La relève ] Ils s'apellent vanish, cloack, ou hideme et sont plus performant que Zap2 sur deux points : d'une part, car ils traitent plus de fichiers ( prise en charge de utmpx/wtmpx, messages, maillog, secure, xferlog ) et d'autre part, car ils utilisent une méthode de nettoyage plus discrète comme nous allons le voir. Je voudrais préciser un détail avant d'aller plus loin : le fait que ces cleaners plus récents nettoyent plus de fichiers, est à la fois un point positif et négatif ; posifit car vos traces seront effaçées plus en profondeur, mais négatif puisque cela vous cantonne à un OS bien précis. Ils prennent en charge maillog et secure, super, c'est parfait pour linux ... mais si on se trouve sous IRIX, ça nous sert pas à grand'chose, vu que sous cet OS, il n'y a ni de fichier "secure" ni de "messages" d'ailleurs, les rôles de ces fichiers étant assurés par "SYSLOG". Si vous voulez utiliser un de ces cleaners sous un OS différent de celui pour lequel ils ont été pensés, vos traçes ne seront effaçées que de utmp/wtmp/lastlog, ce qui n'est déja pas si mal me direz vous ; cela reviendrait au même que d'utiliser Zap2, la méthode de nettoyage mise à part. Ce que j'essaye de vous dire avec plus ou moins de succès, c'est qu'il vaut mieux utiliser un log cleaner qui se limite à utmp/wtmp/ lastlog, lorsque vous êtes sur un système "exotique" pour lequel il n'y a pas de cleaner adapté, et nettoyer manuellement les autres fichiers spécifiques à cet OS. Wipeout est à mon avis un bon compromis entre polyvalence et qualité de nettoyage. [ L'idée ] Nous voulons que les fichiers de log ne contienne pas traçe de notre connection. En somme, aucune entrée ne nous derrange dans ces fichies à part la notre. Deux solutions : soit on supprime l'entrée en l'écrasant, soit on garde toutes les entrées sauf celle-ci. L'idée développée par les autres cleaners est donc de copier dans un fichier temporaire toutes les entrées d'un fichier de log mis à part là notre. Ce fichier temporaire ressemblera donc en tout point au vrai mais notre entrée n'y figurera pas. Il ne restera plus qu'à copier ce nouveau fichier par dessus l'original. [ En pratique ] Voici comment se déroule une session de "nettoyage" d'un fichier, disons "utmp" : Le prog. ouvre le fichier utmp en lecture ; il crée un fichier temporaire et l'ouvre en écriture. Il lit les entrée une par une depuis utmp. Il lit donc la première entrée et la plaçe dans une structure de type utmp. Il compare le champ ut_name ; s'il correspond au nom d'user que vous avez entré en argument, il compare alors le champ ut_host ; si ce champ correspond à l'hote que vous voulez effaçer, le prog sait qu'il est en présence de la bonne entrée ; dans ce cas, il ne la copie pas dans le fichier temporaire. En effet, seront sauvegardées dans le fichier temporaire toutes les entrées sauf la nôtre. Les deux fichiers sont ensuite refermés, puis le temporaire vient écraser l'original, l'entrée ciblée a donc disparu. En résumé : ¤ Ouverture de utmp en lecture. ¤ Création d'un fichier temporaire ouvert en écriture. ¤ Lectures des entrées depuis utmp dans une structure de type utmp. ¤ Comparaison du champ ut_name avec argv[1]. ¤ Compariason du champ ut_host avec argv[2]. ¤ Ecriture dans le fichier temporaire des entrées qui ne correspondent pas. ¤ Fermeture du fichier utmp et du temporaire. ¤ Remplaçement du fichier utmp par sa copie. Nous allons voir comment écrire cela. B. Programmation : __________________ On va aller beaucoup plus vite que dans la pratie 1/B, vu toutes les fontions nécessaires ici ont déja été étudiées plus haut. Nous allons voir ce qui se passe dans le cas du fichier utmp : On commençe par ouvrir le fichier de log tout en gérant l'erreur : size=sizeof(u); if((input=open(UTMP, O_RDONLY))<0){ fprintf(stderr,"Error during processing %s!/n",UTMP); close(input); exit(-1); } C'est ensuite au tour de la copie : if((output=creat(NEWUTMP, O_CREAT | O_WRONLY))<0){ fprintf(stderr,"Error during processing %s!/n",UTMP); close(input); exit(-1); } Nos deux fichiers sont donc ouverts, l'un en lecture, l'autre en écriture, sur deux descripteurs différents, respectivement input et output. On va donc lire les entrées depuis utmp, sur le descripteur d'entrée, et les plaçer dans la structure de type utmp, définie au préalable ; comme plus haut, on va utiliser read : while((read(input, &u, size))>0){ Voila, on compare donc la première entrée avec le nom d'utilisateur que vous voulez effaçer et avec le nom d'hôte : if(strncmp(u.ut_host,host,strlen(host))==0){ if(strncmp(u.ut_name,user,strlen(user))==0){ } } On referme directe les if() et on ne fait rien, car si les noms concordent, c'est qu'on tient l'entrée à effaçer ; comme on ne veut pas la garder, on ne la copie pas dans le fichier temporarie, cela va de soit. Sinon, c'est une entrée qui ne nous derrange pas, donc on la copie en direction du descripteur de sortie : else{ write(output,&u,size); } } } close(input); close(output); On referme les la boucle while() et les if() puis les descripteurs ; Il nous reste à écraser l'ancien fichier utmp avec le nouveau : unlink(UTMP); link(NEWUTMP,UTMP); chmod(UTMP,00644); unlink(NEWUTMP); Nos traçes sont à présent effaçes de utmp, sans avoir eus recours à bzero. Vous voyez que c'est une méthode assez simple. Notez que c'est l'équivalent en C de la commande "grep -v chaine old >> new" qui copie dans "new" toutes les entrées de "old" ne contenant pas à "chaine". Conclusion : ____________ A l'issue de cette article, vous savez désormais programmer un cleaner utilisant l'une ou l'autre méthode de nettoyage. Si toutefois vous avez des questions, des critiques ou des suggestions à me faire parvenir au sujet de ce texte, vous savez où m'écrire.