Eintauchen in Linux-Namespaces, Teil 2

Im vorherigen Teil haben wir nur unsere Zehen in die Gewässer des Namespace getaucht und gleichzeitig gesehen, wie einfach es war, den Prozess in einem isolierten UTS-Namespace zu starten. In diesem Beitrag werden wir den Benutzernamensraum behandeln.


Neben anderen sicherheitsrelevanten Ressourcen isolieren Benutzernamensräume die Kennungen von Benutzern und Gruppen im System. In diesem Beitrag konzentrieren wir uns ausschließlich auf Benutzer- und Gruppen-ID-Ressourcen (UID bzw. GID), da diese eine grundlegende Rolle bei der Durchführung von Berechtigungsprüfungen und anderen sicherheitsrelevanten Aktivitäten im gesamten System spielen.


Unter Linux sind diese IDs einfach Ganzzahlen, die Benutzer und Gruppen im System identifizieren. Einige von ihnen werden jedem Prozess zugewiesen, um festzulegen, auf welche Vorgänge / Ressourcen dieser Prozess zugreifen kann und auf welche nicht. Die Fähigkeit eines Prozesses, Schaden zu verursachen, hängt von den Berechtigungen ab, die den zugewiesenen IDs zugeordnet sind.


Benutzernamensräume


Wir werden die Funktionen von Benutzernamensräumen nur anhand von Benutzer-IDs veranschaulichen. Genau die gleichen Aktionen gelten für Gruppen-IDs, auf die wir später in diesem Beitrag eingehen werden.

Der Benutzernamensraum verfügt über eine eigene Kopie der Benutzer- und Gruppenkennungen. Durch die Isolation können Sie den Prozess abhängig vom aktuellen Benutzernamensraum, zu dem er derzeit gehört, einem anderen Satz von IDs zuordnen. Beispielsweise kann der $pid Prozess von root (UID 0) im Benutzernamensraum P ausgeführt werden und wird plötzlich vom proxy (UID 13) weiter ausgeführt, nachdem zu einem anderen Benutzernamensraum Q gewechselt wurde .


Benutzerbereiche können verschachtelt werden! Dies bedeutet, dass eine Instanz eines benutzerdefinierten Namespace (übergeordnetes Element) null oder mehr untergeordnete Namespaces haben kann und jeder untergeordnete Namespace wiederum seine eigenen untergeordneten Namespaces usw. haben kann (bis das Limit von 32 Verschachtelungsebenen erreicht ist). Wenn ein neuer Namespace C erstellt wird, legt Linux den aktuellen Benutzernamensraum des Prozesses P fest , der C als übergeordnetes Element für C erstellt. Dies kann später nicht mehr geändert werden. Infolgedessen haben alle Benutzernamensräume genau ein übergeordnetes Element und bilden eine baumartige Struktur von Namespaces. Und wie bei Bäumen befindet sich eine Ausnahme von dieser Regel oben, wo wir den Stamm- (oder anfänglichen, Standard-) Namespace haben. Dies ist höchstwahrscheinlich der Benutzernamensraum, zu dem alle Ihre Prozesse gehören, da dies nicht der einzige Benutzernamensraum seit dem Start des Systems ist, wenn Sie noch keine Art von Containermagie ausführen.


In diesem Beitrag verwenden wir die Eingabeaufforderungen P $ und C $, um die Shell anzugeben, die derzeit im übergeordneten P- bzw. untergeordneten C- Benutzernamensraum ausgeführt wird.

Benutzer-ID-Zuordnungen


Der Benutzernamensraum enthält tatsächlich eine Reihe von Kennungen und einige Informationen, die diese IDs mit einer Reihe von IDs eines anderen Benutzernamensraums verbinden. Dieses Duett definiert eine vollständige Vorstellung der IDs der im System verfügbaren Prozesse. Mal sehen, wie es aussehen könnte:


 P$ whoami iffy P$ id uid=1000(iffy) gid=1000(iffy) 

In einem anderen Terminalfenster starten wir die Shell mit unshare (das Flag -U erstellt einen Prozess im neuen Benutzernamensraum):


 P$ whoami iffy P$ unshare -U bash #    ,     user namespace C$ whoami nobody C$ id uid=65534(nobody) gid=65534(nogroup) C$ ls -l my_file -rw-r--r-- 1 nobody nogroup 0 May 18 16:00 my_file 

Moment mal, wer? Jetzt, wo wir uns in einer verschachtelten Shell in C befinden , wird der aktuelle Benutzer zu niemandem? Wir haben möglicherweise vermutet, dass der Prozess eine andere Art von ID hat, da C ein neuer Benutzernamensraum ist. Deshalb haben wir wahrscheinlich nicht erwartet, dass er iffy bleibt, aber nobody ist nicht lustig. Auf der anderen Seite ist es großartig, weil wir die Isolation haben, die wir wollten. Unser Prozess hat jetzt eine andere (wenn auch fehlerhafte) ID-Ersetzung im System - derzeit sieht er jeden als nobody und jede Gruppe als nogroup .


