BPF txikientzat, zero zatia: BPF klasikoa

Berkeley Packet Filters (BPF) orain dela hainbat urte ingelesezko argitalpen teknologikoen lehen orrialdeetan dagoen Linux kernel teknologia bat da. Jardunaldiak BPFren erabilerari eta garapenari buruzko txostenez beteta daude. David Millerrek, Linux sareko azpisistemaren mantentzaileak, Linux Plumbers 2018-n bere hitzaldia deitu du "Hitzaldi hau ez da XDPri buruz" (XDP BPFren erabilera kasu bat da). Brendan Gregg-ek izenburuko hitzaldiak ematen ditu Linux BPF Superbotereak. Toke HΓΈiland-JΓΈrgensen barreaknukleoa gaur egun mikrokernel bat dela. Thomas Graf-ek ideia hori sustatzen du BPF nukleorako javascript da.

Oraindik ez dago BPF-ren deskribapen sistematikorik HabrΓ©-n, eta, beraz, artikulu sorta batean teknologiaren historiaz hitz egiten saiatuko naiz, arkitektura eta garapen-tresnak deskribatzen eta BPF erabiltzearen aplikazio eta praktika-eremuak zehazten. Artikulu honek, zero, seriean, BPF klasikoaren historia eta arkitektura kontatzen ditu, eta bere funtzionamendu-printzipioen sekretuak ere erakusten ditu. tcpdump, seccomp, strace, eta askoz gehiago.

BPF-ren garapena Linux sareen komunitateak kontrolatzen du, BPF-ren aplikazio nagusiak sareekin lotuta daude eta, beraz, baimenarekin @eukariot, serieari β€œBPF txikienei” deitu nion, serie handiaren omenez "Txikienentzako sareak".

BPF-ren historiako ikastaro laburra(c)

BPF teknologia modernoa izen bereko teknologia zaharraren bertsio hobetu eta hedatua da, gaur egun BPF klasikoa deitzen dena nahasketa saihesteko. BPF klasikoan oinarritutako erabilgarritasun ezagun bat sortu zen tcpdump, mekanismoa seccomp, baita hain ezagunak ez diren moduluak ere xt_bpf egiteko iptables eta sailkatzailea cls_bpf. Linux modernoan, BPF programa klasikoak automatikoki itzultzen dira forma berrira, hala ere, erabiltzaileen ikuspuntutik, APIa bere horretan mantendu da eta BPF klasikoaren erabilera berriak aurkitzen ari dira, artikulu honetan ikusiko dugunez. Horregatik, eta baita Linuxen BPF klasikoaren garapenaren historiari jarraituz, argiago geratuko delako nola eta zergatik eboluzionatu zen bere forma modernora, BPF klasikoaren inguruko artikulu batekin hastea erabaki nuen.

Joan den mendeko laurogeiko hamarkadaren amaieran, Lawrence Berkeley Laborategi ospetsuko ingeniariek joan den mendeko laurogeiko hamarkadaren amaieran modernoa zen hardwarean sare-paketeak nola iragazi behar ziren aztertzen hasi ziren. Iragazketaren oinarrizko ideia, jatorriz CSPF (CMU/Stanford Packet Filter) teknologian inplementatua, beharrezkoak ez diren paketeak ahalik eta lehen iragaztea zen, hau da. nukleoko espazioan, horrek alferrikako datuak erabiltzailearen espazioan kopiatzea saihesten baitu. Nukleoko espazioan erabiltzaile-kodea exekutatzeko exekuzio-denboran segurtasuna eskaintzeko, makina birtual bat erabili zen.

Hala ere, lehendik zeuden iragazkien makina birtualak pilan oinarritutako makinetan exekutatzeko diseinatu ziren eta ez ziren eraginkortasunez exekutatzen RISC makina berrienetan. Ondorioz, Berkeley Labs-eko ingeniarien ahaleginaren bidez, BPF (Berkeley Packet Filters) teknologia berri bat garatu zen, zeinaren makina birtualaren arkitektura Motorola 6502 prozesadorean oinarrituta diseinatu zen -halako produktu ezagunen zaldia. Apple II edo NES. Makina birtual berriak iragazkien errendimendua hamar aldiz handitu zuen lehendik zeuden soluzioekin alderatuta.

BPF makinen arkitektura

Lanean arkitektura ezagutuko dugu, adibideak aztertuz. Dena den, hasteko, demagun makinak erabiltzaileak eskura dituen 32 biteko bi erregistro zituela, metagailu bat. A eta indize-erregistroa X, 64 byteko memoria (16 hitz), idazteko eta ondorengo irakurtzeko erabilgarri, eta objektu horiekin lan egiteko komando sistema txiki bat. Baldintzazko adierazpenak ezartzeko jauzi-argibideak ere eskuragarri zeuden programetan, baina programaren amaiera puntuala bermatzeko, jauziak aurrera bakarrik egin zitezkeen, hau da, bereziki, begiztak sortzea debekatuta zegoen.

