Linux

Tout ce qui est GNU / Linux !

Système des capabilities Linux

Introduction et principes généraux des Capabilities

Lorsque l’on veut lancer un processus avec certains droits on peut soit le lancer en tant que root, ce qui est dangereux car le processus a maintenant tous les droits, soit lui attribuer certaines capabilities. Les capabilities ont été créées à la fin des années 90 et sont une façon, en apparence simple, de diviser les droits du root pour n’attribuer que les autorisations voulues (le terme "en apparence" sera justifié par la suite). Ceci permet de respecter le principe du moindre privilège, c’est à dire n’attribuer à un processus que les droits dont il a besoin pour éviter qu’il est trop de "pouvoir". Cette logique s’inscrit dans le système DAC (Discretionary Access Control) qui consiste à confiner les utilisateurs et processus en ne leur attribuant que les droits nécessaires#1. Par exemple, la capability CAP_NET_BIND_SERVICE autorise l’attachement d’un socket aux ports inférieurs à 1024, ce qui normalement n’est autorisé qu’aux processus systèmes. Il faut voir les capabilities comme une granularité du root.

Prenons un exemple simple : si j’essaie d’exécuter ‘tcpdump’ dans un terminal je vais avoir une erreur car je n’ai pas la permission :

image-1588973903658.png
Maintenant, si je lance avec un sudo cela va marcher, mais en examinant le statut du processus on voit que tcpdump a absolument tous les droits, ce que viole la règle du moindre privilège !

image-1588974025207.pngLa solution est donc de donner juste les capabilities nécessaires au fonctionnement de tcpdump. En l’occurrence, tcpdump a besoin de cap_net_raw pour fonctionner. Voici ce que ça donne :

image-1588974114776.pngimage-1588974131421.pngOn voit la valeur 2000. Si on tape la commande ‘capsh –decode=2000’ pour la décoder on obtient bien cap_net_raw.

image-1588974235999.png

Mais que signifie ces CapInh, CapPrm et autres CapEff dans le statut du processus ? Ou le +ep dans la mise en place des capabilities ? Que se passe t’il si un processus engendre un processus fils ? Tant de questions auxquelles je vais tenter de répondre dans cet article. Je vais dans un premier temps expliquer de manière détaillée le fonctionnement des ensembles de capabilities ; dans un second temps éclaircir le système d’héritage ; et je finirai sur un exemple vraiment complet avec la modification du module login. Des captures d’écrans de la console et de morceaux de codes seront à chaque fois présents pour illustrer mes explications.


Actuellement la gestion des capabilities sur un processus fonctionne avec 5 ensembles : Permitted, Effective, Inheritable, Ambient et le Bounding Set. Nous allons, dans un premier temps, nous intéresser à Permitted et Effective.

Permitted

Permitted est l’ensemble de toutes les capabilities qui peuvent être attribuées à un processus à tout moment par le système. Pour modifier l’ensemble Permitted on va utiliser une autre facette des capabilities, qui sont les capabilities de fichiers. En effet, chaque fichier binaire (donc exécutable) possèdent lui aussi les ensembles Permitted, Effective et Inheritable (en réalité, Effective pour fichier n’est pas un ensemble mais un bit d’activation pour les capabilities, mais cela ne change pas grand-chose pour l’utilisation. C’est surtout intéressant à savoir si on s’intéresse à la gestion des capabilities par le système).

Le principe est simple : on utilise la commande bash ‘setcap’ (permet d’attribuer des capabilities aux fichiers binaires de cette facon : sudo setcap cap1,cap2+eip fichier) qui permet d’attribuer des capabilities aux fichiers. Comme vu précédemment, pour utiliser le service tcpdump il faut que le processus possède la capability cap_net_raw. On va donc, dans un terminal, taper la commande ‘sudo setcap cap_net_raw+ep /usr/sbin/tcpdump’. Le + signifie que l’on ajoute la capability et le ep signifie qu’on l’ajoute dans le Permitted et Effective (on peut donc faire +eip pour ajouter dans les 3 ensembles).

Par contre, il existe une règle très importante sur le système des capabilities qui nous suivra tout le long : lorsqu’un setuid d’un utilisateur root vers un non-root est effectué tous les ensembles de capabilities sont effacés par sécurité...sauf à une seule et unique condition : avec la primitive ‘prctl’ (Prctl est une primitive de sys/prctl.h donnant accès à certains aspects de la gestion des processus. Je conseille de lire le manuel de la commande pour en savoir plus) il est possible de placer un SECUREBIT. Ce bit va permettre de conserver Permitted à travers le setuid. Cependant, le bit sera effacé au moment du setuid (utilisation unique), il faudra donc le replacer pour chaque setuid prévus dans la suite du programme. Ceci est important à retenir, ce bit aura un rôle très important par la suite. Ici, un exemple de passage de l’utilisateur 0 (root), vers le 1000 (mon numéro, non root):

