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

news/2024/7/6 4:41:10 标签: bpf, seccomp, ebpf, 过滤系统调用

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

目录

  • seccomp filter 学习内核 bpf 自定义 hook 点的设计
    • 基线信息
    • bpf hook 点如何注册到已有代码中?
    • bpf 代码如何动态 attach 到执行入口中?
    • bpf 代码的校验与翻译在哪里做?校验有哪些条件?jit 在哪个步骤进行?
    • bpf 代码过滤的数据如何准备?有哪些限定?为什么要添加这些限定?
    • 定义 bpf 规则过滤结果的行为
    • 总结

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

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

基线信息

内核源码版本
4.18

bpf_hook__12">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__61">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__87">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__114">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__177">定义 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 结合起来,实现更为灵活的功能。


http://www.niftyadmin.cn/n/311711.html

相关文章

Magic-API的部署

目录 概述简介特性 搭建创建元数据表idea新建spring-boot项目pom.xmlapplication.properties打包上传MagicAPI-0.0.1-SNAPSHOT.jar开启服务访问 magic语法 概述 简介 magic-api是一个基于Java的接口快速开发框架&#xff0c;编写接口将通过magic-api提供的UI界面完成&#xf…

浅谈一下接口工具(jmeter、postman、swagger等)

一、接口都有哪些类型&#xff1f; 接口一般分为两种&#xff1a;1.程序内部的接口 2.系统对外的接口 系统对外的接口&#xff1a;比如你要从别的网站或服务器上获取资源或信息&#xff0c;别人肯定不会把 数据库共享给你&#xff0c;他只能给你提供一个他们写好的方法来获取…

6. N 字形变换

将一个给定字符串 s 根据给定的行数 numRows &#xff0c;以从上往下、从左到右进行 Z 字形排列。 比如输入字符串为 "PAYPALISHIRING" 行数为 3 时&#xff0c;排列如下&#xff1a; P A H NA P L S I I GY I R 之后&#xff0c;你的输出需要从左往右逐…

Java【网络编程1】什么是 TCP/IP 五层模型? 数据传输的封装和分用?

文章目录 前言一、网络协议分层二、数据的网络传输1, 封装2, 分用3, 实际情况 总结 前言 &#x1f4d5;各位读者好, 我是小陈, 这是我的个人主页 &#x1f4d7;小陈还在持续努力学习编程, 努力通过博客输出所学知识 &#x1f4d8;如果本篇对你有帮助, 烦请点赞关注支持一波, 感…

RobotStudio教程:ABB机器人TCP路径轨迹跟踪功能介绍与使用方法

目录 功能介绍 机器人工作站创建 TCP路径轨迹全局跟踪 基于事件管理器的TCP路径轨迹局部跟踪 基于Smart组件的TCP路径轨迹局部跟踪 仿真运行 功能介绍 干涉检查是虚拟仿真工作中非常重要的一个步骤&#xff0c;尤其是机器人工具与工件、工装夹具之间的碰撞干涉&#xff…

如何将Xerosploit移植到MacOS

0x01 前言 笔者很喜欢Xerosploit这个工具 奈何其并不支持MacOS 也就意味着日常使用都需要到虚拟机里面去运行 很是麻烦 仔细看了一下Xero的代码 主要集成bettercap和nmap这两个项目&#xff08;都支持MacOS&#xff09;其代码并不复杂 就算自己基于MacOS重写一个也不是什么难事…

AIGC—— 内容生产力革命的起点

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.AIGC 1.什么是AIGC? 2.AIGC有哪些优势与挑战 &#xff08;1&#xff0…

CentOS7 yum update y更新后黑屏解决方案

解决方法 一 可以ssh访问 因为update的时候更新了系统内核&#xff0c;导致驱动问题&#xff0c;所以会黑屏。 更改一下yum的配置即可解决: vi /etc/yum.conf#增加&#xff1a;excludecentos-release*excludekernel*如果以上问题还未解决&#xff0c;可以试试下面的方法 其…