Zj_W1nd's BLOG

Sandbox 简单介绍

2024/08/05

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
2
3
4
5
6
struct sock_filter {            /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};

每条规则都由这样一个结构维护:

1
2
3
4
struct sock_fprog {    /* Required for SO_ATTACH_FILTER. */
unsigned short len; /* BPF指令的数量 */
struct sock_filter __user *filter; /*指向BPF数组的指针 */
};

因此一个sock_filter数组就构成了一段bpf的指令序列。编写BPF规则有指定的宏(BPF_STMTBPF_JUMP),比如下面这段:

1
2
3
4
5
6
struct sock_filter filter[] = {
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0), //将帧的偏移0处,取4个字节数据,也就是系统调用号的值载入累加器
BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1), //当A == 59时,顺序执行下一条规则,否则跳过下一条规则,这里的59就是x64的execve系统调用号
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL), //返回KILL
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW), //返回ALLOW
};

操作码就不讲了,本质上这是一个结构体数组的声明,这个数组所代表的指令序列就能实现过滤execve系统调用。开启seccomp后程序的所有系统调用都会经过bpf虚拟机,因此一定程度上会影响性能。所有系统调用都会向内核返回一个值,其中有16位是操作SECCOMP_RET_ACTION掩码,比如SECCOMP_RET_KILL这种,这部分会指定内核对该调用采取的操作。

seccomp-BPF程序采用一个结构体作为输入:

1
2
3
4
5
6
struct seccomp_data {
int nr ; /* 系统调用号(依赖于体系架构) */
__u32 arch ; /* 架构(如AUDIT_ARCH_X86_64) */
__u64 instruction_pointer ; /* CPU指令指针 */
__u64 args [6]; /* 系统调用参数,最多有6个参数 */
};

怎么生效?

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <unistd.h>
#include <seccomp.h>
#include <linux/seccomp.h>

int main(void){
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
seccomp_load(ctx);

char * filename = "/bin/sh";
char * argv[] = {"/bin/sh",NULL};
char * envp[] = {NULL};
write(1,"i will give you a shell\n",24);
syscall(59,filename,argv,envp);//execve
return 0;

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 60int 0x80都能触发。结果在系统上是一致的。如果某些程序为了兼容开启了32位系统调用但是又没有做好限制,可以利用32位的x86调用进行逃逸。(但是seccomp默认如果没有对32位系统调用做任何设置的话会全部禁止)

Kernel Vulnerability

内核层面的漏洞,通过系统调用陷入内核触发漏洞完成逃逸。

其他?

我们并不总是需要execve或者写入,很多时候读取就足够了。可以读的系统调用有很多,可以利用sleep, exit(code),崩溃信息,甚至某些能够一位一位传输的调用(yes or no)都可以拿来绕过sandbox,用一些侧信道的思想。

CATALOG
  1. 1. chroot
  2. 2. BPF
    1. 2.1. 怎么生效?
      1. 2.1.1. prctl
      2. 2.1.2. libseccomp
  3. 3. seccomp
    1. 3.1. Permissive Rules
    2. 3.2. Misuse Syscall
    3. 3.3. Kernel Vulnerability
    4. 3.4. 其他?