Qu'est-ce que chroot() ? Il s'agit d'un appel système Unix qui permet de modifier la racine du système de fichier d'un processus donné. Ce que ce dernier prendra pour / ne sera en fait qu'un sous-répertoire très particulier de l'arborescence, /chroot/monservice par exemple. L'intérêt de ce genre d'appel pour la sécurité est évident : il permet d'isoler un processus du reste du système. De manière générale, on l'utilise de deux manière :

  • soit en incluant dans le code de programme un appel à chroot() qui aura pour effet un auto-emprisonnement ;
  • soit en lançant le programme en argument de la commande spécialisée chroot.

OK, on peut isoler un processus. Mais quelle est la portée réelle de cette isolation ?

Une première objection évidente qu'on peut faire est que cette isolation ne porte que sur le système de fichiers. Pour autant qu'elle soit efficacement implémentée, cette protection empêche le processus enfermé de remonter au-delà d'un point précis du système de fichiers. Mais elle ne l'empêchera pas pour autant d'envoyer des signaux à travers tout le système ou encore d'acccéder à des devices. La dernière éventualité est intéressante. Elle vous permet par exemple de lire un système de fichiers en mode raw, voire carrément de le remonter ailleurs, donc de vous ballader en dehors de la cage. La première vous permet d'interagir avec des programmes exécutés en dehors de votre cage. Cette limitation au seul système de fichiers est importante et ne doit jamais être sous-estimée.

La seconde objection est un peu plus technique. Grosso modo, lorsque vous lancez un processus, la structure créée qui lui correspond possède un champ destiné à recevoir la racine du système de fichiers. Et un seul. L'effet de l'appel chroot() sur le processus est de modifier la valeur de ce champ en fournissant un descripteur correspondant à un répertoire arbitraire en lieu et place de /. Mais jamais ne sera stockée l'ancienne racine, celle de laquelle on vient ; pas d'effet poupées russe ou de prison en cascade. Le chroot() n'a qu'un niveau, et si on le casse, on est dehors tout court, ce qui donne lieu à une attaque largement documentée, mais manifestement pas assez connue. Supposons donc un processus chrooté sur lequel on se propose de lancer une seconde fois chroot(). Pour se faire, on crée dans la racine originale[1] un répertoire toto dans lequel on va se chrooter, tout en conservant précieusement le descripteur du répertoire courant. On se trouve donc chrooté dans toto avec un descripteur de fichier pointant en dehors de toto. On exécute alors un fchdir() sur ce descripteur, ce qui a pour effet de nous faire sortir de notre prison. Comme il n'y a pas d'empilement de cages, nous sommes dehors. Tout court. Il nous suffira ensuite de remonter un nombre suffisant de fois dans l'arborescence pour être sûr d'atteindre la véritable racine et de s'y chrooter à nouveau pour se retrouver en position de voir l'ensemble du système de fichiers. CQFD.

Il y a pas mal d'autres techniques pour sortir d'une chroot, mais l'objet de ce billet n'est pas de les passer en revue. Il s'agit juste de vous montrer qu'à partir de ces deux observations, relativement simples dès lors qu'on comprend le fonctionnement de l'appel, on est capable de voir de sérieuses limitations d'une part, mais aussi, et heureusement, des conditions d'utilisation d'autre part. Conditions à respecter scrupuleusement. La seconde objection nous apprend une chose primordiale : comme il faut être root pour utiliser chroot(), un processus chrooté ne doit pas tourner en root. Jamais. Pas de discussion. C'est comme ça. Si vous êtes root dans une chroot(), vous en sortez comme vous voulez. Vraiment. L'emprisonnement d'un processus via l'appel chroot() doit donc toujours être accompagné d'un changement de son propriétaire, chose faisable avec les options -u et -g de la commande chroot ou directement dans votre code. Dans ce dernier cas, le changement d'UID doit être fait consciencieusement, dans l'ordre qui va bien, sous peine de s'exposer à des race conditions dont je ne traiterai pas ici. De la première observation, on déduira que chaque processus chrooté doit avoir son propre UID restreint dédié, pour justement éviter l'envoi de signaux au monde extérieur ou l'accès à des ressources particulières. Si cet UID n'a pas accès en écriture à son monde, c'est encore mieux.


Ces conseils se trouvent dans tous les guides de bonnes pratiques Unix. Sans aller jusqu'à dire que chroot() n'a pas été conçu avec quelques intentions de sécurité, il est assez surprenant de voir ce genre de discussion en 2007, avec des gens qui prennent chroot() pour une une sandbox complète. Car dans le domaine de la sécurité, chroot() est tout de même le parent pauvre d'outils nettement plus aboutis et étoffés, comme le jail() de FreeBSD ou les containers de Solaris 10, deux mécanismes décrits dans MISC 28 par Saâd Kahdi. Rien de comparable non plus avec des environnements d'exécution restreints comme, dans une certaine mesure, la JVM ou carrément la virtualisation d'OS dont on aimerait qu'elle soit plus étanche.

On notera en outre l'existence de patches permettant de durcir le fonctionnement de chroot(). Ainsi, le projet grsecurity inclut, entre autres, des fonctionnalités permettant de bloquer nombres d'attaques connues sur le chroot() comme le double chroot(), la conservation de descripteurs externes lors d'un chroot() ou l'accès à des devices depuis une chroot(). Toutes efficaces qu'elles soient, ces fonctionnalités restent des rustines qui ne doivent en aucun cas, est-il encore besoin de le rappeler, se substituer aux bonne pratiques mentionnées plus haut, mais les compléter.


Comme je le disais plus haut, je ne vise pas ici à déballer un cours sur chroot(), mais juste à attirer votre attention sur ses limitations. Si vous voulez en savoir plus, vous trouverez très facilement sur le net des ressources pertinentes sur la question. Je voulais juste démontrer que dans un monde qui pourtant se renouvelle très vite, certaines idées reçues ont la vie dure. Très dure. Et si la mauvaise utilisation de tels concepts de sécurité laissent des systèmes vulnérables, le plus grave reste l'impression de sécurité que ressentent les gens qui font ces erreurs. Car il n'y a probablement pas de pire situation qu'être vulnérable lorsqu'on se croit en sécurité...

Notes

[1] La première chroot.