(Note: Ce billet date d'environ six mois mais j'ai trop procrastiné pour le relire et le publier. Ca m'a aura au moins permis de mettre à jour certaines sections.)
Le système des capabilities sous UNIX est le parcellement du droit root en de multiples privilèges : lecture RAW du disque, capture du trafic sur la carte réseau, chargement de module, etc.
Ce travail a été débuté par le comité POSIX avec l'écriture de POSIX 1.e, mais après une dizaine d'années de travail, les sponsors ont quitté le navire et le comité a annonçé en 1998 : So the Standard Posix.1e is not likely to be "finished" or "released" anytime soon.
Bien que la standardisation ait été un échec, les développeurs des différents systèmes d'exploitation n'ont pas abandonné les capabilities pour autant, en particulier pour le noyau Linux qui a fait quelques avançées majeures l'année dernière, en particulier avec le 2.6.24 et le 2.6.25
Capabilities
Introduction
Toutes les capabilities existantes sont disponibles dans /usr/include/linux/capability.h elles sont au nombre de 34 (à l'heure actuelle) :
CAP_CHOWN CAP_NET_ADMIN CAP_SYS_RESOURCE CAP_DAC_OVERRIDE CAP_NET_RAW CAP_SYS_TIME CAP_DAC_READ_SEARCH CAP_IPC_LOCK CAP_SYS_TTY_CONFIG CAP_FOWNER CAP_IPC_OWNER CAP_MKNOD CAP_FSETID CAP_SYS_MODULE CAP_LEASE CAP_KILL CAP_SYS_RAWIO CAP_AUDIT_WRITE CAP_SETGID CAP_SYS_CHROOT CAP_AUDIT_CONTROL CAP_SETUID CAP_SYS_PTRACE CAP_SETFCAP CAP_SETPCAP CAP_SYS_PACCT CAP_MAC_OVERRIDE CAP_LINUX_IMMUTABLE CAP_SYS_ADMIN CAP_MAC_ADMIN CAP_NET_BIND_SERVICE CAP_SYS_BOOT CAP_NET_BROADCAST CAP_SYS_NICE
On peut traçer une ligne dans ce groupe avec les capabilities
correspondant une autorisation (c'est à dire "je suis autorisé à
effectuer ce type d'action") et celles élévant les privilèges comme
CAP_MAC_OVERRIDE qui permet d'inhiber l'effet des
permissions du filesystem.
Les capabilities sont acquises soit par héritage du père, par des capabilities attachées au filesystem ou forçée par un processus externe.
La capability CAP_SET_CAP est nécessaire afin de modifier
les capabilities.
Thread capabilities
Un processus, ou thread, possède en fait trois ensembles de capabilities :
Permissions effectives, les privilèges que le processus peut utiliser dès à présent.
Permissions permises, ce sont les privilèges que peut acquérir le processus s'il le désire. En d'autres termes, ce sont les permissions qui peuvent être déplaçées dans l'ensemble des permissions effectives.
Permissions héritées, les privilèges léguées.
Nous verrons dans les sections suivantes comment sont calculées
automatiquement lors d'un execve().
capget permet de consulter les capabilities d'un
processus, le noyau Linux offre également une entrée dans
/proc :
# grep Cap /proc/self/status
CapInh:000000000673b200
CapPrm:0000000000000000
CapEff:0000000000000000
CapBnd:ffffffffffffffff
File capabilities
Les file capabilities étaient jusqu'au 2.6.24 la pièce manquante dans l'implémentation de Linux qui lui permettait d'être complète. Ces privilèges prennent leurs sens lorsqu'un fait un execve sur ce fichier, le processus exécutant le programme verra ses thread capabilities modifiées en conséquence.
Il existe également trois ensemble de file capabilities :
Permissions permises (ou forçées), un processus gagnera automatiquement ces capabilities.
Permissions héritées (ou autorisées), cet ensemble permet de réduire les capabilities héritées d'un thread : un "ET LOGIQUE" sera fait entre les permissions héritées du processus et les permissions héritées du fichier.
Permission effective, cet "interrupteur" pousse les nouvelles permissions permises directement dans les permissions effectives du thread. Autrement, les permissions effectives sont écrasées à 0.
capability bounding set
Grossièrement, c'est l'ensemble des capabilities autorisées sur le système.
Transfert entre processus
Il n'y a que deux moyens pour modifier ses capabilities :
- Manuellement, avec
cap_set_procouprctl - Lors d'un
execve()
Lors d'un fork(), tous les ensembles de capabilities sont
copiées du père vers le fils sans modification. Nous ne nous
intéresserons qu'aux règles de transmission des capabilities lors d'un
execve.
Les règles de transfert sont les suivantes, extraites de la page de manuel capabilities(7) :
P'(permitted) = (P(inheritable) & F(inheritable)) |
(F(permitted) & cap_bset)
P'(effective) = F(effective) ? P'(permitted) : 0
P'(inheritable) = P(inheritable)
where:
P denotes the value of a thread capability set before the
execve(2)
P' denotes the value of a capability set after the execve(2)
F denotes a file capability set
cap_bset is the value of the capability bounding set
On constate que les permissions héritées sont toujours conservées. Les capabilities permises sont quand à elles adaptées avec "P(inheritable) & F(inheritable)" mais sont surchargées par le contenu de F(permitted) (modulo le capability bounding set).
Enfin, les permissions effectives du thread dépend de la permission effective du fichier, si elle est activée, les nouvelles permissions permises sont poussées au processus, autrement, elles sont vidées.
Comportement du Capability bounding set
En d'autres termes, cela signifie que les file capabilities l'emportent toujours sur les thread capabilities. C'est d'ailleurs le seul moyen d'élever ses capabilities, ce qui peut être problématique : dans un environnement cloisonné comme une sandbox, on voudrait bannir cette possibilité d'élevation. C'est le rôle du capability bounding set qui a changé de comportement au 2.6.25.
Pre-2.6.25
Avant, cette variable était globale au système : tous les processus la
partagaient. Cette approche avait au moins un avantage, c'était qu'au
moment où une capability était supprimée, il était garanti qu'elle ne
serait jamais distribuée à nouveau. Les sysadmins aimaient donc
enlever la capacité CAP_SYS_MODULE afin d'empêcher tout
chargement de module.
En réalité, la vérité est moins rose puisque d'une part, le capability bounding set n'a d'impact que sur le transfert des capabilities, tous les processus root, comme init, qui existaient avant la modification ont conservé la capability, si on est root sur un système, on peut toujours injecter du code dans init afin de charger notre module.
Deuxièmement, il est toujours possible de charger un module sans cette
capability : à l'aide de CAP_SYS_RAWIO, on peut par exemple utiliser
/proc/kcore et modifier à la volée la mémoire du noyau.
Post-2.6.25
Depuis le 2.6.25, la capability bounding set est devenu local à un
thread. L'appel système prctl() a été enrichi de la
commande PR_CAPBSET_DROP qui permet de supprimer des
privilèges de son capability bounding set.
Étant donné que toutes les règles de transfert de privilèges respectent cette variable, il est dès lors impossible de récupérer une capability perdue.
Exemple:
# cat > dummy.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/capability.h>
#include <sys/prctl.h>
int main(void) {
char *args[]= {"bash", "-i", NULL };
if (prctl(PR_CAPBSET_DROP, CAP_NET_RAW, 0, 0, 0) != 0) {
perror("prctl()");
return 1;
}
execve("/bin/bash", args, NULL);
}
# gcc -o dummy dummy.c
# ./dummy
inside# getpcaps $$
Capabilities for `17817': =ep cap_net_raw-ep
inside# tcpdump -ni eth0
tcpdump: eth0: You don't have permission to capture on that device
(socket: Operation not permitted)
inside# su - guest
guest@local:~$ su -
Password:
local:~# getpcaps $$
Capabilities for `17966': =ep cap_net_raw-ep
local:~# tcpdump -ni eth0
tcpdump: eth0: You don't have permission to capture on that device
(socket: Operation not permitted)
Ici, malgré les changements d'identité, il n'a pas été possible de récupérer la capability perdue.
Implémentation
Erreurs de jeunesse
Oubli de checks
Après de multiple séries de patches interminables, toutes les vérifications dans le noyau se basent sur une capability particulière plutôt que de simplement vérifier si on est root ou non.
Néanmoins, des vulnérabilités sont de temps en temps remontés sur le manque de check, comme c'était le cas samedi dernier.
Ce changement de méthode d'autorisation impose donc de conserver une compatibilité sur l'existant. Dans le cas normal, les choses sont relativement simples, mais dès qu'on se penche plus en avant sur les détails, les choses se compliquent : prendre en compte les binaires avec bit set-user-id, la complexité de l'appel système setuid(), etc.
L'introduction des capabilities a ouvert une vulnérabilité dans des logiciels mal programmés à cause d'une erreur de logique.
Vulnérabilité userspace
Pour émuler les capabilities sur des processus classiques, le noyau
est obligé de donner des valeurs initiales aux différents ensembles
de permission afin de calquer le comportement UNIX traditionnel, c'est
pour cette raison que les permissions héritées de tous les processus
avaient pI = ~0 (c'est à dire toutes les privilèges étaient hérités).
À l'époque, les règles de transition étaient les suivantes :
pI' = pI
pP' = (X & fP) | (pI & fI)
pE' = fE & pP'
Contrairement au comportement actuel, les capabilities permises
étaient écrasées par les capabities héritées masquées par les file
capabilities héritées (pI & fI) et ces permissions permises étaient
rendues effectives (modulo fE).
Puisque par défaut pI = ~0, un processus pouvait s'enlever des
privilèges de ses capabilities héritées, donc de ses permissions
permises et donc de ses permissions effectives.
Les règles de transition pour émuler le bit setuserid0 était le suivant :
if (uid==0 or file-to-exec-is-setuid0) {
fE = fI = ~0;
fP = 0;
} else {
fI = fP = fE = 0;
}
C'est à dire que si un processus lambda se supprimait une capability héritée (qu'on notera pI-) et qu'il executait un programme setuserid0, alors la formule de transition se transformait en :
pE' = (pI & fI) = pI- & ~0 = pI-
Quel intérêt ? Comme beaucoup de logiciels (ou de crackme :p),
sendmail ne vérifiait pas le code de retour de setuid(), comme dans
l'exemple suivant qui tournait avec uid=0 :
setuid(user);
execve(user_controlled_program);
Dans le noyau, setuid() vérifie la capability CAP_SETUID
pour autoriser ou non le changement d'utilisateur.
L'attaquant avait alors à supprimer cette capability de son héritage,
exécuter sendmail en demandant d'exécuter /bin/bash. Au
moment du setuid(), le noyau refuserait et le programme continuerait
son flot en restant uid=0.
Bien qu'ici, la vulnérabilité soit majoritairement la faute de sendmail et non du noyau, les développeurs du noyau ont modifiés les règles de transitions et de valeurs initiales d'émulation.
Intérêt
Supprimer tous les programmes setuserid0
Fedora a décidé de supprimer tous les binaires setuserid0 afin de d'utiliser les capabilities à la place, par exemple pour ping :
# ls -alh /bin/ping
-rwsr-xr-x 1 root root 33K 2007-12-10 05:20 /bin/ping
# chmod -s /bin/ping
# setcap cap_net_raw=e /bin/ping
# setcap cap_net_raw=ep /bin/ping
# ls -alh /bin/ping
-rwxr-xr-x 1 root root 33K 2007-12-10 05:20 /bin/ping
# getcap /bin/ping
/bin/ping = cap_net_raw+ep
L'intérêt est évident : si le programme est exploité par un attaquant,
seul la capability est compromise et pas le reste du système (même
s'il faut garder à l'esprit l'histoire du CAP_SYS_MOD et
CAP_SYS_RAWIO).
Pourquoi cela n'est pas applicable sur toutes les distributions ? Deux raisons :
La première est que le noyau nécessite le support des file capabilities, ce qui a été ajouté dans le 2.6.24 (donc Debian Etch est hors concours).
La deuxième est que les file capabilities sont stockées dans les attributs étendus des fichiers (xattr). Ces xattr ne sont pas compatibles avec tous les filesystems. De plus, il faut que les outils d'installation (comme dpkg, ou tar au bout du compte) les supportent également.
Pour toutes ces raisons, il est difficile d'imposer cette
solution. Alors il y a 284 jours (d'après le rapport de bug), un
patch avait été soumis afin de supporter les capabilities dans start-stop-daemon, le logiciel qui démarre la majorité
des daemons sous Debian.
start-stop-daemon / minijail
L'idée est : puisqu'on ne peut pas modifier les paquets existant, on peut toujours modifier la façon dont ils sont lançés.
L'option --dropcap permet alors de jeter certaines capabilities
avant de lançer le daemon. In fine, le plan était de :
- Pousser les modifications dans
start-stop-daemon - Déterminer les capabilities nécessaires à chaque daemon d'un serveur classique
- Pousser ces données à chaque mainteneur de paquet
284 jours plus tard, la situation est restée bloquée au premier point par manque de temps (et de réaction côté Debian) :)
À vrai dire, c'est grâce à ChromeOS que je me suis rappellé de ce
post en cours d'écriture et du travail non achevé pour
start-stop-daemon. En effet, les développeurs de Google
ont implémenté
minijail qui implémente la relache de capabilities (et plein d'autres fonctions).