Dans la partie précédente, nous avons simplement plongé nos orteils dans les eaux de l'espace de noms et en même temps , nous avons vu à quel point il était facile de démarrer le processus dans un espace de noms UTS isolé. Dans cet article, nous couvrirons l'espace de noms des utilisateurs.
Parmi les autres ressources liées à la sécurité, les espaces de noms d'utilisateurs isolent les identifiants des utilisateurs et des groupes du système. Dans cet article, nous nous concentrerons uniquement sur les ressources d'ID utilisateur et de groupe (UID et GID, respectivement), car elles jouent un rôle fondamental dans la vérification des autorisations et d'autres activités liées à la sécurité dans tout le système.
Sous Linux, ces ID sont simplement des entiers qui identifient les utilisateurs et les groupes dans le système. Et certains d'entre eux sont affectés à chaque processus afin de définir à quelles opérations / ressources ce processus peut et ne peut pas accéder. La capacité d'un processus à nuire dépend des autorisations associées aux ID attribués.
Espaces de noms d'utilisateurs
Nous allons illustrer les capacités des espaces de noms d'utilisateurs en utilisant uniquement des ID utilisateur. Exactement les mêmes actions s'appliquent aux identifiants de groupe, que nous aborderons plus loin dans ce post.
L'espace de noms des utilisateurs possède sa propre copie des identifiants d'utilisateurs et de groupes. L'isolement vous permet ensuite d'associer le processus à un autre ensemble d'ID, en fonction de l'espace de noms d'utilisateurs auquel il appartient actuellement. Par exemple, le processus $pid
peut s'exécuter à partir de la root
(UID 0) dans l'espace de noms utilisateur P et continue soudainement à s'exécuter à partir du proxy
(UID 13) après être passé à un autre espace de noms d'utilisateurs Q.
Les espaces utilisateurs peuvent être imbriqués! Cela signifie qu'une instance d'un espace de noms personnalisé (parent) peut avoir zéro ou plusieurs espaces de noms enfants, et chaque espace de noms enfants peut, à son tour, avoir ses propres espaces de noms enfants et ainsi de suite ... (jusqu'à atteindre la limite de 32 niveaux d'imbrication). Lorsqu'un nouvel espace de noms C est créé, Linux définit l'espace de noms utilisateur actuel du processus P créant C comme parent pour C et cela ne peut pas être modifié ultérieurement. Par conséquent, tous les espaces de noms d'utilisateurs ont exactement un parent, formant une structure arborescente d'espaces de noms. Et, comme dans le cas des arbres, une exception à cette règle se trouve en haut, où nous avons l'espace de noms racine (ou initial, par défaut). Si vous ne faites pas déjà une sorte de magie de conteneur, c'est probablement l'espace de noms utilisateur auquel tous vos processus appartiennent, car c'est le seul espace de noms utilisateur depuis le démarrage du système.
Dans cet article, nous utiliserons les invites de commande P $ et C $ pour indiquer le shell qui s'exécute actuellement dans l'espace de noms utilisateur parent P et C enfant respectivement.
Mappages d'ID utilisateur
L'espace de noms d'utilisateurs, en fait, contient un ensemble d'identifiants et quelques informations reliant ces ID à un ensemble d'ID d'autres espaces de noms d'utilisateurs - ce duo définit une idée complète des ID des processus disponibles dans le système. Voyons à quoi cela pourrait ressembler:
P$ whoami iffy P$ id uid=1000(iffy) gid=1000(iffy)
Dans une autre fenêtre de terminal, commençons le shell en utilisant unshare
(le drapeau -U
crée un processus dans le nouvel espace de noms utilisateur):
P$ whoami iffy P$ unshare -U bash
Attends une minute, qui? Maintenant que nous sommes dans un shell imbriqué en C , l'utilisateur actuel devient personne? Nous pourrions avoir deviné que puisque C est un nouvel espace de noms d'utilisateurs, le processus peut avoir un type d'ID différent. Par conséquent, nous ne nous attendions probablement pas à ce qu'il reste iffy
, mais nobody
n'est pas drôle. D'un autre côté, c'est super parce que nous avons obtenu l'isolement que nous voulions. Notre processus a maintenant une substitution d'ID différente (quoique cassée) dans le système - actuellement, il voit tout le monde comme nobody
et chaque groupe comme nogroup
.
Les informations liant un UID d'un espace de noms d'utilisateur à un autre sont appelées mappage d'ID utilisateur . Il s'agit d'une table de recherche pour faire correspondre les ID dans l'espace de noms d'utilisateur actuel pour les ID dans d'autres espaces de noms et chaque espace de noms d'utilisateurs est associé à exactement un mappage UID (en plus d'un autre mappage GID pour l'ID de groupe).
Ce mappage est ce qui est cassé dans notre shell unshare
. Il s'avère que les nouveaux espaces de noms d'utilisateurs commencent par un mappage vide et, par conséquent, Linux utilise l'horrible utilisateur nobody
par défaut. Nous devons résoudre ce problème avant de pouvoir effectuer tout travail utile dans notre nouvel espace de noms. Par exemple, actuellement, les appels système (tels que setuid
) qui tentent de travailler avec l'UID échouent. Mais n'ayez pas peur! Fidèle à la tradition du tout-fichier , Linux présente ce mappage en utilisant le système de fichiers /proc
dans /proc/$pid/uid_map
(dans /proc/$pid/gid_map
pour le GID), où $pid
est l'ID du processus. Nous appellerons ces deux fichiers des fichiers de mappage.
Fichiers de carte
Les fichiers de carte sont des fichiers spéciaux du système. Quelles sont les particularités? Eh bien, en renvoyant des contenus différents à chaque lecture, selon ce que votre processus lit. Par exemple, le fichier de mappage /proc/$pid/uid_maps
renvoie le mappage à partir des UID de l'espace de noms utilisateur auquel appartient le processus $pid
, des UID dans l'espace de noms utilisateur du processus de lecture. Et, par conséquent, le contenu renvoyé au processus X peut différer de ce qui est retourné au processus Y , même s'il lit le même fichier de mappage en même temps.
En particulier, le processus X , qui lit le fichier de carte UID /proc/$pid/uid_map
, reçoit un ensemble de chaînes. Chaque ligne mappe une plage continue d'UID à l'espace de noms utilisateur C du processus $pid
, correspondant à une plage d'UID dans un autre espace de noms.
Chaque ligne a le format $fromID $toID $length
, où:
$fromID
est l'UID de départ de la plage pour l'espace de noms utilisateur du processus $pid
$lenght
est la longueur de la plage.- La traduction de
$toID
dépend du processus de lecture X. Si X appartient à un autre espace de noms utilisateur U , alors $toID
est l'UID de départ de la plage en U qui mappe à partir de $fromID
. Sinon, $toID
est l'UID de début de la plage dans P , l'espace de noms utilisateur parent du processus C.
Par exemple, si un processus lit le fichier /proc/1409/uid_map
et parmi les lignes reçues, vous pouvez voir 15 22 5
, puis les UID de 15 à 19 dans l'espace de noms utilisateur du processus 1409
mappés aux UID 22-26 d'un espace de noms utilisateur distinct du processus de lecture.
D'un autre côté, si un processus lit le fichier /proc/$$/uid_map
(ou un fichier de mappage de n'importe quel processus appartenant au même espace de noms d'utilisateur que le processus de lecture) et reçoit 15 22 5
, alors les UID de 15 à 19 dans l'espace de noms utilisateur C correspond aux UID de 22 à 26 du parent pour l'espace de noms utilisateur C.
Essayons-le:
P$ echo $$ 1442
Eh bien, ce n'était pas très excitant, car il s'agissait de deux cas extrêmes, mais cela dit quelques choses:
- L'espace de noms utilisateur nouvellement créé aura en fait des fichiers de carte vides.
- L'UID 4294967295 n'est pas mappable et ne peut pas être utilisé même dans l'espace de noms des utilisateurs
root
. Linux utilise cet UID spécifiquement pour indiquer l' absence d'un ID utilisateur .
Écriture de fichiers de carte UID
Pour corriger notre nouvel espace de noms d'utilisateurs C , nous avons juste besoin de fournir nos mappages nécessaires en écrivant leur contenu dans des fichiers de mappage pour tout processus appartenant à C (nous ne pouvons pas mettre à jour ce fichier après y avoir écrit). L'écriture dans ce fichier indique à Linux deux choses:
- Quels UID sont disponibles pour les processus liés à l'espace de noms d'utilisateur cible C.
- Quels UID dans l'espace de noms d'utilisateur actuel correspondent aux UID en C.
Par exemple, si nous écrivons ce qui suit à partir de l'espace de noms utilisateur parent P dans le fichier de mappage pour l'espace de noms C enfant:
0 1000 1 3 0 1
nous disons essentiellement à Linux que:
- Pour les processus en C , les seuls UID qui existent dans le système sont les UID
0
et 3
. Par exemple, l'appel système setuid(9)
se terminera toujours par quelque chose comme un ID utilisateur non valide . - Les UID
1000
et 0
dans P correspondent aux UID 0
et 3
dans C. Par exemple, si un processus exécuté avec l'UID 1000
dans P bascule vers C , il constatera qu'après la commutation, son UID est devenu root
0
.
Propriétaire d'espace de noms et de privilèges
Dans un article précédent, nous avons mentionné que lors de la création de nouveaux espaces de noms, l'accès avec le niveau superutilisateur est requis. Les espaces de noms des utilisateurs n'imposent pas cette exigence. En fait, une autre caractéristique est qu'ils peuvent posséder d' autres espaces de noms.
Chaque fois qu'un espace de noms non utilisateur N est créé , Linux attribue l'espace de noms utilisateur actuel P du processus qui crée N comme propriétaire de l' espace de noms N. Si P est créé avec d'autres espaces de noms dans le même appel système clone
, Linux garantit que P sera créé en premier et deviendra le propriétaire d'autres espaces de noms.
Le propriétaire des espaces de noms est important car un processus demandant une action privilégiée sur une ressource qui n'est pas un espace de noms utilisateur verra ses privilèges UID comparés au propriétaire de cet espace de noms utilisateur et non à l'espace de noms utilisateur racine. Par exemple, supposons que P est l'espace de noms utilisateur parent de l'enfant C , et P et C possèdent leur propre espace de noms réseau M et N, respectivement. Un processus peut ne pas avoir les privilèges pour créer les périphériques réseau inclus dans M , mais il peut être en mesure de le faire pour N.
La conséquence d'avoir un propriétaire d'espace de noms pour nous est que nous pouvons supprimer l'exigence sudo
lors de l'exécution de commandes en utilisant unshare
ou unshare
si nous demandons également la création d'un espace de noms utilisateur. Par exemple, unshare -u bash
nécessitera sudo
, mais unshare -Uu bash
ne sera plus:
Malheureusement, nous réappliquerons l'exigence de superutilisateur dans le prochain post, car isolate
besoin root
privilèges root
dans l'espace de noms utilisateur root afin de configurer correctement les espaces de noms Mount et Network. Mais nous supprimerons sûrement les privilèges du processus d'équipe pour nous assurer que l'équipe ne dispose pas d'autorisations inutiles.
Comment les identifiants sont résolus
Nous venons de voir un processus en cours d'exécution en tant qu'utilisateur régulier 1000
soudainement passé à root
. Ne vous inquiétez pas, il n'y a pas eu d'escalade de privilèges. Rappelez-vous qu'il ne s'agit que d'un ID de mappage : alors que notre processus pense qu'il s'agit de l' root
sur le système, Linux sait que root
- dans son cas - signifie l'UID 1000
habituel (grâce à notre mappage). Donc, à un moment où les espaces de noms appartenant à son nouvel espace de noms d'utilisateurs (comme l'espace de noms de réseau en C ) reconnaissent ses droits en tant que root
, d'autres (comme l'espace de noms de réseau en P ) ne le font pas. Par conséquent, le processus ne peut rien faire que l'utilisateur 1000
ne pourrait pas faire.
Chaque fois qu'un processus dans un espace de noms d'utilisateurs imbriqué effectue une opération qui nécessite une vérification des autorisations - par exemple, la création d'un fichier - son UID dans cet espace de noms d'utilisateurs est comparé à l'ID utilisateur équivalent dans l'espace de noms d'utilisateurs racine en parcourant les mappages de l'arborescence des espaces de noms jusqu'à la racine. Il y a un mouvement dans la direction opposée, par exemple, quand il lit les ID utilisateur, comme nous le faisons avec ls -l my_file
. L'UID du propriétaire my_file
mappé de l'espace de noms de l'utilisateur racine à celui en cours et l'ID correspondant final (ou personne si le mappage était absent quelque part le long de l'arborescence) est donné au processus de lecture.
ID de groupe
Même si nous étions root en C , nous sommes toujours associés au terrible nogroup
comme identifiant de groupe. Nous avons juste besoin de faire la même chose pour le /proc/$pid/gid_map
. Avant de pouvoir faire cela, nous devons désactiver l' setgroups
système setgroups
(ce n'est pas nécessaire si notre utilisateur a déjà une capacité CAP_SETGID
dans P , mais nous ne le supposerons pas, car cela vient généralement avec des privilèges de superutilisateur) en écrivant "deny" "dans le fichier proc/$pid/setgroups
:
Implémentation
Le code source de cet article peut être trouvé ici .
Comme vous pouvez le voir, la gestion des espaces de noms des utilisateurs présente de nombreuses difficultés, mais la mise en œuvre est assez simple. Tout ce que nous devons faire est d'écrire un tas de lignes dans un fichier - c'était triste de savoir quoi et où écrire. Sans plus tarder, voici nos objectifs:
- Clonez un processus d'équipe dans son propre espace de noms d'utilisateurs.
- Écrivez dans les fichiers de mappage UID et GID du processus d'équipe.
- Réinitialisez tous les privilèges de superutilisateur avant d'exécuter la commande.
1
obtenu en ajoutant simplement l'indicateur CLONE_NEWUSER
à notre appel système clone
.
int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER;
Pour 2
nous ajoutons la fonction prepare_user_ns
, qui représente soigneusement un utilisateur régulier 1000
tant que root
.
static void prepare_userns(int pid) { char path[100]; char line[100]; int uid = 1000; sprintf(path, "/proc/%d/uid_map", pid); sprintf(line, "0 %d 1\n", uid); write_file(path, line); sprintf(path, "/proc/%d/setgroups", pid); sprintf(line, "deny"); write_file(path, line); sprintf(path, "/proc/%d/gid_map", pid); sprintf(line, "0 %d 1\n", uid); write_file(path, line); }
Et nous l'appellerons à partir du processus principal dans l'espace de noms de l'utilisateur parent juste avant de signaler le processus de commande.
...
Pour l'étape 3
nous mettons à jour la fonction cmd_exec
pour nous assurer que la commande est exécutée à partir de l'utilisateur non privilégié habituel 1000
que nous avons fourni dans le mappage (rappelez-vous que l'utilisateur root 0
dans l'espace de noms utilisateur du processus d'équipe est l'utilisateur 1000
):
...
Et c'est tout! isolate
démarre maintenant le processus dans un espace de noms utilisateur isolé.
$ ./isolate sh ===========sh============ $ id uid=0(root) gid=0(root)
Il y avait pas mal de détails dans ce post sur le fonctionnement des espaces de noms d'utilisateurs, mais à la fin, la configuration de l'instance était relativement indolore. Dans le prochain article, nous verrons la possibilité d'exécuter une commande dans notre propre espace de noms Mount en utilisant isolate
(révélant le secret derrière l' instruction FROM
du Dockerfile
). Là, nous devrons aider Linux un peu plus afin de configurer correctement l'instance.