Journal Un autre type de faille locale

Posté par  .
Étiquettes :
44
19
fév.
2010
En 2007, le développeur de grsecurity, Brad Spengler, a dévoilé un nouveau type de brèche dans les systèmes Linux. À cette époque, la mise en avant de cette faille n'a pas fait grand bruit car comme pour la découvrir, il aurait fallu déployer de trésors d'inventivité pour pouvoir l'exploiter. Il ne s'est néanmoins pas découragé et a posté le 16 juillet 2009 un autre exploit du même genre.

Le problème qu'il a soulevé a été assez vite corrigé, mais cette brèche a permis à de nombreuses personnes de rechercher un nouveau type de failles. Elles se succèdent depuis à un rythme soutenu, certaines permettant de contourner les systèmes de protection supplémentaire comme SE Linux ou un paramétrage censé interdire ce type de faille. La dernière en date a fait le tour des médias, car elle affecte toutes les versions du noyau depuis la version 2.4.4, sortie en 2001.

Le développeur C est habitué à vivre dans un univers dangereux et à gérer un certain nombre d'éléments à la main : la mémoire, les entrées/sorties et les chaînes de caractères sont les plus fréquents. Il utilise pour gérer tout cela des pointeurs vers la mémoire virtuelle de la machine, la RAM. Dans la jungle de l'informatique bas-niveau, l'une des rares vérités sur lequel pouvait s'appuyer un développeur a disparu. On pouvait normalement s'attendre à ce que l'utilisation de l'adresse NULL (de valeur 0) provoque une exception du processeur puis un crash logiciel (ou un écran bleu devenu célèbre depuis 1995).

