LSM BPF 实践
本文地址:https://www.ebpf.top/post/lsm_bpf_intro
1. 安全背景知识
国际上对计算机安全概括了三个特性:私密性(Confidentiality)、 完整性(Integrity)、可用性(Availability),简称 CIA。
- 私密性就是数据不被未授权的人看到;
- 完整性指存储或传输的信息不被篡改;
- 可用性是指自己的设备在需要使用的时候能够使用。
计算机系统应对安全挑战的办法大致有四种:隔离、控制、审计、混淆。
访问控制就是对访问进行控制,其用于表示主体可以针对客体进行的操作。访问控制主要工作包括定义主体和客体,定义操作和设定访问策略。一般来说主体主要是系统中运行的进程,客体为内核中的资源对象包括文件、目录、管道、设备、套接字、共享内存、消息队列等系统资源。
针对访问控制就用户控制维度来看,又可分为:
- 自主访问控制 DAC(Discretionary Access Control):资源所有者决定谁可以访问以及他们可以执行哪些操作。
- 强制访问控制 MAC(Mandatory Access Control):是一种更严格的访问控制方式,访问权限由管理员或专家预先定义的策略来管理控制,系统环境中普通用户无法决定的访问控制。
内核安全知识可观看视频内核安全入门视频,视频的制作者为《Linux 内核安全模块深入剖析》书籍的作者李志。
Linux 内核安全的开发从上世纪 90 年代中后期开始,经过 20 多年的开发, Linux 内核中安全相关模块已经比较全面的,有用于强制访问控制的 LSM 、有用于完整性保护的 IMA 和 EVM 、有用于加密的密钥管理模块和加密算法库、还有日志和审计模块、以及一些零碎的安全增强特性。本文的主角 LSM(Linux Security Module)则是与强制访问控制 MAC 相关的内核安全通用框架。
2. 内核安全策略模块通用框架 LSM
2.1 LSM 框架介绍
LSM(Linux Security Module)中文翻译为内核安全模块,尽管从名字上是安全模块,但 LSM 其实是一个在内核各个安全模块的基础上提出(抽象出)的轻量级安全访问控制框架。该框架只是提供一个支持安全模块的接口,本身不能增强系统安全性,具体的工作交给各安全模块来做。通俗一点讲 LSM 只是搭建了一个舞台,唱戏的主角还得有具体实现的模块来捧场。
LSM 在内核中体现为一组安全相关的函数,是提供实施强制访问控制(MAC)模块的必要组件,可实现策略与内核源代码解耦。安全函数在系统调用的执行路径中会被调用,对用户态进程访问内核资源对象进行强制访问控制,受保护的对象类型包括文件、目录、任务对象、凭据等。从版本 5.4 开始,该框架目前包括整个内核的 224 个挂钩点、一个用于注册要在这些挂钩点调用的函数的 API,以及一个用于保留与受保护内核对象关联的内存以供 LSM 使用的 API。如果要想要进一步了解可参考内核文档 LSM 部分内容。
截止到内核 5.7 版本,共有 9 个 LSM 安全模块实现:SELinux、SMACK、AppArmor、TOMOYO、Yama、LoadPin、SafeSetID 和 Lockdown 和 BPF(5.7 版本支持) 。安全模块主要实现如下图所示:
Seccomp 和 LSM 都可实现内核级别限制进程与系统的交互方式,但安全计算模式(seccomp)是关于限制进程可以进行的系统调用,相比之下,LSM 是关于控制对内核中对象的访问。LSM 与安全框架 seccomp 的区别和联系可进一步阅读 LSM vs seccomp。
关于 Major 和 Minor LSM
-
Major LSM:通过用户空间加载配置策略来实现 MAC,一次只能使用单个 LSM,因为它们都假定它们对嵌入在受保护内核对象中的安全上下文指针和安全标识符具有独占访问权限,例如 SELinux、SMACK、AppArmor 和 TOMOYO。
-
Minor LSM – 次要 LSM 实现了特定的安全功能,并堆叠在主要 LSM 之上,并且大多数需要较少的安全性,较少的上下文。次要 LSM 通常只包含用于启用/禁用选项的标志,而不是将从用户空间加载的策略文件作为系统的一部分。如 Yama、loadPin、SetSafeID 和 Lockdown。
关于 LSM 的完整介绍可进一步参考 LSM 简介和 LSM,Linux 内核的安全防护盾。
2.2 LSM 架构
LSM 框架提供了一个模块化的架构,该架构提供了内置于内核中的 “钩子”(hook),并允许安装安全模块,从而加强了访问控制。
LSM 框架主要由五大部分组成:
- 在关键的特定内核数据结构中加入了安全域,例如
struce file
结构中的void *f_security;
; - 在内核源码中不同的关键点处插入对安全钩子函数的调用;
- 提供了一个通用的安全系统调用,允许安全模块为安全相关的应用编写新的系统调用,其风格类似于原有的Linux系统调用socketcall(),是一个多路的系统调用;
- 提供了注册和注销函数,使得访问控制策略可以以内核模块方式实现,主要是通过 security_add_hooks 和 security_delete_hooks;
- 将 capabilities 逻辑的大部分功能移植为一个可选的安全模块。
具体实现上, LSM 框架通过提供一系列的 Hook 即钩子函数来控制对内核对象的操作,其本质是插桩法。以文件 open 函数访问过程为例,Hook 函数的访问示意图如下:
-
通过系统调用进入内核之后,系统首先进行错误检查;
-
错误检查通过之后,进行传统的权限检查即自主访问控制(Discretionary Access Control,DAC)检查(传统权限检查主要是基于用户的,用户通过验证之后就可以访问资源);
-
通过之后才会进行强制访问控制 MAC。强制访问控制是不允许主体干涉的一种访问控制,其采用安全标识、信息分级等信息敏感性进行访问控制,并通过比较主体的级别和资源的敏感性来确定是否允许访问。
关于 LSM 的实现细节建议进一步阅读深入理解 LSM pdf。
2.3 LSM 中的钩子函数
LSM 背后的核心概念是 LSM 钩子。LSM 钩子暴露在内核的关键位置,可通过挂钩进行管制的操作示例包括:
- 文件系统操作
- 打开、创建、移动和删除文件
- 挂载和卸载文件系统
- task/process operations 任务/进程操作
- 分配和释放任务,更改任务的用户和组标识
- 套接字操作
- 创建和绑定套接字
- 接收和发送消息
系统中 LSM 钩子都列在 Linux 内核源码的头文件 lsm_hooks.h [kernel 6.2.0]中查看到。不同的操作对应场景可通过头部注释获,在内核 6.2.0 中以下类别:
|
|
每个定义的钩子函数都包含对应的参数,这些参数根据哪些程序可以实施策略决策提供上下文,并列在 lsm_hook_defs.h, 内核 6.2.0 版本定义数量为 247 个,这里我们给出部分定义实现的样例:
其中 LSM_HOOK 定义格式如下:
|
|
LSM 提供的大多数钩子都需要返回一个整数值(也有部分返回 void,表示忽略运行结果),返回值中的值一般定义如下:
- 0 等同于授权;
- ENOMEM 无可用内存;
- EACCESS,安全策略拒绝访问;
- EPERM,执行此操作需要权限。
3. LSM BPF
在 LSM BPF出现之前,能够实现实施安全策略目标的方式有两种选择:配置现有的 LSM 模块(如AppArmor、SELinux),或编写自定义内核模块。LSM BPF 则提供了第三种实现的方案,灵活且安全,具有可编程性。
Linux 5.7 引入在 LSM 中提供了对于 BPF 的支持(简称 LSM BPF)。使用 LSM BPF,开发人员能够在无需配置或加载内核模块的情况下编写精细策略。LSM BPF 程序会在加载时进行验证,然后在调用路径中到达 LSM hook 时执行。这些 BPF 程序允许特权用户对 LSM 钩子进行运行时检测,以使用 eBPF 实现系统范围的 MAC(强制访问控制)和审计策略。
截止到内核 6.2.0 版本, BPF 自身安全相关的 LSM hook 函数有 7 个,通过编译条件宏 CONFIG_BPF_SYSCALL 控制,主要设涉及 bpf 系统调用、BPF 程序和 BPF map 相关操作:
|
|
在继续并尝试编写 LSM BPF 程序之前,请确保:
-
内核版本至少为 5.7;
-
LSM BPF 已启用。
LSM BPF 的启用可以通过以下方式进行验证,正确的输出应包含 bpf
:
|
|
如果没有,则必须通过将 LSM BPF 添加到内核配置参数中来手动启用它。在我本地 Ubuntu 22.04 系统中的输出中内容中,并没有包括 bpf 选项,因此我们需要手动启用。
|
|
通过调整 GRUB 配置 /etc/default/grub
并在内核参数中添加以下内容来实现:
|
|
然后通过执行 update-grub
命令 重新构建 GRUB 配置(每个命令可能在不同的 Linux 发行版中可用或不可用):
|
|
确定添加到启动参数后,重新启动系统生效。
3.1 BCC 实践
BCC 项目已经提供了 LSM 功能支持,我们可以使用宏 LSM_PROBE
进行函数定义,这里我们实现一个禁止调用 bpf()
系统调用的功能,该程序正常执行后,我们任何涉及到 bpf() 系统调用都将显示权限不足,用魔法来打败魔法:
|
|
正常的运行效果图如下:
需要注意的是,如果修改了 GRUB 参数添加了 bpf,但是并没有重启系统,运行上述程序并会报错,但是并不能达到实际运行效果。 在程序运行后,我们使用 bpftool 参数仍然可以运行:
|
|
这是因为上述代码中的 BPF.support_lsm()
只是通过静态检查系统是否开启了 BTF 和声明了 bpf_lsm_bpf 函数符号,并不能正确反馈系统是否 lsm 中是否启用了 BPF支持。
|
|
bpftrace 目前还不支持 LSM BPF,相关工作已经在讨论开发中,相关工作可参见 Support attaching to bpf LSM hooks 。
3.2 libbpf-bootstrap 框架实践
环境准备参考官方仓库编译章节,关于 libbpf-bootstrap 介绍文档可查考 Building BPF applications with libbpf-bootstrap。
在 /examples/c 目录中添加 lsm.bpf.c:
|
|
这里需要注意的一点是,函数最后多了一个 int ret
,这是由于 LSM hook 是以链表的方式管理,ret 用于反馈上一个 BPF 程序处理的结果,如果当前运行的 BPF 为首个运行,这 ret 的值为 0。
修改 Makefile 文件:
|
|
运行编译命令,会自动生成 lsm.skel.h 文件,编译成功后会生成 lsm 二进制文件:
|
|
运行效果与 BCC 编写的类似:
另外基于 cilium/ebpf Go 库的代码可以参考 lsm-ebpf-experimenting 仓库,这个就不在展示。
4. 总结
本文简单介绍了 LSM 框架的基础知识,并基于 LSM BPF 给出了 BCC 和 libbpf 库的实现样例,希望能够让你快速入门 LSM BPF 的编程。如果你对 LSM BPF 的应用场景希望有更多的了解,推荐你进一步阅读使用 eBPF LSM 热修复 Linux 内核漏洞,在该文中作者基于容器环境中 USER 命名空间提权的场景给出了基于 LSM BPF 规避风险的完整实践,这包括原理、选择 hook 函数和 BPF 代码实现。
5. 附录:LSM 热修内核漏洞查找 hook 点过程
解决一个问题最难的是找到阻断问题的关键路径点,为了方便大家理解,这里将使用 eBPF LSM 热修复 Linux 内核漏洞文章中的确定 LSM hook 点的过程给出,供后续参考,完整的 bpf 代码可在lsm_bpf_monitoring 找到。
允许无特权的用户访问 USER 命名空间始终会带来极大的安全风险。其中一种风险就是特权提升。特权提升是操作系统的常见攻击面。用户可以获取特权的一种方式是通过 unshare syscall 将其命名空间映射到根命名空间,并指定 CLONE_NEWUSER 标志。这会指示 unshare 创建有完整权限的新用户命名空间,并将新用户和组 ID 映射到之前的命名空间。你可以使用 unshare(1) 程序将根映射到我们的原始命名空间:
通过手册页可以知道,unshare 用于改变任务,所以我们来看一下 include/linux/lsm_hooks.h 中基于任务的 hook。早在函数 unshare_userns() 中,我们就看到对 prepare_creds() 的调用。这非常类似于 cred_prepare hook。为了验证我们是否通过 prepare_creds() 获得匹配,我们观察对安全性 hook security_prepare_creds() 的调用,后者最终会调用该 hook:
|
|
|
|
ksys_unshare() 中调用函数与 user 相关的调用函数是 unshare_userns()
|
|
unshare_userns() 源码如下:
|
|
prepare_creds() 函数有关于 LSM hook 的调用:
|
|
BPF 与安全相关参见:https://lwn.net/Kernel/Index/#BPF-Security
- 原文作者:DavidDi
- 原文链接:https://www.ebpf.top/post/lsm_bpf_intro/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 最后更新时间:2024-02-04 15:49:27 +0800 CST