从 seccomp filter 学习内核 bpf 自定义 hook 点的设计实现

本文探讨了内核中BPF自定义hook点的设计,以seccompfilter为例,解释了如何注册bpfhook点,动态attachBPF代码,以及代码的校验、翻译、JIT编译的过程。同时,详细阐述了bpf过滤数据的准备和过滤结果的行为定义。
摘要由CSDN通过智能技术生成

从 seccomp filter 学习内核 bpf 自定义 hook 点的设计


本文以 seccomp filter 为例,探讨内核中 bpf 自定义 hook 点的一种设计实现,主要围绕如下几个核心问题叙述:

  1. bpf hook 点如何注入到 seccomp filter 代码中?需要哪些参数?不同参数的含义是什么?
  2. bpf 代码如何动态 attach 到执行入口中?
  3. bpf 代码的校验与翻译在哪里做?校验有哪些条件?jit 在哪个步骤进行?
  4. bpf 代码过滤的数据如何准备?
  5. 定义 bpf 规则过滤结果的行为

基线信息

内核源码版本
4.18

bpf hook 点如何注册到已有代码中?

可以通过使用 BPF_PROG_RUN宏在已有代码中添加 bpf hook 点。BPF_PROG_RUN
宏的定义如下:

#define BPF_PROG_RUN(filter, ctx)  (*(filter)->bpf_func)(ctx, (filter)->insnsi)

filter 参数代表了一个 bpf_prog结构,此结构的 bpf_func 为 bpf 代码的执行入口。bpf_func 依赖如下两个参数:

  1. ctx 参数(表示 bpf 规则执行的上下文信息)
  2. insn 参数(代表实际的 bpf 指令码)

在 net 模块中搜索,能够搜索到如下类似使用逻辑:

./net/bpf/test_run.c:20:	ret = BPF_PROG_RUN(prog, ctx);
./net/sched/act_bpf.c:53:		filter_res = BPF_PROG_RUN(filter, skb);
./net/sched/act_bpf.c:57:		filter_res = BPF_PROG_RUN(filter, skb);
./net/sched/cls_bpf.c:103:			filter_res = BPF_PROG_RUN(prog->filter, skb);
./net/sched/cls_bpf.c:107:			filter_res = BPF_PROG_RUN(prog->filter, skb);

seccomp filter 的实现中,引用 BPF_PROG_RUN 宏注入 bpf 过滤点相关代码如下:

static u32 seccomp_run_filters(const struct seccomp_data *sd,
			       struct seccomp_filter **match)
{
	u32 ret = SECCOMP_RET_ALLOW;
	/* Make sure cross-thread synced filter points somewhere sane. */
	struct seccomp_filter *f =
			READ_ONCE(current->seccomp.filter);

	/* Ensure unexpected behavior doesn't result in failing open. */
	if (WARN_ON(f == NULL))
		return SECCOMP_RET_KILL_PROCESS;

	/*
	 * All filters in the list are evaluated and the lowest BPF return
	 * value always takes priority (ignoring the DATA).
	 */
	for (; f; f = f->prev) {
		u32 cur_ret = BPF_PROG_RUN(f->prog, sd);

		if (ACTION_ONLY(cur_ret) < ACTION_ONLY(ret)) {
			ret = cur_ret;
			*match = f;
		}
	}
	return ret;
}

上述代码调用 BPF_PROG_RUN 的逻辑在一个 for 循环里,表明一个进程支持绑定多个 seccomp 过滤规则,最终返回的值最小的结果它表示最高优先级。

bpf 代码如何动态 attach 到执行入口中?

在跳入到内核代码之前,先复习下之前上手 seccomp 使用的一个用户态程序,原文链接如下:
上手 seccomp,其中注入 bpf 的代码如下:

tatic int install_filter(int nr, int arch, int error) {
  struct sock_filter filter[] = {
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
    ..........................................................................
  };
  struct sock_fprog prog = {
    .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
    .filter = filter,
  };
  if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
    perror("prctl(PR_SET_SECCOMP)");
    return 1;
  }
  return 0;
}

上述代码首先构建了一个 sock_filter 结构,然后绑定到一个 sock_fprog 结构上,以此结构地址作为 prctl 系统调用的最后一个参数将规则注入到当前程序上。
prctl 系统调用最终调用到 seccomp_attach_filter函数将加载后的规则 attach 到进程上,核心代码如下:

	filter->prev = current->seccomp.filter;
	current->seccomp.filter = filter;

bpf 代码的校验与翻译在哪里做?校验有哪些条件?jit 在哪个步骤进行?

在 prctl attach bpf 规则到进程的过程中,seccomp_prepare_user_filter 负责从用户空间拷贝数据,然后调用 seccomp_prepare_filter 构建必要的数据结构,调用 bpf 模块的代码加载一个 bpf_prog。

bpf_prog_create_from_user bpf 函数完成 bpf 代码的校验翻译,校验可从业务逻辑上分为如下两部分:

  1. bpf 模块自身的校验
  2. 引用 bpf 代码模块的校验

seccomp 的实现中,第二部分由 seccomp_check_filter函数负责,阅读此函数代码发现它不只有校验的功能,还有一些修正的逻辑以保证 bpf 规则正常工作,这部分逻辑修改了 bpf 规则,需要在翻译之前执行。

jit 在每架构实现函数 bpf_int_jit_compile 中进行,翻译成功后生成的代码入口会被设置到 prog 结构的 bpf_func 变量中,jited 变量也被设置为 1 表示 jit 成功,相关代码如下:

   	prog->bpf_func = (void *)image;
		prog->jited = 1;

需要注意的是在 x86 架构上,bpf_jit_compile 函数是一个空函数,实际的翻译过程是在 bpf_migrate_filter 函数中完成的。

上述描述过程部分调用函数栈如下:

bpf_prepare_filter
	bpf_check_classic
	  bpf_migrate_filter(fp);
		bpf_prog_select_runtime
			bpf_prog_select_func(fp);
			    bpf_int_jit_compile(fp)-- 不同架构实现不同		

bpf 代码过滤的数据如何准备?有哪些限定?为什么要添加这些限定?

seccomp 规则的过滤数据定义如下:

struct seccomp_data {
	int nr;
	__u32 arch;
	__u64 instruction_pointer;
	__u64 args[6];
};

上述参数均与特定的架构绑定,x86、x64 架构中在 syscall_trace_enter 函数的如下代码位置进行填充:

	if (work & _TIF_SECCOMP) {
		struct seccomp_data sd;

		sd.arch = arch;
		sd.nr = regs->orig_ax;
		sd.instruction_pointer = regs->ip;
#ifdef CONFIG_X86_64
		if (arch == AUDIT_ARCH_X86_64) {
			sd.args[0] = regs->di;
			sd.args[1] = regs->si;
			sd.args[2] = regs->dx;
			sd.args[3] = regs->r10;
			sd.args[4] = regs->r8;
			sd.args[5] = regs->r9;
		} else
#endif
		{
			sd.args[0] = regs->bx;
			sd.args[1] = regs->cx;
			sd.args[2] = regs->dx;
			sd.args[3] = regs->si;
			sd.args[4] = regs->di;
			sd.args[5] = regs->bp;
		}

		ret = __secure_computing(&sd);
		if (ret == -1)
			return ret;
	}

此后将生成的过滤数据作为参数传递给 __secure_computing 来完成 bpf 过滤系统调用并执行最高优先级上绑定的行为函数。

static void populate_seccomp_data(struct seccomp_data *sd)
{
	struct task_struct *task = current;
	struct pt_regs *regs = task_pt_regs(task);
	unsigned long args[6];

	sd->nr = syscall_get_nr(task, regs);
	sd->arch = syscall_get_arch();
	syscall_get_arguments(task, regs, 0, 6, args);
	sd->args[0] = args[0];
	sd->args[1] = args[1];
	sd->args[2] = args[2];
	sd->args[3] = args[3];
	sd->args[4] = args[4];
	sd->args[5] = args[5];
	sd->instruction_pointer = KSTK_EIP(task);
}

定义 bpf 规则过滤结果的行为

bpf 代码过滤有它的结果,最简单的过滤结果就是成功、失败,实际使用中它可以支持多种不同的结果,上层逻辑可以根据不同的结果执行不同的行为。
seccomp 的过滤结果有如下几种类型定义:

#define SECCOMP_RET_KILL_PROCESS 0x80000000U /* kill the process */
#define SECCOMP_RET_KILL_THREAD	 0x00000000U /* kill the thread */
#define SECCOMP_RET_KILL	 SECCOMP_RET_KILL_THREAD
#define SECCOMP_RET_TRAP	 0x00030000U /* disallow and force a SIGSYS */
#define SECCOMP_RET_ERRNO	 0x00050000U /* returns an errno */
#define SECCOMP_RET_USER_NOTIF	 0x7fc00000U /* notifies userspace */
#define SECCOMP_RET_TRACE	 0x7ff00000U /* pass to a tracer or disallow */
#define SECCOMP_RET_LOG		 0x7ffc0000U /* allow after logging */
#define SECCOMP_RET_ALLOW	 0x7fff0000U /* allow */

当多个类型命中时,执行优先级最高的(数字最小的)类型,可以执行如下命令查看当前系统支持的 seccomp 过滤行为:

cat /proc/sys/kernel/seccomp/actions_avail

总结

本文从概览的角度整理了几个内核实现自定义 bpf hook 点的核心问题,以 seccomp 实现为例进行了描述。从实现层面看还是有一定的复杂度,在后面的文章中可以探讨下 bpf hook 点如何与内核已有的探针机制——tracepoint 结合起来,实现更为灵活的功能。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值