Buch „BPF für Linux Monitoring“

Buch „BPF für Linux Monitoring“Hallo, Khabro-Bewohner! Die virtuelle BPF-Maschine ist eine der wichtigsten Komponenten des Linux-Kernels. Seine ordnungsgemäße Verwendung ermöglicht es Systemingenieuren, Fehler zu finden und selbst die komplexesten Probleme zu lösen. Sie erfahren, wie Sie Programme schreiben, die das Verhalten des Kernels überwachen und ändern, wie Sie Code zur Überwachung von Ereignissen im Kernel sicher implementieren und vieles mehr. David Calavera und Lorenzo Fontana helfen Ihnen dabei, die Kraft von BPF freizusetzen. Erweitern Sie Ihr Wissen über Leistungsoptimierung, Vernetzung und Sicherheit. - Verwenden Sie BPF, um das Verhalten des Linux-Kernels zu überwachen und zu ändern. - Fügen Sie Code ein, um Kernel-Ereignisse sicher zu überwachen, ohne den Kernel neu kompilieren oder das System neu starten zu müssen. — Nutzen Sie praktische Codebeispiele in C, Go oder Python. - Übernehmen Sie die Kontrolle, indem Sie den Lebenszyklus des BPF-Programms übernehmen.

Linux-Kernel-Sicherheit, ihre Funktionen und Seccomp

BPF bietet eine leistungsstarke Möglichkeit, den Kernel zu erweitern, ohne Stabilität, Sicherheit oder Geschwindigkeit zu beeinträchtigen. Aus diesem Grund dachten die Kernel-Entwickler, dass es eine gute Idee wäre, seine Vielseitigkeit zu nutzen, um die Prozessisolation in Seccomp zu verbessern, indem Seccomp-Filter implementiert werden, die von BPF-Programmen unterstützt werden, auch bekannt als Seccomp BPF. In diesem Kapitel erklären wir, was Seccomp ist und wie es verwendet wird. Anschließend lernen Sie, wie Sie Seccomp-Filter mit BPF-Programmen schreiben. Danach schauen wir uns die integrierten BPF-Hooks an, die im Kernel für Linux-Sicherheitsmodule enthalten sind.

Linux Security Modules (LSM) sind ein Framework, das eine Reihe von Funktionen bereitstellt, mit denen verschiedene Sicherheitsmodelle standardisiert implementiert werden können. LSM kann direkt im Kernel-Quellbaum verwendet werden, z. B. Apparmor, SELinux und Tomoyo.

Beginnen wir mit der Diskussion der Fähigkeiten von Linux.

Capabilities

Der Kern der Linux-Fähigkeiten besteht darin, dass Sie einem nicht privilegierten Prozess die Berechtigung erteilen müssen, eine bestimmte Aufgabe auszuführen, ohne jedoch suid für diesen Zweck zu verwenden oder den Prozess auf andere Weise privilegiert zu machen, wodurch die Möglichkeit eines Angriffs verringert und dem Prozess ermöglicht wird, bestimmte Aufgaben auszuführen. Wenn Ihre Anwendung beispielsweise einen privilegierten Port, beispielsweise 80, öffnen muss, anstatt den Prozess als Root auszuführen, können Sie ihr einfach die CAP_NET_BIND_SERVICE-Fähigkeit 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 auf Port 80 (dies ist ein privilegierter Port). Normalerweise führen wir es direkt nach der Kompilierung aus:

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

Da wir jedoch keine Root-Rechte gewähren, löst 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-Manager) ist ein Tool, das eine Shell mit bestimmten Funktionen ausführt.

In diesem Fall können Sie, wie bereits erwähnt, statt volle Root-Rechte zu gewähren, die privilegierte Portbindung aktivieren, indem Sie die Funktion cap_net_bind_service zusammen mit allem anderen bereitstellen, was bereits im Programm enthalten ist. 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 dieses Team ein wenig verstehen.

  • capsh – capsh als Shell verwenden.
  • —caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' – da wir den Benutzer ändern müssen (wir möchten nicht als Root ausgeführt werden), geben wir cap_net_bind_service und die Möglichkeit an, die Benutzer-ID tatsächlich zu ändern root auf niemanden, nämlich cap_setuid und cap_setgid.
  • –keep=1 – wir möchten die installierten Funktionen beibehalten, wenn wir vom Root-Konto wechseln.
  • —user=“nobody“ – der Endbenutzer, der das Programm ausführt, ist niemand.
  • —addamb=cap_net_bind_service — legt das Löschen der zugehörigen Funktionen nach dem Wechsel vom Root-Modus fest.
  • - -c "./capabilities" – führen Sie einfach das Programm aus.

Verknüpfte Funktionen sind eine besondere Art von Funktionen, die von untergeordneten Programmen geerbt werden, wenn das aktuelle Programm sie mit execve() ausführt. Nur Fähigkeiten, die zugeordnet werden dürfen, also Umgebungsfunktionen sind, können vererbt werden.

Sie fragen sich wahrscheinlich, was +eip bedeutet, nachdem Sie die Funktion in der Option --caps angegeben haben. Diese Flags werden verwendet, um zu bestimmen, dass die Fähigkeit:

-muss aktiviert sein (p);

- zur Nutzung verfügbar (e);

-kann von untergeordneten Prozessen (i) geerbt werden.

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ärdatei der Funktionen ausgeführt 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 ohne Änderung der UID getan). Es sieht aus wie cap_net_bind_service+eip.

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

# 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 auch mit libcap schreiben. Weitere Informationen finden Sie unter man 3 libcap.

Beim Schreiben von Programmen kennt der Entwickler häufig nicht im Voraus alle Funktionen, die das Programm zur Laufzeit benötigt; Darüber hinaus können sich diese Funktionen in neuen Versionen ändern.

Um die Fähigkeiten unseres Programms besser zu verstehen, können wir das BCC-fähige Tool verwenden, das die 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

Das Gleiche können wir erreichen, indem wir bpftrace mit einem einzeiligen kprobe in der Kernelfunktion 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 capabilities

Dies wird etwa Folgendes ausgeben, 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 Fähigkeiten, die der Prozess benötigt, und da diese Ausgabe Nicht-Audit-Ereignisse enthält, sehen wir alle Nicht-Audit-Prüfungen und schließlich die erforderliche Fähigkeit, wobei das Audit-Flag (letztes in der Ausgabe) auf 1 gesetzt ist. Fähigkeit. Die Eines, das uns interessiert, ist CAP_NET_BIND_SERVICE. Es ist als Konstante im Kernel-Quellcode in der Datei include/uapi/linux/ability.h mit der Kennung 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">

Für Container wie runC oder Docker werden zur Laufzeit oft Funktionen aktiviert, um ihnen die Ausführung im unprivilegierten Modus zu ermöglichen, ihnen stehen jedoch nur die Funktionen zur Verfügung, die zum Ausführen der meisten Anwendungen erforderlich sind. Wenn eine Anwendung bestimmte Funktionen erfordert, kann Docker diese mithilfe von --cap-add bereitstellen:

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

Dieser Befehl verleiht dem Container die CAP_NET_ADMIN-Fähigkeit, sodass er eine Netzwerkverbindung konfigurieren kann, um die dummy0-Schnittstelle hinzuzufügen.

Im nächsten Abschnitt wird gezeigt, wie Sie Funktionen wie das Filtern verwenden, jedoch mit einer anderen Technik, die es uns ermöglicht, unsere eigenen Filter programmgesteuert zu implementieren.

Sekundenkomp

Seccomp steht für Secure Computing und ist eine im Linux-Kernel implementierte Sicherheitsschicht, die es Entwicklern ermöglicht, bestimmte Systemaufrufe zu filtern. Obwohl Seccomp in seinen Fähigkeiten mit Linux vergleichbar ist, ist es aufgrund seiner Fähigkeit, bestimmte Systemaufrufe zu verwalten, im Vergleich zu diesen wesentlich flexibler.

