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
$ ./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 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:0In 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 1Dasselbe 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 capabilitiesDas 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 1Die 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 dummyDieser 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-writeWie 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
»
»
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
