Das Buch „BPF für das Monitoring von Linux“

Das Buch „BPF für das Monitoring von Linux“Hallo, Habr-Bewohner! Die BPF-VM ist ein wesentlicher Bestandteil des Linux-Kernels. Ihre sachgemäße Anwendung ermöglicht es Systemingenieuren, Fehler zu identifizieren und selbst die komplexesten Probleme zu lösen. Sie werden lernen, Programme zu erstellen, die das Verhalten des Kernels überwachen und modifizieren, sicher Code zur Beobachtung von Ereignissen im Kernel zu integrieren und vieles mehr. David Calavera und Lorenzo Fontana helfen Ihnen, die Möglichkeiten von BPF zu entfalten. Erweitern Sie Ihr Wissen über Leistungsoptimierung, Netzwerke und Sicherheit. — Nutzen Sie BPF zur Überwachung und Modifikation des Verhaltens des Linux-Kernels. — Integrieren Sie Code für sicheres Monitoring von Ereignissen im Kernel – ohne den Kernel neu kompilieren oder das System neu starten zu müssen. — Verwenden Sie praktische Codebeispiele in C, Go oder Python. — Steuern Sie die Situation, indem Sie den Lebenszyklus des BPF-Programms beherrschen.

Die Sicherheit des Linux-Kernels, seine Möglichkeiten und Seccomp

BPF bietet eine leistungsstarke Möglichkeit, den Kernel zu erweitern, ohne die Stabilität, Sicherheit und Geschwindigkeit zu beeinträchtigen. Aus diesem Grund dachten die Kernel-Entwickler, es wäre sinnvoll, seine Vielseitigkeit zur Verbesserung der Prozessisolation in Seccomp zu nutzen, indem Seccomp-Filter implementiert werden, die von BPF-Programmen unterstützt werden und als Seccomp BPF bekannt sind. In diesem Kapitel erklären wir, was Seccomp ist und wie es angewendet wird. Danach erfahren Sie, wie man Seccomp-Filter mit BPF-Programmen schreibt. Anschließend betrachten wir die eingebauten BPF-Fallen, die im Kernel für Sicherheitsmodule von Linux vorhanden sind.

Linux-Sicherheitsmodule (LSM) sind eine Plattform, die eine Reihe von Funktionen bietet, die für die standardisierte Implementierung verschiedener Sicherheitsmodelle eingesetzt werden können. LSM kann direkt im Quellbaum des Kernels verwendet werden, beispielsweise AppArmor, SELinux und Tomoyo.

Lassen Sie uns mit einer Diskussion über die Möglichkeiten von Linux beginnen.

Funktionen

Die Möglichkeiten von Linux bestehen darin, einem nicht privilegierten Prozess die Erlaubnis zu geben, eine bestimmte Aufgabe auszuführen, jedoch ohne suid dafür zu verwenden oder den Prozess anderweitig privilegiert zu machen, wodurch die Möglichkeit von Angriffen verringert wird und dem Prozess die Möglichkeit gegeben wird, bestimmte Aufgaben auszuführen. Wenn beispielsweise Ihre Anwendung einen privilegierten Port, sagen wir 80, öffnen muss, können Sie anstelle des Starts des Prozesses unter root einfach die Fähigkeit CAP_NET_BIND_SERVICE bereitstellen.

Betrachten wir das Programm Go mit dem Namen main.go:

package main
import (
            "net/http"
            "log"
)
func main() {
     log.Fatalf("%v", http.ListenAndServe(":80", nil))
}

Dieses Programm bedient einen HTTP-Server am Port 80 (dies ist ein privilegierter Port). Normalerweise starten wir es direkt nach der Kompilierung:

$ go build -o capabilities main.go
$ ./capabilities

Da wir jedoch keine root-Rechte gewähren, wird dieser Code einen Fehler beim Binden des Ports ausgeben:

2019/04/25 23:17:06 listen tcp :80: bind: permission denied
exit status 1

