BPF mažiesiems, nulinė dalis: klasikinis BPF

„Berkeley Packet Filters“ (BPF) yra „Linux“ branduolio technologija, kuri jau keletą metų yra pirmuosiuose anglų kalba leidžiamų technologijų leidinių puslapiuose. Konferencijose gausu pranešimų apie BPF naudojimą ir plėtrą. Davidas Milleris, „Linux“ tinklo posistemės prižiūrėtojas, vadina savo kalbą „Linux Plumbers 2018“. „Ši kalba ne apie XDP“ (XDP yra vienas BPF naudojimo atvejis). Brendanas Greggas skaito pokalbius Linux BPF supergalios. Toke Høiland-Jørgensen juokiasikad branduolys dabar yra mikrobranduolys. Tomas Grafas propaguoja idėją BPF yra javascript branduoliui.

Vis dar nėra sistemingo BPF aprašymo Habré, todėl straipsnių serijoje pabandysiu papasakoti apie technologijos istoriją, aprašyti architektūrą ir kūrimo įrankius bei apibūdinti BPF taikymo ir praktikos sritis. Šis straipsnis, nulis, serijoje, pasakoja apie klasikinio BPF istoriją ir architektūrą, taip pat atskleidžia jo veikimo principų paslaptis. tcpdump, seccomp, strace, ir daug daugiau.

BPF kūrimą kontroliuoja „Linux“ tinklo bendruomenė, pagrindinės esamos BPF programos yra susijusios su tinklais, todėl su leidimu @eukariotas, serialą pavadinau „BPF mažiesiems“, puikaus serialo garbei „Tinklai mažiesiems“.

Trumpas BPF istorijos kursas (c)

Šiuolaikinė BPF technologija yra patobulinta ir išplėsta senosios technologijos versija tuo pačiu pavadinimu, dabar vadinama klasikiniu BPF, kad būtų išvengta painiavos. Klasikinio BPF pagrindu buvo sukurta gerai žinoma programa tcpdump, mechanizmas seccomp, taip pat mažiau žinomi moduliai xt_bpfiptables ir klasifikatorius cls_bpf. Šiuolaikinėje Linux sistemoje klasikinės BPF programos automatiškai verčiamos į naują formą, tačiau, žiūrint iš vartotojo pusės, API išliko vietoje ir vis dar randama naujų klasikinio BPF panaudojimo būdų, kaip matysime šiame straipsnyje. Dėl šios priežasties, taip pat dėl ​​to, kad sekant klasikinio BPF kūrimo Linux sistemoje istoriją taps aiškiau, kaip ir kodėl jis išsivystė į šiuolaikinę formą, nusprendžiau pradėti nuo straipsnio apie klasikinį BPF.

Praėjusio amžiaus aštuntojo dešimtmečio pabaigoje inžinieriai iš garsiosios Lawrence'o Berkeley laboratorijos susidomėjo klausimu, kaip tinkamai filtruoti tinklo paketus aparatinėje įrangoje, kuri buvo moderni praėjusio amžiaus aštuntojo dešimtmečio pabaigoje. Pagrindinė filtravimo idėja, iš pradžių įgyvendinta CSPF (CMU/Stanford Packet Filter) technologijoje, buvo kuo anksčiau filtruoti nereikalingus paketus, t.y. branduolio erdvėje, nes taip išvengiama nereikalingų duomenų kopijavimo į vartotojo erdvę. Siekiant užtikrinti vykdymo saugą naudotojo kodui paleisti branduolio erdvėje, buvo naudojama smėlio dėžės virtuali mašina.

Tačiau esamų filtrų virtualios mašinos buvo sukurtos taip, kad veiktų dėklo pagrindu veikiančiose mašinose ir neveikė taip efektyviai naujesniuose RISC įrenginiuose. Dėl to Berkeley Labs inžinierių pastangomis buvo sukurta nauja BPF (Berkeley Packet Filters) technologija, kurios virtualios mašinos architektūra sukurta remiantis Motorola 6502 procesoriumi – tokių gerai žinomų produktų kaip darbo arkliu. "Apple II" arba NENURODYTI KITOJE VIETOJE. Naujoji virtuali mašina, palyginti su esamais sprendimais, padidino filtro našumą dešimtis kartų.

BPF mašinos architektūra

Darbiškai susipažinsime su architektūra, analizuodami pavyzdžius. Tačiau pirmiausia tarkime, kad mašina turėjo du 32 bitų registrus, prieinamus vartotojui, akumuliatorių. A ir indeksų registrą X, 64 baitai atminties (16 žodžių), galima rašyti ir vėliau skaityti, ir nedidelė komandų sistema darbui su šiais objektais. Programose buvo ir šuolio instrukcijos, skirtos sąlyginėms išraiškoms įgyvendinti, tačiau norint garantuoti, kad programa būtų baigta laiku, šuolius buvo galima daryti tik į priekį, t.y., ypač, buvo draudžiama kurti kilpas.

