Boek "BPF voor Linux Monitoring"

Boek "BPF voor Linux Monitoring"Hallo, inwoners van Khabro! De virtuele BPF-machine is een van de belangrijkste componenten van de Linux-kernel. Door het juiste gebruik ervan kunnen systeemingenieurs fouten opsporen en zelfs de meest complexe problemen oplossen. Je leert hoe je programma's schrijft die het gedrag van de kernel monitoren en wijzigen, hoe je veilig code implementeert om gebeurtenissen in de kernel te monitoren, en nog veel meer. David Calavera en Lorenzo Fontana helpen je de kracht van BPF te ontsluiten. Vergroot uw kennis van prestatie-optimalisatie, netwerken en beveiliging. - Gebruik BPF om het gedrag van de Linux-kernel te controleren en aan te passen. - Injecteer code om kernelgebeurtenissen veilig te monitoren zonder dat u de kernel opnieuw hoeft te compileren of het systeem opnieuw hoeft op te starten. — Gebruik handige codevoorbeelden in C, Go of Python. - Neem de controle over door eigenaar te worden van de levenscyclus van het BPF-programma.

Linux Kernel-beveiliging, de functies ervan en Seccomp

BPF biedt een krachtige manier om de kernel uit te breiden zonder in te boeten aan stabiliteit, veiligheid of snelheid. Om deze reden dachten de kernelontwikkelaars dat het een goed idee zou zijn om de veelzijdigheid ervan te gebruiken om de procesisolatie in Seccomp te verbeteren door Seccomp-filters te implementeren die worden ondersteund door BPF-programma's, ook bekend als Seccomp BPF. In dit hoofdstuk leggen we uit wat Seccomp is en hoe het wordt gebruikt. Vervolgens leert u hoe u Seccomp-filters schrijft met behulp van BPF-programma's. Daarna zullen we kijken naar de ingebouwde BPF-hooks die zijn opgenomen in de kernel voor Linux-beveiligingsmodules.

Linux Security Modules (LSM) zijn een raamwerk dat een reeks functies biedt die kunnen worden gebruikt om verschillende beveiligingsmodellen op een gestandaardiseerde manier te implementeren. LSM kan rechtstreeks in de kernelbronboom worden gebruikt, zoals Apparmor, SELinux en Tomoyo.

Laten we beginnen met het bespreken van de mogelijkheden van Linux.

Mogelijkheden

De essentie van de mogelijkheden van Linux is dat je een niet-bevoorrecht proces toestemming moet geven om een ​​bepaalde taak uit te voeren, maar zonder suid voor dat doel te gebruiken, of het proces op een andere manier bevoorrecht te maken, waardoor de kans op aanvallen wordt verkleind en het proces bepaalde taken kan uitvoeren. Als uw toepassing bijvoorbeeld een bevoorrechte poort moet openen, bijvoorbeeld 80, in plaats van het proces als root uit te voeren, kunt u deze eenvoudigweg de CAP_NET_BIND_SERVICE-mogelijkheid geven.

Overweeg een Go-programma met de naam main.go:

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

Dit programma bedient een HTTP-server op poort 80 (dit is een geprivilegieerde poort). Meestal voeren we het onmiddellijk na de compilatie uit:

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

Omdat we echter geen rootrechten verlenen, zal deze code een foutmelding geven bij het binden van de poort:

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

capsh (shell manager) is een tool die een shell uitvoert met een specifieke reeks mogelijkheden.

In dit geval kunt u, zoals reeds vermeld, in plaats van volledige rootrechten te verlenen, geprivilegieerde poortbinding inschakelen door de mogelijkheid cap_net_bind_service aan te bieden, samen met al het andere dat al in het programma aanwezig is. Om dit te doen, 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 dit team een ​​beetje begrijpen.

  • capsh - gebruik capsh als shell.
  • —caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' - aangezien we de gebruiker moeten veranderen (we willen niet als root draaien), zullen we cap_net_bind_service specificeren en de mogelijkheid om de gebruikers-ID daadwerkelijk te wijzigen van root voor niemand, namelijk cap_setuid en cap_setgid.
  • —keep=1 — we willen de geïnstalleerde mogelijkheden behouden wanneer we overschakelen van het root-account.
  • —user=“nobody” — de eindgebruiker die het programma uitvoert, zal niemand zijn.
  • —addamb=cap_net_bind_service — stel het wissen van gerelateerde mogelijkheden in na het overschakelen van de rootmodus.
  • - -c "./capabilities" - voer gewoon het programma uit.

Gekoppelde mogelijkheden zijn een speciaal soort mogelijkheden die door onderliggende programma's worden overgenomen wanneer het huidige programma ze uitvoert met behulp van execve(). Alleen capaciteiten die mogen worden geassocieerd, of met andere woorden, als omgevingscapaciteiten, kunnen worden geërfd.

Je vraagt ​​je waarschijnlijk af wat +eip betekent na het specificeren van de mogelijkheid in de --caps optie. Deze vlaggen worden gebruikt om te bepalen dat de mogelijkheid:

-moet geactiveerd zijn (p);

- beschikbaar voor gebruik (e);

-kan worden overgenomen door onderliggende processen (i).

Omdat we cap_net_bind_service willen gebruiken, moeten we dit doen met de e-vlag. Vervolgens starten we de shell in de opdracht. Hierdoor worden de mogelijkheden binair uitgevoerd en moeten we deze markeren met de i-vlag. Ten slotte willen we dat de functie wordt ingeschakeld (we hebben dit gedaan zonder de UID te wijzigen) met p. Het lijkt op cap_net_bind_service+eip.

U kunt het resultaat controleren met ss. Laten we de uitvoer een beetje inkorten zodat deze op de pagina past, maar de bijbehorende poort en gebruikers-ID worden anders dan 0 weergegeven, in dit geval 65:

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

In dit voorbeeld hebben we capsh gebruikt, maar je kunt een shell schrijven met libcap. Voor meer informatie, zie man 3 libcap.

Bij het schrijven van programma's weet de ontwikkelaar vaak niet van tevoren alle functies die het programma tijdens runtime nodig heeft; Bovendien kunnen deze functies in nieuwe versies veranderen.

Om de mogelijkheden van ons programma beter te begrijpen, kunnen we de BCC-compatibele tool gebruiken, die de kprobe instelt voor de cap_capable kernelfunctie:

/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

We kunnen hetzelfde bereiken door bpftrace te gebruiken met een one-liner kprobe in de cap_capable kernelfunctie:

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

Dit zal ongeveer het volgende opleveren als de mogelijkheden van ons programma worden ingeschakeld na 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 1

De vijfde kolom bevat de mogelijkheden die het proces nodig heeft, en aangezien deze uitvoer niet-auditgebeurtenissen bevat, zien we alle niet-auditcontroles en uiteindelijk de vereiste capaciteit met de auditvlag (laatste in de uitvoer) ingesteld op 1. één waarin we geïnteresseerd zijn is CAP_NET_BIND_SERVICE, het wordt gedefinieerd als een constante in de kernelbroncode in het bestand include/uapi/linux/ability.h met identificatie 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 tijdens runtime vaak ingeschakeld door containers zoals runC of Docker, zodat ze in de modus zonder privileges kunnen draaien, maar ze krijgen alleen de mogelijkheden die nodig zijn om de meeste applicaties uit te voeren. Wanneer een applicatie bepaalde mogelijkheden vereist, kan Docker deze bieden met behulp van --cap-add:

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

Deze opdracht geeft de container de CAP_NET_ADMIN-mogelijkheid, waardoor deze een netwerklink kan configureren om de dummy0-interface toe te voegen.

In de volgende sectie ziet u hoe u functies zoals filteren kunt gebruiken, maar met behulp van een andere techniek waarmee we onze eigen filters programmatisch kunnen implementeren.

Seccomp

