Książka „BPF dla monitorowania systemu Linux”

Książka „BPF dla monitorowania systemu Linux”Witam mieszkańców Khabro! Maszyna wirtualna BPF jest jednym z najważniejszych komponentów jądra Linuksa. Jego właściwe wykorzystanie umożliwi inżynierom systemowym znalezienie usterek i rozwiązanie nawet najbardziej skomplikowanych problemów. Dowiesz się, jak pisać programy monitorujące i modyfikujące zachowanie jądra, jak bezpiecznie implementować kod monitorujący zdarzenia w jądrze i wiele więcej. David Calavera i Lorenzo Fontana pomogą Ci odblokować moc BPF. Poszerzaj swoją wiedzę z zakresu optymalizacji wydajności, sieci, bezpieczeństwa. - Użyj BPF do monitorowania i modyfikowania zachowania jądra Linuksa. - Wstrzyknij kod, aby bezpiecznie monitorować zdarzenia jądra bez konieczności ponownej kompilacji jądra lub ponownego uruchamiania systemu. — Korzystaj z wygodnych przykładów kodu w C, Go lub Pythonie. - Przejmij kontrolę, będąc właścicielem cyklu życia programu BPF.

Bezpieczeństwo jądra systemu Linux, jego funkcje i Seccomp

BPF zapewnia potężny sposób rozszerzania jądra bez poświęcania stabilności, bezpieczeństwa i szybkości. Z tego powodu twórcy jądra uznali, że dobrym pomysłem byłoby wykorzystanie jego wszechstronności do poprawy izolacji procesów w Seccomp poprzez wdrożenie filtrów Seccomp obsługiwanych przez programy BPF, znanych również jako Seccomp BPF. W tym rozdziale wyjaśnimy, czym jest Seccomp i jak go używać. Następnie dowiesz się, jak pisać filtry Seccomp przy użyciu programów BPF. Następnie przyjrzymy się wbudowanym hookom BPF zawartym w jądrze dla modułów bezpieczeństwa Linuksa.

Linux Security Modules (LSM) to platforma udostępniająca zestaw funkcji, których można użyć do wdrożenia różnych modeli bezpieczeństwa w ustandaryzowany sposób. LSM można używać bezpośrednio w drzewie źródeł jądra, takim jak Apparmor, SELinux i Tomoyo.

Zacznijmy od omówienia możliwości Linuksa.

Możliwości

Istota możliwości Linuksa polega na tym, że trzeba udzielić nieuprzywilejowanemu procesowi pozwolenia na wykonanie określonego zadania, ale bez użycia w tym celu suidu, lub w inny sposób uprzywilejować proces, zmniejszając możliwość ataku i umożliwiając procesowi wykonanie określonych zadań. Na przykład, jeśli Twoja aplikacja musi otworzyć uprzywilejowany port, powiedzmy 80, zamiast uruchamiać proces jako root, możesz po prostu nadać jej funkcję CAP_NET_BIND_SERVICE.

Rozważmy program Go o nazwie main.go:

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

Ten program obsługuje serwer HTTP na porcie 80 (jest to port uprzywilejowany). Zwykle uruchamiamy go natychmiast po kompilacji:

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

Ponieważ jednak nie przyznajemy uprawnień roota, ten kod zgłosi błąd podczas wiązania portu:

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

capsh (menedżer powłoki) to narzędzie uruchamiające powłokę z określonym zestawem możliwości.

W tym przypadku, jak już wspomniano, zamiast nadawać pełne prawa roota, możesz włączyć uprzywilejowane wiązanie portów, udostępniając funkcję cap_net_bind_service wraz ze wszystkim innym, co jest już w programie. Aby to zrobić, możemy zawrzeć nasz program w capsh:

# 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"

