seccomp – 如何EXIT_SUCCESS?

在严格模式seccomp设置后,如何进入EXIT_SUCCESS。 调用syscall(SYS_exit, EXIT_SUCCESS);是否正确syscall(SYS_exit, EXIT_SUCCESS); 在主要结束?

 #include <stdlib.h> #include <unistd.h> #include <sys/prctl.h> #include <linux/seccomp.h> #include <sys/syscall.h> int main(int argc, char **argv) { prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT); //return EXIT_SUCCESS; // does not work //_exit(EXIT_SUCCESS); // does not work // syscall(__NR_exit, EXIT_SUCCESS); // (EDIT) This works! Is this the ultimate answer and the right way to exit success from seccomp-ed programs? syscall(SYS_exit, EXIT_SUCCESS); // (EDIT) works; SYS_exit equals __NR_exit } // gcc seccomp.c -o seccomp && ./seccomp; echo "${?}" # I want 0 

正如eigenstate.org和SECCOMP(2)所解释的那样:

read(2),write(2),_exit(2)( 但不是 exit_group(2))和sigreturn(2)是唯一允许调用线程调用的系统调用。 其他系统调用导致SIGKILL信号的传送。

因此,我们期望_exit()能够工作,但是它是一个包装函数,它调用exit_group(2) ,这是严格模式( [1] , [2] )中不允许的,因此进程被终止。

甚至在exit(2)报告- Linux手册页 :

在glibc版本2.3中, _exit()包装函数调用了同名内核系统调用。 由于glibc 2.3, 包装函数调用exit_group(2) ,以终止进程中的所有线程。

return语句也会发生同样的情况,最终会以与_exit()非常类似的方式杀死进程。

对这个过程进行分层将会提供进一步的确认(为了让这个显示出来,你不必设置PR_SET_SECCOMP;只是对prctl()注释),并且对于两个非工作的情况我都得到了类似的输出:

 linux12:/home/users/grad1459>gcc seccomp.c -o seccomp linux12:/home/users/grad1459>strace ./seccomp execve("./seccomp", ["./seccomp"], [/* 24 vars */]) = 0 brk(0) = 0x8784000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb775f000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=97472, ...}) = 0 mmap2(NULL, 97472, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7747000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\220\226\1\0004\0\0\0"..., 512) = 512 fstat64(3, {st_mode=S_IFREG|0755, st_size=1730024, ...}) = 0 mmap2(NULL, 1739484, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xdd0000 mmap2(0xf73000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a3) = 0xf73000 mmap2(0xf76000, 10972, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xf76000 close(3) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7746000 set_thread_area({entry_number:-1 -> 6, base_addr:0xb7746900, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0 mprotect(0xf73000, 8192, PROT_READ) = 0 mprotect(0x8049000, 4096, PROT_READ) = 0 mprotect(0x16e000, 4096, PROT_READ) = 0 munmap(0xb7747000, 97472) = 0 exit_group(0) = ? linux12:/home/users/grad1459> 

正如你所看到的, exit_group()被调用,解释了一切!


现在正如你所说的,“ SYS_exit equals __NR_exit ”; 例如它在mit.syscall.h中定义:

 #define SYS_exit __NR_exit 

所以最后两个调用是等价的,即你可以使用你喜欢的那个,输出应该是这样的:

 linux12:/home/users/grad1459>gcc seccomp.c -o seccomp && ./seccomp ; echo "${?}" 0 

PS

你当然可以自己定义一个filter并使用:

 prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, filter); 

如本征态链接中所解释的那样,允许_exit() (或严格来说, exit_group(2) ),但是只有当你真的需要知道你在做什么时才这样做。

问题发生了,因为GNU C库使用了exit_group syscall,如果可用的话,在Linux中而不是exit ,为_exit()函数(见sysdeps/unix/sysv/linux/_exit.c进行验证)记录在man 2 prctl ,严格的seccomp过滤器不允许exit_group syscall。

因为_exit()函数调用是在C库内部发生的,所以我们不能将其与我们自己的版本(只是执行exit syscall)进行干涉。 (正常的进程清理是在其他地方完成的;在Linux中, _exit()函数只执行终止进程的最终系统调用。)

