LSM BPF: Cưỡng Chế An Ninh Ngay Trong Nhân

K
Kai··6 min read

Mười bốn bài qua, mọi chương trình eBPF của ta đều quan sát — đếm, trace, đo. Part V đổi vai: eBPF cưỡng chế. Mở màn là LSM BPF, có lẽ là cách cưỡng chế "danh chính ngôn thuận" nhất, vì nó gắn vào đúng khung an ninh mà cả nhân Linux dựa vào.

LSM là gì, và vì sao eBPF gắn vào đó

LSM (Linux Security Modules) là khung các điểm móc an ninh (security hook) rải khắp nhân: trước khi mở file, trước khi mount, trước khi tạo socket, trước khi nạp một chương trình bpf khác... nhân gọi qua LSM để hỏi "thao tác này có được phép không?". Khung này cũng là nơi SELinuxAppArmor cắm vào. Mỗi hook là một hàm security_* trong nhân; module trả về 0 (cho phép) hoặc số âm errno như -EPERM (từ chối).

Từ kernel 5.7, eBPF trở thành một LSM: ta viết chương trình eBPF gắn thẳng vào các hook đó. Khác mọi loại chương trình trước trong series, giá trị trả về của chương trình LSM quyết định số phận thao tác — trả 0 thì cho, trả -EPERM thì nhân chặn. Đây là điểm bước ngoặt: từ quan sát sang điều khiển.

   Tiến trình gọi open("/etc/passwd")
            │
            ▼
   nhân: security_file_open(file)   ◄── điểm móc LSM
            │  hỏi từng LSM đang hoạt động
            ├── apparmor:  cho
            ├── yama:      cho
            └── bpf:       chương trình eBPF của ta ──► return 0  (cho)
                                                      └► return -EPERM (CHẶN)
            │
            ▼ nếu mọi LSM đều cho
   tiếp tục mở file

Viết một chương trình LSM chặn mở file

Chặn mở bất kỳ file nào tên đúng lsm-secret. Hook là file_open:

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_tracing.h>

SEC("lsm/file_open")
int BPF_PROG(deny_open, struct file *file, int ret)
{
    if (ret != 0)                    // một LSM trước đã quyết -> tôn trọng
        return ret;

    char name[16] = {};
    const unsigned char *p = BPF_CORE_READ(file, f_path.dentry, d_name.name);
    bpf_probe_read_kernel_str(name, sizeof(name), p);   // đọc tên file (CO-RE)

    const char want[] = "lsm-secret";
    for (int i = 0; i < sizeof(want) - 1; i++)
        if (name[i] != want[i])
            return 0;        // không phải mục tiêu -> cho qua
    return -1;                       // -EPERM: CHẶN
}

Vài điểm cốt lõi. SEC("lsm/file_open") gắn vào hook LSM file_open. Macro BPF_PROG (của libbpf) khai triển chữ ký hook — tham số filestruct file *, ret là verdict các LSM trước đã trả. Quy ước tôn trọng quyết định trước: nếu ret != 0 (ai đó đã chặn) thì trả luôn ret, không "mở lại" cái đã bị cấm. Tên file đọc qua BPF_CORE_READ(file, f_path.dentry, d_name.name) — lại là CO-RE (Bài 5), lần theo file → path → dentry → tên. Trả -1 (-EPERM) để chặn.

Lần đầu: nạp được, gắn được, nhưng không chặn

Biên dịch và nạp như mọi chương trình eBPF, rồi thử:

clang -O2 -g -target bpf -I. -c lsmtest.bpf.c -o lsmtest.bpf.o
sudo bpftool prog loadall lsmtest.bpf.o /sys/fs/bpf/lsmtest autoattach
echo secret > /tmp/lsm-secret
cat /tmp/lsm-secret
444: lsm  name deny_open  tag 51ed662ae7c57739  gpl    <- nạp + gắn OK
secret                                                 <- nhưng VẪN đọc được!

Chương trình nạp thành công, hiện ra là loại lsm, gắn vào hook — nhưng cat vẫn đọc được file. Không chặn gì cả. Đây không phải bug trong code. Xem danh sách LSM đang hoạt động:

cat /sys/kernel/security/lsm
lockdown,capability,landlock,yama,apparmor,ima,evm

Không có bpf. Theo tài liệu nhân, LSM BPF cần kernel ≥5.7 với CONFIG_BPF_LSM=y bpf phải nằm trong danh sách LSM hoạt động — qua CONFIG_LSM="...,bpf" lúc biên dịch nhân, hoặc tham số boot lsm=...,bpf. Kiểm config thì CONFIG_BPF_LSM=y có sẵn, nhưng bpf chưa được kích hoạt lúc boot. Nên nhân cho gắn chương trình LSM, nhưng không gọi nó ở các hook — hook LSM của bpf chưa được đăng ký. Gắn được mà không cưỡng chế.

Bật bpf trong danh sách LSM

Thêm bpf vào tham số boot. Sửa /etc/default/grub, giữ nguyên các LSM đang có và thêm bpf:

# /etc/default/grub
GRUB_CMDLINE_LINUX="lsm=landlock,lockdown,yama,integrity,apparmor,bpf"
sudo update-grub && sudo reboot

Sau khi reboot, danh sách LSM đã có bpf:

cat /sys/kernel/security/lsm
lockdown,capability,landlock,yama,apparmor,bpf,ima,evm

Đây là một thao tác cấp hệ thống thật (sửa boot param + reboot một node của cụm). Reboot giữ nguyên IP máy (khác stop/start), tooling đã cài vẫn còn; chỉ /tmp bị xóa nên phải biên dịch lại. Sau reboot cụm tự hồi (Cilium về đúng 140 chương trình eBPF như trước).

Lần hai: chặn thật

Cùng chương trình đó, biên dịch và gắn lại — giờ nó cưỡng chế:

-- BEFORE attach --
secret                                       <- mở bình thường
444: lsm  name deny_open  tag 51ed662ae7c57739  gpl
-- AFTER attach: cat /tmp/lsm-secret --
cat: /tmp/lsm-secret: Operation not permitted    <- bị CHẶN (EPERM)
-- file khác --
ok                                           <- file tên khác: vẫn mở được
-- mở bằng python (đường khác) --
PermissionError: [Errno 1] Operation not permitted: '/tmp/lsm-secret'

cat nhận Operation not permitted — đúng -EPERM chương trình trả. File tên khác (lsm-other) vẫn mở bình thường, xác nhận lọc đúng mục tiêu. Quan trọng nhất: python3 mở cùng file cũng bị chặn với đúng errno — vì chặn nằm ở hook LSM trong nhân, không phải ở cat hay bất kỳ công cụ nào. Mọi đường dẫn tới open(), bất kể ngôn ngữ hay chương trình, đều qua security_file_open và đều bị chặn. Một điểm cưỡng chế ở hook nhân, mọi lối vào open() đều chịu.

Xem link gắn nó vào đâu:

sudo bpftool link show
prog_type lsm  attach_type lsm_mac  target_btf_id 123130

attach_type lsm_mac — MAC là Mandatory Access Control, đúng phạm trù an ninh mà LSM phục vụ; target_btf_id trỏ tới hook file_open trong BTF của nhân.

🧹 Dọn dẹp

Gỡ là bắt buộc — một chương trình LSM còn gắn vẫn tiếp tục cưỡng chế:

sudo rm -rf /sys/fs/bpf/lsmtest      # bỏ pin -> link giải phóng -> gỡ

Bỏ pin xong, bpftool prog show không còn chương trình lsm, và cat /tmp/lsm-secret đọc được lại ngay — gỡ tức thì. (Nếu muốn trả node về trạng thái ban đầu hoàn toàn, gỡ luôn bpf khỏi lsm= trong grub rồi reboot; nhưng để bpf trong danh sách mà không gắn chương trình nào thì vô hại.) Mã nguồn (lsmtest.bpf.c, lệnh build/attach) ở github.com/nghiadaulau/ebpf-from-scratch, thư mục 14-lsm-bpf.

Tổng kết

LSM BPF đưa eBPF từ quan sát sang cưỡng chế: gắn vào khung Linux Security Modules — chính các hook security_* mà SELinux/AppArmor dùng — và giá trị trả về quyết định thao tác: 0 cho qua, -EPERM chặn. Ta viết deny_open (SEC("lsm/file_open"), macro BPF_PROG, đọc tên file bằng BPF_CORE_READ, trả -1 để chặn). Bài học thật: lần đầu chương trình nạp và gắn được nhưng không chặn gì — vì cưỡng chế chỉ xảy ra khi bpf nằm trong danh sách LSM hoạt động (/sys/kernel/security/lsm), cần CONFIG_BPF_LSM=y bpf trong CONFIG_LSM/tham số boot lsm=. Sau khi thêm bpf vào grub và reboot, cùng chương trình đó chặn thật: cat python3 đều nhận Operation not permitted, còn file tên khác vẫn mở — vì điểm chặn ở hook nhân (attach_type lsm_mac), mọi lối vào open() đều chịu. Tôn trọng verdict LSM trước (if (ret != 0) return ret) là quy ước bắt buộc.

Bài 15 nhìn sang một cơ chế lọc khác, có trước eBPF và đang chạy trong mọi container bạn từng chạy: seccomp-bpf — lọc syscall bằng BPF cổ điển (cBPF), nền của sandbox Docker/Kubernetes, và nó khác eBPF ở những điểm rất đáng nắm.

Related Posts