Rozumiemy trochę ten zespół.

  • capsh - użyj capsh jako powłoki.
  • —caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' - ponieważ musimy zmienić użytkownika (nie chcemy uruchamiać jako root), określimy cap_net_bind_service i możliwość faktycznej zmiany identyfikatora użytkownika z root do nikogo, a mianowicie cap_setuid i cap_setgid.
  • —keep=1 — chcemy zachować zainstalowane możliwości przy przełączaniu się z konta root.
  • —user=„nobody” — użytkownik końcowy uruchamiający program będzie nikim.
  • —addamb=cap_net_bind_service — ustawia czyszczenie powiązanych możliwości po przełączeniu z trybu root.
  • - -c "./capabilities" - po prostu uruchom program.

Możliwości połączone to specjalny rodzaj możliwości, które są dziedziczone przez programy potomne, gdy bieżący program wykonuje je za pomocą funkcji execve(). Dziedziczone mogą być tylko możliwości, które można skojarzyć, czyli innymi słowy jako możliwości środowiska.

Prawdopodobnie zastanawiasz się, co oznacza +eip po określeniu możliwości w opcji --caps. Flagi te służą do określenia, czy zdolność:

-musi być aktywowany (p);

- dostępne do użytku (e);

-może być dziedziczony przez procesy potomne (i).

Ponieważ chcemy używać usługi cap_net_bind_service, musimy to zrobić za pomocą flagi e. Następnie uruchomimy powłokę w poleceniu. Spowoduje to uruchomienie pliku binarnego możliwości i musimy oznaczyć go flagą i. Na koniec chcemy, aby funkcja była włączona (zrobiliśmy to bez zmiany UID) za pomocą p. Wygląda jak cap_net_bind_service+eip.

Możesz sprawdzić wynik za pomocą ss. Skróćmy nieco dane wyjściowe, aby zmieściły się na stronie, ale będą one zawierać powiązany port i identyfikator użytkownika inny niż 0, w tym przypadku 65:

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

W tym przykładzie użyliśmy capsh, ale możesz napisać powłokę za pomocą libcap. Aby uzyskać więcej informacji, zobacz man 3 libcap.

Podczas pisania programów dość często programista nie zna z góry wszystkich funkcji, których program potrzebuje w czasie wykonywania; Co więcej, funkcje te mogą ulec zmianie w nowych wersjach.

Aby lepiej zrozumieć możliwości naszego programu, możemy skorzystać z narzędzia obsługującego BCC, które ustawia kprobe dla funkcji jądra cap_capable:

/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

To samo możemy osiągnąć, używając bpftrace z jednoliniową kprobe w funkcji jądra cap_capable:

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

Jeśli po kprobe zostaną włączone możliwości naszego programu, wyświetli się komunikat podobny do poniższego:

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

Piąta kolumna to możliwości potrzebne procesowi, a ponieważ dane wyjściowe obejmują zdarzenia niebędące audytem, ​​widzimy wszystkie kontrole niebędące audytem i na końcu wymaganą zdolność z flagą audytu (ostatnią w wynikach) ustawioną na 1. Zdolność. interesująca nas to CAP_NET_BIND_SERVICE, jest ona zdefiniowana jako stała w kodzie źródłowym jądra w pliku include/uapi/linux/ability.h o identyfikatorze 10:

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

Możliwości są często włączane w czasie wykonywania kontenerów, takich jak runC lub Docker, aby umożliwić im działanie w trybie nieuprzywilejowanym, ale udostępniane są tylko funkcje potrzebne do uruchomienia większości aplikacji. Gdy aplikacja wymaga pewnych możliwości, Docker może je udostępnić za pomocą --cap-add:

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

To polecenie nada kontenerowi funkcję CAP_NET_ADMIN, umożliwiając skonfigurowanie łącza sieciowego w celu dodania interfejsu dummy0.

W następnej sekcji pokazano, jak korzystać z funkcji takich jak filtrowanie, ale przy użyciu innej techniki, która pozwala nam programowo implementować własne filtry.

Drugi komp

Seccomp oznacza Secure Computing i jest warstwą bezpieczeństwa zaimplementowaną w jądrze Linuksa, która umożliwia programistom filtrowanie niektórych wywołań systemowych. Chociaż Seccomp ma możliwości porównywalne z Linuksem, jego zdolność do zarządzania niektórymi wywołaniami systemowymi czyni go znacznie bardziej elastycznym w porównaniu z nimi.

Funkcje Seccomp i Linux nie wykluczają się wzajemnie i często są używane razem, aby skorzystać z obu podejść. Na przykład możesz chcieć nadać procesowi funkcję CAP_NET_ADMIN, ale nie pozwolić mu na akceptowanie połączeń z gniazdem, blokując wywołania systemowe Accept i Accept4.

Metoda filtrowania Seccomp opiera się na filtrach BPF pracujących w trybie SECCOMP_MODE_FILTER, a filtrowanie wywołań systemowych odbywa się analogicznie jak w przypadku pakietów.

Filtry Seccomp są ładowane przy użyciu prctl za pomocą operacji PR_SET_SECCOMP. Filtry te mają postać programu BPF, który jest wykonywany dla każdego pakietu Seccomp reprezentowanego przez strukturę seccomp_data. Struktura ta zawiera architekturę referencyjną, wskaźnik do instrukcji procesora w momencie wywołania systemowego i maksymalnie sześć argumentów wywołania systemowego wyrażonych jako uint64.

Tak wygląda struktura seccomp_data z kodu źródłowego jądra w pliku linux/seccomp.h:

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

Jak widać z tej struktury, możemy filtrować według wywołania systemowego, jego argumentów lub kombinacji obu.

Po otrzymaniu każdego pakietu Seccomp filtr musi przeprowadzić przetwarzanie, aby podjąć ostateczną decyzję i poinformować jądro, co ma dalej robić. Ostateczna decyzja wyrażana jest przez jedną ze zwracanych wartości (kody statusu).

- SECCOMP_RET_KILL_PROCESS - zabija cały proces natychmiast po odfiltrowaniu wywołania systemowego, które z tego powodu nie jest wykonywane.

- SECCOMP_RET_KILL_THREAD - kończy bieżący wątek natychmiast po odfiltrowaniu wywołania systemowego, które z tego powodu nie jest wykonywane.

— SECCOMP_RET_KILL — alias dla SECCOMP_RET_KILL_THREAD, pozostawiony dla kompatybilności wstecznej.

- SECCOMP_RET_TRAP - wywołanie systemowe jest zabronione, a do zadania, które je wywołuje, wysyłany jest sygnał SIGSYS (Bad System Call).

- SECCOMP_RET_ERRNO - Wywołanie systemowe nie jest wykonywane, a część wartości zwracanej przez filtr SECCOMP_RET_DATA jest przekazywana do przestrzeni użytkownika jako wartość errno. W zależności od przyczyny błędu zwracane są różne wartości errno. Lista numerów błędów znajduje się w następnej sekcji.

- SECCOMP_RET_TRACE - Służy do powiadamiania modułu śledzącego ptrace za pomocą - PTRACE_O_TRACESECCOMP do przechwytywania, gdy wykonywane jest wywołanie systemowe w celu zobaczenia i kontrolowania tego procesu. Jeśli moduł śledzący nie jest podłączony, zwracany jest błąd, errno jest ustawiane na -ENOSYS i wywołanie systemowe nie jest wykonywane.

- SECCOMP_RET_LOG - wywołanie systemowe zostało rozwiązane i zarejestrowane.

- SECCOMP_RET_ALLOW - wywołanie systemowe jest po prostu dozwolone.

ptrace to wywołanie systemowe umożliwiające zaimplementowanie mechanizmów śledzenia w procesie zwanym tracee, z możliwością monitorowania i kontrolowania wykonania procesu. Program śledzący może skutecznie wpływać na wykonanie i modyfikować rejestry pamięci programu Trace. W kontekście Seccomp ptrace jest używane, gdy jest wyzwalane przez kod stanu SECCOMP_RET_TRACE, więc moduł śledzący może uniemożliwić wykonanie wywołania systemowego i zaimplementować własną logikę.

Błędy Seccompa

Od czasu do czasu podczas pracy z Seccomp możesz napotkać różne błędy, które są identyfikowane przez wartość zwracaną typu SECCOMP_RET_ERRNO. Aby zgłosić błąd, wywołanie systemowe seccomp zwróci -1 zamiast 0.

Możliwe są następujące błędy:

- EACCESS - Osoba dzwoniąca nie może wykonywać wywołań systemowych. Zwykle dzieje się tak, ponieważ nie ma uprawnień CAP_SYS_ADMIN lub nie ustawiono no_new_privs za pomocą prctl (porozmawiamy o tym później);

— EFAULT — przekazane argumenty (argumenty w strukturze seccomp_data) nie mają poprawnego adresu;

— EINVAL — mogą być tu cztery przyczyny:

-żądana operacja jest nieznana lub nie jest obsługiwana przez jądro w bieżącej konfiguracji;

-określone flagi nie są ważne dla żądanej operacji;

-operacja zawiera BPF_ABS, ale występują problemy z określonym przesunięciem, które może przekroczyć rozmiar struktury seccomp_data;

-liczba instrukcji przekazanych do filtra przekracza maksimum;

— ENOMEM — za mało pamięci do wykonania programu;

- EOPNOTSUPP - operacja wskazała, że ​​przy SECCOMP_GET_ACTION_AVAIL akcja była dostępna, ale jądro nie obsługuje zwrotów w argumentach;

— ESRCH — wystąpił problem podczas synchronizacji innego strumienia;

- ENOSYS - Do akcji SECCOMP_RET_TRACE nie jest dołączony żaden znacznik.

prctl to wywołanie systemowe, które pozwala programowi działającemu w przestrzeni użytkownika manipulować (ustawiać i pobierać) określone aspekty procesu, takie jak koniec bajtów, nazwy wątków, tryb bezpiecznych obliczeń (Seccomp), uprawnienia, zdarzenia Perf itp.

Seccomp może wydawać Ci się technologią typu sandbox, ale tak nie jest. Seccomp to narzędzie, które pozwala użytkownikom rozwijać mechanizm piaskownicy. Przyjrzyjmy się teraz, jak tworzone są programy interakcji z użytkownikiem przy użyciu filtra wywoływanego bezpośrednio przez wywołanie systemowe Seccomp.

Przykład filtra BPF Seccomp

Tutaj pokażemy, jak połączyć dwa omówione wcześniej działania, a mianowicie:

— napiszemy program Seccomp BPF, który posłuży jako filtr z różnymi kodami powrotu w zależności od podjętych decyzji;

— załaduj filtr za pomocą prctl.

Najpierw potrzebujesz nagłówków ze standardowej biblioteki i jądra Linuksa:

#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>

Przed wypróbowaniem tego przykładu musimy upewnić się, że jądro jest skompilowane z CONFIG_SECCOMP i CONFIG_SECCOMP_FILTER ustawionymi na y. Na działającej maszynie możesz to sprawdzić w ten sposób:

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

Pozostała część kodu to dwuczęściowa funkcja install_filter. Pierwsza część zawiera naszą listę instrukcji filtrowania BPF:

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),
  };

Instrukcje ustawiane są przy pomocy makr BPF_STMT i BPF_JUMP zdefiniowanych w pliku linux/filter.h.
Przejdźmy do instrukcji.

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, arch))) - system ładuje i gromadzi z BPF_LD w postaci słowa BPF_W, dane pakietowe znajdują się w stałym przesunięciu BPF_ABS.

- BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) - sprawdza za pomocą BPF_JEQ, czy wartość architektury w stałej akumulatora BPF_K jest równa arch. Jeśli tak, przeskakuje z przesunięciem 0 do następnej instrukcji, w przeciwnym razie przeskakuje z przesunięciem 3 (w tym przypadku), aby zgłosić błąd, ponieważ arch nie pasuje.

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) - Ładuje i gromadzi z BPF_LD w postaci słowa BPF_W, które jest numerem wywołania systemowego zawartym w stałym przesunięciu BPF_ABS.

— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) — porównuje numer wywołania systemowego z wartością zmiennej nr. Jeśli są równe, przechodzi do następnej instrukcji i wyłącza wywołanie systemowe, w przeciwnym razie pozwala na wywołanie systemowe z SECCOMP_RET_ALLOW.

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (błąd & SECCOMP_RET_DATA)) - kończy program z BPF_RET i w rezultacie generuje błąd SECCOMP_RET_ERRNO z numerem ze zmiennej err.

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) - kończy program z BPF_RET i pozwala na wykonanie wywołania systemowego przy pomocy SECCOMP_RET_ALLOW.

