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.
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 guepe . Évalué à 2.
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 Batchyx . Évalué à 3.
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 guepe . Évalué à 2.
[^] # Re: Analyseur statique...
Posté par neologix . Évalué à 8.
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 cowboy . Évalué à 4.
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).
[^] # Re: Précisions
Posté par Krunch (site web personnel) . Évalué à 7.
http://packetstorm.linuxsecurity.com/poisonpen/8lgm/ptchown.(...)
Voir aussi http://lists.immunitysec.com/pipermail/dailydave/2009-Octobe(...)
pertinent adj. Approprié : qui se rapporte exactement à ce dont il est question.
[^] # Re: Précisions
Posté par Krunch (site web personnel) . Évalué à 3.
pertinent adj. Approprié : qui se rapporte exactement à ce dont il est question.
[^] # Re: Précisions
Posté par __o . Évalué à 1.
Avec mmap_min_addr, non c'est pas tout, puisque ça rend le mapping impossible :)
Je pense qu'il fait référence à la façon dont l'exploit passe outre le mmap_min_addr. Il faut réunir beaucoup de conditions pour que ça soit possible.
# Et pour ceux qui n'y connaissent rien...
Posté par cosmocat . Évalué à 3.
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 fcartegnie . Évalué à 5.
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 lasher . Évalué à 1.
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 neologix . Évalué à 9.
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 alice . Évalué à 2.
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 neologix . Évalué à 4.
[^] # Re: Et pour ceux qui n'y connaissent rien...
Posté par M . Évalué à 2.
[^] # Re: Et pour ceux qui n'y connaissent rien...
Posté par barmic . Évalué à 4.
À 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 neologix . Évalué à 2.
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 galactikboulay . É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 barmic . Évalué à 2.
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 Defre . Évalué à 3.
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 neologix . Évalué à 3.
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 M . Évalué à 7.
- 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 M . Évalué à 3.
# Sous debian unstable
Posté par Pierre Tramal (site web personnel) . Évalué à -1.
# cat /proc/sys/vm/mmap_min_addr
65536
je peux dormir tranquille ?
# Petite précision.
Posté par lasher . Évalué à 5.
« 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.