Skip to content

LSM

源代码

本章示例的完整代码可在此处找到。

什么是LSM

LSM代表Linux安全模块,这是一种框架,允许开发人员在Linux内核之上编写安全系统。在Linux内核文档中也有简要描述。

LSM由内核模块使用,或者(自内核5.7起)由eBPF程序使用。最受欢迎的使用LSM的模块包括AppArmor、SELinux、Smack和TOMOYO。eBPF LSM程序允许开发人员使用eBPF API实现上述模块所实现的相同功能。

LSM背后的核心概念是LSM钩子。LSM钩子在内核的关键位置暴露,eBPF程序可以附加到这些钩子上以实现自定义的安全策略。可以通过钩子进行策略控制的操作示例包括:

  • 文件系统操作
  • 打开、创建、移动和删除文件
  • 挂载和卸载文件系统
  • 任务/进程操作
  • 分配和释放任务、更改任务的用户和组标识
  • 套接字操作
  • 创建和绑定套接字
  • 接收和发送消息

上述每个操作都有相应的LSM钩子。每个钩子接受多个参数,这些参数提供有关程序及其操作的上下文,以便实施策略决策。带有其参数的钩子列表可以在lsm_hook_defs.h头文件中找到。

例如,考虑task_setnice钩子,其定义如下:

LSM_HOOK(int, 0, task_setnice, struct task_struct *p, int nice)

该钩子在为系统中的任何进程设置nice值时触发。如果您不熟悉进程优先级的概念,请查看此文章。从定义中可以看出,该钩子接受以下参数:

  • ptask_struct的实例,表示设置nice值的进程
  • nice是nice值

通过附加到该钩子,eBPF程序可以决定是否接受或拒绝给定的nice值。

除了钩子定义中发现的参数外,eBPF程序还可以访问一个额外的参数——ret,这是可能的先前eBPF LSM程序的返回值。

确保启用了BPF LSM

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

  • 您的内核版本至少为5.7。
  • 启用了BPF LSM。

可以通过以下方式检查第二点:

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

正确的输出应包含bpf。如果没有,则必须通过将其添加到内核配置参数中手动启用BPF LSM。可以通过编辑/etc/default/grub中的GRUB配置并将以下内容添加到内核参数来实现:

GRUB_CMDLINE_LINUX="lsm=[YOUR CURRENTLY ENABLED LSMs],bpf"

然后使用以下命令之一重建GRUB配置(每个命令可能在不同的Linux发行版中可用或不可用):

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

最后,重新启动系统。

编写LSM BPF程序

让我们尝试创建一个由task_setnice钩子触发的LSM eBPF程序。该程序的目的是拒绝为特定进程设置低于0的nice值(意味着更高的优先级)。

可以使用renice工具更改niceness值:

$ renice [value] -p [pid]

使用我们的eBPF程序,我们希望让给定pidrenice调用使用负[value]变得不可能。

eBPF项目由两部分组成:eBPF程序和用户空间程序。为了使我们的示例简单,我们可以尝试拒绝更改加载eBPF程序的用户空间进程的nice值。

第一步是创建一个新项目:

$ cargo generate --name lsm-nice -d program_type=lsm -d lsm_hook=task_setnice https://github.com/aya-rs/aya-template

该命令应创建一个新的Aya项目,其中包含一个附加到task_setnice钩子的空程序。让我们进入其目录:

$ cd lsm-nice

传递给task_setnice钩子参数之一是指向task_struct类型的指针。因此,我们需要使用aya-tool生成task_struct的绑定。

如果您不熟悉aya-tool,请参考此部分

$ aya-tool generate task_struct > lsm-nice-ebpf/src/vmlinux.rs

现在是时候修改lsm-nice-ebpf项目并在那里编写实际程序了。完整的程序代码应如下所示:

lsm-nice-ebpf/src/main.rs
#![no_std]
#![no_main]

use aya_ebpf::{cty::c_int, macros::lsm, programs::LsmContext};
use aya_log_ebpf::info;

// (1)
#[allow(non_upper_case_globals)]
#[allow(non_snake_case)]
#[allow(non_camel_case_types)]
#[allow(dead_code)]
mod vmlinux;