Informationen, die eine UID von einem Benutzernamensraum mit einem anderen verknüpfen, werden als Benutzer-ID-Zuordnung bezeichnet . Es handelt sich um eine Nachschlagetabelle für übereinstimmende IDs im aktuellen Benutzernamensraum für IDs in einem anderen Namespace. Jeder Benutzernamensraum ist genau einer UID-Zuordnung zugeordnet (zusätzlich zu einer anderen GID-Zuordnung für die Gruppen-ID).


Diese Zuordnung ist das, was in unserer unshare Shell unshare . Es stellt sich heraus, dass neue Benutzernamensräume mit einer leeren Zuordnung beginnen. Infolgedessen verwendet Linux standardmäßig den schrecklichen Benutzer, den nobody . Wir müssen dies beheben, bevor wir nützliche Arbeiten in unserem neuen Namespace ausführen können. Derzeit setuid beispielsweise Systemaufrufe (z. B. setuid ) fehl, die versuchen, mit der UID zu arbeiten. Aber keine Angst! Gemäß der All-is-File- Tradition präsentiert Linux diese Zuordnung mithilfe des Dateisystems /proc in /proc/$pid/uid_map (in /proc/$pid/gid_map für die GID), wobei $pid die Prozess-ID ist. Wir werden diese beiden Dateien als Map-Dateien bezeichnen.


Kartendateien


Kartendateien sind spezielle Dateien im System. Was ist das Besondere? Nun, indem Sie jedes Mal, wenn Sie daraus lesen, unterschiedliche Inhalte zurückgeben, je nachdem, was Ihr Prozess liest. Beispielsweise gibt die Map-Datei /proc/$pid/uid_maps die Zuordnung von UIDs aus dem Benutzernamensraum zurück, zu dem der $pid Prozess gehört, UIDs im Benutzernamensraum des Leseprozesses. Infolgedessen kann der an Prozess X zurückgegebene Inhalt von dem an Prozess Y zurückgegebenen Inhalt abweichen, selbst wenn dieselbe Zuordnungsdatei gleichzeitig gelesen wird.


Insbesondere Prozess X , der die UID-Zuordnungsdatei /proc/$pid/uid_map , empfängt eine Reihe von Zeichenfolgen. Jede Zeile ordnet dem Benutzernamenraum C des $pid Prozesses einen fortlaufenden Bereich von UIDs zu, der einem Bereich von UIDs in einem anderen Namespace entspricht.


Jede Zeile hat das Format $fromID $toID $length , wobei:


  • $fromID ist die Start-UID des Bereichs für den Benutzernamensraum des $pid Prozesses
  • $lenght ist die Länge des Bereichs.
  • Die Übersetzung von $toID hängt vom Lesevorgang X ab. Wenn X zu einem anderen Benutzernamensraum U gehört , ist $toID die Start-UID des Bereichs in U , der von $fromID . Andernfalls ist $toID die Start-UID des Bereichs in P , dem übergeordneten Benutzernamensraum von Prozess C.

Wenn ein Prozess beispielsweise die Datei /proc/1409/uid_map und unter den empfangenen Zeilen 15 22 5 /proc/1409/uid_map die /proc/1409/uid_map 15 bis 19 im Benutzernamensraum des Prozesses 1409 UIDs 22-26 eines separaten Benutzernamensraums des Leseprozesses zugeordnet.


Wenn andererseits ein Prozess aus der Datei /proc/$$/uid_map (oder einer Zuordnungsdatei eines Prozesses, der zum selben Benutzernamensraum wie der Lesevorgang gehört) liest und 15 22 5 empfängt, werden UIDs von 15 bis 19 in Der Benutzernamensraum C wird UIDs von 22 bis 26 des übergeordneten C- Benutzernamens zugeordnet.


Probieren wir es aus:


 P$ echo $$ 1442 #   user namespace... C$ echo $$ 1409 # C      ,     C$ cat /proc/1409/uid_map #  #   namespace P      # UIDs    UID    P$ cat /proc/1442/uid_map 0 0 4294967295 # UIDs  0  4294967294  P  #  4294967295 -  ID no user -  C. C$ cat /proc/1409/uid_map 0 4294967295 4294967295 

Nun, das war nicht sehr aufregend, da dies zwei Extremfälle waren, aber das sagt ein paar Dinge aus:


  1. Der neu erstellte Benutzernamensraum enthält tatsächlich leere Kartendateien.
  2. Die UID 4294967295 ist nicht abbildbar und auch für die Verwendung im root Benutzernamensraum ungeeignet. Linux verwendet diese UID speziell, um das Fehlen einer Benutzer-ID anzuzeigen.

Schreiben von UID-Map-Dateien


Um unseren neu erstellten Benutzernamensraum C zu reparieren, müssen wir nur die erforderlichen Zuordnungen bereitstellen, indem wir deren Inhalt in Zuordnungsdateien für jeden Prozess schreiben, der zu C gehört (wir können diese Datei nach dem Schreiben nicht aktualisieren). Das Schreiben in diese Datei sagt Linux zwei Dinge:


  1. Welche UIDs sind für Prozesse verfügbar, die sich auf den Zielbenutzernamensraum C beziehen?
  2. Welche UIDs im aktuellen Benutzernamensraum entsprechen den UIDs in C.

Wenn wir beispielsweise Folgendes aus dem übergeordneten Benutzernamensraum P in die Zuordnungsdatei für den untergeordneten C- Namespace schreiben:


 0 1000 1 3 0 1 

Wir sagen Linux im Wesentlichen, dass:


  1. Für Prozesse in C sind die einzigen im System vorhandenen UIDs die UIDs 0 und 3 . Beispielsweise endet der Systemaufruf setuid(9) immer mit einer ungültigen Benutzer-ID .
  2. Die UIDs 1000 und 0 in P entsprechen den UIDs 0 und 3 in C. Wenn beispielsweise ein Prozess, der mit UID 1000 in P ausgeführt wird, auf C umschaltet, stellt er fest, dass seine UID nach dem Umschalten zu root 0 .

Namespace und Berechtigungsinhaber


In einem früheren Beitrag haben wir erwähnt, dass beim Erstellen neuer Namespaces ein Zugriff mit Superuser-Ebene erforderlich ist. Benutzernamensräume stellen diese Anforderung nicht. Ein weiteres Merkmal ist, dass sie andere Namespaces besitzen können.


Immer wenn ein Nichtbenutzernamensraum N erstellt wird , weist Linux den aktuellen Benutzernamensraum P des Prozesses, der N erstellt , als Eigentümer des Namespace N zu. Wenn P zusammen mit anderen Namespaces im selben clone wird, stellt Linux sicher, dass P zuerst erstellt und zum Eigentümer anderer Namespaces gemacht wird.


Der Eigentümer von Namespaces ist wichtig, da bei einem Prozess, der eine privilegierte Aktion für eine Ressource anfordert, die kein Benutzernamensraum ist, die UID-Berechtigungen gegen den Eigentümer dieses Benutzernamensraums und nicht gegen den Stammbenutzernamensraum geprüft werden. Angenommen, P ist der übergeordnete Benutzernamensraum des untergeordneten C , und P und C besitzen ihren eigenen Netzwerk-Namespace M bzw. N. Ein Prozess verfügt möglicherweise nicht über Berechtigungen zum Erstellen der in M enthaltenen Netzwerkgeräte, kann dies jedoch möglicherweise für N tun .


Die Konsequenz eines Namespace-Besitzers für uns ist, dass wir die sudo Anforderung unshare isolate wenn Befehle mit unshare oder isolate wenn wir auch die Erstellung eines Benutzernamensraums anfordern. Zum Beispiel erfordert unshare -u bash sudo , aber unshare -Uu bash ist nicht mehr:


 # UID 1000 --      user namespace P. P$ id uid=1000(iffy) gid=1000(iffy) #           # network namespace. P$ ip link add type veth RTNETLINK answers: Operation not permitted #     ,     #  user  network namespace P$ unshare -nU bash # :  sudo C$ ip link add type veth RTNETLINK answers: Operation not permitted # ,  . ,  # UID 0 (root)    ,  #     nobody.   . C$ echo $$ 13294 #   P,   UID 1000  P  UID 0  C P$ echo "0 1000 1" > /proc/13294/uid_map #   ? C$ id uid=0(root) gid=65534(nogroup) C$ ip link add type veth # ! 

Leider werden wir die Superuser-Anforderung im nächsten Beitrag erneut anwenden, da isolate root Berechtigungen im Root-Benutzernamensraum benötigt, um den Mount- und Netzwerk-Namespace korrekt zu konfigurieren. Aber wir werden sicherlich die Berechtigungen des Teamprozesses fallen lassen, um sicherzustellen, dass das Team nicht über unnötige Berechtigungen verfügt.

Wie IDs aufgelöst werden


