利用eBPF LSM实现Blocking写文件操作

原创 吴就业 157 0 2024-06-27

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/35d6c66a23d543888b4f9ed1c31a6f8b

作者:吴就业
链接:https://wujiuye.com/article/35d6c66a23d543888b4f9ed1c31a6f8b
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

LSM是Linux Security Module的缩写,即Linux 安全模块,它是 Linux 内核的一个基于钩子的框架,用于在Linux内核中实现安全策略和强制访问控制。

Linux 5.7版本引入了eBPF LSM(简称LSM BPF)。eBPF LSM是LSM的一个扩展,允许开发者编写eBPF程序来定义和执行安全策略。

因为这个钩子允许我们返回非0以Blocking掉系统调用,所以可以用来实现拦截文件写入的需求。

回到最初的目标,我们想拦截vfs_write这个系统函数,那么有没有对应的LSM的hook呢?

可以用bootlin这个在线网站直接搜索vfs_write,从源码中找hook。这个网站做的很好,有用本地idea打开的体验。

vfs_write函数调用了rw_verify_area这个函数,并且这个函数返回非0,就会直接return了。

ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos){
 	......
  ret = rw_verify_area(WRITE, file, pos, count);
	if (ret)
		return ret;
  ......
}

rw_verify_area这个函数调用了security_file_permission函数。

int rw_verify_area(int read_write, struct file *file, const loff_t *ppos, size_t count){
  .......
  return security_file_permission(file,read_write == READ ? MAY_READ : MAY_WRITE);
}

security_file_permission函数调用call_int_hook,这里就是调用file_permission这个HOOK了。

int security_file_permission(struct file *file, int mask)
{
	int ret;
	ret = call_int_hook(file_permission, 0, file, mask);
	if (ret)
		return ret;
	return fsnotify_perm(file, mask);
}

call_int_hook定义如下。

#define call_int_hook(FUNC, IRC, ...) ({			\
	int RC = IRC;						\
	do {							\
		struct security_hook_list *P;			\
								\
		hlist_for_each_entry(P, &security_hook_heads.FUNC, list) { \
			RC = P->hook.FUNC(__VA_ARGS__);		\
			if (RC != 0)				\
				break;				\
		}						\
	} while (0);						\
	RC;							\
})

所以我们找到了file_permission这个hook。

file_permission钩子用于在访问打开的文件之前检查文件权限。这个钩子(hook)被各种读写文件的操作调用。安全模块可以使用这个钩子对这些操作执行额外的检查,例如重新验证权限以支持权限括号或策略变更。

该钩子的定义如下:

参数说明:

但是vfs_read也调用这个hook,open等其它地方也会调用这个hook,需要能区分是哪个系统函数调用的,这个方案理论上才能跑通。

截屏2024-06-27 11.32.47

《使用 eBPF Linux 安全模块实时修补 Linux 内核中的安全漏洞》这篇文章介绍了可以从rax寄存器中拿到系统调用的编号。然后从《Searchable Linux Syscall Table》这篇文档找到了对应write系统调用的rax值。

截屏2024-06-27 17.48.46

关于rax的解释:在x86-64架构下,linux内核使用%rax通用寄存器存储系统调用的编号,%rdi,%rsi,%rdx,%r10,%r8,%r9则用于传递系统调用的参数,分别用于传递第一个到第六个参数。

关于ebpf的写法,需要使用BPF_PROG这个宏,参考这篇文章:《libbpf中BPF_PROG宏实现机制分析学习》

c代码实现如下:

//go:build ignore

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

#define X86_64_WRITE_SYSCALL 1
#define WRITE_SYSCALL X86_64_WRITE_SYSCALL

char __license[] SEC("license") = "Dual MIT/GPL";

// 这样写会拿不到参数,需要使用BPF_PROG这个宏
// SEC("lsm/file_permission") 
// int handle_file_permission(struct file *file, int mask) {

// 第一个参数是方法名,最后一个参数是前一个hook的返回值,中间的就是file_permission这个hook定义的参数。
SEC("lsm/file_permission")
int BPF_PROG(handle_file_permission,struct file *file, int mask, int ret) { 
    struct pt_regs *regs;
    struct task_struct *task;
    int syscall;

    task = bpf_get_current_task_btf();
    regs = (struct pt_regs *) bpf_task_pt_regs(task);
    // In x86_64 orig_ax has the syscall interrupt stored here
    syscall = regs->orig_ax;
    if (syscall != WRITE_SYSCALL){
        return 0;
    }
    
    // todo 实现根据文件路径返回-1

    bpf_printk("file_permission write...");
    return -1; // 注意,todo逻辑没做的话,测试的时候可以返回0,不然就要重启系统了。因为系统时刻有vfs_write调用。
}

