当前位置: 首页 > article >正文

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 到该函数:

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

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

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

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


http://www.kler.cn/a/577591.html

相关文章:

  • 芯麦GC4931P与A4931/Allegro在电机驱动应用中的对比与优势
  • C/C++蓝桥杯算法真题打卡(Day3)
  • 计算机毕业设计SpringBoot+Vue.js小区团购管理系统(源码+文档+PPT+讲解)
  • IDE集成开发环境MyEclipse中安装SVN
  • 1.12.信息系统的分类【ES】
  • 表格columns拼接两个后端返回的字段(以umi框架为例)
  • 基于Qwen-VL的手机智能体开发
  • Redis常问八股(一)
  • [HTTP协议]应用层协议HTTP从入门到深刻理解并落地部署自己的云服务(2)实操部署
  • 安装好pycharm后,双击pycharm,出现“无法找到入口”,怎么办?
  • 005-获取内存占用率
  • Go学习笔记:基础语法2
  • 六十天前端强化训练之第十四天之深入理解JavaScript异步编程
  • 【前端】webstorm创建一个导航页面:HTML、CSS 和 JavaScript 的结合
  • 神经网络|(十五)|霍普菲尔德神经网络-Storkey 训练
  • 行为模式---责任链模式
  • 气膜科技赋能冰雪产业,开启可持续发展新路径—轻空间
  • mybatis-plus+springboot3项目实现分页
  • RK Android14 应用打开蓝牙时去掉确认提示
  • GESP2024年6月认证C++三级( 第三部分编程题(2)寻找倍数)