首页 / 科技 / 利用eBPF+LSM实现内核级零侵扰HIDS(主机入侵检测系统)

利用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=yCONFIG_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. 测试与验证(高能时刻)

  1. 启动程序
    sudo go run main.go

  2. 另开一个终端,尝试修改 /etc/passwd
    $ echo "hacker" >> /etc/passwd
    bash: /etc/passwd: Operation not permitted
    结果:操作被直接拒绝,即使你是 root 用户!

  3. 查看内核日志
    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 生产环境部署的坑

  1. 启动参数:大多数发行版默认没有启用 BPF LSM。你需要修改 Grub:
    # /etc/default/grubGRUB_CMDLINE_LINUX="lsm=lockdown,yama,apparmor,bpf"update-grub && reboot

  2. 路径解析:在内核态解析完整路径非常困难且容易死机。推荐策略是基于 inode 号进行保护,而不是字符串路径。

  3. 误伤:不要一上来就拦截所有写操作。建议第一阶段只做 bpf_trace_printk记录日志,观察一周后再开启拦截模式。

5.3 进阶:如何防御无文件攻击?

利用 tracepoint/syscalls/sys_enter_execve配合 LSM 的 bprm_check_security,你可以在程序执行前检查其内存签名或 Hash 值,如果不在白名单内,直接拒绝执行。

6. 总结

本文展示的 LSM eBPF 技术,是目前 Linux 内核安全领域的皇冠明珠。它不仅实现了真正的“内核态防火墙”,还彻底改变了安全软件的架构形态——从“外挂式”Agent 变成了“内嵌式”基础设施。