seccomp
代表secure computing
,是早在2.6.12
版本就引入到内核的特性,用来限制进程可以使用的系统调用
。它作用于进程里的线程(task
)。
最初,seccomp
只允许使用read
, write
, _exit
, sigreturn
4个系统调用,调用其他系统调用时,内核会发送SIGKILL
信号终止进程。当时seccomp
的提出主要是想用于出租空闲的CPU算力。这种模式叫做STRICT
模式。它限制过于严格,在实际应用上并没有太多发展。Linus Torvald
甚至建议把它从内核中砍掉。
下面通过实例展示一下STRICT
模式的seccomp
机制:
strict.c
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <unistd.h> #include <sys/prctl.h> #include <linux/seccomp.h> #include <fcntl.h> int main (int argc, char **argv) { open("/dev/null" , O_RDONLY); #define MESSAGE1 "open called\n" write(STDOUT_FILENO, MESSAGE1, sizeof (MESSAGE1)); prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT); open("/dev/null" , O_RDONLY); #define MESSAGE2 "You can't see this message\n" write(STDOUT_FILENO, MESSAGE2, sizeof (MESSAGE2)); return 0 ; }
程序的执行结果是:
1 2 3 [root@t8 seccomp] open called Killed
可以看到第二个write
调用没有执行到,因为在它之上的open
调用执行时,内核终止了这个进程。我们代码里不使用printf
而是使用write
输出,是因为printf
实现本身可能还会调用write
之外其他的系统调用。
沉寂了一些年之后,在3.5
版本的内核中引入一种新的seccomp
模式。它基于BPF
来过滤系统调用,这种模式叫做SECCOMP_MODE_FILTER
。这种模式下,可以自定义被允许使用的系统调用
,而自定义过滤规则是借由BPF
语言来实现。因而这种模式也叫做Seccomp-BPF
。
过滤规则仍旧使用BPF
的struct socket_filter
结构来表示,但匹配的内容却是系统调用
号和参数内容,但是过滤程序不能解引用指针(dereference pointer
),去匹配指针指向的内容。BPF
程序可以不同的返回值,指示内核进行不同的处理逻辑,如:
SECCOMP_RET_KILL
: 立即终止进程
SECCOMP_RET_TRAP
: 发送一个可捕获的SIGSYS
SECCOMP_RET_ERROR
: 指定errno
的值并返回
SECCOMP_RET_TRACE
: 由被附加的ptrace tracer
裁决
SECCOMP_RET_ALLOW
: 允许这个系统调用继续
随着内核发展,返回值也在变化,5.17
版本上已经有更多的返回值,可以参考内核文档 。
对于同一个系统调用
可以加载多个过滤器。这种场景下,系统调用
的裁决结果以最高优先级的返回值为准,返回值优先级也可以参考不同版本内核的上述文档。
BPF
语言本身提供了一套指令集来实现过滤功能。可以直接基于BPF
指令和内核定义的宏来编写过滤程序。BPF
的指令规范可以参考这里 。
seccomp-BPF
模式的使用流程是这样的: 1、以struct socket_filter
的数组承载过滤规则 2、以struct sock_fprog
结构来封装上述过滤规则 3、使用prctl
系统调用加载上述struct sock_fprog
而BPF
程序的输入是struct seccomp_data
结构:
1 2 3 4 5 6 struct seccomp_data { int nr; __u32 arch; __u64 instruction_pointer; __u64 args[6 ]; };
下面用内核定义的BPF
宏来展示Seccomp-BPF
模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <stdio.h> #include <unistd.h> #include <sys/prctl.h> #include <linux/seccomp.h> #include <linux/filter.h> #include <sys/syscall.h> #include <stdlib.h> #include <stddef.h> int main (int argc, char **argv) { int ret; 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_read, 0 , 1 ), BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_write, 0 , 1 ), BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_exit, 0 , 1 ), BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_exit_group, 0 , 1 ), BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_rt_sigreturn, 0 , 1 ), 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, }; ret = prctl(PR_SET_NO_NEW_PRIVS, 1 , 0 , 0 , 0 ); if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) { printf ("prctl failed\n" ); exit (1 ); } #define MESSAGE1 "Filter loaded\n" write(STDOUT_FILENO, MESSAGE1, sizeof (MESSAGE1)); ret = dup2(1 , 2 ); #define MESSAGE2 "dup2 called\n" write(STDOUT_FILENO, MESSAGE2, sizeof (MESSAGE2)); _exit(0 ); return 0 ; }
我们的过滤程序从seccomp_data
结构中读取nr
字段的值,装载到寄存器中,然后进行一系列的系统调用
号的匹配和跳转操作。如果任何一个系统调用号都没有匹配到,则返回SECCOMP_RET_KILL
, 内核将终止进程。
加载BPF
过滤器,需要调用线程有CAP_SYS_ADMIN
权限,或者将no_new_priv
置位,这可以通过prctl
实现:
1 prctl(PR_SET_NO_NEW_PRIVS, 1);
运行结果如下:
1 2 3 [root@t8 seccomp] Filter loaded Bad system call (core dumped)
我们代码中注释掉了允许dup2
调用的两行程序,当执行到dup2
时,进程将被终止, 因而最后的”dup called”不会输出。
将注释掉的两行代码放开后,执行结果如下:
1 2 3 [root@t8 seccomp] Filter loaded dup2 called
这种方式开发效率非常低下,可以使用libseccomp
这种更高阶的API
库,具体参考官网 。
我们使用libseccomp
来展示示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <unistd.h> #include <seccomp.h> int main (int agrc, char **argv) { scmp_filter_ctx ctx; ctx = seccomp_init(SCMP_ACT_KILL); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigreturn), 0 ); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit ), 0 ); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0 ); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0 ); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(dup2), 2 , SCMP_A0(SCMP_CMP_EQ, 1 ), SCMP_A1(SCMP_CMP_EQ, 2 )); seccomp_load(ctx); #define MESSAGE1 "Filter loaded\n" write(STDOUT_FILENO, MESSAGE1, sizeof (MESSAGE1)); dup2(1 , 2 ); #define MESSAGE2 "Dup2 succeeded\n" write(STDOUT_FILENO, MESSAGE2, sizeof (MESSAGE2)); dup2(2 , 42 ); #define MESSAGE3 "You can't see this message\n" write(STDOUT_FILENO, MESSAGE3, sizeof (MESSAGE3)); return 0 ; }
scmp_filter_ctx
是过滤逻辑所使用的上下文结构,seccomp_init
函数对上下文结构体进行初始化,若参数为SCMP_ACT_ALLOW
, 则过滤为黑名单模式;若为SCMP_ACT_KILL
,则为白名单模式,即进程调用没有匹配到规则的系统调用都会杀死,默认不允许所有的系统调用。seccomp_rule_add
函数用来添加规则,seccomp_load
函数加载过滤器。
我们允许read
,write
, sig_return
, exit
4个系统调用
。对于dup2
调用,我们还会根据传入的参数来进行判断。只有传入的两个参数为1
和2
被允许,即:
编译程序需要链接libseccomp
:
1 gcc seccomp_filter.c -lseccomp -o seccomp_filter
执行结果:
1 2 3 4 [root@t8 seccomp] Filter loaded Dup2 succeeded Bad system call (core dumped)
dup2(1, 2)
成功执行,而后边的dup2(2, 42)
没有成功执行,进程被终止。
之后,在5.0
版本内核又加入了seccomp-unotify
机制,5.9
版本又做了特性增强。seccomp-BPF
模式对系统调用
的裁决是由过滤程序自己完成的,而seccomp-unotify
机制能够将裁决权转移给另一个用户态进程。这个文章 对这个特性介绍的非常详细。
我们将加载过滤程序的进程叫做target
, 接收通知的进程叫做supervisor
。在这个模式中,supervisor
不仅对是否允许系统调用
能够做出裁决,它还可以代替target
进程完成这个系统调用
的行为。这大大扩大了seccomp
机制的应用范围。上边我们介绍过,Seccomp-BPF
模式只能检测系统调用的参数,不能解引用指针。而这个unotify
模式则还可以去查看指针所指向的内存。
具体的使用方式可以参考这里 。
我们再来展示一个示例。target
进程代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/prctl.h> #include <linux/seccomp.h> #include <linux/filter.h> #include <sys/syscall.h> #include <stdlib.h> #include <stddef.h> #include <errno.h> #include <string.h> #include <fcntl.h> int main (int argc, char **argv) { int ret; int notifyfd, fd; if (argc != 2 ) { printf ("usage: %s <file path>\n" , argv[0 ]); exit (-1 ); } 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_open, 0 , 1 ), BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_USER_NOTIF), BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_openat, 0 , 1 ), BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_USER_NOTIF), BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), }; struct sock_fprog prog = { .len = (unsigned short )(sizeof (filter)/sizeof (filter[0 ])), .filter = filter, }; ret = prctl(PR_SET_NO_NEW_PRIVS, 1 , 0 , 0 , 0 ); notifyfd = syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, &prog); if (notifyfd < 0 ) { printf ("seccomp failed: %s\n" , strerror(errno)); exit (-1 ); } printf ("tid: %d, notify fd: %d\n" , syscall(SYS_gettid), notifyfd); fd = open(argv[1 ], O_CREAT|O_RDWR); if (fd < 0 ) { printf ("open failed: %s\n" , strerror(errno)); exit (-1 ); } else { printf ("open succeeded\n" ); } close(fd); close(notifyfd); return 0 ; }
target
进程使用seccomp
系统调用来加载过滤程序,获得一个seccomp-unotify
的fd
, 接着将线程ID
和通知fd
输出。接下来调用open
系统调用。我们的BPF
程序里对于open
和openat
返回SECCOMP_RET_USER_NOTIF
,因而执行到这里时,程序将会阻塞。
再来看supervisor
进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 #include <stdio.h> #include <sys/syscall.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <linux/limits.h> #include <linux/seccomp.h> #include <sys/ioctl.h> #include <assert.h> #include <fcntl.h> static void process_notifications (int notifyfd) { __u64 id; struct seccomp_notif_sizes sizes ; struct seccomp_notif *req ; struct seccomp_notif_resp *resp ; char path[PATH_MAX]; int memfd; ssize_t s; if (syscall(SYS_seccomp, SECCOMP_GET_NOTIF_SIZES, 0 , &sizes) == -1 ) { printf ("seccomp failed: %s" , strerror(errno)); exit (-1 ); } assert((req = malloc (sizes.seccomp_notif))); assert((resp = malloc (sizes.seccomp_notif_resp))); memset (req, 0 , sizes.seccomp_notif); memset (resp, 0 , sizes.seccomp_notif_resp); if (ioctl(notifyfd, SECCOMP_IOCTL_NOTIF_RECV, req) == -1 ) { printf ("ioctl failed: %s\n" , strerror(errno)); exit (-1 ); } printf ("Got notification for PID: %d, id is %llx\n" , req->pid, req->id); id = req->id; if (ioctl(notifyfd, SECCOMP_IOCTL_NOTIF_ID_VALID, &id) == -1 ) { printf ("Notification ID check: target has died: %s\n" , strerror(errno)); exit (-1 ); } snprintf (path, sizeof (path), "/proc/%d/mem" , req->pid); memfd = open(path, O_RDONLY); if (memfd < 0 ) { printf ("open mem file failed: %s\n" , path); exit (-1 ); } printf ("SYSCALL: %d\n" , req->data.nr); if (req->data.nr == SYS_open) { assert(lseek(memfd, req->data.args[0 ], SEEK_SET) >= 0 ); } else if (req->data.nr == SYS_openat) { printf ("memory address: 0x%016X\n" , req->data.args[1 ]); assert(lseek(memfd, req->data.args[1 ], SEEK_SET) >= 0 ); } assert((s = read(memfd, path, sizeof (path))) > 0 ); printf ("open path: %s\n" , path); close(memfd); if (strlen (path) == strlen ("/tmp/noopen.txt" ) && strncmp (path, "/tmp/noopen.txt" , strlen ("/tmp/noopen.txt" )) == 0 ) { printf ("Denied\n" ); resp->error = -EPERM; resp->flags = 0 ; } else { printf ("Allowed\n" ); resp->error = 0 ; resp->flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE; } resp->id = req->id; if (ioctl(notifyfd, SECCOMP_IOCTL_NOTIF_SEND, resp) == -1 ) { if (errno == ENOENT) { printf ("Response failed with ENOENT; perhaps target " "process's syscall was interrupted by signal?\n" ); } else { printf ("ioctl failed: %s\n" , strerror(errno)); exit (-1 ); } } free (req); free (resp); } int main (int argc, char **argv) { int pidfd; int notifyfd, targetfd; pid_t pid; if (argc != 3 ) { printf ("usage: %s <pid> <target fd>\n" , argv[0 ]); exit (-1 ); } pid = atoi(argv[1 ]); targetfd = atoi(argv[2 ]); printf ("PID: %d, TARGET FD: %d\n" , pid, targetfd); pidfd = syscall(SYS_pidfd_open, pid, 0 ); assert(pidfd >= 0 ); printf ("PIDFD: %d\n" , pidfd); notifyfd = syscall(SYS_pidfd_getfd, pidfd, targetfd, 0 ); assert(notifyfd >= 0 ); printf ("NOTIFY FD: %d\n" , notifyfd); process_notifications(notifyfd); return 0 ; }
supervisor
进程能过pidfd_getfd
从target
进程获取到seccomp-unotify
的fd
, 从fd
中获取到系统调用的调用通知。通过/proc/[pid]/mem
文件获取到open
调用传入的指针所指向的文件名。当文件名为/tmp/noopen.txt
时,open
被禁止,其他文件名则允许执行。
我们先执行target
进程,可以看到输出的PID
和FD
, 同时进程被阻塞:
1 2 3 [root@t8 seccomp] tid: 9449, notify fd: 3
然后我们在另一个终端执行supervisor
:
1 2 3 4 5 6 7 8 9 [root@t8 seccomp] PID: 9449, TARGET FD: 3 PIDFD: 3 NOTIFY FD: 4 Got notification for PID: 9449, id is 5928d2192e65a0b5 SYSCALL: 257 memory address: 0x000000008FB87785 open path: /tmp/noopen.txt Denied
可以看到open
被禁止,而另一个终端上target
进程也返回:
1 2 3 [root@t8 seccomp] tid: 9449, notify fd: 3 open failed: Operation not permitted
我们再以非/tmp/noopen.txt
的文件来运行一次target
:
1 2 3 [root@t8 seccomp] tid: 9451, notify fd: 3
程序正常阻塞,然后运行supervisor
:
1 2 3 4 5 6 7 8 9 [root@t8 seccomp] PID: 9451, TARGET FD: 3 PIDFD: 3 NOTIFY FD: 4 Got notification for PID: 9451, id is 211503f5ffd2ddc3 SYSCALL: 257 memory address: 0x000000007E3EF786 open path: /tmp/dummy.txt Allowed
open
调用被允许执行,这时target
进程也返回:
1 2 3 [root@t8 seccomp] tid: 9451, notify fd: 3 open succeeded
查看创建的/tmp/dummy.txt
可以看到文件被成功创建了:
1 2 [root@t8 seccomp] ---sr-----. 1 root root 0 Mar 29 08:01 /tmp/dummy.txt
seccomp-unotify
当前在容器环境里有比较大的使用空间,而且还在不断的发展之中,后续有新的进展,再来单独介绍。