BPF per als més petits, part zero: BPF clàssic

Els filtres de paquets de Berkeley (BPF) és una tecnologia del nucli de Linux que ha estat a les primeres pàgines de les publicacions tecnològiques en anglès des de fa uns quants anys. Les conferències estan plenes d'informes sobre l'ús i desenvolupament de BPF. David Miller, mantenidor del subsistema de xarxa Linux, convoca la seva xerrada a Linux Plumbers 2018 "Aquesta xerrada no és sobre XDP" (XDP és un cas d'ús per a BPF). Brendan Gregg fa una conferència titulada Superpoders de Linux BPF. Toke Høiland-Jørgensen riallesque el nucli és ara un micronucli. Thomas Graf promou la idea que BPF és javascript per al nucli.

Encara no hi ha una descripció sistemàtica de BPF a Habré i, per tant, en una sèrie d'articles intentaré parlar de la història de la tecnologia, descriure l'arquitectura i les eines de desenvolupament i esbossar les àrees d'aplicació i pràctica de l'ús de BPF. Aquest article, zero, de la sèrie, explica la història i l'arquitectura del clàssic BPF, i també revela els secrets dels seus principis de funcionament. tcpdump, seccomp, strace, i molt més.

El desenvolupament de BPF està controlat per la comunitat de xarxes Linux, les principals aplicacions existents de BPF estan relacionades amb xarxes i per tant, amb permís @eucariota, vaig anomenar la sèrie “BPF per als més petits”, en honor a la gran sèrie "Xarxes per als més petits".

Un curs breu sobre la història de BPF(c)

La tecnologia BPF moderna és una versió millorada i ampliada de l'antiga tecnologia amb el mateix nom, ara anomenada BPF clàssic per evitar confusions. Es va crear una coneguda utilitat basada en el clàssic BPF tcpdump, mecanisme seccomp, així com mòduls menys coneguts xt_bpf per iptables i classificador cls_bpf. Al Linux modern, els programes clàssics de BPF es tradueixen automàticament a la nova forma, però, des del punt de vista de l'usuari, l'API s'ha mantingut al seu lloc i encara s'estan trobant nous usos per a BPF clàssic, com veurem en aquest article. Per aquest motiu, i també perquè seguint la història del desenvolupament del BPF clàssic a Linux, es veurà més clar com i per què va evolucionar cap a la seva forma moderna, vaig decidir començar amb un article sobre el BPF clàssic.

A finals dels anys vuitanta del segle passat, els enginyers del famós laboratori Lawrence Berkeley es van interessar per la qüestió de com filtrar correctament els paquets de xarxa en un maquinari que era modern a finals dels vuitanta del segle passat. La idea bàsica del filtratge, implementada originalment a la tecnologia CSPF (CMU/Stanford Packet Filter), era filtrar els paquets innecessaris el més aviat possible, és a dir. a l'espai del nucli, ja que això evita copiar dades innecessàries a l'espai d'usuari. Per proporcionar seguretat en temps d'execució per executar codi d'usuari a l'espai del nucli, es va utilitzar una màquina virtual amb caixa de sorra.

Tanmateix, les màquines virtuals per als filtres existents es van dissenyar per executar-se en màquines basades en la pila i no funcionaven amb tanta eficàcia en màquines RISC més noves. Com a resultat, gràcies als esforços dels enginyers de Berkeley Labs, es va desenvolupar una nova tecnologia BPF (Berkeley Packet Filters), l'arquitectura de la màquina virtual de la qual es va dissenyar basant-se en el processador Motorola 6502, el cavall de batalla de productes tan coneguts com ara Apple II o NS. La nova màquina virtual va augmentar el rendiment del filtre desenes de vegades en comparació amb les solucions existents.

Arquitectura de màquines BPF

Ens familiaritzarem amb l'arquitectura d'una manera de treball, analitzant exemples. Tanmateix, per començar, diguem que la màquina tenia dos registres de 32 bits accessibles per l'usuari, un acumulador A i registre d'índex X, 64 bytes de memòria (16 paraules), disponible per escriure i llegir posteriorment, i un petit sistema d'ordres per treballar amb aquests objectes. Les instruccions de salt per implementar expressions condicionals també estaven disponibles als programes, però per garantir la finalització oportuna del programa, només es podien fer salts cap endavant, és a dir, en particular, estava prohibit crear bucles.

L'esquema general per engegar la màquina és el següent. L'usuari crea un programa per a l'arquitectura BPF i, utilitzant alguns mecanisme del nucli (com ara una trucada al sistema), carrega i connecta el programa a alguns al generador d'esdeveniments del nucli (per exemple, un esdeveniment és l'arribada del següent paquet a la targeta de xarxa). Quan es produeix un esdeveniment, el nucli executa el programa (per exemple, en un intèrpret) i la memòria de la màquina correspon a a alguns regió de memòria del nucli (per exemple, dades d'un paquet entrant).

Amb l'anterior ens n'hi haurà prou per començar a mirar exemples: ens familiaritzarem amb el sistema i el format d'ordres si cal. Si voleu estudiar immediatament el sistema de comandaments d'una màquina virtual i conèixer totes les seves capacitats, podeu llegir l'article original El filtre de paquets BSD i/o la primera meitat de l'expedient Documentació/xarxes/filter.txt de la documentació del nucli. A més, podeu estudiar la presentació libpcap: Una metodologia d'arquitectura i optimització per a la captura de paquets, en què McCanne, un dels autors de BPF, parla de la història de la creació libpcap.

Ara passem a considerar tots els exemples significatius d'utilitzar BPF clàssic a Linux: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.

tcpdump

El desenvolupament de BPF es va dur a terme en paral·lel amb el desenvolupament de la interfície per al filtratge de paquets, una utilitat coneguda tcpdump. I, com que aquest és l'exemple més antic i famós d'ús de BPF clàssic, disponible en molts sistemes operatius, començarem el nostre estudi de la tecnologia amb ell.

(Vaig executar tots els exemples d'aquest article sobre Linux 5.6.0-rc6. La sortida d'algunes ordres s'ha editat per a una millor llegibilitat.)

Exemple: observació de paquets IPv6

Imaginem que volem mirar tots els paquets IPv6 en una interfície eth0. Per fer-ho podem executar el programa tcpdump amb un filtre senzill ip6:

$ sudo tcpdump -i eth0 ip6

En aquest cas, tcpdump compila el filtre ip6 al bytecode de l'arquitectura BPF i enviar-lo al nucli (vegeu els detalls a la secció Tcpdump: carregant). El filtre carregat s'executarà per a cada paquet que passi per la interfície eth0. Si el filtre retorna un valor diferent de zero n, després fins a n bytes del paquet es copiaran a l'espai d'usuari i ho veurem a la sortida tcpdump.

BPF per als més petits, part zero: BPF clàssic

Resulta que podem esbrinar fàcilment quin bytecode es va enviar al nucli tcpdump amb l'ajuda de la tcpdump, si l'executem amb l'opció -d:

$ sudo tcpdump -i eth0 -d ip6
(000) ldh      [12]
(001) jeq      #0x86dd          jt 2    jf 3
(002) ret      #262144
(003) ret      #0

A la línia zero executem l'ordre ldh [12], que significa "càrrega al registre A mitja paraula (16 bits) situada a l'adreça 12” i l'única pregunta és a quin tipus de memòria estem adreçant? La resposta és que a x comença (x+1)octet del paquet de xarxa analitzat. Llegim paquets de la interfície Ethernet eth0i això 1/2que el paquet tingui aquest aspecte (per simplificar, suposem que no hi ha etiquetes VLAN al paquet):

       6              6          2
|Destination MAC|Source MAC|Ether Type|...|

Així que després d'executar l'ordre ldh [12] al registre A hi haurà un camp Ether Type — el tipus de paquet transmès en aquesta trama Ethernet. A la línia 1 comparem el contingut del registre A (tipus de paquet) c 0x86ddi això i hi ha El tipus que ens interessa és IPv6. A la línia 1, a més de l'ordre de comparació, hi ha dues columnes més: jt 2 и jf 3 — marques a les quals cal anar si la comparació té èxit (A == 0x86dd) i sense èxit. Per tant, en un cas d'èxit (IPv6) anem a la línia 2, i en un cas infructuós - a la línia 3. A la línia 3 el programa acaba amb el codi 0 (no copieu el paquet), a la línia 2 el programa acaba amb el codi 262144 (copieu-me un paquet de 256 kilobytes com a màxim).

Un exemple més complicat: mirem els paquets TCP per port de destinació

Vegem com és un filtre que copia tots els paquets TCP amb el port de destinació 666. Considerarem el cas IPv4, ja que el cas IPv6 és més senzill. Després d'estudiar aquest exemple, podeu explorar el filtre IPv6 vosaltres mateixos com a exercici (ip6 and tcp dst port 666) i un filtre per al cas general (tcp dst port 666). Per tant, el filtre que ens interessa és el següent:

$ sudo tcpdump -i eth0 -d ip and tcp dst port 666
(000) ldh      [12]
(001) jeq      #0x800           jt 2    jf 10
(002) ldb      [23]
(003) jeq      #0x6             jt 4    jf 10
(004) ldh      [20]
(005) jset     #0x1fff          jt 10   jf 6
(006) ldxb     4*([14]&0xf)
(007) ldh      [x + 16]
(008) jeq      #0x29a           jt 9    jf 10
(009) ret      #262144
(010) ret      #0

Ja sabem què fan les línies 0 i 1. A la línia 2 ja hem comprovat que es tracta d'un paquet IPv4 (Tipus Ether = 0x800) i carregar-lo al registre A 24è byte del paquet. El nostre paquet sembla

       14            8      1     1
|ethernet header|ip fields|ttl|protocol|...|

el que significa que carreguem al registre A el camp Protocol de la capçalera IP, que és lògic, perquè només volem copiar paquets TCP. Comparem Protocol amb 0x6 (IPPROTO_TCP) a la línia 3.

A les línies 4 i 5 carreguem les mitges paraules situades a l'adreça 20 i fem servir l'ordre jset comproveu si un dels tres està configurat banderes - Portar la mascareta expedida jset s'esborren els tres bits més significatius. Dos dels tres bits ens indiquen si el paquet forma part d'un paquet IP fragmentat i, si és així, si és l'últim fragment. El tercer bit està reservat i ha de ser zero. No volem comprovar els paquets incomplets o trencats, així que comprovem els tres bits.

La línia 6 és la més interessant d'aquesta llista. Expressió ldxb 4*([14]&0xf) vol dir que carreguem al registre X els quatre bits menys significatius del quinzè byte del paquet multiplicats per 4. Els quatre bits menys significatius del quinzeè byte és el camp Longitud de la capçalera d'Internet Capçalera IPv4, que emmagatzema la longitud de la capçalera en paraules, de manera que cal multiplicar per 4. Curiosament, l'expressió 4*([14]&0xf) és una designació per a un esquema especial d'adreçament que només es pot utilitzar en aquest formulari i només per a un registre X, és a dir tampoc ho podem dir ldb 4*([14]&0xf) ни ldxb 5*([14]&0xf) (només podem especificar un desplaçament diferent, per exemple, ldxb 4*([16]&0xf)). És evident que aquest esquema d'adreçament es va afegir a BPF precisament per rebre X (registre d'índex) Longitud de la capçalera IPv4.

Així que a la línia 7 intentem carregar mitja paraula a (X+16). Recordant que 14 bytes els ocupa la capçalera Ethernet, i X conté la longitud de la capçalera IPv4, entenem que a A S'ha carregat el port de destinació TCP:

       14           X           2             2
|ethernet header|ip header|source port|destination port|

Finalment, a la línia 8 comparem el port de destinació amb el valor desitjat i a les línies 9 o 10 tornem el resultat, si es copia el paquet o no.

Tcpdump: carregant

En els exemples anteriors, específicament no ens vam detenir en detall sobre com carreguem el bytecode BPF al nucli per al filtratge de paquets. En termes generals, tcpdump portat a molts sistemes i per treballar amb filtres tcpdump utilitza la biblioteca libpcap. Breument, per col·locar un filtre en una interfície utilitzant libpcap, heu de fer el següent:

Per veure com funciona pcap_setfilter implementat a Linux, fem servir strace (s'han eliminat algunes línies):

$ sudo strace -f -e trace=%network tcpdump -p -i eth0 ip
socket(AF_PACKET, SOCK_RAW, 768)        = 3
bind(3, {sa_family=AF_PACKET, sll_protocol=htons(ETH_P_ALL), sll_ifindex=if_nametoindex("eth0"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=4, filter=0xb00bb00bb00b}, 16) = 0
...

A les dues primeres línies de sortida creem sòcol cru per llegir totes les trames Ethernet i vincular-les a la interfície eth0. De el nostre primer exemple sabem que el filtre ip constarà de quatre instruccions BPF, i a la tercera línia veiem com utilitzar l'opció SO_ATTACH_FILTER trucada al sistema setsockopt carreguem i connectem un filtre de longitud 4. Aquest és el nostre filtre.

Val la pena assenyalar que en el BPF clàssic, la càrrega i connexió d'un filtre sempre es produeix com una operació atòmica, i en la nova versió de BPF, la càrrega del programa i l'enllaç al generador d'esdeveniments estan separats en el temps.

Veritat Amagada

Una versió una mica més completa de la sortida té aquest aspecte:

$ sudo strace -f -e trace=%network tcpdump -p -i eth0 ip
socket(AF_PACKET, SOCK_RAW, 768)        = 3
bind(3, {sa_family=AF_PACKET, sll_protocol=htons(ETH_P_ALL), sll_ifindex=if_nametoindex("eth0"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=1, filter=0xbeefbeefbeef}, 16) = 0
recvfrom(3, 0x7ffcad394257, 1, MSG_TRUNC, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=4, filter=0xb00bb00bb00b}, 16) = 0
...

Com s'ha esmentat anteriorment, carreguem i connectem el nostre filtre al sòcol de la línia 5, però què passa a les línies 3 i 4? Resulta que això libpcap ens cuida - perquè la sortida del nostre filtre no inclogui paquets que no el compleixin, la biblioteca connecta filtre fictici ret #0 (eliminar tots els paquets), canvia el sòcol al mode no bloquejant i intenta restar tots els paquets que podrien quedar dels filtres anteriors.

En total, per filtrar paquets a Linux utilitzant BPF clàssic, cal tenir un filtre en forma d'estructura com ara struct sock_fprog i un sòcol obert, després del qual el filtre es pot connectar al sòcol mitjançant una trucada al sistema setsockopt.

Curiosament, el filtre es pot connectar a qualsevol endoll, no només en brut. Aquí exemple un programa que talla tots els datagrames UDP entrants excepte els dos primers bytes. (He afegit comentaris al codi per no desordenar l'article.)

Més detalls sobre l'ús setsockopt per connectar filtres, vegeu endoll (7), sinó sobre escriure els teus propis filtres com struct sock_fprog sense ajuda tcpdump en parlarem a l'apartat Programant BPF amb les nostres pròpies mans.

BPF clàssic i el segle XXI

BPF es va incloure a Linux el 1997 i ha estat un cavall de batalla durant molt de temps libpcap sense cap canvi especial (canvis específics de Linux, és clar, Érem, però no van canviar el panorama global). Els primers signes seriosos que BPF evolucionaria van arribar el 2011, quan Eric Dumazet va proposar pegat, que afegeix Just In Time Compiler al nucli, un traductor per convertir el codi de bytes BPF a natiu x86_64 el codi.

El compilador JIT va ser el primer de la cadena de canvis: el 2012 va aparèixer capacitat per escriure filtres seccomp, utilitzant BPF, el gener de 2013 hi havia afegit mòdul xt_bpf, que us permet escriure regles per a iptables amb l'ajuda de BPF, i l'octubre de 2013 va ser afegit també un mòdul cls_bpf, que us permet escriure classificadors de trànsit mitjançant BPF.

Aviat veurem tots aquests exemples amb més detall, però primer ens serà útil aprendre a escriure i compilar programes arbitraris per a BPF, ja que les capacitats que ofereix la biblioteca libpcap limitat (exemple simple: filtre generat libpcap només pot retornar dos valors: 0 o 0x40000) o en general, com en el cas de seccomp, no són aplicables.

Programant BPF amb les nostres pròpies mans

Familiaritzem-nos amb el format binari de les instruccions BPF, és molt senzill:

   16    8    8     32
| code | jt | jf |  k  |

Cada instrucció ocupa 64 bits, en els quals els primers 16 bits són el codi d'instrucció, després hi ha dos sagnats de vuit bits, jt и jf, i 32 bits per a l'argument K, la finalitat del qual varia d'una ordre a una altra. Per exemple, l'ordre ret, que finalitza el programa té el codi 6, i el valor de retorn es pren de la constant K. En C, una única instrucció BPF es representa com una estructura

struct sock_filter {
        __u16   code;
        __u8    jt;
        __u8    jf;
        __u32   k;
}

i tot el programa està en forma d'estructura

struct sock_fprog {
        unsigned short len;
        struct sock_filter *filter;
}

Així, ja podem escriure programes (per exemple, coneixem els codis d'instruccions de [1]). Així serà el filtre ip6 d' el nostre primer exemple:

struct sock_filter code[] = {
        { 0x28, 0, 0, 0x0000000c },
        { 0x15, 0, 1, 0x000086dd },
        { 0x06, 0, 0, 0x00040000 },
        { 0x06, 0, 0, 0x00000000 },
};
struct sock_fprog prog = {
        .len = ARRAY_SIZE(code),
        .filter = code,
};

programa prog podem utilitzar legalment en una trucada

setsockopt(sk, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog))

Escriure programes en forma de codis de màquina no és molt convenient, però de vegades és necessari (per exemple, per depurar, crear proves unitàries, escriure articles sobre Habré, etc.). Per comoditat, a l'arxiu <linux/filter.h> Es defineixen les macros d'ajuda: el mateix exemple anterior es podria reescriure com a

struct sock_filter code[] = {
        BPF_STMT(BPF_LD|BPF_H|BPF_ABS, 12),
        BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, ETH_P_IPV6, 0, 1),
        BPF_STMT(BPF_RET|BPF_K, 0x00040000),
        BPF_STMT(BPF_RET|BPF_K, 0),
}

Tanmateix, aquesta opció no és gaire convenient. Això és el que van raonar els programadors del nucli de Linux, i per tant al directori tools/bpf als nuclis podeu trobar un assemblador i un depurador per treballar amb BPF clàssic.

El llenguatge ensamblador és molt semblant a la sortida de depuració tcpdump, però a més podem especificar etiquetes simbòliques. Per exemple, aquí hi ha un programa que elimina tots els paquets excepte TCP/IPv4:

$ cat /tmp/tcp-over-ipv4.bpf
ldh [12]
jne #0x800, drop
ldb [23]
jneq #6, drop
ret #-1
drop: ret #0

Per defecte, l'assemblador genera codi en el format <количество инструкций>,<code1> <jt1> <jf1> <k1>,..., per al nostre exemple amb TCP serà

$ tools/bpf/bpf_asm /tmp/tcp-over-ipv4.bpf
6,40 0 0 12,21 0 3 2048,48 0 0 23,21 0 1 6,6 0 0 4294967295,6 0 0 0,

Per a la comoditat dels programadors C, es pot utilitzar un format de sortida diferent:

$ tools/bpf/bpf_asm -c /tmp/tcp-over-ipv4.bpf
{ 0x28,  0,  0, 0x0000000c },
{ 0x15,  0,  3, 0x00000800 },
{ 0x30,  0,  0, 0x00000017 },
{ 0x15,  0,  1, 0x00000006 },
{ 0x06,  0,  0, 0xffffffff },
{ 0x06,  0,  0, 0000000000 },

Aquest text es pot copiar a la definició de l'estructura de tipus struct sock_filter, com vam fer al principi d'aquesta secció.

Extensions de Linux i netsniff-ng

A més de BPF estàndard, Linux i tools/bpf/bpf_asm suport i conjunt no estàndard. Bàsicament, s'utilitzen instruccions per accedir als camps d'una estructura struct sk_buff, que descriu un paquet de xarxa al nucli. Tanmateix, també hi ha altres tipus d'instruccions d'ajuda, per exemple ldw cpu es carregarà al registre A resultat d'executar una funció del nucli raw_smp_processor_id(). (A la nova versió de BPF, aquestes extensions no estàndard s'han estès per proporcionar als programes un conjunt d'ajudants del nucli per accedir a la memòria, les estructures i la generació d'esdeveniments.) Aquí teniu un exemple interessant d'un filtre en el qual copiem només el capçaleres de paquets a l'espai d'usuari mitjançant l'extensió poff, compensació de càrrega útil:

ld poff
ret a

Les extensions BPF no es poden utilitzar tcpdump, però aquesta és una bona raó per familiaritzar-se amb el paquet d'utilitat netsniff-ng, que, entre altres coses, conté un programa avançat netsniff-ng, que, a més de filtrar mitjançant BPF, també conté un generador de trànsit eficaç, i més avançat que tools/bpf/bpf_asm, anomenat un assemblador BPF bpfc. El paquet conté documentació força detallada, vegeu també els enllaços al final de l'article.

seccomp

Així doncs, ja sabem com escriure programes BPF de complexitat arbitrària i estem preparats per mirar nous exemples, el primer dels quals és la tecnologia seccomp, que permet, mitjançant filtres BPF, gestionar el conjunt i el conjunt d'arguments de crida del sistema disponibles per un procés determinat i els seus descendents.

La primera versió de seccomp es va afegir al nucli l'any 2005 i no va ser molt popular, ja que només proporcionava una única opció: limitar el conjunt de trucades al sistema disponibles per a un procés a les següents: read, write, exit и sigreturn, i el procés que infringia les regles es va matar utilitzant SIGKILL. Tanmateix, el 2012, seccomp va afegir la possibilitat d'utilitzar filtres BPF, que us permeten definir un conjunt de trucades al sistema permeses i fins i tot realitzar comprovacions dels seus arguments. (Curiosament, Chrome va ser un dels primers usuaris d'aquesta funcionalitat, i la gent de Chrome actualment està desenvolupant un mecanisme KRSI basat en una nova versió de BPF i que permet personalitzar els mòduls de seguretat de Linux.) Al final es poden trobar enllaços a documentació addicional. de l'article.

Tingueu en compte que ja hi ha hagut articles al centre sobre l'ús de seccomp, potser algú voldrà llegir-los abans (o en lloc de) llegir les subseccions següents. A l'article Contenidors i seguretat: seccomp proporciona exemples d'ús de seccomp, tant la versió 2007 com la versió que utilitza BPF (els filtres es generen mitjançant libseccomp), parla de la connexió de seccomp amb Docker i també ofereix molts enllaços útils. A l'article Aïllar els dimonis amb systemd o "no necessiteu Docker per a això!" Cobreix, en particular, com afegir llistes negres o llistes blanques de trucades del sistema per als dimonis que executen systemd.

A continuació veurem com escriure i carregar filtres seccomp en C nu i utilitzant la biblioteca libseccomp i quins són els avantatges i els contres de cada opció i, finalment, veurem com fa servir el programa seccomp strace.

Escriptura i càrrega de filtres per a seccomp

Ja sabem com escriure programes BPF, així que primer mirem la interfície de programació seccomp. Podeu establir un filtre a nivell de procés i tots els processos secundaris heretaran les restriccions. Això es fa mitjançant una trucada al sistema seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

on &filter - això és un punter a una estructura que ja ens coneixem struct sock_fprog, és a dir Programa BPF.

En què es diferencien els programes per a seccomp dels programes per a sockets? Context transmès. En el cas dels sòcols, ens van donar una àrea de memòria que contenia el paquet, i en el cas de seccomp se'ns va donar una estructura com

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

Aquí nr és el número de la trucada del sistema que s'ha de llançar, arch - arquitectura actual (més sobre això a continuació), args - fins a sis arguments de trucada del sistema, i instruction_pointer és un punter a la instrucció d'espai d'usuari que va fer la trucada al sistema. Així, per exemple, per carregar el número de trucada del sistema al registre A hem de dir

ldw [0]

Hi ha altres funcions per als programes seccomp, per exemple, només es pot accedir al context mitjançant l'alineació de 32 bits i no es pot carregar mitja paraula o un byte, quan s'intenta carregar un filtre. ldh [0] trucada al sistema seccomp tornarà EINVAL. La funció comprova els filtres carregats seccomp_check_filter() nuclis. (El curiós és que a la confirmació original que va afegir la funcionalitat seccomp, es van oblidar d'afegir permís per utilitzar la instrucció a aquesta funció mod (resta de divisió) i ara no està disponible per als programes seccomp BPF, des de la seva incorporació trencarà ABI.)

Bàsicament, ja ho sabem tot per escriure i llegir programes seccomp. Normalment, la lògica del programa s'organitza com una llista blanca o negra de trucades al sistema, per exemple, el programa

ld [0]
jeq #304, bad
jeq #176, bad
jeq #239, bad
jeq #279, bad
good: ret #0x7fff0000 /* SECCOMP_RET_ALLOW */
bad: ret #0

comprova una llista negra de quatre trucades al sistema numerades 304, 176, 239, 279. Quines són aquestes trucades al sistema? No podem dir-ho amb certesa, ja que no sabem per a quina arquitectura va ser escrit el programa. Per tant, els autors de seccomp oferta iniciar tots els programes amb una comprovació d'arquitectura (l'arquitectura actual s'indica en el context com a camp arch estructures struct seccomp_data). Amb l'arquitectura marcada, el començament de l'exemple seria el següent:

ld [4]
jne #0xc000003e, bad_arch ; SCMP_ARCH_X86_64

i llavors els nostres números de trucada del sistema obtindrien certs valors.

Escrivim i carguem filtres per a seccomp utilitzant libseccomp

L'escriptura de filtres en codi natiu o en assemblatge BPF permet tenir un control total sobre el resultat, però al mateix temps, de vegades és preferible tenir codi portàtil i/o llegible. La biblioteca ens ajudarà amb això libseccomp, que proporciona una interfície estàndard per escriure filtres en blanc o negre.

Anem, per exemple, a escriure un programa que executi un fitxer binari que triï l'usuari, després d'haver instal·lat prèviament una llista negra de trucades al sistema de l'article anterior (el programa s'ha simplificat per a una major llegibilitat, es pot trobar la versió completa aquí):

#include <seccomp.h>
#include <unistd.h>
#include <err.h>

static int sys_numbers[] = {
        __NR_mount,
        __NR_umount2,
       // ... еще 40 системных вызовов ...
        __NR_vmsplice,
        __NR_perf_event_open,
};

int main(int argc, char **argv)
{
        scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);

        for (size_t i = 0; i < sizeof(sys_numbers)/sizeof(sys_numbers[0]); i++)
                seccomp_rule_add(ctx, SCMP_ACT_TRAP, sys_numbers[i], 0);

        seccomp_load(ctx);

        execvp(argv[1], &argv[1]);
        err(1, "execlp: %s", argv[1]);
}

Primer definim una matriu sys_numbers de més de 40 números de trucada del sistema per bloquejar. A continuació, inicialitzeu el context ctx i digueu a la biblioteca què volem permetre (SCMP_ACT_ALLOW) totes les trucades del sistema per defecte (és més fàcil crear llistes negres). A continuació, una per una, afegim totes les trucades del sistema de la llista negra. En resposta a una trucada del sistema de la llista, demanem SCMP_ACT_TRAP, en aquest cas seccomp enviarà un senyal al procés SIGSYS amb una descripció de quina trucada del sistema ha infringit les regles. Finalment, carreguem el programa al nucli utilitzant seccomp_load, que compilarà el programa i l'adjuntarà al procés mitjançant una trucada al sistema seccomp(2).

Per a una compilació correcta, el programa ha d'estar enllaçat amb la biblioteca libseccomp, per exemple:

cc -std=c17 -Wall -Wextra -c -o seccomp_lib.o seccomp_lib.c
cc -o seccomp_lib seccomp_lib.o -lseccomp

Exemple d'un llançament reeixit:

$ ./seccomp_lib echo ok
ok

Exemple d'una trucada de sistema bloquejada:

$ sudo ./seccomp_lib mount -t bpf bpf /tmp
Bad system call

Fem servir straceper als detalls:

$ sudo strace -e seccomp ./seccomp_lib mount -t bpf bpf /tmp
seccomp(SECCOMP_SET_MODE_FILTER, 0, {len=50, filter=0x55d8e78428e0}) = 0
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0xboobdeadbeef, si_syscall=__NR_mount, si_arch=AUDIT_ARCH_X86_64} ---
+++ killed by SIGSYS (core dumped) +++
Bad system call

com podem saber que el programa es va acabar a causa de l'ús d'una trucada il·legal al sistema mount(2).

Per tant, vam escriure un filtre utilitzant la biblioteca libseccomp, ajustant codi no trivial en quatre línies. A l'exemple anterior, si hi ha un gran nombre de trucades al sistema, el temps d'execució es pot reduir notablement, ja que la comprovació és només una llista de comparacions. Per a l'optimització, libseccomp tenia recentment pegat inclòs, que afegeix suport per a l'atribut de filtre SCMP_FLTATR_CTL_OPTIMIZE. Establir aquest atribut a 2 convertirà el filtre en un programa de cerca binari.

Si voleu veure com funcionen els filtres de cerca binaris, feu una ullada a guió senzill, que genera aquests programes a l'assemblador BPF marcant els números de trucada del sistema, per exemple:

$ echo 1 3 6 8 13 | ./generate_bin_search_bpf.py
ld [0]
jeq #6, bad
jgt #6, check8
jeq #1, bad
jeq #3, bad
ret #0x7fff0000
check8:
jeq #8, bad
jeq #13, bad
ret #0x7fff0000
bad: ret #0

És impossible escriure res significativament més ràpid, ja que els programes BPF no poden fer salts de sagnat (no podem fer, per exemple, jmp A o jmp [label+X]) i per tant totes les transicions són estàtiques.

seccomp i strace

Tothom sap la utilitat strace és una eina indispensable per estudiar el comportament dels processos a Linux. No obstant això, molts també n'han sentit a parlar problemes de rendiment quan utilitzeu aquesta utilitat. El fet és que strace implementat utilitzant ptrace(2), i en aquest mecanisme no podem especificar en quin conjunt de trucades al sistema hem d'aturar el procés, és a dir, per exemple, les ordres

$ time strace du /usr/share/ >/dev/null 2>&1

real    0m3.081s
user    0m0.531s
sys     0m2.073s

и

$ time strace -e open du /usr/share/ >/dev/null 2>&1

real    0m2.404s
user    0m0.193s
sys     0m1.800s

es processen aproximadament al mateix temps, encara que en el segon cas només volem rastrejar una trucada al sistema.

Nova opció --seccomp-bpf, afegit a strace versió 5.3, us permet accelerar el procés moltes vegades i el temps d'inici sota el rastre d'una trucada al sistema ja és comparable al temps d'un inici normal:

$ time strace --seccomp-bpf -e open du /usr/share/ >/dev/null 2>&1

real    0m0.148s
user    0m0.017s
sys     0m0.131s

$ time du /usr/share/ >/dev/null 2>&1

real    0m0.140s
user    0m0.024s
sys     0m0.116s

(Aquí, per descomptat, hi ha un petit engany en què no estem rastrejant la crida principal del sistema d'aquesta ordre. Si estiguéssim rastrejant, per exemple, newfsstat, Llavors strace frenaria tan fort com sense --seccomp-bpf.)

Com funciona aquesta opció? Sense ella strace es connecta al procés i el comença a utilitzar PTRACE_SYSCALL. Quan un procés gestionat emet una (qualsevol) trucada al sistema, el control es transfereix a strace, que mira els arguments de la crida del sistema i l'executa amb PTRACE_SYSCALL. Després d'un temps, el procés completa la trucada al sistema i en sortir-ne, el control es transfereix de nou strace, que mira els valors de retorn i comença el procés utilitzant PTRACE_SYSCALL, etcètera.

BPF per als més petits, part zero: BPF clàssic

Amb seccomp, però, aquest procés es pot optimitzar exactament com voldríem. És a dir, si volem mirar només la trucada del sistema X, llavors podem escriure un filtre BPF per a això X retorna un valor SECCOMP_RET_TRACE, i per a trucades que no ens interessen - SECCOMP_RET_ALLOW:

ld [0]
jneq #X, ignore
trace: ret #0x7ff00000
ignore: ret #0x7fff0000

En aquest cas, strace inicialment comença el procés com PTRACE_CONT, el nostre filtre es processa per a cada trucada al sistema, si no ho és X, aleshores el procés continua executant-se, però si això X, llavors seccomp transferirà el control straceque mirarà els arguments i començarà el procés com PTRACE_SYSCALL (ja que seccomp no té la capacitat d'executar un programa en sortir d'una trucada al sistema). Quan torna la trucada del sistema, strace reiniciarà el procés utilitzant PTRACE_CONT i esperarà nous missatges de seccomp.

BPF per als més petits, part zero: BPF clàssic

Quan utilitzeu l'opció --seccomp-bpf hi ha dues restriccions. En primer lloc, no serà possible unir-se a un procés ja existent (opció -p programes strace), ja que això no és compatible amb seccomp. En segon lloc, no hi ha possibilitat no mireu els processos fills, ja que els filtres seccomp són heretats per tots els processos fills sense la possibilitat de desactivar-ho.

Una mica més de detall sobre com exactament strace funciona amb seccomp es pot trobar des de informe recent. Per a nosaltres, el fet més interessant és que el clàssic BPF representat per seccomp encara es fa servir avui dia.

xt_bpf

Tornem ara al món de les xarxes.

Antecedents: fa molt de temps, l'any 2007, el nucli era afegit mòdul xt_u32 per a netfilter. Va ser escrit per analogia amb un classificador de trànsit encara més antic cls_u32 i us va permetre escriure regles binàries arbitràries per a iptables mitjançant les següents operacions senzilles: carregar 32 bits d'un paquet i realitzar-hi un conjunt d'operacions aritmètiques. Per exemple,

sudo iptables -A INPUT -m u32 --u32 "6&0xFF=1" -j LOG --log-prefix "seen-by-xt_u32"

Carrega els 32 bits de la capçalera IP, començant pel farciment 6, i els aplica una màscara 0xFF (agafa el byte baix). Aquest camp protocol Capçalera IP i la comparem amb 1 (ICMP). Podeu combinar moltes comprovacions en una regla i també podeu executar l'operador @ — mou X bytes cap a la dreta. Per exemple, la regla

iptables -m u32 --u32 "6&0xFF=0x6 && 0>>22&0x3C@4=0x29"

comprova si el número de seqüència TCP no és igual 0x29. No entraré més en detalls, ja que ja està clar que escriure aquestes regles a mà no és molt convenient. A l'article BPF: el bytecode oblidat, hi ha diversos enllaços amb exemples d'ús i generació de regles xt_u32. Vegeu també els enllaços al final d'aquest article.

Des del 2013 mòdul en lloc de mòdul xt_u32 podeu utilitzar un mòdul basat en BPF xt_bpf. Qualsevol que hagi llegit fins aquí ja hauria de tenir clar el principi del seu funcionament: executar el codi de bytes BPF com a regles d'iptables. Podeu crear una regla nova, per exemple, com aquesta:

iptables -A INPUT -m bpf --bytecode <байткод> -j LOG

aquí <байткод> - aquest és el codi en format de sortida de l'assemblador bpf_asm per defecte, per exemple,

$ cat /tmp/test.bpf
ldb [9]
jneq #17, ignore
ret #1
ignore: ret #0

$ bpf_asm /tmp/test.bpf
4,48 0 0 9,21 0 1 17,6 0 0 1,6 0 0 0,

# iptables -A INPUT -m bpf --bytecode "$(bpf_asm /tmp/test.bpf)" -j LOG

En aquest exemple estem filtrant tots els paquets UDP. Context per a un programa BPF en un mòdul xt_bpf, per descomptat, apunta als paquets de dades, en el cas d'iptables, al començament de la capçalera IPv4. Valor de retorn del programa BPF booleàOn false vol dir que el paquet no coincideix.

Està clar que el mòdul xt_bpf admet filtres més complexos que l'exemple anterior. Vegem exemples reals de Cloudfare. Fins fa poc feien servir el mòdul xt_bpf per protegir-se dels atacs DDoS. A l'article Presentació de les eines BPF expliquen com (i per què) generen filtres BPF i publiquen enllaços a un conjunt d'utilitats per crear aquests filtres. Per exemple, utilitzant la utilitat bpfgen podeu crear un programa BPF que coincideixi amb una consulta DNS per a un nom habr.com:

$ ./bpfgen --assembly dns -- habr.com
ldx 4*([0]&0xf)
ld #20
add x
tax

lb_0:
    ld [x + 0]
    jneq #0x04686162, lb_1
    ld [x + 4]
    jneq #0x7203636f, lb_1
    ldh [x + 8]
    jneq #0x6d00, lb_1
    ret #65535

lb_1:
    ret #0

En el programa primer carreguem al registre X adreça d'inici de línia x04habrx03comx00 dins d'un datagrama UDP i després comproveu la sol·licitud: 0x04686162 <-> "x04hab" etcètera

Una mica més tard, Cloudfare va publicar el codi del compilador p0f -> BPF. A l'article Presentació del compilador p0f BPF parlen sobre què és p0f i com convertir signatures p0f a BPF:

$ ./bpfgen p0f -- 4:64:0:0:*,0::ack+:0
39,0 0 0 0,48 0 0 8,37 35 0 64,37 0 34 29,48 0 0 0,
84 0 0 15,21 0 31 5,48 0 0 9,21 0 29 6,40 0 0 6,
...

Actualment ja no s'utilitza Cloudfare xt_bpf, ja que es van traslladar a XDP: una de les opcions per utilitzar la nova versió de BPF, vegeu. L4Drop: mitigacions XDP DDoS.

cls_bpf

L'últim exemple d'ús de BPF clàssic al nucli és el classificador cls_bpf per al subsistema de control de trànsit a Linux, afegit a Linux a finals de 2013 i substituint conceptualment l'antic cls_u32.

Tanmateix, ara no descriurem el treball cls_bpf, ja que des del punt de vista del coneixement sobre BPF clàssic això no ens aportarà res, ja ens hem familiaritzat amb tota la funcionalitat. A més, en articles posteriors que parlen de l'Extended BPF, trobarem aquest classificador més d'una vegada.

Una altra raó per no parlar de l'ús de BPF clàssic c cls_bpf El problema és que, en comparació amb Extended BPF, l'àmbit d'aplicabilitat en aquest cas es redueix radicalment: els programes clàssics no poden canviar el contingut dels paquets i no poden desar l'estat entre trucades.

Així que és hora d'acomiadar-se del clàssic BPF i mirar cap al futur.

Adéu al clàssic BPF

Vam observar com la tecnologia BPF, desenvolupada a principis dels noranta, va viure amb èxit durant un quart de segle i fins al final va trobar noves aplicacions. Tanmateix, de manera similar a la transició de les màquines de pila a RISC, que va servir d'impuls per al desenvolupament del BPF clàssic, a la dècada del 32 hi va haver una transició de les màquines de 64 bits a XNUMX bits i el BPF clàssic va començar a quedar obsolet. A més, les capacitats del BPF clàssic són molt limitades i, a més de l'arquitectura obsoleta, no tenim la capacitat de desar l'estat entre trucades a programes BPF, no hi ha possibilitat d'interacció directa amb l'usuari, no hi ha possibilitat d'interaccionar. amb el nucli, excepte per llegir un nombre limitat de camps d'estructura sk_buff i llançant les funcions d'ajuda més senzilles, no podeu canviar el contingut dels paquets i redirigir-los.

De fet, actualment tot el que queda del clàssic BPF a Linux és la interfície API, i dins del nucli tots els programes clàssics, ja siguin filtres de socket o filtres seccomp, es tradueixen automàticament a un nou format, Extended BPF. (Parlarem de com passa això exactament al proper article.)

La transició a una nova arquitectura va començar el 2013, quan Alexey Starovoitov va proposar un esquema d'actualització de BPF. El 2014 els pegats corresponents va començar a aparèixer al nucli. Pel que entenc, el pla inicial era només optimitzar l'arquitectura i el compilador JIT per executar-se de manera més eficient en màquines de 64 bits, però en canvi aquestes optimitzacions van marcar l'inici d'un nou capítol en el desenvolupament de Linux.

Altres articles d'aquesta sèrie tractaran l'arquitectura i les aplicacions de la nova tecnologia, coneguda inicialment com a BPF intern, després BPF estès, i ara simplement BPF.

Referències

  1. Steven McCanne i Van Jacobson, "El filtre de paquets BSD: una nova arquitectura per a la captura de paquets a nivell d'usuari", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne, "libpcap: una metodologia d'arquitectura i optimització per a la captura de paquets", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. Tutorial de coincidència IPtable U32.
  5. BPF - el bytecode oblidat: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. Presentació de l'eina BPF: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. Una visió general de seccomp: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: Contenidors i seguretat: seccomp
  11. habr: aïllant dimonis amb systemd o "no necessiteu Docker per a això!"
  12. Paul Chaignon, "strace --seccomp-bpf: una mirada sota el capó", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Font: www.habr.com

Afegeix comentari