BPF kõige väiksematele, osa null: klassikaline BPF

Berkeley Packet Filters (BPF) on Linuxi tuumatehnoloogia, mis on olnud ingliskeelsete tehnikaväljaannete esikülgedel juba mitu aastat. Konverentsid on täidetud aruannetega BPF-i kasutamise ja arendamise kohta. David Miller, Linuxi võrgu alamsüsteemi hooldaja, nimetab oma kõnet Linux Plumbers 2018-s "See jutt ei puuduta XDP-d" (XDP on üks BPF-i kasutusjuhtum). Brendan Gregg peab kõnesid pealkirjaga Linuxi BPF supervõimed. Toke Høiland-Jørgensen naerabet kernel on nüüd mikrotuum. Thomas Graf propageerib seda ideed BPF on javascript kerneli jaoks.

BPF-i süstemaatilist kirjeldust Habré kohta ikka veel ei ole ja seetõttu püüan artiklite sarjas rääkida tehnoloogia ajaloost, kirjeldada arhitektuuri ja arendustööriistu ning visandada BPF-i kasutus- ja praktikavaldkondi. See artikkel, seeria null, räägib klassikalise BPF-i ajaloost ja arhitektuurist ning paljastab ka selle tööpõhimõtete saladused. tcpdump, seccomp, strace, ja palju muud.

BPF-i arendamist kontrollib Linuxi võrgukogukond, peamised olemasolevad BPF-i rakendused on seotud võrkudega ja seetõttu loal @eukariot, panin sarja suurepäraste sarjade auks nimeks “BPF for the littlees”. "Võrgustikud väikestele".

Lühikursus BPF ajaloost (c)

Kaasaegne BPF-tehnoloogia on sama nimega vana tehnoloogia täiustatud ja laiendatud versioon, mida nüüd segaduse vältimiseks nimetatakse klassikaliseks BPF-iks. Klassikalise BPF-i põhjal loodi tuntud utiliit tcpdump, mehhanism seccomp, aga ka vähemtuntud moodulid xt_bpf eest iptables ja klassifikaator cls_bpf. Kaasaegses Linuxis tõlgitakse klassikalised BPF-i programmid automaatselt uude vormi, kuid kasutaja seisukohast on API jäänud paigale ja klassikalisele BPF-ile leitakse endiselt uusi kasutusvõimalusi, nagu sellest artiklist näeme. Sel põhjusel ja ka seetõttu, et klassikalise BPF-i arengulugu Linuxis jälgides saab selgemaks, kuidas ja miks see moodsaks kujunes, otsustasin alustada artikliga klassikalisest BPF-ist.

Eelmise sajandi kaheksakümnendate lõpus hakkas kuulsa Lawrence Berkeley laboratooriumi insenerid huvitama küsimus, kuidas eelmise sajandi kaheksakümnendate lõpus kaasaegse riistvara võrgupakette õigesti filtreerida. Algselt CSPF (CMU/Stanford Packet Filter) tehnoloogias rakendatud filtreerimise põhiidee oli mittevajalike pakettide võimalikult varane filtreerimine, s.t. kerneli ruumis, kuna see väldib mittevajalike andmete kopeerimist kasutajaruumi. Kernelruumis kasutajakoodi käitamiseks käitusaegse turvalisuse tagamiseks kasutati liivakastiga virtuaalmasinat.

Olemasolevate filtrite virtuaalsed masinad olid aga loodud töötama pinupõhistes masinates ja ei töötanud nii tõhusalt uuemates RISC-seadmetes. Selle tulemusena töötati Berkeley Labsi inseneride jõupingutustega välja uus BPF (Berkeley Packet Filters) tehnoloogia, mille virtuaalmasina arhitektuur põhines Motorola 6502 protsessoril - selliste tuntud toodete tööhobune nagu Apple II või NES. Uus virtuaalmasin suurendas filtri jõudlust olemasolevate lahendustega võrreldes kümneid kordi.

BPF masina arhitektuur

Tutvume arhitektuuriga töiselt, näiteid analüüsides. Kuid alustuseks oletame, et masinal oli kaks kasutajale ligipääsetavat 32-bitist registrit, aku A ja indeks register X, 64 baiti mälu (16 sõna), mis on saadaval kirjutamiseks ja järgnevaks lugemiseks, ning väike käskude süsteem nende objektidega töötamiseks. Programmides olid olemas ka hüppejuhised tingimusavaldiste realiseerimiseks, kuid programmi õigeaegse valmimise tagamiseks sai hüppeid teha ainult edasi, s.t eelkõige oli keelatud luua silmuseid.

