前言
在 vdso——虚拟 elf 动态共享库 这篇博客中,我描述了 vdso 的相关内容。其中有提到在 vdso 中导出的符号(系统调用)不会被 strace、seccomp 捕获到的情况。
seccomp 对我来说相当陌生,man seccomp 发现它能够用来限定程序能够执行的系统调用,并且是通过 bpf 虚拟机完成的,基于这两个因素,我在本篇文章中描述下上手 seccomp 的过程。
上手 seccomp
使用 seccomp 前需要开启必要的内核选项,一般来说需要开启下面这三个内核配置项目:
- CONFIG_SECCOMP
- CONFIG_HAVE_ARCH_SECCOMP_FILTER
- CONFIG_SECCOMP_FILTER
我使用如下 demo 进行测试,此 demo 摘自 《Linux 内核观测技术BPF》第 8 章。
#include <errno.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <unistd.h>
static int install_filter(int nr, int arch, int error) {
struct sock_filter filter[] = {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3),
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
perror("prctl(PR_SET_SECCOMP)");
return 1;
}
return 0;
}
int main(int argc, char const *argv[]) {
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("prctl(NO_NEW_PRIVS)");
return 1;
}
install_filter(__NR_write, AUDIT_ARCH_X86_64, EPERM);
return system(argv[1]);
}
此程序使用 install_filter 注册了一个阻止 write 系统调用的过滤条件,它使用 cbpf 虚拟机实现。
这个程序并没有使用 seccomp 系统调用,而是使用 prctl 的 PR_SET_SECCOMP option 来设定,seccomp 系统调用能够提供更多的功能,不过在这里用以限定系统调用的执行倒是绰绰有余了!
与普通 c 程序一样,直接执行 gcc 编译即可。编译后执行如下命令进行测试:
$ ls
README.md capabilities main.c output seccomp test.c test.go
$ ./seccomp ls
$
第一次 ls 命令输出了当前目录的文件内容,第二次 ls 命令由上述 demo 编译生成的 seccomp 程序执行,没有任何输出,与预期一致。
使用 strace -f 跟踪 ls 命令的执行过程,能够看到下面这几个关键的系统调用执行结果:
[pid 7399] write(1, "README.md capabilities main.c "..., 66) = -1 EPERM (Operation not permitted)
[pid 7399] close(1) = 0
[pid 7399] write(2, "ls: ", 4) = -1 EPERM (Operation not permitted)
[pid 7399] write(2, "write error", 11) = -1 EPERM (Operation not permitted)
[pid 7399] write(2, "\n", 1) = -1 EPERM (Operation not permitted)
可以确定,由于设定了过滤 write 系统调用,ls 命令在执行 write 系统调用时返回了 EPERM (操作不被允许)的错误值,seccomp 上手了!
demo 中的隐含内容
上面 demo 中我们用 seccomp 去加载 ls 命令,而设定过滤规则却是在 seccomp 程序中完成的,这说明在 fork 后 ls 命令继承了来自 seccomp 的过滤规则。
其实 seccomp 程序设定的系统调用过滤规则能传递给子程序的关键在于如下系统调用:
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
man prctl 得到了如下信息:
PR_SET_NO_NEW_PRIVS (since Linux 3.5)
Set the calling thread's no_new_privs bit to the value in arg2. With no_new_privs set to 1, execve(2) promises not to grant privileges to do anything
that could not have been done without the execve(2) call (for example, rendering the set-user-ID and set-group-ID mode bits, and file capabilities non-
functional). Once set, this bit cannot be unset. The setting of this bit is inherited by children created by fork(2) and clone(2), and preserved across
execve(2).
Since Linux 4.10, the value of a thread's no_new_privs bit can be viewed via the NoNewPrivs field in the /proc/[pid]/status file.
For more information, see the kernel source file Documentation/userspace-api/no_new_privs.rst (or Documentation/prctl/no_new_privs.txt before Linux 4.13).
See also seccomp(2).
设置了 PR_SET_NO_NEW_PRIVS 后,子进程将不会拥有比父进程更大的权限,并且 PR_SET_NO_NEW_PRIVS 的设定将会被使用 fork 与 clone 创建的子进程继承,并且在 execve 时也会保留。
总结
seccomp 并不是什么新技术,它已经问世了好些年头,可我对这个技术还非常陌生,通过本篇文章上手 seccomp,希望以后能够用上这一功能!