Ciao, Habrozhiteli! La macchina virtuale BPF è uno dei componenti più importanti del kernel Linux. Il suo utilizzo appropriato permetterà agli ingegneri di sistema di individuare guasti e risolvere anche i problemi più complessi. Imparerai a creare programmi che monitorano e modificano il comportamento del kernel, potrai implementare in modo sicuro codice per la registrazione degli eventi nel kernel e molto altro ancora. David Calavera e Lorenzo Fontana ti aiuteranno a scoprire le potenzialità del BPF. Amplia le tue conoscenze sull'ottimizzazione delle prestazioni, sulle reti e sulla sicurezza. - Usa BPF per monitorare e modificare il comportamento del kernel Linux. - Implementa codice per il monitoraggio sicuro degli eventi nel kernel, senza la necessità di ricompilare il kernel o riavviare il sistema. - Usa esempi di codice pratici in C, Go o Python. - Gestisci la situazione, avendo il controllo del ciclo di vita del programma BPF.
Sicurezza del kernel Linux, le sue capacità e Seccomp
BPF fornisce un modo potente per estendere il kernel senza compromettere stabilità, sicurezza e velocità. Per questo motivo, gli sviluppatori del kernel hanno pensato che sarebbe stato utile utilizzare la sua versatilità per migliorare l'isolamento dei processi in Seccomp tramite l'implementazione di filtri Seccomp supportati da programmi BPF, noti anche come Seccomp BPF. In questo capitolo parleremo di cosa sia Seccomp e come venga applicato. Poi scoprirai come scrivere filtri Seccomp utilizzando programmi BPF. Successivamente, esamineremo le trappole incorporate nel BPF presenti nel kernel per i moduli di sicurezza Linux.
I moduli di sicurezza Linux (LSM) sono una piattaforma che fornisce un insieme di funzionalità che possono essere applicate per implementare in modo standardizzato vari modelli di sicurezza. LSM può essere utilizzato direttamente nell'albero del codice sorgente del kernel, come Apparmor, SELinux e Tomoyo.
Iniziamo a discutere delle capacità di Linux.
Funzionalità
L'essenza delle funzionalità di Linux consiste nel fornire a un processo non privilegiato il permesso di eseguire un compito specifico, senza utilizzare suid per questo scopo, o in altro modo rendere il processo privilegiato, riducendo la possibilità di attacchi e consentendo al processo di svolgere determinate funzioni. Ad esempio, se la tua applicazione ha bisogno di aprire una porta privilegiata, ad esempio 80, invece di eseguire il processo come root, puoi semplicemente fornirle la possibilità CAP_NET_BIND_SERVICE.
Consideriamo un programma Go chiamato main.go:
package main
import (
"net/http"
"log"
)
func main() {
log.Fatalf("%v", http.ListenAndServe(":80", nil))
}Questo programma gestisce un server HTTP sulla porta 80 (che è una porta privilegiata). Di solito lo eseguiamo subito dopo la compilazione:
$ go build -o capabilities main.go
$ ./capabilitiesTuttavia, poiché non stiamo fornendo privilegi root, questo codice restituirà un errore durante il tentativo di bind della porta:
2019/04/25 23:17:06 listen tcp :80: bind: permission denied
exit status 1capsh (strumento di gestione della shell) è uno strumento che avvia una shell con un insieme specifico di capacità.
In questo caso, come già detto, invece di fornire i pieni diritti root, possiamo consentire il binding delle porte privilegiate fornendo la capacità cap_net_bind_service insieme a tutte le altre già presenti nel programma. A tal fine, possiamo racchiudere il nostro programma in 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"Analizziamo un po' questo comando.
- capsh — utilizziamo capsh come shell.
- —caps=‘cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep’ — poiché dobbiamo cambiare utente (non vogliamo eseguirci con i diritti root), indichiamo cap_net_bind_service e la capacità di cambiare effettivamente l'identificativo utente da root a nobody, ovvero cap_setuid e cap_setgid.
- —keep=1 — vogliamo mantenere le capacità impostate quando avviene il cambio dall'account root.
- —user="nobody" — l'utente finale che eseguirà il programma sarà nobody.
- —addamb=cap_net_bind_service — impostiamo la pulizia delle capacità associate dopo il passaggio dalla modalità root.
- — -c "./capabilities" — eseguiamo semplicemente il programma.
Le capacità associate sono un particolare tipo di capacità che vengono ereditate dai processi figli quando il programma attuale le esegue tramite execve(). Possono essere ereditate solo le capacità autorizzate come associate, o, in altre parole, come capacità ambientali.
Probabilmente ti starai chiedendo cosa significa +eip dopo l'indicazione della possibilità nell'opzione —caps. Questi flag vengono utilizzati per determinare se la possibilità:
-deve essere attivata (p);
-è disponibile per l'applicazione (e);
-può essere ereditata dai processi figli (i).
Poiché vogliamo utilizzare cap_net_bind_service, dobbiamo farlo con il flag e. Poi avvieremo una shell nel comando. Di conseguenza verrà eseguito un file binario delle capacità, e abbiamo bisogno di contrassegnarlo con il flag i. Infine, vogliamo che la possibilità sia attivata (lo abbiamo fatto senza cambiare UID) utilizzando p. Questo si presenta come cap_net_bind_service+eip.
Puoi controllare il risultato usando ss. Ridurremo un po' l'output per adattarlo alla pagina, ma mostrerà la porta correlata e l'identificativo utente, diversi da 0, in questo caso 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 questo esempio abbiamo utilizzato capsh, ma puoi scrivere una shell usando libcap. Per ulteriori informazioni, consulta man 3 libcap.
Quando si scrivono programmi, gli sviluppatori spesso non conoscono in anticipo tutte le possibilità necessarie al programma durante l'esecuzione; inoltre, nelle nuove versioni queste possibilità possono cambiare.
Per comprendere meglio le capacità del nostro programma, possiamo utilizzare lo strumento BCC capable, che imposta un kprobe per la funzione del kernel 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 1Possiamo ottenere lo stesso risultato usando bpftrace con un kprobe su una riga nella funzione del kernel cap_capable:
bpftrace -e
'kprobe:cap_capable {
time("%H:%M:%S ");
printf("%-6d %-6d %-16s %-4d %dn", uid, pid, comm, arg2, arg3);
}'
| grep -i capabilitiesQuesto restituirà qualcosa di simile al seguente, se le possibilità del nostro programma sono state attivate dopo il 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 1La quinta colonna rappresenta le capacità di cui ha bisogno il processo e, poiché questi output includono eventi non auditabili, vediamo tutti i controlli non auditabili e, infine, la possibilità richiesta con il flag di audit (l'ultimo nell'output) impostato a 1. La possibilità che ci interessa è CAP_NET_BIND_SERVICE, definita come costante nel codice sorgente del kernel nel file include/uapi/linux/ability.h con identificatore 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">Le capacità vengono spesso attivate durante l'esecuzione di contenitori come runC o Docker, affinché funzionino in modalità non privilegiata, ma consentendo loro solo le capacità necessarie per eseguire la maggior parte delle applicazioni. Quando un'applicazione richiede determinate capacità, in Docker è possibile fornirle utilizzando —cap-add:
docker run -it --rm --cap-add=NET_ADMIN ubuntu ip link add dummy0 type dummyQuesto comando fornirà al contenitore la capacità CAP_NET_ADMIN, consentendogli di configurare il collegamento di rete per aggiungere l'interfaccia dummy0.
Nella sezione successiva verrà mostrato l'uso di tali capacità, come il filtraggio, ma con un metodo diverso che ci consentirà di implementare programmaticamente i nostri filtri.
Seccomp
Seccomp significa Secure Computing, ed è un livello di sicurezza implementato nel kernel di Linux che consente agli sviluppatori di filtrare determinate chiamate di sistema. Sebbene Seccomp sia paragonabile alle capacità di Linux, la sua capacità di gestire determinate chiamate di sistema lo rende molto più flessibile rispetto a esse.
Seccomp e le capacità di Linux non si escludono a vicenda e spesso vengono utilizzati insieme per trarre vantaggio da entrambi gli approcci. Ad esempio, potresti voler fornire a un processo la capacità CAP_NET_ADMIN, ma non consentirgli di accettare connessioni tramite socket, bloccando le chiamate di sistema accept e accept4.
Il metodo di filtraggio di Seccomp si basa su filtri BPF, operando in modalità SECCOMP_MODE_FILTER, e il filtraggio delle chiamate di sistema avviene in modo simile a quello dei pacchetti.
I filtri Seccomp vengono caricati utilizzando prctl tramite l'operazione PR_SET_SECCOMP. Questi filtri hanno la forma di un programma BPF, che viene eseguito per ogni pacchetto Seccomp presentato con la struttura seccomp_data. Questa struttura contiene l'architettura di riferimento, un puntatore alle istruzioni del processore durante la chiamata di sistema e un massimo di sei argomenti della chiamata di sistema, espressi come uint64.
Ecco come appare la struttura seccomp_data dal codice sorgente del kernel nel file linux/seccomp.h:
struct seccomp_data {
int nr;
__u32 arch;
__u64 instruction_pointer;
__u64 args[6];
};Come si può vedere da questa struttura, possiamo filtrare in base alla chiamata di sistema, ai suoi argomenti o a una loro combinazione.
Dopo aver ricevuto ciascun pacchetto, il filtro Seccomp è obbligato a eseguire un'elaborazione per prendere una decisione finale e comunicare al kernel cosa fare dopo. La decisione finale è espressa da uno dei valori restituiti (codici di stato).
— SECCOMP_RET_KILL_PROCESS — termina l'intero processo immediatamente dopo la filtrazione della chiamata di sistema, che perciò non viene eseguita.
— SECCOMP_RET_KILL_THREAD — termina il thread corrente immediatamente dopo la filtrazione della chiamata di sistema, che perciò non viene eseguita.
— SECCOMP_RET_KILL — alias per SECCOMP_RET_KILL_THREAD, rimasto per compatibilità.
— SECCOMP_RET_TRAP — la chiamata di sistema è vietata e viene inviato un segnale SIGSYS (Bad System Call) al processo chiamante.
— SECCOMP_RET_ERRNO — la chiamata di sistema non viene eseguita e parte del valore restituito dal filtro SECCOMP_RET_DATA viene passato nello spazio utente come valore di errno. A seconda della causa dell'errore, vengono restituiti diversi valori di errno. L'elenco dei codici di errore è riportato nella sezione seguente.
— SECCOMP_RET_TRACE — utilizzato per notificare il debugger ptrace con — PTRACE_O_TRACESECCOMP per intercettare quando viene eseguita la chiamata di sistema, per vedere e controllare questo processo. Se il debugger non è connesso, viene restituito un errore, errno è impostato su -ENOSYS e la chiamata di sistema non viene eseguita.
— SECCOMP_RET_LOG — la chiamata di sistema è consentita e registrata nel log.
— SECCOMP_RET_ALLOW — la chiamata di sistema è semplicemente consentita.
ptrace è una chiamata di sistema per implementare meccanismi di tracciamento in un processo chiamato tracee, con la possibilità di osservare e controllare l'esecuzione del processo. Il programma di tracciamento può influenzare efficacemente l'esecuzione e modificare i registri di memoria di tracee. Nel contesto di Seccomp, ptrace è utilizzato quando viene attivato lo stato SECCOMP_RET_TRACE, quindi il debugger può prevenire l'esecuzione della chiamata di sistema e implementare la propria logica.
Errori di Seccomp
Di tanto in tanto, lavorando con Seccomp, si possono incontrare vari errori, identificati dal valore restituito di tipo SECCOMP_RET_ERRNO. Per segnalare un errore, la chiamata di sistema seccomp restituirà -1 invece di 0.
Possono verificarsi i seguenti errori:
— EACCESS — la parte chiamante non è autorizzata a effettuare una chiamata di sistema. Di solito ciò accade perché non ha i privilegi CAP_SYS_ADMIN o perché no_new_privs non è impostato tramite prctl (ne parleremo più avanti);
— EFAULT — gli argomenti forniti (args nella struttura seccomp_data) non hanno un indirizzo valido;
— EINVAL — qui possono esserci quattro motivi:
- l'operazione richiesta è sconosciuta o non supportata dal kernel nella configurazione attuale;
- i flag specificati non sono validi per l'operazione richiesta;
- l'operazione include BPF_ABS, ma ci sono problemi con l'offset specificato, che potrebbe superare la dimensione della struttura seccomp_data;
- il numero di istruzioni fornite al filtro supera il massimo;
— ENOMEM — non c'è abbastanza memoria per eseguire il programma;
— EOPNOTSUPP — l'operazione ha indicato che l'azione SECCOMP_GET_ACTION_AVAIL era disponibile, tuttavia il kernel non supporta il ritorno negli argomenti;
— ESRCH — si è verificato un problema nella sincronizzazione di un altro thread;
— ENOSYS — non c'è tracer attaccato all'azione SECCOMP_RET_TRACE.
prctl è una chiamata di sistema che consente ai programmi in spazio utente di gestire (impostare e ottenere) specifici aspetti del processo, come il numero di sequenza dei byte, i nomi dei thread, la modalità di calcolo sicura (Seccomp), i privilegi, gli eventi di Perf, ecc.
Seccomp potrebbe sembrare una tecnologia di sandbox, ma non è così. Seccomp è un'utilità che consente agli utenti di sviluppare un meccanismo di sandbox. Ora vediamo come vengono creati i programmi di interazione utente usando un filtro chiamato direttamente dalla chiamata di sistema Seccomp.
Esempio di filtro BPF Seccomp
Qui mostreremo come combinare le due azioni precedentemente esaminate, ovvero:
— scriveremo un programma Seccomp BPF che sarà applicato come filtro con diversi codici di ritorno a seconda delle decisioni adottate;
— caricheremo il filtro utilizzando prctl.
Per cominciare, sono necessari gli header dalla libreria standard e dal kernel 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>Prima di tentare di eseguire questo esempio, dobbiamo assicurarci che il kernel sia stato compilato con CONFIG_SECCOMP e CONFIG_SECCOMP_FILTER impostati su y. Su una macchina in funzione, è possibile verificarlo in questo modo:
cat /proc/config.gz | zcat | grep -i CONFIG_SECCOMP
La parte rimanente del codice è una funzione install_filter, composta da due parti. La prima parte contiene la nostra lista di istruzioni di filtraggio 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),
}; Le istruzioni vengono impostate utilizzando i macro BPF_STMT e BPF_JUMP definiti nel file linux/filter.h.
Esaminiamo le istruzioni.
— BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, arch))) — il sistema carica e accumula con BPF_LD in forma di parola BPF_W, i dati del pacchetto sono posizionati con uno spostamento fisso BPF_ABS.
— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) — verifica utilizzando BPF_JEQ se il valore dell'architettura nella costante dell'accumulatore BPF_K è uguale ad arch. Se è così, passa con uno spostamento di 0 alla prossima istruzione, altrimenti, per emettere un errore, salta con uno spostamento di 3 (in questo caso), perché arch non corrisponde.
— BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) — carica e accumula con BPF_LD in forma di parola BPF_W, che è il numero della chiamata di sistema, contenuto in uno spostamento fisso BPF_ABS.
— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) — confronta il numero della chiamata di sistema con il valore della variabile nr. Se sono uguali, passa alla prossima istruzione e nega la chiamata di sistema, altrimenti consente la chiamata di sistema con SECCOMP_RET_ALLOW.
— BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)) — termina il programma con BPF_RET e restituisce un errore SECCOMP_RET_ERRNO con il numero dalla variabile error.
— BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) — termina il programma con BPF_RET e consente l'esecuzione della chiamata di sistema con SECCOMP_RET_ALLOW.
SECCOMP È CBPF
Probabilmente ti starai chiedendo perché venga utilizzata una lista di istruzioni anziché un oggetto ELF compilato o un programma C compilato con JIT.Ci sono due motivi.
• Prima di tutto, Seccomp applica cBPF (classico BPF), non eBPF, il che significa: non ha registri, ma solo un accumulatore per memorizzare l'ultimo risultato dei calcoli, come si può vedere nell'esempio.
• In secondo luogo, Seccomp accetta un puntatore a un array di istruzioni BPF direttamente e nient'altro. I macro che abbiamo usato servono solo a definire queste istruzioni in una forma comoda per gli sviluppatori.
Se hai bisogno di ulteriore aiuto per comprendere questa implementazione, considera il pseudocodice che fa la stessa cosa:
if (arch != AUDIT_ARCH_X86_64) {
return SECCOMP_RET_ALLOW;
}
if (nr == __NR_write) {
return SECCOMP_RET_ERRNO;
}
return SECCOMP_RET_ALLOW;Dopo aver definito il codice del filtro nella struttura socket_filter, è necessario definire sock_fprog, che contiene il codice e la lunghezza calcolata del filtro. Questa struttura dati è necessaria come argomento per dichiarare in seguito il processo di lavoro:
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};Rimane solo un'ultima cosa da fare nella funzione install_filter: caricare il programma stesso! A questo scopo utilizziamo prctl, prendendo PR_SET_SECCOMP come opzione, per entrare nella modalità di calcolo sicuro. Quindi specificheremo al modo di caricare il filtro utilizzando SECCOMP_MODE_FILTER, che è contenuto nella variabile prog del tipo sock_fprog:
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
perror("prctl(PR_SET_SECCOMP)");
return 1;
}
return 0;
}Infine, possiamo utilizzare la nostra funzione install_filter, ma prima dobbiamo usare prctl per impostare PR_SET_NO_NEW_PRIVS per l'esecuzione corrente e così evitare situazioni in cui i processi figlio ottengono privilegi più ampi rispetto ai genitori. A questo punto possiamo effettuare le seguenti chiamate prctl nella funzione install_filter, senza avere diritti di root.
Ora possiamo chiamare la funzione install_filter. Bloccheremo tutte le chiamate di sistema write relative all'architettura X86-64 e semplicemente concederemo un permesso che blocca tutti i tentativi. Dopo aver impostato il filtro, continuiamo l'esecuzione utilizzando il primo argomento:
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]);
}Iniziamo. Per compilare il nostro programma possiamo usare sia clang che gcc, in entrambi i casi si tratta semplicemente di compilare il file main.c senza opzioni speciali:
clang main.c -o filter-writeCome è stato notato, abbiamo bloccato tutte le scritture nel programma. Per verificare ciò, ci serve un programma che produca qualcosa in output, e ls sembra un buon candidato. Ecco come si comporta di solito:
ls -la
totale 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
Ottimo! Ecco come appare l'uso del nostro programma shell: passiamo semplicemente il programma che vogliamo testare come primo argomento:
.\/filter-write "ls -la"Dopo l'esecuzione, questo programma produce un output completamente vuoto. Tuttavia, possiamo utilizzare strace per vedere cosa sta succedendo:
strace -f .\/filter-write "ls -la"Il risultato è notevolmente ridotto, ma la parte corrispondente mostra che le registrazioni vengono bloccate con l'errore EPERM — proprio quello che abbiamo configurato. Questo significa che il programma non produce output perché non riesce ad accedere alla chiamata di sistema write:
[pid 25099] write(2, "ls: ", 4) = -1 EPERM (Operazione non consentita)
[pid 25099] write(2, "errore di scrittura", 17) = -1 EPERM (Operazione non consentita)
[pid 25099] write(2, "n", 1) = -1 EPERM (Operazione non consentita)Ora capisci come funziona Seccomp BPF e hai una buona idea di cosa si può fare con esso. Ma non sarebbe interessante ottenere lo stesso con eBPF invece di cBPF, per sfruttare tutta la sua potenza?
Pensando ai programmi eBPF, la maggior parte delle persone pensa che li scrivano e li carichino con privilegi di amministratore. Anche se questa affermazione è sostanzialmente corretta, il kernel implementa un insieme di meccanismi per proteggere gli oggetti eBPF su vari livelli. Questi meccanismi si chiamano trappole BPF LSM.
Trappole BPF LSM
Per garantire un controllo indipendente dall'architettura sugli eventi di sistema, LSM implementa il concetto di trappole. Tecnicamente, una chiamata di trappola è simile a una chiamata di sistema, ma è indipendente dal sistema e integrata con l'infrastruttura. LSM fornisce un nuovo concetto, in cui il livello di astrazione è in grado di aiutare a evitare problemi che sorgono durante l'interazione con le chiamate di sistema su diverse architetture.
Al momento della scrittura del libro, il kernel aveva sette trappole associate ai programmi BPF, e SELinux era l'unico LSM integrato che le implementava.
Il codice sorgente delle trappole si trova nell'albero del kernel nel file 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);Ognuna di esse sarà invocata in diverse fasi di esecuzione:
— security_bpf — esegue un controllo iniziale delle chiamate di sistema BPF eseguite;
— security_bpf_map — verifica quando il kernel restituisce un descrittore di file per la mappa;
— security_bpf_prog — verifica quando il kernel restituisce un descrittore di file per il programma eBPF;
— security_bpf_map_alloc — verifica se il campo di sicurezza all'interno delle mappe BPF è stato inizializzato;
— security_bpf_map_free — verifica se viene eseguita la pulizia del campo di sicurezza all'interno delle mappe BPF;
— security_bpf_prog_alloc — verifica se il campo di sicurezza all'interno dei programmi BPF è stato inizializzato;
— security_bpf_prog_free — verifica se il campo di sicurezza all'interno dei programmi BPF è stato pulito.
Ora, vedendo tutto ciò, comprendiamo: l'idea dei ganci LSM BPF è che possono garantire la protezione di ogni oggetto eBPF, assicurando che solo coloro che hanno le appropriate autorizzazioni possano eseguire operazioni su mappe e programmi.
Riepilogo
La sicurezza non è qualcosa che puoi applicare in modo universale a tutto ciò che vuoi proteggere. È importante essere in grado di proteggere i sistemi a diversi livelli e in modi differenti. Credeteci o no, il modo migliore per mettere in sicurezza un sistema è organizzare diversi livelli di protezione da posizioni diverse, affinché il calo della sicurezza di un livello non consenta l'accesso all'intero sistema. Gli sviluppatori del kernel hanno svolto un grande lavoro, fornendoci un insieme di strati e punti di interazione diversi. Speriamo di avervi dato una buona comprensione di cosa siano gli strati e come utilizzare i programmi BPF per lavorarci.
A proposito degli autori
David Calavera è direttore tecnico presso Netlify. Ha lavorato nel supporto clienti di Docker e ha partecipato allo sviluppo di strumenti come Runc, Go e BCC, così come ad altri progetti open source. È noto per il suo lavoro sui progetti Docker e per lo sviluppo dell'ecosistema dei plugin Docker. David ama molto i flame graph e cerca sempre di ottimizzare le prestazioni.
Lorenzo Fontana lavora in un team di sviluppatori software open source in Sysdig, dove si occupa principalmente di Falco — un progetto della Cloud Native Computing Foundation, che garantisce la sicurezza dell'ambiente di esecuzione dei contenitori e la rilevazione delle anomalie tramite il modulo del kernel e eBPF. È appassionato di sistemi distribuiti, reti definibili dal software, kernel Linux e analisi delle prestazioni.
» Maggiori dettagli sul libro sono disponibili su
»
»
Per gli utenti di Habr sconto del 25% con il coupon — Linux
Una volta effettuato il pagamento della versione cartacea del libro, riceverai un'email con il libro elettronico.
Fonte: habr.com
