Linux 内核 BPF LSM 编程指南:用 eBPF 在运行时实现 MAC 强制访问控制与审计策略
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
本文以 Linux 内核文档Documentation/bpf/prog_lsm.rst为主线,系统讲解 LSM BPF 程序的完整开发与加载流程:如何用 CO-RE 方式声明内核结构体、如何借助BPF_PROG宏编写挂在file_mprotect等 LSM hook 上的 eBPF 程序、如何通过bpf(2)系统调用加载与附着程序,并结合内核源码kernel/bpf/bpf_lsm.c与仓库自带自测用例tools/testing/selftests/bpf/progs/lsm.c验证底层实现细节。读完本文,你可以独立完成一个 LSM BPF 程序的设计、编译、加载与挂接,并理解校验器(verifier)对这类程序施加的额外约束。
LSM BPF 程序解决什么问题
LSM(Linux Security Modules)框架为内核的关键操作点定义了一组安全钩子(hook),各安全模块(如 SELinux、AppArmor)通过注册 hook 回调来实现访问控制。LSM BPF 程序(LSM BPF Programs)允许特权用户在运行时直接对这些 LSM hook 进行 instrumentation(插桩),用 eBPF 实现系统级的 MAC(Mandatory Access Control,强制访问控制)策略与审计(Audit)策略,而无需重新编译内核或加载传统 LSM 模块。
这意味着两条实际能力:
- 强制访问控制:BPF 程序在 hook 中返回非 0 值(如
-EPERM)即可拒绝对应操作; - 审计:hook 返回 0 放行,同时将关键信息写入 perf 事件缓冲区,供用户态采集分析。
内核侧支持:bpf_lsm hook 与编译依赖
从源码结构看,BPF 程序并非直接挂在传统 LSM hook 上,而是挂在名为bpf_lsm_<NAME>的一组专用桩函数上。bpf_lsm.c 中通过宏展开为每个允许的 LSM hook 生成一个弱符号桩函数:
#define LSM_HOOK(RET, DEFAULT, NAME, ...) \ __weak noinline RET bpf_lsm_##NAME(__VA_ARGS__) \ { \ return DEFAULT; \ } #include <linux/lsm_hook_defs.h>这段代码遍历include/linux/lsm_hook_defs.h中定义的全部 LSM hook,为每个 hook 生成形如bpf_lsm_file_mprotect的可附着函数,默认返回该 hook 的缺省值(即放行)。文档中提示"其他可插桩的 LSM hook 可以在security/security.c中找到",对应的完整 hook 清单即由 lsm_hook_defs.h 统一定义。
关键编译开关
启用该特性需要内核开启CONFIG_BPF_LSM。kernel/bpf/Kconfig 定义了该选项及其依赖:
config BPF_LSM bool "Enable BPF LSM Instrumentation" depends on BPF_EVENTS depends on BPF_SYSCALL depends on SECURITY depends on BPF_JIT help Enables instrumentation of the security hooks with BPF programs for implementing dynamic MAC and Audit Policies.因此适用前提很明确:内核必须同时启用 BPF 系统调用、BPF JIT、安全框架(SECURITY)与 BPF 事件,且目标 hook 的bpf_lsm_*桩函数要出现在内核 BTF 中。此外,bpf_lsm.c 还维护了一个bpf_lsm_disabled_hooks集合,列出了禁止BPF 附着的安全敏感 hook(如vm_enough_memory、file_alloc_security等),校验器遇到这些 hook 会直接拒绝加载。
校验器的额外约束
bpf_lsm_verify_prog(bpf_lsm.c)在验证阶段强制检查:
- 程序必须是GPL 兼容许可证,否则报错
LSM programs must have a GPL compatible license; attach_btf_id必须指向bpf_lsm_hooks集合中注册的 hook,否则报points to wrong type name。
这就是为什么示例代码末尾要写char _license[] SEC("license") = "GPL";——对普通 tracing 程序这通常只是惯例,对 LSM 程序则是硬性要求。
编写 eBPF 程序
目标 hook:file_mprotect
文档以file_mprotecthook 为例,其签名为:
int file_mprotect(struct vm_area_struct *vma, unsigned long reqprot, unsigned long prot);该 hook 在进程调用mprotect()改变内存区域保护属性时被触发,是演示 MAC/审计策略的经典入口。
用 CO-RE 方式声明结构体,无需内核头文件
基于 BTF(BPF Type Format)的 eBPF 程序不需要包含完整的内核头文件即可访问上下文结构体:只需在 eBPF 程序中声明用到的字段,并给结构体加上__attribute__((preserve_access_index)):
struct mm_struct { unsigned long start_brk, brk, start_stack; } __attribute__((preserve_access_index)); struct vm_area_struct { unsigned long start_brk, brk, start_stack; unsigned long vm_start, vm_end; struct mm_struct *vm_mm; } __attribute__((preserve_access_index));注意事项:
- 字段顺序无关(原文档明确 note:字段顺序不影响匹配,BTF 按类型名与字段名定位);
preserve_access_index是 clang 特性,允许 BPF 校验器在运行时依据 BTF 信息更新字段偏移量(CO-RE,Compile Once Run Everywhere)。由于校验器了解这些类型的完整信息,它同时会验证程序中对这些类型的所有字段访问是否合法。
更简洁的方式:生成 vmlinux.h
如果构建环境可以拿到 BTF 信息,推荐直接生成vmlinux.h,eBPF 程序中#include "vmlinux.h"即可,无需手工声明类型:
# bpftool btf dump file <path-to-btf-vmlinux> format c > vmlinux.h其中<path-to-btf-vmlinux>在构建环境与部署环境内核一致时可取/sys/kernel/btf/vmlinux。
用 BPF_PROG 宏声明程序
eBPF 程序通过 bpf_tracing.h 定义的BPF_PROG宏声明。该宏的设计动机在头文件注释中写得很清楚:这一类程序(tp_btf/fentry/fexit 及 LSM)的原始上下文是一个未加类型的 u64 数组,BPF_PROG允许用户像普通 C 函数一样写出"有类型、有名字"的形参列表,宏会在底层自动完成ctx[i]的取值与类型转换(见 BPF_PROG 宏定义)。
文档给出的完整示例:
SEC("lsm/file_mprotect") int BPF_PROG(mprotect_audit, struct vm_area_struct *vma, unsigned long reqprot, unsigned long prot, int ret) { /* ret is the return value from the previous BPF program * or 0 if it's the first hook. */ if (ret != 0) return ret; int is_heap; is_heap = (vma->vm_start >= vma->vm_mm->start_brk && vma->vm_end <= vma->vm_mm->brk); /* Return an -EPERM or write information to the perf events buffer * for auditing */ if (is_heap) return -EPERM; }逐点解读:
SEC("lsm/file_mprotect"):section 名称中"lsm/file_mprotect"指明程序必须附着到的 LSM hook。lsm/前缀对应非睡眠(non-sleepable)附着,lsm.s/前缀对应可睡眠(sleepable)附着;mprotect_audit是 eBPF 程序名;- 形参中的
int ret是 LSM BPF 特有的链式返回值:它是上一个 BPF 程序在该 hook 上的返回值;如果是第一个执行到的程序则为 0。因此规范做法是ret != 0时直接透传该错误码——保证前序策略的拒绝决定不被覆盖; - 示例策略:判断目标 VMA 是否完全落在堆区间
[start_brk, brk]内,若是则返回-EPERM拒绝mprotect;也可改为向 perf 事件缓冲区写审计信息后返回 0。
加载程序
eBPF 程序可通过bpf(2)系统调用的BPF_PROG_LOAD操作加载,libbpf 的bpf_objectAPI 封装如下:
struct bpf_object *obj; obj = bpf_object__open("./my_prog.o"); bpf_object__load(obj);更省事的做法是用bpftool生成骨架(skeleton)头文件:
# bpftool gen skeleton my_prog.o > my_prog.skel.h之后在用户态程序#include "my_prog.skel.h",调用生成的my_prog__open_and_load()即可完成打开与加载,map 指针、程序句柄等都以结构体字段形式暴露,免去手工 fd 管理。
附着到 LSM Hook
附着操作对应bpf(2)系统调用的BPF_RAW_TRACEPOINT_OPEN(新内核上由 libbpf 自动走bpf_link_create路径)。从源码看,libbpf 的实现链路是:
bpf_program__attach_lsm()(libbpf.c)直接复用通用的bpf_program__attach_btf_id(),后者以bpf_program__expected_attach_type(prog)(即BPF_LSM)为附着类型调用bpf_link_create(prog_fd, 0, ...),由内核根据 section 名解析目标 hook 的 btf_id;- 解除附着通过销毁返回的
bpf_link完成:调用bpf_link__destroy(link); - 若使用骨架,则
my_prog__attach()负责附着、my_prog__destroy()负责清理,与手工 API 等价。
struct bpf_link *link = bpf_program__attach_lsm(prog); if (is_err_ptr(link)) /* 处理失败 */; /* ...运行期... */ bpf_link__destroy(link);仓库自带示例:tools/testing/selftests/bpf/progs/lsm.c
文档指出的官方示例位于 progs/lsm.c,配套用户态测试代码在 test_lsm.c。这份示例比文档中的片段覆盖更多实战要点:
char _license[] SEC("license") = "GPL"; int monitored_pid = 0; int mprotect_count = 0; SEC("lsm/file_mprotect") int BPF_PROG(test_int_hook, struct vm_area_struct *vma, unsigned long reqprot, unsigned long prot, int ret) { struct mm_struct *mm = vma->vm_mm; if (ret != 0 || !mm) return ret; __s32 pid = bpf_get_current_pid_tgid() >> 32; int is_stack = 0; is_stack = (vma->vm_start <= mm->start_stack && vma->vm_end >= mm->start_stack); if (is_stack && monitored_pid == pid) { mprotect_count++; ret = -EPERM; } return ret; }要点:
- 用
bpf_get_current_pid_tgid() >> 32取当前进程 PID,与 map 中维护的monitored_pid比较,实现针对特定进程的堆栈 mprotect 拦截(monitored_pid由用户态写 map 设置); - 开头
if (ret != 0 || !mm) return ret;同时体现了链式返回值透传与空指针防御。
该文件还展示了lsm.s/睡眠附着与前缀规则:
SEC("lsm.s/bprm_committed_creds") int BPF_PROG(test_void_hook, struct linux_binprm *bprm) { ... return 0; } SEC("lsm/task_free") /* lsm/ is ok, lsm.s/ fails */ int BPF_PROG(test_task_free, struct task_struct *task) { return 0; }bprm_committed_creds是 void 返回型 hook(只审计不可拒绝),程序在其中对 8 种 map 类型(BPF_MAP_TYPE_ARRAY、HASH、LRU_HASH、PERCPU_ARRAY、PERCPU_HASH、LRU_PERCPU_HASH、ARRAY_OF_MAPS、HASH_OF_MAPS)逐一读写,验证 LSM BPF 程序对各类 map 操作的放行;而task_free的注释明确指出该 hook 是睡眠 hook,只能以lsm/前缀附着、lsm.s/会失败——这是选前缀时可直接借鉴的源码证据。
小结与适用边界
- 能力边界:LSM BPF 只能附着到内核 BTF 中注册的
bpf_lsm_*hook,bpf_lsm_disabled_hooks列出的敏感 hook 被校验器硬性禁止; - 硬性要求:GPL 兼容许可证、内核开启
CONFIG_BPF_LSM(依赖BPF_EVENTS/BPF_SYSCALL/SECURITY/BPF_JIT)、运行时具备 CAP_BPF/CAP_SYS_ADMIN 等特权; - 开发流程:声明结构体(或
vmlinux.h)→ 用BPF_PROG宏写SEC("lsm/<hook>")程序 →bpftool生成骨架 →bpf_program__attach_lsm附着 →bpf_link__destroy清理; - 完整可参考的自测用例见 progs/lsm.c 与 test_lsm.c,内核附着与验证逻辑见 bpf_lsm.c,BPF_PROG 宏实现见 bpf_tracing.h,libbpf 附着实现见 libbpf.c。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考