Masina käivitamise üldine skeem on järgmine. Kasutaja loob programmi BPF arhitektuuri ja kasutades mõned kerneli mehhanism (nt süsteemikutse), laadib programmi ja ühendab sellega mõnele sündmuste generaatorisse tuumas (näiteks sündmus on järgmise paketi saabumine võrgukaardile). Sündmuse toimumisel käivitab kernel programmi (näiteks tõlgis) ja masina mälu vastab mõnele kerneli mälupiirkond (näiteks sissetuleva paketi andmed).

Näidete vaatamiseks piisab ülaltoodust: tutvume vajadusel süsteemi ja käsuvorminguga. Kui soovite kohe uurida virtuaalmasina käsusüsteemi ja õppida tundma selle kõiki võimalusi, saate lugeda originaalartiklit BSD paketifilter ja/või faili esimene pool Dokumentatsioon/võrk/filter.txt kerneli dokumentatsioonist. Lisaks saate tutvuda esitlusega libpcap: pakettide hõivamise arhitektuur ja optimeerimise metoodika, milles BPF-i üks autoreid McCanne räägib loomise ajaloost libpcap.

Nüüd käsitleme kõiki olulisi näiteid klassikalise BPF-i kasutamisest Linuxis: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.

tcpdump

BPF-i arendus viidi läbi paralleelselt pakettide filtreerimise esiosa arendamisega - see on tuntud utiliit. tcpdump. Ja kuna see on vanim ja kuulsaim näide klassikalise BPF-i kasutamisest, mis on saadaval paljudes operatsioonisüsteemides, alustame tehnoloogia uurimist sellega.

(Kõik selle artikli näited kasutasin Linuxis 5.6.0-rc6. Mõne käsu väljundit on parema loetavuse huvides muudetud.)

Näide: IPv6 pakettide vaatlemine

Kujutagem ette, et tahame vaadata kõiki liidese IPv6 pakette eth0. Selleks saame programmi käivitada tcpdump lihtsa filtriga ip6:

$ sudo tcpdump -i eth0 ip6

Sel juhul tcpdump koostab filtri ip6 BPF-i arhitektuuri baitkoodi ja saatke see kernelile (vt üksikasju jaotisest Tcpdump: laadimine). Laaditud filter käivitatakse iga liidest läbiva paketi jaoks eth0. Kui filter tagastab nullist erineva väärtuse n, siis kuni n baiti paketist kopeeritakse kasutajaruumi ja me näeme seda väljundis tcpdump.

BPF kõige väiksematele, osa null: klassikaline BPF

Selgub, et saame hõlpsalt teada, milline baitkood kernelile saadeti tcpdump abiga tcpdump, kui käivitame selle valikuga -d:

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

Nullreal käivitame käsu ldh [12], mis tähistab "load into register A pool sõna (16 bitti), mis asub aadressil 12” ja küsimus on ainult selles, millist mälu me käsitleme? Vastus on, et kl x algab (x+1)analüüsitud võrgupaketi bait. Lugesime pakette Etherneti liidesest eth0ja see vahendidet pakett näeb välja selline (lihtsuse huvides eeldame, et paketis pole VLAN-i silte):

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

Nii et pärast käsu täitmist ldh [12] registris A sinna tuleb põld Ether Type — selles Etherneti kaadris edastatud paketi tüüp. 1. real võrdleme registri sisu A (pakenditüüp) c 0x86ddja see ja on Tüüp, millest meid huvitab, on IPv6. Real 1 on lisaks võrdluskäsule veel kaks veergu - jt 2 и jf 3 — hinded, milleni peate minema, kui võrdlus õnnestub (A == 0x86dd) ja ebaõnnestus. Nii et edukal juhul (IPv6) läheme reale 2 ja ebaõnnestunud juhul reale 3. Real 3 lõpetab programm koodiga 0 (ära kopeeri paketti), real 2 lõpetab programm koodiga 262144 (kopeeri mulle maksimaalselt 256 kilobaidine pakett).

Keerulisem näide: vaatame TCP-pakette sihtpordi järgi

Vaatame, kuidas näeb välja filter, mis kopeerib kõik TCP-paketid sihtpordiga 666. Vaatleme IPv4 juhtumit, kuna IPv6 juhtum on lihtsam. Pärast selle näite uurimist saate harjutusena ise uurida IPv6 filtrit (ip6 and tcp dst port 666) ja filter üldjuhtumi jaoks (tcp dst port 666). Seega näeb meid huvitav filter välja selline:

$ 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

Me juba teame, mida read 0 ja 1 teevad. 2. real oleme juba kontrollinud, et see on IPv4 pakett (eetritüüp = 0x800) ja laadige see registrisse A Paketi 24. bait. Meie pakett näeb välja selline

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

mis tähendab, et laadime registrisse A IP päise Protokolli väli, mis on loogiline, sest tahame kopeerida ainult TCP pakette. Võrdleme protokolli 0x6 (IPPROTO_TCP) real 3.

Ridadel 4 ja 5 laadime poolsõnad, mis asuvad aadressil 20, ja kasutame käsku jset kontrollige, kas üks kolmest on seatud lipud - välja antud maski kandmine jset kolm kõige olulisemat bitti kustutatakse. Kaks kolmest bitist näitavad meile, kas pakett on osa killustatud IP-paketist ja kui jah, siis kas see on viimane fragment. Kolmas bitt on reserveeritud ja peab olema null. Me ei taha kontrollida ei mittetäielikke ega katkisi pakette, seega kontrollime kõiki kolme bitti.

6. rida on selle loendi kõige huvitavam. Väljendus ldxb 4*([14]&0xf) tähendab, et laadime registrisse X paketi viieteistkümnenda baidi vähima tähtsusega neli bitti, mis on korrutatud 4-ga. Viieteistkümnenda baidi väikseima tähtsusega neli bitti on väli Interneti-päise pikkus IPv4 päis, mis salvestab päise pikkuse sõnades, nii et peate seejärel korrutama 4-ga. Huvitaval kombel on avaldis 4*([14]&0xf) on tähis spetsiaalsele adresseerimisskeemile, mida saab kasutada ainult sellisel kujul ja ainult registri jaoks X, st. me ei oska ka öelda ldb 4*([14]&0xf) või ldxb 5*([14]&0xf) (saame määrata ainult erineva nihke, näiteks ldxb 4*([16]&0xf)). On selge, et see adresseerimisskeem lisati BPF-ile just vastuvõtmiseks X (indeks register) IPv4 päise pikkus.

Nii et real 7 proovime laadida pool sõna at (X+16). Pidades meeles, et Etherneti päis võtab enda alla 14 baiti ja X sisaldab IPv4 päise pikkust, saame sellest aru A TCP sihtport on laaditud:

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

Lõpuks võrdleme real 8 sihtporti soovitud väärtusega ja real 9 või 10 tagastame tulemuse – kas pakett kopeerida või mitte.

Tcpdump: laadimine

Eelmistes näidetes me konkreetselt ei peatunud üksikasjalikult sellel, kuidas me BPF baitkoodi pakettide filtreerimiseks kernelisse laadime. Üldiselt, tcpdump porditakse paljudesse süsteemidesse ja filtritega töötamiseks tcpdump kasutab raamatukogu libpcap. Lühidalt, et asetada filter liidesele kasutades libpcap, peate tegema järgmist:

Et näha, kuidas funktsioon pcap_setfilter rakendatud Linuxis, kasutame strace (mõned read on eemaldatud):

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

Loome kahel esimesel väljundreal toores pistikupesa et lugeda kõiki Etherneti kaadreid ja siduda see liidesega eth0. Pärit meie esimene näide me teame, et filter ip koosneb neljast BPF-i juhisest ja kolmandal real näeme, kuidas seda valikut kasutada SO_ATTACH_FILTER süsteemikõne setsockopt laadime ja ühendame filtri pikkusega 4. See on meie filter.

Tasub teada, et klassikalises BPF-is toimub filtri laadimine ja ühendamine alati aatomioperatsioonina ning BPF-i uues versioonis on programmi laadimine ja sidumine sündmuste generaatoriga ajaliselt eraldatud.

Varjatud tõde

Väljundi pisut täielikum versioon näeb välja selline:

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

Nagu eespool mainitud, laadime ja ühendame oma filtri 5. liini pistikupessa, kuid mis juhtub liinidel 3 ja 4? Selgub, et see libpcap hoolitseb meie eest – et meie filtri väljund ei sisaldaks pakette, mis seda ei rahulda, teeki ühendab näiv filter ret #0 (viska kõik paketid maha), lülitab sokli mitteblokeerivasse režiimi ja proovib lahutada kõik paketid, mis võisid eelmistest filtritest alles jääda.

Kokkuvõttes peab Linuxis pakettide filtreerimiseks klassikalise BPF-i abil olema filter sellise struktuuri kujul nagu struct sock_fprog ja lahtine pesa, mille järel saab filtri süsteemikõne abil pesa külge kinnitada setsockopt.

Huvitaval kombel saab filtrit kinnitada igasse pesa, mitte ainult toorelt. Siin näide programm, mis lõikab kõigist sissetulevatest UDP-datagrammidest ära kõik peale kahe esimese baidi. (Lisasin koodi kommentaarid, et artiklit mitte segamini ajada.)