SECCOMP TO CBPF
Być może zastanawiasz się, dlaczego zamiast skompilowanego obiektu ELF lub skompilowanego programu C w JIT użyto listy instrukcji.

Są ku temu dwa powody.

• Po pierwsze, Seccomp używa cBPF (klasyczny BPF), a nie eBPF, co oznacza: nie ma rejestrów, a jedynie akumulator do przechowywania ostatniego wyniku obliczenia, jak widać na przykładzie.

• Po drugie, Seccomp akceptuje bezpośrednio wskaźnik do tablicy instrukcji BPF i nic więcej. Makra, których użyliśmy, po prostu pomagają określić te instrukcje w sposób przyjazny dla programisty.

Jeśli potrzebujesz dodatkowej pomocy w zrozumieniu tego zestawu, rozważ pseudokod, który robi to samo:

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

Po zdefiniowaniu kodu filtra w strukturze Socket_filter należy zdefiniować sock_fprog zawierający kod i obliczoną długość filtra. Ta struktura danych jest potrzebna jako argument do zadeklarowania późniejszego uruchomienia procesu:

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

W funkcji install_filter pozostała już tylko jedna rzecz - załadować sam program! Aby to zrobić, używamy prctl, przyjmując PR_SET_SECCOMP jako opcję wejścia w tryb bezpiecznego przetwarzania. Następnie mówimy trybowi, aby załadował filtr za pomocą SECCOMP_MODE_FILTER, który jest zawarty w zmiennej prog typu sock_fprog:

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

Na koniec możemy użyć naszej funkcji install_filter, ale wcześniej musimy użyć prctl, aby ustawić PR_SET_NO_NEW_PRIVS dla bieżącego wykonania i w ten sposób uniknąć sytuacji, w której procesy potomne otrzymają więcej uprawnień niż ich rodzice. Dzięki temu możemy wykonać następujące wywołania prctl w funkcji install_filter bez posiadania uprawnień roota.

Teraz możemy wywołać funkcję install_filter. Zablokujmy wszystkie wywołania systemowe zapisu związane z architekturą X86-64 i po prostu dajmy pozwolenie, które blokuje wszelkie próby. Po zainstalowaniu filtra kontynuujemy wykonanie korzystając z pierwszego argumentu:

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]);
 }

Zacznijmy. Aby skompilować nasz program, możemy użyć clang lub gcc, w obu przypadkach jest to po prostu kompilacja pliku main.c bez specjalnych opcji:

clang main.c -o filter-write

Jak zauważyliśmy, zablokowaliśmy wszystkie wpisy w programie. Aby to przetestować, potrzebujesz programu, który coś wypisuje - ls wydaje się dobrym kandydatem. Tak się zwykle zachowuje:

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

Wspaniały! Oto jak wygląda użycie naszego programu opakowującego: Po prostu przekazujemy program, który chcemy przetestować, jako pierwszy argument:

./filter-write "ls -la"

Po wykonaniu program ten generuje całkowicie puste dane wyjściowe. Możemy jednak użyć strace, aby zobaczyć, co się dzieje:

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

Wynik pracy jest znacznie skrócony, ale odpowiednia jej część pokazuje, że zapisy są blokowane błędem EPERM - tym samym, który skonfigurowaliśmy. Oznacza to, że program nic nie wyprowadza, ponieważ nie może uzyskać dostępu do wywołania systemowego zapisu:

[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)

Teraz rozumiesz, jak działa Seccomp BPF i masz dobre pojęcie o tym, co możesz z nim zrobić. Ale czy nie chciałbyś osiągnąć tego samego z eBPF zamiast z cBPF, aby wykorzystać jego pełną moc?

