Seccomp BPF 详细解析

seccomp简介

Secommp (SECure COMPuting) 是 Linux 内核 2.6.12 版本引入的安全模块,主要是用来限制某一进程可用的系统调用 (system call)。它最初被用于 cpushare 这个项目,让人们可以出租自己空闲的 cpu cycle 来执行 untrusted code。这个 feature 本身并不是一个沙盒 (sandbox),它只是一种减少 Linux 内核暴露的机制,是构建一个安全的沙盒的重要组成部分。

seccomp的几种模式:
strict mode:这是最严格的模式,只允许进程调用指定的系统调用。如果进程尝试调用其他系统调用,则会导致进程被终止。
filter mode:这是一种类似于白名单的模式,允许指定哪些系统调用是允许的,哪些是禁止的。如果进程尝试调用未授权的系统调用,则会由进程自己来处理。
log mode:这种模式与filter模式类似,但是当进程尝试调用未授权的系统调用时,内核会记录一条日志以供审计。
notify mode:这种模式与log模式类似,但是当进程尝试调用未授权的系统调用时,内核会向进程发送一个SIGSYS信号,由进程自行处理。

Seccomp BPF

Seccomp BPF (Berkeley Packet Filter) 是 Linux 内核提供的一种功能,用于限制进程能够进行的系统调用。它允许进程在运行时安装一个 BPF 过滤器程序来控制进程的系统调用,从而提高整个系统的安全性。Seccomp BPF 过滤器使用类似于 Berkeley Packet Filter 的语言来描述规则,针对特定的系统调用进行过滤,可以对系统调用的参数进行检查和修改。与传统的系统调用过滤方式相比,Seccomp BPF 过滤器工作在内核空间中,因此可以减少对系统资源的占用,同时过滤器程序本身也更容易编写和维护

BPF定义了一个可以在内核内实现的虚拟机(VM)。

BPF程序

BPF 程序在Linux内核中主要在filter.h和bpf_common.h中实现,主要的数据结构包括以下几个:
Linux v5.18.4/include/uapi/linux/filte.h -> sock_fprog

struct sock_fprog {    /* Required for SO_ATTACH_FILTER. */
    unsigned short        len;    /* BPF指令的数量 */
    struct sock_filter __user *filter;  /*指向BPF数组的指针 */
};

这个结构体记录了过滤规则个数与规则数组起始位置 , 而 filter 域指向了具体的规则,每一条规则的形式如下:

Linux v5.18.4/include/uapi/linux/filte.h -> sock_filter

struct sock_filter {    /* Filter block */
    __u16    code;   /* 过滤指令 */
    __u8    jt;    /* 为真时跳转 */
    __u8    jf;    /* 为假时跳转 */
    __u32    k;      /* 被操作值 */
};

BPF指令可以手工编写,但是,开发人员定义了符号常量和两个方便的宏BPF_STMT和BPF_JUMP可以用来方便的编写BPF规则。
Linux v5.18.4/include/uapi/linux/filte.h -> BPF_STMT&BPF_JUMP

/*
 * Macros for filter block array initializers.
 */
#ifndef BPF_STMT
#define BPF_STMT(code, k) { (unsigned short)(code), 0, 0, k }
#endif
#ifndef BPF_JUMP
#define BPF_JUMP(code, k, jt, jf) { (unsigned short)(code), jt, jf, k }
#endif
BPF操作码

bpf_common.h

指令类型操作:
BPF_LD:读取操作
BPF_LDX:扩展读取操作
BPF_ST:存储操作
BPF_STX:扩展存储操作
BPF_ALU:算数逻辑操作
BPF_JMP:跳转操作
BPF_RET:返回操作
BPF_MISC:其它操作

数据大小定义
BPF_W:32位
BPF_H:16位
BPF_B:8位
BPF_DW:64位(仅 eBPF)

模式定义
BPF_IMM:立即数模式
BPF_ABS:绝对寻址模式
BPF_IND:间接寻址模式
BPF_MEM:内存寻址模式
BPF_LEN:数据长度模式
BPF_MSH:数据位移模式

算数逻辑操作
BPF_ADD:加法
BPF_SUB:减法
BPF_MUL:乘法
BPF_DIV:除法
BPF_OR:按位或
BPF_AND:按位与
BPF_LSH:左移
BPF_RSH:右移
BPF_NEG:取反
BPF_MOD:取模
BPF_XOR:异或

跳转操作
BPF_JA:无条件跳转
BPF_JEQ:相等跳转
BPF_JGT:大于跳转
BPF_JGE:大于等于跳转
BPF_JSET:位集合跳转

源定义:
BPF_K:立即数源
BPF_X:寄存器源

操作数可以通过或(|)或者加(+)操作进行组合

BPF_STMT

BPF_STMT
BPF_STMT有两个参数,操作码(code)和值(k),举个例子:

BPF_STMT(BPF_LD | BPF_W | BPF_ABS,(offsetof(struct seccomp_data, arch)))

这里的操作码是由三个指令相或组成的,BPF_LD: 建一个 BPF 加载操作 ;BPF_W:操作数大小是一个字,BPF_ABS: 使用绝对偏移,即使用指令中的值作为数据区的偏移量,该值是体系结构字段与数据区域的偏移量 。offsetof()生成数据区域中期望字段的偏移量。
该指令的功能是将体系架构数加载到累加器中。

BPF_JUMP

BPF_JUMP 中有四个参数:操作码、值(k)、为真跳转(jt)和为假跳转(jf),举个例子:
这里的跳转地址是相对地址,也就是说,是相对BPF的pc值的地址,要注意之前说过,BPF定义了一个可以在内核内实现的虚拟机

BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K ,AUDIT_ARCH_X86_64 , 1, 0)

BPF_JMP | BPF JEQ会创建一个相等跳转指令,它将指令中的值(即第二个参数AUDIT_ARCH_X86_64)与累加器中的值(BPF_K)进行比较。判断是否相等,也就是说,如果架构是 x86-64,则跳过下一条指令(jt=1,代表测试为真跳过一条指令),否则将执行下一条指令(jf=0,代表如果测试为假,则跳过0条指令,也就是继续执行下一条指令)。

BPF相关数据

一旦为程序配置了seccomp-BPF,每个系统调用都会经过seccomp过滤器,这在一定程度上会影响系统的性能。此外,Seccomp过滤器会向内核返回一个值,指示是否允许该系统调用,该返回值是一个 32 位的数值,其中最重要的 16 位(SECCOMP_RET_ACTION掩码)指定内核应该采取的操作,其他位(SECCOMP_RET_DATA 掩码)用于返回与操作关联的数据 。
seccomp.h中定义了返还值类型,

SECCOMP_RET_ALLOW:允许系统调用通过。
SECCOMP_RET_KILL_PROCESS:杀死整个进程,即结束进程。
SECCOMP_RET_KILL_THREAD:杀死线程,即终止当前线程。
SECCOMP_RET_KILL:与 SECCOMP_RET_KILL_THREAD 含义相同,只是别名。
SECCOMP_RET_TRAP:禁止并强制引发 SIGSYS 信号(与 SIGILL、SIGABRT 类似)。
SECCOMP_RET_ERRNO:返回一个 errno 错误码。
SECCOMP_RET_USER_NOTIF:通知用户空间的监听程序,即向用户态传递信息。
SECCOMP_RET_TRACE:不允许并将事件传递给跟踪器(tracee)。
SECCOMP_RET_LOG:记录事件到内核日志中。

每一个seccomp-BPF程序都使用seccomp_data结构作为输入参数,在seccomp.h中定义该输入参数

struct seccomp_data {
  int nr ;                    /* 系统调用号(依赖于体系架构) */
  __u32 arch ;                /* 架构(如AUDIT_ARCH_X86_64) */
  __u64 instruction_pointer ; /* CPU指令指针 */
  __u64 args [6];             /* 系统调用参数,最多有6个参数 */
};

Seccomp BPF使用

在介绍使用流程之前,需要先介绍Prctl()

Prctl
#include <sys/prctl.h>

int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);

与seccomp有关的option主要有两个: PR_SET_NO_NEW_PRIVS()和PR_SET_SECCOMP()。

  • PR_SET_NO_NEW_PRIVS():是在Linux 3.5 之后引入的特性,当一个进程或者子进程设置了PR_SET_NO_NEW_PRIVS 属性,则其不能访问一些无法共享的操作,如setuid、chroot等。配置seccomp-BPF的程序必须拥有Capabilities 中 的CAP_SYS_ADMIN,或者程序已经定义了no_new_privs属性。 若不这样做 非 root 用户使用该程序时 seccomp保护将会失效,设置了 PR_SET_NO_NEW_PRIVS 位后能保证 seccomp 对所有用户都能起作用

    prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
    

    如果将其第二个参数设置为1,则这个操作能保证seccomp对所有用户都能起作用,并且会使子进程即execve后的进程依然受到seccomp的限制。

  • R_SET_SECCOMP(): 为进程设置seccomp; 通常的形式如下

    prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);
    

    SECCOMP_MODE_FILTER参数表示设置的seccomp的过滤模式,如果设置为SECCOMP_MODE_STRICT,则代表严格模式;若为过滤模式,则对应的系统调用限制通过&prog结构体定义(上面提到过的 struct sock_fprog)。

Seccomp的模式

一般有严格模式和过滤模式两种,严格模式实用性不强,主要用的是过滤模式

Seccomp过滤模式

执行流程
在这里插入图片描述
代码框架:

	struct sock_filter filter[] = {
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr))),
        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_fork, 3, 0),
        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_clone, 2, 0),
        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_vfork, 1, 0),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    };
    struct sock_fprog prog = {
        .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])), // 规则条数
        .filter = filter,                                            // 结构体数组指针
    };

    prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); // 设置NO_NEW_PRIVS                
    prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog); // 加载seccomp过滤器

案例:
禁止fork系统调用产生子进程

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stddef.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/unistd.h>

void configure_seccomp()
{
    printf("Configuring seccomp\n");
    prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
}

int main()
{
    struct sock_filter filter[] = {
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr))),
        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_fork, 3, 0),
        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_clone, 2, 0),
        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_vfork, 1, 0),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    };
    struct sock_fprog prog = {
        .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])), // 规则条数
        .filter = filter,                                            // 结构体数组指针
    };

    prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); // 设置NO_NEW_PRIVS
                                            
    // 加载seccomp过滤器
    if(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) {
        perror("加载seccomp 失败");
        exit(-1);
    }
    

    pid_t pid = fork();
    if (pid < 0)
    {
        printf("Error: Failed to create child process\n");
    }
    else if (pid == 0)
    {
        // 这是子进程的代码
        printf("Hello from child process!\n");
    }
    else
    {
        // 这是父进程的代码
        printf("Hello from parent process!\n");
    }

    printf("hello");

    return 0;
}

参考

Seccomp BPF与容器安全

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值