Hallo Bewohner! Die virtuelle BPF-Maschine ist eine der wichtigsten Komponenten des Linux-Kernels. Dank seiner intelligenten Anwendung können Systemingenieure Fehler finden und selbst die komplexesten Probleme lösen. Sie lernen, wie Sie Programme schreiben, die das Verhalten des Kernels überwachen und ändern, Code sicher einfügen können, um Ereignisse im Kernel zu beobachten, und vieles mehr. David Calavera und Lorenzo Fontana helfen Ihnen dabei, die Kraft von BPF freizuschalten. Erweitern Sie Ihr Wissen über Leistungsoptimierung, Vernetzung und Sicherheit. - Verwenden Sie BPF, um das Verhalten des Linux-Kernels zu verfolgen und zu ändern. - Injizieren Sie Code, um Ereignisse im Kernel sicher zu überwachen - ohne dass Sie den Kernel neu kompilieren oder das System neu starten müssen. - Verwenden Sie praktische Codebeispiele in C, Go oder Python. - Verwalten Sie die Situation, indem Sie den Lebenszyklus des BPF-Programms besitzen.
Sicherheit, Funktionen und Seccomp des Linux-Kernels
BPF bietet eine leistungsstarke Möglichkeit, den Kernel zu erweitern, ohne die Stabilität, Sicherheit oder Geschwindigkeit zu beeinträchtigen. Aus diesem Grund hielten es die Kernel-Entwickler für eine gute Idee, seine Vielseitigkeit zu nutzen, um die Prozessisolation in Seccomp zu verbessern, indem Seccomp-Filter implementiert werden, die von BPF-Programmen unterstützt werden, die auch als Seccomp BPF bezeichnet werden. In diesem Kapitel erklären wir, was Seccomp ist und wie es angewendet wird. Anschließend lernen Sie, wie Sie Seccomp-Filter mit BPF-Programmen schreiben. Schauen wir uns danach die integrierten BPF-Hooks an, die der Kernel für Linux-Sicherheitsmodule hat.
Linux Security Modules (LSMs) ist eine Plattform, die eine Reihe von Funktionen bietet, mit denen die Implementierung verschiedener Sicherheitsmodelle standardisiert werden kann. LSM kann direkt im Kernel-Quellbaum wie Apparmor, SELinux und Tomoyo verwendet werden.
Beginnen wir mit der Erörterung der Linux-Funktionen.
Fähigkeiten
Der Kern der Linux-Funktionen besteht darin, dass Sie eine nicht privilegierte Prozessberechtigung erteilen müssen, um eine bestimmte Aufgabe auszuführen, jedoch ohne Suid für diesen Zweck, oder den Prozess auf andere Weise privilegieren müssen, wodurch die Möglichkeit von Angriffen verringert wird und der Prozess bestimmte Aufgaben ausführen kann. Wenn Ihre Anwendung beispielsweise einen privilegierten Port öffnen muss, z. B. 80, anstatt den Prozess als Root auszuführen, können Sie ihm einfach die CAP_NET_BIND_SERVICE-Funktion zuweisen.
Betrachten Sie ein Go-Programm namens main.go:
package main
import (
"net/http"
"log"
)
func main() {
log.Fatalf("%v", http.ListenAndServe(":80", nil))
}
Dieses Programm bedient einen HTTP-Server an Port 80 (dies ist ein privilegierter Port). Wir führen es normalerweise direkt nach der Kompilierung aus:
$ go build -o capabilities main.go
$ ./capabilities
Da wir jedoch keine Root-Berechtigungen gewähren, gibt dieser Code beim Binden des Ports einen Fehler aus:
2019/04/25 23:17:06 listen tcp :80: bind: permission denied
exit status 1
capsh (Shell Control Tool) ist ein Tool, mit dem eine Shell mit bestimmten Funktionen gestartet wird.
In diesem Fall können Sie, wie bereits erwähnt, anstelle der Gewährung vollständiger Root-Rechte privilegierte Portbindungen aktivieren, indem Sie cap_net_bind_service zusammen mit allem anderen aktivieren, was bereits im Programm enthalten ist. Dazu können wir unser Programm in capsh verpacken:
# 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 ein wenig über diesen Befehl verstehen.
- capsh - benutze capsh als Shell.
- --caps = 'cap_net_bind_service + eip cap_setpcap, cap_setuid, cap_setgid + ep' - da wir den Benutzer ändern müssen (wir wollen nicht als root ausgeführt werden), geben wir cap_net_bind_service und die Möglichkeit an, die Benutzer-ID von root zu niemandem zu ändern, nämlich cap_setuid und cap_setgid ...
- --keep=1 — , root.
- --user=«nobody» — , , nobody.
- --addamb=cap_net_bind_service — root.
- — -c "./capabilities" — .
— , , execve(). , , , , .
Sie fragen sich wahrscheinlich, was + eip bedeutet, nachdem Sie in der Option --caps eine Funktion angegeben haben. Diese Flags werden verwendet, um anzugeben, dass die Funktion:
-muss aktiviert sein muss (p);
-verfügbar für Antrag (e);
-kann von untergeordneten Prozessen geerbt werden (i).
Da wir cap_net_bind_service verwenden möchten, müssen wir dies mit dem e-Flag tun. Dann starten wir die Shell im Befehl. Dadurch wird die Binärfunktion gestartet, und wir müssen sie mit dem i-Flag markieren. Schließlich möchten wir, dass die Funktion mit p aktiviert wird (wir haben dies getan, ohne die UID zu ändern). Es sieht aus wie cap_net_bind_service + eip.
Sie können das Ergebnis mit ss überprüfen. Verkleinern Sie die Ausgabe ein wenig, um auf die Seite zu passen, aber es werden der zugehörige Port und die Benutzer-ID angezeigt, die nicht 0 sind, 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. Weitere Informationen finden Sie unter man 3 libcap.
Beim Schreiben von Programmen kennt der Entwickler häufig nicht alle Funktionen, die das Programm zur Laufzeit benötigt. Darüber hinaus können sich diese Funktionen in neuen Versionen ändern.
Um die Funktionen unseres Programms besser zu verstehen, können wir das BCC-fähige Tool verwenden, das kprobe für die Kernelfunktion cap_capable festlegt:
/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
Wir können dasselbe erreichen, indem wir bpftrace mit der einzeiligen kprobe in der Kernelfunktion cap_capable verwenden:
bpftrace -e \
'kprobe:cap_capable {
time("%H:%M:%S ");
printf("%-6d %-6d %-16s %-4d %d\n", uid, pid, comm, arg2, arg3);
}' \
| grep -i capabilities
Dies gibt ungefähr Folgendes aus, wenn die Funktionen unseres Programms nach kprobe aktiviert werden:
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 enthält die Funktionen, die der Prozess benötigt. Da diese Ausgabe Nicht-Audit-Ereignisse enthält, werden alle Nicht-Audit-Prüfungen und schließlich die erforderlichen Funktionen angezeigt, wobei das Audit-Flag (das letzte in der Ausgabe) auf 1 gesetzt ist. Was uns interessiert, ist CAP_NET_BIND_SERVICE. Es ist als Konstante im Kernel-Quellcode in der Datei include / uapi / linux / Capability.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">
Funktionen werden häufig zur Laufzeit genutzt, damit Container wie runC oder Docker im nicht privilegierten Modus ausgeführt werden können. Es sind jedoch nur die Funktionen zulässig, die zum Ausführen der meisten Anwendungen erforderlich sind. Wenn eine Anwendung bestimmte Funktionen erfordert, kann Docker sie mit --cap-add:
docker run -it --rm --cap-add=NET_ADMIN ubuntu ip link add dummy0 type dummy
Dieser Befehl stellt dem Container die CAP_NET_ADMIN-Funktion zur Verfügung, mit der er eine Netzwerkverbindung zum Hinzufügen der Dummy0-Schnittstelle konfigurieren kann.
Der nächste Abschnitt zeigt, wie Funktionen wie das Filtern verwendet werden, jedoch mit einer anderen Methode, mit der wir unsere eigenen Filter programmgesteuert implementieren können.
Seccomp
Seccomp steht für Secure Computing. Es handelt sich um eine im Linux-Kernel implementierte Sicherheitsebene, mit der Entwickler bestimmte Systemaufrufe herausfiltern können. Während Seccomp mit den Funktionen von Linux vergleichbar ist, ist Seccomp aufgrund seiner Fähigkeit, bestimmte Systemaufrufe zu verarbeiten, viel flexibler als es ist.
Die Funktionen von Seccomp und Linux schließen sich nicht gegenseitig aus und werden häufig zusammen verwendet, um von beiden Ansätzen zu profitieren. Beispielsweise möchten Sie einem Prozess möglicherweise die CAP_NET_ADMIN-Funktion zuweisen, ihm jedoch nicht erlauben, Socket-Verbindungen zu akzeptieren, indem Sie die Systemaufrufe accept und accept4 blockieren.
Die Seccomp-Filtermethode basiert auf BPF-Filtern, die im SECCOMP_MODE_FILTER-Modus ausgeführt werden, und die Systemaufruffilterung wird auf die gleiche Weise wie für Pakete durchgeführt.
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 durch die Struktur seccomp_data dargestellt wird. Diese Struktur enthält die Referenzarchitektur, einen Zeiger auf die Prozessoranweisungen während des Systemaufrufs und maximal sechs Systemaufrufargumente, ausgedrückt als uint64.
So sieht die Struktur seccomp_data aus der Kernelquelle in der Datei linux / seccomp.h aus:
struct seccomp_data {
int nr;
__u32 arch;
__u64 instruction_pointer;
__u64 args[6];
};
Wie Sie dieser Struktur entnehmen können, können wir nach dem Systemaufruf, seinen Argumenten oder einer Kombination aus beiden filtern.
Nach dem Empfang jedes Seccomp-Pakets muss der Filter die Verarbeitung durchführen, um eine endgültige Entscheidung zu treffen und dem Kernel mitzuteilen, was als nächstes zu tun ist. Die endgültige Entscheidung wird in einem der Rückgabewerte (Statuscodes) ausgedrückt.
- SECCOMP_RET_KILL_PROCESS - Beenden des gesamten Prozesses unmittelbar nach dem Filtern eines Systemaufrufs, der aus diesem Grund nicht ausgeführt wird.
- SECCOMP_RET_KILL_THREAD - Beendigung des aktuellen Threads unmittelbar nach dem Filtern eines Systemaufrufs, der aus diesem Grund nicht ausgeführt wird.
- SECCOMP_RET_KILL - Alias für SECCOMP_RET_KILL_THREAD, aus Gründen der Abwärtskompatibilität links.
- SECCOMP_RET_TRAP - Der Systemaufruf ist deaktiviert und das Signal SIGSYS (Bad System Call) wird an die aufrufende Task gesendet.
- SECCOMP_RET_ERRNO - Der Systemaufruf wird nicht ausgeführt, und ein Teil des Rückgabewerts des Filters SECCOMP_RET_DATA wird als errno an den Benutzerbereich übergeben. Abhängig von der Fehlerursache werden unterschiedliche Fehlerwerte zurückgegeben. Die Fehlernummern sind im nächsten Abschnitt aufgeführt.
- SECCOMP_RET_TRACE - Wird verwendet, um den ptrace mit - PTRACE_O_TRACESECCOMP zu benachrichtigen, um abzufangen, wenn ein Systemaufruf ausgeführt wird, um diesen Prozess zu sehen und zu steuern. Wenn kein Tracer 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 zulässig und wird protokolliert.
- SECCOMP_RET_ALLOW - Der Systemaufruf ist einfach zulässig.
ptrace ist ein Systemaufruf zum Implementieren von Ablaufverfolgungsmechanismen in einem Prozess namens tracee mit der Fähigkeit, die Ausführung des Prozesses zu überwachen und zu steuern. Das Trace-Programm kann die Ausführung effektiv beeinflussen und die Trace-Speicherregister ändern. Im Kontext von Seccomp wird ptrace verwendet, wenn es durch den Statuscode SECCOMP_RET_TRACE ausgelöst wird, sodass der Tracer die Ausführung des Systemaufrufs verhindern und seine eigene Logik implementieren kann.
Seccomp-Fehler
Bei der Arbeit mit Seccomp treten von Zeit zu Zeit verschiedene Fehler auf, die durch einen Rückgabewert vom Typ SECCOMP_RET_ERRNO gekennzeichnet sind. Um einen Fehler zu melden, gibt der seccomp-Systemaufruf -1 anstelle von 0 zurück.
Folgende Fehler sind möglich:
- EACCESS - Der Aufrufer darf keinen Systemaufruf tätigen. Dies geschieht normalerweise, weil es nicht über das CAP_SYS_ADMIN-Privileg verfügt oder no_new_privs nicht mit prctl festgelegt ist (dazu später mehr).
- EFAULT - Die übergebenen Argumente (Argumente 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 in der aktuellen Konfiguration vom Kernel nicht unterstützt.
-die angegebenen Flags sind für die angeforderte Operation ungültig;
-operation enthält BPF_ABS, es gibt jedoch Probleme mit dem angegebenen Offset, die die Größe der Struktur seccomp_data überschreiten können.
- Die Anzahl der an den Filter übergebenen Anweisungen überschreitet das Maximum.
- ENOMEM - nicht genügend Speicher, um das Programm auszuführen;
- EOPNOTSUPP - Die Operation zeigte an, dass eine Aktion mit SECCOMP_GET_ACTION_AVAIL verfügbar war, der Kernel jedoch keine Rückgabe in Argumenten unterstützt.
- ESRCH - Beim Synchronisieren eines anderen Streams ist ein Problem aufgetreten.
- ENOSYS - Der Aktion SECCOMP_RET_TRACE ist kein Tracer zugeordnet.
prctl ist ein Systemaufruf, mit dem ein User-Space-Programm bestimmte Aspekte eines Prozesses bearbeiten (festlegen und abrufen) kann, z. B. Bytesequenz, Thread-Namen, Secure Computing-Modus (Seccomp), Berechtigungen, Perf-Ereignisse usw.
Seccomp mag Ihnen wie eine Sandbox-Technologie erscheinen, ist es aber nicht. Seccomp ist ein Dienstprogramm, mit dem Benutzer einen Sandbox-Mechanismus entwickeln können. Schauen wir uns nun an, wie Sie benutzerdefinierte Interaktionsprogramme mithilfe eines Filters erstellen, der direkt vom Seccomp-Systemaufruf aufgerufen wird.
Beispiel BPF Seccomp Filter
Hier zeigen wir, wie die beiden zuvor betrachteten Aktionen kombiniert werden können:
- Schreiben Sie das Seccomp BPF-Programm, das je nach den getroffenen Entscheidungen als Filter mit unterschiedlichen Rückkehrcodes verwendet wird.
- Laden Sie den Filter mit prctl.
Zuerst benötigen Sie Header 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 dieses Beispiel versuchen, müssen wir sicherstellen, dass der Kernel mit CONFIG_SECCOMP und CONFIG_SECCOMP_FILTER kompiliert ist, die auf y gesetzt sind. Auf einer Produktionsmaschine können Sie dies folgendermaßen testen:
cat /proc/config.gz| zcat | grep -i CONFIG_SECCOMP
Der Rest des Codes ist eine zweiteilige Funktion install_filter. Der erste Teil enthält unsere Liste mit 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),
};
Anweisungen werden mithilfe der in der Datei linux / filter.h definierten Makros BPF_STMT und BPF_JUMP festgelegt.
Lassen Sie uns die Anweisungen durchgehen.
- BPF_STMT (BPF_LD + BPF_W + BPF_ABS (Offset von (struct seccomp_data, arch))) - Das System lädt und akkumuliert mit BPF_LD in Form des Wortes BPF_W, Paketdaten befinden sich an einem festen Offset BPF_ABS.
- BPF_JUMP (BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) - prüft mit BPF_JEQ, ob der Architekturwert in der BPF_K-Akkumulatorkonstante gleich arch ist. Wenn dies der Fall ist, springt es mit Offset 0 zum nächsten Befehl, andernfalls springt es mit Offset 3 (in diesem Fall), um einen Fehler auszulösen, da der Bogen nicht übereinstimmt.
- BPF_STMT (BPF_LD + BPF_W + BPF_ABS (offsetof (struct seccomp_data, nr))) - lädt BPF_LD in Form des Wortes BPF_W herunter und akkumuliert es. Dies ist die Systemrufnummer, die im festen Offset BPF_ABS enthalten ist.
- BPF_JUMP (BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) - vergleicht die Systemrufnummer mit dem Wert der Variablen nr. Wenn sie gleich sind, wird mit der nächsten Anweisung fortgefahren und der Systemaufruf nicht zugelassen. Andernfalls wird der Systemaufruf mit SECCOMP_RET_ALLOW aktiviert.
- BPF_STMT (BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (Fehler & SECCOMP_RET_DATA)) - Beendet das Programm mit BPF_RET und gibt als Ergebnis einen SECCOMP_RET_ERRNO-Fehler mit einer Nummer aus der Fehlervariablen aus.
- BPF_STMT (BPF_RET + BPF_K, SECCOMP_RET_ALLOW) - Beendet das Programm mit BPF_RET und ermöglicht die Ausführung eines Systemaufrufs mit SECCOMP_RET_ALLOW.
SECCOMP IS CBPF Sie
fragen sich möglicherweise, warum eine Liste von Anweisungen anstelle eines kompilierten ELF-Objekts oder eines JIT-kompilierten C-Programms verwendet wird.
Dafür gibt es zwei Gründe.
• Erstens verwendet Seccomp cBPF (klassisches BPF), nicht eBPF. Dies bedeutet, dass es keine Register hat, sondern nur einen Akkumulator zum Speichern des letzten Berechnungsergebnisses, wie Sie im Beispiel sehen können.
• Zweitens zeigt Seccomp direkt auf ein Array von BPF-Anweisungen und sonst nichts. Die von uns verwendeten Makros helfen nur dabei, diese Anweisungen in einer für Programmierer geeigneten Form anzugeben.
Wenn Sie weitere Hilfe zum Verständnis dieser Assembly benötigen, ziehen Sie einen Pseudocode in Betracht, 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 Sie den Filtercode in der Struktur socket_filter definiert haben, müssen Sie ein sock_fprog definieren, das den Code und die berechnete Filterlänge enthält. Diese Datenstruktur wird als Argument benötigt, um die Arbeit des Prozesses in der Zukunft zu deklarieren:
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
In der Funktion install_filter bleibt nur noch eines zu tun: Laden Sie das Programm selbst herunter! Zu diesem Zweck verwenden wir prctl und verwenden PR_SET_SECCOMP als Option, um in den sicheren Rechenmodus zu wechseln. Dann weisen wir den Modus an, den Filter mit SECCOMP_MODE_FILTER zu laden, das in der Prog-Variablen 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 verwenden, um PR_SET_NO_NEW_PRIVS für die aktuelle Ausführung festzulegen und so eine Situation zu vermeiden, in der untergeordnete Prozesse mehr Berechtigungen erhalten als ihre übergeordneten. Damit können wir die folgenden Aufrufe von prctl in der Funktion install_filter ausführen, ohne über Root-Rechte zu verfügen.
Jetzt können wir die Funktion install_filter aufrufen. Lassen Sie uns alle Schreibsystemaufrufe blockieren, die sich auf die X86-64-Architektur beziehen, und nur die Berechtigung erteilen, die alle Versuche blockiert. Setzen Sie nach der Installation des Filters 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. Wir können entweder clang oder gcc verwenden, um unser Programm zu kompilieren. In beiden Fällen wird lediglich die Datei main.c ohne spezielle Optionen kompiliert:
clang main.c -o filter-write
Wie bereits erwähnt, haben wir alle Einträge im Programm blockiert. Um dies zu testen, benötigen Sie ein Programm, das etwas ausgibt - es scheint ein guter Kandidat zu sein. So verhält sie 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
Perfekt! So sieht unser Shell-Programm aus: Wir übergeben nur das Programm, das wir testen möchten, als erstes Argument:
./filter-write "ls -la"
Bei der Ausführung erzeugt dieses Programm eine vollständig leere Ausgabe. Wir können jedoch strace verwenden, um zu sehen, was los ist:
strace -f ./filter-write "ls -la"
Das Ergebnis der Arbeit ist stark verkürzt, aber der entsprechende Teil zeigt, dass die Datensätze mit dem EPERM-Fehler blockiert sind - dem gleichen, den wir konfiguriert haben. Dies bedeutet, dass das Programm nichts ausgibt, da es nicht auf den Aufruf des Schreibsystems zugreifen kann:
[pid 25099] write(2, "ls: ", 4) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "write error", 11) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "\n", 1) = -1 EPERM (Operation not permitted)
Jetzt verstehen Sie, wie Seccomp BPF funktioniert, und haben eine gute Vorstellung davon, was damit getan werden kann. Aber möchten Sie nicht dasselbe mit eBPF anstelle von cBPF tun, um die volle Leistung zu nutzen?
Wenn man an eBPF-Programme denkt, denken die meisten Leute, dass sie sie nur schreiben und mit Administratorrechten laden. Während diese Aussage im Allgemeinen zutrifft, implementiert der Kernel eine Reihe von Mechanismen zum Schutz von eBPF-Objekten auf verschiedenen Ebenen. Diese Mechanismen werden als BPF-LSM-Fallen bezeichnet.
Fallen BPF LSM
Um eine architekturunabhängige Überwachung von Systemereignissen zu ermöglichen, implementiert LSM das Konzept der Traps. Ein Hook-Aufruf ähnelt technisch einem Systemaufruf, ist jedoch systemunabhängig und in die Infrastruktur integriert. LSM bietet ein neues Konzept, bei dem eine Abstraktionsschicht dazu beitragen kann, Probleme zu vermeiden, die beim Umgang mit Systemaufrufen auf verschiedenen Architekturen auftreten.
Zum Zeitpunkt dieses Schreibens verfügt der Kernel über sieben Hooks, die BPF-Programmen zugeordnet sind, und SELinux ist das einzige integrierte LSM, das diese implementiert.
Der Quellcode für die Hooks 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);
Jeder von ihnen wird in verschiedenen Ausführungsphasen aufgerufen:
- security_bpf - führt erste Überprüfungen der ausgeführten BPF-Systemaufrufe durch;
- security_bpf_map - prüft, wann der Kernel einen Dateideskriptor für die Map zurückgibt;
- security_bpf_prog - Überprüft, ob der Kernel einen Dateideskriptor für das eBPF-Programm zurückgibt.
- security_bpf_map_alloc - prüft, ob das Sicherheitsfeld in BPF-Maps initialisiert ist;
- security_bpf_map_free - prüft, ob das Sicherheitsfeld in BPF-Karten gelöscht ist;
- security_bpf_prog_alloc - prüft, ob das Sicherheitsfeld in BPF-Programmen initialisiert ist;
- security_bpf_prog_free - prüft, ob das Sicherheitsfeld in BPF-Programmen gelöscht ist.
Nachdem wir dies alles gesehen haben, verstehen wir, dass die Idee hinter LSM-BPF-Abfangjägern darin besteht, dass sie jedes eBPF-Objekt schützen können und sicherstellen, dass nur diejenigen mit den entsprechenden Berechtigungen Operationen an Karten und Programmen ausführen können.
Zusammenfassung
Sicherheit ist nichts, was Sie für alles, was Sie schützen möchten, einheitlich durchsetzen können. Es ist wichtig, Systeme auf verschiedenen Ebenen und auf unterschiedliche Weise schützen zu können. Ob Sie es glauben oder nicht, der beste Weg, ein System zu sichern, besteht darin, verschiedene Schutzstufen von verschiedenen Positionen aus zu organisieren, sodass die Sicherheitsverschlechterung einer Stufe den Zugriff auf das gesamte System verhindert. Die Kernel-Entwickler haben großartige Arbeit geleistet und uns verschiedene Ebenen und Berührungspunkte zur Verfügung gestellt. Wir hoffen, wir haben Ihnen ein gutes Verständnis dafür vermittelt, was Ebenen sind und wie Sie BPF-Programme verwenden, um mit ihnen zu arbeiten.
Über die Autoren
David Calavera ist CTO bei Netlify. Er hat für den Docker Support gearbeitet und zur Entwicklung von Runc-, Go- und BCC-Tools sowie anderer Open Source-Projekte beigetragen. Bekannt für seine Arbeit an Docker-Projekten und die Entwicklung des Docker-Plugin-Ökosystems. David liebt Flammengraphen sehr und ist stets bemüht, die Leistung zu optimieren.
Lorenzo Fontana ist Teil des Open-Source-Entwicklungsteams von Sysdig, wo er hauptsächlich an Falco beteiligt ist, einem Projekt der Cloud Native Computing Foundation, das Container-Laufzeitsicherheit und Anomalieerkennung über das Kernelmodul und eBPF bietet. Er ist begeistert von verteilten Systemen, softwaredefinierten Netzwerken, dem Linux-Kernel und der Leistungsanalyse.
»Weitere Details zum Buch finden Sie auf der Website des Verlags
» Inhaltsverzeichnis
» Auszug
für Einwohner 25% Rabatt auf Gutschein - Linux
Nach Zahlungseingang für die Papierversion des Buches wird ein E-Book per E-Mail verschickt.