我们可以要求GNU C库开发人员在Linux中只有当有多个线程时才使用exit_group系统调用,但不幸的是,这不是一件容易的事情,即使现在添加,也需要相当长的一段时间该功能在大多数Linux发行版上都可用。

幸运的是,我们可以抛弃默认的严格过滤器,而是定义我们自己的过滤器。 行为有一个小的差别:杀死进程的明显信号将从SIGKILL变为SIGSYS 。 (信号并不是实际传送的,因为内核确实会终止进程;只会导致进程死亡的表观信号编号发生变化。)

而且,这不是那么困难。 我浪费了一些时间来研究一些GCC宏观技巧,这将使得管理允许的系统调用列表变得微不足道,但是我认为这不是一个好方法:应该仔细考虑允许的系统调用列表 – 我们只添加exit_group()相比严格的过滤器,在这里! – 所以有点困难是可以的。

下面的代码,例如example.c ,已经被验证在x86-64(对于x86和x86-64,即32位 64位的二进制文件)上的4.4内核(应该可以在内核3.5或更高版本上工作) 。 它应该可以在所有Linux体系结构上工作,但是它不需要或不使用libseccomp库。

 #define _GNU_SOURCE #include <stdlib.h> #include <stddef.h> #include <sys/prctl.h> #include <sys/syscall.h> #include <linux/seccomp.h> #include <linux/filter.h> #include <stdio.h> static const struct sock_filter strict_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof (struct seccomp_data, nr))), BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_rt_sigreturn, 5, 0), BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_read, 4, 0), BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_write, 3, 0), BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_exit, 2, 0), BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_exit_group, 1, 0), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW) }; static const struct sock_fprog strict = { .len = (unsigned short)( sizeof strict_filter / sizeof strict_filter[0] ), .filter = (struct sock_filter *)strict_filter }; int main(void) { /* To be able to set a custom filter, we need to set the "no new privs" flag. The Documentation/prctl/no_new_privs.txt file in the Linux kernel recommends this exact form: */ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { fprintf(stderr, "Cannot set no_new_privs: %m.\n"); return EXIT_FAILURE; } if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &strict)) { fprintf(stderr, "Cannot install seccomp filter: %m.\n"); return EXIT_FAILURE; } /* The seccomp filter is now active. It differs from SECCOMP_SET_MODE_STRICT in two ways: 1. exit_group syscall is allowed; it just terminates the process 2. Parent/reaper sees SIGSYS as the killing signal instead of SIGKILL, if the process tries to do a syscall not in the explicitly allowed list */ return EXIT_SUCCESS; } 

编译使用例如

 gcc -Wall -O2 example.c -o example 

并运行使用

 ./example 

或者在strace下查看系统调用和库调用;

 strace ./example 

strict_filter BPF程序实际上是微不足道的。 第一个操作码将系统调用号码加载到累加器中。 接下来的五个操作码将其与可接受的系统调用号码进行比较,如果找到,则跳转到允许系统调用的最终操作码。 否则,倒数第二个操作码将终止进程。

请注意,虽然文档中提到sigreturn是允许的系统调用,但是Linux系统调用的实际名称是rt_sigreturn 。 ( sigreturn不赞成rt_sigreturn多年前。)

此外,安装过滤器时,操作码被复制到内核内存中(请参阅Linux内核源kernel/seccomp.c中的kernel/seccomp.c ),因此稍后修改数据时不会以任何方式影响过滤器。 换句话说,具有static const的结构没有安全影响。

我使用了static因为在这个编译单元之外(或者在一个被剥离的二进制文件中),不需要这些符号是可见的, const是把这些数据放到ELF二进制文件的只读数据部分。

BPF_JUMP(BPF_JMP | BPF_JEQ, nr, equals, differs)很简单:将累加器(系统调用号)与nr进行比较。 如果它们相等,则跳过下一个equals操作码。 否则,跳过下一个differs操作码。

由于等号跳转到最后一个操作码,所以你可以在最上面添加新的操作码(也就是在初始操作码之后),增加每个操作码的等于跳过次数。

请注意,在安装seccomp过滤器之后, printf()将不起作用,因为在内部,C库想要执行fstat syscall(在标准输出上)以及一个brk系统调用来为缓冲区分配一些内存。