Bendra mašinos paleidimo schema yra tokia. Vartotojas sukuria BPF architektūros programą ir, naudodamas kai kurie branduolio mechanizmas (pvz., sistemos iškvietimas), įkelia ir sujungia programą kai kuriems į įvykių generatorių branduolyje (pavyzdžiui, įvykis yra kito paketo atėjimas į tinklo plokštę). Kai įvyksta įvykis, branduolys paleidžia programą (pavyzdžiui, interpretatoriuje), o mašinos atmintis atitinka kai kuriems branduolio atminties sritis (pavyzdžiui, gaunamo paketo duomenys).

To pakaks, kad pradėtume žvalgytis į pavyzdžius: pagal poreikį susipažinsime su sistema ir komandų formatu. Jei norite nedelsiant ištirti virtualios mašinos komandų sistemą ir sužinoti apie visas jos galimybes, galite perskaityti originalų straipsnį BSD paketų filtras ir (arba) pirmąją bylos pusę Dokumentacija/tinklas/filter.txt iš branduolio dokumentacijos. Be to, galite studijuoti pristatymą libpcap: Paketų fiksavimo architektūra ir optimizavimo metodika, kuriame McCanne, vienas iš BPF autorių, pasakoja apie kūrybos istoriją libpcap.

Dabar pereiname prie visų reikšmingų klasikinio BPF naudojimo Linux sistemoje pavyzdžių: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.

tcpdump

BPF kūrimas buvo vykdomas lygiagrečiai su paketų filtravimo priekinės dalies kūrimu - gerai žinomu įrankiu. tcpdump. Ir kadangi tai yra seniausias ir žinomiausias klasikinio BPF naudojimo pavyzdys, prieinamas daugelyje operacinių sistemų, mes pradėsime nuo jo technologijos tyrimą.

(Visus šiame straipsnyje pateiktus pavyzdžius paleidau „Linux“. 5.6.0-rc6. Kai kurių komandų išvestis buvo redaguota, kad būtų geriau skaitoma.)

Pavyzdys: IPv6 paketų stebėjimas

Įsivaizduokime, kad norime peržiūrėti visus IPv6 paketus sąsajoje eth0. Norėdami tai padaryti, galime paleisti programą tcpdump su paprastu filtru ip6:

$ sudo tcpdump -i eth0 ip6

Šiuo atveju, tcpdump sukompiliuoja filtrą ip6 į BPF architektūros baito kodą ir nusiųskite jį į branduolį (žr Tcpdump: įkeliama). Įkeltas filtras bus paleistas kiekvienam paketui, praeinančiam per sąsają eth0. Jei filtras grąžina ne nulinę reikšmę n, tada iki n baitai paketo bus nukopijuoti į vartotojo erdvę ir pamatysime jį išvestyje tcpdump.

BPF mažiesiems, nulinė dalis: klasikinis BPF

Pasirodo, nesunkiai galime sužinoti, kuris baito kodas buvo išsiųstas į branduolį tcpdump su pagalba tcpdump, jei paleisime su parinktimi -d:

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

Nulinėje eilutėje vykdome komandą ldh [12], kuris reiškia „įkelti į registrą A pusė žodžio (16 bitų), esančio 12“ adresu, ir tik klausimas, į kokią atmintį mes kreipiamės? Atsakymas yra tas, kad x prasideda (x+1)analizuojamo tinklo paketo baitas. Mes skaitome paketus iš Ethernet sąsajos eth0, ir šis reiškia,kad paketas atrodo taip (paprastumo dėlei darome prielaidą, kad pakete nėra VLAN žymų):

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

Taigi įvykdžius komandą ldh [12] registre A bus laukas Ether Type — šiame Ethernet rėmelyje perduodamo paketo tipas. 1 eilutėje palyginame registro turinį A (paketo tipas) c 0x86dd, ir šis ir turi Mus dominantis tipas yra IPv6. 1 eilutėje, be palyginimo komandos, yra dar du stulpeliai - jt 2 и jf 3 - pažymiai, į kuriuos reikia eiti, jei palyginimas sėkmingas (A == 0x86dd) ir nesėkmingai. Taigi sėkmingu atveju (IPv6) einame į 2 eilutę, o nesėkmingu - į 3 eilutę. 3 eilutėje programa baigiasi kodu 0 (nekopijuokite paketo), 2 eilutėje programa baigiasi kodu 262144 (nukopijuokite man ne daugiau kaip 256 kilobaitų paketą).

Sudėtingesnis pavyzdys: mes žiūrime į TCP paketus pagal paskirties prievadą

Pažiūrėkime, kaip atrodo filtras, kuris kopijuoja visus TCP paketus su paskirties prievadu 666. Nagrinėsime IPv4 atvejį, nes IPv6 atvejis yra paprastesnis. Išstudijavę šį pavyzdį, galite patys ištirti IPv6 filtrą kaip pratimą (ip6 and tcp dst port 666) ir filtras bendram atvejui (tcp dst port 666). Taigi, mus dominantis filtras atrodo taip:

$ 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

