有个朋友在某次安全技术交流结束后跑来问我:你说我用 eBPF 写一个杀毒软件,是不是很酷?我第一反应是,eBPF 配合 IMA LSM 做内核态检测原型,确实可行,但你写出来的东西大概率活不过第一轮性能压测。他很快反问:那为什么现在这么多项目都在讨论 eBPF 做 AV、做 EDR?我意识到,很多人把“能拿到内核事件”和“能做成杀毒软件”画了等号。
我想聊的,就是怎么用 eBPF 配合 IMA LSM,把一个“crappy ring-0 toy antivirus”造出来——顺带说清楚它为什么只能是个 toy。
1. 很多人想用 eBPF 写杀毒,先要搞清楚它到底在杀什么
1.1 “ring-0 玩具杀软”这个说法,其实已经把重点说透了
标题里的 ring-0,在常见的安全语境里指的是 CPU 的最高特权级,也就是内核态。Windows 上很多杀软有内核驱动,Linux 上过去要做一个同样的事,通常也得写内核模块。内核模块出错就是宕机,开发调试门槛很高。eBPF 的出现把一个相对可控的“内核内执行环境”带给了普通开发者,于是很多项目开始尝试在 eBPF 里做安全检测。
但“ring-0”不等于“安全”。你可以把一个程序放进内核,不代表它能挡住所有恶意行为。“crappy”这个词才是项目的真实底色:它能跑,功能有限,边界粗糙,只适合当玩具或者教学原型。明白这一点,再去看 eBPF + IMA 的杀毒方案,才不会产生不切实际的期待。
这个玩具的核心价值,不是真的去对抗一轮 APT 攻击,而是让你理解:一次文件执行,在内核里经过哪些路径;一个安全检测系统,需要哪些模块才能成立。
1.2 IMA、eBPF、LSM 三个概念,放到一件小事里说清
假设用户执行了/tmp/demo.sh。
- IMA(Integrity Measurement Architecture)会先对这个文件做哈希度量,并把度量结果记录到内核的运行时度量列表中。它回答的是“这个文件的内容是什么”。
- LSM(Linux Security Module)提供了一组内核安全钩子,例如 file_open、bprm_check。内核在执行脚本前会经过这些钩子,安全模块可以在钩子函数里决定是否允许继续。
- eBPF 是一种内核内虚拟机,可以让开发者安全地挂载到 tracepoint、kprobe,以及 BPF LSM 提供的挂钩点,从而看到事件发生、修改返回值、记录上下文。
三个东西合在一起,就组成了一条完整的检测链路:eBPF 负责“看到文件被执行”,IMA 负责“拿到文件的完整性度量”,LSM 钩子负责“在被执行前插入我们的检查逻辑”。用户态程序再根据哈希和策略决定放行或告警。
1.3 为什么不是写一个用户态扫描器
传统做法是扫描磁盘文件,把文件和病毒库比对。这种模式的问题在于,你无法实时知道一个文件什么时候被创建、被修改、被执行。要实时,就得轮询文件系统,要么扫描全盘,要么使用 inotify/Fanotify。但这些都是用户态机制,和内核态之间始终隔着一层上下文切换,而且你拿不到执行链路中的精确上下文。
eBPF 的优势是实时、低开销、内核视角。一个文件被 execve 进来,eBPF 程序可以在事件发生的路径上立刻感知。IMA 又提供了哈希度量作为数据源,两者结合,就可以实现一个简单的“文件行为监控器”。
但它并不是真正的杀毒软件。真正的杀毒软件还需要病毒特征库、静态启发式分析、模拟执行、宏分析、固件检测、云端信誉系统等。eBPF + IMA 只能给你一个“检测管线”的骨架,离商业安全产品还差很多层。
2. 先搭一条最小可运行链路:文件被打开,哈希被校验,事件被记录
2.1 内核侧要准备什么
我建议先准备一个可随时重启的虚拟机,不要在主机的物理内核上做这种实验。IMA 策略一旦在内核中存在,部分版本里无法在运行时完全清空;eBPF 程序如果写得不合适,也会影响系统行为。
先确认内核配置是否支持要用的能力。常见检查方式:
grep -E 'CONFIG_(IMA|BPF|BPF_SYSCALL|BPF_LSM|SECURITYFS)' /boot/config-$(uname -r)如果输出里CONFIG_BPF=y和CONFIG_BPF_SYSCALL=y,说明 eBPF 基础能力没问题;CONFIG_IMA=y说明 IMA 功能被编译进内核;CONFIG_BPF_LSM=y说明 BPF LSM 支持可用;CONFIG_SECURITYFS=y说明需要挂载 securityfs 来管理 LSM 策略。
在实际发行版里,这几个配置不一定全部开启。特别是CONFIG_BPF_LSM,不少发行版默认没有启用。如果没启用,BPF LSM 路径就无法工作,但 tracepoint 路径仍然可以用。
2.2 用 IMA 测量模式先拿到基线
IMA 有多种模式,常见的有 measure、appraise、audit。measure 模式会计算并记录文件哈希,但不阻止执行;appraise 模式会在哈希校验不通过时拒绝访问。玩具方案里,我建议先用 measure,先看数据,不要一上来就强制拦截。
需要挂载 securityfs:
mount -t securityfs securityfs /sys/kernel/security写入一条测量策略,让内核在每次执行二进制或脚本时计算哈希:
echo "measure func=BPRM_CHECK" > /sys/kernel/security/ima/policy注意:这条命令只是一种常见写法,具体策略语法取决于内核版本和发行版配置。如果写入失败,第一步先看dmesg,第二步确认当前用户是否有权限,第三步确认是否已有策略规则存在。在某些内核配置下,一旦写入过策略,后续清理需要重启虚拟机。
写入成功后,可以查看 IMA 的运行时度量记录:
tail /sys/kernel/security/ima/ascii_runtime_measurements如果能看到类似boot_aggregate或执行文件的哈希记录,说明 IMA 测量链路已经走通。这一步的价值是拿到“文件哈希”的权威来源,后续和 eBPF 采集的事件做关联。
2.3 再用 eBPF 监听执行与打开事件
IMA 负责度量,eBPF 负责事件。最快速的验证方式是先用 bpftrace 看能不能捕获 execve 系统调用:
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%d %s %s\n", pid, comm, str(args->filename)); }'这个命令会在每次执行新进程时打印 PID、进程名和被执行的路径。如果这个能跑通,说明 eBPF 事件通道没问题。
如果要更贴近“杀毒”场景,可以挂到 LSM 钩子上,比如security_file_open。这样文件被打开时就能感知。但 LSM hook 的可用性依赖CONFIG_BPF_LSM,并且需要把bpf注册到 LSM 列表中,否则程序无法 attach。很多发行版里,CONFIG_BPF_LSM没有开启,你会遇到类似unknown func或invalid argument的错误。
从工程经验看,如果只是想先跑通一个玩具原型,不需要执着于 LSM 钩子。tracepoint 已经能覆盖“看到文件被执行”这个需求。LSM 的价值在于“执行前拦截”,这个后面再补。
2.4 用户态决策:哈希、白名单和告警
内核侧的 eBPF 程序把事件抛给用户态,用户态进程负责决策。为什么不能全部放在 eBPF 里做?因为 eBPF 程序有复杂度限制,不适合在 BPF 指令流里维护一个大哈希表、加载特征库、做正则匹配。更合理的分工是:
- eBPF 只负责采集事件,把 PID、路径、调用链等基本信息发出去。
- IMA 负责提供文件哈希。
- 用户态程序维护白名单或规则集,计算哈希后与白名单比对。
- 如果匹配失败,用户态程序可以记录告警,也可以调用接口终止进程。
这个链路里,真正的“决策”在用户态。虽然牺牲了一点实时性,但对玩具原型来说完全够用,而且更容易调试和更新规则。
3. 把“拿到事件”变成“内核态检测”,中间有多道坎
3.1 决策要不要放在内核态
很多人会把“放在内核态”当成优势本身。实际上,决策在用户态还是内核态,要看你做什么判断。
如果只是对少量文件做哈希比对,用户态完全能胜任,而且写起来简单得多。用户态可以做 SQLite 查询、加载 YARA 规则、访问外部 API,这些都是 eBPF 里很难实现的。
但如果你要追求极低的延迟,或者要在进程 exec 之前就完成拒绝,那就必须依赖内核态动作。IMA 的 appraise 模式可以在内核态直接拒绝哈希不匹配的文件,但它依赖签名和密钥管理体系,这比“用 bpftrace 看一眼事件”复杂得多。
更务实的做法是:先让 eBPF 事件通知用户态,由用户态调用kill或通过系统调用干预。这个过程会有几百微秒到几毫秒的延迟,对玩具场景足够了。真正进入生产级 EDR 时,再考虑把关键决策下放到内核,使用 LSM 钩子返回错误码来阻断。
3.2 LSM 钩子、eBPF 和 IMA 策略可能会打架
Linux 的 LSM 框架里可以同时启用多个安全模块,模块之间存在固定顺序。BPF LSM 在这个顺序里的位置,直接影响你的 eBPF 程序能否看到其他 LSM 已经处理过的结果。IMA 本身也实现了一些安全钩子,如果你的策略和 BPF LSM 策略同时存在,会让问题变得难排查。
最常见的现象是:你写了一个 BPF LSM 程序想拦截某个文件执行,但同一个 hook 上还有 AppArmor 或 SELinux 的策略,二者返回值和处理逻辑叠加,最后文件还是被执行了,或者反而被拒绝了。你在 eBPF 里看到的输入是原始参数,但在它之前可能已经有模块修改了路径、改写了挂载命名空间,或者对文件做了 overlay 映射。
所以原型环境里,建议先关闭不必要的 LSM 模块,或者在启动参数里明确指定 LSM 顺序。这不是为了“绕过”安全机制,而是为了让实验变量足够干净,方便你确认到底是 BPF 程序没触发,还是 LSM 顺序干扰了结果。
3.3 一个比较稳妥的原型判断流程
把整个体系看作一条流水线:
- 事件发生:
execve或open触发。 - eBPF 程序在事件路径上采集路径、PID、进程名、返回结果等上下文。
- eBPF 把事件写入 ring buffer 或 perf buffer。
- 用户态程序接收事件,拿到真实路径,计算文件哈希。
- 用户态程序查询白名单/黑名单。
- 如果文件不在白名单中,记录告警,并决定是否终止进程。
每一步都不复杂,但串联起来就是一个可用的安全检测原型。这个流程最容易被忽略的是“真实路径”这一层。用户看到的/tmp/demo.sh可能是一个符号链接,真实文件在/usr/local/packages/demo.sh;也可能是一个 overlayfs 路径,在宿主机和容器里看到的路径不一样。如果只拿用户态路径去做白名单,哈希永远匹配不上。
4. 参数、边界和排错:这个玩具离生产到底差多远
4.1 内核配置、启动参数和挂载点,先花十分钟确认
很多问题不是写错代码,而是环境没准备好。我列了一个检查维度,可以直接当作上手清单:
| 检查项 | 判断标准 | 常见问题 |
|---|---|---|
| 内核版本 | 至少 5.7 以上,BPF LSM 才可用 | 内核太老,部分能力缺失 |
| CONFIG_BPF / CONFIG_BPF_SYSCALL | 需要 y | eBPF 程序无法加载 |
| CONFIG_BPF_LSM | 需要 y | BPF LSM attach 失败 |
| CONFIG_IMA | 需要 y | IMA 目录不存在 |
| SECURITYFS 挂载 | /sys/kernel/security有内容 | 策略无法写入 |
| IMA policy 状态 | 能读到规则,或能写入规则 | 策略为只读 |
| 当前用户权限 | 需要 root 或 CAP_SYS_ADMIN | 写入与加载被拒绝 |
先花十分钟跑一遍,能避免绝大多数“为什么没反应”的疑问。
4.2 最常见的问题是“事件没出来”和“策略没生效”
我先说一个通用排查链路:现象 → 输入 → 环境 → 参数 → 日志。不要一上来就怀疑代码写得不对。
- 现象:bpftrace 执行后没有任何输出。
- 输入:确认是不是真的执行了一个二进制文件,还是执行了一个内建 shell 命令。
for、cd、echo这些命令不一定触发 execve。 - 环境:确认内核配置,确认是否有 root 权限,确认是否在容器里缺少 securityfs 可见性。
- 参数:确认 attach 点名称是否正确,比如 tracepoint 路径里有没有打错字,LSM hook 是否被内核支持。
- 日志:运行
dmesg -w观察内核日志,很多时候 eBPF verifier 会明确告诉你失败原因。
如果 IMA 策略没有生效,可以这样排查:
- 查看
/sys/kernel/security/ima/ascii_runtime_measurements是否有内容。 - 如果没有,检查 policy 写入是否成功,以及是否选择了
measure而不是其他模式。 - 再确认是不是所有文件都被过滤了,比如 IMA 策略里可能写了
uid=0或fsname=ext4等条件。
4.3 五个边界:为什么它只能叫 toy antivirus
第一,特征能力缺失。它没有恶意特征库,不能识别未知恶意软件。白名单模式只能发现“不在列表里的文件”,无法回答“这个文件为什么是恶意的”。
第二,检测面有限。用 execve 和 open 事件做检测,只能看到“文件被打开/执行”这个维度。一个恶意脚本可以完全不做文件落地,直接内存执行,或者借用合法工具进程做间接操作。toy 方案看不到这些。
第三,性能问题。如果对每个执行文件都做完整哈希,在大规模环境中会带来明显延迟。尤其是在打包脚本、编译任务、批量工具调用场景中,哈希计算会成为瓶颈。
第四,策略维护成本高。白名单需要持续更新。开发环境里每天都会生成新二进制,临时脚本、构建产物,都会触发误报。玩具系统可以人工加白名单,生产系统这就变成了一个需要流程化管理的策略平台。
第五,自身防护不足。eBPF 程序可以被预期约束限制,但并不能完全阻止有足够权限的人卸载你的程序。真正的安全产品还需要自我完整性校验、防篡改机制、内核侧审计日志,这些已经远远超出“toy”的范畴。
5. 从玩具到可用原型,我建议你按这个框架拆
5.1 五层拆开:事件采集、数据源、决策引擎、动作、日志
我把这类项目拆成五层,这样既能避免一开始就陷入某段代码,也方便后面替换模块。
| 分层 | 职责 | 玩具实现 | 生产级方向 |
|---|---|---|---|
| 事件采集 | 感知内核事件 | tracepoint / BPF LSM | 多 hook 组合、事件去重、上下文关联 |
| 数据源 | 提供判断依据 | IMA 哈希、文件路径 | 特征库、信誉查询、威胁情报 |
| 决策引擎 | 判断是否放行 | 本地白名单 | 动态策略、ML/规则引擎、多源评分 |
| 动作 | 执行处置 | 记日志/杀进程 | LSM 拦截、隔离、云侧联动 |
| 日志 | 保留审计记录 | 输出到文件 | 审计平台、Kafka、SIEM 集成 |
这个框架的好处是每一层都可以单独迭代。事件采集不完善时,可以用日志层补信息;决策引擎不成熟时,可以先只告警不阻断。每一层之间用明确的接口连接,后面换掉任何一层都不会影响其他部分。
5.2 先跑通、再批量、最后自动化
我的建议是分三个阶段推进。
第一阶段,先跑通最小链路。找几个测试文件,手动执行,确认 eBPF 事件能出来,IMA 哈希能取到,用户态白名单能正确判断。
第二阶段,扩大样本集。放几十个系统命令进去,观察误报率和性能开销。重点看两个东西:一是文件哈希计算的耗时,二是批量执行时事件是否丢。eBPF ring buffer 如果满了,事件会被丢,这在日志里可能表现为“偶尔缺记录”。
第三阶段,再考虑自动化。把白名单放进配置文件或数据库,把告警接到已经存在的监控体系里,比如文件日志、消息队列、工单系统。这时候你已经不是在写一个玩具,而是把玩具身上练出来的经验,落成一个轻量级检测原型的骨架。
5.3 这类项目真正适合谁,不适合谁
它适合三类人:
- 学 eBPF 和内核安全事件机制,需要一个有画面感的练手项目的人。
- 安全平台开发工程师,想快速验证“实时检测文件执行”这类需求的可行性。
- 反病毒或者 EDR 产品的新人,想理解内核态检测中事件、策略、哈希、阻断之间的关系。
它不适合:
- 想用它保护真实生产环境的人。
- 想直接替换商业杀软或 EDR 产品的人。
- 没有时间维护白名单和策略,指望一套规则解决所有问题的人。
把预期放低,反而能做出一个不错的学习项目。如果你一开始就期待它成为一个“全网最强”的杀毒软件,那大概率会在性能、误报、日志风暴里很快放弃。
回到朋友那个问题。用 eBPF 写一个玩具杀毒,值不值得?我觉得很值得。它最大的收获不是得到一套能用的杀毒软件,而是让你把“用户态扫描”这件事放到内核视角重新看一遍。真正可复用的,是你对事件、哈希、策略、拒绝、日志这条链路的理解。
等你要做真正的安全产品,或者想评估 eBPF 是否能承担某类安全检测时,这个玩具会是一个非常清晰的参考系。先跑通一条事件链,再谈对抗强度,这比一开始就追求“什么都检测”要靠谱得多。