capsh (Shell-Verwaltungstool) ist ein Werkzeug, das eine Shell mit einem bestimmten Funktionssatz startet.

In diesem Fall, wie bereits erwähnt, anstatt volle root-Rechte zu gewähren, können wir das Binden von privilegierten Ports erlauben, indem wir die Fähigkeit cap_net_bind_service zusammen mit allen anderen, die bereits im Programm vorhanden sind, gewähren. Dazu können wir unser Programm in capsh einschließen:

# capsh --caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' 
   --keep=1 --user="nobody" 
   --addamb=cap_net_bind_service -- -c "./capabilities"

Lassen Sie uns diesen Befehl etwas genauer betrachten.

  • capsh – wir verwenden capsh als Shell.
  • —caps=’cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep’ – da wir den Benutzer wechseln müssen (wir wollen nicht mit root-Rechten laufen), geben wir cap_net_bind_service an und die Fähigkeit, tatsächlich die Benutzer-ID von root auf nobody zu ändern, nämlich cap_setuid und cap_setgid.
  • —keep=1 – wir möchten die gesetzten Fähigkeiten beibehalten, wenn der Wechsel vom root-Konto vorgenommen wird.
  • —user=„nobody“ – der Endbenutzer, der das Programm ausführt, wird nobody sein.
  • —addamb=cap_net_bind_service – wir definieren die Bereinigung der damit verbundenen Fähigkeiten nach dem Wechsel aus dem root-Modus.
  • — -c ". /capabilities" – wir starten einfach das Programm.

Verbundene Fähigkeiten sind eine besondere Art von Fähigkeiten, die von untergeordneten Programmen geerbt werden, wenn das aktuelle Programm sie mit execve() ausführt. Nur Fähigkeiten, die als verbunden zugelassen sind, oder mit anderen Worten, als Umgebungsfähigkeiten, können vererbt werden.

Wahrscheinlich interessiert Sie, was +eip nach der Angabe der Fähigkeit in der Option —caps bedeutet. Diese Flags werden verwendet, um festzustellen, dass die Fähigkeit:

-aktiviert sein muss (p);

-für die Anwendung verfügbar ist (e);

-von untergeordneten Prozessen geerbt werden kann (i).

Da wir cap_net_bind_service verwenden möchten, müssen wir dies mit dem Flag e tun. Dann starten wir eine Shell im Befehl. Dadurch wird die Binärdatei capabilities gestartet, und wir müssen sie mit dem Flag i kennzeichnen. Schließlich möchten wir, dass die Fähigkeit aktiviert wird (wir haben dies getan, ohne die UID zu ändern) mit p. Das sieht aus wie cap_net_bind_service+eip.

Sie können das Ergebnis mit ss überprüfen. Wir kürzen die Ausgabe etwas, damit sie auf die Seite passt, aber sie zeigt den zugehörigen Port und die Benutzer-ID ungleich 0, in diesem Fall 65 534:

# ss -tulpn -e -H | cut -d' ' -f17-
128 *:80 *:*
users:(("capabilities",pid=30040,fd=3)) uid:65534 ino:11311579 sk:2c v6only:0

In diesem Beispiel haben wir capsh verwendet, aber Sie können eine Shell mit libcap schreiben. Für weitere Informationen siehe man 3 libcap.

Beim Programmieren weiß der Entwickler ziemlich oft nicht im Voraus, welche Fähigkeiten das Programm zur Laufzeit benötigt; darüber hinaus können sich diese Fähigkeiten in neuen Versionen ändern.

Um die Fähigkeiten unseres Programms besser zu verstehen, können wir das BCC-Tool capable verwenden, das einen kprobe für die Kernel-Funktion cap_capable setzt:

/usr/share/bcc/tools/capable
TIME      UID  PID   TID   COMM               CAP    NAME           AUDIT
10:12:53 0 424     424     systemd-udevd 12 CAP_NET_ADMIN         1
10:12:57 0 1103   1101   timesync        25 CAP_SYS_TIME         1
10:12:57 0 19545 19545 capabilities       10 CAP_NET_BIND_SERVICE 1