然后使用ebpf-go实现将lsm bpf程序附加到内核上。

package main

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64 lsmbpf lsm-bpf.c -- -I/usr/include

import (
	"github.com/cilium/ebpf/link"
	"github.com/cilium/ebpf/rlimit"
	"log"
	"os"
	"os/signal"
)

func main() {
	if err := rlimit.RemoveMemlock(); err != nil {
		log.Fatal("Removing memlock:", err)
	}

	var objs lsmbpfObjects
	if err := loadLsmbpfObjects(&objs, nil); err != nil {
		log.Fatal("Loading eBPF objects:", err)
	}
	defer objs.Close()

	kp, err := link.AttachLSM(link.LSMOptions{
		Program: objs.HandleFilePermission, // 对应了BPF_PROG的第一个参数handle_file_permission
		Cookie:  0,
	})
	if err != nil {
		log.Fatalf("attach lsm: %s", err)
	}
	defer kp.Close()

	stop := make(chan os.Signal, 5)
	signal.Notify(stop, os.Interrupt)
	for {
		select {
		case <-stop:
			log.Print("Received signal, exiting..")
			return
		}
	}
}

该方案要求内核版本不低于5.15,并且要看系统是否启用了lsm模块。

另外,当前LSM只支持x86_64架构,适用于ARM64的LSM BPF还在开发。

截屏2024-06-27 17.42.09

先简单的验证完Blocking写的逻辑,例如拦截文件名以te开头的文件写,代码如下:

//go:build ignore

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

#define X86_64_WRITE_SYSCALL 1
#define WRITE_SYSCALL X86_64_WRITE_SYSCALL

char __license[] SEC("license") = "Dual MIT/GPL";

SEC("lsm/file_permission")
int BPF_PROG(handle_file_permission,struct file *file, int mask, int ret) {
    struct pt_regs *regs;
    struct task_struct *task;
    int syscall;

    task = bpf_get_current_task_btf();
    regs = (struct pt_regs *) bpf_task_pt_regs(task);
    // In x86_64 orig_ax has the syscall interrupt stored here
    syscall = regs->orig_ax;
    if (syscall != WRITE_SYSCALL){
        return 0;
    }

    // todo 实现根据文件路径返回-1

    struct dentry *dentry = BPF_CORE_READ(&file->f_path,dentry);
    const unsigned char *filename;
    filename = BPF_CORE_READ(dentry,d_name.name);
    char filename_str[256];
    int name_len = bpf_probe_read_kernel_str(filename_str, sizeof(filename_str), filename);
    
  	// 简单的判断拦截
  	if (name_len>8 && filename_str[0]=='t'&& filename_str[1]=='e') {
        bpf_printk("block write...%s",filename_str);
         return -1;
    }

    return 0;
}

这是用vim写text.log文件的测试结果。

ebpf程序的输出日记:

截屏2024-06-27 21.00.41

vim修改文件保存失败:

截屏2024-06-27 21.00.36

#eBPF

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

eBPF拦截文件写操作的可行性调研

折腾很久的一个技术方案调研,如何拦截文件写操作,拒绝用户超量使用nfs文件系统。

看图理解linux内核网络流量控制工具tc(Traffic Control)

tc是Linux内核中用于流量控制的一套功能强大的工具和框架、是内核的一个子系统。本篇通过画图理解tc涉及的概念,并通过实验理解这些概念,以及了解tc命令怎么用。

一个稍微复杂的ebpf-go学习案例:给vfs_write写操作挂个hook

借助eBPF,给vfs_write写操作挂个hook,可以用这个hook实现一些可观察需求。本案例仅供参考学习,可以把案例中的vfs_write改成其它的。

ebpf-go c结构体和go结构体的映射

使用ebpf-go,假如bpf map的value需要用到结构体,而value由go程序写入,c程序读,value结构体在c中声明,那么怎么生成对应的go结构体呢?

踩坑记,如何使用ebpf-go,实战案例:拦截vfs_read函数获取文件名输出

新手入门调研学习使用ebpf-go的笔记, 记录了笔者在实现demo案例过程中踩的坑,很详细。

如何使用bpftrace追踪系统调用性能问题

所以,这个案例的用途是:在read函数调用之前,记录时间戳,在read函数return前计算方法执行耗时,将结果保存到`us`全局变量。使用`@`声明的全局变量,会在bpftrace脚本进程结束时输出到控制台。