BPF untuk si kecil, bahagian sifar: BPF klasik

Penapis Paket Berkeley (BPF) ialah teknologi kernel Linux yang telah berada di muka depan penerbitan teknologi berbahasa Inggeris selama beberapa tahun sekarang. Persidangan dipenuhi dengan laporan mengenai penggunaan dan pembangunan BPF. David Miller, penyelenggara subsistem rangkaian Linux, memanggil ceramahnya di Linux Plumbers 2018 β€œCakap ini bukan tentang XDP” (XDP ialah satu kes penggunaan untuk BPF). Brendan Gregg memberikan ceramah bertajuk Kuasa Besar BPF Linux. Toke HΓΈiland-JΓΈrgensen ketawabahawa kernel kini adalah mikrokernel. Thomas Graf mempromosikan idea bahawa BPF ialah javascript untuk kernel.

Masih tiada penerangan sistematik tentang BPF pada HabrΓ©, dan oleh itu dalam satu siri artikel saya akan cuba bercakap tentang sejarah teknologi, menerangkan seni bina dan alat pembangunan, dan menggariskan bidang aplikasi dan amalan menggunakan BPF. Artikel ini, sifar, dalam siri ini, menceritakan sejarah dan seni bina BPF klasik, dan juga mendedahkan rahsia prinsip operasinya. tcpdump, seccomp, strace, dan banyak lagi.

Pembangunan BPF dikawal oleh komuniti rangkaian Linux, aplikasi utama BPF sedia ada berkaitan dengan rangkaian dan oleh itu, dengan kebenaran @eucariot, saya memanggil siri ini "BPF untuk si kecil", sebagai penghormatan kepada siri yang hebat "Rangkaian untuk si kecil".

Kursus pendek dalam sejarah BPF(c)

Teknologi BPF moden ialah versi teknologi lama yang dipertingkat dan dikembangkan dengan nama yang sama, kini dipanggil BPF klasik untuk mengelakkan kekeliruan. Utiliti terkenal telah dicipta berdasarkan BPF klasik tcpdump, mekanisme seccomp, serta modul yang kurang dikenali xt_bpf untuk iptables dan pengelas cls_bpf. Dalam Linux moden, program BPF klasik diterjemahkan secara automatik ke dalam bentuk baharu, walau bagaimanapun, dari sudut pandangan pengguna, API kekal di tempatnya dan kegunaan baharu untuk BPF klasik, seperti yang akan kita lihat dalam artikel ini, masih ditemui. Atas sebab ini, dan juga kerana mengikuti sejarah pembangunan BPF klasik di Linux, ia akan menjadi lebih jelas bagaimana dan mengapa ia berkembang menjadi bentuk modennya, saya memutuskan untuk memulakan dengan artikel tentang BPF klasik.

Pada penghujung tahun lapan puluhan abad yang lalu, jurutera dari Makmal Lawrence Berkeley yang terkenal mula berminat dengan persoalan bagaimana untuk menapis dengan betul paket rangkaian pada perkakasan yang moden pada akhir tahun lapan puluhan abad yang lalu. Idea asas penapisan, yang pada asalnya dilaksanakan dalam teknologi CSPF (CMU/Stanford Packet Filter), adalah untuk menapis paket yang tidak diperlukan seawal mungkin, i.e. dalam ruang kernel, kerana ini mengelakkan penyalinan data yang tidak diperlukan ke dalam ruang pengguna. Untuk menyediakan keselamatan masa jalan untuk menjalankan kod pengguna dalam ruang kernel, mesin maya kotak pasir telah digunakan.

Walau bagaimanapun, mesin maya untuk penapis sedia ada direka untuk dijalankan pada mesin berasaskan tindanan dan tidak berfungsi dengan cekap pada mesin RISC yang lebih baharu. Hasilnya, melalui usaha jurutera dari Berkeley Labs, teknologi BPF (Berkeley Packet Filters) baharu telah dibangunkan, seni bina mesin maya yang direka berdasarkan pemproses Motorola 6502 - kuda kerja produk terkenal seperti Apple II atau NES. Mesin maya baharu meningkatkan prestasi penapis berpuluh kali ganda berbanding penyelesaian sedia ada.

Seni bina mesin BPF

Kami akan berkenalan dengan seni bina dengan cara yang berfungsi, menganalisis contoh. Walau bagaimanapun, sebagai permulaan, katakan bahawa mesin mempunyai dua daftar 32-bit yang boleh diakses oleh pengguna, penumpuk A dan daftar indeks X, 64 bait memori (16 perkataan), tersedia untuk menulis dan bacaan seterusnya, dan sistem arahan kecil untuk bekerja dengan objek ini. Arahan lompat untuk melaksanakan ungkapan bersyarat juga tersedia dalam atur cara, tetapi untuk menjamin penyiapan program tepat pada masanya, lompatan hanya boleh dibuat ke hadapan, iaitu, khususnya, adalah dilarang untuk membuat gelung.