image-1588974489219.pngimage-1588974505379.png                                                                  Avant

image-1588974500200.pngAprès

Maintenant avec le SECUREBIT positionné :

image-1588974629040.pngimage-1588974632029.pngAprès changement d'ID

On voit très bien la conservation de l’ensemble Permitted à travers le setuid.

Attention : il est impossible de modifier l’ensemble Permitted d’un processus en cours d’exécution. Les librairies libcap et libcap-ng devraient le permettre grâce à certaines primitives, mais je n’ai jamais réussi à le faire. Voici un code qui devrait permettre de modifier Permitted en cours d’exécution, ainsi que les valeurs de l’ensemble Permitted avant et après modification. Avant exécution Permitted ne possède que cap_sys_nice, le code devrait rajouter cap_net_raw. Aucun changement n’a lieu, la valeur reste à 800000:

image-1588974837716.png

image-1588974855538.png

image-1588974858131.png

Je ne sais pas s’il s’agit d’un bug ou si cela est voulu, mais cela reste néanmoins étrange et je n’ai trouvé aucune explication concrète à ceci. Je pars donc du principe que Permitted est invariant durant l’exécution.

Effective

L’ensemble Effective représente les capabilities qui sont effectivement attribuées au processus à un instant t, c’est à dire les capabilities qui sont en effet exploitable. Effective est un sous ensemble de Permitted ce qui signifie qu’une capability présente dans Effective est obligatoirement présente dans Permitted. Effective est un ensemble modifiable pendant l’exécution du programme. Cela signifie que deux solutions s’offrent à nous pour le modifier : soit utiliser setcap comme pour Permitted, les capabilities seront ainsi présentes dans Effective dès le début de l’exécution, soit utiliser les primitives des librairies libcap et libcap-ng (libcap et libcap-ng sont les librairies en C permettant de travailler sur la structures des capabilities de manière simplifiée, libcap-ng est une variante de libcap rendant les modifications encore plus aisées) dans le code du programme et ainsi modifier les capabilities en cours d’exécution.

Un exemple de code qui permet de mettre cap_net_raw dans Effective en cours d’exécution. La capability était présente dans Permitted auparavant avec ‘setcap’. Ensuite, deux captures de la console pour montrer l’apparition de la capability dans Effective :

image-1588975004718.png

Avant                                               Après

image-1588975009842.pngimage-1588975015704.png

En résumé, nous avons deux ensembles : Effective qui représente toutes les capabilities qui peuvent être utilisées à cet instant, et Permitted qui représente toutes les capabilities qui pourront être acquises ou héritées par le processus pendant l’exécution. Hérité ? J’ai déjà utilisé ce mot dans l’introduction, mais de quoi s’agit-il ? Et bien, nous allons voir ça tout de suite ! Et c’est ce qui va constituer la plus grande partie de ce rapport.

Alors, l’héritage qu’est-ce que c’est ?

 

Le principe d'héritage

Durant l’exécution d’un processus il peut arriver que ce dernier souhaite lancer un processus fils à partir d’un fichier binaire à l’aide d’une primitive de type ‘exec’ (en réalité, ceci arrive très souvent, il suffit de regarder l’arborescence avec la commande ‘pstree’ pour s’en rendre compte). A partir de là, quelles seront les capabilities du processus fils ? Celles du fichier binaire ? Celles du processus père peut-être ? Et bien c’est un peu plus compliqué que cela, et c’est là que les ensembles Inheritable et Ambient entrent en jeu. Pour l’instant nous allons nous concentrer sur Inheritable.

Inheritable

Inheritable représente l’ensemble des capabilities que le processus père va pouvoir transmettre à son fils. Comme Effective, cet ensemble ne peut contenir que les capabilities se trouvant dans Permitted (donc le processus père peut, au maximum, donner les capabilities qui lui sont permises), sauf à une seule et unique condition : si la capability CAP_SETPCAP se trouve à la fois dans Permitted ET dans Effective, Inheritable pourra contenir des capabilities non présentes dans Permitted. Ceci ne fonctionne que pour Inheritable, pas pour Effective (et ne permet pas non plus de modifier Permitted en cours d’exécution).

La modification d’Inheritable est un peu plus délicate que pour les ensembles précédents et pourrait presque sembler buguée. Pour donner des capabilities Inheritable au fichier binaire qui sera exécuté dans le exec un simple ‘setcap + i’ suffit (avec la capability et le fichier voulu, bien entendu). Cependant, il faut que le processus père ait aussi des capabilities Inheritable. On pourrait penser que mettre des capabilities dans le Inheritable de son binaire puis de le lancer donnera les capabilities voulues au processus… Et bien non ! En réalité le binaire a les capabilities dans Inheritable (on le voit en faisant un ‘getcap’ dessus), mais le processus résultant ne les a pas, et ne peut donc pas les transmettre au fils lors du exec.

