chroot
change root操作,既可以指一个系统调用,也可以指/usr/bin/chroot这个shell程序。会将制定的目录挂载为根目录,从而实现简单的sandbox.
但是chroot只是提供最简单的隔离,它只是单纯的更换了进程结构体的记录的根目录和限制cd操作,并不会更改当前工作目录,也不会释放任何指定根目录外的资源(包括文件描述符,进程通信等等全都不禁止)。最简单的,如果我们调用chroot的时候cwd在根目录外,我们依然能够进行操作。
同时, 如果我们能拿到权限,再次chroot一次,就会直接覆盖先前的结果。
如果能获取到根目录外的一个已经打开的文件,也可以用openat等一系列xxat调用(相对操作)完成逃逸。因此chroot可谓是四面漏风。
BPF
(现在我们说的)seccomp的本质是内核中的seccomp-bpf虚拟机。BPF这个词我们在wireshark抓包的时候见到过,了解BPF有助于我们了解seccomp的本质。
BPF本质是一个运行在内核态的vm程序,他拥有一个累加器,一个索引寄存器,一个内存和一个隐含的计数器,可以执行赋值,算术,跳转等等指令。每一条指令就是这样一个结构体:
1 | struct sock_filter { /* Filter block */ |
每条规则都由这样一个结构维护:
1 | struct sock_fprog { /* Required for SO_ATTACH_FILTER. */ |
因此一个sock_filter
数组就构成了一段bpf的指令序列。编写BPF规则有指定的宏(BPF_STMT
和BPF_JUMP
),比如下面这段:
1 | struct sock_filter filter[] = { |
操作码就不讲了,本质上这是一个结构体数组的声明,这个数组所代表的指令序列就能实现过滤execve系统调用。开启seccomp后程序的所有系统调用都会经过bpf虚拟机,因此一定程度上会影响性能。所有系统调用都会向内核返回一个值,其中有16位是操作SECCOMP_RET_ACTION
掩码,比如SECCOMP_RET_KILL
这种,这部分会指定内核对该调用采取的操作。
seccomp-BPF程序采用一个结构体作为输入:
1 | struct seccomp_data { |
怎么生效?
prctl
prctl是一个系统调用,用来控制进程的属性或程序设置,非常灵活。它可以在程序中显式的用函数形式调用:
1 | int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5); |
在option中启用PR_SET_SECCOMP
就可以为当前进程开启seccomp过滤。将arg2设置为FILTER过滤模式就可以用sock_frog
来指定过滤了。
另外,strict严格模式只允许read, write, sigreturn和exit四个系统调用
libseccomp
prctl执行的模式可能不够灵活,因此有了这么一个项目。这个库提供的函数允许不了解BPF规则也能使用seccomp过滤,提供了简化的接口。比如下面这一小段:
1 |
|
ctx就是一个规则(类似于上面说的结构体数组)。调用seccomp_rule_add
就能简单方便的添加规则,最后用seccomp_load启用,将其加载至内核。
seccomp
允许开发者手动限制程序的系统调用。本质上是在内核中有一个ebpf过滤器的小虚拟机。现代操作系统中,如果seccomp的过滤规则完美无缺,那么是无法逃逸的。但是几乎不存在这样的过滤,因此seccomp也是有办法逃逸。大体分为三种:过于宽松的规则,系统调用误用以及内核漏洞。
Permissive Rules
原本的限制就不严,可能没有禁用全部的可能有风险的系统调用,比如ptrace()
允许启动调试器附加到进程,如果附加到一个没有沙盒的进程就完成了逃逸。另外比较冷门的还可以使用sendmsg()
在进程间传递文件,prctl()
, process_vm_write()
等等很多方法。
Misuse Syscall
amd64与x86的系统调用完全不一样,但amd64为了兼容其实是都支持的。如exit()
使用syscall 60
与int 0x80
都能触发。结果在系统上是一致的。如果某些程序为了兼容开启了32位系统调用但是又没有做好限制,可以利用32位的x86调用进行逃逸。(但是seccomp默认如果没有对32位系统调用做任何设置的话会全部禁止)
Kernel Vulnerability
内核层面的漏洞,通过系统调用陷入内核触发漏洞完成逃逸。
其他?
我们并不总是需要execve
或者写入,很多时候读取就足够了。可以读的系统调用有很多,可以利用sleep, exit(code),崩溃信息,甚至某些能够一位一位传输的调用(yes or no)都可以拿来绕过sandbox,用一些侧信道的思想。