Lisateavet kasutamise kohta setsockopt filtrite ühendamiseks vt pistikupesa (7), vaid oma filtrite kirjutamise kohta nagu struct sock_fprog ilma abita tcpdump räägime rubriigis BPF-i programmeerimine oma kätega.

Klassikaline BPF ja XNUMX. sajand

BPF lisati Linuxi 1997. aastal ja on pikka aega jäänud tööhobuseks libpcap ilma eriliste muudatusteta (muidugi Linuxispetsiifilised muudatused, olid, kuid need ei muutnud globaalset pilti). Esimesed tõsised märgid BPF-i arengust ilmnesid 2011. aastal, kui Eric Dumazet tegi ettepaneku plaaster, mis lisab kernelile Just In Time Compileri – tõlkija BPF baitkoodi natiivseks teisendamiseks x86_64 kood.

JIT-i koostaja oli muudatuste ahelas esimene: 2012.a ilmus võime kirjutada filtreid seccomp, kasutades BPF-i, 2013. aasta jaanuaris oli lisatud moodul xt_bpf, mis võimaldab kirjutada reegleid iptables BPF-i abiga ja 2013. aasta oktoobris oli lisatud ka moodul cls_bpf, mis võimaldab kirjutada liikluse klassifikaatoreid kasutades BPF-i.

Vaatleme kõiki neid näiteid varsti üksikasjalikumalt, kuid kõigepealt on meil kasulik õppida, kuidas kirjutada ja kompileerida suvalisi BPF-i programme, kuna raamatukogu pakutavad võimalused libpcap piiratud (lihtne näide: filter loodud libpcap saab tagastada ainult kaks väärtust - 0 või 0x40000) või üldiselt, nagu seccompi puhul, need ei kehti.

BPF-i programmeerimine oma kätega

Tutvume BPF-i juhiste binaarvorminguga, see on väga lihtne:

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

Iga käsk võtab enda alla 64 bitti, millest esimesed 16 bitti on käsukood, seejärel on kaks kaheksabitist taanet, jt и jfja argumendi jaoks 32 bitti K, mille eesmärk on käsuti erinev. Näiteks käsk ret, mis lõpetab programmi, omab koodi 6, ja tagastatav väärtus võetakse konstandist K. C-s on üks BPF-i käsk kujutatud struktuurina

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

ja kogu programm on struktuuri kujul

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

Seega oskame juba programme kirjutada (näiteks teame käsukoode, millest [1]). Selline näeb filter välja ip6 kohta meie esimene näide:

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

programm prog saame kõnes seaduslikult kasutada

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

Programmide kirjutamine masinkoodide kujul ei ole väga mugav, kuid mõnikord on see vajalik (näiteks silumiseks, ühikutestide loomiseks, Habré-teemaliste artiklite kirjutamiseks jne). Mugavuse huvides failis <linux/filter.h> abimakrod on defineeritud - sama näide nagu ülal, võib ümber kirjutada

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

See valik pole aga kuigi mugav. Seda põhjendasid Linuxi tuuma programmeerijad ja seetõttu ka kataloogis tools/bpf kernelitest leiate klassikalise BPF-iga töötamiseks komplekteerija ja siluri.

Assamblee keel on väga sarnane silumisväljundiga tcpdump, kuid lisaks saame määrata sümboolseid silte. Näiteks siin on programm, mis tühistab kõik paketid peale TCP/IPv4:

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

Vaikimisi genereerib assembler koodi vormingus <количество инструкций>,<code1> <jt1> <jf1> <k1>,..., meie näiteks TCP-ga on see nii

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

C programmeerijate mugavuse huvides saab kasutada teistsugust väljundvormingut:

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

Selle teksti saab kopeerida tüübistruktuuri definitsiooni struct sock_filter, nagu me tegime selle jaotise alguses.

Linuxi ja netsniff-ng laiendused

Lisaks standardsele BPF-ile, Linuxile ja tools/bpf/bpf_asm toetada ja mittestandardne komplekt. Põhimõtteliselt kasutatakse struktuuri väljadele juurdepääsuks juhiseid struct sk_buff, mis kirjeldab tuumas olevat võrgupaketti. Samas on ka näiteks teist tüüpi abikäsklusi ldw cpu laaditakse registrisse A kerneli funktsiooni käivitamise tulemus raw_smp_processor_id(). (BPF-i uues versioonis on neid mittestandardseid laiendusi laiendatud, et pakkuda programmidele kerneli abistajate komplekti mälule, struktuuridele ja sündmuste genereerimiseks.) Siin on huvitav näide filtrist, milles kopeerime ainult pakettide päised kasutajaruumi laienduse abil poff, kasuliku koormuse nihe:

ld poff
ret a

