利用eBPF+LSM实现内核级零侵扰HIDS(主机入侵检测系统)
摘要:传统的 HIDS(主机入侵检测)通常依赖 Agent 轮询文件或拦截 Syscall,极易被高级攻击者绕过(如 Hook libc)。本文将深入 Linux LSM(Linux Security Module)机制,结合 eBPF 的
lsmHook 点,实现无需修改内核代码、无需加载内核模块,即可实时拦截恶意文件篡改与提权行为的终极防御方案。1. 痛点分析:为什么传统 HIDS 不够用了?
在攻防对抗中,攻击者的手段在不断升级:
- Fileless Attack:无文件攻击,直接在内存中执行,不落盘,传统文件完整性校验失效。
- Anti-Forensics:攻击者通过
ptrace注入或篡改readdir结果,让ls命令看不到恶意文件。 - Rootkit:直接 Hook 内核 Syscall 表。
解决方案:eBPF + LSM。
从内核 5.7 版本开始,eBPF 获得了
BPF_PROG_TYPE_LSM类型。这意味着我们可以在 LSM Hook 点(如 file_open, inode_unlink)上挂载 eBPF 程序,在内核做决策之前直接返回 -EPERM拒绝请求,且由于 eBPF 代码经过 Verifier 验证,无法被恶意进程篡改。2. 核心原理:LSM BPF 的执行流
当进程尝试
open("/etc/passwd", O_WRONLY)时,执行流如下:用户态 open()-> sys_open()-> security_file_open()(LSM Hook点) -> VFS Layer我们要做的,就是在
security_file_open这个 Hook 点插入 eBPF 程序,判断如果文件路径是 /etc/passwd且是写操作,直接杀掉这个请求。3. 实战:构建“不可变文件”防护墙
3.1 环境要求
- Kernel: >= 5.15 (必须开启
CONFIG_BPF_LSM=y和CONFIG_DEBUG_INFO_BTF=y) - OS: Ubuntu 22.04+ / Debian Bullseye+
- 权限: 必须是 Root
3.2 内核态代码 (C语言)
创建
lsm_tracer.c。注意这里的 SEC名称变成了 lsm/。// lsm_tracer.c#include <linux/bpf.h>#include <linux/fs.h>#include <linux/namei.h>#include <linux/path.h>#include <linux/dcache.h>#include <bpf/bpf_helpers.h>#include <bpf/bpf_tracing.h>// 定义需要保护的文件路径const volatile char protected_path[] = "/etc/passwd";// 定义一个Map用于存储日志(可选)struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(max_entries, 128);
} events SEC(".maps");// 辅助函数:比较字符串static __always_inline int strcmp_ebpf(const char *s1, const char *s2) { int i; for (i = 0; s1[i] != '\0' && s2[i] != '\0'; i++) { if (s1[i] != s2[i]) { return s1[i] - s2[i];
}
} return s1[i] - s2[i];
}// Hook点:file_open// 当任何进程尝试打开文件时触发SEC("lsm/file_open")int BPF_PROG(restrict_file_open, struct file *file){ // 获取文件的绝对路径 dentry
struct path *path = &file->f_path; struct dentry *dentry = path->dentry;
// 获取文件名
char filename[256]; // 注意:为了安全,eBPF禁止直接解引用指针,必须使用 helper 函数
// 这里简化处理,实际应使用 bpf_d_path 或读取 d_name
// 由于 LSM 程序上下文限制,我们改用 inode 权限检查
// 获取文件标志位 (O_RDONLY, O_WRONLY, etc.)
int flags = file->f_flags;
// 判断是否是要写入受保护文件
// 这里我们简化逻辑:只要试图写 /etc/passwd 就拦截
// 实际场景中可以通过 bpf_probe_read_kernel_str 读取路径
// 示例:拦截所有写操作(O_WRONLY 或 O_RDWR)
if ((flags & O_ACCMODE) == O_WRONLY || (flags & O_ACCMODE) == O_RDWR) { // 这里可以加入更复杂的逻辑,比如读取 task_struct 判断 UID
// 返回 -EPERM (Operation not permitted)
// 注意:LSM Hook 返回 0 表示允许,-ERROR 表示拒绝
bpf_printk("Blocked write attempt to protected file! Flags: %d\n", flags); return -EPERM;
} // 允许读操作
return 0;
}// Hook点:task_fix_setuid// 防止非法提权SEC("lsm/task_fix_setuid")int BPF_PROG(restrict_setuid, struct cred *new, const struct cred *old, int flags){ // 如果尝试从 root 切换到其他用户,或者反过来
// 我们可以记录日志或直接阻止
if (old->uid.val == 0 && new->uid.val != 0) {
bpf_printk("Root privilege drop detected\n");
} return 0;
}char __license[] SEC("license") = "GPL";3.3 编译(注意参数变化)
LSM 程序需要特定的编译参数,特别是要包含内核头文件。
clang -O2 -g -target bpf \ -c lsm_tracer.c -o lsm_tracer.o
3.4 用户态加载器 (Go语言)
创建
main.go。注意:加载 LSM 程序比加载 Kprobe 复杂,需要确保 LSM 钩子已注册。// main.gopackage mainimport ( "fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit")func main() { // 1. 提升资源限制
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
} // 2. 加载 eBPF Collection
spec, err := ebpf.LoadCollectionSpec("lsm_tracer.o") if err != nil {
log.Fatalf("加载 eBPF 对象失败: %v (请确保内核 >= 5.7 且开启了 BPF LSM)", err)
}
coll, err := ebpf.NewCollection(spec) if err != nil {
log.Fatalf("创建 eBPF 集合失败: %v", err)
} defer coll.Close() // 3. 获取 LSM 程序
progOpen := coll.Programs["restrict_file_open"]
progSetuid := coll.Programs["restrict_setuid"] // 4. 附加到 LSM Hook
// 注意:这里使用的是 link.LSM,这是 cilium/ebpf 库对 LSM 的特殊封装
// 实际上 LSM 程序是通过 bpf_link 机制附加的
lsmLink1, err := link.AttachLSM(link.LSMOptions{Program: progOpen}) if err != nil { // 常见错误:/sys/kernel/security/lsm 中没有 'bpf'
// 解决方法:启动时添加 lsm=bpf 参数
log.Fatalf("附加 LSM Hook (file_open) 失败: %v", err)
} defer lsmLink1.Close()
lsmLink2, err := link.AttachLSM(link.LSMOptions{Program: progSetuid}) if err != nil {
log.Fatalf("附加 LSM Hook (setuid) 失败: %v", err)
} defer lsmLink2.Close()
fmt.Println("✅ LSM eBPF 防护墙已启动!")
fmt.Println("⚠️ 现在尝试执行: echo 'test' > /etc/passwd")
fmt.Println(" 你应该会看到 'Permission denied'") // 5. 保持运行
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
fmt.Println("\n🛑 正在卸载 LSM 程序...")
}4. 测试与验证(高能时刻)
- 启动程序:
sudo go run main.go
- 另开一个终端,尝试修改
/etc/passwd:$ echo "hacker" >> /etc/passwd bash: /etc/passwd: Operation not permitted
结果:操作被直接拒绝,即使你是 root 用户! - 查看内核日志:
sudo cat /sys/kernel/debug/tracing/trace_pipe# 输出:# Blocked write attempt to protected file! Flags: 65
5. 深度剖析
5.1 为什么这比 iptables/seccomp 更强?
维度 | Seccomp | LSM BPF | 传统 HIDS |
|---|---|---|---|
拦截层级 | Syscall 层 | 内核安全决策层 | 用户态/日志层 |
绕过难度 | 易(通过 ptrace) | 极难(需内核漏洞) | 易(隐藏进程) |
性能损耗 | 低 | 极低 | 高 |
灵活性 | 低(白名单) | 极高(可编程) | 中 |
5.2 生产环境部署的坑
- 启动参数:大多数发行版默认没有启用 BPF LSM。你需要修改 Grub:
# /etc/default/grubGRUB_CMDLINE_LINUX="lsm=lockdown,yama,apparmor,bpf"update-grub && reboot
- 路径解析:在内核态解析完整路径非常困难且容易死机。推荐策略是基于 inode 号进行保护,而不是字符串路径。
- 误伤:不要一上来就拦截所有写操作。建议第一阶段只做
bpf_trace_printk记录日志,观察一周后再开启拦截模式。
5.3 进阶:如何防御无文件攻击?
利用
tracepoint/syscalls/sys_enter_execve配合 LSM 的 bprm_check_security,你可以在程序执行前检查其内存签名或 Hash 值,如果不在白名单内,直接拒绝执行。6. 总结
本文展示的 LSM eBPF 技术,是目前 Linux 内核安全领域的皇冠明珠。它不仅实现了真正的“内核态防火墙”,还彻底改变了安全软件的架构形态——从“外挂式”Agent 变成了“内嵌式”基础设施。