Makina abiarazteko eskema orokorra honakoa da. Erabiltzaileak BPF arkitekturarako programa bat sortzen du eta, erabiliz batzuk nukleoaren mekanismoa (adibidez, sistema dei bat), programa kargatzen eta konektatzen du batzuei nukleoko gertaera-sorgailura (adibidez, gertaera bat sare-txarteleko hurrengo paketearen etorrera da). Gertaera bat gertatzen denean, nukleoak programa exekutatzen du (adibidez, interprete batean), eta makinaren memoriari dagokio. batzuei nukleoaren memoria-eskualdea (adibidez, sarrerako pakete baten datuak).

Aurrekoa nahikoa izango zaigu adibideak aztertzen hasteko: sistema eta komando formatua behar den moduan ezagutuko ditugu. Makina birtual baten komando-sistema berehala aztertu eta bere gaitasun guztiak ezagutu nahi badituzu, jatorrizko artikulua irakur dezakezu BSD Pakete Iragazkia eta/edo espedientearen lehen erdia Dokumentazioa/sareak/filter.txt nukleoaren dokumentaziotik. Horrez gain, aurkezpena azter dezakezu libpcap: Pakete Harrapatzeko Arkitektura eta Optimizazio Metodologia, eta bertan, McCanne, BPFren egileetako bat, sorkuntzaren historiaz hitz egiten du libpcap.

Orain Linux-en BPF klasikoa erabiltzearen adibide esanguratsu guztiak kontuan hartuko ditugu: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.

tcpdump

BPF-ren garapena paketeen iragazketarako frontend-aren garapenarekin batera egin zen - erabilgarritasun ezaguna. tcpdump. Eta, sistema eragile askotan eskuragarri dagoen BPF klasikoa erabiltzeko adibiderik zaharrena eta ospetsuena denez, honekin hasiko dugu teknologiaren azterketa.

(Artikulu honetako adibide guztiak Linux-en exekutatu ditut 5.6.0-rc6. Komando batzuen irteera editatu da irakurgarritasun hobea izateko.)

Adibidez: IPv6 paketeak behatzea

Imajina dezagun interfaze batean IPv6 pakete guztiak aztertu nahi ditugula eth0. Horretarako programa exekutatu dezakegu tcpdump iragazki soil batekin ip6:

$ sudo tcpdump -i eth0 ip6

Kasu honetan, tcpdump iragazkia konpilatzen du ip6 BPF arkitekturaren byte-kodean sartu eta nukleora bidali (ikus xehetasunak atalean Tcpdump: kargatzen). Kargatutako iragazkia interfazetik pasatzen den pakete bakoitzeko exekutatuko da eth0. Iragazkiak zero ez den balio bat itzultzen badu n, gero gora n paketearen byteak erabiltzailearen espazioan kopiatuko dira eta irteeran ikusiko dugu tcpdump.

BPF txikientzat, zero zatia: BPF klasikoa

Ematen du erraz jakin dezakegula zein bytecode bidali zen nukleora tcpdump ren laguntzarekin tcpdump, aukerarekin exekutatzen badugu -d:

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

Zero lerroan komandoa exekutatzen dugu ldh [12], β€œload into register A 16” helbidean kokatutako hitz erdi bat (12 bit) eta galdera bakarra zer nolako memoria jorratzen ari garen da? Erantzuna hau da x hasten da (x+1)analizatutako sare-paketearen byte-a. Ethernet interfazetik paketeak irakurtzen ditugu eth0eta hau esan nahi dupaketeak honen itxura duela (sinpletasunerako, paketean VLAN etiketarik ez dagoela suposatzen dugu):

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

Beraz, komandoa exekutatu ondoren ldh [12] erregistroan A eremu bat egongo da Ether Type β€” Ethernet trama honetan transmititzen den pakete mota. 1. lerroan erregistroaren edukia konparatzen dugu A (pakete mota) c 0x86ddeta hau eta badago Interesatzen zaigun mota IPv6 da. 1. lerroan, konparazio komandoaz gain, beste bi zutabe daude - jt 2 ΠΈ jf 3 β€” alderaketa arrakastatsua izanez gero joan behar dituzun markak (A == 0x86dd) eta arrakastarik gabe. Beraz, kasu arrakastatsu batean (IPv6) 2. lerrora joaten gara, eta arrakastarik gabeko kasu batean - 3. lerrora. 3. lerroan programa 0 kodearekin amaitzen da (ez kopiatu paketea), 2. lerroan programa kodearekin amaitzen da. 262144 (kopia iezadazu gehienez 256 kilobyteko paketea).

Adibide konplikatuagoa: TCP paketeei begiratzen diegu helmugako atakaren arabera

Ikus dezagun nolakoa den 666 helmugako ataka duten TCP pakete guztiak kopiatzen dituen iragazki batek. IPv4 kasua hartuko dugu kontuan, IPv6 kasua sinpleagoa baita. Adibide hau aztertu ondoren, IPv6 iragazkia arakatu dezakezu ariketa gisa (ip6 and tcp dst port 666) eta kasu orokorrerako iragazkia (tcp dst port 666). Beraz, interesatzen zaigun iragazkia honelakoa da:

$ 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

Dagoeneko badakigu 0 eta 1 lerroek zer egiten duten. 2. lerroan dagoeneko egiaztatu dugu hau IPv4 pakete bat dela (Ether Type = 0x800) eta kargatu erregistroan A Paketearen 24. bytea. Gure paketeak itxura du

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

horrek esan nahi du erregistroan kargatzen dugula A IP goiburuko Protokolo eremua, logikoa dena, TCP paketeak soilik kopiatu nahi ditugulako. Protokoloarekin alderatzen dugu 0x6 (IPPROTO_TCP) 3. lerroan.

4. eta 5. lerroetan 20. helbidean dauden hitz erdiak kargatzen ditugu eta komandoa erabiltzen dugu jset egiaztatu hiruretako bat ezarrita dagoen banderak - emandako maskara jantzita jset hiru bit esanguratsuenak garbitzen dira. Hiru bitetatik bik esaten digute paketea zatitutako IP pakete baten parte den, eta hala bada, azken zatia den. Hirugarren bit-a erreserbatuta dago eta zero izan behar du. Ez ditugu pakete osatugabeak edo hautsitakoak egiaztatu nahi, beraz, hiru bitak egiaztatzen ditugu.

6. lerroa da zerrenda honetako interesgarriena. Adierazpena ldxb 4*([14]&0xf) Erregistroan kargatzen dugula esan nahi du X paketearen hamabosgarren bytearen lau bit esanguratsuenak bider 4z. Hamabosgarren bytearen lau bit esanguratsuenak eremua da. Interneteko goiburuaren luzera IPv4 goiburua, goiburuaren luzera hitzetan gordetzen duena, beraz, 4z biderkatu behar duzu. Interesgarria da adierazpena. 4*([14]&0xf) helbideratze-eskema berezi baterako izendapena da, formulario honetan soilik erabil daitekeena eta erregistro baterako soilik X, hau da. guk ere ezin dugu esan ldb 4*([14]&0xf) ezta ldxb 5*([14]&0xf) (desplazamendu ezberdin bat bakarrik zehaztu dezakegu, adibidez, ldxb 4*([16]&0xf)). Argi dago helbideratze-eskema hori BPF-ri gehitu zitzaiola jasotzeko hain zuzen X (indize erregistroa) IPv4 goiburuaren luzera.

Beraz, 7. lerroan hitz erdi bat at kargatzen saiatzen gara (X+16). 14 byte Ethernet goiburuak okupatzen dituela gogoratuz, eta X IPv4 goiburuaren luzera dauka, hau ulertzen dugu A TCP helmuga ataka kargatu da:

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

Azkenik, 8. lerroan helmugako ataka nahi den balioarekin konparatzen dugu eta 9. edo 10. lerroetan emaitza itzuliko dugu - paketea kopiatu ala ez.

Tcpdump: kargatzen

Aurreko adibideetan, zehazki, ez dugu zehatz-mehatz azaldu BPF bytecode nukleoan paketeak iragazteko nola kargatzen dugun. Oro har, tcpdump sistema askotara eraman eta iragazkiekin lan egiteko tcpdump liburutegia erabiltzen du libpcap. Laburbilduz, erabiliz interfaze batean iragazkia jartzeko libpcap, honako hau egin behar duzu:

Nola funtzionatzen duen ikusteko pcap_setfilter Linux-en ezarrita, erabiltzen dugu strace (lerro batzuk kendu dira):

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

Lehen bi irteera-lerroetan sortzen ditugu entxufe gordina Ethernet fotograma guztiak irakurtzeko eta interfazera lotzeko eth0. batetik gure lehen adibidea badakigu iragazkia ip BPF lau argibide izango ditu, eta hirugarren lerroan aukera nola erabiltzen ikusiko dugu SO_ATTACH_FILTER sistema deia setsockopt 4 luzerako iragazki bat kargatu eta konektatzen dugu. Hau da gure iragazkia.

Aipatzekoa da BPF klasikoan iragazki bat kargatzea eta konektatzea beti eragiketa atomiko gisa gertatzen dela, eta BPFren bertsio berrian, programa kargatzea eta gertaera-sorgailura lotzea denboran bereizita daudela.

Ezkutuko Egia

Irteeraren bertsio apur bat osatuagoa itxura hau du:

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

Goian esan bezala, gure iragazkia kargatu eta konektatzen dugu 5. lineako entxufera, baina zer gertatzen da 3. eta 4. lerroetan? Gertatzen da hau libpcap zaintzen gaitu - gure iragazkiaren irteerak betetzen ez duten paketerik ez izateko, liburutegiak lotzen du iragazki finkoa ret #0 (jaregin pakete guztiak), socket-a blokeorik gabeko modura aldatzen du eta aurreko iragazkietatik gera daitezkeen pakete guztiak kentzen saiatzen da.

Guztira, Linux-en paketeak BPF klasikoa erabiliz iragazteko, iragazkia izan behar duzu egitura baten moduan. struct sock_fprog eta entxufe ireki bat, ondoren iragazkia entxufeari lotu ahal izateko sistema-dei bat erabiliz setsockopt.

Interesgarria da iragazkia edozein entxufetara lotu daitekeela, ez gordin. Hemen Adibidez sarrerako UDP datagrama guztietatik lehenengo bi byteak izan ezik mozten dituen programa. (Iruzkinak gehitu ditut kodean, artikulua ez nahasteko.)

