Hallo, Habr-Community! Die virtuelle BPF-Maschine ist eine der wichtigsten Komponenten des Linux-Kernels. Ihre sachgerechte Anwendung ermöglicht es Systemingenieuren, Fehler zu erkennen und selbst die komplexesten Probleme zu lösen. Sie lernen, Programme zu erstellen, die das Verhalten des Kernels überwachen und modifizieren, können sicher Code für die Überwachung von Ereignissen im Kernel einfügen und vieles mehr. David Calavera und Lorenzo Fontana werden Ihnen helfen, die Möglichkeiten von BPF zu entfalten. Erweitern Sie Ihr Wissen über Leistungstuning, Netzwerke und Sicherheit. - Nutzen Sie BPF zur Überwachung und Modifikation des Linux-Kernel-Verhaltens. - Fügen Sie Code für die sichere Überwachung von Ereignissen im Kernel ein, ohne den Kernel neu kompilieren oder das System neu starten zu müssen. - Profitieren Sie von praktischen Codebeispielen in C, Go oder Python. - Behalten Sie die Kontrolle, indem Sie den Lebenszyklus des BPF-Programms beherrschen.
Sicherheit des Linux-Kernels, seine Möglichkeiten und Seccomp
BPF bietet eine leistungsstarke Möglichkeit, den Kernel zu erweitern, ohne Stabilität, Sicherheit und Geschwindigkeit zu beeinträchtigen. Aus diesem Grund dachten die Kernel-Entwickler, dass es sinnvoll wäre, seine Vielseitigkeit zu nutzen, um die Prozessisolierung in Seccomp zu verbessern, indem sie von BPF unterstützte Seccomp-Filter implementieren, die auch als Seccomp BPF bekannt sind. In diesem Kapitel werden wir erläutern, was Seccomp ist und wie es angewendet wird. Danach erfahren Sie, wie Sie Seccomp-Filter mit BPF-Programmen schreiben können. Abschließend betrachten wir die integrierten BPF-Fallen, die im Kernel für Sicherheitsmodule von Linux vorhanden sind.
Linux-Sicherheitsmodule (LSM) sind eine Plattform, die eine Vielzahl von Funktionen bereitstellt, die für die standardisierte Implementierung verschiedener Sicherheitsmodelle angewendet werden können. LSM kann direkt im Quellbaum des Kernels verwendet werden, beispielsweise AppArmor, SELinux und Tomoyo.
Lassen Sie uns mit der Diskussion über die Möglichkeiten von Linux beginnen.
Funktionen
Die Stärken von Linux liegen darin, dass Sie einem nicht privilegierten Prozess die Berechtigung erteilen können, eine bestimmte Aufgabe auszuführen, ohne dafür SUID zu verwenden oder den Prozess auf andere Weise privilegiert zu machen. Dies verringert die Angriffsfläche und ermöglicht dem Prozess die Ausführung bestimmter Aufgaben. Wenn beispielsweise Ihre Anwendung einen privilegierten Port öffnen muss, zum Beispiel 80, können Sie den Prozess einfach die Fähigkeit CAP_NET_BIND_SERVICE gewähren, anstatt ihn als Root auszuführen.
Betrachten wir ein Go-Programm 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 auf Port 80 (dies ist ein privilegierter Port). In der Regel führen wir es sofort nach der Kompilierung aus:
$ go build -o capabilities main.go
$ ./capabilitiesDa 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 1capsh (Shell-Verwaltungstool) ist ein Werkzeug, das eine Shell mit einem bestimmten Satz von Fähigkeiten startet.
In diesem Fall, wie bereits erwähnt, können anstelle der Bereitstellung vollständiger Root-Rechte die Berechtigungen zum Binden an privilegierte Ports gewährt werden, indem die Möglichkeit cap_net_bind_service zusammen mit allen anderen bereits im Programm vorhandenen Rechten bereitgestellt wird. 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 näher 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 möchten nicht mit Root-Rechten starten), geben wir cap_net_bind_service sowie die Möglichkeit an, die Benutzer-ID von Root auf nobody zu ändern, nämlich cap_setuid und cap_setgid.
- —keep=1 – wir möchten die festgelegten Möglichkeiten beibehalten, wenn der Wechsel vom Root-Konto erfolgt.
- —user=«nobody» – der Endbenutzer, der das Programm startet, wird nobody sein.
- —addamb=cap_net_bind_service – wir legen die Bereinigung der zugehörigen Möglichkeiten nach dem Wechsel aus dem Root-Modus fest.
- — -c "./capabilities" – wir führen einfach das Programm aus.
Verbundene Berechtigungen sind eine spezielle Art von Berechtigungen, die von Kindprozessen geerbt werden, wenn das aktuelle Programm sie über execve() ausführt. Vermieden werden können nur Berechtigungen, die als verbunden genehmigt sind, oder anders ausgedrückt, als Umgebungsberechtigungen.
Sie fragen sich wahrscheinlich, was +eip nach der Angabe einer Berechtigung in der Option —caps bedeutet. Diese Flags dienen dazu, zu bestimmen, dass die Berechtigung:
-aktiviert werden muss (p);
-anwendbar ist (e);
-von Kindprozessen 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 die Shell im Befehl. Dadurch wird die binäre Datei capabilities gestartet, und wir müssen sie mit dem Flag i kennzeichnen. Schließlich möchten wir, dass die Berechtigung aktiviert wird (das haben wir gemacht, 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 werden die Ausgabe ein wenig kürzen, damit sie auf die Seite passt, aber sie zeigt den verbundenen Port und eine Benutzer-ID, die von 0 abweicht, 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:0In diesem Beispiel haben wir capsh verwendet, aber Sie können auch eine Shell mit libcap schreiben. Für weitere Informationen lesen Sie man 3 libcap.
Beim Programmieren weiß der Entwickler häufig nicht im Voraus über alle Möglichkeiten Bescheid, die für das Programm zur Laufzeit erforderlich sind; darüber hinaus können sich diese Möglichkeiten in neuen Versionen ändern.
Um die Funktionen unseres Programms besser zu verstehen, können wir das Tool BCC capable verwenden, das einen kprobe für die Kernel-Funktion cap_capable einrichtet:
/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 1Das Gleiche können wir erreichen, indem wir bpftrace mit einem einzeiligen kprobe in der Kernel-Funktion cap_capable verwenden:
bpftrace -e
'kprobe:cap_capable {
time("%H:%M:%S ");
printf("%-6d %-6d %-16s %-4d %dn", uid, pid, comm, arg2, arg3);
}'
| grep -i capabilitiesDies wird etwas Ähnliches wie das Folgende ausgeben, wenn die Fähigkeiten unseres Programms nach dem kprobe aktiviert sind:
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 1Die fünfte Spalte sind die Berechtigungen, die der Prozess benötigt. Da diese Ausgabe auch nicht auditable Ereignisse umfasst, sehen wir alle nicht auditierbaren Überprüfungen und schließlich die erforderliche Berechtigung mit dem Audit-Flag (letzter in der Ausgabe), das auf 1 gesetzt ist. Die für uns interessante Berechtigung ist CAP_NET_BIND_SERVICE, die als Konstante im Quellcode des Kernels in der Datei include/uapi/linux/ability.h mit der ID 10 definiert ist.
/* Allows binding to TCP/UDP sockets below 1024 */
/* Allows binding to ATM VCIs below 32 */
#define CAP_NET_BIND_SERVICE 10<source lang="go">Berechtigungen werden häufig zur Laufzeit von Containern wie runC oder Docker aktiviert, damit sie im unprivilegierten Modus laufen, jedoch nur die Berechtigungen erhalten, die für den Betrieb der meisten Anwendungen erforderlich sind. Wenn eine Anwendung bestimmte Berechtigungen benötigt, können sie in Docker über --cap-add bereitgestellt werden:
docker run -it --rm --cap-add=NET_ADMIN ubuntu ip link add dummy0 type dummyDieser Befehl gibt dem Container die Berechtigung CAP_NET_ADMIN, was es ihm ermöglicht, eine Netzwerkverbindung zu konfigurieren, um das Interface dummy0 hinzuzufügen.
Im nächsten Abschnitt wird die Verwendung solcher Berechtigungen gemeinsam mit einer anderen Methode gezeigt, die es uns ermöglicht, unsere eigenen Filter programmgesteuert zu implementieren.
Seccomp
Seccomp steht für Secure Computing und ist eine Sicherheitsebene, die im Linux-Kernel implementiert ist und es Entwicklern ermöglicht, bestimmte Systemaufrufe zu filtern. Während Seccomp mit den Möglichkeiten von Linux vergleichbar ist, macht seine Fähigkeit, spezifische Systemaufrufe zu steuern, ihn deutlich flexibler.
Seccomp und die Möglichkeiten von Linux schließen sich nicht gegenseitig aus; sie werden häufig zusammen verwendet, um von beiden Ansätzen zu profitieren. Beispielsweise könnten Sie einem Prozess die Fähigkeit CAP_NET_ADMIN gewähren, ihm aber nicht erlauben, Verbindungen über einen Socket herzustellen, indem Sie die Systemaufrufe accept und accept4 blockieren.
Die Filtermethode von Seccomp basiert auf BPF-Filtern, die im Modus SECCOMP_MODE_FILTER arbeiten, und die Filterung der Systemaufrufe erfolgt ähnlich wie bei Paketfiltern.
Seccomp-Filter werden unter Verwendung von prctl durch die Operation PR_SET_SECCOMP geladen. Diese Filter haben die Form eines BPF-Programms, das für jedes Seccomp-Paket ausgeführt wird, das über die Struktur seccomp_data bereitgestellt wird. Diese Struktur enthält die Referenzarchitektur, den Zeiger auf den Prozessorbefehl während des Systemaufrufs und maximal sechs Systemaufrufsargumente, die als uint64 ausgedrückt sind.
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 hervorgeht, können wir nach Systemaufrufen, deren Argumenten oder deren Kombination filtern.
Nach dem Empfang jedes Seccomp-Pakets muss der Filter verarbeiten, um eine endgültige Entscheidung zu treffen und dem Kernel mitzuteilen, was als Nächstes zu tun ist. Die endgültige Entscheidung wird durch einen der zurückgegebenen Werte (Statuscodes) ausgedrückt.
— SECCOMP_RET_KILL_PROCESS — Beendet den gesamten Prozess sofort nach der Filterung des Systemaufrufs, der daraufhin nicht ausgeführt wird.
— SECCOMP_RET_KILL_THREAD — Beendet den aktuellen Thread sofort nach der Filterung des Systemaufrufs, der daraufhin nicht ausgeführt wird.
— SECCOMP_RET_KILL — Alias für SECCOMP_RET_KILL_THREAD, zur Rückwärtskompatibilität beibehalten.
— SECCOMP_RET_TRAP — Der Systemaufruf ist verboten, 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 in den Benutzerraum übertragen. 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 sehen und zu kontrollieren. Wenn der Debugger nicht verbunden ist, wird ein Fehler zurückgegeben, errno wird auf -ENOSYS gesetzt, und der Systemaufruf wird nicht ausgeführt.
— SECCOMP_RET_LOG — Der Systemaufruf ist erlaubt und wird protokolliert.
— SECCOMP_RET_ALLOW — Der Systemaufruf wird einfach erlaubt.
ptrace ist ein Systemaufruf zur Implementierung von Tracing-Mechanismen in einem Prozess, der als tracee bezeichnet wird, mit der Möglichkeit, die Ausführung dieses Prozesses zu überwachen und zu kontrollieren. Das Tracing-Programm kann die Ausführung effektiv beeinflussen und die Speicherregister des tracee ändern. Im Zusammenhang mit Seccomp wird ptrace verwendet, wenn es mit dem Statuscode SECCOMP_RET_TRACE ausgelöst wird; dadurch kann der Tracer die Ausführung des Systemaufrufs verhindern und eigene Logik implementieren.
Seccomp-Fehler
Gelegentlich werden Sie beim Arbeiten mit Seccomp auf verschiedene Fehler stoßen, die durch den Rückgabewert vom Typ SECCOMP_RET_ERRNO identifiziert werden. Um einen Fehler zu melden, gibt der Systemaufruf seccomp -1 anstelle von 0 zurück.
Folgende Fehler sind möglich:
— EACCESS — der aufrufenden Seite ist es nicht gestattet, den Systemaufruf auszuführen. Normalerweise geschieht dies, weil die erforderlichen Berechtigungen (CAP_SYS_ADMIN) fehlen oder no_new_privs mit prctl nicht gesetzt ist (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 Ursachen geben:
- Die angeforderte Operation ist unbekannt oder wird im aktuellen Kernel-Setup nicht unterstützt;
- Die angegebenen Flags sind für die angeforderte Operation ungültig;
- Die Operation umfasst BPF_ABS, aber es gibt Probleme mit dem angegebenen Offset, das die Größe der Struktur seccomp_data überschreiten könnte;
- Die Anzahl der an den Filter übergebenen Anweisungen überschreitet das Maximum;
— ENOMEM — nicht genügend Speicher, um das Programm auszuführen;
— EOPNOTSUPP — Die Operation hat angegeben, dass mit SECCOMP_GET_ACTION_AVAIL eine Aktion verfügbar war, jedoch unterstützt der Kernel keinen Rückgabewert in den Argumenten;
— ESRCH — Es gab ein Problem bei der Synchronisierung eines anderen Threads;
— ENOSYS — Kein Tracer ist mit der Aktion SECCOMP_RET_TRACE verbunden.
prctl ist ein Systemaufruf, der es einem Benutzerprogramm ermöglicht, spezifische Aspekte eines Prozesses zu steuern (festlegen und abrufen), wie die Byte-Reihenfolge, die Namen der Threads, den Modus für geschützte Berechnungen (Seccomp), Berechtigungen, Perf-Ereignisse usw.
Seccomp mag wie eine Sandbox-Technologie erscheinen, ist jedoch mehr als das. Seccomp ist ein hilfreiches Werkzeug, das es Benutzern ermöglicht, eine Sandbox-Mechanismus zu entwickeln. Schauen wir uns nun an, wie Programme für Benutzerinteraktionen mithilfe eines Filters erstellt werden, der direkt durch den Seccomp-Systemaufruf aufgerufen wird.
Beispiel für einen Seccomp BPF-Filter
Hier zeigen wir, wie wir die beiden zuvor betrachteten Aktionen kombinieren, nämlich:
— wir erstellen ein Seccomp BPF-Programm, das als Filter mit verschiedenen Rückgabecodes angewendet wird, basierend auf den getroffenen Entscheidungen;
— wir laden den Filter mit prctl hoch.
Zunächst 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 der Arbeitsmaschine kann dies folgendermaßen überprüft werden:
cat /proc/config.gz | zcat | grep -i CONFIG_SECCOMP
Der Rest des Codes umfasst die Funktion install_filter, die aus zwei Teilen besteht. Der erste Teil enthält unsere Liste der 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 BPF_STMT- und BPF_JUMP-Makros festgelegt, 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 von Wörtern BPF_W, die Paketdaten werden mit fester Verschiebung BPF_ABS angeordnet.
— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) — überprüft mit BPF_JEQ, ob der Wert der Architektur im Akkumulator BPF_K gleich arch ist. Falls ja, wechselt es mit einer Verschiebung von 0 zur nächsten Anweisung, andernfalls springt es mit einer Verschiebung von 3 (in diesem Fall), weil arch nicht übereinstimmt, um einen Fehler zu melden.
— BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) — lädt und speichert mit BPF_LD in Form von Wörtern BPF_W, die die Nummer des Systemaufrufs sind, die sich an der festen Verschiebung BPF_ABS befinden.
— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) — vergleicht die Nummer des Systemaufrufs mit dem Wert der Variablen nr. Wenn sie übereinstimmen, wird zur nächsten Anweisung gewechselt und der Systemaufruf wird verweigert, andernfalls wird der Systemaufruf mit SECCOMP_RET_ALLOW genehmigt.
— BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)) — beendet das Programm mit BPF_RET und gibt als Ergebnis den 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 genehmigt die Ausführung des Systemaufrufs mit SECCOMP_RET_ALLOW.
SECCOMP — DAS IST CBPF
Möglicherweise fragen Sie sich, warum anstelle eines kompilierten ELF-Objekts oder eines in C mit JIT kompilierten Programms eine Liste von Anweisungen verwendet wird.Es gibt zwei Gründe dafür.
• Erstens wendet Seccomp cBPF (klassisches BPF) an, nicht eBPF, was bedeutet: Es gibt keine Register, sondern nur einen Akku für die Speicherung des letzten Ergebnisses der Berechnungen, wie im Beispiel zu sehen ist.
• Zweitens akzeptiert Seccomp einen Pointer auf ein Array von BPF-Anweisungen direkt und nichts weiter. Die Makros, die wir verwendet haben, helfen nur dabei, diese Anweisungen in einer für Programmierer bequemen Form anzugeben.
Wenn Sie zusätzliche Hilfe benötigen, um diese Konfiguration zu verstehen, betrachten Sie den Pseudocode, der dasselbe bewirkt:
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, muss sock_fprog, das den Code und die berechnete Länge des Filters enthält, definiert werden. Diese Datenstruktur ist als Argument für die Deklaration der Arbeitsweise des Prozesses in der Folge erforderlich:
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};Es bleibt nur noch eine Sache zu erledigen in der Funktion install_filter – das Programm selbst zu laden! Dazu verwenden wir prctl und setzen PR_SET_SECCOMP als Option, um in den Modus der geschützten Berechnungen zu wechseln. Anschließend geben wir an, dass der Filter mit SECCOMP_MODE_FILTER, der in der Variable prog vom Typ sock_fprog enthalten ist, geladen werden soll:
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
perror("prctl(PR_SET_SECCOMP)");
return 1;
}
return 0;
}Jetzt können wir unsere Funktion install_filter nutzen, aber zuvor müssen wir prctl aktivieren, um PR_SET_NO_NEW_PRIVS für den aktuellen Prozess festzulegen und so zu vermeiden, dass Kindprozesse mehr Privilegien erhalten als die Elternprozesse. Dabei können wir die folgenden prctl-Aufrufe in der Funktion install_filter durchführen, ohne Root-Rechte zu benötigen.
Nun können wir die Funktion install_filter aufrufen. Wir blockieren alle Systemaufrufe write, die zur Architektur X86-64 gehören, und gewähren lediglich die Erlaubnis, die alle Versuche blockiert. Nach dem Setzen 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]);
}Legen wir los. Für die Kompilierung 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-writeWie bereits erwähnt, haben wir alle Schreibzugriffe 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
Wunderbar! So sieht die Anwendung 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 zurück. Dennoch können wir 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, den wir konfiguriert haben. Das bedeutet, dass das Programm nichts ausgibt, da es keinen Zugriff auf den Systemaufruf write erhält:
[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 damit möglich ist. Aber wäre es nicht besser, dasselbe mit eBPF anstelle von cBPF zu erreichen, um seine volle Leistung zu nutzen?
Wenn es um eBPF-Programme geht, denken die meisten Menschen, dass sie einfach erstellt und mit Administratorrechten geladen werden. Während diese Aussage im Großen und Ganzen korrekt ist, implementiert der Kernel eine Reihe von Mechanismen zum Schutz von eBPF-Objekten auf verschiedenen Ebenen. Diese Mechanismen werden als BPF LSM-Fallen bezeichnet.
BPF LSM-Fallen
Um eine architekturunabhängige Kontrolle über Systemereignisse zu gewährleisten, implementiert LSM das Konzept von Fallen. Technisch betrachtet ähnelt der Aufruf einer Falle einem Systemaufruf, ist jedoch systemunabhängig und in die Infrastruktur integriert. LSM bietet ein neues Konzept, bei dem die Abstraktionsebene hilft, Probleme im Zusammenhang mit Systemaufrufen auf unterschiedlichen Architekturen zu vermeiden.
Zum Zeitpunkt des Schreibens des Buches hatte der Kernel sieben Fallen, die mit BPF-Programmen in Zusammenhang stehen, und SELinux ist der einzige integrierte LSM, der diese 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 dieser Funktionen wird in verschiedenen Phasen der Ausführung aufgerufen:
— security_bpf — führt eine erste Überprüfung der durchgeführten BPF-Systemaufrufe durch;
— security_bpf_map — überprüft, wenn der Kernel einen Dateideskriptor für die Karte zurückgibt;
— security_bpf_prog — überprüft, wenn der Kernel einen Dateideskriptor für das eBPF-Programm zurückgibt;
— security_bpf_map_alloc — prüft, ob das Sicherheitsfeld innerhalb der BPF-Karten initialisiert ist;
— security_bpf_map_free — überprüft, ob das Sicherheitsfeld innerhalb der BPF-Karten bereinigt wird;
— security_bpf_prog_alloc — prüft, ob das Sicherheitsfeld innerhalb der BPF-Programme initialisiert wird;
— security_bpf_prog_free — überprüft, ob das Sicherheitsfeld innerhalb der BPF-Programme bereinigt wird.
Wenn wir all dies betrachten, erkennen wir: Die Idee der LSM BPF-Hooks besteht darin, dass sie den Schutz jedes eBPF-Objekts gewährleisten können, indem nur diejenigen, die über entsprechende Berechtigungen verfügen, Operationen an Karten und Programmen durchführen können.
Zusammenfassung
Sicherheit ist kein universelles Konzept, das einfach auf alles angewendet werden kann, was Sie schützen möchten. Es ist entscheidend, Systeme auf verschiedenen Ebenen und auf unterschiedliche Weise absichern zu können. Glauben Sie es oder nicht, der beste Weg, ein System zu schützen, besteht darin, unterschiedliche Sicherheitsstufen aus verschiedenen Positionen zu organisieren, sodass eine Schwächung einer Ebene nicht den Zugriff auf das gesamte System ermöglicht. Die Kernel-Entwickler haben großartige Arbeit geleistet, um uns eine Reihe verschiedener Schichten und Interaktionspunkte bereitzustellen. Wir hoffen, Ihnen einen guten Überblick darüber gegeben zu haben, was Schichten sind und wie Sie BPF-Programme nutzen können, um mit ihnen zu arbeiten.
Über die Autoren
David Calavera ist Technischer Direktor bei Netlify. Zuvor war er im Support-Team von Docker tätig und hat an der Entwicklung von Tools wie Runc, Go und BCC mitgewirkt sowie an weiteren Open-Source-Projekten. Er ist bekannt für seine Arbeit an Docker-Projekten und der Entwicklung des Docker-Plugin-Ökosystems. David hat eine große Leidenschaft für Flamegraphs und strebt stets nach Performance-Optimierung.
Lorenzo Fontana 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 von Container-Laufzeitumgebungen gewährleistet und Anomalien über Kernel-Module und eBPF erkennt. Er interessiert sich leidenschaftlich für verteilte Systeme, softwaredefinierte Netzwerke, den Linux-Kernel und Leistungsanalysen.
» Weitere Informationen zu dem Buch finden Sie auf
»
»
Für Habr-Besucher gilt ein Rabatt von 25 % mit dem Code — Linux
Nach Erhalt der Zahlung für die gedruckte Version des Buches wird das E-Book per E-Mail versendet.
Quelle: habr.com
