尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

用eBPF和IMA构建内核级文件哈希拦截:一个玩具杀毒原型

用eBPF和IMA构建内核级文件哈希拦截:一个玩具杀毒原型 这篇笔记想聊的是用 eBPF 和内核 IMAIntegrity Measurement Architecture作为 LSM 能力的一部分去实现一个只配叫玩具级别的 antivirus。很多人看到 ring-0 就会联想到内核模块甚至恶意程序但这里的 ring-0 其实指的是 BPF LSM 运行在内核态。目标很单纯在文件被打开时基于 IMA 算出来的文件哈希去一个拦截名单里查命中就拒绝这次打开。它不会扫描磁盘没有病毒特征库也处理不了压缩包和免杀但它能把 eBPF、IMA、LSM 三块东西串成一个可以实际运行的链路适合想理解内核态访问控制的人拿来当学习项目。先提醒一句这个项目里的 IMA 不是智能知识库助手也不是数学建模工具而是 Linux 内核里的 Integrity Measurement Architecture。它的职责是给文件做完整性度量最常见就是算哈希。下面的内容会围绕这个定义展开。1. 先搞清楚这个玩具杀毒到底在做什么1.1 三个关键词各自解决什么问题eBPF 提供在内核态运行受限程序的能力不用改内核源码也不用像传统内核模块那样容易把系统搞崩。IMA 负责对文件做完整性度量最常见的动作是计算文件哈希并可以基于策略把结果写到扩展属性或安全日志里。LSM 是 Linux Security Module 框架提供一组安全钩子eBPF 可以通过 BPF LSM 类型挂到这些钩子上。在这个项目里三者配合方式很清楚IMA 负责算哈希LSM 钩子负责拦截动作eBPF 负责把策略逻辑放进内核态。传统杀毒更多依赖用户态扫描和系统调用审计而这个链路从文件打开动作触发时就开始检查不需要反复遍历文件系统适合做“访问控制前的一层快速闸门”。1.2 为什么它只是“玩具”不是生产杀软标题里特意写了 crappy toy原因很简单。真正的 antivirus 要解决未知威胁、内存扫描、行为分析、文件监控、事件关联、样本库更新、误报率控制。这个项目只做一件事判断文件的 IMA 哈希是否在一个固定名单里。它没有机器学习没有行为特征没有应急响应。更关键的是哈希黑白名单只能挡住已经知道哈希的样本遇到同一样本换一个字节就失效。所以不要把它理解成“用 eBPF 重写杀毒”更准确的定位是“用 eBPFIMALSM 做文件完整性策略实验”。如果公司里已经有 HIDS agent 或者文件监控系统这个也不是要替代它们而是演示一种更低层、更精准的拦截点。1.3 这种项目最适合谁适合三类人。第一类是刚接触 eBPF想找一个小而完整的练习目标。第二类是搞 Linux 安全加固想理解 LSM 钩子怎么落地。第三类是运维和平台研发想给特定服务器加一道自定义文件保护策略但又不想写内核模块。这三类人都能从这个原型里拿到一个熟悉的骨架再改成自己的场景。如果只是想用 BPF 做系统审计那挂 tracepoint 就够了不需要走到 LSM。如果想做强制访问控制BPF LSM 会更有针对性。这个玩具的价值是让你看清楚“能监控”和“能拦截”之间差多少个环节。2. 运行这个原型需要满足哪些条件2.1 内核版本和编译选项建议先用较新的长期支持内核做验证比如 5.15 或 6.x 系列。不是所有内核都支持同一组 BPF helper尤其bpf_ima_file_hash这类 helper 比较晚才出现。可以先检查当前内核配置grep -E CONFIG_BPF|CONFIG_BPF_SYSCALL|CONFIG_BPF_LSM|CONFIG_IMA|CONFIG_SECURITY /boot/config-$(uname -r)至少需要CONFIG_BPFy和CONFIG_BPF_SYSCALLyCONFIG_BPF_LSMyCONFIG_IMAyCONFIG_SECURITYy开启 BTF通常对应CONFIG_DEBUG_INFO_BTFy如果检查发现CONFIG_BPF_LSM没有开启后面程序挂不到 LSM hook 上。这里不用急着换内核可以先用一个开启这些选项的虚拟机或云主机测试。2.2 工具链和权限编译 BPF 程序需要 clang、LLVM版本尽量新一点。用 libbpf 时通常还需要 libelf、zlib 以及 bpftool。Ubuntu/Debian 可以安装clang llvm libbpf-dev linux-tools-commonFedora 则用clang llvm libbpf-devel bpftool。实际包名以发行版为准这里给的是通用思路。加载 BPF LSM 程序需要 root或者有足够 capabilities。最简单方案就是 root 用户操作。如果权限不够会在加载时看到 permission denied而不是运行时报错。我一般建议在虚拟机或者一次性测试环境里做不要直接在重要服务器上试因为一个返回码写错root 也可能打不开文件。2.3 IMA 策略的准备IMA 并不是自动算所有文件的哈希需要先有策略。IMA 策略通常放在 securityfs 下面。先把 securityfs 挂载出来mount -t securityfs securityfs /sys/kernel/security cat /sys/kernel/security/ima/policy如果 policy 是空的需要追加一条规则让常见文件操作触发度量。一个最小示例echo measure funcFILE_CHECK maskMAY_READ /sys/kernel/security/ima/policy注意不同内核 IMA 的写法会有差异有些环境不允许空 policy 时直接写入有些环境要求uid0或明确路径前缀。这里给的是通用写法实际策略要以当前内核文档为准。2.4 一个可以复现的验证环境我自己测试时用的组合是 Ubuntu 22.04 的内核 5.15clang 14libbpf 较新版本root shell。先用虚拟机快照做实验避免把开发机搞乱。因为 BPF LSM 是强制访问控制一个 return 写错就可能让 root 也打不开文件所以建议在测试机里操作而不是直接在生产服务器上试。一个小技巧先写一个什么都不拒绝、只打印日志的版本确认 LSM hook 能被触发再逐步加拦截逻辑。这样能把“环境没配好”和“策略写错”分开排查。3. 从文件哈希到拒绝访问核心链路拆解3.1 为什么用 IMA 而不是自己读文件算哈希从用户态也能算 SHA256比如sha256sum。但在这个场景里我们希望文件被打开时能在内核态立刻判断如果每次 open 都从用户空间读一遍文件内容性能会非常差。IMA 作为内核完整性度量框架本身会在文件访问、执行等时机计算哈希并且有缓存机制结果可以供 BPF helper 使用。这样内核态的程序只需要拿到哈希结果不用自己重新打开文件。这不是说 IMA 没有开销。首次访问一个文件时哈希计算仍然需要读文件内容只是不需要在每次 open 时都做一次完整文件扫描。如果 IMA 没有为待测文件建立哈希记录bpf_ima_file_hash可能拿不到结果所以前一步的 IMA policy 很关键。3.2 为什么选择 file_open 这个 LSM 钩子BPF LSM 可以挂很多安全钩子比如file_open、file_permission、path_mknod、bprm_check。选择file_open的理由很直观文件被打开是后续读取和执行的前提拦在这里最省事只需要检查一次。代价也很明显。如果文件在进程里已经被打开之后不会再触发 file_open。比如一个长期运行的服务已经在启动时读了一个文件即使后来这个文件被判定为需要拦截服务仍可能持续使用已经打开的文件描述符。所以 file_open 适合做“新访问控制”不适合做“持续擦除已有句柄”。另外一个考虑是file_openhook 的参数签名里有struct file *file和const struct cred *cred程序拿到struct file *之后可以直接调用 IMA helper。不同 hook 的签名不一样挂 hook 前最好先查一下当前内核的 BTF 信息。3.3 哈希黑名单查找和返回码设计核心逻辑并不复杂先调用bpf_ima_file_hash拿到文件哈希然后用这个哈希作为 key 去一个 hash map 里查能查到就返回-EPERM查不到就返回 0。返回 0 表示放行返回非 0 会被 LSM 当作权限错误返回给系统调用。实际返回哪个 errno 需要看场景-EPERM对“文件被拒绝打开”比较常见。这里有一个容易忽略的点拿不到哈希的时候应该怎么处理。玩具原型里可以先返回 0放行避免把系统搞成什么都打不开。但生产环境不应该这样写否则只要 IMA policy 没生效整个拦截策略就形同虚设。更稳妥的做法是记录日志并继续放行交给用户态监控进程去告警。3.4 一个示意代码下面是核心 BPF 程序的示意代码。它利用 libbpf 的宏定义 LSM hook并使用bpf_ima_file_hash获取哈希。不同 Linux 发行版、内核版本甚至 clang 版本可能都会影响编译结果请把它当学习骨架不要直接当成通用补丁。// ima_lsm_antivirus.bpf.c #include linux/bpf.h #include bpf/bpf_helpers.h #include bpf/bpf_tracing.h #include linux/errno.h #define MAX_HASH_SIZE 64 struct hash_key { unsigned char data[MAX_HASH_SIZE]; unsigned int len; }; struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 1024); __type(key, struct hash_key); __type(value, unsigned char); } deny_hash_map SEC(.maps); SEC(lsm/file_open) int BPF_PROG(deny_known_hash, struct file *file, const struct cred *cred) { unsigned char hash[MAX_HASH_SIZE] {}; struct hash_key key {}; long hash_len; hash_len bpf_ima_file_hash(file, hash, sizeof(hash)); if (hash_len 0 || hash_len MAX_HASH_SIZE) return 0; __builtin_memcpy(key.data, hash, hash_len); key.len hash_len; if (bpf_map_lookup_elem(deny_hash_map, key)) return -EPERM; return 0; } char LICENSE[] SEC(license) GPL;代码里的哈希结构体用了固定 64 字节常见 SHA256 是 32 字节SHA1 是 20 字节。用 64 字节只是为了预留空间实际匹配时要保证 key 长度和 IMA 使用的哈希算法一致。3.5 如何确认 helper 和 hook 是否可用如果编译报错说bpf_ima_file_hash未定义先检查内核头文件和 libbpf 版本另外也可以在运行时通过 BTF 查看 helper 信息。比如bpftool feature probe | grep -i ima bpftool btf dump file /sys/kernel/btf/vmlinux | grep bpf_imaBPF_PROG这类宏属于 libbpf 提供的bpf_tracing.h如果 BCC 环境不兼容可以直接改用原始SEC(lsm/file_open) int ...的写法但要自己处理参数。建议在同一套 libbpf 环境里编译避免宏不一致。4. 落地步骤先跑通单文件再扩展到目录和进程4.1 准备测试文件并记录 IMA 实际哈希先在测试目录里创建一个标记文件比如叫blocked.binmkdir -p /tmp/ima-lsm-demo echo this file should be blocked /tmp/ima-lsm-demo/blocked.bin不要直接用sha256sum的结果去填 map因为 IMA 的哈希策略和哈希算法可能和你手动算的不完全一致。更可靠的办法是让 BPF 程序先把 hash 打印出来再把这个 hash 填进 map。具体做法是先把上面的代码里的拦截逻辑注释掉只保留bpf_printk输出bpf_printk(file hash len%d first%x\n, hash_len, hash[0]);然后打开这个文件再查看 trace 日志cat /sys/kernel/debug/tracing/trace_pipe日志里能看到这个文件对应的哈希字节序列。把这段字节序列作为 map key 填到 deny_hash_map 中。这样就能避免 IMA 算法和用户态工具不一致导致的匹配问题。如果不方便用 trace也可以选择固定 IMA 为 SHA256再手动对比。只是要给 IMA 策略加上哈希算法指定不同内核的写法有差异没有 trace 方式通用。4.2 编译并加载 BPF 程序编译命令大致如下clang -O2 -g -target bpf -c ima_lsm_antivirus.bpf.c -o ima_lsm_antivirus.bpf.o然后使用 bpftool 或 libbpf skeleton 加载。如果 bpftool 支持自动 attachbpftool prog load ima_lsm_antivirus.bpf.o /sys/fs/bpf/ima_lsm_antivirus autoattach加载后确认程序存在bpftool prog show如果 bpftool 不支持 autoattach就用 libbpf 的 skeleton 方式先用bpftool gen skeleton生成头文件再写一个用户态 C 程序调用 open、load、attach。skeleton 的优点是可以直接在用户态初始化 map不需要手工在命令行填 key。4.3 验证拦截效果加载程序前先确认blocked.bin可以正常读取cat /tmp/ima-lsm-demo/blocked.bin再把该文件的 IMA 哈希填入 deny map之后再次读取cat /tmp/ima-lsm-demo/blocked.bin预期会看到类似cat: /tmp/ima-lsm-demo/blocked.bin: Permission denied的报错。与此同时其他文件的打开操作仍然正常。这里要多留意一点有些环境会先缓存文件或者测试进程已经有打开的文件句柄导致多次执行cat时没有重新触发 file_open。最简单的方法是每次验证都重新执行一个新的cat进程进程每次都会重新打开文件。4.4 扩展到只拦截特定路径或特定进程不想拦所有文件时可以在 BPF 程序里加路径判断。比如只要文件名包含blocked才检查 hash// 示意通过 d_path 获取路径字符串然后做前缀匹配 char path[256] {}; if (bpf_d_path(file-f_path, path, sizeof(path)) 0) return 0; if (strncmp(path, /tmp/ima-lsm-demo/, 18) ! 0) return 0;需要注意的是BPF 程序中字符串比较不能直接使用普通 libc 函数通常需要内联或使用受限 helper。这里只给思路实际实现时要根据当前内核的 helper 支持情况调整。也可以改成按进程 pid、cgroup id、uid 过滤把 map 的设计改得更灵活。另一个扩展方向是把 map 改成 LRU hash、数组或 percpu 结构匹配速度更快。但对于玩具项目标准 hash map 已经足够。5. 常见问题和排查链路5.1 程序没有触发 / helper 获取不到哈希最容易出现的情况是 BPF LSM 没有加载成功。先看bpftool prog show里有没有对应程序再看 attach 是否成功。如果程序加载成功但没有触发可能是挂到了错误的 hook或者内核的 LSM hook 顺序和你预期不一致。如果bpf_ima_file_hash返回负值先看 IMA policy 是否生效。可以执行cat /sys/kernel/security/ima/ascii_runtime_measurements这个文件记录了 IMA 已经度量过的文件。如果测试文件不在列表里说明 IMA 没有对它做度量BPF helper 自然拿不到结果。此时需要更新 IMA policy或者换一个已经触发过度量的文件测试。5.2 明明在拦截名单里却不能拒绝先检查返回值和 map key。返回值要写为负 errno比如-EPERM不能写 0也不能写EPERM正数。然后检查 key 的字节是不是真的和bpf_ima_file_hash返回的字节一致。我踩过的一个坑是用户态用 hex 字符串填 mapBPF 里用二进制字节查 map两边数据布局不一致。建议先跑一个只打印哈希的版本把 hash 打印出来再用脚本以二进制方式写入 map。不要凭肉眼核对十六进制字符串字节序很容易搞错。5.3 哈希算法不一致导致匹配不上IMA 默认可能用 SHA1也可能在发行版里被改成 SHA256。如果内核模块开启了完整性校验哈希计算还可能与文件 metadata 有关单纯拿sha256sum结果对不上很正常。解决办法是放弃用户态猜 hash而是从 trace 或 ascii_runtime_measurements 里拿 IMA 的真实结果。还可以在 IMA policy 里显式指定算法但要先确认内核支持哪些算法。5.4 性能、并发和日志注意事项BPF LSM 每次文件 open 都会触发即使 map 查不到也会消耗一点 CPU。如果机器上文件打开非常频繁建议先用路径过滤或 uid 过滤减少 helper 调用。bpf_printk虽然方便但生产环境不要一直开启否则会被日志拖慢。玩具项目里用来调试可以实际使用时应该通过 perf event 或 ring buffer 把数据发到用户态处理而不是直接打印。还有一点BPF 栈大小通常限制在 512 字节。上面代码里 hash 占 64 字节、key 占 68 字节加上其他临时变量一般够用。但如果同时声明多个 256 字节数组就很可能超限导致编译失败。6. 从玩具到可用还差哪些东西6.1 策略管理不能把哈希写死在代码里玩具项目可以在 map 里手填 key但真实场景需要一套管理流程样本更新、黑白名单下发、过期策略清理、审计日志记录。最好通过用户态守护进程定期把新的哈希写入 map同时通过 ring buffer 接收拦截事件。map 本身还可以设置 BPF_F
返回列表