Seccomp- und Linux-Funktionen schließen sich nicht gegenseitig aus und werden oft zusammen verwendet, um von beiden Ansätzen zu profitieren. Beispielsweise möchten Sie möglicherweise einem Prozess die CAP_NET_ADMIN-Fähigkeit erteilen, ihm aber nicht erlauben, Socket-Verbindungen zu akzeptieren, wodurch die Systemaufrufe „accept“ und „accept4“ blockiert werden.

Die Seccomp-Filtermethode basiert auf BPF-Filtern, die im SECCOMP_MODE_FILTER-Modus arbeiten, 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 Prozessoranweisungen zum Zeitpunkt des Systemaufrufs und maximal sechs Systemaufrufargumente, ausgedrückt als uint64.

So sieht die Struktur seccomp_data aus dem Kernel-Quellcode 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 beidem filtern.

Nach dem Empfang jedes Seccomp-Pakets muss der Filter eine 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 durch einen der Rückgabewerte (Statuscodes) ausgedrückt.

- SECCOMP_RET_KILL_PROCESS – beendet den gesamten Prozess sofort nach dem Filtern eines Systemaufrufs, der aus diesem Grund nicht ausgeführt wird.

- SECCOMP_RET_KILL_THREAD – beendet den aktuellen Thread sofort 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 verboten und das Signal SIGSYS (Bad System Call) wird an die Aufgabe gesendet, die ihn aufruft.

- SECCOMP_RET_ERRNO – Der Systemaufruf wird nicht ausgeführt und ein Teil des SECCOMP_RET_DATA-Filterrückgabewerts wird als errno-Wert an den Benutzerbereich übergeben. Abhängig von der 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-Tracer zu benachrichtigen. - PTRACE_O_TRACESECCOMP, um abzufangen, wenn ein Systemaufruf ausgeführt wird, um diesen Prozess zu sehen und zu steuern. Wenn kein Tracer 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 aufgelöst und protokolliert.

- SECCOMP_RET_ALLOW – der Systemaufruf wird einfach erlaubt.

ptrace ist ein Systemaufruf zur Implementierung von Tracing-Mechanismen in einem Prozess namens Tracee mit der Möglichkeit, die Ausführung des Prozesses zu überwachen und zu steuern. Das Trace-Programm kann die Ausführung effektiv beeinflussen und die Speicherregister des Tracees ändern. Im Seccomp-Kontext 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 werden Sie von Zeit zu Zeit auf verschiedene Fehler stoßen, die durch einen Rückgabewert vom Typ SECCOMP_RET_ERRNO gekennzeichnet sind. Um einen Fehler zu melden, gibt der Systemaufruf seccomp -1 statt 0 zurück.

Folgende Fehler sind möglich:

- EACCESS - Der Anrufer darf keinen Systemaufruf tätigen. Dies geschieht normalerweise, weil es keine CAP_SYS_ADMIN-Berechtigungen hat oder no_new_privs nicht mit prctl festgelegt wurde (wir werden später darüber sprechen).

– 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 vom Kernel in der aktuellen Konfiguration nicht unterstützt;

-die angegebenen Flags sind für den angeforderten Vorgang ungültig;

-operation enthält BPF_ABS, es gibt jedoch Probleme mit dem angegebenen Offset, der möglicherweise die Größe der seccomp_data-Struktur überschreitet;

-die Anzahl der an den Filter übergebenen Anweisungen überschreitet das Maximum;

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

- EOPNOTSUPP – die Operation gab an, dass die Aktion mit SECCOMP_GET_ACTION_AVAIL verfügbar war, der Kernel jedoch keine Rückgaben 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, der es einem User-Space-Programm ermöglicht, bestimmte Aspekte eines Prozesses zu manipulieren (einzustellen und abzurufen), z. B. Byte-Endianness, Thread-Namen, sicherer Berechnungsmodus (Seccomp), Berechtigungen, Perf-Ereignisse usw.