Seccomp staat voor Secure Computing en is een beveiligingslaag geïmplementeerd in de Linux-kernel waarmee ontwikkelaars bepaalde systeemaanroepen kunnen filteren. Hoewel Seccomp qua mogelijkheden vergelijkbaar is met Linux, maakt de mogelijkheid om bepaalde systeemaanroepen te beheren het veel flexibeler vergeleken met hen.

Seccomp- en Linux-functies sluiten elkaar niet uit en worden vaak samen gebruikt om van beide benaderingen te profiteren. U wilt bijvoorbeeld een proces mogelijk de CAP_NET_ADMIN-mogelijkheid geven, maar niet toestaan ​​dat het socketverbindingen accepteert, waardoor de accept- en accept4-systeemaanroepen worden geblokkeerd.

De Seccomp-filtermethode is gebaseerd op BPF-filters die in de SECCOMP_MODE_FILTER-modus werken, en het filteren van systeemaanroepen wordt op dezelfde manier uitgevoerd als voor pakketten.

Seccomp-filters worden geladen met behulp van prctl via de PR_SET_SECCOMP-bewerking. Deze filters hebben de vorm van een BPF-programma dat wordt uitgevoerd voor elk Seccomp-pakket dat wordt weergegeven door de seccomp_data-structuur. Deze structuur bevat de referentiearchitectuur, een verwijzing naar processorinstructies op het moment van de systeemaanroep, en maximaal zes systeemaanroepargumenten, uitgedrukt als uint64.

Dit is hoe de seccomp_data-structuur eruit ziet vanuit de kernelbroncode in het linux/seccomp.h-bestand:

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

Zoals je in deze structuur kunt zien, kunnen we filteren op de systeemaanroep, de argumenten ervan of een combinatie van beide.

Na ontvangst van elk Seccomp-pakket moet het filter een verwerking uitvoeren om een ​​definitieve beslissing te nemen en de kernel te vertellen wat hij vervolgens moet doen. De uiteindelijke beslissing wordt uitgedrukt door een van de retourwaarden (statuscodes).

- SECCOMP_RET_KILL_PROCESS - beëindigt het hele proces onmiddellijk na het filteren van een systeemaanroep die hierdoor niet wordt uitgevoerd.

- SECCOMP_RET_KILL_THREAD - beëindigt de huidige thread onmiddellijk na het filteren van een systeemoproep die hierdoor niet wordt uitgevoerd.

— SECCOMP_RET_KILL — alias voor SECCOMP_RET_KILL_THREAD, links voor achterwaartse compatibiliteit.

- SECCOMP_RET_TRAP - de systeemaanroep is verboden en het SIGSYS-signaal (Bad System Call) wordt verzonden naar de taak die deze oproept.

- SECCOMP_RET_ERRNO - De systeemaanroep wordt niet uitgevoerd en een deel van de SECCOMP_RET_DATA-filterretourwaarde wordt doorgegeven aan de gebruikersruimte als de errno-waarde. Afhankelijk van de oorzaak van de fout worden verschillende errno-waarden geretourneerd. In de volgende sectie vindt u een lijst met foutnummers.

- SECCOMP_RET_TRACE - Wordt gebruikt om de ptrace-tracer op de hoogte te stellen met behulp van - PTRACE_O_TRACESECCOMP om te onderscheppen wanneer een systeemoproep wordt uitgevoerd om dat proces te zien en te controleren. Als er geen tracer is aangesloten, wordt er een fout geretourneerd, wordt errno ingesteld op -ENOSYS en wordt de systeemaanroep niet uitgevoerd.

- SECCOMP_RET_LOG - de systeemoproep wordt opgelost en geregistreerd.

- SECCOMP_RET_ALLOW - de systeemoproep is gewoon toegestaan.

