介绍

    seccomp按照字面意思可以理解为security computing,最早是作用于网格计算,主要通过限制进程的系统调用来完成部分沙箱隔离功能。

    通过man prctl可以发现,seccomp的引入是在内核2.6.23,早期的seccomp主要限制read、write、exit以及sigreturn四个系统调用。内核3.5版本引入filter模式,将seccomp分成strict和filter两种。其中strict依旧限制四种系统调用,而filter模式则借助了Berkeley Pakcet Filter来进行更细致的指令和参数控制。

 

STRICT模式

    我们可以查阅2.6.32内核代码来了解strict模式下的系统调用控制是如何实现的。

设置

    seccomp是通过prctl系统调用来进行设置的。

    具体使用: prctl(PR_SET_SECCOMP, 1,  0, 0, 0),第二个参数设置为1即可。

    通过sys_prctl进行追踪,在prctl_set_seccomp函数中,将当前进程结构(struct task_struct结构)中的seccomp中的mode标记设置为1,并将thread_info中flag设置为TIF_SECCOMP。

    我们可以由此猜测,只要将对应的进程task_struct结构中的对应标记mode置位,则可以进行进程系统调用限制。程序可通过prctl给自己做限制,如果想限制其他程序,则需要在自己制作的测试驱动中,通过对不同的测试程序设置mode,来测试该功能,参考如下:

read_lock(&tasklck);
for_each_process(p) {
    if (p->pid == seccomp_pid) {
        p_pid_address = &(p->seccomp.mode);
        ori_seccomp_mode = *p_pid_address;
        *p_pid_address = 1;
        break;
    }
 }
read_unlock(&tasklck);

 

拦截

    每个系统调用都会陷入系统调用控制函数syscall_trace_enter中,该函数中调用了secure_computing,系统调用号作为参数传递。

    在__secure_computing函数中,通过current->seccomp.mode提取标志位mode,如果mode置1,则说明当前进程已经设置了seccomp限制,通过系统调用号与__NR_read、__NR_write 、__NR_exit和__NR_sigreturn等四个系统调用值进行比较,判断当前系统调用是否被允许。如果系统调用是被允许的,则直接reurn返回,如果是其他的系统调用则调用do_exit(SIGKILL)退出。

 

FILTER模式

    我们可以查阅3.10版本的内核代码来了解filter模式下系统调用是如何进行控制的。

设置

    seccomp是通过prctl系统调用来进行设置的,其中第一个参数是PR_SET_SECCOMP,第二个参数为非0值,从SECCOMP_MODE_STRICT(严格模式,值为1)、SECCOMP_MODE_FILTER(指令过滤,值为2),第三个参数为struct sock_fprog结构指针类型的数据。

    在sys_prctl中,调用了prctl_set_seccomp函数,在该函数中给current->seccomp.mode赋值为1或者2,既进程对应的task_struct结构中的mode置位了,则说明该进程所调用的系调用需要受到seccomp的限制。

    用户态传递的第三个参数是struct sock_fprog结构指针,结构如下:

    struct sock_filter 

    {

        __u16 code;

        __u8 jt;

        __u8 jf;

        __u32 k;

    }

    struct sock_fprog

    {

        unsigned short len;

        struct sock_filter *filter;

    }

    其中len为filter指令过滤块的个数,filter为指令过滤块指针,多个过滤块内存连续。内核在seccomp_attach_filter函数中,将第三个参数的值赋值到了filter中(struct seccomp_filter结构体指针类型),并将filter的prev指针指向原有的current->seccomp.filter,将current->seccomp.filter更新为新的filter指针,此时新的指令过滤结构就串联到了对应的task_struct单向链表上。

    struct seccomp_filter

    {

        struct seccomp_filter *prev;   //通过prev节点将seccomp_filter结构串联成链表

        unsigned short len; // 当前seccomp_filter的指令的数量

        struct sock_filter insns[]; 

    }

    在seccomp_attach_filter中,分别调用了sk_chk_filter和seccomp_check_filter。

    此处只针对seccomp的业务逻辑进行简单描述,在sk_chk_filter中做了指令码的转换,如将BPF_LD + BPF_W + BPF_ABS转换成BPF_S_LD_W_ABS。

    在seccomp_check_filter函数中,也对一些seccomp模块需要用到的指令做了转换,用来区seccomp所用到的指令。比如在使用prctl的filter模式时,构建的指令码为BPF_LD + BPF_W + BPF_ABS,而在sk_chk_filter中可以转换为BPF_S_LD_W_ABS,此时为了区分seccomp和网络数据包的指令,则将code修改为BPF_S_ANC_SECCOMP_LD_W,作为seccomp的特有指令。

    从内核代码中发现一个需要注意的地方,prctl生效有个前提条件是,当前进程的no_new_privs已经设置了或者当前进程在自己的命名空间中拥有CAP_SYS_ADMIN能力。

