本文地址: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。

image-20240104102658824

关于 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 中以下类别:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ grep "Security hooks for" include/linux/lsm_hooks.h
 * Security hooks for program execution operations.
 * Security hooks for mount using fs_context.
 * Security hooks for filesystem operations.
 * Security hooks for inode operations.
 * Security hooks for kernfs node operations
 * Security hooks for file operations
 * Security hooks for task operations.
 * Security hooks for Netlink messaging.
 * Security hooks for Unix domain networking.
 * Security hooks for socket operations.
 * Security hooks for SCTP
 * Security hooks for Infiniband
 * Security hooks for XFRM operations.
 * Security hooks for individual messages held in System V IPC message queues
 * Security hooks for System V IPC Message Queues
 * Security hooks for System V Shared Memory Segments
 * Security hooks for System V Semaphores
 * Security hooks for Audit
 * Security hooks for the general notification queue:
 * Security hooks for using the eBPF maps and programs functionalities through
 * Security hooks for perf events
 * Security hooks for io_uring

每个定义的钩子函数都包含对应的参数,这些参数根据哪些程序可以实施策略决策提供上下文,并列在 lsm_hook_defs.h, 内核 6.2.0 版本定义数量为 247 个,这里我们给出部分定义实现的样例:

其中 LSM_HOOK 定义格式如下:

1
LSM_HOOK(<return_type>, <default_value>, <hook_name>, args...)

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 相关操作:

1
2
3
4
5
6
7
8
9
#ifdef CONFIG_BPF_SYSCALL
LSM_HOOK(int, 0, bpf, int cmd, union bpf_attr *attr, unsigned int size)
LSM_HOOK(int, 0, bpf_map, struct bpf_map *map, fmode_t fmode)
LSM_HOOK(int, 0, bpf_prog, struct bpf_prog *prog)
LSM_HOOK(int, 0, bpf_map_alloc_security, struct bpf_map *map)
LSM_HOOK(void, LSM_RET_VOID, bpf_map_free_security, struct bpf_map *map)
LSM_HOOK(int, 0, bpf_prog_alloc_security, struct bpf_prog_aux *aux)
LSM_HOOK(void, LSM_RET_VOID, bpf_prog_free_security, struct bpf_prog_aux *aux)
#endif /* CONFIG_BPF_SYSCALL */

在继续并尝试编写 LSM BPF 程序之前,请确保:

  • 内核版本至少为 5.7;

  • LSM BPF 已启用。

LSM BPF 的启用可以通过以下方式进行验证,正确的输出应包含 bpf

1
2
3
4
5
$ grep CONFIG_BPF_LSM  /boot/config-6.2.0-34-generic
CONFIG_BPF_LSM=y

$ cat /sys/kernel/security/lsm
capability,lockdown,landlock,yama,apparmor,bpf

如果没有,则必须通过将 LSM BPF 添加到内核配置参数中来手动启用它。在我本地 Ubuntu 22.04 系统中的输出中内容中,并没有包括 bpf 选项,因此我们需要手动启用。

1
lockdown,capability,landlock,yama,apparmor

通过调整 GRUB 配置 /etc/default/grub 并在内核参数中添加以下内容来实现:

1
GRUB_CMDLINE_LINUX="lsm=lockdown,capability,landlock,yama,apparmor,bpf"

然后通过执行 update-grub 命令 重新构建 GRUB 配置(每个命令可能在不同的 Linux 发行版中可用或不可用):

1
2
3
4
5
6
7
8
$ update-grub

# 通过 grep 启动命令行确认
$ grep lsm /boot/grub/grub.cfg

#update-grub2
#grub2-mkconfig -o /boot/grub2/grub.cfg
#grub-mkconfig -o /boot/grub/grub.cfg

确定添加到启动参数后,重新启动系统生效。

3.1 BCC 实践

BCC 项目已经提供了 LSM 功能支持,我们可以使用宏 LSM_PROBE 进行函数定义,这里我们实现一个禁止调用 bpf() 系统调用的功能,该程序正常执行后,我们任何涉及到 bpf() 系统调用都将显示权限不足,用魔法来打败魔法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os
import sys
import time

from bcc import BPF, libbcc

