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)被各种读写文件的操作调用。安全模块可以使用这个钩子对这些操作执行额外的检查,例如重新验证权限以支持权限括号或策略变更。
该钩子的定义如下:
参数说明:
- @file 包含正在访问的文件结构。
- @mask 包含请求的权限。
- 如果权限被授权,返回0,否则返回其它。
但是vfs_read也调用这个hook,open等其它地方也会调用这个hook,需要能区分是哪个系统函数调用的,这个方案理论上才能跑通。
《使用 eBPF Linux 安全模块实时修补 Linux 内核中的安全漏洞》这篇文章介绍了可以从rax寄存器中拿到系统调用的编号。然后从《Searchable Linux Syscall Table》这篇文档找到了对应write系统调用的rax值。
关于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还在开发。
先简单的验证完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程序的输出日记:
vim修改文件保存失败: