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 支持 同时禁止多个程序

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

相关推荐
海风极客3 小时前
Go小技巧&易错点100例(二十五)
开发语言·后端·golang
二狗哈4 小时前
go游戏后端开发34:补杠功能与到时出牌
数据库·游戏·golang
余瑾瑜5 小时前
宝塔面板安装MySQL数据库并通过内网穿透工具实现公网远程访问
开发语言·后端·golang
顾云澜9 小时前
Apache Superset本地部署结合内网穿透实现无公网IP远程查看数据
开发语言·后端·golang
北极象10 小时前
使用Golang打包jar应用
python·golang·jar
二狗哈11 小时前
go游戏后端开发29:实现游戏内聊天
服务器·游戏·golang
muxue17814 小时前
go:实现最简单区块链
开发语言·后端·golang
Achou.Wang14 小时前
go语言内存泄漏的常见形式
开发语言·golang
_yingty_1 天前
Go语言入门-反射4(动态构建类型)
开发语言·笔记·后端·golang
顾琬清1 天前
Linux系统Docker部署开源在线协作笔记Trilium Notes与远程访问详细教程
开发语言·后端·golang