En fait, sous Linux, il est possible de charger en mémoire en tant que simple utilisateur des instructions à cette adresse 0 (NULL) via l'appel système mmap. De ce fait, le déférencement d'un pointeur initialisé à NULL ne provoque pas d'exception de la part de la MMU, puisque l'adresse 0 (NULL) est une adresse valide (suite à l'utilisation de mmap). Ainsi, il est possible, en détournant une variable du noyau Linux initialisée à NULL, d'exécuter du code, précédemment installé par un utilisation via l'utilisation de mmap, en Ring 0 - les droits noyau.

Shéma explicatif


Et c'est là où se situe le nœud du problème : NULL, étant considéré par le développeur C comme une adresse non valide, est utilisé comme élément neutre pour les pointeurs : initialisation les pointeurs, retour d'erreur de certaines fonctions (notamment malloc). Le compilateur GCC fait d'ailleurs de même, pour initialiser certaines variables statiques qui sont stockées dans le segment BSS.

Les pointeurs se voient assignés allègrement la valeur NULL, et les cas où la présence de cette valeur n'est pas vérifiée avant un déférencement sont nombreux. Les lignes de code du noyau sont trop nombreuses pour pouvoir raisonnablement espérer que les développeurs du noyau les corrigent toutes à travers les correctifs de sécurités.

Une défense qui semblait efficace consistait à modifier le paramètre situé dans /proc/sys/vm/mmap_min_addr. Ce paramètre permet de spécifier l'adresse minimale utilisable par les programmes. En indiquant un nombre supérieur à zéro, toutes les failles potentielles sont corrigées, sauf si la machine utilise un système de protection supplémentaire, tel que SELinux, édité par Red Hat. Un défaut dans son système de règle (policy) rendait ce paramètre inefficient et le système vulnérable.

Ce défaut a été corrigé sur Fedora mais n'a pas été corrigé via des paquets sur les RHEL 4 ou 5, afin de maintenir la compatibilité descendante. Ainsi, lwn.net a noté que des machines se sont déjà fait rooter par cet exploit, dont le code source est disponible et facilement utilisable. Red Hat a ouvert une page sur le sujet, décrivant les commandes nécessaires pour rendre l'attaque inopérante sur ses systèmes d'exploitation.

Néanmoins, vu le potentiel derrière cette faille, il est probable que d'autres failles de la même catégorie soient dévoilées de temps à autres.
  • # Analyseur statique...

    Posté par  . Évalué à 2.

    pour un problème complexe (enfin je trouve ^^) : merci pour ces précisions, j'ai suivi l'affaire depuis son apparition sur les blogs/forums, mais ce journal est particulièrement bien écrit.

    Merci !

    Je me pose une question : il me semble qu'écrire un analyseur de code statique réalisant deux étapes :
    1) Le graphe des liens entre fonctions (un arbre - quoique très grand, il serait probablement possible de scinder la recherche en sous-parties)
    2) Analyser les sections ayant une initialisation des pointeurs à 0, et vérifier une déférence suivante (via le parcours de l'arbre construit avant)

    J'imagine bien que si cela n'a pas été réalisé, c'est qu'il y a une très bonne raison (genre ma proposition ne tiens pas pour telle ou telle raison que je n'ai pas vue)

    Quelqu'un pour éclairer ma lanterne à ce propos ?
    • [^] # Re: Analyseur statique...

      Posté par  . Évalué à 3.

      Ça suffit lorsque tu sait exactement de A à Z le déroulement de ton programme. Dès qu'il prend une entrée, ça tombe à l'eau. Surtout pour un noyau, vu qu'il peut se prendre des interruptions à n'importe quel moment dans l'exécution de son code, en fonction du matériel et des logiciels utilisés.

      Et la théorie indique qu'il n'existe pas d'algorithme permettant de vérifier dans tout les cas une propriété dynamique d'un programme qui attend une entrée. c'est un théorème mathématique qui le dit. Dans la pratique, on doit se contenter d'approximations, de réponses du type "je sais pas", ou par des revue de code à la main ;)

      • [^] # Re: Analyseur statique...

        Posté par  . Évalué à 2.

        Effectivement je n'avais pas pensé aux interruptions etc... Cependant, les failles trouvées dépendent-elles de ces interruptions, ou auraient-elles pu être trouvées via un analyseur de code statique ?
    • [^] # Re: Analyseur statique...

      Posté par  . Évalué à 8.

      En fait si, ça existe déjà.
      Des analyseurs statiques comme coverity avaient déjà détecté de tels bugs (déréférencement pointeur NULL), mais pas tous.
      Ensuite, dans le cas évoqué, l'auteur du journal a oublié de préciser une chose. L'exploit utilisait un comportement de gcc très particulier : en analysant le flot du code, gcc voyait qu'on effectuait un check sur la nullité du pointeur _après_ l'avoir déréférencé (mais ce déréférencement n'était pas exploitable), et du coup supprimait (optimisait) la vérification du pointeur null, se disant qu'il ne pouvait pas l'être parce que s'il était null, on n'aurait pas pu le déréférencer (ou on aurait crashé). Du coup, l'éxécution se poursuivait alors que le code n'aurait _jamais_ dû être exécuté.
      Brad a fait tout un foin de cette faille, mais ce n'est en rien une "nouveau type de faille". C'est juste un déréférencement du pointeur non initialisé, point barre.
  • # Précisions

    Posté par  . Évalué à 4.

    Les failles de type NULL pointer dereference sont connues depuis bien avant 2006.
    Brad Spengler n'a par ailleurs pas découvert ni exploité les failles lui-même, il a principalement repris des exploits développés par d'autres gens pour rajouter ses charges utiles.

    Concernant les trésors d'inventivité pour exploiter un null deref : mapper du code, trigger le bug, fini (en gros).
  • # Et pour ceux qui n'y connaissent rien...

    Posté par  . Évalué à 3.

    Quel est donc l'interet de charger en mémoire en tant que simple utilisateur des instructions à cette adresse 0 (NULL) via l'appel système mmap?

    Pourquoi cet appel système accepte la valeur 0 et non pas toute valeur > 0 (mais pas 0)?

    Merci de vos réponses...
    • [^] # Re: Et pour ceux qui n'y connaissent rien...

      Posté par  . Évalué à 5.

      Parce ce n'est PAS une valeur obligatoire de NULL.
      Le fait est que dans la majorité des implémentations/architectures, elle prend la valeur 0.

      http://www.open-std.org/JTC1/SC22/wg14/www/docs/n897.pdf
      Page 79
      Section 6.7.8
      • [^] # Re: Et pour ceux qui n'y connaissent rien...

        Posté par  . Évalué à 1.

        Euh non, NULL == 0 ou NULL == (void*)0, mais dans tous les cas NULL == 0, sinon dans le K&R, tous les cas de test de type if (!ptr) { ... } seraient à jeter.

        Section 7.17 de la normel :

        « NULL can be defined as any null pointer constant. Thus existing code can retain definitions of
        NULL as 0 or 0L, but an implementation may also choose to define it as (void*)0. This latter
        form of definition is convenient on architectures where sizeof(void*) does not equal the size
        of any integer type. It has never been wise to use NULL in place of an arbitrary pointer as a
        function 30 argument, however, since pointers to different types need not be the same size.
        The library avoids this problem by providing special macros for the arguments to signal, the one
        library function that might see a null function pointer. »

        En pratique, POSIX demande que NULL == (void*)0 (désolé je n'ai pas le standard sous les yeux).
        • [^] # Re: Et pour ceux qui n'y connaissent rien...

          Posté par  . Évalué à 9.

          Euh non, NULL == 0 ou NULL == (void*)0, mais dans tous les cas NULL == 0, sinon dans le K&R, tous les cas de test de type if (!ptr) { ... } seraient à jeter.

          Dans un contexte de pointeur, un 0 est converti en pointeur NULL. Donc if (!ptr), qui est en fait if (ptr != 0) est converti en if (ptr != NULL).
          Par contre, NULL n'est pas forcément défini comme ayant tous les bits à 0. Dans un programme, il est représenté par 0 ou (void *)0 mais derrière, le code généré ne se traduit pas forcément par 0x00000000 :

          Some Honeywell-Bull mainframes use the bit pattern 06000 for (internal) null pointers. The CDC Cyber 180 Series has 48-bit pointers consisting of a ring, segment, and offset. Most users (in ring 11) have null pointers of 0xB00000000000. It was common on old CDC ones- complement machines to use an all-one-bits word as a special flag for all kinds of data, including invalid addresses.

          Read more: http://www.faqs.org/faqs/C-faq/faq/#ixzz0g5YOWMAY


          Pour plus d'informations, la bible :
          http://www.faqs.org/faqs/C-faq/faq/

          section 5
        • [^] # Re: Et pour ceux qui n'y connaissent rien...

          Posté par  . Évalué à 2.

          En C, on conseille d'utiliser NULL plutôt que 0.

          En C++, on conseille d'utiliser 0 plutôt que NULL.

          En C++1x, il faudra certainement utiliser une nouvelle valeur : nullptr.

          ...
    • [^] # Re: Et pour ceux qui n'y connaissent rien...

      Posté par  . Évalué à 4.

      Parce que certaines applications ont besoin de pouvoir ce mapper à l'adresse 0, par exemple quand on bosse avec des émulateurs ou machine virtuelles (dosemu, wine, et d'autres). Mais le mieux c'est d'utiliser les capabilities juste pour l'application en question.
      • [^] # Re: Et pour ceux qui n'y connaissent rien...

        Posté par  . Évalué à 2.

        Ca sert surtout a utiliser vm86 qui permet d'excecuter du code real mode nativement. Notamment pour faire tourner des applis dos, sans avoir trop de chose à émuler.
    • [^] # Re: Et pour ceux qui n'y connaissent rien...

      Posté par  . Évalué à 4.

      Moi ce qui me surprend surtout c'est la possibilité d'utiliser cette adresse. Je croyais que processus avait son propre espace d'adressage duquel il ne peut pas sortir, en fait je pensais que l'adresse 0 était l'adresse 0 du programme et pas du système. (Je sais pas si c'est très clair) Hors au début de l'espace d'adressage il y à la section de code qui je pense devrais être en lecture seule, non ?

      À moins que l'on ne parle pas de l'userland et je suis à coté de la plaque.

      Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

      • [^] # Re: Et pour ceux qui n'y connaissent rien...

        Posté par  . Évalué à 2.

        Je croyais que processus avait son propre espace d'adressage duquel il ne peut pas sortir
        En mode utilisateur, oui. Il y a une table des pages par process, avec le kernel mappé dans chaque table à partir d'une adresse fixe (par défaut 0xC0000000).
        Lorsque le process passe en mode noyau, il a accès à toutes les adresses du système (adresses virtuelles, logiques ou physiques).

        en fait je pensais que l'adresse 0 était l'adresse 0 du programme et pas du système. (Je sais pas si c'est très clair) Hors au début de l'espace d'adressage il y à la section de code qui je pense devrais être en lecture seule, non ?
        Lorsque tu mmap, en mode utilisateur, du code exécutable à l'adresse zero (_pour le process_), il est marqué, dans la table des pages _du process_ , comme exécutable : donc, lorsque plus tard, le process, qui tourne cette fois en mode noyau, déréférence l'adresse 0, il peut exécuter le code en question (car l'entrée correspondante dans sa table des pages est marquée comme exécutable, pas d'exception levée par le processeur), le code est tout simplement executé, _en mode noyau_. Et là, c'est le drame.
        • [^] # Re: Et pour ceux qui n'y connaissent rien...

          Posté par  . Évalué à 3.


          Lorsque le process passe en mode noyau, il a accès à toutes les adresses du système (adresses virtuelles, logiques ou physiques).


          Euh non, en mode noyau, on travaille toujours avec des adresse virtuelles uniquement. Après effectivement le noyau a la possibilité de changer les tables de la MMU pour faire correspondre adresse virtuelle -> adresse physique comme il le souhaite.
        • [^] # Re: Et pour ceux qui n'y connaissent rien...

          Posté par  . Évalué à 2.

          J'ai certaines difficultés à comprendre. Si un processus en espace utilisateur accède à l'adresse 0, il accède à l'adresse virtuelle 0 de l'ensemble du système ? Je croyais que chaque processus avait sa propre adresse 0.

          Dans un cas comme dans l'autre je ne vois pas pourquoi l'adresse 0 n'est pas en read only (et exécutable si c'est une adresse 0 par processus).

          Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

          • [^] # Re: Et pour ceux qui n'y connaissent rien...

            Posté par  . Évalué à 3.

            Le noyau est mappé dans l'espace virtuel de chaque processus (plus simple ainsi pour le noyau d'accéder directement à la mémoire du processus lorsqu'il est sollicité).

            Des permissions (~ ring) empêchent simplement le processus d'accéder à la mémoire noyau, bien que celle-ci soit mappée en intégralité dans l'espace du processus.

            Comme si tu avais des binaires dans un même dossier, avec les permissions d'exécution (rwx le plus souvent) pour tout le monde en général (~ fonctions propres au processus), et certains accessibles uniquement au superviseur (~ fonctions propres au noyau). Reste que le superviseur peut exécuter tout le code, le processus malicieux n'a qu'à tromper sa vigilance.
          • [^] # Re: Et pour ceux qui n'y connaissent rien...

            Posté par  . Évalué à 3.

            Si un processus en espace utilisateur accède à l'adresse 0, il accède à l'adresse virtuelle 0 de l'ensemble du système ?

            Il n'y a pas "d'adresse virtuelle 0 de l'ensemble du système". Il y a une adresses virtuelle 0 par process, par table des pages. L'adresse virtuelle 0 du process X ne correspond pas à l'adresse virtuelle 0 du process Y (pour peu qu'elles soient mappées).

            Dans un cas comme dans l'autre je ne vois pas pourquoi l'adresse 0 n'est pas en read only (et exécutable si c'est une adresse 0 par processus).

            Pour les raisons expliquées plus haut (vm86, doesmu), l'adresse 0 doit pouvoir être mappée et exécutable dans certains cas.

            En fait, on s'en fout que ce soit l'adresse 0. Dis-toi que c'est comme n'importe quel déréférencement de pointeur invalide. Si l'adresse en question appartient à une page est mappée et exécutable, alors tu as le même risque d'exploit. La seule différence, c'est qu'un pointeur null est bien plus courant et donc facile à exploiter que 0xc76b8d00.
      • [^] # Re: Et pour ceux qui n'y connaissent rien...

        Posté par  . Évalué à 7.

        Parce que malheureusement les mmu des cpu marche par niveau :
        - le code user ne peut pas faire tourner du code superviseur (kernel)
        - le code superviseur à tout les droit (un peut comme root sous les unix).

        Et pour des question de perf, quand un process user fait une transition vers le monde kernel on garde le meme mapping mmu.
        • [^] # Re: Et pour ceux qui n'y connaissent rien...

          Posté par  . Évalué à 3.

          A noter que la segmentation des X86 permet de faire des contrôle par niveau (en pratique marquer la première page non lisible en data en mode superviseur), mais ce n'est plus vraiment utilisé (a la fois parce que les nouvelles instructions de transition user<->kernel ne s'en servent plus, ca fonctionne plus en mode 64 bits, et c'est spécifique X86).
  • # Sous debian unstable

    Posté par  (site web personnel) . Évalué à -1.

    sans rien modifier on a cela:

    # cat /proc/sys/vm/mmap_min_addr
    65536

    je peux dormir tranquille ?
  • # Petite précision.

    Posté par  . Évalué à 5.

    Quelques précisions:

    « Il [le programmeur C] utilise pour gérer tout cela des pointeurs vers la mémoire virtuelle de la machine, la RAM. »

    La RAM, c'est la mémoire physique, pas la mémoire virtuelle. En fait le C ne sait pas ce qu'est que la RAM en elle-même, il a juste une notion de « mémoire ».

    Ah et juste parce qu'il est encore vendredi quelque part dans le monde: le C est un langage de haut-niveau. O:-)

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.