Voici les capabilities d’un processus pour lequel j’ai mis des capabilities dans eip. On voit qu’Inheritable n’a pas récupéré la capability :

image-1588975324300.png

La seule solution pour ajouter des capabilities dans l’ensemble Inheritable d’un processus qu’on vient de lancer est de le faire pendant l’exécution de ce dernier à l’aide de libcap et libcap-ng (encore et toujours). Voici un exemple de remplissage d’Inheritable sans SETPCAP (donc la capability est déjà dans Permitted) :

image-1588975384484.pngimage-1588975388263.png

Et un exemple avec SETPCAP :

image-1588975460948.pngimage-1588975463686.png

Au lancement seulement SETPCAP et FOWNER sont présentes dans Permitted et Effective (FOWNER est juste là pour l’exemple, il n’a pas d’utilité ici). On voit sur la deuxième capture que j’ai réussi à passer CAP_SYS_NICE dans Inheritable. Et ceci grâce à SETPCAP et ce code :

 

Bon, c’est bien beau tout ça, on sait comment remplir l’ensemble Inheritable d’un processus qu’on veut lancer...mais maintenant, à quoi sert concrètement cet ensemble ? Il va entrer en jeu dans le calcul suivant, qui permet de connaître les capabilities qu’aura le fils :

P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset)

P'(effective) = F(effective) ? P'(permitted) : 0

P'(inheritable) = P(inheritable)

Dans ce calcul, P’ représente le fils, P le père et F le fichier binaire (on retrouve le fait que l’Effective du fichier n’est qu’un bit activé ou non).

cap_bset représente quant à lui le Bounding Set dont j’ai parlé au début. Il s’agit de l’ensemble maximal des capabilities que le système peut attribuer à un processus. Cet ensemble permet simplement de créer des systèmes restreint.

Voyons ce que ça donne concrètement. J’ai donc un programme auquel je donne CAP_SYS_NICE dans le Permitted avec ‘setcap’. Dans le code de ce programme je mets cette même capability dans Inheritable puis je lance un processus fils à l’aide d’un execve sur un binaire possédant la même capability dans Inheritable et Effective. Le processus fils ne fait rien (juste un sleep(20)). Voici le code du processus père et les capabilities du fils :

image-1588975611841.png

image-1588975772250.png

Si on regarde les règles de calculs ainsi que les ensembles où j’ai placé les capabilities on voit que cela marche parfaitement. Il est maintenant l’heure de nous attaquer au deuxième ensemble gérant l’héritage, Ambient. Un deuxième ? Pourquoi faire ?

Ambient

A l’origine seuls les trois ensembles vu précédemment existaient. Seulement, un certain nombre de défauts sont apparus et la création d’un nouvel ensemble avec un fonctionnement différent est devenu nécessaire. Ambient a donc vu le jour en 2015. Pour expliquer la création de l’Ambient, voici une liste sans doute non exhaustive des problèmes :

Ambient a pour objectif de régler ces problèmes, et va permettre de faire ce que l’héritage précédent était censé permettre de faire. Voici donc les règles de calculs maintenant :


P'(ambient) = (file get caps) ? 0 : P(ambient)

P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset) | P'(ambient)

P'(effective) = F(effective) ? P'(permitted) : P'(ambient)

P'(inheritable) = P(inheritable)

Maintenant, Ambient est toujours ajouté à Permitted et Effective, permettant un héritage permanent. Surtout qu’Ambient peut être modifié sans problème en cours d’exécution grâce à la primitive ‘prctl’, de la même façon qu’Inheritable avec libcap ou libcap-ng. Voici le lien vers un blog sur lequel se trouve un très bon code pour tester Ambient :

Reprenons le code pour tester Inheritable, mais en rajoutant l’ajout dans Ambient :

 

image-1588976098451.pngimage-1588976102746.png

Par contre, comme on le voit dans le calcul de l’Ambient du fils, pour qu’il récupère l’Ambient du père il faut que le fichier binaire ne contienne absolument aucune capabilities ! (La commande ‘sudo setcap -r nomDuFichier’ permet de les effacer). On retrouve dans cette règle la logique qui a amené à la création d’Ambient : les fichiers n’ayant pas de capabilities bloquent la transmission.

De plus, Ambient respecte l’invariant comme quoi il ne peux contenir que les capabilities se trouvant à la fois dans Permitted et Inheritable, impossible autrement. Une tentative de remplir Ambient avec Inheritable manquant :

image-1588976142179.png