Skim umum untuk memulakan mesin adalah seperti berikut. Pengguna mencipta program untuk seni bina BPF dan, menggunakan beberapa mekanisme kernel (seperti panggilan sistem), memuatkan dan menyambungkan program ke kepada sesetengah orang kepada penjana acara dalam kernel (contohnya, acara ialah ketibaan paket seterusnya pada kad rangkaian). Apabila peristiwa berlaku, kernel menjalankan program (contohnya, dalam penterjemah), dan memori mesin sepadan dengan kepada sesetengah orang kawasan memori kernel (contohnya, data paket masuk).

Perkara di atas sudah cukup untuk kita mula melihat contoh: kita akan membiasakan diri dengan sistem dan format arahan seperti yang diperlukan. Jika anda ingin segera mengkaji sistem arahan mesin maya dan mengetahui tentang semua keupayaannya, maka anda boleh membaca artikel asal Penapis Paket BSD dan/atau separuh pertama fail Dokumentasi/rangkaian/filter.txt daripada dokumentasi kernel. Di samping itu, anda boleh mengkaji pembentangan libpcap: Seni Bina dan Metodologi Pengoptimuman untuk Tangkapan Paket, di mana McCanne, salah seorang pengarang BPF, bercakap tentang sejarah penciptaan libpcap.

Kami kini beralih untuk mempertimbangkan semua contoh penting penggunaan BPF klasik di Linux: tcpdump (libpcap), sekomp, xt_bpf, cls_bpf.

tcpdump

Pembangunan BPF telah dijalankan selari dengan pembangunan bahagian hadapan untuk penapisan paket - utiliti yang terkenal tcpdump. Dan, memandangkan ini adalah contoh tertua dan paling terkenal menggunakan BPF klasik, tersedia pada banyak sistem pengendalian, kami akan memulakan kajian kami tentang teknologi dengannya.

(Saya menjalankan semua contoh dalam artikel ini di Linux 5.6.0-rc6. Output beberapa arahan telah diedit untuk kebolehbacaan yang lebih baik.)

Contoh: memerhati paket IPv6

Bayangkan kita ingin melihat semua paket IPv6 pada antara muka eth0. Untuk melakukan ini kita boleh menjalankan program tcpdump dengan penapis mudah ip6:

$ sudo tcpdump -i eth0 ip6

Dalam kes ini, tcpdump menyusun penapis ip6 ke dalam bytecode seni bina BPF dan hantar ke kernel (lihat butiran dalam bahagian Tcpdump: memuatkan). Penapis yang dimuatkan akan dijalankan untuk setiap paket yang melalui antara muka eth0. Jika penapis mengembalikan nilai bukan sifar n, kemudian sehingga n bait paket akan disalin ke ruang pengguna dan kami akan melihatnya dalam output tcpdump.

BPF untuk si kecil, bahagian sifar: BPF klasik

Ternyata kita boleh mengetahui kod bait mana yang dihantar ke kernel dengan mudah tcpdump dengan bantuan daripada tcpdump, jika kita menjalankannya dengan pilihan -d:

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

Pada baris sifar kami menjalankan arahan ldh [12], yang bermaksud β€œmuat ke dalam daftar A setengah perkataan (16 bit) terletak di alamat 12” dan satu-satunya persoalan ialah jenis memori yang sedang kita tangani? Jawapannya ialah pada x bermula (x+1)bait ke atas paket rangkaian yang dianalisis. Kami membaca paket dari antara muka Ethernet eth0, dan ini bermaknabahawa paket kelihatan seperti ini (untuk kesederhanaan, kami menganggap bahawa tiada teg VLAN dalam paket):

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

Jadi selepas melaksanakan arahan ldh [12] dalam daftar A akan ada padang Ether Type β€” jenis paket yang dihantar dalam bingkai Ethernet ini. Pada baris 1 kami membandingkan kandungan daftar A (jenis pakej) c 0x86dd, dan ini dan mempunyai Jenis yang kami minati ialah IPv6. Pada baris 1, sebagai tambahan kepada arahan perbandingan, terdapat dua lagi lajur - jt 2 ΠΈ jf 3 β€” tanda yang anda perlu pergi jika perbandingan berjaya (A == 0x86dd) dan tidak berjaya. Jadi, dalam kes yang berjaya (IPv6) kita pergi ke baris 2, dan dalam kes yang tidak berjaya - ke baris 3. Pada baris 3 program ditamatkan dengan kod 0 (jangan salin paket), pada baris 2 program ditamatkan dengan kod 262144 (salin saya maksimum pakej 256 kilobait).

Contoh yang lebih rumit: kita melihat paket TCP mengikut port destinasi

Mari lihat rupa penapis yang menyalin semua paket TCP dengan port destinasi 666. Kami akan mempertimbangkan kes IPv4, kerana kes IPv6 adalah lebih mudah. Selepas mengkaji contoh ini, anda boleh meneroka sendiri penapis IPv6 sebagai latihan (ip6 and tcp dst port 666) dan penapis untuk kes umum (tcp dst port 666). Jadi, penapis yang kami minati kelihatan seperti ini:

$ 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

Kita sudah tahu apa yang baris 0 dan 1 lakukan. Pada baris 2 kami telah menyemak bahawa ini adalah paket IPv4 (Jenis Eter = 0x800) dan muatkannya ke dalam daftar A bait ke-24 paket. Pakej kami kelihatan seperti

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

yang bermaksud kita memuatkan ke dalam daftar A medan Protokol pengepala IP, yang logik, kerana kami mahu menyalin paket TCP sahaja. Kami membandingkan Protokol dengan 0x6 (IPPROTO_TCP) pada baris 3.

Pada baris 4 dan 5 kami memuatkan separuh perkataan yang terletak di alamat 20 dan menggunakan arahan jset semak jika salah satu daripada tiga ditetapkan bendera - memakai topeng yang dikeluarkan jset tiga bit yang paling ketara dibersihkan. Dua daripada tiga bit memberitahu kami sama ada paket itu adalah sebahagian daripada paket IP yang berpecah, dan jika ya, sama ada ia adalah serpihan terakhir. Bit ketiga dikhaskan dan mestilah sifar. Kami tidak mahu menyemak sama ada paket yang tidak lengkap atau rosak, jadi kami menyemak ketiga-tiga bit.

Baris 6 adalah yang paling menarik dalam penyenaraian ini. Ungkapan ldxb 4*([14]&0xf) bermakna kita memuatkan ke dalam daftar X empat bit bererti terkecil bagi bait kelima belas paket didarab dengan 4. Empat bit bererti terkecil bagi bait kelima belas ialah medan Panjang Pengepala Internet Pengepala IPv4, yang menyimpan panjang pengepala dalam perkataan, jadi anda perlu mendarab dengan 4. Menariknya, ungkapan 4*([14]&0xf) ialah sebutan untuk skim pengalamatan khas yang hanya boleh digunakan dalam borang ini dan hanya untuk daftar X, iaitu kita pun tak boleh cakap ldb 4*([14]&0xf) atau ldxb 5*([14]&0xf) (kita hanya boleh menentukan offset yang berbeza, contohnya, ldxb 4*([16]&0xf)). Adalah jelas bahawa skim pengalamatan ini telah ditambah kepada BPF dengan tepat untuk menerima X (daftar indeks) Panjang pengepala IPv4.

Jadi pada baris 7 kami cuba memuatkan separuh perkataan di (X+16). Mengingati bahawa 14 bait diduduki oleh pengepala Ethernet, dan X mengandungi panjang pengepala IPv4, kami faham bahawa dalam A Port destinasi TCP dimuatkan:

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

Akhirnya, pada baris 8 kita membandingkan port destinasi dengan nilai yang dikehendaki dan pada baris 9 atau 10 kita mengembalikan hasilnya - sama ada untuk menyalin paket atau tidak.

Tcpdump: memuatkan

Dalam contoh sebelumnya, kami secara khusus tidak membincangkan secara terperinci tentang cara kami memuatkan kod bait BPF ke dalam kernel untuk penapisan paket. Secara umumnya, tcpdump dialihkan ke banyak sistem dan untuk bekerja dengan penapis tcpdump menggunakan perpustakaan libpcap. Secara ringkas, untuk meletakkan penapis pada antara muka menggunakan libpcap, anda perlu melakukan perkara berikut:

Untuk melihat bagaimana fungsi pcap_setfilter dilaksanakan dalam Linux, kami gunakan strace (beberapa baris telah dialih keluar):

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

Pada dua baris pertama output yang kami buat soket mentah untuk membaca semua bingkai Ethernet dan mengikatnya pada antara muka eth0. daripada contoh pertama kami kita tahu bahawa penapis ip akan terdiri daripada empat arahan BPF, dan pada baris ketiga kita melihat cara menggunakan pilihan SO_ATTACH_FILTER panggilan sistem setsockopt kami memuatkan dan menyambung penapis panjang 4. Ini adalah penapis kami.

Perlu diingat bahawa dalam BPF klasik, pemuatan dan penyambungan penapis sentiasa berlaku sebagai operasi atom, dan dalam versi baharu BPF, memuatkan program dan mengikatnya ke penjana acara dipisahkan mengikut masa.

Kebenaran Tersembunyi

Versi output yang sedikit lebih lengkap kelihatan seperti ini:

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

Seperti yang dinyatakan di atas, kami memuatkan dan melampirkan penapis kami pada soket pada baris 5, tetapi apa yang berlaku pada baris 3 dan 4? Ternyata ini libpcap menjaga kami - supaya output penapis kami tidak termasuk paket yang tidak memuaskannya, perpustakaan menyambung penapis dummy ret #0 (lepaskan semua paket), tukar soket kepada mod tidak menyekat dan cuba menolak semua paket yang boleh kekal daripada penapis sebelumnya.

Secara keseluruhan, untuk menapis pakej di Linux menggunakan BPF klasik, anda perlu mempunyai penapis dalam bentuk struktur seperti struct sock_fprog dan soket terbuka, selepas itu penapis boleh dipasang pada soket menggunakan panggilan sistem setsockopt.

Menariknya, penapis boleh dipasang pada mana-mana soket, bukan hanya mentah. Di sini contoh program yang memotong semua kecuali dua bait pertama daripada semua datagram UDP yang masuk. (Saya menambah komen dalam kod supaya tidak mengacaukan artikel.)

Butiran lanjut tentang penggunaan setsockopt untuk menyambungkan penapis, lihat soket(7), tetapi tentang menulis penapis anda sendiri seperti struct sock_fprog tanpa bantuan tcpdump kita akan bercakap di bahagian Memprogramkan BPF dengan tangan kita sendiri.

BPF klasik dan abad ke-21

BPF telah dimasukkan ke dalam Linux pada tahun 1997 dan kekal sebagai usaha keras untuk masa yang lama libpcap tanpa sebarang perubahan khas (perubahan khusus Linux, sudah tentu, ialah, tetapi mereka tidak mengubah gambaran global). Tanda-tanda serius pertama yang BPF akan berkembang datang pada tahun 2011, apabila Eric Dumazet mencadangkan tampalan, yang menambah Just In Time Compiler pada kernel - penterjemah untuk menukar kod bait BPF kepada asli x86_64 kod.

Pengkompil JIT adalah yang pertama dalam rantaian perubahan: pada tahun 2012 muncul keupayaan untuk menulis penapis untuk sekomp, menggunakan BPF, pada Januari 2013 terdapat tambah modul xt_bpf, yang membolehkan anda menulis peraturan untuk iptables dengan bantuan BPF, dan pada Oktober 2013 adalah tambah juga modul cls_bpf, yang membolehkan anda menulis pengelas trafik menggunakan BPF.

Kami akan melihat semua contoh ini dengan lebih terperinci tidak lama lagi, tetapi pertama sekali adalah berguna untuk kita belajar cara menulis dan menyusun atur cara sewenang-wenangnya untuk BPF, kerana keupayaan yang disediakan oleh perpustakaan libpcap terhad (contoh mudah: penapis dihasilkan libpcap boleh mengembalikan hanya dua nilai - 0 atau 0x40000) atau secara amnya, seperti dalam kes seccomp, tidak berkenaan.

Memprogramkan BPF dengan tangan kita sendiri

Mari kita berkenalan dengan format binari arahan BPF, ia sangat mudah:

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

Setiap arahan menduduki 64 bit, di mana 16 bit pertama adalah kod arahan, kemudian terdapat dua inden lapan bit, jt ΠΈ jf, dan 32 bit untuk hujah K, yang tujuannya berbeza dari satu perintah ke satu perintah. Sebagai contoh, arahan ret, yang menamatkan program mempunyai kod 6, dan nilai pulangan diambil daripada pemalar K. Dalam C, satu arahan BPF diwakili sebagai struktur

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

dan keseluruhan program adalah dalam bentuk struktur

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

Oleh itu, kita sudah boleh menulis atur cara (contohnya, kita tahu kod arahan daripada [1]). Ini adalah rupa penapis ip6 daripada contoh pertama kami:

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 kita boleh gunakan secara sah dalam panggilan

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

Menulis program dalam bentuk kod mesin tidak begitu mudah, tetapi kadangkala ia perlu (contohnya, untuk penyahpepijatan, membuat ujian unit, menulis artikel tentang HabrΓ©, dll.). Untuk kemudahan, dalam fail <linux/filter.h> makro pembantu ditakrifkan - contoh yang sama seperti di atas boleh ditulis semula sebagai

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

Walau bagaimanapun, pilihan ini tidak begitu mudah. Inilah alasan pengaturcara kernel Linux, dan oleh itu dalam direktori tools/bpf kernel anda boleh mencari pemasang dan penyahpepijat untuk bekerja dengan BPF klasik.

Bahasa perhimpunan sangat serupa dengan output nyahpepijat tcpdump, tetapi sebagai tambahan kita boleh menentukan label simbolik. Sebagai contoh, berikut ialah program yang menggugurkan semua paket kecuali TCP/IPv4:

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

Secara lalai, pemasang menjana kod dalam format <количСство инструкций>,<code1> <jt1> <jf1> <k1>,..., untuk contoh kami dengan TCP ia akan menjadi

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

Untuk kemudahan pengaturcara C, format output yang berbeza boleh digunakan:

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

Teks ini boleh disalin ke dalam definisi struktur jenis struct sock_filter, seperti yang kita lakukan pada permulaan bahagian ini.

Sambungan Linux dan netsniff-ng

Sebagai tambahan kepada BPF standard, Linux dan tools/bpf/bpf_asm sokongan dan set bukan standard. Pada asasnya, arahan digunakan untuk mengakses medan struktur struct sk_buff, yang menerangkan paket rangkaian dalam kernel. Walau bagaimanapun, terdapat juga jenis arahan pembantu yang lain, sebagai contoh ldw cpu akan dimuatkan ke dalam daftar A hasil daripada menjalankan fungsi kernel raw_smp_processor_id(). (Dalam versi baharu BPF, sambungan bukan standard ini telah diperluaskan untuk menyediakan program dengan set pembantu kernel untuk mengakses memori, struktur dan menjana peristiwa.) Berikut ialah contoh penapis yang menarik di mana kami hanya menyalin pengepala paket ke dalam ruang pengguna menggunakan sambungan poff, offset muatan:

ld poff
ret a

Sambungan BPF tidak boleh digunakan dalam tcpdump, tetapi ini adalah sebab yang baik untuk membiasakan diri dengan pakej utiliti netsniff-ng, yang, antara lain, mengandungi program lanjutan netsniff-ng, yang, sebagai tambahan kepada penapisan menggunakan BPF, juga mengandungi penjana trafik yang berkesan, dan lebih maju daripada tools/bpf/bpf_asm, pemasang BPF dipanggil bpfc. Pakej ini mengandungi dokumentasi yang agak terperinci, lihat juga pautan pada akhir artikel.

sekomp

Jadi, kita sudah tahu cara menulis program BPF dengan kerumitan sewenang-wenangnya dan bersedia untuk melihat contoh baharu, yang pertama ialah teknologi seccomp, yang membolehkan, menggunakan penapis BPF, mengurus set dan set hujah panggilan sistem yang tersedia untuk proses yang diberikan dan keturunannya.

Versi pertama seccomp telah ditambahkan pada kernel pada tahun 2005 dan tidak begitu popular, kerana ia hanya menyediakan satu pilihan - untuk mengehadkan set panggilan sistem yang tersedia untuk proses kepada yang berikut: read, write, exit ΠΈ sigreturn, dan proses yang melanggar peraturan telah dibunuh menggunakan SIGKILL. Walau bagaimanapun, pada tahun 2012, seccomp menambah keupayaan untuk menggunakan penapis BPF, membolehkan anda menentukan set panggilan sistem yang dibenarkan dan juga melakukan semakan pada hujah mereka. (Menariknya, Chrome ialah salah satu pengguna pertama fungsi ini dan orang Chrome sedang membangunkan mekanisme KRSI berdasarkan versi baharu BPF dan membenarkan penyesuaian Modul Keselamatan Linux.) Pautan ke dokumentasi tambahan boleh didapati di penghujung. daripada artikel tersebut.

Ambil perhatian bahawa sudah ada artikel mengenai hab tentang menggunakan seccom, mungkin seseorang ingin membacanya sebelum (atau bukannya) membaca subseksyen berikut. Dalam artikel Bekas dan keselamatan: seccom menyediakan contoh penggunaan seccomp, kedua-dua versi 2007 dan versi menggunakan BPF (penapis dijana menggunakan libseccomp), bercakap tentang sambungan seccomp dengan Docker, dan juga menyediakan banyak pautan berguna. Dalam artikel Mengasingkan daemon dengan systemd atau "anda tidak memerlukan Docker untuk ini!" Ia meliputi, khususnya, cara menambah senarai hitam atau senarai putih panggilan sistem untuk daemon yang menjalankan systemd.

Seterusnya kita akan melihat cara menulis dan memuatkan penapis untuk seccomp dalam C kosong dan menggunakan perpustakaan libseccomp dan apakah kebaikan dan keburukan setiap pilihan, dan akhirnya, mari lihat cara seccomp digunakan oleh program strace.

Menulis dan memuatkan penapis untuk seccomp

Kita sudah tahu cara menulis program BPF, jadi mari kita lihat antara muka pengaturcaraan seccom. Anda boleh menetapkan penapis pada tahap proses dan semua proses anak akan mewarisi sekatan. Ini dilakukan menggunakan panggilan sistem seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

mana &filter - ini adalah penunjuk kepada struktur yang sudah biasa kepada kita struct sock_fprog, iaitu program BPF.

Bagaimanakah program untuk seccom berbeza daripada program untuk soket? Konteks yang dihantar. Dalam kes soket, kami diberi kawasan memori yang mengandungi paket, dan dalam kes seccomp kami diberi struktur seperti

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

ia adalah nr ialah nombor panggilan sistem yang akan dilancarkan, arch - seni bina semasa (lebih lanjut mengenai ini di bawah), args - sehingga enam argumen panggilan sistem, dan instruction_pointer ialah penunjuk kepada arahan ruang pengguna yang membuat panggilan sistem. Oleh itu, sebagai contoh, untuk memuatkan nombor panggilan sistem ke dalam daftar A kita kena cakap

ldw [0]

Terdapat ciri lain untuk program seccomp, contohnya, konteks hanya boleh diakses dengan penjajaran 32-bit dan anda tidak boleh memuatkan setengah perkataan atau bait - apabila cuba memuatkan penapis ldh [0] panggilan sistem seccomp akan kembali EINVAL. Fungsi menyemak penapis yang dimuatkan seccomp_check_filter() isirong. (Perkara yang lucu ialah, dalam komit asal yang menambah fungsi seccomp, mereka terlupa untuk menambah kebenaran untuk menggunakan arahan untuk fungsi ini mod (baki bahagian) dan kini tidak tersedia untuk program BPF seccom, sejak penambahannya akan pecah ABI.)

Pada asasnya, kita sudah tahu segala-galanya untuk menulis dan membaca program seccom. Biasanya logik program disusun sebagai senarai putih atau hitam panggilan sistem, contohnya program

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

menyemak senarai hitam empat panggilan sistem bernombor 304, 176, 239, 279. Apakah panggilan sistem ini? Kami tidak boleh mengatakan dengan pasti, kerana kami tidak tahu seni bina mana program itu ditulis. Oleh itu, pengarang seccom tawaran mulakan semua program dengan semakan seni bina (seni bina semasa ditunjukkan dalam konteks sebagai medan arch struktur struct seccomp_data). Dengan seni bina diperiksa, permulaan contoh akan kelihatan seperti:

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

dan kemudian nombor panggilan sistem kami akan mendapat nilai tertentu.

Kami menulis dan memuatkan penapis untuk penggunaan seccom libseccomp

Menulis penapis dalam kod asli atau dalam pemasangan BPF membolehkan anda mempunyai kawalan penuh ke atas hasilnya, tetapi pada masa yang sama, kadangkala lebih baik untuk mempunyai kod mudah alih dan/atau boleh dibaca. Perpustakaan akan membantu kami dalam hal ini libseccomp, yang menyediakan antara muka standard untuk menulis penapis hitam atau putih.

Mari, sebagai contoh, tulis program yang menjalankan fail binari pilihan pengguna, setelah memasang senarai hitam panggilan sistem daripada artikel di atas (program ini telah dipermudahkan untuk kebolehbacaan yang lebih baik, versi penuh boleh didapati di sini):

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

Mula-mula kita tentukan array sys_numbers daripada 40+ nombor panggilan sistem untuk disekat. Kemudian, mulakan konteks ctx dan beritahu perpustakaan apa yang kami mahu benarkan (SCMP_ACT_ALLOW) semua panggilan sistem secara lalai (lebih mudah untuk membina senarai hitam). Kemudian, satu demi satu, kami menambah semua panggilan sistem daripada senarai hitam. Sebagai tindak balas kepada panggilan sistem daripada senarai, kami meminta SCMP_ACT_TRAP, dalam kes ini seccom akan menghantar isyarat kepada proses SIGSYS dengan penerangan tentang panggilan sistem yang melanggar peraturan. Akhirnya, kami memuatkan program ke dalam kernel menggunakan seccomp_load, yang akan menyusun atur cara dan melampirkannya pada proses menggunakan panggilan sistem seccomp(2).

Untuk penyusunan yang berjaya, program mesti dikaitkan dengan perpustakaan libseccomp, sebagai contoh:

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

Contoh pelancaran yang berjaya:

$ ./seccomp_lib echo ok
ok

Contoh panggilan sistem yang disekat:

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

Kami guna straceuntuk butiran:

$ 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

bagaimana kita boleh tahu bahawa program itu telah ditamatkan kerana penggunaan panggilan sistem yang tidak sah mount(2).

Jadi, kami menulis penapis menggunakan perpustakaan libseccomp, memasukkan kod bukan remeh kepada empat baris. Dalam contoh di atas, jika terdapat sejumlah besar panggilan sistem, masa pelaksanaan boleh dikurangkan dengan ketara, kerana semakan itu hanyalah senarai perbandingan. Untuk pengoptimuman, libseccomp baru-baru ini telah patch disertakan, yang menambah sokongan untuk atribut penapis SCMP_FLTATR_CTL_OPTIMIZE. Menetapkan atribut ini kepada 2 akan menukar penapis kepada program carian binari.

Jika anda ingin melihat cara penapis carian binari berfungsi, sila lihat skrip mudah, yang menjana program sedemikian dalam pemasang BPF dengan mendail nombor panggilan sistem, contohnya:

$ 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

Tidak mustahil untuk menulis apa-apa dengan lebih pantas, kerana program BPF tidak dapat melakukan lompatan lekukan (kita tidak boleh lakukan, contohnya, jmp A atau jmp [label+X]) dan oleh itu semua peralihan adalah statik.

sekomp dan strace

Semua orang tahu utiliti itu strace ialah alat yang sangat diperlukan untuk mengkaji tingkah laku proses di Linux. Namun, ramai juga yang pernah mendengarnya isu prestasi apabila menggunakan utiliti ini. Hakikatnya ialah strace dilaksanakan menggunakan ptrace(2), dan dalam mekanisme ini kita tidak dapat menentukan set panggilan sistem yang kita perlukan untuk menghentikan proses, iaitu, contohnya, arahan

$ 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

diproses dalam masa yang lebih kurang sama, walaupun dalam kes kedua kami ingin mengesan hanya satu panggilan sistem.

Pilihan baharu --seccomp-bpf, ditambah kepada strace versi 5.3, membolehkan anda mempercepatkan proses berkali-kali dan masa permulaan di bawah jejak satu panggilan sistem sudah setanding dengan masa permulaan biasa:

$ 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

(Di sini, sudah tentu, terdapat sedikit penipuan kerana kami tidak mengesan panggilan sistem utama arahan ini. Jika kami menjejaki, sebagai contoh, newfsstat, Kemudian strace akan brek sama kuat seperti tanpa --seccomp-bpf.)

Bagaimanakah pilihan ini berfungsi? Tanpa dia strace menyambung kepada proses dan mula menggunakannya PTRACE_SYSCALL. Apabila proses terurus mengeluarkan (sebarang) panggilan sistem, kawalan dipindahkan ke strace, yang melihat hujah-hujah panggilan sistem dan menjalankannya dengan PTRACE_SYSCALL. Selepas beberapa lama, proses itu melengkapkan panggilan sistem dan apabila keluar darinya, kawalan dipindahkan semula strace, yang melihat nilai pulangan dan memulakan proses menggunakan PTRACE_SYSCALL, dan sebagainya.

BPF untuk si kecil, bahagian sifar: BPF klasik

Walau bagaimanapun, dengan seccom, proses ini boleh dioptimumkan tepat seperti yang kita mahukan. Iaitu, jika kita ingin melihat hanya pada panggilan sistem X, maka kita boleh menulis penapis BPF yang untuk X mengembalikan nilai SECCOMP_RET_TRACE, dan untuk panggilan yang tidak menarik minat kami - SECCOMP_RET_ALLOW:

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

Dalam kes ini strace pada mulanya memulakan proses sebagai PTRACE_CONT, penapis kami diproses untuk setiap panggilan sistem, jika panggilan sistem tidak X, maka proses itu terus berjalan, tetapi jika ini X, maka seccom akan memindahkan kawalan straceyang akan melihat hujah dan memulakan proses seperti PTRACE_SYSCALL (memandangkan seccomp tidak mempunyai keupayaan untuk menjalankan program semasa keluar dari panggilan sistem). Apabila panggilan sistem kembali, strace akan memulakan semula proses menggunakan PTRACE_CONT dan akan menunggu mesej baharu daripada seccom.

BPF untuk si kecil, bahagian sifar: BPF klasik

Apabila menggunakan pilihan --seccomp-bpf terdapat dua sekatan. Pertama, tidak mungkin untuk menyertai proses yang sedia ada (pilihan -p program strace), kerana ini tidak disokong oleh seccom. Kedua, tidak ada kemungkinan tiada lihat proses kanak-kanak, kerana penapis seccomp diwarisi oleh semua proses kanak-kanak tanpa keupayaan untuk melumpuhkan ini.

Sedikit lebih terperinci tentang bagaimana sebenarnya strace bekerja dengan seccomp boleh didapati daripada laporan terkini. Bagi kami, fakta yang paling menarik ialah BPF klasik yang diwakili oleh seccomp masih digunakan hari ini.

xt_bpf

Sekarang mari kita kembali ke dunia rangkaian.

Latar belakang: lama dahulu, pada tahun 2007, terasnya adalah tambah modul xt_u32 untuk penapis bersih. Ia ditulis dengan analogi dengan pengelas trafik yang lebih kuno cls_u32 dan membenarkan anda menulis peraturan binari sewenang-wenangnya untuk iptables menggunakan operasi mudah berikut: memuatkan 32 bit daripada pakej dan melaksanakan satu set operasi aritmetik padanya. Sebagai contoh,

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

Memuatkan 32 bit pengepala IP, bermula pada padding 6, dan menggunakan topeng padanya 0xFF (ambil bait rendah). Medan ini protocol Pengepala IP dan kami membandingkannya dengan 1 (ICMP). Anda boleh menggabungkan banyak semakan dalam satu peraturan, dan anda juga boleh melaksanakan pengendali @ β€” gerakkan X bait ke kanan. Sebagai contoh, peraturan

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

menyemak sama ada TCP Sequence Number tidak sama 0x29. Saya tidak akan menjelaskan lebih lanjut, kerana sudah jelas bahawa menulis peraturan sedemikian dengan tangan tidak begitu mudah. Dalam artikel BPF - bytecode yang terlupa, terdapat beberapa pautan dengan contoh penggunaan dan penjanaan peraturan untuk xt_u32. Lihat juga pautan pada akhir artikel ini.

Sejak 2013 modul dan bukannya modul xt_u32 anda boleh menggunakan modul berasaskan BPF xt_bpf. Sesiapa yang telah membaca sejauh ini sepatutnya sudah jelas tentang prinsip operasinya: jalankan BPF bytecode sebagai peraturan iptables. Anda boleh membuat peraturan baharu, contohnya, seperti ini:

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

di sini <Π±Π°ΠΉΡ‚ΠΊΠΎΠ΄> - ini ialah kod dalam format output pemasang bpf_asm secara lalai, contohnya,

$ 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

Dalam contoh ini kami menapis semua paket UDP. Konteks untuk program BPF dalam modul xt_bpf, sudah tentu, menunjuk kepada data paket, dalam kes iptables, kepada permulaan pengepala IPv4. Nilai pulangan daripada program BPF booleanJika false bermakna paket tidak sepadan.

Adalah jelas bahawa modul xt_bpf menyokong penapis yang lebih kompleks daripada contoh di atas. Mari lihat contoh sebenar daripada Cloudfare. Sehingga baru-baru ini mereka menggunakan modul tersebut xt_bpf untuk melindungi daripada serangan DDoS. Dalam artikel Memperkenalkan Alat BPF mereka menerangkan bagaimana (dan mengapa) mereka menjana penapis BPF dan menerbitkan pautan ke set utiliti untuk mencipta penapis sedemikian. Sebagai contoh, menggunakan utiliti bpfgen anda boleh mencipta program BPF yang sepadan dengan pertanyaan DNS untuk nama 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

Dalam program ini kita mula-mula memuatkan ke dalam daftar X alamat permulaan talian x04habrx03comx00 di dalam datagram UDP dan kemudian semak permintaan: 0x04686162 <-> "x04hab" dan lain-lain

Tidak lama kemudian, Cloudfare menerbitkan kod pengkompil p0f -> BPF. Dalam artikel Memperkenalkan pengkompil p0f BPF mereka bercakap tentang apa itu p0f dan cara menukar tandatangan p0f kepada 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,
...

Pada masa ini tidak lagi menggunakan Cloudfare xt_bpf, kerana mereka berpindah ke XDP - salah satu pilihan untuk menggunakan versi baharu BPF, lihat. L4Drop: Tebatan XDP DDoS.

cls_bpf

Contoh terakhir menggunakan BPF klasik dalam kernel ialah pengelas cls_bpf untuk subsistem kawalan trafik di Linux, ditambahkan pada Linux pada penghujung tahun 2013 dan secara konsep menggantikan sistem purba cls_u32.

Walau bagaimanapun, kami tidak akan menerangkan kerja itu sekarang cls_bpf, kerana dari sudut pandangan pengetahuan tentang BPF klasik ini tidak akan memberi kita apa-apa - kita sudah biasa dengan semua fungsi. Di samping itu, dalam artikel seterusnya bercakap tentang BPF Lanjutan, kami akan bertemu pengelas ini lebih daripada sekali.

Satu lagi sebab untuk tidak bercakap tentang menggunakan BPF klasik c cls_bpf Masalahnya ialah, berbanding dengan Extended BPF, skop kebolehgunaan dalam kes ini secara radikal disempitkan: program klasik tidak boleh mengubah kandungan pakej dan tidak boleh menyimpan keadaan antara panggilan.

Jadi sudah tiba masanya untuk mengucapkan selamat tinggal kepada BPF klasik dan melihat masa depan.

Selamat tinggal BPF klasik

Kami melihat bagaimana teknologi BPF, yang dibangunkan pada awal tahun sembilan puluhan, berjaya hidup selama suku abad dan sehingga akhirnya menemui aplikasi baharu. Walau bagaimanapun, sama seperti peralihan daripada mesin tindanan kepada RISC, yang berfungsi sebagai dorongan untuk pembangunan BPF klasik, pada tahun 32-an terdapat peralihan daripada mesin 64-bit kepada XNUMX-bit dan BPF klasik mula menjadi usang. Di samping itu, keupayaan BPF klasik adalah sangat terhad, dan sebagai tambahan kepada seni bina yang ketinggalan zaman - kami tidak mempunyai keupayaan untuk menyimpan keadaan antara panggilan ke program BPF, tidak ada kemungkinan interaksi pengguna langsung, tidak ada kemungkinan berinteraksi dengan kernel, kecuali untuk membaca bilangan medan struktur yang terhad sk_buff dan melancarkan fungsi pembantu yang paling mudah, anda tidak boleh menukar kandungan paket dan mengubah halanya.

Malah, pada masa ini semua yang tinggal dalam BPF klasik di Linux ialah antara muka API, dan di dalam kernel semua program klasik, sama ada penapis soket atau penapis seccomp, diterjemahkan secara automatik ke dalam format baharu, BPF Diperluas. (Kami akan bercakap tentang bagaimana ini berlaku dalam artikel seterusnya.)

Peralihan kepada seni bina baharu bermula pada 2013, apabila Alexey Starovoitov mencadangkan skim kemas kini BPF. Pada tahun 2014 patch yang sepadan mula muncul dalam inti. Setakat yang saya faham, rancangan awal hanya untuk mengoptimumkan seni bina dan pengkompil JIT untuk berjalan dengan lebih cekap pada mesin 64-bit, tetapi sebaliknya pengoptimuman ini menandakan permulaan lembaran baharu dalam pembangunan Linux.

Artikel lanjut dalam siri ini akan merangkumi seni bina dan aplikasi teknologi baharu, pada mulanya dikenali sebagai BPF dalaman, kemudian BPF lanjutan, dan kini hanya BPF.

rujukan

  1. Steven McCanne dan Van Jacobson, "Penapis Paket BSD: Seni Bina Baharu untuk Tangkapan Paket Peringkat Pengguna", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne, "libpcap: Metodologi Seni Bina dan Pengoptimuman untuk Tangkapan Paket", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. Tutorial Perlawanan U32 IPtable.
  5. BPF - bytecode yang terlupa: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. Memperkenalkan Alat BPF: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. Gambaran keseluruhan seccom: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: Bekas dan keselamatan: seccomp
  11. habr: Mengasingkan daemon dengan systemd atau "anda tidak memerlukan Docker untuk ini!"
  12. Paul Chaignon, "strace --seccomp-bpf: lihat di bawah tudung", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Sumber: www.habr.com

Tambah komen