Seccomp kommt Ihnen vielleicht wie eine Sandbox-Technologie vor, ist es aber nicht. Seccomp ist ein Dienstprogramm, mit dem Benutzer einen Sandbox-Mechanismus entwickeln können. Schauen wir uns nun an, wie Benutzerinteraktionsprogramme mithilfe eines Filters erstellt werden, der direkt vom Seccomp-Systemaufruf aufgerufen wird.

Beispiel für einen BPF-Seccomp-Filter

Hier zeigen wir, wie man die beiden zuvor besprochenen Aktionen kombiniert, nämlich:

— Wir werden ein Seccomp BPF-Programm schreiben, das je nach den getroffenen Entscheidungen als Filter mit unterschiedlichen Rückkehrcodes verwendet wird;

— Laden Sie den Filter mit prctl.

Zunächst 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 wird, die auf y gesetzt sind. Auf einer funktionierenden Maschine können Sie dies folgendermaßen überprüfen:

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

Der Rest des Codes ist eine zweiteilige install_filter-Funktion. 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 mithilfe der Makros BPF_STMT und BPF_JUMP festgelegt, die in der Datei linux/filter.h definiert sind.
Gehen wir die Anweisungen durch.

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, arch))) – das System lädt und akkumuliert aus 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 Akkumulatorkonstante BPF_K gleich arch ist. Wenn ja, springt bei Offset 0 zur nächsten Anweisung, andernfalls springt bei Offset 3 (in diesem Fall), um einen Fehler auszulösen, weil arch nicht übereinstimmt.

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) – Lädt und akkumuliert von BPF_LD in Form des Wortes BPF_W, das die Systemaufrufnummer ist, die im festen Offset von BPF_ABS enthalten ist.

– BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) – vergleicht die Systemaufrufnummer mit dem Wert der Variablen nr. Wenn sie gleich sind, wird mit der nächsten Anweisung fortgefahren und der Systemaufruf deaktiviert. Andernfalls wird der Systemaufruf mit SECCOMP_RET_ALLOW zugelassen.

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

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) – beendet das Programm mit BPF_RET und ermöglicht die Ausführung des Systemaufrufs mit SECCOMP_RET_ALLOW.

SECCOMP IST CBPF
Sie fragen sich vielleicht, 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) und nicht eBPF, was bedeutet: Es verfügt über keine Register, sondern nur einen Akkumulator zum Speichern des letzten Berechnungsergebnisses, wie im Beispiel zu sehen ist.

• Zweitens akzeptiert Seccomp einen Zeiger auf ein Array von BPF-Anweisungen direkt und nichts anderes. Die von uns verwendeten Makros helfen lediglich dabei, diese Anweisungen auf programmiererfreundliche Weise zu spezifizieren.

Wenn Sie weitere Hilfe beim Verständnis dieser Assembly benötigen, ziehen Sie den 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 Länge des Filters enthält. Diese Datenstruktur wird als Argument für die Deklaration des Prozesses zur späteren Ausführung benötigt:

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: das Programm selbst laden! Dazu verwenden wir prctl und verwenden PR_SET_SECCOMP als Option, um in den sicheren Computermodus zu wechseln. Dann weisen wir den Modus an, den Filter mit SECCOMP_MODE_FILTER zu laden, der 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 install_filter-Funktion verwenden, aber vorher müssen wir prctl verwenden, um PR_SET_NO_NEW_PRIVS für die aktuelle Ausführung festzulegen und so die Situation zu vermeiden, in der untergeordnete Prozesse mehr Privilegien erhalten als ihre übergeordneten. Damit können wir die folgenden prctl-Aufrufe in der Funktion install_filter durchführen, ohne über Root-Rechte zu verfügen.

Jetzt können wir die Funktion install_filter aufrufen. Lassen Sie uns alle Schreibsystemaufrufe im Zusammenhang mit der X86-64-Architektur blockieren und einfach eine Berechtigung erteilen, die alle Versuche blockiert. Nach der Installation 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. Um unser Programm zu kompilieren, können wir entweder clang oder gcc verwenden. 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 – ls 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

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

./filter-write "ls -la"

Bei der Ausführung erzeugt dieses Programm eine völlig leere Ausgabe. Allerdings können wir strace verwenden, um zu sehen, was los ist:

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

Das Ergebnis der Arbeit ist stark gekürzt, aber der entsprechende Teil davon zeigt, dass Datensätze mit dem EPERM-Fehler blockiert werden – dem gleichen, den wir konfiguriert haben. Das bedeutet, dass das Programm nichts ausgibt, da es nicht auf den Systemaufruf write 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 Sie damit machen können. Aber möchten Sie nicht dasselbe mit eBPF anstelle von cBPF erreichen, um dessen volle Leistung auszuschöpfen?

Wenn man über eBPF-Programme nachdenkt, denken die meisten Leute, dass sie sie einfach schreiben und mit Administratorrechten laden. Obwohl diese Aussage im Allgemeinen zutrifft, implementiert der Kernel eine Reihe von Mechanismen zum Schutz von eBPF-Objekten auf verschiedenen Ebenen. Diese Mechanismen werden BPF-LSM-Traps genannt.

BPF-LSM-Fallen

Um eine architekturunabhängige Überwachung von Systemereignissen zu ermöglichen, implementiert LSM das Konzept von 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 des 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 Traps 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 unterschiedlichen Phasen der Ausführung aufgerufen:

– security_bpf – führt eine erste Prüfung der ausgeführten BPF-Systemaufrufe durch;

- security_bpf_map – prüft, wann der Kernel einen Dateideskriptor für die Karte 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 in BPF-Maps initialisiert ist;

- security_bpf_map_free – prüft, ob das Sicherheitsfeld in BPF-Maps 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.

Wenn wir das alles sehen, verstehen wir: Die Idee hinter LSM-BPF-Interceptoren besteht darin, 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 lässt sich nicht pauschal für alles implementieren, was Sie schützen möchten. 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 abzusichern, besteht darin, unterschiedliche Schutzebenen von verschiedenen Positionen aus zu organisieren, sodass die Reduzierung der Sicherheit einer Ebene keinen Zugriff auf das gesamte System ermöglicht. Die Kernentwickler haben großartige Arbeit geleistet und uns eine Reihe verschiedener Ebenen und Kontaktpunkte zur Verfügung gestellt. Wir hoffen, dass wir Ihnen ein gutes Verständnis davon vermittelt haben, was Ebenen sind und wie Sie BPF-Programme verwenden, um mit ihnen zu arbeiten.

Über die Autoren

David Calavera ist der CTO bei Netlify. Er arbeitete im Docker-Support und trug zur Entwicklung der Runc-, Go- und BCC-Tools sowie anderer Open-Source-Projekte bei. Bekannt für seine Arbeit an Docker-Projekten und der Entwicklung des Docker-Plugin-Ökosystems. David hat eine große Leidenschaft für Flammendiagramme und ist stets auf der Suche nach einer Optimierung der Leistung.

Lorenzo Fontana arbeitet im Open-Source-Team von Sysdig, wo er sich hauptsächlich auf Falco konzentriert, ein Projekt der Cloud Native Computing Foundation, das Container-Laufzeitsicherheit und Anomalieerkennung durch ein Kernel-Modul und eBPF bietet. Seine Leidenschaft gilt verteilten Systemen, softwaredefinierten Netzwerken, dem Linux-Kernel und Leistungsanalysen.

» Weitere Details zum Buch finden Sie unter Website des Verlags
» Inhaltsverzeichnis
» Auszug

Für Khabrozhiteley 25 % Rabatt mit Gutschein - Linux

Nach Bezahlung der Papierversion des Buches wird ein elektronisches Buch per E-Mail verschickt.

Source: habr.com

Kommentar hinzufügen