Myśląc o programach eBPF, większość ludzi myśli, że po prostu je pisze i ładuje z uprawnieniami administratora. Chociaż to stwierdzenie jest ogólnie prawdziwe, jądro implementuje zestaw mechanizmów chroniących obiekty eBPF na różnych poziomach. Mechanizmy te nazywane są pułapkami BPF LSM.

Pułapki BPF LSM

Aby zapewnić niezależne od architektury monitorowanie zdarzeń systemowych, LSM implementuje koncepcję pułapek. Wywołanie hooka jest technicznie podobne do wywołania systemowego, ale jest niezależne od systemu i zintegrowane z infrastrukturą. LSM zapewnia nową koncepcję, w której warstwa abstrakcji może pomóc uniknąć problemów napotykanych podczas obsługi wywołań systemowych w różnych architekturach.

W chwili pisania tego tekstu jądro ma siedem zaczepów powiązanych z programami BPF, a SELinux jest jedynym wbudowanym LSM, który je implementuje.

Kod źródłowy pułapek znajduje się w drzewie jądra w pliku 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);

Każdy z nich będzie wywoływany na różnych etapach wykonania:

— security_bpf — wykonuje wstępną kontrolę wykonanych wywołań systemowych BPF;

- security_bpf_map - sprawdza, kiedy jądro zwraca deskryptor pliku dla mapy;

- security_bpf_prog - sprawdza, kiedy jądro zwraca deskryptor pliku dla programu eBPF;

— security_bpf_map_alloc — sprawdza, czy pole bezpieczeństwa wewnątrz map BPF zostało zainicjowane;

- security_bpf_map_free - sprawdza, czy pole bezpieczeństwa jest wyczyszczone wewnątrz map BPF;

— security_bpf_prog_alloc — sprawdza, czy pole bezpieczeństwa jest inicjowane w programach BPF;

- security_bpf_prog_free - sprawdza, czy pole bezpieczeństwa jest wyczyszczone w programach BPF.

Teraz, widząc to wszystko, rozumiemy: ideą przechwytywaczy LSM BPF jest to, że mogą zapewnić ochronę każdemu obiektowi eBPF, zapewniając, że tylko osoby posiadające odpowiednie uprawnienia mogą wykonywać operacje na kartach i programach.

Streszczenie

Bezpieczeństwo nie jest czymś, co można wdrożyć w uniwersalny sposób dla wszystkiego, co chcesz chronić. Ważne jest, aby móc chronić systemy na różnych poziomach i na różne sposoby. Wierzcie lub nie, ale najlepszym sposobem zabezpieczenia systemu jest zorganizowanie różnych poziomów ochrony z różnych stanowisk, tak aby zmniejszenie bezpieczeństwa o jeden poziom nie pozwoliło na dostęp do całego systemu. Główni programiści wykonali świetną robotę, udostępniając nam zestaw różnych warstw i punktów styku. Mamy nadzieję, że dobrze zrozumieliśmy, czym są warstwy i jak używać programów BPF do pracy z nimi.

O autorach

Davida Calaverę jest dyrektorem technicznym w Netlify. Pracował we wsparciu Dockera i brał udział w rozwoju narzędzi Runc, Go i BCC, a także innych projektów open source. Znany z pracy nad projektami Docker i rozwojem ekosystemu wtyczek Docker. David jest wielkim pasjonatem wykresów płomienia i zawsze stara się optymalizować wydajność.

Lorenzo Fontana pracuje w zespole open source w firmie Sysdig, gdzie koncentruje się głównie na Falco, projekcie Cloud Native Computing Foundation, który zapewnia bezpieczeństwo środowiska wykonawczego kontenerów i wykrywanie anomalii za pośrednictwem modułu jądra i eBPF. Pasjonuje się systemami rozproszonymi, sieciami zdefiniowanymi programowo, jądrem Linuksa i analizą wydajności.

» Więcej szczegółów na temat książki można znaleźć na stronie strona wydawcy
» Spis treści
» Fragment

Dla Khabrozhiteley 25% zniżki przy użyciu kuponu - Linux

Po opłaceniu papierowej wersji książki, e-mailem zostanie wysłana książka elektroniczna.

Źródło: www.habr.com

Dodaj komentarz