書籍《BPF 用於 Linux 監控》

書籍《BPF 用於 Linux 監控》哈布羅居民大家好! BPF 虛擬機器是 Linux 核心最重要的元件之一。 正確使用它將使系統工程師能夠發現故障並解決最複雜的問題。 您將學習如何編寫監視和修改內核行為的程序,如何安全地實現代碼來監視內核中的事件,等等。 David Calavera 和 Lorenzo Fontana 將幫助您釋放 BPF 的力量。 擴展您在效能最佳化、網路、安全性方面的知識。 - 使用BPF來監視和修改Linux核心的行為。 - 注入程式碼以安全地監視核心事件,而無需重新編譯核心或重新啟動系統。 — 使用 C、Go 或 Python 編寫的方便的程式碼範例。 - 透過擁有 BPF 程式生命週期來進行控制。

Linux 核心安全、其特性和 Seccomp

BPF 提供了一種強大的方法來擴展內核,而無需犧牲穩定性、安全性或速度。 因此,核心開發人員認為,透過實作 BPF 程式(也稱為 Seccomp BPF)支援的 Seccomp 過濾器,利用其多功能性來改善 Seccomp 中的進程隔離是一個好主意。 在本章中,我們將解釋什麼是 Seccomp 以及如何使用它。 然後您將學習如何使用 BPF 程式編寫 Seccomp 過濾器。 之後,我們將了解 Linux 安全模組核心中包含的內建 BPF 掛鉤。

Linux 安全模組 (LSM) 是一個提供一組功能的框架,可用於以標準化方式實作各種安全模型。 LSM可以直接在核心原始碼樹中使用,例如Apparmor、SELinux和Tomoyo。

我們先討論 Linux 的功能。

功能

Linux能力的本質是,你需要授予非特權程序執行某項任務的權限,但不能為此目的使用suid,或者以其他方式使該進程具有特權,從而減少攻擊的可能性並允許該進程執行某些任務。 例如,如果您的應用程式需要開啟特權連接埠(例如 80),您可以簡單地為其提供 CAP_NET_BIND_SERVICE 功能,而不是以 root 身分執行該進程。

考慮一個名為 main.go 的 Go 程式:

package main
import (
            "net/http"
            "log"
)
func main() {
     log.Fatalf("%v", http.ListenAndServe(":80", nil))
}

該程式在連接埠 80(這是一個特權連接埠)上為 HTTP 伺服器提供服務。 通常我們編譯後立即執行:

$ go build -o capabilities main.go
$ ./capabilities

但是,由於我們沒有授予 root 權限,因此這段程式碼在綁定連接埠時會拋出錯誤:

2019/04/25 23:17:06 listen tcp :80: bind: permission denied
exit status 1

capsh(shell 管理員)是一種運行具有一組特定功能的 shell 的工具。

在這種情況下,如同已經提到的,您可以透過提供 cap_net_bind_service 功能以及程式中已有的所有其他內容來啟用特權連接埠綁定,而不是授予完全 root 權限。 為此,我們可以將程式封裝在 capsh 中:

# capsh --caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' 
   --keep=1 --user="nobody" 
   --addamb=cap_net_bind_service -- -c "./capabilities"

讓我們稍微了解一下這個團隊。

  • capsh - 使用 capsh 作為 shell。
  • —caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' - 由於我們需要更改使用者(我們不想以 root 身份運行),因此我們將指定 cap_net_bind_service 以及實際更改用戶 ID 的能力cap_setgid。
  • —keep=1 — 我們希望在從 root 帳號切換時保留已安裝的功能。
  • —user=“nobody” — 執行該程式的最終使用者將是nobody。
  • —addamba=cap_net_bind_service —設定從root模式切換後相關能力的清除。
  • - -c "./capability" - 只需執行該程式。

連結功能是一種特殊的功能,噹噹前程式使用 execve() 執行子程式時,它們會被子程式繼承。 只有允許關聯的能力,即環境能力,才可以被繼承。

您可能想知道在 --caps 選項中指定功能後 +eip 意味著什麼。 這些標誌用於確定該功能:

- 必須啟動(p);

-可供使用(e);

- 可以被子進程繼承(i)。

由於我們想要使用 cap_net_bind_service,因此我們需要使用 e 標誌來完成此操作。 然後我們將在命令中啟動shell。 這將運行功能二進位文件,我們需要用 i 標誌來標記它。 最後,我們希望使用 p 啟用該功能(我們這樣做時沒有更改 UID)。 看起來像 cap_net_bind_service+eip。

您可以使用 ss 檢查結果。 讓我們稍微縮短輸出以適合頁面,但它將顯示除 0 之外的關聯連接埠和使用者 ID,在本例中為 65:

# ss -tulpn -e -H | cut -d' ' -f17-
128 *:80 *:*
users:(("capabilities",pid=30040,fd=3)) uid:65534 ino:11311579 sk:2c v6only:0

在本範例中,我們使用了 capsh,但您可以使用 libcap 編寫 shell。 有關詳細信息,請參閱 man 3 libcap。

在編寫程式時,開發人員通常不會事先知道程式在執行時所需的所有功能; 此外,這些功能在新版本中可能會發生變化。

為了更好地理解我們程式的功能,我們可以使用 BCC 功能工具,它為 cap_capable 核心函數設定 kprobe:

/usr/share/bcc/tools/capable
TIME      UID  PID   TID   COMM               CAP    NAME           AUDIT
10:12:53 0 424     424     systemd-udevd 12 CAP_NET_ADMIN         1
10:12:57 0 1103   1101   timesync        25 CAP_SYS_TIME         1
10:12:57 0 19545 19545 capabilities       10 CAP_NET_BIND_SERVICE 1

我們可以透過在 cap_capable 核心函數中使用 bpftrace 和單行 kprobe 來實現相同的效果:

bpftrace -e 
   'kprobe:cap_capable {
      time("%H:%M:%S ");
      printf("%-6d %-6d %-16s %-4d %dn", uid, pid, comm, arg2, arg3);
    }' 
    | grep -i capabilities

如果我們的程式的功能在 kprobe 之後啟用,這將輸出類似以下內容:

12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 10 1

第五列是流程所需的功能,由於此輸出包括非審核事件,因此我們會看到所有非審核檢查,最後是審核標誌(輸出中最後一個)設定為 1 的所需功能。功能。我們感興趣的是CAP_NET_BIND_SERVICE,它在核心原始碼中被定義為一個常數,位於檔案include/uapi/linux/ability.h 中,識別碼為10:

/* Allows binding to TCP/UDP sockets below 1024 */
/* Allows binding to ATM VCIs below 32 */
#define CAP_NET_BIND_SERVICE 10<source lang="go">

通常會在運行時為 runC 或 Docker 等容器啟用功能,以允許它們在非特權模式下運行,但它們只允許運行大多數應用程式所需的功能。 當應用程式需要某些功能時,Docker 可以使用 --cap-add 提供它們:

docker run -it --rm --cap-add=NET_ADMIN ubuntu ip link add dummy0 type dummy

此命令將為容器提供 CAP_NET_ADMIN 功能,允許其配置網路連結以新增 dummy0 介面。

下一節將展示如何使用過濾等功能,但使用不同的技術,讓我們以程式設計方式實現我們自己的過濾器。

塞康普

Seccomp 代表安全運算,是在 Linux 核心中實作的安全層,允許開發人員過濾某些系統呼叫。 儘管 Seccomp 在功能上與 Linux 相當,但它管理某些系統呼叫的能力使其更加靈活。

Seccomp 和 Linux 功能並不互相排斥,通常會一起使用以從這兩種方法中受益。 例如,您可能想要為進程提供 CAP_NET_ADMIN 功能,但不允許它接受套接字連接,從而阻止accept 和accept4 系統呼叫。

Seccomp過濾方法是基於在SECCOMP_MODE_FILTER模式下運作的BPF過濾器,系統呼叫過濾的執行方式與封包相同。

Seccomp 過濾器是透過 PR_SET_SECCOMP 操作使用 prctl 載入的。 這些過濾器採用 BPF 程式的形式,針對由 seccomp_data 結構所表示的每個 Seccomp 封包執行。 此結構包含參考體系結構、系統呼叫時指向處理器指令的指標以及最多六個系統呼叫參數(表示為 uint64)。

這是 linux/seccomp.h 檔案中核心原始碼中的 seccomp_data 結構的樣子:

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

正如您從該結構中看到的,我們可以按系統呼叫、其參數或兩者的組合進行過濾。

接收到每個 Seccomp 封包後,過濾器必須進行處理以做出最終決定並告訴核心下一步要做什麼。 最終的決定由返回值(狀態代碼)之一來表示。

- SECCOMP_RET_KILL_PROCESS - 在過濾因此未執行的系統呼叫後立即終止整個進程。

- SECCOMP_RET_KILL_THREAD - 在過濾因此未執行的系統呼叫後立即終止目前執行緒。

— SECCOMP_RET_KILL — SECCOMP_RET_KILL_THREAD 的別名,保留用於向後相容。

- SECCOMP_RET_TRAP - 系統呼叫被禁止,並向呼叫它的任務發送SIGSYS(錯誤系統呼叫)訊號。

- SECCOMP_RET_ERRNO - 系統呼叫不執行,SECCOMP_RET_DATA 篩選器傳回值的一部分會作為 errno 值傳遞到使用者空間。 根據錯誤原因,傳回不同的errno值。 下一節提供了錯誤號碼清單。

- SECCOMP_RET_TRACE - 用於通知 ptrace 追蹤器使用 - PTRACE_O_TRACESECCOMP 攔截執行系統呼叫以查看和控制該進程。 如果未連接追蹤器,則傳回錯誤,errno 設定為 -ENOSYS,且不執行系統呼叫。

- SECCOMP_RET_LOG - 系統呼叫已解析並記錄。

- SECCOMP_RET_ALLOW - 簡單地允許系統呼叫。

ptrace是一個系統調用,用於在稱為tracee的進程中實現追蹤機制,具有監視和控制進程執行的能力。 追蹤程序可以有效地影響執行並修改被追蹤者的記憶體暫存器。 在Seccomp上下文中,由SECCOMP_RET_TRACE狀態碼觸發時使用ptrace,因此追蹤器可以阻止系統呼叫執行並實作自己的邏輯。

安全計算錯誤

在使用 Seccomp 時,您有時會遇到各種錯誤,這些錯誤由 SECCOMP_RET_ERRNO 類型的回傳值標識。 要報告錯誤,seccomp 系統呼叫將傳回 -1 而不是 0。

可能出現以下錯誤:

- EACCESS - 不允許呼叫者進行系統呼叫。 這通常是因為它沒有 CAP_SYS_ADMIN 權限或沒有使用 prctl 設定 no_new_privs (我們稍後會討論這個);

— EFAULT — 傳遞的參數(seccomp_data 結構中的 args)沒有有效位址;

— EINVAL — 這裡可能有四個原因:

- 請求的操作未知或目前配置中的核心不支援;

-指定的標誌對於請求的操作無效;

-操作包含BPF_ABS,但指定的偏移有問題,可能超出seccomp_data結構體的大小;

-傳遞給過濾器的指令數量超過最大值;

— ENOMEM — 沒有足夠的記憶體來執行程式;

- EOPNOTSUPP - 此操作顯示使用 SECCOMP_GET_ACTION_AVAIL 此操作可用,但核心不支援在參數中傳回;

— ESRCH — 同步另一個串流時出現問題;

- ENOSYS - SECCOMP_RET_TRACE 操作沒有附加追蹤器。

prctl 是一個系統調用,允許用戶空間程序操作(設定和獲取)進程的特定方面,例如字節字節順序、線程名稱、安全計算模式 (Seccomp)、特權、Perf 事件等。

對您來說,Seccomp 可能看起來像是一種沙箱技術,但事實並非如此。 Seccomp 是一個允許使用者開發沙箱機制的實用程式。 現在讓我們看看如何使用 Seccomp 系統呼叫直接呼叫的篩選器來建立使用者互動程式。

BPF Seccomp 過濾器範例

在這裡,我們將展示如何結合前面討論的兩個操作,即:

— 我們將編寫一個 Seccomp BPF 程序,該程序將用作過濾器,根據所做的決策具有不同的返回代碼;

— 使用 prctl 載入過濾器。

首先,您需要來自標準函式庫和 Linux 核心的標頭:

#include <errno.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <unistd.h>

在嘗試此範例之前,我們必須確保核心是在 CONFIG_SECCOMP 和 CONFIG_SECCOMP_FILTER 設定為 y 的情況下編譯的。 在工作機器上,您可以這樣檢查:

cat /proc/config.gz| zcat | grep -i CONFIG_SECCOMP

其餘程式碼是由兩部分組成的 install_filter 函數。 第一部分包含我們的 BPF 過濾指令清單:

static int install_filter(int nr, int arch, int error) {
  struct sock_filter filter[] = {
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3),
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
  };

這些指令是使用 linux/filter.h 檔案中定義的 BPF_STMT 和 BPF_JUMP 巨集來設定的。
讓我們看一下說明。

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, arch))) - 系統以字 BPF_W 的形式從 BPF_LD 載入並累加,封包資料位於固定偏移量 BPF_ABS 處。

- BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) - 使用 BPF_JEQ 檢查 BPF_K 累加器常數中的架構值是否等於 arch。 如果是,則在偏移量 0 處跳到下一條指令,否則在偏移量 3 處跳轉(在本例中)以拋出錯誤,因為 arch 不匹配。

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) - 以字 BPF_W 的形式從 BPF_LD 載入並累加,BPF_W 是包含在 BPF_ABS 固定偏移量中的系統呼叫號碼。

— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) — 將系統呼叫號碼與 nr 變數的值進行比較。 如果它們相等,則繼續執行下一條指令並停用系統調用,否則允許使用 SECCOMP_RET_ALLOW 進行系統調用。

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)) - 使用 BPF_RET 終止程序,並因此產生錯誤 SECCOMP_RET_ERRNO,其編號來自 err 變數。

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) - 使用 BPF_RET 終止程式並允許使用 SECCOMP_RET_ALLOW 執行系統呼叫。

SECCOMP 是 CBPF
您可能想知道為什麼使用指令列表而不是編譯的 ELF 物件或 JIT 編譯的 C 程式。

有兩個原因。

• 首先,Seccomp 使用cBPF(經典BPF)而不是eBPF,這意味著:它沒有暫存器,只有一個累加器來儲存最後的計算結果,如範例所示。

• 其次,Seccomp 直接接受指向 BPF 指令數組的指針,而不接受其他任何東西。 我們使用的巨集只是幫助以程式設計師友善的方式指定這些指令。

如果您需要更多幫助來理解此程序集,請考慮執行相同操作的偽代碼:

if (arch != AUDIT_ARCH_X86_64) {
    return SECCOMP_RET_ALLOW;
}
if (nr == __NR_write) {
    return SECCOMP_RET_ERRNO;
}
return SECCOMP_RET_ALLOW;

在socket_filter結構體中定義過濾器程式碼後,需要定義一個sock_fprog,其中包含程式碼和過濾器的計算長度。 需要此資料結構作為聲明稍後運行的進程的參數:

struct sock_fprog prog = {
   .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
   .filter = filter,
};

install_filter 函數中只剩下一件事要做 - 載入程式本身! 為此,我們使用 prctl,將 PR_SET_SECCOMP 作為進入安全計算模式的選項。 然後我們告訴模式使用 SECCOMP_MODE_FILTER 載入過濾器,它包含在 sock_fprog 類型的 prog 變數中:

  if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
    perror("prctl(PR_SET_SECCOMP)");
    return 1;
  }
  return 0;
}

最後,我們可以使用install_filter函數,但在此之前我們需要使用prctl為目前執行設定PR_SET_NO_NEW_PRIVS,從而避免子進程獲得比父親進程更多的權限的情況。 這樣,我們就可以在 install_filter 函數中進行以下 prctl 調用,而無需 root 權限。

現在我們可以呼叫 install_filter 函數。 讓我們阻止所有與 X86-64 架構相關的寫入系統調用,並簡單地授予阻止所有嘗試的權限。 安裝過濾器後,我們使用第一個參數繼續執行:

int main(int argc, char const *argv[]) {
  if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
   perror("prctl(NO_NEW_PRIVS)");
   return 1;
  }
   install_filter(__NR_write, AUDIT_ARCH_X86_64, EPERM);
  return system(argv[1]);
 }

讓我們開始吧。 為了編譯我們的程序,我們可以使用 clang 或 gcc,無論哪種方式,它都只是編譯 main.c 文件,沒有特殊選項:

clang main.c -o filter-write

如前所述,我們已封鎖該程式中的所有條目。 要測試這一點,您需要一個可以輸出某些內容的程式 - ls 似乎是一個不錯的候選人。 她平時的表現是這樣的:

ls -la
total 36
drwxr-xr-x 2 fntlnz users 4096 Apr 28 21:09 .
drwxr-xr-x 4 fntlnz users 4096 Apr 26 13:01 ..
-rwxr-xr-x 1 fntlnz users 16800 Apr 28 21:09 filter-write
-rw-r--r-- 1 fntlnz users 19 Apr 28 21:09 .gitignore
-rw-r--r-- 1 fntlnz users 1282 Apr 28 21:08 main.c

精彩的! 使用我們的包裝程式的情況如下:我們只需將要測試的程式作為第一個參數傳遞即可:

./filter-write "ls -la"

執行時,該程式產生完全空的輸出。 不過,我們可以使用 strace 來查看發生了什麼:

strace -f ./filter-write "ls -la"

工作結果大大縮短,但其對應部分顯示記錄因 EPERM 錯誤而被阻止 - 與我們配置的錯誤相同。 這意味著程式不會輸出任何內容,因為它無法存取 write 系統呼叫:

[pid 25099] write(2, "ls: ", 4) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "write error", 11) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "n", 1) = -1 EPERM (Operation not permitted)

現在您了解了 Seccomp BPF 的工作原理,並清楚可以用它做什麼。 但是您不想使用 eBPF 而不是 cBPF 來實現相同的目標,以充分利用其功能嗎?

在考慮 eBPF 程式時,大多數人認為他們只是編寫它們並使用管理員權限載入它們。 雖然這種說法通常是正確的,但核心實作了一組機制來保護各個層級的 eBPF 物件。 這些機制稱為 BPF LSM 陷阱。

BPF LSM 陷阱

為了提供獨立於體系結構的系統事件監視,LSM 實作了陷阱的概念。 鉤子調用在技術上類似於系統調用,但它獨立於系統並與基礎設施整合。 LSM 提供了一個新的概念,其中抽象層可以幫助避免在處理不同架構上的系統呼叫時遇到的問題。

在撰寫本文時,核心有 XNUMX 個與 BPF 程式相關的鉤子,SELinux 是唯一實現它們的內建 LSM。

陷阱的原始碼位於核心樹的 include/linux/security.h 檔案中:

extern int security_bpf(int cmd, union bpf_attr *attr, unsigned int size);
extern int security_bpf_map(struct bpf_map *map, fmode_t fmode);
extern int security_bpf_prog(struct bpf_prog *prog);
extern int security_bpf_map_alloc(struct bpf_map *map);
extern void security_bpf_map_free(struct bpf_map *map);
extern int security_bpf_prog_alloc(struct bpf_prog_aux *aux);
extern void security_bpf_prog_free(struct bpf_prog_aux *aux);

它們中的每一個都將在不同的執行階段中被呼叫:

— security_bpf — 對執行的 BPF 系統呼叫進行初始檢查;

- security_bpf_map - 檢查核心何時傳回映射的檔案描述符;

- security_bpf_prog - 檢查核心何時傳回 eBPF 程式的檔案描述子;

— security_bpf_map_alloc — 檢查 BPF 映射內的安全欄位是否已初始化;

- security_bpf_map_free - 檢查BPF映射內的安全欄位是否已清除;

— security_bpf_prog_alloc — 檢查安全欄位是否在 BPF 程式內部初始化;

- security_bpf_prog_free - 檢查 BPF 程式內的安全性欄位是否已清除。

現在,看到這一切,我們明白了:LSM BPF 攔截器背後的想法是它們可以為每個 eBPF 物件提供保護,確保只有具有適當權限的人才能對卡片和程式執行操作。

總結

安全性並不是您可以以一刀切的方式針對您想要保護的所有內容實施的。 能夠以不同方式保護不同等級的系統非常重要。 不管你相信與否,保護系統安全的最佳方法是從不同位置組織不同層級的保護,這樣降低某一層級的安全性就不允許存取整個系統。 核心開發人員為我們提供了一組不同的層和接觸點,做得非常好。 我們希望我們已經讓您很好地理解了什麼是層以及如何使用 BPF 程式來處理它們。

關於作者

大衛卡拉維拉 是 Netlify 的技術長。 他從事 Docker 支援工作,並為 Runc、Go 和 BCC 工具以及其他開源專案的開發做出了貢獻。 因在 Docker 專案和 Docker 插件生態系統開發方面的工作而聞名。 David 對火焰圖非常熱衷,並且始終致力於優化效能。

洛倫佐·豐塔納 他在 Sysdig 的開源團隊工作,主要關注 Falco,這是一個雲端原生運算基金會項目,透過核心模組和 eBPF 提供容器運行時安全性和異常檢測。 他熱衷於分散式系統、軟體定義網路、Linux 核心和效能分析。

» 有關本書的更多詳細信息,請訪問 出版商的網站
» 目錄
» 摘抄

對於 Khabrozhiteley 使用優惠券可享 25% 折扣 - Linux

支付紙本書籍的費用後,將透過電子郵件發送電子書。

來源: www.habr.com

添加評論