seccomp ---如何EXIT_SUCCESS?

MCH*_*MCH 20 c linux sandbox exit-code seccomp

设置严格模式seccomp后如何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
Run Code Online (Sandbox Code Playgroud)

gsa*_*ras 12

如在解释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>
Run Code Online (Sandbox Code Playgroud)

如你所见,exit_group()被称为,解释一切!


现在你正确地说," SYS_exit equals __NR_exit"; 例如,它在mit.syscall.h中定义:

#define SYS_exit __NR_exit
Run Code Online (Sandbox Code Playgroud)

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

linux12:/home/users/grad1459>gcc seccomp.c -o seccomp && ./seccomp ; echo "${?}" 
0
Run Code Online (Sandbox Code Playgroud)

PS

您当然可以定义filter自己并使用:

prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, filter);
Run Code Online (Sandbox Code Playgroud)

正如在本征状态链接中所解释的那样,允许_exit()(或严格地说exit_group(2)),但只有在您真正需要并知道自己在做什么时才这样做.

  • 至于OP提出的问题,我会说当进程中只有一个线程时,GNU C库执行`exit_group`系统调用而不是`exit` ..是一个错误.我不喜欢通过直接调用`exit`系统调用来绕过库清理的想法.换句话说,OP*应该能够只返回EXIT_SUCCESS;`或`exit(EXIT_SUCCESS);`而不会被信号杀死.只有对C库内部的更改才会改变它.我要说,是时候报告一个glibc错误了. (2认同)

Nom*_*mal 7

出现此问题,因为GNU C库使用exit_group的系统调用,如果是可用的,在Linux的替代exit,对于_exit()功能(参见sysdeps/unix/sysv/linux/_exit.c验证),并且在记录man 2 prctl时,exit_group系统调用不被严格过滤的Seccomp允许.

因为_exit()函数调用发生在C库中,所以我们不能将它与我们自己的版本一起插入(这只会进行exit系统调用).(正常的进程清理在其他地方完成;在Linux中,该_exit()函数只执行终止进程的最终系统调用.)

我们可以要求GNU C库开发人员exit_group只在当前进程中有多个线程时才在Linux中使用syscall,但不幸的是,这并不容易,即使现在添加,也需要相当长的时间才能完成.功能在大多数Linux发行版上都可用.

幸运的是,我们可以抛弃默认的严格过滤器,而是定义我们自己的.行为有一个小的差异:杀死过程的明显信号将从SIGKILL变为SIGSYS.(信号实际上并没有传递,因为内核会杀死进程;只有导致进程死亡的明显信号数会发生变化.)

此外,这甚至不是那么困难.我确实浪费了一些时间来研究一些GCC宏诡计,这会使管理允许的系统调用列表变得微不足道,但我认为这不是一个好方法:应该仔细考虑允许的系统调用列表 - 我们只加上exit_group()相比严格的过滤器,在这里!- 所以让它有点困难是可以的.

例如,以下代码example.c已经过验证,可用于x86-64上的4.4内核(应该在内核3.5或更高版本上运行)(对于x86和x86-64,即32位 64位二进制文​​件).它应该在所有的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;
}
Run Code Online (Sandbox Code Playgroud)

使用例如编译

gcc -Wall -O2 example.c -o example
Run Code Online (Sandbox Code Playgroud)

并运行

./example
Run Code Online (Sandbox Code Playgroud)

或者strace看到完成的系统调用和库调用;

strace ./example
Run Code Online (Sandbox Code Playgroud)

strict_filterBPF程序是真是小巫见大巫.第一个操作码将系统调用号加载到累加器中.接下来的五个操作码将它与可接受的系统调用号进行比较,如果找到,则跳转到允许系统调用的最终操作码.否则,倒数第二个操作码会终止该过程.

请注意,尽管文档指的sigreturn是允许的系统调用,但Linux中的系统调用的实际名称是rt_sigreturn.(sigreturnrt_sigreturn很久以前就被弃用了.)

此外,安装过滤器时,操作码将复制到内核内存(请参阅kernel/seccomp.cLinux内核源代码中),因此如果以后修改数据,它不会以任何方式影响过滤器.static const换句话说,拥有这些结构对安全性没有影响.

我使用过,static因为不需要在这个编译单元外部(或在剥离的二进制文件中)可以看到符号,并将const数据放入ELF二进制文件的只读数据部分.

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

由于equals情况跳转到最终的操作码,您可以在顶部添加新的操作码(即,在初始操作码之后),增加每个操作码的等于跳过计数.

请注意,printf()在安装seccomp过滤器后将无法工作,因为在内部,C库需要执行fstat系统调用(在标准输出上),并且brk系统调用为缓冲区分配一些内存.

  • @Rhymoid赏金就像一个原子!:)不要担心,Nomimal是一个很酷的家伙! (2认同)