BPF-i laiendusi ei saa kasutada tcpdump, kuid see on hea põhjus kommunaalpaketiga tutvumiseks netsniff-ng, mis sisaldab muu hulgas täiustatud programmi netsniff-ng, mis lisaks BPF-i abil filtreerimisele sisaldab ka tõhusat liikluse generaatorit ja täiustatud kui tools/bpf/bpf_asm, kutsus BPF-i koostaja bpfc. Pakett sisaldab üsna üksikasjalikku dokumentatsiooni, vaata ka linke artikli lõpus.

seccomp

Niisiis, me juba teame, kuidas kirjutada suvalise keerukusega BPF-programme ja oleme valmis vaatama uusi näiteid, millest esimene on seccomp-tehnoloogia, mis võimaldab BPF-filtrite abil hallata süsteemikutsete argumentide komplekti ja komplekti. antud protsess ja selle järglased.

Seccompi esimene versioon lisati kernelile 2005. aastal ja see ei olnud eriti populaarne, kuna see pakkus ainult ühte võimalust - piirata protsessi jaoks saadaolevate süsteemikutsete komplekti järgmistega: read, write, exit и sigreturn, ja reegleid rikkunud protsess tapeti kasutades SIGKILL. 2012. aastal lisas seccomp aga võimaluse kasutada BPF-filtreid, võimaldades teil määratleda lubatud süsteemikutsete komplekti ja isegi kontrollida nende argumente. (Huvitav on see, et Chrome oli selle funktsiooni üks esimesi kasutajaid ja Chrome'i inimesed arendavad praegu BPF-i uuel versioonil põhinevat KRSI-mehhanismi, mis võimaldab Linuxi turvamooduleid kohandada.) Lingid lisadokumentatsioonile leiate lõpust. artiklist.

Pange tähele, et jaoturis on juba olnud artikleid seccompi kasutamise kohta, võib-olla soovib keegi neid enne järgmiste alajaotiste lugemist (või selle asemel) lugeda. Artiklis Konteinerid ja turvalisus: seccomp toob näiteid seccompi kasutamisest, nii 2007. aasta versioonist kui ka BPF-i kasutavast versioonist (filtrid genereeritakse libseccompi abil), räägib seccompi ühendamisest Dockeriga ja pakub ka palju kasulikke linke. Artiklis Deemonite isoleerimine systemd-iga või "selleks pole Dockerit vaja!" See hõlmab eelkõige seda, kuidas lisada systemd töötavatele deemonitele süsteemikutsete musti või valgeid loendeid.

Järgmisena näeme, kuidas filtreid kirjutada ja laadida seccomp tühjas C-s ja raamatukogu kasutades libseccomp ja millised on iga valiku plussid ja miinused ning lõpuks vaatame, kuidas programm seccompi kasutab strace.

Seccompi filtrite kirjutamine ja laadimine

Me juba teame, kuidas BPF-programme kirjutada, seega vaatame esmalt seccompi programmeerimisliidest. Saate määrata filtri protsessi tasemel ja kõik alamprotsessid pärivad piirangud. Seda tehakse süsteemikõne abil seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

kus &filter - see on osuti meile juba tuttavale struktuurile struct sock_fprog, st. BPF programm.

Mille poolest erinevad seccompi programmid pistikupesade programmidest? Edastatud kontekst. Socketide puhul anti meile paketti sisaldav mäluala ja seccompi puhul struktuur nagu

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

see on nr on käivitatava süsteemikõne number, arch - praegune arhitektuur (sellest lähemalt allpool), args - kuni kuus süsteemikutse argumenti ja instruction_pointer on kursor kasutajaruumi juhisele, mis tegi süsteemikutse. Seega näiteks süsteemi kõne numbri registrisse laadimiseks A me peame ütlema

ldw [0]

Seccompi programmide jaoks on ka muid funktsioone, näiteks pääseb konteksti juurde ainult 32-bitise joondusega ja filtri laadimisel ei saa laadida poolt sõna ega baiti ldh [0] süsteemikõne seccomp tuleb tagasi EINVAL. Funktsioon kontrollib laaditud filtreid seccomp_check_filter() tuumad. (Naljakas on see, et algses sissekandes, mis lisas seccompi funktsiooni, unustasid nad lisada loa kasutada antud funktsiooni mod (ülejäänud jagamine) ja pole nüüd pärast lisamist seccompi BPF-i programmide jaoks saadaval läheb katki ABI.)

Põhimõtteliselt teame juba kõike seccomp-programmide kirjutamiseks ja lugemiseks. Tavaliselt on programmiloogika paigutatud süsteemikutsete, näiteks programmi, valge või musta nimekirjana

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

kontrollib musta nimekirja neljast süsteemikõnest numbritega 304, 176, 239, 279. Mis need süsteemikõned on? Me ei saa kindlalt öelda, kuna me ei tea, millise arhitektuuri jaoks programm on kirjutatud. Seetõttu on seccompi autorid pakkuma käivitage kõik programmid arhitektuurikontrolliga (praegune arhitektuur näidatakse kontekstis väljana arch struktuur struct seccomp_data). Kui arhitektuur on kontrollitud, näeks näite algus välja selline:

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

ja siis saavad meie süsteemi kõnenumbrid teatud väärtused.

Kirjutame ja laadime filtreid seccompi kasutamiseks libseccomp

Filtrite kirjutamine natiivses koodis või BPF-koosseisus võimaldab teil tulemust täielikult kontrollida, kuid samal ajal on mõnikord eelistatav kaasaskantav ja/või loetav kood. Raamatukogu aitab meid selles libseccomp, mis pakub standardset liidest mustade või valgete filtrite kirjutamiseks.

Kirjutame näiteks programmi, mis käivitab kasutaja valitud binaarfaili, olles eelnevalt installinud süsteemikõnede musta nimekirja ülaltoodud artikkel (programmi on parema loetavuse huvides lihtsustatud, täisversiooni leiate siin):

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

Kõigepealt defineerime massiivi sys_numbers 40+ süsteemikõnede numbrit, mida blokeerida. Seejärel lähtestage kontekst ctx ja ütle raamatukogule, mida tahame lubada (SCMP_ACT_ALLOW) vaikimisi kõik süsteemikutsed (mustade nimekirjade koostamine on lihtsam). Seejärel lisame ükshaaval kõik süsteemikõned mustast nimekirjast. Vastuseks loendis olevale süsteemikutsele palume SCMP_ACT_TRAP, sel juhul saadab seccomp protsessile signaali SIGSYS kirjeldusega, milline süsteemikõne reegleid rikkus. Lõpuks laadime programmi kernelisse kasutades seccomp_load, mis kompileerib programmi ja lisab selle süsteemikõne abil protsessile seccomp(2).

Edukaks koostamiseks peab programm olema seotud raamatukoguga libseccomp, näiteks:

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

Näide edukast käivitamisest:

$ ./seccomp_lib echo ok
ok

Blokeeritud süsteemikõne näide:

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

Me kasutame straceüksikasjad:

$ 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

kuidas me saame teada, et programm lõpetati ebaseadusliku süsteemikutse kasutamise tõttu mount(2).

Niisiis, me kirjutasime raamatukogu kasutades filtri libseccomp, sobitades mittetriviaalse koodi neljale reale. Ülaltoodud näites, kui süsteemikõnesid on palju, saab täitmisaega märgatavalt lühendada, kuna kontroll on vaid võrdluste loend. Optimeerimiseks kasutas libseccomp hiljuti plaaster kaasas, mis lisab filtri atribuudi tuge SCMP_FLTATR_CTL_OPTIMIZE. Kui määrate selle atribuudi väärtuseks 2, teisendab filter binaarseks otsinguprogrammiks.

Kui soovite näha, kuidas binaarsed otsingufiltrid töötavad, vaadake lihtne skript, mis genereerib sellised programmid BPF-i assembleris, valides süsteemi kõnenumbreid, näiteks:

$ 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

Te ei saa midagi oluliselt kiiremini kirjutada, kuna BPF-programmid ei saa sooritada taandehüppeid (me ei saa teha näiteks jmp A või jmp [label+X]) ja seetõttu on kõik üleminekud staatilised.

seccomp ja strace

Kõik teavad kasulikkust strace on asendamatu tööriist protsesside käitumise uurimiseks Linuxis. Paljud on aga ka sellest kuulnud jõudlusprobleemid selle utiliidi kasutamisel. Fakt on see, et strace rakendatakse kasutades ptrace(2), ja selle mehhanismi puhul ei saa me määrata, milliste süsteemikutsete puhul peame protsessi peatama, st näiteks käskude puhul

$ 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

töödeldakse ligikaudu sama ajaga, kuigi teisel juhul tahame jälgida ainult ühte süsteemikutset.

Uus variant --seccomp-bpf, lisatud strace versioon 5.3, võimaldab protsessi mitu korda kiirendada ja ühe süsteemikõne jälgedes käivitusaeg on juba võrreldav tavalise käivitamise ajaga:

$ 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

(Siin on muidugi väike pettus, et me ei jälgi selle käsu peamist süsteemikutset. Kui jälgiksime näiteks, newfsstatsiis strace pidurdaks sama tugevalt kui ilma --seccomp-bpf.)

Kuidas see valik töötab? Ilma temata strace ühendub protsessiga ja alustab seda kasutama PTRACE_SYSCALL. Kui hallatav protsess väljastab (mis tahes) süsteemikutse, antakse juhtimine üle strace, mis vaatab süsteemikõne argumente ja käivitab selle kasutades PTRACE_SYSCALL. Mõne aja pärast lõpetab protsess süsteemikõne ja sellest väljumisel antakse juhtimine uuesti üle strace, mis vaatab tagastusväärtusi ja alustab protsessi kasutades PTRACE_SYSCALL, ja nii edasi.

BPF kõige väiksematele, osa null: klassikaline BPF

Seccompiga saab seda protsessi aga optimeerida täpselt nii, nagu me soovime. Nimelt kui tahame vaadata ainult süsteemikutset X, siis saame kirjutada selle jaoks BPF-filtri X tagastab väärtuse SECCOMP_RET_TRACEja kõnede puhul, mis meid ei huvita - SECCOMP_RET_ALLOW:

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

Sel juhul strace algselt alustab protsessi kui PTRACE_CONT, töödeldakse meie filtrit iga süsteemikõne jaoks, kui süsteemikõne seda ei ole X, siis protsess jätkub, kuid kui see X, siis annab seccomp juhtimise üle stracemis vaatab argumente ja alustab protsessi nagu PTRACE_SYSCALL (kuna seccompil pole võimalust süsteemikutsest väljumisel programmi käivitada). Kui süsteemikõne naaseb, strace taaskäivitab protsessi kasutades PTRACE_CONT ja ootab uusi sõnumeid seccompilt.

BPF kõige väiksematele, osa null: klassikaline BPF

Võimaluse kasutamisel --seccomp-bpf on kaks piirangut. Esiteks ei ole võimalik liituda juba olemasoleva protsessiga (valik -p programmid strace), kuna seccomp seda ei toeta. Teiseks pole võimalust ei vaadake alamprotsesse, kuna seccomp-filtrid pärivad kõik alamprotsessid, ilma et saaksite seda keelata.

Natuke täpsemalt, kuidas täpselt strace töötab koos seccomp võib leida alates hiljutine aruanne. Meie jaoks on kõige huvitavam fakt, et klassikalist BPF-i, mida esindab seccomp, kasutatakse ka tänapäeval.

xt_bpf

Lähme nüüd tagasi võrkude maailma.

Taust: ammu, aastal 2007, oli tuum lisatud moodul xt_u32 netfiltri jaoks. See on kirjutatud analoogia põhjal veelgi iidsema liiklusklassifikaatoriga cls_u32 ja võimaldas teil kirjutada iptablesi jaoks suvalisi binaarreegleid, kasutades järgmisi lihtsaid toiminguid: laadige paketist 32 bitti ja tehke nendega aritmeetiliste toimingute komplekt. Näiteks,

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

Laadib IP-päise 32 bitti, alustades täidisest 6, ja rakendab neile maski 0xFF (võta madalam bait). See väli protocol IP-päis ja võrdleme seda 1-ga (ICMP). Saate kombineerida palju kontrolle ühes reeglis ja saate ka operaatori täita @ — liigutage X baiti paremale. Näiteks reegel

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

kontrollib, kas TCP järjestuse number ei ole võrdne 0x29. Ma ei lasku üksikasjalikumalt, kuna on juba selge, et selliste reeglite käsitsi kirjutamine pole eriti mugav. Artiklis BPF – unustatud baitkood, on mitmeid linke kasutusnäidetega ja reeglite loomise kohta xt_u32. Vaadake ka selle artikli lõpus olevaid linke.

Alates 2013. aastast mooduli asemel moodul xt_u32 saate kasutada BPF-põhist moodulit xt_bpf. Kes siiani lugenud on, peaks juba selge olema selle toimimise põhimõte: käivita BPF bytecode iptablesi reeglitena. Saate luua uue reegli, näiteks järgmiselt:

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

siin <байткод> - see on kood assembleri väljundvormingus bpf_asm vaikimisi näiteks

$ 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

Selles näites filtreerime kõik UDP-paketid. Kontekst BPF-programmi jaoks moodulis xt_bpf, osutab muidugi pakettandmetele, iptablesi puhul IPv4 päise algusesse. BPF-programmi tagastusväärtus tõeväärtusKus false tähendab, et pakett ei sobinud.

On selge, et moodul xt_bpf toetab keerukamaid filtreid kui ülaltoodud näide. Vaatame tõelisi näiteid Cloudfare'ist. Kuni viimase ajani kasutasid nad seda moodulit xt_bpf DDoS-i rünnakute eest kaitsmiseks. Artiklis Tutvustame BPF-i tööriistu nad selgitavad, kuidas (ja miks) nad genereerivad BPF-filtreid ning avaldavad linke selliste filtrite loomiseks mõeldud utiliitide komplektile. Näiteks utiliidi kasutamine bpfgen saate luua BPF-programmi, mis vastab nime DNS-päringule 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

Programmis laadime esmalt registrisse X rea alguse aadress x04habrx03comx00 UDP datagrammis ja seejärel kontrollige päringut: 0x04686162 <-> "x04hab" jne

Veidi hiljem avaldas Cloudfare kompilaatori koodi p0f -> BPF. Artiklis Tutvustame p0f BPF kompilaatorit nad räägivad sellest, mis on p0f ja kuidas teisendada p0f-allkirju BPF-iks:

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

Praegu ei kasutata enam Cloudfare'i xt_bpf, kuna nad kolisid XDP-le - üks BPF-i uue versiooni kasutamise võimalustest, vt. L4Drop: XDP DDoS-i leevendused.

cls_bpf

Viimane näide klassikalise BPF-i kasutamisest tuumas on klassifikaator cls_bpf Linuxi liiklusjuhtimise alamsüsteemi jaoks, mis lisati Linuxile 2013. aasta lõpus ja mis põhimõtteliselt asendab iidse cls_u32.

Teost me aga nüüd kirjeldama ei hakka cls_bpf, kuna klassikalise BPF-i teadmiste seisukohast ei anna see meile midagi - oleme kõigi funktsioonidega juba tuttavaks saanud. Lisaks kohtame järgmistes artiklites, mis räägivad laiendatud BPF-ist, selle klassifikaatoriga rohkem kui üks kord.

Veel üks põhjus, miks mitte rääkida klassikalise BPF c kasutamisest cls_bpf Probleem on selles, et võrreldes Extended BPF-iga on rakendusala sel juhul radikaalselt ahenenud: klassikalised programmid ei saa muuta pakettide sisu ega salvesta kõnede vahel olekut.

Seega on aeg jätta hüvasti klassikalise BPF-iga ja vaadata tulevikku.

Hüvasti klassikalise BPF-iga

Vaatasime, kuidas üheksakümnendate alguses välja töötatud BPF-tehnoloogia elas edukalt veerand sajandit ja leidis kuni lõpuni uusi rakendusi. Sarnaselt üleminekuga stack-masinatelt RISC-le, mis andis tõuke klassikalise BPF-i arendamiseks, toimus 32. aastatel üleminek 64-bitistelt masinatelt XNUMX-bitistele ja klassikaline BPF hakkas vananema. Lisaks on klassikalise BPF-i võimalused väga piiratud ja lisaks vananenud arhitektuurile - meil puudub võimalus salvestada olekut BPF-programmide kõnede vahel, puudub otsene kasutaja suhtlus, puudub suhtlusvõimalus kerneliga, välja arvatud piiratud arvu struktuuriväljade lugemiseks sk_buff ja lihtsaimate abifunktsioonide käivitamisel ei saa pakettide sisu muuta ja neid ümber suunata.

Tegelikult on praegu Linuxis klassikalisest BPF-ist alles vaid API-liides ja kerneli sees tõlgitakse kõik klassikalised programmid, olgu need siis pesafiltrid või seccomp-filtrid, automaatselt uude vormingusse Extended BPF. (Kuidas see täpselt juhtub, räägime järgmises artiklis.)

Üleminek uuele arhitektuurile algas 2013. aastal, kui Aleksei Starovoitov pakkus välja BPF-i värskendusskeemi. 2014. aastal vastavad plaastrid hakkas paistma tuumas. Niipalju kui mina aru saan, oli esialgne plaan ainult arhitektuuri ja JIT-kompilaatori optimeerimine 64-bitistes masinates tõhusamaks töötamiseks, kuid selle asemel tähistasid need optimeerimised uue peatüki algust Linuxi arenduses.

Selle seeria edasised artiklid käsitlevad uue tehnoloogia arhitektuuri ja rakendusi, mida algselt tunti sisemise BPF-i, seejärel laiendatud BPF-i ja nüüd lihtsalt BPF-i nime all.

Viited

  1. Steven McCanne ja Van Jacobson, "BSD paketifilter: uus arhitektuur kasutajataseme pakettide hõivamiseks", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne, "libpcap: pakettide hõivamise arhitektuur ja optimeerimise metoodika", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. IPtable U32 sobitamise õpetus.
  5. BPF - unustatud baidikood: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. BPF-i tööriista tutvustus: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. Seccompi ülevaade: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: Konteinerid ja turvalisus: seccomp
  11. habr: Deemonite isoleerimine systemd-iga või "selleks pole Dockerit vaja!"
  12. Paul Chaignon, "strace --seccomp-bpf: pilk kapoti alla", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Allikas: www.habr.com

Lisa kommentaar