ptrace is een systeemaanroep om traceringsmechanismen te implementeren in een proces genaamd tracee, met de mogelijkheid om de uitvoering van het proces te monitoren en te controleren. Het traceerprogramma kan de uitvoering effectief beïnvloeden en de geheugenregisters van tracee wijzigen. In de Seccomp-context wordt ptrace gebruikt wanneer het wordt geactiveerd door de SECCOMP_RET_TRACE-statuscode, zodat de tracer kan voorkomen dat de systeemaanroep wordt uitgevoerd en zijn eigen logica kan implementeren.

Seccomp-fouten

Van tijd tot tijd zult u tijdens het werken met Seccomp verschillende fouten tegenkomen, die worden geïdentificeerd door een retourwaarde van het type SECCOMP_RET_ERRNO. Om een ​​fout te melden, retourneert de seccomp-systeemaanroep -1 in plaats van 0.

De volgende fouten zijn mogelijk:

- EACCESS - De beller mag geen systeemoproep plaatsen. Dit gebeurt meestal omdat het geen CAP_SYS_ADMIN-rechten heeft of no_new_privs niet is ingesteld met behulp van prctl (we zullen hier later over praten);

— EFAULT — de doorgegeven argumenten (args in de seccomp_data-structuur) hebben geen geldig adres;

– EINVAL – hier kunnen vier redenen voor zijn:

-de gevraagde bewerking is onbekend of wordt niet ondersteund door de kernel in de huidige configuratie;

-de opgegeven vlaggen zijn niet geldig voor de gevraagde bewerking;

-bewerking 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 aan het filter wordt doorgegeven, overschrijdt het maximum;

— ENOMEM — niet genoeg geheugen 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 geen return in argumenten;

— ESRCH — er is een probleem opgetreden bij het synchroniseren van een andere stream;

- ENOSYS - Er is geen tracer gekoppeld aan de SECCOMP_RET_TRACE-actie.

prctl is een systeemaanroep waarmee een gebruikersruimteprogramma specifieke aspecten van een proces kan manipuleren (instellen en ophalen), zoals byte-endianness, threadnamen, veilige berekeningsmodus (Seccomp), privileges, Perf-gebeurtenissen, enz.

Seccomp lijkt voor u misschien een sandbox-technologie, maar dat is het niet. Seccomp is een hulpprogramma waarmee gebruikers een sandbox-mechanisme kunnen ontwikkelen. Laten we nu eens kijken hoe gebruikersinteractieprogramma's worden gemaakt met behulp van een filter dat rechtstreeks wordt aangeroepen door de Seccomp-systeemaanroep.

BPF Seccomp-filtervoorbeeld

Hier laten we zien hoe je de twee eerder besproken acties kunt combineren, namelijk:

— we zullen een Seccomp BPF-programma schrijven, dat zal worden gebruikt als een filter met verschillende retourcodes, afhankelijk van de genomen beslissingen;

— laad het filter met prctl.

Eerst heb je headers nodig uit de standaardbibliotheek en de Linux-kernel:

#include <errno.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <unistd.h>

Voordat we dit voorbeeld proberen, moeten we ervoor zorgen dat de kernel is gecompileerd met CONFIG_SECCOMP en CONFIG_SECCOMP_FILTER ingesteld op y. Op een werkende machine kun je dit als volgt controleren:

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

De rest van de code is een tweedelige install_filter-functie. 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 macro's BPF_STMT en BPF_JUMP 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 van BPF_LD in de vorm van het woord BPF_W, pakketgegevens bevinden zich op een vaste offset BPF_ABS.

- BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) - controleert met behulp van BPF_JEQ of de architectuurwaarde in de accumulatorconstante BPF_K gelijk is aan arch. Als dat zo is, springt hij bij offset 0 naar de volgende instructie, anders springt hij bij offset 3 (in dit geval) om een ​​fout te genereren omdat de boog niet overeenkomt.

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) - Laadt en accumuleert vanuit BPF_LD in de vorm van het woord BPF_W, wat het systeemoproepnummer is dat is opgenomen in de vaste offset van BPF_ABS.

— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) — vergelijkt het systeemoproepnummer met de waarde van de nr-variabele. Als ze gelijk zijn, gaat u verder met de volgende instructie en schakelt u de systeemaanroep uit, anders staat u de systeemaanroep toe met SECCOMP_RET_ALLOW.

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (fout & SECCOMP_RET_DATA)) - beëindigt het programma met BPF_RET en produceert als resultaat een fout SECCOMP_RET_ERRNO met het nummer van de err-variabele.

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) - beëindigt het programma met BPF_RET en staat toe dat de systeemaanroep wordt uitgevoerd met SECCOMP_RET_ALLOW.

SECCOMP IS CBPF
Je vraagt ​​je misschien af ​​waarom een ​​lijst met instructies wordt gebruikt in plaats van een gecompileerd ELF-object of een JIT-gecompileerd C-programma.

Hiervoor zijn twee redenen.

• Ten eerste gebruikt Seccomp cBPF (klassiek BPF) en niet eBPF, wat betekent: het heeft geen registers, maar alleen een accumulator om het laatste rekenresultaat op te slaan, zoals te zien is in het voorbeeld.

• Ten tweede accepteert Seccomp rechtstreeks een verwijzing naar een reeks BPF-instructies en niets anders. De macro's die we hebben gebruikt, helpen eenvoudigweg deze instructies op een programmeurvriendelijke manier te specificeren.

Als je meer hulp nodig hebt bij het begrijpen van deze assembly, overweeg 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;

Nadat u de filtercode in de socket_filter-structuur hebt gedefinieerd, moet u een sock_fprog definiëren die de code en de berekende lengte van het filter bevat. Deze gegevensstructuur is nodig als argument om het proces later uit te voeren:

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

Er hoeft nog maar één ding te worden gedaan in de install_filter-functie: het programma zelf laden! Om dit te doen gebruiken we prctl, waarbij we PR_SET_SECCOMP als optie nemen om naar de veilige computermodus te gaan. Vervolgens vertellen we de modus om het filter te laden met behulp van SECCOMP_MODE_FILTER, dat is opgenomen in de prog-variabele van het type sock_fprog:

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

Ten slotte kunnen we onze install_filter-functie gebruiken, maar daarvoor moeten we prctl gebruiken om PR_SET_NO_NEW_PRIVS in te stellen voor de huidige uitvoering en daardoor de situatie te vermijden waarin onderliggende processen meer rechten krijgen dan hun ouders. Hiermee kunnen we de volgende prctl-aanroepen doen in de install_filter-functie zonder rootrechten te hebben.

Nu kunnen we de install_filter-functie aanroepen. Laten we alle schrijfsysteemoproepen met betrekking tot de X86-64-architectuur blokkeren en eenvoudigweg toestemming geven die alle pogingen blokkeert. Nadat we het filter hebben geïnstalleerd, gaan we verder met de uitvoering met behulp van het eerste argument:

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. Om ons programma te compileren kunnen we clang of gcc gebruiken, hoe dan ook, het compileert gewoon het main.c-bestand zonder speciale opties:

clang main.c -o filter-write

Zoals opgemerkt hebben we alle vermeldingen in het programma geblokkeerd. Om dit te testen heb je een programma nodig dat iets uitvoert - ls lijkt een goede kandidaat. Dit is hoe ze zich meestal gedraagt:

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

Prachtig! Zo ziet het gebruik van ons wrapper-programma eruit: We geven eenvoudigweg het programma dat we willen testen als eerste argument door:

./filter-write "ls -la"

Wanneer dit programma wordt uitgevoerd, produceert het een volledig lege uitvoer. We kunnen strace echter gebruiken om te zien wat er aan de hand is:

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

Het resultaat van het werk is aanzienlijk ingekort, maar het overeenkomstige deel ervan laat zien dat records worden geblokkeerd met de EPERM-fout - dezelfde die we hebben geconfigureerd. Dit betekent dat het programma niets uitvoert omdat het geen toegang heeft tot de schrijfsysteemaanroep:

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

