linux安全之seccomp
介绍seccomp按照字面意思可以理解为security computing,最早是作用于网格计算,主要通过限制进程的系统调用来完成部分沙箱隔离功能。通过man prctl可以发现,seccomp的引入是在内核2.6.23,早期的seccomp主要限制read、write、exit以及sigreturn四个系统调用。内核3.5版本引入filter模式,将seccomp分成strict和filter
介绍
seccomp按照字面意思可以理解为security computing,最早是作用于网格计算,主要通过限制进程的系统调用来完成部分沙箱隔离功能。
通过man prctl可以发现,seccomp的引入是在内核2.6.23,早期的seccomp主要限制read、write、exit以及sigreturn四个系统调用。内核3.5版本引入filter模式,将seccomp分成strict和filter两种。其中strict依旧限制四种系统调用,而filter模式则借助了Berkeley Pakcet Filter来进行更细致的指令和参数控制。
STRICT模式
我们可以查阅2.6.32内核代码来了解strict模式下的系统调用控制是如何实现的。
设置
seccomp是通过prctl系统调用来进行设置的。
具体使用: prctl(PR_SET_SECCOMP, 1, 0, 0, 0),第二个参数设置为1即可。
通过sys_prctl进行追踪,在prctl_set_seccomp函数中,将当前进程结构(struct task_struct结构)中的seccomp中的mode标记设置为1,并将thread_info中flag设置为TIF_SECCOMP。
我们可以由此猜测,只要将对应的进程task_struct结构中的对应标记mode置位,则可以进行进程系统调用限制。程序可通过prctl给自己做限制,如果想限制其他程序,则需要在自己制作的测试驱动中,通过对不同的测试程序设置mode,来测试该功能,参考如下:
read_lock(&tasklck);
for_each_process(p) {
if (p->pid == seccomp_pid) {
p_pid_address = &(p->seccomp.mode);
ori_seccomp_mode = *p_pid_address;
*p_pid_address = 1;
break;
}
}
read_unlock(&tasklck);
拦截
每个系统调用都会陷入系统调用控制函数syscall_trace_enter中,该函数中调用了secure_computing,系统调用号作为参数传递。
在__secure_computing函数中,通过current->seccomp.mode提取标志位mode,如果mode置1,则说明当前进程已经设置了seccomp限制,通过系统调用号与__NR_read、__NR_write 、__NR_exit和__NR_sigreturn等四个系统调用值进行比较,判断当前系统调用是否被允许。如果系统调用是被允许的,则直接reurn返回,如果是其他的系统调用则调用do_exit(SIGKILL)退出。
FILTER模式
我们可以查阅3.10版本的内核代码来了解filter模式下系统调用是如何进行控制的。
设置
seccomp是通过prctl系统调用来进行设置的,其中第一个参数是PR_SET_SECCOMP,第二个参数为非0值,从SECCOMP_MODE_STRICT(严格模式,值为1)、SECCOMP_MODE_FILTER(指令过滤,值为2),第三个参数为struct sock_fprog结构指针类型的数据。
在sys_prctl中,调用了prctl_set_seccomp函数,在该函数中给current->seccomp.mode赋值为1或者2,既进程对应的task_struct结构中的mode置位了,则说明该进程所调用的系调用需要受到seccomp的限制。
用户态传递的第三个参数是struct sock_fprog结构指针,结构如下:
struct sock_filter
{
__u16 code;
__u8 jt;
__u8 jf;
__u32 k;
}
struct sock_fprog
{
unsigned short len;
struct sock_filter *filter;
}
其中len为filter指令过滤块的个数,filter为指令过滤块指针,多个过滤块内存连续。内核在seccomp_attach_filter函数中,将第三个参数的值赋值到了filter中(struct seccomp_filter结构体指针类型),并将filter的prev指针指向原有的current->seccomp.filter,将current->seccomp.filter更新为新的filter指针,此时新的指令过滤结构就串联到了对应的task_struct单向链表上。
struct seccomp_filter
{
struct seccomp_filter *prev; //通过prev节点将seccomp_filter结构串联成链表
unsigned short len; // 当前seccomp_filter的指令的数量
struct sock_filter insns[];
}
在seccomp_attach_filter中,分别调用了sk_chk_filter和seccomp_check_filter。
此处只针对seccomp的业务逻辑进行简单描述,在sk_chk_filter中做了指令码的转换,如将BPF_LD + BPF_W + BPF_ABS转换成BPF_S_LD_W_ABS。
在seccomp_check_filter函数中,也对一些seccomp模块需要用到的指令做了转换,用来区seccomp所用到的指令。比如在使用prctl的filter模式时,构建的指令码为BPF_LD + BPF_W + BPF_ABS,而在sk_chk_filter中可以转换为BPF_S_LD_W_ABS,此时为了区分seccomp和网络数据包的指令,则将code修改为BPF_S_ANC_SECCOMP_LD_W,作为seccomp的特有指令。
从内核代码中发现一个需要注意的地方,prctl生效有个前提条件是,当前进程的no_new_privs已经设置了或者当前进程在自己的命名空间中拥有CAP_SYS_ADMIN能力。
拦截
每个系统调用都会陷入系统调用控制函数syscall_trace_enter中,该函数中调用了secure_computing,系统调用号作为参数传递。
在__secure_computing函数中,通过current->seccomp.mode提取标志位mode,如果mode为1或者2,则说明当前进程已经设置了seccomp限制。
如果为1使用STRICT模式,通过系统调用号与__NR_read、__NR_write 、__NR_exit和__NR_sigreturn等四个系统调用值进行比较,判断当前系统调用是否被允许。如果系统调用是被允许的,则直接reurn返回,如果是其他的系统调用则调用do_exit(SIGKILL)退出(向当前进程发送SIGKILL信号)。
如果值为2则使用了FILTER模式,则调用seccomp_run_filters函数来进行所有指令判断过滤,系统调用号作为参数传递,根据返回值来进行后续处理。
- 当返回值为SECCOMP_RET_ALLOW时允许系统调用执行。
- 当返回值为SECCOMP_RET_KILL时程序会收到SIGSYS信号,并立即退出。
- 当返回值是SECCOMP_RET_ERRNO时会返回系统调用错误信息,并赋值errno。
- 当返回值是SECCOMP_RET_TRAP时,当前运行进程也会收到SIGSYS信号,不过可以在进程中捕获信号。
- 当返回值是SECCOMP_RET_TRACE时,可以给ptrace等调试程序一个控制机会。
在seccomp_run_filters函数中,通过遍历current->seccomp.filter链表,并调用sk_run_filter函数处理每一个节点中的指令数组insns。函数sk_run_filter中有针对不同的指令的处理流程,可根据具体的业务需求再做仔细查看。
接下来以禁用write系统调用为例,通过一个应用层例子程序来做个简单的指令解说。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <signal.h>
4 #include <sys/prctl.h>
5 #include <linux/seccomp.h>
6 #include <linux/filter.h>
7
8 int main(int argc, char **argv)
9 {
10 struct sock_filter filter[] = {
11 // 提取系统调用号,赋值给A
12 {BPF_LD+BPF_ABS+BPF_W, 0, 0, 0},
13 // 将A与k进行比较,让k等于__NR_write,既对__NR_write进行过滤
14 // 通过查看/usr/include/asm/unistd_64.h,返现__NR_write值为1
15 // fentry += (A == K) ? fentry->jt : fentry->jf;
16 // 让jt等于0,则命中write系统调用时不跳转,既返回KILL.
17 // 让jf等于1,则没有命中时进行跳转,既返回ALLOW.
18 {BPF_JMP+BPF_JEQ, 0, 1, 1},
19 // 返回k,让k等于SECCOMP_RET_KILL,既返回KILL
20 {BPF_RET+BPF_K, 0, 0, SECCOMP_RET_KILL},
21 // 返回k,让k等于SECCOMP_RET_ALLOW,既返回允许
22 {BPF_RET+BPF_K, 0, 0, SECCOMP_RET_ALLOW},
23 };
24 struct sock_fprog prog = {
25 .len = (sizeof(filter)/sizeof(struct sock_filter)),
26 .filter = filter
27 };
28 prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
29 write(1, "hello world\n", sizeof("hello world\n") - 1);
30
31 return 0;
32 }
上述代码中,提取系统调用号使用的指令是 BPF_LD + BPF_W + BPF_ABS组合,在seccomp_check_filter中该指令码修改为BPF_S_ANC_SECCOMP_LD_W,当发现BPF_S_ANC_SECCOMP_LD_W指令时,会调用seccomp_bpf_load函数,在函数中根据传递的k值来提取不同的信息。提取系统调用号时给k赋的值为struct seccomp_data数据结构中对应元素的偏移值。
struct seccomp_data
{
int nr;
__u32 arch;
__u64 instruction_pointer;
__u64 args[6];
}
其中nr代表系统调用号,偏移值为0,则k应该赋值为0。
上述测试的例子程序,是在root用户下运行的,既拥有CAP_SYS_ADMIN能力,否则还需要先进行prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)设置。
更多推荐
所有评论(0)