如果你在 Linux 上做安全产品,最容易被问到的问题是:防护模块是不是必须写成内核驱动,进 ring-0?按照传统思路,答案接近“是”。因为你要拦截执行、检查文件、杀掉恶意进程,没有内核特权好像就做不了真正意义上的“杀毒”。但 ring-0 驱动这条路对普通开发者太不友好:驱动签名、内核版本兼容、loadable module 的限制、调试崩溃,每一项都能消耗你大量时间。
这篇文章给出的路线要轻得多:用 IMA 做文件完整性测量,用 eBPF LSM 在内核执行路径bprm_check_security上挂一个“黑名单检查”。最后做出来的东西,就是标题里说的 "crappy ring-0 toy antivirus"。
它当然不是一个能拿去打攻防演练的杀毒软件,甚至不能扫描病毒。它真正演示的是一套“静态完整性 + 动态执行阻断”的内核安全组装逻辑。读完你会理解三件事:ring-0 工具在系统调用路径上如何生效,IMA 和 LSM 各自解决什么问题,以及怎么把一个 eBPF LSM 程序编译、加载、测试到 Linux 内核里。如果你正在做 HIDS、零信任执行管控、软件供应链防篡改,这篇文章值得看完。
1. eBPF + LSM 做“杀毒”的价值在哪
传统杀毒软件在内核态的常规做法,是写一个驱动程序,hook 系统调用,或者绑定文件系统过滤回调。Windows 上有 minifilter,Linux 上有 fanotify、LSM、内核模块。问题是这些方案要么权限要求高,要么开发调试成本大,要么部署到不同发行版时会被内核模块校验挡住。
eBPF 提供的是另一种思路:你写一段受限的 C 代码,交给内核验证器检查,确认不会死循环、不会越界访问、不会非法修改内核内存后,再把它挂载到指定的 hook 点。用 eBPF 做安全检查,最大的优势不是性能,而是“失败不会拖垮内核”。这不代表没有风险,但相比直接加载模块,eBPF 的失控半径被缩小了很多。
那为什么还要用到 LSM?因为现代 Linux 已经内置了一套安全钩子框架。每次 execve、open、mmap 等关键操作发生时,都会走一遍 LSM 钩子链。过去你只能在这些位置选择 AppArmor、SELinux 这种现成策略,而现在 BPF LSM 允许你自己写一小段程序安插进去。IMA 同样属于这个安全链条的一员,不过它关心的不是“这个操作合不合法”,而是“这个文件是否被改动过、当前内容是什么”。
把三者放在一起,架构就很清晰:
- IMA 负责“度量”:可执行文件被 execve 时,计算哈希并记录到内核运行日志;
- eBPF LSM 负责“决策”:在 execve 系统调用到达真正执行点之前,按哈希或路径特征判断是否放行;
- 用户态脚本负责“策略下发”:计算被拉黑文件的哈希,写入 eBPF map。
这个组合正好覆盖了“检测”和“阻断”两个环节。传统杀毒软件的特征码扫描,本质也是这个逻辑,只是它把特征库维护在厂商服务器上,而我们可以把它收敛到内核里的一个小 map 中。
2. 核心概念:ring-0、LSM、IMA、eBPF LSM
2.1 ring-0 与杀毒软件的“特权执念”
ring-0 是 CPU 的最高特权级别。在内核态,代码可以访问所有内存、I/O 设备,也可以控制系统调用行为。杀毒软件之所以想留在 ring-0,是因为恶意代码往往会藏到系统调用背后,用户态扫描看不到。但 ring-0 也是一把双刃剑:一个 bug 就能让整个系统宕机,驱动签名和版本兼容又提高了发布门槛。
eBPF 程序虽然运行在内核态,但它是由内核验证器约束过的“受限代码”。这不是严格意义上的 ring-0 驱动开发,却又真正跑在 ring-0 的执行路径上。这个特性让很多安全工具开始从“写内核模块”转向“写 eBPF 程序”。
2.2 LSM:Linux 安全模块的标准接口
LSM 不是一个安全产品,而是一套内核安全钩子。它在关键操作发生时调用已经注册的安全回调,回调返回允许或拒绝。举例来说:
file_open:文件被打开时;bprm_check_security:程序被 execve 准备执行时;file_permission:文件被读写时。
SELinux、AppArmor 都是通过 LSM 框架生效的。从内核 5.8 开始,eBPF 自身也被注册为一个 LSM 模块,开发者可以在lsm/bprm_check_security这种 hook 位置挂载自己的 eBPF 程序。
2.3 IMA:完整性度量架构
IMA(Integrity Measurement Architecture)是一套文件完整性测量机制。它能在文件被打开、执行、mmap 时计算哈希,并把这些度量结果记录到内核的运行时度量列表中。IMA 支持三种策略模式:
| 模式 | 作用 | 是否阻断 |
|---|---|---|
| measure | 记录文件哈希 | 否 |
| appraise | 校验文件签名或哈希 | 是 |
| audit | 记录安全审计日志 | 否 |
在本文演示中,我们主要用 measure 模式来验证“文件被篡改后哈希会变化”。实际生产系统可以配合 IMA 签名,在 appraise 模式下拒绝执行未签名文件。
2.4 eBPF LSM 的架构位置
BPF LSM 本质上是把 LSM 钩子开放给了 eBPF 程序。它的调用链路是:
execve() -> do_execve() -> bprm_check_security() // LSM hook -> IMA 的测量逻辑(如果策略包含 BPRM_CHECK) -> eBPF LSM 程序(决策是否放行) -> 放行 / 拒绝这里必须区分清楚:IMA 和 eBPF LSM 处在同一条 LSM 链上,但职责不一样。IMA 做测量、记录、校验;eBPF LSM 做自定义的强制访问控制。我们让 IMA 负责“发现异常痕迹”,让 eBPF LSM 负责“执行黑名单阻断”,这正是标题所说“toy antivirus”的核心架构。
3. 整体架构设计
这个项目的目标很明确:构建一个能够在内核执行路径上,根据文件哈希或路径,拒绝特定可执行文件运行的最小防护系统。
模块划分如下:
- 策略管理员(用户态):决定哪些文件需要被拉黑,计算文件路径的哈希,并通过 bpftool 写入 eBPF map。
- IMA 测量层:对
BPRM_CHECK事件记录文件哈希,作为审计和举证数据。 - eBPF LSM 阻断层:每次
bprm_check_security触发时,读取正在执行的程序路径,计算哈希,查询黑名单 map,如果命中则返回-EPERM。 - 内核安全执行链:把以上两步串联起来。
实际效果等价于:用户态无法绕过的“文件访问黑名单”。因为检查点位于内核 LSM 路径,即使攻击者拿到 shell,只要 execve 被拦,他也很难执行指定文件。当然,如果攻击者已经有 root 权限并能加载 eBPF 程序,他可以直接卸载或覆盖我们的 eBPF map,这是后话。toy 级设计无法对抗同权限级对手。
4. 环境准备与内核特性检查
4.1 推荐环境
强烈建议在虚拟机中做实验,不要在生产环境直接操作。原因有两个:实验过程中需要修改内核启动参数,一旦配置错误可能导致系统无法启动;IMA policy 写错也可能影响系统命令执行。
推荐环境:
- Ubuntu 22.04 或更新的 Linux 发行版;
- 内核版本 5.15 以上,BPF LSM 已经足够稳定;
- 安装 clang、llvm、libbpf-dev、bpftool;
- 内核开启 CONFIG_BPF_LSM、CONFIG_IMA 相关选项。
多数发行版默认内核已经带有相关配置,但需要启动参数显式启用 BPF LSM。
4.2 检查 LSM 启用状态
执行以下命令:
cat /sys/kernel/security/lsm输出中如果包含bpf,说明 BPF LSM 已经启用。如果没有,你需要修改 GRUB 启动参数,追加:
lsm=lockdown,yama,integrity,apparmor,bpf修改前先备份/etc/default/grub,然后运行:
sudo update-grub sudo reboot如果你使用的是云主机,尤其要注意:修改 GRUB 参数前确认有 VNC 或带外控制台,避免参数错误导致无法登录系统。
4.3 检查 IMA 是否可用
ls -l /sys/kernel/security/ima cat /sys/kernel/security/ima/ascii_runtime_measurements | tail -n 5如果/sys/kernel/security/ima不存在,需要先挂载 securityfs:
sudo mkdir -p /sys/kernel/security sudo mount -t securityfs securityfs /sys/kernel/security如果安全文件系统已经挂载,但 IMA 目录仍然不存在,说明当前内核没有启用 IMA,需要重新编译内核,或者换一个默认开启 IMA 的发行版内核。不建议为了一个 toy 项目重新编译内核,除非你本身就有内核开发环境。
4.4 检查 eBPF 所需权限
加载 eBPF LSM 程序需要 root 权限,或者当前用户具有CAP_BPF、CAP_SYS_ADMIN能力。普通用户直接执行加载命令会失败:
Operation not permitted认证资料显示,Linux 的新权限体系中 BPF 能力被拆分成了CAP_BPF和CAP_PERFMON,但为了减少环境变量干扰,本文所有验证都在 root 用户下完成。
5. IMA 完整性度量配置与验证
5.1 写入 measure 策略
IMA 的运行时策略在 securityfs 的/sys/kernel/security/ima/policy文件中写入。我们只记录可执行文件的度量结果:
echo "measure func=BPRM_CHECK" | sudo tee /sys/kernel/security/ima/policy除了BPRM_CHECK,IMA 还支持FILE_CHECK(文件打开)、MMAP_CHECK(内存映射执行)等事件类型。为了演示,只启用最小策略。
5.2 触发一次度量
执行一个普通命令,让 IMA 产生度量记录:
/usr/bin/md5sum /etc/hostname > /dev/null sudo tail -n 5 /sys/kernel/security/ima/ascii_runtime_measurements输出中会出现类似下面的字段:
10 <template-hash> ima-ng sha256:<文件哈希> /usr/bin/md5sum这里我使用了<template-hash>和<文件哈希>作为占位符,因为不同发行版、不同文件内容得到的哈希值完全不一样。你只需要确认:ascii_runtime_measurements末尾增加了新的记录,并且里面包含/usr/bin/md5sum路径。
5.3 验证文件修改会导致哈希变化
如果你尝试手动修改一个文件,并在修改前后分别执行,会看到 IMA 记录中的哈希值不同。这说明 IMA 已经完成了“静态完整性度量”的职责。
注意:当前 measure 模式只记录,不阻断。即使文件被篡改,系统也能执行它。真正阻断工作交给下一节的 eBPF LSM 程序。
6. eBPF LSM 执行阻断实现
6.1 程序逻辑设计
eBPF LSM 程序要做的事情比较朴素:
- 在
bprm_check_security钩子处读取当前被执行的程序路径; - 对路径计算一个简单的 djb2 哈希;
- 查询名为
blocked_files的 eBPF map; - 如果路径哈希存在于 map 中,返回
-EPERM; - 否则放行。
这里用 djb2 哈希纯粹是为了演示,真实项目请改用更强健的哈希或直接使用路径长度固定前缀匹配。djb2 存在碰撞可能,而且这里计算的是路径哈希而不是文件内容哈希。这一点在最后的安全边界中会详细解释。
6.2 完整 eBPF C 程序
创建文件ima_guard.bpf.c:
// 文件路径:ima_guard.bpf.c #include <linux/bpf.h> #include <linux/types.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> #define MAX_PATH 256 #define MAX_ENTRIES 1024 char LICENSE[] SEC("license") = "GPL"; struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, MAX_ENTRIES); __type(key, __u32); __type(value, __u32); } blocked_files SEC(".maps"); static __always_inline __u32 djb2_path(const char *path) { __u32 hash = 5381; int i; for (i = 0; i < MAX_PATH; i++) { int c = path[i]; if (c == 0) { break; } hash = ((hash << 5) + hash) + c; } return hash; } SEC("lsm/bprm_check_security") int BPF_PROG(guard_bprm_check, struct linux_binprm *bprm) { char filename[MAX_PATH] = {}; __u32 key; long ret; ret = bpf_probe_read_kernel_str(filename, sizeof(filename), BPF_CORE_READ(bprm, filename)); if (ret <= 0) { return 0; } key = djb2_path(filename); if (bpf_map_lookup_elem(&blocked_files, &key)) { return -EPERM; } return 0; }这段代码的关键点有三个。
第一,BPF_CORE_READ(bprm, filename)从struct linux_binprm中读取filename指针,然后通过bpf_probe_read_kernel_str安全地把内核态字符串拷贝到栈上。栈上的filename属于 eBPF 程序自身栈空间,verifier 可以确认访问边界。
第二,djb2_path遍历栈上字符串,并且循环次数被限制在MAX_PATH以内。这样 verifier 可以验证循环不会死循环,程序能够通过加载检查。
第三,SEC("lsm/bprm_check_security")表明这段程序要挂载到bprm_check_security这个 LSM 钩子。返回0表示允许,返回负错误码表示拒绝,这里是-EPERM,所以用户侧看到的是 “Permission denied”。
6.3 编译命令
使用 clang 编译 eBPF 目标文件:
clang -O2 -g -Wall -target bpf -D__TARGET_ARCH_x86 -c ima_guard.bpf.c -o ima_guard.bpf.o如果你的 CPU 是 ARM64,把__TARGET_ARCH_x86换成__TARGET_ARCH_arm64,并且注意交叉编译环境。编译成功后会生成ima_guard.bpf.o。
6.4 加载并挂载 eBPF 程序
使用 bpftool 加载:
sudo bpftool prog load ima_guard.bpf.o /sys/fs/bpf/ima_guard autoattach如果你的 bpftool 版本较旧,不支持autoattach或 LSM attach,也可以尝试手动挂载:
sudo bpftool prog load ima_guard.bpf.o /sys/fs/bpf/ima_guard sudo bpftool prog attach name guard_bprm_check type lsm bprm_check_security加载成功后,可以查看程序信息:
sudo bpftool prog show name guard_bprm_check如果这里报错,常见原因是内核配置或启动参数没有启用 BPF LSM。检查CONFIG_BPF_LSM=y,并且/sys/kernel/security/lsm中包含bpf。
6.5 查看 eBPF map ID
map 被程序引用,但加载后你需要知道它的 ID,或者直接用名字操作:
sudo bpftool map show name blocked_files输出中会显示 map 的 id、类型、key 大小、value 大小和当前元素数量。接下来向 map 写入黑名单。
7. 联动测试:让一个文件“被杀”
7.1 先创建一个测试脚本
mkdir -p /tmp/av_demo cat > /tmp/av_demo/hello.sh <<'EOF' #!/bin/bash echo "I am running" EOF chmod +x /tmp/av_demo/hello.sh先直接执行它,确认能够正常运行:
/tmp/av_demo/hello.sh预期输出:
I am running7.2 计算路径哈希
这里必须注意:eBPF 程序读取的是传入 execve 的路径。为了让流程稳定,请使用绝对路径执行,并且计算绝对路径的哈希。
python3 - <<'EOF' import struct def djb2(s): h = 5381 for c in s.encode(): h = ((h << 5) + h + c) & 0xFFFFFFFF return h path = "/tmp/av_demo/hello.sh" key = djb2(path) print("key bytes:", " ".join(f"{b:02x}" for b in struct.pack("<I", key))) EOF脚本会输出类似:
key bytes: 27 3a 9c a2这个字节序列是小端序。bpftool 写入 map 时,必须使用相同字节序,否则 eBPF 程序计算出的 key 与 map 中保存的 key 不匹配,查找不到。
7.3 写入 eBPF map
假设上面输出的是27 3a 9c a2,执行:
sudo bpftool map update name blocked_files key hex 27 3a 9c a2 value hex 00 00 00 01value 任意写一个非零值即可,因为 eBPF 程序只检查这个 key 是否存在。写入后查看:
sudo bpftool map dump name blocked_files预期能看到一个 key-value 条目。
7.4 再次执行目标文件
/tmp/av_demo/hello.sh预期输出:
bash: /tmp/av_demo/hello.sh: Permission denied为什么不是 “No such file” 或 “Syntax error”?因为错误发生在内核 execve 路径上,shell 还没能把脚本内容读进内存,进程就被拒绝了。这正是 LSM hook 生效的典型表现。
7.5 验证 IMA 记录
执行被拒绝后,再查看 IMA 度量记录:
sudo tail -n 5 /sys/kernel/security/ima/ascii_runtime_measurements这里可能有两种情况。如果 IMA 策略在 eBPF LSM 之前执行,日志里会多一条/tmp/av_demo/hello.sh的度量记录;如果 eBPF LSM 先返回拒绝,IMA 日志可能没有该文件记录。不同内核版本的 LSM 钩子执行顺序不完全一致,这并不影响方案有效性,因为我们的阻断决策由 eBPF LSM 负责。
7.6 删除黑名单,恢复执行
为了确认是 eBPF map 导致的拒绝,而不是文件权限问题,删除 map 元素后再次执行:
sudo bpftool map delete name blocked_files key hex 27 3a 9c a2 /tmp/av_demo/hello.sh此时应该恢复输出:
I am running到这里,一个完整的“静态哈希拉黑 + 内核执行阻断”链路已经跑通。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| bpftool 报 “Operation not permitted” | 当前用户缺少 CAP_BPF 或 CAP_SYS_ADMIN | sudo执行;检查内核能力和用户组 | 使用 root 用户;在测试环境可临时赋予相应 capability |
| bpftool prog load 报 “unknown attach type” | 内核版本过低或 bpftool 版本过旧 | 查看uname -r和bpftool version | 升级内核和 bpftool,至少保证内核 5.8+,推荐 5.15+ |
| eBPF 程序加载后,执行任何程序都被拒绝 | LSM hook 挂载异常,或 verifier 误判;更常见的是黑名单 map 中写入了系统程序路径的哈希 | 先用bpftool map dump name blocked_files查看 map 内容;再检查程序逻辑 | 删除错误 map 项;测试时先只拉黑一个明确的测试文件 |
/sys/kernel/security/lsm中没有bpf | 内核启动参数未启用 BPF LSM | cat /proc/cmdline查看当前启动参数 | 追加lsm=lockdown,yama,integrity,apparmor,bpf后重启 |
IMA 策略写不进/sys/kernel/security/ima/policy | 内核没有 CONFIG_IMA_WRITE_POLICY,或已存在默认不可写策略 | 查看内核 config 和安全 fs 挂载情况 | 使用支持动态写策略的内核;或在编译内核时开启 CONFIG_IMA_WRITE_POLICY |
| 路径哈希不匹配,map 明明有值但拦截不生效 | 字节序或路径不一致,比如相对路径和绝对路径都传入过 execve | 用bpftool map dump查看实际 key;用 Python 分别计算相对/绝对路径的哈希 | 统一使用绝对路径执行;写入 map 时按照小端字节序填入 |
| 修改 GRUB 参数后系统启动失败 | LSM 列表写错或顺序不对 | 通过带外控制台查看启动日志 | 恢复 GRUB 备份参数;在虚拟机中先验证配置 |
所有这些排查动作,都应该在可控的测试环境完成。涉及内核启动参数和 IMA 策略时,记住一个原则:先备份,再修改。
9. 生产级方案演进与安全边界
9.1 为什么这里只是“toy”
本文实现的方案有几个明显短板:
第一,它拦截的是“路径哈希”,不是“文件内容哈希”。攻击者只要把黑名单文件复制成另一个路径,拦截就会失效。在真实安全产品中,你需要用 IMA 计算出的文件摘要作为 key,或者直接把哈希结果与签名校验绑定。
第二,它拦不住“无文件执行”。如果恶意代码直接把 shellcode 注入到已存在的合法进程中,不走 execve 路径,这个 eBPF 程序根本看不到。
第三,它没有做持久化。一旦重启,eBPF 程序和 map 内容都会丢失。
第四,它依赖 eBPF map 的完整性。如果攻击者已经拿到 root 权限并能加载 eBPF 程序,他可以清空 map 或卸载我们的程序。对抗同权限级对手,需要更深层的信任链保障。
9.2 真实项目应该怎么演进
如果要在生产环境做类似的防篡改执行管控,更推荐的组合是:
- IMA 开启 appraise 模式,配合 IMA 签名机制,只允许执行被信任签名的文件;
- 将 IMA 度量基准哈希存储在 TPM PCR 中,实现远程证明;
- eBPF LSM 程序不是维护静态黑名单,而是查询一个集中的策略下发结果,配合用户态 agent 做实时更新;
- 对白名单之外的可执行文件,先记录审计日志,灰度观察一段时间后再切换为阻断模式;
- 对关键文件系统目录启用 dm-verity 或 overlayfs 只读保护,从文件系统层降低被篡改的风险。
eBPF LSM 最适合的场景,是那些“你希望按自定义规则快速拦截特定执行事件”的专项需求。它做不到万能的杀毒,但可以让你的防护策略成为内核安全检查链路中的一环。对于刚接触 eBPF 的开发者,用这个例子理解 LSM hook 的挂