25 KiB
Les capabilities
Historiquement, dans la tradition UNIX, on distinguait deux catégories de processus :
- les processus privilégiés : dont l'identifiant numérique de son utilisateur est 0 ;
- les processus non-privilégiés : dont l'identifiant numérique de son utilisateur n'est pas 0.
Lors des différents tests de permission faits par le noyau, les processus privilégiés outrepassaient ces tests, tandis que les autres devaient passer les tests de l'effective UID, effective GID, et autres groupes supplémentaires...
Dans les années 90, ce système s'est rélévé être un peu trop basique et conduisait régulièrement à des abus, au moyen de vulnérabilités trouvées dans les programmes setuid root.
Avant de regarder plus en détail les solutions qui ont été apportées à ce problème, commençons par mettre le doigt sur les difficultés.
setuid
: être root
le temps de l'exécution
De manière générale, un processus s'exécute dans le contexte de privilège de l'utilisateur qui l'a démarré. Ainsi, lorsque l'on souhaite supprimer un fichier ou un répertoire, en tant que simple utilisateur on ne pourra supprimer que ses propres fichiers, et en aucun cas on ne pourra supprimer ... la racine par exemple.
Il y a cependant des tâches nécessitant des privilèges, que l'on souhaite
pouvoir réaliser en tant que simple utilisateur. C'est le cas notamment de la
modification de son mot de passe : il serait inconcevable de devoir demander à
son administrateur à chaque fois que l'on souhaite modifier son mot de
passe. Pourtant bien que l'on puisse lire le fichier /etc/passwd
, seul root
peut y apporter des modifications (il en est de même pour /etc/shadow
qui
contient les hashs des mots de passe).
C'est ainsi qu'est apparu le suid-bit
parmi les modes de fichiers. Lorsque
ce bit est défini sur un binaire exécutable, au moment de l'exécution, le
contexte passe à celui du propriétaire du fichier (root
si le propriétaire
est root
, mais cela fonctionne quelque soit le propriétaire du fichier : on
ne devient pas root
, mais bien l'utilisateur propriétaire).\
Un autre exemple : pour émettre un ping, il est nécessaire d'envoyer des
paquets ICMP. À la différence des datagrammes UDP ou des segments TCP, il n'est
pas forcément simple d'envoyer des paquets ICMP lorsque l'on est simple
utilisateur, car l'usage du protocole ICMP dans une socket est restreint : il
faut soit être super-utilisateur, soit que le noyau ait été configuré pour
autoriser certains utilisateurs à envoyer des ECHO_REQUEST
.
Pour permettre à tous les utilisateurs de pouvoir envoyer des ping, le programme est donc généralement setuid root, pour permettre à n'importe quel utilisateur de prendre les droits du super-utilisateur, le temps de l'exécution du programme.\
Les problèmes surviennent lorsque l'on découvre des vulnérabilités dans les
programmes setuid root. En effet, s'il devient possible pour un utilisateur
d'exécuter du code arbitraire, ce code sera exécuté avec les privilèges de
l'utilisateur root ! Dans le cas de ping
, on se retrouverait alors à
pouvoir lire l'intégralité de la mémoire, alors que l'on avait juste besoin
d'écrire sur une interface réseau.
Et ainsi les privilèges furent séparés
Depuis Linux 2.2 (en 1998), les différentes actions réclamant des privilèges sont regroupées en catégories que l'on appelle capabilities. Chacune donne accès à un certain nombre d'actions, on trouve notamment :
CAP_CHOWN
: permet de modifier le propriétaire d'un fichier de manière arbitraire ;CAP_KILL
: permet de tuer n'importe quel processus ;CAP_SYS_BOOT
: permet d'arrêter ou de redémarrer la machine ;CAP_SYS_MODULE
: permet de charger et décharger des modules ;- et beaucoup d'autres, il y en a environ 41 en tout (ça dépend de la version du noyau) !
::::: {.more}
Petit point historique, Linux n'est pas à l'origine du concept de capabilities, il s'agit au départ de la norme POSIX 1003.1e, mais celle-ci s'est éteinte après le 17\textsuperscript{ème} draft. Il devait y être standardisé de nombreux composants améliorant la sécurité des systèmes POSIX, notamment les capabilities POSIX, les ACL POSIX, ...
:::::
Ainsi, ping
pourrait se contenter de CAP_NET_RAW
, une capability qui
permet notamment d'envoyer des données brutes sur n'importe quelle socket,
sans passer par les types de socket plus restreints, mais accessibles aux
utilisateurs.
C'est peut-être encore beaucoup, mais au moins, une vulnérabilité dans ping
ne permettrait plus d'accéder à tous les fichiers ou à toute la mémoire.
::::: {.question}
Peut-on faire mieux pour ping
? {-}
Un paramètre existe bien depuis 2011 dans le
noyau :
net.ipv4.ping_group_range
. Mais ce n'est que depuis
2020 que les distributions
comme Fedora et Ubuntu se mettent à fournir par défaut une configuration qui
permette de se passer de capabilities pour lancer ping
.
Cette option prend un intervalle d'identifiant numérique de groupes autorisés à
créer de ECHO_REQUEST
. Par défaut la valeur invalide de 1 0
est définie, ce
qui signifie qu'aucun groupe n'est autorisé à créer des ECHO_REQUEST
à moins
d'être privilégié.
:::::
::::: {.warning}
Nous allons par la suite travailler avec le binaire ping
pour appréhender les
capabilities. Si vous vous rendez compte que votre binaire ping
est dans le
cas figure décrit juste avant (avec une distribution ayant mis en œuvre
l'option net.ipv4.ping_group_range
), vous pouvez retrouver le comportement
historique en désactivant l'option :
Pas d'inquiétudes, le paramètre est changé de manière temporaire seulement, au prochain redémarrage il reprendra sa valeur définie par la distribution.
:::::
Les ensembles de capabilities
Tout d'abord, il faut noter que chaque thread dispose de 5 ensembles de capabilities. Au cours de son exécution, il peut changer ses ensembles de capabilities. Ceux-ci sont utilisés de la façon suivante :
-
bounding (B) : c'est l'ensemble limitant des capabilities qui pourront faire l'objet d'un calcul lors des prochaines exécutions. Quelques soient les capabilities que peut nous faire gagner une exécution, si la capability ne se trouve pas dans le bounding set, elle ne sera pas considérée et il sera impossible de l'obtenir. L'option
--cap-drop
de Docker modifie cet ensemble pour restreindre les capabilities disponibles dans un conteneur ; -
effective (E) : il s'agit des capabilities actuellement actives, qui seront vérifiées lors des tests de privilèges ;
-
permitted (P) : ce sont les capabilities que la tâche peut placer dans l'ensemble effective via
capset(2)
. L'ensemble effective ne peut pas avoir de capabilities qui ne sont pas dans l'ensemble permitted ; -
inheritable (I) : est utilisé au moment de la résolution des capabilities lors de l'exécution d'un nouveau processus. Il s'agit des capabilities qui seront transmises au processus fil. À moins d'avoir la capability
CAP_SETPCAP
, cet ensemble ne peut pas avoir plus de capability que celles présentent dans l'ensemble permitted ; -
ambient (A) : existe depuis Linux 4.3 pour permettre aux utilisateurs non-
root
de conserver des capabilities d'une exécution à l'autre (sans que l'ensemble des binaires soient marqués avec toutes les capabilities dans leur ensemble inheritable)1. L'ensemble évolue automatiquement à la baisse si une capability n'est plus permitted.
Les attributs de fichier étendus
Une grosse majorité des systèmes de fichiers (ext[234], XFS, btrfs, ...) permet d'enregistrer, pour chaque fichier, des attributs (dits attributs étendus, par opposition aux attributs réguliers qui sont réservés à l'usage du système de fichiers, tels que le propriétaire, le groupe, les modes du fichier, ...).
Sous Linux, les attributs sont regroupés dans des espaces de noms :
- security : espace utilisé par les modules de sécurité du noyau, tel que SELinux, ... ;
- system : espace utilisé par le noyau pour stocker des objets système, tels que les ACL POSIX ;
- trusted: espace dont la lecture et l'écriture est limité au super-utilisateur ;
- user : modifiable sans restriction, à partir du moment où l'on est le propriétaire du fichier.
Par exemple, on peut définir un attribut sur un fichier comme cela :
Lorsque l'on est propriétaire du fichier, on peut modifier les attributs des espaces security, system et user. Évidemment, root peut aussi intervenir et par exemple les ACL POSIX2 (espace system) :
Dans cet exemple, bien que les droits UNIX traditionnels ne vous donnent pas accès au fichier, les ACL POSIX vous autorisent à le lire.
Vous pouvez voir ces attributs bruts avec la commande :
Il s'agit d'une structure du noyau encodée en base64 (la valeur débute par
0s
, de la même manière que l'on a l'habitude de reconnaître l'hexadécimal
avec 0x
, voir getfattr(1)
), pas forcément très lisible en l'état. On
utilisera plutôt getfacl
pour la version lisible.
::::: {.code}
Les structures utilisées pour stocker les ACL POSIX dans les attributs étendus
sont définies dans linux/posix_acl_xattr.h
:\
D'abord un en-tête, composé de la version avec laquelle la suite des octets a été enregistrée (actuellement 2) :
Puis on trouve directement la liste d'entrée(s) :
Le tag identifie de quel type d'entrée il s'agit (propriétaire, utilisateur, groupe, masque, reste du monde, ...). Les permissions sont un champ de bits pour la lecture, écriture et exécution. Enfin l'identifiant renseigne sur le numéro d'utilisateur ou de groupe à qui s'applique l'entrée.\
Les constantes utilisées sont définies dans linux/posix_acl.h
.
:::::
Capabilities et attributs étendus pour ping
De la même manière que l'on peut définir de façon plus fine les droits d'accès par utilisateur, un attribut de l'espace de nom security peut être défini pour accroître les capabilities d'un processus lorsqu'il est lancé par un utilisateur non-privilégié. On peut voir le setuid root comme l'utilisation de cet attribut, qui accroîtrait l'ensemble des capabilities.
Si votre distribution profite de ces attributs étendus, vous devriez obtenir :
Suivant votre distribution, d'autres programmes sont susceptibles de profiter
des attributs étendus pour se passer du setuid root, par exemple : chvt
,
beep
, iftop
, mii-tool
, mtr
, nethogs
, ...
Comme pour les ACL POSIX, une structure du noyau est enregistrée comme attribut
du fichier ; et on peut l'afficher dans sa version plus lisible avec getcap
:
::::: {.code}
La structure utilisée pour stocker les capabilities dans les attributs étendus
est définie dans linux/capability.h
:
La valeur magic
contient la version sur 1 octet, puis 3 octets sont réservés
pour des flags. Actuellement un seul flag existe, il s'agit de
VFS_CAP_FLAGS_EFFECTIVE
qui détermine si la liste effective de capabilities
du programme doit être remplie avec les capabilities permitted si elle doit
rester vide (auquel cas ce sera au programme de s'ajouter les capabilities au
cours de l'exécution).\
Deux entiers de 32 bits représentent ensuite les capabilities de 0 à 31 sous
forme de champ de bits, un entier pour la liste des capabilities permises, un
autre pour les capabilities héritables. Comme il y a une quarantaine de
capabilities, celles de 32 à 40 se retrouvent à la suite, dans deux nouveaux
entiers de 32 bits. C'est pour cela que data
est un tableau, avec
VFS_CAP_U32
valant 2, car on a deux fois nos 2 entiers de 32 bits à la
suite.\
Il s'agit de la version 2 de la structure. La version 1 était utilisée lorsqu'il n'y avait encore que moins de 33 capabilities : il n'y avait alors pas de tableau, seulement les deux entiers de 32 bits. On remarque que les deux versions sont facilement compatibles entre-elles, la seconde version étendant simplement la première.\
Une version 3 existe, la structure obtient un champ supplémentaire rootid
:
Ce nouveau champ rootid
est utilisé pour stocker un identifiant d'utilisateur
root
au sein d'un namespace User. C'est utile pour pouvoir jouer avec les
capabilities au sein d'un conteneur non-privilégié. S'il vaut autre chose que
0 et que l'on ne se trouve pas dans un namespace User, les capabilities ne
seront pas appliquées.
:::::
Gagner des capabilities
C'est à l'exécution (execve(2)
) que sont calculés les nouveaux ensembles de
capabilities : c'est donc uniquement à ce moment que l'on peut en gagner de
nouvelles. Voici comment les différents ensembles du nouveau processus sont
calculés :
p′_A = (\textsf{file caps or setuid or setgid}\: ?\: ∅\: :\: p_A)
p′_P = (p_I\: \&\: f_I)\ |\ (f_P\: \&\: p_B)\ |\ p′_A
p′_E = f_E\: ?\: p_P′\: :\: p′_A
p′_I = p_I
p′_B = p_B
Avec p
le processus avant l'exécution, f
le fichier exécutable donnant
naissance au nouveau processus et p′
le nouveau processus.\
On remarque que sans les ambients capabilities, on perd systématiquement les
capabilities dont on disposait avant l'execve(2)
, car f_I
n'est que très
rarement défini sur un exécutable. Dans le cas général, on récupère donc soit
f_P
, soit p_A
(les deux étant exclusifs : si f_P
est défini, p′_A
est
vide).
Bien entendu, lorsque l'on se trouve dans le contexte d'exécution de root
(ou
que l'on exécute un binaire setuid root), ces calculs sont biaisés, car le
super-utilisateur a normalement toutes les capabilities (mais toujours
limitées par l'ensemble bounding) : f_P
et f_I
contiennent alors toutes
les capabilities, indifféremment du fichier considéré. Les calculs peuvent
alors être simplifiés en :
p′_P = (p_I\: \&\: p_B)
p′_E = p′_P
Il y a cependant une exception, lorsque l'utilisateur réel n'est pas root
(comme par exemple face à un binaire setuid root, seul l'utilisateur effectif
change) : dans ce cas, si le fichier expose des capabilities, seulement
celles-ci sont gagnées.
::::: {.question}
Peut-on placer des capabilities sur un script ? {-}
De la même manière qu'il n'est pas possible d'avoir de script setuid root sous Linux3, ajouter des capabilities à un script ne permettra pas d'en gagner. Le calcul s'effectue en effet sur les capabilities de l'exécutable de l'interpréteur et non sur celles du script.
:::::
Gérer ses capabilities
Un thread peut agir comme il le souhaite sur les ensembles effective,
permitted et inheritable. À condition bien sûr de ne jamais dépasser les
capabilities déjà contenues dans l'ensemble permitted, sauf si l'on dispose
de la capability CAP_SETPCAP
: dans ce cas, on se retrouve limité seulement
par l'ensemble bounding.
::::: {.code}
On utilise les appels système capget(2)
et capset(2)
pour respectivement
connaître les capabilities actuelles de notre fil d'exécution et pour les
écraser. Ces appels système utilisent une structure d'en-tête et une structure
de données, qui sont définies dans linux/capability.h
:
/* V2 added in Linux 2.6.25; deprecated */
#define _LINUX_CAPABILITY_VERSION_2 0x20071026 #define _LINUX_CAPABILITY_U32S_2 2
/* V3 added in Linux 2.6.26 */
#define _LINUX_CAPABILITY_VERSION_3 0x20080522 #define _LINUX_CAPABILITY_U32S_3 2
typedef struct __user_cap_header_struct { uint32_t version; int pid; } *cap_user_header_t;
typedef struct __user_cap_data_struct { uint32_t effective; uint32_t permitted; uint32_t inheritable; } *cap_user_data_t;
</div>
La structure d'en-tête permet de renseigner sur la version de la structure de
données que l'on souhaite utiliser ou recevoir. Comme pour les *capabilities*
dans les attributs étendus, la première version était utilisée lorsqu'il y
avait moins de 33 *capabilities*, ce qui permettait de tout stocker sur un seul
entier de 32 bits non signé. Les versions 2 et 3 sont identiques et permettent
de récupérer les *capabilities* au moyen d'un tableau de 2 structures.\
::::: {.warning}
Les versions 2 et 3 ici ne sont pas comparables aux versions 2 et 3 de nos
structures `vfs_cap_data` et `vfs_ns_cap_data` : il n'y a pas de notion de
*namespace* ici.
Il y a eu un couac dans les en-têtes distribués avec Linux 2.6.25, causant des
*buffers overflow* dans les applications qui n'avaient pas prévu de gérer les
versions. Cela a été corrigé dans la version 2.6.26 : un avertissement est
consigné dans les journaux système en cas d'utilisation malheureuse de la
version 2.
:::::
Dans la pratique, on préférera utiliser `cap_get_proc(3)` et `cap_set_proc(3)`
fournis par la `libcap`.
:::::
Pour agir sur l'ensemble *bounding*, il faut disposer de la *capability*
`CAP_SETPCAP`[^WHY_BOUNDING_SETPCAP]. Lorsque l'on retire une *capability* de
cet ensemble, elle n'est pas retirée des autres ensembles et on peut donc
continuer de bénéficier des privilèges qu'elle accorde.
Il faut bien penser, lorsque l'on retire une *capability* de l'ensemble
*bounding*, à également la retirer de l'ensemble *inheritable*, sans quoi si le
programme exécuté a la *capability* en question dans ses attributs, celle-ci
sera préservée (revoir la formule pour $p′_P$, l'ensemble *bounding* n'est pas
considéré avec l'ensemble *inheritable*).
[^WHY_BOUNDING_SETPCAP]: En effet, avant Linux 2.6.25, cet ensemble était
utilisé par tout le système, pas seulement pas le *thread* courant et ses
fils.
::::: {.code}
La consultation et la modification de l'ensemble *bounding* se fait au moyen de
`prctl(2)`, en utilisant les paramètres `PR_CAPBSET_READ` ou `PR_CAPBSET_DROP`.
Le second paramètre attendu est l'une des constantes représentant une
*capability*.\
Avec `PR_CAPBSET_READ`, `prctl(2)` retournera 0 si la *capability* ne fait pas
partie de l'ensemble *bounding*, ou 1 si elle est bien présente.
Avec `PR_CAPBSET_DROP`, `prctl(2)` retirera la *capability* passée en argument
de l'ensemble *bounding*. Une fois cette action effectuée, il est impossible de
revenir en arrière.\
Dans la pratique, on préférera utiliser `cap_get_bound(3)` et
`cap_drop_bound(3)` fournis par la `libcap`.
:::::
Enfin, le *thread* peut aussi modifier à sa guise l'ensemble *ambient*, à
condition que les *capabilities* ajoutées soient dans les ensembles *permitted*
et *inheritable*.
::::: {.code}
On consulte et modifie l'ensemble *ambient* avec `prctl(2)` à qui l'on passe
comme premier paramètre `PR_CAP_AMBIENT`. Le second paramètre permet de
préciser l'action que l'on veut réaliser :\
- `PR_CAP_AMBIENT_RAISE` : ajoute la *capability* précisée comme troisième
paramètre ;
- `PR_CAP_AMBIENT_LOWER` : retire la *capability* précisée comme troisième
paramètre ;
- `PR_CAP_AMBIENT_IS_SET` : retourne 1 si la *capability* précisée comme
troisième paramètre fait partie de l'ensemble *ambient*, sinon retourne 0 ;
- `PR_CAP_AMBIENT_CLEAR_ALL` : vide l'ensemble *ambient*.
:::::
### Explorer les *capabilities* avec un shell
La `libcap`, au travers des paquets `libcap2-bin` (Debian et ses dérivées) ou
`libcap` (Alpine, Archlinux, Gentoo et leurs dérivées), apporte le programme
`capsh(1)`, très utile pour appréhender les *capabilities*.
<div lang="en-US">
42sh# capsh --drop=cap_net_raw -- -c "/bin/ping nemunai.re" /bin/bash: line 1: /bin/ping: Operation not permitted
</div>
Une autre commande intéressante est `pscap(1)`, de la `libcap-ng`. Parmi tous
les programmes en cours d'exécution, cet utilitaire va afficher tous les
programmes s'exécutant actuellement avec des *capabilities*, en indiquant pour
chacun lesquelles sont actives et disponibles.
::::: {.exercice}
### Visualisateur de *capabilities* d'un processus {-}
Écrivons maintenant un programme permettant de voir les *capabilities*
d'un processus :
<div lang="en-US">
42sh$ ./view_caps 1 cap_user_header_t
Version: 20080522 PID: 1
cap_user_data_t
effective: 0x3fffffffff CAP_AUDIT_CONTROL CAP_AUDIT_READ [...] CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_SYSLOG CAP_WAKE_ALARM permitted: 0x3fffffffff CAP_AUDIT_CONTROL CAP_AUDIT_READ [...] CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_SYSLOG CAP_WAKE_ALARM inheritable: 0x0
</div>
Appelé sans argument, `view_caps` affichera les *capabilities* du processus
courant, avec en plus les informations sur son ensemble *ambient* et
*bounding* :
<div lang="en-US">
42sh# ./view_caps cap_user_header_t
Version: 20080522 PID: 42
cap_user_data_t
effective: 0x3fffffffff CAP_AUDIT_CONTROL CAP_AUDIT_READ [...] CAP_SYSLOG CAP_WAKE_ALARM permitted: 0x3fffffffff CAP_AUDIT_CONTROL CAP_AUDIT_READ [...] CAP_SYSLOG CAP_WAKE_ALARM inheritable: 0x0
ambient: CAP_AUDIT_CONTROL CAP_AUDIT_READ [...] CAP_SYSLOG CAP_WAKE_ALARM
bounding: CAP_AUDIT_CONTROL CAP_AUDIT_READ [...] CAP_SYSLOG CAP_WAKE_ALARM
</div>
<div lang="en-US">
42sh$ ./view_caps cap_user_header_t
Version: 20080522 PID: 42
cap_user_data_t
effective: 0x0 permitted: 0x0 inheritable: 0x0
ambient:
bounding: CAP_AUDIT_CONTROL CAP_AUDIT_READ [...] CAP_SYSLOG CAP_WAKE_ALARM
</div>
Il peut être intéressant de lancer `view_caps` au sein d'un conteneur Docker
pour voir évoluer l'ensemble *bounding*, ou bien d'utiliser `capsh(1)` en
amont.
:::::
### Pour aller plus loin {-}
Je vous recommande la lecture des *man* suivants :
* `capabilities(7)` : énumérant tous les *capabilities*, leur utilisation, etc. ;
* `xattrs(7)` : à propos des attributs étendus.
Et de ces quelques articles :
* [Linux Capabilities: Why They Exist and How They Work](https://blog.container-solutions.com/linux-capabilities-why-they-exist-and-how-they-work) :\
<https://blog.container-solutions.com/linux-capabilities-why-they-exist-and-how-they-work>
* [Guidelines for extended attributes](https://www.freedesktop.org/wiki/CommonExtendedAttributes/) :\
<https://www.freedesktop.org/wiki/CommonExtendedAttributes/>
* [File-based capabilities](https://lwn.net/Articles/211883/) : <https://lwn.net/Articles/211883/>
* [A bid to resurrect Linux capabilities](https://lwn.net/Articles/199004/) : <https://lwn.net/Articles/199004/>
* [False Boundaries and Arbitrary Code Execution](https://forums.grsecurity.net/viewtopic.php?f=7&t=2522#p10271) :\
<https://forums.grsecurity.net/viewtopic.php?f=7&t=2522#p10271>
* [Linux Capabilities on HackTricks](https://book.hacktricks.xyz/linux-unix/privilege-escalation/linux-capabilities) :\
<https://book.hacktricks.xyz/linux-unix/privilege-escalation/linux-capabilities>
- [POSIX Access Control Lists on Linux](https://www.usenix.org/legacy/publications/library/proceedings/usenix03/tech/freenix03/full_papers/gruenbacher/gruenbacher_html/main.html) :\
<https://www.usenix.org/legacy/publications/library/proceedings/usenix03/tech/freenix03/full_papers/gruenbacher/gruenbacher_html/main.html>
-
Le problème ayant donné naissance aux ambient capabilities est décrit dans cet échange : https://lwn.net/Articles/636533/ ↩︎
-
Les ACL POSIX sont des permissions supplémentaires qui viennent s'ajouter aux modes standards du fichier (propriétaire, groupe, reste du monde). Avec les ACL POSIX, on peut doonner des droits à un ou plusieurs utilisateurs ou groupe, de manière spécifique. ↩︎