BPF fyrir litlu börnin, hluti núll: klassískt BPF

Berkeley Packet Filters (BPF) er Linux kjarnatækni sem hefur verið á forsíðum tækniútgáfu á ensku í nokkur ár núna. Ráðstefnur eru fullar af skýrslum um notkun og þróun BPF. David Miller, umsjónarmaður Linux netkerfis undirkerfis, kallar ræðu sína á Linux Plumbers 2018 „Þessi ræða snýst ekki um XDP“ (XDP er eitt notkunartilvik fyrir BPF). Brendan Gregg heldur fyrirlestra undir yfirskriftinni Linux BPF ofurkraftar. Toke Høiland-Jørgensen hlærað kjarninn sé nú örkjarna. Thomas Graf ýtir undir þá hugmynd BPF er javascript fyrir kjarnann.

Enn er engin kerfisbundin lýsing á BPF á Habré og því mun ég í greinaröð reyna að fjalla um sögu tækninnar, lýsa arkitektúr og þróunarverkfærum og útlista notkunar- og framkvæmdasvið notkunar BPF. Þessi grein, núll, í röðinni, segir sögu og arkitektúr klassísks BPF, og sýnir einnig leyndarmál rekstrarreglna þess. tcpdump, seccomp, strace, Og mikið meira.

Þróun BPF er stjórnað af Linux netsamfélaginu, helstu núverandi forrit BPF eru tengd netkerfum og því, með leyfi @eucariot, kallaði ég seríuna „BPF fyrir litlu börnin“, til heiðurs frábæru þáttaröðinni „Net fyrir litlu börnin“.

Stutt námskeið í sögu BPF(c)

Nútíma BPF tækni er endurbætt og aukin útgáfa af gömlu tækninni með sama nafni, nú kölluð klassísk BPF til að forðast rugling. Vel þekkt tól var búið til byggt á klassíska BPF tcpdump, vélbúnaður seccomp, auk minna þekktra eininga xt_bpf í iptables og flokkari cls_bpf. Í nútíma Linux eru klassísk BPF forrit sjálfkrafa þýdd í nýja mynd, en frá notendasjónarmiði hefur API verið áfram á sínum stað og ný notkun fyrir klassíska BPF, eins og við munum sjá í þessari grein, er enn að finna. Af þessum sökum, og einnig vegna þess að í kjölfar sögu þróunar klassísks BPF í Linux, mun verða skýrara hvernig og hvers vegna það þróaðist í nútíma form, ákvað ég að byrja á grein um klassíska BPF.

Í lok níunda áratugar síðustu aldar fengu verkfræðingar frá hinni frægu Lawrence Berkeley rannsóknarstofu áhuga á spurningunni um hvernig ætti að sía netpakka á réttan hátt á vélbúnaði sem var nútímalegur seint á níunda áratug síðustu aldar. Grunnhugmynd síunar, upphaflega útfærð í CSPF (CMU/Stanford Packet Filter) tækni, var að sía óþarfa pakka eins fljótt og hægt er, þ.e. í kjarnarými, þar sem þetta kemur í veg fyrir að afrita óþarfa gögn inn í notendarými. Til að veita keyrsluöryggi fyrir keyrslu notendakóða í kjarnarými var sýndarvél með sandkassa notuð.

Hins vegar voru sýndarvélarnar fyrir núverandi síur hannaðar til að keyra á vélum sem byggja á stafla og keyrðu ekki eins vel á nýrri RISC vélum. Fyrir vikið, með viðleitni verkfræðinga frá Berkeley Labs, var ný BPF (Berkeley Packet Filters) tækni þróuð, sýndarvélaarkitektúr hennar var hannaður byggður á Motorola 6502 örgjörvanum - vinnuhest svo vel þekktra vara eins og Apple II eða NES. Nýja sýndarvélin jók síunarafköst tugfalt miðað við núverandi lausnir.

BPF vélararkitektúr

Við kynnumst arkitektúr á virkan hátt, greindum dæmi. Hins vegar, til að byrja með, segjum að vélin hafi verið með tvær 32-bita skrár sem notandinn getur aðgang að, rafgeymi A og vísitöluskrá X, 64 bæti af minni (16 orð), tiltækt fyrir ritun og síðari lestur, og lítið kerfi skipana til að vinna með þessa hluti. Stökkleiðbeiningar til að útfæra skilyrtar tjáningar voru einnig fáanlegar í forritunum, en til að tryggja tímanlega klára forritinu var aðeins hægt að hoppa áfram, þ.e.a.s. var sérstaklega bannað að búa til lykkjur.

Almennt fyrirkomulag til að ræsa vélina er sem hér segir. Notandinn býr til forrit fyrir BPF arkitektúrinn og notar sumir kjarnakerfi (svo sem kerfiskall), hleður og tengir forritið við til sumra til atburðaframleiðandans í kjarnanum (til dæmis er atburður komu næsta pakka á netkortið). Þegar atburður á sér stað keyrir kjarninn forritið (til dæmis í túlk) og minni vélarinnar samsvarar til sumra kjarnaminnissvæði (til dæmis gögn um pakka sem kemur inn).