Nu begrijp je hoe Seccomp BPF werkt en heb je een goed idee van wat je ermee kunt doen. Maar zou u niet hetzelfde willen bereiken met eBPF in plaats van cBPF om de volledige kracht ervan te benutten?

Als ze aan eBPF-programma's denken, denken de meeste mensen dat ze deze eenvoudigweg schrijven en laden met beheerdersrechten. Hoewel deze bewering over het algemeen waar is, implementeert de kernel een reeks mechanismen om eBPF-objecten op verschillende niveaus te beschermen. Deze mechanismen worden BPF LSM-traps genoemd.

BPF LSM-vallen

Om architectuuronafhankelijke monitoring van systeemgebeurtenissen mogelijk te maken, implementeert LSM het concept van traps. Een hook call is technisch gezien vergelijkbaar met een systeemoproep, maar is systeemonafhankelijk en geïntegreerd met de infrastructuur. LSM biedt een nieuw concept waarbij een abstractielaag kan helpen problemen te voorkomen die optreden bij het omgaan met systeemaanroepen op verschillende architecturen.

Op het moment van schrijven heeft de kernel zeven hooks die geassocieerd zijn met BPF-programma's, en SELinux is de enige ingebouwde LSM die deze implementeert.

De broncode voor de traps bevindt zich in de kernelboom 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 in verschillende stadia van uitvoering worden opgeroepen:

— security_bpf — voert een eerste controle uit van uitgevoerde BPF-systeemoproepen;

- 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 het beveiligingsveld is gewist in BPF-kaarten;

— security_bpf_prog_alloc — controleert of het beveiligingsveld is geïnitialiseerd in BPF-programma's;

- security_bpf_prog_free - controleert of het beveiligingsveld is gewist in BPF-programma's.

Nu we dit allemaal zien, begrijpen we het: het idee achter LSM BPF-interceptors is dat ze bescherming kunnen bieden aan elk eBPF-object en ervoor zorgen dat alleen degenen met de juiste rechten bewerkingen op kaarten en programma's kunnen uitvoeren.

Beknopt

Beveiliging is niet iets dat u op een one-size-fits-all manier kunt implementeren voor alles wat u wilt beschermen. Het is belangrijk om systemen op verschillende niveaus en op verschillende manieren te kunnen beveiligen. Geloof het of niet, de beste manier om een ​​systeem te beveiligen is door verschillende beschermingsniveaus vanuit verschillende posities te organiseren, zodat het verminderen van de beveiliging van één niveau geen toegang tot het hele systeem mogelijk maakt. De kernontwikkelaars hebben uitstekend werk verricht door ons een reeks verschillende lagen en contactpunten te bieden. We hopen dat we u een goed begrip hebben gegeven van wat lagen zijn en hoe u BPF-programma's kunt gebruiken om ermee te werken.

Over de Auteurs

David Calavera is de CTO bij Netlify. Hij werkte in de Docker-ondersteuning en droeg bij aan de ontwikkeling van Runc-, Go- en BCC-tools, evenals aan andere open source-projecten. Bekend om zijn werk aan Docker-projecten en de ontwikkeling van het Docker-plug-in-ecosysteem. David heeft een grote passie voor vlamgrafieken en is altijd op zoek naar het optimaliseren van de prestaties.

Lorenzo Fontana werkt in het open source-team van Sysdig, waar hij zich vooral richt op Falco, een Cloud Native Computing Foundation-project dat containerruntime-beveiliging en anomaliedetectie biedt via een kernelmodule en eBPF. Hij heeft een passie voor gedistribueerde systemen, softwaregedefinieerde netwerken, de Linux-kernel en prestatieanalyse.

» Meer details over het boek zijn te vinden op website van de uitgever
» inhoudsopgave
» Uittreksel

Voor Khabrozhiteley 25% korting met coupon - Linux

Na betaling van de papieren versie van het boek wordt een elektronisch boek per e-mail verzonden.

Bron: www.habr.com

Voeg een reactie