Erabilerari buruzko xehetasun gehiago setsockopt iragazkiak konektatzeko, ikus entxufea (7), baina zure iragazkiak idazteari buruz struct sock_fprog laguntzarik gabe tcpdump atalean hitz egingo dugu BPF gure eskuekin programatzen.

BPF klasikoa eta XXI

BPF 1997an sartu zen Linux-en eta denbora luzez lan-zaldi bat izan da libpcap aldaketa berezirik gabe (Linux-en berariazko aldaketak, noski, Ginen, baina ez zuten panorama globala aldatu). BPF eboluzionatuko zuen lehen zantzu larriak 2011n izan ziren, Eric Dumazetek proposatu zuenean adabaki, Just In Time Compiler nukleoari gehitzen diona - BPF bytecode jatorrizko bihurtzeko itzultzailea x86_64 kodea.

JIT konpilatzailea izan zen aldaketen katean lehena: 2012an agertu iragazkiak idazteko gaitasuna seccomp, BPF erabiliz, 2013ko urtarrilean zegoen gehitu du modulua xt_bpf, arauak idazteko aukera ematen duena iptables BPFren laguntzarekin, eta 2013ko urrian izan zen gehitu du modulu bat ere cls_bpf, BPF erabiliz trafiko-sailkatzaileak idazteko aukera ematen duena.

Adibide hauek guztiak zehatzago aztertuko ditugu laster, baina lehenik eta behin baliagarria izango zaigu BPFrako programa arbitrarioak idazten eta konpilatzen ikastea, liburutegiak eskaintzen dituen gaitasunak direla eta. libpcap mugatua (adibide sinplea: sortutako iragazkia libpcap bi balio bakarrik itzuli ditzake - 0 edo 0x40000) edo, oro har, seccomp-en kasuan bezala, ez dira aplikagarriak.

BPF gure eskuekin programatzen

Ezagutu dezagun BPF instrukzioen formatu bitarra, oso erraza da:

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

Instrukzio bakoitzak 64 bit hartzen ditu, lehen 16 bitak instrukzio-kodea dira, gero zortzi biteko koska daude, jt ΠΈ jf, eta 32 bit argumenturako K, horren helburua komando batetik bestera aldatzen da. Adibidez, komandoa ret, programa amaitzen duena kodea du 6, eta itzulerako balioa konstantetik hartzen da K. C-n, BPF instrukzio bakar bat egitura gisa irudikatzen da

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

eta programa osoa egitura baten moduan dago

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

Horrela, dagoeneko programak idatz ditzakegu (adibidez, instrukzio kodeak ezagutzen ditugu [1]). Honela izango da iragazkia ip6 - gure lehen adibidea:

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 legez erabil dezakegu dei batean

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

Programak makina-kode moduan idaztea ez da oso erosoa, baina batzuetan beharrezkoa da (adibidez, arazketarako, unitate-probak sortzeko, HabrΓ©-ri buruzko artikuluak idazteko, etab.). Erosotasunerako, fitxategian <linux/filter.h> laguntzaile makroak definitzen dira - goiko adibide bera honela berridatzi liteke

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

Hala ere, aukera hau ez da oso erosoa. Horixe arrazoitu zuten Linux nukleoko programatzaileek, eta, beraz, direktorioan tools/bpf nukleoak BPF klasikoarekin lan egiteko mihiztatzaile eta arazketa bat aurki dezakezu.

Mihiztadura-lengoaia arazketa-irteeraren oso antzekoa da tcpdump, baina horrez gain etiketa sinbolikoak zehaztu ditzakegu. Adibidez, hona hemen TCP/IPv4 izan ezik pakete guztiak kentzen dituen programa bat:

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

Lehenespenez, mihiztatzaileak kodea sortzen du formatuan <количСство инструкций>,<code1> <jt1> <jf1> <k1>,..., gure adibidez TCP-rekin izango da

$ 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 programatzaileen erosotasunerako, irteera formatu ezberdin bat erabil daiteke:

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

Testu hau mota-egituraren definizioan kopia daiteke struct sock_filter, atal honen hasieran egin genuen bezala.

Linux eta netsniff-ng luzapenak

BPF estandarrez gain, Linux eta tools/bpf/bpf_asm laguntza eta multzo ez estandarra. Funtsean, egitura baten eremuetara sartzeko argibideak erabiltzen dira struct sk_buff, nukleoko sare-pakete bat deskribatzen duena. Hala ere, badaude beste argibide laguntzaile mota batzuk ere, adibidez ldw cpu erregistroan kargatuko da A nukleoaren funtzio bat exekutatzearen emaitza raw_smp_processor_id(). (BPF-ren bertsio berrian, estandar ez diren luzapen hauek programak memoria, egiturak eta gertaerak sortzeko nukleoaren laguntzaile multzo bat eskaintzeko hedatu dira.) Hona hemen iragazki baten adibide interesgarri bat, zeinetan soilik kopiatzen dugun. paketeen goiburuak erabiltzailearen espaziora luzapena erabiliz poff, kargaren desplazamendua:

ld poff
ret a

BPF luzapenak ezin dira erabili tcpdump, baina hau erabilgarritasun paketea ezagutzeko arrazoi ona da netsniff-ng, besteak beste, programa aurreratu bat dauka netsniff-ng, BPF erabiliz iragazteaz gain, trafiko-sorgailu eraginkor bat ere badu, eta baino aurreratuagoa tools/bpf/bpf_asm, BPF muntatzaile bat deitzen da bpfc. Paketeak dokumentazio nahiko zehatza dauka, ikusi artikuluaren amaierako estekak ere.

seccomp

Beraz, dagoeneko badakigu konplexutasun arbitrarioko BPF programak nola idazten diren eta adibide berriak ikusteko prest gaude, horietako lehena seccomp teknologia da, zeinak, BPF iragazkiak erabiliz, eskuragarri dauden sistema-deien argumentuen multzoa eta multzoa kudeatzea ahalbidetzen duena. prozesu jakin bat eta bere ondorengoak.

Seccomp-en lehen bertsioa 2005ean gehitu zen nukleoan eta ez zen oso ezaguna izan, aukera bakarra eskaintzen baitzuen - prozesu baterako erabilgarri dauden sistema-deien multzoa honako honetara mugatzea: read, write, exit ΠΈ sigreturn, eta arauak urratzen zituen prozesua hil zen erabiliz SIGKILL. Hala ere, 2012an, seccomp-ek BPF iragazkiak erabiltzeko gaitasuna gehitu zuen, baimendutako sistema-deien multzo bat definitzeko eta haien argumentuen egiaztapenak egiteko aukera emanez. (Interesgarria da, Chrome funtzionaltasun honen lehen erabiltzaileetako bat izan zen, eta Chrome-ko jendea BPFren bertsio berri batean oinarritutako KRSI mekanismo bat garatzen ari da eta Linux Segurtasun Moduluak pertsonalizatzeko aukera ematen duena.) Dokumentazio osagarrirako estekak aurki daitezke amaieran. artikuluaren.

Kontuan izan hub-ean seccomp erabiltzeari buruzko artikuluak egon direla, agian norbaitek irakurri nahi izango ditu ondorengo azpiatalak irakurri aurretik (edo beharrean). Artikuluan Ontziak eta segurtasuna: seccomp seccomp erabiltzearen adibideak ematen ditu, bai 2007ko bertsioa bai BPF erabiltzen duen bertsioa (iragazkiak libseccomp erabiliz sortzen dira), seccomp-ek Dockerrekin duen konexioari buruz hitz egiten du eta esteka erabilgarriak ere eskaintzen ditu. Artikuluan Deabruak isolatzea systemd edo "ez duzu Docker behar horretarako!" Bereziki, sistema-deien zerrenda beltzak edo zerrenda zuriak nola gehitzen diren azaltzen du systemd exekutatzen duten deabruentzat.

Jarraian, iragazkiak nola idatzi eta kargatu ikusiko dugu seccomp C hutsean eta liburutegia erabiliz libseccomp eta zeintzuk diren aukera bakoitzaren alde onak eta txarrak, eta azkenik, ikus dezagun seccomp programak nola erabiltzen duen strace.

seccomp-erako iragazkiak idaztea eta kargatzea

Dagoeneko badakigu BPF programak idazten, beraz, ikus dezagun lehenik seccomp programazio interfazea. Iragazki bat ezar dezakezu prozesu mailan, eta haur prozesu guztiek heredatuko dituzte murrizketak. Hau sistema dei bat erabiliz egiten da seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

non &filter - dagoeneko ezaguna zaigun egitura baten erakuslea da struct sock_fprog, hau da. BPF programa.

Nola desberdintzen dira seccomp-erako programak socketetarako programekin? Transmititutako testuingurua. Socketen kasuan, paketea duen memoria-eremu bat eman ziguten, eta seccomp-en kasuan bezalako egitura bat eman ziguten.

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

Hemen nr abiarazi beharreko sistema-deiaren zenbakia da, arch - egungo arkitektura (behean honi buruz gehiago), args - gehienez sei sistema-deien argumentu, eta instruction_pointer sistema-deia egin duen erabiltzailearen espazioaren instrukzioaren erakuslea da. Horrela, adibidez, sistemako dei-zenbakia erregistroan kargatzeko A esan behar dugu

ldw [0]

Seccomp programetarako beste ezaugarri batzuk daude, adibidez, testuingurura 32 biteko lerrokadurarekin bakarrik sar daiteke eta ezin duzu hitz erdi bat edo byte bat kargatu - iragazki bat kargatzen saiatzen zarenean. ldh [0] sistema deia seccomp itzuliko da EINVAL. Funtzioak kargatutako iragazkiak egiaztatzen ditu seccomp_check_filter() nukleoak. (Dibertigarria da, seccomp funtzionaltasuna gehitzen zuen jatorrizko konpromezuan, funtzio honi instrukzioa erabiltzeko baimena gehitzea ahaztu zitzaiela mod (zatiketa hondarra) eta orain ez dago erabilgarri seccomp BPF programetarako, gehitu zenetik hautsi egingo da ABI.)

Funtsean, seccomp programak idazteko eta irakurtzeko dena dakigu jada. Normalean programaren logika sistema-deien zerrenda zuri edo beltz gisa antolatzen da, adibidez programa

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

304, 176, 239, 279 zenbakidun sistemako lau deien zerrenda beltza egiaztatzen du. Zer dira sistema dei hauek? Ezin dugu ziur esan, ez baitakigu zein arkitekturatarako idatzi den programa. Horregatik, seccomp-en egileek eskaintza abiarazi programa guztiak arkitektura egiaztapen batekin (uneko arkitektura testuinguruan adierazten da eremu gisa arch egitura struct seccomp_data). Arkitektura egiaztatuta, adibidearen hasiera honela izango litzateke:

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

eta orduan gure sistemako dei-zenbakiek balio jakin batzuk lortuko lituzkete.

seccomp erabiliz iragazkiak idazten eta kargatzen ditugu libseccomp

Iragazkiak jatorrizko kodean edo BPF muntaian idazteak emaitzaren kontrol osoa izatea ahalbidetzen du, baina, aldi berean, batzuetan hobe da kode eramangarria eta/edo irakurgarria izatea. Liburutegiak lagunduko digu horretan libseccomp, iragazkiak zuri-beltzak edo zuriak idazteko interfaze estandarra eskaintzen duena.

Idatzi dezagun, adibidez, erabiltzaileak aukeratutako fitxategi bitar bat exekutatzen duen programa bat, aurretik sistema-deien zerrenda beltza instalatuta. goiko artikulua (programa sinplifikatu da irakurgarritasun handiagoa izateko, bertsio osoa aurki daiteke Hemen):

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

Lehenik eta behin array bat definitzen dugu sys_numbers blokeatzeko sistemako 40 dei-zenbaki baino gehiago. Ondoren, hasieratu testuingurua ctx eta esan liburutegiari zer baimendu nahi dugun (SCMP_ACT_ALLOW) sistema-dei guztiak lehenespenez (errazagoa da zerrenda beltzak sortzea). Ondoren, banan-banan, zerrenda beltzeko sistema-dei guztiak gehitzen ditugu. Zerrendako sistema-dei bati erantzunez, eskatzen dugu SCMP_ACT_TRAP, kasu honetan seccomp-ek seinale bat bidaliko dio prozesuari SIGSYS sistema-deiak arauak urratu dituen deskribapenarekin. Azkenik, programa kernelera kargatuko dugu erabiliz seccomp_load, programa konpilatu eta prozesuari erantsiko diona sistema-dei bat erabiliz seccomp(2).

Bilaketa arrakastatsua izateko, programa liburutegiarekin lotu behar da libseccomp, adibidez:

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

Abiarazte arrakastatsu baten adibidea:

$ ./seccomp_lib echo ok
ok

Blokeatutako sistema-dei baten adibidea:

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

Erabiltzen dugu stracexehetasunetarako:

$ 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

nola jakin dezakegu programa legez kanpoko sistema dei bat erabiltzeagatik amaitu zela mount(2).

Beraz, iragazki bat idatzi dugu liburutegia erabiliz libseccomp, kode ez hutsala lau lerrotan egokituz. Goiko adibidean, sistema-dei kopuru handia badago, exekuzio-denbora nabarmen murriztu daiteke, egiaztapena konparazio zerrenda bat besterik ez baita. Optimizaziorako, libseccomp izan duela gutxi adabakia barne, iragazki-atributuaren laguntza gehitzen duena SCMP_FLTATR_CTL_OPTIMIZE. Atributu hau 2-n ezartzeak iragazkia bilaketa-programa bitar bihurtuko du.

Bilaketa-iragazki bitarrak nola funtzionatzen duten ikusi nahi baduzu, begiratu gidoi sinplea, BPF muntatzailean halako programak sortzen dituena sistemako dei-zenbakiak markatuz, adibidez:

$ 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

Ezinezkoa da ezer azkarrago idaztea, BPF programek ezin baitute koska-jauzirik egin (ezin dugu egin, adibidez, jmp A edo jmp [label+X]) eta, beraz, trantsizio guztiak estatikoak dira.

seccomp eta strace

Denek ezagutzen dute erabilgarritasuna strace Linux-en prozesuen portaera aztertzeko ezinbesteko tresna da. Hala ere, askok ere entzun dute errendimendu arazoak utilitate hau erabiltzean. Kontua da strace erabiliz inplementatu ptrace(2), eta mekanismo honetan ezin dugu zehaztu zein sistema-deien multzotan gelditu behar dugun prozesua, hau da, komandoak adibidez.

$ 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

gutxi gorabehera denbora berean prozesatzen dira, nahiz eta bigarren kasuan sistema-dei bakarra trazatu nahi dugun.

Aukera berria --seccomp-bpf, gehituta strace 5.3 bertsioak, prozesua hainbat aldiz bizkortzeko aukera ematen du eta sistema-dei baten aztarnaren azpian abiarazteko denbora ohiko abiarazte baten denborarekin parekoa da jada:

$ 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

(Hemen, noski, engainu txiki bat dago komando honen sistema-dei nagusiaren jarraipena egiten ez dugulako. Trazatuko bagenu, adibidez, newfsstatondoren strace gabe bezain gogor frenatuko luke --seccomp-bpf.)

Nola funtzionatzen du aukera honek? Bera gabe strace prozesura konektatzen da eta erabiltzen hasten da PTRACE_SYSCALL. Kudeatutako prozesu batek (edozein) sistema-dei bat egiten duenean, kontrola hari transferitzen zaio strace, sistema-deiaren argumentuak aztertzen dituena eta exekutatzen duena erabiliz PTRACE_SYSCALL. Denbora pixka bat igaro ondoren, prozesuak sistemaren deia amaitzen du eta bertatik irtetean, kontrola berriro transferitzen da strace, itzultzeko balioak aztertzen dituena eta prozesua erabiltzen hasten duena PTRACE_SYSCALL, eta abar.

BPF txikientzat, zero zatia: BPF klasikoa

Seccomp-ekin, ordea, prozesu hau guk nahi bezala optimizatu daiteke. Alegia, sistema-deiari bakarrik begiratu nahi badiogu X, orduan BPF iragazki bat idatz dezakegu horretarako X balioa ematen du SECCOMP_RET_TRACE, eta gure interesekoak ez diren deietarako - SECCOMP_RET_ALLOW:

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

Kasu honetan strace hasieran bezala hasten da prozesua PTRACE_CONT, gure iragazkia sistema-dei bakoitzeko prozesatzen da, sistema-deia ez bada X, orduan prozesuak martxan jarraitzen du, baina hau bada X, orduan seccomp-ek kontrola transferituko du straceargudioak aztertu eta prozesuari ekingo diona bezala PTRACE_SYSCALL (seccomp-ek ez baitu sistema-dei batetik irtetean programa bat exekutatzeko gaitasunik). Sistema-deia itzultzen denean, strace prozesua berrabiaraziko du erabiliz PTRACE_CONT eta seccomp-en mezu berrien zain egongo da.

BPF txikientzat, zero zatia: BPF klasikoa

Aukera erabiltzean --seccomp-bpf bi murrizketa daude. Lehenik eta behin, ezin izango da lehendik dagoen prozesu batean sartu (aukera -p programak strace), hau ez baita seccomp-ek onartzen. Bigarrenik, ez dago aukerarik ez begiratu haur prozesuei, seccomp iragazkiak haur prozesu guztiek heredatzen baitituzte hau desgaitzeko gaitasunik gabe.

Xehetasun apur bat gehiago nola zehazki strace lan egiten du seccomp tik aurki daiteke azken txostena. Guretzat, daturik interesgarriena da seccomp-ek irudikatzen duen BPF klasikoa gaur egun ere erabiltzen dela.

xt_bpf

Goazen orain sareen mundura.

Aurrekariak: aspaldi, 2007an, muina zen gehitu du modulua xt_u32 netfilter-erako. Are antzinakoagoa den trafiko sailkatzaile batekin analogiaz idatzi zen cls_u32 eta iptablesentzako arau bitar arbitrarioak idazteko aukera ematen zuen eragiketa erraz hauek erabiliz: pakete batetik 32 bit kargatu eta eragiketa aritmetiko multzo bat egin. Adibidez,

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

IP goiburuko 32 bitak kargatzen ditu, betegarri 6tik hasita, eta maskara bat aplikatzen die 0xFF (hartu behe-bytea). Eremu hau protocol IP goiburua eta 1ekin alderatzen dugu (ICMP). Egiaztapen asko konbina ditzakezu arau batean, eta operadorea ere exekutatu dezakezu @ β€” mugitu X byte eskuinera. Adibidez, araua

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

TCP Sekuentzia Zenbakia berdina ez den egiaztatzen du 0x29. Ez naiz gehiago sakonduko, dagoeneko argi baitago halako arauak eskuz idaztea ez dela oso erosoa. Artikuluan BPF - ahaztutako bytecodea, hainbat esteka daude erabileraren eta arauen sorreraren adibideekin xt_u32. Ikusi ere artikulu honen amaierako estekak.

2013tik modulua moduluaren ordez xt_u32 BPF oinarritutako modulua erabil dezakezu xt_bpf. Honaino irakurri duenak argi izan beharko luke bere funtzionamenduaren printzipioa: exekutatu BPF bytecode iptables arau gisa. Arau berri bat sor dezakezu, adibidez, honela:

iptables -A INPUT -m bpf --bytecode <Π±Π°ΠΉΡ‚ΠΊΠΎΠ΄> -j LOG

Hemen <Π±Π°ΠΉΡ‚ΠΊΠΎΠ΄> - hau da mihiztagailuaren irteera formatuan dagoen kodea bpf_asm lehenespenez, adibidez,

$ 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

Adibide honetan UDP pakete guztiak iragazten ari gara. Modulu bateko BPF programa baten testuingurua xt_bpf, jakina, datu-paketeei seinalatzen die, iptablesen kasuan, IPv4 goiburuaren hasierara. BPF programaren itzulera balioa boolearraNon false paketea ez datorrela bat esan nahi du.

Argi dago modulua xt_bpf goiko adibidea baino iragazki konplexuagoak onartzen ditu. Ikus ditzagun Cloudfare-ren benetako adibideak. Duela gutxi arte modulua erabiltzen zuten xt_bpf DDoS erasoetatik babesteko. Artikuluan BPF tresnak aurkezten BPF iragazkiak nola (eta zergatik) sortzen dituzten azaltzen dute eta iragazkiak sortzeko utilitate multzo baterako estekak argitaratzen dituzte. Adibidez, erabilgarritasuna erabiliz bpfgen izen baterako DNS kontsulta batekin bat datorren BPF programa bat sor dezakezu 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

Programan lehenengo erregistroan kargatzen dugu X lerroaren hasierako helbidea x04habrx03comx00 UDP datagrama baten barruan eta gero egiaztatu eskaera: 0x04686162 <-> "x04hab" eta abar.

Pixka bat geroago, Cloudfare-k p0f -> BPF konpiladore kodea argitaratu zuen. Artikuluan p0f BPF konpilatzailea aurkezten p0f zer den eta nola p0f sinadurak BPF bihurtzeko hitz egiten dute:

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

Gaur egun ez dago Cloudfare erabiltzen xt_bpf, XDPra joan zirenetik - BPF-ren bertsio berria erabiltzeko aukeretako bat, ikus. L4Drop: XDP DDoS aringarriak.

cls_bpf

Nukleoan BPF klasikoa erabiltzearen azken adibidea sailkatzailea da cls_bpf Linux-en trafikoa kontrolatzeko azpisistemarako, 2013aren amaieran Linux-era gehitu zen eta kontzeptualki antzinako cls_u32.

Hala ere, orain ez dugu lana deskribatuko cls_bpf, BPF klasikoaren inguruko ezagutzaren ikuspuntutik honek ez digu ezer emango - dagoeneko funtzionalitate guztiak ezagutzen ditugu. Horrez gain, Extended BPF buruz hitz egiten duten hurrengo artikuluetan, behin baino gehiagotan ezagutuko dugu sailkatzaile honekin.

BPF klasikoa erabiltzeari buruz ez hitz egiteko beste arrazoi bat c cls_bpf Arazoa da, Extended BPF-rekin alderatuta, kasu honetan aplikazio-esparrua zeharo murrizten dela: programa klasikoek ezin dute paketeen edukia aldatu eta ezin dute egoera gorde deien artean.

Beraz, BPF klasikoari agur esateko eta etorkizunera begiratzeko garaia da.

Agur BPF klasikoari

Laurogeita hamarreko hamarkadaren hasieran garatutako BPF teknologiak mende laurdenean arrakastaz bizi izan zuen eta amaierara arte aplikazio berriak aurkitu zituen aztertu genuen. Hala ere, pila-makinetatik RISCrako trantsizioaren antzera, BPF klasikoaren garapenerako bultzada gisa balio izan zuena, 32ko hamarkadan 64 biteko makinetatik XNUMX biteko trantsizioa gertatu zen eta BPF klasikoa zaharkituta geratzen hasi zen. Horrez gain, BPF klasikoaren gaitasunak oso mugatuak dira, eta arkitektura zaharkituaz gain - ez dugu BPF programetarako deien artean egoera gordetzeko gaitasunik, ez dago erabiltzaileen interakzio zuzena izateko aukerarik, ez dago elkarreragiteko aukerarik. nukleoarekin, egitura-eremu kopuru mugatu bat irakurtzeko izan ezik sk_buff eta laguntzaile-funtzio errazenak abiaraziz, ezin duzu paketeen edukia aldatu eta birbideratu.

Izan ere, gaur egun Linuxen BPF klasikotik geratzen dena API interfazea da, eta nukleoaren barruan programa klasiko guztiak, izan socket iragazkiak edo seccomp iragazkiak, automatikoki formatu berri batera itzultzen dira, Extended BPF. (Hurrengo artikuluan nola gertatzen den zehatz-mehatz hitz egingo dugu.)

Arkitektura berri baterako trantsizioa 2013an hasi zen, Alexey Starovoitovek BPF eguneratze eskema proposatu zuenean. 2014an dagozkion adabakiak agertzen hasi zen muinean. Ulertzen dudanez, jatorrizko plana arkitektura eta JIT konpilatzailea optimizatzea baino ez zen 64 biteko makinetan eraginkorrago exekutatzeko, baina optimizazio hauek Linux garapenaren kapitulu berri baten hasiera markatu zuten.

Multzo honetako artikulu gehiagok teknologia berriaren arkitektura eta aplikazioak landuko dituzte, hasieran barneko BPF bezala ezagutzen zena, gero BPF hedatua, eta orain BPF besterik ez.

Erreferentziak

  1. Steven McCanne eta Van Jacobson, "BSD Pakete Iragazkia: Erabiltzaile-mailako Pakete Harrapatzeko Arkitektura Berria", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne, "libpcap: Packet Capturerako arkitektura eta optimizazio metodologia", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. IPtable U32 Match Tutoriala.
  5. BPF - ahaztutako bytecodea: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. BPF tresnaren aurkezpena: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. Seccomp ikuspegi orokorra: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: Ontziak eta segurtasuna: seccomp
  11. habr: deabruak isolatzea systemd edo "ez duzu Docker behar honetarako!"
  12. Paul Chaignon, "strace --seccomp-bpf: begirada bat kanpaiaren azpian", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Iturria: www.habr.com

Gehitu iruzkin berria