Berkeley Packet Filters (BPF) on Linux-ydintekniikka, joka on ollut englanninkielisten teknisten julkaisujen etusivuilla jo useita vuosia. Konferenssit ovat täynnä raportteja BPF:n käytöstä ja kehityksestä. David Miller, Linux-verkkoalijärjestelmän ylläpitäjä, kutsuu puheensa Linux Plumbers 2018 -tapahtumassa "Tämä keskustelu ei koske XDP:tä" (XDP on yksi BPF:n käyttötapaus). Brendan Gregg pitää puheen otsikolla Linuxin BPF-supervoimat. Toke Høiland-Jørgensen nauraaettä ydin on nyt mikroydin. Thomas Graf edistää ajatusta BPF on javascript ytimelle.
Habrén BPF:stä ei vieläkään ole systemaattista kuvausta, ja siksi yritän artikkelisarjassa puhua tekniikan historiasta, kuvailla arkkitehtuuria ja kehitystyökaluja sekä hahmotella BPF:n käyttöalueita ja käytäntöjä. Tämä artikkeli, nolla, sarjassa, kertoo klassisen BPF:n historiasta ja arkkitehtuurista sekä paljastaa sen toimintaperiaatteiden salaisuudet. tcpdump, seccomp, strace, ja paljon enemmän.
BPF:n kehitystä ohjaa Linux-verkkoyhteisö, BPF:n tärkeimmät olemassa olevat sovellukset liittyvät verkkoihin ja siksi luvalla @eukariot, kutsuin sarjaa "BPF pienimmille" suuren sarjan kunniaksi "Verkostot pienimmille".
Lyhyt kurssi BPF:n historiasta(c)
Nykyaikainen BPF-tekniikka on parannettu ja laajennettu versio vanhasta tekniikasta, jolla on sama nimi, jota kutsutaan nykyään klassiseksi BPF:ksi sekaannusten välttämiseksi. Klassiseen BPF:ään perustuen luotiin tunnettu apuohjelma tcpdump, mekanismi seccomp, sekä vähemmän tunnettuja moduuleja xt_bpf varten iptables ja luokitin cls_bpf. Nykyaikaisessa Linuxissa klassiset BPF-ohjelmat käännetään automaattisesti uuteen muotoon, mutta käyttäjän näkökulmasta API on pysynyt paikallaan ja klassiselle BPF:lle löytyy edelleen uusia käyttötapoja, kuten tässä artikkelissa näemme. Tästä syystä ja myös siksi, että seurataan klassisen BPF:n kehityshistoriaa Linuxissa, tulee entistä selvemmäksi, miten ja miksi se kehittyi nykyaikaiseen muotoonsa, päätin aloittaa artikkelilla klassisesta BPF:stä.
Viime vuosisadan XNUMX-luvun lopulla kuuluisan Lawrence Berkeley -laboratorion insinöörit kiinnostuivat kysymyksestä, kuinka verkkopaketteja suodatetaan oikein viime vuosisadan XNUMX-luvun lopulla moderneilla laitteilla. Alun perin CSPF (CMU/Stanford Packet Filter) -teknologiaan toteutetun suodatuksen perusideana oli suodattaa tarpeettomat paketit mahdollisimman varhaisessa vaiheessa, ts. ydintilassa, koska näin vältetään tarpeettoman tiedon kopioiminen käyttäjätilaan. Ajonaikaisen suojauksen tarjoamiseksi käyttäjäkoodin suorittamiselle ydintilassa käytettiin hiekkalaatikko-virtuaalikonetta.
Nykyisten suodattimien virtuaalikoneet on kuitenkin suunniteltu toimimaan pinopohjaisissa koneissa, eivätkä ne toimineet yhtä tehokkaasti uudemmissa RISC-koneissa. Tuloksena Berkeley Labsin insinöörien ponnistelujen avulla kehitettiin uusi BPF (Berkeley Packet Filters) -tekniikka, jonka virtuaalikoneen arkkitehtuuri on suunniteltu Motorola 6502 -prosessorin pohjalta, joka on tunnettujen tuotteiden, kuten esim. Apple II tai NES. Uusi virtuaalikone paransi suodattimen suorituskykyä kymmeniä kertoja verrattuna olemassa oleviin ratkaisuihin.
BPF-konearkkitehtuuri
Tutustumme arkkitehtuuriin toimivalla tavalla esimerkkejä analysoiden. Oletetaan kuitenkin aluksi, että koneessa oli kaksi 32-bittistä rekisteriä, jotka ovat käyttäjän käytettävissä, akku A ja hakemistorekisteri X, 64 tavua muistia (16 sanaa), joka on käytettävissä kirjoittamista ja myöhempää lukemista varten, ja pieni komentojärjestelmä näiden objektien kanssa työskentelemiseen. Ohjelmissa oli myös hyppyohjeita ehdollisten lausekkeiden toteuttamiseen, mutta ohjelman oikea-aikaisen valmistumisen takaamiseksi hyppyjä sai tehdä vain eteenpäin, eli erityisesti silmukoiden luominen oli kiellettyä.
Yleinen kaavio koneen käynnistämiseksi on seuraava. Käyttäjä luo ohjelman BPF-arkkitehtuurille ja käyttämällä jotkut ydinmekanismi (kuten järjestelmäkutsu), lataa ohjelman ja yhdistää siihen jollekin ytimen tapahtumageneraattoriin (esimerkiksi tapahtuma on seuraavan paketin saapuminen verkkokortille). Kun tapahtuma tapahtuu, ydin suorittaa ohjelman (esimerkiksi tulkissa) ja koneen muisti vastaa jollekin ytimen muistialue (esimerkiksi saapuvan paketin tiedot).
Yllä oleva riittää, jotta voimme alkaa tarkastella esimerkkejä: tutustumme järjestelmään ja komentomuotoon tarpeen mukaan. Jos haluat välittömästi tutkia virtuaalikoneen komentojärjestelmää ja oppia kaikista sen ominaisuuksista, voit lukea alkuperäisen artikkelin BSD-pakettisuodatin ja/tai tiedoston ensimmäinen puolisko Documentation/Networking/filter.txt ytimen dokumentaatiosta. Lisäksi voit tutkia esitystä libpcap: Pakettikaappauksen arkkitehtuuri- ja optimointimenetelmä, jossa McCanne, yksi BPF:n kirjoittajista, puhuu luomisen historiasta libpcap.
Siirrymme tarkastelemaan kaikkia merkittäviä esimerkkejä klassisen BPF:n käytöstä Linuxissa: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.
tcpdump
BPF:n kehitys tehtiin rinnakkain pakettisuodatuksen käyttöliittymän kehittämisen kanssa - hyvin tunnettu apuohjelma tcpdump. Ja koska tämä on vanhin ja tunnetuin esimerkki klassisen BPF:n käytöstä, joka on saatavilla moniin käyttöjärjestelmiin, aloitamme tekniikan tutkimuksen sillä.
(Käytin kaikki tämän artikkelin esimerkit Linuxissa 5.6.0-rc6. Joidenkin komentojen tulosta on muokattu luettavuuden parantamiseksi.)
Esimerkki: IPv6-pakettien tarkkailu
Kuvitellaan, että haluamme tarkastella kaikkia IPv6-paketteja rajapinnassa eth0. Tätä varten voimme suorittaa ohjelman tcpdump yksinkertaisella suodattimella ip6:
$ sudo tcpdump -i eth0 ip6
Tässä tapauksessa tcpdump kokoaa suodattimen ip6 BPF-arkkitehtuurin tavukoodiin ja lähetä se ytimeen (katso yksityiskohdat kohdasta Tcpdump: latautuu). Ladattu suodatin suoritetaan jokaiselle rajapinnan läpi kulkevalle paketille eth0. Jos suodatin palauttaa nollasta poikkeavan arvon n, sitten asti n tavua paketista kopioidaan käyttäjätilaan ja näemme sen lähdössä tcpdump.
Osoittautuu, että voimme helposti selvittää, mikä tavukoodi lähetettiin ytimeen tcpdump avulla tcpdump, jos käytämme sitä vaihtoehdolla -d:
$ sudo tcpdump -i eth0 -d ip6
(000) ldh [12]
(001) jeq #0x86dd jt 2 jf 3
(002) ret #262144
(003) ret #0
Rivillä nolla suoritamme komennon ldh [12], joka tarkoittaa "load into register A puoli sanaa (16 bittiä), joka sijaitsee osoitteessa 12" ja ainoa kysymys on, millaista muistia käsittelemme? Vastaus on, että klo x alkaa (x+1)analysoidun verkkopaketin tavu. Luimme paketteja Ethernet-liitännästä eth0ja tämä välineetettä paketti näyttää tältä (yksinkertaisuuden vuoksi oletetaan, että paketissa ei ole VLAN-tunnisteita):
6 6 2
|Destination MAC|Source MAC|Ether Type|...|
Siis komennon suorittamisen jälkeen ldh [12] rekisterissä A tulee kenttä Ether Type — tässä Ethernet-kehyksessä lähetetyn paketin tyyppi. Rivillä 1 vertaamme rekisterin sisältöä A (pakettityyppi) c 0x86ddja tämä ja on Tyyppi, josta olemme kiinnostuneita, on IPv6. Rivillä 1 on vertailukomennon lisäksi kaksi muuta saraketta - jt 2 и jf 3 - arvosanat, joihin sinun on mentävä, jos vertailu on onnistunut (A == 0x86dd) ja epäonnistunut. Joten onnistuneessa tapauksessa (IPv6) mennään riville 2 ja epäonnistuneessa tapauksessa riville 3. Rivillä 3 ohjelma päättyy koodiin 0 (älä kopioi pakettia), rivillä 2 ohjelma päättyy koodiin 262144 (kopioi minulle enintään 256 kilotavun paketti).
Monimutkaisempi esimerkki: tarkastelemme TCP-paketteja kohdeportin mukaan
Katsotaan miltä näyttää suodatin, joka kopioi kaikki TCP-paketit kohdeportilla 666. Tarkastellaan IPv4-tapausta, koska IPv6-tapaus on yksinkertaisempi. Kun olet tutkinut tämän esimerkin, voit tutkia IPv6-suodatinta itse harjoituksena (ip6 and tcp dst port 666) ja suodatin yleiseen tapaukseen (tcp dst port 666). Joten meitä kiinnostava suodatin näyttää tältä:
$ 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
Tiedämme jo, mitä rivit 0 ja 1 tekevät. Rivillä 2 olemme jo tarkistaneet, että tämä on IPv4-paketti (Ether Type = 0x800) ja lataa se rekisteriin A Paketin 24. tavu. Pakettimme näyttää
mikä tarkoittaa, että lataamme rekisteriin A IP-otsikon Protocol-kenttä, mikä on loogista, koska haluamme kopioida vain TCP-paketteja. Vertaamme pöytäkirjaa 0x6 (IPPROTO_TCP) rivillä 3.
Riveillä 4 ja 5 lataamme osoitteessa 20 olevat puolisanat ja käytämme komentoa jset tarkista, onko jokin kolmesta asetettu liput - myönnetty maski päällä jset kolme tärkeintä bittiä tyhjennetään. Kaksi kolmesta bitistä kertoo meille, onko paketti osa pirstoutunutta IP-pakettia, ja jos on, onko se viimeinen fragmentti. Kolmas bitti on varattu ja sen on oltava nolla. Emme halua tarkistaa puutteellisia tai rikkoutuneita paketteja, joten tarkistamme kaikki kolme bittiä.
Rivi 6 on tämän listauksen mielenkiintoisin. Ilmaisu ldxb 4*([14]&0xf) tarkoittaa, että lataamme rekisteriin X paketin viidennentoista tavun vähiten merkitsevät neljä bittiä kerrottuna 4:llä. Viidennentoista tavun vähiten merkitsevät neljä bittiä on kenttä Internet-otsikon pituus IPv4-otsikko, joka tallentaa otsikon pituuden sanoina, joten sinun täytyy sitten kertoa 4:llä. Mielenkiintoista on, että lauseke 4*([14]&0xf) on nimitys erityiselle osoitusjärjestelmälle, jota voidaan käyttää vain tässä muodossa ja vain rekisterissä X, eli emme myöskään osaa sanoa ldb 4*([14]&0xf) tai ldxb 5*([14]&0xf) (voimme määrittää vain erilaisen siirtymän, esim. ldxb 4*([16]&0xf)). On selvää, että tämä osoitejärjestelmä lisättiin BPF:ään juuri vastaanottamista varten X (indeksirekisteri) IPv4-otsikon pituus.
Joten rivillä 7 yritämme ladata puoli sanaa osoitteessa (X+16). Muista, että Ethernet-otsikko varaa 14 tavua ja X sisältää IPv4-otsikon pituuden, ymmärrämme, että A TCP-kohdeportti on ladattu:
14 X 2 2
|ethernet header|ip header|source port|destination port|
Lopuksi rivillä 8 verrataan kohdeporttia haluttuun arvoon ja rivillä 9 tai 10 palautetaan tulos - kopioidaanko paketti vai ei.
Tcpdump: latautuu
Edellisissä esimerkeissä emme tarkastellut yksityiskohtaisesti, kuinka BPF-tavukoodi ladataan ytimeen pakettisuodatusta varten. Yleisesti ottaen, tcpdump siirretty moniin järjestelmiin ja suodattimien kanssa työskentelemiseen tcpdump käyttää kirjastoa libpcap. Lyhyesti sanottuna suodattimen sijoittaminen käyttöliittymään käyttämällä libpcap, sinun on tehtävä seuraava:
luo tyyppikuvaaja pcap_t käyttöliittymän nimestä: pcap_create,
Luomme kahdelle ensimmäiselle riville raaka pistorasia lukeaksesi kaikki Ethernet-kehykset ja sitoaksesi sen liitäntään eth0. Alkaen ensimmäinen esimerkkimme tiedämme, että suodatin ip koostuu neljästä BPF-ohjeesta, ja kolmannella rivillä näemme, kuinka vaihtoehtoa käytetään SO_ATTACH_FILTER järjestelmäpuhelu setsockopt lataamme ja kytkemme suodattimen, jonka pituus on 4. Tämä on suodattimemme.
On syytä huomata, että klassisessa BPF:ssä suodattimen lataaminen ja kytkeminen tapahtuu aina atomioperaationa, ja BPF:n uudessa versiossa ohjelman lataaminen ja sitominen tapahtumageneraattoriin erotetaan ajallisesti.
Piilotettu Totuus
Hieman täydellisempi versio lähdöstä näyttää tältä:
Kuten edellä mainittiin, lataamme ja kiinnitämme suodattimen linjan 5 liitäntään, mutta mitä tapahtuu linjoilla 3 ja 4? Osoittautuu, että tämä libpcap huolehtii meistä - jotta suodattimemme tulos ei sisällä paketteja, jotka eivät täytä sitä, kirjasto yhdistää tyhjä suodatin ret #0 (pudota kaikki paketit), vaihtaa socketin estotilaan ja yrittää vähentää kaikki paketit, jotka voivat jäädä aiemmista suodattimista.
Kaiken kaikkiaan, jotta voit suodattaa paketteja Linuxissa käyttämällä klassista BPF:ää, sinulla on oltava suodatin rakenteen muodossa struct sock_fprog ja avoin pistoke, jonka jälkeen suodatin voidaan kiinnittää kantaan järjestelmäkutsulla setsockopt.
Mielenkiintoista on, että suodatin voidaan kiinnittää mihin tahansa pistorasiaan, ei vain raakaan. Tässä esimerkki ohjelma, joka katkaisee kaikki paitsi kaksi ensimmäistä tavua kaikista saapuvista UDP-datagrameista. (Lisäsin koodiin kommentteja, jotta en sotkenut artikkelia.)
Tarkemmat tiedot käytöstä setsockopt suodattimien liittäminen, katso pistorasia (7), vaan omien suodattimien kirjoittamisesta, kuten struct sock_fprog ilman apua tcpdump puhumme osiossa BPF:n ohjelmointi omin käsin.
Klassinen BPF ja XNUMX-luku
BPF sisällytettiin Linuxiin vuonna 1997 ja on pysynyt työhevosena pitkään libpcap ilman erityisiä muutoksia (Linux-kohtaiset muutokset tietysti, Olimme, mutta ne eivät muuttaneet yleiskuvaa). Ensimmäiset vakavat merkit BPF:n kehittymisestä tulivat vuonna 2011, kun Eric Dumazet ehdotti läikkä, joka lisää ytimeen Just In Time -kääntäjän - kääntäjän BPF-tavukoodin muuntamiseen alkuperäiseksi x86_64 koodi.
JIT-kääntäjä oli ensimmäinen muutosketjussa: vuonna 2012 ilmestyi kyky kirjoittaa suodattimia seccomp, käyttäen BPF:ää, tammikuussa 2013 oli lisätty moduuli xt_bpf, jonka avulla voit kirjoittaa sääntöjä iptables BPF:n avulla, ja lokakuussa 2013 oli lisätty myös moduuli cls_bpf, jonka avulla voit kirjoittaa liikenneluokituksia BPF:n avulla.
Tarkastelemme kaikkia näitä esimerkkejä tarkemmin pian, mutta ensin on hyödyllistä oppia kirjoittamaan ja kääntämään mielivaltaisia ohjelmia BPF:lle, koska kirjaston tarjoamat ominaisuudet libpcap rajoitettu (yksinkertainen esimerkki: suodatin luotu libpcap voi palauttaa vain kaksi arvoa - 0 tai 0x40000) tai yleensä, kuten seccompin tapauksessa, ne eivät sovellu.
BPF:n ohjelmointi omin käsin
Tutustutaan BPF-ohjeiden binäärimuotoon, se on hyvin yksinkertaista:
16 8 8 32
| code | jt | jf | k |
Kukin käsky vie 64 bittiä, joista ensimmäiset 16 bittiä ovat ohjekoodi, sitten on kaksi kahdeksan bitin sisennystä, jt и jf, ja 32 bittiä argumentille K, jonka tarkoitus vaihtelee käskystä toiseen. Esimerkiksi komento ret, joka lopettaa ohjelman, jolla on koodi 6, ja palautusarvo otetaan vakiosta K. C:ssä yksittäinen BPF-käsky esitetään rakenteena
Ohjelmien kirjoittaminen konekoodien muodossa ei ole kovin kätevää, mutta joskus se on välttämätöntä (esimerkiksi virheenkorjaukseen, yksikkötestien luomiseen, Habré-artikkeleiden kirjoittamiseen jne.). Tiedoston käyttömukavuuden vuoksi <linux/filter.h> auttajamakrot on määritelty - sama esimerkki kuin yllä voidaan kirjoittaa uudelleen
Tämä vaihtoehto ei kuitenkaan ole kovin kätevä. Tätä Linux-ytimen ohjelmoijat päättelivät ja siksi hakemistossa tools/bpf ytimistä löydät kokoajan ja virheenkorjaajan työskennelläksesi klassisen BPF:n kanssa.
Assembly-kieli on hyvin samankaltainen kuin debug-tulosteen tcpdump, mutta lisäksi voimme määrittää symbolisia tunnisteita. Esimerkiksi tässä on ohjelma, joka pudottaa kaikki paketit paitsi TCP/IPv4:n:
$ cat /tmp/tcp-over-ipv4.bpf
ldh [12]
jne #0x800, drop
ldb [23]
jneq #6, drop
ret #-1
drop: ret #0
Oletusarvoisesti kokoaja luo koodin muodossa <количество инструкций>,<code1> <jt1> <jf1> <k1>,..., esimerkiksi TCP:n kanssa se on
Tämä teksti voidaan kopioida tyyppirakenteen määritelmään struct sock_filter, kuten teimme tämän osion alussa.
Linux- ja netsniff-ng-laajennukset
Standardin BPF:n lisäksi Linux ja tools/bpf/bpf_asm tukea ja ei-standardi setti. Periaatteessa ohjeita käytetään pääsyyn rakenteen kenttiin struct sk_buff, joka kuvaa ytimessä olevaa verkkopakettia. On kuitenkin olemassa myös muunlaisia apuohjeita mm ldw cpu latautuu rekisteriin A ydinfunktion suorittamisen tulos raw_smp_processor_id(). (BPF:n uudessa versiossa näitä epästandardeja laajennuksia on laajennettu tarjoamaan ohjelmille joukko ytimen apulaitteita muistiin, rakenteisiin ja tapahtumien luomiseen.) Tässä on mielenkiintoinen esimerkki suodattimesta, jossa kopioimme vain pakettien otsikot käyttäjätilaan laajennuksen avulla poff, hyötykuorman siirtymä:
ld poff
ret a
BPF-laajennuksia ei voi käyttää tcpdump, mutta tämä on hyvä syy tutustua hyödyllisyyspakettiin netsniff-ng, joka sisältää muun muassa edistyneen ohjelman netsniff-ng, joka sisältää BPF-suodatuksen lisäksi tehokkaan liikennegeneraattorin ja edistyneemmän kuin tools/bpf/bpf_asm, kutsui BPF:n kokoaja bpfc. Paketti sisältää melko yksityiskohtaisen dokumentaation, katso myös artikkelin lopussa olevat linkit.
seccomp
Joten tiedämme jo kuinka kirjoittaa mielivaltaisen monimutkaisia BPF-ohjelmia ja olemme valmiita katsomaan uusia esimerkkejä, joista ensimmäinen on seccomp-tekniikka, jonka avulla BPF-suodattimia käyttämällä voidaan hallita käytettävissä olevia järjestelmäkutsuargumenttien joukkoa ja joukkoa. tietty prosessi ja sen jälkeläiset.
Ensimmäinen seccomp-versio lisättiin ytimeen vuonna 2005, eikä se ollut kovin suosittu, koska se tarjosi vain yhden vaihtoehdon - rajoittaa prosessin käytettävissä olevien järjestelmäkutsujen joukko seuraaviin: read, write, exit и sigreturn, ja sääntöjä rikkonut prosessi tapettiin käyttämällä SIGKILL. Vuonna 2012 seccomp kuitenkin lisäsi mahdollisuuden käyttää BPF-suodattimia, jolloin voit määrittää joukon sallittuja järjestelmäkutsuja ja jopa tarkistaa niiden argumentteja. (Mielenkiintoista kyllä, Chrome oli yksi tämän toiminnon ensimmäisistä käyttäjistä, ja Chrome-ihmiset kehittävät parhaillaan KRSI-mekanismia, joka perustuu BPF:n uuteen versioon ja mahdollistaa Linuxin suojausmoduulien mukauttamisen.) Linkit lisädokumentaatioihin löytyvät lopusta. artikkelista.
Huomaa, että keskittimessä on jo kirjoitettu artikkeleita seccompin käytöstä, ehkä joku haluaa lukea ne ennen (tai sen sijaan) lukea seuraavat alakohdat. Artikkelissa Kontit ja turvallisuus: seccomp tarjoaa esimerkkejä seccompin käytöstä, sekä vuoden 2007 versiosta että BPF:ää käyttävästä versiosta (suodattimet luodaan libseccompilla), puhuu seccompin kytkemisestä Dockeriin ja tarjoaa myös monia hyödyllisiä linkkejä. Artikkelissa Demonien eristäminen systemd:llä tai "et tarvitse Dockeria tähän!" Se kattaa erityisesti kuinka lisätä mustia tai sallittuja listoja järjestelmäkutsuista systemd:tä käyttäville demoneille.
Seuraavaksi näemme, kuinka suodattimia kirjoitetaan ja ladataan seccomp paljaalla C:llä ja käyttämällä kirjastoa libseccomp ja mitkä ovat kunkin vaihtoehdon edut ja haitat, ja lopuksi katsotaan kuinka ohjelma käyttää seccompia strace.
Kirjoitus- ja lataussuodattimet seccompille
Osaamme jo kirjoittaa BPF-ohjelmia, joten katsotaanpa ensin seccomp-ohjelmointirajapintaa. Voit asettaa suodattimen prosessitasolla, ja kaikki aliprosessit perivät rajoitukset. Tämä tehdään järjestelmäkutsulla seccomp(2):
seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)
missä &filter - Tämä on osoitus meille jo tutusta rakenteesta struct sock_fprog, eli BPF ohjelma.
Miten seccomp-ohjelmat eroavat socket-ohjelmista? Lähetetty konteksti. Sockettien tapauksessa saimme paketin sisältävän muistialueen ja seccompin tapauksessa rakenteen kuten
Täällä nr on aloitettavan järjestelmäkutsun numero, arch - nykyinen arkkitehtuuri (lisätietoja alla), args - enintään kuusi järjestelmäkutsuargumenttia ja instruction_pointer on osoitin käyttäjätilan käskyyn, joka teki järjestelmäkutsun. Näin esimerkiksi järjestelmän kutsunumeron lataamiseen rekisteriin A meidän on sanottava
ldw [0]
seccomp-ohjelmissa on muitakin ominaisuuksia, esimerkiksi kontekstiin pääsee käsiksi vain 32-bittisellä tasauksella etkä voi ladata puolta sanaa tai tavua - kun yrität ladata suodatinta ldh [0] järjestelmäpuhelu seccomp palaa EINVAL. Toiminto tarkistaa ladatut suodattimet seccomp_check_filter() ytimiä. (Hauska asia on, että alkuperäisessä commitissa, joka lisäsi seccomp-toiminnon, he unohtivat lisätä luvan käyttää ohjetta tähän toimintoon mod (jakojäännös) ja se ei ole nyt saatavilla seccomp BPF -ohjelmille sen lisäämisen jälkeen katkeaa BI.)
Periaatteessa tiedämme jo kaiken seccomp-ohjelmien kirjoittamisesta ja lukemisesta. Yleensä ohjelmalogiikka on järjestetty valkoiseksi tai mustaksi listaksi järjestelmäkutsuista, esimerkiksi ohjelmasta
ld [0]
jeq #304, bad
jeq #176, bad
jeq #239, bad
jeq #279, bad
good: ret #0x7fff0000 /* SECCOMP_RET_ALLOW */
bad: ret #0
tarkistaa mustan listan neljästä järjestelmäkutsusta numeroilla 304, 176, 239, 279. Mitä nämä järjestelmäkutsut ovat? Emme voi sanoa varmaksi, koska emme tiedä, mille arkkitehtuurille ohjelma on kirjoitettu. Siksi seccompin kirjoittajat tarjous käynnistä kaikki ohjelmat arkkitehtuurin tarkistuksella (nykyinen arkkitehtuuri ilmoitetaan kontekstissa kentällä arch rakenteet struct seccomp_data). Kun arkkitehtuuri on tarkistettu, esimerkin alku näyttää tältä:
ld [4]
jne #0xc000003e, bad_arch ; SCMP_ARCH_X86_64
ja sitten järjestelmämme kutsunumerot saisivat tietyt arvot.
Kirjoitamme ja lataamme suodattimia seccomp-käyttöön libseccomp
Suodattimien kirjoittaminen natiivikoodiin tai BPF-kokoonpanoon mahdollistaa tuloksen täyden hallinnan, mutta samaan aikaan on joskus parempi käyttää kannettavaa ja/tai luettavaa koodia. Kirjasto auttaa meitä tässä libseccomp, joka tarjoaa vakiokäyttöliittymän mustien tai valkoisten suodattimien kirjoittamiseen.
Kirjoitetaan esimerkiksi ohjelma, joka suorittaa käyttäjän valitseman binaaritiedoston, kun hän on aiemmin asentanut mustan listan järjestelmäkutsuista yllä oleva artikkeli (ohjelmaa on yksinkertaistettu luettavuuden parantamiseksi, täysversio löytyy täällä):
#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]);
}
Ensin määritämme taulukon sys_numbers yli 40 estettävästä järjestelmänumerosta. Alusta sitten konteksti ctx ja kerro kirjastolle, mitä haluamme sallia (SCMP_ACT_ALLOW) oletuksena kaikki järjestelmäkutsut (mustien listojen luominen on helpompaa). Sitten lisäämme yksitellen kaikki järjestelmäkutsut mustalta listalta. Pyydämme vastauksena luettelosta tulevaan järjestelmäkutsuun SCMP_ACT_TRAP, tässä tapauksessa seccomp lähettää signaalin prosessille SIGSYS ja kuvauksen siitä, mikä järjestelmäpuhelu rikkoi sääntöjä. Lopuksi lataamme ohjelman ytimeen käyttämällä seccomp_load, joka kääntää ohjelman ja liittää sen prosessiin järjestelmäkutsulla seccomp(2).
Onnistunut kääntäminen edellyttää, että ohjelma on linkitetty kirjastoon libseccomp, esimerkiksi:
cc -std=c17 -Wall -Wextra -c -o seccomp_lib.o seccomp_lib.c
cc -o seccomp_lib seccomp_lib.o -lseccomp
Esimerkki onnistuneesta käynnistämisestä:
$ ./seccomp_lib echo ok
ok
Esimerkki estetystä järjestelmäkutsusta:
$ sudo ./seccomp_lib mount -t bpf bpf /tmp
Bad system call
käyttö straceyksityiskohtia varten:
$ 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
Mistä voimme tietää, että ohjelma lopetettiin laittoman järjestelmäkutsun käytön vuoksi mount(2).
Joten kirjoitimme suodattimen kirjaston avulla libseccomp, sovittamalla ei-triviaali koodin neljälle riville. Yllä olevassa esimerkissä, jos järjestelmäkutsuja on paljon, suoritusaikaa voidaan lyhentää huomattavasti, koska tarkistus on vain lista vertailuista. Optimointia varten libseccompilla oli äskettäin laastari mukana, joka lisää tuen suodatinattribuutille SCMP_FLTATR_CTL_OPTIMIZE. Tämän attribuutin arvoksi asettaminen 2 muuntaa suodattimen binäärihakuohjelmaksi.
Jos haluat nähdä, miten binäärihakusuodattimet toimivat, katso yksinkertainen käsikirjoitus, joka luo tällaisia ohjelmia BPF assemblerissä valitsemalla järjestelmän kutsunumerot, esimerkiksi:
$ 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
Et voi kirjoittaa mitään merkittävästi nopeammin, koska BPF-ohjelmat eivät voi suorittaa sisennyshyppyjä (emme voi tehdä esim. jmp A tai jmp [label+X]) ja siksi kaikki siirtymät ovat staattisia.
seccomp ja strace
Kaikki tietävät hyödyn strace on välttämätön työkalu prosessien käyttäytymisen tutkimiseen Linuxissa. Monet ovat kuitenkin myös kuulleet siitä suorituskykyongelmia kun käytät tätä apuohjelmaa. Tosiasia on, että strace toteutettu käyttämällä ptrace(2), ja tässä mekanismissa emme voi määritellä, missä järjestelmäkutsujen joukossa meidän on pysäytettävä prosessi, eli esimerkiksi komennot
$ 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
käsitellään suunnilleen samassa ajassa, vaikka toisessa tapauksessa haluamme jäljittää vain yhden järjestelmäkutsun.
Uusi vaihtoehto --seccomp-bpf, lisätty strace versio 5.3, mahdollistaa prosessin nopeuttamisen monta kertaa ja käynnistysaika yhden järjestelmäkutsun jälkeen on jo verrattavissa tavalliseen käynnistykseen:
$ 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
(Tässä on tietysti pieni petos, että emme jäljitä tämän komennon pääjärjestelmäkutsua. Jos jäljittäisimme esim. newfsstat, Sitten strace jarruttaisi yhtä voimakkaasti kuin ilman --seccomp-bpf.)
Miten tämä vaihtoehto toimii? Ilman häntä strace muodostaa yhteyden prosessiin ja aloittaa sen käytön PTRACE_SYSCALL. Kun hallittu prosessi lähettää (mikä tahansa) järjestelmäkutsun, ohjaus siirretään strace, joka tarkastelee järjestelmäkutsun argumentteja ja suorittaa sen kanssa PTRACE_SYSCALL. Jonkin ajan kuluttua prosessi päättää järjestelmäkutsun ja siitä poistuttaessa ohjaus siirtyy uudelleen strace, joka tarkastelee palautusarvoja ja aloittaa prosessin käyttämällä PTRACE_SYSCALL, ja niin edelleen.
Seccompilla tämä prosessi voidaan kuitenkin optimoida juuri niin kuin haluamme. Nimittäin, jos haluamme katsoa vain järjestelmäkutsua X, niin voimme kirjoittaa BPF-suodattimen, joka on X palauttaa arvon SECCOMP_RET_TRACE, ja puheluille, jotka eivät kiinnosta meitä - SECCOMP_RET_ALLOW:
ld [0]
jneq #X, ignore
trace: ret #0x7ff00000
ignore: ret #0x7fff0000
Tässä tapauksessa strace aloittaa prosessin aluksi nimellä PTRACE_CONT, suodattimemme käsitellään jokaiselle järjestelmäkutsulle, jos järjestelmäkutsu ei ole X, prosessi jatkuu, mutta jos tämä X, seccomp siirtää ohjauksen stracejoka tarkastelee argumentteja ja aloittaa prosessin kuten PTRACE_SYSCALL (koska seccomp ei pysty ajamaan ohjelmaa järjestelmäkutsusta poistuttaessa). Kun järjestelmäkutsu palaa, strace käynnistää prosessin uudelleen käyttämällä PTRACE_CONT ja odottaa uusia viestejä seccompilta.
Kun käytät vaihtoehtoa --seccomp-bpf on kaksi rajoitusta. Ensinnäkin ei ole mahdollista liittyä jo olemassa olevaan prosessiin (vaihtoehto -p ohjelmat strace), koska seccomp ei tue tätä. Toiseksi, mahdollisuutta ei ole ei katso lapsiprosesseja, koska seccomp-suodattimet perivät kaikki lapsiprosessit ilman mahdollisuutta poistaa tätä käytöstä.
Hieman tarkemmin kuinka tarkalleen strace työskennellä seccomp löytyy osoitteesta tuore raportti. Meille mielenkiintoisin tosiasia on, että seccompin edustama klassinen BPF on käytössä edelleen.
xt_bpf
Palataan nyt takaisin verkkojen maailmaan.
Taustaa: Kauan sitten, vuonna 2007, ydin oli lisätty moduuli xt_u32 netfilterille. Se kirjoitettiin analogisesti vielä muinaisemman liikenteen luokittelulaitteen kanssa cls_u32 ja voit kirjoittaa mielivaltaisia binäärisääntöjä iptablesille seuraavien yksinkertaisten toimintojen avulla: lataa 32 bittiä paketista ja suorita niille joukko aritmeettisia operaatioita. Esimerkiksi,
Lataa IP-otsikon 32 bittiä alkaen täytteestä 6 ja lisää niille maskin 0xFF (ota alhainen tavu). Tämä kenttä protocol IP-otsikko ja vertaamme sitä 1:een (ICMP). Voit yhdistää useita tarkistuksia yhteen sääntöön, ja voit myös suorittaa operaattorin @ - siirrä X tavua oikealle. Esimerkiksi sääntö
tarkistaa, onko TCP-sekvenssinumero erilainen 0x29. En mene yksityiskohtiin, koska on jo selvää, että tällaisten sääntöjen kirjoittaminen käsin ei ole kovin kätevää. Artikkelissa BPF - unohtunut tavukoodi, siellä on useita linkkejä, joissa on esimerkkejä käytöstä ja sääntöjen luomisesta xt_u32. Katso myös tämän artikkelin lopussa olevat linkit.
Vuodesta 2013 lähtien moduuli moduulin sijaan xt_u32 voit käyttää BPF-pohjaista moduulia xt_bpf. Jokaisen, joka on lukenut tähän asti, pitäisi jo olla selvä sen toimintaperiaatteesta: suorita BPF-tavukoodi iptables-säännöinä. Voit luoda uuden säännön esimerkiksi näin:
iptables -A INPUT -m bpf --bytecode <байткод> -j LOG
täällä <байткод> - tämä on koodi assembler-tulostusmuodossa bpf_asm oletuksena esim.
Tässä esimerkissä suodatamme kaikki UDP-paketit. Konteksti BPF-ohjelmalle moduulissa xt_bpf, tietenkin, osoittaa pakettidataan, iptablesin tapauksessa IPv4-otsikon alkuun. Palautusarvo BPF-ohjelmasta booleanMissä false tarkoittaa, että paketti ei täsmää.
On selvää, että moduuli xt_bpf tukee monimutkaisempia suodattimia kuin yllä oleva esimerkki. Katsotaanpa todellisia esimerkkejä Cloudfaresta. Viime aikoihin asti he käyttivät moduulia xt_bpf suojaamaan DDoS-hyökkäyksiä vastaan. Artikkelissa Esittelyssä BPF-työkalut he selittävät, kuinka (ja miksi) he luovat BPF-suodattimia ja julkaisevat linkkejä tällaisten suodattimien luomiseen tarkoitettuihin apuohjelmiin. Esimerkiksi apuohjelman avulla bpfgen voit luoda BPF-ohjelman, joka vastaa nimen DNS-kyselyä 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
Ohjelmassa lataamme ensin rekisteriin X rivin alun osoite x04habrx03comx00 UDP-datagrammin sisällä ja tarkista sitten pyyntö: 0x04686162 <-> "x04hab" jne.
Hieman myöhemmin Cloudfare julkaisi p0f -> BPF-kääntäjäkoodin. Artikkelissa Esittelyssä p0f BPF -kääntäjä he puhuvat siitä, mitä p0f on ja kuinka p0f-allekirjoitukset muunnetaan BPF:ksi:
Tällä hetkellä ei enää käytä Cloudfarea xt_bpf, koska he siirtyivät XDP:hen - yksi BPF:n uuden version käyttövaihtoehdoista, katso. L4Drop: XDP DDoS Mitigations.
cls_bpf
Viimeinen esimerkki klassisen BPF:n käytöstä ytimessä on luokitin cls_bpf Linuxin liikenteenohjauksen alijärjestelmää varten, joka lisättiin Linuxiin vuoden 2013 lopussa ja korvaa käsitteellisesti vanhan cls_u32.
Emme kuitenkaan nyt kuvaile työtä cls_bpf, koska klassisen BPF:n tietämyksen kannalta tämä ei anna meille mitään - olemme jo tutustuneet kaikkiin toimintoihin. Lisäksi seuraavissa artikkeleissa, joissa puhutaan Extended BPF:stä, tapaamme tämän luokituksen useammin kuin kerran.
Toinen syy olla puhumatta klassisen BPF c:n käytöstä cls_bpf Ongelmana on, että Extended BPF:ään verrattuna sovellettavuus on tässä tapauksessa kapeampi: klassiset ohjelmat eivät voi muuttaa pakettien sisältöä eivätkä tallentaa tilaa puhelujen välillä.
Joten on aika sanoa hyvästit klassiselle BPF:lle ja katsoa tulevaisuuteen.
Hyvästit klassiselle BPF:lle
Tarkastelimme, kuinka 32-luvun alussa kehitetty BPF-tekniikka eli neljännesvuosisadan menestyksekkäästi ja löysi loppuun asti uusia sovelluksia. Kuitenkin samalla tavalla kuin siirtyminen pinokoneista RISC:hen, joka toimi sysäyksenä klassisen BPF:n kehitykselle, 64-luvulla tapahtui siirtyminen XNUMX-bittisistä koneista XNUMX-bittisiin koneisiin ja klassinen BPF alkoi vanhentua. Lisäksi klassisen BPF:n ominaisuudet ovat hyvin rajalliset, ja vanhentuneen arkkitehtuurin lisäksi meillä ei ole mahdollisuutta tallentaa tilaa BPF-ohjelmien kutsujen välillä, ei ole mahdollisuutta suoraan käyttäjän vuorovaikutukseen, ei ole mahdollisuutta vuorovaikutukseen ytimen kanssa, lukuun ottamatta rajoitetun määrän rakennekenttien lukemista sk_buff ja käynnistämällä yksinkertaisimmat aputoiminnot, et voi muuttaa pakettien sisältöä ja ohjata niitä uudelleen.
Itse asiassa tällä hetkellä Linuxin klassisesta BPF:stä on jäljellä vain API-rajapinta, ja ytimen sisällä kaikki klassiset ohjelmat, olivatpa ne sitten socket-suodattimet tai seccomp-suodattimet, käännetään automaattisesti uuteen muotoon, Extended BPF -muotoon. (Puhumme tarkalleen kuinka tämä tapahtuu seuraavassa artikkelissa.)
Siirtyminen uuteen arkkitehtuuriin alkoi vuonna 2013, kun Aleksei Starovoitov ehdotti BPF-päivitysjärjestelmää. Vuonna 2014 vastaavat korjaukset alkoi ilmestyä ytimessä. Ymmärtääkseni alkuperäinen suunnitelma oli vain optimoida arkkitehtuuri ja JIT-kääntäjä toimimaan tehokkaammin 64-bittisissä koneissa, mutta sen sijaan nämä optimoinnit merkitsivät uuden luvun alkua Linux-kehityksessä.
Tämän sarjan muut artikkelit käsittelevät uuden tekniikan arkkitehtuuria ja sovelluksia, jotka tunnettiin alun perin nimellä sisäinen BPF, sitten laajennettu BPF ja nyt yksinkertaisesti BPF.
viittaukset
Steven McCanne ja Van Jacobson, "BSD Packet Filter: A New Architecture for User-level Packet Capture", https://www.tcpdump.org/papers/bpf-usenix93.pdf
Steven McCanne, "libpcap: An Architecture and Optimization Methodology for Packet Capture", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf