--------------------------------------------------------------------------------------- VIII. Surveillance des binaires SUID Neofox --------------------------------------------------------------------------------------- ###################################### #### Fichier Joint : #### #### Suid-Surveyor.1.0.tar.gz #### ###################################### [ Introduction ] VOici un article à tendance résoluement 'whitehat' pour une fois, et on ne va pas s'en plaindre. Ce texte va me servir de support pour vous présenter la version 1.0 d'un outil dont je viens juste de terminer la mise au point. Je l'ai bapthisé 'Suid Surveyor', le plus simplement du monde. Il a pour fonction d'alerter l'adiministrateur lorsque de nouveaux binaires suid sont ajoutés sur le système. Je ne vais pas rapeller l'interêt d'une inspection journalière du système à la recherche de binaires SUID suspects (type setuid backdoor) ; sachez simplement que ce prog peut être utlisé avec cron dans le cadre d'une surveillance continue. L'idée m'est venue d'implémenter ceci en C arpés être tombé sur un script comparable, en trainant sous FreeBSD. Cet outil a été testé avec succès sous Redhat et Slackware. En revanche, un bug m'a été signalé sous Mandrake. Il semblerait que fonction 'nftw()' servant à parcourir l'arboresence, retourne systématiquement une erreur, ce qui rend impossible la recherche dans les répertoires. Ce qui est d'autant plus curieux, c'est que ce problème n'a lieu que sous Mandrake. Je ne vois pas comment l'expliquer... Je n'ai pas pu tester mon outil sous toutes les distrib linux. Prevenez-moi si vous constatez un bug sous votre OS. ++++ | Partie 1 : Présentation | ++++ => Description => Programmation I. Description : ________________ [ L'idée ] Le principe est tout simple ; il consiste à parcourir une première fois l'ensemble des répertoires du système et de lister tout les binaires SUID trouvés dans un premier fichier. On répète ensuie l'opréation une seconde fois, et nous obtenons donc une seconde liste dans un nouveau fichier. On compare ensuite leur deux tailles. Si elles sont identiques tout est normal. En revanche si l'on constate que la taille du second fichier est supérieure à celle du premier, cela signifie qu'il comporte au moins une nouvelle entrée, ce qui correspond à un nouveau binaire SUID repéré sur la machine. Dans ce cas, un fichier de rapport est crée par défaut dans '/root' et le nom du nouveau binaire y est inscrit. [ Les fichiers ] Suid Surveyor v1.0 s'appuie sur 3 fichiers : ¤ /var/log/suid.yesterday : précédente liste des binaires SUID ¤ /var/log/suid.today : nouvelle liste des binaires SUID ¤ /root/suid.rapport : contient les noms des nouveaux binaires Par défaut, Suid Surveyor v1.0 est exécuté une fois par jour via cron, et pour ce faire, il utilise '/etc/cron.daily/suid'. Ce programme DOIT IMPERATIVEMENT être installé dans '/root', dans le cas contraire, vous devrez modifier les PATH du Makefile. [ L'installation ] J'insiste sur le fait qu'il est important de plaçer l'archive tar.gz dans /root, puis de la décompresser. Vous devez de ce fait procéder à l'instal- -lation EN TANT QUE ROOT ! Il est également important de respecter le nom et l'emplaçement du répertoire /root/Suid-Surveyor.1.0. Si vous souhaitez les modifier, il vous faudra modifier en conséquences les PATH dans le code source et le Makefile. L'installation est tout ce qu'il y a de plus simple, grâce à ce joli Makefile que MeiK m'a écrit ( il était pas beau mon install.sh ?? ). Voici le tout en *image* : [root@localhost /root]# pwd /root [root@localhost /root]# whoami root [root@localhost /root]# ls -al /root/*.tar.gz -rwxrwxr-x 1 root root 4214 oct 30 15:15 Suid-Surveyor.1.0.tar.gz [root@localhost /root]# gzip -cd *.tar.gz | tar xvf - Suid-Surveyor.1.0/ Suid-Surveyor.1.0/ss.c Suid-Surveyor.1.0/readme Suid-Surveyor.1.0/Makefile [root@localhost /root]# cd Suid-Surveyor.1.0 [root@localhost Suid-Surveyor.1.0]#ls Makefile readme ss.c [root@localhost Suid-Surveyor.1.0]# make cc -c -g -0 ss.c cc ss.o -o ss [root@localhost Suid-Surveyor.1.0]# make install install: ok! [root@localhost Suid-Surveyor.1.0]# ls -al /etc/cron.daily/suid -rwxrwxr-x 1 root root 46 oct 30 15:15 suid [root@localhost Suid-Surveyor.1.0]# cat /etc/cron.daily/suid #!/bin/sh /root/Suid-Surveyor.1.0/ss cron & [root@localhost Suid-Surveyor.1.0]# Vous constatez à l'issue de l'installation que le fichier 'suid' a été crée dans /etc/cron.daily. C'est grâce à lui que Suid Surveyor sera exécuté une fois par jour. [ Le fonctionnement ] Deux possibilités s'offrent à vous : soit vous pouvez exécuter l'outil manuellement et vous verrez s'affichier le résultat du scan à l'écran, soit vous laissez cron s'en charger. Voici les deux syntaxes : ¤ manuellement : # ./ss show ¤ avec cron : # ./ss cron & La syntaxe utilisée avec cron est celle inscrite dans /etc/cron.daily/suid. Voici à présent une démonstration du fonctionnement de mon outil : [root@localhost Suid-Surveyor.1.0]# ./ss **** Suid Surveyor v1.0 -by Neofox[IOC] **** This tool performs a scan throught the whole file system, then looks for new suspect suid binaries Usage: 1- ./ss cron & when it's run by cron 2- ./ss show to output the results [root@localhost Suid-Surveyor.1.0]# ./ss show *]- Suid Surveyor v1.0 -by Neofox[IOC] [1]- Updating logs... failed! [2]- Jumping to / ... done! [3]- Browsing directories... done! [4]- Setting logfile permission... done! [5]- Counting suid files : 103 [6]- Checking for new suid binaries... failed! [*]- Fatal error, aborting! [root@localhost Suid-Surveyor.1.0]# Cette erreur est normale ; il s'agit enfait de la première utilisation du programme. De ce fait, les fichiers 'suid.today' et 'suid.yesterday' n'existent pas. Il y a donc échec lors de la rotation des fichier de log. Par ailleurs, le programme va créer au cours de son exécution le fichier /var/log/suid.today. Mais comme il n'y a pas de fichier antérieur avec lequel le comparer, on obtient une "Fatal Error" et le programme met fin à son exécution. Voyons ce qui se passe lors d'une exécution normale : [root@localhost Suid-Surveyor.1.0]# ./ss show [*]- Suid Surveyor v1.0 -by Neofox[IOC] [1]- Updating logs... done! [2]- Jumping to / ... done! [3]- Browsing directories... done! [4]- Setting logfile permission... done! [5]- Counting suid files : 103 [6]- Checking for new suid binaries... done! [*]- SUID files allright! [root@localhost Suid-Surveyor.1.0]# mkidr /opt/.hacker [root@localhost Suid-Surveyor.1.0]# cp /bin/sh /opt/.hacker/shell_suid [root@localhost Suid-Surveyor.1.0]# chmod 6755 /opt/.hacker/shell_suid [root@localhost Suid-Surveyor.1.0]# ./ss show [*]- Suid Surveyor v1.0 -by Neofox[IOC] [1]- Updating logs... done! [2]- Jumping to / ... done! [3]- Browsing directories... done! [4]- Setting logfile permission... done! [5]- Counting suid files : 104 [6]- Checking for new suid binaries... done! **** WARNING **** //opt/.hacker/shell_suid is new [*]- Writting /root/suid.rapport... done! [*]- 1 new SUID binaries has been found since last check! [root@localhost Suid-Surveyor.1.0]# ls -al /root/suid.rapport -rw-r--r-- 1 root root 24 oct 30 15:20 suid.rapport [root@localhost Suid-Surveyor.1.0]# cat /root/suid/rapport //opt/.hacker/shell_suid [root@localhost Suid-Surveyor.1.0]# rm -f /opt/.hacker/* [root@localhost Suid-Surveyor.1.0]# ./ss show [*]- Suid Surveyor v1.0 -by Neofox[IOC] [1]- Updating logs... done! [2]- Jumping to / ... done! [3]- Browsing directories... done! [4]- Setting logfile permission... done! [5]- Counting suid files : 103 [6]- Checking for new suid binaries... done! [*]- SUID binaries removed since last check! [root@localhost Suid-Surveyor.1.0]# Prennez le temps de lire attentivement la démonstration cî-dessus, vous conviendrez que c'est on ne peut plus parlant ! Bien, il me reste manitenant à détailler un peu le code ss.c, et à faire la lumière sur quelques zones d'ombre. II. Programmation : ___________________ Je vais expliquer mon code petit à petit, mais uniquement les points qui m'on posé problème pendant la programmation. [ nftw() ] La première étape est de pouvoir naviguer depuis la racine, dans tous les répertoires et les sous-répertoires. Si on ne connait pas la fonction, on ne peut pas l'inventer ! Un gros pavé sur le C m'a tiré d'affaire. La fonction qui va nous dépanner est 'ftw()' qui siginfie "File Tree Walking" ou plus exactement nous allons utiliser sa cousine, nftw(), plus complète. Je tiens à vous prévenir desuite, cette fonction a une syntaxe à coucher dehors, dumoins à première vue ... elle est définie dans l'header 'ftw.h'. Je vous renvoie aux pages du man pour plus d'information. /* * nftw("/your/path"), // browse all dirs * scan_suid, // find suid binaries * 10, // depth * 1 // FTW_PHYS = 1 * ); * */ On va faire le point sur les arguments, histoire d'y voir plus clair : Le premier argument est le répertoire de départ. La fonction va partir de ce répertoire et parcourir en parcourir tous les sous-répertoires. Dans notre cas, nous devons partir de la racine. Le second argument est une sous fonction qui sera appliquée à chaque répertoire. J'ai crée pourcela la fonction 'scan_suid()' que j'aurais tout aussi bien pu apeller 'toto_par_a_la_campagne()', j'ai hésité, mais la première allait mieux. Notre sous fonction va pour chaque répertoire, l'ouvrir, le parcourir, lister les fichiers suid puis le refermer. Le troisième argument est la profondeur de recherche, c'est à dire le nombre maximum d'étages de l'arboresence à parcourir. J'ai mis 10, c'est déja pas mal, mettez 100 si ça vous chante. Enfin, le dernier argument est là pour faire 3ll3t, mais dans la pratique, il spécifie à nftw() de ne pas suivre les liens symboliques. Voici comment j'ai géré cette fonction dans mon code : -------8<---------------------------------------------------------------- /* * Here we go, let's have a look in our dirs ! */ fprintf(stderr,"\033[1;37m[%d]\033[0m- Browsing directories... ",step++); if((nftw((getcwd(path, 256)),scan_suid,10,1))==-1){ fprintf(stdout,"failed!\n"); fprintf(stdout,"\033[1;37m[*]\033[0m- Fatal error, aborting!\n"); exit(1); } -------8<---------------------------------------------------------------- [ strtok() ] Cette fonction définie dans 'string.h' n'intervient que beaucoup plus tard dans le prog, mais je la traite maintenant, non seulement parcequ'il me faut bien remplir mon article déja que j'en ai pas fait lourd, mais aussi parce que c'est le second point sur lequel j'ai bloqué. A ce stade du programme, nous nous trouvons dans la sous-fonction apellée 'find_new_entry()'. Nos deux fichiers de log, suid.yesterday et suid.today sont normalement crées. On a lu le contenu du premier et on la plaçé dans le buffer 'oldfile_buffer'. On a lu ensuite le conetnu du second fichier et on l'a plaçé dans 'newfile_buffer'. Le problèmes est que nous devons savoir si toutes les entrées de 'newfile_buffer' figurent ou non dans 'oldfile_buffer'. Pour cela, nous devons rechercher chaque entrée de 'newfile_buffer' dans 'oldfile_buffer' à l'aide de la fonction strstr(). Dans la réalité, vous l'aurez compris, une entrée correspond au nom d'un binaire suid. C'est ici qu'intervient strtok(). Cette fonction va nous permettre d'isoler chaque entrée du buffer 'newfile_buffer' et d'aller ensuite la compier dans une structure de notre conception. A l'aide de strstr() on peut ensuite rechercher séparemment chaque ligne du 'newfile_buffer' dans 'oldfile_buffer'. Si une ligne est présente dans le 'newfile_buffer' mais pas dans l'autre, c'est qu'il s'agit d'une nouvelle entrée, comprendre d'un nouveau binaire suid. Voici à présent l'extrait du code : -------8<---------------------------------------------------------------- while((read(newfile_input, newfile_buffer, MAX))>0){ // this is used to order datas, // one entry per line ptr = strtok(newfile_buffer,"\n"); while(ptr!=NULL){ strcpy(entry[line].name,ptr); line--; ptr = strtok(NULL,"\n"); } } // une fois le buffer segmenté, on peut rechercher séparrement // chaque entrée dans 'oldfile_buffer' r=0; found = 0; while((read(oldfile_input, oldfile_buffer, MAX))>0){ for(line=0;line /* frpintf */ #include /* strcmp, strtok */ #include /* hum ? */ #include /* is it useful ? */ #include /* stat, opendir */ #include /* stat */ #include /* open, close */ #include /* opendir, closedir */ #include /* perror */ #include /* nftw */ #include /* getpwuid */ /* Well known logfiles */ #define TODAY "/var/log/suid.today" #define YESTERDAY "/var/log/suid.yesterday" #define RAPPORT "/root/suid.rapport" #define SUID_MODE 0004000 // S_ISUID #define MAX 9999 // a big value FILE *output; int i, count; // main counters char buffer[500]; // well used buf struct stat file; /* current file status */ struct stat status; /* logfile status */ struct passwd *pwd; /* basic stuff */ void display_msg(){ fprintf(stdout,"\033[1;37m[*]- Suid Surveyor v1.0 -by Neofox[IOC]\n\033[0m"); } void display_help(char **argument){ fprintf(stdout,"\n\033[1;37m**** Suid Surveyor v1.0 -by Neofox[IOC] ****\033[0m\n" "This tool performs a scan throught the whole file\n" "system, then looks for new suspect suid binaries\n" "Usage: 1- %s cron & when it's run by cron\n" " 2- %s show to output the results\n\n", argument[0],argument[0]); exit(1); } /* * This is the easyest way to update logs, * the oldest logfile beeing replaced by * the newest one */ int logrotate(void){ if((stat(TODAY,&status))!=-1){ if((rename(TODAY,YESTERDAY))!=0) return -1; else return 0; } } /* End of Logrotate */ /* * This function will be used to save datas * in both the old and the new logfile, and * also to save results in an other */ int save_data(const char *logfile, const char *buffer, FILE * file_descriptor){ if((output=(fopen(logfile,"a+")))!=NULL){ count ++; /* writting into the logfile */ fprintf(output,"%s\n",buffer); } fclose(output); return 0; } /* * Here is the subfunction applied by nftw() * It lists suid binaries, then save the list * in the today logfile */ int scan_suid(const char *path, const struct stat *status, int flag){ DIR *dirp = 0; struct dirent *dp; flag = FTW_F; if ((dirp = opendir(path))!=NULL) { while((dp=readdir(dirp))!=0){ // this buf will store complete paths sprintf(buffer,"%s/%s",path,dp->d_name); if((stat(buffer,&file))!=-1){ if((file.st_mode&S_ISUID)){ if((save_data(TODAY,buffer,output))!=0) return -1; } } errno = 0; } if(errno!=0) return -1; if(closedir(dirp)==-1) return -1; } /* continue */ return 0; } /* * The compare() function is used to know if * the new logfile is the same than the old one * or not. If it's not, an unusual event may * happend, so we'll look further for a new entry. */ int compare(void){ int diff; off_t yesterday_size, today_size; if((stat(TODAY,&status))==-1){ return -2; goto end; } else today_size = status.st_size; if((stat(YESTERDAY,&status))==-1){ return -2; goto end; } else yesterday_size = status.st_size; // if sizes don't match if(today_size != yesterday_size){ // if the difference is positive */ if(today_size > yesterday_size){ // suid.today has grown bigger diff = today_size - yesterday_size; return 1; } else // if the difference is negative, // they are missing bytes! return -1; /* Otherwise, it's normal */ } else return 0; end: } /* * It's one of the most important function which will search * into the new logfile for a filename which is not written in * the old logfile. If there's one, it means that a new suid * binary has been added since the previous check */ int find_new_entry(const char *oldfile, const char *newfile, int nbr){ struct find { char name[500]; } entry[nbr]; FILE *rapport; int r, line, found; // counters int oldfile_input, newfile_input; char oldfile_buffer[MAX],newfile_buffer[MAX]; char *ptr; // used with strtok() // opening today logfile if((newfile_input=open(newfile,O_RDONLY))==-1){ perror("fopen newfile"); close(newfile_input); return -1; goto end; } // opening yesterday logfile if((oldfile_input=open(oldfile,O_RDONLY))==-1){ perror("fopen oldfile"); close(oldfile_input); return -1; goto end; } line = nbr; while((read(newfile_input, newfile_buffer, MAX))>0){ // this is used to order datas, // one entry per line ptr = strtok(newfile_buffer,"\n"); while(ptr!=NULL){ strcpy(entry[line].name,ptr); line--; ptr = strtok(NULL,"\n"); } } r=0; found = 0; while((read(oldfile_input, oldfile_buffer, MAX))>0){ for(line=0;linepw_name); exit(1); } /* Arguments check */ if(argc!=2) display_help(argv); if((strcmp(argv[1],"cron"))==0){ // if it's run directly by cron, // we close standart and errors outputs close(1); close(2); } else { if((strcmp(argv[1],"show"))==0) display_msg(); else display_help(argv); } step=1; // reset step counter fprintf(stderr,"\033[1;37m[%d]\033[0m- Updating logs... ",step++); if((logrotate())!=0) fprintf(stdout,"failed!\n"); else fprintf(stdout,"done!\n"); /* * Here, we need to chdir to "/" before launching the scan, * otherwise nftw() would only browse the current directory */ fprintf(stderr,"\033[1;37m[%d]\033[0m- Jumping to / ... ",step++); if(chdir("/")!=-1) fprintf(stdout,"done!\n"); else { fprintf(stdout,"failed!\n"); fprintf(stdout,"\033[1;37m[*]-\033[0m Fatal error, aborting!\n"); exit(1); } /* * Here we go, let's have a look in our dirs ! */ fprintf(stderr,"\033[1;37m[%d]\033[0m- Browsing directories... ",step++); if((nftw((getcwd(path, 256)),scan_suid,10,1))==-1){ fprintf(stdout,"failed!\n"); fprintf(stdout,"\033[1;37m[*]\033[0m- Fatal error, aborting!\n"); exit(1); } else { fprintf(stdout,"done!\n"); fprintf(stderr,"\033[1;37m[%d]\033[0m- Setting logfile permission... ",step++); if((chmod(TODAY,0600))==-1) fprintf(stdout,"failed!\n"); else fprintf(stdout,"done!\n"); } fprintf(stderr,"\033[1;37m[%d]\033[0m- Counting suid files: %d\n",step++,count); fprintf(stderr,"\033[1;37m[%d]\033[0m- Checking for new suid... ",step++); if((alert=(compare()))==0){ /* Everything is OK */ fprintf(stderr,"done!\n"); fprintf(stdout,"\033[1;37m[*]\033[0m- SUID binaries allright !\n"); exit(1); } else { /* an error occurs with compare() */ if(alert==-2){ fprintf(stdout,"failed!\n"); fprintf(stdout,"\033[1;37m[*]\033[0m- Fatal error, aborting!\n"); exit(1); } /* the size has become smaller */ if(alert==-1){ fprintf(stdout,"done!\n"); fprintf(stdout,"\033[1;37m[*]\033[0m- SUID binaries removed since last check!\n"); } /* the size has grown bigger */ if(alert>0){ fprintf(stdout,"done!\n"); fprintf(stdout,"\n\a \033[1;31m**** WARNING ****\033[0m\n"); if((new=(find_new_entry(YESTERDAY,TODAY,count)))==-1){ fprintf(stdout,"erreur find"); exit(1); } fprintf(stdout,"\033[1;37m[*]-\033[0m %d SUID binaries added since last check!\n", new); } } return 0; } --------------8<------------ Makefile ------------------------------------------------------- CC = cc LD = cc CFLAGS = -g -O LDFLAGS = .SUFFIXES: .o .c .c.o: $(CC) -c $(CFLAGS) $< all: ss ss: ss.o $(LD) ss.o -o ss $(LDFLAGS) install: @echo "#!/bin/sh" > /etc/cron.daily/suid @echo "/root/Suid-Surveyor.1.0/ss cron &" >> /etc/cron.daily/suid @chmod +x /etc/cron.daily/suid @echo "install: ok!" clean: @rm -f *.o --------------8<-------------------------------------------------------------------------------- [ Conclusion ] N'hésitez pas à m'écrire pour me signaler un bug, me faire part d'une amélioration, ou pour tout autre commentaire. Vous pouver trouver cet outil en ligne ici : http://www.rootshell.be/~ioc/progs/secuirty/. Je voudrais remercier MeiK pour le Makefile ainsi que, Marcel, Jamu et Blashow pour m'avoir aidé à tester ce prog. Je profite de cette conlcusion pour vous conseiller un article intéressant: http://www.c2i2.com/~dentonj/system-hardening