Dasselbe können wir mit bpftrace und einem einzeiligen kprobe in der Kernel-Funktion cap_capable erreichen:

bpftrace -e 
   'kprobe:cap_capable {
      time("%H:%M:%S ");
      printf("%-6d %-6d %-16s %-4d %dn", uid, pid, comm, arg2, arg3);
    }' 
    | grep -i capabilities

Das wird etwas Ähnliches wie das Folgende ausgeben, wenn die Fähigkeiten unseres Programms nach dem kprobe aktiviert wurden:

12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 10 1

Die fünfte Spalte sind die Fähigkeiten, die der Prozess benötigt, und da diese Ausgabe unter anderem nicht auditierte Ereignisse enthält, sehen wir alle nicht auditiven Überprüfungen und schließlich die erforderliche Fähigkeit mit dem Auditflag (das letzte in der Ausgabe), das auf 1 gesetzt ist. Die Fähigkeit, die uns interessiert, ist CAP_NET_BIND_SERVICE, sie wird als Konstante im Quellcode des Kernels in der Datei include/uapi/linux/ability.h mit der ID 10 definiert:

/* Allows binding to TCP/UDP sockets below 1024 */
/* Allows binding to ATM VCIs below 32 */
#define CAP_NET_BIND_SERVICE 10<source lang="go">

Möglichkeiten werden oft beim Ausführen von Containern wie runC oder Docker genutzt, um sie im nicht privilegierten Modus auszuführen, wobei jedoch nur die Berechtigungen gewährt werden, die für den Betrieb der meisten Anwendungen erforderlich sind. Wenn eine Anwendung bestimmte Berechtigungen benötigt, können diese in Docker mit —cap-add bereitgestellt werden:

docker run -it --rm --cap-add=NET_ADMIN ubuntu ip link add dummy0 type dummy

Dieser Befehl gewährt dem Container die Berechtigung CAP_NET_ADMIN, was es ihm ermöglicht, eine Netzwerkverbindung zu konfigurieren, um das dummy0-Interface hinzuzufügen.

Im nächsten Abschnitt wird gezeigt, wie solche Berechtigungen verwendet werden können, jedoch mit einer anderen Methode, die es uns ermöglicht, unsere eigenen Filter programmgesteuert zu implementieren.

Seccomp

Seccomp steht für Secure Computing und ist ein Sicherheitslevel, der im Linux-Kernel implementiert ist und es Entwicklern ermöglicht, bestimmte Systemaufrufe zu filtern. Obwohl Seccomp mit Berechtigungen in Linux vergleichbar ist, macht seine Fähigkeit, bestimmte Systemaufrufe zu steuern, es viel flexibler im Vergleich zu diesen.

Seccomp und Linux-Berechtigungen schließen sich nicht aus, sie werden oft gemeinsam genutzt, um von beiden Ansätzen zu profitieren. Beispielsweise möchten Sie einem Prozess die Berechtigung CAP_NET_ADMIN gewähren, ihm jedoch nicht erlauben, Verbindungen über Sockets anzunehmen, indem Sie die Systemaufrufe accept und accept4 blockieren.

Die Filtering-Methode von Seccomp basiert auf BPF-Filtern, die im SECCOMP_MODE_FILTER-Modus arbeiten, und das Filtern von Systemaufrufen erfolgt analog zum Filtern von Paketen.

Die Seccomp-Filter werden mit prctl über die Operation PR_SET_SECCOMP geladen. Diese Filter haben die Form eines BPF-Programms, das für jedes Seccomp-Paket ausgeführt wird, das mit der Struktur seccomp_data bereitgestellt wird. Diese Struktur enthält die Modellarchitektur, einen Zeiger auf die CPU-Instruktionen während des Systemaufrufs und bis zu sechs Argumente des Systemaufrufs, ausgedrückt als uint64.

So sieht die Struktur seccomp_data aus dem Quellcode des Kernels in der Datei linux/seccomp.h aus:

struct seccomp_data {
int nr;
      __u32 arch;
      __u64 instruction_pointer;
      __u64 args[6];
};

Wie aus dieser Struktur ersichtlich ist, können wir nach Systemaufrufen, deren Argumenten oder Kombinationen davon filtern.

Nach dem Empfang jedes Pakets muss der Seccomp-Filter eine Verarbeitung durchführen, um eine endgültige Entscheidung zu treffen und dem Kernel mitzuteilen, wie es weitergehen soll. Die endgültige Entscheidung wird durch einen der Rückgabewerte (Statuscodes) ausgedrückt.

— SECCOMP_RET_KILL_PROCESS — beendet den gesamten Prozess sofort nach der Filterung des Systemaufrufs, der dadurch nicht ausgeführt wird.

— SECCOMP_RET_KILL_THREAD — beendet den aktuellen Thread sofort nach der Filterung des Systemaufrufs, der dadurch nicht ausgeführt wird.

— SECCOMP_RET_KILL — Alias für SECCOMP_RET_KILL_THREAD, aus Gründen der Abwärtskompatibilität beibehalten.

— SECCOMP_RET_TRAP — Der Systemaufruf wird verweigert, und das Signal SIGSYS (Bad System Call) wird an den aufrufenden Prozess gesendet.

— SECCOMP_RET_ERRNO — Der Systemaufruf wird nicht ausgeführt, und ein Teil des Rückgabewerts des Filters SECCOMP_RET_DATA wird als errno-Wert an den Benutzerspeicher übergeben. Je nach Fehlerursache werden unterschiedliche errno-Werte zurückgegeben. Eine Liste der Fehlernummern finden Sie im nächsten Abschnitt.

— SECCOMP_RET_TRACE — wird verwendet, um den ptrace-Debugger mit — PTRACE_O_TRACESECCOMP zu benachrichtigen, um abzufangen, wenn ein Systemaufruf ausgeführt wird, um diesen Prozess zu beobachten und zu steuern. Wenn kein Debugger angeschlossen ist, wird ein Fehler zurückgegeben, errno wird auf -ENOSYS gesetzt und der Systemaufruf wird nicht ausgeführt.

— SECCOMP_RET_LOG — Der Systemaufruf wird genehmigt und im Protokoll erfasst.

— SECCOMP_RET_ALLOW — Der Systemaufruf wird einfach genehmigt.

ptrace ist ein Systemaufruf zur Implementierung von Mechanismen zur Verfolgung in einem Prozess, der als Tracee bezeichnet wird, mit der Fähigkeit, die Ausführung des Prozesses zu beobachten und zu steuern. Das Verfolgungsprogramm kann die Ausführung effektiv beeinflussen und die Registern des Tracee ändern. Im Kontext von Seccomp wird ptrace verwendet, wenn der Statuscode SECCOMP_RET_TRACE ausgelöst wird, sodass der Debugger die Ausführung des Systemaufrufs verhindern und eigene Logik implementieren kann.

Fehler von Seccomp

Von Zeit zu Zeit werden Sie bei der Arbeit mit Seccomp auf verschiedene Fehler stoßen, die durch den Rückgabewert vom Typ SECCOMP_RET_ERRNO identifiziert werden. Um auf einen Fehler hinzuweisen, wird der Systemaufruf seccomp -1 anstelle von 0 zurückgeben.

Folgende Fehler können auftreten:

— EACCESS — Der aufrufenden Seite ist es nicht gestattet, einen Systemaufruf durchzuführen. Dies geschieht normalerweise, weil ihr die Berechtigung CAP_SYS_ADMIN fehlt oder no_new_privs nicht mit prctl gesetzt wurde (darüber sprechen wir später);

— EFAULT — Die übergebenen Argumente (args in der Struktur seccomp_data) haben keine gültige Adresse;

— EINVAL — Hier kann es vier Gründe geben:

- Die angeforderte Operation ist unbekannt oder wird vom Kernel in der aktuellen Konfiguration nicht unterstützt;

- Die angegebenen Flags sind für die angeforderte Operation ungültig;

- Die Operation umfasst BPF_ABS, es gibt jedoch Probleme mit dem angegebenen Offset, das die Größe der seccomp_data-Struktur überschreiten kann;

- Die Anzahl der an den Filter übergebenen Instruktionen überschreitet das Maximum;

— ENOMEM — Nicht genügend Speicher, um das Programm auszuführen;

— EOPNOTSUPP — Die Operation hat angegeben, dass mit SECCOMP_GET_ACTION_AVAIL die Aktion verfügbar war, jedoch unterstützt der Kernel die Rückgabe in den Argumenten nicht;

— ESRCH — Es gab ein Problem beim Synchronisieren eines anderen Threads;

— ENOSYS — Es ist kein Tracer an die SECCOMP_RET_TRACE-Aktion angehängt.

prctl ist ein Systemaufruf, der es Programmen im Benutzerraum ermöglicht, spezifische Aspekte eines Prozesses zu verwalten (einzustellen und abzurufen), wie die Byte-Reihenfolgen, Thread-Namen, den Modus der geschützten Berechnungen (Seccomp), Berechtigungen, Perf-Ereignisse usw.

Seccomp mag Ihnen wie eine Sandbox-Technologie erscheinen, ist es aber nicht. Seccomp ist ein Dienstprogramm, das es Benutzern ermöglicht, einen Sandkastenmechanismus zu entwickeln. Sehen wir uns nun an, wie Programme für Benutzerinteraktionen mit einem Filter erstellt werden, der direkt durch den Systemaufruf Seccomp aufgerufen wird.

Beispiel für einen BPF Seccomp-Filter

Hier zeigen wir, wie man zwei zuvor behandelte Aktionen kombiniert, nämlich:

— Wir schreiben ein Seccomp BPF-Programm, das als Filter mit verschiedenen Rückgabecodes entsprechend den getroffenen Entscheidungen verwendet wird;

— Wir laden den Filter unter Verwendung von prctl hoch.

Zu Beginn benötigen wir die Header-Dateien aus der Standardbibliothek und dem Linux-Kernel:

#include <errno.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <unistd.h>

Bevor wir versuchen, dieses Beispiel auszuführen, müssen wir sicherstellen, dass der Kernel mit CONFIG_SECCOMP und CONFIG_SECCOMP_FILTER, die auf y gesetzt sind, kompiliert wurde. Auf dem Arbeitsrechner kann dies so überprüft werden:

cat /proc/config.gz | zcat | grep -i CONFIG_SECCOMP

Der verbleibende Teil des Codes stellt die Funktion install_filter dar, die aus zwei Teilen besteht. Der erste Teil enthält unsere Liste von BPF-Filteranweisungen:

static int install_filter(int nr, int arch, int error) {
  struct sock_filter filter[] = {
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3),
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
  };

Die Anweisungen werden mit den Makros BPF_STMT und BPF_JUMP eingerichtet, die in der Datei linux/filter.h definiert sind.
Lassen Sie uns die Anweisungen durchgehen.

— BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, arch))) — das System lädt und speichert mit BPF_LD in Form eines Wortes BPF_W, die Paketdaten befinden sich an einer festen Offset-Position BPF_ABS.

— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) — überprüft mit BPF_JEQ, ob der Wert der Architektur im konstanten Akkumulator BPF_K dem arch entspricht. Wenn ja, wechselt es mit einem Offset von 0 zur nächsten Anweisung, andernfalls springt es mit einem Offset von 3 (in diesem Fall), weil arch nicht übereinstimmt.

— BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) — lädt und speichert mit BPF_LD in Form eines Wortes BPF_W, das die Nummer des Systemaufrufs enthält, die sich an einer festen Offset-Position BPF_ABS befindet.

— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) — vergleicht die Nummer des Systemaufrufs mit dem Wert der Variablen nr. Wenn sie gleich sind, wechselt es zur nächsten Anweisung und verweigert den Systemaufruf, andernfalls erlaubt es den Systemaufruf mit SECCOMP_RET_ALLOW.

— BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)) — beendet das Programm mit BPF_RET und gibt im Ergebnis einen Fehler SECCOMP_RET_ERRNO mit der Nummer aus der Variablen err aus.

— BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) — beendet das Programm mit BPF_RET und erlaubt die Ausführung des Systemaufrufs mit SECCOMP_RET_ALLOW.

SECCOMP IST CBPF
Vielleicht interessiert Sie, warum anstelle eines kompilierte ELF-Objekts oder eines in C mit JIT kompilierten Programms eine Liste von Anweisungen verwendet wird.

Es gibt dafür zwei Gründe.

• Erstens wendet Seccomp cBPF (klassisches BPF) an, nicht eBPF, was bedeutet: Es hat keine Register, sondern nur einen Akkumulator zur Speicherung des letzten Berechnungsergebnisses, wie im Beispiel zu sehen ist.

• Zweitens akzeptiert Seccomp einen Zeiger auf ein BPF-Befehlssatzarray direkt und nichts weiter. Die von uns verwendeten Makros helfen lediglich dabei, diese Anweisungen in einer programmerfreundlichen Form anzugeben.

Wenn Sie zusätzliche Hilfe benötigen, um diesen Code zu verstehen, betrachten Sie den folgenden Pseudocode, der dasselbe tut:

if (arch != AUDIT_ARCH_X86_64) {
    return SECCOMP_RET_ALLOW;
}
if (nr == __NR_write) {
    return SECCOMP_RET_ERRNO;
}
return SECCOMP_RET_ALLOW;

Nachdem der Filtercode in der Struktur socket_filter definiert wurde, müssen wir sock_fprog definieren, die den Code und die berechnete Länge des Filters enthält. Diese Datenstruktur ist erforderlich, um sie als Argument für die Erklärung der weiteren Arbeitsweise des Prozesses zu verwenden:

struct sock_fprog prog = {
   .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
   .filter = filter,
};

Es bleibt nur noch eine Sache zu tun in der Funktion install_filter: das Programm selbst zu laden! Dafür verwenden wir prctl, wobei wir PR_SET_SECCOMP als Option nehmen, um in den Modus für geschützte Berechnungen zu wechseln. Dann geben wir an, dass der Filter mit SECCOMP_MODE_FILTER geladen werden soll, der in der Variable prog vom Typ sock_fprog enthalten ist:

  if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
    perror("prctl(PR_SET_SECCOMP)");
    return 1;
  }
  return 0;
}

Schließlich können wir unsere Funktion install_filter verwenden, aber vorher müssen wir prctl aktivieren, um PR_SET_NO_NEW_PRIVS für die aktuelle Ausführung festzulegen und damit zu vermeiden, dass untergeordnete Prozesse erweiterte Berechtigungen erhalten als die Elternprozesse. Wir können die folgenden prctl-Aufrufe in der Funktion install_filter tätigen, ohne Root-Rechte zu haben.

Jetzt können wir die Funktion install_filter aufrufen. Wir blockieren alle Systemaufrufe write, die zur Architektur X86-64 gehören, und erteilen einfach die Erlaubnis, die alle Versuche blockiert. Nach der Festlegung des Filters setzen wir die Ausführung mit dem ersten Argument fort:

int main(int argc, char const *argv[]) {
  if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
   perror("prctl(NO_NEW_PRIVS)");
   return 1;
  }
   install_filter(__NR_write, AUDIT_ARCH_X86_64, EPERM);
  return system(argv[1]);
 }

