seccomp --- 如何 EXIT_SUCCESS?
Posted
技术标签:
【中文标题】seccomp --- 如何 EXIT_SUCCESS?【英文标题】:seccomp --- how to EXIT_SUCCESS? 【发布时间】:2015-10-15 13:46:31 【问题描述】:设置严格模式 seccomp 后如何退出。在 main 结束时调用 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
【问题讨论】:
不能直接返回 EXIT_SUCCESS 吗? (哎呀:没关系 - 没有足够仔细地查看您的代码。) 我遇到了同样的问题,我的进程被杀死了。_exit(EXIT_SUCCESS)
不起作用很奇怪,因为手册页明确指出,在严格的 seccomp 模式下,“调用线程被允许进行的唯一系统调用是读取 (2) 、write(2)、_exit(2)(但不是 exit_group(2))和 sigreturn(2)。” (括号中的数字当然是手册部分)。
@user263688 我不认为问题出在您作为答案发布的内容(不是投票者),我发布了一个答案,如果您能看一下就好了! :)
【参考方案1】:
如eigenstate.org 和SECCOMP (2) 中所述:
调用线程被允许进行的唯一系统调用 make 是 read(2), write(2), _exit(2) (但不是 exit_group(2)), 和 sigreturn(2)。其他系统调用导致交付 SIGKILL 信号。
因此,人们期望_exit()
可以工作,但它是一个调用exit_group(2)
的包装函数,这在严格模式下是不允许的([1], [2]),因此进程被终止。
它甚至在exit(2) - Linux man page中被报道:
在直到 2.3 版的 glibc 中,_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);
如 eigenstate 链接中所述,允许 _exit()
(或者,严格来说,exit_group(2)
),但只有在您确实需要并且知道自己在做什么时才这样做。
【讨论】:
此外,return EXIT_SUCCESS;
也失败的原因是相同的:在这种情况下,GNU C 库也会执行exit_group()
。如果您有兴趣,我确实有一些用于 x86-64 SYSV ABI 的独立 C 来证明 exit
系统调用在 prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT)
调用之后可以正常工作。
在strace
(即strace ./example
)下运行二进制文件足以证明C库使用exit_group
系统调用而不是exit
。
嘿@NominalAnimal! :) 是的,好主意,其中一个链接中也提到了它,但没有显示,更新了!你喜欢现在的答案吗?
哦,我对原样的答案很满意——但是,添加strace
确实消除了任何怀疑的可能性,允许其他人自己验证。我喜欢那样。 :)
至于 OP 提出的问题,当进程中只有一个线程时,我会说 GNU C 库执行 exit_group
系统调用而不是 exit
.. 是一个错误。我不喜欢通过直接调用exit
系统调用来绕过库清理的想法。换句话说,OP 应该能够在return EXIT_SUCCESS;
或exit(EXIT_SUCCESS);
不被信号杀死。只有更改 C 库内部结构才能改变这一点。我想说,是时候报告一个 glibc 错误了。【参考方案2】:
出现问题是因为 GNU C 库使用 exit_group
系统调用(如果可用)在 Linux 中而不是 exit
中用于 _exit()
函数(请参阅 sysdeps/unix/sysv/linux/_exit.c
进行验证),并且如文档所述在man 2 prctl
中,严格的seccomp 过滤器不允许exit_group
系统调用。
因为 _exit()
函数调用发生在 C 库中,我们不能用我们自己的版本插入它(那只会执行 exit
系统调用)。 (正常的进程清理在其他地方完成;在 Linux 中,_exit()
函数只执行终止进程的最终系统调用。)
我们可以要求 GNU C 库开发人员仅在当前进程中有多个线程时才在 Linux 中使用 exit_group
系统调用,但不幸的是,这并不容易,即使现在添加,也会该功能需要很长时间才能在大多数 Linux 发行版上可用。
幸运的是,我们可以放弃默认的严格过滤器,而是定义我们自己的过滤器。行为上有一点不同:杀死进程的明显信号将从SIGKILL
变为SIGSYS
。 (实际上并没有传递信号,因为内核确实杀死了进程;只有导致进程死亡的明显信号编号发生了变化。)
此外,这甚至没有那么困难。我确实浪费了一些时间来研究一些 GCC 宏技巧,这些技巧会使管理允许的系统调用列表变得微不足道,但我认为这不是一个好方法:应该仔细考虑允许的系统调用列表——我们只与严格过滤器相比,添加exit_group()
,在这里! -- 所以让它变得有点困难是可以的。
下面的代码,比如example.c
,已经过验证可以在 x86-64(对于 x86 和 x86-64,即 32 位)的 4.4 内核(应该在 3.5 或更高版本的内核)上工作和 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;
使用例如编译
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
),因此如果以后修改数据,它不会以任何方式影响过滤器。换句话说,拥有static const
结构对安全性影响为零。
我使用了static
,因为在此编译单元之外(或在剥离的二进制文件中)不需要显示符号,而const
将数据放入 ELF 二进制文件的只读数据部分.
BPF_JUMP(BPF_JMP | BPF_JEQ, nr, equals, differs)
的形式很简单:累加器(系统调用号)与nr
进行比较。如果它们相等,则跳过下一个equals
操作码。否则,将跳过下一个differs
操作码。
由于 equals 案例跳转到最后的操作码,您可以在顶部添加新的操作码(即,就在初始操作码之后),增加每个操作码的 equals 跳过计数。
请注意printf()
在安装 seccomp 过滤器后将无法工作,因为在内部,C 库想要执行 fstat
系统调用(在标准输出上)和 brk
系统调用来为缓冲区。
【讨论】:
这是我想要分享赏金的时刻之一。 @Rhymoid 赏金就像一个原子! :) 不过别担心,Nomimal 是个很酷的人! 这是不安全,需要检查架构号。如果您为 x86-64 编译它,那么除了 x86-64 系统调用 15 (rt_sigreturn),您还允许 x86-32 系统调用 15,即 chmod。您想要您的主目录世界可写和 setuid 网络浏览器缓存吗?以上是关于seccomp --- 如何 EXIT_SUCCESS?的主要内容,如果未能解决你的问题,请参考以下文章
[qemu][seccomp]虚拟化安全sandbox技术分析
Debian 安装vsftpd出现500 OOPS: prctl PR_SET_SECCOMP failed的解决办法