Mes jau žinome, ką daro eilutės 0 ir 1. 2 eilutėje jau patikrinome, ar tai IPv4 paketas (Ether Type = 0x800) ir įkelkite jį į registrą A 24-asis paketo baitas. Mūsų pakuotė atrodo taip

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

o tai reiškia, kad įkeliame į registrą A IP antraštės Protocol lauką, o tai logiška, nes norime kopijuoti tik TCP paketus. Protokolą lyginame su 0x6 (IPPROTO_TCP) 3 eilutėje.

4 ir 5 eilutėse įkeliame pusžodžius, esančius adresu 20, ir naudojame komandą jset patikrinkite, ar nustatytas vienas iš trijų vėliavos - dėvėti išduotą kaukę jset trys svarbiausi bitai išvalomi. Du iš trijų bitų nurodo, ar paketas yra suskaidyto IP paketo dalis ir, jei taip, ar tai paskutinis fragmentas. Trečiasis bitas yra rezervuotas ir turi būti nulis. Nenorime tikrinti nei nepilnų, nei sugadintų paketų, todėl tikriname visus tris bitus.

6 eilutė yra įdomiausia šiame sąraše. Išraiška ldxb 4*([14]&0xf) reiškia, kad įkeliame į registrą X mažiausiai reikšmingi keturi penkiolikto paketo baito bitai, padauginti iš 4. Mažiausiai reikšmingi keturi penkioliktojo baito bitai yra laukas Interneto antraštės ilgis IPv4 antraštė, kurioje saugomas antraštės ilgis žodžiais, todėl tuomet reikia padauginti iš 4. Įdomu tai, kad išraiška 4*([14]&0xf) yra specialios adresavimo schemos žymėjimas, kuris gali būti naudojamas tik tokia forma ir tik registrui X, t.y. mes taip pat negalime pasakyti ldb 4*([14]&0xf) nei ldxb 5*([14]&0xf) (galime nurodyti tik kitokį poslinkį, pvz. ldxb 4*([16]&0xf)). Akivaizdu, kad ši adresavimo schema buvo pridėta prie BPF būtent tam, kad gautų X (indekso registras) IPv4 antraštės ilgis.

Taigi 7 eilutėje bandome įkelti pusę žodžio at (X+16). Prisimenant, kad 14 baitų užima Ethernet antraštė, ir X yra IPv4 antraštės ilgis, suprantame, kad A Įkeltas TCP paskirties prievadas:

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

Galiausiai 8 eilutėje palyginame paskirties prievadą su norima reikšme ir 9 arba 10 eilutėse grąžiname rezultatą – kopijuoti paketą ar ne.

Tcpdump: įkeliama

Ankstesniuose pavyzdžiuose mes konkrečiai nenagrinėjome, kaip tiksliai įkeliame BPF baitinį kodą į branduolį, kad būtų galima filtruoti paketus. Paprastai kalbant, tcpdump perkeliama į daugelį sistemų ir darbui su filtrais tcpdump naudojasi biblioteka libpcap. Trumpai tariant, norėdami įdėti filtrą į sąsają naudojant libpcap, turite atlikti šiuos veiksmus:

Norėdami pamatyti, kaip veikia pcap_setfilter įdiegtas Linux, mes naudojame strace (kai kurios eilutės pašalintos):

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

Pirmose dviejose išvesties eilutėse sukuriame neapdorotas lizdas norėdami perskaityti visus eterneto kadrus ir susieti jį su sąsaja eth0, Nuo pirmasis mūsų pavyzdys mes žinome, kad filtras ip sudarys keturios BPF instrukcijos, o trečioje eilutėje matome, kaip naudoti šią parinktį SO_ATTACH_FILTER sistemos skambutis setsockopt įkeliame ir prijungiame 4 ilgio filtrą. Tai mūsų filtras.

Verta pažymėti, kad klasikiniame BPF filtro įkėlimas ir prijungimas visada vyksta kaip atominė operacija, o naujoje BPF versijoje programos įkėlimas ir susiejimas su įvykių generatoriumi yra atskirti laike.

Paslėpta Tiesa

Šiek tiek išsamesnė išvesties versija atrodo taip:

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

Kaip minėta pirmiau, mes įkeliame ir prijungiame filtrą prie 5 linijos lizdo, bet kas nutinka 3 ir 4 eilutėse? Pasirodo, kad šis libpcap rūpinasi mumis – kad mūsų filtro išvestyje nepatektų jo netenkinančių paketų, biblioteka jungiasi manekenas filtras ret #0 (atmesti visus paketus), perjungia lizdą į neblokavimo režimą ir bando atimti visus paketus, kurie galėjo likti iš ankstesnių filtrų.

Iš viso, norėdami filtruoti paketus „Linux“ naudodami klasikinį BPF, turite turėti tokios struktūros filtrą kaip struct sock_fprog ir atviras lizdas, po kurio filtrą galima pritvirtinti prie lizdo naudojant sistemos skambutį setsockopt.

Įdomu tai, kad filtrą galima pritvirtinti prie bet kokio lizdo, ne tik neapdoroto. Čia pavyzdys programa, kuri iš visų gaunamų UDP datagramų atjungia visus, išskyrus pirmuosius du, baitus. (Į kodą pridėjau komentarų, kad neperkrautų straipsnio.)

Daugiau informacijos apie naudojimą setsockopt apie filtrų prijungimą žr lizdas (7), bet apie tai, kaip rašyti savo filtrus struct sock_fprog be pagalbos tcpdump kalbėsime skyriuje BPF programavimas savo rankomis.

Klasikinis BPF ir XXI a

BPF buvo įtrauktas į Linux 1997 m. ir ilgą laiką išliko darbinis arkliukas libpcap be jokių ypatingų pakeitimų (žinoma, specifiniai „Linux“ pakeitimai, buvo, bet jie nepakeitė pasaulinio vaizdo). Pirmieji rimti ženklai, kad BPF vystysis, atsirado 2011 m., kai Ericas Dumazetas pasiūlė pleistras, kuris prideda prie branduolio „Just In Time“ kompiliatorių - vertėją, skirtą BPF baito kodui konvertuoti į vietinį x86_64 kodas.

JIT kompiliatorius buvo pirmasis pokyčių grandinėje: 2012 m pasirodė galimybė rašyti filtrus sekkomp, naudojant BPF, 2013 m. sausio mėn pridėta modulis xt_bpf, kuri leidžia rašyti taisykles iptables su BPF pagalba, o 2013 metų spalį buvo pridėta taip pat modulis cls_bpf, kuri leidžia rašyti srauto klasifikatorius naudojant BPF.

Netrukus apžvelgsime visus šiuos pavyzdžius išsamiau, bet pirmiausia mums bus naudinga išmokti rašyti ir kompiliuoti savavališkas BPF programas, nes bibliotekos suteikiamos galimybės libpcap ribotas (paprastas pavyzdys: sugeneruotas filtras libpcap gali grąžinti tik dvi reikšmes - 0 arba 0x40000) arba apskritai, kaip ir seccomp atveju, netaikomos.

BPF programavimas savo rankomis

Susipažinkime su dvejetainiu BPF instrukcijų formatu, tai labai paprasta:

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

Kiekviena instrukcija užima 64 bitus, iš kurių pirmieji 16 bitų yra instrukcijos kodas, tada yra dvi aštuonių bitų įtraukos, jt и jf, ir 32 bitai argumentui K, kurio paskirtis skiriasi priklausomai nuo komandos. Pavyzdžiui, komanda ret, kuris baigia programą, turi kodą 6, o grąžinama vertė paimama iš konstantos K. C, viena BPF instrukcija vaizduojama kaip struktūra

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

o visa programa yra struktūros pavidalu

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

Taigi, mes jau galime rašyti programas (pavyzdžiui, žinome instrukcijų kodus iš [1]). Taip atrodys filtras ip6pirmasis mūsų pavyzdys:

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

programa prog galime legaliai naudoti skambutyje

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

Programų rašymas mašininių kodų pavidalu nėra labai patogus, bet kartais būtinas (pavyzdžiui, derinant, kurti vienetų testus, rašyti straipsnius apie Habré ir pan.). Patogumui – byloje <linux/filter.h> Apibrėžiamos pagalbinės makrokomandos – tą patį pavyzdį, kaip nurodyta aukščiau, galima perrašyti kaip

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

Tačiau ši parinktis nėra labai patogi. Taip samprotavo Linux branduolio programuotojai, taigi ir kataloge tools/bpf branduoliuose galite rasti surinkėją ir derintuvą darbui su klasikiniu BPF.

Surinkimo kalba yra labai panaši į derinimo išvestį tcpdump, bet papildomai galime nurodyti simbolines etiketes. Pavyzdžiui, čia yra programa, kuri atmeta visus paketus, išskyrus TCP/IPv4:

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

Pagal numatytuosius nustatymus surinkėjas generuoja kodą tokiu formatu <количество инструкций>,<code1> <jt1> <jf1> <k1>,..., mūsų pavyzdys su TCP taip bus

$ 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 programuotojų patogumui galima naudoti kitą išvesties formatą:

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

Šį tekstą galima nukopijuoti į tipo struktūros apibrėžimą struct sock_filter, kaip tai padarėme šios dalies pradžioje.

„Linux“ ir „netsniff-ng“ plėtiniai

Be standartinio BPF, Linux ir tools/bpf/bpf_asm palaikyti ir nestandartinis komplektas. Iš esmės instrukcijos naudojamos norint pasiekti struktūros laukus struct sk_buff, kuris apibūdina tinklo paketą branduolyje. Tačiau yra ir kitų tipų pagalbinių nurodymų, pavyzdžiui ldw cpu bus įkeltas į registrą A branduolio funkcijos vykdymo rezultatas raw_smp_processor_id(). (Naujoje BPF versijoje šie nestandartiniai plėtiniai buvo išplėsti, kad programoms būtų suteiktas branduolio pagalbinių elementų rinkinys, skirtas pasiekti atmintį, struktūras ir generuoti įvykius.) Štai įdomus filtro, kuriame kopijuojame tik paketų antraštes į vartotojo erdvę naudojant plėtinį poff, naudingosios apkrovos poslinkis:

ld poff
ret a

BPF plėtinių negalima naudoti tcpdump, tačiau tai yra gera priežastis susipažinti su komunalinių paslaugų paketu netsniff-ng, kuriame, be kita ko, yra pažangi programa netsniff-ng, kuriame, be filtravimo naudojant BPF, taip pat yra efektyvus srauto generatorius ir pažangesnis nei tools/bpf/bpf_asm, paskambino BPF surinkėjas bpfc. Pakuotėje yra gana išsami dokumentacija, taip pat žiūrėkite nuorodas straipsnio pabaigoje.

sekkomp

Taigi, mes jau žinome, kaip rašyti savavališko sudėtingumo BPF programas ir esame pasirengę pažvelgti į naujus pavyzdžius, iš kurių pirmasis yra seccomp technologija, leidžianti, naudojant BPF filtrus, valdyti sistemos iškvietimo argumentų rinkinį ir rinkinį. duotas procesas ir jo palikuonys.

Pirmoji seccomp versija buvo pridėta prie branduolio 2005 m. ir nebuvo labai populiari, nes joje buvo tik viena parinktis – apriboti procesui prieinamų sistemos iškvietimų rinkinį iki šių: read, write, exit и sigreturn, o taisykles pažeidęs procesas buvo nužudytas naudojant SIGKILL. Tačiau 2012 m. seccomp pridėjo galimybę naudoti BPF filtrus, leidžiančius apibrėžti leidžiamų sistemos iškvietimų rinkinį ir netgi patikrinti jų argumentus. (Įdomu tai, kad „Chrome“ buvo vienas pirmųjų šios funkcijos naudotojų, o „Chrome“ žmonės šiuo metu kuria KRSI mechanizmą, pagrįstą nauja BPF versija ir leidžiančiu tinkinti „Linux“ saugos modulius.) Nuorodų į papildomą dokumentaciją rasite pabaigoje. straipsnio.

Atkreipkite dėmesį, kad centre jau buvo straipsnių apie seccomp naudojimą, galbūt kas nors norės juos perskaityti prieš (arba vietoj) skaitydamas kitus poskyrius. Straipsnyje Konteineriai ir apsauga: seccomp pateikiami seccomp naudojimo pavyzdžiai, tiek 2007 m. versija, tiek versija naudojant BPF (filtrai generuojami naudojant libseccomp), kalbama apie seccomp ryšį su Docker, taip pat pateikiama daug naudingų nuorodų. Straipsnyje Demonų išskyrimas naudojant systemd arba "tam nereikia Docker!" Jame visų pirma aprašoma, kaip įtraukti sistemos iškvietimų juoduosius sąrašus arba baltuosius sąrašus, skirtus demonams, kuriuose veikia systemd.

Toliau pamatysime, kaip rašyti ir įkelti filtrus seccomp be C ir naudojant biblioteką libseccomp ir kokie yra kiekvienos parinkties privalumai ir trūkumai, ir galiausiai pažiūrėkime, kaip programa naudoja seccomp strace.

Rašymo ir įkėlimo filtrai, skirti seccomp

Mes jau žinome, kaip rašyti BPF programas, todėl pirmiausia pažvelkime į seccomp programavimo sąsają. Galite nustatyti filtrą proceso lygiu, o visi antriniai procesai paveldės apribojimus. Tai atliekama naudojant sistemos skambutį seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

kur &filter - tai rodyklė į mums jau pažįstamą struktūrą struct sock_fprog, t.y. BPF programa.

Kuo „seccomp“ skirtos programos skiriasi nuo „socket“ programų? Perduotas kontekstas. Lizdų atveju mums buvo suteikta atminties sritis, kurioje yra paketas, o seccomp atveju mums buvo suteikta tokia struktūra kaip

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

Čia nr yra sistemos skambučio, kurį reikia pradėti, numeris, arch - dabartinė architektūra (daugiau apie tai žemiau), args - iki šešių sistemos iškvietimo argumentų ir instruction_pointer yra žymeklis į vartotojo erdvės nurodymą, kuris iškvietė sistemą. Pavyzdžiui, į registrą įkelti sistemos skambučio numerį A turime pasakyti

ldw [0]

Yra ir kitų seccomp programoms skirtų funkcijų, pavyzdžiui, kontekstą galima pasiekti tik 32 bitų lygiavimu ir negalite įkelti pusės žodžio ar baito – bandant įkelti filtrą ldh [0] sistemos skambutis seccomp grįš EINVAL. Funkcija tikrina įkeltus filtrus seccomp_check_filter() branduoliai. (Juokinga tai, kad pradiniame įsipareigojime, kuriame buvo pridėta seccomp funkcija, jie pamiršo pridėti leidimą naudoti šios funkcijos nurodymus mod (padalinio likutis) ir dabar nepasiekiamas seccomp BPF programoms, nes buvo pridėtas sulaužys ABI.)

Iš esmės mes jau žinome viską, kad galėtume rašyti ir skaityti seccomp programas. Paprastai programos logika yra išdėstyta kaip baltas arba juodas sistemos iškvietimų sąrašas, pavyzdžiui, programa

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

patikrina keturių sistemos skambučių, numeruotų 304, 176, 239, 279, juodąjį sąrašą. Kas tai yra sistemos skambučiai? Negalime tiksliai pasakyti, nes nežinome, kokiai architektūrai programa buvo parašyta. Todėl seccomp autoriai pasiūlymas paleiskite visas programas su architektūros patikrinimu (dabartinė architektūra kontekste nurodoma kaip laukas arch struktūra struct seccomp_data). Patikrinus architektūrą, pavyzdžio pradžia atrodytų taip:

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

ir tada mūsų sistemos skambučių numeriai gautų tam tikras reikšmes.

Rašome ir įkeliame filtrus, skirtus seccomp libseccomp

Filtrų rašymas vietiniame kode arba BPF rinkinyje leidžia visiškai kontroliuoti rezultatą, tačiau tuo pat metu kartais pageidautina turėti nešiojamąjį ir (arba) skaitomą kodą. Biblioteka mums tai padės libseccomp, kuri suteikia standartinę sąsają juodos arba baltos spalvos filtrų rašymui.

Pavyzdžiui, parašykime programą, kuri paleidžia vartotojo pasirinktą dvejetainį failą, prieš tai įdiegęs juodąjį sistemos skambučių sąrašą iš aukščiau esantis straipsnis (programa buvo supaprastinta, kad būtų lengviau skaitoma, galima rasti pilną versiją čia):

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

Pirmiausia apibrėžiame masyvą sys_numbers iš daugiau nei 40 sistemos skambučių numerių, kuriuos norite blokuoti. Tada inicijuokite kontekstą ctx ir pasakykite bibliotekai, ką norime leisti (SCMP_ACT_ALLOW) pagal numatytuosius nustatymus visi sistemos iškvietimai (juoduosius sąrašus sudaryti lengviau). Tada po vieną įtraukiame visus sistemos skambučius iš juodojo sąrašo. Atsakydami į sistemos iškvietimą iš sąrašo, prašome SCMP_ACT_TRAP, tokiu atveju seccomp siųs signalą procesui SIGSYS su aprašymu, kuris sistemos skambutis pažeidė taisykles. Galiausiai programą įkeliame į branduolį naudodami seccomp_load, kuri sukompiliuos programą ir pridės ją prie proceso naudodama sistemos iškvietimą seccomp(2).

Norint sėkmingai kompiliuoti, programa turi būti susieta su biblioteka libseccomp, pavyzdžiui:

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

Sėkmingo paleidimo pavyzdys:

$ ./seccomp_lib echo ok
ok

Užblokuoto sistemos skambučio pavyzdys:

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

Mes naudojame stracedaugiau informacijos:

$ 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

kaip mes galime žinoti, kad programa buvo nutraukta dėl neteisėto sistemos iškvietimo mount(2).

Taigi, mes parašėme filtrą naudodami biblioteką libseccomp, sutalpindamas ne trivialų kodą į keturias eilutes. Aukščiau pateiktame pavyzdyje, jei yra daug sistemos iškvietimų, vykdymo laikas gali būti pastebimai sutrumpintas, nes patikrinimas yra tik palyginimų sąrašas. Optimizavimui libseccomp neseniai turėjo pridedamas pleistras, kuris prideda filtro atributo palaikymą SCMP_FLTATR_CTL_OPTIMIZE. Nustačius šį atributą 2, filtras bus konvertuojamas į dvejetainę paieškos programą.

Jei norite pamatyti, kaip veikia dvejetainės paieškos filtrai, pažiūrėkite paprastas scenarijus, kuri tokias programas generuoja BPF asamblėjoje rinkdama sistemos skambučių numerius, pavyzdžiui:

$ 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

Jūs negalėsite nieko parašyti žymiai greičiau, nes BPF programos negali atlikti įtraukos šuolių (mes negalime, pvz. jmp A arba jmp [label+X]), todėl visi perėjimai yra statiniai.

seccomp ir strace

Visi žino naudingumą strace yra nepakeičiamas įrankis procesų elgsenai Linux sistemoje tirti. Tačiau daugelis girdėjo ir apie veiklos problemos kai naudojate šią priemonę. Faktas yra tas strace įgyvendintas naudojant ptrace(2), ir šiame mechanizme negalime nurodyti, kuriame sistemos iškvietimų rinkinyje turime sustabdyti procesą, t. y., pavyzdžiui, komandas.

$ 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

apdorojami maždaug per tą patį laiką, nors antruoju atveju norime atsekti tik vieną sistemos iškvietimą.

Naujas variantas --seccomp-bpf, pridėtas prie strace 5.3 versija leidžia daug kartų pagreitinti procesą, o paleidimo laikas po vieno sistemos skambučio jau yra panašus į įprasto paleidimo laiką:

$ 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

(Čia, žinoma, yra nedidelė apgaulė, nes mes neatsekame pagrindinio šios komandos sistemos iškvietimo. Jei sektume, pvz. newfsstat, Tada strace stabdytų taip pat stipriai kaip ir be --seccomp-bpf.)

Kaip veikia ši parinktis? Be jos strace prisijungia prie proceso ir pradeda jį naudoti PTRACE_SYSCALL. Kai valdomas procesas iškviečia (bet kurį) sistemos iškvietimą, valdymas perduodamas strace, kuris peržiūri sistemos iškvietimo argumentus ir paleidžia jį su PTRACE_SYSCALL. Po kurio laiko procesas užbaigia sistemos iškvietimą ir iš jo išėjus, valdymas vėl perduodamas strace, kuris peržiūri grąžinamas reikšmes ir pradeda procesą naudodamas PTRACE_SYSCALL, ir taip toliau.

BPF mažiesiems, nulinė dalis: klasikinis BPF

Tačiau naudojant seccomp, šį procesą galima optimizuoti tiksliai taip, kaip norėtume. Būtent, jei norime žiūrėti tik į sistemos iškvietimą X, tada galime parašyti BPF filtrą, kuris skirtas X grąžina vertę SECCOMP_RET_TRACE, o už skambučius, kurie mūsų nedomina - SECCOMP_RET_ALLOW:

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

Šiuo atveju strace iš pradžių procesą pradeda kaip PTRACE_CONT, mūsų filtras apdorojamas kiekvienam sistemos skambučiui, jei sistemos skambučio nėra X, tada procesas tęsiamas, bet jei tai X, tada seccomp perduos valdymą stracekuri pažvelgs į argumentus ir pradės procesą kaip PTRACE_SYSCALL (kadangi seccomp neturi galimybės paleisti programos išeinant iš sistemos skambučio). Kai grįžta sistemos skambutis, strace iš naujo pradės procesą naudodami PTRACE_CONT ir lauks naujų pranešimų iš seccomp.

BPF mažiesiems, nulinė dalis: klasikinis BPF

Kai naudojate parinktį --seccomp-bpf yra du apribojimai. Pirma, nebus galima prisijungti prie jau esamo proceso (parinktis -p programos strace), nes to nepalaiko seccomp. Antra, nėra galimybės ne pažvelkite į antrinius procesus, nes seccomp filtrus paveldi visi antriniai procesai be galimybės to išjungti.

Šiek tiek išsamiau, kaip tiksliai strace veikia su seccomp galima rasti iš naujausia ataskaita. Mums įdomiausias faktas yra tai, kad klasikinis BPF, kurį atstovauja seccomp, naudojamas ir šiandien.

xt_bpf

Dabar grįžkime į tinklų pasaulį.

Fonas: seniai, 2007 m., branduolys buvo pridėta modulis xt_u32 skirtas netfilter. Jis buvo parašytas pagal analogiją su dar senesniu eismo klasifikatoriumi cls_u32 ir leido parašyti savavališkas dvejetaines iptables taisykles naudojant šias paprastas operacijas: įkelti 32 bitus iš paketo ir atlikti su jais aritmetinių operacijų rinkinį. Pavyzdžiui,

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

Įkelia 32 IP antraštės bitus, pradedant nuo 6 užpildymo, ir pritaiko jiems kaukę 0xFF (paimkite žemą baitą). Šis laukas protocol IP antraštę ir lyginame su 1 (ICMP). Galite sujungti daug patikrinimų vienoje taisyklėje, taip pat galite vykdyti operatorių @ — perkelkite X baitus į dešinę. Pavyzdžiui, taisyklė

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

patikrina, ar TCP sekos numeris nėra lygus 0x29. Toliau į smulkmenas nesileisiu, nes jau aišku, kad tokias taisykles rašyti ranka nėra labai patogu. Straipsnyje BPF – pamirštas baito kodas, yra keletas nuorodų su naudojimo pavyzdžiais ir taisyklių generavimu xt_u32. Taip pat žiūrėkite nuorodas šio straipsnio pabaigoje.

Nuo 2013 metų modulis vietoj modulio xt_u32 galite naudoti BPF pagrįstą modulį xt_bpf. Kas skaitė iki šiol, jau turėtų būti aiškus jo veikimo principas: paleiskite BPF baitinį kodą kaip iptables taisykles. Galite sukurti naują taisyklę, pavyzdžiui:

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

čia <байткод> - tai yra surinkėjo išvesties formato kodas bpf_asm pagal numatytuosius nustatymus, pvz.

$ 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

Šiame pavyzdyje mes filtruojame visus UDP paketus. Kontekstas BPF programai modulyje xt_bpf, žinoma, nurodo paketinius duomenis, iptables atveju – į IPv4 antraštės pradžią. Grąžinama vertė iš BPF programos loginisKur false reiškia, kad paketas neatitiko.

