Cześć, Habrzytelnicy! Wirtualna maszyna BPF to jeden z najważniejszych komponentów rdzenia Linuxa. Jej właściwe zastosowanie pozwoli inżynierom systemowym wykrywać awarie i rozwiązywać nawet najtrudniejsze problemy. Nauczysz się pisać programy, które będą śledzić i modyfikować zachowanie rdzenia, możesz bezpiecznie wprowadzać kod do monitorowania zdarzeń w rdzeniu i wiele więcej. David Calavera i Lorenzo Fontana pomogą ci odkryć możliwości BPF. Rozszerz swoją wiedzę na temat optymalizacji wydajności, sieci i bezpieczeństwa. — Używaj BPF do śledzenia i modyfikacji zachowania rdzenia Linuxa. — Wprowadzaj kod do bezpiecznego monitorowania zdarzeń w rdzeniu — bez potrzeby rekompilacji rdzenia lub ponownego uruchamiania systemu. — Korzystaj z wygodnych przykładów kodu w C, Go lub Pythonie. — Zarządzaj sytuacją, opanowując cykl życia programu BPF.
Bezpieczeństwo rdzenia Linuxa, jego możliwości i Seccomp
BPF oferuje potężny sposób na rozszerzanie rdzenia bez uszczerbku dla stabilności, bezpieczeństwa i prędkości. Z tego powodu deweloperzy rdzenia uznali, że warto wykorzystać jego uniwersalność do poprawy izolacji procesów w Seccomp poprzez wdrożenie filtrów Seccomp wspieranych przez programy BPF, znanych również jako Seccomp BPF. W tej rozdziale opowiemy, czym jest Seccomp i jak jest stosowany. Następnie dowiesz się, jak pisać filtry Seccomp przy użyciu programów BPF. Po tym przyjrzymy się wbudowanym pułapkom BPF, które są w rdzeniu dla modułów bezpieczeństwa Linuxa.
Moduły bezpieczeństwa Linuxa (LSM) to platforma, która oferuje zestaw funkcji, które można zastosować do standaryzowanej realizacji różnych modeli bezpieczeństwa. LSM można używać bezpośrednio w drzewie kodu źródłowego rdzenia, na przykład Apparmor, SELinux i Tomoyo.
Zacznijmy od omówienia możliwości Linuxa.
Możliwości
Istotą możliwości Linuksa jest to, że musisz zezwolić nieuprzywilejowanemu procesowi na wykonanie określonego zadania, ale bez użycia suid, aby to osiągnąć, lub w inny sposób uczynić proces uprzywilejowanym, zmniejszając możliwości ataków i umożliwiając procesowi wykonywanie 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 przyznać mu możliwość 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). Zazwyczaj uruchamiamy go zaraz po kompilacji:
$ go build -o capabilities main.go
$ ./capabilitiesJednakże, ponieważ 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 1capsh (narzędzie do zarządzania powłoką) to narzędzie, które uruchamia powłokę z określonym zestawem możliwości.
W tym przypadku, jak już wspomniano, zamiast przyznawać pełne uprawnienia roota, można zezwolić na wiązanie uprzywilejowanych portów, przyznając możliwość cap_net_bind_service obok wszystkich innych, które już istnieją w programie. W tym celu możemy zainicjować 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"Rozłóżmy teraz tę komendę.
- capsh — używamy capsh jako powłokę.
- —caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' — ponieważ musimy zmienić użytkownika (nie chcemy uruchamiać z uprawnieniami roota), wskażemy cap_net_bind_service oraz zdolność do faktycznej zmiany identyfikatora użytkownika z roota na nobody, a mianowicie cap_setuid i cap_setgid.
- —keep=1 — chcemy zachować ustalone możliwości, gdy zostanie przeprowadzone przełączenie z konta root.
- —user='nobody' — ostatecznym użytkownikiem uruchamiającym program będzie nobody.
- —addamb=cap_net_bind_service — ustalamy czyszczenie powiązanych możliwości po przełączeniu z trybu roota.
- — -c "./capabilities" — po prostu uruchamiamy program.
Powiązane możliwości to szczególny rodzaj możliwości, które są dziedziczone przez programy potomne, gdy bieżący program wykonuje je za pomocą execve(). Mogą dziedziczyć tylko możliwości, które są dozwolone jako powiązane, lub innymi słowy, jako możliwości środowiska.
Prawdopodobnie zastanawiasz się, co oznacza +eip po wskazaniu możliwości w opcji —caps. Te flagi są używane do określenia, że możliwość:
- powinna być aktywna (p);
- dostępna do zastosowania (e);
- może być dziedziczona przez procesy podrzędne (i).
Ponieważ chcemy użyć cap_net_bind_service, musimy to zrobić z flagą e. Następnie uruchomimy powłokę w poleceniu. W rezultacie uruchomi się binarny plik capabilities, i musimy oznaczyć go flagą i. Na koniec chcemy, aby możliwość była aktywna (zrobiliśmy to, nie zmieniając UID) za pomocą p. To wygląda jak cap_net_bind_service+eip.
Możesz sprawdzić wynik za pomocą ss. Nieco skrócimy wyjście, aby zmieściło się na stronie, ale pokaże ono powiązany port i identyfikator użytkownika różny od 0, w tym przypadku 65 534:
# ss -tulpn -e -H | cut -d' ' -f17-
128 *:80 *:*
users:(("capabilities",pid=30040,fd=3)) uid:65534 ino:11311579 sk:2c v6only:0W tym przykładzie użyliśmy capsh, ale możesz napisać powłokę za pomocą libcap. Po więcej informacji skontaktuj się z man 3 libcap.
Podczas pisania programów deweloper często nie zna z góry wszystkich możliwości, które będą potrzebne programowi w czasie wykonywania; co więcej, w nowych wersjach te możliwości mogą się zmieniać.
Aby lepiej zrozumieć możliwości naszego programu, możemy wziąć narzędzie BCC capable, 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 1To samo możemy osiągnąć, używając bpftrace z jednowierszowym 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 capabilitiesTo wyprodukuje coś podobnego do poniższego, jeśli możliwości naszego programu będą aktywowane po kprobe:
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 1Piąta kolumna to możliwości, których potrzebuje proces, a ponieważ te dane wyjściowe zawierają również nieaudytowane zdarzenia, widzimy wszystkie nieaudytowane kontrole i na końcu wymaganą możliwość z flagą audytu (ostatnia w wyjściu), ustawioną na 1. Możliwość, która nas interesuje, 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 z identyfikatorem 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">Funkcje są często wykorzystywane podczas uruchamiania kontenerów, takich jak runC czy Docker, aby działały w trybie nieuprzywilejowanym, ale przyznano im tylko te funkcje, które są niezbędne do uruchomienia większości aplikacji. Gdy aplikacja wymaga określonych możliwości, można je dodać w Dockerze za pomocą —cap-add:
docker run -it --rm --cap-add=NET_ADMIN ubuntu ip link add dummy0 type dummyTa komenda przyzna kontenerowi możliwość CAP_NET_ADMIN, co pozwoli mu skonfigurować połączenie sieciowe do dodania interfejsu dummy0.
W następnej sekcji zaprezentowane jest wykorzystanie takich funkcji, jak filtrowanie, ale przy użyciu innej metody, która pozwoli nam programowo wdrożyć własne filtry.
Seccomp
Seccomp oznacza Secure Computing, to poziom bezpieczeństwa wdrażany w jądrze Linux, który pozwala deweloperom filtrować określone wywołania systemowe. Chociaż Seccomp jest porównywalny z możliwościami Linuxa, jego zdolność do zarządzania określonymi wywołaniami systemowymi sprawia, że jest znacznie bardziej elastyczny w porównaniu do nich.
Seccomp i możliwości Linuxa nie wykluczają się nawzajem, często są używane razem, aby skorzystać z obu podejść. Na przykład możesz chcieć dać procesowi możliwość CAP_NET_ADMIN, ale nie zezwolić mu na nawiązywanie połączeń za pomocą gniazda, blokując wywołania systemowe accept i accept4.
Metoda filtrowania Seccomp opiera się na filtrach BPF działających w trybie SECCOMP_MODE_FILTER, a filtrowanie wywołań systemowych wykonuje się tak samo, jak dla pakietów.
Filtry Seccomp są załadowane za pomocą prctl przy operacji PR_SET_SECCOMP. Te filtry mają postać programu BPF, który jest wykonywany dla każdego pakietu Seccomp przesłanego za pomocą struktury seccomp_data. Ta struktura zawiera architekturę referencyjną, wskaźnik instrukcji procesora w czasie wywołania systemowego oraz maksymalnie sześć argumentów wywołania systemowego, wyrażonych jako uint64.
Oto jak 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ć na podstawie wywołania systemowego, jego argumentów lub ich kombinacji.
Po otrzymaniu każdego pakietu filtr Seccomp musi przeprowadzić przetwarzanie, aby podjąć ostateczną decyzję i poinformować jądro, co robić dalej. Ostateczna decyzja wyrażana jest jednym z wartości zwracanych (kodów stanu).
— SECCOMP_RET_KILL_PROCESS — zakończenie całego procesu natychmiast po filtrowaniu wywołania systemowego, które w związku z tym nie jest wykonywane.
— SECCOMP_RET_KILL_THREAD — zakończenie bieżącego wątku natychmiast po filtrowaniu wywołania systemowego, które w związku z tym nie jest wykonywane.
— SECCOMP_RET_KILL — alias dla SECCOMP_RET_KILL_THREAD, pozostawiony dla zachowania zgodności wstecznej.
— SECCOMP_RET_TRAP — wywołanie systemowe jest zabronione, a sygnał SIGSYS (Zły wywołanie systemowe) jest wysyłany do zadania, które go wywołało.
— SECCOMP_RET_ERRNO — wywołanie systemowe nie jest wykonywane, a część zwracanej wartości filtra 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 kodów błędów znajduje się w następnej sekcji.
— SECCOMP_RET_TRACE — używane do powiadamiania debuggera ptrace za pomocą — PTRACE_O_TRACESECCOMP w celu przechwytywania, gdy wywołanie systemowe jest wykonywane, aby móc obserwować i kontrolować ten proces. Jeśli debugger nie jest podłączony, zwracany jest błąd, errno jest ustawiane na -ENOSYS, a wywołanie systemowe nie jest wykonywane.
— SECCOMP_RET_LOG — wywołanie systemowe jest dozwolone i zarejestrowane w dzienniku.
— SECCOMP_RET_ALLOW — wywołanie systemowe jest po prostu dozwolone.
ptrace to wywołanie systemowe do realizacji mechanizmów śledzenia w procesie zwanym tracee, z możliwością obserwowania i kontrolowania wykonania procesu. Program śledzący może skutecznie wpływać na wykonanie i zmieniać rejestry pamięci tracee. W kontekście Seccomp ptrace jest używane, gdy kod stanu SECCOMP_RET_TRACE jest uruchamiany, w związku z czym debugger może zapobiec wykonaniu wywołania systemowego i zastosować własną logikę.
Błędy Seccomp
Od czasu do czasu, pracując z Seccomp, napotkasz różne błędy, które są identyfikowane przez zwracane wartości 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 — strona wywołująca nie ma pozwolenia na dokonanie wywołania systemowego. Zwykle dzieje się tak, ponieważ nie ma uprawnień CAP_SYS_ADMIN lub nie jest ustawiona opcja no_new_privs przy pomocy prctl (o tym porozmawiamy później);
— EFAULT — przekazane argumenty (args w strukturze seccomp_data) nie mają ważnego adresu;
— EINVAL — tutaj mogą być cztery powody:
-żądana operacja jest nieznana lub nieobsługiwana przez jądro w aktualnej konfiguracji;
-podane flagi są nieważne dla żądanej operacji;
-operacja obejmuje BPF_ABS, ale występują problemy z podanym przesunięciem, które może przekraczać rozmiar struktury seccomp_data;
-liczba instrukcji przesłanych do filtru przekracza maksymalną;
— ENOMEM — niewystarczająca ilość pamięci do wykonania programu;
— EOPNOTSUPP — operacja wskazała, że dla SECCOMP_GET_ACTION_AVAIL działanie było dostępne, jednak jądro nie wspiera zwracania w argumentach;
— ESRCH — wystąpił problem podczas synchronizacji innego wątku;
— ENOSYS — brak śledzenia przypisanego do działania SECCOMP_RET_TRACE.
prctl to wywołanie systemowe, które pozwala programowi w przestrzeni użytkownika zarządzać (ustawiać i uzyskiwać) konkretnymi aspektami procesu, takimi jak numer porządkowy bajtów, nazwy wątków, tryb zabezpieczonych obliczeń (Seccomp), uprawnienia, zdarzenia Perf itp.
Seccomp może wydawać się technologią piaskownicy, ale nie jest. Seccomp to narzędzie, które pozwala użytkownikom rozwijać mechanizm piaskownicy. Teraz przyjrzyjmy się, jak tworzone są programy interakcji użytkownika z wykorzystaniem filtra wywoływanego bezpośrednio przez wywołanie systemowe Seccomp.
Przykład filtru BPF Seccomp
Tutaj pokażemy, jak połączyć dwa wcześniej omówione działania, to znaczy:
— napiszemy program Seccomp BPF, który będzie stosowany jako filtr z różnymi kodami zwrotu w zależności od podejmowanych decyzji;
— załadujemy filtr, używając prctl.
Na początek potrzebne są nagłówki z standardowej biblioteki oraz jądra Linux:
#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>Zanim spróbujemy przeprowadzić ten przykład, musimy upewnić się, że jądro zostało skompilowane z CONFIG_SECCOMP i CONFIG_SECCOMP_FILTER ustawionymi na y. Na maszynie roboczej można to sprawdzić w ten sposób:
cat /proc/config.gz | zcat | grep -i CONFIG_SECCOMP
Reszta kodu to funkcja install_filter, składająca się z dwóch części. Pierwsza część zawiera naszą listę instrukcji filtracji 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 są ustawiane za pomocą 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 akumuluje za pomocą BPF_LD w postaci słowa BPF_W, dane pakietowe znajdują się w stałej pozycji BPF_ABS.
— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) — sprawdza, używając BPF_JEQ, czy wartość architektury w stałej akumulatora BPF_K jest równa arch. Jeśli tak, przechodzi ze skokiem 0 do następnej instrukcji, w przeciwnym razie, aby zgłosić błąd, skacze ze skokiem 3 (w tym przypadku), ponieważ arch nie pasuje.
— BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) — ładuje i akumuluje za pomocą BPF_LD w postaci słowa BPF_W, które jest numerem wywołania systemowego znajdującym się w stałej pozycji 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 zabrania wywołania systemowego, w przeciwnym razie zezwala na wywołanie systemowe za pomocą SECCOMP_RET_ALLOW.
— BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)) — kończy program z BPF_RET i w wyniku zgłasza błąd SECCOMP_RET_ERRNO z numerem z zmiennej err.
— BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) — kończy program z BPF_RET i zezwala na wykonanie wywołania systemowego za pomocą SECCOMP_RET_ALLOW.
SECCOMP — TO CBPF
Możesz się zastanawiać, dlaczego zamiast skompilowanego obiektu ELF lub programu w C, skompilowanego z JIT, używana jest lista instrukcji.Są ku temu dwa powody.
• Po pierwsze, Seccomp stosuje cBPF (klasyczny BPF), a nie eBPF, co oznacza: nie ma rejestrów, a jedynie akumulator do przechowywania ostatniego wyniku obliczeń, jak można zobaczyć w przykładzie.
Po drugie, Seccomp przyjmuje wskaźnik do tablicy instrukcji BPF bezpośrednio i nic więcej. Makra, które użyliśmy, po prostu pomagają określić te instrukcje w formie wygodnej dla programistów.
Jeśli potrzebujesz dodatkowej pomocy, aby zrozumieć tę budowę, 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ącą kod i obliczoną długość filtra. Ta struktura danych jest niezbędna jako argument do ogłoszenia działania procesu w przyszłości:
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};Pozostała tylko jedna rzecz do zrobienia w funkcji install_filter — załadować samą program! W tym celu używamy prctl, ustawiając PR_SET_SECCOMP jako opcję, aby wejść w tryb zabezpieczonych obliczeń. Następnie wskaźnik na filter ustalamy na SECCOMP_MODE_FILTER, który znajduje się 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 skorzystać z naszej funkcji install_filter, ale przed tym musimy zaangażować prctl, aby ustawić PR_SET_NO_NEW_PRIVS dla bieżącego wykonania, aby uniknąć sytuacji, w której procesy podrzędne uzyskują szersze uprawnienia niż macierzyste. Przy tym możemy wykonywać następujące wywołania prctl w funkcji install_filter, nie mając praw root.
Teraz możemy wywołać funkcję install_filter. Zablokujemy wszystkie wywołania systemowe write dotyczące architektury X86-64 i po prostu damy zezwolenie, blokując wszelkie próby. Po ustawieniu 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. Do kompilacji naszego programu możemy użyć zarówno clang, jak i gcc, w każdym przypadku będzie to po prostu kompilacja pliku main.c bez specjalnych opcji:
clang main.c -o filter-writeJak wspomniano, zablokowaliśmy wszystkie zapisy w programie. Aby to sprawdzić, potrzebny jest program, który coś wyświetla — ls wydaje się dobrym kandydatem. Oto jak zachowuje się zwykle:
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
Świetnie! Oto jak wygląda użycie naszego programu powłokowego: po prostu przekazujemy program, który chcemy przetestować, jako pierwszy argument:
./filter-write "ls -la"Po wykonaniu ten program zwraca całkowicie pusty wynik. Możemy jednak zastosować strace, aby zobaczyć, co się dzieje:
strace -f ./filter-write "ls -la"Wynik działania jest mocno skrócony, ale jego odpowiednia część pokazuje, że zapisy są blokowane z błędem EPERM - tym samym, który skonfigurowaliśmy. Oznacza to, że program nic nie wyświetla, ponieważ nie może uzyskać dostępu do wywołania systemowego write:
[pid 25099] write(2, "ls: ", 4) = -1 EPERM (Operacja nie jest dozwolona)
[pid 25099] write(2, "błąd zapisu", 11) = -1 EPERM (Operacja nie jest dozwolona)
[pid 25099] write(2, "n", 1) = -1 EPERM (Operacja nie jest dozwolona)Teraz rozumiesz, jak działa Seccomp BPF i masz dobry obraz tego, co można z nim zrobić. Ale czy nie chciałbyś osiągnąć tego samego za pomocą eBPF zamiast cBPF, aby wykorzystać jego pełną moc?
Myśląc o programach eBPF, większość ludzi sądzi, że po prostu je piszą i ładują z uprawnieniami administratora. Choć to stwierdzenie jest w zasadzie prawdziwe, jądro wdraża zestaw mechanizmów do ochrony obiektów eBPF na różnych poziomach. Mechanizmy te nazywają się pułapkami BPF LSM.
Pułapki BPF LSM
Aby zapewnić niezależną od architektury kontrolę zdarzeń systemowych, LSM wdraża koncepcję pułapek. Technicznie wywołanie pułapki przypomina wywołanie systemowe, lecz jest niezależne od systemu i zintegrowane z infrastrukturą. LSM wprowadza nową koncepcję, w której poziom abstrakcji może pomóc uniknąć problemów związanych z wywołaniami systemowymi na różnych architekturach.
W momencie pisania książki jądro miało siedem pułapek związanych z programami BPF, a SELinux był jedynym wbudowanym LSM, które je wdrażało.
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żda z nich będzie wywoływana na różnych etapach wykonania:
— security_bpf — przeprowadza wstępną kontrolę wykonanych systemowych wywołań 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 jest zainicjalizowane;
— security_bpf_map_free — sprawdza, czy odbywa się czyszczenie pola bezpieczeństwa wewnątrz map BPF;
— security_bpf_prog_alloc — sprawdza, czy pole bezpieczeństwa wewnątrz programów BPF jest zainicjalizowane;
— security_bpf_prog_free — sprawdza, czy pole bezpieczeństwa wewnątrz programów BPF jest czyszczone.
Teraz, widząc to wszystko, rozumiemy: idea przechwytów LSM BPF polega na tym, że mogą one zapewnić ochronę każdego obiektu eBPF, gwarantując, że tylko te, które mają odpowiednie uprawnienia, mogą wykonywać operacje na mapach i programach.
Podsumowanie
Bezpieczeństwo to nie coś, co można wdrożyć w uniwersalny sposób dla wszystkiego, co chcesz chronić. Ważne jest, aby mieć możliwość zabezpieczania systemów na różnych poziomach i różnymi metodami. Chcesz wierzyć, chcesz nie, najlepszym sposobem na zabezpieczenie systemu jest organizowanie różnych poziomów ochrony z różnych pozycji, aby osłabienie bezpieczeństwa jednego poziomu nie pozwoliło na dostęp do całego systemu. Programiści jądra wykonali dużą pracę, dostarczając nam zestaw różnych warstw i punktów interakcji. Mamy nadzieję, że daliśmy ci dobre pojęcie o tym, czym są warstwy i jak używać programów BPF do pracy z nimi.
O autorach
David Calavera jest dyrektorem technicznym w Netlify. Pracował w wsparciu technicznym Docker i uczestniczył w tworzeniu narzędzi Runc, Go i BCC, a także innych projektów z otwartym kodem źródłowym. Znany jest ze swojej pracy nad projektami Docker i rozwoju ekosystemu wtyczek Docker. David bardzo lubi diagramy flame graph i zawsze dąży do optymalizacji wydajności.
Lorenzo Fontana Pracuje w zespole programistów oprogramowania o otwartym kodzie źródłowym w Sysdig, gdzie głównie zajmuje się Falco — projektem Cloud Native Computing Foundation, który zapewnia bezpieczeństwo środowiska uruchomieniowego kontenerów i wykrywanie anomalii poprzez moduł jądra i eBPF. Pasjonuje się systemami rozproszonymi, sieciami definiowanymi programowo, jądrem Linuxa oraz analizą wydajności.
» Więcej szczegółów o książce można znaleźć na
»
»
Dla użytkowników Habr zniżka 25% przy użyciu kuponu — Linuxa
Po dokonaniu płatności za wersję papierową książki, na e-mail zostanie wysłana elektroniczna wersja książki.
Źródło: habr.com
