Go+eBPF kprobe 禁止运行指定程序
1. 说明
本文属于专栏 Go语言+libbpfgo实战eBPF开发,示例代码目录为 001。
如何下载并运行代码,请参考 专栏介绍。
注: 老学员可以直接
git pull拉取最新代码。
2. 引言
上节课,我们学习了如何通过 tracepoint 监控进程的执行。今天,我们更进一步,学习如何使用 eBPF + kprobe 来禁止运行指定程序。
在某些场景下,我们希望限制某些程序的运行,比如:
- 禁止
reboot,防止服务器被重启 - 禁止
wget,防止未经授权的文件下载 - 禁止
insmod,防止恶意模块加载
那么,如何用 eBPF 实现这一需求呢?🤔
3. 原理
在 Linux 中,execve 是用户态程序创建进程、执行新程序的关键系统调用。它的作用是用新的可执行文件替换当前进程的地址空间 。我们可以利用 eBPF 挂载到 execve,拦截其执行并进行控制。
3.1 kprobe 介绍
什么是 kprobe?
kprobe(Kernel Probe)是一种非常强大的 Linux 机制,它允许我们动态插入探针(Probe),以监控内核中的任意函数。
当被探测的内核函数执行时,kprobe 会触发回调函数,我们可以在回调函数中收集信息、修改参数,甚至影响内核行为。
kprobe 的工作方式
kprobe 主要包含以下几种类型:
kprobe:在目标函数的入口处插入探针kretprobe:在目标函数返回时插入探针jprobe(已废弃):可以捕获函数的所有参数
本项目使用 kprobe,即在 execve 被调用时立刻触发,并决定是否拦截该系统调用。
3.2 实现思路
我们的核心思路如下:
- 使用
kprobe挂载到__x64_sys_execve,监听所有进程的execve调用 - 读取要执行的文件路径 ,获取
filename - 检查规则列表(rule_list),判断该路径是否在禁止名单中
- 拦截
execve调用 :如果匹配,则调用bpf_override_return(),直接让execve返回-1,进程执行失败
📌
bpf_override_return()是eBPF提供的 API,它允许我们修改被 Hook 函数的返回值 。在本例中,我们让execve返回-1,程序就无法运行了。
3.3 kprobe 挂载点选择
在 Linux 内核中,execve 主要有两种:
sys_execve(老版本4.17之前的内核)__x64_sys_execve(4.17及之后的 x86_64 内核)
大多数现代 x86_64 内核都使用 __x64_sys_execve,因此我们挂载 kprobe 到该函数:
c
SEC("kprobe/__x64_sys_execve")
int BPF_KPROBE(probe_execve, struct pt_regs *regs)
📌 为什么不使用 tracepoint?
tracepoint设计的目的主要是用来监控(trace), 而不是拦截。它虽然也能监控到execve的执行, 但是拦截起来比较麻烦。kprobe更灵活,可以在execve真正执行前 进行拦截,适合阻止程序运行。- 注: 需要开启
CONFIG_BPF_KPROBE_OVERRIDE=y, Ubuntu 24.04 默认开启。
- 注: 需要开启
4. 代码详解
4.1 eBPF 代码
4.1.1 代码整体逻辑
BPF 代码主要完成以下几件事:
- 监听
execve - 读取要执行的文件路径
- 遍历
rule_list,判断是否在禁止列表中 - 如果匹配,则拦截
execve,并向用户空间发送事件
4.1.2 代码解析
c
struct event_t {
pid_t ppid;
pid_t pid;
int ret;
char comm[16];
char filename[FILE_NAME_MAX];
};
SEC("kprobe/__x64_sys_execve")
int BPF_KPROBE(probe_execve, struct pt_regs *regs)
{
struct event_t event = { 0, };
fill_event_base_info(&event);
// 获取进程要执行的文件路径
const char *filename_str = (char *)PT_REGS_PARM1_CORE(regs);
bpf_probe_read_str(&event.filename, FILE_NAME_MAX, filename_str);
// 遍历 rule_list,判断是否禁止执行
bpf_for_each_map_elem(&rule_list, &rule_list_cb, &event, 0);
if (event.ret == -1) {
// 拦截 execve,返回 -1,阻止进程执行
bpf_override_return(ctx, event.ret);
// 只上报被禁止的执行事件
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
}
return 0;
}
📌 关键点解析:
-
BPF_KPROBE宏, 方便我们定义kprobe探针, 他会自动帮我们转换函数参数, 宏展开后大概是这个样子:cint probe_execve(struct pt_regs *ctx){ regs = (struct pt_regs *)PT_REGS_PARM1_CORE(ctx); return ___probe_execve(ctx, regs) } int ____probe_execve(struct pt_regs *ctx, struct pt_regs *regs)- 这里出现2次
struct pt_regs可能有些难理解, 我尝试解释一下: struct pt_regs *ctx这个ctx参数是kprobe机制提供的, 里面包括了被hook函数的参数信息struct pt_regs *regs这个regs参数是__x64_sys_execve系统调用的参数,4.17内核之后所有的系统调用参数都统一是struct pt_regs *regs, 而实际要用到的参数(例如文件路径)需要额外再使用PT_REGS_PARMx_CORE获取.
- 这里出现2次
-
PT_REGS_PARM1_CORE(regs)获取execve系统调用的第一个参数,即要执行的程序路径 -
bpf_probe_read_str()读取该路径 -
bpf_for_each_map_elem()遍历rule_list,检查是否禁止 -
如果匹配,
bpf_override_return(ctx, event.ret)直接让execve失败
4.2 Go 代码
4.2.1 代码整体逻辑
用户态 Go 代码的职责:
- 加载 BPF :加载
bpf代码,并挂载到kprobe - 管理规则 :向
rule_list添加要禁止的程序 - 监听事件 :通过
perf buffer监听bpf发送的事件
4.2.2 代码解析
go
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
log.SetLevel(log.DebugLevel)
// 加载 BPF
bpfModule, err := util.BpfLoadAndAttach("bpf.o")
if err != nil {
log.Fatalf("%+v", err)
}
defer bpfModule.Close()
// 获取规则 map
facMap, err := bpfModule.GetMap("rule_list")
if err != nil {
log.Fatalf("get rule_list map error: %v", err)
}
// 添加规则:禁止 /usr/bin/ping
r := NewRule("/usr/bin/ping", 1)
err = r.UpdateMap(facMap, 0)
if err != nil {
log.Fatalf("add rule error: %v", err)
}
// 监听 perf buffer 事件
eventsChannel := make(chan []byte)
lostChannel := make(chan uint64)
pb, err := bpfModule.InitPerfBuf("events", eventsChannel, lostChannel, 1024)
if err != nil {
log.Fatalf("%+v", err)
}
pb.Start()
defer pb.Close()
processEvents(eventsChannel, lostChannel, ctx)
}
📌 关键点解析:
util.BpfLoadAndAttach("bpf.o")加载bpf代码facMap, err := bpfModule.GetMap("rule_list")获取eBPF mapr := NewRule("/usr/bin/ping", 1)添加规则,禁止pingprocessEvents(eventsChannel, lostChannel, ctx)监听bpf发送的事件
这里应该没有太多难点, 如果大家有问题欢迎留言交流.
5. 总结
在本篇文章中,我们学习了如何:
- 使用
kprobe监听execve - 通过
bpf_override_return阻止程序执行 - 在
Go代码中管理eBPF map - 监听
perf buffer,查看哪些进程被阻止
✅ 你现在可以用 eBPF 禁止特定程序的运行了!
6. 练习题
- 修改代码,使其可以通过 命令行参数 传入要禁止的程序路径
- 让
rule_list支持 同时禁止多个程序
👉 你能实现吗?试试看! 🚀