拦截

    每个系统调用都会陷入系统调用控制函数syscall_trace_enter中,该函数中调用了secure_computing,系统调用号作为参数传递。

    在__secure_computing函数中,通过current->seccomp.mode提取标志位mode,如果mode为1或者2,则说明当前进程已经设置了seccomp限制。

    如果为1使用STRICT模式,通过系统调用号与__NR_read、__NR_write 、__NR_exit和__NR_sigreturn等四个系统调用值进行比较,判断当前系统调用是否被允许。如果系统调用是被允许的,则直接reurn返回,如果是其他的系统调用则调用do_exit(SIGKILL)退出(向当前进程发送SIGKILL信号)。

    如果值为2则使用了FILTER模式,则调用seccomp_run_filters函数来进行所有指令判断过滤,系统调用号作为参数传递,根据返回值来进行后续处理。

  • 当返回值为SECCOMP_RET_ALLOW时允许系统调用执行。
  • 当返回值为SECCOMP_RET_KILL时程序会收到SIGSYS信号,并立即退出。
  • 当返回值是SECCOMP_RET_ERRNO时会返回系统调用错误信息,并赋值errno。
  • 当返回值是SECCOMP_RET_TRAP时,当前运行进程也会收到SIGSYS信号,不过可以在进程中捕获信号。
  • 当返回值是SECCOMP_RET_TRACE时,可以给ptrace等调试程序一个控制机会。

    在seccomp_run_filters函数中,通过遍历current->seccomp.filter链表,并调用sk_run_filter函数处理每一个节点中的指令数组insns。函数sk_run_filter中有针对不同的指令的处理流程,可根据具体的业务需求再做仔细查看。

    接下来以禁用write系统调用为例,通过一个应用层例子程序来做个简单的指令解说。

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <signal.h>
  4 #include <sys/prctl.h>
  5 #include <linux/seccomp.h>
  6 #include <linux/filter.h>
  7 
  8 int main(int argc, char **argv)
  9 {
 10     struct sock_filter filter[] = {
 11         // 提取系统调用号,赋值给A
 12         {BPF_LD+BPF_ABS+BPF_W, 0, 0, 0},
 13         // 将A与k进行比较,让k等于__NR_write,既对__NR_write进行过滤
 14         // 通过查看/usr/include/asm/unistd_64.h,返现__NR_write值为1
 15         // fentry += (A == K) ? fentry->jt : fentry->jf;
 16         // 让jt等于0,则命中write系统调用时不跳转,既返回KILL.
 17         // 让jf等于1,则没有命中时进行跳转,既返回ALLOW.
 18         {BPF_JMP+BPF_JEQ, 0, 1, 1},
 19         // 返回k,让k等于SECCOMP_RET_KILL,既返回KILL
 20         {BPF_RET+BPF_K, 0, 0, SECCOMP_RET_KILL},
 21         // 返回k,让k等于SECCOMP_RET_ALLOW,既返回允许
 22         {BPF_RET+BPF_K, 0, 0, SECCOMP_RET_ALLOW},
 23     };
 24     struct sock_fprog prog = {
 25         .len = (sizeof(filter)/sizeof(struct sock_filter)),
 26         .filter = filter
 27     };
 28     prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
 29     write(1, "hello world\n", sizeof("hello world\n") - 1);
 30 
 31     return 0;
 32 }

    上述代码中,提取系统调用号使用的指令是 BPF_LD + BPF_W + BPF_ABS组合,在seccomp_check_filter中该指令码修改为BPF_S_ANC_SECCOMP_LD_W,当发现BPF_S_ANC_SECCOMP_LD_W指令时,会调用seccomp_bpf_load函数,在函数中根据传递的k值来提取不同的信息。提取系统调用号时给k赋的值为struct seccomp_data数据结构中对应元素的偏移值。

    struct seccomp_data

    {

        int nr;

        __u32 arch;

        __u64 instruction_pointer;

        __u64 args[6];

    }

    其中nr代表系统调用号,偏移值为0,则k应该赋值为0。

    上述测试的例子程序,是在root用户下运行的,既拥有CAP_SYS_ADMIN能力,否则还需要先进行prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)设置。

 

 

 

 

 

 

Logo

更多推荐