BPF untuk si kecil, bagian nol: BPF klasik

Berkeley Packet Filters (BPF) adalah teknologi kernel Linux yang telah menjadi halaman depan publikasi teknologi berbahasa Inggris selama beberapa tahun sekarang. Konferensi diisi dengan laporan penggunaan dan pengembangan BPF. David Miller, pengelola subsistem jaringan Linux, menyampaikan ceramahnya di Linux Plumbers 2018 “Pembicaraan ini bukan tentang XDP” (XDP adalah salah satu kasus penggunaan untuk BPF). Brendan Gregg memberikan ceramah bertajuk Kekuatan Super BPF Linux. Toke Høiland-Jørgensen tertawabahwa kernel sekarang menjadi mikrokernel. Thomas Graf mempromosikan gagasan itu BPF adalah javascript untuk kernel.

Masih belum ada gambaran sistematis tentang BPF di Habré, oleh karena itu dalam rangkaian artikel saya akan mencoba berbicara tentang sejarah teknologi, menjelaskan arsitektur dan alat pengembangan, dan menguraikan bidang penerapan dan praktik penggunaan BPF. Artikel ini, nol, dalam seri ini, menceritakan sejarah dan arsitektur BPF klasik, dan juga mengungkap rahasia prinsip pengoperasiannya. tcpdump, seccomp, strace, dan banyak lagi.

Pengembangan BPF dikendalikan oleh komunitas jaringan Linux, aplikasi utama BPF yang ada terkait dengan jaringan dan oleh karena itu, dengan izin @eucariot, Saya menamakan serial ini “BPF untuk si kecil”, untuk menghormati serial hebat tersebut "Jaringan untuk si kecil".

Kursus singkat tentang sejarah BPF(c)

Teknologi BPF modern merupakan versi perbaikan dan perluasan dari teknologi lama dengan nama yang sama, sekarang disebut BPF klasik untuk menghindari kebingungan. Utilitas terkenal dibuat berdasarkan BPF klasik tcpdump, mekanisme seccomp, serta modul yang kurang dikenal xt_bpf untuk iptables dan pengklasifikasi cls_bpf. Di Linux modern, program BPF klasik secara otomatis diterjemahkan ke dalam bentuk baru, namun dari sudut pandang pengguna, API tetap ada dan kegunaan baru untuk BPF klasik, seperti yang akan kita lihat di artikel ini, masih ditemukan. Untuk alasan ini, dan juga karena mengikuti sejarah perkembangan BPF klasik di Linux, akan menjadi lebih jelas bagaimana dan mengapa ia berkembang menjadi bentuk modernnya, saya memutuskan untuk memulai dengan artikel tentang BPF klasik.

Pada akhir tahun delapan puluhan abad yang lalu, para insinyur dari Laboratorium Lawrence Berkeley yang terkenal menjadi tertarik pada pertanyaan tentang bagaimana memfilter paket jaringan dengan benar pada perangkat keras yang modern pada akhir tahun delapan puluhan abad yang lalu. Ide dasar pemfilteran, yang awalnya diterapkan dalam teknologi CSPF (CMU/Stanford Packet Filter), adalah memfilter paket yang tidak perlu sedini mungkin, yaitu. di ruang kernel, karena ini menghindari penyalinan data yang tidak perlu ke ruang pengguna. Untuk memberikan keamanan runtime untuk menjalankan kode pengguna di ruang kernel, mesin virtual sandbox digunakan.

Namun, mesin virtual untuk filter yang ada dirancang untuk berjalan pada mesin berbasis tumpukan dan tidak berjalan seefisien pada mesin RISC yang lebih baru. Hasilnya, melalui upaya para insinyur dari Berkeley Labs, teknologi BPF (Berkeley Packet Filters) baru dikembangkan, arsitektur mesin virtual yang dirancang berdasarkan prosesor Motorola 6502 - pekerja keras dari produk terkenal seperti Apple II или SPN. Mesin virtual baru meningkatkan kinerja filter puluhan kali lipat dibandingkan solusi yang ada.

Arsitektur mesin BPF

Kita akan mengenal arsitektur secara kerja, menganalisis contoh-contoh. Namun, untuk memulainya, katakanlah mesin tersebut memiliki dua register 32-bit yang dapat diakses oleh pengguna, sebuah akumulator A dan daftar indeks X, memori 64 byte (16 kata), tersedia untuk menulis dan membaca selanjutnya, dan sistem perintah kecil untuk bekerja dengan objek ini. Instruksi lompatan untuk mengimplementasikan ekspresi kondisional juga tersedia dalam program, tetapi untuk menjamin penyelesaian program tepat waktu, lompatan hanya dapat dilakukan ke depan, yaitu, khususnya, dilarang membuat loop.

Skema umum untuk menghidupkan mesin adalah sebagai berikut. Pengguna membuat program untuk arsitektur BPF dan, menggunakan beberapa mekanisme kernel (seperti panggilan sistem), memuat dan menghubungkan program ke untuk sebagian ke generator kejadian di kernel (misalnya, suatu peristiwa adalah kedatangan paket berikutnya di kartu jaringan). Ketika suatu peristiwa terjadi, kernel menjalankan program (misalnya, dalam penerjemah), dan memori mesin berhubungan dengannya untuk sebagian wilayah memori kernel (misalnya, data paket masuk).

Hal di atas sudah cukup bagi kita untuk mulai melihat contoh: kita akan mengenal sistem dan format perintah jika diperlukan. Jika Anda ingin segera mempelajari sistem perintah mesin virtual dan mempelajari segala kemampuannya, maka Anda dapat membaca artikel aslinya Filter Paket BSD dan/atau paruh pertama file Dokumentasi/jaringan/filter.txt dari dokumentasi kernel. Selain itu, Anda dapat mempelajari presentasinya libpcap: Arsitektur dan Metodologi Optimasi untuk Pengambilan Paket, di mana McCanne, salah satu penulis BPF, berbicara tentang sejarah penciptaan libpcap.

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

tcpdump

Pengembangan BPF dilakukan secara paralel dengan pengembangan frontend untuk pemfilteran paket - sebuah utilitas terkenal tcpdump. Dan, karena ini adalah contoh tertua dan paling terkenal dalam penggunaan BPF klasik, tersedia di banyak sistem operasi, kami akan memulai studi tentang teknologi ini dengannya.

(Saya menjalankan semua contoh dalam artikel ini di Linux 5.6.0-rc6. Output dari beberapa perintah telah diedit agar lebih mudah dibaca.)

Contoh: mengamati paket IPv6

Bayangkan kita ingin melihat semua paket IPv6 pada sebuah antarmuka eth0. Untuk melakukan ini kita dapat menjalankan programnya tcpdump dengan filter sederhana ip6:

$ sudo tcpdump -i eth0 ip6

Dalam hal ini, tcpdump mengkompilasi filter ip6 ke dalam bytecode arsitektur BPF dan mengirimkannya ke kernel (lihat detailnya di bagian Tcpdump: memuat). Filter yang dimuat akan dijalankan untuk setiap paket yang melewati antarmuka eth0. Jika filter mengembalikan nilai bukan nol n, lalu sampai n byte paket akan disalin ke ruang pengguna dan kita akan melihatnya di output tcpdump.

BPF untuk si kecil, bagian nol: BPF klasik

Ternyata kita bisa dengan mudah mengetahui bytecode mana yang dikirimkan ke kernel tcpdump dengan bantuan tcpdump, jika kita menjalankannya dengan opsi -d:

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

Di baris nol kita menjalankan perintah ldh [12], yang merupakan singkatan dari “memuat ke dalam register A setengah kata (16 bit) terletak di alamat 12” dan satu-satunya pertanyaan adalah jenis memori apa yang kita tangani? Jawabannya adalah pada x dimulai (x+1)byte ke-th dari paket jaringan yang dianalisis. Kami membaca paket dari antarmuka Ethernet eth0dan ini saranabahwa paketnya terlihat seperti ini (untuk mempermudah, kita asumsikan tidak ada tag VLAN di dalam paket):

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

Jadi setelah menjalankan perintah ldh [12] di daftar A akan ada lapangan Ether Type — jenis paket yang dikirimkan dalam frame Ethernet ini. Pada baris 1 kami membandingkan isi register A (jenis paket) c 0x86dddan ini dan miliki Tipe yang kami minati adalah IPv6. Pada baris 1, selain perintah perbandingan, ada dua kolom lagi - jt 2 и jf 3 — tanda yang harus Anda tuju jika perbandingan berhasil (A == 0x86dd) dan tidak berhasil. Jadi, dalam kasus yang berhasil (IPv6) kita pergi ke baris 2, dan dalam kasus yang gagal - ke baris 3. Pada baris 3 program diakhiri dengan kode 0 (jangan menyalin paket), pada baris 2 program diakhiri dengan kode 262144 (salin paket saya maksimal 256 kilobyte).

Contoh yang lebih rumit: kita melihat paket TCP berdasarkan port tujuan

Mari kita lihat seperti apa filter yang menyalin semua paket TCP dengan port tujuan 666. Kita akan mempertimbangkan kasus IPv4, karena kasus IPv6 lebih sederhana. Setelah mempelajari contoh ini, Anda dapat menjelajahi sendiri filter IPv6 sebagai latihan (ip6 and tcp dst port 666) dan filter untuk kasus umum (tcp dst port 666). Jadi, filter yang kami minati terlihat 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 mengetahui fungsi baris 0 dan 1. Pada baris 2 kita telah memeriksa bahwa ini adalah paket IPv4 (Ether Type = 0x800) dan memuatnya ke dalam register A byte ke-24 dari paket tersebut. Paket kami terlihat seperti

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

yang berarti kita memuat ke dalam register A bidang Protokol pada header IP, yang logis, karena kita hanya ingin menyalin paket TCP. Kami membandingkan Protokol dengan 0x6 (IPPROTO_TCP) pada jalur 3.

Pada baris 4 dan 5 kami memuat setengah kata yang terletak di alamat 20 dan menggunakan perintah jset periksa apakah salah satu dari ketiganya disetel bendera - memakai masker yang dikeluarkan jset tiga bit paling signifikan dihapus. Dua dari tiga bit memberitahu kita apakah paket tersebut merupakan bagian dari paket IP yang terfragmentasi, dan jika demikian, apakah itu merupakan fragmen terakhir. Bit ketiga dicadangkan dan harus nol. Kami tidak ingin memeriksa paket integer atau paket yang rusak, jadi kami memeriksa ketiga bit tersebut.

Baris 6 adalah yang paling menarik dalam daftar ini. Ekspresi ldxb 4*([14]&0xf) berarti kita memuat ke dalam register X empat bit paling tidak signifikan dari byte kelima belas paket dikalikan dengan 4. Empat bit paling tidak signifikan dari byte kelima belas adalah bidang Panjang Tajuk Internet Header IPv4, yang menyimpan panjang header dalam kata-kata, jadi Anda perlu mengalikannya dengan 4. Menariknya, ekspresinya 4*([14]&0xf) adalah sebutan untuk skema pengalamatan khusus yang hanya dapat digunakan dalam formulir ini dan hanya untuk register X, yaitu kami juga tidak bisa mengatakannya ldb 4*([14]&0xf) atau ldxb 5*([14]&0xf) (kita hanya dapat menentukan offset yang berbeda, misalnya, ldxb 4*([16]&0xf)). Jelas bahwa skema pengalamatan ini ditambahkan ke BPF justru untuk menerima X (daftar indeks) Panjang header IPv4.

Jadi pada baris 7 kami mencoba memuat setengah kata di (X+16). Mengingat bahwa 14 byte ditempati oleh header Ethernet, dan X berisi panjang header IPv4, kami memahaminya di A Port tujuan TCP dimuat:

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

Terakhir, pada baris 8 kami membandingkan port tujuan dengan nilai yang diinginkan dan pada baris 9 atau 10 kami mengembalikan hasilnya - apakah akan menyalin paket atau tidak.

Tcpdump: memuat

Pada contoh sebelumnya, kami secara khusus tidak membahas secara rinci bagaimana tepatnya kami memuat bytecode BPF ke dalam kernel untuk pemfilteran paket. Secara umum, tcpdump porting ke banyak sistem dan untuk bekerja dengan filter tcpdump menggunakan perpustakaan libpcap. Secara singkat, untuk menempatkan filter pada antarmuka menggunakan libpcap, Anda perlu melakukan hal berikut:

Untuk melihat bagaimana fungsinya pcap_setfilter diimplementasikan di Linux, kami menggunakan strace (beberapa baris telah dihapus):

$ 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 keluaran pertama yang kita buat soket mentah untuk membaca semua frame Ethernet dan mengikatnya ke antarmuka eth0. dari contoh pertama kita kita tahu bahwa filter ip akan terdiri dari empat instruksi BPF, dan pada baris ketiga kita melihat bagaimana menggunakan opsi tersebut SO_ATTACH_FILTER panggilan sistem setsockopt kami memuat dan menghubungkan filter dengan panjang 4. Ini adalah filter kami.

Perlu dicatat bahwa di BPF klasik, memuat dan menghubungkan filter selalu terjadi sebagai operasi atom, dan di BPF versi baru, memuat program dan mengikatnya ke generator peristiwa dipisahkan dalam waktu.

Kebenaran Tersembunyi

Versi keluaran yang sedikit lebih lengkap terlihat 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 disebutkan di atas, kami memuat dan menghubungkan filter kami ke soket di jalur 5, tetapi apa yang terjadi di jalur 3 dan 4? Ternyata ini libpcap menjaga kami - sehingga keluaran dari filter kami tidak menyertakan paket yang tidak memuaskannya, perpustakaan menghubungkan penyaring tiruan ret #0 (jatuhkan semua paket), mengalihkan soket ke mode non-pemblokiran dan mencoba mengurangi semua paket yang tersisa dari filter sebelumnya.

Secara total, untuk memfilter paket di Linux menggunakan BPF klasik, Anda perlu memiliki filter dalam bentuk struktur seperti struct sock_fprog dan soket terbuka, setelah itu filter dapat dipasang ke soket menggunakan panggilan sistem setsockopt.

Menariknya, filter tersebut dapat dipasang ke soket apa pun, tidak hanya mentah. Di Sini contoh sebuah program yang memotong semua kecuali dua byte pertama dari semua datagram UDP yang masuk. (Saya menambahkan komentar di kode agar tidak mengacaukan artikel.)