Akivaizdu, kad modulis xt_bpf palaiko sudėtingesnius filtrus nei anksčiau pateiktame pavyzdyje. Pažvelkime į tikrus „Cloudfare“ pavyzdžius. Dar visai neseniai jie naudojo modulį xt_bpf apsisaugoti nuo DDoS atakų. Straipsnyje Pristatome BPF įrankius jie paaiškina, kaip (ir kodėl) generuoja BPF filtrus ir skelbia nuorodas į tokių filtrų kūrimo paslaugų rinkinį. Pavyzdžiui, naudojant naudingumą bpfgen galite sukurti BPF programą, atitinkančią vardo DNS užklausą 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

Programoje pirmiausia įkeliame į registrą X eilutės pradžios adresas x04habrx03comx00 UDP datagramoje ir patikrinkite užklausą: 0x04686162 <-> "x04hab" ir tt

Šiek tiek vėliau „Cloudfare“ paskelbė p0f -> BPF kompiliatoriaus kodą. Straipsnyje Pristatome p0f BPF kompiliatorių jie kalba apie tai, kas yra p0f ir kaip konvertuoti p0f parašus į 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,
...

Šiuo metu nebenaudojama „Cloudfare“. xt_bpf, nes jie persikėlė į XDP - vieną iš naujos BPF versijos naudojimo variantų, žr. L4Drop: XDP DDoS mažinimas.

cls_bpf

Paskutinis klasikinio BPF naudojimo branduolyje pavyzdys yra klasifikatorius cls_bpf „Linux“ eismo valdymo posistemiui, 2013 m. pabaigoje pridėtam prie „Linux“ ir konceptualiai pakeičiančiame senovinę cls_u32.

Tačiau dabar darbo neaprašysime cls_bpf, kadangi iš žinių apie klasikinį BPF taško tai mums nieko neduos – su visomis funkcijomis jau susipažinome. Be to, tolesniuose straipsniuose, kuriuose kalbama apie išplėstinį BPF, su šiuo klasifikatoriumi susitiksime ne kartą.

Dar viena priežastis nekalbėti apie klasikinio BPF c cls_bpf Problema ta, kad, palyginti su Extended BPF, taikymo sritis šiuo atveju yra radikaliai susiaurinta: klasikinės programos negali pakeisti paketų turinio ir negali išsaugoti būsenos tarp skambučių.

Taigi laikas atsisveikinti su klasikiniu BPF ir pažvelgti į ateitį.

Atsisveikinimas su klasikiniu BPF

Pažiūrėjome, kaip BPF technologija, sukurta dešimtojo dešimtmečio pradžioje, sėkmingai gyvavo ketvirtį amžiaus ir iki galo rado naujų pritaikymų. Tačiau, panašiai kaip perėjimas nuo stack mašinų prie RISC, kuris buvo akstinas kuriant klasikinį BPF, 32-aisiais įvyko perėjimas nuo 64 bitų prie XNUMX bitų mašinų ir klasikinis BPF pradėjo pasenti. Be to, klasikinio BPF galimybės yra labai ribotos, o be pasenusios architektūros – neturime galimybės išsaugoti būsenos tarp skambučių į BPF programas, nėra tiesioginio vartotojo sąveikos galimybės, nėra galimybės sąveikauti su branduoliu, išskyrus riboto skaičiaus struktūros laukų skaitymą sk_buff ir paleidus paprasčiausias pagalbines funkcijas, negalima keisti paketų turinio ir jų peradresuoti.

Tiesą sakant, šiuo metu iš klasikinio BPF Linux sistemoje lieka tik API sąsaja, o branduolio viduje visos klasikinės programos, nesvarbu, ar tai būtų lizdų filtrai, ar seccomp filtrai, automatiškai verčiamos į naują formatą Extended BPF. (Apie tai, kaip tai vyksta tiksliai, pakalbėsime kitame straipsnyje.)

Perėjimas prie naujos architektūros prasidėjo 2013 m., kai Aleksejus Starovoitovas pasiūlė BPF atnaujinimo schemą. 2014 m. atitinkami pleistrai pradėjo atsirasti šerdyje. Kiek supratau, pradinis planas buvo tik optimizuoti architektūrą ir JIT kompiliatorių, kad jis veiktų efektyviau 64 bitų įrenginiuose, tačiau vietoj šių optimizacijų prasidėjo naujas Linux kūrimo skyrius.

Kiti šios serijos straipsniai apims naujosios technologijos, iš pradžių žinomos kaip vidinis BPF, vėliau išplėstas BPF, o dabar tiesiog BPF, architektūrą ir pritaikymus.

Nuorodos

  1. Stevenas McCanne'as ir Van Jacobsonas, „BSD paketų filtras: nauja vartotojo lygio paketų fiksavimo architektūra“, https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Stevenas McCanne'as, „libpcap: paketų fiksavimo architektūra ir optimizavimo metodika“, https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. „IPtable U32 Match“ pamoka.
  5. BPF – pamirštas baito kodas: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. Pristatome BPF įrankį: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. Seccomp apžvalga: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: Konteineriai ir apsauga: seccomp
  11. habr: Demonų išskyrimas naudojant systemd arba "tam nereikia Docker!"
  12. Paul Chaignon, „strace --seccomp-bpf: žvilgsnis po gaubtu“, https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Šaltinis: www.habr.com

Добавить комментарий