BPF za najmlajše, ničelni del: klasični BPF

Berkeley Packet Filters (BPF) je tehnologija jedra Linuxa, ki je že nekaj let na naslovnicah angleških tehničnih publikacij. Konference so polne poročil o uporabi in razvoju BPF. David Miller, vzdrževalec omrežnega podsistema Linux, pokliče svoj govor na Linux Plumbers 2018 "Ta pogovor ni o XDP" (XDP je en primer uporabe za BPF). Brendan Gregg ima predavanja z naslovom Velesile Linux BPF. Toke Høiland-Jørgensen se smejida je jedro zdaj mikrojedro. Thomas Graf promovira idejo, da BPF je javascript za jedro.

Na Habréju še vedno ni sistematičnega opisa BPF, zato bom v seriji člankov poskušal govoriti o zgodovini tehnologije, opisati arhitekturo in razvojna orodja ter orisati področja uporabe in prakse uporabe BPF. Ta članek, nič v seriji, pripoveduje zgodovino in arhitekturo klasičnega BPF ter razkriva skrivnosti njegovih načel delovanja. tcpdump, seccomp, strace, in veliko več.

Razvoj BPF nadzira omrežna skupnost Linux, glavne obstoječe aplikacije BPF so povezane z omrežji in zato z dovoljenjem @evkariot, sem serijo poimenoval "BPF za najmlajše", v čast veliki seriji "Omrežja za najmlajše".

Kratek tečaj zgodovine BPF (c)

Sodobna tehnologija BPF je izboljšana in razširjena različica stare tehnologije z enakim imenom, ki se zdaj imenuje klasični BPF, da bi se izognili zmedi. Na podlagi klasičnega BPF je bil ustvarjen dobro znani pripomoček tcpdump, mehanizem seccomp, pa tudi manj znane module xt_bpf za iptables in klasifikator cls_bpf. V sodobnem Linuxu so klasični programi BPF samodejno prevedeni v novo obliko, vendar je z uporabniškega vidika API ostal na mestu in nove uporabe klasičnega BPF, kot bomo videli v tem članku, še vedno najdemo. Iz tega razloga in tudi zato, ker bo po zgodovini razvoja klasičnega BPF v Linuxu postalo bolj jasno, kako in zakaj se je razvil v sodobno obliko, sem se odločil začeti s člankom o klasičnem BPF.

Konec osemdesetih let prejšnjega stoletja je inženirje iz znamenitega Laboratorija Lawrence Berkeley začelo zanimati vprašanje, kako pravilno filtrirati omrežne pakete na strojni opremi, ki je bila moderna v poznih osemdesetih letih prejšnjega stoletja. Osnovna ideja filtriranja, prvotno implementirana v tehnologiji CSPF (CMU/Stanford Packet Filter), je bila filtriranje nepotrebnih paketov čim prej, tj. v prostoru jedra, saj se s tem izognete kopiranju nepotrebnih podatkov v uporabniški prostor. Da bi zagotovili varnost izvajanja za izvajanje uporabniške kode v prostoru jedra, je bil uporabljen virtualni stroj v peskovniku.

Vendar so bili navidezni stroji za obstoječe filtre zasnovani za delovanje na strojih, ki temeljijo na skladu, in niso delovali tako učinkovito na novejših strojih RISC. Posledično je bila s prizadevanji inženirjev iz Berkeley Labs razvita nova tehnologija BPF (Berkeley Packet Filters), katere arhitektura virtualnega stroja je bila zasnovana na osnovi procesorja Motorola 6502 - delovnega konja tako dobro znanih izdelkov, kot so Apple II ali NES. Novi virtualni stroj je večdesetkrat povečal zmogljivost filtra v primerjavi z obstoječimi rešitvami.

Arhitektura stroja BPF

Z arhitekturo se bomo seznanili delovno, z analizo primerov. Vendar za začetek povejmo, da je imel stroj dva uporabniku dostopna 32-bitna registra, akumulator A in indeksni register X, 64 bajtov pomnilnika (16 besed), ki so na voljo za pisanje in naknadno branje, ter majhen sistem ukazov za delo s temi objekti. V programih so bila na voljo tudi navodila za skoke za implementacijo pogojnih izrazov, vendar so bili zaradi zagotavljanja pravočasnega zaključka programa skoki možni samo naprej, predvsem je bilo prepovedano ustvarjanje zank.

Splošna shema zagona stroja je naslednja. Uporabnik ustvari program za arhitekturo BPF in z uporabo nekaj mehanizem jedra (kot je sistemski klic), naloži program in se z njim poveže nekaterim generatorju dogodkov v jedru (na primer, dogodek je prihod naslednjega paketa na omrežno kartico). Ko pride do dogodka, jedro zažene program (na primer v tolmaču), pomnilnik stroja pa ustreza nekaterim območje pomnilnika jedra (na primer podatki dohodnega paketa).

Našteto bo dovolj, da začnemo z ogledom primerov: po potrebi se bomo seznanili s sistemom in obliko ukazov. Če želite takoj preučiti ukazni sistem virtualnega stroja in spoznati vse njegove zmožnosti, potem lahko preberete izvirni članek Paketni filter BSD in/ali prvo polovico datoteke Documentation/networking/filter.txt iz dokumentacije jedra. Poleg tega lahko preučite predstavitev libpcap: Metodologija arhitekture in optimizacije za zajem paketov, v katerem McCanne, eden od avtorjev BPF, govori o zgodovini ustvarjanja libpcap.

Zdaj bomo obravnavali vse pomembne primere uporabe klasičnega BPF v Linuxu: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.

tcpdump

Razvoj BPF je potekal vzporedno z razvojem vmesnika za filtriranje paketov - dobro znanega pripomočka tcpdump. In ker je to najstarejši in najbolj znan primer uporabe klasičnega BPF, ki je na voljo v številnih operacijskih sistemih, bomo našo študijo tehnologije začeli z njim.

(Vse primere v tem članku sem uporabil za Linux 5.6.0-rc6. Izhod nekaterih ukazov je bil urejen za boljšo berljivost.)

Primer: opazovanje paketov IPv6

Predstavljajmo si, da si želimo ogledati vse pakete IPv6 na vmesniku eth0. Za to lahko zaženemo program tcpdump s preprostim filtrom ip6:

$ sudo tcpdump -i eth0 ip6

V tem primeru tcpdump sestavi filter ip6 v bajtno kodo arhitekture BPF in jo pošljite jedru (glejte podrobnosti v razdelku Tcpdump: nalaganje). Naložen filter se bo zagnal za vsak paket, ki gre skozi vmesnik eth0. Če filter vrne vrednost, ki ni enaka nič n, nato do n bajti paketa bodo kopirani v uporabniški prostor in to bomo videli v izhodu tcpdump.

BPF za najmlajše, ničelni del: klasični BPF

Izkazalo se je, da lahko zlahka ugotovimo, katera bajtna koda je bila poslana jedru tcpdump s pomočjo tcpdump, če ga zaženemo z možnostjo -d:

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

V ničelni vrstici zaženemo ukaz ldh [12], kar pomeni »naloži v register A pol besede (16 bitov), ​​ki se nahaja na naslovu 12" in edino vprašanje je, kakšen pomnilnik naslavljamo? Odgovor je, da pri x se začne (x+1)bajt analiziranega omrežnega paketa. Pakete beremo iz vmesnika Ethernet eth0, in to pomenida je paket videti takole (zaradi poenostavitve predpostavimo, da v paketu ni oznak VLAN):

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

Torej po izvedbi ukaza ldh [12] v registru A tam bo polje Ether Type — vrsta paketa, poslanega v tem ethernetnem okviru. V vrstici 1 primerjamo vsebino registra A (vrsta paketa) c 0x86dd, in to in so Vrsta, ki nas zanima, je IPv6. V 1. vrstici sta poleg primerjalnega ukaza še dva stolpca - jt 2 и jf 3 — oznake, do katerih morate iti, če je primerjava uspešna (A == 0x86dd) in neuspešno. Torej, v uspešnem primeru (IPv6) gremo v vrstico 2, v neuspešnem primeru pa v vrstico 3. V vrstici 3 se program konča s kodo 0 (ne kopirajte paketa), v vrstici 2 se program konča s kodo 262144 (kopiraj mi največ 256 kilobajtni paket).

Bolj zapleten primer: pakete TCP gledamo po ciljnih vratih

Poglejmo, kako izgleda filter, ki kopira vse TCP pakete s ciljnimi vrati 666. Upoštevali bomo primer IPv4, saj je primer IPv6 preprostejši. Ko preučite ta primer, lahko sami raziščete filter IPv6 kot vajo (ip6 and tcp dst port 666) in filter za splošni primer (tcp dst port 666). Torej, filter, ki nas zanima, izgleda takole:

$ 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

Že vemo, kaj počneta vrstici 0 in 1. V 2. vrstici smo že preverili, ali je to paket IPv4 (Ether Type = 0x800) in ga naložite v register A 24. bajt paketa. Naš paket izgleda takole

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

kar pomeni, da naložimo v register A polje Protocol glave IP, kar je logično, saj želimo kopirati samo TCP pakete. Protokol primerjamo z 0x6 (IPPROTO_TCP) v vrstici 3.

V vrstici 4 in 5 naložimo polbesede, ki se nahajajo na naslovu 20, in uporabimo ukaz jset preverite, ali je eden od treh nastavljen zastave - nošenje izdane maske jset počistijo se trije najpomembnejši biti. Dva od treh bitov nam povesta, ali je paket del fragmentiranega paketa IP, in če je, ali je zadnji fragment. Tretji bit je rezerviran in mora biti nič. Ne želimo preverjati niti nepopolnih niti pokvarjenih paketov, zato preverimo vse tri bite.

Vrstica 6 je najbolj zanimiva v tem seznamu. Izraz ldxb 4*([14]&0xf) pomeni, da naložimo v register X najmanj pomembni štirje biti petnajstega bajta paketa, pomnoženi s 4. Najmanj pomembni štirje biti petnajstega bajta so polje Dolžina internetne glave Glava IPv4, ki shrani dolžino glave v besedah, tako da jo morate nato pomnožiti s 4. Zanimivo je, da izraz 4*([14]&0xf) je oznaka za posebno shemo naslavljanja, ki se lahko uporablja samo v tej obliki in samo za register X, tj. tudi ne moremo reči ldb 4*([14]&0xf) niti ldxb 5*([14]&0xf) (določimo lahko samo drugačen odmik, npr. ldxb 4*([16]&0xf)). Jasno je, da je bila ta shema naslavljanja dodana BPF ravno zato, da bi prejemala X (indeksni register) Dolžina glave IPv4.

Torej v vrstici 7 poskušamo naložiti pol besede na (X+16). Ne pozabite, da 14 bajtov zaseda glava Ethernet in X vsebuje dolžino glave IPv4, razumemo, da v A Ciljna vrata TCP so naložena:

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

Končno v vrstici 8 primerjamo ciljna vrata z želeno vrednostjo in v vrsticah 9 ali 10 vrnemo rezultat - ali kopirati paket ali ne.

Tcpdump: nalaganje

V prejšnjih primerih se nismo podrobneje ukvarjali s tem, kako natančno naložimo bajtno kodo BPF v jedro za filtriranje paketov. Na splošno, tcpdump prenesen na številne sisteme in za delo s filtri tcpdump uporablja knjižnico libpcap. Na kratko, za namestitev filtra na vmesnik z uporabo libpcap, morate narediti naslednje:

Če želite videti, kako deluje pcap_setfilter implementirano v Linuxu, uporabljamo strace (nekaj vrstic je bilo odstranjenih):

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

V prvih dveh vrsticah izhoda ustvarimo surova vtičnica da prebere vse ethernetne okvirje in jih poveže z vmesnikom eth0. Iz naš prvi primer vemo, da filter ip bo sestavljen iz štirih navodil BPF, v tretji vrstici pa vidimo, kako uporabljati možnost SO_ATTACH_FILTER sistemski klic setsockopt naložimo in priključimo filter dolžine 4. To je naš filter.

Omeniti velja, da v klasičnem BPF nalaganje in povezovanje filtra vedno potekata kot atomska operacija, v novi različici BPF pa sta nalaganje programa in njegova vezava na generator dogodkov časovno ločena.

Skrita resnica

Nekoliko bolj popolna različica izhoda izgleda takole:

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

Kot je navedeno zgoraj, naš filter naložimo in priključimo na vtičnico na liniji 5, toda kaj se zgodi na liniji 3 in 4? Izkazalo se je, da to libpcap skrbi za nas – da izhod našega filtra ne vsebuje paketov, ki ga ne izpolnjujejo, knjižnica povezuje lažni filter ret #0 (izpusti vse pakete), preklopi vtičnico v način brez blokiranja in poskuša odšteti vse pakete, ki bi lahko ostali od prejšnjih filtrov.

Za filtriranje paketov v Linuxu z uporabo klasičnega BPF morate imeti filter v obliki strukture, kot je struct sock_fprog in odprto vtičnico, po kateri je mogoče filter priključiti na vtičnico s sistemskim klicem setsockopt.

Zanimivo je, da je filter mogoče pritrditi na katero koli vtičnico, ne samo na surovo. Tukaj Primer program, ki iz vseh dohodnih datagramov UDP odreže vse bajte razen prvih dveh. (V kodo sem dodal komentarje, da ne bi bil članek v neredu.)

Več podrobnosti o uporabi setsockopt za povezovanje filtrov glejte vtičnica (7), ampak o pisanju lastnih filtrov, kot je struct sock_fprog brez pomoči tcpdump bomo govorili v rubriki Programiranje BPF z lastnimi rokami.

Klasični BPF in XNUMX. stoletje

BPF je bil vključen v Linux leta 1997 in je dolgo časa ostal delovni konj libpcap brez kakršnih koli posebnih sprememb (seveda sprememb, specifičnih za Linux, so bili, vendar niso spremenili globalne slike). Prvi resni znaki, da se bo BPF razvil, so se pojavili leta 2011, ko je Eric Dumazet predlagal obliž, ki jedru doda Just In Time Compiler – prevajalnik za pretvorbo bajtne kode BPF v izvorno x86_64 Koda.

Prevajalnik JIT je bil prvi v verigi sprememb: leta 2012 pojavil sposobnost pisanja filtrov za seccomp, z uporabo BPF, januarja 2013 je bilo dodano modul xt_bpf, ki vam omogoča pisanje pravil za iptables s pomočjo BPF, oktobra 2013 pa je bil dodano tudi modul cls_bpf, ki vam omogoča pisanje klasifikatorjev prometa z uporabo BPF.

Kmalu si bomo podrobneje ogledali vse te primere, najprej pa nam bo koristno, da se naučimo pisati in prevajati poljubne programe za BPF, saj zmožnosti, ki jih ponuja knjižnica libpcap omejeno (preprost primer: ustvarjen filter libpcap lahko vrne samo dve vrednosti - 0 ali 0x40000) ali na splošno, kot v primeru seccomp, niso uporabne.

Programiranje BPF z lastnimi rokami

Spoznajmo binarno obliko navodil BPF, je zelo preprosta:

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

Vsako navodilo zavzema 64 bitov, v katerih je prvih 16 bitov koda ukaza, nato sledita dve osembitni alineji, jt и jfin 32 bitov za argument K, katerega namen se razlikuje od ukaza do ukaza. Na primer ukaz ret, ki konča program, ima kodo 6, vrnjena vrednost pa je vzeta iz konstante K. V C je en sam ukaz BPF predstavljen kot struktura

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

in celoten program je v obliki strukture

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

Tako že lahko pišemo programe (npr. poznamo ukazne kode iz [1]). Takole bo videti filter ip6 z dne naš prvi primer:

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

program prog lahko zakonito uporabimo v klicu

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

Pisanje programov v obliki strojnih kod ni zelo priročno, vendar je včasih potrebno (na primer za odpravljanje napak, ustvarjanje enotnih testov, pisanje člankov na Habré itd.). Za udobje v datoteki <linux/filter.h> so definirani pomožni makri - isti primer kot zgoraj bi lahko prepisali kot

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

Vendar ta možnost ni zelo priročna. Tako so razmišljali programerji jedra Linuxa in zato v imeniku tools/bpf jedra lahko najdete sestavljalnik in razhroščevalnik za delo s klasičnim BPF.

Zbirni jezik je zelo podoben izhodu za odpravljanje napak tcpdump, poleg tega pa lahko določimo simbolne oznake. Tukaj je na primer program, ki odstrani vse pakete razen TCP/IPv4:

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

Privzeto asembler ustvari kodo v formatu <количество инструкций>,<code1> <jt1> <jf1> <k1>,..., za naš primer s TCP bo

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

Za udobje programerjev C je mogoče uporabiti drugačen izhodni 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 },

To besedilo je mogoče kopirati v definicijo strukture tipa struct sock_filter, kot smo storili na začetku tega razdelka.

Linux in razširitve netsniff-ng

Poleg standardnega BPF, Linux in tools/bpf/bpf_asm podporo in nestandardni komplet. V bistvu se navodila uporabljajo za dostop do polj strukture struct sk_buff, ki opisuje omrežni paket v jedru. Vendar pa obstajajo tudi druge vrste pomožnih navodil, na primer ldw cpu se naloži v register A rezultat izvajanja funkcije jedra raw_smp_processor_id(). (V novi različici BPF so bile te nestandardne razširitve razširjene tako, da nudijo programom nabor pomočnikov jedra za dostop do pomnilnika, struktur in generiranje dogodkov.) Tukaj je zanimiv primer filtra, v katerega kopiramo samo glave paketov v uporabniški prostor z uporabo razširitve poff, odmik tovora:

ld poff
ret a

Razširitev BPF ni mogoče uporabiti v tcpdump, vendar je to dober razlog, da se seznanite s paketom pripomočkov netsniff-ng, ki med drugim vsebuje napredni program netsniff-ng, ki poleg filtriranja s pomočjo BPF vsebuje tudi učinkovit generator prometa in naprednejši od tools/bpf/bpf_asm, imenovan zbirnik BPF bpfc. Paket vsebuje precej podrobno dokumentacijo, glejte tudi povezave na koncu članka.

seccomp

Tako že znamo pisati programe BPF poljubne zapletenosti in smo pripravljeni pogledati nove primere, od katerih je prvi tehnologija seccomp, ki omogoča z uporabo filtrov BPF upravljanje niza in niza argumentov sistemskega klica, ki so na voljo danega procesa in njegovih potomcev.

Prva različica seccomp je bila dodana v jedro leta 2005 in ni bila zelo priljubljena, saj je nudila samo eno možnost - omejitev nabora sistemskih klicev, ki so na voljo procesu, na naslednje: read, write, exit и sigreturn, postopek, ki je kršil pravila, pa je bil ukinjen z uporabo SIGKILL. Vendar pa je leta 2012 seccomp dodal možnost uporabe filtrov BPF, kar vam omogoča, da definirate niz dovoljenih sistemskih klicev in celo izvajate preverjanja njihovih argumentov. (Zanimivo je, da je bil Chrome eden prvih uporabnikov te funkcionalnosti in Chromovi ljudje trenutno razvijajo mehanizem KRSI, ki temelji na novi različici BPF in omogoča prilagajanje varnostnih modulov Linuxa.) Povezave do dodatne dokumentacije najdete na koncu članka.

Upoštevajte, da so v središču že bili članki o uporabi seccomp, morda jih bo kdo želel prebrati, preden (ali namesto) prebere naslednje pododdelke. V članku Zabojniki in varnost: seccomp nudi primere uporabe seccomp, tako različice 2007 kot različice, ki uporablja BPF (filtri so ustvarjeni z uporabo libseccomp), govori o povezavi seccomp z Dockerjem in ponuja tudi številne uporabne povezave. V članku Izolacija demonov s systemd ali "za to ne potrebujete Dockerja!" Zlasti pokriva, kako dodati črne sezname ali sezname dovoljenih sistemskih klicev za demone, ki izvajajo systemd.

Nato bomo videli, kako napisati in naložiti filtre za seccomp v golem C in z uporabo knjižnice libseccomp in kakšne so prednosti in slabosti posamezne možnosti, in končno poglejmo, kako program uporablja seccomp strace.

Pisanje in nalaganje filtrov za seccomp

Programe BPF že znamo pisati, zato si najprej poglejmo programski vmesnik seccomp. Filter lahko nastavite na ravni procesa in vsi podrejeni procesi bodo podedovali omejitve. To se naredi s sistemskim klicem seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

če &filter - to je kazalec na strukturo, ki nam je že znana struct sock_fprog, tj. program BPF.

Kako se programi za seccomp razlikujejo od programov za vtičnice? Preneseni kontekst. V primeru vtičnic smo dobili pomnilniško območje, ki vsebuje paket, v primeru seccomp pa strukturo, kot je

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

Tukaj nr je številka sistemskega klica, ki se sproži, arch - trenutna arhitektura (več o tem spodaj), args - do šest argumentov sistemskega klica in instruction_pointer je kazalec na navodilo uporabniškega prostora, ki je izvedlo sistemski klic. Tako na primer naložiti sistemsko klicno številko v register A moramo reči

ldw [0]

Obstajajo še druge funkcije za programe seccomp, na primer, do konteksta je mogoče dostopati samo z 32-bitno poravnavo in ne morete naložiti pol besede ali bajta - ko poskušate naložiti filter ldh [0] sistemski klic seccomp se bo vrnil EINVAL. Funkcija preverja naložene filtre seccomp_check_filter() jedrca. (Smešno je, da so v prvotni objavi, ki je dodala funkcijo seccomp, pozabili dodati dovoljenje za uporabo navodil za to funkcijo mod (ostanek pri deljenju) in zdaj ni na voljo za programe seccomp BPF, odkar je bil dodan se bo zlomil ABI.)

Za pisanje in branje programov seccomp v bistvu že znamo vse. Običajno je programska logika urejena kot beli ali črni seznam sistemskih klicev, na primer programa

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

preveri črni seznam štirih sistemskih klicev s številkami 304, 176, 239, 279. Kateri so ti sistemski klici? Ne moremo reči zagotovo, saj ne vemo, za katero arhitekturo je bil program napisan. Zato avtorji seccomp ponudba zaženite vse programe s preverjanjem arhitekture (trenutna arhitektura je navedena v kontekstu kot polje arch struktur struct seccomp_data). S preverjeno arhitekturo bi bil začetek primera videti takole:

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

in potem bi naše sistemske klicne številke dobile določene vrednosti.

Pišemo in nalagamo filtre za uporabo seccomp libseccomp

Pisanje filtrov v izvorni kodi ali v sestavu BPF vam omogoča popoln nadzor nad rezultatom, hkrati pa je včasih bolje imeti prenosljivo in/ali berljivo kodo. Pri tem nam bo pomagala knjižnica libseccomp, ki ponuja standardni vmesnik za pisanje črno-belih filtrov.

Napišimo na primer program, ki izvaja binarno datoteko po izbiri uporabnika, pri čemer je predhodno namestil črni seznam sistemskih klicev iz zgornji članek (program je poenostavljen za večjo berljivost, polno različico lahko najdete tukaj):

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

Najprej definiramo matriko sys_numbers več kot 40 sistemskih klicnih številk za blokiranje. Nato inicializirajte kontekst ctx in povej knjižnici, kaj želimo dovoliti (SCMP_ACT_ALLOW) vsi sistemski klici privzeto (lažje je sestaviti črne sezname). Nato enega za drugim dodajamo vse sistemske klice s črne liste. Kot odgovor na sistemski klic s seznama zahtevamo SCMP_ACT_TRAP, bo v tem primeru seccomp procesu poslal signal SIGSYS z opisom, kateri sistemski klic je kršil pravila. Na koncu naložimo program v jedro z uporabo seccomp_load, ki bo prevedel program in ga s sistemskim klicem priložil procesu seccomp(2).

Za uspešno prevajanje mora biti program povezan s knjižnico libseccomp, na primer:

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

Primer uspešnega zagona:

$ ./seccomp_lib echo ok
ok

Primer blokiranega sistemskega klica:

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

Uporaba straceza podrobnosti:

$ 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

kako lahko vemo, da je bil program prekinjen zaradi uporabe nedovoljenega sistemskega klica mount(2).

Tako smo napisali filter z uporabo knjižnice libseccomp, prilagajanje netrivialne kode v štiri vrstice. V zgornjem primeru, če je sistemskih klicev veliko, se lahko čas izvajanja opazno zmanjša, saj je preverjanje le seznam primerjav. Za optimizacijo je pred kratkim imel libseccomp priložen obliž, ki dodaja podporo za atribut filtra SCMP_FLTATR_CTL_OPTIMIZE. Če ta atribut nastavite na 2, boste filter pretvorili v binarni iskalni program.

Če želite videti, kako delujejo binarni iskalni filtri, si oglejte preprosta skripta, ki generira takšne programe v zbirniku BPF z izbiranjem sistemskih klicnih številk, na primer:

$ 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

Nemogoče je kaj bistveno hitreje napisati, saj programi BPF ne morejo izvajati skokov v zamike (ne moremo npr. jmp A ali jmp [label+X]) in zato so vsi prehodi statični.

seccomp in strace

Vsi poznajo uporabnost strace je nepogrešljivo orodje za proučevanje obnašanja procesov v Linuxu. Vendar pa so mnogi slišali tudi za težave z zmogljivostjo pri uporabi tega pripomočka. Dejstvo je, da strace izvajajo z uporabo ptrace(2), in v tem mehanizmu ne moremo določiti, pri katerem naboru sistemskih klicev moramo ustaviti proces, tj. na primer ukaze

$ 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

se obdelajo v približno istem času, čeprav želimo v drugem primeru izslediti le en sistemski klic.

Nova možnost --seccomp-bpf, dodano strace različica 5.3, omogoča večkratno pospešitev postopka in čas zagona po sledi enega sistemskega klica je že primerljiv s časom običajnega zagona:

$ 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

(Tukaj je seveda rahla prevara, saj ne sledimo glavnemu sistemskemu klicu tega ukaza. Če bi sledili npr. newfsstat, Potem strace bi zaviral enako močno kot brez --seccomp-bpf.)

Kako deluje ta možnost? Brez nje strace se poveže s procesom in ga začne uporabljati PTRACE_SYSCALL. Ko upravljani proces izda (kateri koli) sistemski klic, se nadzor prenese na strace, ki pregleda argumente sistemskega klica in ga zažene z uporabo PTRACE_SYSCALL. Po določenem času proces zaključi sistemski klic in ob izhodu iz njega se nadzor znova prenese strace, ki pogleda vrnjene vrednosti in začne postopek z uporabo PTRACE_SYSCALL, in tako naprej.

BPF za najmlajše, ničelni del: klasični BPF

S seccompom pa lahko ta proces optimiziramo točno tako, kot želimo. Namreč, če želimo pogledati samo sistemski klic X, potem lahko napišemo filter BPF, ki za X vrne vrednost SECCOMP_RET_TRACEin za klice, ki nas ne zanimajo - SECCOMP_RET_ALLOW:

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

V tem primeru strace sprva začne postopek kot PTRACE_CONT, se naš filter obdela za vsak sistemski klic, če sistemski klic ni X, potem postopek teče naprej, če pa ta X, potem bo seccomp prenesel nadzor straceki bo preučila argumente in začela postopek kot PTRACE_SYSCALL (ker seccomp nima možnosti zagnati programa ob izhodu iz sistemskega klica). Ko se sistemski klic vrne, strace bo ponovno zagnal postopek z uporabo PTRACE_CONT in bo čakal na nova sporočila od seccomp.

BPF za najmlajše, ničelni del: klasični BPF

Pri uporabi možnosti --seccomp-bpf obstajata dve omejitvi. Prvič, ne bo se mogoče pridružiti že obstoječemu procesu (možnost -p programi strace), ker seccomp tega ne podpira. Drugič, ni možnosti ne poglejte podrejene procese, saj filtre seccomp podedujejo vsi podrejeni procesi, ne da bi to mogli onemogočiti.

Malo več o tem, kako natančno strace deluje z seccomp je mogoče najti iz nedavno poročilo. Za nas je najbolj zanimivo dejstvo, da se klasični BPF, ki ga predstavlja seccomp, uporablja še danes.

xt_bpf

Vrnimo se zdaj v svet omrežij.

Ozadje: davno, leta 2007, je jedro dodano modul xt_u32 za netfilter. Napisana je bila po analogiji s še bolj starodavnim prometnim klasifikatorjem cls_u32 in vam je omogočil pisanje poljubnih binarnih pravil za iptables z uporabo naslednjih preprostih operacij: naložite 32 bitov iz paketa in na njih izvedite niz aritmetičnih operacij. na primer

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

Naloži 32 bitov glave IP, začenši s polnjenje 6, in jim doda masko 0xFF (vzemite nizki bajt). To polje protocol IP glavo in ga primerjamo z 1 (ICMP). V enem pravilu lahko združite veliko preverjanj in lahko tudi izvedete operator @ — premakni X bajtov v desno. Na primer pravilo

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

preveri, ali zaporedna številka TCP ni enaka 0x29. Ne bom se spuščal v podrobnosti, saj je že jasno, da ročno pisanje takšnih pravil ni zelo priročno. V članku BPF - pozabljena bajtna koda, obstaja več povezav s primeri uporabe in generiranjem pravil za xt_u32. Oglejte si tudi povezave na koncu tega članka.

Od leta 2013 modul namesto modul xt_u32 lahko uporabite modul, ki temelji na BPF xt_bpf. Vsakemu, ki je prebral tako daleč, bi moralo biti že jasno načelo njegovega delovanja: zaženi bajtno kodo BPF kot pravila iptables. Ustvarite lahko novo pravilo, na primer takole:

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

tukaj <байткод> - to je koda v izhodnem formatu asemblerja bpf_asm privzeto npr.

$ 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

V tem primeru filtriramo vse pakete UDP. Kontekst za program BPF v modulu xt_bpf, seveda kaže na paketne podatke, v primeru iptables na začetek glave IPv4. Vrnjena vrednost iz programa BPF logičnoČe false pomeni, da se paket ni ujemal.

Jasno je, da modul xt_bpf podpira bolj zapletene filtre kot zgornji primer. Poglejmo resnične primere iz Cloudfare. Do nedavnega so uporabljali modul xt_bpf za zaščito pred DDoS napadi. V članku Predstavljamo orodja BPF pojasnjujejo, kako (in zakaj) ustvarjajo filtre BPF in objavljajo povezave do nabora pripomočkov za ustvarjanje takih filtrov. Na primer z uporabo pripomočka bpfgen ustvarite lahko program BPF, ki se ujema s poizvedbo DNS za ime 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

V programu najprej naložimo v register X naslov začetka vrstice x04habrx03comx00 znotraj datagrama UDP in nato preverite zahtevo: 0x04686162 <-> "x04hab" itd

Malo kasneje je Cloudfare objavil kodo prevajalnika p0f -> BPF. V članku Predstavljamo prevajalnik p0f BPF govorijo o tem, kaj je p0f in kako pretvoriti podpise p0f v 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,
...

Trenutno ne uporabljam več Cloudfare xt_bpf, odkar so se preselili na XDP - ena od možnosti za uporabo nove različice BPF, glej. L4Drop: ublažitve XDP DDoS.

cls_bpf

Zadnji primer uporabe klasičnega BPF v jedru je klasifikator cls_bpf za podsistem za nadzor prometa v Linuxu, dodan Linuxu konec leta 2013 in konceptualno nadomešča staro cls_u32.

Vendar dela ne bomo zdaj opisovali cls_bpf, saj nam z vidika poznavanja klasičnega BPF to ne bo dalo nič - z vsemi funkcionalnostmi smo se že seznanili. Poleg tega bomo v naslednjih člankih, ki govorijo o razširjenem BPF, ta klasifikator srečali večkrat.

Še en razlog, da ne govorimo o uporabi klasičnega BPF c cls_bpf Težava je v tem, da je v tem primeru obseg uporabnosti v primerjavi z Extended BPF radikalno zožen: klasični programi ne morejo spreminjati vsebine paketov in ne morejo shraniti stanja med klici.

Torej je čas, da se poslovite od klasičnega BPF in pogledate v prihodnost.

Zbogom klasični BPF

Pogledali smo, kako je tehnologija BPF, razvita v začetku devetdesetih, uspešno živela četrt stoletja in do konca našla nove aplikacije. Toda podobno kot pri prehodu s skladovnih strojev na RISC, ki je služil kot spodbuda za razvoj klasičnega BPF, je v 32-ih prišlo do prehoda iz 64-bitnih na XNUMX-bitne stroje in klasični BPF je začel zastareti. Poleg tega so zmožnosti klasičnega BPF zelo omejene in poleg zastarele arhitekture – nimamo možnosti shranjevanja stanja med klici BPF programov, ni možnosti neposredne interakcije uporabnika, ni možnosti interakcije z jedrom, razen za branje omejenega števila strukturnih polj sk_buff in zagon najpreprostejših pomožnih funkcij, ne morete spremeniti vsebine paketov in jih preusmeriti.

Pravzaprav je trenutno vse, kar ostane od klasičnega BPF v Linuxu, vmesnik API, znotraj jedra pa so vsi klasični programi, naj gre za filtre vtičnic ali filtre seccomp, samodejno prevedeni v nov format, Extended BPF. (O tem, kako točno se to zgodi, bomo govorili v naslednjem članku.)

Prehod na novo arhitekturo se je začel leta 2013, ko je Alexey Starovoitov predlagal shemo posodobitve BPF. Leta 2014 ustrezni popravki začela pojavljati v jedru. Kolikor razumem, je bil prvotni načrt samo optimizacija arhitekture in prevajalnika JIT za učinkovitejše delovanje na 64-bitnih strojih, a namesto tega so te optimizacije označile začetek novega poglavja v razvoju Linuxa.

Nadaljnji članki v tej seriji bodo pokrivali arhitekturo in aplikacije nove tehnologije, sprva znane kot notranji BPF, nato razširjeni BPF in zdaj preprosto BPF.

reference

  1. Steven McCanne in Van Jacobson, "Paketni filter BSD: Nova arhitektura za zajem paketov na ravni uporabnika", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne, "libpcap: Metodologija arhitekture in optimizacije za zajem paketov", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. Vadnica za ujemanje IPtable U32.
  5. BPF - pozabljena bajtna koda: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. Predstavljamo orodje BPF: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. Pregled seccompa: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: Kontejnerji in varnost: seccomp
  11. habr: Izolacija demonov s systemd ali "za to ne potrebujete Dockerja!"
  12. Paul Chaignon, "strace --seccomp-bpf: pogled pod pokrov", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Vir: www.habr.com

Dodaj komentar