Wir haben gerade einen Prozess gesehen, der ausgeführt wurde, als ein regulärer Benutzer 1000 plötzlich zu root . Keine Sorge, es gab keine Eskalation der Privilegien. Denken Sie daran, dass dies nur eine Zuordnungs- ID ist: Während unser Prozess denkt, dass es sich um den root auf dem System handelt, weiß Linux, dass root in seinem Fall die übliche UID 1000 (dank unserer Zuordnung). Zu einer Zeit, in der Namespaces, die zu seinem neuen Benutzernamensraum gehören (wie der Netzwerknamespace in C ), seine Rechte als root , tun dies andere (wie der Netzwerknamespace in P ) nicht. Daher kann der Prozess nichts tun, was der Benutzer 1000 nicht könnte.


Wenn ein Prozess in einem verschachtelten Benutzernamensraum eine Operation ausführt, für die eine Berechtigungsprüfung erforderlich ist, z. B. das Erstellen einer Datei, wird seine UID in diesem Benutzernamensraum mit der entsprechenden Benutzer-ID im Stammbenutzernamensraum verglichen, indem die Zuordnungen im Namespace-Baum zum Stamm übertragen werden. Es gibt eine Bewegung in die entgegengesetzte Richtung, zum Beispiel wenn er Benutzer-IDs liest, wie wir es mit ls -l my_file . Die UID des Besitzers my_file vom my_file dem aktuellen zugeordnet, und die endgültige entsprechende ID (oder niemand, wenn die Zuordnung irgendwo im gesamten Baum fehlte) wird dem Lesevorgang übergeben.


Gruppen-ID


Selbst wenn wir in C verwurzelt waren, sind wir immer noch mit der schrecklichen nogroup als Gruppen-ID verbunden. Wir müssen dasselbe für die entsprechende /proc/$pid/gid_map . Bevor wir dies tun können, müssen wir den setgroups deaktivieren (dies ist nicht erforderlich, wenn unser Benutzer bereits über eine CAP_SETGID Funktion in P verfügt, dies wird jedoch nicht angenommen, da dies normalerweise mit Superuser-Berechtigungen verbunden ist), indem wir "verweigern" schreiben "zur Datei proc/$pid/setgroups :


 #  13294 -- pid  unshared  C$ id uid=0(root) gid=65534(nogroup) P$ echo deny > /proc/13294/setgroups P$ echo "0 1000 1" > /proc/13294/gid_map #  group ID   C$ id uid=0(root) gid=0(root) 

Implementierung


Den Quellcode für diesen Beitrag finden Sie hier .

Wie Sie sehen, gibt es viele Schwierigkeiten bei der Verwaltung von Benutzernamensräumen, aber die Implementierung ist recht einfach. Wir müssen nur ein paar Zeilen in eine Datei schreiben - es war trostlos herauszufinden, was und wo wir schreiben sollen. Hier sind ohne weiteres unsere Ziele:


  1. Klonen Sie einen Teamprozess in einem eigenen Benutzernamensraum.
  2. Schreiben Sie in die UID- und GID-Map-Dateien des Teamprozesses.
  3. Setzen Sie alle Superuser-Berechtigungen zurück, bevor Sie den Befehl ausführen.

1 erreicht, indem einfach das CLONE_NEWUSER Flag zu unserem CLONE_NEWUSER hinzugefügt wird.


 int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER; 

Für 2 fügen wir die Funktion prepare_user_ns , die sorgfältig einen regulären Benutzer 1000 als 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); } 

Und wir werden es vom Hauptprozess im übergeordneten Benutzernamensraum aufrufen, bevor wir den Befehlsprozess signalisieren.


  ... //      . int pipe = params.fd[1]; //      namespace ... prepare_userns(cmd_pid); //   ,     . ... 

In Schritt 3 aktualisieren wir die Funktion cmd_exec , um sicherzustellen, dass der Befehl von dem üblichen nicht privilegierten Benutzer 1000 , den wir in der Zuordnung angegeben haben (denken Sie daran, dass der cmd_exec 0 im Benutzernamensraum des Teamprozesses Benutzer 1000 ):


  ... //   ' '   . await_setup(params->fd[0]); if (setgid(0) == -1) die("Failed to setgid: %m\n"); if (setuid(0) == -1) die("Failed to setuid: %m\n"); ... 

Und das ist alles! isolate startet den Prozess jetzt in einem isolierten Benutzernamensraum.


 $ ./isolate sh ===========sh============ $ id uid=0(root) gid=0(root) 

In diesem Beitrag gab es einige Details zur Funktionsweise von Benutzernamensräumen, aber am Ende war das Einrichten der Instanz relativ schmerzlos. Im nächsten Beitrag werden wir die Möglichkeit untersuchen, einen Befehl in unserem eigenen Mount-Namespace mit isolate Dockerfile (das Geheimnis hinter der FROM Anweisung aus der Dockerfile ). Dort müssen wir Linux ein bisschen mehr helfen, um die Instanz richtig zu konfigurieren.

Source: https://habr.com/ru/post/de459574/


All Articles