一个轻量级的进程隔离工具,利用Linux命名空间和seccomp-bpf系统调用过滤器(借助kafel bpf语言)
概述
NsJail是一个用于Linux的进程隔离工具。它利用Linux命名空间子系统、资源限制和Linux内核的seccomp-bpf系统调用过滤器。
它可以帮助你(除其他外):
- 隔离网络服务(例如Web、时间、DNS),使它们与操作系统的其他部分隔离
- 托管计算机安全挑战(所谓的CTF)
- 包含侵入性的系统调用级操作系统模糊测试工具
功能:
- 提供三种不同的操作模式。更多信息请参见此部分。
- 使用kafel seccomp-bpf配置语言进行灵活的系统调用策略定义。
- 使用基于ProtoBuf的配置文件
- 它非常稳定。
它提供了哪些隔离形式
- Linux命名空间:UTS(主机名)、MOUNT(chroot)、PID(独立的PID树)、IPC、NET(独立的网络上下文)、USER、CGROUPS
- 文件系统约束:chroot()、pivot_root()、只读重新挂载、自定义的
/proc和tmpfs挂载点 - 资源限制(挂起时间/CPU时间限制、VM/内存地址空间限制等)
- 可编程的seccomp-bpf系统调用过滤器(通过kafel语言)
- 克隆和隔离的以太网接口
- Cgroups用于内存和PID利用率控制
支持的用例
网络服务的隔离(inetd风格)
注意:你需要在/chroot中有一个有效的文件系统树。如果没有,请将/chroot改为/
- 服务器:
$ ./nsjail -Ml --port 9000 --chroot /chroot/ --user 99999 --group 99999 -- /bin/sh -i
- 客户端:
$ nc 127.0.0.1 9000
/ $ ifconfig
/ $ ifconfig -a
lo Link encap:Local Loopback
LOOPBACK MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
/ $ ps wuax
PID USER COMMAND
1 99999 /bin/sh -i
3 99999 {busybox} ps wuax
/ $
访问私有克隆接口的隔离(需要root/setuid权限)
注意:你需要在/chroot中有一个有效的文件系统树。如果没有,请将/chroot改为/
$ sudo ./nsjail --user 9999 --group 9999 --macvlan_iface eth0 --chroot /chroot/ -Mo --macvlan_vs_ip 192.168.0.44 --macvlan_vs_nm 255.255.255.0 --macvlan_vs_gw 192.168.0.1 -- /bin/sh -i
/ $ id
uid=9999 gid=9999
/ $ ip addr sh
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: vs: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue
link/ether ca:a2:69:21:33:66 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.44/24 brd 192.168.0.255 scope global vs
valid_lft forever preferred_lft forever
inet6 fe80::c8a2:69ff:fe21:cd66/64 scope link
valid_lft forever preferred_lft forever
/ $ nc 217.146.165.209 80
GET / HTTP/1.0
HTTP/1.0 302 Found
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Location: https://www.google.ch/?gfe_rd=cr&ei=cEzWVrG2CeTI8ge88ofwDA
Content-Length: 258
Date: Wed, 02 Mar 2016 02:14:08 GMT
...
...
/ $
本地进程的隔离
注意:你需要在/chroot中有一个有效的文件系统树。如果没有,请将/chroot改为/
$ ./nsjail -Mo --chroot /chroot/ --user 99999 --group 99999 -- /bin/sh -i
/ $ ifconfig -a
lo Link encap:Local Loopback
LOOPBACK MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
/ $ id
uid=99999 gid=99999
/ $ ps wuax
PID USER COMMAND
1 99999 /bin/sh -i
4 99999 {busybox} ps wuax
/ $exit
$
本地进程的隔离(并在必要时重新运行)
注意:你需要在/chroot中有一个有效的文件系统树。如果没有,请将/chroot改为/
$ ./nsjail -Mr --chroot /chroot/ --user 99999 --group 99999 -- /bin/sh -i
BusyBox v1.21.1 (Ubuntu 1:1.21.0-1ubuntu1) built-in shell (ash)
Enter 'help' for a list of built-in commands.
/ $ ps wuax
PID USER COMMAND
1 99999 /bin/sh -i
2 99999 {busybox} ps wuax
/ $ exit
BusyBox v1.21.1 (Ubuntu 1:1.21.0-1ubuntu1) built-in shell (ash)
Enter 'help' for a list of built-in commands.
/ $ ps wuax
PID USER COMMAND
1 99999 /bin/sh -i
2 99999 {busybox} ps wuax
/ $
在最小文件系统中运行Bash,uid==0且仅访问/dev/urandom
$ ./nsjail -Mo --user 0 --group 99999 -R /bin/ -R /lib -R /lib64/ -R /usr/ -R /sbin/ -T /dev -R /dev/urandom --keep_caps -- /bin/bash -i
[2017-05-24T17:08:02+0200] Mode: STANDALONE_ONCE
[2017-05-24T17:08:02+0200] Jail parameters: hostname:'NSJAIL', chroot:'(null)', process:'/bin/bash', bind:[::]:0, max_conns_per_ip:0, time_limit:0, personality:0, daemonize:false, clone_newnet:true, clone_newuser:true, clone_newns:true, clone_newpid:true, clone_newipc:true, clonew_newuts:true, clone_newcgroup:false, keep_caps:true, tmpfs_size:4194304, disable_no_new_privs:false, pivot_root_only:false
[2017-05-24T17:08:02+0200] Mount point: src:'none' dst:'/' type:'tmpfs' flags:MS_RDONLY|0 options:'' isDir:True
[2017-05-24T17:08:02+0200] Mount point: src:'none' dst:'/proc' type:'proc' flags:MS_RDONLY|0 options:'' isDir:True
[2017-05-24T17:08:02+0200] Mount point: src:'/bin/' dst:'/bin/' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:08:02+0200] Mount point: src:'/lib' dst:'/lib' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:08:02+0200] Mount point: src:'/lib64/' dst:'/lib64/' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:08:02+0200] Mount point: src:'/usr/' dst:'/usr/' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:08:02+0200] Mount point: src:'/sbin/' dst:'/sbin/' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:08:02+0200] Mount point: src:'none' dst:'/dev' type:'tmpfs' flags:0 options:'size=4194304' isDir:True
[2017-05-24T17:08:02+0200] Mount point: src:'/dev/urandom' dst:'/dev/urandom' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:False
[2017-05-24T17:08:02+0200] Uid map: inside_uid:0 outside_uid:69664
[2017-05-24T17:08:02+0200] Gid map: inside_gid:99999 outside_gid:5000
[2017-05-24T17:08:02+0200] Executing '/bin/bash' for '[STANDALONE_MODE]'
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
bash-4.3# ls -l
total 28
drwxr-xr-x 2 65534 65534 4096 May 15 14:04 bin
drwxrwxrwt 2 0 99999 60 May 24 15:08 dev
drwxr-xr-x 28 65534 65534 4096 May 15 14:10 lib
drwxr-xr-x 2 65534 65534 4096 May 15 13:56 lib64
dr-xr-xr-x 391 65534 65534 0 May 24 15:08 proc
drwxr-xr-x 2 65534 65534 12288 May 15 14:16 sbin
drwxr-xr-x 17 65534 65534 4096 May 15 13:58 usr
bash-4.3# id
uid=0 gid=99999 groups=65534,99999
bash-4.3# exit
exit
[2017-05-24T17:08:05+0200] PID: 129839 exited with status: 0, (PIDs left: 0)
在最小文件系统中运行/usr/bin/find(仅从/usr/bin访问/usr/bin/find)
$ ./nsjail -Mo --user 99999 --group 99999 -R /lib/x86_64-linux-gnu/ -R /lib/x86_64-linux-gnu -R /lib64 -R /usr/bin/find -R /dev/urandom --keep_caps -- /usr/bin/find / | wc -l
[2017-05-24T17:04:37+0200] Mode: STANDALONE_ONCE
[2017-05-24T17:04:37+0200] Jail parameters: hostname:'NSJAIL', chroot:'(null)', process:'/usr/bin/find', bind:[::]:0, max_conns_per_ip:0, time_limit:0, personality:0, daemonize:false, clone_newnet:true, clone_newuser:true, clone_newns:true, clone_newpid:true, clone_newipc:true, clonew_newuts:true, clone_newcgroup:false, keep_caps:true, tmpfs_size:4194304, disable_no_new_privs:false, pivot_root_only:false
[2017-05-24T17:04:37+0200] Mount point: src:'none' dst:'/' type:'tmpfs' flags:MS_RDONLY|0 options:'' isDir:True
[2017-05-24T17:04:37+0200] Mount point: src:'none' dst:'/proc' type:'proc' flags:MS_RDONLY|0 options:'' isDir:True
[2017-05-24T17:04:37+0200] Mount point: src:'/lib/x86_64-linux-gnu/' dst:'/lib/x86_64-linux-gnu/' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:04:37+0200] Mount point: src:'/lib/x86_64-linux-gnu' dst:'/lib/x86_64-linux-gnu' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:04:37+0200] Mount point: src:'/lib64' dst:'/lib64' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:04:37+0200] Mount point: src:'/usr/bin/find' dst:'/usr/bin/find' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:False
[2017-05-24T17:04:37+0200] Mount point: src:'/dev/urandom' dst:'/dev/urandom' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:False
[2017-05-24T17:04:37+0200] Uid map: inside_uid:99999 outside_uid:69664
[2017-05-24T17:04:37+0200] Gid map: inside_gid:99999 outside_gid:5000
[2017-05-24T17:04:37+0200] Executing '/usr/bin/find' for '[STANDALONE_MODE]'
/usr/bin/find: `/proc/tty/driver': Permission denied
2289
[2017-05-24T17:04:37+0200] PID: 129525 exited with status: 1, (PIDs left: 0)
使用/etc/subuid
$ tail -n1 /etc/subuid
user:10000000:1
$ ./nsjail -R /lib -R /lib64/ -R /usr/lib -R /usr/bin/ -R /usr/sbin/ -R /bin/ -R /sbin/ -R /dev/null -U 0:10000000:1 -u 0 -R /tmp/ -T /tmp/ -- /bin/ls -l /usr/
[2017-05-24T17:12:31+0200] Mode: STANDALONE_ONCE
[2017-05-24T17:12:31+0200] Jail parameters: hostname:'NSJAIL', chroot:'(null)', process:'/bin/ls', bind:[::]:0, max_conns_per_ip:0, time_limit:0, personality:0, daemonize:false, clone_newnet:true, clone_newuser:true, clone_newns:true, clone_newpid:true, clone_newipc:true, clonew_newuts:true, clone_newcgroup:false, keep_caps:false, tmpfs_size:4194304, disable_no_new_privs:false, pivot_root_only:false
[2017-05-24T17:12:31+0200] Mount point: src:'none' dst:'/' type:'tmpfs' flags:MS_RDONLY|0 options:'' isDir:True
[2017-05-24T17:12:31+0200] Mount point: src:'none' dst:'/proc' type:'proc' flags:MS_RDONLY|0 options:'' isDir:True
[2017-05-24T17:12:31+0200] Mount point: src:'/lib' dst:'/lib' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:12:31+0200] Mount point: src:'/lib64/' dst:'/lib64/' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:12:31+0200] Mount point: src:'/usr/lib' dst:'/usr/lib' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:12:31+0200] Mount point: src:'/usr/bin/' dst:'/usr/bin/' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:12:31+0200] Mount point: src:'/usr/sbin/' dst:'/usr/sbin/' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:12:31+0200] Mount point: src:'/bin/' dst:'/bin/' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:12:31+0200] Mount point: src:'/sbin/' dst:'/sbin/' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:12:31+0200] Mount point: src:'/dev/null' dst:'/dev/null' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:False
[2017-05-24T17:12:31+0200] Mount point: src:'/tmp/' dst:'/tmp/' type:'' flags:MS_RDONLY|MS_BIND|MS_REC|0 options:'' isDir:True
[2017-05-24T17:12:31+0200] Mount point: src:'none' dst:'/tmp/' type:'tmpfs' flags:0 options:'size=4194304' isDir:True
[2017-05-24T17:12:31+0200] Uid map: inside_uid:0 outside_uid:69664
[2017-05-24T17:12:31+0200] Gid map: inside_gid:5000 outside_gid:5000
[2017-05-24T17:12:31+0200] Newuid mapping: inside_uid:'0' outside_uid:'10000000' count:'1'
[2017-05-24T17:12:31+0200] Executing '/bin/ls' for '[STANDALONE_MODE]'
total 120
drwxr-xr-x 5 65534 65534 77824 May 24 12:25 bin
drwxr-xr-x 210 65534 65534 20480 May 22 16:11 lib
drwxr-xr-x 4 65534 65534 20480 May 24 00:24 sbin
[2017-05-24T17:12:31+0200] PID: 130841 exited with status: 0, (PIDs left: 0)
更受限制的shell(带有seccomp-bpf策略)
$ ./nsjail --chroot / --seccomp_string 'ALLOW { write, execve, brk, access, mmap, open, openat, newfstat, close, read, mprotect, arch_prctl, munmap, getuid, getgid, getpid, rt_sigaction, geteuid, getppid, getcwd, getegid, ioctl, fcntl, newstat, clone, wait4, rt_sigreturn, exit_group } DEFAULT KILL' -- /bin/sh -i
[2017-01-15T21:53:08+0100] Mode: STANDALONE_ONCE
[2017-01-15T21:53:08+0100] Jail parameters: hostname:'NSJAIL', chroot:'/', process:'/bin/sh', bind:[::]:0, max_conns_per_ip:0, uid:(ns:1000, global:1000), gid:(ns:1000, global:1000), time_limit:0, personality:0, daemonize:false, clone_newnet:true, clone_newuser:true, clone_newns:true, clone_newpid:true, clone_newipc:true, clonew_newuts:true, clone_newcgroup:false, keep_caps:false, tmpfs_size:4194304, disable_no_new_privs:false, pivot_root_only:false
[2017-01-15T21:53:08+0100] Mount point: src:'/' dst:'/' type:'' flags:0x5001 options:''
[2017-01-15T21:53:08+0100] Mount point: src:'(null)' dst:'/proc' type:'proc' flags:0x0 options:''
[2017-01-15T21:53:08+0100] PID: 18873 about to execute '/bin/sh' for [STANDALONE_MODE]
/bin/sh: 0: can't access tty; job control turned off
$ set
IFS='
'
OPTIND='1'
PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
PPID='0'
PS1='$ '
PS2='> '
PS4='+ '
PWD='/'
$ id
Bad system call
$ exit
[2017-01-15T21:53:17+0100] PID: 18873 exited with status: 159, (PIDs left: 0)
配置文件
你还可以在configs目录中找到所有示例。
config.proto包含了nsjail配置格式的ProtoBuf模式。
你可以在configs/bash-with-fake-geteuid.cfg中查看一个示例配置文件。
用法:
$ ./nsjail --config configs/bash-with-fake-geteuid.cfg
你也可以通过命令行选项覆盖某些配置。在这里,执行的二进制文件(/bin/bash)被覆盖为_/usr/bin/id_,但_configs/bash-with-fake-geteuid.cfg_中的选项仍然适用。
$ ./nsjail --config configs/bash-with-fake-geteuid.cfg -- /usr/bin/id
...
[INSIDE-JAIL]: id
uid=999999 gid=999998 euid=4294965959 groups=999998,65534
[INSIDE-JAIL]: exit
[2017-05-27T18:45:40+0200] PID: 16579 exited with status: 0, (PIDs left: 0)
你也可以尝试使用configs/home-documents-with-xorg-no-net.cfg。
$ ./nsjail --config configs/home-documents-with-xorg-no-net.cfg -- /usr/bin/evince /user/Documents/doc.pdf
$ ./nsjail --config configs/home-documents-with-xorg-no-net.cfg -- /usr/bin/geeqie /user/Documents/
$ ./nsjail --config configs/home-documents-with-xorg-no-net.cfg -- /usr/bin/gv /user/Documents/doc.pdf
$ ./nsjail --config configs/home-documents-with-xorg-no-net.cfg -- /usr/bin/mupdf /user/Documents/doc.pdf
configs/firefox-with-net.cfg配置文件将允许你在沙盒环境中运行firefox:
$ ./nsjail --config configs/firefox-with-net.cfg
一个更复杂的设置,利用虚拟化(克隆)的以太网接口(将其与主网络命名空间分离),可以在configs/firefox-with-cloned-net.cfg中找到。使用前请更改相关的UID和以太网接口名称。
由于使用克隆的以太网接口(MACVTAP)需要root权限,你必须在sudo下运行:
$ sudo ./nsjail --config configs/firefox-with-cloned-net.cfg
更多信息
命令行选项应该是自解释的,而proto-buf配置选项在config.proto中有描述。
./nsjail --help
用法: ./nsjail [选项] -- 命令路径 [参数]
选项:
--help|-h
帮助信息..
--mode|-M 值
执行模式(默认:'o' [MODE_STANDALONE_ONCE]):
l: 在TCP端口上等待连接(使用--port指定)[MODE_LISTEN_TCP]
o: 在控制台上使用clone/execve启动单个进程 [MODE_STANDALONE_ONCE]
e: 在控制台上使用execve启动单个进程 [MODE_STANDALONE_EXECVE]
r: 在控制台上使用clone/execve启动单个进程,并持续运行 [MODE_STANDALONE_RERUN]
--config|-C 值
配置文件,使用config.proto ProtoBuf格式(参见configs/目录中的示例)
--exec_file|-x 值
要执行的文件(默认:argv[0])
--execute_fd
使用execveat()执行文件描述符,而不是执行二进制路径。在这种情况下,argv[0]/exec_file表示挂载命名空间之前的文件路径
--chroot|-c 值
包含jail的根目录(默认:无)
--no_pivotroot
在创建挂载命名空间时,使用mount(MS_MOVE)和chroot而不是pivot_root。当pivot_root被禁止时(例如initramfs)很有用。注意:在某些配置中可能被绕过
--rw
将chroot目录(/)挂载为读写(默认:只读)
--user|-u 值
jail内进程的用户名/uid(默认:当前uid)。你也可以使用inside_ns_uid:outside_ns_uid:count的格式。可以多次指定
--group|-g 值
jail内进程的组名/gid(默认:当前gid)。你也可以使用inside_ns_gid:global_ns_gid:count的格式。可以多次指定
--hostname|-H 值
jail的UTS名称(主机名)(默认:'NSJAIL')
--cwd|-D 值
命名空间内进程运行的目录(默认:'/')
--port|-p 值
绑定的TCP端口(启用MODE_LISTEN_TCP)(默认:0)
--bindhost 值
绑定端口的IP地址(仅在[MODE_LISTEN_TCP]中),(默认:'::')
--max_conns 值
所有IP的最大连接数(仅在[MODE_LISTEN_TCP]中),(默认:0(无限制))
--max_conns_per_ip|-i 值
每个IP的最大连接数(仅在[MODE_LISTEN_TCP]中),(默认:0(无限制))
--log|-l 值
日志文件(默认:使用log_fd)
--log_fd|-L 值
日志文件描述符(默认:2)
--time_limit|-t 值
jail存在的最长时间,以秒为单位(默认:600)
--max_cpus 值
单个jail进程可以使用的最大CPU数(默认:0 '无限制')
--daemon|-d
启动后守护进程化
--verbose|-v
详细输出
--quiet|-q
仅记录警告及更重要的消息
--really_quiet|-Q
仅记录致命消息
--keep_env|-e
将所有环境变量传递给子进程(默认:清除所有环境变量)
--env|-E 值
额外的环境变量(可以多次使用)。如果环境变量不包含'='(例如只是'DISPLAY'字符串),则使用当前环境变量的值
--keep_caps
不丢弃任何能力
--cap 值
保留此能力,例如CAP_PTRACE(可以多次指定)
--silent
将子进程的fd:0/1/2重定向到/dev/null
--stderr_to_null
将子进程的fd:2(STDERR_FILENO)重定向到/dev/null
--skip_setsid
不调用setsid(),允许沙盒进程中的终端信号处理。危险
--pass_fd 值
在执行子进程之前不关闭此文件描述符(可以多次指定),默认情况下:0/1/2保持打开
--disable_no_new_privs
不设置prctl(NO_NEW_PRIVS, 1)(危险)
--rlimit_as 值
RLIMIT_AS,以MB为单位,'max'或'hard'表示当前硬限制,'def'或'soft'表示当前软限制,'inf'表示RLIM64_INFINITY(默认:4096)
--rlimit_core 值
RLIMIT_CORE,以MB为单位,'max'或'hard'表示当前硬限制,'def'或'soft'表示当前软限制,'inf'表示RLIM64_INFINITY(默认:0)
--rlimit_cpu 值
RLIMIT_CPU,'max'或'hard'表示当前硬限制,'def'或'soft'表示当前软限制,'inf'表示RLIM64_INFINITY(默认:600)
--rlimit_fsize 值
RLIMIT_FSIZE,以MB为单位,'max'或'hard'表示当前硬限制,'def'或'soft'表示当前软限制,'inf'表示RLIM64_INFINITY(默认:1)
--rlimit_nofile 值
RLIMIT_NOFILE,'max'或'hard'表示当前硬限制,'def'或'soft'表示当前软限制,'inf'表示RLIM64_INFINITY(默认:32)
--rlimit_nproc 值
RLIMIT_NPROC,'max'或'hard'表示当前硬限制,'def'或'soft'表示当前软限制,