# support_lsm() 函数检查系统是否支持 BTF 和具有 bpf_lsm_bpf 函数变量,并不能反应 LSM BPF 是否可以工作
if not BPF.support_lsm():
        print("LSM not supported")

prog = """
#include <uapi/asm-generic/errno-base.h>
LSM_PROBE(bpf, int cmd, union bpf_attr *attr, unsigned int size)
{
    bpf_trace_printk("LSM BPF hook Worked");
    return -EPERM;
}
"""

b = BPF(text=prog)

while True:
    b.trace_print()

正常的运行效果图如下:

需要注意的是,如果修改了 GRUB 参数添加了 bpf,但是并没有重启系统,运行上述程序并会报错,但是并不能达到实际运行效果。 在程序运行后,我们使用 bpftool 参数仍然可以运行:

1
2
3
4
5
6
$ bpftool prog list
922: lsm  name bpf  tag 30c9cae9d49a8659  gpl
	loaded_at 2024-01-04T14:58:00+0800  uid 0
	xlated 120B  jited 79B  memlock 4096B
	btf_id 398
	pids python3(927881)

这是因为上述代码中的 BPF.support_lsm() 只是通过静态检查系统是否开启了 BTF 和声明了 bpf_lsm_bpf 函数符号,并不能正确反馈系统是否 lsm 中是否启用了 BPF支持。

1
2
3
4
5
6
7
8
# src/python/bcc/__init__.py
    def support_lsm():
        if not lib.bpf_has_kernel_btf():
            return False
        # kernel symbol "bpf_lsm_bpf" indicates BPF LSM support
        if BPF.ksymname(b"bpf_lsm_bpf") != -1:
            return True
        return False

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <errno.h>

char LICENSE[] SEC("license") = "GPL";

SEC("lsm/bpf")
int BPF_PROG(lsm_bpf, int cmd, union bpf_attr *attr, unsigned int size, int ret)
{
    /* ret is the return value from the previous BPF program
         * or 0 if it's the first hook.
    */
    if (ret != 0)
        return ret;

    bpf_printk("LSM: block bpf() worked");
    return -EPERM;
}

这里需要注意的一点是,函数最后多了一个 int ret ,这是由于 LSM hook 是以链表的方式管理,ret 用于反馈上一个 BPF 程序处理的结果,如果当前运行的 BPF 为首个运行,这 ret 的值为 0。

修改 Makefile 文件:

1
2
-APPS = minimal minimal_legacy bootstrap uprobe kprobe fentry usdt sockfilter tc ksyscall task_iter
+APPS = minimal minimal_legacy bootstrap uprobe kprobe fentry usdt sockfilter tc ksyscall task_iter lsm

运行编译命令,会自动生成 lsm.skel.h 文件,编译成功后会生成 lsm 二进制文件:

1
2
3
4
5
  # make
  BPF      .output/lsm.bpf.o
  GEN-SKEL .output/lsm.skel.h
  CC       .output/lsm.o
  BINARY   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:

1
2
3
4
5
unshare (系统调用 )
-> ksys_unshare
   -> unshare_userns
     -> prepare_creds
        -> security_prepare_creds  (LSM hook 函数)

unshare 系统调用定义

1
2
3
4
SYSCALL_DEFINE1(unshare, unsigned long, unshare_flags)
{
	return ksys_unshare(unshare_flags);
}

ksys_unshare() 中调用函数与 user 相关的调用函数是 unshare_userns()

1
2
3
4
5
6
int unshare_userns(unsigned long unshare_flags, struct cred **new_cred)
{
	// ...
	cred = prepare_creds();
	// ...
}

unshare_userns() 源码如下:

1
2
3
4
5
6
7
int unshare_userns(unsigned long unshare_flags, struct cred **new_cred)
{
	// ...
	cred = prepare_creds();
	// ...
	return err;
}

prepare_creds() 函数有关于 LSM hook 的调用:

1
2
3
4
5
6
struct cred *prepare_creds(void)
{
	// ...
	if (security_prepare_creds(new, old, GFP_KERNEL_ACCOUNT) < 0)
		goto error;
}

BPF 与安全相关参见:https://lwn.net/Kernel/Index/#BPF-Security