use vmlinux::task_struct;

// (2)
/// PID of the process for which setting a negative nice value is denied.
#[no_mangle]
static PID: i32 = 0;

#[lsm(hook = "task_setnice")]
pub fn task_setnice(ctx: LsmContext) -> i32 {
    match unsafe { try_task_setnice(ctx) } {
        Ok(ret) => ret,
        Err(ret) => ret,
    }
}

// (3)
unsafe fn try_task_setnice(ctx: LsmContext) -> Result<i32, i32> {
    let p: *const task_struct = ctx.arg(0);
    let nice: c_int = ctx.arg(1);
    let ret: c_int = ctx.arg(2);
    let global_pid: c_int = core::ptr::read_volatile(&PID);
    let pid: c_int = (*p).pid;

    info!(&ctx,
          "The PID supplied to this program is: {}, with nice value {} and return value {}. Monitoring for changes in PID: {}",
          pid, nice, ret, global_pid);
    if ret != 0 {
        return Err(ret);
    }

    if pid == global_pid && nice < 0 {
        return Err(-1);
    }

    Ok(0)
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    unsafe { core::hint::unreachable_unchecked() }
}
  1. 我们包含自动生成的task_struct绑定:
  2. 然后我们定义一个全局变量PID。我们将值初始化为0,但在运行时,用户空间部分将用我们感兴趣的实际pid修补该值。
  3. 最后,我们有程序和关于nice值的处理逻辑。

之后,我们还需要修改用户空间部分。我们不需要像eBPF部分那样多的工作,但我们需要:

  1. 获取PID。
  2. 记录它。
  3. 将其写入eBPF对象中的全局变量。

最终结果应如下所示:

lsm-nice/src/main.rs
use std::process;

use aya::{include_bytes_aligned, programs::Lsm, BpfLoader, Btf};
use aya_log::BpfLogger;
use log::{info, warn};
use tokio::signal;

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    env_logger::init();

    // (1)
    let pid = process::id() as i32;
    info!("PID: {}", pid);

    // This will include your eBPF object file as raw bytes at compile-time and load it at
    // runtime. This approach is recommended for most real-world use cases. If you would
    // like to specify the eBPF program at runtime rather than at compile-time, you can
    // reach for `Bpf::load_file` instead.
    #[cfg(debug_assertions)]
    let mut bpf = BpfLoader::new().set_global("PID", &pid, true).load(
        include_bytes_aligned!(
            "../../target/bpfel-unknown-none/debug/lsm-nice"
        ),
    )?;

    #[cfg(not(debug_assertions))]
    let mut bpf = BpfLoader::new().set_global("PID", &pid, true).load(
        include_bytes_aligned!(
            "../../target/bpfel-unknown-none/release/lsm-nice"
        ),
    )?;
    if let Err(e) = BpfLogger::init(&mut bpf) {
        // This can happen if you remove all log statements from your eBPF program.
        warn!("failed to initialize eBPF logger: {}", e);
    }
    let btf = Btf::from_sys_fs()?;
    let program: &mut Lsm =
        bpf.program_mut("task_setnice").unwrap().try_into()?;
    program.load("task_setnice", &btf)?;
    program.attach()?;

    info!("Waiting for Ctrl-C...");
    signal::ctrl_c().await?;
    info!("Exiting...");

    Ok(())
}
  1. 我们从获取和记录PID开始:
  2. 然后我们设置全局变量:

之后,我们可以使用以下命令构建并运行我们的项目:

$ RUST_LOG=info cargo xtask run

输出应包含显示用户空间进程PID的日志行,例如:

16:32:30 [INFO] lsm_nice: [lsm-nice/src/main.rs:22] PID: 573354

现在我们可以尝试更改该进程的nice值。设置正值(降低优先级)应仍然有效:

$ renice 10 -p 587184
587184 (process ID) old priority 0, new priority 10

但设置负值应不被允许:

$ renice -10 -p 587184
renice: failed to set priority for 587184 (process ID): Operation not permitted

如果这样做导致Operation not permitted,恭喜,您的LSM eBPF程序正常工作!