BPF foar de lytsen, diel nul: klassike BPF

Berkeley Packet Filters (BPF) is in Linux-kerneltechnology dy't al ferskate jierren op 'e foarsiden fan Ingelsktalige tech-publikaasjes stiet. Konferinsjes binne fol mei rapporten oer it gebrûk en ûntwikkeling fan BPF. David Miller, ûnderhâlder fan Linux netwurk subsysteem, neamt syn praat by Linux Plumbers 2018 "Dit petear giet net oer XDP" (XDP is ien gebrûk foar BPF). Brendan Gregg jout petearen rjocht Linux BPF Superpowers. Toke Høiland-Jørgensen laketdat de kearn no in mikrokernel is. Thomas Graf befoarderet it idee dat BPF is javascript foar de kernel.

D'r is noch gjin systematyske beskriuwing fan BPF op Habré, en dêrom sil ik yn in searje artikels besykje oer de skiednis fan 'e technology te praten, de ark foar arsjitektuer en ûntwikkeling te beskriuwen, en de gebieten fan tapassing en praktyk fan it brûken fan BPF te beskriuwen. Dit artikel, nul, yn 'e searje, fertelt de skiednis en arsjitektuer fan klassike BPF, en ûntbleatet ek de geheimen fan har operaasjeprinsipes. tcpdump, seccomp, strace, en safolle mear.

De ûntwikkeling fan BPF wurdt regele troch de Linux-netwurkmienskip, de wichtichste besteande applikaasjes fan BPF binne relatearre oan netwurken en dêrom, mei tastimming @eukariot, Ik neamde de searje "BPF foar de lytsen", ta eare fan 'e grutte searje "Netwurken foar de lytse bern".

In koarte kursus yn 'e skiednis fan BPF (c)

Moderne BPF-technology is in ferbettere en útwreide ferzje fan 'e âlde technology mei deselde namme, no klassike BPF neamd om betizing te foarkommen. In bekend nut is makke basearre op de klassike BPF tcpdump, meganisme seccomp, lykas minder bekende modules xt_bpf foar iptables en klassifikaasje cls_bpf. Yn moderne Linux wurde klassike BPF-programma's automatysk oerset yn 'e nije foarm, lykwols, út in brûker eachpunt, is de API op syn plak bleaun en nije gebrûk foar klassike BPF, lykas wy sille sjen yn dit artikel, wurde noch fûn. Om dizze reden, en ek om't nei de skiednis fan 'e ûntwikkeling fan klassike BPF yn Linux, it dúdliker wurde sil hoe en wêrom't it evoluearre yn syn moderne foarm, besleat ik om te begjinnen mei in artikel oer klassike BPF.

Oan 'e ein fan' e jierren tachtich fan 'e foarige ieu waarden yngenieurs fan' e ferneamde Lawrence Berkeley Laboratory ynteressearre yn 'e fraach hoe't jo netwurkpakketten goed kinne filterje op hardware dy't modern wie yn' e lette jierren tachtich fan 'e foarige ieu. It basisidee fan filterjen, oarspronklik ymplementearre yn CSPF (CMU/Stanford Packet Filter) technology, wie om ûnnedige pakketten sa betiid mooglik te filterjen, d.w.s. yn kernelromte, om't dit it kopiearjen fan ûnnedige gegevens yn brûkersromte foarkomt. Om runtime feiligens te leverjen foar it útfieren fan brûkerskoade yn kernelromte, waard in sânboxed firtuele masine brûkt.

De firtuele masines foar besteande filters waarden lykwols ûntworpen om te rinnen op stapelbasearre masines en rûnen net sa effisjint op nijere RISC-masines. As gefolch, troch de ynspanningen fan yngenieurs fan Berkeley Labs, waard in nije BPF (Berkeley Packet Filters) technology ûntwikkele, wêrfan de firtuele masine-arsjitektuer is ûntworpen basearre op de Motorola 6502-prosessor - it wurkhynder fan sokke bekende produkten as Apple II of NES. De nije firtuele masine fergrutte filterprestaasjes tsientallen kearen yn ferliking mei besteande oplossingen.

BPF masine arsjitektuer

Wy sille op in wurkjende manier yn 'e kunde komme mei arsjitektuer, foarbylden analysearje. Litte wy lykwols om te begjinnen sizze dat de masine twa 32-bit registers hie tagonklik foar de brûker, in accumulator A en yndeks register X, 64 bytes ûnthâld (16 wurden), beskikber foar skriuwen en folgjende lêzen, en in lyts systeem fan kommando's foar it wurkjen mei dizze objekten. Sprongynstruksjes foar it útfieren fan betingsten útdrukkingen wiene ek beskikber yn 'e programma's, mar om de yntiidske foltôging fan it programma te garandearjen, koenen sprongen allinich foarút makke wurde, d.w.s. it wie benammen ferbean om loops te meitsjen.

