一次使用 eBPF LSM 來解決系統時間被回調的記錄
最近遇到一個客戶環境運行的虛擬機環境時間不準的問題,雖然環境中都部署了 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 的好像沒什么用的知識。