Go+eBPF kprobe 禁止运行指定程序
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
到该函数:
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 代码解析
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
探针, 他会自动帮我们转换函数参数, 宏展开后大概是这个样子:-
int 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
获取.
-
-
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 代码解析
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 map
r := NewRule("/usr/bin/ping", 1)
添加规则,禁止ping
processEvents(eventsChannel, lostChannel, ctx)
监听bpf
发送的事件
这里应该没有太多难点, 如果大家有问题欢迎留言交流.
5. 总结
在本篇文章中,我们学习了如何:
- 使用
kprobe
监听execve
- 通过
bpf_override_return
阻止程序执行 - 在
Go
代码中管理eBPF map
- 监听
perf buffer
,查看哪些进程被阻止
✅ 你现在可以用 eBPF
禁止特定程序的运行了!
6. 练习题
- 修改代码,使其可以通过 命令行参数 传入要禁止的程序路径
- 让
rule_list
支持 同时禁止多个程序
👉 你能实现吗?试试看! 🚀