Das aus Prozessen und Sitzungen bestehende Linux-Modell als Teil des Security-Alertings und ‑Monitorings

illustration-radar-security.png

Das in Elastic verfügbare Linux-Prozessmodell erlaubt es Nutzer:innen, sehr gezielte Alerting-Regeln zu schreiben und sich genauer anzusehen, was auf ihren Linux-Servern und -Desktops passiert.

In diesem Blogpost erläutern wir das Linux-Prozessmodell, das eine wichtige Rolle dabei spielt, wie Linux-Workloads dargestellt werden.

Linux folgt dem Unix-Prozessmodell aus den 1970er-Jahren, das in den 1980er-Jahren um das Konzept der „Sitzungen“ (Sessions) erweitert wurde. Dies lässt sich zumindest anhand des Veröffentlichungsdatums früher POSIX-Dokumente vermuten, in denen der Systemaufruf setsid() eingeführt wurde.

Das Linux-Prozessmodell ist eine gute Abstraktion für die Aufzeichnung von Computer-Workloads (also welche Programme ausgeführt werden) und für das Schreiben von Regeln, die festlegen, wie auf diese Ereignisse reagiert werden soll. Mit ihm lässt sich ganz klar darstellen, wer was wann auf welchem Server getan hat, was Zwecken wie Alerting, Compliance und Threat Hunting zugutekommt.

Durch die Erfassung von Angaben zur Prozesserstellung, zur Eskalation von Zugriffsrechten und zur Lebensdauer erhalten Nutzer:innen Informationen dazu, wie Anwendungen und Dienste implementiert sind und welche Programmausführungsmuster als normal gelten. Sobald Klarheit darüber herrscht, was bei der Programmausführung normal ist, kann durch Schreiben entsprechender Regeln dafür gesorgt werden, dass bei der Feststellung von abweichendem Verhalten Alerts ausgegeben werden.

Detaillierte Prozessinformationen ermöglichen es, sehr gezielte Regeln für Alerts zu erstellen – wichtig, wenn es darum geht, Fehlalarmen und Alarmmüdigkeit vorzubeugen.  Außerdem ist es auf diese Weise möglich, Linux-Sitzungen einer der folgenden Kategorien zuzuordnen:

    • autonome Dienste, die beim Booten gestartet werden (z. B. cron)
    • Dienste für den Fernzugriff (z. B. sshd)
    • interaktiver Fernzugriff (wahrscheinlich durch Menschen) (z. B. ein über ssh gestartetes Bash-Terminal)
    • nicht interaktiver Fernzugriff (z. B. Installation von Software durch Ansible über ssh)

Durch diese Kategorisierungen sind äußerst präzise Regeln und eine sehr genaue Überprüfung möglich. So könnten zum Beispiel alle interaktiven Sitzungen auf bestimmten Servern in einem bestimmten Zeitraum überprüft werden.

In diesem Blogpost wird beschrieben, wie das Linux-Prozessmodell funktioniert, und er enthält hilfreiche Informationen für das Schreiben von Regeln zur Warnung bei Workload-Ereignissen und zur Reaktion auf sie. Das Linux-Prozessmodell verstehen zu lernen, ist auch ein wichtiger erster Schritt, Container und die Namespaces und cgroups zu verstehen, aus denen sich Container zusammensetzen.

Prozessmodellerfassung vs. Systemaufruf-Logdaten

Das Erfassen von Änderungen am Sitzungsmodell hinsichtlich neuer Prozesse, neuer Sitzungen, vorhandener Prozesse usw. ist einfacher und übersichtlicher als das Erfassen der Systemaufrufe, die zum Umsetzen dieser Änderungen verwendet werden. Bei Linux gibt es etwa 400 Systemaufrufe, die nach ihrer Veröffentlichung nicht mehr überarbeitet werden. Dieser Ansatz sorgt für eine stabile Application Binary Interface (ABI), d. h., Programme, die vor Jahren für Linux kompiliert wurden, sollten auch heute noch unter Linux laufen, ohne dass sie auf der Basis des Quellcodes neu entwickelt werden müssen.

Statt die Systemaufrufe zu überarbeiten (Refactoring), werden zur Verbesserung von Funktionalität oder Sicherheit neue Systemaufrufe hinzugefügt (sodass die ABI intakt bleibt). Das führt jedoch dazu, dass das Zuordnen einer zeitlich geordneten Liste von Systemaufrufen und ihrer Parameter zu den von ihnen ausgeführten logischen Aktionen ein erhebliches Maß an Fachwissen erfordert. Darüber hinaus ermöglichen neuere Systemaufrufe, wie die von io_uring, das Lesen und Schreiben von Dateien und Sockets ohne zusätzliche Systemaufrufe, indem sie den zwischen Kernel- und Nutzerraum zugewiesenen Arbeitsspeicher verwenden.

Im Unterschied dazu ist das Prozessmodell stabil (es hat sich seit den 1970er-Jahren kaum verändert) und deckt dennoch umfassend die Aktionen auf einem System ab, wenn man den Dateizugriff, die Vernetzung und andere logische Operationen einbezieht.

Prozessbildung: Der erste Prozess nach dem Booten ist init

Nach dem Starten erstellt der Linux-Kernel einen speziellen Prozess, den sogenannten init-Prozess. Ein Prozess verkörpert die Ausführung eines oder mehrerer Programme. Der init-Prozess hat immer die Prozess-ID (PID) 1 und wird mit der Nutzer-ID 0 (root) ausgeführt. Die meisten modernen Linux-Distributionen verwenden als ausführbares Programm für ihren init-Prozess systemd.

Die Aufgabe von init besteht darin, die konfigurierten Dienste, wie Datenbanken, Webserver und Dienste für den Fernzugriff, z. B. sshd, zu starten. Diese Dienste sind in der Regel in eigenen Sitzungen gekapselt, was das Starten und Beenden von Diensten vereinfacht, da alle Prozesse jedes Dienstes unter einer gemeinsamen Sitzungs-ID (SID) zusammengefasst werden.

Wenn jemand aus der Ferne, z. B. über das SSH-Protokoll, auf einen sshd-Dienst zugreift, wird für diese:n Nutzer:in eine neue Linux-Sitzung erstellt. Diese Sitzung führt zunächst das Programm aus, das von der remoten Nutzerin bzw. dem remoten Nutzer angefordert wurde – häufig eine interaktive Shell –, und der oder die verbundenen Prozesse haben alle dieselbe SID.

Die Funktionsweise der Erstellung eines Prozesses

Mit Ausnahme des init-Prozesses haben alle Prozesse genau einen übergeordneten Prozess. Jeder Prozess hat eine PPID, bei der es sich um die Prozess-ID des übergeordneten Prozesses handelt (bei init lautet die ID „0/no-parent“). Wenn ein übergeordneter Prozess so beendet wird, dass die untergeordneten Prozesse nicht ebenfalls beendet werden, kann es zum „Reparenting“ kommen.

Dabei wird in der Regel init als neuer übergeordneter Prozess (parent) ausgewählt; init verfügt über speziellen Code, der dafür sorgt, dass nach Beendigung dieser adoptierten untergeordneten Prozesse (children) aufgeräumt wird. Ohne diese Adoption und den Aufräum-Code würden aus den verwaisten untergeordneten Prozessen „Zombie“-Prozesse werden (das ist kein Witz!). Sie würden so lange quasi untot weiterexistieren, bis der zugehörige übergeordnete Prozess sie wieder „übernimmt“ (das sogenannte „Reapen“), um deren Beenden-Code zu prüfen – ein Indikator dafür, ob das untergeordnete Programm seine Aufgaben erfolgreich abgeschlossen hat.

Mit dem Aufkommen von „Containern“, insbesondere von pid-Namespaces, wurde es nötig, andere Prozesse als init als „Sub-Reaper“ (Prozesse, die bereit sind, verwaiste Prozesse zu adoptieren) festlegen zu können. Sub-Reaper sind typischerweise der erste Prozess in einem Container. Dies geschieht, weil die Prozesse im Container keine Prozesse in den Vorgänger-pid-Namespaces „sehen“ können (d. h., ihr PPID-Wert würde keinen Sinn machen, wenn der übergeordnete Prozess sich in einem Vorgänger-pid-Namespace befände).

Zum Erstellen eines untergeordneten Prozesses klont der übergeordnete Prozess sich über den fork()- oder clone()-Systemaufruf selbst. Nach dem Forken/Klonen wird die Ausführung sofort sowohl im übergeordneten als auch im untergeordneten Prozess fortgesetzt (wobei die CLONE_VFORK-Option von vfork() und clone() ignoriert wird), aber aufgrund des Rückgabecodewertes aus fork()/clone() über unterschiedliche Codepfade.

Ja, Sie lesen ganz richtig: ein fork()/clone()-Systemaufruf erzeugt einen Rückgabecode in zwei verschiedenen Prozessen! Der übergeordnete Prozess erhält als Rückgabecode die PID des untergeordneten Prozesses, und der untergeordnete Prozess erhält 0, damit der vom übergeordneten und vom untergeordneten Prozess gemeinsam genutzte Code sich auf der Basis dieses Wertes verzweigen kann. Beim Klonen durch übergeordnete Multi-Thread-Prozesse und im Zusammenhang mit dem Copy-on-Write-Arbeitsspeicher gibt es aus Effizienzgründen einige kleine Unterschiede, auf die wir hier aber nicht näher eingehen müssen. Der untergeordnete Prozess erbt den Arbeitsspeicherstatus des übergeordneten Prozesses und, sofern vorhanden, dessen geöffnete Dateien, Netzwerk-Sockets und das Steuerterminal.

Typischerweise erfasst der übergeordnete Prozess die PID des untergeordneten Prozesses, um dessen Lebenszyklus zu überwachen (siehe die Erläuterung zum Reapen oben). Das Verhalten des untergeordneten Prozesses hängt vom Programm ab, das sich selbst geklont hat (das stellt auf der Basis des Rückgabecodes von fork() einen zu befolgenden Ausführungspfad bereit).

Ein Webserver wie nginx könnte sich selbst klonen und so einen untergeordneten Prozess für das Handling von http-Verbindungen erstellen. In solchen Fällen führt der untergeordnete Prozess kein neues Programm aus, sondern einfach einen anderen Codepfad im selben Programm, um in diesem Beispiel die http-Verbindungen zu handeln. Zur Erinnerung: Der Rückgabecode von einem Klon oder Fork sagt dem untergeordneten Prozess, dass er der untergeordnete Prozess ist, damit er diesen Codepfad wählen kann.

Interaktive Shell-Prozesse (z. B. ein bash-, sh-, fish-, zsh-Prozess usw. mit einem Steuerterminal), möglicherweise aus einer ssh-Sitzung, klonen sich bei jeder Eingabe eines Befehls selbst. Der untergeordnete Prozess, der immer noch einen Codepfad aus dem übergeordneten Prozess / der Shell ausführt, hat viel zu tun, bevor der Codepfad im untergeordneten Prozess z. B. den Systemaufruf execve() aufruft, um innerhalb dieses Prozesses ein anderes Programm auszuführen – er muss Dateideskriptoren für die IO-Umleitung einrichten, die Prozessgruppe festlegen und mehr.

Wenn Sie in Ihre Shell ls eingeben, wird Ihre Shell geforkt, die oben beschriebene Einrichtung erfolgt durch die Shell / den untergeordneten Prozess und anschließend wird das ls-Programm (üblicherweise von der Datei /de/usr/bin/ls aus) ausgeführt, um den Inhalt dieses Prozesses durch den Maschinencode für ls zu ersetzen. Einen guten Einblick in die innere Arbeitsweise von Shells und Prozessgruppen bietet dieser Artikel zum Implementieren einer Shell-Job-Steuerung.

Zu beachten ist, dass ein Prozess execve() mehr als einmal aufrufen kann, weshalb die Datenmodelle für die Workload-Erfassung auch dies berücksichtigen müssen. Das hat zur Folge, dass ein Prozess zu vielen verschiedenen Programmen werden kann, bevor er beendet wird – also nicht nur zu seinem übergeordneten Prozessprogramm, dem optional ein Programm folgt. Wie dies in einer Shell erreicht werden kann (also wie das Shell-Programm im selben Prozess durch ein anderes ersetzt werden kann), können Sie in der Linux-Dokumentation unter „Shell Builtin Commands“ nachlesen.

Ein weiterer Aspekt der Ausführung eines Programms in einem Prozess ist, dass einige offene Dateideskriptoren (die, die als „close-on-exec“ markiert sind) möglicherweise vor der Ausführung des neuen Programms geschlossen werden, während andere weiter für das neue Programm zur Verfügung stehen. Wir erinnern uns: Ein einzelner fork()-/clone()-Aufruf stellt einen Rückgabecode in gleich zwei Prozessen bereit: dem übergeordneten und dem untergeordneten Prozess. Der Systemaufruf execve() ist auch insofern seltsam, als ein erfolgreicher execve() keinen Rückgabecode für den Erfolgsfall hat, weil er zur Ausführung eines neuen Programms führt, sodass man nirgendwohin zurückkehren kann, außer wenn execve() fehlschlägt.

Erstellen neuer Sitzungen

Aktuell erstellt Linux neue Sitzungen mit nur einem Systemaufruf (setsid()), der von dem Prozess initiiert wird, der der Leiter der neuen Sitzung wird. Dieser Systemaufruf ist oft Teil des Codepfads des geklonten untergeordneten Prozesses, der vor der Ausführung eines anderen Programms in diesem Prozess ausgeführt wird (d. h., der Aufruf wird vom Code des übergeordneten Prozesses geplant und darin aufgenommen). Alle Prozesse innerhalb einer Sitzung haben dieselbe SID, die mit der PID des Prozesses übereinstimmt, der setsid() aufruft. Dieser Prozess wird auch als „Sitzungsleiter“ bezeichnet. Mit anderen Worten: Ein Sitzungsleiter ist jeder Prozess mit einer PID, die mit dessen SID übereinstimmt. Das Beenden des Sitzungsleiterprozesses löst die Beendigung seiner unmittelbaren untergeordneten Prozessgruppen aus.

Erstellen neuer Prozessgruppen

Linux verwendet zur Kennzeichnung einer Gruppe von Prozessen, die innerhalb einer Sitzung zusammenarbeiten, Prozessgruppen. Sie haben alle dieselbe SID und PGID (Process Group ID). Die PGID ist die PID des Prozessgruppenleiters. Für den Prozessgruppenleiter gibt es keinen Sonderstatus; er kann enden, ohne dass dies Auswirkungen auf andere Mitglieder der Prozessgruppe hat, und diese behalten dieselbe PGID – auch wenn der Prozess mit dieser PID gar nicht mehr vorhanden ist.

Zu beachten ist, dass der Linux-Kernel auch bei pid-wrap (Wiederverwendung einer kürzlich verwendeten PID auf stark beanspruchten Systemen) sicherstellt, dass die PID eines beendeten Prozessgruppenleiters so lange nicht wiederverwendet wird, bis alle Mitglieder dieser Prozessgruppe beendet sind (d. h., es gibt keine Möglichkeit, dass deren PGID versehentlich auf einen neuen Prozess verweist).

Prozessgruppen sind wertvoll für Shell-Pipeline-Befehle wie die folgenden:

cat foo.txt | grep bar | wc -l

Dieser Befehl erstellt drei Prozesse für drei verschiedene Programme (cat, grep und wc) und verbindet diese mit Pipes. Shells erstellen eine neue Prozessgruppe auch nur für einzelne Programmbefehle wie ls. Der Zweck von Prozessgruppen besteht darin, die gezielte Weiterleitung von Signalen an eine Gruppe von Prozessen zu ermöglichen und eine Gruppe von Prozessen – die Vordergrundprozessgruppe – zu identifizieren, die vollen Lese- und Schreibzugriff auf das Steuerterminal ihrer Sitzung haben, sofern vorhanden.

Mit anderen Worten: Strg + C in Ihrer Shell sendet allen Prozessen in der Vordergrundprozessgruppe ein Unterbrechungssignal (der negative PGID-Wert als PID-Ziel des Signals unterscheidet zwischen der Gruppe und dem Prozess des Prozessgruppenleiters selbst). Die Steuerterminal-Zuordnung gewährleistet, dass Prozesse, die die Eingaben des Terminals lesen, nicht miteinander konkurrieren und Probleme verursachen (Terminal-Ausgaben aus Nicht-Vordergrundprozessgruppen können zulässig sein).

Nutzer:innen und Gruppen

Wie bereits erwähnt hat der init-Prozess die Nutzer-ID 0 (root). Jedem Prozess sind ein:e Nutzer:in und eine Gruppe zugeordnet. Diese können dazu verwendet werden, den Zugriff auf Systemaufrufe und Dateien einzuschränken. Nutzer:innen und Gruppen haben numerische IDs und können einen zugehörigen Namen wie root oder ms haben. root ist der Superuser, der alles machen kann, und sollte daher nur verwendet werden, wenn so etwas aus Sicherheitsgründen zwingend erforderlich ist.

Für den Linux-Kernel sind nur IDs relevant. Namen sind optional und sollen Nutzer:innen nur das Hantieren erleichtern. Sie werden durch die Dateien /etc/passwd und /etc/group zur Verfügung gestellt. Mit dem Name Service Switch (NSS) können diese Dateien mit Nutzer:innen und Gruppen aus LDAP und anderen Verzeichnissen erweitert werden (wenn Sie die Kombination aus /etc/passwd und vom NSS bereitgestellten Nutzer:innen sehen wollen, verwenden Sie getent passwd).

Jedem Prozess können mehrere Nutzer:innen und (reale, effektive, gespeicherte und ergänzende) Gruppen zugeordnet sein. Weitere Informationen finden Sie unter „credentials(7) — Linux manual page“.

Die zunehmende Verwendung von Containern, deren Root-Dateisysteme durch Container-Images definiert sind, hat die Wahrscheinlichkeit erhöht, dass /de/etc/passwd und /etc/group nicht vorhanden sind oder einige Namen von Nutzer:innen und Gruppen-IDs fehlen, die möglicherweise in Gebrauch sind. Da den Linux-Kernel aber nur die IDs und keine Namen interessieren, ist das kein Problem.

Zusammenfassung

Das Linux-Prozessmodell ermöglicht eine präzise und kurze Darstellung von Server-Workloads, was wiederum sehr gezielte Alerting-Regeln und Überprüfungen ermöglicht. Eine leicht verständliche sitzungsspezifische Darstellung des Prozessmodells in Ihrem Browser würde einen guten Einblick in Ihre Server-Workloads geben.

Wenn Sie noch heute loslegen möchten, können Sie Elastic Cloud 14 Tage lang kostenlos ausprobieren. Und wenn Sie das Ganze lieber selbst verwalten möchten, können Sie den Elastic Stack kostenlos herunterladen.

Weitere Informationen

Eine hervorragende Quelle für Informationen sind die Linux-„man pages“. Details zum hier beschriebenen Linux-Prozessmodell finden Sie auf den folgenden „man pages“: