Cuốn sách "Giám sát BPF cho Linux"

Cuốn sách "Giám sát BPF cho Linux"Xin chào cư dân Khabro! Máy ảo BPF là một trong những thành phần quan trọng nhất của nhân Linux. Việc sử dụng hợp lý nó sẽ cho phép các kỹ sư hệ thống tìm ra lỗi và giải quyết ngay cả những vấn đề phức tạp nhất. Bạn sẽ học cách viết chương trình giám sát và sửa đổi hành vi của kernel, cách triển khai mã một cách an toàn để giám sát các sự kiện trong kernel, v.v. David Calavera và Lorenzo Fontana sẽ giúp bạn giải phóng sức mạnh của BPF. Mở rộng kiến ​​thức của bạn về tối ưu hóa hiệu suất, kết nối mạng, bảo mật. - Sử dụng BPF để theo dõi và sửa đổi hành vi của nhân Linux. - Chèn mã để giám sát các sự kiện kernel một cách an toàn mà không cần phải biên dịch lại kernel hoặc khởi động lại hệ thống. — Sử dụng các ví dụ mã thuận tiện trong C, Go hoặc Python. - Kiểm soát bằng cách sở hữu vòng đời chương trình BPF.

Bảo mật hạt nhân Linux, các tính năng của nó và Seccomp

BPF cung cấp một cách mạnh mẽ để mở rộng kernel mà không làm mất đi tính ổn định, bảo mật hoặc tốc độ. Vì lý do này, các nhà phát triển hạt nhân cho rằng sẽ là một ý tưởng hay nếu sử dụng tính linh hoạt của nó để cải thiện khả năng cách ly quy trình trong Seccomp bằng cách triển khai các bộ lọc Seccomp được hỗ trợ bởi các chương trình BPF, còn được gọi là Seccomp BPF. Trong chương này chúng tôi sẽ giải thích Seccomp là gì và nó được sử dụng như thế nào. Sau đó, bạn sẽ học cách viết bộ lọc Seccomp bằng chương trình BPF. Sau đó, chúng ta sẽ xem xét các hook BPF tích hợp sẵn có trong kernel dành cho các mô-đun bảo mật Linux.

Mô-đun bảo mật Linux (LSM) là một khung cung cấp một tập hợp các chức năng có thể được sử dụng để triển khai các mô hình bảo mật khác nhau theo cách tiêu chuẩn hóa. LSM có thể được sử dụng trực tiếp trong cây nguồn kernel, chẳng hạn như Apparmor, SELinux và Tomoyo.

Hãy bắt đầu bằng việc thảo luận về các khả năng của Linux.

Khả năng

Bản chất của các khả năng của Linux là bạn cần cấp quyền cho quy trình không có đặc quyền để thực hiện một tác vụ nhất định nhưng không sử dụng suid cho mục đích đó hoặc đặt đặc quyền cho quy trình, giảm khả năng bị tấn công và cho phép quy trình thực hiện một số tác vụ nhất định. Ví dụ: nếu ứng dụng của bạn cần mở một cổng đặc quyền, chẳng hạn như 80, thay vì chạy quy trình với quyền root, bạn chỉ cần cung cấp cho nó khả năng CAP_NET_BIND_SERVICE.

Hãy xem xét một chương trình Go có tên main.go:

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

Chương trình này phục vụ máy chủ HTTP trên cổng 80 (đây là cổng đặc quyền). Thông thường chúng tôi chạy nó ngay sau khi biên dịch:

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

Tuy nhiên, vì chúng tôi không cấp quyền root nên mã này sẽ báo lỗi khi liên kết cổng:

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

capsh (trình quản lý shell) là một công cụ chạy shell với một bộ khả năng cụ thể.

Trong trường hợp này, như đã đề cập, thay vì cấp toàn quyền root, bạn có thể kích hoạt liên kết cổng đặc quyền bằng cách cung cấp khả năng cap_net_bind_service cùng với mọi thứ khác đã có trong chương trình. Để làm điều này, chúng ta có thể gửi chương trình của mình bằng 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"

Hãy hiểu đội này một chút.

  • capsh - sử dụng capsh làm vỏ.
  • —caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' - vì chúng tôi cần thay đổi người dùng (chúng tôi không muốn chạy bằng root), chúng tôi sẽ chỉ định cap_net_bind_service và khả năng thực sự thay đổi ID người dùng từ root không ai cả, cụ thể là cap_setuid và cap_setgid.
  • —keep=1 — chúng tôi muốn giữ lại các khả năng đã cài đặt khi chuyển từ tài khoản root.
  • —user=“nobody” — người dùng cuối chạy chương trình sẽ không là ai cả.
  • —addamb=cap_net_bind_service - thiết lập việc xóa các khả năng liên quan sau khi chuyển từ chế độ gốc.
  • - -c "./capabilities" - chỉ cần chạy chương trình.

Khả năng liên kết là một loại khả năng đặc biệt được các chương trình con kế thừa khi chương trình hiện tại thực thi chúng bằng execve(). Chỉ những khả năng được phép liên kết, hay nói cách khác, là khả năng của môi trường, mới có thể được kế thừa.

Có lẽ bạn đang thắc mắc +eip nghĩa là gì sau khi chỉ định khả năng trong tùy chọn --caps. Những cờ này được sử dụng để xác định rằng khả năng:

-phải được kích hoạt (p);

-có sẵn để sử dụng (e);

-có thể được kế thừa bởi các tiến trình con (i).

Vì chúng ta muốn sử dụng cap_net_bind_service nên chúng ta cần thực hiện việc này với cờ e. Sau đó chúng ta sẽ khởi động shell trong lệnh. Điều này sẽ chạy nhị phân khả năng và chúng ta cần đánh dấu nó bằng cờ i. Cuối cùng, chúng tôi muốn bật tính năng này (chúng tôi đã thực hiện việc này mà không thay đổi UID) bằng p. Có vẻ như cap_net_bind_service+eip.

Bạn có thể kiểm tra kết quả bằng cách sử dụng ss. Hãy rút ngắn đầu ra một chút để vừa với trang, nhưng nó sẽ hiển thị cổng liên quan và ID người dùng khác 0, trong trường hợp này là 65:

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

Trong ví dụ này chúng tôi sử dụng capsh, nhưng bạn có thể viết shell bằng libcap. Để biết thêm thông tin, xem man 3 libcap.

Khi viết chương trình, nhà phát triển thường không biết trước tất cả các tính năng mà chương trình cần trong thời gian chạy; Hơn nữa, những tính năng này có thể thay đổi trong các phiên bản mới.

Để hiểu rõ hơn về khả năng của chương trình, chúng ta có thể sử dụng công cụ có khả năng BCC, công cụ này đặt kprobe cho hàm kernel cap_capable:

/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

Chúng ta có thể đạt được điều tương tự bằng cách sử dụng bpftrace với kprobe một lớp trong hàm kernel cap_capable:

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

Điều này sẽ xuất ra kết quả như sau nếu khả năng của chương trình của chúng tôi được bật sau 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

Cột thứ năm là các khả năng mà quy trình cần và vì đầu ra này bao gồm các sự kiện phi kiểm toán nên chúng tôi thấy tất cả các hoạt động kiểm tra phi kiểm toán và cuối cùng là khả năng cần thiết với cờ kiểm tra (cuối cùng trong đầu ra) được đặt thành 1. Khả năng. một cái mà chúng tôi quan tâm là CAP_NET_BIND_SERVICE, nó được định nghĩa là một hằng số trong mã nguồn kernel trong tệp include/uapi/linux/ability.h với mã định danh 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">

Các khả năng thường được kích hoạt trong thời gian chạy cho các vùng chứa như runC hoặc Docker để cho phép chúng chạy ở chế độ không có đặc quyền, nhưng chúng chỉ được phép có các khả năng cần thiết để chạy hầu hết các ứng dụng. Khi một ứng dụng yêu cầu một số khả năng nhất định, Docker có thể cung cấp chúng bằng cách sử dụng --cap-add:

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

Lệnh này sẽ cung cấp cho vùng chứa khả năng CAP_NET_ADMIN, cho phép nó định cấu hình liên kết mạng để thêm giao diện dummy0.

Phần tiếp theo cho thấy cách sử dụng các tính năng như lọc, nhưng sử dụng một kỹ thuật khác cho phép chúng tôi triển khai các bộ lọc của riêng mình theo chương trình.

bí mật

Seccomp là viết tắt của Secure Computing và là lớp bảo mật được triển khai trong nhân Linux cho phép các nhà phát triển lọc các lệnh gọi hệ thống nhất định. Mặc dù Seccomp có khả năng tương đương với Linux nhưng khả năng quản lý một số cuộc gọi hệ thống nhất định khiến nó linh hoạt hơn nhiều so với chúng.

Các tính năng của Seccomp và Linux không loại trừ lẫn nhau và thường được sử dụng cùng nhau để hưởng lợi từ cả hai phương pháp. Ví dụ: bạn có thể muốn cung cấp cho một quy trình khả năng CAP_NET_ADMIN nhưng không cho phép nó chấp nhận các kết nối ổ cắm, chặn các cuộc gọi hệ thống chấp nhận và chấp nhận4.

Phương pháp lọc Seccomp dựa trên các bộ lọc BPF hoạt động ở chế độ SECCOMP_MODE_FILTER và việc lọc cuộc gọi hệ thống được thực hiện theo cách tương tự như đối với các gói.

Bộ lọc Seccomp được tải bằng prctl thông qua thao tác PR_SET_SECCOMP. Các bộ lọc này có dạng chương trình BPF được thực thi cho mỗi gói Seccomp được biểu thị bằng cấu trúc seccomp_data. Cấu trúc này chứa kiến ​​trúc tham chiếu, một con trỏ tới các lệnh của bộ xử lý tại thời điểm gọi hệ thống và tối đa sáu đối số của lệnh gọi hệ thống, được biểu thị dưới dạng uint64.

Đây là cấu trúc seccomp_data trông như thế nào từ mã nguồn kernel trong tệp linux/seccomp.h:

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

Như bạn có thể thấy từ cấu trúc này, chúng ta có thể lọc theo lệnh gọi hệ thống, các đối số của nó hoặc kết hợp cả hai.

Sau khi nhận được từng gói Seccomp, bộ lọc phải thực hiện xử lý để đưa ra quyết định cuối cùng và cho kernel biết phải làm gì tiếp theo. Quyết định cuối cùng được thể hiện bằng một trong các giá trị trả về (mã trạng thái).

- SECCOMP_RET_KILL_PROCESS - hủy bỏ toàn bộ quá trình ngay sau khi lọc một cuộc gọi hệ thống không được thực thi vì lý do này.

- SECCOMP_RET_KILL_THREAD - chấm dứt luồng hiện tại ngay lập tức sau khi lọc một cuộc gọi hệ thống không được thực thi vì lý do này.

— SECCOMP_RET_KILL - bí danh của SECCOMP_RET_KILL_THREAD, được để lại để tương thích ngược.

- SECCOMP_RET_TRAP - cuộc gọi hệ thống bị cấm và tín hiệu SIGSYS (Cuộc gọi hệ thống xấu) được gửi đến tác vụ gọi nó.

- SECCOMP_RET_ERRNO - Lệnh gọi hệ thống không được thực thi và một phần giá trị trả về của bộ lọc SECCOMP_RET_DATA được chuyển tới không gian người dùng dưới dạng giá trị errno. Tùy thuộc vào nguyên nhân gây ra lỗi mà các giá trị errno khác nhau được trả về. Danh sách các số lỗi được cung cấp trong phần tiếp theo.

- SECCOMP_RET_TRACE - Được sử dụng để thông báo cho trình theo dõi ptrace bằng cách sử dụng - PTRACE_O_TRACESECCOMP để chặn khi lệnh gọi hệ thống được thực thi để xem và kiểm soát quá trình đó. Nếu bộ theo dõi không được kết nối, lỗi sẽ được trả về, errno được đặt thành -ENOSYS và lệnh gọi hệ thống không được thực thi.

- SECCOMP_RET_LOG - cuộc gọi hệ thống được giải quyết và ghi lại.

- SECCOMP_RET_ALLOW - cuộc gọi hệ thống được cho phép đơn giản.

ptrace là lệnh gọi hệ thống để triển khai các cơ chế theo dõi trong một quy trình được gọi là tracee, với khả năng giám sát và kiểm soát việc thực hiện quy trình. Chương trình theo dõi có thể ảnh hưởng một cách hiệu quả đến việc thực thi và sửa đổi các thanh ghi bộ nhớ của người theo dõi. Trong ngữ cảnh Seccomp, ptrace được sử dụng khi được kích hoạt bởi mã trạng thái SECCOMP_RET_TRACE, do đó trình theo dõi có thể ngăn lệnh gọi hệ thống thực thi và triển khai logic của chính nó.

lỗi seccomp

Đôi khi, khi làm việc với Seccomp, bạn sẽ gặp phải nhiều lỗi khác nhau, được xác định bằng giá trị trả về thuộc loại SECCOMP_RET_ERRNO. Để báo lỗi, lệnh gọi hệ thống seccomp sẽ trả về -1 thay vì 0.

Các lỗi sau có thể xảy ra:

- EACCESS - Người gọi không được phép thực hiện cuộc gọi hệ thống. Điều này thường xảy ra vì nó không có đặc quyền CAP_SYS_ADMIN hoặc no_new_privs không được đặt bằng prctl (chúng ta sẽ nói về vấn đề này sau);

— EFAULT - các đối số được truyền (args trong cấu trúc seccomp_data) không có địa chỉ hợp lệ;

— EINVAL — ở đây có thể có bốn lý do:

-hoạt động được yêu cầu không xác định hoặc không được kernel hỗ trợ trong cấu hình hiện tại;

-các cờ được chỉ định không hợp lệ cho hoạt động được yêu cầu;

-hoạt động bao gồm BPF_ABS, nhưng có vấn đề với phần bù được chỉ định, có thể vượt quá kích thước của cấu trúc seccomp_data;

-số lượng lệnh được truyền tới bộ lọc vượt quá mức tối đa;

— ENOMEM — không đủ bộ nhớ để thực thi chương trình;

- EOPNOTSUPP - thao tác chỉ ra rằng với SECCOMP_GET_ACTION_AVAIL, hành động này có sẵn nhưng kernel không hỗ trợ trả về trong các đối số;

— ESRCH — xảy ra sự cố khi đồng bộ hóa luồng khác;

- ENOSYS - Không có chất đánh dấu nào được gắn vào hành động SECCOMP_RET_TRACE.

prctl là một cuộc gọi hệ thống cho phép chương trình không gian người dùng thao tác (đặt và nhận) các khía cạnh cụ thể của một quy trình, chẳng hạn như độ bền byte, tên luồng, chế độ tính toán an toàn (Seccomp), đặc quyền, sự kiện Perf, v.v.

Đối với bạn, Seccomp có vẻ giống như một công nghệ hộp cát, nhưng thực tế không phải vậy. Seccomp là một tiện ích cho phép người dùng phát triển cơ chế sandbox. Bây giờ, hãy xem cách các chương trình tương tác người dùng được tạo bằng cách sử dụng bộ lọc được gọi trực tiếp bằng lệnh gọi hệ thống Seccomp.

Ví dụ về bộ lọc Seccomp BPF

Ở đây chúng tôi sẽ chỉ ra cách kết hợp hai hành động đã thảo luận trước đó, cụ thể là:

— chúng tôi sẽ viết chương trình Seccomp BPF, chương trình này sẽ được sử dụng làm bộ lọc với các mã trả về khác nhau tùy thuộc vào các quyết định được đưa ra;

— tải bộ lọc bằng prctl.

Trước tiên, bạn cần các tiêu đề từ thư viện chuẩn và nhân 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>

Trước khi thử ví dụ này, chúng ta phải đảm bảo rằng kernel được biên dịch với CONFIG_SECCOMP và CONFIG_SECCOMP_FILTER được đặt thành y. Trên máy đang hoạt động, bạn có thể kiểm tra điều này như sau:

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

Phần còn lại của mã là hàm install_filter gồm hai phần. Phần đầu tiên chứa danh sách hướng dẫn lọc BPF của chúng tôi:

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

Các hướng dẫn được đặt bằng macro BPF_STMT và BPF_JUMP được xác định trong tệp linux/filter.h.
Chúng ta hãy đi qua các hướng dẫn.

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, Arch))) - hệ thống tải và tích lũy từ BPF_LD dưới dạng word BPF_W, dữ liệu gói được đặt tại offset BPF_ABS cố định.

- BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, Arch, 0, 3) - kiểm tra bằng cách sử dụng BPF_JEQ xem giá trị kiến ​​trúc trong hằng số tích lũy BPF_K có bằng Arch hay không. Nếu vậy, nhảy ở offset 0 tới lệnh tiếp theo, nếu không thì nhảy ở offset 3 (trong trường hợp này) để đưa ra lỗi vì vòm không khớp.

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) - Tải và tích lũy từ BPF_LD dưới dạng từ BPF_W, là số gọi hệ thống chứa trong offset cố định của BPF_ABS.

— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) — so sánh số cuộc gọi hệ thống với giá trị của biến nr. Nếu chúng bằng nhau, chuyển sang lệnh tiếp theo và vô hiệu hóa lệnh gọi hệ thống, nếu không thì cho phép lệnh gọi hệ thống với SECCOMP_RET_ALLOW.

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (lỗi & SECCOMP_RET_DATA)) - kết thúc chương trình với BPF_RET và kết quả là tạo ra lỗi SECCOMP_RET_ERRNO với số từ biến err.

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) - kết thúc chương trình bằng BPF_RET và cho phép lệnh gọi hệ thống được thực thi bằng SECCOMP_RET_ALLOW.

SECCOMP LÀ CBPF
Bạn có thể thắc mắc tại sao một danh sách các hướng dẫn được sử dụng thay vì đối tượng ELF được biên dịch hoặc chương trình C được biên dịch bởi JIT.

Có hai lý do cho việc này.

• Thứ nhất, Seccomp sử dụng cBPF (BPF cổ điển) chứ không phải eBPF, có nghĩa là: nó không có thanh ghi mà chỉ có bộ tích lũy để lưu trữ kết quả tính toán cuối cùng, như có thể thấy trong ví dụ.

• Thứ hai, Seccomp chấp nhận một con trỏ tới một mảng các lệnh BPF một cách trực tiếp và không có gì khác. Các macro mà chúng tôi đã sử dụng chỉ đơn giản là giúp chỉ định các hướng dẫn này theo cách thân thiện với người lập trình.

Nếu bạn cần thêm trợ giúp để hiểu tổ hợp này, hãy xem xét mã giả thực hiện chức năng tương tự:

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

Sau khi xác định mã bộ lọc trong cấu trúc socket_filter, bạn cần xác định sock_fprog chứa mã và độ dài tính toán của bộ lọc. Cấu trúc dữ liệu này cần thiết làm đối số để khai báo tiến trình sẽ chạy sau:

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

Chỉ còn một việc phải làm trong hàm install_filter - tải chương trình! Để thực hiện việc này, chúng tôi sử dụng prctl, lấy PR_SET_SECCOMP làm tùy chọn để vào chế độ tính toán an toàn. Sau đó, chúng tôi yêu cầu chế độ tải bộ lọc bằng SECCOMP_MODE_FILTER, được chứa trong biến prog của loại sock_fprog:

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

Cuối cùng, chúng ta có thể sử dụng hàm install_filter, nhưng trước đó chúng ta cần sử dụng prctl để đặt PR_SET_NO_NEW_PRIVS cho quá trình thực thi hiện tại và do đó tránh được tình trạng các tiến trình con nhận được nhiều đặc quyền hơn tiến trình cha của chúng. Với điều này, chúng ta có thể thực hiện các lệnh gọi prctl sau trong hàm install_filter mà không cần có quyền root.

Bây giờ chúng ta có thể gọi hàm install_filter. Hãy chặn tất cả các lệnh gọi hệ thống ghi liên quan đến kiến ​​trúc X86-64 và chỉ cần cấp quyền chặn mọi nỗ lực. Sau khi cài đặt bộ lọc, chúng ta tiếp tục thực thi bằng đối số đầu tiên:

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

Bắt đầu nào. Để biên dịch chương trình của chúng tôi, chúng tôi có thể sử dụng clang hoặc gcc, dù bằng cách nào thì nó cũng chỉ biên dịch tệp main.c mà không có các tùy chọn đặc biệt:

clang main.c -o filter-write

Như đã lưu ý, chúng tôi đã chặn tất cả các mục trong chương trình. Để kiểm tra điều này, bạn cần một chương trình xuất ra thứ gì đó - có vẻ như đó là một ứng cử viên sáng giá. Đây là cách cô ấy thường cư xử:

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

Tuyệt vời! Sau đây là cách sử dụng chương trình trình bao bọc của chúng tôi: Chúng tôi chỉ cần chuyển chương trình mà chúng tôi muốn kiểm tra làm đối số đầu tiên:

./filter-write "ls -la"

Khi được thực thi, chương trình này tạo ra kết quả hoàn toàn trống. Tuy nhiên, chúng ta có thể sử dụng strace để xem chuyện gì đang xảy ra:

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

Kết quả của công việc được rút ngắn đáng kể, nhưng phần tương ứng của nó cho thấy các bản ghi bị chặn do lỗi EPERM - lỗi tương tự mà chúng tôi đã cấu hình. Điều này có nghĩa là chương trình không xuất ra bất cứ thứ gì vì nó không thể truy cập lệnh gọi hệ thống ghi:

[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)

Bây giờ bạn đã hiểu cách Seccomp BPF hoạt động và có ý tưởng hay về những gì bạn có thể làm với nó. Nhưng bạn có muốn đạt được điều tương tự với eBPF thay vì cBPF để khai thác toàn bộ sức mạnh của nó không?

Khi nghĩ về các chương trình eBPF, hầu hết mọi người đều nghĩ rằng họ chỉ đơn giản viết chúng và tải chúng với đặc quyền của quản trị viên. Mặc dù tuyên bố này nói chung là đúng, nhưng kernel thực hiện một tập hợp các cơ chế để bảo vệ các đối tượng eBPF ở nhiều cấp độ khác nhau. Các cơ chế này được gọi là bẫy BPF LSM.

Bẫy LSM BPF

Để cung cấp khả năng giám sát các sự kiện hệ thống một cách độc lập với kiến ​​trúc, LSM triển khai khái niệm bẫy. Cuộc gọi hook về mặt kỹ thuật tương tự như cuộc gọi hệ thống, nhưng độc lập với hệ thống và được tích hợp với cơ sở hạ tầng. LSM cung cấp một khái niệm mới trong đó lớp trừu tượng có thể giúp tránh các vấn đề gặp phải khi xử lý các lệnh gọi hệ thống trên các kiến ​​trúc khác nhau.

Tại thời điểm viết bài, kernel có bảy hook liên kết với các chương trình BPF và SELinux là LSM tích hợp duy nhất triển khai chúng.

Mã nguồn của các bẫy nằm trong cây nhân trong tệp 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);

Mỗi người trong số họ sẽ được gọi ở các giai đoạn thực hiện khác nhau:

— security_bpf - thực hiện kiểm tra ban đầu các cuộc gọi hệ thống BPF đã thực hiện;

- security_bpf_map - kiểm tra khi kernel trả về bộ mô tả tệp cho bản đồ;

- security_bpf_prog - kiểm tra khi kernel trả về bộ mô tả tệp cho chương trình eBPF;

— security_bpf_map_alloc — kiểm tra xem trường bảo mật bên trong bản đồ BPF có được khởi tạo hay không;

- security_bpf_map_free - kiểm tra xem trường bảo mật có bị xóa trong bản đồ BPF hay không;

— security_bpf_prog_alloc — kiểm tra xem trường bảo mật có được khởi tạo bên trong các chương trình BPF hay không;

- security_bpf_prog_free - kiểm tra xem trường bảo mật có bị xóa trong các chương trình BPF hay không.

Bây giờ, khi nhìn thấy tất cả những điều này, chúng tôi hiểu: ý tưởng đằng sau các thiết bị chặn LSM BPF là chúng có thể cung cấp sự bảo vệ cho mọi đối tượng eBPF, đảm bảo rằng chỉ những người có đặc quyền phù hợp mới có thể thực hiện các thao tác trên thẻ và chương trình.

Tóm tắt thông tin

Bảo mật không phải là thứ bạn có thể triển khai theo cách phù hợp cho tất cả mọi thứ bạn muốn bảo vệ. Điều quan trọng là có thể bảo vệ các hệ thống ở các cấp độ khác nhau và theo những cách khác nhau. Dù bạn có tin hay không, cách tốt nhất để bảo mật hệ thống là tổ chức các cấp độ bảo vệ khác nhau từ các vị trí khác nhau, do đó việc giảm mức độ bảo mật của một cấp độ không cho phép truy cập vào toàn bộ hệ thống. Các nhà phát triển cốt lõi đã làm rất tốt khi cung cấp cho chúng tôi một tập hợp các lớp và điểm tiếp xúc khác nhau. Chúng tôi hy vọng đã giúp bạn hiểu rõ về lớp là gì và cách sử dụng các chương trình BPF để làm việc với chúng.

Về tác giả

David Calavera là CTO tại Netlify. Anh làm việc trong bộ phận hỗ trợ Docker và đóng góp vào việc phát triển các công cụ Runc, Go và BCC cũng như các dự án nguồn mở khác. Được biết đến với công việc về các dự án Docker và phát triển hệ sinh thái plugin Docker. David rất đam mê biểu đồ ngọn lửa và luôn tìm cách tối ưu hóa hiệu suất.

Lorenzo Fontana làm việc trong nhóm nguồn mở tại Sysdig, nơi anh chủ yếu tập trung vào Falco, một dự án Cloud Native Computing Foundation cung cấp bảo mật thời gian chạy container và phát hiện bất thường thông qua mô-đun hạt nhân và eBPF. Anh ấy đam mê các hệ thống phân tán, mạng được xác định bằng phần mềm, nhân Linux và phân tích hiệu suất.

» Thông tin chi tiết về cuốn sách có thể tìm thấy tại trang web của nhà xuất bản
» Mục lục
» Đoạn trích

Đối với Khabrozhiteley giảm giá 25% khi sử dụng phiếu giảm giá - Linux

Sau khi thanh toán phiên bản giấy của cuốn sách, một cuốn sách điện tử sẽ được gửi qua e-mail.

Nguồn: www.habr.com

Thêm một lời nhận xét