Lass uns anfangen. Zum Kompilieren unseres Programms können wir entweder clang oder gcc verwenden, in jedem Fall handelt es sich einfach um die Kompilierung der Datei main.c ohne spezielle Optionen:

clang main.c -o filter-write

Wie bereits erwähnt, haben wir alle Schreibvorgänge im Programm blockiert. Um dies zu überprüfen, benötigen wir ein Programm, das etwas ausgibt – ls scheint ein guter Kandidat zu sein. So verhält es sich normalerweise:

ls -la
total 36
drwxr-xr-x 2 fntlnz users 4096 Apr 28 21:09 .
drwxr-xr-x 4 fntlnz users 4096 Apr 26 13:01 ..
-rwxr-xr-x 1 fntlnz users 16800 Apr 28 21:09 filter-write
-rw-r--r-- 1 fntlnz users 19 Apr 28 21:09 .gitignore
-rw-r--r-- 1 fntlnz users 1282 Apr 28 21:08 main.c

Ausgezeichnet! So sieht die Verwendung unseres Shell-Programms aus: Wir übergeben einfach das Programm, das wir testen möchten, als erstes Argument:

‚./filter-write "ls -la"

Nach der Ausführung gibt dieses Programm eine völlig leere Ausgabe aus. Wir können jedoch strace anwenden, um zu sehen, was passiert:

strace -f ./filter-write "ls -la"

Das Ergebnis ist stark verkürzt, aber der entsprechende Teil zeigt, dass die Schreibvorgänge mit dem Fehler EPERM blockiert werden – genau der Fehler, den wir eingestellt haben. Das bedeutet, dass das Programm nichts ausgibt, weil es keinen Zugriff auf den Systemaufruf write hat:

[pid 25099] write(2, "ls: ", 4) = -1 EPERM (Operation nicht erlaubt)
[pid 25099] write(2, "Schreibfehler", 11) = -1 EPERM (Operation nicht erlaubt)
[pid 25099] write(2, "n", 1) = -1 EPERM (Operation nicht erlaubt)

Jetzt verstehen Sie, wie Seccomp BPF funktioniert, und haben eine gute Vorstellung davon, was man damit machen kann. Aber wäre es nicht wünschenswert, dasselbe mit eBPF anstelle von cBPF zu erreichen, um seine gesamte Kraft zu nutzen?

Wenn man an eBPF-Programme denkt, gehen die meisten Menschen davon aus, dass sie sie einfach schreiben und mit Administratorrechten hochladen. Obwohl diese Aussage im Großen und Ganzen richtig ist, implementiert der Kernel eine Reihe von Mechanismen, um eBPF-Objekte auf verschiedenen Ebenen zu schützen. Diese Mechanismen werden BPF LSM-Fallen genannt.

BPF LSM-Fallen

Um eine architekturunabhängige Kontrolle über Systemereignisse zu gewährleisten, implementiert LSM das Konzept von Fallen. Technisch gesehen ähnelt der Fallenaufruf einem Systemaufruf, ist jedoch unabhängig vom System und in die Infrastruktur integriert. LSM bietet ein neues Konzept, bei dem die Abstraktionsebene helfen kann, Probleme zu vermeiden, die im Zusammenhang mit Systemaufrufen auf verschiedenen Architekturen auftreten.

Zum Zeitpunkt der Buchverfassung hatte der Kernel sieben Fallen in Bezug auf BPF-Programme, und SELinux ist der einzige integrierte LSM, der sie implementiert.

Der Quellcode der Fallen befindet sich im Kernelbaum in der Datei include/linux/security.h:

extern int security_bpf(int cmd, union bpf_attr *attr, unsigned int size);
extern int security_bpf_map(struct bpf_map *map, fmode_t fmode);
extern int security_bpf_prog(struct bpf_prog *prog);
extern int security_bpf_map_alloc(struct bpf_map *map);
extern void security_bpf_map_free(struct bpf_map *map);
extern int security_bpf_prog_alloc(struct bpf_prog_aux *aux);
extern void security_bpf_prog_free(struct bpf_prog_aux *aux);

Jede von ihnen wird in verschiedenen Phasen der Ausführung aufgerufen:

— security_bpf — führt eine erste Überprüfung der ausgeführten systemweiten BPF-Aufrufe durch;

— security_bpf_map — prüft, wann der Kernel einen Dateideskriptor für die Map zurückgibt;

— security_bpf_prog — prüft, wann der Kernel einen Dateideskriptor für das eBPF-Programm zurückgibt;

— security_bpf_map_alloc — prüft, ob das Sicherheitsfeld innerhalb der BPF-Maps initialisiert ist;

— security_bpf_map_free — prüft, ob das Sicherheitsfeld innerhalb der BPF-Maps bereinigt wird;

— security_bpf_prog_alloc — prüft, ob das Sicherheitsfeld innerhalb der BPF-Programme initialisiert wird;

— security_bpf_prog_free — prüft, ob das Sicherheitsfeld innerhalb der BPF-Programme bereinigt wird.

Jetzt, da wir all das sehen, verstehen wir: Die Idee der LSM BPF-Hooks besteht darin, dass sie den Schutz jedes eBPF-Objekts gewährleisten können, indem sie sicherstellen, dass nur die über die entsprechenden Berechtigungen verfügen, Operationen auf Maps und Programmen durchführen können.

Zusammenfassung

Sicherheit ist nicht etwas, was man universell für alles Implementieren kann, was man schützen möchte. Es ist wichtig, Systeme auf verschiedenen Ebenen und auf unterschiedliche Weise zu schützen. Glauben Sie es oder glauben Sie es nicht, der beste Weg, ein System abzusichern, besteht darin, verschiedene Schutzebenen aus unterschiedlichen Positionen zu organisieren, sodass die Absenkung der Sicherheit einer Ebene nicht den Zugriff auf das gesamte System erlaubt. Die Kernel-Entwickler haben großartige Arbeit geleistet, indem sie uns eine Vielzahl von Schichten und Schnittstellen zur Verfügung gestellt haben. Wir hoffen, dass wir Ihnen ein gutes Verständnis davon vermittelt haben, was Schichten sind und wie man BPF-Programme zur Arbeit mit ihnen verwendet.

Über die Autoren

David Calavera ist technischer Direktor bei Netlify. Er hat im Docker-Support gearbeitet und war an der Entwicklung von Runc-, Go- und BCC-Tools sowie an anderen Open-Source-Projekten beteiligt. Er ist bekannt für seine Arbeit an Docker-Projekten und die Entwicklung des Docker-Plugin-Ökosystems. David liebt Flame-Graphs und hat stets das Ziel, die Leistung zu optimieren.

Lorenzo Fontana Er arbeitet im Team der Open-Source-Softwareentwickler bei Sysdig, wo er hauptsächlich an Falco arbeitet – einem Projekt der Cloud Native Computing Foundation, das die Sicherheit der Ausführungsumgebung von Containern und die Anomalieerkennung über das Kernel-Modul und eBPF gewährleistet. Er ist begeistert von verteilten Systemen, softwaredefinierten Netzwerken, dem Linux-Kernel und Leistungsanalysen.

» Mehr über das Buch erfahren Sie auf der Verlagswebsite
» Inhaltsverzeichnis
» Auszug

Für die Bewohner von Chabro Rabatt von 25% mit dem Gutschein — Linux

Nach der Bezahlung der gedruckten Version des Buches wird ein elektronisches Buch an die E-Mail-Adresse gesendet.

Quelle: habr.com

Zuverlässiges Hosting für Websites mit DDoS-Schutz kaufen, VPS VDS Server 🔥 Zuverlässiges Hosting für Websites mit DDoS-Schutz kaufen, VPS VDS Server - ProHoster