Lebih detail tentang penggunaan setsockopt untuk menghubungkan filter, lihat soket(7), tapi tentang menulis filter Anda sendiri seperti struct sock_fprog tanpa bantuan tcpdump kita akan berbicara di bagian ini Memprogram BPF dengan tangan kita sendiri.

BPF klasik dan abad ke-XNUMX

BPF dimasukkan ke dalam Linux pada tahun 1997 dan tetap menjadi pekerja keras untuk waktu yang lama libpcap tanpa perubahan khusus apa pun (perubahan khusus Linux, tentu saja, Kami pernah, tapi mereka tidak mengubah gambaran global). Tanda-tanda serius pertama bahwa BPF akan berkembang muncul pada tahun 2011, ketika Eric Dumazet mengajukan usulan tambalan, yang menambahkan Just In Time Compiler ke kernel - penerjemah untuk mengonversi bytecode BPF ke asli x86_64 kode.

Kompiler JIT adalah yang pertama dalam rantai perubahan: pada tahun 2012 muncul kemampuan untuk menulis filter untuk detik, menggunakan BPF, pada bulan Januari 2013 ada ditambahkan modul xt_bpf, yang memungkinkan Anda menulis aturan untuk iptables dengan bantuan BPF, dan pada bulan Oktober 2013 telah ditambahkan juga sebuah modul cls_bpf, yang memungkinkan Anda menulis pengklasifikasi lalu lintas menggunakan BPF.

Kita akan segera melihat semua contoh ini secara lebih rinci, tetapi pertama-tama akan berguna bagi kita untuk mempelajari cara menulis dan mengkompilasi program arbitrer untuk BPF, karena kemampuan yang disediakan oleh perpustakaan libpcap terbatas (contoh sederhana: filter dihasilkan libpcap hanya dapat mengembalikan dua nilai - 0 atau 0x40000) atau secara umum, seperti dalam kasus seccomp, tidak berlaku.

Memprogram BPF dengan tangan kita sendiri

Mari berkenalan dengan format biner instruksi BPF, sangat sederhana:

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

Setiap instruksi menempati 64 bit, dimana 16 bit pertama adalah kode instruksi, kemudian ada dua indentasi delapan bit, jt и jf, dan 32 bit untuk argumennya K, tujuannya bervariasi dari satu perintah ke perintah lainnya. Misalnya perintah ret, yang mengakhiri program yang memiliki kode 6, dan nilai kembalian diambil dari konstanta K. Di C, satu instruksi BPF direpresentasikan sebagai sebuah struktur

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

dan keseluruhan program berbentuk struktur

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

Dengan demikian, kita sudah dapat menulis program (misalnya kita mengetahui kode instruksi dari [1]). Seperti inilah tampilan filternya ip6 dari contoh pertama kita:

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 dapat menggunakannya secara legal dalam panggilan

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

Menulis program dalam bentuk kode mesin sangat tidak nyaman, tetapi terkadang diperlukan (misalnya, untuk debugging, membuat pengujian unit, menulis artikel di Habré, dll.). Untuk kenyamanan, dalam file <linux/filter.h> makro pembantu didefinisikan - contoh yang sama seperti di atas dapat ditulis ulang menjadi

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

Namun, opsi ini sangat tidak nyaman. Inilah alasan pemrogram kernel Linux, dan oleh karena itu ada di direktori tools/bpf kernel Anda dapat menemukan assembler dan debugger untuk bekerja dengan BPF klasik.

Bahasa rakitan sangat mirip dengan keluaran debug tcpdump, tapi selain itu kita dapat menentukan label simbolik. Misalnya, berikut adalah program yang menghapus 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 default, assembler menghasilkan kode dalam format <количество инструкций>,<code1> <jt1> <jf1> <k1>,..., untuk contoh kita dengan TCP akan seperti itu

$ 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 kenyamanan programmer C, format output yang berbeda dapat 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 dapat disalin ke dalam definisi struktur tipe struct sock_filter, seperti yang kami lakukan di awal bagian ini.

Ekstensi Linux dan netsniff-ng

Selain BPF standar, Linux dan tools/bpf/bpf_asm dukungan dan set non-standar. Pada dasarnya, instruksi digunakan untuk mengakses bidang suatu struktur struct sk_buff, yang menjelaskan paket jaringan di kernel. Namun, ada juga jenis instruksi pembantu lainnya, misalnya ldw cpu akan dimuat ke dalam register A hasil menjalankan fungsi kernel raw_smp_processor_id(). (Dalam versi baru BPF, ekstensi non-standar ini telah diperluas untuk menyediakan program dengan sekumpulan pembantu kernel untuk mengakses memori, struktur, dan menghasilkan peristiwa.) Berikut adalah contoh menarik dari filter di mana kita hanya menyalin header paket ke ruang pengguna menggunakan ekstensi poff, offset muatan:

ld poff
ret a

Ekstensi BPF tidak dapat digunakan di tcpdump, tapi ini adalah alasan bagus untuk mengenal paket utilitas netsniff-ng, yang antara lain berisi program lanjutan netsniff-ng, yang selain memfilter menggunakan BPF, juga berisi generator lalu lintas yang efektif, dan lebih canggih dari tools/bpf/bpf_asm, seorang perakit BPF menelepon bpfc. Paket tersebut berisi dokumentasi yang cukup detail, lihat juga link di akhir artikel.

detik

Jadi, kita sudah tahu cara menulis program BPF dengan kompleksitas yang berubah-ubah dan siap untuk melihat contoh baru, yang pertama adalah teknologi seccomp, yang memungkinkan, dengan menggunakan filter BPF, untuk mengelola kumpulan argumen panggilan sistem yang tersedia untuk proses tertentu dan keturunannya.

Versi pertama seccomp ditambahkan ke kernel pada tahun 2005 dan tidak begitu populer, karena hanya menyediakan satu pilihan - untuk membatasi rangkaian panggilan sistem yang tersedia untuk suatu proses sebagai berikut: read, write, exit и sigreturn, dan proses yang melanggar aturan dihentikan menggunakan SIGKILL. Namun, pada tahun 2012, seccomp menambahkan kemampuan untuk menggunakan filter BPF, memungkinkan Anda menentukan sekumpulan panggilan sistem yang diizinkan dan bahkan melakukan pemeriksaan pada argumennya. (Menariknya, Chrome adalah salah satu pengguna pertama fungsi ini, dan orang-orang Chrome saat ini sedang mengembangkan mekanisme KRSI berdasarkan versi baru BPF dan memungkinkan penyesuaian Modul Keamanan Linux.) Tautan ke dokumentasi tambahan dapat ditemukan di bagian akhir artikel tersebut.

Perhatikan bahwa sudah ada artikel di hub tentang penggunaan seccomp, mungkin seseorang ingin membacanya sebelum (atau sebagai gantinya) membaca subbagian berikut. Di dalam artikel Kontainer dan keamanan: seccomp memberikan contoh penggunaan seccomp, baik versi 2007 maupun versi yang menggunakan BPF (filter dibuat menggunakan libseccomp), membahas tentang koneksi seccomp dengan Docker, dan juga menyediakan banyak tautan bermanfaat. Di dalam artikel Mengisolasi daemon dengan systemd atau “Anda tidak memerlukan Docker untuk ini!” Ini mencakup, khususnya, cara menambahkan daftar hitam atau daftar putih panggilan sistem untuk daemon yang menjalankan systemd.

Selanjutnya kita akan melihat cara menulis dan memuat filter seccomp di bare C dan menggunakan perpustakaan libseccomp dan apa kelebihan dan kekurangan masing-masing opsi, dan terakhir, mari kita lihat bagaimana seccomp digunakan oleh program ini strace.

Menulis dan memuat filter untuk seccomp

Kita sudah mengetahui cara menulis program BPF, jadi mari kita lihat dulu antarmuka pemrograman seccomp. Anda dapat mengatur filter pada tingkat proses, dan semua proses anak akan mewarisi batasan tersebut. Ini dilakukan dengan menggunakan panggilan sistem seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

dimana &filter - ini adalah penunjuk ke struktur yang sudah kita kenal struct sock_fprog, yaitu. program BPF.

Apa perbedaan program untuk seccomp dengan program untuk soket? Konteks yang ditransmisikan. Dalam kasus soket, kami diberi area memori yang berisi paket, dan dalam kasus seccomp kami diberi struktur seperti

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

Di sini nr adalah nomor panggilan sistem yang akan diluncurkan, arch - arsitektur saat ini (lebih lanjut tentang ini di bawah), args - hingga enam argumen panggilan sistem, dan instruction_pointer adalah penunjuk ke instruksi ruang pengguna yang membuat panggilan sistem. Jadi, misalnya, untuk memuat nomor panggilan sistem ke dalam register A kita harus mengatakan

ldw [0]

Ada fitur lain untuk program seccomp, misalnya, konteksnya hanya dapat diakses dengan penyelarasan 32-bit dan Anda tidak dapat memuat setengah kata atau satu byte - saat mencoba memuat filter ldh [0] panggilan sistem seccomp akan kembali EINVAL. Fungsi ini memeriksa filter yang dimuat seccomp_check_filter() kernel. (Lucunya, dalam komit asli yang menambahkan fungsionalitas seccomp, mereka lupa menambahkan izin untuk menggunakan instruksi ke fungsi ini mod (sisa pembagian) dan sekarang tidak tersedia untuk program seccomp BPF, sejak penambahannya akan pecah ABI.)

Pada dasarnya kita sudah mengetahui segalanya untuk menulis dan membaca program seccomp. Biasanya logika program disusun sebagai daftar putih atau hitam dari panggilan sistem, misalnya program

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

memeriksa daftar hitam empat panggilan sistem bernomor 304, 176, 239, 279. Apa saja panggilan sistem ini? Kami tidak dapat memastikannya, karena kami tidak mengetahui arsitektur apa yang digunakan untuk menulis program tersebut. Oleh karena itu, penulis seccomp tawaran mulai semua program dengan pemeriksaan arsitektur (arsitektur saat ini ditunjukkan dalam konteks sebagai bidang arch struktur struct seccomp_data). Dengan arsitektur yang diperiksa, contoh awal akan terlihat seperti:

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

dan kemudian nomor panggilan sistem kita akan mendapatkan nilai tertentu.

Kami menulis dan memuat filter untuk menggunakan seccomp libseccomp

Menulis filter dalam kode asli atau dalam rakitan BPF memungkinkan Anda memiliki kontrol penuh atas hasilnya, namun pada saat yang sama, terkadang lebih baik memiliki kode yang portabel dan/atau dapat dibaca. Perpustakaan akan membantu kami dalam hal ini libseccomp, yang menyediakan antarmuka standar untuk menulis filter hitam atau putih.

Mari kita, misalnya, menulis sebuah program yang menjalankan file biner pilihan pengguna, setelah sebelumnya menginstal daftar hitam panggilan sistem dari artikel di atas (program telah disederhanakan agar lebih mudah dibaca, versi lengkap dapat ditemukan 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]);
}

Pertama kita mendefinisikan sebuah array sys_numbers dari 40+ nomor panggilan sistem untuk diblokir. Kemudian, inisialisasi konteksnya ctx dan beri tahu perpustakaan apa yang ingin kami izinkan (SCMP_ACT_ALLOW) semua panggilan sistem secara default (lebih mudah untuk membuat daftar hitam). Lalu, satu per satu, kami menambahkan semua panggilan sistem dari daftar hitam. Menanggapi panggilan sistem dari daftar, kami meminta SCMP_ACT_TRAP, dalam hal ini seccomp akan mengirimkan sinyal ke proses SIGSYS dengan penjelasan panggilan sistem mana yang melanggar aturan. Terakhir, kami memuat program ke dalam kernel menggunakan seccomp_load, yang akan mengkompilasi program dan melampirkannya ke proses menggunakan panggilan sistem seccomp(2).

Agar kompilasi berhasil, program harus terhubung dengan perpustakaan libseccomp, misalnya:

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

Contoh peluncuran yang sukses:

$ ./seccomp_lib echo ok
ok

Contoh panggilan sistem yang diblokir:

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

Gunakan straceuntuk detailnya:

$ 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 bisa mengetahui bahwa program tersebut dihentikan karena penggunaan panggilan sistem ilegal mount(2).

Jadi, kami menulis filter menggunakan perpustakaan libseccomp, memasukkan kode non-sepele ke dalam empat baris. Dalam contoh di atas, jika terdapat sejumlah besar panggilan sistem, waktu eksekusi dapat dikurangi secara signifikan, karena pemeriksaan hanyalah daftar perbandingan. Untuk pengoptimalan, libseccomp baru-baru ini memilikinya sudah termasuk tambalan, yang menambahkan dukungan untuk atribut filter SCMP_FLTATR_CTL_OPTIMIZE. Menyetel atribut ini ke 2 akan mengubah filter menjadi program pencarian biner.

Jika Anda ingin melihat cara kerja filter pencarian biner, lihatlah skrip sederhana, yang menghasilkan program seperti itu di assembler BPF dengan menghubungi nomor panggilan sistem, misalnya:

$ 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 mungkin menulis apa pun secara signifikan lebih cepat, karena program BPF tidak dapat melakukan lompatan indentasi (misalnya, kita tidak dapat melakukan jmp A или jmp [label+X]) dan oleh karena itu semua transisi bersifat statis.

seccomp dan strace

Semua orang tahu kegunaannya strace adalah alat yang sangat diperlukan untuk mempelajari perilaku proses di Linux. Namun, banyak juga yang telah mendengarnya masalah kinerja saat menggunakan utilitas ini. Faktanya adalah itu strace diimplementasikan menggunakan ptrace(2), dan dalam mekanisme ini kita tidak dapat menentukan pada rangkaian panggilan sistem mana kita perlu menghentikan proses, misalnya perintah

$ 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 waktu yang kira-kira sama, meskipun dalam kasus kedua kami hanya ingin melacak satu panggilan sistem.

Pilihan baru --seccomp-bpfditambahkan ke strace versi 5.3, memungkinkan Anda mempercepat proses berkali-kali dan waktu pengaktifan di bawah jejak satu panggilan sistem sudah sebanding dengan waktu pengaktifan 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, tentu saja, ada sedikit penipuan karena kami tidak menelusuri panggilan sistem utama dari perintah ini. Jika kami menelusuri, misalnya, newfsstat, Kemudian strace akan mengerem sekeras tanpanya --seccomp-bpf.)

Bagaimana cara kerja opsi ini? Tanpanya strace terhubung ke proses dan mulai menggunakannya PTRACE_SYSCALL. Ketika proses terkelola mengeluarkan panggilan sistem (apa pun), kendali ditransfer ke strace, yang melihat argumen dari panggilan sistem dan menjalankannya PTRACE_SYSCALL. Setelah beberapa waktu, proses menyelesaikan panggilan sistem dan ketika keluar, kontrol ditransfer lagi strace, yang melihat nilai yang dikembalikan dan memulai proses menggunakan PTRACE_SYSCALL, dan seterusnya.

BPF untuk si kecil, bagian nol: BPF klasik

Namun, dengan seccomp, proses ini dapat dioptimalkan persis seperti yang kita inginkan. Yaitu jika kita ingin melihat system callnya saja X, maka kita dapat menulis filter BPF untuk itu X mengembalikan nilai SECCOMP_RET_TRACE, dan untuk panggilan yang tidak menarik bagi kami - SECCOMP_RET_ALLOW:

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

Dalam hal ini strace awalnya memulai proses sebagai PTRACE_CONT, filter kami diproses untuk setiap panggilan sistem, jika panggilan sistem tidak X, maka proses tetap berjalan, namun jika ini X, maka seccomp akan mentransfer kendali straceyang akan melihat argumen dan memulai prosesnya PTRACE_SYSCALL (karena seccomp tidak memiliki kemampuan untuk menjalankan program saat keluar dari panggilan sistem). Ketika panggilan sistem kembali, strace akan memulai kembali proses menggunakan PTRACE_CONT dan akan menunggu pesan baru dari seccomp.

BPF untuk si kecil, bagian nol: BPF klasik

Saat menggunakan opsi --seccomp-bpf ada dua batasan. Pertama, tidak mungkin untuk bergabung dengan proses yang sudah ada (option -p program strace), karena ini tidak didukung oleh seccomp. Kedua, tidak ada kemungkinan tidak lihat proses anak, karena filter seccomp diwarisi oleh semua proses anak tanpa kemampuan untuk menonaktifkannya.

Sedikit lebih detail tentang bagaimana tepatnya strace bekerja dengan seccomp dapat ditemukan dari laporan terbaru. Bagi kami, fakta yang paling menarik adalah BPF klasik yang diwakili oleh seccomp masih digunakan sampai sekarang.

xt_bpf

Sekarang mari kita kembali ke dunia jaringan.

Latar Belakang: Dahulu kala, pada tahun 2007, intinya adalah ditambahkan modul xt_u32 untuk netfilter. Itu ditulis dengan analogi dengan pengklasifikasi lalu lintas yang lebih kuno cls_u32 dan memungkinkan Anda menulis aturan biner arbitrer untuk iptables menggunakan operasi sederhana berikut: memuat 32 bit dari sebuah paket dan melakukan serangkaian operasi aritmatika pada paket tersebut. Misalnya,

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

Memuat 32 bit header IP, dimulai dari padding 6, dan menerapkan mask pada bit tersebut 0xFF (ambil byte rendah). Lapangan ini protocol IP header dan kita bandingkan dengan 1 (ICMP). Anda dapat menggabungkan banyak pemeriksaan dalam satu aturan, dan Anda juga dapat menjalankan operator @ — pindahkan X byte ke kanan. Misalnya saja aturannya

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

memeriksa apakah Nomor Urutan TCP tidak sama 0x29. Saya tidak akan menjelaskan lebih jauh, karena sudah jelas bahwa menulis aturan seperti itu dengan tangan sangat tidak nyaman. Di dalam artikel BPF - bytecode yang terlupakan, ada beberapa tautan dengan contoh penggunaan dan pembuatan aturan xt_u32. Lihat juga tautan di akhir artikel ini.

Sejak 2013 modul bukan modul xt_u32 Anda dapat menggunakan modul berbasis BPF xt_bpf. Siapapun yang telah membaca sejauh ini seharusnya sudah memahami prinsip operasinya: jalankan bytecode BPF sebagai aturan iptables. Anda dapat membuat aturan baru, misalnya seperti ini:

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

di sini <байткод> - ini adalah kode dalam format keluaran assembler bpf_asm secara default, misalnya,

$ 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 memfilter semua paket UDP. Konteks untuk program BPF dalam sebuah modul xt_bpf, tentu saja, menunjuk ke paket data, dalam kasus iptables, ke awal header IPv4. Nilai pengembalian dari program BPF booleanDimana false berarti paketnya tidak cocok.

Jelas bahwa modul xt_bpf mendukung filter yang lebih kompleks daripada contoh di atas. Mari kita lihat contoh nyata dari Cloudfare. Sampai saat ini mereka menggunakan modul tersebut xt_bpf untuk melindungi terhadap serangan DDoS. Di dalam artikel Memperkenalkan Alat BPF mereka menjelaskan bagaimana (dan mengapa) mereka menghasilkan filter BPF dan mempublikasikan link ke serangkaian utilitas untuk membuat filter tersebut. Misalnya dengan menggunakan utilitas bpfgen Anda dapat membuat program BPF yang cocok dengan kueri DNS untuk sebuah 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

Di programnya kita load dulu ke register X alamat awal baris x04habrx03comx00 di dalam datagram UDP dan kemudian periksa permintaan: 0x04686162 <-> "x04hab" dan lain-lain

Beberapa saat kemudian, Cloudfare menerbitkan kode kompiler p0f -> BPF. Di dalam artikel Memperkenalkan kompiler p0f BPF mereka berbicara tentang apa itu p0f dan bagaimana mengkonversi tanda tangan p0f ke 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,
...

Saat ini tidak lagi menggunakan Cloudfare xt_bpf, sejak mereka pindah ke XDP - salah satu opsi untuk menggunakan BPF versi baru, lihat. L4Drop: Mitigasi DDoS XDP.

cls_bpf

Contoh terakhir penggunaan BPF klasik di kernel adalah classifier cls_bpf untuk subsistem kontrol lalu lintas di Linux, ditambahkan ke Linux pada akhir tahun 2013 dan secara konseptual menggantikan yang kuno cls_u32.

Namun, sekarang kami tidak akan menjelaskan pekerjaannya cls_bpf, karena dari sudut pandang pengetahuan tentang BPF klasik, ini tidak akan memberi kita apa pun - kita sudah terbiasa dengan semua fungsinya. Selain itu, pada artikel selanjutnya yang membahas tentang BPF yang Diperpanjang, kita akan bertemu dengan pengklasifikasi ini lebih dari sekali.

Alasan lain untuk tidak membicarakan penggunaan BPF klasik c cls_bpf Masalahnya adalah, dibandingkan dengan Extended BPF, cakupan penerapannya dalam hal ini sangat menyempit: program klasik tidak dapat mengubah konten paket dan tidak dapat menyimpan status di antara panggilan.

Jadi inilah waktunya untuk mengucapkan selamat tinggal pada BPF klasik dan menatap masa depan.

Perpisahan dengan BPF klasik

Kami melihat bagaimana teknologi BPF, yang dikembangkan pada awal tahun sembilan puluhan, berhasil bertahan selama seperempat abad dan hingga akhir menemukan penerapan baru. Namun, serupa dengan transisi dari mesin tumpukan ke RISC, yang menjadi pendorong pengembangan BPF klasik, pada tahun 32-an terjadi transisi dari mesin 64-bit ke XNUMX-bit dan BPF klasik mulai menjadi usang. Selain itu, kemampuan BPF klasik sangat terbatas, dan selain arsitekturnya yang ketinggalan jaman, kami tidak memiliki kemampuan untuk menyimpan status antara panggilan ke program BPF, tidak ada kemungkinan interaksi langsung dengan pengguna, tidak ada kemungkinan untuk berinteraksi dengan kernel, kecuali untuk membaca sejumlah bidang struktur sk_buff dan meluncurkan fungsi pembantu yang paling sederhana, Anda tidak dapat mengubah isi paket dan mengalihkannya.

Faktanya, saat ini yang tersisa dari BPF klasik di Linux hanyalah antarmuka API, dan di dalam kernel semua program klasik, baik itu filter soket atau filter seccomp, secara otomatis diterjemahkan ke dalam format baru, Extended BPF. (Kita akan membahas bagaimana tepatnya hal ini terjadi di artikel berikutnya.)

Transisi ke arsitektur baru dimulai pada tahun 2013, ketika Alexei Starovoitov mengusulkan skema pembaruan BPF. Pada tahun 2014 patch yang sesuai mulai muncul di inti. Sejauh yang saya pahami, rencana awalnya hanya untuk mengoptimalkan arsitektur dan kompiler JIT agar berjalan lebih efisien pada mesin 64-bit, namun optimalisasi ini menandai dimulainya babak baru dalam pengembangan Linux.

Artikel selanjutnya dalam seri ini akan membahas arsitektur dan aplikasi teknologi baru, yang awalnya dikenal sebagai BPF internal, kemudian diperluas menjadi BPF, dan sekarang hanya disebut BPF.

referensi

  1. Steven McCanne dan Van Jacobson, "Filter Paket BSD: Arsitektur Baru untuk Pengambilan Paket Tingkat Pengguna", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne, "libpcap: Sebuah Arsitektur dan Metodologi Optimasi untuk Pengambilan Paket", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. Tutorial Pertandingan U32 IPtable.
  5. BPF - bytecode yang terlupakan: 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. Ikhtisar detik: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: Wadah dan keamanan: seccomp
  11. habr: Mengisolasi daemon dengan systemd atau “Anda tidak memerlukan Docker untuk ini!”
  12. Paul Chaignon, "strace --seccomp-bpf: lihat di balik terpal", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Sumber: www.habr.com

Tambah komentar