一次使用 eBPF LSM 来解决系统时间被回调的记录

开发 前端
vm-agent 应该是虚拟机的一个管理进程,可能的原因是物理机的时间慢 2min 左右,然后通过 vm-agent 来调整了当前主机所有虚拟机的时间。

最近遇到一个客户环境运行的虚拟机环境时间不准的问题,虽然环境中都部署了 ntp/chronyd 进行时间同步,校准了时间,但是隔一段时间系统时间仍会被意外调慢约 2 分钟。于是做了一个分析。大概包含下面这些内容:

  • 如何使用 eBPF LSM 编程拦截特定的操作
  • 如何修改 grub 内核启动参数
  • grub 修改后未生效怎么办
  • grub bls 技术是什么,grubby 命令使用
  • rust eBPF 编程实践

万事开头难:使用 eBPF LSM 进行行为监控

为了精确定位修改系统时间的肇事者进程,利用 eBPF LSM(Linux Security Module)钩子函数进行监控,观测是谁在调用 settime,收集调用进程的详细信息,包括进程 ID、进程名称,以便后面做拦截。这里依然使用 rust aya 进行 eBPF 编程。

#[lsm]
pub fn hook_settime(ctx: LsmContext) -> i32 {
    let comm_bytes = match ctx.command() {
        Ok(bytes) => { bytes }
        Err(_) => { return 0; }
    };

    let len = comm_bytes.iter()
        .position(|&x| x == 0)
        .unwrap_or(comm_bytes.len());

    if len > 0 {
        let comm_str = unsafe { core::str::from_utf8_unchecked(&comm_bytes[..len]) };
        info!(&ctx, "lsm called: settime {}/{}", ctx.pid(), comm_str);
    }

    0
}

但是运行以后的 eBPF 程序以后,手动调用命令设置系统时间,eBPF 程序没有任何输出。经过一番调试,发现问题出在 LSM(Linux Security Module)的配置上。通过检查 LSM 配置状态:

cat /sys/kernel/security/lsm

capability,selinux

可以到当前系统仅启用了 capability 和 selinux 模块,缺少必需的 bpf 模块支持。正确配置下的 LSM 输出应该包含:

cat /sys/kernel/security/lsm

capability,yama,bpf

按常规步骤修改了 GRUB 配置 /etc/default/grub,在 GRUB_CMDLINE_LINUX 末尾新增 lsm 配置选项

lsm=ndlock,lockdown,yama,integrity,apparmor,bpf

接下来调用 grub2-mkconfig 命令更新 grub 配置,随后重启

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

但是一顿操作下来,重启后发现 /sys/kernel/security/lsm 还是没有 bpf 选项。查看 /proc/cmdline 也没有对应的内核启动时传递的启动参数。

# 检查当前 LSM 模块状态
$ cat /sys/kernel/security/lsm
capability,selinux    # 显示配置未生效

# 检查内核启动参数
$ cat /proc/cmdline   # 未发现新增的 LSM 参数

BOOT_IMAGE=(hd0,msdos1)/vmlinuz-5.10.134-16.2.an8.x86_64 root=UUID=7b851053-3729-47c6-a73e-aec3083f4a82 ro resume=UUID=8c6bad2a-6a76-491d-99a2-90cbccf2ba33 rhgb quiet cgroup.memory=nokmem crashkernel=0M-2G:0M,2G-8G:192M,8G-:256M

从 dmesg 中启动日志也可以同步确认。

$ dmesg -T | less

...
[三 11月  6 08:45:01 2024] Linux version 5.10.134-16.2.an8.x86_64 (mockbuild@anolis-build-02.openanolis.cn) (gcc (GCC) 8.5.0 20210514 (Anolis 8.5.0-18.0.4), GNU ld version 2.30-119.0.2.an8.2) #1 SMP Mon Mar 4 16:14:16 CST 2024
[三 11月  6 08:45:01 2024] Command line: BOOT_IMAGE=(hd0,msdos1)/vmlinuz-5.10.134-16.2.an8.x86_64 root=UUID=7b851053-3729-47c6-a73e-aec3083f4a82 ro resume=UUID=8c6bad2a-6a76-491d-99a2-90cbccf2ba33 rhgb quiet cgroup.memory=nokmem crashkernel=0M-2G:0M,2G-8G:192M,8G-:256M

...

为什么修改了 /etc/default/grub 未按预期生效

这里暂时不知道为什么没有生效,去 /boot 目录搜索相关的关键字,看下是不是有其它的配置文件

图片图片

找到了 /boot/loader/entries 文件,经过确认,这是因为这个 Linux 发行版比较新,引入了 BLS(Boot Loader Specification)功能。

BLS 是一个新的启动加载项配置规范,旨在统一和简化 Linux 系统的启动配置管理。它由 Fedora/Red Hat 推出,现已被多个主流 Linux 发行版采用,配置文件位于 /boot/loader/entries/ 目录。它可以不再需要手动编辑 grub.cfg 文件,同时支持多个内核版本的独立配置。

可以使用 grubby 工具进行修改,常见的操作如下:

# 查看默认内核
grubby --default-kernel

# 查看所有内核信息
grubby --info=ALL

# 修改默认内核参数
grubby --args="xxx=xxx" --update-kernel=DEFAULT

# 删除内核参数
grubby --update-kernel=ALL --remove-args="quiet"

比如 grubby info 命令可以看到当前的 grub 列表

grubby --info=ALL

index=0
kernel="/boot/vmlinuz-5.10.134-16.2.an8.x86_64"
args="ro resume=UUID=8c6bad2a-6a76-491d-99a2-90cbccf2ba33 rhgb quiet $tuned_params cgroup.memory=nokmem crashkernel=0M-2G:0M,2G-8G:192M,8G-:256M"
root="UUID=7b851053-3729-47c6-a73e-aec3083f4a82"
initrd="/boot/initramfs-5.10.134-16.2.an8.x86_64.img $tuned_initrd"
title="Anolis OS (5.10.134-16.2.an8.x86_64) 8"
id="de564eb41d9d4440b67b167404b867a4-5.10.134-16.2.an8.x86_64"

index=1
kernel="/boot/vmlinuz-0-rescue-de564eb41d9d4440b67b167404b867a4"
args="ro crashkernel=auto resume=UUID=8c6bad2a-6a76-491d-99a2-90cbccf2ba33 rhgb quiet"
root="UUID=7b851053-3729-47c6-a73e-aec3083f4a82"
initrd="/boot/initramfs-0-rescue-de564eb41d9d4440b67b167404b867a4.img"
title="Anolis OS (0-rescue-de564eb41d9d4440b67b167404b867a4) 8"
id="de564eb41d9d4440b67b167404b867a4-0-rescue"

有了这些知识,更新 grub 就变简单了

# 使用 grubby 工具更新内核参数:

grubby --args="lsm=lockdown,capability,landlock,yama,apparmor,bpf" --update-kernel=DEFAULT

系统重启后,通过检查 LSM 配置确认更改已生效:

cat /sys/kernel/security/lsm

capability,yama,bpf

现在我们可以继续使用 eBPF 程序来监控系统时间修改行为。

使用 bpf 拦截

在成功启用 LSM BPF 支持后,我们快速定位到了时间异常调整的源头,是因为虚拟机的 vm-agent 进程在定时做修改。(图中的另外一个 date 是我在手动调用 date 修改时间)

图片图片

接下来要做的就是匹配 vm-agent,然后拦截它。

const VM_AGENT_BYTES: &'static [u8] = b"vm-agent";

#[lsm]
pub fn hook_settime(ctx: LsmContext) -> i32 {
    let comm_bytes = match ctx.command() {
        Ok(bytes) => { bytes }
        Err(_) => { return 0; }
    };

    let len = comm_bytes.iter()
        .position(|&x| x == 0)
        .unwrap_or(comm_bytes.len());

    if len == VM_AGENT_BYTES.len() && &comm_bytes[..len] == VM_AGENT_BYTES {
        info!(&ctx, "match vm-agent, return -1");
        return -1;
    }
    0
}

userspace 端的代码比较简单,加载 eBPF 程序,然后永久等待。

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    std::env::set_var("RUST_LOG", "debug");
    env_logger::init();

    let mut ebpf = aya::Ebpf::load(aya::include_bytes_aligned!(concat!(
        env!("OUT_DIR"),
        "/time-backward"
    )))?;

    if let Err(e) = aya_log::EbpfLogger::init(&mut ebpf) {
        warn!("failed to initialize eBPF logger: {}", e);
    }

    let btf = Btf::from_sys_fs()?;
    let program: &mut Lsm = ebpf.program_mut("hook_settime").unwrap().try_into()?;
    program.load("settime", &btf)?;
    program.attach()?;

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

    Ok(())
}

这样实现以后,就有效阻止了 vm-agent 的修改时间操作(下面是一个模拟的、假的名为 vm-agent 的程序去设置时间的操作被拒绝)。

# 拦截效果验证
./vm-agent +%T -s "10:07:48"
./vm-agent: cannot set date: Operation not permitted
10:07:48

把程序放到虚拟机中运行,就可以拦截掉搞事情的 vm-agent 了。

图片图片

由于没有 vm-agent 乱改时间,现在系统的时间就正常了,经过长时间的观测,时钟非常准确。

图片图片

小结

vm-agent 应该是虚拟机的一个管理进程,可能的原因是物理机的时间慢 2min 左右,然后通过 vm-agent 来调整了当前主机所有虚拟机的时间。

ebpf 代码很简单,但是往往是它运行的环境需要比较小心的去准备。ps: 又学到了一点 grub 的好像没什么用的知识。

责任编辑:武晓燕 来源: 张师傅的博客
点赞
收藏

51CTO技术栈公众号