適合小朋友的 BPF,第 XNUMX 部分:經典 BPF

Berkeley Packet Filters (BPF) 是一項 Linux 核心技術,多年來一直出現在英語技術出版物的頭版上。 會議上充斥著關於BPF的使用和開發的報告。 Linux 網路子系統維護者 David Miller 在 Linux Plumbers 2018 上發表演講 “這個談話不是關於 XDP” (XDP 是 BPF 的用例之一)。 布倫丹·格雷格發表題為“ Linux BPF 的超能力。 托克·霍伊蘭-約根森 內核現在是微內核。 托馬斯·格拉夫(Thomas Graf)提倡這樣的想法: BPF 是核心的 javascript.

Habré 上仍然沒有對 BPF 進行系統性的描述,因此在一系列文章中我將嘗試談論該技術的歷史,描述架構和開發工具,並概述使用 BPF 的應用領域和實踐。 系列文章零,講述了經典BPF的歷史和架構,也揭示了其運作原理的秘密。 tcpdump, seccomp, strace, и многое другое。

BPF的開發由Linux網路社群控制,BPF現有的主要應用程式與網路相關,因此,經過許可 @eucariot,我將這個系列稱為“BPF for the Little Ones”,以紀念這個偉大的系列 “適合小孩子的網路”.

BPF 歷史短期課程(c)

現代 BPF 技術是同名舊技術的改進和擴展版本,現在稱為經典 BPF 以避免混淆。 基於經典 BPF 創建了一個著名的實用程序 tcpdump, 機制 seccomp,以及鮮為人知的模組 xt_bpfiptables 和分類器 cls_bpf。 在現代 Linux 中,經典 BPF 程式會自動轉換為新形式,但是,從使用者的角度來看,API 仍然存在,正如我們將在本文中看到的,經典 BPF 的新用途仍在被發現。 出於這個原因,也因為跟隨經典 BPF 在 Linux 中的發展歷史,會更清楚它是如何以及為何演變成現代形式的,所以我決定從一篇關於經典 BPF 的文章開始。

在八十年代末,著名的勞倫斯伯克利實驗室的工程師對如何在上世紀八十年代末現代化的硬體上正確過濾網路資料包的問題產生了興趣。 過濾的基本思想,最初是在CSPF(CMU/Stanford Packet Filter)技術中實現的,是儘早過濾掉不必要的資料包,即在核心空間中,因為這可以避免將不必要的資料複製到使用者空間中。 為了為在核心空間中運行的使用者程式碼提供運行時安全性,使用了沙盒虛擬機器。

然而,現有過濾器的虛擬機器被設計為在基於堆疊的機器上運行,並且在較新的 RISC 機器上運行效率不高。 於是,經過柏克萊實驗室工程師的努力,開發出了一種新的BPF(柏克萊包過濾器)技術,其虛擬機架構是基於摩托羅拉6502處理器設計的,摩托羅拉XNUMX處理器是諸如 蘋果II未列明在其他編號。 與現有解決方案相比,新的虛擬機器將過濾器效能提高了數十倍。

BPF機器架構

我們將以工作方式透過分析範例來熟悉架構。 然而,首先,假設機器有兩個可供使用者存取的 32 位元暫存器,一個累加器 A 和索引暫存器 X、64 位元組記憶體(16 個字),可用於寫入和後續讀取,以及用於處理這些物件的小型命令系統。 程式中也有實現條件表達式的跳轉指令,但為了確保程式及時完成,跳轉只能向前進行,即特別禁止創建循環。

啟動機器的整體方案如下。 使用者為 BPF 架構建立一個程序,並使用 一些 核心機制(例如係統呼叫),載入並連接程序 對某些人 到核心中的事件產生器(例如,事件是網路卡上下一個資料包的到達)。 當事件發生時,核心運行程式(例如在解釋器中),機器記憶體對應 對某些人 內核記憶體區域(例如,傳入資料包的資料)。

以上內容足以讓我們開始查看範例:我們將根據需要熟悉系統和命令格式。 如果你想立即研究虛擬機器的命令系統並了解其所有功能,那麼你可以閱讀原文 BSD 封包過濾器 和/或文件的前半部分 文檔/網路/filter.txt 來自內核文檔。 此外,您還可以研究演示 libpcap:資料包擷取的架構和最佳化方法其中,BPF 的作者之一 McCanne 講述了創作的歷史 libpcap.

我們現在繼續考慮在 Linux 上使用經典 BPF 的所有重要範例: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.

轉儲

BPF 的開發與包過濾前端的開發同時進行 - 一個眾所周知的實用程序 tcpdump。 而且,由於這是使用經典 BPF 的最古老和最著名的範例,可在許多作業系統上使用,因此我們將從它開始研究該技術。

(我在 Linux 上運行了本文中的所有範例 5.6.0-rc6。 一些命令的輸出已被編輯以提高可讀性。)

範例:觀察 IPv6 封包

假設我們想要查看介面上的所有 IPv6 封包 eth0。 為此,我們可以運行該程序 tcpdump 用一個簡單的過濾器 ip6:

$ sudo tcpdump -i eth0 ip6

在這種情況下, tcpdump 編譯過濾器 ip6 寫入 BPF 架構字節碼並將其傳送到核心(詳細資訊請參閱 Tcpdump:正在加載)。 載入的過濾器將對通過介面的每個資料包運行 eth0。 如果過濾器傳回非零值 n,然後直到 n 封包的位元組將被複製到用戶空間,我們將在輸出中看到它 tcpdump.

適合小朋友的 BPF,第 XNUMX 部分:經典 BPF

事實證明,我們可以很容易地找出哪些字節碼發送到了內核 tcpdump 在的幫助下 tcpdump,如果我們使用選項來運行它 -d:

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

在第 XNUMX 行我們執行命令 ldh [12],它代表「載入到暫存器 A 半個字(16 位元)位於位址 12”,唯一的問題是我們正在尋址什麼樣的記憶體? 答案是,在 x 開始 (x+1)分析的網路資料包的第一個位元組。 我們從乙太網路介面讀取封包 eth0, 和這個 手段資料包如下所示(為簡單起見,我們假設資料包中沒有 VLAN 標記):

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

所以執行命令後 ldh [12] 在暫存器中 A 將會有一個字段 Ether Type — 此乙太網路訊框中傳輸的資料包類型。 在第 1 行,我們比較暫存器的內容 A (封裝類型)c 0x86dd, 和這個 和吃 我們感興趣的類型是 IPv6。 第 1 行,除了比較指令之外,還有兩列 - jt 2 и jf 3 — 如果比較成功則需要轉到的標記(A == 0x86dd)並且不成功。 因此,在成功的情況下(IPv6),我們轉到第2 行,在不成功的情況下,轉到第3 行。在第3 行,程式以代碼0 終止(不複製資料包),在第2 行,程式以代碼終止262144(給我複製一個最大256KB的包)。

一個更複雜的例子:我們按目標連接埠查看 TCP 封包

讓我們看看複製目標連接埠 666 的所有 TCP 封包的過濾器是什麼樣的。我們將考慮 IPv4 情況,因為 IPv6 情況更簡單。 研究完此範例後,您可以自行探索 IPv6 過濾器作為練習(ip6 and tcp dst port 666)和一般情況的過濾器(tcp dst port 666)。 因此,我們感興趣的過濾器如下所示:

$ 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

我們已經知道第 0 行和第 1 行的作用。 在第 2 行,我們已經檢查過這是一個 IPv4 封包(乙太網路類型 = 0x800)並將其載入到暫存器中 A 資料包的第 24 個位元組。 我們的包裹看起來像

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

這意味著我們加載到寄存器中 A IP 標頭的協定字段,這是合乎邏輯的,因為我們只想複製 TCP 封包。 我們將協議與 0x6 (IPPROTO_TCP)第 3 行。

在第 4 行和第 5 行,我們載入位於位址 20 的半字並使用指令 jset 檢查是否設定了三者之一 旗幟 - 配戴發放的口罩 jset 三個最高有效位元被清除。 三個位元中的兩個告訴我們該封包是否是分段 IP 封包的一部分,如果是,則它是否是最後一個分段。 第三位是保留的且必須為零。 我們不想檢查不完整或損壞的資料包,因此我們檢查所有三位。

第 6 行是此列表中最有趣的。 表達 ldxb 4*([14]&0xf) 意味著我們加載到寄存器中 X 封包第 4 個位元組的最低有效 XNUMX 位元乘以 XNUMX。第 XNUMX 個位元組的最低有效 XNUMX 位元是字段 網路標頭長度 IPv4 標頭,以字為單位儲存標頭長度,因此需要乘以 4。有趣的是,表達式 4*([14]&0xf) 是一種特殊尋址方案的名稱,只能以這種形式使用,並且只能用於暫存器 X, IE。 我們也不能說 ldb 4*([14]&0xf)ldxb 5*([14]&0xf) (我們只能指定不同的偏移量,例如, ldxb 4*([16]&0xf))。 很明顯,這個尋址方案被添加到 BPF 正是為了接收 X (索引暫存器)IPv4 標頭長度。

所以在第 7 行我們嘗試載入半個單字 (X+16)。 請記住,乙太網路標頭佔用了 14 個字節,並且 X 包含IPv4標頭的長度,我們理解在 A TCP 目標連接埠已載入:

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

最後,在第 8 行,我們將目標連接埠與所需值進行比較,在第 9 行或第 10 行,我們傳回結果 - 是否複製資料包。

Tcpdump:正在加載

在前面的範例中,我們沒有具體詳細說明如何將 BPF 字節碼載入到核心中以進行封包過濾。 一般來說, tcpdump 移植到許多系統並與過濾器一起使用 tcpdump 使用圖書館 libpcap。 簡而言之,使用以下方法在介面上放置濾波器 libpcap,您需要執行以下操作:

看看功能如何 pcap_setfilter 在Linux中實現,我們使用 strace (有些行已被刪除):

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

在輸出的前兩行我們創建 原始套接字 讀取所有乙太網路幀並將其綁定到介面 eth0。 從 我們的第一個例子 我們知道過濾器 ip 將由四個 BPF 指令組成,在第三行我們看到如何使用該選項 SO_ATTACH_FILTER 系統調用 setsockopt 我們載入並連接一個長度為 4 的過濾器。這是我們的過濾器。

值得注意的是,在經典BPF中,載入和連接過濾器總是作為原子操作發生,而在新版本的BPF中,載入程式和將其綁定到事件產生器在時間上是分開的。

隱藏的真相

輸出的稍微完整的版本如下所示:

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

如上所述,我們載入過濾器並將其連接到第 5 行的套接字,但是第 3 行和第 4 行會發生什麼情況? 事實證明,這 libpcap 照顧我們 - 以便我們的過濾器的輸出不包括不滿足它的資料包,庫 連接 虛擬過濾器 ret #0 (丟棄所有資料包),將套接字切換到非阻塞模式,並嘗試減去先前過濾器中可能保留的所有資料包。

總的來說,要使用經典 BPF 在 Linux 上過濾包,您需要一個結構形式如下的過濾器 struct sock_fprog 和一個開啟的套接字,之後可以使用系統呼叫將過濾器附加到套接字 setsockopt.

有趣的是,過濾器可以連接到任何插座,而不僅僅是原始插座。 這裡 例子 一個程序,它切斷所有傳入 UDP 資料報中除前兩個位元組之外的所有位元組。 (我在程式碼中添加了註釋,以免文章變得混亂。)

更多使用詳情 setsockopt 有關連接過濾器的信息,請參見 插座(7),但是關於編寫自己的過濾器,例如 struct sock_fprog 不靠別人幫助 tcpdump 我們將在本節中討論 自己動手編程BPF.

經典 BPF 與 XNUMX 世紀

BPF 於 1997 年被納入 Linux,並且在很長一段時間內一直是主力 libpcap 無需任何特殊更改(當然,Linux 特定的更改, ,但他們並沒有改變全球格局)。 BPF 發展的第一個重要跡像出現在 2011 年,當時 Eric Dumazet 提出 修補,它將 Just In Time Compiler 添加到內核 - 用於將 BPF 字節碼轉換為本機的翻譯器 x86_64 代碼。

JIT 編譯器是變革鏈中的第一個:2012 年 出現 編寫過濾器的能力 賽康,使用 BPF,2013 年 XNUMX 月有 添加xt_bpf,它允許您編寫規則 iptables 在 BPF 的幫助下,並於 2013 年 XNUMX 月 添加 也是一個模組 cls_bpf,它允許您使用 BPF 編寫流量分類器。

我們很快就會更詳細地查看所有這些範例,但首先,學習如何為 BPF 編寫和編譯任意程式對我們很有用,因為該程式庫提供的功能 libpcap 有限(簡單的例子:過濾器生成 libpcap 只能傳回兩個值 - 0 或 0x40000)或通常(如 seccomp 的情況)不適用。

自己動手編程BPF

我們來熟悉一下BPF指令的二進位格式,非常簡單:

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

每條指令佔用64位,其中前16位是指令代碼,然後有兩個八位縮進, jt и jf,參數為 32 位 K,其目的因命令而異。 例如,命令 ret,終止程式的程式碼是 6,傳回值取自常數 K。 在C中,單一BPF指令被表示為一個結構體

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

整個程序是一個結構體的形式

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

這樣,我們就已經可以編寫程式了(例如,我們知道指令碼為 [1])。 這是過濾器的樣子 ip6我們的第一個例子:

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

程式 prog 我們可以在通話中合法使用

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

以機器程式碼的形式編寫程式不是很方便,但有時是必要的(例如,用於偵錯、建立單元測試、撰寫關於 Habré 的文章等)。 為了方便起見,在文件中 <linux/filter.h> 定義了輔助巨集 - 與上面相同的範例可以重寫為

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

然而,這個選項不是很方便。 這是 Linux 核心程式設計師的推理,因此在目錄中 tools/bpf 在核心中,您可以找到用於使用經典 BPF 的彙編器和偵錯器。

彙編語言與偵錯輸出非常相似 tcpdump,但除此之外我們還可以指定符號標籤。 例如,以下是一個丟棄除 TCP/IPv4 之外的所有資料包的程式:

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

預設情況下,彙編器產生的程式碼格式為 <количество инструкций>,<code1> <jt1> <jf1> <k1>,...,對於我們的 TCP 範例來說,它將是

$ 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 程式設計師,可以使用不同的輸出格式:

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

可以將此文字複製到類型結構定義中 struct sock_filter,正如我們在本節開頭所做的那樣。

Linux 和 netsniff-ng 擴充

除了標準 BPF 之外,Linux 和 tools/bpf/bpf_asm 支持和 非標套裝。 基本上,指令用於存取結構體的字段 struct sk_buff,它描述了核心中的網路資料包。 但是,還有其他類型的幫助指令,例如 ldw cpu 將載入到暫存器中 A 運行核函數的結果 raw_smp_processor_id()。 (在新版本的 BPF 中,這些非標準擴展已擴展,為程式提供一組內核幫助程序,用於存取記憶體、結構和生成事件。)這是一個有趣的過濾器範例,其中我們僅複製使用擴充將資料包標頭放入使用者空間 poff,有效載荷偏移量:

ld poff
ret a

BPF 擴充不能用於 tcpdump,但這是熟悉實用程式包的一個很好的理由 netsniff-ng,其中包含一個高級程序 netsniff-ng,除了使用BPF進行過濾之外,還包含一個有效的流量產生器,並且比 tools/bpf/bpf_asm,一個 BPF 彙編器稱為 bpfc。 該軟體包包含非常詳細的文檔,另請參閱文章末尾的連結。

賽康

因此,我們已經知道如何編寫任意複雜度的 BPF 程序,並準備好查看新的範例,其中第一個是 seccomp 技術,它允許使用 BPF 過濾器來管理可用的系統呼叫參數集。給定的進程及其後代。

seccomp 的第一個版本於 2005 年添加到核心中,但並不是很受歡迎,因為它只提供了一個選項 - 將進程可用的系統呼叫集限制為以下內容: read, write, exit и sigreturn,並且違反規則的進程被使用殺死 SIGKILL。 然而,在 2012 年,seccomp 新增了使用 BPF 過濾器的功能,可讓您定義一組允許的系統調用,甚至對其參數執行檢查。 (有趣的是,Chrome 是該功能的首批使用者之一,Chrome 人員目前正在開發基於新版本 BPF 的 KRSI 機制,並允許自訂 Linux 安全模組。)其他文件的連結可以在末尾找到文章的。

請注意,中心上已經有關於使用 seccomp 的文章,也許有人會想在閱讀以下小節之前(或代替)閱讀它們。 文章中 容器和安全性:seccomp 提供了使用 seccomp 的範例,包括 2007 版本和使用 BPF 的版本(使用 libseccomp 產生篩選器),討論了 seccomp 與 Docker 的連接,也提供了許多有用的連結。 文章中 使用 systemd 隔離守護程式或“你不需要 Docker!” 它特別介紹如何為執行 systemd 的守護程式新增系統呼叫黑名單或白名單。

接下來我們將看到如何編寫和載入過濾器 seccomp 在裸 C 中並使用該庫 libseccomp 以及每個選項的優缺點是什麼,最後讓我們看看程式如何使用 seccomp strace.

為 seccomp 編寫和載入過濾器

我們已經知道如何寫BPF程序,所以我們先來看看seccomp程式介面。 您可以在進程層級設定過濾器,所有子進程都會繼承這些限制。 這是使用系統呼叫完成的 seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

哪裡 &filter - 這是一個指向我們已經熟悉的結構的指針 struct sock_fprog, IE。 BPF 計劃。

seccomp 程式與套接字程式有何不同? 傳輸的上下文。 在套接字的情況下,我們得到一個包含資料包的記憶體區域,在 seccomp 的情況下,我們得到一個像這樣的結構

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

這裡 nr 是要啟動的系統呼叫的編號, arch - 目前架構(更多內容請見下文), args - 最多六個系統呼叫參數,以及 instruction_pointer 是指向進行系統呼叫的使用者空間指令的指標。 因此,例如,將系統呼叫號碼載入到暫存器中 A 我們不得不說

ldw [0]

seccomp 程式還有其他功能,例如,上下文只能透過 32 位元對齊訪問,並且在嘗試載入過濾器時無法載入半個字或一個位元組 ldh [0] 系統調用 seccomp 將返回 EINVAL。 該函數檢查已載入的過濾器 seccomp_check_filter() 內核。 (有趣的是,在添加 seccomp 功能的原始提交中,他們忘記添加使用該函數的指令的權限 mod (除法餘數),現在不可用於 seccomp BPF 程序,因為它的添加 會打破 阿比。)

基本上,我們已經知道編寫和讀取 seccomp 程式的所有內容。 通常程序邏輯被安排為系統呼叫的白名單或黑名單,例如程序

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。這些系統調用是什麼? 我們不能肯定地說,因為我們不知道程式是為哪種架構編寫的。 因此,seccomp 的作者 提供 透過架構檢查啟動所有程式(目前架構在上下文中指示為字段 arch 結構 struct seccomp_data)。 檢查架構後,範例的開頭將如下所示:

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

然後我們的系統呼叫號就會得到一定的值。

我們使用 seccomp 來編寫和載入過濾器 libseccomp

在本機程式碼或 BPF 程式集中編寫過濾器可讓您完全控制結果,但同時,有時最好擁有可移植和/或可讀的程式碼。 圖書館會幫助我們解決這個問題 libseccomp,它提供了用於編寫黑色或白色濾鏡的標準介面。

例如,讓我們編寫一個程式來運行使用者選擇的二進位文件,之前已經安裝了來自以下位置的系統呼叫黑名單: 上面的文章 (該程式已被簡化以提高可讀性,完整版本可以找到 這裡):

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

首先我們定義一個數組 sys_numbers 40 多個要封鎖的系統呼叫號碼。 然後,初始化上下文 ctx 並告訴圖書館我們想要允許什麼(SCMP_ACT_ALLOW)預設所有系統呼叫(更容易建立黑名單)。 然後,我們一一加入黑名單中的所有系統呼叫。 為了回應列表中的系統調用,我們請求 SCMP_ACT_TRAP,在這種情況下 seccomp 將向進程發送訊號 SIGSYS 並描述哪個系統呼叫違反了規則。 最後,我們使用以下命令將程式載入到核心中 seccomp_load,它將編譯程式並使用系統呼叫將其附加到進程 seccomp(2).

為了成功編譯,程式必須與函式庫連結 libseccomp例如:

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

成功啟動範例:

$ ./seccomp_lib echo ok
ok

被阻止的系統呼叫的範例:

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

我們用 strace詳情:

$ 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

我們如何知道程式因使用非法系統呼叫而終止 mount(2).

因此,我們使用該庫編寫了一個過濾器 libseccomp,將重要的程式碼裝入四行。 在上面的範例中,如果存在大量系統調用,則執行時間可以顯著減少,因為檢查只是比較清單。 為了優化,libseccomp 最近有 包含補丁,這增加了對過濾器屬性的支持 SCMP_FLTATR_CTL_OPTIMIZE。 將此屬性設為 2 會將篩選器轉換為二分搜尋程式。

如果您想了解二分搜尋過濾器的工作原理,請查看 簡單的腳本,它透過撥打系統呼叫號碼在BPF彙編器中產生此類程序,例如:

$ 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

不可能以更快的速度編寫任何東西,因為 BPF 程式無法執行縮排跳轉(例如,我們不能這樣做, jmp Ajmp [label+X])因此所有轉換都是靜態的。

seccomp 和 strace

大家都知道實用性 strace 是研究 Linux 上進程行為不可或缺的工具。 不過很多人也聽過 性能問題 使用此實用程式時。 事實是 strace 實施使用 ptrace(2),並且在這種機制中,我們無法指定我們需要停止進程的系統呼叫集,即,例如,命令

$ 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

儘管在第二種情況下我們只想追蹤一個系統調用,但它們的處理時間大約相同。

新選項 --seccomp-bpf, 添加到 strace 5.3版本,可以讓你的進程加速很多倍,並且在一個系統呼叫的追蹤下的啟動時間已經可以與常規啟動的時間相媲美:

$ 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

(當然,這裡有一個輕微的欺騙,因為我們沒有跟踪這個命令的主系統調用。例如,如果我們正在跟踪, newfsstat,然後 strace 煞車會跟沒有煞車一樣猛烈 --seccomp-bpf.)

這個選項如何運作? 沒有她 strace 連接到進程並使用以下命令啟動它 PTRACE_SYSCALL。 當託管進程發出(任何)系統呼叫時,控制權將轉移到 strace,它查看系統調用的參數並運行它 PTRACE_SYSCALL。 一段時間後,進程完成系統調用,退出時,控制權再次轉移 strace,它查看返回值並使用以下命令啟動進程 PTRACE_SYSCALL, 等等。

適合小朋友的 BPF,第 XNUMX 部分:經典 BPF

然而,使用 seccomp,這個過程可以完全按照我們想要的方式進行最佳化。 也就是說,如果我們只想查看系統調用 X,那麼我們可以寫一個 BPF 過濾器 X 返回一個值 SECCOMP_RET_TRACE,以及我們不感興趣的電話 - SECCOMP_RET_ALLOW:

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

在這種情況下 strace 該過程最初啟動為 PTRACE_CONT,我們的過濾器會針對每個系統呼叫進行處理,如果系統呼叫不是 X,那麼該過程繼續運行,但如果這 X,那麼 seccomp 將轉移控制權 strace它將查看參數並啟動該過程,例如 PTRACE_SYSCALL (因為 seccomp 無法在系統呼叫退出時執行程式)。 當系統呼叫返回時, strace 將使用重新啟動該過程 PTRACE_CONT 並將等待來自 seccomp 的新訊息。

適合小朋友的 BPF,第 XNUMX 部分:經典 BPF

使用該選項時 --seccomp-bpf 有兩個限制。 首先,不可能加入已經存在的流程(選項 -p 節目 strace),因為 seccomp 不支援這一點。 其次,沒有可能 沒有 查看子進程,因為 seccomp 過濾器由所有子進程繼承,而無法停用它。

關於具體如何進行的更多細節 strace 與...合作 seccomp 可以從 最近的報告。 對我們來說,最有趣的事實是,以 seccomp 為代表的經典 BPF 至今仍在使用。

xt_bpf

現在讓我們回到網路世界。

背景:很久以前,2007年,核心是 添加xt_u32 對於網路過濾器。 它是透過類比更古老的流量分類器來編寫的 cls_u32 並允許您使用以下簡單操作為 iptables 編寫任意二進位規則:從套件中載入 32 位元並對它們執行一組算術運算。 例如,

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

載入 IP 標頭的 32 位元(從填充 6 開始),並對它們套用遮罩 0xFF (取低位元組)。 這個領域 protocol IP 標頭,我們將其與 1 (ICMP) 進行比較。 您可以將多項檢查合併到一條規則中,也可以執行運算符 @ — 向右移動 X 個位元組。 例如,規則

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

檢查 TCP 序號是否不相等 0x29。 我不會進一步詳細說明,因為很明顯,手動編寫此類規則不是很方便。 文章中 BPF-被遺忘的字節碼,有幾個鏈接,其中包含用法和規則生成的示例 xt_u32。 另請參閱本文末尾的連結。

自2013年起模組取代模組 xt_u32 您可以使用基於 BPF 的模組 xt_bpf。 讀到這裡的任何人都應該已經清楚其操作原理:按照 iptables 規則運行 BPF 字節碼。 您可以建立一個新規則,例如,如下所示:

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

這裡 <байткод> - 這是彙編輸出格式的程式碼 bpf_asm 預設情況下,例如

$ 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

在此範例中,我們將過濾所有 UDP 封包。 模組中 BPF 程式的上下文 xt_bpf當然,在 iptables 的情況下,指向封包資料到 IPv4 標頭的開頭。 BPF 程式的回傳值 布林值哪裡 false 表示資料包不匹配。

很明顯,該模組 xt_bpf 支援比上面的範例更複雜的過濾器。 讓我們來看看 Cloudfare 的真實範例。 直到最近他們還在使用該模組 xt_bpf 以防止 DDoS 攻擊。 文章中 BPF 工具簡介 他們解釋瞭如何(以及為什麼)產生 BPF 過濾器,並發布了一組用於創建此類過濾器的實用程式的連結。 例如,使用實用程式 bpfgen 您可以建立一個與名稱的 DNS 查詢相符的 BPF 程序 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

在程式中我們首先載入到暫存器中 X 行起始位址 x04habrx03comx00 在 UDP 資料封包中,然後檢查請求: 0x04686162 <-> "x04hab" 等等

稍後,Cloudfare 發布了 p0f -> BPF 編譯器程式碼。 文章中 介紹 p0f BPF 編譯器 他們談論 p0f 是什麼以及如何將 p0f 簽名轉換為 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,
...

目前不再使用 Cloudfare xt_bpf,因為他們轉向了 XDP——使用新版本 BPF 的選項之一,請參閱。 L4Drop:XDP DDoS 緩解措施.

cls_bpf

在內核中使用經典 BPF 的最後一個例子是分類器 cls_bpf 用於 Linux 中的流量控制子系統,於 2013 年底添加到 Linux 中,並在概念上取代了古老的 cls_u32.

不過,我們現在不會描述這項工作 cls_bpf,因為從經典 BPF 知識的角度來看,這不會為我們帶來任何東西 - 我們已經熟悉了所有功能。 另外,在後續討論擴展BPF的文章中,我們會不只一次遇到這個分類器。

不談論使用經典 BPF c 的另一個原因 cls_bpf 問題在於,與擴展 BPF 相比,這種情況的適用範圍從根本上縮小了:經典程式無法更改套件的內容,也無法在呼叫之間保存狀態。

所以是時候告別經典的 BPF 並展望未來了。

告別經典的 BPF

我們研究了 32 年代初開發的 BPF 技術如何成功地生活了四分之一個世紀,直到最後找到了新的應用。 然而,類似於從堆疊機到RISC的轉變,推動了經典BPF的發展,在64年代,出現了從XNUMX位元機器到XNUMX位元機器的轉變,經典BPF開始過時。 另外,經典BPF的能力非常有限,而且除了過時的架構——我們沒有能力在BPF程式的呼叫之間保存狀態,沒有直接用戶交互的可能性,沒有交互的可能性與內核一起使用,除了讀取有限數量的結構字段 sk_buff 並啟動最簡單的輔助函數,您無法更改資料包的內容並重定向它們。

事實上,目前Linux中經典的BPF僅剩下API接口,並且在核心內部所有經典程序,無論是socket過濾器還是seccomp過濾器,都會自動轉換為新的格式,即擴展BPF。 (我們將在下一篇文章中詳細討論這是如何發生的。)

向新架構的過渡始於 2013 年,當時 Alexey Starovoitov 提出了 BPF 更新方案。 2014年對應補丁 開始出現 在核心。 據我了解,最初的計劃只是優化架構和 JIT 編譯器,以便在 64 位元機器上更有效率地運行,但這些最佳化卻標誌著 Linux 開發新篇章的開始。

本系列的其他文章將涵蓋新技術的架構和應用,最初稱為內部 BPF,然後是擴展 BPF,現在簡稱為 BPF。

引用

  1. Steven McCanne 和 Van Jacobson,“BSD 資料包過濾器:用戶級資料包擷取的新架構”, https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne,“libpcap:資料包捕獲的架構和最佳化方法”, https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. IPtable U32 匹配教程.
  5. BPF - 被遺忘的字節碼: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. BPF工具簡介: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. seccomp 概述: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr:容器與安全性:seccomp
  11. habr:使用 systemd 隔離守護程式或“你不需要 Docker!”
  12. Paul Chaignon,“strace --seccomp-bpf:深入了解”, https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

來源: www.habr.com

添加評論