Linux--Sandbox

在计算机安全领域,沙箱(Sandbox)是一种程序的隔离运行机制,其目的是限制不可信进程或不可信代码运行时的访问权限。沙箱技术经常被用于执行未经测试的或不可信的客户程序。为了阻止不可信程序可能破坏系统程序或破坏其它用户程序的运行,沙箱技术通过为不可信客户程序提供虚拟化的内存、文件系统、网络等资源,而这种虚拟化手段对客户程序来说是透明的。由于沙箱里的资源被虚拟化(或被间接化),所以沙箱里的不可信程序的恶意行为可以被限制在沙箱中,或者在沙箱里只允许执行在白名单里规定的有限的API操作。

沙箱技术一直是系统安全领域的挑战,不存在说哪一种方案是足够安全的。沙箱技术方案通常是需要结合多种系统安全技术来实现,采用防御纵深(Defence in Depth)的设计原则,筑建多道防御屏障,尽可能地将安全风险将为最低。比如,Google App Engine系统就是一个较好的实现,用户编写的java code会在Java沙箱中运行。但除了Java Sandbox之外,底层还有Linux沙箱。也就是说,即使用户(也许是一个比较资深的黑客)已经掌握了Java沙箱的0-day漏洞,攻破了Java沙箱,但也不会给系统造成影响,因为底层还有Linux沙箱。除非该用户同时还掌握了Linux的0-day漏洞,能以很快的速度突破Linux沙箱,使自己从一个普通uid提权到root,否则我们不能说攻击成功。

Hypervisor技术天然地就可以用于构建虚拟机沙箱,比如基于Xen或KVM的虚拟机沙箱已经开始在某些云计算平台中使用起来。但考虑到虚拟机沙箱的性能开销,它并不能满足对性能敏感的应用场景。因此,这里我们主要讨论如何利用Linux kernel自身所提供的安全功能来构建有效的轻量级沙箱技术。(关于重量级的虚拟机沙箱,我以后再另作介绍)

在讨论之前,我们简单罗列一下Linux安全模型相关的内容(假设读者对此已经非常熟悉了):
(1) 每个进程都有自己的地址空间;
(2) MMU硬件机制来保证地址空间的隔离;
(3) Kernel是系统的TCB(Trusted Computing Base),是安全策略的制定者和执行者;
(4) 进程是最小的权限边界;
(5) root具有最高权限,它能控制一切;
(6) 其它用户受DAC(Discretionary Access Control)限制,如文件系统的UGO权限控制。

Linux Kernel还提供了与“进程降权(drop privilege)与访问限制”有关的一些功能:
(1) setuid
(2) posix.1e capability
(3) chroot jail
(4) quota control (eg, cgroup, namespace)
(5) Linux Container
(6) Linux Security Module (LSM)
 

接下来我将介绍在实践中如何利用这些安全构件来打造一个有效的Linux sandbox.

Setuid Sandbox主要基于Linux Kernel所提供的安全机制来实现。简单地说,就是利用 random uid/gid + chroot() + capability 来达到限制不可信进程的访问权限。Setuid Sandbox的设计主要考虑以下几方面:

1. Setuid

Linux中每个进程都会有一个uid,uid=0则为root用户进程(privileged),uid>0则为普通用户进程(unprivileged)。不同uid进程之间(不包括root进程)是相互隔离的,各自都有自己独立的权限,互不干扰。而root进程具有特权,它能干任何事情。Linux uid/gid机制主要是用于进程的权限隔离。如果你打算执行不可信的程序,那么你可以在启动该程序时为其分配一个random uid。一个可能的执行流程如下:

fork() -> setuid() -> {设置相关的进程资源限制, eg, RLIMIT_NPROC (0,0)} -> execve()

注意,setuid()只能由root权限(或拥有 CAP_SETUID capability的普通用户权限)才能成功调用,所以这个执行流程需要借助某个拥有root权限的helper程序。比如,将helper程序设置为setuid root。

2. Chroot

Chroot是Linux kernel提供的另一个安全功能,它用于修改进程的根目录。比如执行chroot("/tmp/sandbox/1/")则可以设置当前进程的根目录为"/tmp/sandbox/1/",那么该进程的文件操作将被限制在"/tmp/sandbox/1/"中。注意,chroot()只能由root权限(或拥有CAP_SYS_CHROOT capability的普通用户权限)才能成功调用。也许你马上会想到按如下方式修改上面的执行流程:

fork() ->chroot() -> setuid() -> {...} -> execve(),

但注意这样做是行不通的,因为在chroot()之后,execve()本要执行的binary文件已经不可用了(进程的根目录已经被重定位了)。一个解决此问题的简单方法如下:

(1) helper创建一个子进程H,注意要用clone()和CLONE_FS,使得helper和H可以共享根目录、当前目录、等等;
(2) helper降权后执行execve("worker");
(3) worker(原helper进程)请求H去执行chroot();
(4) H执行chroot(),新的根目录会对H和worker同时生效。
(5) H退出。

这个方法工作的前提是Helper需要设置RLIMIT_NOFILE为(0,0),并且对于不可信的Worker进程来说,在执行第4步之前应是可控的。

此外,对于Helper程序来说,由于它是以root身份运行,那么就可能会成为攻击点,即存在所谓的 "Confused Deputy Problem"。这个问题是说,因为Helper进程是root身份运行,它能干所有特权操作;而实际我们在设计上是希望它只能做两件事,即setuid和chroot,不希望它有更多的特权。一个进程的特权太多了,就会成为坏人攻击的焦点,甚至有时是在无知的情况下,不经意地就帮坏人干了坏事。为了解决这个问题,我们就可以在设计上引入Linux Capability机制。

3. Linux Capability

Linux支持Capability的主要目的是细化root的特权,使一个进程能够以“最小权限原则”去执行任务。比如拿ping程序来说,它需要使用原始套接字(raw_sockets),如果没有Capability,那么它就需要使用root特权才能运行;如果有了Capability机制,由于该程序只需要一个CAP_NET_RAW的Capability即可运行,那么根据最小权限原则,该程序运行时可以丢弃所有多余的Capability,以防止被误用或被攻击。所以,Capability机制可以将root特权进行很好的细分,当前kernel(2.6.18)已支持30多种不同的Capability。注意在之前的kernel实现中,Capability只能由root进程持有,非root进程是不能保持任何Capability的。但是在2.6.24及以上的kernel版本中一个普通用户进程也将可以持有capability。

小结

Setuid Sandbox实现起来比较简单,无需修改Kernel,对线上的生产系统来说没有什么负面影响,在一定程度上可以用于隔离不可信代码的运行,目前它已被用作Google Chromium系统的隔离屏障之一。由于它完全依赖于kernel所提供的安全机制,除非攻击者能找到kernel的0-day漏洞并通过攻击获得root权限,否则setuid sandbox所提供的安全隔离是可以保证的。

猜你喜欢

转载自blog.csdn.net/jekenzhuang/article/details/81329117
今日推荐