Ofangreint mun vera nóg fyrir okkur til að byrja að skoða dæmi: við munum kynnast kerfinu og skipanasniðinu eftir þörfum. Ef þú vilt strax rannsaka stjórnkerfi sýndarvélar og læra um alla getu hennar, þá geturðu lesið upprunalegu greinina BSD pakkasían og/eða fyrri helmingur skráarinnar Documentation/networking/filter.txt úr kjarnaskjölunum. Auk þess er hægt að kynna sér kynninguna libpcap: Arkitektúr og hagræðingaraðferðir fyrir pakkafanga, þar sem McCanne, einn af höfundum BPF, fjallar um sköpunarsöguna libpcap.

Við höldum nú áfram að íhuga öll mikilvæg dæmi um notkun klassískt BPF á Linux: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.

tcpdump

Þróun BPF fór fram samhliða þróun framenda fyrir pakkasíun - vel þekkt tól tcpdump. Og þar sem þetta er elsta og frægasta dæmið um að nota klassískt BPF, fáanlegt á mörgum stýrikerfum, munum við hefja rannsókn okkar á tækninni með því.

(Ég rak öll dæmin í þessari grein á Linux 5.6.0-rc6. Úttak sumra skipana hefur verið breytt fyrir betri læsileika.)

Dæmi: að fylgjast með IPv6 pökkum

Við skulum ímynda okkur að við viljum skoða alla IPv6 pakka á viðmóti eth0. Til að gera þetta getum við keyrt forritið tcpdump með einfaldri síu ip6:

$ sudo tcpdump -i eth0 ip6

Í þessu tilviki, tcpdump setur saman síuna ip6 inn í BPF arkitektúr bækikóðann og sendu hann til kjarnans (sjá upplýsingar í kaflanum Tcpdump: hleðsla). Hlaðna sían verður keyrð fyrir hvern pakka sem fer í gegnum viðmótið eth0. Ef sían skilar gildi sem er ekki núll n, þá allt að n bæti pakkans verða afrituð í notendarými og við munum sjá það í úttakinu tcpdump.

BPF fyrir litlu börnin, hluti núll: klassískt BPF

Það kemur í ljós að við getum auðveldlega fundið út hvaða bætikóði var sendur í kjarnann tcpdump með aðstoð tcpdump, ef við keyrum það með valkostinum -d:

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

Á línu núll keyrum við skipunina ldh [12], sem stendur fyrir „load into register A hálft orð (16 bitar) staðsett á heimilisfangi 12” og spurningin er bara hvers konar minni erum við að tala um? Svarið er að kl x byrjar (x+1)bæti greinda netpakkans. Við lesum pakka úr Ethernet viðmótinu eth0og þetta þýðirað pakkinn líti svona út (til einföldunar gerum við ráð fyrir að engin VLAN merki séu í pakkanum):

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

Svo eftir að hafa framkvæmt skipunina ldh [12] í skránni A þar verður völlur Ether Type — gerð pakka sem send er í þessum Ethernet ramma. Á línu 1 berum við saman innihald skrárinnar A (tegund pakka) c 0x86ddog þetta og það er Tegundin sem við höfum áhuga á er IPv6. Á línu 1, auk samanburðarskipunarinnar, eru tveir dálkar í viðbót - jt 2 и jf 3 — merki sem þú þarft að fara í ef samanburðurinn heppnast (A == 0x86dd) og árangurslaust. Svo, í heppnuðu tilviki (IPv6) förum við í línu 2, og í misheppnuðu tilviki - í línu 3. Á línu 3 lýkur forritinu með kóða 0 (ekki afrita pakkann), á línu 2 lýkur forritinu með kóða 262144 (afritaðu mig að hámarki 256 kílóbæta pakka).

Flóknara dæmi: við skoðum TCP pakka eftir áfangastað

Við skulum sjá hvernig sía lítur út sem afritar alla TCP pakka með áfangastað 666. Við munum íhuga IPv4 málið, þar sem IPv6 málið er einfaldara. Eftir að hafa kynnt þér þetta dæmi geturðu skoðað IPv6 síuna sjálfur sem æfingu (ip6 and tcp dst port 666) og sía fyrir almennt tilfelli (tcp dst port 666). Svo, sían sem við höfum áhuga á lítur svona út:

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

Við vitum nú þegar hvað línur 0 og 1 gera. Á línu 2 höfum við þegar athugað að þetta sé IPv4 pakki (Ether Type = 0x800) og hlaðið því inn í skrána A 24. bæti pakkans. Pakkinn okkar lítur út eins og

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

sem þýðir að við hleðum inn í skrána A Protocol reitinn í IP hausnum, sem er rökrétt, vegna þess að við viljum afrita aðeins TCP pakka. Við berum saman bókun við 0x6 (IPPROTO_TCP) á línu 3.

Á línum 4 og 5 hleðum við hálforðunum sem staðsett eru á heimilisfangi 20 og notum skipunina jset athugaðu hvort einn af þremur sé stilltur fánar - með útgefin grímu jset þrír mikilvægustu bitarnir eru hreinsaðir. Tveir af þremur bitum segja okkur hvort pakkinn sé hluti af sundurliðuðum IP pakka, og ef svo er, hvort það sé síðasta brotið. Þriðji bitinn er frátekinn og verður að vera núll. Við viljum ekki athuga hvorki ófullkomna eða bilaða pakka, svo við athugum alla þrjá bitana.

Lína 6 er áhugaverðust í þessari skráningu. Tjáning ldxb 4*([14]&0xf) þýðir að við hleðum inn í skrána X minnstu fjórir bitarnir af fimmtánda bæti pakkans margfaldaðir með 4. Minnstu fjórir bitarnir af fimmtánda bæti eru reiturinn Lengd internethaus IPv4 haus, sem geymir lengd haussins í orðum, svo þú þarft að margfalda með 4. Athyglisvert er að tjáningin 4*([14]&0xf) er tilnefning fyrir sérstakt heimilisfangakerfi sem aðeins er hægt að nota á þessu formi og aðeins fyrir skrá X, þ.e. við getum ekki sagt heldur ldb 4*([14]&0xf)ldxb 5*([14]&0xf) (við getum aðeins tilgreint annað móti, til dæmis, ldxb 4*([16]&0xf)). Það er ljóst að þetta ávarpskerfi var bætt við BPF einmitt til að fá X (vísitöluskrá) IPv4 haus lengd.

Svo á línu 7 reynum við að hlaða hálfu orði kl (X+16). Mundu að 14 bæti eru upptekin af Ethernet hausnum, og X inniheldur lengd IPv4 haussins, við skiljum að í A TCP áfangastaðagátt er hlaðið:

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

Að lokum, á línu 8, berum við ákvörðunargáttina saman við æskilegt gildi og á línum 9 eða 10 skilum við niðurstöðunni - hvort sem á að afrita pakkann eða ekki.

Tcpdump: hleðsla

Í fyrri dæmunum fórum við ekki sérstaklega yfir nákvæmlega hvernig við hleðum BPF bætikóða inn í kjarnann fyrir pakkasíun. Almennt talað, tcpdump flutt í mörg kerfi og til að vinna með síur tcpdump notar bókasafnið libpcap. Í stuttu máli, til að setja síu á viðmót með því að nota libpcap, þú þarft að gera eftirfarandi:

Til að sjá hvernig virka pcap_setfilter innleitt í Linux, sem við notum strace (sumar línur hafa verið fjarlægðar):

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

Á fyrstu tveimur framleiðslulínunum búum við til hrá fals til að lesa alla Ethernet ramma og binda það við viðmótið eth0. Frá fyrsta dæmið okkar við vitum að sían ip mun samanstanda af fjórum BPF leiðbeiningum og á þriðju línu sjáum við hvernig valkosturinn er notaður SO_ATTACH_FILTER kerfiskall setsockopt við hleðum og tengjum síu af lengd 4. Þetta er sían okkar.

Það er athyglisvert að í klassískum BPF fer hleðsla og tenging síu alltaf fram sem atómaðgerð og í nýju útgáfunni af BPF er hleðsla forritsins og tenging við atburðarrafallinn aðskilin í tíma.

Falinn sannleikur

Örlítið fullkomnari útgáfa af úttakinu lítur svona út:

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

Eins og fram kemur hér að ofan hleðjum við og tengjum síuna okkar við innstunguna á línu 5, en hvað gerist á línum 3 og 4? Það kemur í ljós að þetta libpcap sér um okkur - þannig að úttak síunnar okkar innihaldi ekki pakka sem uppfylla það ekki, bókasafnið tengir dummy sía ret #0 (slepptu öllum pökkum), skiptir falsinu yfir í ólokandi ham og reynir að draga alla pakka sem gætu verið eftir frá fyrri síum.

Alls, til að sía pakka á Linux með klassískum BPF, þarftu að hafa síu í formi uppbyggingu eins og struct sock_fprog og opna fals, eftir það er hægt að festa síuna við innstunguna með kerfiskalli setsockopt.

Athyglisvert er að hægt er að festa síuna við hvaða innstungu sem er, ekki bara hráa. Hérna Dæmi forrit sem sleppir öllum bætum nema fyrstu tveimur bætum frá öllum UDP gagnaskrám sem berast. (Ég bætti við athugasemdum í kóðanum til að gera greinina ekki ringulreið.)

Nánari upplýsingar um notkun setsockopt til að tengja síur, sjá fals (7), heldur um að skrifa þínar eigin síur eins og struct sock_fprog án hjálpar tcpdump við tölum í kaflanum Forritun BPF með eigin höndum.

Klassískt BPF og XNUMX. öldin

BPF var innifalinn í Linux árið 1997 og hefur verið vinnuhestur í langan tíma libpcap án sérstakra breytinga (Linux-sértækar breytingar, auðvitað, voru, en þeir breyttu ekki heimsmyndinni). Fyrstu alvarlegu merki þess að BPF myndi þróast komu árið 2011, þegar Eric Dumazet lagði til plástur, sem bætir Just In Time þýðanda við kjarnann - þýðandi til að breyta BPF bætikóða í innfæddan x86_64 kóða.

JIT þýðandinn var sá fyrsti í breytingakeðjunni: árið 2012 birtist getu til að skrifa síur fyrir secomp, með BPF, í janúar 2013 var bætt við mát xt_bpf, sem gerir þér kleift að skrifa reglur fyrir iptables með aðstoð BPF, og í október 2013 var bætt við einnig mát cls_bpf, sem gerir þér kleift að skrifa umferðarflokkara með BPF.

Við munum skoða öll þessi dæmi nánar fljótlega, en fyrst mun það vera gagnlegt fyrir okkur að læra hvernig á að skrifa og setja saman handahófskennd forrit fyrir BPF, þar sem hæfileikarnir sem bókasafnið býður upp á libpcap takmarkað (einfalt dæmi: sía búin til libpcap getur aðeins skilað tveimur gildum - 0 eða 0x40000) eða almennt, eins og þegar um seccomp er að ræða, eiga þau ekki við.

Forritun BPF með eigin höndum

Við skulum kynnast tvíundarsniði BPF leiðbeininga, það er mjög einfalt:

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

Hver kennsla tekur 64 bita, þar sem fyrstu 16 bitarnir eru leiðbeiningarkóðinn, síðan eru tvær átta bita inndrættir, jt и jf, og 32 bita fyrir rökin K, en tilgangurinn er mismunandi eftir skipunum. Til dæmis skipunina ret, sem lýkur forritinu hefur kóðann 6, og skilagildið er tekið úr fastanum K. Í C er ein BPF kennsla táknuð sem uppbygging

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

og öll dagskráin er í formi uppbyggingar

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

Þannig getum við nú þegar skrifað forrit (til dæmis, við þekkjum leiðbeiningarkóðana frá [1]). Svona mun sían líta út ip6 á fyrsta dæmið okkar:

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

forrit prog við getum löglega notað í símtali

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

Að skrifa forrit í formi vélkóða er ekki mjög þægilegt, en stundum er það nauðsynlegt (til dæmis til að villa, búa til einingapróf, skrifa greinar um Habré osfrv.). Til hægðarauka, í skránni <linux/filter.h> hjálparfjölvi eru skilgreind - sama dæmi og hér að ofan gæti verið endurskrifað sem

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

Hins vegar er þessi valkostur ekki mjög þægilegur. Þetta er það sem Linux kjarnaforritararnir rökstuddu og því í möppunni tools/bpf kjarna þú getur fundið assembler og kembiforrit til að vinna með klassískt BPF.

Samsetningartungumál er mjög svipað kembiúttak tcpdump, en auk þess getum við tilgreint táknræn merki. Til dæmis, hér er forrit sem sleppir öllum pökkum nema TCP/IPv4:

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

Sjálfgefið er að assembler býr til kóða á sniðinu <количество инструкций>,<code1> <jt1> <jf1> <k1>,..., fyrir dæmi okkar með TCP mun það vera

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

Til þæginda fyrir C forritara er hægt að nota annað úttakssnið:

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

Hægt er að afrita þennan texta inn í skilgreiningu tegundarbyggingarinnar struct sock_filter, eins og við gerðum í upphafi þessa kafla.

Linux og netsniff-ng viðbætur

Í viðbót við venjulegt BPF, Linux og tools/bpf/bpf_asm stuðning og óstaðlað sett. Í grundvallaratriðum eru leiðbeiningar notaðar til að fá aðgang að sviðum mannvirkis struct sk_buff, sem lýsir netpakka í kjarnanum. Hins vegar eru líka til annars konar hjálparleiðbeiningar, td ldw cpu mun hlaðast inn í skrána A afleiðing af því að keyra kjarnaaðgerð raw_smp_processor_id(). (Í nýju útgáfunni af BPF hafa þessar óstöðluðu viðbætur verið framlengdar til að veita forritum sett af kjarnahjálpum til að fá aðgang að minni, uppbyggingu og búa til atburði.) Hér er áhugavert dæmi um síu þar sem við afritum aðeins pakkahausa inn í notendarými með því að nota viðbótina poff, hleðslujöfnun:

ld poff
ret a

Ekki er hægt að nota BPF viðbætur í tcpdump, en þetta er góð ástæða til að kynna sér gagnapakkann netsniff-ng, sem meðal annars inniheldur háþróað forrit netsniff-ng, sem, auk þess að sía með BPF, inniheldur einnig áhrifaríkan umferðargjafa, og fullkomnari en tools/bpf/bpf_asm, BPF assembler kallaður bpfc. Pakkinn inniheldur nokkuð ítarleg skjöl, sjá einnig tenglana í lok greinarinnar.

secomp

Þannig að við vitum nú þegar hvernig á að skrifa BPF forrit af handahófskenndum flóknum hætti og erum tilbúin til að skoða ný dæmi, það fyrsta er seccomp tæknin, sem gerir, með því að nota BPF síur, að stjórna menginu og menginu af kerfiskallarrökum sem eru tiltækar fyrir tiltekið ferli og afkomendur þess.

Fyrsta útgáfan af seccomp var bætt við kjarnann árið 2005 og var ekki mjög vinsæl, þar sem hún gaf aðeins einn valmöguleika - að takmarka mengið af kerfissímtölum sem eru í boði fyrir ferli við eftirfarandi: read, write, exit и sigreturn, og ferlið sem braut reglurnar var drepið með SIGKILL. Hins vegar, árið 2012, bætti seccomp við möguleikanum á að nota BPF síur, sem gerir þér kleift að skilgreina sett af leyfðum kerfissímtölum og jafnvel framkvæma athuganir á rökum þeirra. (Athyglisvert er að Chrome var einn af fyrstu notendum þessarar virkni og Chrome fólkið er nú að þróa KRSI vélbúnað sem byggir á nýrri útgáfu af BPF og gerir kleift að sérsníða Linux öryggiseiningar.) Tenglar á viðbótarskjöl má finna í lokin greinarinnar.

Athugaðu að það hafa þegar verið greinar á miðstöðinni um notkun seccomp, kannski vill einhver lesa þær áður en (eða í staðinn fyrir) að lesa eftirfarandi undirkafla. Í greininni Gámar og öryggi: seccomp gefur dæmi um notkun seccomp, bæði 2007 útgáfuna og útgáfuna sem notar BPF (síur eru búnar til með libseccomp), talar um tengingu seccomp við Docker og gefur einnig marga gagnlega tengla. Í greininni Að einangra púka með systemd eða „þú þarft ekki Docker fyrir þetta!“ Það fjallar sérstaklega um hvernig á að bæta við svörtum listum eða hvítlistum yfir kerfiskall fyrir púka sem keyra systemd.

Næst munum við sjá hvernig á að skrifa og hlaða síum fyrir seccomp í berum C og nota bókasafnið libseccomp og hverjir eru kostir og gallar hvers valkosts, og að lokum skulum við sjá hvernig seccomp er notað af forritinu strace.

Að skrifa og hlaða síur fyrir seccomp

Við vitum nú þegar hvernig á að skrifa BPF forrit, svo við skulum fyrst líta á seccomp forritunarviðmótið. Þú getur stillt síu á ferlisstigi og öll undirferli munu erfa takmarkanirnar. Þetta er gert með því að nota kerfiskall seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

þar sem &filter - þetta er vísbending um uppbyggingu sem við þekkjum nú þegar struct sock_fprog, þ.e. BPF forrit.

Hvernig eru forrit fyrir seccomp frábrugðin forritum fyrir innstungur? Sendt samhengi. Þegar um innstungur var að ræða fengum við minnissvæði sem innihélt pakkann, og þegar um seccomp var að ræða fengum við uppbyggingu eins og

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

Hér nr er númer kerfissímtalsins sem á að hefja, arch - núverandi arkitektúr (meira um þetta hér að neðan), args - allt að sex kerfiskallarrök, og instruction_pointer er bendi á leiðbeiningar um notendarýmið sem lét kerfið kalla. Þannig til dæmis að hlaða kerfissímtalsnúmerinu inn í skrána A verðum við að segja

ldw [0]

Það eru aðrir eiginleikar fyrir seccomp forrit, til dæmis er aðeins hægt að nálgast samhengið með 32 bita röðun og þú getur ekki hlaðið hálft orð eða bæti - þegar reynt er að hlaða síu ldh [0] kerfiskall seccomp kem aftur EINVAL. Aðgerðin athugar hlaðnar síur seccomp_check_filter() kjarna. (Fyndið er að í upprunalegu commitinu sem bætti við seccomp virkninni gleymdu þeir að bæta við leyfi til að nota leiðbeiningarnar við þessa aðgerð mod (afgangur af deild) og er nú ekki tiltækur fyrir seccomp BPF forrit, eftir að það var bætt við mun brotna ABI.)

Í grundvallaratriðum vitum við nú þegar allt til að skrifa og lesa seccomp forrit. Venjulega er forritunarrökfræðinni raðað upp sem hvítum eða svörtum lista yfir kerfissímtöl, til dæmis forritið

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

skoðar svartan lista yfir fjögur kerfissímtöl með númerin 304, 176, 239, 279. Hvað eru þessi kerfissímtöl? Við getum ekki sagt með vissu, þar sem við vitum ekki fyrir hvaða arkitektúr forritið var skrifað. Þess vegna hafa höfundar seccomp Tilboðið ræstu öll forrit með arkitektúrathugun (núverandi arkitektúr er tilgreindur í samhenginu sem reit arch mannvirki struct seccomp_data). Þegar arkitektúrinn er merktur myndi byrjun dæmisins líta svona út:

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

og þá myndu kerfissímtalsnúmerin okkar fá ákveðin gildi.

Við skrifum og hleðum síur til að nota seccomp libseccomp

Að skrifa síur í innfæddan kóða eða í BPF samsetningu gerir þér kleift að hafa fulla stjórn á niðurstöðunni, en á sama tíma er stundum æskilegt að hafa færanlegan og/eða læsanlegan kóða. Bókasafnið mun aðstoða okkur við þetta libsecomp, sem veitir staðlað viðmót til að skrifa svartar eða hvítar síur.

Við skulum til dæmis skrifa forrit sem keyrir tvíundarskrá að eigin vali, eftir að hafa áður sett upp svartan lista yfir kerfissímtöl frá ofangreindri grein (Forritið hefur verið einfaldað til að auka læsileika, fulla útgáfuna er að finna hér):

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

Fyrst skilgreinum við fylki sys_numbers af 40+ kerfissímtölum til að loka. Síðan skaltu frumstilla samhengið ctx og segðu bókasafninu hvað við viljum leyfa (SCMP_ACT_ALLOW) öll kerfissímtöl sjálfgefið (það er auðveldara að búa til svartan lista). Síðan, eitt af öðru, bætum við öllum kerfissímtölum af svarta listanum. Til að bregðast við kerfiskalli af listanum óskum við eftir SCMP_ACT_TRAP, í þessu tilfelli mun seccomp senda merki til ferlisins SIGSYS með lýsingu á því hvaða kerfiskall braut reglurnar. Að lokum hleðum við forritinu inn í kjarnann með því að nota seccomp_load, sem mun setja saman forritið og hengja það við ferlið með því að nota kerfiskall seccomp(2).

Til að söfnunin gangi vel verður forritið að vera tengt við bókasafnið libseccomp, til dæmis:

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

Dæmi um vel heppnaða sjósetningu:

$ ./seccomp_lib echo ok
ok

Dæmi um lokað kerfissímtal:

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

Við notum stracefyrir nánari upplýsingar:

$ 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

hvernig getum við vitað að forritinu var hætt vegna notkunar á ólöglegu kerfiskalli mount(2).

Svo við skrifuðum síu með því að nota bókasafnið libseccomp, sem passar ekki léttvægan kóða í fjórar línur. Í dæminu hér að ofan, ef það er mikill fjöldi kerfiskalla, getur framkvæmdartíminn minnkað verulega, þar sem ávísunin er bara listi yfir samanburð. Til hagræðingar hafði libsecomp nýlega plástur fylgir með, sem bætir við stuðningi við síueiginleikann SCMP_FLTATR_CTL_OPTIMIZE. Ef þessi eiginleiki er stilltur á 2 mun síunni breytast í tvíundarleitarforrit.

Ef þú vilt sjá hvernig tvíundarleitarsíur virka skaltu skoða einfalt handrit, sem býr til slík forrit í BPF assembler með því að hringja í kerfissímtalsnúmer, til dæmis:

$ 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

Það er ómögulegt að skrifa neitt verulega hraðar þar sem BPF forrit geta ekki framkvæmt inndráttarstökk (við getum ekki gert td. jmp A eða jmp [label+X]) og því eru allar umbreytingar kyrrstæðar.

secomp og strace

Allir þekkja gagnsemina strace er ómissandi tæki til að rannsaka hegðun ferla á Linux. Hins vegar hafa margir líka heyrt um frammistöðuvandamál þegar þú notar þetta tól. Staðreyndin er sú strace útfært með því að nota ptrace(2), og í þessu fyrirkomulagi getum við ekki tilgreint á hvaða mengi kerfiskalla við þurfum til að stöðva ferlið, þ.e.a.s. skipanir

$ 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

eru afgreidd á nokkurn veginn sama tíma, þó að í öðru tilvikinu viljum við rekja aðeins eitt kerfiskall.

Nýr valkostur --seccomp-bpf, bætt við strace útgáfa 5.3, gerir þér kleift að flýta ferlinu mörgum sinnum og ræsingartíminn undir spori eins kerfiskalls er nú þegar sambærilegur við venjulega ræsingu:

$ 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

(Hér er auðvitað smá blekking að því leyti að við erum ekki að rekja aðalkerfiskall þessarar skipunar. Ef við værum að rekja td. newfsstatþá strace myndi bremsa jafn hart og án --seccomp-bpf.)

Hvernig virkar þessi valkostur? Án hennar strace tengist ferlinu og byrjar að nota það PTRACE_SYSCALL. Þegar stýrt ferli gefur út (hvað sem er) kerfiskall er stjórn flutt til strace, sem skoðar rök kerfiskallsins og keyrir það með PTRACE_SYSCALL. Eftir nokkurn tíma lýkur ferlið kerfissímtalinu og þegar það er hætt er stjórn flutt aftur strace, sem skoðar skilagildin og byrjar ferlið með því að nota PTRACE_SYSCALL, og svo framvegis.

BPF fyrir litlu börnin, hluti núll: klassískt BPF

Með seccomp er hins vegar hægt að fínstilla þetta ferli nákvæmlega eins og við viljum. Nefnilega ef við viljum horfa aðeins á kerfiskallið X, þá getum við skrifað BPF síu sem fyrir X skilar gildi SECCOMP_RET_TRACE, og fyrir símtöl sem hafa ekki áhuga fyrir okkur - SECCOMP_RET_ALLOW:

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

Í þessu tilfelli strace upphaflega byrjar ferlið sem PTRACE_CONT, sían okkar er unnin fyrir hvert kerfiskall, ef kerfiskallið er það ekki X, þá heldur ferlið áfram að keyra, en ef þetta X, þá mun seccomp flytja stjórnina stracesem mun skoða rökin og hefja ferlið eins og PTRACE_SYSCALL (þar sem seccomp hefur ekki getu til að keyra forrit þegar farið er úr kerfiskalli). Þegar kerfiskallið kemur aftur, strace mun endurræsa ferlið með því að nota PTRACE_CONT og mun bíða eftir nýjum skilaboðum frá seccomp.

BPF fyrir litlu börnin, hluti núll: klassískt BPF

Þegar valmöguleikinn er notaður --seccomp-bpf það eru tvær takmarkanir. Í fyrsta lagi verður ekki hægt að taka þátt í ferli sem þegar er til (valkostur -p forrit strace), þar sem þetta er ekki stutt af seccomp. Í öðru lagi er enginn möguleiki ekki skoðaðu barnaferla, þar sem seccomp síur erfast af öllum barnaferlum án þess að geta slökkt á þessu.

Smá nánari upplýsingar um hvernig nákvæmlega strace vinna með seccomp má finna frá nýlegri skýrslu. Fyrir okkur er áhugaverðasta staðreyndin sú að klassískt BPF táknað með seccomp er enn notað í dag.

xt_bpf

Við skulum nú hverfa aftur í heim netkerfisins.

Bakgrunnur: fyrir löngu síðan, árið 2007, var kjarninn bætt við mát xt_u32 fyrir netfilter. Það var skrifað á hliðstæðan hátt við enn fornari umferðarflokkara cls_u32 og leyfði þér að skrifa handahófskenndar tvíundarreglur fyrir iptables með því að nota eftirfarandi einfaldar aðgerðir: hlaða 32 bita úr pakka og framkvæma sett af reikniaðgerðum á þeim. Til dæmis,

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

Hleður 32 bitum IP-haussins, byrjar á fyllingu 6, og setur grímu á þá 0xFF (taktu lága bætið). Þessi völlur protocol IP haus og við berum það saman við 1 (ICMP). Þú getur sameinað margar athuganir í einni reglu og þú getur líka framkvæmt símafyrirtækið @ — færa X bæti til hægri. Til dæmis reglan

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

athugar hvort TCP raðnúmer sé ekki jafnt 0x29. Ég ætla ekki að fara nánar út í það, þar sem það er þegar ljóst að það er ekki mjög þægilegt að skrifa slíkar reglur í höndunum. Í greininni BPF - gleymdi bætikóðinn, það eru nokkrir tenglar með dæmi um notkun og reglugerð fyrir xt_u32. Sjá einnig tenglana í lok þessarar greinar.

Síðan 2013 mát í stað máts xt_u32 þú getur notað BPF byggða mát xt_bpf. Allir sem hafa lesið þetta langt ættu nú þegar að vera skýrir um meginregluna um starfsemi þess: keyrðu BPF bætikóða sem iptables reglur. Þú getur búið til nýja reglu, til dæmis, svona:

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

hér <байткод> - þetta er kóðinn í assembler úttakssniði bpf_asm sjálfgefið, td.

$ 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

Í þessu dæmi erum við að sía alla UDP pakka. Samhengi fyrir BPF forrit í einingu xt_bpf, auðvitað bendir á pakkagögnin, ef um er að ræða iptables, á upphaf IPv4 haussins. Skila gildi frá BPF forriti Booleanhvar false þýðir að pakkinn passaði ekki.

Það er ljóst að mát xt_bpf styður flóknari síur en dæmið hér að ofan. Við skulum skoða raunveruleg dæmi frá Cloudfare. Þar til nýlega notuðu þeir eininguna xt_bpf til að verjast DDoS árásum. Í greininni Við kynnum BPF tólin þeir útskýra hvernig (og hvers vegna) þeir búa til BPF síur og birta tengla á safn tóla til að búa til slíkar síur. Til dæmis að nota tólið bpfgen þú getur búið til BPF forrit sem passar við DNS fyrirspurn um nafn 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

Í forritinu hleðum við fyrst inn í skrána X upphaf línu heimilisfangs x04habrx03comx00 inni í UDP gagnagrammi og athugaðu síðan beiðnina: 0x04686162 <-> "x04hab" o.fl.

Nokkru síðar gaf Cloudfare út p0f -> BPF þýðandakóðann. Í greininni Við kynnum p0f BPF þýðanda þeir tala um hvað p0f er og hvernig á að breyta p0f undirskriftum í 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,
...

Sem stendur notar ekki lengur Cloudfare xt_bpf, þar sem þeir fluttu til XDP - einn af valkostunum til að nota nýju útgáfuna af BPF, sjá. L4Drop: XDP DDoS mótvægisaðgerðir.

cls_bpf

Síðasta dæmið um að nota klassískt BPF í kjarnanum er flokkarinn cls_bpf fyrir undirkerfi umferðarstjórnunar í Linux, bætt við Linux í lok árs 2013 og kemur hugmyndalega í stað hins forna cls_u32.

Hins vegar munum við ekki lýsa verkinu núna cls_bpf, þar sem frá sjónarhóli þekkingar um klassískt BPF mun þetta ekki gefa okkur neitt - við höfum þegar kynnst allri virkni. Að auki, í síðari greinum um Extended BPF, munum við hitta þennan flokkara oftar en einu sinni.

Önnur ástæða til að tala ekki um að nota klassíska BPF c cls_bpf Vandamálið er að miðað við Extended BPF er gildissviðið í þessu tilfelli verulega þrengt: klassísk forrit geta ekki breytt innihaldi pakka og getur ekki vistað ástand milli símtala.

Það er því kominn tími til að kveðja klassíska BPF og horfa til framtíðar.

Kveðjum klassíska BPF

Við skoðuðum hvernig BPF tæknin, sem þróuð var snemma á tíunda áratugnum, lifði farsællega í aldarfjórðung og fann ný forrit þar til yfir lauk. Hins vegar, svipað og umskiptin frá staflavélum yfir í RISC, sem virkaði sem hvati að þróun klassísks BPF, á 32 var umskipti úr 64-bita í XNUMX-bita vélar og klassísk BPF fór að verða úrelt. Að auki eru hæfileikar klassísks BPF mjög takmarkaðir og til viðbótar við gamaldags arkitektúr - höfum við ekki getu til að vista ástand á milli símtala í BPF forrit, það er enginn möguleiki á beinni notendasamskiptum, það er enginn möguleiki á að hafa samskipti með kjarnanum, nema að lesa takmarkaðan fjölda uppbyggingarreita sk_buff og ræsir einföldustu hjálparaðgerðirnar, þú getur ekki breytt innihaldi pakka og beina þeim áfram.

Reyndar er allt sem eftir er af klassíska BPF í Linux API viðmótið, og inni í kjarnanum eru öll klassísk forrit, hvort sem það eru falssíur eða seccomp síur, sjálfkrafa þýdd á nýtt snið, Extended BPF. (Við munum tala um nákvæmlega hvernig þetta gerist í næstu grein.)

Umskipti yfir í nýjan arkitektúr hófust árið 2013, þegar Alexey Starovoitov lagði til BPF uppfærslukerfi. Árið 2014 samsvarandi plástrar fór að birtast í kjarnanum. Eftir því sem ég skil, var upphaflega áætlunin aðeins að fínstilla arkitektúrinn og JIT þýðandann til að keyra á skilvirkari hátt á 64-bita vélum, en í staðinn markaði þessar hagræðingar upphaf nýs kafla í Linux þróun.

Frekari greinar í þessari röð munu fjalla um arkitektúr og notkun nýju tækninnar, upphaflega þekkt sem innri BPF, síðan framlengdur BPF, og nú einfaldlega BPF.

tilvísanir

  1. Steven McCanne og Van Jacobson, "The BSD Packet Filter: A New Architecture for User-level Packet Capture", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne, "libpcap: arkitektúr og hagræðingaraðferð fyrir pakkafanga", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. IPtable U32 Match Kennsla.
  5. BPF - gleymdi bækikóði: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. Við kynnum BPF tólið: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. Secomp yfirlit: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: Gámar og öryggi: secomp
  11. habr: Einangra púka með systemd eða "þú þarft ekki Docker fyrir þetta!"
  12. Paul Chaignon, "strace --seccomp-bpf: kíkja undir hettuna", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Heimild: www.habr.com

Bæta við athugasemd