Une petite précision : même si Ambient est un ensemble servant à l’héritage, la capability SETPCAP n’agit pas dessus. Même en la positionnant dans Permitted et Effective il est impossible de passer des capabilities absente de Permitted dans Ambient.

Le code suivant ajoute SETPCAP à Effective (Permitted est ajouté avec ‘setcap’) puis tente d’ajouter une autre capability à Inheritable et Ambient. La première capture de console montre le statut du processus au début et la deuxième après l’ajout de la deuxième capability : Inheritable s’ajoute bien (comme vu précédemment) mais Ambient renvoie une erreur.

image-1588976265895.png

image-1588976270482.png

image-1588976273790.png

Enfin, lorsque qu’un setuid allant d’un id root vers un id non root est effectué Ambient est inévitablement effacé, impossible de le conserver (mais si Permitted a été conservé avec un SECUREBIT, il est toujours possible de le remplir après le setuid).


Maintenant qu’Ambient existe et semble bien fonctionner, on est en droit de se demander à quoi sert encore Inheritable et pourquoi il n’a pas été supprimé. Et bien, au vu de mes recherches et de mes tests, je pense effectivement qu’Inheritable ne sert plus à grand-chose… Il est toujours là car une phase de transition est toujours nécessaire et ce système est très important sur les kernels Unix. C’est aussi pour ça qu’Ambient a toujours besoin d’Inheritable pour pouvoir se remplir, mais je pense que ce dernier est amené à disparaître.

Il ne reste plus qu’une chose à voir avant de passer à l’exemple concret : les capabilities d’utilisateur.

 

Les capabilities d'utilisateur

Et oui, je n’en ai pas encore parlé pour ne pas tout mélanger, mais les utilisateurs peuvent aussi avoir des capabilities qui leur sont attribuées. En théorie (le mot théorie est important ici, on va y revenir plus tard) cela permet de donner des capabilities au programme login lorsque l’utilisateur s’authentifie (login récupère les capabilities de l’utilisateur). Ensuite login exécute le bash, qui devrait donc irriter de ses capabilities et ainsi, tous programmes lancé par l’utilisateur depuis le bash devrait avoir les capabilities du bash (donc de l’utilisateur).

Pour donner des capabilities à un utilisateur, rien de bien compliqué : il suffit de modifier le fichier capability.conf se trouvant généralement dans le dossier /etc/security. La modification se fait comme les exemples déjà positionnés : pour chaque utilisateur, sur une ligne mettre la suite de capabilities voulues séparées par des virgules, suivi du nom de l’utilisateur :

image-1588976406384.png

La lecture dans ce fichier se fait lorsque login appelle PAM.

PAM ( Pluggable Auhentification Modules) est un système permettant, je cite «l’intégration de différents schémas d'authentification de bas niveau dans une API de haut niveau, permettant de ce fait de rendre indépendants du schéma les logiciels réclamant une authentification » (https://fr.wikipedia.org/wiki/Pluggable_Authentication_Modules). Pour faire plus simple, cela permet, lors de l’authentification de l’utilisateur, de charger et d’attribuer à chaque service ses droits et autorisations. De plus, PAM permet de modifier très facilement tous ces paramètres sans avoir à recompiler tous le programme, et c’est indépendant du kernel ! Chaque service voulant disposer de PAM possède un fichier dans /etc/pam.d où tous les paramètres et librairies à appeler sont rentrés.

Donc, lorsque login appelle PAM il fait appelle à la librairie pam_cap.so qui devrait permettre de lui donner les capabilities et ensuite pouvoir les transmettre. Mais comme je l’ai dis, Ambient est récent et n’est donc pas encore implémenter partout, laissant certains services sans possibilité d’héritage...et c’est le cas du login. On a beau rentrer toutes les capabilities que l’on veut dans capability.conf, aucune n’est récupérée par le bash. On essaie avec CAP_NET_RAW pour tcpdump :

image-1588976638126.png

Comme on le voit, tcpdump ne fonctionne pas, alors que l’utilisateur devrait avoir la capability requise. On est donc toujours obligé de faire un ‘setcap cap_net_raw+ep’ pour avoir tcpdump...pas très satisfaisant.

Je vais exploiter cette situation dans un exemple concret et implémenter Ambient dans le module login et pam_cap.so, et ainsi permettre au login de donner ses capabilities au bash. Une fois les capabilities dans l’Ambient du bash tous les programmes lancés par l’utilisateur auront ses capabilities.

 

Exemple concret : le module login

Comme dit précédemment, login utilise la librairie pam_cap.so pour obtenir ses capabilities. Il convient donc d’aller regarder le code de cette librairie se trouvant dans le package libcap pour comprendre ce qu’il s’y passe. Voici donc des captures d’écrans du code mettant en place les capabilities ainsi que les explications nécessaires :