想象一下这个场景:
凌晨 3 点,你还在睡梦中。
黑客已经通过某个漏洞登录了你的服务器,悄无声息地修改了 /etc/passwd 添加了后门用户,篡改了 /var/log/auth.log 抹掉了入侵痕迹,甚至替换了 /bin/login 来记录所有管理员的密码。
等你早上醒来,服务器已经不再属于你了。
传统防护手段?形同虚设。
AIDE?Tripwire?它们只能告诉你"文件被改了",但无法阻止。
等你发现,黑客早就跑了。
😰
多年前为了过等保三,亲手用 python 撸了一套类似系统,但未达到理想目标。
但今天,我要给你介绍一个内核级的解决方案——用 Rust + eBPF 实现文件防篡改,在文件被修改之前,直接拦截!
先来科普一个残酷的现实:
传统的文件完整性工具,都是"事后诸葛亮"。
等你知道被黑的时候,黄花菜都凉了。
而 eBPF 不一样。
它是 Linux 内核的"瑞士军刀",可以在系统调用的关键时刻插入代码,实时拦截、修改、阻止操作。
什么意思?
当有人试图打开文件并写入时,eBPF 程序在内核中就能检测到,直接返回"拒绝访问",连磁盘都不让你碰。
这不是防火墙,这是直接给文件系统装了个金刚罩。
一个安全 + 一个高性能 = 绝配。
今天我们用 Aya 库(Rust 生态最流行的 eBPF 框架)来实现。
首先,确保你的 Linux 内核版本 >= 5.7,并且启用了 BPF LSM:
# 检查 LSM 是否包含 bpf
cat /sys/kernel/security/lsm输出应该包含 bpf,如果没有,编辑 GRUB 配置添加:
lsm=...,bpf然后重启。
安装 Rust 和 Aya 工具链:
# 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 安装 eBPF 开发工具
cargo install cargo-generate
cargo install bpf-linker生成项目模板:
cargo generate --name file-protect -d program_type=lsm -d lsm_hook=file_open https://github.com/aya-rs/aya-template
cd file-protect我们使用 LSM 的 file_open 钩子来拦截文件打开操作。
如果是写模式且目标文件在保护列表中,直接返回 -EPERM(权限拒绝)。
编辑 ebpf/src/main.rs:
#![no_std]
#![no_main]
use aya_bpf::{
macros::{lsm, map},
programs::LsmContext,
maps::HashMap,
cty::{c_int, c_long},
};
#[map]
static mut PROTECTED_FILES: HashMap<u32, u32> = HashMap::<u32, u32>::with_max_entries(1024, 0);
#[lsm(hook = "file_open")]
pub fn file_open(ctx: LsmContext) -> i32 {
unsafe {
let file = ctx.arg::<*const file>(0);
let flags = (*file).f_flags as u32;
// 检查是否为写模式
if flags & (O_WRONLY | O_RDWR) == 0 {
return 0; // 只读操作,放行
}
// 获取 inode 号作为唯一标识
let inode = (*(*file).f_inode).i_ino as u32;
// 检查是否在保护列表中
if let Some(_) = PROTECTED_FILES.get(&inode) {
return -EPERM; // 拒绝!不允许写入
}
0
}
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unreachable!()
}核心逻辑:
简单粗暴,但极其有效。
编译 eBPF 程序:
cargo xtask build-ebpfeBPF 程序跑在内核里,但需要用户空间程序来加载、配置和管理。
编辑 userspace/src/main.rs:
use aya::{Bpf, maps::HashMap, include_bytes_aligned};
use std::fs::metadata;
use std::path::Path;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
// 1. 加载编译好的 eBPF 程序
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/debug/file-protect"
))?;
// 2. 获取 eBPF 程序并附加到 LSM 钩子
let program: &mut Lsm = bpf.program_mut("file_open").unwrap();
program.load()?;
program.attach()?;
// 3. 获取用户空间和内核共享的 HashMap
let mut protected_files: HashMap<_, u32, u32> = HashMap::try_from(
bpf.map_mut("PROTECTED_FILES")?
)?;
// 4. 添加保护文件:例如 /etc/passwd
let path = Path::new("/etc/passwd");
let inode = metadata(path)?.ino() as u32;
protected_files.insert(&inode, &1, 0)?;
println!("🛡️ 文件防篡改程序已启动!");
println!("📁 已保护文件:/etc/passwd");
// 5. 保持程序运行
loop {}
}运行程序:
sudo RUST_LOG=info cargo run现在,尝试修改 /etc/passwd:
sudo vim /etc/passwd或者:
echo "test:x:0:0::/root:/bin/bash" >> /etc/passwd会发生什么?
Permission denied无论你用 vim、echo 还是任何工具,都别想修改这个文件。
黑客再牛,也突破不了内核级别的拦截。
爽不爽?
上面的代码只是一个起点,真正的防护需要更多钩子的配合:
使用 inode_unlink 钩子:
#[lsm(hook = "inode_unlink")]
pub fn inode_unlink(ctx: LsmContext) -> i32 {
// 类似逻辑:检查 inode 是否在保护列表
// 如果是,返回 -EPERM
}使用 inode_rename 钩子:
#[lsm(hook = "inode_rename")]
pub fn inode_rename(ctx: LsmContext) -> i32 {
// 检查 old_dir 和 new_dir 的 inode
// 防止受保护文件被移动到其他位置
}可以参考 ebpfguard 项目,从 YAML 文件加载保护策略:
protected_files:
- /etc/passwd
- /etc/shadow
- /etc/ssh/sshd_config
- /var/log/auth.log当拦截到篡改尝试时,通过 perf event 发送通知到用户空间:
// 内核空间
prog_helper::send_alert(inode);
// 用户空间
let mut perf = PerfBuffer::try_from(bpf.map("EVENTS")?)?;
loop {
let events = perf.read(&mut callbacks).await?;
for event in events {
println!("🚨 警报!有人试图篡改文件 inode: {}", event.inode);
}
}如果你想看看生产级的实现,可以参考这两个开源项目:
它们都使用了类似的技术思路,但功能更完善、边界处理更细致。
建议:先从这个小项目开始理解原理,再参考它们的代码。
组件 | 作用 |
|---|---|
Rust | 编写安全的用户空间和 eBPF 程序 |
Aya | Rust 的 eBPF 库,简化开发流程 |
LSM (Linux Security Modules) | 内核安全框架,提供钩子 |
eBPF | 内核级编程,无需修改内核 |
安全不是口号,是行动。
用 Rust + eBPF 给自己装一个内核级金刚罩,让黑客无处下手。
代码已经给你了,去试试吧!
如果这篇内容对你有帮助,点个在赞,让更多同行学会这项硬核技能🚀