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 主要包含以下几种类型:

  1. kprobe :在目标函数的入口处插入探针
  2. kretprobe :在目标函数返回时插入探针
  3. jprobe(已废弃):可以捕获函数的所有参数

本项目使用 kprobe,即在 execve 被调用时立刻触发,并决定是否拦截该系统调用。


3.2 实现思路

我们的核心思路如下:

  1. 使用 kprobe 挂载到 __x64_sys_execve ,监听所有进程的 execve 调用
  2. 读取要执行的文件路径 ,获取 filename
  3. 检查规则列表(rule_list),判断该路径是否在禁止名单中
  4. 拦截 execve 调用 :如果匹配,则调用 bpf_override_return(),直接让 execve 返回 -1,进程执行失败

📌 bpf_override_return()eBPF 提供的 API,它允许我们修改被 Hook 函数的返回值 。在本例中,我们让 execve 返回 -1,程序就无法运行了。


3.3 kprobe 挂载点选择

在 Linux 内核中,execve 主要有两种:

  1. sys_execve(老版本4.17之前的内核)
  2. __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 探针, 他会自动帮我们转换函数参数, 宏展开后大概是这个样子:

    c 复制代码
    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 代码的职责:

  1. 加载 BPF :加载 bpf 代码,并挂载到 kprobe
  2. 管理规则 :向 rule_list 添加要禁止的程序
  3. 监听事件 :通过 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 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. 练习题

  1. 修改代码,使其可以通过 命令行参数 传入要禁止的程序路径
  2. rule_list 支持 同时禁止多个程序

👉 你能实现吗?试试看! 🚀

相关推荐
JiMoKuangXiangQu5 小时前
Linux eBPF 案例:sk_filter 读取 IP 地址崩溃
linux·ebpf·sk_filter
吴老弟i6 小时前
Go 多版本管理实战指南
golang·go
Grassto10 小时前
HTTP请求超时?大数据量下的网关超时问题处理方案,流式处理,附go语言实现
后端·http·golang·go
Paul_092010 小时前
golang编程题2
开发语言·后端·golang
代码N年归来仍是新手村成员10 小时前
【Go】从defer关键字到锁
开发语言·后端·golang
源代码•宸1 天前
Leetcode—746. 使用最小花费爬楼梯【简单】
后端·算法·leetcode·职场和发展·golang·记忆化搜索·动规
x70x801 天前
Go中nil的使用
开发语言·后端·golang
源代码•宸1 天前
Leetcode—47. 全排列 II【中等】
经验分享·后端·算法·leetcode·面试·golang·深度优先
漫漫求1 天前
Go的panic、defer、recover的关系
开发语言·后端·golang
Tony Bai1 天前
2025 Go 官方调查解读:91% 满意度背后的隐忧与 AI 时代的“双刃剑”
开发语言·后端·golang