Hallo, Habr-leden! De virtuele BPF-machine is een van de belangrijkste componenten van de Linux-kernel. Een goede toepassing ervan stelt systeemingenieurs in staat om storingen te ontdekken en zelfs de meest complexe problemen op te lossen. Je leert programma's te maken die het gedrag van de kernel volgen en wijzigen, je kunt veilig code implementeren voor het monitoren van gebeurtenissen in de kernel en nog veel meer. David Calavera en Lorenzo Fontana helpen je de mogelijkheden van BPF te ontdekken. Breid je kennis uit over prestatie-optimalisatie, netwerken en beveiliging. — Gebruik BPF voor het volgen en wijzigen van het gedrag van de Linux-kernel. — Implementeer code voor veilige monitoring van gebeurtenissen in de kernel — zonder dat je de kernel opnieuw hoeft te compileren of het systeem opnieuw moet opstarten. — Maak gebruik van handige codevoorbeelden in C, Go of Python. — Beheer de situatie door de levenscyclus van het BPF-programma te begrijpen.
Beveiliging van de Linux-kernel, zijn mogelijkheden en Seccomp
BPF biedt een krachtige manier om de kernel uit te breiden zonder in te boeten op stabiliteit, veiligheid en snelheid. Om deze reden dachten kernelontwikkelaars dat het een goed idee zou zijn om de veelzijdigheid ervan te gebruiken voor het verbeteren van procesisolatie in Seccomp door middel van Seccomp-filters die worden ondersteund door BPF-programma's, ook wel Seccomp BPF genoemd. In dit hoofdstuk bespreken we wat Seccomp is en hoe het wordt toegepast. Vervolgens leer je hoe je Seccomp-filters kunt schrijven met behulp van BPF-programma's. Daarna bekijken we de ingebouwde BPF-filters die in de kernel aanwezig zijn voor Linux-beveiligingsmodules.
Linux-beveiligingsmodules (LSM) zijn een platform dat een reeks functies biedt die kunnen worden toegepast voor gestandaardiseerde implementatie van verschillende beveiligingsmodellen. LSM kan direct in de broncode van de kernelboom worden gebruikt, zoals Apparmor, SELinux en Tomoyo.
Laten we beginnen met een bespreking van de mogelijkheden van Linux.
Functionaliteiten
De kern van de mogelijkheden van Linux is dat je een niet-privilege proces toestemming moet geven om een bepaalde taak uit te voeren, zonder hiervoor suid te gebruiken, of anderszins het proces privileged te maken, waardoor de kans op aanvallen vermindert en het proces de mogelijkheid heeft om bepaalde taken uit te voeren. Bijvoorbeeld, als je applicatie een privilege poort moet openen, bijvoorbeeld 80, in plaats van het proces als root uit te voeren, kun je het gewoon de mogelijkheden CAP_NET_BIND_SERVICE geven.
Laten we een Go-programma met de naam main.go bekijken:
package main
import (
"net/http"
"log"
)
func main() {
log.Fatalf("%v", http.ListenAndServe(":80", nil))
}Dit programma draait een HTTP-server op poort 80 (dit is een privilege poort). Normaal gesproken draaien we het direct na compilatie:
$ go build -o capabilities main.go
$ ./capabilitiesEchter, aangezien we geen root-rechten geven, zal deze code een foutmelding geven bij het verbinden met de poort:
2019/04/25 23:17:06 listen tcp :80: bind: permission denied
exit status 1capsh (shell management tool) is een hulpmiddel dat een shell runt met een bepaalde set mogelijkheden.
In dit geval, zoals eerder genoemd, in plaats van volledige root-rechten te geven, kunnen we het binden van privilegepoorten toestaan door de mogelijkheid cap_net_bind_service toe te kennen naast alle andere die al in het programma aanwezig zijn. Hiervoor kunnen we ons programma in capsh omsluiten:
# 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"Laten we deze opdracht eens nader bekijken.
- capsh - we gebruiken capsh als shell.
- —caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' - aangezien we de gebruiker willen veranderen (we willen niet als root draaien), geven we cap_net_bind_service en de mogelijkheid om de gebruikersidentiteit echt te veranderen van root naar nobody, namelijk cap_setuid en cap_setgid.
- —keep=1 - we willen de ingestelde mogelijkheden behouden wanneer we overschakelen van het root-account.
- —user='nobody' - de eindgebruiker die het programma uitvoert, zal nobody zijn.
- —addamb=cap_net_bind_service - we stellen de opruiming van verbonden mogelijkheden in na de wisseling vanuit de root-modus.
- — -c "./capabilities" - we draaien gewoon het programma.
Verbonden mogelijkheden zijn een speciaal type mogelijkheden die worden overgedragen aan dochterprogramma's wanneer het huidige programma deze uitvoert met execve(). Alleen mogelijkheden die als verbonden zijn toegestaan, of met andere woorden, als omgevingsmogelijkheden kunnen worden geërfd.
Waarschijnlijk vraag je je af wat +eip betekent na het opgeven van een mogelijkheid in de optie —caps. Deze vlaggen worden gebruikt om te bepalen wat de mogelijkheid is:
-moet geactiveerd zijn (p);
-beschikbaar is voor gebruik (e);
-geërfd kan worden door dochterprocessen (i).
Omdat we cap_net_bind_service willen gebruiken, moeten we dit doen met de e-vlag. Start vervolgens een shell in het commando. Uiteindelijk zal het binaire bestand capabilities starten, en we moeten het markeren met de i-vlag. Tot slot willen we dat de mogelijkheid geactiveerd is (dat hebben we gedaan zonder de UID te wijzigen) met p. Dit ziet eruit als cap_net_bind_service+eip.
Je kunt het resultaat controleren met ss. We verkorten de uitvoer een beetje om het op de pagina te laten passen, maar het zal de gerelateerde poort en gebruikers-ID, verschillend van 0, tonen, in dit geval 65 534:
# ss -tulpn -e -H | cut -d' ' -f17-
128 *:80 *:*
users:(("capabilities",pid=30040,fd=3)) uid:65534 ino:11311579 sk:2c v6only:0In dit voorbeeld hebben we capsh gebruikt, maar je kunt een shell schrijven met libcap. Voor meer informatie kun je man 3 libcap raadplegen.
Wanneer ontwikkelaars programma's schrijven, weten ze vaak niet van tevoren welke mogelijkheden het programma tijdens runtime nodig heeft; bovendien kunnen deze mogelijkheden veranderen in nieuwere versies.
Om de mogelijkheden van ons programma beter te begrijpen, kunnen we het BCC-gereedschap capable gebruiken, dat kprobe instelt voor de kernelfunctie 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 1We kunnen hetzelfde bereiken door bpftrace te gebruiken met een enkele regel kprobe in de kernelfunctie cap_capable:
bpftrace -e
'kprobe:cap_capable {
time("%H:%M:%S ");
printf("%-6d %-6d %-16s %-4d %dn", uid, pid, comm, arg2, arg3);
}'
| grep -i capabilitiesDit zal iets vergelijkbaars opleveren als onze mogelijkheden geactiveerd zijn na de 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 1De vijfde kolom toont de mogelijkheden die het proces nodig heeft, en omdat deze uitvoer ook niet-audit gebeurtenissen omvat, zien we alle niet-audit controles en uiteindelijk de vereiste mogelijkheid met de auditvlag (de laatste in de uitvoer) ingesteld op 1. De mogelijkheid die ons interesseert, is CAP_NET_BIND_SERVICE, en deze wordt als constante gedefinieerd in de kernelbroncode in het bestand include/uapi/linux/ability.h met identificatienummer 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">Mogelijkheden worden vaak gebruikt tijdens de uitvoering van containers, zoals runC of Docker, zodat ze in ongeprivilegieerde modus kunnen werken, maar alleen die mogelijkheden hebben die nodig zijn om de meeste toepassingen uit te voeren. Wanneer een toepassing bepaalde mogelijkheden vereist, kunnen deze in Docker worden aangeboden met —cap-add:
docker run -it --rm --cap-add=NET_ADMIN ubuntu ip link add dummy0 type dummyDeze opdracht geeft de container de mogelijkheid CAP_NET_ADMIN, waardoor deze een netwerkverbinding kan instellen om het dummy0-interface toe te voegen.
In de volgende sectie wordt het gebruik van dergelijke mogelijkheden, zoals filtering, getoond, maar met een andere methode die ons in staat stelt om onze eigen filters programmatisch te implementeren.
Seccomp
Seccomp betekent Secure Computing, dit is een beveiligingsniveau dat in de Linux-kernel is geïmplementeerd en dat ontwikkelaars toestaat om bepaalde systeemoproepen te filteren. Hoewel Seccomp vergelijkbaar is met Linux-mogelijkheden, maakt de capaciteit om bepaalde systeemoproepen te beheren het veel flexibeler in vergelijking met hen.
Seccomp en Linux-mogelijkheden sluiten elkaar niet uit; ze worden vaak samen gebruikt om voordeel te halen uit beide benaderingen. Bijvoorbeeld, je wilt misschien een proces de mogelijkheid CAP_NET_ADMIN geven, maar het niet toestaan om verbindingen via een socket te accepteren, door de systeemoproepen accept en accept4 te blokkeren.
De filtermethode van Seccomp is gebaseerd op BPF-filters die werken in SECCOMP_MODE_FILTER, en het filteren van systeemoproepen gebeurt op dezelfde manier als voor pakketten.
Seccomp-filters worden geladen met behulp van prctl via de operatie PR_SET_SECCOMP. Deze filters zijn in de vorm van een BPF-programma dat wordt uitgevoerd voor elk Seccomp-pakket dat wordt gepresenteerd met behulp van de seccomp_data-structuur. Deze structuur bevat de referentie-architectuur, een aanwijzer naar de instructies van de CPU tijdens de systeemoproep en maximaal zes argumenten van de systeemoproep, weergegeven als uint64.
Zo ziet de seccomp_data-structuur eruit vanuit de broncode van de kernel in het bestand linux/seccomp.h:
struct seccomp_data {
int nr;
__u32 arch;
__u64 instruction_pointer;
__u64 args[6];
};Uit deze structuur blijkt dat we kunnen filteren op systeemoproep, zijn argumenten of hun combinatie.
Na ontvangst van elk pakket moet de Seccomp-filter een verwerking uitvoeren om een definitieve beslissing te nemen en de kernel te informeren wat te doen. De definitieve beslissing wordt uitgedrukt in een van de geretourneerde waarden (statuscodes).
— SECCOMP_RET_KILL_PROCESS — beëindigen van het gehele proces onmiddellijk na de filtering van de systeemaanroep die hierdoor niet wordt uitgevoerd.
— SECCOMP_RET_KILL_THREAD — beëindigen van de huidige thread onmiddellijk na de filtering van de systeemaanroep die hierdoor niet wordt uitgevoerd.
— SECCOMP_RET_KILL — alias voor SECCOMP_RET_KILL_THREAD, behouden voor achterwaartse compatibiliteit.
— SECCOMP_RET_TRAP — de systeemaanroep is verboden, en de SIGSYS (Bad System Call) signaal wordt verzonden naar de aanroepende taak.
— SECCOMP_RET_ERRNO — de systeemaanroep wordt niet uitgevoerd, en een deel van de geretourneerde waarde van de filter SECCOMP_RET_DATA wordt doorgegeven aan de gebruikersruimte als de waarde errno. Afhankelijk van de reden voor de fout worden verschillende errno-waarden geretourneerd. Een lijst van foutnummers wordt in de volgende sectie gegeven.
— SECCOMP_RET_TRACE — wordt gebruikt om de tracer ptrace te informeren met — PTRACE_O_TRACESECCOMP om op te vangen wanneer de systeemaanroep wordt uitgevoerd, zodat dit proces kan worden gezien en gecontroleerd. Als de tracer niet is verbonden, wordt een fout geretourneerd, errno wordt ingesteld op -ENOSYS, en de systeemaanroep wordt niet uitgevoerd.
— SECCOMP_RET_LOG — de systeemaanroep is toegestaan en geregistreerd in het logboek.
— SECCOMP_RET_ALLOW — de systeemaanroep wordt simpelweg toegestaan.
ptrace is een systeemaanroep voor het implementeren van traceermechanismen in een proces, genaamd tracee, met de mogelijkheid om het uitvoeren van het proces te observeren en te controleren. Het traceerprogramma kan effectief invloed uitoefenen op de uitvoering en de registers van de tracee wijzigen. In de context van Seccomp wordt ptrace gebruikt wanneer de SECCOMP_RET_TRACE statuscode wordt geactiveerd, waardoor de tracer de uitvoering van de systeemaanroep kan voorkomen en zijn eigen logica kan implementeren.
Seccomp-fouten
Van tijd tot tijd, bij het werken met Seccomp, kunt u verschillende fouten tegenkomen die worden geïdentificeerd door de geretourneerde waarde van het type SECCOMP_RET_ERRNO. Om een fout te rapporteren, zal de systeemaanroep seccomp -1 retourneren in plaats van 0.
De volgende fouten zijn mogelijk:
— EACCESS — De aanroepende partij is niet gemachtigd om de systeemaanroep uit te voeren. Dit gebeurt meestal omdat het geen CAP_SYS_ADMIN-rechten heeft of no_new_privs niet ingesteld is met behulp van prctl (hierover later meer);
— EFAULT — De overgegeven argumenten (args in de seccomp_data-structuur) hebben geen geldig adres;
— EINVAL — hier kunnen vier oorzaken voor zijn:
- de gevraagde operatie is onbekend of wordt niet ondersteund door de kernel in de huidige configuratie;
- de opgegeven vlaggen zijn ongeldig voor de gevraagde operatie;
- de operatie omvat BPF_ABS, maar er zijn problemen met de opgegeven offset, die de grootte van de seccomp_data-structuur kan overschrijden;
- het aantal instructies dat naar de filter wordt verzonden, overschrijdt het maximum;
— ENOMEM — Er is onvoldoende geheugen beschikbaar om het programma uit te voeren;
— EOPNOTSUPP — De operatie gaf aan dat met SECCOMP_GET_ACTION_AVAIL de actie beschikbaar was, maar de kernel ondersteunt de terugkeer in de argumenten niet;
— ESRCH — Er is een probleem opgetreden bij het synchroniseren van een andere thread;
— ENOSYS — Er is geen tracer gekoppeld aan de SECCOMP_RET_TRACE-actie.
prctl is een systeemaanroep waarmee een gebruikersruimteprogramma specifieke aspecten van het proces kan beheren (instellen en verkrijgen), zoals de byte-volgorde, thread-namen, de modus voor beveiligde berekeningen (Seccomp), rechten, Perf-gebeurtenissen, enz.
Seccomp lijkt misschien een sandbox-technologie, maar dat is het niet. Seccomp is een hulpmiddel dat gebruikers in staat stelt om een sandbox-mechanisme te ontwikkelen. Laten we nu bekijken hoe gebruikersinteracties worden ontwikkeld met behulp van een filter dat wordt aangeroepen via de systeemaanroep Seccomp.
Voorbeeld van een Seccomp BPF-filter
Hier laten we zien hoe we de twee eerder besproken acties kunnen combineren, namelijk:
— we zullen een Seccomp BPF-programma schrijven dat wordt toegepast als filter met verschillende retourcodes, afhankelijk van de genomen beslissingen;
— we zullen het filter laden met behulp van prctl.
Allereerst zijn de headers uit de standaardbibliotheek en de Linux-kernel nodig:
#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>Voordat we proberen dit voorbeeld uit te voeren, moeten we ervoor zorgen dat de kernel is gecompileerd met CONFIG_SECCOMP en CONFIG_SECCOMP_FILTER ingesteld op y. Op een werkmachine kan dit als volgt worden gecontroleerd:
cat /proc/config.gz | zcat | grep -i CONFIG_SECCOMP
De rest van de code is een functie install_filter die uit twee delen bestaat. Het eerste deel bevat onze lijst met BPF-filterinstructies:
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),
}; De instructies worden ingesteld met behulp van de BPF_STMT- en BPF_JUMP-macros die zijn gedefinieerd in het bestand linux/filter.h.
Laten we de instructies doornemen.
— BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, arch))) — het systeem laadt en accumuleert met BPF_LD in de vorm van een woord BPF_W, terwijl de pakketgegevens zich bevinden met een vaste offset BPF_ABS.
— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) — controleert met behulp van BPF_JEQ of de waarde van de architectuur in de constante accumulator BPF_K gelijk is aan arch. Als dat zo is, springt het met offset 0 naar de volgende instructie, anders springt het met offset 3 (in dit geval) om een fout te genereren omdat arch niet overeenkomt.
— BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) — laadt en accumuleert met BPF_LD in de vorm van een woord BPF_W, wat het systeemoproepnummer is dat zich bevindt met een vaste offset BPF_ABS.
— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) — vergelijkt het systeemoproepnummer met de waarde van de variabele nr. Als ze gelijk zijn, gaat het naar de volgende instructie en blokkeert de systeemoproep, anders staat het de systeemoproep toe met SECCOMP_RET_ALLOW.
— BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)) — beëindigt het programma met BPF_RET en retourneert daarmee een fout SECCOMP_RET_ERRNO met het nummer uit de variabele err.
— BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) — beëindigt het programma met BPF_RET en staat de uitvoering van de systeemoproep toe met SECCOMP_RET_ALLOW.
SECCOMP IS CBPF
Misschien vraag je je af waarom er in plaats van een gecompileerd ELF-object of een C-programma dat met JIT is gecompileerd, een lijst met instructies wordt gebruikt.Daar zijn twee redenen voor.
• Ten eerste past Seccomp cBPF (klassieke BPF) toe en niet eBPF, wat betekent: het heeft geen registers, maar alleen een accumulator voor het opslaan van het laatste resultaat van berekeningen, zoals je kunt zien in het voorbeeld.
• Ten eerste neemt Seccomp een pointer naar een array met BPF-instructies direct en niets meer. De macro's die we hebben gebruikt, helpen alleen om deze instructies op een programmervriendelijke manier op te geven.
Als je meer hulp nodig hebt om deze opbouw te begrijpen, bekijk dan de pseudocode die hetzelfde doet:
if (arch != AUDIT_ARCH_X86_64) {
return SECCOMP_RET_ALLOW;
}
if (nr == __NR_write) {
return SECCOMP_RET_ERRNO;
}
return SECCOMP_RET_ALLOW;Na het definiëren van de filtercode in de structuur socket_filter, moet je de sock_fprog definiëren, die de code en de berekende lengte van de filter bevat. Deze datastructuur is nodig als argument voor de verdere functieverklaring:
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};Er is nog maar één ding te doen in de functie install_filter: de eigenlijke programma laden! Hiervoor gebruiken we prctl en nemen PR_SET_SECCOMP als opties om in de modus van veilige berekeningen te gaan. Dan geven we aan dat de filter geladen moet worden met behulp van SECCOMP_MODE_FILTER, dat in de variabele prog van het type sock_fprog zit:
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
perror("prctl(PR_SET_SECCOMP)");
return 1;
}
return 0;
}Ten slotte kunnen we onze functie install_filter aanroepen, maar hiervoor moeten we prctl gebruiken om PR_SET_NO_NEW_PRIVS voor de huidige uitvoering in te stellen en zo te voorkomen dat kindprocessen bredere privileges krijgen dan de ouderprocessen. Hierbij kunnen we de volgende prctl-aanroepen in de functie install_filter doen zonder root-rechten.
Nu kunnen we de functie install_filter aanroepen. We blokkeren alle systeemaanroepen write die betrekking hebben op de X86-64-architectuur en geven simpelweg toestemming om alle pogingen te blokkeren. Na het instellen van de filter gaan we verder met de uitvoering met de eerste parameter:
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]);
}Laten we beginnen. Voor het compileren van ons programma kunnen we zowel clang als gcc gebruiken, in ieder geval is het gewoon het compileren van het bestand main.c zonder speciale opties:
clang main.c -o filter-writeZoals opgemerkt, hebben we alle schrijfbewerking in het programma geblokkeerd. Om dit te controleren, hebben we een programma nodig dat iets uitvoert, en ls lijkt een goede kandidaat. Hier is hoe het zich gewoonlijk gedraagt:
ls -la
totaal 36
drwxr-xr-x 2 fntlnz gebruikers 4096 Apr 28 21:09 .
drwxr-xr-x 4 fntlnz gebruikers 4096 Apr 26 13:01 ..
-rwxr-xr-x 1 fntlnz gebruikers 16800 Apr 28 21:09 filter-write
-rw-r--r-- 1 fntlnz gebruikers 19 Apr 28 21:09 .gitignore
-rw-r--r-- 1 fntlnz gebruikers 1282 Apr 28 21:08 main.c
Perfect! Dit is hoe het gebruik van ons shellprogramma eruit ziet: we geven eenvoudig het programma dat we willen testen door als eerste argument:
./filter-write "ls -la"Na uitvoering geeft dit programma een volkomen lege output. We kunnen echter strace toepassen om te zien wat er gebeurt:
strace -f ./filter-write "ls -la"De uitvoer is sterk verkort, maar het relevante gedeelte laat zien dat schrijfopdrachten worden geblokkeerd met de fout EPERM — precies diegene die we hebben ingesteld. Dit betekent dat het programma geen uitvoer geeft omdat het geen toegang kan krijgen tot de systeemaanroep write:
[pid 25099] write(2, "ls: ", 4) = -1 EPERM (Bewerking niet toegestaan)
[pid 25099] write(2, "schrijf fout", 11) = -1 EPERM (Bewerking niet toegestaan)
[pid 25099] write(2, "n", 1) = -1 EPERM (Bewerking niet toegestaan)Nu begrijp je hoe Seccomp BPF werkt en heb je een goed idee van wat je ermee kunt doen. Maar zou het niet geweldig zijn om hetzelfde te bereiken met eBPF in plaats van cBPF, zodat je al zijn kracht kunt benutten?
Bij het nadenken over eBPF-programma's denken de meeste mensen dat ze ze gewoon schrijven en met beheerdersrechten uploaden. Hoewel deze uitspraak in het algemeen waar is, implementeert de kernel een set mechanismen ter bescherming van eBPF-objecten op verschillende niveaus. Deze mechanismen worden BPF LSM-traps genoemd.
BPF LSM-traps
Om architectononafhankelijke controle over systeemgebeurtenissen te waarborgen, implementeert LSM het concept van traps. Technisch gezien lijkt een trapoproep op een systeemaanroep, maar is onafhankelijk van het systeem en geïntegreerd met de infrastructuur. LSM biedt een nieuw concept waarbij het abstractieniveau helpt om problemen te vermijden die optreden bij het werken met systeemaanroepen op verschillende architecturen.
Op het moment van schrijven had de kernel zeven traps gerelateerde aan BPF-programma's, en SELinux was de enige ingebouwde LSM die ze implementeerde.
De broncode van de traps is te vinden in de kernelstructuur in het bestand 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);Elk van hen zal worden aangeroepen op verschillende stadia van de uitvoering:
— security_bpf — voert een initiële controle uit op de uitgevoerde systeemaanroepen van BPF;
— security_bpf_map — controleert wanneer de kernel een bestandsdescriptor voor de kaart retourneert;
— security_bpf_prog — controleert wanneer de kernel een bestandsdescriptor voor het eBPF-programma retourneert;
— security_bpf_map_alloc — controleert of het beveiligingsveld binnen BPF-kaarten is geïnitialiseerd;
— security_bpf_map_free — controleert of de opschoning van het beveiligingsveld binnen BPF-kaarten plaatsvindt;
— security_bpf_prog_alloc — controleert of het beveiligingsveld binnen BPF-programma's wordt geïnitialiseerd;
— security_bpf_prog_free — controleert of het beveiligingsveld binnen BPF-programma's wordt opgeschoond.
Nu we dit alles zien, begrijpen we: het idee van LSM BPF-interceptoren is dat ze de bescherming van elk eBPF-object kunnen bieden, waarbij ervoor wordt gezorgd dat alleen degenen met de juiste privileges operaties op kaarten en programma's kunnen uitvoeren.
Samenvatting
Beveiliging is niet iets wat je op een uniforme manier kunt implementeren voor alles wat je wilt beschermen. Het is belangrijk om systemen op verschillende niveaus en op verschillende manieren te kunnen beveiligen. Of je het nu gelooft of niet, de beste manier om een systeem te beveiligen is door verschillende niveaus van bescherming vanuit verschillende posities te organiseren, zodat een afname van de beveiliging op één niveau niet de toegang tot het hele systeem mogelijk maakt. De kernelontwikkelaars hebben geweldig werk verricht door ons een set verschillende lagen en interactiepunten te bieden. We hopen dat we je een goed idee hebben gegeven van wat lagen zijn en hoe je BPF-programma's kunt gebruiken om ermee te werken.
Over de auteurs
David Calavera is technisch directeur bij Netlify. Hij heeft gewerkt in de ondersteuning van Docker en heeft bijgedragen aan de ontwikkeling van tools zoals Runc, Go en BCC, evenals andere open-source projecten. Hij is bekend om zijn werk aan Docker-projecten en de ontwikkeling van het Docker-plugin-ecosysteem. David houdt erg van flame graphs en streeft altijd naar prestatieoptimalisatie.
Lorenzo Fontana werkt in een team van softwareontwikkelaars met open source bij Sysdig, waar hij zich voornamelijk bezighoudt met Falco - een project van de Cloud Native Computing Foundation dat de veiligheid van de containeruitvoeringsomgeving garandeert en anomaliedetectie mogelijk maakt via de kernelmodule en eBPF. Hij is gepassioneerd door gedistribueerde systemen, softwaregedefinieerde netwerken, de Linux-kernel en prestatieanalyse.
» Meer informatie over het boek is te vinden op
»
»
Voor Habr-bewoners is er 25% korting met de coupon — Linux
Na betaling van de papieren versie van het boek ontvangt u de elektronische versie per e-mail.
Bron: habr.com
