(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 :

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 :

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 :

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 :

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 :

  1. Pousser les modifications dans start-stop-daemon
  2. Déterminer les capabilities nécessaires à chaque daemon d'un serveur classique
  3. 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).