It algemiene skema foar it starten fan 'e masine is as folget. De brûker makket in programma foar de BPF-arsjitektuer en, mei help fan guon kernel meganisme (lykas in systeem oprop), loads en ferbynt it programma oan oan guon nei de evenemintgenerator yn 'e kearn (in evenemint is bygelyks de komst fan it folgjende pakket op' e netwurkkaart). As in evenemint bart, rint de kernel it programma (bygelyks yn in tolk), en it masineûnthâld komt oerien mei oan guon kernel ûnthâld regio (bygelyks gegevens fan in ynkommende pakket).

It boppesteande sil genôch wêze foar ús om nei foarbylden te begjinnen: wy sille as nedich yn 'e kunde komme mei it systeem en it kommando-formaat. As jo ​​​​it kommandosysteem fan in firtuele masine direkt wolle studearje en leare oer al har mooglikheden, dan kinne jo it orizjinele artikel lêze It BSD-pakketfilter en/of de earste helte fan it bestân Documentation/netwurk/filter.txt út de kernel dokumintaasje. Derneist kinne jo de presintaasje bestudearje libpcap: In arsjitektuer en optimisaasjemetoadyk foar pakket capture, wêryn McCanne, ien fan 'e skriuwers fan BPF, fertelt oer de skiednis fan 'e skepping libpcap.

Wy geane no troch om alle wichtige foarbylden te beskôgjen fan it brûken fan klassike BPF op Linux: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.

tcpdump

De ûntwikkeling fan BPF waard útfierd parallel mei de ûntwikkeling fan it frontend foar pakketfiltering - in bekend nut tcpdump. En, om't dit it âldste en meast ferneamde foarbyld is fan it brûken fan klassike BPF, beskikber op in protte bestjoeringssystemen, sille wy ús stúdzje fan 'e technology dêrmei begjinne.

(Ik rûn alle foarbylden yn dit artikel oer Linux 5.6.0-rc6. De útfier fan guon kommando's is bewurke foar bettere lêsberens.)

Foarbyld: observearjen fan IPv6-pakketten

Litte wy ús foarstelle dat wy alle IPv6-pakketten op in ynterface wolle sjen eth0. Om dit te dwaan kinne wy ​​it programma útfiere tcpdump mei in ienfâldich filter ip6:

$ sudo tcpdump -i eth0 ip6

sa tcpdump kompilearret it filter ip6 yn 'e BPF-arsjitektuerbytekoade en stjoer it nei de kernel (sjoch details yn' e seksje Tcpdump: laden). It laden filter sil wurde útfierd foar elk pakket dat troch de ynterface giet eth0. As it filter jout in net-nul wearde n, dan oant n bytes fan it pakket wurde kopiearre nei brûkersromte en wy sille it sjen yn 'e útfier tcpdump.

BPF foar de lytsen, diel nul: klassike BPF

It docht bliken dat wy maklik fine kinne hokker bytekoade nei de kernel stjoerd is tcpdump mei help fan de tcpdump, as wy it útfiere mei de opsje -d:

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

Op rigel nul rinne wy ​​it kommando ldh [12], wat stiet foar "load into register A in heal wurd (16 bits) leit op adres 12 "en de ienige fraach is wat soarte fan ûnthâld binne wy ​​oanpakke? It antwurd is dat by x begjint (x+1)e byte fan it analysearre netwurkpakket. Wy lêze pakketten fan 'e Ethernet-ynterface eth0en dit betsjutdat it pakket der sa útsjocht (foar ienfâld geane wy ​​der fan út dat d'r gjin VLAN-tags yn it pakket binne):

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

Dus nei it útfieren fan it kommando ldh [12] yn it register A der sil in fjild wêze Ether Type - it type pakket dat wurdt oerbrocht yn dit Ethernet-frame. Op rigel 1 fergelykje wy de ynhâld fan it register A (pakkettype) c 0x86dden dit en der is It type wêryn wy ynteressearre binne is IPv6. Op rigel 1, neist it fergelikingskommando, binne d'r noch twa kolommen - jt 2 и jf 3 - markearrings wêrnei't jo moatte gean as de fergeliking suksesfol is (A == 0x86dd) en net slagge. Dus, yn in suksesfolle saak (IPv6) geane wy ​​nei rigel 2, en yn in mislearre gefal - nei rigel 3. Op rigel 3 einiget it programma mei koade 0 (kopiearje it pakket net), op rigel 2 einiget it programma mei koade 262144 (kopiearje my maksimaal 256 kilobytes pakket).

In mear komplisearre foarbyld: wy sjogge nei TCP-pakketten troch bestimmingspoarte

Litte wy sjen hoe't in filter derút sjocht dat alle TCP-pakketten kopiearret mei bestimmingspoarte 666. Wy sille de IPv4-saak beskôgje, om't de IPv6-saak ienfâldiger is. Nei it bestudearjen fan dit foarbyld kinne jo it IPv6-filter sels ferkenne as in oefening (ip6 and tcp dst port 666) en in filter foar it algemiene gefal (tcp dst port 666). Dat, it filter wêryn wy ynteressearre binne, sjocht der sa út:

$ 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

Wy witte al wat rigels 0 en 1 dogge. Op rigel 2 hawwe wy al kontrolearre dat dit in IPv4-pakket is (Ether Type = 0x800) en lade it yn it register A 24e byte fan it pakket. Us pakket liket

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

wat betsjut dat wy laden yn it register A it Protokolfjild fan 'e IP-koptekst, wat logysk is, om't wy allinich TCP-pakketten kopiearje wolle. Wy ferlykje Protokol mei 0x6 (IPPROTO_TCP) op rigel 3.

Op de rigels 4 en 5 laden wy de healwurden op adres 20 en brûke it kommando jset kontrolearje oft ien fan de trije is ynsteld flaggen - it dragen fan it útjûn masker jset de trije meast wichtige bits wurde wiske. Twa fan 'e trije bits fertelle ús oft it pakket diel is fan in fragmintearre IP-pakket, en as dat sa is, oft it it lêste fragmint is. It tredde bit is reservearre en moat nul wêze. Wy wolle net of ûnfolsleine of brutsen pakketten kontrolearje, dus kontrolearje wy alle trije bits.

Line 6 is de meast nijsgjirrige yn dizze list. Útdrukking ldxb 4*([14]&0xf) betsjut dat wy laden yn it register X de minst signifikante fjouwer bits fan de fyftjinde byte fan it pakket fermannichfâldige mei 4. De minst signifikante fjouwer bits fan de fyftjinde byte is it fjild Ynternet Header Lengte IPv4-koptekst, dy't de lingte fan 'e koptekst yn wurden bewarret, dus jo moatte dan fermannichfâldigje mei 4. Nijsgjirrich is de útdrukking 4*([14]&0xf) is in oantsjutting foar in spesjaal adressearskema dat allinnich yn dizze foarm en allinnich foar in register brûkt wurde kin X, d.w.s. wy kinne ek net sizze ldb 4*([14]&0xf) gjin ldxb 5*([14]&0xf) (wy kinne allinich in oare offset oantsjutte, bygelyks, ldxb 4*([16]&0xf)). It is dúdlik dat dit adresskema waard tafoege oan BPF krekt om te ûntfangen X (yndeks register) IPv4 header lingte.

Dus op rigel 7 besykje wy in heal wurd te laden by (X+16). Unthâld dat 14 bytes wurde beset troch de Ethernet header, en X befettet de lingte fan 'e IPv4-header, wy begripe dat yn A TCP-bestimmingspoarte wurdt laden:

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

Uteinlik fergelykje wy op rigel 8 de bestimmingspoarte mei de winske wearde en op rigels 9 of 10 jouwe wy it resultaat werom - of it pakket te kopiearjen of net.

Tcpdump: laden

Yn 'e foargeande foarbylden hawwe wy spesifyk net yn detail oer hoe't wy BPF-bytekoade yn 'e kernel lade foar pakketfiltering. Oer 't algemien, tcpdump porteare nei in protte systemen en foar wurkjen mei filters tcpdump brûkt de bibleteek libpcap. Koartsein, om in filter te pleatsen op in ynterface mei libpcap, moatte jo it folgjende dwaan:

Om te sjen hoe't de funksje pcap_setfilter ymplementearre yn Linux, wy brûke strace (guon rigels binne fuortsmiten):

$ 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
...

Op de earste twa rigels fan útfier meitsje wy rau socket om alle Ethernet-frames te lêzen en te binen oan de ynterface eth0. Of ús earste foarbyld wy witte dat it filter ip sil bestean út fjouwer BPF ynstruksjes, en op 'e tredde rigel wy sjogge hoe't mei help fan de opsje SO_ATTACH_FILTER systeem oprop setsockopt wy lade en ferbine in filter fan lingte 4. Dit is ús filter.

It is de muoite wurdich op te merken dat yn klassike BPF it laden en ferbinen fan in filter altyd foarkomt as in atoomoperaasje, en yn 'e nije ferzje fan BPF wurdt it laden fan it programma en it ferbinen oan 'e evenemintgenerator yn 'e tiid skieden.

Ferburgen wierheid

In wat folsleine ferzje fan 'e útfier sjocht der sa út:

$ 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
...

Lykas hjirboppe neamd, laden en ferbine wy ​​ús filter oan 'e socket op line 5, mar wat bart der op rigels 3 en 4? It docht bliken dat dit libpcap soarget foar ús - sadat de útfier fan ús filter gjin pakketten omfettet dy't it net befredigje, de bibleteek ferbynt dummy filter ret #0 (drop alle pakketten), skeakelet de socket nei net-blokkearjende modus en besiket alle pakketten ôf te lûken dy't kinne bliuwe fan eardere filters.

Yn totaal, om pakketten op Linux te filterjen mei klassike BPF, moatte jo in filter hawwe yn 'e foarm fan in struktuer lykas struct sock_fprog en in iepen socket, wêrnei't it filter kin wurde hechte oan 'e socket mei in systeemoprop setsockopt.

Opfallend is dat it filter kin wurde hechte oan elke socket, net allinich rau. Hjir foarbyld in programma dat alles útsein de earste twa bytes fan alle ynkommende UDP-datagrammen ôfsnijt. (Ik haw opmerkingen tafoege yn 'e koade om it artikel net te rommeljen.)

Mear details oer gebrûk setsockopt foar it ferbinen fan filters, sjoch socket (7), mar oer it skriuwen fan jo eigen filters lykas struct sock_fprog sûnder help tcpdump wy sille prate yn 'e seksje Programming BPF mei har eigen hannen.

Klassike BPF en de XNUMXe ieu

BPF waard opnommen yn Linux yn 1997 en is in lange tiid in wurkhynder bleaun libpcap sûnder spesjale feroarings (linux-spesifike feroarings, fansels, it wie, mar se hawwe it globale byld net feroare). De earste serieuze tekens dat BPF soe evoluearje kamen yn 2011, doe't Eric Dumazet foarstelde patch, dy't Just In Time Compiler tafoegje oan 'e kernel - in oersetter foar it konvertearjen fan BPF-bytekoade nei native x86_64 de koade.

JIT-kompiler wie de earste yn 'e keten fan feroaringen: yn 2012 ferskynde mooglikheid om te skriuwen filters foar secomp, mei help fan BPF, yn jannewaris 2013 wie der tafoege module xt_bpf, wêrmei jo regels te skriuwen foar iptables mei help fan BPF, en yn oktober 2013 wie tafoege ek in module cls_bpf, wêrmei jo ferkearsklassifisearrings kinne skriuwe mei BPF.

Wy sille al dizze foarbylden gau yn mear detail besjen, mar earst sil it nuttich wêze foar ús om te learen hoe't jo willekeurige programma's foar BPF kinne skriuwe en kompilearje, om't de mooglikheden oanbean troch de bibleteek libpcap beheind (ienfâldich foarbyld: filter oanmakke libpcap kin mar twa wearden weromjaan - 0 of 0x40000) of oer it algemien, lykas yn it gefal fan seccomp, binne net fan tapassing.

Programming BPF mei har eigen hannen

Litte wy yn 'e kunde komme mei it binêre formaat fan BPF-ynstruksjes, it is heul ienfâldich:

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

Elke ynstruksje beslacht 64 bits, wêryn de earste 16 bits de ynstruksjekoade binne, dan binne d'r twa acht-bit ynspringen, jt и jf, en 32 bits foar it argumint K, wêrfan it doel ferskilt fan kommando ta kommando. Bygelyks, it kommando ret, dy't it programma beëiniget, hat de koade 6, en it weromkommen wearde wurdt nommen út de konstante K. Yn C wurdt in inkele BPF-ynstruksje fertsjintwurdige as in struktuer

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

en it hiele programma is yn 'e foarm fan in struktuer

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

Sa kinne wy ​​al programma's skriuwe (wy kenne bygelyks de ynstruksjekoades fan [1]). Dit is hoe't it filter derút sil sjen ip6 fan ús earste foarbyld:

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,
};

programma prog wy kinne juridysk brûke yn in oprop

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

Programma's skriuwe yn 'e foarm fan masinekoades is net heul handich, mar soms is it nedich (bygelyks foar debuggen, it meitsjen fan ienheidstests, it skriuwen fan artikels oer Habré, ensfh.). Foar gemak, yn 'e triem <linux/filter.h> helper makros wurde definiearre - itselde foarbyld as hjirboppe koe wurde herskreaun as

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),
}

Dizze opsje is lykwols net heul handich. Dit is wat de Linux kernel-programmeurs redeneare, en dus yn 'e map tools/bpf kernels kinne jo in assembler en debugger fine foar wurkjen mei klassike BPF.

Assembly taal is hiel ferlykber mei debug útfier tcpdump, mar boppedat kinne wy ​​oantsjutte symboalyske labels. Hjir is bygelyks in programma dat alle pakketten falt útsein TCP/IPv4:

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

Standert genereart de assembler koade yn it formaat <количество инструкций>,<code1> <jt1> <jf1> <k1>,..., foar ús foarbyld mei TCP sil it wêze

$ 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,

Foar it gemak fan C-programmeurs kin in oar útfierformaat brûkt wurde:

$ 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 },

Dizze tekst kin kopiearre wurde yn de typestruktuerdefinysje struct sock_filter, lykas wy diene oan it begjin fan dizze paragraaf.

Linux en netsniff-ng tafoegings

Neist standert BPF, Linux en tools/bpf/bpf_asm stipe en net-standert set. Yn prinsipe wurde ynstruksjes brûkt om tagong te krijen ta de fjilden fan in struktuer struct sk_buff, dy't in netwurkpakket yn 'e kernel beskriuwt. Der binne lykwols ek oare soarten helpynstruksjes, bygelyks ldw cpu sil lade yn it register A resultaat fan it útfieren fan in kernelfunksje raw_smp_processor_id(). (Yn de nije ferzje fan BPF binne dizze net-standert tafoegings útwreide om programma's te foarsjen mei in set kernelhelpers foar tagong ta ûnthâld, struktueren en eveneminten.) Hjir is in nijsgjirrich foarbyld fan in filter wêryn wy allinich de pakketkoppen yn brûkersromte mei de tafoeging poff, lading offset:

ld poff
ret a

BPF-útwreidingen kinne net brûkt wurde yn tcpdump, mar dit is in goede reden om yn 'e kunde te kommen mei it nutspakket netsniff-ng, dy't ûnder oare in avansearre programma befettet netsniff-ng, dy't, neist it filterjen mei BPF, ek in effektive ferkearsgenerator befettet, en mear avansearre as tools/bpf/bpf_asm, in BPF assembler neamd bpfc. It pakket befettet frij detaillearre dokumintaasje, sjoch ek de keppelings oan 'e ein fan it artikel.

secomp

Dat, wy witte al hoe't BPF-programma's fan willekeurige kompleksiteit skriuwe en binne ree om te sjen nei nije foarbylden, wêrfan de earste de seccomp-technology is, dy't, mei help fan BPF-filters, de set en set fan arguminten foar systeemoprop kinne beheare. in opjûne proses en syn neiteam.

De earste ferzje fan seccomp waard tafoege oan 'e kernel yn 2005 en wie net heul populêr, om't it mar ien opsje levere - om de set fan systeemoproppen beskikber foar in proses te beheinen ta it folgjende: read, write, exit и sigreturn, en it proses dat skeind de regels waard fermoarde brûkend SIGKILL. Yn 2012 hat seccomp lykwols de mooglikheid tafoege om BPF-filters te brûken, wêrtroch jo in set tastiene systeemoproppen kinne definiearje en sels kontrôles útfiere op har arguminten. (Ynteressant wie Chrome ien fan 'e earste brûkers fan dizze funksjonaliteit, en de Chrome-minsken ûntwikkelje op it stuit in KRSI-meganisme basearre op in nije ferzje fan BPF en it tastean fan oanpassing fan Linux Security Modules.) Links nei ekstra dokumintaasje kinne fûn wurde oan 'e ein fan it artikel.

Tink derom dat d'r al artikels west hawwe op 'e hub oer it brûken fan seccomp, miskien wol immen se lêze foardat (of ynstee fan) de folgjende ûnderseksjes lêze. Yn it artikel Containers en feiligens: secomp jout foarbylden fan it brûken fan seccomp, sawol de ferzje fan 2007 as de ferzje mei BPF (filters wurde generearre mei libseccomp), praat oer de ferbining fan seccomp mei Docker, en leveret ek in protte nuttige keppelings. Yn it artikel Daemons isolearje mei systemd of "jo hawwe Docker net nedich foar dit!" It behannelt, yn it bysûnder, hoe't jo blacklists of whitelists tafoegje kinne fan systeemoproppen foar daemons dy't systemd rinne.

Folgjende sille wy sjen hoe't jo filters skriuwe en laden foar seccomp yn bleate C en mei help fan de bibleteek libseccomp en wat binne de foar- en neidielen fan elke opsje, en as lêste, litte wy sjen hoe't seccomp wurdt brûkt troch it programma strace.

Skriuwen en laden filters foar seccomp

Wy witte al hoe't jo BPF-programma's skriuwe, dus litte wy earst sjen nei de seccomp-programmearring-ynterface. Jo kinne in filter ynstelle op it proses nivo, en alle bern prosessen sille ervje de beheinings. Dit wurdt dien mei in systeemoprop seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

wêr &filter - dit is in oanwizer nei in struktuer dy't ús al bekend is struct sock_fprog, d.w.s. BPF programma.

Hoe ferskille programma's foar seccomp fan programma's foar sockets? Oerdroegen kontekst. Yn it gefal fan sockets krigen wy in ûnthâldgebiet mei it pakket, en yn it gefal fan seccomp krigen wy in struktuer lykas

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

it is nr is it nûmer fan 'e systeemoprop om te lansearjen, arch - hjoeddeistige arsjitektuer (mear oer dit hjirûnder), args - oant seis systeem call arguminten, en instruction_pointer is in oanwizer nei de ynstruksje foar brûkersromte dy't it systeem oprop makke. Sa, bygelyks, te laden it systeem oprop nûmer yn it register A wy moatte sizze

ldw [0]

D'r binne oare funksjes foar seccomp-programma's, bygelyks de kontekst kin allinich tagonklik wurde troch 32-bit ôfstimming en jo kinne gjin heal wurd of in byte lade - as jo besykje in filter te laden ldh [0] systeem oprop seccomp sil weromkomme EINVAL. De funksje kontrolearret de laden filters seccomp_check_filter() kearnen. (Grappich ding is, yn 'e orizjinele commit dy't de seccomp-funksjonaliteit tafoege, fergeaten se tastimming ta te foegjen om de ynstruksje te brûken foar dizze funksje mod (ôfdieling rest) en is no net beskikber foar seccomp BPF-programma's, sûnt syn tafoeging sil brekke ABI.)

Yn prinsipe witte wy al alles om seccomp-programma's te skriuwen en te lêzen. Meastal wurdt de programmalogika regele as in wite of swarte list fan systeemoproppen, bygelyks it programma

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

kontrolearret in swarte list fan fjouwer systeem oproppen nûmere 304, 176, 239, 279. Wat binne dizze systeem calls? Wy kinne net wis sizze, om't wy net witte foar hokker arsjitektuer it programma is skreaun. Dêrom, de skriuwers fan seccomp oanbod start alle programma's mei in arsjitektuerkontrôle (de hjoeddeistige arsjitektuer wurdt yn 'e kontekst as in fjild oanjûn arch struktueren struct seccomp_data). Mei de arsjitektuer kontrolearre, soe it begjin fan it foarbyld der útsjen:

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

en dan soene ús systeemopropnûmers bepaalde wearden krije.

Wy skriuwe en laden filters foar seccomp brûkend libseccomp

It skriuwen fan filters yn native koade as BPF-assemblage kinne jo folsleine kontrôle hawwe oer it resultaat, mar tagelyk is it soms de foarkar om draachbere en / of lêsbere koade te hawwen. De bibleteek sil ús dêrby helpe libsecomp, dy't in standert ynterface leveret foar it skriuwen fan swarte of wyt filters.

Litte wy bygelyks in programma skriuwe dat in binêr bestân rint fan 'e brûker syn kar, nei't wy earder in swarte list fan systeemoproppen ynstalleare hawwe it boppesteande artikel (it programma is ferienfâldige foar gruttere lêsberens, de folsleine ferzje is te finen hjir):

#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]);
}

Earst definiearje wy in array sys_numbers fan 40+ systeem call nûmers te blokkearjen. Dan, inisjalisearje de kontekst ctx en fertel de bibleteek wat wy tastean wolle (SCMP_ACT_ALLOW) standert alle systeemoproppen (it is makliker om swartelisten te bouwen). Dan foegje wy ien foar ien alle systeemoproppen ta fan 'e swarte list. As antwurd op in systeem oprop út de list, wy freegje SCMP_ACT_TRAP, yn dit gefal sil seccomp in sinjaal stjoere nei it proses SIGSYS mei in beskriuwing fan hokker systeem call skeind de regels. Uteinlik laden wy it programma yn 'e kernel mei seccomp_load, dat sil kompilearje it programma en heakje it oan it proses mei help fan in systeem oprop seccomp(2).

Foar suksesfolle kompilaasje moat it programma keppele wurde oan de bibleteek libseccompbygelyks:

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

Foarbyld fan in suksesfolle lansearring:

$ ./seccomp_lib echo ok
ok

Foarbyld fan in blokkearre systeemoprop:

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

Wy brûke stracefoar details:

$ 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

hoe kinne wy ​​witte dat it programma waard beëinige troch it brûken fan in yllegale systeem oprop mount(2).

Dat, wy hawwe in filter skreaun mei de bibleteek libseccomp, fitting net-triviale koade yn fjouwer rigels. Yn it foarbyld hjirboppe, as d'r in grut oantal systeemoproppen binne, kin de útfieringstiid merkber fermindere wurde, om't de kontrôle gewoan in list fan fergelikingen is. Foar optimalisaasje hie libsecomp koartlyn patch ynbegrepen, dy't stipe tafoeget foar it filterattribút SCMP_FLTATR_CTL_OPTIMIZE. It ynstellen fan dit attribút op 2 sil it filter konvertearje yn in binêre sykprogramma.

As jo ​​​​wolle sjen hoe't binêre sykfilters wurkje, sjoch dan ris nei ienfâldich skript, dy't sokke programma's genereart yn BPF assembler troch systeemopropnûmers te skiljen, bygelyks:

$ 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

Jo sille neat folle rapper kinne skriuwe, om't BPF-programma's gjin ynspringsprongen kinne útfiere (wy kinne bygelyks net dwaan, jmp A of jmp [label+X]) en dêrom binne alle transysjes statysk.

secomp en strace

Elkenien wit it nut strace is in ûnmisber ark foar it bestudearjen fan it gedrach fan prosessen op Linux. In protte hawwe lykwols ek heard oer prestaasjes problemen by it brûken fan dit hulpprogramma. It feit is dat strace ymplemintearre mei help ptrace(2), en yn dit meganisme kinne wy ​​net oantsjutte op hokker set fan systeemoproppen wy moatte stopje it proses, dat wol sizze, bygelyks, kommando's

$ 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

wurde ferwurke yn likernôch deselde tiid, hoewol't yn it twadde gefal wolle wy trace mar ien systeem oprop.

Nije opsje --seccomp-bpf, tafoege oan strace ferzje 5.3, lit jo it proses in protte kearen fersnelle en de opstarttiid ûnder it spoar fan ien systeemoprop is al te fergelykjen mei de tiid fan in gewoane opstart:

$ 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

(Hjir is der fansels in lichte mislieding yn dat wy de haadsysteemoprop fan dit kommando net traceerje. As wy bgl. newfsstat, dan strace soe like hurd remme as sûnder --seccomp-bpf.)

Hoe wurket dizze opsje? Sûnder har strace ferbynt mei it proses en begjint it te brûken PTRACE_SYSCALL. Wannear't in beheard proses problemen (elk) systeem oprop, kontrôle wurdt oerdroegen oan strace, dy't sjocht nei de arguminten fan it systeem oprop en rint it mei PTRACE_SYSCALL. Nei in skoft foltôget it proses de systeemoprop en by it ôfsluten wurdt de kontrôle wer oerdroegen strace, dy't sjocht nei de weromkommende wearden en begjint it proses mei PTRACE_SYSCALL, ensafuorthinne.

BPF foar de lytsen, diel nul: klassike BPF

Mei seccomp kin dit proses lykwols krekt wurde optimalisearre lykas wy wolle. Nammentlik, as wy wolle sjen allinne op it systeem oprop X, dan kinne wy ​​skriuwe in BPF filter dat foar X jout in wearde werom SECCOMP_RET_TRACE, en foar petearen dy't ús net fan belang binne - SECCOMP_RET_ALLOW:

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

Yn dit gefal strace ynearsten begjint it proses as PTRACE_CONT, ús filter wurdt ferwurke foar elke systeemoprop, as de systeemoprop net is X, dan giet it proses troch te rinnen, mar as dit X, dan sil secomp kontrôle oerdrage stracedy't sil sjen nei de arguminten en begjinne it proses lykas PTRACE_SYSCALL (sûnt seccomp net de mooglikheid hat om in programma út te fieren by ôfsluting fan in systeemoprop). As de systeemoprop weromkomt, strace sil it proses opnij starte mei PTRACE_CONT en sil wachtsje op nije berjochten fan seccomp.

BPF foar de lytsen, diel nul: klassike BPF

By it brûken fan de opsje --seccomp-bpf der binne twa beheinings. As earste sil it net mooglik wêze om mei te dwaan oan in al besteande proses (opsje -p programma strace), om't dit net wurdt stipe troch secomp. Twadder is d'r gjin mooglikheid net sjoch bern prosessen, sûnt seccomp filters wurde erfde troch alle bern prosessen sûnder de mooglikheid om te skeakeljen dit.

In bytsje mear detail oer hoe krekt strace wurkje mei seccomp kin fûn wurde út resint rapport. Foar ús is it meast nijsgjirrige feit dat de klassike BPF fertsjintwurdige troch seccomp hjoed noch brûkt wurdt.

xt_bpf

Litte wy no weromgean nei de wrâld fan netwurken.

Eftergrûn: lang lyn, yn 2007, wie de kearn tafoege module xt_u32 foar netfilter. It waard skreaun troch analogy mei in noch âldere ferkearsklassifikaasje cls_u32 en tastean jo te skriuwen willekeurige binêre regels foar iptables mei help fan de folgjende ienfâldige operaasjes: laden 32 bits út in pakket en útfiere in set fan rekkenjen operaasjes op harren. Bygelyks,

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

Laadt de 32 bits fan 'e IP-header, begjinnend by padding 6, en past in masker op har ta 0xFF (nim de lege byte). Dit fjild protocol IP-koptekst en wy fergelykje it mei 1 (ICMP). Jo kinne kombinearje protte kontrôles yn ien regel, En jo kinne ek útfiere de operator @ - ferpleatse X bytes nei rjochts. Bygelyks de regel

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

kontrolearret oft TCP Sequence Number is net gelyk 0x29. Ik gean net fierder op details, om't it al dúdlik is dat it skriuwen fan sokke regels mei de hân net heul handich is. Yn it artikel BPF - de fergetten bytekoade, der binne ferskate keppelings mei foarbylden fan gebrûk en regel generaasje foar xt_u32. Sjoch ek de keppelings oan 'e ein fan dit artikel.

Sûnt 2013 module ynstee fan module xt_u32 Jo kinne in BPF basearre module brûke xt_bpf. Elkenien dy't sa fier lêzen hat, moat al dúdlik wêze oer it prinsipe fan syn wurking: BPF-bytekoade útfiere as iptables-regels. Jo kinne in nije regel oanmeitsje, bygelyks sa:

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

hjir <байткод> - dit is de koade yn assembler-útfierformaat bpf_asm standert, bygelyks,

$ 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

Yn dit foarbyld filterje wy alle UDP-pakketten. Kontekst foar in BPF-programma yn in module xt_bpf, fansels, wiist op de pakketgegevens, yn it gefal fan iptables, nei it begjin fan 'e IPv4-header. Weromsette wearde út BPF programma booleanwêr false betsjut dat it pakket net oerienkomt.

It is dúdlik dat de module xt_bpf stipet kompleksere filters dan it foarbyld hjirboppe. Litte wy nei echte foarbylden fan Cloudfare sjen. Oant koartlyn brûkten se de module xt_bpf om te beskermjen tsjin DDoS-oanfallen. Yn it artikel Yntroduksje fan de BPF Tools se ferklearje hoe (en wêrom) se generearje BPF filters en publisearje keppelings nei in set fan nutsfoarsjennings foar it meitsjen fan sokke filters. Bygelyks, mei help fan it nut bpfgen jo kinne in BPF-programma meitsje dat oerienkomt mei in DNS-fraach foar in namme 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

Yn it programma laden wy earst yn it register X begjin fan line adres x04habrx03comx00 binnen in UDP-datagram en kontrolearje dan it fersyk: 0x04686162 <-> "x04hab" en sa fierder.

In bytsje letter publisearre Cloudfare de p0f -> BPF-kompilerkoade. Yn it artikel Yntroduksje fan de p0f BPF-kompiler se prate oer wat p0f is en hoe't jo p0f-hantekeningen kinne konvertearje nei 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,
...

Op it stuit net mear brûke Cloudfare xt_bpf, sûnt se ferhuze nei XDP - ien fan 'e opsjes foar it brûken fan de nije ferzje fan BPF, sjoch. L4Drop: XDP DDoS Mitigations.

cls_bpf

It lêste foarbyld fan it brûken fan klassike BPF yn 'e kernel is de klassifikaasje cls_bpf foar it ferkearskontrôlesubsysteem yn Linux, tafoege oan Linux oan 'e ein fan 2013 en konseptueel it âlde ferfangen cls_u32.

Wy sille it wurk no lykwols net beskriuwe cls_bpf, om't út it eachpunt fan kennis oer klassike BPF dit ús neat sil jaan - wy binne al bekend wurden mei alle funksjonaliteit. Derneist, yn folgjende artikels oer Extended BPF, sille wy dizze klassifikaasje mear as ien kear moetsje.

In oare reden om net te praten oer it brûken fan klassike BPF c cls_bpf It probleem is dat, yn ferliking mei Extended BPF, it berik fan tapasberens yn dit gefal is radikaal beheind: klassike programma's kinne de ynhâld fan pakketten net feroarje en kinne gjin steat bewarje tusken petearen.

Dat it is tiid om ôfskied te nimmen fan klassike BPF en nei de takomst te sjen.

Ofskied fan klassike BPF

Wy seagen nei hoe't BPF-technology, ûntwikkele yn 'e iere jierren njoggentich, mei súkses libbe foar in fjirde ieu en oant it ein nije applikaasjes fûn. Lykwols, fergelykber mei de oergong fan stack masines nei RISC, dy't tsjinne as ympuls foar de ûntwikkeling fan klassike BPF, yn 'e 32s wie der in oergong fan 64-bit nei XNUMX-bit masines en klassike BPF begûn te wurden ferâldere. Derneist binne de mooglikheden fan klassike BPF heul beheind, en neist de ferâldere arsjitektuer - wy hawwe net de mooglikheid om steat te bewarjen tusken oproppen nei BPF-programma's, d'r is gjin mooglikheid fan direkte brûkersynteraksje, d'r is gjin mooglikheid fan ynteraksje mei de kearn, útsein foar it lêzen fan in beheind oantal struktuerfjilden sk_buff en it starten fan de ienfâldichste helpfunksjes, kinne jo de ynhâld fan pakketten net feroarje en omliede.

Yn feite is op it stuit alles wat oerbliuwt fan 'e klassike BPF yn Linux de API-ynterface, en binnen de kernel wurde alle klassike programma's, of it no socketfilters as seccomp-filters binne, automatysk oerset yn in nij formaat, Extended BPF. (Wy sille prate oer krekt hoe't dit bart yn it folgjende artikel.)

De oergong nei in nije arsjitektuer begûn yn 2013, doe't Alexey Starovoitov foarstelde BPF update skema. Yn 2014 de oerienkommende patches begûn te ferskinen yn de kearn. Foar safier't ik begryp, wie it earste plan allinich om de arsjitektuer en JIT-kompiler te optimalisearjen om effisjinter te rinnen op 64-bit masines, mar ynstee markearren dizze optimisaasjes it begjin fan in nij haadstik yn Linux-ûntwikkeling.

Fierdere artikels yn dizze searje sille de arsjitektuer en tapassingen fan 'e nije technology dekke, yn earste ynstânsje bekend as ynterne BPF, doe útwreide BPF, en no gewoan BPF.

referinsjes

  1. Steven McCanne en Van Jacobson, "The BSD Packet Filter: A New Architecture for User-level Packet Capture", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne, "libpcap: An Architecture and Optimization Methodology for Packet Capture", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. IPtable U32 Match Tutorial.
  5. BPF - de fergetten bytekoade: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. Yntroduksje fan it BPF-ark: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. In twadde oersjoch: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: Containers en feiligens: secomp
  11. habr: Daemons isolearje mei systemd of "jo hawwe Docker net nedich!"
  12. Paul Chaignon, "strace --seccomp-bpf: a look under the hood", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Boarne: www.habr.com

Add a comment